toolcraft 0.0.23 → 0.0.24

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.
Files changed (147) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +50 -13
  5. package/dist/error-report.js +32 -3
  6. package/dist/human-in-loop/approval-tasks.d.ts +1 -0
  7. package/dist/human-in-loop/approval-tasks.js +7 -5
  8. package/dist/human-in-loop/approvals-commands.js +51 -8
  9. package/dist/human-in-loop/runner.js +24 -19
  10. package/dist/human-in-loop/state-machine.d.ts +3 -3
  11. package/dist/human-in-loop/state-machine.js +13 -5
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.js +6 -1
  14. package/dist/mcp-proxy.js +85 -19
  15. package/dist/mcp.compile-check.js +1 -0
  16. package/dist/mcp.d.ts +1 -0
  17. package/dist/mcp.js +50 -8
  18. package/dist/renderer.js +119 -13
  19. package/dist/sdk.compile-check.js +1 -0
  20. package/dist/sdk.d.ts +1 -0
  21. package/dist/sdk.js +56 -11
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
  24. package/node_modules/@poe-code/agent-defs/package.json +1 -1
  25. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
  26. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
  27. package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
  28. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
  29. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
  30. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
  31. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
  32. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
  33. package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
  34. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
  35. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
  38. package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
  42. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
  43. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
  44. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
  45. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
  46. package/node_modules/@poe-code/config-mutations/package.json +1 -1
  47. package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
  48. package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
  49. package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
  50. package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
  51. package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
  52. package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
  53. package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
  54. package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
  55. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
  56. package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
  57. package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
  58. package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
  59. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
  60. package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
  61. package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
  62. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
  63. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
  64. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
  65. package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
  66. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
  67. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
  68. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
  71. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
  73. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
  75. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
  76. package/node_modules/@poe-code/design-system/dist/index.js +2 -1
  77. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
  78. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
  79. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
  80. package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
  81. package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
  82. package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
  83. package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
  84. package/node_modules/@poe-code/design-system/package.json +2 -1
  85. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +244 -110
  86. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +16 -4
  87. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
  88. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +16 -1
  89. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  90. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  91. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
  92. package/node_modules/@poe-code/process-runner/dist/types.d.ts +3 -0
  93. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +57 -0
  94. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +484 -0
  95. package/node_modules/@poe-code/process-runner/package.json +1 -1
  96. package/node_modules/@poe-code/task-list/README.md +0 -2
  97. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
  98. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
  99. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
  100. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
  101. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
  102. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  103. package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
  104. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
  105. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  106. package/node_modules/@poe-code/task-list/dist/index.js +2 -0
  107. package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
  108. package/node_modules/@poe-code/task-list/dist/move.js +215 -0
  109. package/node_modules/@poe-code/task-list/dist/open.js +3 -4
  110. package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
  111. package/node_modules/@poe-code/task-list/dist/state.js +9 -0
  112. package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
  113. package/node_modules/@poe-code/task-list/package.json +1 -2
  114. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  115. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +7 -0
  116. package/node_modules/auth-store/dist/encrypted-file-store.js +69 -7
  117. package/node_modules/auth-store/dist/index.d.ts +1 -1
  118. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  119. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  120. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  121. package/node_modules/auth-store/dist/provider-store.js +55 -7
  122. package/node_modules/auth-store/dist/types.d.ts +3 -1
  123. package/node_modules/auth-store/package.json +2 -1
  124. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
  125. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
  126. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
  127. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
  128. package/node_modules/mcp-oauth/package.json +1 -0
  129. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
  130. package/node_modules/tiny-mcp-client/dist/internal.d.ts +8 -4
  131. package/node_modules/tiny-mcp-client/dist/internal.js +237 -67
  132. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
  133. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
  134. package/node_modules/tiny-mcp-client/package.json +2 -1
  135. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
  136. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
  137. package/node_modules/tiny-mcp-client/src/internal.ts +279 -77
  138. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
  139. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
  140. package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
  141. package/package.json +10 -12
  142. package/node_modules/@poe-code/file-lock/README.md +0 -52
  143. package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
  144. package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
  145. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
  146. package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
  147. 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[eventName];
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
- return "file";
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
  }
@@ -35,5 +41,6 @@ export declare class EncryptedFileStore implements SecretStore {
35
41
  get(): Promise<string | null>;
36
42
  set(value: string): Promise<void>;
37
43
  delete(): Promise<void>;
44
+ private assertRegularCredentialPath;
38
45
  private getEncryptionKey;
39
46
  }
@@ -9,6 +9,7 @@ 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;
@@ -24,6 +25,7 @@ export class EncryptedFileStore {
24
25
  this.getRandomBytes = input.getRandomBytes ?? randomBytes;
25
26
  }
26
27
  async get() {
28
+ await this.assertRegularCredentialPath();
27
29
  let rawDocument;
28
30
  try {
29
31
  rawDocument = await this.fs.readFile(this.filePath, "utf8");
@@ -57,6 +59,7 @@ export class EncryptedFileStore {
57
59
  }
58
60
  }
59
61
  async set(value) {
62
+ await this.assertRegularCredentialPath();
60
63
  const key = await this.getEncryptionKey();
61
64
  const iv = this.getRandomBytes(ENCRYPTION_IV_BYTES);
62
65
  const cipher = createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
@@ -72,12 +75,26 @@ export class EncryptedFileStore {
72
75
  ciphertext: ciphertext.toString("base64")
73
76
  };
74
77
  await this.fs.mkdir(path.dirname(this.filePath), { recursive: true });
75
- await this.fs.writeFile(this.filePath, JSON.stringify(document), {
76
- encoding: "utf8"
77
- });
78
- await this.fs.chmod(this.filePath, ENCRYPTION_FILE_MODE);
78
+ await this.assertRegularCredentialPath();
79
+ const temporaryPath = `${this.filePath}.${process.pid}.${temporaryFileSequence++}.tmp`;
80
+ try {
81
+ await this.fs.writeFile(temporaryPath, JSON.stringify(document), {
82
+ encoding: "utf8",
83
+ flag: "wx",
84
+ mode: ENCRYPTION_FILE_MODE
85
+ });
86
+ await this.fs.chmod(temporaryPath, ENCRYPTION_FILE_MODE);
87
+ await this.fs.rename(temporaryPath, this.filePath);
88
+ }
89
+ catch (error) {
90
+ if (!isAlreadyExistsError(error)) {
91
+ await removeIfPresent(this.fs, temporaryPath).catch(() => undefined);
92
+ }
93
+ throw error;
94
+ }
79
95
  }
80
96
  async delete() {
97
+ await this.assertRegularCredentialPath();
81
98
  try {
82
99
  await this.fs.unlink(this.filePath);
83
100
  }
@@ -87,13 +104,53 @@ export class EncryptedFileStore {
87
104
  }
88
105
  }
89
106
  }
107
+ async assertRegularCredentialPath() {
108
+ const resolvedPath = path.resolve(this.filePath);
109
+ const protectedPaths = [path.dirname(resolvedPath), resolvedPath];
110
+ for (const currentPath of protectedPaths) {
111
+ try {
112
+ const stats = await this.fs.lstat(currentPath);
113
+ if (stats.isSymbolicLink()) {
114
+ throw new Error(`Refusing to use encrypted credential path through symbolic link: ${currentPath}`);
115
+ }
116
+ }
117
+ catch (error) {
118
+ if (isNotFoundError(error)) {
119
+ return;
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+ }
90
125
  getEncryptionKey() {
91
126
  if (!this.keyPromise) {
92
- this.keyPromise = deriveEncryptionKey(this.getMachineIdentity, this.salt);
127
+ const retryableKeyPromise = deriveEncryptionKey(this.getMachineIdentity, this.salt).catch((error) => {
128
+ if (this.keyPromise === retryableKeyPromise) {
129
+ this.keyPromise = null;
130
+ }
131
+ throw error;
132
+ });
133
+ this.keyPromise = retryableKeyPromise;
93
134
  }
94
135
  return this.keyPromise;
95
136
  }
96
137
  }
138
+ async function removeIfPresent(fileSystem, filePath) {
139
+ try {
140
+ await fileSystem.unlink(filePath);
141
+ }
142
+ catch (error) {
143
+ if (!isNotFoundError(error)) {
144
+ throw error;
145
+ }
146
+ }
147
+ }
148
+ function isAlreadyExistsError(error) {
149
+ return (typeof error === "object" &&
150
+ error !== null &&
151
+ "code" in error &&
152
+ error.code === "EEXIST");
153
+ }
97
154
  function defaultMachineIdentity() {
98
155
  return {
99
156
  hostname: hostname(),
@@ -103,7 +160,7 @@ function defaultMachineIdentity() {
103
160
  async function deriveEncryptionKey(getMachineIdentity, salt) {
104
161
  const machineIdentity = await getMachineIdentity();
105
162
  const secret = `${machineIdentity.hostname}:${machineIdentity.username}`;
106
- const cacheKey = `${secret}:${salt}`;
163
+ const cacheKey = JSON.stringify([machineIdentity.hostname, machineIdentity.username, salt]);
107
164
  const cached = derivedKeyCache.get(cacheKey);
108
165
  if (cached) {
109
166
  return cached;
@@ -118,7 +175,12 @@ async function deriveEncryptionKey(getMachineIdentity, salt) {
118
175
  });
119
176
  });
120
177
  derivedKeyCache.set(cacheKey, keyPromise);
121
- return keyPromise;
178
+ return keyPromise.catch((error) => {
179
+ if (derivedKeyCache.get(cacheKey) === keyPromise) {
180
+ derivedKeyCache.delete(cacheKey);
181
+ }
182
+ throw error;
183
+ });
122
184
  }
123
185
  function parseEncryptedDocument(raw) {
124
186
  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 type KeychainCommandRunner = (command: string, args: string[]) => Promise<KeychainCommandResult>;
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
- "-w",
31
- value,
32
- "-U"
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
- return await this.runCommand(SECURITY_CLI, args);
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 ?? 0
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
- if (result.exitCode === KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE) {
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(): Promise<string | null>;
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.store.set(legacyValue);
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.store.set(value);
24
- await this.legacyStore?.set(value);
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.store.delete();
28
- await this.legacyStore?.delete();
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(): Promise<string | null>;
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": [
@@ -15,7 +15,6 @@ export function createDefaultOAuthClientProvider(options) {
15
15
  const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
16
16
  const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore);
17
17
  const now = options.now ?? Date.now;
18
- const sessions = new Map();
19
18
  const registeredClients = new Map();
20
19
  const refreshPromises = new Map();
21
20
  const authorizationPromises = new Map();
@@ -25,7 +24,10 @@ export function createDefaultOAuthClientProvider(options) {
25
24
  const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
26
25
  const session = await ensureAuthorizedSession(requestUrl, undefined, input.fetch, false);
27
26
  const accessToken = session?.tokens?.accessToken;
28
- if (session === null || accessToken === undefined) {
27
+ if (session === null
28
+ || accessToken === undefined
29
+ || session.tokens === undefined
30
+ || isExpired(session.tokens, now)) {
29
31
  return;
30
32
  }
31
33
  assertRequestMatchesResource(requestUrl, session.resource);
@@ -71,10 +73,11 @@ export function createDefaultOAuthClientProvider(options) {
71
73
  return session;
72
74
  }
73
75
  }
74
- if (!allowInteractive || sessionDiscovery === undefined) {
75
- return session;
76
- }
77
76
  if (forceRefresh && session?.tokens !== undefined) {
77
+ session = clearSessionTokens(session);
78
+ await saveSession(canonicalResource, session);
79
+ }
80
+ if (!allowInteractive || sessionDiscovery === undefined) {
78
81
  return session;
79
82
  }
80
83
  return authorizeSession(canonicalResource, session, sessionDiscovery, fetch);
@@ -125,7 +128,10 @@ export function createDefaultOAuthClientProvider(options) {
125
128
  }
126
129
  const updatedSession = {
127
130
  ...session,
128
- tokens: refreshedTokens,
131
+ tokens: {
132
+ ...refreshedTokens,
133
+ refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken,
134
+ },
129
135
  discovery: toStoredDiscovery(discovery),
130
136
  };
131
137
  await saveSession(resource, updatedSession);
@@ -285,7 +291,7 @@ export function createDefaultOAuthClientProvider(options) {
285
291
  });
286
292
  const payload = await readOAuthJsonObjectResponse(response);
287
293
  if (typeof payload.client_id !== "string" ||
288
- payload.client_id.length === 0) {
294
+ payload.client_id.trim().length === 0) {
289
295
  throw new Error("OAuth client registration response missing client_id");
290
296
  }
291
297
  const registeredClient = {
@@ -302,19 +308,12 @@ export function createDefaultOAuthClientProvider(options) {
302
308
  };
303
309
  }
304
310
  async function loadSession(resource) {
305
- if (sessions.has(resource)) {
306
- return sessions.get(resource) ?? null;
307
- }
308
- const session = await sessionStore.load(resource);
309
- sessions.set(resource, session);
310
- return session;
311
+ return normalizeLoadedSession(await sessionStore.load(resource));
311
312
  }
312
313
  async function saveSession(resource, session) {
313
- sessions.set(resource, session);
314
314
  await sessionStore.save(resource, session);
315
315
  }
316
316
  async function clearSession(resource) {
317
- sessions.delete(resource);
318
317
  await sessionStore.clear(resource);
319
318
  }
320
319
  async function loadRegisteredClient(issuer) {
@@ -325,6 +324,10 @@ export function createDefaultOAuthClientProvider(options) {
325
324
  return null;
326
325
  }
327
326
  const client = await clientStore.load(issuer);
327
+ if (client !== null && !isUsableClient(client)) {
328
+ await clientStore.clear(issuer);
329
+ return null;
330
+ }
328
331
  registeredClients.set(issuer, client);
329
332
  return client;
330
333
  }
@@ -382,6 +385,34 @@ function clearSessionTokens(session) {
382
385
  function hasCachedAccessToken(session) {
383
386
  return session?.tokens?.accessToken !== undefined;
384
387
  }
388
+ function normalizeLoadedSession(session) {
389
+ if (session === null) {
390
+ return null;
391
+ }
392
+ if (!isUsableClient(session.client)) {
393
+ return { ...session, client: { clientId: "" }, tokens: undefined };
394
+ }
395
+ return isUsableTokens(session.tokens)
396
+ ? session
397
+ : { ...session, tokens: undefined };
398
+ }
399
+ function isUsableClient(client) {
400
+ return (typeof client.clientId === "string"
401
+ && client.clientId.trim().length > 0
402
+ && (client.clientSecret === undefined
403
+ || (typeof client.clientSecret === "string" && client.clientSecret.trim().length > 0)));
404
+ }
405
+ function isUsableTokens(tokens) {
406
+ if (tokens === undefined) {
407
+ return true;
408
+ }
409
+ return (typeof tokens.accessToken === "string"
410
+ && tokens.accessToken.trim().length > 0
411
+ && tokens.tokenType === "Bearer"
412
+ && (tokens.expiresAt === null || (typeof tokens.expiresAt === "number" && Number.isFinite(tokens.expiresAt)))
413
+ && (tokens.refreshToken === undefined
414
+ || (typeof tokens.refreshToken === "string" && tokens.refreshToken.trim().length > 0)));
415
+ }
385
416
  function getClientMetadata(client) {
386
417
  return client.metadata;
387
418
  }