@chance722/dsh-inbox 0.1.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 +21 -0
- package/README.md +182 -0
- package/README.zh.md +182 -0
- package/cordis.patch.yml +11 -0
- package/lib/cli.js +259 -0
- package/lib/client.js +3823 -0
- package/lib/index.js +3718 -0
- package/lib/types/cli.d.ts +49 -0
- package/lib/types/client/card.d.ts +52 -0
- package/lib/types/client/dock.d.ts +52 -0
- package/lib/types/client/heading.d.ts +54 -0
- package/lib/types/client/index.d.ts +24 -0
- package/lib/types/client/manual.d.ts +22 -0
- package/lib/types/client/scheme.d.ts +47 -0
- package/lib/types/host/capture.d.ts +117 -0
- package/lib/types/host/classify/model.d.ts +112 -0
- package/lib/types/host/classify/redact.d.ts +18 -0
- package/lib/types/host/classify/rules.d.ts +58 -0
- package/lib/types/host/command.d.ts +18 -0
- package/lib/types/host/crypto/secret-box.d.ts +60 -0
- package/lib/types/host/index.d.ts +21 -0
- package/lib/types/host/link-title.d.ts +64 -0
- package/lib/types/host/remote/auto-push.d.ts +37 -0
- package/lib/types/host/remote/merge.d.ts +80 -0
- package/lib/types/host/remote/pull.d.ts +60 -0
- package/lib/types/host/remote/push.d.ts +101 -0
- package/lib/types/host/remote/remove.d.ts +65 -0
- package/lib/types/host/remote/writer.d.ts +43 -0
- package/lib/types/host/rpc.d.ts +36 -0
- package/lib/types/host/s3/client.d.ts +216 -0
- package/lib/types/host/s3/probe.d.ts +28 -0
- package/lib/types/host/tools.d.ts +56 -0
- package/lib/types/host/ui/config.d.ts +54 -0
- package/lib/types/host/vault/lease.d.ts +54 -0
- package/lib/types/host/vault/query.d.ts +38 -0
- package/lib/types/host/vault/spec.d.ts +193 -0
- package/lib/types/host/vault/vault.d.ts +269 -0
- package/lib/types/host/webdav/client.d.ts +102 -0
- package/lib/types/host/webdav/config.d.ts +129 -0
- package/lib/types/host/webdav/probe.d.ts +22 -0
- package/lib/types/host/webdav/run.d.ts +34 -0
- package/lib/types/shared/constants.d.ts +23 -0
- package/lib/types/shared/panel-wire.d.ts +474 -0
- package/lib/types/shared/vocabulary.d.ts +39 -0
- package/package.json +101 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The box a credential's text is kept in.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here decides *when* to seal — that is the vault's job (`vault.ts`) —
|
|
5
|
+
* this module is only the arithmetic: derive a key from a password, seal a
|
|
6
|
+
* string, open it again, and say clearly when the envelope is not ours.
|
|
7
|
+
*
|
|
8
|
+
* Deliberate choices:
|
|
9
|
+
* - **scrypt**, not a plain hash: a master password is a person's password, and
|
|
10
|
+
* the cost of guessing it has to be paid per attempt.
|
|
11
|
+
* - **AES-256-GCM**, not CBC: a credential whose bytes were altered should fail
|
|
12
|
+
* to open, loudly, rather than decrypt into plausible garbage.
|
|
13
|
+
* - the envelope names its own version, so a later format can be read *and*
|
|
14
|
+
* told apart from this one.
|
|
15
|
+
*/
|
|
16
|
+
/** Work factors, recorded beside the salt so a future change can still open old boxes. */
|
|
17
|
+
export interface KdfParams {
|
|
18
|
+
n: number;
|
|
19
|
+
r: number;
|
|
20
|
+
p: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 2^15 · r8 · p1: ~100ms and ~33MB on the development machine — slow enough to
|
|
24
|
+
* be a speed bump for a guesser, fast enough that unlocking does not feel broken.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_KDF: KdfParams;
|
|
27
|
+
/** A fresh salt, one per master password. */
|
|
28
|
+
export declare function newSalt(): Buffer;
|
|
29
|
+
/**
|
|
30
|
+
* Derive the sealing key from a master password.
|
|
31
|
+
*
|
|
32
|
+
* The password is normalised first: the same characters typed on another
|
|
33
|
+
* keyboard (or pasted from a password manager) must derive the same key.
|
|
34
|
+
*
|
|
35
|
+
* @param password - what the user typed.
|
|
36
|
+
* @param salt - the salt stored beside the KDF parameters.
|
|
37
|
+
* @param kdf - the recorded work factors.
|
|
38
|
+
* @returns the 32-byte key.
|
|
39
|
+
*/
|
|
40
|
+
export declare function deriveKey(password: string, salt: Buffer, kdf?: KdfParams): Buffer;
|
|
41
|
+
/**
|
|
42
|
+
* Seal one string.
|
|
43
|
+
*
|
|
44
|
+
* @param key - a key from {@link deriveKey}.
|
|
45
|
+
* @param plaintext - what must not be readable on disk.
|
|
46
|
+
* @returns the envelope to store: `v1:iv:tag:ciphertext`, all base64.
|
|
47
|
+
*/
|
|
48
|
+
export declare function seal(key: Buffer, plaintext: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Open one envelope.
|
|
51
|
+
*
|
|
52
|
+
* Every way of failing — a wrong password, a truncated field, a tampered byte —
|
|
53
|
+
* comes back as `undefined`. Callers cannot tell them apart, and should not:
|
|
54
|
+
* the only useful question is "can this be read with the key I have".
|
|
55
|
+
*
|
|
56
|
+
* @param key - a key from {@link deriveKey}.
|
|
57
|
+
* @param envelope - what {@link seal} returned.
|
|
58
|
+
* @returns the plaintext, or undefined when this key cannot open it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function open(key: Buffer, envelope: string): string | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
/** Stable Cordis plugin name for the host half. */
|
|
3
|
+
export declare const name = "dsh-inbox";
|
|
4
|
+
/** Tool registry, command surface, and the storage domain form we persist through. */
|
|
5
|
+
export declare const inject: string[];
|
|
6
|
+
/**
|
|
7
|
+
* Claim the vault and publish the tools.
|
|
8
|
+
*
|
|
9
|
+
* Loading is asynchronous while `apply` is not, so the handle arrives later:
|
|
10
|
+
* every caller reads whatever is open at call time, and reports the failure
|
|
11
|
+
* instead of pretending the vault is empty when it is not.
|
|
12
|
+
*
|
|
13
|
+
* The claim goes through `./vault/lease.js` because dsh loads this plugin twice
|
|
14
|
+
* in one process (profile bundle + agent preset) while the storage domain
|
|
15
|
+
* allows a single open per name — the second instance used to fail with
|
|
16
|
+
* `domain 'dsh_inbox' is already open` and every tool it published answered
|
|
17
|
+
* 「仓库没有打开」. The first instance owns the vault; the rest borrow it.
|
|
18
|
+
*
|
|
19
|
+
* @param ctx - host plugin context carrying the tool registry and storage.
|
|
20
|
+
*/
|
|
21
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headline behind a link, fetched from the page itself.
|
|
3
|
+
*
|
|
4
|
+
* A pasted article arrives as a URL, and a list of URLs is a list of things you
|
|
5
|
+
* cannot tell apart — while the page carries a headline that names it perfectly.
|
|
6
|
+
* Reading `<title>` costs **no model tokens**: it is one plain HTTP GET through
|
|
7
|
+
* the harness's own web seam (`ctx.web`), which pins public addresses, refuses
|
|
8
|
+
* private ones, follows only same-origin redirects, and caps both time and size.
|
|
9
|
+
* It runs after the record is stored, never in front of the paste.
|
|
10
|
+
*
|
|
11
|
+
* Three rules keep it honest:
|
|
12
|
+
* - it never writes `title`. That field is the user's own word, and the
|
|
13
|
+
* credentials rule depends on it staying that way (`AGENTS.md` 3); the fetched
|
|
14
|
+
* headline lives in `linkTitle` instead;
|
|
15
|
+
* - it writes nothing when the record already has a name, and nothing when the
|
|
16
|
+
* user renamed it while the fetch was in flight;
|
|
17
|
+
* - it never turns a failure into a problem: a dead link, a slow host or a page
|
|
18
|
+
* that is not HTML simply leaves the URL as the name, which is where this
|
|
19
|
+
* started.
|
|
20
|
+
*/
|
|
21
|
+
import type { WebFetchResult } from '@deepseek-ai/dsh-web';
|
|
22
|
+
import type { Vault } from './vault/vault.js';
|
|
23
|
+
/** Ceiling on a stored headline. The card clamps far earlier (24 chars + …). */
|
|
24
|
+
export declare const MAX_LINK_TITLE_CHARS = 200;
|
|
25
|
+
/**
|
|
26
|
+
* The slice of `ctx.web` this module uses.
|
|
27
|
+
*
|
|
28
|
+
* Structural on purpose: the service belongs to `@deepseek-ai/dsh-web`, but this
|
|
29
|
+
* half only ever calls one method of it, and a profile without that seam should
|
|
30
|
+
* simply not get headlines rather than fail to load.
|
|
31
|
+
*/
|
|
32
|
+
export interface WebFetchSeam {
|
|
33
|
+
fetch(request: {
|
|
34
|
+
readonly url: string;
|
|
35
|
+
}, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pull the `<title>` out of an HTML document.
|
|
39
|
+
*
|
|
40
|
+
* A regex over the raw text, not a parser: we want one string out of a page we
|
|
41
|
+
* are never going to render, and pulling an HTML parser into the host half for
|
|
42
|
+
* that would be a dependency bought for nothing.
|
|
43
|
+
*
|
|
44
|
+
* @param html - the decoded document.
|
|
45
|
+
* @returns the collapsed, decoded headline, or undefined when there is none.
|
|
46
|
+
*/
|
|
47
|
+
export declare function titleFromHtml(html: string): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Fetch one link's headline and store it, if it is still worth storing.
|
|
50
|
+
*
|
|
51
|
+
* The record is re-read here rather than taken from the caller: a caller's copy
|
|
52
|
+
* is by definition the one from *before* the network, and a record that is
|
|
53
|
+
* already named (or already gone) must not cost a request at all.
|
|
54
|
+
*
|
|
55
|
+
* @param vault - the open vault.
|
|
56
|
+
* @param id - the record that was just filed.
|
|
57
|
+
* @param web - `ctx.web`, the harness's own web seam.
|
|
58
|
+
* @param log - where a miss is explained. Silent failure is undebuggable: the
|
|
59
|
+
* user sees a URL where a headline should be and has nowhere to look. Only the
|
|
60
|
+
* **host** is logged, never the whole URL — a link can carry a token in its
|
|
61
|
+
* query string, and a log file is not the place for one.
|
|
62
|
+
* @returns the stored headline, or undefined when nothing was stored.
|
|
63
|
+
*/
|
|
64
|
+
export declare function fetchLinkTitle(vault: Vault, id: string, web: WebFetchSeam, log?: (message: string) => void): Promise<string | undefined>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pushing by itself, a few seconds after the user stops typing.
|
|
3
|
+
*
|
|
4
|
+
* The rule the user asked for: 入库后防抖自动推 — file something, and a moment
|
|
5
|
+
* later it is in the cloud, without anyone pressing anything. The debounce is
|
|
6
|
+
* what makes that bearable for the remote and for the user: pasting five things
|
|
7
|
+
* in a row is *one* push five seconds after the last one, not five pushes.
|
|
8
|
+
*
|
|
9
|
+
* Its failures are deliberately quiet. A background nicety must never turn a
|
|
10
|
+
* successful paste into an error toast, and the panel's 刷新 button reports the
|
|
11
|
+
* same push when the user presses it — so a broken remote is still visible the
|
|
12
|
+
* moment anyone looks.
|
|
13
|
+
*/
|
|
14
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { Vault } from '../vault/vault.js';
|
|
17
|
+
/** How long the vault stays quiet before it pushes. */
|
|
18
|
+
export declare const AUTO_PUSH_DELAY_MS = 5000;
|
|
19
|
+
/** The real push, injectable so the debounce can be tested without a server. */
|
|
20
|
+
export type PushNow = () => Promise<unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Queue a push for a moment from now, replacing any push already queued.
|
|
23
|
+
*
|
|
24
|
+
* @param run - what to run; the real one is {@link makeAutoPush}.
|
|
25
|
+
*/
|
|
26
|
+
export declare function scheduleAutoPush(run: PushNow): void;
|
|
27
|
+
/** Forget a queued push; used by tests and by a clean shutdown. */
|
|
28
|
+
export declare function cancelAutoPush(): void;
|
|
29
|
+
/**
|
|
30
|
+
* The push the debouncer runs, wired to a real vault.
|
|
31
|
+
*
|
|
32
|
+
* @param ctx - host context carrying settings and credentials.
|
|
33
|
+
* @param vault - the open vault.
|
|
34
|
+
* @param attachments - where attachment bytes live.
|
|
35
|
+
* @returns a function that pushes once.
|
|
36
|
+
*/
|
|
37
|
+
export declare function makeAutoPush(ctx: Context, vault: () => Vault | undefined, attachments: () => AttachmentStore | undefined): PushNow;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bringing other devices' records into this one.
|
|
3
|
+
*
|
|
4
|
+
* The rules are the user's (2026-09-20): a conflict is settled by `updatedAt` —
|
|
5
|
+
* **newer wins, no conflict copies** — deletes travel as tombstones, and what
|
|
6
|
+
* the push wrote is the source of truth, not the `.txt` beside it (a rendered
|
|
7
|
+
* view has no fields to compare).
|
|
8
|
+
*
|
|
9
|
+
* The objects under `sync/` are ours, which is exactly why the *drop folder*
|
|
10
|
+
* pull skips that whole tree — but "ours" does not mean "written by this
|
|
11
|
+
* machine". A record this device has never seen is simply a local miss.
|
|
12
|
+
*/
|
|
13
|
+
import type { Vault } from '../vault/vault.js';
|
|
14
|
+
import { type AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
/** The merge's admission callbacks, named so the wiring below reads plainly. */
|
|
17
|
+
type Admit = {
|
|
18
|
+
image: (bytes: Uint8Array, mime: string, name: string) => Promise<{
|
|
19
|
+
storeId: string;
|
|
20
|
+
} | undefined>;
|
|
21
|
+
file: (bytes: Uint8Array, name: string) => Promise<{
|
|
22
|
+
storeId: string;
|
|
23
|
+
} | undefined>;
|
|
24
|
+
};
|
|
25
|
+
/** One object in the vault's own tree on the remote. */
|
|
26
|
+
export interface SyncObject {
|
|
27
|
+
path: string;
|
|
28
|
+
lastModified?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The slice of a remote this module reads.
|
|
32
|
+
*
|
|
33
|
+
* Both protocols can answer these two questions (S3 by prefix, WebDAV by
|
|
34
|
+
* `PROPFIND`), and keeping the seam this thin is what lets the merge be tested
|
|
35
|
+
* without either of them.
|
|
36
|
+
*/
|
|
37
|
+
export interface SyncTree {
|
|
38
|
+
list(prefix: string): Promise<SyncObject[]>;
|
|
39
|
+
read(path: string): Promise<Uint8Array>;
|
|
40
|
+
}
|
|
41
|
+
/** What one merge did. */
|
|
42
|
+
export interface MergeOutcome {
|
|
43
|
+
/** Records that were new here, or overwritten because the remote was newer. */
|
|
44
|
+
merged: number;
|
|
45
|
+
/** Records the remote had and this machine already had, newer or equal. */
|
|
46
|
+
kept: number;
|
|
47
|
+
/** Attachment objects pulled down (bytes plus their row). */
|
|
48
|
+
attachments: number;
|
|
49
|
+
/** Things that went wrong, one line each. */
|
|
50
|
+
failures: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Pull other devices' records out of `sync/` and settle them against the local
|
|
54
|
+
* vault, then fetch whatever attachment bytes those records need.
|
|
55
|
+
*
|
|
56
|
+
* @param vault - the open vault.
|
|
57
|
+
* @param tree - the remote, as two questions.
|
|
58
|
+
* @param prefix - the sync root inside the configured directory.
|
|
59
|
+
* @param admit - how bytes become a local attachment (the harness's own
|
|
60
|
+
* admission, so a pulled image is validated exactly like a pasted one).
|
|
61
|
+
* @returns what happened, including the lines worth showing.
|
|
62
|
+
*/
|
|
63
|
+
export declare function mergeOnce(vault: Vault, tree: SyncTree, prefix: string, admit: {
|
|
64
|
+
image: Admit['image'];
|
|
65
|
+
file: Admit['file'];
|
|
66
|
+
}): Promise<MergeOutcome>;
|
|
67
|
+
/**
|
|
68
|
+
* The merge, wired for real: read the configuration, build a tree over `sync/`,
|
|
69
|
+
* and hand it to {@link mergeOnce}.
|
|
70
|
+
*
|
|
71
|
+
* Called by every pull the product has (startup, the panel's refresh, 立即同步) so
|
|
72
|
+
* "another device's records show up" does not depend on which button you press.
|
|
73
|
+
*
|
|
74
|
+
* @param ctx - host context carrying settings and credentials.
|
|
75
|
+
* @param vault - the open vault.
|
|
76
|
+
* @param attachments - where attachment bytes go.
|
|
77
|
+
* @returns the counts and the lines worth showing; never throws.
|
|
78
|
+
*/
|
|
79
|
+
export declare function mergeRemote(ctx: Context, vault: Vault, attachments: AttachmentStore): Promise<MergeOutcome>;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-way ingest, whatever the remote happens to speak.
|
|
3
|
+
*
|
|
4
|
+
* The protocol lives behind `RemoteSource`: WebDAV and S3 each know how to list
|
|
5
|
+
* and read, and everything else — which file is new, what counts as text, how a
|
|
6
|
+
* repeat is merged, what a failure looks like — happens exactly once, here.
|
|
7
|
+
* Two copies of that logic would drift, and the drifting copy would be the one
|
|
8
|
+
* that quietly files something twice.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately one-way. Pulling only, never pushing, is what keeps this simple
|
|
11
|
+
* enough to be trustworthy: there is no merge to get wrong, and the phone never
|
|
12
|
+
* has to understand the vault.
|
|
13
|
+
*/
|
|
14
|
+
import { type AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
15
|
+
import { type PullResult } from '../../shared/panel-wire.js';
|
|
16
|
+
import type { S3Config, S3Deps } from '../s3/client.js';
|
|
17
|
+
import type { Vault } from '../vault/vault.js';
|
|
18
|
+
import { type WebdavDeps } from '../webdav/client.js';
|
|
19
|
+
export type { PullResult } from '../../shared/panel-wire.js';
|
|
20
|
+
/** One thing a remote offers to pull. */
|
|
21
|
+
export interface RemoteEntry {
|
|
22
|
+
/** Remote path or key; also the display name's source. */
|
|
23
|
+
path: string;
|
|
24
|
+
lastModified?: string;
|
|
25
|
+
contentType?: string;
|
|
26
|
+
}
|
|
27
|
+
/** What the ingest needs from a remote, and nothing more. */
|
|
28
|
+
export interface RemoteSource {
|
|
29
|
+
list(): Promise<RemoteEntry[]>;
|
|
30
|
+
read(entry: RemoteEntry): Promise<{
|
|
31
|
+
bytes: Uint8Array;
|
|
32
|
+
contentType: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pull one remote and file everything new.
|
|
37
|
+
*
|
|
38
|
+
* Never throws: a remote that is down must not stop the harness from starting,
|
|
39
|
+
* so every failure becomes a `failed` result the caller can show.
|
|
40
|
+
*
|
|
41
|
+
* @param vault - the open vault.
|
|
42
|
+
* @param source - the protocol-specific read side.
|
|
43
|
+
* @param attachments - the store that owns pulled bytes.
|
|
44
|
+
* @returns what happened.
|
|
45
|
+
*/
|
|
46
|
+
export declare function ingestFrom(vault: Vault, source: RemoteSource, attachments: AttachmentStore): Promise<PullResult>;
|
|
47
|
+
/** What the user configures for WebDAV. */
|
|
48
|
+
export interface WebdavConfig {
|
|
49
|
+
baseUrl: string;
|
|
50
|
+
directory?: string;
|
|
51
|
+
username?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Pull over WebDAV. */
|
|
54
|
+
export declare function pullRemote(vault: Vault, config: WebdavConfig, deps: WebdavDeps & {
|
|
55
|
+
attachments: AttachmentStore;
|
|
56
|
+
}): Promise<PullResult>;
|
|
57
|
+
/** Pull over S3. */
|
|
58
|
+
export declare function pullS3(vault: Vault, config: S3Config, prefix: string, deps: S3Deps & {
|
|
59
|
+
attachments: AttachmentStore;
|
|
60
|
+
}): Promise<PullResult>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sending the vault to the remote.
|
|
3
|
+
*
|
|
4
|
+
* The rules this implements are the ones the user decided on 2026-09-20:
|
|
5
|
+
* **only credential bodies travel encrypted** (they are ciphertext on disk
|
|
6
|
+
* already), everything else goes up as it is; a conflict is settled by writing
|
|
7
|
+
* what is newer; and the layout is `sync/items/<id>.json` plus
|
|
8
|
+
* `sync/attachments/<id>` under the configured directory — one file per object,
|
|
9
|
+
* so "increment" is "whatever got touched since last time".
|
|
10
|
+
*
|
|
11
|
+
* Deliberately *not* here yet: deleting remote objects for records the user
|
|
12
|
+
* emptied out of the bin, and merging what other devices pushed. Those are the
|
|
13
|
+
* next two steps; this one only ever adds and overwrites.
|
|
14
|
+
*/
|
|
15
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
17
|
+
import type { PushResult } from '../../shared/panel-wire.js';
|
|
18
|
+
import type { Attachment, Item } from '../vault/spec.js';
|
|
19
|
+
import type { Vault } from '../vault/vault.js';
|
|
20
|
+
/** What one write to the remote looks like, whichever protocol is configured. */
|
|
21
|
+
type Writer = (path: string, bytes: Uint8Array, contentType: string) => Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* The suffix one attachment should carry.
|
|
24
|
+
*
|
|
25
|
+
* The file's *own* name wins when it has one: a `报税表.xlsx` knows what it is
|
|
26
|
+
* far better than any table here does, and guessing from the media type would
|
|
27
|
+
* hand back `bin` for every format nobody thought to list. The table is the
|
|
28
|
+
* fallback, and `bin` the last resort — a name that says "unknown" rather than a
|
|
29
|
+
* name that lies.
|
|
30
|
+
*
|
|
31
|
+
* @param filename - the name the file arrived with, when there is one.
|
|
32
|
+
* @param mime - its media type.
|
|
33
|
+
* @returns the extension without the dot.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extensionOf(filename: string | undefined, mime: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* The object name one attachment gets on the remote.
|
|
38
|
+
*
|
|
39
|
+
* `<our id>.<extension>`: the id keeps it unique and idempotent, the extension
|
|
40
|
+
* keeps it *readable* — a cloud drive with a folder full of extension-less files
|
|
41
|
+
* cannot preview a photo, cannot open it, and cannot tell you which one is which.
|
|
42
|
+
* The user asked exactly that question ("我的图片呢?都是 json 文件?"), and a bare
|
|
43
|
+
* id was the whole reason.
|
|
44
|
+
*
|
|
45
|
+
* @param attachmentId - our row id for the attachment.
|
|
46
|
+
* @param record - the attachment row, for its own name and media type.
|
|
47
|
+
* @returns the file name to PUT.
|
|
48
|
+
*/
|
|
49
|
+
export declare function attachmentObjectName(attachmentId: string, record: Pick<Attachment, 'mime' | 'filename'>): string;
|
|
50
|
+
/**
|
|
51
|
+
* The record as a text file a person can open in the cloud drive.
|
|
52
|
+
*
|
|
53
|
+
* The JSON beside it is the source of truth — this is a **view**, regenerated on
|
|
54
|
+
* every push and never read back. It exists because a bucket full of
|
|
55
|
+
* `{"format":"dsh-inbox-item/1",…}` is a bucket you cannot *use*: you cannot read
|
|
56
|
+
* the article you saved, you cannot see which photo belongs to which record, and
|
|
57
|
+
* you cannot tell a note from a link without a JSON viewer.
|
|
58
|
+
*
|
|
59
|
+
* A credential's body is the one thing it never contains: it is ciphertext on
|
|
60
|
+
* this machine and it stays ciphertext on the remote, so the text file says so
|
|
61
|
+
* instead of pretending the record is empty.
|
|
62
|
+
*
|
|
63
|
+
* @param item - the record.
|
|
64
|
+
* @param attachmentNames - the remote names of its attachments, in order.
|
|
65
|
+
* @returns the file's text.
|
|
66
|
+
*/
|
|
67
|
+
export declare function renderItemText(item: Item, attachmentNames: readonly string[]): string;
|
|
68
|
+
/**
|
|
69
|
+
* Push everything that changed since the last push.
|
|
70
|
+
*
|
|
71
|
+
* @param ctx - host context carrying settings and credentials.
|
|
72
|
+
* @param vault - the open vault.
|
|
73
|
+
* @param attachments - where attachment bytes live.
|
|
74
|
+
* @returns what happened, including the reasons a caller can show.
|
|
75
|
+
*/
|
|
76
|
+
export declare function pushRemote(ctx: Context, vault: Vault, attachments: AttachmentStore | undefined,
|
|
77
|
+
/** `all` ignores the cursor and sends everything again — the repair button. */
|
|
78
|
+
options?: {
|
|
79
|
+
all?: boolean;
|
|
80
|
+
}): Promise<PushResult>;
|
|
81
|
+
/**
|
|
82
|
+
* The push itself, with the transport injected.
|
|
83
|
+
*
|
|
84
|
+
* Split out the same way the pull is (`pullOnce` behind `pullRemote`), and for
|
|
85
|
+
* the same reason: the interesting rules — what counts as "changed", what
|
|
86
|
+
* happens when one write of twenty fails, that an attachment is uploaded once
|
|
87
|
+
* however many records point at it — are worth testing without a server.
|
|
88
|
+
*
|
|
89
|
+
* @param vault - the open vault.
|
|
90
|
+
* @param attachments - where attachment bytes live.
|
|
91
|
+
* @param writer - one write to the remote.
|
|
92
|
+
* @param basePath - the sync root inside the configured directory.
|
|
93
|
+
* @param options - `all` re-sends records the cursor thinks are already up.
|
|
94
|
+
* @param remover - optional: clears the pre-extension attachment name (see the
|
|
95
|
+
* call site), something only a real remote can answer.
|
|
96
|
+
* @returns what happened.
|
|
97
|
+
*/
|
|
98
|
+
export declare function pushOnce(vault: Vault, attachments: AttachmentStore, writer: Writer, basePath: string, options?: {
|
|
99
|
+
all?: boolean;
|
|
100
|
+
}, remover?: (path: string) => Promise<void>): Promise<PushResult>;
|
|
101
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deleting on the remote what the user deleted here.
|
|
3
|
+
*
|
|
4
|
+
* "Delete" has two very different meanings in this vault, and the merge made the
|
|
5
|
+
* difference visible: a **soft delete** is a tombstone that travels, so other
|
|
6
|
+
* devices learn about it; **emptying the recycle bin** is the user saying "gone",
|
|
7
|
+
* and until this module existed it was only gone locally — the next pull brought
|
|
8
|
+
* it back from the cloud.
|
|
9
|
+
*
|
|
10
|
+
* Deletion is targeted, never a sweep. A blind "delete whatever is not in my
|
|
11
|
+
* vault" pass would remove another device's objects that this one simply has not
|
|
12
|
+
* merged yet, which is data loss dressed as housekeeping.
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import type { Attachment } from '../vault/spec.js';
|
|
16
|
+
/** What the caller removed locally, so the same things can go from the cloud. */
|
|
17
|
+
export interface RemovedRecords {
|
|
18
|
+
/** Record ids that no longer exist locally. */
|
|
19
|
+
itemIds: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* Attachment rows that no local record references any more.
|
|
22
|
+
*
|
|
23
|
+
* The row itself is needed, not just its id: the object's name carries an
|
|
24
|
+
* extension derived from the file's own name and media type.
|
|
25
|
+
*/
|
|
26
|
+
attachments: readonly Attachment[];
|
|
27
|
+
}
|
|
28
|
+
/** What one cleanup did. */
|
|
29
|
+
export interface RemoveOutcome {
|
|
30
|
+
/** Objects deleted from the remote. */
|
|
31
|
+
removed: number;
|
|
32
|
+
/** Things that went wrong, one line each. */
|
|
33
|
+
failures: string[];
|
|
34
|
+
/** Set when there is no remote to talk to — not an error, just nothing to do. */
|
|
35
|
+
skipped?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Remove the remote copies of records the user emptied out of the bin.
|
|
39
|
+
*
|
|
40
|
+
* Three objects per record at most: the machine-readable `.json`, the readable
|
|
41
|
+
* `.txt`, and — for an attachment no other record still points at — its bytes
|
|
42
|
+
* and its metadata row. The extension-less name is attempted too: objects
|
|
43
|
+
* written before attachments carried extensions are exactly this shape, and this
|
|
44
|
+
* is the only pass that will ever clean them up.
|
|
45
|
+
*
|
|
46
|
+
* @param ctx - host context carrying settings and credentials.
|
|
47
|
+
* @param removed - what was just deleted locally.
|
|
48
|
+
* @returns how many objects went, and what could not.
|
|
49
|
+
*/
|
|
50
|
+
export declare function removeRemoteRecords(ctx: Context, removed: RemovedRecords): Promise<RemoveOutcome>;
|
|
51
|
+
/**
|
|
52
|
+
* The deletion itself, with the transport injected.
|
|
53
|
+
|
|
54
|
+
* Split out the way the push and the merge are, so the naming rule — which
|
|
55
|
+
* objects one record owns, and that an attachment's pre-extension name is asked
|
|
56
|
+
* for too — is testable without a server.
|
|
57
|
+
*
|
|
58
|
+
* @param writer - one write/remove pair for the configured remote.
|
|
59
|
+
* @param root - the sync root inside the configured directory.
|
|
60
|
+
* @param removed - what was just deleted locally.
|
|
61
|
+
* @returns how many objects went, and what could not.
|
|
62
|
+
*/
|
|
63
|
+
export declare function removeWith(writer: {
|
|
64
|
+
remove(path: string): Promise<void>;
|
|
65
|
+
}, root: string, removed: RemovedRecords): Promise<RemoveOutcome>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that turns settings into "somewhere to put and remove objects".
|
|
3
|
+
*
|
|
4
|
+
* Push writes, purge deletes, and both need the same three answers: which
|
|
5
|
+
* protocol, which credentials, and where the vault's own tree begins. Building
|
|
6
|
+
* that twice is how the two halves drift — the push root and the merge root
|
|
7
|
+
* disagreeing by one slash would look exactly like "the other device never sent
|
|
8
|
+
* anything", and a purge that deleted from a different prefix would leave
|
|
9
|
+
* everything behind.
|
|
10
|
+
*/
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
12
|
+
import { type WebdavSettings } from '../webdav/config.js';
|
|
13
|
+
/** Everything the vault does to the remote. */
|
|
14
|
+
export interface RemoteWriter {
|
|
15
|
+
write(path: string, bytes: Uint8Array, contentType: string): Promise<void>;
|
|
16
|
+
remove(path: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/** Either a usable writer, or the reason there is none. */
|
|
19
|
+
export type RemoteWriterResult = {
|
|
20
|
+
status: 'ok';
|
|
21
|
+
writer: RemoteWriter;
|
|
22
|
+
root: string;
|
|
23
|
+
} | {
|
|
24
|
+
status: 'unconfigured';
|
|
25
|
+
reason: string;
|
|
26
|
+
} | {
|
|
27
|
+
status: 'failed';
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Where the vault's own objects live inside the configured directory.
|
|
32
|
+
*
|
|
33
|
+
* @param settings - the remote settings.
|
|
34
|
+
* @returns the path prefix, without a trailing slash.
|
|
35
|
+
*/
|
|
36
|
+
export declare function syncRoot(settings: WebdavSettings): string;
|
|
37
|
+
/**
|
|
38
|
+
* Build the writer for whatever this profile is configured with.
|
|
39
|
+
*
|
|
40
|
+
* @param ctx - host context carrying settings and credentials.
|
|
41
|
+
* @returns the writer and the sync root, or why there is none.
|
|
42
|
+
*/
|
|
43
|
+
export declare function remoteWriter(ctx: Context): Promise<RemoteWriterResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The panel's wire: the browser half submits pasted content, pages the vault,
|
|
3
|
+
* edits and deletes records, and streams attachment bytes back.
|
|
4
|
+
*
|
|
5
|
+
* Transport: exact Fetch routes on the shared `/api` channel. Connection
|
|
6
|
+
* applies its Host/Origin trust fence **and** the signed browser cookie before
|
|
7
|
+
* dispatching anything under `/api`, so the vault is exactly as reachable as
|
|
8
|
+
* the rest of the GUI and no further. A bare `webServer` route would instead
|
|
9
|
+
* answer any process on the machine and any page that can issue a simple
|
|
10
|
+
* cross-origin POST — not somewhere to put a vault.
|
|
11
|
+
*
|
|
12
|
+
* Attachment bytes never enter the vault domain. They go through dsh's own
|
|
13
|
+
* attachment store (content-addressed, normalized, never auto-deleted), and the
|
|
14
|
+
* domain keeps the reference plus the metadata we can show without reading
|
|
15
|
+
* bytes back.
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import { type Vault } from './vault/vault.js';
|
|
19
|
+
/** Ceiling on one pasted string, so a runaway paste cannot bloat the domain. */
|
|
20
|
+
export declare const MAX_TEXT_CHARS = 200000;
|
|
21
|
+
/**
|
|
22
|
+
* Register the panel's endpoints.
|
|
23
|
+
*
|
|
24
|
+
* Both services are optional on purpose: a profile without `connection` (the
|
|
25
|
+
* headless development profiles) still loads this plugin, it just has no panel
|
|
26
|
+
* and therefore no endpoints to serve.
|
|
27
|
+
*
|
|
28
|
+
* Captures are serialised through one promise chain. Repeat detection is a
|
|
29
|
+
* read-then-write pass over the domain, and the domain only serialises each
|
|
30
|
+
* individual write — two overlapping submissions of the same link would
|
|
31
|
+
* otherwise both miss the existing record and store it twice.
|
|
32
|
+
*
|
|
33
|
+
* @param ctx - host context; `apply` does not have to await anything.
|
|
34
|
+
* @param vault - reads the currently open vault, which may not be open yet.
|
|
35
|
+
*/
|
|
36
|
+
export declare function registerInboxRpc(ctx: Context, vault: () => Vault | undefined): void;
|