@sagmans/dsh-tui 0.3.0 → 0.5.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.
- package/LICENSE +1 -1
- package/README.md +407 -53
- package/cordis.patch.yml +14 -0
- package/lib/agent/present.js +1 -1
- package/lib/agent/present.js.map +1 -1
- package/lib/agent/projections.d.ts +22 -6
- package/lib/agent/projections.d.ts.map +1 -1
- package/lib/agent/projections.js +21 -10
- package/lib/agent/projections.js.map +1 -1
- package/lib/agent/prompt-history.d.ts +89 -0
- package/lib/agent/prompt-history.d.ts.map +1 -0
- package/lib/agent/prompt-history.js +382 -0
- package/lib/agent/prompt-history.js.map +1 -0
- package/lib/agent/status.d.ts +6 -0
- package/lib/agent/status.d.ts.map +1 -1
- package/lib/agent/status.js +1 -0
- package/lib/agent/status.js.map +1 -1
- package/lib/cards.d.ts +83 -13
- package/lib/cards.d.ts.map +1 -1
- package/lib/cards.js +96 -28
- package/lib/cards.js.map +1 -1
- package/lib/export.d.ts +3 -3
- package/lib/export.d.ts.map +1 -1
- package/lib/export.js +23 -13
- package/lib/export.js.map +1 -1
- package/lib/fold-cursor.d.ts +12 -5
- package/lib/fold-cursor.d.ts.map +1 -1
- package/lib/fold-cursor.js +16 -5
- package/lib/fold-cursor.js.map +1 -1
- package/lib/gates.d.ts.map +1 -1
- package/lib/gates.js +42 -9
- package/lib/gates.js.map +1 -1
- package/lib/herdr/client.d.ts +57 -0
- package/lib/herdr/client.d.ts.map +1 -0
- package/lib/herdr/client.js +218 -0
- package/lib/herdr/client.js.map +1 -0
- package/lib/herdr/constants.d.ts +93 -0
- package/lib/herdr/constants.d.ts.map +1 -0
- package/lib/herdr/constants.js +79 -0
- package/lib/herdr/constants.js.map +1 -0
- package/lib/herdr/reporter.d.ts +60 -0
- package/lib/herdr/reporter.d.ts.map +1 -0
- package/lib/herdr/reporter.js +217 -0
- package/lib/herdr/reporter.js.map +1 -0
- package/lib/herdr/state.d.ts +57 -0
- package/lib/herdr/state.d.ts.map +1 -0
- package/lib/herdr/state.js +62 -0
- package/lib/herdr/state.js.map +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +700 -81
- package/lib/index.js.map +1 -1
- package/lib/injection.d.ts +9 -0
- package/lib/injection.d.ts.map +1 -0
- package/lib/injection.js +128 -0
- package/lib/injection.js.map +1 -0
- package/lib/input/actions.d.ts +19 -2
- package/lib/input/actions.d.ts.map +1 -1
- package/lib/input/actions.js +60 -11
- package/lib/input/actions.js.map +1 -1
- package/lib/input/cancel.d.ts +55 -0
- package/lib/input/cancel.d.ts.map +1 -0
- package/lib/input/cancel.js +47 -0
- package/lib/input/cancel.js.map +1 -0
- package/lib/input/completion.d.ts +5 -2
- package/lib/input/completion.d.ts.map +1 -1
- package/lib/input/completion.js +79 -4
- package/lib/input/completion.js.map +1 -1
- package/lib/input/file-search.d.ts +105 -0
- package/lib/input/file-search.d.ts.map +1 -0
- package/lib/input/file-search.js +665 -0
- package/lib/input/file-search.js.map +1 -0
- package/lib/input/fuzzy.d.ts +42 -0
- package/lib/input/fuzzy.d.ts.map +1 -0
- package/lib/input/fuzzy.js +289 -0
- package/lib/input/fuzzy.js.map +1 -0
- package/lib/input/ghost.d.ts +46 -0
- package/lib/input/ghost.d.ts.map +1 -0
- package/lib/input/ghost.js +58 -0
- package/lib/input/ghost.js.map +1 -0
- package/lib/input/keymap.d.ts +3 -1
- package/lib/input/keymap.d.ts.map +1 -1
- package/lib/input/keymap.js +16 -3
- package/lib/input/keymap.js.map +1 -1
- package/lib/input/match.d.ts +12 -4
- package/lib/input/match.d.ts.map +1 -1
- package/lib/input/match.js +50 -51
- package/lib/input/match.js.map +1 -1
- package/lib/input/submission.d.ts +34 -1
- package/lib/input/submission.d.ts.map +1 -1
- package/lib/input/submission.js +33 -5
- package/lib/input/submission.js.map +1 -1
- package/lib/keys-command.d.ts +19 -3
- package/lib/keys-command.d.ts.map +1 -1
- package/lib/keys-command.js +34 -31
- package/lib/keys-command.js.map +1 -1
- package/lib/settings-notice.d.ts +5 -0
- package/lib/settings-notice.d.ts.map +1 -1
- package/lib/settings-notice.js +9 -7
- package/lib/settings-notice.js.map +1 -1
- package/lib/stash/lock.d.ts +72 -0
- package/lib/stash/lock.d.ts.map +1 -0
- package/lib/stash/lock.js +374 -0
- package/lib/stash/lock.js.map +1 -0
- package/lib/stash/paths.d.ts +22 -0
- package/lib/stash/paths.d.ts.map +1 -0
- package/lib/stash/paths.js +84 -0
- package/lib/stash/paths.js.map +1 -0
- package/lib/stash/private-fs.d.ts +61 -0
- package/lib/stash/private-fs.d.ts.map +1 -0
- package/lib/stash/private-fs.js +379 -0
- package/lib/stash/private-fs.js.map +1 -0
- package/lib/stash/schema.d.ts +56 -0
- package/lib/stash/schema.d.ts.map +1 -0
- package/lib/stash/schema.js +108 -0
- package/lib/stash/schema.js.map +1 -0
- package/lib/stash/store.d.ts +92 -0
- package/lib/stash/store.d.ts.map +1 -0
- package/lib/stash/store.js +262 -0
- package/lib/stash/store.js.map +1 -0
- package/lib/stash.d.ts +122 -0
- package/lib/stash.d.ts.map +1 -0
- package/lib/stash.js +353 -0
- package/lib/stash.js.map +1 -0
- package/lib/terminal/external-editor.d.ts +93 -0
- package/lib/terminal/external-editor.d.ts.map +1 -0
- package/lib/terminal/external-editor.js +263 -0
- package/lib/terminal/external-editor.js.map +1 -0
- package/lib/terminal/host-writes.d.ts +39 -0
- package/lib/terminal/host-writes.d.ts.map +1 -0
- package/lib/terminal/host-writes.js +136 -0
- package/lib/terminal/host-writes.js.map +1 -0
- package/lib/terminal/signals.d.ts +25 -0
- package/lib/terminal/signals.d.ts.map +1 -0
- package/lib/terminal/signals.js +39 -0
- package/lib/terminal/signals.js.map +1 -0
- package/lib/terminal/warning-screen.d.ts +19 -1
- package/lib/terminal/warning-screen.d.ts.map +1 -1
- package/lib/terminal/warning-screen.js +37 -1
- package/lib/terminal/warning-screen.js.map +1 -1
- package/lib/terminal-text.d.ts +72 -0
- package/lib/terminal-text.d.ts.map +1 -0
- package/lib/terminal-text.js +603 -0
- package/lib/terminal-text.js.map +1 -0
- package/lib/text.d.ts +45 -6
- package/lib/text.d.ts.map +1 -1
- package/lib/text.js +106 -46
- package/lib/text.js.map +1 -1
- package/lib/theme-command.d.ts +7 -4
- package/lib/theme-command.d.ts.map +1 -1
- package/lib/theme-command.js +32 -12
- package/lib/theme-command.js.map +1 -1
- package/lib/theme-files.d.ts +110 -0
- package/lib/theme-files.d.ts.map +1 -0
- package/lib/theme-files.js +352 -0
- package/lib/theme-files.js.map +1 -0
- package/lib/theme-schema.d.ts +121 -0
- package/lib/theme-schema.d.ts.map +1 -0
- package/lib/theme-schema.js +87 -0
- package/lib/theme-schema.js.map +1 -0
- package/lib/theme-settings.d.ts +72 -10
- package/lib/theme-settings.d.ts.map +1 -1
- package/lib/theme-settings.js +162 -45
- package/lib/theme-settings.js.map +1 -1
- package/lib/theme-tokens.d.ts +39 -7
- package/lib/theme-tokens.d.ts.map +1 -1
- package/lib/theme-tokens.js +104 -13
- package/lib/theme-tokens.js.map +1 -1
- package/lib/theme.d.ts +24 -2
- package/lib/theme.d.ts.map +1 -1
- package/lib/theme.js +49 -6
- package/lib/theme.js.map +1 -1
- package/lib/todo-guard.d.ts +116 -0
- package/lib/todo-guard.d.ts.map +1 -0
- package/lib/todo-guard.js +259 -0
- package/lib/todo-guard.js.map +1 -0
- package/lib/tool-display.d.ts +57 -0
- package/lib/tool-display.d.ts.map +1 -0
- package/lib/tool-display.js +51 -0
- package/lib/tool-display.js.map +1 -0
- package/lib/transcript.d.ts +28 -0
- package/lib/transcript.d.ts.map +1 -1
- package/lib/transcript.js +126 -37
- package/lib/transcript.js.map +1 -1
- package/lib/ui/diff.d.ts +49 -0
- package/lib/ui/diff.d.ts.map +1 -0
- package/lib/ui/diff.js +208 -0
- package/lib/ui/diff.js.map +1 -0
- package/lib/ui/dock.d.ts.map +1 -1
- package/lib/ui/dock.js +9 -6
- package/lib/ui/dock.js.map +1 -1
- package/lib/ui/editor.d.ts +41 -1
- package/lib/ui/editor.d.ts.map +1 -1
- package/lib/ui/editor.js +100 -4
- package/lib/ui/editor.js.map +1 -1
- package/lib/ui/frame.d.ts +16 -5
- package/lib/ui/frame.d.ts.map +1 -1
- package/lib/ui/frame.js +32 -13
- package/lib/ui/frame.js.map +1 -1
- package/lib/ui/history-picker.d.ts +15 -0
- package/lib/ui/history-picker.d.ts.map +1 -0
- package/lib/ui/history-picker.js +26 -0
- package/lib/ui/history-picker.js.map +1 -0
- package/lib/ui/keymap-picker.d.ts +16 -0
- package/lib/ui/keymap-picker.d.ts.map +1 -0
- package/lib/ui/keymap-picker.js +41 -0
- package/lib/ui/keymap-picker.js.map +1 -0
- package/lib/ui/layout.d.ts +33 -0
- package/lib/ui/layout.d.ts.map +1 -0
- package/lib/ui/layout.js +33 -0
- package/lib/ui/layout.js.map +1 -0
- package/lib/ui/markdown.d.ts +25 -3
- package/lib/ui/markdown.d.ts.map +1 -1
- package/lib/ui/markdown.js +12 -8
- package/lib/ui/markdown.js.map +1 -1
- package/lib/ui/picker-card.d.ts +42 -0
- package/lib/ui/picker-card.d.ts.map +1 -0
- package/lib/ui/picker-card.js +148 -0
- package/lib/ui/picker-card.js.map +1 -0
- package/lib/ui/picker.d.ts +50 -2
- package/lib/ui/picker.d.ts.map +1 -1
- package/lib/ui/picker.js +66 -7
- package/lib/ui/picker.js.map +1 -1
- package/lib/ui/prompt.d.ts +12 -2
- package/lib/ui/prompt.d.ts.map +1 -1
- package/lib/ui/prompt.js +25 -2
- package/lib/ui/prompt.js.map +1 -1
- package/lib/ui/rows.d.ts +8 -6
- package/lib/ui/rows.d.ts.map +1 -1
- package/lib/ui/rows.js +10 -18
- package/lib/ui/rows.js.map +1 -1
- package/lib/ui/stash-picker.d.ts +43 -0
- package/lib/ui/stash-picker.d.ts.map +1 -0
- package/lib/ui/stash-picker.js +78 -0
- package/lib/ui/stash-picker.js.map +1 -0
- package/lib/ui/status.d.ts +2 -0
- package/lib/ui/status.d.ts.map +1 -1
- package/lib/ui/status.js +25 -16
- package/lib/ui/status.js.map +1 -1
- package/lib/ui/theme-picker.d.ts +18 -0
- package/lib/ui/theme-picker.d.ts.map +1 -0
- package/lib/ui/theme-picker.js +68 -0
- package/lib/ui/theme-picker.js.map +1 -0
- package/lib/ui/view.d.ts +146 -22
- package/lib/ui/view.d.ts.map +1 -1
- package/lib/ui/view.js +461 -154
- package/lib/ui/view.js.map +1 -1
- package/lib/work.d.ts +3 -1
- package/lib/work.d.ts.map +1 -1
- package/lib/work.js +9 -1
- package/lib/work.js.map +1 -1
- package/package.json +7 -1
- package/themes/deepseek-blue.yaml +183 -0
- package/themes/violet-orbit.yaml +178 -0
|
@@ -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,56 @@
|
|
|
1
|
+
export declare const STASH_SCHEMA_VERSION = 2;
|
|
2
|
+
/** A stash longer than this is refused rather than stored. */
|
|
3
|
+
export declare const MAX_STASH_ENTRY_BYTES = 1048576;
|
|
4
|
+
export declare function isSafeEntryId(value: string): boolean;
|
|
5
|
+
export declare function assertSafeEntryId(value: string): void;
|
|
6
|
+
/** Refuse one draft that no amount of storage capacity could justify holding. */
|
|
7
|
+
export declare function assertSafeStashText(text: string): void;
|
|
8
|
+
/** The form of a draft that is safe to hand back to a terminal. */
|
|
9
|
+
export declare function sanitizeStashText(text: string): string;
|
|
10
|
+
export interface StashEntry {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly text: string;
|
|
13
|
+
readonly createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
export interface StashFile {
|
|
16
|
+
readonly version: typeof STASH_SCHEMA_VERSION;
|
|
17
|
+
/**
|
|
18
|
+
* The exact session this bank belongs to.
|
|
19
|
+
*
|
|
20
|
+
* The key alone cannot prove ownership: it is a flattened label that two
|
|
21
|
+
* sessions could once have shared. The exact id is compared on every read, so
|
|
22
|
+
* a file that belongs to another session is quarantined rather than read or
|
|
23
|
+
* deleted as this one's.
|
|
24
|
+
*/
|
|
25
|
+
readonly sessionId: string;
|
|
26
|
+
readonly createdAt: number;
|
|
27
|
+
readonly updatedAt: number;
|
|
28
|
+
/** Newest first: `entries[0]` is `stash@{0}`. */
|
|
29
|
+
readonly entries: readonly StashEntry[];
|
|
30
|
+
}
|
|
31
|
+
export type Clock = () => number;
|
|
32
|
+
export declare const createNewId: () => string;
|
|
33
|
+
export declare function createEmptyStashFile(sessionId: string, now: number): StashFile;
|
|
34
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
35
|
+
export declare function normalizeEntry(raw: unknown): StashEntry | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Read one stash file, or nothing when it is not this surface's current format.
|
|
38
|
+
*
|
|
39
|
+
* Strict on purpose: a duplicate id, a missing timestamp, or a foreign session
|
|
40
|
+
* is corruption the store quarantines rather than a shape to repair, because a
|
|
41
|
+
* silently repaired file can resurrect entries the reader thought they dropped —
|
|
42
|
+
* or hand another session's drafts to this one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseStashFile(raw: unknown): StashFile | undefined;
|
|
45
|
+
export interface ResolvedEntry {
|
|
46
|
+
readonly entry: StashEntry;
|
|
47
|
+
readonly index: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a selector against newest-first entries.
|
|
51
|
+
*
|
|
52
|
+
* Index 0 is the newest, mirroring git's index-0-is-tip convention. A selector
|
|
53
|
+
* is an entry id, a numeric index string, or empty for the most recent entry.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveBySelector(entries: readonly StashEntry[], selector: string | undefined): ResolvedEntry | undefined;
|
|
56
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/stash/schema.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAErC,8DAA8D;AAC9D,eAAO,MAAM,qBAAqB,UAAY,CAAA;AAQ9C,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,SAAS,EAAE,MAAM,CAAA;IAC1B,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,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAE9E;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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Stash entry + file schema and pure parsing/validation helpers.
|
|
2
|
+
//
|
|
3
|
+
// Every disk format is validated on read, so a corrupt or hand-edited stash file
|
|
4
|
+
// can never crash the editor: an unparseable file is treated as empty and
|
|
5
|
+
// quarantined by the store. Keeping this module free of fs dependencies makes
|
|
6
|
+
// the schema fully unit-testable.
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { stripControlCharacters } from "../text.js";
|
|
9
|
+
export const STASH_SCHEMA_VERSION = 2;
|
|
10
|
+
/** A stash longer than this is refused rather than stored. */
|
|
11
|
+
export const MAX_STASH_ENTRY_BYTES = 1_048_576;
|
|
12
|
+
const ENTRY_ID_MAX_LENGTH = 128;
|
|
13
|
+
const ENTRY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
14
|
+
const NUMERIC_SELECTOR_PATTERN = /^\d+$/u;
|
|
15
|
+
const INVALID_ENTRY_ID_MESSAGE = 'invalid stash entry id';
|
|
16
|
+
const ENTRY_TOO_LARGE_MESSAGE = 'stashed draft is too large';
|
|
17
|
+
export function isSafeEntryId(value) {
|
|
18
|
+
return value.length <= ENTRY_ID_MAX_LENGTH && ENTRY_ID_PATTERN.test(value);
|
|
19
|
+
}
|
|
20
|
+
export function assertSafeEntryId(value) {
|
|
21
|
+
if (!isSafeEntryId(value))
|
|
22
|
+
throw new Error(INVALID_ENTRY_ID_MESSAGE);
|
|
23
|
+
}
|
|
24
|
+
/** Refuse one draft that no amount of storage capacity could justify holding. */
|
|
25
|
+
export function assertSafeStashText(text) {
|
|
26
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_STASH_ENTRY_BYTES)
|
|
27
|
+
throw new Error(ENTRY_TOO_LARGE_MESSAGE);
|
|
28
|
+
}
|
|
29
|
+
/** The form of a draft that is safe to hand back to a terminal. */
|
|
30
|
+
export function sanitizeStashText(text) {
|
|
31
|
+
return stripControlCharacters(text);
|
|
32
|
+
}
|
|
33
|
+
export const createNewId = () => randomUUID();
|
|
34
|
+
export function createEmptyStashFile(sessionId, now) {
|
|
35
|
+
return { version: STASH_SCHEMA_VERSION, sessionId, createdAt: now, updatedAt: now, entries: [] };
|
|
36
|
+
}
|
|
37
|
+
export function isRecord(value) {
|
|
38
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function isValidTimestamp(value) {
|
|
41
|
+
return typeof value === 'number' && Number.isFinite(value) && !Number.isNaN(new Date(value).getTime());
|
|
42
|
+
}
|
|
43
|
+
export function normalizeEntry(raw) {
|
|
44
|
+
if (!isRecord(raw))
|
|
45
|
+
return undefined;
|
|
46
|
+
const { id, text, createdAt } = raw;
|
|
47
|
+
if (typeof id !== 'string' || !isSafeEntryId(id))
|
|
48
|
+
return undefined;
|
|
49
|
+
if (typeof text !== 'string')
|
|
50
|
+
return undefined;
|
|
51
|
+
if (!isValidTimestamp(createdAt))
|
|
52
|
+
return undefined;
|
|
53
|
+
return { id, text: sanitizeStashText(text), createdAt };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read one stash file, or nothing when it is not this surface's current format.
|
|
57
|
+
*
|
|
58
|
+
* Strict on purpose: a duplicate id, a missing timestamp, or a foreign session
|
|
59
|
+
* is corruption the store quarantines rather than a shape to repair, because a
|
|
60
|
+
* silently repaired file can resurrect entries the reader thought they dropped —
|
|
61
|
+
* or hand another session's drafts to this one.
|
|
62
|
+
*/
|
|
63
|
+
export function parseStashFile(raw) {
|
|
64
|
+
if (!isRecord(raw) || raw.version !== STASH_SCHEMA_VERSION)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (typeof raw.sessionId !== 'string')
|
|
67
|
+
return undefined;
|
|
68
|
+
if (!isValidTimestamp(raw.createdAt) || !isValidTimestamp(raw.updatedAt))
|
|
69
|
+
return undefined;
|
|
70
|
+
if (!Array.isArray(raw.entries))
|
|
71
|
+
return undefined;
|
|
72
|
+
const entries = [];
|
|
73
|
+
const ids = new Set();
|
|
74
|
+
for (const rawEntry of raw.entries) {
|
|
75
|
+
const entry = normalizeEntry(rawEntry);
|
|
76
|
+
if (entry === undefined || ids.has(entry.id))
|
|
77
|
+
return undefined;
|
|
78
|
+
ids.add(entry.id);
|
|
79
|
+
entries.push(entry);
|
|
80
|
+
}
|
|
81
|
+
return { version: STASH_SCHEMA_VERSION, sessionId: raw.sessionId, createdAt: raw.createdAt, updatedAt: raw.updatedAt, entries };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a selector against newest-first entries.
|
|
85
|
+
*
|
|
86
|
+
* Index 0 is the newest, mirroring git's index-0-is-tip convention. A selector
|
|
87
|
+
* is an entry id, a numeric index string, or empty for the most recent entry.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveBySelector(entries, selector) {
|
|
90
|
+
if (entries.length === 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
const trimmed = selector?.trim();
|
|
93
|
+
if (trimmed === undefined || trimmed === '') {
|
|
94
|
+
const entry = entries[0];
|
|
95
|
+
return entry === undefined ? undefined : { entry, index: 0 };
|
|
96
|
+
}
|
|
97
|
+
const index = entries.findIndex(entry => entry.id === trimmed);
|
|
98
|
+
if (index >= 0)
|
|
99
|
+
return { entry: entries[index], index };
|
|
100
|
+
if (!NUMERIC_SELECTOR_PATTERN.test(trimmed))
|
|
101
|
+
return undefined;
|
|
102
|
+
const numeric = Number(trimmed);
|
|
103
|
+
if (!Number.isSafeInteger(numeric) || numeric >= entries.length)
|
|
104
|
+
return undefined;
|
|
105
|
+
const entry = entries[numeric];
|
|
106
|
+
return entry === undefined ? undefined : { entry, index: numeric };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/stash/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { rename } from 'node:fs/promises';
|
|
2
|
+
import { type StashFailurePhase } from './lock.ts';
|
|
3
|
+
import type { StashPaths } from './paths.ts';
|
|
4
|
+
import { syncDirectoryEntry } from './private-fs.ts';
|
|
5
|
+
import { type Clock, type ResolvedEntry, type StashEntry, type StashFile } from './schema.ts';
|
|
6
|
+
/**
|
|
7
|
+
* A stash file larger than this is refused, never quarantined: hiding a
|
|
8
|
+
* legitimate bank because it grew past a cap would be worse than refusing it.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_STASH_FILE_BYTES = 16777216;
|
|
11
|
+
/**
|
|
12
|
+
* A bank that cannot be written because it would no longer be readable.
|
|
13
|
+
*
|
|
14
|
+
* The cap is only useful if it is enforced before the write: a file that grows
|
|
15
|
+
* past it is refused by every later read, so letting one save through would turn
|
|
16
|
+
* a full bank into an unreadable one.
|
|
17
|
+
*/
|
|
18
|
+
export declare class StashFileTooLargeError extends Error {
|
|
19
|
+
readonly bytes: number;
|
|
20
|
+
constructor(bytes: number);
|
|
21
|
+
}
|
|
22
|
+
export interface AddEntryInput {
|
|
23
|
+
readonly text: string;
|
|
24
|
+
/** Caller-supplied id; generated when omitted. */
|
|
25
|
+
readonly id?: string;
|
|
26
|
+
/** Caller-supplied timestamp, used only when a recovery path restores an entry. */
|
|
27
|
+
readonly createdAt?: number;
|
|
28
|
+
}
|
|
29
|
+
type LoadResult = {
|
|
30
|
+
kind: 'ready';
|
|
31
|
+
file: StashFile;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'corrupt';
|
|
34
|
+
quarantinedTo: string | undefined;
|
|
35
|
+
quarantineSyncFailed: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'unsupported';
|
|
38
|
+
version: number;
|
|
39
|
+
};
|
|
40
|
+
/** Where a read moved an unreadable bank, and whether that move is durable. */
|
|
41
|
+
export interface QuarantineRecord {
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly syncFailed: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface StashWriteOutcome {
|
|
46
|
+
readonly committed: true;
|
|
47
|
+
readonly phase: StashFailurePhase;
|
|
48
|
+
readonly error: unknown;
|
|
49
|
+
}
|
|
50
|
+
export type StashWriter = (filePath: string, file: StashFile, syncDirectory?: typeof syncDirectoryEntry, replaceFile?: typeof rename) => Promise<void | StashWriteOutcome>;
|
|
51
|
+
/** Stash data written by a newer build, which this one must not touch. */
|
|
52
|
+
export declare class UnsupportedStashSchemaError extends Error {
|
|
53
|
+
readonly detectedVersion: number;
|
|
54
|
+
readonly supportedVersion: number;
|
|
55
|
+
constructor(detectedVersion: number, supportedVersion?: number);
|
|
56
|
+
}
|
|
57
|
+
export declare class StashStore {
|
|
58
|
+
private file;
|
|
59
|
+
private corruptRecovery;
|
|
60
|
+
private readonly filePath;
|
|
61
|
+
private readonly paths;
|
|
62
|
+
private readonly now;
|
|
63
|
+
private readonly write;
|
|
64
|
+
constructor(paths: StashPaths, loaded: LoadResult, now?: Clock, write?: StashWriter);
|
|
65
|
+
get entries(): readonly StashEntry[];
|
|
66
|
+
get entryCount(): number;
|
|
67
|
+
/** The recovery location the last read produced, if any, reported once. */
|
|
68
|
+
takeQuarantine(): QuarantineRecord | undefined;
|
|
69
|
+
/** Resolve a selector against the entries currently held. */
|
|
70
|
+
find(selector: string | undefined): ResolvedEntry | undefined;
|
|
71
|
+
refresh(): Promise<void>;
|
|
72
|
+
add(input: AddEntryInput): Promise<ResolvedEntry>;
|
|
73
|
+
/** Remove one entry by its exact id, reporting what was removed. */
|
|
74
|
+
removeById(id: string): Promise<ResolvedEntry | undefined>;
|
|
75
|
+
/** Remove the entry a selector names, reporting what was removed. */
|
|
76
|
+
drop(selector: string | undefined): Promise<ResolvedEntry | undefined>;
|
|
77
|
+
/**
|
|
78
|
+
* Remove drafts, reporting how many were dropped.
|
|
79
|
+
*
|
|
80
|
+
* Ids restrict the removal to exactly what a reader confirmed. Clearing
|
|
81
|
+
* whatever is on disk instead would delete drafts another surface added while
|
|
82
|
+
* the confirmation was on screen, which is a loss nobody agreed to.
|
|
83
|
+
*/
|
|
84
|
+
clear(ids?: readonly string[]): Promise<number>;
|
|
85
|
+
private removeAt;
|
|
86
|
+
private persistMutation;
|
|
87
|
+
private reloadFresh;
|
|
88
|
+
}
|
|
89
|
+
export declare function loadStashStore(paths: StashPaths, now?: Clock, write?: StashWriter): Promise<StashStore>;
|
|
90
|
+
export declare function writeStashFile(filePath: string, file: StashFile, syncDirectory?: typeof syncDirectoryEntry, replaceFile?: typeof rename): Promise<void | StashWriteOutcome>;
|
|
91
|
+
export {};
|
|
92
|
+
//# sourceMappingURL=store.d.ts.map
|