@sagmans/dsh-tui 0.3.0 → 0.4.0

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 (113) hide show
  1. package/README.md +158 -6
  2. package/cordis.patch.yml +14 -0
  3. package/lib/agent/projections.d.ts +22 -6
  4. package/lib/agent/projections.d.ts.map +1 -1
  5. package/lib/agent/projections.js +21 -10
  6. package/lib/agent/projections.js.map +1 -1
  7. package/lib/agent/prompt-history.d.ts +89 -0
  8. package/lib/agent/prompt-history.d.ts.map +1 -0
  9. package/lib/agent/prompt-history.js +380 -0
  10. package/lib/agent/prompt-history.js.map +1 -0
  11. package/lib/agent/status.d.ts +6 -0
  12. package/lib/agent/status.d.ts.map +1 -1
  13. package/lib/agent/status.js +1 -0
  14. package/lib/agent/status.js.map +1 -1
  15. package/lib/herdr/client.d.ts +57 -0
  16. package/lib/herdr/client.d.ts.map +1 -0
  17. package/lib/herdr/client.js +218 -0
  18. package/lib/herdr/client.js.map +1 -0
  19. package/lib/herdr/constants.d.ts +93 -0
  20. package/lib/herdr/constants.d.ts.map +1 -0
  21. package/lib/herdr/constants.js +79 -0
  22. package/lib/herdr/constants.js.map +1 -0
  23. package/lib/herdr/reporter.d.ts +60 -0
  24. package/lib/herdr/reporter.d.ts.map +1 -0
  25. package/lib/herdr/reporter.js +217 -0
  26. package/lib/herdr/reporter.js.map +1 -0
  27. package/lib/herdr/state.d.ts +57 -0
  28. package/lib/herdr/state.d.ts.map +1 -0
  29. package/lib/herdr/state.js +62 -0
  30. package/lib/herdr/state.js.map +1 -0
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +252 -20
  33. package/lib/index.js.map +1 -1
  34. package/lib/input/actions.d.ts +1 -1
  35. package/lib/input/actions.d.ts.map +1 -1
  36. package/lib/input/actions.js +3 -0
  37. package/lib/input/actions.js.map +1 -1
  38. package/lib/input/ghost.d.ts +46 -0
  39. package/lib/input/ghost.d.ts.map +1 -0
  40. package/lib/input/ghost.js +58 -0
  41. package/lib/input/ghost.js.map +1 -0
  42. package/lib/input/keymap.d.ts.map +1 -1
  43. package/lib/input/keymap.js +2 -0
  44. package/lib/input/keymap.js.map +1 -1
  45. package/lib/input/submission.d.ts +24 -1
  46. package/lib/input/submission.d.ts.map +1 -1
  47. package/lib/input/submission.js +28 -1
  48. package/lib/input/submission.js.map +1 -1
  49. package/lib/stash/lock.d.ts +72 -0
  50. package/lib/stash/lock.d.ts.map +1 -0
  51. package/lib/stash/lock.js +374 -0
  52. package/lib/stash/lock.js.map +1 -0
  53. package/lib/stash/paths.d.ts +22 -0
  54. package/lib/stash/paths.d.ts.map +1 -0
  55. package/lib/stash/paths.js +83 -0
  56. package/lib/stash/paths.js.map +1 -0
  57. package/lib/stash/private-fs.d.ts +61 -0
  58. package/lib/stash/private-fs.d.ts.map +1 -0
  59. package/lib/stash/private-fs.js +379 -0
  60. package/lib/stash/private-fs.js.map +1 -0
  61. package/lib/stash/schema.d.ts +57 -0
  62. package/lib/stash/schema.d.ts.map +1 -0
  63. package/lib/stash/schema.js +127 -0
  64. package/lib/stash/schema.js.map +1 -0
  65. package/lib/stash/store.d.ts +92 -0
  66. package/lib/stash/store.d.ts.map +1 -0
  67. package/lib/stash/store.js +262 -0
  68. package/lib/stash/store.js.map +1 -0
  69. package/lib/stash.d.ts +91 -0
  70. package/lib/stash.d.ts.map +1 -0
  71. package/lib/stash.js +296 -0
  72. package/lib/stash.js.map +1 -0
  73. package/lib/theme-settings.d.ts +37 -8
  74. package/lib/theme-settings.d.ts.map +1 -1
  75. package/lib/theme-settings.js +60 -3
  76. package/lib/theme-settings.js.map +1 -1
  77. package/lib/theme-tokens.d.ts +1 -1
  78. package/lib/theme-tokens.d.ts.map +1 -1
  79. package/lib/theme-tokens.js +8 -0
  80. package/lib/theme-tokens.js.map +1 -1
  81. package/lib/todo-guard.d.ts +116 -0
  82. package/lib/todo-guard.d.ts.map +1 -0
  83. package/lib/todo-guard.js +259 -0
  84. package/lib/todo-guard.js.map +1 -0
  85. package/lib/ui/editor.d.ts +41 -1
  86. package/lib/ui/editor.d.ts.map +1 -1
  87. package/lib/ui/editor.js +98 -4
  88. package/lib/ui/editor.js.map +1 -1
  89. package/lib/ui/history-picker.d.ts +15 -0
  90. package/lib/ui/history-picker.d.ts.map +1 -0
  91. package/lib/ui/history-picker.js +26 -0
  92. package/lib/ui/history-picker.js.map +1 -0
  93. package/lib/ui/picker.d.ts +11 -1
  94. package/lib/ui/picker.d.ts.map +1 -1
  95. package/lib/ui/picker.js +17 -2
  96. package/lib/ui/picker.js.map +1 -1
  97. package/lib/ui/prompt.d.ts +2 -0
  98. package/lib/ui/prompt.d.ts.map +1 -1
  99. package/lib/ui/prompt.js +4 -0
  100. package/lib/ui/prompt.js.map +1 -1
  101. package/lib/ui/stash-picker.d.ts +43 -0
  102. package/lib/ui/stash-picker.d.ts.map +1 -0
  103. package/lib/ui/stash-picker.js +78 -0
  104. package/lib/ui/stash-picker.js.map +1 -0
  105. package/lib/ui/status.d.ts +2 -0
  106. package/lib/ui/status.d.ts.map +1 -1
  107. package/lib/ui/status.js +7 -0
  108. package/lib/ui/status.js.map +1 -1
  109. package/lib/work.d.ts +3 -1
  110. package/lib/work.d.ts.map +1 -1
  111. package/lib/work.js +9 -1
  112. package/lib/work.js.map +1 -1
  113. package/package.json +5 -1
@@ -0,0 +1,83 @@
1
+ // Disk layout for prompt stashes.
2
+ //
3
+ // A stash is scoped to the exact working directory: a linked worktree, a bare
4
+ // checkout, and a plain folder are all just distinct cwd values, so keying on
5
+ // cwd covers every case without any git discovery. The cwd is flattened into a
6
+ // filename-safe key that stays human-readable in the agent directory.
7
+ //
8
+ // Readability cannot be bought with injectivity: a separator made of hyphens
9
+ // cannot be told apart from a hyphen inside a directory name, and truncating a
10
+ // deep path can land on a name a shorter path already owns. Two working
11
+ // directories sharing one bank would let one directory read or delete another's
12
+ // drafts, so every key ends with a digest of the exact cwd. The readable part is
13
+ // then only a label: two distinct cwds collide in the label, never in the file.
14
+ import { createHash } from 'node:crypto';
15
+ import { homedir } from 'node:os';
16
+ import path from 'node:path';
17
+ /** The directory `DSH_HOME` points at when the environment names none. */
18
+ const DEFAULT_DSH_HOME_DIR = '.dsh';
19
+ /** The subtree of `DSH_HOME` this surface owns. */
20
+ const STASH_DIR_NAME = 'tui-stash';
21
+ // POSIX filenames may not contain "/" or NUL, and everything else is legal, so
22
+ // once separators are flattened the result is filename-safe on the platforms
23
+ // this surface targets. 200 keeps the whole key, digest included, well under the
24
+ // common 255-byte filename limit.
25
+ const SANITIZE_MAX_LENGTH = 200;
26
+ const SEPARATOR = '--';
27
+ const KEY_FORMAT_VERSION = 'v1';
28
+ const KEY_PREFIX = `${KEY_FORMAT_VERSION}${SEPARATOR}`;
29
+ const ESCAPE_CHARACTER = '%';
30
+ const ESCAPED_ESCAPE_CHARACTER = '%25';
31
+ const ESCAPED_SEPARATOR = '%2D%2D';
32
+ const BACKSLASH = '\\';
33
+ const ESCAPED_BACKSLASH = '%5C';
34
+ const HASH_ALGORITHM = 'sha256';
35
+ /** 64 bits of digest, which no deliberate name can be built to collide with. */
36
+ const HASH_LENGTH = 16;
37
+ /** Escape one path segment so it cannot collide with the separator or an escape. */
38
+ function escapeSegment(segment) {
39
+ return segment
40
+ .replaceAll(ESCAPE_CHARACTER, ESCAPED_ESCAPE_CHARACTER)
41
+ .replaceAll(BACKSLASH, ESCAPED_BACKSLASH)
42
+ .replaceAll(SEPARATOR, ESCAPED_SEPARATOR);
43
+ }
44
+ /** Keep whole characters while fitting a byte budget, so a cut cannot split one. */
45
+ function truncateToUtf8Bytes(value, maxBytes) {
46
+ let bytes = 0;
47
+ let result = '';
48
+ for (const character of value) {
49
+ const characterBytes = Buffer.byteLength(character);
50
+ if (bytes + characterBytes > maxBytes)
51
+ break;
52
+ result += character;
53
+ bytes += characterBytes;
54
+ }
55
+ return result;
56
+ }
57
+ export function sanitizeCwd(cwd) {
58
+ const readable = cwd.split('/').filter(Boolean).map(escapeSegment).join(SEPARATOR);
59
+ const digest = createHash(HASH_ALGORITHM).update(cwd, 'utf8').digest('hex').slice(0, HASH_LENGTH);
60
+ const suffix = `${SEPARATOR}${digest}`;
61
+ const budget = SANITIZE_MAX_LENGTH - Buffer.byteLength(KEY_PREFIX) - Buffer.byteLength(suffix);
62
+ return `${KEY_PREFIX}${truncateToUtf8Bytes(readable, Math.max(budget, 0))}${suffix}`;
63
+ }
64
+ /**
65
+ * The directory the harness keeps its own state in.
66
+ *
67
+ * DSH_HOME is the one thing that moves that directory, and it is resolved the
68
+ * same way the settings document resolves it, so a scratch home keeps stash data
69
+ * out of a reader's real home during a test run.
70
+ */
71
+ export function dshHomeDir(env = process.env, home = homedir()) {
72
+ const configured = env.DSH_HOME?.trim();
73
+ return configured === undefined || configured === '' ? path.join(home, DEFAULT_DSH_HOME_DIR) : configured;
74
+ }
75
+ /** The root every working directory's stash file sits under. */
76
+ export function stashBaseDir(env = process.env, home = homedir()) {
77
+ return path.join(dshHomeDir(env, home), STASH_DIR_NAME);
78
+ }
79
+ export function resolveStashPaths(cwd, baseDir = stashBaseDir()) {
80
+ const key = sanitizeCwd(cwd);
81
+ return { cwd, key, file: path.join(baseDir, `${key}.json`) };
82
+ }
83
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/stash/paths.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import { type Stats } from 'node:fs';
2
+ export declare const PRIVATE_DIR_MODE = 448;
3
+ export declare const PRIVATE_FILE_MODE = 384;
4
+ type FileIdentity = Pick<Stats, 'dev' | 'ino'>;
5
+ export interface PrivateTextFile {
6
+ readonly text: string;
7
+ readonly identity: FileIdentity;
8
+ }
9
+ /** Where a quarantined file went, and whether that location reached the disk. */
10
+ export interface QuarantineResult {
11
+ readonly path: string;
12
+ /** Set when the directory naming the copy could not be synced. */
13
+ readonly syncError: unknown;
14
+ }
15
+ /** A file that exceeded the caller's byte budget, which is a refusal, not corruption. */
16
+ export declare class FileTooLargeError extends Error {
17
+ constructor(label: string);
18
+ }
19
+ /**
20
+ * Prepare the storage directory, and make the directories that name it durable.
21
+ *
22
+ * A directory that names a new child holds the only record of that child, so
23
+ * every directory this call creates is flushed through its parent before the
24
+ * call returns. A step that fails takes the created directories away again:
25
+ * leaving them behind would turn the next attempt into one that finds nothing
26
+ * left to flush, and a save could then report success for a tree that a power
27
+ * loss is free to drop.
28
+ *
29
+ * The rollback only ever removes an empty directory. Another surface can reach a
30
+ * directory this call just created and save into it while this one is flushing,
31
+ * and a recursive removal would then delete that surface's committed bank: an
32
+ * entry left unflushed costs durability, while a removed bank costs the draft.
33
+ */
34
+ export declare function ensurePrivateDirectory(directory: string, label?: string, syncDirectory?: typeof syncDirectoryEntry): Promise<void>;
35
+ /**
36
+ * Flush one directory entry without asserting who owns the directory.
37
+ *
38
+ * An ancestor may legitimately be root-owned (`/tmp`, a mount point) and still
39
+ * have to be synced for the entry below it to survive a power loss, so the
40
+ * ownership rule that guards the storage directory itself is not applied here.
41
+ * A directory that is already gone needs no flushing.
42
+ */
43
+ export declare function syncDirectoryEntry(directory: string): Promise<void>;
44
+ export declare function assertPrivateDirectory(directory: string, label: string): Promise<Stats>;
45
+ export declare function syncPrivateDirectory(directory: string, label?: string): Promise<void>;
46
+ export declare function readPrivateTextFile(filePath: string, label?: string, maxBytes?: number): Promise<PrivateTextFile>;
47
+ export declare function writePrivateFileExclusive(filePath: string, data: string | Uint8Array): Promise<void>;
48
+ /**
49
+ * Move a file aside without losing it.
50
+ *
51
+ * A hard-link reservation keeps the original bytes reachable under a new name
52
+ * before the source is unlinked, so an interrupted quarantine leaves either the
53
+ * readable original or the readable copy, never neither.
54
+ */
55
+ export declare function quarantinePrivateFile(filePath: string, identity: FileIdentity, label: string, syncDirectory?: typeof syncPrivateDirectory): Promise<QuarantineResult>;
56
+ export declare function removePrivateDirectory(directory: string, label?: string): Promise<void>;
57
+ export declare function assertRegularOwnedFile(stats: Stats, label: string): void;
58
+ export declare function pathExists(target: string): Promise<boolean>;
59
+ export declare function hasErrorCode(value: unknown, code: string): boolean;
60
+ export {};
61
+ //# sourceMappingURL=private-fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-fs.d.ts","sourceRoot":"","sources":["../../src/stash/private-fs.ts"],"names":[],"mappings":"AAQA,OAAO,EAAa,KAAK,KAAK,EAAE,MAAM,SAAS,CAAA;AAI/C,eAAO,MAAM,gBAAgB,MAAQ,CAAA;AACrC,eAAO,MAAM,iBAAiB,MAAQ,CAAA;AAgBtC,KAAK,YAAY,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,CAAA;AAE9C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;CAChC;AAED,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED,yFAAyF;AACzF,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,KAAK,EAAE,MAAM;CAI1B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,KAAK,SAAsB,EAC3B,aAAa,GAAE,OAAO,kBAAuC,GAC5D,OAAO,CAAC,IAAI,CAAC,CAyBf;AAuGD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAazE;AAED,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAO7F;AAED,wBAAsB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOxG;AAED,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAe,EACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAa1B;AAeD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAa1G;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,EACb,aAAa,GAAE,OAAO,oBAA2C,GAChE,OAAO,CAAC,gBAAgB,CAAC,CAgC3B;AAED,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAY1G;AAwBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIxE;AAmBD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQjE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAOlE"}
@@ -0,0 +1,379 @@
1
+ // No-follow filesystem primitives for sensitive draft storage.
2
+ //
3
+ // Path-based chmod/read helpers can follow a link swapped into place after a
4
+ // check. Opening the validated inode and operating through its handle keeps
5
+ // permission repair and reads bound to the object that was inspected. Ownership
6
+ // and symlink checks are what keep one user's drafts out of another's reach on a
7
+ // shared machine or a network home.
8
+ import { constants } from 'node:fs';
9
+ import { link, lstat, mkdir, open, readlink, rmdir, rm, unlink } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ export const PRIVATE_DIR_MODE = 0o700;
12
+ export const PRIVATE_FILE_MODE = 0o600;
13
+ const MAX_QUARANTINE_ATTEMPTS = 1000;
14
+ const NO_FOLLOW_FLAG = constants.O_NOFOLLOW ?? 0;
15
+ const DIRECTORY_FLAG = constants.O_DIRECTORY ?? 0;
16
+ const ROOT_UID = 0;
17
+ const WORLD_WRITABLE_MODE = 0o002;
18
+ const GROUP_WRITABLE_MODE = 0o020;
19
+ const STICKY_MODE = 0o1000;
20
+ /** The kernel's own limit on a link chain, past which the path cannot be opened anyway. */
21
+ const MAX_LINK_HOPS = 32;
22
+ const TOO_MANY_LINKS_MESSAGE = 'passes through too many links to be checked';
23
+ const UNTRUSTED_ANCESTOR_MESSAGE = 'is writable by other users, so a stash kept there could be redirected to another directory by anyone on this machine';
24
+ /** A file that exceeded the caller's byte budget, which is a refusal, not corruption. */
25
+ export class FileTooLargeError extends Error {
26
+ constructor(label) {
27
+ super(`${label} is too large`);
28
+ this.name = 'FileTooLargeError';
29
+ }
30
+ }
31
+ /**
32
+ * Prepare the storage directory, and make the directories that name it durable.
33
+ *
34
+ * A directory that names a new child holds the only record of that child, so
35
+ * every directory this call creates is flushed through its parent before the
36
+ * call returns. A step that fails takes the created directories away again:
37
+ * leaving them behind would turn the next attempt into one that finds nothing
38
+ * left to flush, and a save could then report success for a tree that a power
39
+ * loss is free to drop.
40
+ *
41
+ * The rollback only ever removes an empty directory. Another surface can reach a
42
+ * directory this call just created and save into it while this one is flushing,
43
+ * and a recursive removal would then delete that surface's committed bank: an
44
+ * entry left unflushed costs durability, while a removed bank costs the draft.
45
+ */
46
+ export async function ensurePrivateDirectory(directory, label = 'storage directory', syncDirectory = syncDirectoryEntry) {
47
+ await assertTrustedAncestors(directory, label);
48
+ const { created, namingParents } = await createPrivateDirectory(directory);
49
+ try {
50
+ const handle = await openValidatedDirectory(directory, label);
51
+ try {
52
+ await handle.chmod(PRIVATE_DIR_MODE);
53
+ await handle.sync();
54
+ }
55
+ finally {
56
+ await handle.close();
57
+ }
58
+ for (const parent of namingParents)
59
+ await syncDirectory(parent);
60
+ }
61
+ catch (error) {
62
+ // Deepest first, so a directory is only removed once nothing is named by it.
63
+ for (const entry of created) {
64
+ try {
65
+ await rmdir(entry);
66
+ }
67
+ catch {
68
+ // Anything still in the way belongs to another writer now, and leaving it
69
+ // is the whole point of not removing the tree recursively.
70
+ break;
71
+ }
72
+ }
73
+ throw error;
74
+ }
75
+ }
76
+ /** Create the directory chain, reporting what it created and what names it. */
77
+ async function createPrivateDirectory(directory) {
78
+ const created = [];
79
+ let current = path.resolve(directory);
80
+ for (;;) {
81
+ if (await pathExists(current))
82
+ break;
83
+ created.push(current);
84
+ const parent = path.dirname(current);
85
+ if (parent === current)
86
+ break;
87
+ current = parent;
88
+ }
89
+ await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
90
+ // The parent of a created directory is where its entry lives; a directory that
91
+ // was itself just created is named by the next step of the walk.
92
+ return { created, namingParents: created.map(entry => path.dirname(entry)) };
93
+ }
94
+ /**
95
+ * Refuse a storage path that anyone but this user (or root) can rewrite.
96
+ *
97
+ * Opening the storage directory without following a link protects that one
98
+ * component, but a directory another user can write is enough to redirect the
99
+ * storage by replacing an entry below it, so every directory that the path
100
+ * passes through has to be checked. The writable bit that matters is any bit but
101
+ * the owner's: a group member can rename an entry just as a stranger can, so only
102
+ * the sticky bit — which reserves renaming to an entry's owner — makes a shared
103
+ * directory acceptable.
104
+ *
105
+ * Links are followed on purpose, because a link like macOS's `/tmp` is not a
106
+ * threat and the permissions of what it resolves to are what count. Each link is
107
+ * therefore read as a link and its target walked in turn, rather than resolved in
108
+ * one step: a chain that jumps through a shared directory would otherwise be
109
+ * checked only where it lands, and the directory it jumped through is the one
110
+ * another user can rewrite.
111
+ */
112
+ async function assertTrustedAncestors(directory, label) {
113
+ const currentUid = process.getuid?.();
114
+ const pending = [path.resolve(directory)];
115
+ const checked = new Set();
116
+ let hops = 0;
117
+ while (pending.length > 0) {
118
+ const current = pending.pop();
119
+ if (checked.has(current))
120
+ continue;
121
+ checked.add(current);
122
+ let stats;
123
+ try {
124
+ stats = await lstat(current);
125
+ }
126
+ catch (error) {
127
+ // A directory that is not there yet is created by the caller; its
128
+ // ancestors still have to be checked, so a miss is not a failure here.
129
+ if (!hasErrorCode(error, 'ENOENT'))
130
+ throw error;
131
+ pending.push(...parentsOf(current));
132
+ continue;
133
+ }
134
+ // A link's own mode decides nothing, but its owner decides everything: in a
135
+ // directory that is sticky, the one account that can repoint a link is the
136
+ // one that owns it, so a link this user does not own is not followed.
137
+ if (stats.isSymbolicLink()) {
138
+ if (currentUid !== undefined && stats.uid !== currentUid && stats.uid !== ROOT_UID) {
139
+ throw new Error(`${label} at ${current} is owned by another user`);
140
+ }
141
+ hops += 1;
142
+ if (hops > MAX_LINK_HOPS)
143
+ throw new Error(`${label} at ${current} ${TOO_MANY_LINKS_MESSAGE}`);
144
+ // The target is not normalized: `..` after a link has to be resolved by the
145
+ // filesystem against what the link points at, and collapsing it here would
146
+ // walk a directory the kernel never visits.
147
+ const target = await readlink(current);
148
+ pending.push(path.isAbsolute(target) ? target : `${path.dirname(current)}/${target}`, ...parentsOf(current));
149
+ continue;
150
+ }
151
+ assertTrustedStats(stats, current, label, currentUid);
152
+ pending.push(...parentsOf(current));
153
+ }
154
+ }
155
+ /** The one directory that names `current`, if it has one. */
156
+ function parentsOf(current) {
157
+ const parent = path.dirname(current);
158
+ return parent === current ? [] : [parent];
159
+ }
160
+ function assertTrustedStats(stats, directory, label, currentUid) {
161
+ if (currentUid !== undefined && stats.uid !== currentUid && stats.uid !== ROOT_UID) {
162
+ throw new Error(`${label} at ${directory} is owned by another user`);
163
+ }
164
+ const shared = stats.mode & (WORLD_WRITABLE_MODE | GROUP_WRITABLE_MODE);
165
+ if (shared !== 0 && (stats.mode & STICKY_MODE) === 0) {
166
+ throw new Error(`${label} at ${directory} ${UNTRUSTED_ANCESTOR_MESSAGE}`);
167
+ }
168
+ }
169
+ /**
170
+ * Flush one directory entry without asserting who owns the directory.
171
+ *
172
+ * An ancestor may legitimately be root-owned (`/tmp`, a mount point) and still
173
+ * have to be synced for the entry below it to survive a power loss, so the
174
+ * ownership rule that guards the storage directory itself is not applied here.
175
+ * A directory that is already gone needs no flushing.
176
+ */
177
+ export async function syncDirectoryEntry(directory) {
178
+ let handle;
179
+ try {
180
+ handle = await open(directory, constants.O_RDONLY | DIRECTORY_FLAG);
181
+ }
182
+ catch (error) {
183
+ if (hasErrorCode(error, 'ENOENT'))
184
+ return;
185
+ throw error;
186
+ }
187
+ try {
188
+ await handle.sync();
189
+ }
190
+ finally {
191
+ await handle.close();
192
+ }
193
+ }
194
+ export async function assertPrivateDirectory(directory, label) {
195
+ const handle = await openValidatedDirectory(directory, label);
196
+ try {
197
+ return await handle.stat();
198
+ }
199
+ finally {
200
+ await handle.close();
201
+ }
202
+ }
203
+ export async function syncPrivateDirectory(directory, label = 'storage directory') {
204
+ const handle = await openValidatedDirectory(directory, label);
205
+ try {
206
+ await handle.sync();
207
+ }
208
+ finally {
209
+ await handle.close();
210
+ }
211
+ }
212
+ export async function readPrivateTextFile(filePath, label = 'stash file', maxBytes) {
213
+ const before = await lstat(filePath);
214
+ assertRegularOwnedFile(before, label);
215
+ const handle = await open(filePath, constants.O_RDONLY | NO_FOLLOW_FLAG);
216
+ try {
217
+ const opened = await handle.stat();
218
+ assertRegularOwnedFile(opened, label);
219
+ assertSameIdentity(before, opened, label);
220
+ await handle.chmod(PRIVATE_FILE_MODE);
221
+ return { text: await readText(handle, maxBytes, label), identity: identityOf(opened) };
222
+ }
223
+ finally {
224
+ await handle.close();
225
+ }
226
+ }
227
+ async function readText(handle, maxBytes, label) {
228
+ if (maxBytes === undefined)
229
+ return handle.readFile('utf8');
230
+ const buffer = Buffer.alloc(maxBytes + 1);
231
+ let bytesRead = 0;
232
+ while (bytesRead < buffer.length) {
233
+ const chunk = await handle.read(buffer, bytesRead, buffer.length - bytesRead, null);
234
+ if (chunk.bytesRead === 0)
235
+ break;
236
+ bytesRead += chunk.bytesRead;
237
+ }
238
+ if (bytesRead > maxBytes)
239
+ throw new FileTooLargeError(label);
240
+ return buffer.subarray(0, bytesRead).toString('utf8');
241
+ }
242
+ export async function writePrivateFileExclusive(filePath, data) {
243
+ const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NO_FOLLOW_FLAG;
244
+ const handle = await open(filePath, flags, PRIVATE_FILE_MODE);
245
+ let succeeded = false;
246
+ try {
247
+ await handle.writeFile(data);
248
+ await handle.chmod(PRIVATE_FILE_MODE);
249
+ await handle.sync();
250
+ succeeded = true;
251
+ }
252
+ finally {
253
+ await handle.close();
254
+ if (!succeeded)
255
+ await rm(filePath, { force: true });
256
+ }
257
+ }
258
+ /**
259
+ * Move a file aside without losing it.
260
+ *
261
+ * A hard-link reservation keeps the original bytes reachable under a new name
262
+ * before the source is unlinked, so an interrupted quarantine leaves either the
263
+ * readable original or the readable copy, never neither.
264
+ */
265
+ export async function quarantinePrivateFile(filePath, identity, label, syncDirectory = syncPrivateDirectory) {
266
+ for (let attempt = 0; attempt < MAX_QUARANTINE_ATTEMPTS; attempt += 1) {
267
+ const candidate = `${filePath}.${label}${attempt === 0 ? '' : `-${attempt}`}`;
268
+ try {
269
+ await link(filePath, candidate);
270
+ }
271
+ catch (error) {
272
+ if (hasErrorCode(error, 'EEXIST'))
273
+ continue;
274
+ throw error;
275
+ }
276
+ let moved = false;
277
+ try {
278
+ const candidateStats = await lstat(candidate);
279
+ const sourceStats = await lstat(filePath);
280
+ assertSameIdentity(candidateStats, identity, 'quarantine source');
281
+ assertSameIdentity(sourceStats, identity, 'quarantine source');
282
+ await unlink(filePath);
283
+ moved = true;
284
+ try {
285
+ await syncDirectory(path.dirname(filePath));
286
+ }
287
+ catch (error) {
288
+ // The bytes are already under the recovery name and the original is
289
+ // gone, so the path is the one thing the reader must still be told.
290
+ // Throwing here would lose it and leave the next read starting empty
291
+ // with no notice at all.
292
+ return { path: candidate, syncError: error };
293
+ }
294
+ return { path: candidate, syncError: undefined };
295
+ }
296
+ finally {
297
+ if (!moved)
298
+ await unlink(candidate).catch(() => undefined);
299
+ }
300
+ }
301
+ throw new Error(`unable to reserve quarantine path for ${filePath}`);
302
+ }
303
+ export async function removePrivateDirectory(directory, label = 'storage directory') {
304
+ let stats;
305
+ try {
306
+ stats = await lstat(directory);
307
+ }
308
+ catch (error) {
309
+ if (hasErrorCode(error, 'ENOENT'))
310
+ return;
311
+ throw error;
312
+ }
313
+ assertDirectory(stats, label);
314
+ assertCurrentUserOwns(stats, label);
315
+ await rm(directory, { recursive: true });
316
+ await syncPrivateDirectory(path.dirname(directory));
317
+ }
318
+ async function openValidatedDirectory(directory, label) {
319
+ const before = await lstat(directory);
320
+ assertDirectory(before, label);
321
+ assertCurrentUserOwns(before, label);
322
+ const handle = await open(directory, constants.O_RDONLY | DIRECTORY_FLAG | NO_FOLLOW_FLAG);
323
+ try {
324
+ const opened = await handle.stat();
325
+ assertDirectory(opened, label);
326
+ assertCurrentUserOwns(opened, label);
327
+ assertSameIdentity(before, opened, label);
328
+ return handle;
329
+ }
330
+ catch (error) {
331
+ await handle.close();
332
+ throw error;
333
+ }
334
+ }
335
+ function assertDirectory(stats, label) {
336
+ if (stats.isSymbolicLink())
337
+ throw new Error(`${label} must not be a symbolic link`);
338
+ if (!stats.isDirectory())
339
+ throw new Error(`${label} must be a directory`);
340
+ }
341
+ export function assertRegularOwnedFile(stats, label) {
342
+ if (stats.isSymbolicLink())
343
+ throw new Error(`${label} must not be a symbolic link`);
344
+ if (!stats.isFile())
345
+ throw new Error(`${label} must be a regular file`);
346
+ assertCurrentUserOwns(stats, label);
347
+ }
348
+ function assertCurrentUserOwns(stats, label) {
349
+ const currentUid = process.getuid?.();
350
+ if (currentUid !== undefined && stats.uid !== currentUid) {
351
+ throw new Error(`${label} must be owned by the current user`);
352
+ }
353
+ }
354
+ function assertSameIdentity(actual, expected, label) {
355
+ if (actual.dev !== expected.dev || actual.ino !== expected.ino) {
356
+ throw new Error(`${label} changed during validation`);
357
+ }
358
+ }
359
+ function identityOf(stats) {
360
+ return { dev: stats.dev, ino: stats.ino };
361
+ }
362
+ export async function pathExists(target) {
363
+ try {
364
+ await lstat(target);
365
+ return true;
366
+ }
367
+ catch (error) {
368
+ if (hasErrorCode(error, 'ENOENT'))
369
+ return false;
370
+ throw error;
371
+ }
372
+ }
373
+ export function hasErrorCode(value, code) {
374
+ return (typeof value === 'object' &&
375
+ value !== null &&
376
+ 'code' in value &&
377
+ value.code === code);
378
+ }
379
+ //# sourceMappingURL=private-fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-fs.js","sourceRoot":"","sources":["../../src/stash/private-fs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ export declare const STASH_SCHEMA_VERSION = 1;
2
+ /** A stash longer than this is refused rather than stored. */
3
+ export declare const MAX_STASH_ENTRY_BYTES = 1048576;
4
+ export declare function stripControlCharacters(text: string): string;
5
+ export declare function isSafeEntryId(value: string): boolean;
6
+ export declare function assertSafeEntryId(value: string): void;
7
+ /** Refuse one draft that no amount of storage capacity could justify holding. */
8
+ export declare function assertSafeStashText(text: string): void;
9
+ /** The form of a draft that is safe to hand back to a terminal. */
10
+ export declare function sanitizeStashText(text: string): string;
11
+ export interface StashEntry {
12
+ readonly id: string;
13
+ readonly text: string;
14
+ readonly createdAt: number;
15
+ }
16
+ export interface StashFile {
17
+ readonly version: typeof STASH_SCHEMA_VERSION;
18
+ /**
19
+ * The exact working directory this bank belongs to.
20
+ *
21
+ * The key alone cannot prove ownership: it is a flattened label that two
22
+ * directories could once have shared. The exact path is compared on every
23
+ * read, so a file that belongs to another directory is quarantined rather than
24
+ * read or deleted as this one's.
25
+ */
26
+ readonly cwd: string;
27
+ readonly createdAt: number;
28
+ readonly updatedAt: number;
29
+ /** Newest first: `entries[0]` is `stash@{0}`. */
30
+ readonly entries: readonly StashEntry[];
31
+ }
32
+ export type Clock = () => number;
33
+ export declare const createNewId: () => string;
34
+ export declare function createEmptyStashFile(cwd: string, now: number): StashFile;
35
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
36
+ export declare function normalizeEntry(raw: unknown): StashEntry | undefined;
37
+ /**
38
+ * Read one stash file, or nothing when it is not this surface's current format.
39
+ *
40
+ * Strict on purpose: a duplicate id, a missing timestamp, or a foreign working
41
+ * directory is corruption the store quarantines rather than a shape to repair,
42
+ * because a silently repaired file can resurrect entries the reader thought they
43
+ * dropped — or hand another directory's drafts to this one.
44
+ */
45
+ export declare function parseStashFile(raw: unknown): StashFile | undefined;
46
+ export interface ResolvedEntry {
47
+ readonly entry: StashEntry;
48
+ readonly index: number;
49
+ }
50
+ /**
51
+ * Resolve a selector against newest-first entries.
52
+ *
53
+ * Index 0 is the newest, mirroring git's index-0-is-tip convention. A selector
54
+ * is an entry id, a numeric index string, or empty for the most recent entry.
55
+ */
56
+ export declare function resolveBySelector(entries: readonly StashEntry[], selector: string | undefined): ResolvedEntry | undefined;
57
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/stash/schema.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAErC,8DAA8D;AAC9D,eAAO,MAAM,qBAAqB,UAAY,CAAA;AA2B9C,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAErD;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,oBAAoB,CAAA;IAC7C;;;;;;;OAOG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;CACxC;AAED,MAAM,MAAM,KAAK,GAAG,MAAM,MAAM,CAAA;AAEhC,eAAO,MAAM,WAAW,QAAO,MAAsB,CAAA;AAErD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAExE;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAMD,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAOnE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAclE;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,aAAa,GAAG,SAAS,CAc3B"}