@autokap/core 1.7.0 → 1.7.2

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Sanitize a single path segment (NOT a full path — slashes are stripped) into
3
+ * a safe, deterministic filename token.
4
+ *
5
+ * - Collapses any run of characters outside `[A-Za-z0-9._-]` to a single `-`.
6
+ * - Collapses repeated `-`.
7
+ * - Trims leading/trailing `.`/`-`/space (Windows rejects trailing dot/space;
8
+ * a leading dot would hide the file on POSIX).
9
+ * - Prefixes `_` when the result collides with a Windows reserved device name.
10
+ * - Caps length at 80 chars.
11
+ * - Falls back to `'artifact'` when nothing usable remains.
12
+ */
13
+ export declare function sanitizePathToken(value: string): string;
14
+ /**
15
+ * Resolve `fileName` inside `outputDir`, refusing any result that escapes the
16
+ * directory (via `..` segments or an absolute component). Returns the absolute
17
+ * contained path; throws otherwise.
18
+ *
19
+ * `fileName` should already be sanitized with `sanitizePathToken`; this is the
20
+ * defense-in-depth boundary check.
21
+ */
22
+ export declare function resolveContainedPath(outputDir: string, fileName: string): string;
23
+ /**
24
+ * Map an asset `format` token (`png`, `webp`, `gif`, `mp4`, …) to a file
25
+ * extension (without the leading dot). `jpeg` normalizes to `jpg`. Unknown
26
+ * formats fall back to `bin`.
27
+ */
28
+ export declare function extFromFormat(format: string): string;
29
+ /**
30
+ * Map a MIME type to a file extension (without the leading dot). Mirrors the
31
+ * mime→ext ladder previously inline in `cli-runner-local.ts`. Unknown types
32
+ * fall back to `bin`.
33
+ */
34
+ export declare function extFromMimeType(mimeType: string): string;
35
+ /**
36
+ * Atomically write `data` to `dest`: write to a per-process tmp sibling, then
37
+ * rename over the destination. A bare `writeFile` can be interrupted (crash,
38
+ * OOM, signal) mid-write, leaving a truncated file in the user's repo;
39
+ * tmp+rename is a single `rename(2)` syscall, atomic on POSIX (same volume)
40
+ * and on Windows NTFS.
41
+ *
42
+ * Unlike `~/.autokap` writes, this does NOT chmod — the destination is the
43
+ * user's project directory and should inherit normal umask permissions.
44
+ */
45
+ export declare function writeFileAtomic(dest: string, data: Buffer): Promise<void>;
@@ -0,0 +1,138 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ /**
4
+ * Shared filesystem-path helpers for the CLI and the MCP server.
5
+ *
6
+ * These were originally private to `src/cli-runner-local.ts` (CLI-only). The
7
+ * MCP download tools (`autokap_download_assets`) need the same path-safety and
8
+ * atomic-write guarantees when writing fetched assets into a user-chosen
9
+ * directory, so they live here in `@autokap/core` — imported by both surfaces.
10
+ *
11
+ * Cross-platform notes (CLAUDE.md §8):
12
+ * - Always go through `path.*` (join/resolve/relative); never string-concat
13
+ * with `/`.
14
+ * - `sanitizePathToken` additionally guards Windows reserved device names and
15
+ * trailing dots/spaces, which are illegal in NTFS filenames but harmless on
16
+ * POSIX.
17
+ * - `writeFileAtomic` deliberately does NOT chmod — downloads land in the
18
+ * user's project directory (a repo `docs/images` dir), not a secrets dir,
19
+ * so the restrictive 0o600 used for `~/.autokap/config.json` would be wrong.
20
+ */
21
+ // Windows reserved device names (case-insensitive, with or without extension):
22
+ // CON, PRN, AUX, NUL, COM1-9, LPT1-9. A file literally named `CON.png` is
23
+ // rejected by the Win32 API, so we prefix an underscore to make it legal.
24
+ const WINDOWS_RESERVED_NAME_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
25
+ const MAX_PATH_TOKEN_LENGTH = 80;
26
+ /**
27
+ * Sanitize a single path segment (NOT a full path — slashes are stripped) into
28
+ * a safe, deterministic filename token.
29
+ *
30
+ * - Collapses any run of characters outside `[A-Za-z0-9._-]` to a single `-`.
31
+ * - Collapses repeated `-`.
32
+ * - Trims leading/trailing `.`/`-`/space (Windows rejects trailing dot/space;
33
+ * a leading dot would hide the file on POSIX).
34
+ * - Prefixes `_` when the result collides with a Windows reserved device name.
35
+ * - Caps length at 80 chars.
36
+ * - Falls back to `'artifact'` when nothing usable remains.
37
+ */
38
+ export function sanitizePathToken(value) {
39
+ const cleaned = value
40
+ .trim()
41
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
42
+ .replace(/-+/g, '-')
43
+ // Strip leading/trailing dots, dashes and spaces. Leading `.` hides the
44
+ // file on POSIX; trailing `.`/space is silently dropped by Win32 (so two
45
+ // distinct tokens could collapse to the same on-disk name).
46
+ .replace(/^[.\-\s]+/, '')
47
+ .replace(/[.\-\s]+$/, '');
48
+ if (cleaned.length === 0) {
49
+ return 'artifact';
50
+ }
51
+ const capped = cleaned.slice(0, MAX_PATH_TOKEN_LENGTH);
52
+ // Re-trim after the slice — capping could have left a trailing `.`/`-`.
53
+ const trimmed = capped.replace(/[.\-\s]+$/, '');
54
+ const safe = trimmed.length > 0 ? trimmed : 'artifact';
55
+ if (WINDOWS_RESERVED_NAME_RE.test(safe)) {
56
+ return `_${safe}`;
57
+ }
58
+ return safe;
59
+ }
60
+ /**
61
+ * Resolve `fileName` inside `outputDir`, refusing any result that escapes the
62
+ * directory (via `..` segments or an absolute component). Returns the absolute
63
+ * contained path; throws otherwise.
64
+ *
65
+ * `fileName` should already be sanitized with `sanitizePathToken`; this is the
66
+ * defense-in-depth boundary check.
67
+ */
68
+ export function resolveContainedPath(outputDir, fileName) {
69
+ const resolved = path.resolve(outputDir, fileName);
70
+ const relative = path.relative(outputDir, resolved);
71
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
72
+ throw new Error(`Refusing to write outside output directory: ${fileName}`);
73
+ }
74
+ return resolved;
75
+ }
76
+ const FORMAT_EXTENSIONS = {
77
+ png: 'png',
78
+ jpg: 'jpg',
79
+ jpeg: 'jpg',
80
+ webp: 'webp',
81
+ gif: 'gif',
82
+ mp4: 'mp4',
83
+ webm: 'webm',
84
+ };
85
+ /**
86
+ * Map an asset `format` token (`png`, `webp`, `gif`, `mp4`, …) to a file
87
+ * extension (without the leading dot). `jpeg` normalizes to `jpg`. Unknown
88
+ * formats fall back to `bin`.
89
+ */
90
+ export function extFromFormat(format) {
91
+ return FORMAT_EXTENSIONS[format.trim().toLowerCase()] ?? 'bin';
92
+ }
93
+ /**
94
+ * Map a MIME type to a file extension (without the leading dot). Mirrors the
95
+ * mime→ext ladder previously inline in `cli-runner-local.ts`. Unknown types
96
+ * fall back to `bin`.
97
+ */
98
+ export function extFromMimeType(mimeType) {
99
+ const mime = mimeType.split(';', 1)[0].trim().toLowerCase();
100
+ switch (mime) {
101
+ case 'image/png':
102
+ return 'png';
103
+ case 'image/jpeg':
104
+ return 'jpg';
105
+ case 'image/webp':
106
+ return 'webp';
107
+ case 'image/gif':
108
+ return 'gif';
109
+ case 'video/mp4':
110
+ return 'mp4';
111
+ case 'video/webm':
112
+ return 'webm';
113
+ default:
114
+ return 'bin';
115
+ }
116
+ }
117
+ /**
118
+ * Atomically write `data` to `dest`: write to a per-process tmp sibling, then
119
+ * rename over the destination. A bare `writeFile` can be interrupted (crash,
120
+ * OOM, signal) mid-write, leaving a truncated file in the user's repo;
121
+ * tmp+rename is a single `rename(2)` syscall, atomic on POSIX (same volume)
122
+ * and on Windows NTFS.
123
+ *
124
+ * Unlike `~/.autokap` writes, this does NOT chmod — the destination is the
125
+ * user's project directory and should inherit normal umask permissions.
126
+ */
127
+ export async function writeFileAtomic(dest, data) {
128
+ const tmp = `${dest}.${process.pid}.${Date.now().toString(36)}.tmp`;
129
+ try {
130
+ await fs.writeFile(tmp, data);
131
+ await fs.rename(tmp, dest);
132
+ }
133
+ catch (err) {
134
+ await fs.rm(tmp, { force: true }).catch(() => undefined);
135
+ throw err;
136
+ }
137
+ }
138
+ //# sourceMappingURL=fs-paths.js.map
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export * from './types.js';
2
2
  export * from './config.js';
3
3
  export * from './api-client.js';
4
4
  export * from './endpoint-helpers.js';
5
+ export * from './fs-paths.js';
5
6
  export * from './logger.js';
package/dist/index.js CHANGED
@@ -2,5 +2,6 @@ export * from './types.js';
2
2
  export * from './config.js';
3
3
  export * from './api-client.js';
4
4
  export * from './endpoint-helpers.js';
5
+ export * from './fs-paths.js';
5
6
  export * from './logger.js';
6
7
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autokap/core",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "Shared core library for AutoKap CLI and MCP server",
5
5
  "license": "ISC",
6
6
  "author": "AutoKap",
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/endpoint-helpers.d.ts",
34
34
  "default": "./dist/endpoint-helpers.js"
35
35
  },
36
+ "./fs-paths": {
37
+ "types": "./dist/fs-paths.d.ts",
38
+ "default": "./dist/fs-paths.js"
39
+ },
36
40
  "./types": {
37
41
  "types": "./dist/types.d.ts",
38
42
  "default": "./dist/types.js"