@chances-ai/engine 24.0.0
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/dist/agents/discover.d.ts +30 -0
- package/dist/agents/discover.d.ts.map +1 -0
- package/dist/agents/discover.js +183 -0
- package/dist/agents/discover.js.map +1 -0
- package/dist/agents/index.d.ts +20 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +52 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/parse.d.ts +61 -0
- package/dist/agents/parse.d.ts.map +1 -0
- package/dist/agents/parse.js +527 -0
- package/dist/agents/parse.js.map +1 -0
- package/dist/agents/types.d.ts +52 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +8 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/ai/adapters/ai-sdk-stream.d.ts +19 -0
- package/dist/ai/adapters/ai-sdk-stream.d.ts.map +1 -0
- package/dist/ai/adapters/ai-sdk-stream.js +125 -0
- package/dist/ai/adapters/ai-sdk-stream.js.map +1 -0
- package/dist/ai/adapters/ai-sdk.d.ts +56 -0
- package/dist/ai/adapters/ai-sdk.d.ts.map +1 -0
- package/dist/ai/adapters/ai-sdk.js +112 -0
- package/dist/ai/adapters/ai-sdk.js.map +1 -0
- package/dist/ai/adapters/mock.d.ts +13 -0
- package/dist/ai/adapters/mock.d.ts.map +1 -0
- package/dist/ai/adapters/mock.js +54 -0
- package/dist/ai/adapters/mock.js.map +1 -0
- package/dist/ai/adapters/openai-compatible.d.ts +23 -0
- package/dist/ai/adapters/openai-compatible.d.ts.map +1 -0
- package/dist/ai/adapters/openai-compatible.js +45 -0
- package/dist/ai/adapters/openai-compatible.js.map +1 -0
- package/dist/ai/cost.d.ts +3 -0
- package/dist/ai/cost.d.ts.map +1 -0
- package/dist/ai/cost.js +5 -0
- package/dist/ai/cost.js.map +1 -0
- package/dist/ai/index.d.ts +12 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +11 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/known-models.d.ts +20 -0
- package/dist/ai/known-models.d.ts.map +1 -0
- package/dist/ai/known-models.js +129 -0
- package/dist/ai/known-models.js.map +1 -0
- package/dist/ai/registry.d.ts +12 -0
- package/dist/ai/registry.d.ts.map +1 -0
- package/dist/ai/registry.js +24 -0
- package/dist/ai/registry.js.map +1 -0
- package/dist/ai/retry.d.ts +11 -0
- package/dist/ai/retry.d.ts.map +1 -0
- package/dist/ai/retry.js +14 -0
- package/dist/ai/retry.js.map +1 -0
- package/dist/ai/router.d.ts +25 -0
- package/dist/ai/router.d.ts.map +1 -0
- package/dist/ai/router.js +36 -0
- package/dist/ai/router.js.map +1 -0
- package/dist/ai/setup.d.ts +23 -0
- package/dist/ai/setup.d.ts.map +1 -0
- package/dist/ai/setup.js +47 -0
- package/dist/ai/setup.js.map +1 -0
- package/dist/ai/summarizer.d.ts +24 -0
- package/dist/ai/summarizer.d.ts.map +1 -0
- package/dist/ai/summarizer.js +56 -0
- package/dist/ai/summarizer.js.map +1 -0
- package/dist/ai/types.d.ts +83 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +2 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/core/compaction/circuit-breaker.d.ts +32 -0
- package/dist/core/compaction/circuit-breaker.d.ts.map +1 -0
- package/dist/core/compaction/circuit-breaker.js +42 -0
- package/dist/core/compaction/circuit-breaker.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +75 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -0
- package/dist/core/compaction/compactor.js +261 -0
- package/dist/core/compaction/compactor.js.map +1 -0
- package/dist/core/compaction/estimate.d.ts +39 -0
- package/dist/core/compaction/estimate.d.ts.map +1 -0
- package/dist/core/compaction/estimate.js +74 -0
- package/dist/core/compaction/estimate.js.map +1 -0
- package/dist/core/compaction/index.d.ts +5 -0
- package/dist/core/compaction/index.d.ts.map +1 -0
- package/dist/core/compaction/index.js +5 -0
- package/dist/core/compaction/index.js.map +1 -0
- package/dist/core/compaction/prune.d.ts +43 -0
- package/dist/core/compaction/prune.d.ts.map +1 -0
- package/dist/core/compaction/prune.js +51 -0
- package/dist/core/compaction/prune.js.map +1 -0
- package/dist/core/engine.d.ts +268 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +767 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/task-tool.d.ts +175 -0
- package/dist/core/task-tool.d.ts.map +1 -0
- package/dist/core/task-tool.js +901 -0
- package/dist/core/task-tool.js.map +1 -0
- package/dist/core/workspace-query.d.ts +83 -0
- package/dist/core/workspace-query.d.ts.map +1 -0
- package/dist/core/workspace-query.js +217 -0
- package/dist/core/workspace-query.js.map +1 -0
- package/dist/core/worktree/active-marker.d.ts +31 -0
- package/dist/core/worktree/active-marker.d.ts.map +1 -0
- package/dist/core/worktree/active-marker.js +109 -0
- package/dist/core/worktree/active-marker.js.map +1 -0
- package/dist/core/worktree/create.d.ts +40 -0
- package/dist/core/worktree/create.d.ts.map +1 -0
- package/dist/core/worktree/create.js +121 -0
- package/dist/core/worktree/create.js.map +1 -0
- package/dist/core/worktree/errors.d.ts +7 -0
- package/dist/core/worktree/errors.d.ts.map +1 -0
- package/dist/core/worktree/errors.js +11 -0
- package/dist/core/worktree/errors.js.map +1 -0
- package/dist/core/worktree/gc.d.ts +39 -0
- package/dist/core/worktree/gc.d.ts.map +1 -0
- package/dist/core/worktree/gc.js +146 -0
- package/dist/core/worktree/gc.js.map +1 -0
- package/dist/core/worktree/git.d.ts +53 -0
- package/dist/core/worktree/git.d.ts.map +1 -0
- package/dist/core/worktree/git.js +166 -0
- package/dist/core/worktree/git.js.map +1 -0
- package/dist/core/worktree/index.d.ts +8 -0
- package/dist/core/worktree/index.d.ts.map +1 -0
- package/dist/core/worktree/index.js +8 -0
- package/dist/core/worktree/index.js.map +1 -0
- package/dist/core/worktree/paths.d.ts +26 -0
- package/dist/core/worktree/paths.d.ts.map +1 -0
- package/dist/core/worktree/paths.js +57 -0
- package/dist/core/worktree/paths.js.map +1 -0
- package/dist/core/worktree/slug.d.ts +6 -0
- package/dist/core/worktree/slug.d.ts.map +1 -0
- package/dist/core/worktree/slug.js +21 -0
- package/dist/core/worktree/slug.js.map +1 -0
- package/dist/local-vault/file-store.d.ts +64 -0
- package/dist/local-vault/file-store.d.ts.map +1 -0
- package/dist/local-vault/file-store.js +225 -0
- package/dist/local-vault/file-store.js.map +1 -0
- package/dist/local-vault/index.d.ts +57 -0
- package/dist/local-vault/index.d.ts.map +1 -0
- package/dist/local-vault/index.js +68 -0
- package/dist/local-vault/index.js.map +1 -0
- package/dist/local-vault/keychain.d.ts +58 -0
- package/dist/local-vault/keychain.d.ts.map +1 -0
- package/dist/local-vault/keychain.js +200 -0
- package/dist/local-vault/keychain.js.map +1 -0
- package/dist/local-vault/mutex.d.ts +20 -0
- package/dist/local-vault/mutex.d.ts.map +1 -0
- package/dist/local-vault/mutex.js +44 -0
- package/dist/local-vault/mutex.js.map +1 -0
- package/dist/local-vault/passphrase.d.ts +30 -0
- package/dist/local-vault/passphrase.d.ts.map +1 -0
- package/dist/local-vault/passphrase.js +72 -0
- package/dist/local-vault/passphrase.js.map +1 -0
- package/dist/lsp/config.d.ts +34 -0
- package/dist/lsp/config.d.ts.map +1 -0
- package/dist/lsp/config.js +68 -0
- package/dist/lsp/config.js.map +1 -0
- package/dist/lsp/detect.d.ts +7 -0
- package/dist/lsp/detect.d.ts.map +1 -0
- package/dist/lsp/detect.js +78 -0
- package/dist/lsp/detect.js.map +1 -0
- package/dist/lsp/errors.d.ts +11 -0
- package/dist/lsp/errors.d.ts.map +1 -0
- package/dist/lsp/errors.js +11 -0
- package/dist/lsp/errors.js.map +1 -0
- package/dist/lsp/formatters.d.ts +147 -0
- package/dist/lsp/formatters.d.ts.map +1 -0
- package/dist/lsp/formatters.js +259 -0
- package/dist/lsp/formatters.js.map +1 -0
- package/dist/lsp/index.d.ts +31 -0
- package/dist/lsp/index.d.ts.map +1 -0
- package/dist/lsp/index.js +31 -0
- package/dist/lsp/index.js.map +1 -0
- package/dist/lsp/instance.d.ts +72 -0
- package/dist/lsp/instance.d.ts.map +1 -0
- package/dist/lsp/instance.js +489 -0
- package/dist/lsp/instance.js.map +1 -0
- package/dist/lsp/lazy-load.d.ts +27 -0
- package/dist/lsp/lazy-load.d.ts.map +1 -0
- package/dist/lsp/lazy-load.js +57 -0
- package/dist/lsp/lazy-load.js.map +1 -0
- package/dist/lsp/manager.d.ts +59 -0
- package/dist/lsp/manager.d.ts.map +1 -0
- package/dist/lsp/manager.js +242 -0
- package/dist/lsp/manager.js.map +1 -0
- package/dist/lsp/ops.d.ts +13 -0
- package/dist/lsp/ops.d.ts.map +1 -0
- package/dist/lsp/ops.js +225 -0
- package/dist/lsp/ops.js.map +1 -0
- package/dist/lsp/rpc.d.ts +47 -0
- package/dist/lsp/rpc.d.ts.map +1 -0
- package/dist/lsp/rpc.js +134 -0
- package/dist/lsp/rpc.js.map +1 -0
- package/dist/lsp/safe-uri.d.ts +18 -0
- package/dist/lsp/safe-uri.d.ts.map +1 -0
- package/dist/lsp/safe-uri.js +96 -0
- package/dist/lsp/safe-uri.js.map +1 -0
- package/dist/lsp/types.d.ts +70 -0
- package/dist/lsp/types.d.ts.map +1 -0
- package/dist/lsp/types.js +16 -0
- package/dist/lsp/types.js.map +1 -0
- package/dist/mcp/bridge.d.ts +57 -0
- package/dist/mcp/bridge.d.ts.map +1 -0
- package/dist/mcp/bridge.js +98 -0
- package/dist/mcp/bridge.js.map +1 -0
- package/dist/mcp/category.d.ts +22 -0
- package/dist/mcp/category.d.ts.map +1 -0
- package/dist/mcp/category.js +11 -0
- package/dist/mcp/category.js.map +1 -0
- package/dist/mcp/client.d.ts +228 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +352 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/content.d.ts +86 -0
- package/dist/mcp/content.d.ts.map +1 -0
- package/dist/mcp/content.js +147 -0
- package/dist/mcp/content.js.map +1 -0
- package/dist/mcp/env.d.ts +19 -0
- package/dist/mcp/env.d.ts.map +1 -0
- package/dist/mcp/env.js +50 -0
- package/dist/mcp/env.js.map +1 -0
- package/dist/mcp/host.d.ts +199 -0
- package/dist/mcp/host.d.ts.map +1 -0
- package/dist/mcp/host.js +530 -0
- package/dist/mcp/host.js.map +1 -0
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +17 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/load-mcp-host.d.ts +32 -0
- package/dist/mcp/load-mcp-host.d.ts.map +1 -0
- package/dist/mcp/load-mcp-host.js +49 -0
- package/dist/mcp/load-mcp-host.js.map +1 -0
- package/dist/mcp/oauth/callback-server.d.ts +73 -0
- package/dist/mcp/oauth/callback-server.d.ts.map +1 -0
- package/dist/mcp/oauth/callback-server.js +280 -0
- package/dist/mcp/oauth/callback-server.js.map +1 -0
- package/dist/mcp/oauth/config-hash.d.ts +24 -0
- package/dist/mcp/oauth/config-hash.d.ts.map +1 -0
- package/dist/mcp/oauth/config-hash.js +55 -0
- package/dist/mcp/oauth/config-hash.js.map +1 -0
- package/dist/mcp/oauth/error-normalize.d.ts +39 -0
- package/dist/mcp/oauth/error-normalize.d.ts.map +1 -0
- package/dist/mcp/oauth/error-normalize.js +91 -0
- package/dist/mcp/oauth/error-normalize.js.map +1 -0
- package/dist/mcp/oauth/provider.d.ts +190 -0
- package/dist/mcp/oauth/provider.d.ts.map +1 -0
- package/dist/mcp/oauth/provider.js +305 -0
- package/dist/mcp/oauth/provider.js.map +1 -0
- package/dist/mcp/oauth/refresh-coalescer.d.ts +46 -0
- package/dist/mcp/oauth/refresh-coalescer.d.ts.map +1 -0
- package/dist/mcp/oauth/refresh-coalescer.js +77 -0
- package/dist/mcp/oauth/refresh-coalescer.js.map +1 -0
- package/dist/mcp/oauth/sdk-shapes.d.ts +77 -0
- package/dist/mcp/oauth/sdk-shapes.d.ts.map +1 -0
- package/dist/mcp/oauth/sdk-shapes.js +21 -0
- package/dist/mcp/oauth/sdk-shapes.js.map +1 -0
- package/dist/mcp/parse.d.ts +28 -0
- package/dist/mcp/parse.d.ts.map +1 -0
- package/dist/mcp/parse.js +209 -0
- package/dist/mcp/parse.js.map +1 -0
- package/dist/mcp/prompts.d.ts +31 -0
- package/dist/mcp/prompts.d.ts.map +1 -0
- package/dist/mcp/prompts.js +71 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/redact.d.ts +62 -0
- package/dist/mcp/redact.d.ts.map +1 -0
- package/dist/mcp/redact.js +87 -0
- package/dist/mcp/redact.js.map +1 -0
- package/dist/mcp/resources.d.ts +70 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +170 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/types.d.ts +123 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/memory/frontmatter.d.ts +18 -0
- package/dist/memory/frontmatter.d.ts.map +1 -0
- package/dist/memory/frontmatter.js +81 -0
- package/dist/memory/frontmatter.js.map +1 -0
- package/dist/memory/index.d.ts +5 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +5 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/store.d.ts +44 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +237 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/tools.d.ts +11 -0
- package/dist/memory/tools.d.ts.map +1 -0
- package/dist/memory/tools.js +159 -0
- package/dist/memory/tools.js.map +1 -0
- package/dist/memory/types.d.ts +32 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +20 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/plugin-api/index.d.ts +167 -0
- package/dist/plugin-api/index.d.ts.map +1 -0
- package/dist/plugin-api/index.js +151 -0
- package/dist/plugin-api/index.js.map +1 -0
- package/dist/plugin-logger/index.d.ts +21 -0
- package/dist/plugin-logger/index.d.ts.map +1 -0
- package/dist/plugin-logger/index.js +59 -0
- package/dist/plugin-logger/index.js.map +1 -0
- package/dist/session/index.d.ts +125 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +202 -0
- package/dist/session/index.js.map +1 -0
- package/dist/tools/approval.d.ts +33 -0
- package/dist/tools/approval.d.ts.map +1 -0
- package/dist/tools/approval.js +53 -0
- package/dist/tools/approval.js.map +1 -0
- package/dist/tools/builtins/_shared.d.ts +94 -0
- package/dist/tools/builtins/_shared.d.ts.map +1 -0
- package/dist/tools/builtins/_shared.js +246 -0
- package/dist/tools/builtins/_shared.js.map +1 -0
- package/dist/tools/builtins/ask-user-question.d.ts +27 -0
- package/dist/tools/builtins/ask-user-question.d.ts.map +1 -0
- package/dist/tools/builtins/ask-user-question.js +191 -0
- package/dist/tools/builtins/ask-user-question.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts +3 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -0
- package/dist/tools/builtins/bash.js +158 -0
- package/dist/tools/builtins/bash.js.map +1 -0
- package/dist/tools/builtins/diff.d.ts +3 -0
- package/dist/tools/builtins/diff.d.ts.map +1 -0
- package/dist/tools/builtins/diff.js +83 -0
- package/dist/tools/builtins/diff.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts +3 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -0
- package/dist/tools/builtins/edit.js +40 -0
- package/dist/tools/builtins/edit.js.map +1 -0
- package/dist/tools/builtins/glob.d.ts +3 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -0
- package/dist/tools/builtins/glob.js +37 -0
- package/dist/tools/builtins/glob.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +3 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -0
- package/dist/tools/builtins/grep.js +81 -0
- package/dist/tools/builtins/grep.js.map +1 -0
- package/dist/tools/builtins/lsp.d.ts +3 -0
- package/dist/tools/builtins/lsp.d.ts.map +1 -0
- package/dist/tools/builtins/lsp.js +102 -0
- package/dist/tools/builtins/lsp.js.map +1 -0
- package/dist/tools/builtins/pty.d.ts +64 -0
- package/dist/tools/builtins/pty.d.ts.map +1 -0
- package/dist/tools/builtins/pty.js +536 -0
- package/dist/tools/builtins/pty.js.map +1 -0
- package/dist/tools/builtins/read.d.ts +3 -0
- package/dist/tools/builtins/read.d.ts.map +1 -0
- package/dist/tools/builtins/read.js +18 -0
- package/dist/tools/builtins/read.js.map +1 -0
- package/dist/tools/builtins/web-fetch.d.ts +4 -0
- package/dist/tools/builtins/web-fetch.d.ts.map +1 -0
- package/dist/tools/builtins/web-fetch.js +353 -0
- package/dist/tools/builtins/web-fetch.js.map +1 -0
- package/dist/tools/builtins/write.d.ts +3 -0
- package/dist/tools/builtins/write.d.ts.map +1 -0
- package/dist/tools/builtins/write.js +48 -0
- package/dist/tools/builtins/write.js.map +1 -0
- package/dist/tools/builtins.d.ts +9 -0
- package/dist/tools/builtins.d.ts.map +1 -0
- package/dist/tools/builtins.js +29 -0
- package/dist/tools/builtins.js.map +1 -0
- package/dist/tools/diff.d.ts +18 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +57 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/permission.d.ts +120 -0
- package/dist/tools/permission.d.ts.map +1 -0
- package/dist/tools/permission.js +208 -0
- package/dist/tools/permission.js.map +1 -0
- package/dist/tools/registry.d.ts +12 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +19 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +244 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +15 -0
- package/dist/tools/types.js.map +1 -0
- package/package.json +109 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const WORKTREE_DIR_NAME = "worktrees";
|
|
2
|
+
export declare const CHANCES_DIR_NAME = ".chances";
|
|
3
|
+
export declare const ACTIVE_MARKER_FILE = "chances-active.json";
|
|
4
|
+
/** `<canonicalRoot>/.chances/worktrees`. Parent directory for every agent
|
|
5
|
+
* worktree under this repo. Created lazily on first `createAgentWorktree`. */
|
|
6
|
+
export declare function worktreesDir(canonicalRoot: string): string;
|
|
7
|
+
/** `<canonicalRoot>/.chances/worktrees/<slug>`. The exact path `git worktree
|
|
8
|
+
* add` is invoked against. */
|
|
9
|
+
export declare function worktreePathFor(canonicalRoot: string, slug: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Liveness marker for an active agent worktree. Stored INSIDE the worktree's
|
|
12
|
+
* git-admin directory (`<repo>/.git/worktrees/<slug>/chances-active.json`),
|
|
13
|
+
* not inside the worktree's working tree — so `git status` never sees it as
|
|
14
|
+
* an untracked file (which would otherwise mark the worktree dirty and
|
|
15
|
+
* defeat the GC). The admin dir is created by `git worktree add` and lives
|
|
16
|
+
* for the worktree's lifetime; cleanup is handled by `git worktree remove`.
|
|
17
|
+
*
|
|
18
|
+
* For a linked worktree, `<worktreePath>/.git` is a pointer file whose first
|
|
19
|
+
* line reads `gitdir: <admin-dir>`. We resolve it on demand.
|
|
20
|
+
*/
|
|
21
|
+
export declare function activeMarkerPathFor(worktreePath: string): Promise<string>;
|
|
22
|
+
/** Resolves the worktree's git-admin directory. For linked worktrees this is
|
|
23
|
+
* `<repo>/.git/worktrees/<slug>/`; for the main worktree (rarely the case
|
|
24
|
+
* for agent worktrees, but supported) it's `<repo>/.git`. */
|
|
25
|
+
export declare function resolveWorktreeGitDir(worktreePath: string): Promise<string>;
|
|
26
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/core/worktree/paths.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAC7C,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AAExD;8EAC8E;AAC9E,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;8BAC8B;AAC9B,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG/E;AAED;;6DAE6D;AAC7D,wBAAsB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBjF"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
export const WORKTREE_DIR_NAME = "worktrees";
|
|
4
|
+
export const CHANCES_DIR_NAME = ".chances";
|
|
5
|
+
export const ACTIVE_MARKER_FILE = "chances-active.json";
|
|
6
|
+
/** `<canonicalRoot>/.chances/worktrees`. Parent directory for every agent
|
|
7
|
+
* worktree under this repo. Created lazily on first `createAgentWorktree`. */
|
|
8
|
+
export function worktreesDir(canonicalRoot) {
|
|
9
|
+
return join(canonicalRoot, CHANCES_DIR_NAME, WORKTREE_DIR_NAME);
|
|
10
|
+
}
|
|
11
|
+
/** `<canonicalRoot>/.chances/worktrees/<slug>`. The exact path `git worktree
|
|
12
|
+
* add` is invoked against. */
|
|
13
|
+
export function worktreePathFor(canonicalRoot, slug) {
|
|
14
|
+
return join(worktreesDir(canonicalRoot), slug);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Liveness marker for an active agent worktree. Stored INSIDE the worktree's
|
|
18
|
+
* git-admin directory (`<repo>/.git/worktrees/<slug>/chances-active.json`),
|
|
19
|
+
* not inside the worktree's working tree — so `git status` never sees it as
|
|
20
|
+
* an untracked file (which would otherwise mark the worktree dirty and
|
|
21
|
+
* defeat the GC). The admin dir is created by `git worktree add` and lives
|
|
22
|
+
* for the worktree's lifetime; cleanup is handled by `git worktree remove`.
|
|
23
|
+
*
|
|
24
|
+
* For a linked worktree, `<worktreePath>/.git` is a pointer file whose first
|
|
25
|
+
* line reads `gitdir: <admin-dir>`. We resolve it on demand.
|
|
26
|
+
*/
|
|
27
|
+
export async function activeMarkerPathFor(worktreePath) {
|
|
28
|
+
const adminDir = await resolveWorktreeGitDir(worktreePath);
|
|
29
|
+
return join(adminDir, ACTIVE_MARKER_FILE);
|
|
30
|
+
}
|
|
31
|
+
/** Resolves the worktree's git-admin directory. For linked worktrees this is
|
|
32
|
+
* `<repo>/.git/worktrees/<slug>/`; for the main worktree (rarely the case
|
|
33
|
+
* for agent worktrees, but supported) it's `<repo>/.git`. */
|
|
34
|
+
export async function resolveWorktreeGitDir(worktreePath) {
|
|
35
|
+
const dotGit = join(worktreePath, ".git");
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = await readFile(dotGit, "utf8");
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// `.git` may be a directory in the (unusual) main-worktree case; fall
|
|
42
|
+
// back to using that directory as the admin dir.
|
|
43
|
+
if (e.code === "EISDIR")
|
|
44
|
+
return dotGit;
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
const m = body.match(/^gitdir:\s*(.+?)\s*$/m);
|
|
48
|
+
if (!m)
|
|
49
|
+
return dotGit;
|
|
50
|
+
// (Round-2 SHOULD-FIX #6) Older Git or special configs can write a
|
|
51
|
+
// RELATIVE `gitdir:` path; the standard resolution rule is "relative
|
|
52
|
+
// to the directory containing the `.git` pointer file" (i.e. the
|
|
53
|
+
// worktree dir).
|
|
54
|
+
const target = m[1];
|
|
55
|
+
return isAbsolute(target) ? target : resolve(dirname(dotGit), target);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/worktree/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE/D,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAC7C,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAC3C,MAAM,CAAC,MAAM,kBAAkB,GAAG,qBAAqB,CAAC;AAExD;8EAC8E;AAC9E,MAAM,UAAU,YAAY,CAAC,aAAqB;IAChD,OAAO,IAAI,CAAC,aAAa,EAAE,gBAAgB,EAAE,iBAAiB,CAAC,CAAC;AAClE,CAAC;AAED;8BAC8B;AAC9B,MAAM,UAAU,eAAe,CAAC,aAAqB,EAAE,IAAY;IACjE,OAAO,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,YAAoB;IAC5D,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,YAAY,CAAC,CAAC;IAC3D,OAAO,IAAI,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;AAC5C,CAAC;AAED;;6DAE6D;AAC7D,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,YAAoB;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAC1C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,sEAAsE;QACtE,iDAAiD;QACjD,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC;QAClE,MAAM,CAAC,CAAC;IACV,CAAC;IACD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC9C,IAAI,CAAC,CAAC;QAAE,OAAO,MAAM,CAAC;IACtB,mEAAmE;IACnE,qEAAqE;IACrE,iEAAiE;IACjE,iBAAiB;IACjB,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IACrB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function newAgentSlug(): string;
|
|
2
|
+
export declare function isAgentSlug(s: string): boolean;
|
|
3
|
+
/** Slug → branch name. The `chances/` namespace is ours; users can `git
|
|
4
|
+
* branch -D chances/*` to reclaim. */
|
|
5
|
+
export declare function branchNameFor(slug: string): string;
|
|
6
|
+
//# sourceMappingURL=slug.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.d.ts","sourceRoot":"","sources":["../../../src/core/worktree/slug.ts"],"names":[],"mappings":"AAQA,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED;sCACsC;AACtC,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKlD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
/** Slug format: `agent-` followed by exactly 8 lowercase hex chars. Matches
|
|
3
|
+
* claude-code's per-subagent slug shape. Branch-name-safe (no `/`), short
|
|
4
|
+
* enough to read, ~4 billion-space entropy so birthday collisions are
|
|
5
|
+
* negligible at any realistic concurrent-subagent count. */
|
|
6
|
+
const SLUG_RE = /^agent-[0-9a-f]{8}$/;
|
|
7
|
+
export function newAgentSlug() {
|
|
8
|
+
return `agent-${randomBytes(4).toString("hex")}`;
|
|
9
|
+
}
|
|
10
|
+
export function isAgentSlug(s) {
|
|
11
|
+
return SLUG_RE.test(s);
|
|
12
|
+
}
|
|
13
|
+
/** Slug → branch name. The `chances/` namespace is ours; users can `git
|
|
14
|
+
* branch -D chances/*` to reclaim. */
|
|
15
|
+
export function branchNameFor(slug) {
|
|
16
|
+
if (!isAgentSlug(slug)) {
|
|
17
|
+
throw new Error(`invalid slug for branch name: ${slug}`);
|
|
18
|
+
}
|
|
19
|
+
return `chances/worktree-${slug}`;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=slug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../../../src/core/worktree/slug.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;4DAG4D;AAC5D,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAEtC,MAAM,UAAU,YAAY;IAC1B,OAAO,SAAS,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACzB,CAAC;AAED;sCACsC;AACtC,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,oBAAoB,IAAI,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (3.7) AES-256-GCM encrypted JSON file backend with scrypt KDF.
|
|
3
|
+
*
|
|
4
|
+
* On-disk format (versioned for forward-compat):
|
|
5
|
+
* { v: 1, salt: base64, iv: base64, ciphertext: base64, tag: base64 }
|
|
6
|
+
*
|
|
7
|
+
* - Salt: 16 random bytes generated on first write; persisted unchanged
|
|
8
|
+
* thereafter so the derived key stays stable across writes.
|
|
9
|
+
* - IV: 12 random bytes ROTATED on every write (GCM requires unique
|
|
10
|
+
* IV per key/plaintext pair).
|
|
11
|
+
* - KDF: scryptSync (N=16384, r=8, p=1). Matches claude-code's LocalVault
|
|
12
|
+
* parameters. Cached per-passphrase+salt to amortise the ~80–150 ms
|
|
13
|
+
* derivation cost across writes within one process.
|
|
14
|
+
*
|
|
15
|
+
* **(codex Round-1 MUST-FIX #1)** All mutations go through `mergeMutate`
|
|
16
|
+
* which serialises per-key via the KeyedMutex and preserves unrelated
|
|
17
|
+
* fields. Atomic-rename alone is insufficient — concurrent partial
|
|
18
|
+
* updates would clobber each other.
|
|
19
|
+
*/
|
|
20
|
+
export interface FileStoreOptions {
|
|
21
|
+
filePath: string;
|
|
22
|
+
passphrase: string;
|
|
23
|
+
/** Test seam — defaults to `randomBytes`. */
|
|
24
|
+
generateBytes?: (n: number) => Buffer;
|
|
25
|
+
}
|
|
26
|
+
export declare class FileStore {
|
|
27
|
+
private readonly mutex;
|
|
28
|
+
private readonly opts;
|
|
29
|
+
private cachedKey?;
|
|
30
|
+
constructor(opts: FileStoreOptions);
|
|
31
|
+
/** Returns the JSON value at `key`, or `undefined` when absent. */
|
|
32
|
+
read<T = unknown>(key: string): Promise<T | undefined>;
|
|
33
|
+
/** Returns every top-level key. */
|
|
34
|
+
list(): Promise<readonly string[]>;
|
|
35
|
+
/** Removes a key. No-op when absent. */
|
|
36
|
+
remove(key: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* The cornerstone API. Serialises per-key R-W-CAS:
|
|
39
|
+
* 1. Decrypt the whole on-disk blob.
|
|
40
|
+
* 2. Call `mutator(currentValueAtKey)`.
|
|
41
|
+
* 3. If mutator returns a value, replace; if it returns undefined AND
|
|
42
|
+
* `allowDelete` is true, delete the entry.
|
|
43
|
+
* 4. Encrypt + atomic-write.
|
|
44
|
+
*
|
|
45
|
+
* **(3.7 codex Round-2 MUST-FIX #1)** Uses a SINGLE file-wide mutex
|
|
46
|
+
* (constant `FILE_LOCK_KEY`), not a per-key mutex. The on-disk format
|
|
47
|
+
* is one encrypted JSON blob — read-modify-write of `key=A` and
|
|
48
|
+
* `key=B` both decrypt the SAME ciphertext, mutate different slots,
|
|
49
|
+
* and the last writer's `encryptAndWrite()` clobbers the other's
|
|
50
|
+
* mutation. Per-key locking only protects same-key writes; cross-key
|
|
51
|
+
* concurrency loses data. Serialising every file write behind one
|
|
52
|
+
* lock costs concurrency that file-vault setups don't have (write
|
|
53
|
+
* volume is "every few minutes when a token refreshes") and buys
|
|
54
|
+
* correctness on the only invariant that matters.
|
|
55
|
+
*/
|
|
56
|
+
mergeMutate<T>(key: string, mutator: (current: T | undefined) => T | undefined | Promise<T | undefined>, allowDelete?: boolean): Promise<void>;
|
|
57
|
+
/** Decrypts the whole file. Returns `{}` when the file doesn't exist. */
|
|
58
|
+
private decryptAll;
|
|
59
|
+
/** Encrypts a payload and atomically writes the file. */
|
|
60
|
+
private encryptAndWrite;
|
|
61
|
+
/** Cached scrypt derivation. */
|
|
62
|
+
private deriveKey;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=file-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../../src/local-vault/file-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAyBD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,SAAS,CAAC,CAAgC;gBAEtC,IAAI,EAAE,gBAAgB;IAIlC,mEAAmE;IAC7D,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAU5D,mCAAmC;IAC7B,IAAI,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAYxC,wCAAwC;IAClC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxC;;;;;;;;;;;;;;;;;;OAkBG;IACG,WAAW,CAAC,CAAC,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,EAC3E,WAAW,UAAQ,GAClB,OAAO,CAAC,IAAI,CAAC;IAiChB,yEAAyE;IACzE,OAAO,CAAC,UAAU;IAsClB,yDAAyD;IACzD,OAAO,CAAC,eAAe;IA0CvB,gCAAgC;IAChC,OAAO,CAAC,SAAS;CAYlB"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (3.7) AES-256-GCM encrypted JSON file backend with scrypt KDF.
|
|
3
|
+
*
|
|
4
|
+
* On-disk format (versioned for forward-compat):
|
|
5
|
+
* { v: 1, salt: base64, iv: base64, ciphertext: base64, tag: base64 }
|
|
6
|
+
*
|
|
7
|
+
* - Salt: 16 random bytes generated on first write; persisted unchanged
|
|
8
|
+
* thereafter so the derived key stays stable across writes.
|
|
9
|
+
* - IV: 12 random bytes ROTATED on every write (GCM requires unique
|
|
10
|
+
* IV per key/plaintext pair).
|
|
11
|
+
* - KDF: scryptSync (N=16384, r=8, p=1). Matches claude-code's LocalVault
|
|
12
|
+
* parameters. Cached per-passphrase+salt to amortise the ~80–150 ms
|
|
13
|
+
* derivation cost across writes within one process.
|
|
14
|
+
*
|
|
15
|
+
* **(codex Round-1 MUST-FIX #1)** All mutations go through `mergeMutate`
|
|
16
|
+
* which serialises per-key via the KeyedMutex and preserves unrelated
|
|
17
|
+
* fields. Atomic-rename alone is insufficient — concurrent partial
|
|
18
|
+
* updates would clobber each other.
|
|
19
|
+
*/
|
|
20
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
21
|
+
import { existsSync, readFileSync, renameSync, statSync, writeFileSync, chmodSync, mkdirSync } from "node:fs";
|
|
22
|
+
import { dirname } from "node:path";
|
|
23
|
+
import { KeyedMutex } from "./mutex.js";
|
|
24
|
+
const SALT_LEN = 16;
|
|
25
|
+
const IV_LEN = 12;
|
|
26
|
+
const KEY_LEN = 32;
|
|
27
|
+
const SCRYPT_N = 16384;
|
|
28
|
+
const SCRYPT_R = 8;
|
|
29
|
+
const SCRYPT_P = 1;
|
|
30
|
+
/** (codex Round-2 MUST-FIX #1) Single file-wide lock key — every
|
|
31
|
+
* `mergeMutate` rewrites the whole encrypted blob, so cross-key
|
|
32
|
+
* concurrency is unsafe with per-key granularity. See class docstring. */
|
|
33
|
+
const FILE_LOCK_KEY = "__file_lock__";
|
|
34
|
+
export class FileStore {
|
|
35
|
+
mutex = new KeyedMutex();
|
|
36
|
+
opts;
|
|
37
|
+
cachedKey;
|
|
38
|
+
constructor(opts) {
|
|
39
|
+
this.opts = opts;
|
|
40
|
+
}
|
|
41
|
+
/** Returns the JSON value at `key`, or `undefined` when absent. */
|
|
42
|
+
async read(key) {
|
|
43
|
+
const release = await this.mutex.acquire(key);
|
|
44
|
+
try {
|
|
45
|
+
const payload = this.decryptAll();
|
|
46
|
+
return payload[key];
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
release();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Returns every top-level key. */
|
|
53
|
+
async list() {
|
|
54
|
+
// Global lock — we read every key. Mutate ops do per-key locking; this
|
|
55
|
+
// is the lone exception that competes with EVERY pending write.
|
|
56
|
+
const release = await this.mutex.acquire("__list__");
|
|
57
|
+
try {
|
|
58
|
+
const payload = this.decryptAll();
|
|
59
|
+
return Object.keys(payload);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
release();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Removes a key. No-op when absent. */
|
|
66
|
+
async remove(key) {
|
|
67
|
+
await this.mergeMutate(key, (current) => {
|
|
68
|
+
if (current === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
return undefined; // Marker for delete (vs no-change)
|
|
71
|
+
}, /*allowDelete=*/ true);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The cornerstone API. Serialises per-key R-W-CAS:
|
|
75
|
+
* 1. Decrypt the whole on-disk blob.
|
|
76
|
+
* 2. Call `mutator(currentValueAtKey)`.
|
|
77
|
+
* 3. If mutator returns a value, replace; if it returns undefined AND
|
|
78
|
+
* `allowDelete` is true, delete the entry.
|
|
79
|
+
* 4. Encrypt + atomic-write.
|
|
80
|
+
*
|
|
81
|
+
* **(3.7 codex Round-2 MUST-FIX #1)** Uses a SINGLE file-wide mutex
|
|
82
|
+
* (constant `FILE_LOCK_KEY`), not a per-key mutex. The on-disk format
|
|
83
|
+
* is one encrypted JSON blob — read-modify-write of `key=A` and
|
|
84
|
+
* `key=B` both decrypt the SAME ciphertext, mutate different slots,
|
|
85
|
+
* and the last writer's `encryptAndWrite()` clobbers the other's
|
|
86
|
+
* mutation. Per-key locking only protects same-key writes; cross-key
|
|
87
|
+
* concurrency loses data. Serialising every file write behind one
|
|
88
|
+
* lock costs concurrency that file-vault setups don't have (write
|
|
89
|
+
* volume is "every few minutes when a token refreshes") and buys
|
|
90
|
+
* correctness on the only invariant that matters.
|
|
91
|
+
*/
|
|
92
|
+
async mergeMutate(key, mutator, allowDelete = false) {
|
|
93
|
+
const release = await this.mutex.acquire(FILE_LOCK_KEY);
|
|
94
|
+
try {
|
|
95
|
+
// Re-read inside the lock so we always see the latest state, even if
|
|
96
|
+
// another tab/process raced us between the mutex-acquire and our read.
|
|
97
|
+
// (Cross-process is still out of scope — see § 1 of the design doc —
|
|
98
|
+
// but in-process correctness is locked down here.)
|
|
99
|
+
const payload = this.decryptAll();
|
|
100
|
+
const previous = payload[key];
|
|
101
|
+
const next = await mutator(previous);
|
|
102
|
+
if (next === undefined) {
|
|
103
|
+
if (!allowDelete && previous === undefined) {
|
|
104
|
+
// Mutator declined to set, and nothing was there — write a no-op.
|
|
105
|
+
// Returning here skips the disk write to avoid pointless IV churn.
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (allowDelete) {
|
|
109
|
+
delete payload[key];
|
|
110
|
+
}
|
|
111
|
+
else if (previous !== undefined) {
|
|
112
|
+
// Mutator returned undefined but allowDelete is false — interpret
|
|
113
|
+
// as "no change," not delete. Defensive — keeps the API
|
|
114
|
+
// explicit about the destructive intent of `remove`.
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
payload[key] = next;
|
|
120
|
+
}
|
|
121
|
+
this.encryptAndWrite(payload);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
release();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Decrypts the whole file. Returns `{}` when the file doesn't exist. */
|
|
128
|
+
decryptAll() {
|
|
129
|
+
if (!existsSync(this.opts.filePath))
|
|
130
|
+
return {};
|
|
131
|
+
const raw = readFileSync(this.opts.filePath, "utf8");
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(raw);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
throw new Error(`local-vault: failed to parse ${this.opts.filePath}: ${e.message}. ` +
|
|
138
|
+
`The file may be corrupted; back it up and delete to start fresh.`);
|
|
139
|
+
}
|
|
140
|
+
if (parsed.v !== 1) {
|
|
141
|
+
throw new Error(`local-vault: unsupported on-disk version ${parsed.v} (this build supports v1)`);
|
|
142
|
+
}
|
|
143
|
+
const salt = Buffer.from(parsed.salt, "base64");
|
|
144
|
+
const iv = Buffer.from(parsed.iv, "base64");
|
|
145
|
+
const ciphertext = Buffer.from(parsed.ciphertext, "base64");
|
|
146
|
+
const tag = Buffer.from(parsed.tag, "base64");
|
|
147
|
+
const key = this.deriveKey(salt);
|
|
148
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
149
|
+
decipher.setAuthTag(tag);
|
|
150
|
+
let plaintext;
|
|
151
|
+
try {
|
|
152
|
+
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
throw new Error(`local-vault: decryption failed — wrong passphrase OR corrupted file. ` +
|
|
156
|
+
`Underlying: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
throw new Error(`local-vault: decrypted JSON is malformed: ${e.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Encrypts a payload and atomically writes the file. */
|
|
166
|
+
encryptAndWrite(payload) {
|
|
167
|
+
// Reuse the existing salt when present, else mint a new one.
|
|
168
|
+
let salt;
|
|
169
|
+
if (this.cachedKey) {
|
|
170
|
+
salt = this.cachedKey.salt;
|
|
171
|
+
}
|
|
172
|
+
else if (existsSync(this.opts.filePath)) {
|
|
173
|
+
// Edge: cache empty but file exists. Read the salt from disk.
|
|
174
|
+
const existing = JSON.parse(readFileSync(this.opts.filePath, "utf8"));
|
|
175
|
+
salt = Buffer.from(existing.salt, "base64");
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
salt = (this.opts.generateBytes ?? randomBytes)(SALT_LEN);
|
|
179
|
+
}
|
|
180
|
+
const key = this.deriveKey(salt);
|
|
181
|
+
const iv = (this.opts.generateBytes ?? randomBytes)(IV_LEN);
|
|
182
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
183
|
+
const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
|
|
184
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
185
|
+
const tag = cipher.getAuthTag();
|
|
186
|
+
const wire = {
|
|
187
|
+
v: 1,
|
|
188
|
+
salt: salt.toString("base64"),
|
|
189
|
+
iv: iv.toString("base64"),
|
|
190
|
+
ciphertext: ciphertext.toString("base64"),
|
|
191
|
+
tag: tag.toString("base64"),
|
|
192
|
+
};
|
|
193
|
+
// Ensure parent dir exists. Atomic write via tmp + rename.
|
|
194
|
+
mkdirSync(dirname(this.opts.filePath), { recursive: true });
|
|
195
|
+
const tmp = `${this.opts.filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
196
|
+
writeFileSync(tmp, JSON.stringify(wire), { mode: 0o600 });
|
|
197
|
+
renameSync(tmp, this.opts.filePath);
|
|
198
|
+
if (process.platform !== "win32") {
|
|
199
|
+
// Re-assert 0600 — atomic rename preserves source mode, but a tmp
|
|
200
|
+
// file created with umask interference could land at 0644.
|
|
201
|
+
try {
|
|
202
|
+
const stat = statSync(this.opts.filePath);
|
|
203
|
+
if ((stat.mode & 0o777) !== 0o600)
|
|
204
|
+
chmodSync(this.opts.filePath, 0o600);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
/* race; fall through */
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Cached scrypt derivation. */
|
|
212
|
+
deriveKey(salt) {
|
|
213
|
+
if (this.cachedKey && this.cachedKey.salt.equals(salt)) {
|
|
214
|
+
return this.cachedKey.key;
|
|
215
|
+
}
|
|
216
|
+
const key = scryptSync(this.opts.passphrase, salt, KEY_LEN, {
|
|
217
|
+
N: SCRYPT_N,
|
|
218
|
+
r: SCRYPT_R,
|
|
219
|
+
p: SCRYPT_P,
|
|
220
|
+
});
|
|
221
|
+
this.cachedKey = { salt, key };
|
|
222
|
+
return key;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=file-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-store.js","sourceRoot":"","sources":["../../src/local-vault/file-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (3.7) `@chances-ai/local-vault` — encrypted credential store.
|
|
3
|
+
*
|
|
4
|
+
* Public surface: `createLocalVault(opts?) -> Promise<LocalVault>`.
|
|
5
|
+
*
|
|
6
|
+
* Backend selection at boot:
|
|
7
|
+
* 1. Probe `@napi-rs/keyring` — if it loads AND a set/get round-trip
|
|
8
|
+
* against a probe key succeeds, backend = keychain.
|
|
9
|
+
* 2. Otherwise, backend = AES-256-GCM file at
|
|
10
|
+
* `<rootDir>/local-vault.enc.json`, with passphrase resolved per
|
|
11
|
+
* `passphrase.ts` (env > sibling file > generated).
|
|
12
|
+
*
|
|
13
|
+
* Both backends share the same `LocalVault` surface: read/list/remove/
|
|
14
|
+
* mergeMutate/diagnostics. The mergeMutate primitive (per-key serialised
|
|
15
|
+
* read-modify-write) is the codex Round-1 MUST-FIX #1 fix — atomic
|
|
16
|
+
* rename alone doesn't prevent partial-update clobber.
|
|
17
|
+
*/
|
|
18
|
+
import { type KeyringLoader } from "./keychain.js";
|
|
19
|
+
export interface LocalVault {
|
|
20
|
+
read<T = unknown>(key: string): Promise<T | undefined>;
|
|
21
|
+
list(): Promise<readonly string[]>;
|
|
22
|
+
remove(key: string): Promise<void>;
|
|
23
|
+
/** Per-key serialised read-modify-write. Mutator receives the current
|
|
24
|
+
* value (or undefined). Returning a value writes it. Returning
|
|
25
|
+
* undefined is a no-op UNLESS `allowDelete=true`. */
|
|
26
|
+
mergeMutate<T>(key: string, mutator: (current: T | undefined) => T | undefined | Promise<T | undefined>, allowDelete?: boolean): Promise<void>;
|
|
27
|
+
diagnostics(): VaultDiagnostics;
|
|
28
|
+
}
|
|
29
|
+
export interface VaultDiagnostics {
|
|
30
|
+
backend: "keychain" | "file";
|
|
31
|
+
/** Human-readable reasons explaining why the keychain backend was
|
|
32
|
+
* bypassed, when applicable. */
|
|
33
|
+
warnings: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
export interface CreateLocalVaultOptions {
|
|
36
|
+
/** Where vault data lives. Defaults to `~/.chances/`. */
|
|
37
|
+
rootDir?: string;
|
|
38
|
+
/** Where the passphrase file lives. Defaults to
|
|
39
|
+
* `<dirname(homedir)>/.chances-vault-passphrase` — OUTSIDE the
|
|
40
|
+
* vault tree so a backup of `rootDir` alone doesn't leak the key
|
|
41
|
+
* (codex Round-1 SHOULD-FIX). */
|
|
42
|
+
passphraseFile?: string;
|
|
43
|
+
/** Test seam — swap the keyring loader. Defaults to the real loader. */
|
|
44
|
+
keyringLoader?: KeyringLoader;
|
|
45
|
+
/** Force the file backend even when keychain is available. Used for
|
|
46
|
+
* smoke tests of the fallback path; users shouldn't set this. */
|
|
47
|
+
forceFileBackend?: boolean;
|
|
48
|
+
/** Test seam — override the passphrase resolution entirely. */
|
|
49
|
+
fixedPassphrase?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare const VAULT_FILE_NAME = "local-vault.enc.json";
|
|
52
|
+
export declare function createLocalVault(opts?: CreateLocalVaultOptions): Promise<LocalVault>;
|
|
53
|
+
export { FileStore } from "./file-store.js";
|
|
54
|
+
export { KeychainStore, type KeyringLoader, type KeyringModule } from "./keychain.js";
|
|
55
|
+
export { resolvePassphrase, ENV_VAR as VAULT_ENV_VAR } from "./passphrase.js";
|
|
56
|
+
export { KeyedMutex } from "./mutex.js";
|
|
57
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/local-vault/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAGlE,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACvD,IAAI,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC;;0DAEsD;IACtD,WAAW,CAAC,CAAC,EACX,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,EAC3E,WAAW,CAAC,EAAE,OAAO,GACpB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,WAAW,IAAI,gBAAgB,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,UAAU,GAAG,MAAM,CAAC;IAC7B;qCACiC;IACjC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;sCAGkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;sEACkE;IAClE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,eAAe,yBAAyB,CAAC;AAEtD,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG,OAAO,CAAC,UAAU,CAAC,CAqB9F;AAuBD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AACtF,OAAO,EAAE,iBAAiB,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (3.7) `@chances-ai/local-vault` — encrypted credential store.
|
|
3
|
+
*
|
|
4
|
+
* Public surface: `createLocalVault(opts?) -> Promise<LocalVault>`.
|
|
5
|
+
*
|
|
6
|
+
* Backend selection at boot:
|
|
7
|
+
* 1. Probe `@napi-rs/keyring` — if it loads AND a set/get round-trip
|
|
8
|
+
* against a probe key succeeds, backend = keychain.
|
|
9
|
+
* 2. Otherwise, backend = AES-256-GCM file at
|
|
10
|
+
* `<rootDir>/local-vault.enc.json`, with passphrase resolved per
|
|
11
|
+
* `passphrase.ts` (env > sibling file > generated).
|
|
12
|
+
*
|
|
13
|
+
* Both backends share the same `LocalVault` surface: read/list/remove/
|
|
14
|
+
* mergeMutate/diagnostics. The mergeMutate primitive (per-key serialised
|
|
15
|
+
* read-modify-write) is the codex Round-1 MUST-FIX #1 fix — atomic
|
|
16
|
+
* rename alone doesn't prevent partial-update clobber.
|
|
17
|
+
*/
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { FileStore } from "./file-store.js";
|
|
21
|
+
import { KeychainStore } from "./keychain.js";
|
|
22
|
+
import { resolvePassphrase } from "./passphrase.js";
|
|
23
|
+
export const VAULT_FILE_NAME = "local-vault.enc.json";
|
|
24
|
+
export async function createLocalVault(opts = {}) {
|
|
25
|
+
const rootDir = opts.rootDir ?? join(homedir(), ".chances");
|
|
26
|
+
const passphraseFile = opts.passphraseFile ?? join(homedir(), ".chances-vault-passphrase");
|
|
27
|
+
const warnings = [];
|
|
28
|
+
// 1. Try keychain first, unless explicitly forced to file.
|
|
29
|
+
if (!opts.forceFileBackend) {
|
|
30
|
+
const probe = await KeychainStore.tryCreate(opts.keyringLoader);
|
|
31
|
+
if (probe.store) {
|
|
32
|
+
return wrapKeychain(probe.store);
|
|
33
|
+
}
|
|
34
|
+
warnings.push(probe.reason);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
warnings.push("forceFileBackend=true (test/diagnostic mode)");
|
|
38
|
+
}
|
|
39
|
+
// 2. Fall back to file backend.
|
|
40
|
+
const filePath = join(rootDir, VAULT_FILE_NAME);
|
|
41
|
+
const passphrase = opts.fixedPassphrase ?? resolvePassphrase({ passphraseFile }).passphrase;
|
|
42
|
+
const file = new FileStore({ filePath, passphrase });
|
|
43
|
+
return wrapFile(file, warnings);
|
|
44
|
+
}
|
|
45
|
+
function wrapKeychain(store) {
|
|
46
|
+
return {
|
|
47
|
+
read: (k) => store.read(k),
|
|
48
|
+
list: () => store.list(),
|
|
49
|
+
remove: (k) => store.remove(k),
|
|
50
|
+
mergeMutate: (k, m, allowDelete) => store.mergeMutate(k, m, allowDelete),
|
|
51
|
+
diagnostics: () => ({ backend: "keychain", warnings: [] }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function wrapFile(store, warnings) {
|
|
55
|
+
const frozen = Object.freeze([...warnings]);
|
|
56
|
+
return {
|
|
57
|
+
read: (k) => store.read(k),
|
|
58
|
+
list: () => store.list(),
|
|
59
|
+
remove: (k) => store.remove(k),
|
|
60
|
+
mergeMutate: (k, m, allowDelete) => store.mergeMutate(k, m, allowDelete),
|
|
61
|
+
diagnostics: () => ({ backend: "file", warnings: frozen }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export { FileStore } from "./file-store.js";
|
|
65
|
+
export { KeychainStore } from "./keychain.js";
|
|
66
|
+
export { resolvePassphrase, ENV_VAR as VAULT_ENV_VAR } from "./passphrase.js";
|
|
67
|
+
export { KeyedMutex } from "./mutex.js";
|
|
68
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/local-vault/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (3.7) OS keychain adapter wrapping `@napi-rs/keyring`.
|
|
3
|
+
*
|
|
4
|
+
* Loaded via a computed-specifier dynamic import (same `bun build
|
|
5
|
+
* --compile` reason as 3.6 OTel) so the binary still compiles when the
|
|
6
|
+
* optional peer is absent. A consumer that didn't install the keyring
|
|
7
|
+
* package falls back to the file backend (`./file-store.ts`) — same
|
|
8
|
+
* graceful degradation as before, no opaque "module not found" crash
|
|
9
|
+
* at boot.
|
|
10
|
+
*
|
|
11
|
+
* Layout: service name `chances-cli` (fixed), account name = vault key.
|
|
12
|
+
* Values are JSON-stringified before storage; parsed on read.
|
|
13
|
+
*
|
|
14
|
+
* **(claude-code anti-pattern noted)** Don't store full RFC 8414 metadata
|
|
15
|
+
* here — Windows Credential Manager has a 4096-byte limit on stored
|
|
16
|
+
* values. § 4.2 of the design doc enforces "URLs only" at the higher
|
|
17
|
+
* (provider) layer; this adapter doesn't try to enforce shape.
|
|
18
|
+
*/
|
|
19
|
+
export interface KeyringEntry {
|
|
20
|
+
setPassword(password: string): void | Promise<void>;
|
|
21
|
+
getPassword(): string | null | Promise<string | null>;
|
|
22
|
+
deletePassword(): boolean | Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
export interface KeyringModule {
|
|
25
|
+
Entry: new (service: string, account: string) => KeyringEntry;
|
|
26
|
+
}
|
|
27
|
+
export type KeyringLoader = () => Promise<KeyringModule | null>;
|
|
28
|
+
export declare class KeychainStore {
|
|
29
|
+
private readonly mod;
|
|
30
|
+
private readonly mutex;
|
|
31
|
+
private constructor();
|
|
32
|
+
/**
|
|
33
|
+
* Loads the keyring module + runs an access probe. Returns:
|
|
34
|
+
* - the store, when the module loaded AND the probe succeeded;
|
|
35
|
+
* - `null` with a reason string, otherwise.
|
|
36
|
+
*
|
|
37
|
+
* The probe is critical on macOS — the module loads fine, but the FIRST
|
|
38
|
+
* keychain op can throw with a user-denied dialog. We catch that here so
|
|
39
|
+
* the fallback path runs without hanging the consumer.
|
|
40
|
+
*/
|
|
41
|
+
static tryCreate(loader?: KeyringLoader): Promise<{
|
|
42
|
+
store: KeychainStore;
|
|
43
|
+
} | {
|
|
44
|
+
store: null;
|
|
45
|
+
reason: string;
|
|
46
|
+
}>;
|
|
47
|
+
read<T = unknown>(key: string): Promise<T | undefined>;
|
|
48
|
+
list(): Promise<readonly string[]>;
|
|
49
|
+
remove(key: string): Promise<void>;
|
|
50
|
+
mergeMutate<T>(key: string, mutator: (current: T | undefined) => T | undefined | Promise<T | undefined>, allowDelete?: boolean): Promise<void>;
|
|
51
|
+
/** (codex Round-2 NICE #3) Update the index under its OWN mutex so
|
|
52
|
+
* concurrent `mergeMutate("a")` / `mergeMutate("b")` can't race on
|
|
53
|
+
* the shared `__chances_keys__` entry. Each holds its per-key lock
|
|
54
|
+
* for the value write, then queues behind the index mutex for the
|
|
55
|
+
* list update. `list()` could otherwise drop entries. */
|
|
56
|
+
private recordKey;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=keychain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keychain.d.ts","sourceRoot":"","sources":["../../src/local-vault/keychain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAUH,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,WAAW,IAAI,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,cAAc,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,YAAY,CAAC;CAC/D;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;AAehE,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAE1C,OAAO;IAIP;;;;;;;;OAQG;WACU,SAAS,CACpB,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC;QAAE,KAAK,EAAE,aAAa,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAwBhE,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAgBtD,IAAI,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAWlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgClC,WAAW,CAAC,CAAC,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,EAC3E,WAAW,UAAQ,GAClB,OAAO,CAAC,IAAI,CAAC;IA6BhB;;;;8DAI0D;YAC5C,SAAS;CAsBxB"}
|