toolcraft-openapi 0.0.24 → 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.
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  interface GenerateCliFileSystem {
3
+ lstat(targetPath: string): Promise<{
4
+ isDirectory(): boolean;
5
+ isSymbolicLink(): boolean;
6
+ }>;
3
7
  mkdir(directoryPath: string, options?: {
4
8
  recursive?: boolean;
5
9
  }): Promise<unknown>;
@@ -9,9 +13,6 @@ interface GenerateCliFileSystem {
9
13
  force?: boolean;
10
14
  }): Promise<void>;
11
15
  realpath(targetPath: string): Promise<string>;
12
- stat(targetPath: string): Promise<{
13
- isDirectory(): boolean;
14
- }>;
15
16
  writeFile(filePath: string, contents: string, encoding: BufferEncoding): Promise<void>;
16
17
  }
17
18
  interface GenerateCliWriter {
@@ -81,7 +81,7 @@ export async function syncGeneratedClient(options, services) {
81
81
  if (!options.check && drifted) {
82
82
  try {
83
83
  await writeGeneratedFiles(services.fs, outputDir, updatedFiles);
84
- await deleteGeneratedFiles(services.fs, deletedFiles);
84
+ await deleteGeneratedFiles(services.fs, outputDir, deletedFiles);
85
85
  }
86
86
  catch (error) {
87
87
  await restoreGeneratedFiles(services.fs, outputDir, currentFiles, updatedFiles, deletedFiles);
@@ -170,10 +170,17 @@ function tryParseUrl(input) {
170
170
  async function readGeneratedFiles(fs, directoryPath) {
171
171
  const files = new Map();
172
172
  try {
173
+ const directoryStats = await fs.lstat(directoryPath);
174
+ if (directoryStats.isSymbolicLink()) {
175
+ throw new Error("Generated output must remain inside the output directory.");
176
+ }
173
177
  const entries = await fs.readdir(directoryPath);
174
178
  for (const entry of entries) {
175
179
  const entryPath = path.resolve(directoryPath, entry);
176
- const stats = await fs.stat(entryPath);
180
+ const stats = await fs.lstat(entryPath);
181
+ if (stats.isSymbolicLink()) {
182
+ throw new Error("Generated output must remain inside the output directory.");
183
+ }
177
184
  if (stats.isDirectory()) {
178
185
  for (const [nestedPath, nestedContents] of await readGeneratedFiles(fs, entryPath)) {
179
186
  files.set(nestedPath, nestedContents);
@@ -228,8 +235,9 @@ async function assertSafeOutputPath(fs, outputDir, filePath) {
228
235
  throw new Error("Generated output must remain inside the output directory.");
229
236
  }
230
237
  }
231
- async function deleteGeneratedFiles(fs, filePaths) {
238
+ async function deleteGeneratedFiles(fs, outputDir, filePaths) {
232
239
  for (const filePath of filePaths) {
240
+ await assertSafeOutputPath(fs, outputDir, filePath);
233
241
  await fs.rm(filePath, { force: true });
234
242
  }
235
243
  }
@@ -237,6 +245,7 @@ async function restoreGeneratedFiles(fs, outputDir, currentFiles, updatedFiles,
237
245
  for (const file of updatedFiles) {
238
246
  const previousContents = currentFiles.get(file.path);
239
247
  if (previousContents === undefined) {
248
+ await assertSafeOutputPath(fs, outputDir, file.path);
240
249
  await fs.rm(file.path, { force: true });
241
250
  continue;
242
251
  }
@@ -33,6 +33,7 @@ export interface EncryptedFileStoreInput {
33
33
  export declare class EncryptedFileStore implements SecretStore {
34
34
  private readonly fs;
35
35
  private readonly filePath;
36
+ private readonly symbolicLinkCheckStartPath;
36
37
  private readonly salt;
37
38
  private readonly getMachineIdentity;
38
39
  private readonly getRandomBytes;
@@ -13,6 +13,7 @@ let temporaryFileSequence = 0;
13
13
  export class EncryptedFileStore {
14
14
  fs;
15
15
  filePath;
16
+ symbolicLinkCheckStartPath;
16
17
  salt;
17
18
  getMachineIdentity;
18
19
  getRandomBytes;
@@ -20,7 +21,16 @@ export class EncryptedFileStore {
20
21
  constructor(input) {
21
22
  this.fs = input.fs ?? fs;
22
23
  this.salt = input.salt;
23
- this.filePath = input.filePath ?? path.join((input.getHomeDirectory ?? homedir)(), input.defaultDirectory ?? ".auth-store", input.defaultFileName ?? "credentials.enc");
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
+ }
24
34
  this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
25
35
  this.getRandomBytes = input.getRandomBytes ?? randomBytes;
26
36
  }
@@ -106,7 +116,7 @@ export class EncryptedFileStore {
106
116
  }
107
117
  async assertRegularCredentialPath() {
108
118
  const resolvedPath = path.resolve(this.filePath);
109
- const protectedPaths = [path.dirname(resolvedPath), resolvedPath];
119
+ const protectedPaths = getProtectedCredentialPaths(resolvedPath, this.symbolicLinkCheckStartPath);
110
120
  for (const currentPath of protectedPaths) {
111
121
  try {
112
122
  const stats = await this.fs.lstat(currentPath);
@@ -135,6 +145,30 @@ export class EncryptedFileStore {
135
145
  return this.keyPromise;
136
146
  }
137
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
+ }
138
172
  async function removeIfPresent(fileSystem, filePath) {
139
173
  try {
140
174
  await fileSystem.unlink(filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-openapi",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,7 +31,7 @@
31
31
  "@clack/core": "^1.0.0",
32
32
  "@clack/prompts": "^1.0.0",
33
33
  "@poe-code/design-system": "^0.0.2",
34
- "toolcraft": "0.0.24",
34
+ "toolcraft": "0.0.25",
35
35
  "auth-store": "^0.0.1",
36
36
  "yaml": "^2.8.2"
37
37
  },