toolcraft 0.0.23 → 0.0.25
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 +2 -2
- package/dist/cli.compile-check.js +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +50 -13
- package/dist/error-report.js +32 -3
- package/dist/human-in-loop/approval-tasks.d.ts +1 -0
- package/dist/human-in-loop/approval-tasks.js +7 -5
- package/dist/human-in-loop/approvals-commands.js +51 -8
- package/dist/human-in-loop/runner.js +24 -19
- package/dist/human-in-loop/state-machine.d.ts +3 -3
- package/dist/human-in-loop/state-machine.js +13 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -1
- package/dist/mcp-proxy.js +85 -19
- package/dist/mcp.compile-check.js +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +50 -8
- package/dist/renderer.js +119 -13
- package/dist/sdk.compile-check.js +1 -0
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +56 -11
- package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
- package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
- package/node_modules/@poe-code/agent-defs/package.json +1 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
- package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
- package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
- package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
- package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
- package/node_modules/@poe-code/config-mutations/package.json +1 -1
- package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
- package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
- package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
- package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
- package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
- package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
- package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/design-system/dist/index.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
- package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
- package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
- package/node_modules/@poe-code/design-system/package.json +2 -1
- package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +377 -130
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +78 -10
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +21 -5
- package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
- package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +61 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +503 -0
- package/node_modules/@poe-code/process-runner/package.json +1 -1
- package/node_modules/@poe-code/task-list/README.md +0 -2
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
- package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/task-list/dist/index.js +2 -0
- package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/move.js +215 -0
- package/node_modules/@poe-code/task-list/dist/open.js +3 -4
- package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
- package/node_modules/@poe-code/task-list/dist/state.js +9 -0
- package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
- package/node_modules/@poe-code/task-list/package.json +1 -2
- package/node_modules/auth-store/dist/create-secret-store.js +4 -1
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +8 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +104 -8
- package/node_modules/auth-store/dist/index.d.ts +1 -1
- package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
- package/node_modules/auth-store/dist/keychain-store.js +18 -16
- package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
- package/node_modules/auth-store/dist/provider-store.js +55 -7
- package/node_modules/auth-store/dist/types.d.ts +3 -1
- package/node_modules/auth-store/package.json +2 -1
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
- package/node_modules/mcp-oauth/package.json +1 -0
- package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +9 -4
- package/node_modules/tiny-mcp-client/dist/internal.js +244 -66
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
- package/node_modules/tiny-mcp-client/package.json +2 -1
- package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
- package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
- package/node_modules/tiny-mcp-client/src/internal.ts +287 -76
- package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
- package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
- package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
- package/package.json +10 -12
- package/node_modules/@poe-code/file-lock/README.md +0 -52
- package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
- package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
- package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
- package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
- package/node_modules/@poe-code/file-lock/package.json +0 -23
|
@@ -59,7 +59,9 @@ export function eventsFromState(machine, fromState) {
|
|
|
59
59
|
return events;
|
|
60
60
|
}
|
|
61
61
|
export function findEvent(machine, fromState, eventName) {
|
|
62
|
-
const event = machine.events
|
|
62
|
+
const event = Object.prototype.hasOwnProperty.call(machine.events, eventName)
|
|
63
|
+
? machine.events[eventName]
|
|
64
|
+
: undefined;
|
|
63
65
|
if (event === undefined) {
|
|
64
66
|
return undefined;
|
|
65
67
|
}
|
|
@@ -10,6 +10,15 @@ export const defaultStateMachine = {
|
|
|
10
10
|
archive: { from: "*", to: "archived" }
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
|
+
Object.freeze(defaultStateMachine.states);
|
|
14
|
+
for (const event of Object.values(defaultStateMachine.events)) {
|
|
15
|
+
if (event.from !== "*") {
|
|
16
|
+
Object.freeze(event.from);
|
|
17
|
+
}
|
|
18
|
+
Object.freeze(event);
|
|
19
|
+
}
|
|
20
|
+
Object.freeze(defaultStateMachine.events);
|
|
21
|
+
Object.freeze(defaultStateMachine);
|
|
13
22
|
function deriveLegacyTransitions(machine) {
|
|
14
23
|
const transitions = Object.fromEntries(machine.states.map((state) => [state, new Set()]));
|
|
15
24
|
for (const fromState of machine.states) {
|
|
@@ -64,15 +64,12 @@ export interface TaskDefaults {
|
|
|
64
64
|
metadata?: Record<string, unknown>;
|
|
65
65
|
}
|
|
66
66
|
export interface TaskListFs {
|
|
67
|
+
lstat(path: string): Promise<{
|
|
68
|
+
isSymbolicLink(): boolean;
|
|
69
|
+
}>;
|
|
67
70
|
mkdir(path: string, options?: {
|
|
68
71
|
recursive?: boolean;
|
|
69
72
|
}): Promise<void>;
|
|
70
|
-
open(path: string, flags: string): Promise<{
|
|
71
|
-
close(): Promise<void>;
|
|
72
|
-
writeFile(data: string | NodeJS.ArrayBufferView, options?: BufferEncoding | {
|
|
73
|
-
encoding?: BufferEncoding;
|
|
74
|
-
}): Promise<void>;
|
|
75
|
-
}>;
|
|
76
73
|
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
77
74
|
readdir(path: string): Promise<string[]>;
|
|
78
75
|
rename(fromPath: string, toPath: string): Promise<void>;
|
|
@@ -88,6 +85,45 @@ export interface TaskListFs {
|
|
|
88
85
|
}): Promise<void>;
|
|
89
86
|
}
|
|
90
87
|
export type OpenTaskListOptions = OpenMarkdownDirOptions | OpenYamlFileOptions | OpenGhIssuesOptions;
|
|
88
|
+
export type TaskListOptions = OpenTaskListOptions;
|
|
89
|
+
export interface MoveTasksOptions {
|
|
90
|
+
source: TaskListOptions;
|
|
91
|
+
target: TaskListOptions;
|
|
92
|
+
deleteSource?: boolean;
|
|
93
|
+
limit?: number;
|
|
94
|
+
rate?: number;
|
|
95
|
+
dryRun?: boolean;
|
|
96
|
+
stateMap?: Record<string, string>;
|
|
97
|
+
onProgress?: (event: MoveProgressEvent) => void;
|
|
98
|
+
}
|
|
99
|
+
export interface MoveResult {
|
|
100
|
+
created: number;
|
|
101
|
+
skipped: number;
|
|
102
|
+
errors: Array<{
|
|
103
|
+
id: string;
|
|
104
|
+
error: string;
|
|
105
|
+
}>;
|
|
106
|
+
}
|
|
107
|
+
export type MoveProgressEvent = {
|
|
108
|
+
type: "created";
|
|
109
|
+
id: string;
|
|
110
|
+
source: Task;
|
|
111
|
+
target: Task;
|
|
112
|
+
targetList: string;
|
|
113
|
+
targetState: string;
|
|
114
|
+
} | {
|
|
115
|
+
type: "skipped";
|
|
116
|
+
id: string;
|
|
117
|
+
source: Task;
|
|
118
|
+
targetList: string;
|
|
119
|
+
targetState: string;
|
|
120
|
+
reason: "dry-run";
|
|
121
|
+
} | {
|
|
122
|
+
type: "error";
|
|
123
|
+
id: string;
|
|
124
|
+
source: Task;
|
|
125
|
+
error: string;
|
|
126
|
+
};
|
|
91
127
|
export interface OpenMarkdownDirOptions {
|
|
92
128
|
type: "markdown-dir";
|
|
93
129
|
path: string;
|
|
@@ -95,8 +131,6 @@ export interface OpenMarkdownDirOptions {
|
|
|
95
131
|
create?: boolean;
|
|
96
132
|
singleList?: string;
|
|
97
133
|
frontmatterMode?: "strict" | "passthrough";
|
|
98
|
-
lockStaleMs?: number;
|
|
99
|
-
lockRetries?: number;
|
|
100
134
|
fs?: TaskListFs;
|
|
101
135
|
stateMachine?: StateMachineDef;
|
|
102
136
|
}
|
|
@@ -105,18 +139,21 @@ export interface OpenYamlFileOptions {
|
|
|
105
139
|
path: string;
|
|
106
140
|
defaults?: TaskDefaults;
|
|
107
141
|
create?: boolean;
|
|
108
|
-
lockStaleMs?: number;
|
|
109
|
-
lockRetries?: number;
|
|
110
142
|
fs?: TaskListFs;
|
|
111
143
|
stateMachine?: StateMachineDef;
|
|
112
144
|
}
|
|
113
145
|
export interface OpenGhIssuesOptions {
|
|
114
146
|
type: "gh-issues";
|
|
115
147
|
repo: string;
|
|
116
|
-
project
|
|
148
|
+
project?: {
|
|
117
149
|
owner: string;
|
|
118
150
|
number: number;
|
|
119
151
|
};
|
|
152
|
+
filter?: string;
|
|
153
|
+
state?: {
|
|
154
|
+
labelPrefix?: string;
|
|
155
|
+
};
|
|
156
|
+
stateMachine?: StateMachineDef;
|
|
120
157
|
defaults?: TaskDefaults;
|
|
121
158
|
auth?: {
|
|
122
159
|
token: string;
|
|
@@ -128,8 +165,6 @@ export interface BackendDeps {
|
|
|
128
165
|
defaults: Required<TaskDefaults>;
|
|
129
166
|
singleList?: string;
|
|
130
167
|
frontmatterMode: "strict" | "passthrough";
|
|
131
|
-
lockStaleMs: number;
|
|
132
|
-
lockRetries: number;
|
|
133
168
|
create: boolean;
|
|
134
169
|
fs: TaskListFs;
|
|
135
170
|
stateMachine?: StateMachineDef;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsc",
|
|
15
|
+
"build": "node ../../scripts/guard-package-dist.mjs && tsc",
|
|
16
16
|
"test": "cd ../.. && vitest run packages/task-list/src",
|
|
17
17
|
"test:unit": "cd ../.. && vitest run packages/task-list/src"
|
|
18
18
|
},
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@poe-code/file-lock": "*",
|
|
24
23
|
"@poe-code/process-runner": "*",
|
|
25
24
|
"yaml": "*"
|
|
26
25
|
}
|
|
@@ -31,5 +31,8 @@ function resolveBackend(input) {
|
|
|
31
31
|
if (configuredBackend === "keychain") {
|
|
32
32
|
return "keychain";
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
if (configuredBackend === undefined || configuredBackend === "file") {
|
|
35
|
+
return "file";
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Unsupported auth store backend: ${configuredBackend}`);
|
|
35
38
|
}
|
|
@@ -7,10 +7,16 @@ export interface EncryptedFileStoreFileSystem {
|
|
|
7
7
|
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
8
8
|
writeFile(path: string, data: string | NodeJS.ArrayBufferView, options?: {
|
|
9
9
|
encoding?: BufferEncoding;
|
|
10
|
+
flag?: string;
|
|
11
|
+
mode?: number;
|
|
10
12
|
}): Promise<void>;
|
|
11
13
|
mkdir(path: string, options?: {
|
|
12
14
|
recursive?: boolean;
|
|
13
15
|
}): Promise<void | string | undefined>;
|
|
16
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
17
|
+
lstat(path: string): Promise<{
|
|
18
|
+
isSymbolicLink(): boolean;
|
|
19
|
+
}>;
|
|
14
20
|
unlink(path: string): Promise<void>;
|
|
15
21
|
chmod(path: string, mode: number): Promise<void>;
|
|
16
22
|
}
|
|
@@ -27,6 +33,7 @@ export interface EncryptedFileStoreInput {
|
|
|
27
33
|
export declare class EncryptedFileStore implements SecretStore {
|
|
28
34
|
private readonly fs;
|
|
29
35
|
private readonly filePath;
|
|
36
|
+
private readonly symbolicLinkCheckStartPath;
|
|
30
37
|
private readonly salt;
|
|
31
38
|
private readonly getMachineIdentity;
|
|
32
39
|
private readonly getRandomBytes;
|
|
@@ -35,5 +42,6 @@ export declare class EncryptedFileStore implements SecretStore {
|
|
|
35
42
|
get(): Promise<string | null>;
|
|
36
43
|
set(value: string): Promise<void>;
|
|
37
44
|
delete(): Promise<void>;
|
|
45
|
+
private assertRegularCredentialPath;
|
|
38
46
|
private getEncryptionKey;
|
|
39
47
|
}
|
|
@@ -9,9 +9,11 @@ const ENCRYPTION_KEY_BYTES = 32;
|
|
|
9
9
|
const ENCRYPTION_IV_BYTES = 12;
|
|
10
10
|
const ENCRYPTION_AUTH_TAG_BYTES = 16;
|
|
11
11
|
const ENCRYPTION_FILE_MODE = 0o600;
|
|
12
|
+
let temporaryFileSequence = 0;
|
|
12
13
|
export class EncryptedFileStore {
|
|
13
14
|
fs;
|
|
14
15
|
filePath;
|
|
16
|
+
symbolicLinkCheckStartPath;
|
|
15
17
|
salt;
|
|
16
18
|
getMachineIdentity;
|
|
17
19
|
getRandomBytes;
|
|
@@ -19,11 +21,21 @@ export class EncryptedFileStore {
|
|
|
19
21
|
constructor(input) {
|
|
20
22
|
this.fs = input.fs ?? fs;
|
|
21
23
|
this.salt = input.salt;
|
|
22
|
-
|
|
24
|
+
if (input.filePath === undefined) {
|
|
25
|
+
const homeDirectory = (input.getHomeDirectory ?? homedir)();
|
|
26
|
+
const defaultDirectory = input.defaultDirectory ?? ".auth-store";
|
|
27
|
+
this.filePath = path.join(homeDirectory, defaultDirectory, input.defaultFileName ?? "credentials.enc");
|
|
28
|
+
this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.filePath = input.filePath;
|
|
32
|
+
this.symbolicLinkCheckStartPath = null;
|
|
33
|
+
}
|
|
23
34
|
this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
|
|
24
35
|
this.getRandomBytes = input.getRandomBytes ?? randomBytes;
|
|
25
36
|
}
|
|
26
37
|
async get() {
|
|
38
|
+
await this.assertRegularCredentialPath();
|
|
27
39
|
let rawDocument;
|
|
28
40
|
try {
|
|
29
41
|
rawDocument = await this.fs.readFile(this.filePath, "utf8");
|
|
@@ -57,6 +69,7 @@ export class EncryptedFileStore {
|
|
|
57
69
|
}
|
|
58
70
|
}
|
|
59
71
|
async set(value) {
|
|
72
|
+
await this.assertRegularCredentialPath();
|
|
60
73
|
const key = await this.getEncryptionKey();
|
|
61
74
|
const iv = this.getRandomBytes(ENCRYPTION_IV_BYTES);
|
|
62
75
|
const cipher = createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
|
|
@@ -72,12 +85,26 @@ export class EncryptedFileStore {
|
|
|
72
85
|
ciphertext: ciphertext.toString("base64")
|
|
73
86
|
};
|
|
74
87
|
await this.fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
75
|
-
await this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
await this.assertRegularCredentialPath();
|
|
89
|
+
const temporaryPath = `${this.filePath}.${process.pid}.${temporaryFileSequence++}.tmp`;
|
|
90
|
+
try {
|
|
91
|
+
await this.fs.writeFile(temporaryPath, JSON.stringify(document), {
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
flag: "wx",
|
|
94
|
+
mode: ENCRYPTION_FILE_MODE
|
|
95
|
+
});
|
|
96
|
+
await this.fs.chmod(temporaryPath, ENCRYPTION_FILE_MODE);
|
|
97
|
+
await this.fs.rename(temporaryPath, this.filePath);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!isAlreadyExistsError(error)) {
|
|
101
|
+
await removeIfPresent(this.fs, temporaryPath).catch(() => undefined);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
79
105
|
}
|
|
80
106
|
async delete() {
|
|
107
|
+
await this.assertRegularCredentialPath();
|
|
81
108
|
try {
|
|
82
109
|
await this.fs.unlink(this.filePath);
|
|
83
110
|
}
|
|
@@ -87,13 +114,77 @@ export class EncryptedFileStore {
|
|
|
87
114
|
}
|
|
88
115
|
}
|
|
89
116
|
}
|
|
117
|
+
async assertRegularCredentialPath() {
|
|
118
|
+
const resolvedPath = path.resolve(this.filePath);
|
|
119
|
+
const protectedPaths = getProtectedCredentialPaths(resolvedPath, this.symbolicLinkCheckStartPath);
|
|
120
|
+
for (const currentPath of protectedPaths) {
|
|
121
|
+
try {
|
|
122
|
+
const stats = await this.fs.lstat(currentPath);
|
|
123
|
+
if (stats.isSymbolicLink()) {
|
|
124
|
+
throw new Error(`Refusing to use encrypted credential path through symbolic link: ${currentPath}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (isNotFoundError(error)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
90
135
|
getEncryptionKey() {
|
|
91
136
|
if (!this.keyPromise) {
|
|
92
|
-
|
|
137
|
+
const retryableKeyPromise = deriveEncryptionKey(this.getMachineIdentity, this.salt).catch((error) => {
|
|
138
|
+
if (this.keyPromise === retryableKeyPromise) {
|
|
139
|
+
this.keyPromise = null;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
});
|
|
143
|
+
this.keyPromise = retryableKeyPromise;
|
|
93
144
|
}
|
|
94
145
|
return this.keyPromise;
|
|
95
146
|
}
|
|
96
147
|
}
|
|
148
|
+
function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
|
|
149
|
+
const [firstSegment] = defaultDirectory.split(/[\\/]+/).filter(Boolean);
|
|
150
|
+
return path.resolve(homeDirectory, firstSegment ?? ".");
|
|
151
|
+
}
|
|
152
|
+
function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
|
|
153
|
+
if (symbolicLinkCheckStartPath === null) {
|
|
154
|
+
return [path.dirname(resolvedPath), resolvedPath];
|
|
155
|
+
}
|
|
156
|
+
const resolvedStartPath = path.resolve(symbolicLinkCheckStartPath);
|
|
157
|
+
if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
|
|
158
|
+
return [path.dirname(resolvedPath), resolvedPath];
|
|
159
|
+
}
|
|
160
|
+
const protectedPaths = [resolvedStartPath];
|
|
161
|
+
let currentPath = resolvedStartPath;
|
|
162
|
+
for (const segment of path.relative(resolvedStartPath, resolvedPath).split(path.sep).filter(Boolean)) {
|
|
163
|
+
currentPath = path.join(currentPath, segment);
|
|
164
|
+
protectedPaths.push(currentPath);
|
|
165
|
+
}
|
|
166
|
+
return protectedPaths;
|
|
167
|
+
}
|
|
168
|
+
function isPathInsideOrEqual(childPath, parentPath) {
|
|
169
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
170
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
171
|
+
}
|
|
172
|
+
async function removeIfPresent(fileSystem, filePath) {
|
|
173
|
+
try {
|
|
174
|
+
await fileSystem.unlink(filePath);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (!isNotFoundError(error)) {
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function isAlreadyExistsError(error) {
|
|
183
|
+
return (typeof error === "object" &&
|
|
184
|
+
error !== null &&
|
|
185
|
+
"code" in error &&
|
|
186
|
+
error.code === "EEXIST");
|
|
187
|
+
}
|
|
97
188
|
function defaultMachineIdentity() {
|
|
98
189
|
return {
|
|
99
190
|
hostname: hostname(),
|
|
@@ -103,7 +194,7 @@ function defaultMachineIdentity() {
|
|
|
103
194
|
async function deriveEncryptionKey(getMachineIdentity, salt) {
|
|
104
195
|
const machineIdentity = await getMachineIdentity();
|
|
105
196
|
const secret = `${machineIdentity.hostname}:${machineIdentity.username}`;
|
|
106
|
-
const cacheKey =
|
|
197
|
+
const cacheKey = JSON.stringify([machineIdentity.hostname, machineIdentity.username, salt]);
|
|
107
198
|
const cached = derivedKeyCache.get(cacheKey);
|
|
108
199
|
if (cached) {
|
|
109
200
|
return cached;
|
|
@@ -118,7 +209,12 @@ async function deriveEncryptionKey(getMachineIdentity, salt) {
|
|
|
118
209
|
});
|
|
119
210
|
});
|
|
120
211
|
derivedKeyCache.set(cacheKey, keyPromise);
|
|
121
|
-
return keyPromise
|
|
212
|
+
return keyPromise.catch((error) => {
|
|
213
|
+
if (derivedKeyCache.get(cacheKey) === keyPromise) {
|
|
214
|
+
derivedKeyCache.delete(cacheKey);
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
});
|
|
122
218
|
}
|
|
123
219
|
function parseEncryptedDocument(raw) {
|
|
124
220
|
try {
|
|
@@ -4,4 +4,4 @@ export { KeychainStore } from "./keychain-store.js";
|
|
|
4
4
|
export { key, MigratingSecretStore } from "./provider-store.js";
|
|
5
5
|
export type { SecretStore, StoreBackend, CreateSecretStoreInput, CreateSecretStoreResult } from "./types.js";
|
|
6
6
|
export type { MachineIdentity, EncryptedFileStoreInput, EncryptedFileStoreFileSystem } from "./encrypted-file-store.js";
|
|
7
|
-
export type { KeychainStoreInput, KeychainCommandRunner, KeychainCommandResult } from "./keychain-store.js";
|
|
7
|
+
export type { KeychainStoreInput, KeychainCommandRunner, KeychainCommandResult, KeychainCommandOptions } from "./keychain-store.js";
|
|
@@ -4,7 +4,10 @@ export interface KeychainCommandResult {
|
|
|
4
4
|
stderr: string;
|
|
5
5
|
exitCode: number;
|
|
6
6
|
}
|
|
7
|
-
export
|
|
7
|
+
export interface KeychainCommandOptions {
|
|
8
|
+
stdin?: string;
|
|
9
|
+
}
|
|
10
|
+
export type KeychainCommandRunner = (command: string, args: string[], options?: KeychainCommandOptions) => Promise<KeychainCommandResult>;
|
|
8
11
|
export interface KeychainStoreInput {
|
|
9
12
|
runCommand?: KeychainCommandRunner;
|
|
10
13
|
service: string;
|
|
@@ -21,16 +21,18 @@ export class KeychainStore {
|
|
|
21
21
|
throw createSecurityCliFailure("read secret from macOS Keychain", result);
|
|
22
22
|
}
|
|
23
23
|
async set(value) {
|
|
24
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
25
|
+
throw new Error("Keychain secrets cannot contain line breaks");
|
|
26
|
+
}
|
|
24
27
|
const result = await this.executeSecurityCommand([
|
|
25
28
|
"add-generic-password",
|
|
26
29
|
"-s",
|
|
27
30
|
this.service,
|
|
28
31
|
"-a",
|
|
29
32
|
this.account,
|
|
30
|
-
"-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
], "store secret in macOS Keychain");
|
|
33
|
+
"-U",
|
|
34
|
+
"-w"
|
|
35
|
+
], "store secret in macOS Keychain", { stdin: value });
|
|
34
36
|
if (result.exitCode !== 0) {
|
|
35
37
|
throw createSecurityCliFailure("store secret in macOS Keychain", result);
|
|
36
38
|
}
|
|
@@ -42,9 +44,12 @@ export class KeychainStore {
|
|
|
42
44
|
}
|
|
43
45
|
throw createSecurityCliFailure("delete secret from macOS Keychain", result);
|
|
44
46
|
}
|
|
45
|
-
async executeSecurityCommand(args, operation) {
|
|
47
|
+
async executeSecurityCommand(args, operation, options) {
|
|
46
48
|
try {
|
|
47
|
-
|
|
49
|
+
if (options === undefined) {
|
|
50
|
+
return await this.runCommand(SECURITY_CLI, args);
|
|
51
|
+
}
|
|
52
|
+
return await this.runCommand(SECURITY_CLI, args, options);
|
|
48
53
|
}
|
|
49
54
|
catch (error) {
|
|
50
55
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -52,10 +57,10 @@ export class KeychainStore {
|
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
|
-
function runSecurityCommand(command, args) {
|
|
60
|
+
function runSecurityCommand(command, args, options) {
|
|
56
61
|
return new Promise((resolve) => {
|
|
57
62
|
const child = spawn(command, args, {
|
|
58
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
63
|
+
stdio: [options?.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
|
|
59
64
|
});
|
|
60
65
|
let stdout = "";
|
|
61
66
|
let stderr = "";
|
|
@@ -67,6 +72,9 @@ function runSecurityCommand(command, args) {
|
|
|
67
72
|
child.stderr?.on("data", (chunk) => {
|
|
68
73
|
stderr += chunk.toString();
|
|
69
74
|
});
|
|
75
|
+
if (options?.stdin !== undefined) {
|
|
76
|
+
child.stdin?.end(options.stdin);
|
|
77
|
+
}
|
|
70
78
|
child.on("error", (error) => {
|
|
71
79
|
const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
72
80
|
resolve({
|
|
@@ -79,7 +87,7 @@ function runSecurityCommand(command, args) {
|
|
|
79
87
|
resolve({
|
|
80
88
|
stdout,
|
|
81
89
|
stderr,
|
|
82
|
-
exitCode: code ??
|
|
90
|
+
exitCode: code ?? 1
|
|
83
91
|
});
|
|
84
92
|
});
|
|
85
93
|
});
|
|
@@ -94,13 +102,7 @@ function stripTrailingLineBreak(value) {
|
|
|
94
102
|
return value;
|
|
95
103
|
}
|
|
96
104
|
function isKeychainEntryNotFound(result) {
|
|
97
|
-
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
const output = `${result.stderr}\n${result.stdout}`.toLowerCase();
|
|
101
|
-
return (output.includes("could not be found") ||
|
|
102
|
-
output.includes("item not found") ||
|
|
103
|
-
output.includes("errsecitemnotfound"));
|
|
105
|
+
return result.exitCode === KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE;
|
|
104
106
|
}
|
|
105
107
|
function createSecurityCliFailure(operation, result) {
|
|
106
108
|
const details = result.stderr.trim() || result.stdout.trim();
|
|
@@ -3,8 +3,12 @@ export declare function key(providerId: string): string;
|
|
|
3
3
|
export declare class MigratingSecretStore implements SecretStore {
|
|
4
4
|
private readonly store;
|
|
5
5
|
private readonly legacyStore;
|
|
6
|
+
private pendingMutation;
|
|
6
7
|
constructor(store: SecretStore, legacyStore?: SecretStore | null);
|
|
7
|
-
get(
|
|
8
|
+
get(options?: {
|
|
9
|
+
readOnly?: boolean;
|
|
10
|
+
}): Promise<string | null>;
|
|
8
11
|
set(value: string): Promise<void>;
|
|
9
12
|
delete(): Promise<void>;
|
|
13
|
+
private mutate;
|
|
10
14
|
}
|
|
@@ -4,27 +4,75 @@ export function key(providerId) {
|
|
|
4
4
|
export class MigratingSecretStore {
|
|
5
5
|
store;
|
|
6
6
|
legacyStore;
|
|
7
|
+
pendingMutation = Promise.resolve();
|
|
7
8
|
constructor(store, legacyStore = null) {
|
|
8
9
|
this.store = store;
|
|
9
10
|
this.legacyStore = legacyStore;
|
|
10
11
|
}
|
|
11
|
-
async get() {
|
|
12
|
+
async get(options = {}) {
|
|
12
13
|
const value = await this.store.get();
|
|
13
14
|
if (value !== null || !this.legacyStore) {
|
|
14
15
|
return value;
|
|
15
16
|
}
|
|
16
17
|
const legacyValue = await this.legacyStore.get();
|
|
17
|
-
if (legacyValue !== null) {
|
|
18
|
-
await this.
|
|
18
|
+
if (legacyValue !== null && !options.readOnly) {
|
|
19
|
+
await this.mutate(async () => {
|
|
20
|
+
if (await this.store.get() === null) {
|
|
21
|
+
try {
|
|
22
|
+
await this.store.set(legacyValue);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
19
29
|
}
|
|
20
30
|
return legacyValue;
|
|
21
31
|
}
|
|
22
32
|
async set(value) {
|
|
23
|
-
await this.
|
|
24
|
-
|
|
33
|
+
await this.mutate(async () => {
|
|
34
|
+
const previousValue = await this.store.get();
|
|
35
|
+
const previousLegacyValue = await this.legacyStore?.get() ?? null;
|
|
36
|
+
await this.store.set(value);
|
|
37
|
+
try {
|
|
38
|
+
await this.legacyStore?.set(value);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
await restore(this.store, previousValue);
|
|
42
|
+
if (this.legacyStore) {
|
|
43
|
+
await restore(this.legacyStore, previousLegacyValue);
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
25
48
|
}
|
|
26
49
|
async delete() {
|
|
27
|
-
await this.
|
|
28
|
-
|
|
50
|
+
await this.mutate(async () => {
|
|
51
|
+
const previousValue = await this.store.get();
|
|
52
|
+
const previousLegacyValue = await this.legacyStore?.get() ?? null;
|
|
53
|
+
await this.store.delete();
|
|
54
|
+
try {
|
|
55
|
+
await this.legacyStore?.delete();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
await restore(this.store, previousValue);
|
|
59
|
+
if (this.legacyStore) {
|
|
60
|
+
await restore(this.legacyStore, previousLegacyValue);
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
29
65
|
}
|
|
66
|
+
async mutate(action) {
|
|
67
|
+
const operation = this.pendingMutation.then(action, action);
|
|
68
|
+
this.pendingMutation = operation.catch(() => undefined);
|
|
69
|
+
await operation;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function restore(store, value) {
|
|
73
|
+
if (value === null) {
|
|
74
|
+
await store.delete();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await store.set(value);
|
|
30
78
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { EncryptedFileStoreInput } from "./encrypted-file-store.js";
|
|
2
2
|
import type { KeychainStoreInput } from "./keychain-store.js";
|
|
3
3
|
export interface SecretStore {
|
|
4
|
-
get(
|
|
4
|
+
get(options?: {
|
|
5
|
+
readOnly?: boolean;
|
|
6
|
+
}): Promise<string | null>;
|
|
5
7
|
set(value: string): Promise<void>;
|
|
6
8
|
delete(): Promise<void>;
|
|
7
9
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auth-store",
|
|
3
3
|
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
4
5
|
"type": "module",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
}
|
|
12
13
|
},
|
|
13
14
|
"scripts": {
|
|
14
|
-
"build": "tsc"
|
|
15
|
+
"build": "node ../../scripts/guard-package-dist.mjs && tsc"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {},
|
|
17
18
|
"files": [
|