@kisev/safe-fs 1.0.0-dev.46.gfbe1e4c6992e

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 ADDED
@@ -0,0 +1,18 @@
1
+ # safe-fs
2
+
3
+ [Русская версия](README.ru.md)
4
+
5
+ `@kisev/safe-fs` holds the shared filesystem safety primitives used by the
6
+ kisev agent packages: `writeAtomic` for private single-link atomic replacement,
7
+ `readRegular` for reading regular files only, `assertSafePath` for
8
+ symlink-free traversal, and the `LifecycleError` failure type.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @kisev/safe-fs
14
+ ```
15
+
16
+ Every write goes through an exclusive temporary file with `fsync`, a mode
17
+ restriction, and a directory sync, and rejects symlinks, multi-link files, and
18
+ non-directory parents.
package/README.ru.md ADDED
@@ -0,0 +1,18 @@
1
+ # safe-fs
2
+
3
+ [English version](README.md)
4
+
5
+ `@kisev/safe-fs` — общие примитивы безопасной работы с файловой системой для
6
+ пакетов kisev: `writeAtomic` для приватной атомарной замены файлов с одной
7
+ ссылкой, `readRegular` для чтения только обычных файлов, `assertSafePath` для
8
+ обхода без симлинков и тип ошибки `LifecycleError`.
9
+
10
+ ## Установка
11
+
12
+ ```bash
13
+ npm install @kisev/safe-fs
14
+ ```
15
+
16
+ Каждая запись идёт через эксклюзивный временный файл с `fsync`, ограничением
17
+ прав и синхронизацией каталога; симлинки, файлы с несколькими ссылками и
18
+ каталоги-не-родители отклоняются.
@@ -0,0 +1 @@
1
+ export { LifecycleError, assertSafePath, ensureDirectory, lstatSafe, readRegular, writeAtomic, } from "./safe-fs.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { LifecycleError, assertSafePath, ensureDirectory, lstatSafe, readRegular, writeAtomic, } from "./safe-fs.js";
@@ -0,0 +1,12 @@
1
+ export declare class LifecycleError extends Error {
2
+ readonly code: string;
3
+ constructor(code: string, message: string);
4
+ }
5
+ export declare function lstatSafe(path: string): Promise<import("fs").Stats | undefined>;
6
+ export declare function assertSafePath(path: string, options?: {
7
+ target?: "file" | "directory";
8
+ allowMissing?: boolean;
9
+ }): Promise<void>;
10
+ export declare function readRegular(path: string): Promise<Buffer | undefined>;
11
+ export declare function ensureDirectory(path: string, mode: number): Promise<string[]>;
12
+ export declare function writeAtomic(path: string, content: Buffer, mode: number): Promise<void>;
@@ -0,0 +1,113 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
4
+ import { basename, dirname, join, parse, resolve, sep } from "node:path";
5
+ export class LifecycleError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.code = code;
10
+ }
11
+ }
12
+ export async function lstatSafe(path) {
13
+ try {
14
+ return await lstat(path);
15
+ }
16
+ catch (error) {
17
+ if (error.code === "ENOENT")
18
+ return undefined;
19
+ throw error;
20
+ }
21
+ }
22
+ export async function assertSafePath(path, options = {}) {
23
+ const target = resolve(path);
24
+ const parsed = parse(target);
25
+ let current = parsed.root;
26
+ const pieces = target.slice(parsed.root.length).split(sep).filter(Boolean);
27
+ for (let index = 0; index < pieces.length; index += 1) {
28
+ current = join(current, pieces[index]);
29
+ const metadata = await lstatSafe(current);
30
+ if (!metadata) {
31
+ if (options.allowMissing !== false)
32
+ return;
33
+ throw new LifecycleError("unsafe_path", `Required path is missing: ${current}`);
34
+ }
35
+ if (metadata.isSymbolicLink())
36
+ throw new LifecycleError("unsafe_path", `Symlink is not allowed: ${current}`);
37
+ const final = index === pieces.length - 1;
38
+ if (!final && !metadata.isDirectory())
39
+ throw new LifecycleError("unsafe_path", `Path parent is not a directory: ${current}`);
40
+ if (final && options.target === "file" && !metadata.isFile())
41
+ throw new LifecycleError("unsafe_path", `Target is not a regular file: ${current}`);
42
+ if (final && options.target === "directory" && !metadata.isDirectory())
43
+ throw new LifecycleError("unsafe_path", `Target is not a directory: ${current}`);
44
+ }
45
+ }
46
+ export async function readRegular(path) {
47
+ await assertSafePath(path);
48
+ const metadata = await lstatSafe(path);
49
+ if (!metadata)
50
+ return undefined;
51
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1) {
52
+ throw new LifecycleError("unsafe_path", `Target is not a single-link regular file: ${path}`);
53
+ }
54
+ return readFile(path);
55
+ }
56
+ export async function ensureDirectory(path, mode) {
57
+ const target = resolve(path);
58
+ const parsed = parse(target);
59
+ let current = parsed.root;
60
+ const created = [];
61
+ for (const piece of target.slice(parsed.root.length).split(sep).filter(Boolean)) {
62
+ current = join(current, piece);
63
+ const metadata = await lstatSafe(current);
64
+ if (metadata) {
65
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
66
+ throw new LifecycleError("unsafe_path", `Unsafe directory: ${current}`);
67
+ continue;
68
+ }
69
+ try {
70
+ await mkdir(current, { mode });
71
+ created.push(current);
72
+ }
73
+ catch (error) {
74
+ if (error.code !== "EEXIST")
75
+ throw error;
76
+ const raced = await lstat(current);
77
+ if (!raced.isDirectory() || raced.isSymbolicLink())
78
+ throw new LifecycleError("unsafe_path", `Unsafe directory: ${current}`);
79
+ }
80
+ }
81
+ return created;
82
+ }
83
+ export async function writeAtomic(path, content, mode) {
84
+ await ensureDirectory(dirname(path), 0o700);
85
+ await assertSafePath(path);
86
+ const current = await lstatSafe(path);
87
+ if (current && (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1)) {
88
+ throw new LifecycleError("unsafe_path", `Target is not a single-link regular file: ${path}`);
89
+ }
90
+ const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
91
+ const handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, mode);
92
+ try {
93
+ await handle.writeFile(content);
94
+ await handle.sync();
95
+ }
96
+ finally {
97
+ await handle.close();
98
+ }
99
+ try {
100
+ await chmod(temporary, mode);
101
+ await rename(temporary, path);
102
+ const directory = await open(dirname(path), constants.O_RDONLY | constants.O_DIRECTORY);
103
+ try {
104
+ await directory.sync();
105
+ }
106
+ finally {
107
+ await directory.close();
108
+ }
109
+ }
110
+ finally {
111
+ await rm(temporary, { force: true });
112
+ }
113
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@kisev/safe-fs",
3
+ "version": "1.0.0-dev.46.gfbe1e4c6992e",
4
+ "description": "Safe filesystem primitives: private atomic writes and symlink-free path traversal.",
5
+ "homepage": "https://github.com/kisev/skills#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/kisev/skills/issues"
8
+ },
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/kisev/skills.git",
13
+ "directory": "packages/safe-fs"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "README.ru.md"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "build": "node scripts/clean-dist.mjs && tsc --project tsconfig.json",
29
+ "pack:check": "node test/pack-allowlist.mjs"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "22.15.30",
33
+ "typescript": "7.0.2"
34
+ },
35
+ "engines": {
36
+ "node": ">=22"
37
+ }
38
+ }