playwriter 0.0.33 → 0.0.37
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/aria-snapshot.d.ts +68 -0
- package/dist/aria-snapshot.d.ts.map +1 -0
- package/dist/aria-snapshot.js +359 -0
- package/dist/aria-snapshot.js.map +1 -0
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +95 -5
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cdp-session.d.ts +24 -3
- package/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js +23 -0
- package/dist/cdp-session.js.map +1 -1
- package/dist/debugger-api.md +4 -3
- package/dist/debugger.d.ts +4 -3
- package/dist/debugger.d.ts.map +1 -1
- package/dist/debugger.js +3 -1
- package/dist/debugger.js.map +1 -1
- package/dist/editor-api.md +2 -2
- package/dist/editor.d.ts +2 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1 -0
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +151 -14
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.js +340 -5
- package/dist/mcp.test.js.map +1 -1
- package/dist/protocol.d.ts +12 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/react-source.d.ts +3 -3
- package/dist/react-source.d.ts.map +1 -1
- package/dist/react-source.js +3 -1
- package/dist/react-source.js.map +1 -1
- package/dist/scoped-fs.d.ts +94 -0
- package/dist/scoped-fs.d.ts.map +1 -0
- package/dist/scoped-fs.js +356 -0
- package/dist/scoped-fs.js.map +1 -0
- package/dist/styles-api.md +3 -3
- package/dist/styles.d.ts +3 -3
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +3 -1
- package/dist/styles.js.map +1 -1
- package/package.json +13 -13
- package/src/aria-snapshot.ts +446 -0
- package/src/assets/aria-labels-github-snapshot.txt +605 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-google-snapshot.txt +110 -0
- package/src/assets/aria-labels-google.png +0 -0
- package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/cdp-relay.ts +103 -5
- package/src/cdp-session.ts +50 -3
- package/src/debugger.ts +6 -4
- package/src/editor.ts +4 -3
- package/src/index.ts +8 -0
- package/src/mcp.test.ts +424 -5
- package/src/mcp.ts +242 -66
- package/src/prompt.md +209 -167
- package/src/protocol.ts +14 -1
- package/src/react-source.ts +5 -3
- package/src/scoped-fs.ts +411 -0
- package/src/styles.ts +5 -3
package/src/scoped-fs.ts
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A sandboxed fs wrapper that restricts all file operations to allowed directories.
|
|
7
|
+
* Any attempt to access files outside the allowed directories will throw an EPERM error.
|
|
8
|
+
*
|
|
9
|
+
* By default, allows access to:
|
|
10
|
+
* - Current working directory (process.cwd())
|
|
11
|
+
* - /tmp
|
|
12
|
+
* - os.tmpdir()
|
|
13
|
+
*
|
|
14
|
+
* This is used in the MCP VM context to prevent agents from accessing sensitive system files.
|
|
15
|
+
*/
|
|
16
|
+
export class ScopedFS {
|
|
17
|
+
private allowedDirs: string[]
|
|
18
|
+
|
|
19
|
+
constructor(allowedDirs?: string[]) {
|
|
20
|
+
// Default allowed directories: cwd, /tmp, os.tmpdir()
|
|
21
|
+
const defaultDirs = [process.cwd(), '/tmp', os.tmpdir()]
|
|
22
|
+
|
|
23
|
+
// Use provided dirs or defaults, resolve all to absolute paths
|
|
24
|
+
const dirs = allowedDirs ?? defaultDirs
|
|
25
|
+
this.allowedDirs = [...new Set(dirs.map((d) => path.resolve(d)))]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a resolved path is within any of the allowed directories.
|
|
30
|
+
*/
|
|
31
|
+
private isPathAllowed(resolved: string): boolean {
|
|
32
|
+
return this.allowedDirs.some((dir) => {
|
|
33
|
+
return resolved === dir || resolved.startsWith(dir + path.sep)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a path and ensure it stays within allowed directories.
|
|
39
|
+
* Throws EPERM if the resolved path escapes the sandbox.
|
|
40
|
+
*/
|
|
41
|
+
private resolvePath(filePath: string): string {
|
|
42
|
+
// If it's an absolute path, use it directly
|
|
43
|
+
// If it's relative, resolve from cwd
|
|
44
|
+
const resolved = path.resolve(filePath)
|
|
45
|
+
|
|
46
|
+
if (!this.isPathAllowed(resolved)) {
|
|
47
|
+
const error = new Error(
|
|
48
|
+
`EPERM: operation not permitted, access outside allowed directories: ${filePath}`,
|
|
49
|
+
) as NodeJS.ErrnoException
|
|
50
|
+
error.code = 'EPERM'
|
|
51
|
+
error.errno = -1
|
|
52
|
+
error.syscall = 'access'
|
|
53
|
+
error.path = filePath
|
|
54
|
+
throw error
|
|
55
|
+
}
|
|
56
|
+
return resolved
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Sync methods
|
|
60
|
+
|
|
61
|
+
readFileSync = (filePath: fs.PathOrFileDescriptor, options?: any): any => {
|
|
62
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
63
|
+
return fs.readFileSync(resolved, options)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
writeFileSync = (filePath: fs.PathOrFileDescriptor, data: any, options?: any): void => {
|
|
67
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
68
|
+
fs.writeFileSync(resolved, data, options)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
appendFileSync = (filePath: fs.PathOrFileDescriptor, data: any, options?: any): void => {
|
|
72
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
73
|
+
fs.appendFileSync(resolved, data, options)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
readdirSync = (dirPath: fs.PathLike, options?: any): any => {
|
|
77
|
+
const resolved = this.resolvePath(dirPath.toString())
|
|
78
|
+
return fs.readdirSync(resolved, options)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
mkdirSync = (dirPath: fs.PathLike, options?: any): any => {
|
|
82
|
+
const resolved = this.resolvePath(dirPath.toString())
|
|
83
|
+
return fs.mkdirSync(resolved, options)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
rmdirSync = (dirPath: fs.PathLike, options?: any): void => {
|
|
87
|
+
const resolved = this.resolvePath(dirPath.toString())
|
|
88
|
+
fs.rmdirSync(resolved, options)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
unlinkSync = (filePath: fs.PathLike): void => {
|
|
92
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
93
|
+
fs.unlinkSync(resolved)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
statSync = (filePath: fs.PathLike, options?: any): any => {
|
|
97
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
98
|
+
return fs.statSync(resolved, options)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lstatSync = (filePath: fs.PathLike, options?: any): any => {
|
|
102
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
103
|
+
return fs.lstatSync(resolved, options)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
existsSync = (filePath: fs.PathLike): boolean => {
|
|
107
|
+
try {
|
|
108
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
109
|
+
return fs.existsSync(resolved)
|
|
110
|
+
} catch {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
accessSync = (filePath: fs.PathLike, mode?: number): void => {
|
|
116
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
117
|
+
fs.accessSync(resolved, mode)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
copyFileSync = (src: fs.PathLike, dest: fs.PathLike, mode?: number): void => {
|
|
121
|
+
const resolvedSrc = this.resolvePath(src.toString())
|
|
122
|
+
const resolvedDest = this.resolvePath(dest.toString())
|
|
123
|
+
fs.copyFileSync(resolvedSrc, resolvedDest, mode)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
renameSync = (oldPath: fs.PathLike, newPath: fs.PathLike): void => {
|
|
127
|
+
const resolvedOld = this.resolvePath(oldPath.toString())
|
|
128
|
+
const resolvedNew = this.resolvePath(newPath.toString())
|
|
129
|
+
fs.renameSync(resolvedOld, resolvedNew)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
chmodSync = (filePath: fs.PathLike, mode: fs.Mode): void => {
|
|
133
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
134
|
+
fs.chmodSync(resolved, mode)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
chownSync = (filePath: fs.PathLike, uid: number, gid: number): void => {
|
|
138
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
139
|
+
fs.chownSync(resolved, uid, gid)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
utimesSync = (filePath: fs.PathLike, atime: fs.TimeLike, mtime: fs.TimeLike): void => {
|
|
143
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
144
|
+
fs.utimesSync(resolved, atime, mtime)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
realpathSync = (filePath: fs.PathLike, options?: any): any => {
|
|
148
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
149
|
+
const real = fs.realpathSync(resolved, options)
|
|
150
|
+
// Verify the real path is also within allowed directories (handles symlinks)
|
|
151
|
+
const realStr = real.toString()
|
|
152
|
+
if (!this.isPathAllowed(realStr)) {
|
|
153
|
+
const error = new Error(`EPERM: operation not permitted, realpath escapes allowed directories`) as NodeJS.ErrnoException
|
|
154
|
+
error.code = 'EPERM'
|
|
155
|
+
throw error
|
|
156
|
+
}
|
|
157
|
+
return real
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
readlinkSync = (filePath: fs.PathLike, options?: any): any => {
|
|
161
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
162
|
+
return fs.readlinkSync(resolved, options)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
symlinkSync = (target: fs.PathLike, linkPath: fs.PathLike, type?: fs.symlink.Type | null): void => {
|
|
166
|
+
const resolvedLink = this.resolvePath(linkPath.toString())
|
|
167
|
+
// Target is relative to link location, resolve it to check bounds
|
|
168
|
+
const linkDir = path.dirname(resolvedLink)
|
|
169
|
+
const resolvedTarget = path.resolve(linkDir, target.toString())
|
|
170
|
+
if (!this.isPathAllowed(resolvedTarget)) {
|
|
171
|
+
const error = new Error(`EPERM: operation not permitted, symlink target outside allowed directories`) as NodeJS.ErrnoException
|
|
172
|
+
error.code = 'EPERM'
|
|
173
|
+
throw error
|
|
174
|
+
}
|
|
175
|
+
fs.symlinkSync(target, resolvedLink, type)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
rmSync = (filePath: fs.PathLike, options?: fs.RmOptions): void => {
|
|
179
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
180
|
+
fs.rmSync(resolved, options)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Async callback methods
|
|
184
|
+
|
|
185
|
+
readFile = (filePath: any, ...args: any[]): void => {
|
|
186
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
187
|
+
;(fs.readFile as any)(resolved, ...args)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
writeFile = (filePath: any, data: any, ...args: any[]): void => {
|
|
191
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
192
|
+
;(fs.writeFile as any)(resolved, data, ...args)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
appendFile = (filePath: any, data: any, ...args: any[]): void => {
|
|
196
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
197
|
+
;(fs.appendFile as any)(resolved, data, ...args)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
readdir = (dirPath: any, ...args: any[]): void => {
|
|
201
|
+
const resolved = this.resolvePath(dirPath.toString())
|
|
202
|
+
;(fs.readdir as any)(resolved, ...args)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
mkdir = (dirPath: any, ...args: any[]): void => {
|
|
206
|
+
const resolved = this.resolvePath(dirPath.toString())
|
|
207
|
+
;(fs.mkdir as any)(resolved, ...args)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
rmdir = (dirPath: any, ...args: any[]): void => {
|
|
211
|
+
const resolved = this.resolvePath(dirPath.toString())
|
|
212
|
+
;(fs.rmdir as any)(resolved, ...args)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
unlink = (filePath: any, callback: any): void => {
|
|
216
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
217
|
+
fs.unlink(resolved, callback)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
stat = (filePath: any, ...args: any[]): void => {
|
|
221
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
222
|
+
;(fs.stat as any)(resolved, ...args)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
lstat = (filePath: any, ...args: any[]): void => {
|
|
226
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
227
|
+
;(fs.lstat as any)(resolved, ...args)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
access = (filePath: any, ...args: any[]): void => {
|
|
231
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
232
|
+
;(fs.access as any)(resolved, ...args)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
copyFile = (src: any, dest: any, ...args: any[]): void => {
|
|
236
|
+
const resolvedSrc = this.resolvePath(src.toString())
|
|
237
|
+
const resolvedDest = this.resolvePath(dest.toString())
|
|
238
|
+
;(fs.copyFile as any)(resolvedSrc, resolvedDest, ...args)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
rename = (oldPath: any, newPath: any, callback: any): void => {
|
|
242
|
+
const resolvedOld = this.resolvePath(oldPath.toString())
|
|
243
|
+
const resolvedNew = this.resolvePath(newPath.toString())
|
|
244
|
+
fs.rename(resolvedOld, resolvedNew, callback)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
chmod = (filePath: any, mode: any, callback: any): void => {
|
|
248
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
249
|
+
fs.chmod(resolved, mode, callback)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
chown = (filePath: any, uid: any, gid: any, callback: any): void => {
|
|
253
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
254
|
+
fs.chown(resolved, uid, gid, callback)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
rm = (filePath: any, ...args: any[]): void => {
|
|
258
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
259
|
+
;(fs.rm as any)(resolved, ...args)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
exists = (filePath: any, callback: any): void => {
|
|
263
|
+
try {
|
|
264
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
265
|
+
fs.exists(resolved, callback)
|
|
266
|
+
} catch {
|
|
267
|
+
callback(false)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Stream methods
|
|
272
|
+
|
|
273
|
+
createReadStream = (filePath: fs.PathLike, options?: any): fs.ReadStream => {
|
|
274
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
275
|
+
return fs.createReadStream(resolved, options)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
createWriteStream = (filePath: fs.PathLike, options?: any): fs.WriteStream => {
|
|
279
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
280
|
+
return fs.createWriteStream(resolved, options)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Watch methods
|
|
284
|
+
|
|
285
|
+
watch = (filePath: any, ...args: any[]): fs.FSWatcher => {
|
|
286
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
287
|
+
return (fs.watch as any)(resolved, ...args)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
watchFile = (filePath: any, ...args: any[]): fs.StatWatcher => {
|
|
291
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
292
|
+
return (fs.watchFile as any)(resolved, ...args)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
unwatchFile = (filePath: any, listener?: any): void => {
|
|
296
|
+
const resolved = this.resolvePath(filePath.toString())
|
|
297
|
+
fs.unwatchFile(resolved, listener)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Promise-based API (fs.promises equivalent)
|
|
301
|
+
get promises() {
|
|
302
|
+
const self = this
|
|
303
|
+
return {
|
|
304
|
+
readFile: async (filePath: fs.PathLike, options?: any) => {
|
|
305
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
306
|
+
return fs.promises.readFile(resolved, options)
|
|
307
|
+
},
|
|
308
|
+
writeFile: async (filePath: fs.PathLike, data: any, options?: any) => {
|
|
309
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
310
|
+
return fs.promises.writeFile(resolved, data, options)
|
|
311
|
+
},
|
|
312
|
+
appendFile: async (filePath: fs.PathLike, data: any, options?: any) => {
|
|
313
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
314
|
+
return fs.promises.appendFile(resolved, data, options)
|
|
315
|
+
},
|
|
316
|
+
readdir: async (dirPath: fs.PathLike, options?: any) => {
|
|
317
|
+
const resolved = self.resolvePath(dirPath.toString())
|
|
318
|
+
return fs.promises.readdir(resolved, options)
|
|
319
|
+
},
|
|
320
|
+
mkdir: async (dirPath: fs.PathLike, options?: any) => {
|
|
321
|
+
const resolved = self.resolvePath(dirPath.toString())
|
|
322
|
+
return fs.promises.mkdir(resolved, options)
|
|
323
|
+
},
|
|
324
|
+
rmdir: async (dirPath: fs.PathLike, options?: any) => {
|
|
325
|
+
const resolved = self.resolvePath(dirPath.toString())
|
|
326
|
+
return fs.promises.rmdir(resolved, options)
|
|
327
|
+
},
|
|
328
|
+
unlink: async (filePath: fs.PathLike) => {
|
|
329
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
330
|
+
return fs.promises.unlink(resolved)
|
|
331
|
+
},
|
|
332
|
+
stat: async (filePath: fs.PathLike, options?: any) => {
|
|
333
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
334
|
+
return fs.promises.stat(resolved, options)
|
|
335
|
+
},
|
|
336
|
+
lstat: async (filePath: fs.PathLike, options?: any) => {
|
|
337
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
338
|
+
return fs.promises.lstat(resolved, options)
|
|
339
|
+
},
|
|
340
|
+
access: async (filePath: fs.PathLike, mode?: number) => {
|
|
341
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
342
|
+
return fs.promises.access(resolved, mode)
|
|
343
|
+
},
|
|
344
|
+
copyFile: async (src: fs.PathLike, dest: fs.PathLike, mode?: number) => {
|
|
345
|
+
const resolved = self.resolvePath(src.toString())
|
|
346
|
+
const resolvedDest = self.resolvePath(dest.toString())
|
|
347
|
+
return fs.promises.copyFile(resolved, resolvedDest, mode)
|
|
348
|
+
},
|
|
349
|
+
rename: async (oldPath: fs.PathLike, newPath: fs.PathLike) => {
|
|
350
|
+
const resolvedOld = self.resolvePath(oldPath.toString())
|
|
351
|
+
const resolvedNew = self.resolvePath(newPath.toString())
|
|
352
|
+
return fs.promises.rename(resolvedOld, resolvedNew)
|
|
353
|
+
},
|
|
354
|
+
chmod: async (filePath: fs.PathLike, mode: fs.Mode) => {
|
|
355
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
356
|
+
return fs.promises.chmod(resolved, mode)
|
|
357
|
+
},
|
|
358
|
+
chown: async (filePath: fs.PathLike, uid: number, gid: number) => {
|
|
359
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
360
|
+
return fs.promises.chown(resolved, uid, gid)
|
|
361
|
+
},
|
|
362
|
+
rm: async (filePath: fs.PathLike, options?: fs.RmOptions) => {
|
|
363
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
364
|
+
return fs.promises.rm(resolved, options)
|
|
365
|
+
},
|
|
366
|
+
realpath: async (filePath: fs.PathLike, options?: any) => {
|
|
367
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
368
|
+
const real = await fs.promises.realpath(resolved, options)
|
|
369
|
+
const realStr = real.toString()
|
|
370
|
+
if (!self.isPathAllowed(realStr)) {
|
|
371
|
+
const error = new Error(`EPERM: operation not permitted, realpath escapes allowed directories`) as NodeJS.ErrnoException
|
|
372
|
+
error.code = 'EPERM'
|
|
373
|
+
throw error
|
|
374
|
+
}
|
|
375
|
+
return real
|
|
376
|
+
},
|
|
377
|
+
readlink: async (filePath: fs.PathLike, options?: any) => {
|
|
378
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
379
|
+
return fs.promises.readlink(resolved, options)
|
|
380
|
+
},
|
|
381
|
+
symlink: async (target: fs.PathLike, linkPath: fs.PathLike, type?: string) => {
|
|
382
|
+
const resolvedLink = self.resolvePath(linkPath.toString())
|
|
383
|
+
const linkDir = path.dirname(resolvedLink)
|
|
384
|
+
const resolvedTarget = path.resolve(linkDir, target.toString())
|
|
385
|
+
if (!self.isPathAllowed(resolvedTarget)) {
|
|
386
|
+
const error = new Error(
|
|
387
|
+
`EPERM: operation not permitted, symlink target outside allowed directories`,
|
|
388
|
+
) as NodeJS.ErrnoException
|
|
389
|
+
error.code = 'EPERM'
|
|
390
|
+
throw error
|
|
391
|
+
}
|
|
392
|
+
return fs.promises.symlink(target, resolvedLink, type as any)
|
|
393
|
+
},
|
|
394
|
+
utimes: async (filePath: fs.PathLike, atime: fs.TimeLike, mtime: fs.TimeLike) => {
|
|
395
|
+
const resolved = self.resolvePath(filePath.toString())
|
|
396
|
+
return fs.promises.utimes(resolved, atime, mtime)
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Constants passthrough
|
|
402
|
+
constants = fs.constants
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Create a scoped fs instance with allowed directories.
|
|
407
|
+
* Defaults to cwd, /tmp, and os.tmpdir() if no directories specified.
|
|
408
|
+
*/
|
|
409
|
+
export function createScopedFS(allowedDirs?: string[]): ScopedFS {
|
|
410
|
+
return new ScopedFS(allowedDirs)
|
|
411
|
+
}
|
package/src/styles.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CDPSession } from './cdp-session.js'
|
|
1
|
+
import type { ICDPSession, CDPSession } from './cdp-session.js'
|
|
2
2
|
import type { Locator } from 'playwright-core'
|
|
3
3
|
|
|
4
4
|
export interface StyleSource {
|
|
@@ -66,13 +66,15 @@ interface CSSStyleSheetHeader {
|
|
|
66
66
|
|
|
67
67
|
export async function getStylesForLocator({
|
|
68
68
|
locator,
|
|
69
|
-
cdp,
|
|
69
|
+
cdp: cdpSession,
|
|
70
70
|
includeUserAgentStyles = false,
|
|
71
71
|
}: {
|
|
72
72
|
locator: Locator
|
|
73
|
-
cdp:
|
|
73
|
+
cdp: ICDPSession
|
|
74
74
|
includeUserAgentStyles?: boolean
|
|
75
75
|
}): Promise<StylesResult> {
|
|
76
|
+
// Cast to CDPSession for internal type safety - at runtime both are compatible
|
|
77
|
+
const cdp = cdpSession as CDPSession
|
|
76
78
|
await cdp.send('DOM.enable')
|
|
77
79
|
await cdp.send('CSS.enable')
|
|
78
80
|
|