@itookit/dsht 0.3.0 → 0.3.3

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 (134) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +39 -23
  3. package/README.zh.md +40 -24
  4. package/dist/catalog/controller.d.ts +32 -0
  5. package/dist/catalog/controller.js +88 -0
  6. package/dist/catalog/index.d.ts +2 -0
  7. package/dist/catalog/index.js +2 -0
  8. package/dist/{cli.js → cli/index.js} +42 -29
  9. package/dist/controller/connection.d.ts +80 -0
  10. package/dist/controller/connection.js +190 -0
  11. package/dist/controller/controller.d.ts +269 -0
  12. package/dist/controller/controller.js +372 -0
  13. package/dist/controller/index.d.ts +5 -0
  14. package/dist/controller/index.js +3 -0
  15. package/dist/controller/memory-log.d.ts +35 -0
  16. package/dist/controller/memory-log.js +95 -0
  17. package/dist/cost/config.d.ts +17 -0
  18. package/dist/cost/config.js +68 -0
  19. package/dist/cost/controller.d.ts +41 -0
  20. package/dist/cost/controller.js +115 -0
  21. package/dist/cost/index.d.ts +10 -0
  22. package/dist/cost/index.js +8 -0
  23. package/dist/cost/ledger-files.d.ts +16 -0
  24. package/dist/cost/ledger-files.js +103 -0
  25. package/dist/cost/ledger.d.ts +78 -0
  26. package/dist/cost/ledger.js +146 -0
  27. package/dist/cost/pricing.d.ts +86 -0
  28. package/dist/cost/pricing.js +223 -0
  29. package/dist/cost/records.d.ts +17 -0
  30. package/dist/cost/records.js +79 -0
  31. package/dist/cost/scanner.d.ts +22 -0
  32. package/dist/cost/scanner.js +95 -0
  33. package/dist/cost/types.d.ts +85 -0
  34. package/dist/cost/types.js +7 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +2 -0
  37. package/dist/session/connection-view.d.ts +18 -0
  38. package/dist/session/connection-view.js +1 -0
  39. package/dist/{controller.d.ts → session/controller.d.ts} +102 -136
  40. package/dist/session/controller.js +616 -0
  41. package/dist/session/export-html.d.ts +9 -0
  42. package/dist/session/export-html.js +39 -0
  43. package/dist/{export.d.ts → session/export.d.ts} +1 -1
  44. package/dist/{export.js → session/export.js} +6 -20
  45. package/dist/{history.d.ts → session/history.d.ts} +17 -0
  46. package/dist/{history.js → session/history.js} +163 -2
  47. package/dist/session/index.d.ts +17 -0
  48. package/dist/session/index.js +10 -0
  49. package/dist/session/markdown.d.ts +39 -0
  50. package/dist/session/markdown.js +255 -0
  51. package/dist/session/math.d.ts +11 -0
  52. package/dist/session/math.js +82 -0
  53. package/dist/session/navigation.d.ts +37 -0
  54. package/dist/session/navigation.js +84 -0
  55. package/dist/{references.js → session/references.js} +1 -1
  56. package/dist/{telemetry.d.ts → session/telemetry.d.ts} +1 -1
  57. package/dist/{telemetry.js → session/telemetry.js} +1 -1
  58. package/dist/{transcript.d.ts → session/transcript.d.ts} +65 -1
  59. package/dist/{transcript.js → session/transcript.js} +141 -20
  60. package/dist/session/types.d.ts +18 -0
  61. package/dist/session/types.js +2 -0
  62. package/dist/state.d.ts +41 -0
  63. package/dist/state.js +9 -0
  64. package/dist/storage/directories.d.ts +14 -0
  65. package/dist/storage/directories.js +24 -0
  66. package/dist/storage/files.d.ts +51 -0
  67. package/dist/storage/files.js +152 -0
  68. package/dist/storage/heap-snapshot.d.ts +19 -0
  69. package/dist/storage/heap-snapshot.js +29 -0
  70. package/dist/storage/index.d.ts +4 -0
  71. package/dist/storage/index.js +4 -0
  72. package/dist/transport/auth.js +65 -0
  73. package/dist/transport/host.d.ts +13 -0
  74. package/dist/transport/host.js +1 -0
  75. package/dist/ui/app.d.ts +11 -0
  76. package/dist/ui/app.js +790 -0
  77. package/dist/ui/chat/header.d.ts +11 -0
  78. package/dist/ui/chat/header.js +14 -0
  79. package/dist/ui/chat/history-view.d.ts +12 -0
  80. package/dist/ui/chat/history-view.js +17 -0
  81. package/dist/ui/chat/status.d.ts +91 -0
  82. package/dist/ui/chat/status.js +386 -0
  83. package/dist/ui/chat/viewport.d.ts +17 -0
  84. package/dist/ui/chat/viewport.js +14 -0
  85. package/dist/ui/commands/parse.d.ts +99 -0
  86. package/dist/ui/commands/parse.js +126 -0
  87. package/dist/ui/commands/registry.d.ts +33 -0
  88. package/dist/ui/commands/registry.js +73 -0
  89. package/dist/ui/copy-mode.d.ts +4 -0
  90. package/dist/ui/copy-mode.js +6 -0
  91. package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
  92. package/dist/{cost-view.js → ui/dialogs/cost.js} +6 -6
  93. package/dist/ui/dialogs/index.d.ts +120 -0
  94. package/dist/ui/dialogs/index.js +113 -0
  95. package/dist/ui/dialogs/picker.d.ts +18 -0
  96. package/dist/ui/dialogs/picker.js +38 -0
  97. package/dist/ui/frozen.d.ts +8 -0
  98. package/dist/ui/frozen.js +7 -0
  99. package/dist/ui/input/references.d.ts +12 -0
  100. package/dist/ui/input/references.js +15 -0
  101. package/dist/ui/mount.d.ts +6 -0
  102. package/dist/ui/mount.js +11 -0
  103. package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
  104. package/package.json +19 -13
  105. package/dist/app.d.ts +0 -21
  106. package/dist/app.js +0 -805
  107. package/dist/auth.js +0 -108
  108. package/dist/controller.js +0 -961
  109. package/dist/cost.d.ts +0 -119
  110. package/dist/cost.js +0 -313
  111. package/dist/history-view.d.ts +0 -8
  112. package/dist/history-view.js +0 -12
  113. package/dist/navigation.d.ts +0 -11
  114. package/dist/navigation.js +0 -36
  115. package/dist/status.d.ts +0 -28
  116. package/dist/status.js +0 -157
  117. /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
  118. /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
  119. /package/dist/{memory.js → session/memory.js} +0 -0
  120. /package/dist/{references.d.ts → session/references.d.ts} +0 -0
  121. /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
  122. /package/dist/{client.d.ts → transport/client.d.ts} +0 -0
  123. /package/dist/{client.js → transport/client.js} +0 -0
  124. /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
  125. /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
  126. /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
  127. /package/dist/{wire.js → transport/wire.js} +0 -0
  128. /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
  129. /package/dist/{input-history.js → ui/input/history.js} +0 -0
  130. /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
  131. /package/dist/{input.js → ui/input/input.js} +0 -0
  132. /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
  133. /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
  134. /package/dist/{theme.js → ui/theme/index.js} +0 -0
@@ -0,0 +1,41 @@
1
+ /** Application state shared by the controller facade and every domain controller. */
2
+ import { Transcript } from './session/transcript.ts';
3
+ import type { HistorySearch, RemovalTarget } from './session/types.ts';
4
+ import type { ObjectValue } from './transport/wire.ts';
5
+ /** State shared by the picker and conversation view. */
6
+ export interface State {
7
+ version: number;
8
+ online: boolean;
9
+ busy: boolean;
10
+ screen: 'workspaces' | 'sessions' | 'chat' | 'path';
11
+ status: string;
12
+ error: string;
13
+ workspaces: ObjectValue[];
14
+ sessions: ObjectValue[];
15
+ showAllSessions: boolean;
16
+ workspaceId?: string;
17
+ sessionId?: string;
18
+ pending: ObjectValue[];
19
+ controlError?: string;
20
+ modelError?: string;
21
+ presetError?: string;
22
+ presets?: ObjectValue[];
23
+ defaultModel?: ObjectValue;
24
+ transcript: Transcript;
25
+ }
26
+ /** The state contract every domain controller writes through. */
27
+ export interface ControllerStore {
28
+ /** Current immutable state snapshot. */
29
+ readonly state: State;
30
+ /** Publish a state patch and notify observers. */
31
+ update(patch: Partial<State>): void;
32
+ /** Monotonic selector generation; a changed value discards in-flight session work. */
33
+ selection(): number;
34
+ /** Advance the selector generation when the selected workspace or session changes. */
35
+ bumpSelection(): void;
36
+ }
37
+ /** Build the initial state before any connection exists.
38
+ * @returns A fresh state whose transcript is empty and disconnected.
39
+ */
40
+ export declare function initialState(): State;
41
+ export type { HistorySearch, RemovalTarget };
package/dist/state.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Application state shared by the controller facade and every domain controller. */
2
+ import { Transcript } from "./session/transcript.js";
3
+ /** Build the initial state before any connection exists.
4
+ * @returns A fresh state whose transcript is empty and disconnected.
5
+ */
6
+ export function initialState() {
7
+ return { version: 0, online: false, busy: false, screen: 'workspaces', status: 'Connecting…',
8
+ error: '', workspaces: [], sessions: [], showAllSessions: false, pending: [], transcript: new Transcript() };
9
+ }
@@ -0,0 +1,14 @@
1
+ /** Create a directory tree with owner-only permissions, leaving an existing directory as it is.
2
+ * @param path - Directory path.
3
+ */
4
+ export declare function ensureDirectory(path: string): Promise<void>;
5
+ /** Create a directory tree and require it to stay owner-only.
6
+ * @param path - Directory path.
7
+ * @param label - Name used in the validation error, such as `Cookie directory`.
8
+ */
9
+ export declare function ensurePrivateDirectory(path: string, label: string): Promise<void>;
10
+ /** List the entry names of one directory.
11
+ * @param path - Directory path.
12
+ * @returns Entry names in filesystem order.
13
+ */
14
+ export declare function listEntries(path: string): Promise<string[]>;
@@ -0,0 +1,24 @@
1
+ /** Private directory creation and listing for the client's configuration and state. */
2
+ import { lstat, mkdir, readdir } from 'node:fs/promises';
3
+ /** Create a directory tree with owner-only permissions, leaving an existing directory as it is.
4
+ * @param path - Directory path.
5
+ */
6
+ export async function ensureDirectory(path) {
7
+ await mkdir(path, { recursive: true, mode: 0o700 });
8
+ }
9
+ /** Create a directory tree and require it to stay owner-only.
10
+ * @param path - Directory path.
11
+ * @param label - Name used in the validation error, such as `Cookie directory`.
12
+ */
13
+ export async function ensurePrivateDirectory(path, label) {
14
+ await ensureDirectory(path);
15
+ const stat = await lstat(path);
16
+ if (!stat.isDirectory() || process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
17
+ throw new Error(`${label} must be owned by the current user with permissions 0700`);
18
+ }
19
+ }
20
+ /** List the entry names of one directory.
21
+ * @param path - Directory path.
22
+ * @returns Entry names in filesystem order.
23
+ */
24
+ export async function listEntries(path) { return readdir(path); }
@@ -0,0 +1,51 @@
1
+ /** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
2
+ * @param path - Absolute file path.
3
+ * @returns File contents, or undefined when the file does not exist.
4
+ */
5
+ export declare function readText(path: string): Promise<string | undefined>;
6
+ /** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
7
+ * @param path - Absolute file path.
8
+ * @param label - Name used in the validation error, such as `Cookie file`.
9
+ * @returns File contents, or undefined when the file does not exist.
10
+ */
11
+ export declare function readPrivateFile(path: string, label: string): Promise<string | undefined>;
12
+ /** Move one private file within its directory, ignoring a source that is already gone.
13
+ *
14
+ * Used to set an unreadable file aside under a new name without deleting the bytes. A missing source
15
+ * is not an error: the caller listed the directory earlier and another process may have removed it.
16
+ * @param from - Existing path.
17
+ * @param to - Destination path, replaced when it already exists.
18
+ */
19
+ export declare function renameFile(from: string, to: string): Promise<void>;
20
+ /** Replace a file atomically with owner-only contents, leaving no partial file behind.
21
+ * @param path - Destination path.
22
+ * @param contents - Complete file contents.
23
+ */
24
+ export declare function writePrivateFile(path: string, contents: string): Promise<void>;
25
+ /** Create an owner-only file only when it does not exist.
26
+ * @param path - Destination path.
27
+ * @param contents - Complete file contents.
28
+ * @returns False when the file already existed, leaving it untouched.
29
+ */
30
+ export declare function createPrivateFile(path: string, contents: string): Promise<boolean>;
31
+ /** Stream bytes into a new owner-only file, removing it unless the write completed.
32
+ *
33
+ * The destination is created before the source is requested, so an existing file fails without
34
+ * contacting the host, and a source or streaming failure removes the partial file.
35
+ * @param path - Destination path; an existing file is never replaced.
36
+ * @param source - Produces the byte stream once the destination exists.
37
+ * @param signal - Cancels the write and removes the partial file.
38
+ */
39
+ export declare function writeExclusiveStream(path: string, source: () => Promise<AsyncIterable<Uint8Array>>, signal?: AbortSignal): Promise<void>;
40
+ /** Append to an owner-only file, creating it when absent.
41
+ *
42
+ * Appending keeps a long-lived diagnostic file cheap to extend; callers that need a bounded file
43
+ * rewrite it periodically with `writePrivateFile`.
44
+ * @param path - Destination path.
45
+ * @param contents - Text to append.
46
+ */
47
+ export declare function appendPrivateFile(path: string, contents: string): Promise<void>;
48
+ /** Remove a file, treating an already absent file as success.
49
+ * @param path - File to remove.
50
+ */
51
+ export declare function removeFile(path: string): Promise<void>;
@@ -0,0 +1,152 @@
1
+ /** Every filesystem read and write the client performs; no other module opens a file. */
2
+ import { constants } from 'node:fs';
3
+ import { open, readFile, rename, unlink, writeFile } from 'node:fs/promises';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { dirname, join } from 'node:path';
6
+ /** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
7
+ * @param path - Absolute file path.
8
+ * @returns File contents, or undefined when the file does not exist.
9
+ */
10
+ export async function readText(path) {
11
+ try {
12
+ return await readFile(path, 'utf8');
13
+ }
14
+ catch (error) {
15
+ if (isMissing(error))
16
+ return undefined;
17
+ throw error;
18
+ }
19
+ }
20
+ /** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
21
+ * @param path - Absolute file path.
22
+ * @param label - Name used in the validation error, such as `Cookie file`.
23
+ * @returns File contents, or undefined when the file does not exist.
24
+ */
25
+ export async function readPrivateFile(path, label) {
26
+ let handle;
27
+ try {
28
+ handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
29
+ }
30
+ catch (error) {
31
+ if (isMissing(error))
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ try {
36
+ const stat = await handle.stat();
37
+ if (!stat.isFile() || process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
38
+ throw new Error(`${label} must be owned by the current user with permissions 0600`);
39
+ }
40
+ return await handle.readFile('utf8');
41
+ }
42
+ finally {
43
+ await handle.close();
44
+ }
45
+ }
46
+ /** Move one private file within its directory, ignoring a source that is already gone.
47
+ *
48
+ * Used to set an unreadable file aside under a new name without deleting the bytes. A missing source
49
+ * is not an error: the caller listed the directory earlier and another process may have removed it.
50
+ * @param from - Existing path.
51
+ * @param to - Destination path, replaced when it already exists.
52
+ */
53
+ export async function renameFile(from, to) {
54
+ try {
55
+ await rename(from, to);
56
+ }
57
+ catch (error) {
58
+ if (isMissing(error))
59
+ return;
60
+ throw error;
61
+ }
62
+ }
63
+ /** Replace a file atomically with owner-only contents, leaving no partial file behind.
64
+ * @param path - Destination path.
65
+ * @param contents - Complete file contents.
66
+ */
67
+ export async function writePrivateFile(path, contents) {
68
+ const temporary = join(dirname(path), `${randomUUID()}.tmp`);
69
+ try {
70
+ await writeFile(temporary, contents, { mode: 0o600, flag: 'wx' });
71
+ await rename(temporary, path);
72
+ }
73
+ finally {
74
+ await removeFile(temporary);
75
+ }
76
+ }
77
+ /** Create an owner-only file only when it does not exist.
78
+ * @param path - Destination path.
79
+ * @param contents - Complete file contents.
80
+ * @returns False when the file already existed, leaving it untouched.
81
+ */
82
+ export async function createPrivateFile(path, contents) {
83
+ try {
84
+ await writeFile(path, contents, { mode: 0o600, flag: 'wx' });
85
+ return true;
86
+ }
87
+ catch (error) {
88
+ if (isCode(error, 'EEXIST'))
89
+ return false;
90
+ throw error;
91
+ }
92
+ }
93
+ /** Stream bytes into a new owner-only file, removing it unless the write completed.
94
+ *
95
+ * The destination is created before the source is requested, so an existing file fails without
96
+ * contacting the host, and a source or streaming failure removes the partial file.
97
+ * @param path - Destination path; an existing file is never replaced.
98
+ * @param source - Produces the byte stream once the destination exists.
99
+ * @param signal - Cancels the write and removes the partial file.
100
+ */
101
+ export async function writeExclusiveStream(path, source, signal) {
102
+ const file = await open(path, 'wx', 0o600);
103
+ let complete = false;
104
+ try {
105
+ await file.writeFile(await source(), { signal });
106
+ signal?.throwIfAborted();
107
+ complete = true;
108
+ }
109
+ finally {
110
+ try {
111
+ await file.close();
112
+ }
113
+ finally {
114
+ if (!complete)
115
+ await removeFile(path);
116
+ }
117
+ }
118
+ }
119
+ /** Append to an owner-only file, creating it when absent.
120
+ *
121
+ * Appending keeps a long-lived diagnostic file cheap to extend; callers that need a bounded file
122
+ * rewrite it periodically with `writePrivateFile`.
123
+ * @param path - Destination path.
124
+ * @param contents - Text to append.
125
+ */
126
+ export async function appendPrivateFile(path, contents) {
127
+ const handle = await open(path, 'a', 0o600);
128
+ try {
129
+ await handle.writeFile(contents);
130
+ }
131
+ finally {
132
+ await handle.close();
133
+ }
134
+ }
135
+ /** Remove a file, treating an already absent file as success.
136
+ * @param path - File to remove.
137
+ */
138
+ export async function removeFile(path) {
139
+ try {
140
+ await unlink(path);
141
+ }
142
+ catch (error) {
143
+ if (!isMissing(error))
144
+ throw error;
145
+ }
146
+ }
147
+ /** Recognize one error code without assuming the error's prototype. */
148
+ function isCode(error, code) {
149
+ return error instanceof Error && 'code' in error && error.code === code;
150
+ }
151
+ /** Whether an operation failed because the path does not exist. */
152
+ function isMissing(error) { return isCode(error, 'ENOENT'); }
@@ -0,0 +1,19 @@
1
+ /** Build a snapshot filename from a sampling-point tag and a time.
2
+ *
3
+ * The tag is a user label such as `after-stress`; everything that could steer the write outside the
4
+ * destination directory is folded into a dash, so a tag can never become a path.
5
+ * @param tag - Sampling-point label.
6
+ * @param time - Epoch milliseconds that keeps repeated snapshots distinct.
7
+ * @returns A `.heapsnapshot` filename that names one file in one directory.
8
+ */
9
+ export declare function heapSnapshotName(tag: string, time: number): string;
10
+ /** Write a V8 heap snapshot into one directory.
11
+ *
12
+ * V8 serializes the whole heap synchronously, so the client pauses while the file is written; the
13
+ * result is the artifact DevTools records, meant for offline leak analysis rather than display.
14
+ * @param directory - Destination directory.
15
+ * @param tag - Sampling-point label naming the file.
16
+ * @param write - Snapshot writer; injectable so tests do not have to serialize a heap.
17
+ * @returns Absolute path of the written snapshot.
18
+ */
19
+ export declare function writeHeapSnapshot(directory: string, tag?: string, write?: (file: string) => string): string;
@@ -0,0 +1,29 @@
1
+ /** V8 heap snapshots written to the working directory so memory growth can be inspected offline. */
2
+ import { join } from 'node:path';
3
+ import { writeHeapSnapshot as writeV8HeapSnapshot } from 'node:v8';
4
+ /** Longest sampling-point tag kept in a snapshot filename. */
5
+ const TAG_LIMIT = 48;
6
+ /** Build a snapshot filename from a sampling-point tag and a time.
7
+ *
8
+ * The tag is a user label such as `after-stress`; everything that could steer the write outside the
9
+ * destination directory is folded into a dash, so a tag can never become a path.
10
+ * @param tag - Sampling-point label.
11
+ * @param time - Epoch milliseconds that keeps repeated snapshots distinct.
12
+ * @returns A `.heapsnapshot` filename that names one file in one directory.
13
+ */
14
+ export function heapSnapshotName(tag, time) {
15
+ const safe = tag.trim().replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[.-]+/, '').slice(0, TAG_LIMIT).replace(/[.-]+$/, '');
16
+ return `${safe || 'snapshot'}-${time}.heapsnapshot`;
17
+ }
18
+ /** Write a V8 heap snapshot into one directory.
19
+ *
20
+ * V8 serializes the whole heap synchronously, so the client pauses while the file is written; the
21
+ * result is the artifact DevTools records, meant for offline leak analysis rather than display.
22
+ * @param directory - Destination directory.
23
+ * @param tag - Sampling-point label naming the file.
24
+ * @param write - Snapshot writer; injectable so tests do not have to serialize a heap.
25
+ * @returns Absolute path of the written snapshot.
26
+ */
27
+ export function writeHeapSnapshot(directory, tag = 'snapshot', write = writeV8HeapSnapshot) {
28
+ return write(join(directory, heapSnapshotName(tag, Date.now())));
29
+ }
@@ -0,0 +1,4 @@
1
+ /** Storage domain: every filesystem operation the client performs. */
2
+ export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, renameFile, writeExclusiveStream, writePrivateFile } from './files.ts';
3
+ export { ensureDirectory, ensurePrivateDirectory, listEntries } from './directories.ts';
4
+ export { heapSnapshotName, writeHeapSnapshot } from './heap-snapshot.ts';
@@ -0,0 +1,4 @@
1
+ /** Storage domain: every filesystem operation the client performs. */
2
+ export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, renameFile, writeExclusiveStream, writePrivateFile } from "./files.js";
3
+ export { ensureDirectory, ensurePrivateDirectory, listEntries } from "./directories.js";
4
+ export { heapSnapshotName, writeHeapSnapshot } from "./heap-snapshot.js";
@@ -0,0 +1,65 @@
1
+ /** Origin-scoped cookie persistence; launch tokens are never written to disk. */
2
+ import { createHash } from 'node:crypto';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { ensurePrivateDirectory, readPrivateFile, writePrivateFile } from "../storage/index.js";
6
+ import { HttpError } from "./client.js";
7
+ /** Fatal authentication failure requiring a new startup token. */
8
+ export class AuthenticationRequired extends Error {
9
+ }
10
+ /** Private, atomic cookie files, separated by the complete HTTP origin. */
11
+ export class CookieStore {
12
+ directory;
13
+ constructor(directory = process.env.DSHT_AUTH_DIR
14
+ ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'dsht', 'auth')) {
15
+ this.directory = directory;
16
+ }
17
+ /** Read an unexpired cookie, rejecting unsafe permissions or malformed storage. */
18
+ async load(origin) {
19
+ const raw = await readPrivateFile(this.path(origin), 'Cookie file');
20
+ if (raw === undefined)
21
+ return undefined;
22
+ let saved;
23
+ try {
24
+ saved = JSON.parse(raw);
25
+ }
26
+ catch {
27
+ throw new Error('Invalid cookie file JSON. Remove the origin cookie file and log in again.');
28
+ }
29
+ if (saved === null || typeof saved !== 'object' || !('version' in saved) || saved.version !== 1
30
+ || !('origin' in saved) || saved.origin !== origin || !('cookie' in saved) || typeof saved.cookie !== 'string'
31
+ || !('expiresAt' in saved) || typeof saved.expiresAt !== 'number' || !Number.isSafeInteger(saved.expiresAt)) {
32
+ throw new Error('Invalid saved cookie. Remove the origin cookie file and log in again.');
33
+ }
34
+ return saved.expiresAt > Date.now() ? saved.cookie : undefined;
35
+ }
36
+ /** Atomically save a persistent host cookie with owner-only POSIX permissions. */
37
+ async save(origin, cookie, expiresAt) {
38
+ await ensurePrivateDirectory(this.directory, 'Cookie directory');
39
+ await writePrivateFile(this.path(origin), JSON.stringify({ version: 1, origin, cookie, expiresAt }) + '\n');
40
+ }
41
+ path(origin) {
42
+ return join(this.directory, `${createHash('sha256').update(origin).digest('hex')}.json`);
43
+ }
44
+ }
45
+ /** Reuse a validated cookie first; only authentication rejection permits token fallback. */
46
+ export async function login(client, token, store) {
47
+ const cookie = await store.load(client.base.origin);
48
+ if (cookie) {
49
+ client.restoreCookie(cookie);
50
+ try {
51
+ await client.call('session/list', { _request: {} });
52
+ return;
53
+ }
54
+ catch (error) {
55
+ if (!(error instanceof HttpError) || error.status !== 401)
56
+ throw error;
57
+ }
58
+ }
59
+ if (!token)
60
+ throw new AuthenticationRequired('Login required: export DSH_TOKEN, or export DSH_URL as the URL printed by dsh web');
61
+ await client.authenticate(token);
62
+ const session = client.persistentCookie;
63
+ if (session)
64
+ await store.save(client.base.origin, session.cookie, session.expiresAt);
65
+ }
@@ -0,0 +1,13 @@
1
+ /** Host access contract every domain controller uses for the active connection generation. */
2
+ import type { Client } from './client.ts';
3
+ /** One authenticated host connection, as seen by a domain that never owns it. */
4
+ export interface HostAccess {
5
+ /** Connected client, or undefined while offline or between generations. */
6
+ client(): Client | undefined;
7
+ /** Connected client, or a `Not connected` failure. */
8
+ require(): Client;
9
+ /** Whether the current generation has applied its host baseline. */
10
+ online(): boolean;
11
+ /** Client lifetime signal, aborted when the process closes the connection. */
12
+ signal(): AbortSignal;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { type Theme } from './theme/index.ts';
2
+ import { Controller } from '../controller/controller.ts';
3
+ /** The caller owns starting and stopping the controller around the Ink render lifetime.
4
+ * @param props - Controller, panel lifetime and semantic theme.
5
+ * @returns The rendered terminal interface.
6
+ */
7
+ export declare function App({ controller, panelLifetimeMs, theme }: {
8
+ controller: Controller;
9
+ panelLifetimeMs?: number;
10
+ theme?: Theme;
11
+ }): import("react").JSX.Element;