@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,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vault facade: one open domain plus the operations the tools and the panel
|
|
3
|
+
* need. Callers never touch the storage backend directly.
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
import type { Category, CategorySource, Kind, Source } from '../../shared/vocabulary.js';
|
|
7
|
+
import { type ItemQuery } from './query.js';
|
|
8
|
+
/** A capture that needs the key while the vault is locked. */
|
|
9
|
+
export declare class VaultLockedError extends Error {
|
|
10
|
+
constructor();
|
|
11
|
+
}
|
|
12
|
+
import { type Attachment, type Item, type VaultGlobal } from './spec.js';
|
|
13
|
+
/** Everything a caller supplies when filing something new. */
|
|
14
|
+
export interface NewItem {
|
|
15
|
+
kind: Kind;
|
|
16
|
+
category: Category;
|
|
17
|
+
/** Defaults to `rule`: the vault files by rule unless told otherwise. */
|
|
18
|
+
categorySource?: CategorySource;
|
|
19
|
+
source: Source;
|
|
20
|
+
title?: string;
|
|
21
|
+
/** A headline fetched from the link's own page; see `link-title.ts`. */
|
|
22
|
+
linkTitle?: string;
|
|
23
|
+
/** A short code explaining a missed headline fetch (see `link-title.ts`). */
|
|
24
|
+
linkTitleError?: string;
|
|
25
|
+
text?: string;
|
|
26
|
+
/** A sealed credential body, from `sealSecret` (never both with `text`). */
|
|
27
|
+
secret?: string;
|
|
28
|
+
/** The keyed digest that matches a re-paste of the same credential. */
|
|
29
|
+
secretDigest?: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
platform?: string;
|
|
32
|
+
note?: string;
|
|
33
|
+
tags?: readonly string[];
|
|
34
|
+
attachmentIds?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
/** Fields a later edit may replace. Classification and the note are the point. */
|
|
37
|
+
export interface ItemPatch {
|
|
38
|
+
category?: Category;
|
|
39
|
+
/** Set to `user` when the person editing is the one choosing the category. */
|
|
40
|
+
categorySource?: CategorySource;
|
|
41
|
+
watchLater?: boolean;
|
|
42
|
+
title?: string;
|
|
43
|
+
linkTitle?: string;
|
|
44
|
+
/** A short code explaining a missed headline fetch; empty string clears it. */
|
|
45
|
+
linkTitleError?: string;
|
|
46
|
+
note?: string;
|
|
47
|
+
platform?: string;
|
|
48
|
+
tags?: readonly string[];
|
|
49
|
+
attachmentIds?: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
/** Where the vault's key state stands, for the panel to show and the tools to check. */
|
|
52
|
+
export interface VaultLockState {
|
|
53
|
+
/** A master password exists, so credentials can be sealed at all. */
|
|
54
|
+
configured: boolean;
|
|
55
|
+
/** The key is in memory: credentials can be read and written. */
|
|
56
|
+
unlocked: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare class Vault {
|
|
59
|
+
private readonly ctx;
|
|
60
|
+
private readonly domain;
|
|
61
|
+
/** Absolute path of the domain's unit directory, for diagnostics. */
|
|
62
|
+
readonly unit: string;
|
|
63
|
+
private closed;
|
|
64
|
+
/**
|
|
65
|
+
* The derived key, **memory only**.
|
|
66
|
+
*
|
|
67
|
+
* Nothing on disk can be used to read a credential: the master password is
|
|
68
|
+
* never stored, the key is never stored, and a restart therefore locks the
|
|
69
|
+
* vault again. That is the trade the red line asks for ("主密码永不上传"),
|
|
70
|
+
* and it is why the panel has an explicit unlock.
|
|
71
|
+
*/
|
|
72
|
+
private key?;
|
|
73
|
+
private constructor();
|
|
74
|
+
/**
|
|
75
|
+
* Open the vault's domain over whatever backend the composition routed it to.
|
|
76
|
+
*
|
|
77
|
+
* @param ctx - host context carrying the storage domain facility.
|
|
78
|
+
* @param unit - unit location, recorded for diagnostics only.
|
|
79
|
+
* @returns the open vault.
|
|
80
|
+
*/
|
|
81
|
+
static open(ctx: Context, unit?: string): Promise<Vault>;
|
|
82
|
+
/**
|
|
83
|
+
* Bring records written by version 1/2 into the version-3 shape: strip the
|
|
84
|
+
* `待看` tag, because the flag now says the same thing.
|
|
85
|
+
*
|
|
86
|
+
* The old read/unread pair is **not** converted, on purpose. 待看 is the
|
|
87
|
+
* user's own mark, and back-filling it would have flagged twelve records on
|
|
88
|
+
* their behalf at first launch.
|
|
89
|
+
*
|
|
90
|
+
* (A note for whoever reads this next: you cannot even see the old `status`
|
|
91
|
+
* from here. The domain validates on read and zod drops unknown keys, so by
|
|
92
|
+
* the time a record reaches this loop the field is gone — measured, after the
|
|
93
|
+
* first version of this migration silently converted nothing and the rail
|
|
94
|
+
* read 「待看 0」.)
|
|
95
|
+
*/
|
|
96
|
+
private migrateOnce;
|
|
97
|
+
private get items();
|
|
98
|
+
private get attachments();
|
|
99
|
+
/** Where the key state stands; what the panel shows and the tools consult. */
|
|
100
|
+
get lockState(): VaultLockState;
|
|
101
|
+
/**
|
|
102
|
+
* Set (or replace) the master password, and seal everything that needs it.
|
|
103
|
+
*
|
|
104
|
+
* Replacing it is allowed on purpose: a user who wrote the password down
|
|
105
|
+
* badly, or wants a stronger one, must be able to fix that. Records sealed
|
|
106
|
+
* with the *old* password are re-sealed with the new key, which is why the
|
|
107
|
+
* old password has to be supplied again in the panel.
|
|
108
|
+
*
|
|
109
|
+
* @param password - the new master password, never stored anywhere.
|
|
110
|
+
* @returns how many records were sealed in the process.
|
|
111
|
+
*/
|
|
112
|
+
setMasterPassword(password: string): Promise<number>;
|
|
113
|
+
/**
|
|
114
|
+
* Derive the key from the stored salt and check it against the verifier.
|
|
115
|
+
*
|
|
116
|
+
* @param password - what the user typed.
|
|
117
|
+
* @returns true when the vault is now unlocked.
|
|
118
|
+
*/
|
|
119
|
+
unlock(password: string): Promise<boolean>;
|
|
120
|
+
/** Drop the key. Credentials stay on disk, unreadable until the next unlock. */
|
|
121
|
+
lock(): void;
|
|
122
|
+
/**
|
|
123
|
+
* Seal one credential body for storage.
|
|
124
|
+
*
|
|
125
|
+
* @param plaintext - the credential as the user pasted it.
|
|
126
|
+
* @returns the envelope to store, and the keyed digest used for de-duplication.
|
|
127
|
+
*/
|
|
128
|
+
sealSecret(plaintext: string): {
|
|
129
|
+
secret: string;
|
|
130
|
+
secretDigest: string;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* The plaintext behind a credential, when it can be read.
|
|
134
|
+
*
|
|
135
|
+
* @param item - the record.
|
|
136
|
+
* @returns the plaintext, or undefined while locked.
|
|
137
|
+
*/
|
|
138
|
+
secretText(item: Item): string | undefined;
|
|
139
|
+
/**
|
|
140
|
+
* The keyed digest a re-paste is matched against (see `spec.ts`).
|
|
141
|
+
*
|
|
142
|
+
* @param plaintext - the credential as pasted.
|
|
143
|
+
* @returns a hex digest, stable for one vault and useless without its key.
|
|
144
|
+
*/
|
|
145
|
+
digestOf(plaintext: string): string;
|
|
146
|
+
/**
|
|
147
|
+
* Move credentials that are still sitting in `text` into the sealed field.
|
|
148
|
+
*
|
|
149
|
+
* Version-5 vaults kept a credential's body in plain text — that is what this
|
|
150
|
+
* whole change is about — so the first unlock rewrites them. A record that is
|
|
151
|
+
* not a `secret` is left alone, even if it looks like one: the category is the
|
|
152
|
+
* user's own statement about what a record is.
|
|
153
|
+
*
|
|
154
|
+
* @returns how many records were sealed.
|
|
155
|
+
*/
|
|
156
|
+
private sealLegacySecrets;
|
|
157
|
+
/**
|
|
158
|
+
* Re-seal everything that was sealed with the previous key.
|
|
159
|
+
*
|
|
160
|
+
* @param previous - the key in use until a moment ago, if there was one.
|
|
161
|
+
* @returns how many records were re-sealed.
|
|
162
|
+
*/
|
|
163
|
+
private resealSecrets;
|
|
164
|
+
/** Total records held, including soft-deleted ones. */
|
|
165
|
+
get size(): number;
|
|
166
|
+
/**
|
|
167
|
+
* File a new item.
|
|
168
|
+
*
|
|
169
|
+
* @param input - the caller-owned fields; id and timestamps are assigned here.
|
|
170
|
+
* @returns the stored record.
|
|
171
|
+
*/
|
|
172
|
+
create(input: NewItem): Promise<Item>;
|
|
173
|
+
/** Read one live or soft-deleted record. */
|
|
174
|
+
get(id: string): Item | undefined;
|
|
175
|
+
/** Records sitting in the recycle bin, newest first. */
|
|
176
|
+
getBin(): Item[];
|
|
177
|
+
/**
|
|
178
|
+
* List records matching a query, newest first.
|
|
179
|
+
*
|
|
180
|
+
* @param query - filters and paging.
|
|
181
|
+
* @returns matching records.
|
|
182
|
+
*/
|
|
183
|
+
list(query?: ItemQuery): Item[];
|
|
184
|
+
/**
|
|
185
|
+
* Replace editable fields. The atomic read-modify-write keeps concurrent
|
|
186
|
+
* edits from interleaving on the domain's write chain.
|
|
187
|
+
*
|
|
188
|
+
* @param id - record key.
|
|
189
|
+
* @param patch - fields to replace.
|
|
190
|
+
* @returns the stored record.
|
|
191
|
+
*/
|
|
192
|
+
patch(id: string, patch: ItemPatch): Promise<Item>;
|
|
193
|
+
/** Flag or unflag a record for later. */
|
|
194
|
+
setWatchLater(id: string, on?: boolean): Promise<Item>;
|
|
195
|
+
/**
|
|
196
|
+
* Strip one tag from every record that carries it.
|
|
197
|
+
*
|
|
198
|
+
* A tag is not an entity here — it is a word on a record — so "delete this
|
|
199
|
+
* tag" can only mean "take this word off everything". Returns how many
|
|
200
|
+
* records changed, so the panel can say so instead of guessing.
|
|
201
|
+
*
|
|
202
|
+
* @param tag - the exact tag to remove.
|
|
203
|
+
* @returns the number of records that carried it.
|
|
204
|
+
*/
|
|
205
|
+
removeTag(tag: string): Promise<number>;
|
|
206
|
+
/**
|
|
207
|
+
* Soft delete: the record stops appearing in lists but keeps its bytes, so the
|
|
208
|
+
* recycle bin (and restoring) needs no second table.
|
|
209
|
+
*/
|
|
210
|
+
softDelete(id: string): Promise<Item>;
|
|
211
|
+
/** Undo a soft delete. */
|
|
212
|
+
restore(id: string): Promise<Item>;
|
|
213
|
+
/**
|
|
214
|
+
* Delete one record for good, together with its attachment rows.
|
|
215
|
+
*
|
|
216
|
+
* The bytes behind an attachment live in dsh's own store, which never deletes
|
|
217
|
+
* automatically — emptying the recycle bin drops our references, not their
|
|
218
|
+
* objects. A row another record still references is left alone.
|
|
219
|
+
*
|
|
220
|
+
* @param id - record key.
|
|
221
|
+
* @returns whether the record existed.
|
|
222
|
+
*/
|
|
223
|
+
remove(id: string): Promise<boolean>;
|
|
224
|
+
/**
|
|
225
|
+
* Record an attachment's metadata. The bytes stay in the store named by
|
|
226
|
+
* `storeId`; this row is our own index over them, keyed by a generated id
|
|
227
|
+
* because store ids are not path-safe.
|
|
228
|
+
*/
|
|
229
|
+
addAttachment(input: Omit<Attachment, 'id' | 'createdAt'> & {
|
|
230
|
+
createdAt?: string;
|
|
231
|
+
}): Promise<Attachment>;
|
|
232
|
+
/** Read one attachment record. */
|
|
233
|
+
getAttachment(id: string): Attachment | undefined;
|
|
234
|
+
/** Find our index row for a store-side attachment id, if we have one. */
|
|
235
|
+
findAttachmentByStoreId(storeId: string): Attachment | undefined;
|
|
236
|
+
/** Current sync state; M6 writes it. */
|
|
237
|
+
get global(): VaultGlobal;
|
|
238
|
+
/** Replace the global slot. */
|
|
239
|
+
setGlobal(value: VaultGlobal): Promise<void>;
|
|
240
|
+
/** Record today's model-fallback spend. */
|
|
241
|
+
setModelSpend(spend: NonNullable<VaultGlobal['model']>, last?: string): Promise<void>;
|
|
242
|
+
/** Record where the last WebDAV pull got to. */
|
|
243
|
+
setSync(sync: VaultGlobal['sync']): Promise<void>;
|
|
244
|
+
/**
|
|
245
|
+
* Write a record exactly as another device had it.
|
|
246
|
+
*
|
|
247
|
+
* The merge's only write, and deliberately not `create`: an imported record
|
|
248
|
+
* keeps the id other devices already know, the `createdAt` it was made with and
|
|
249
|
+
* the `updatedAt` the conflict was settled on. Re-stamping any of them would
|
|
250
|
+
* make the next pull decide the other way and bounce the record back and forth.
|
|
251
|
+
*
|
|
252
|
+
* @param item - the record, already validated by the merge's own parse.
|
|
253
|
+
* @returns the stored record.
|
|
254
|
+
*/
|
|
255
|
+
import(item: Item): Promise<Item>;
|
|
256
|
+
/**
|
|
257
|
+
* The same for one attachment's row.
|
|
258
|
+
*
|
|
259
|
+
* `storeId` is the *local* store's id for the bytes the merge just admitted;
|
|
260
|
+
* everything else — our row id in particular — travels with the object so the
|
|
261
|
+
* record's `attachmentIds` still point at something.
|
|
262
|
+
*
|
|
263
|
+
* @param record - the row, with a local `storeId`.
|
|
264
|
+
* @returns the stored row.
|
|
265
|
+
*/
|
|
266
|
+
importAttachment(record: Attachment): Promise<Attachment>;
|
|
267
|
+
/** Release the domain handle. Idempotent. */
|
|
268
|
+
close(): Promise<void>;
|
|
269
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest WebDAV surface the inbox needs: list one folder, read one file.
|
|
3
|
+
*
|
|
4
|
+
* No dependency and no XML library: a `PROPFIND` answer is read with two small
|
|
5
|
+
* expressions over the response blocks, because the alternative (a full parser)
|
|
6
|
+
* buys nothing for the two fields we actually use.
|
|
7
|
+
*
|
|
8
|
+
* `fetch` is injected so the whole thing is testable without a server, and the
|
|
9
|
+
* caller never sees bytes it did not ask for.
|
|
10
|
+
*/
|
|
11
|
+
/** Basic-auth credentials, when the server is not anonymous. */
|
|
12
|
+
export interface WebdavAuth {
|
|
13
|
+
username: string;
|
|
14
|
+
password: string;
|
|
15
|
+
}
|
|
16
|
+
/** One file found in a remote folder. */
|
|
17
|
+
export interface RemoteFile {
|
|
18
|
+
/** Absolute path on the server, as the `href` reported it. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** `getlastmodified`, when the server sent one. */
|
|
21
|
+
lastModified?: string;
|
|
22
|
+
/** `getcontenttype`, when the server sent one. */
|
|
23
|
+
contentType?: string;
|
|
24
|
+
}
|
|
25
|
+
/** The subset of `fetch` this module uses, so tests can stand in for it. */
|
|
26
|
+
export interface FetchResponseLike {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
status: number;
|
|
29
|
+
text(): Promise<string>;
|
|
30
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
31
|
+
headers?: {
|
|
32
|
+
get(name: string): string | null;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export type FetchLike = (url: string, init: {
|
|
36
|
+
method: string;
|
|
37
|
+
headers: Record<string, string>;
|
|
38
|
+
body?: string;
|
|
39
|
+
}) => Promise<FetchResponseLike>;
|
|
40
|
+
/** Everything the client needs besides the URL. */
|
|
41
|
+
export interface WebdavDeps {
|
|
42
|
+
fetch: FetchLike;
|
|
43
|
+
auth?: WebdavAuth;
|
|
44
|
+
/** What to send as `User-Agent`; empty falls back to the plugin's own. */
|
|
45
|
+
userAgent?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Basic auth header, or nothing for an anonymous server. */
|
|
48
|
+
export declare function authHeaders(auth?: WebdavAuth): Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* The identity this request presents.
|
|
51
|
+
*
|
|
52
|
+
* Same header, same reason as the S3 side: 数据胶囊 answers a request that
|
|
53
|
+
* does not claim to be the application its access key is bound to with
|
|
54
|
+
* `403 Client type mismatch.`, and nothing in the status code hints at it.
|
|
55
|
+
*
|
|
56
|
+
* @param deps - the client's dependencies.
|
|
57
|
+
* @returns the header to send.
|
|
58
|
+
*/
|
|
59
|
+
export declare function userAgentHeaders(deps: WebdavDeps): Record<string, string>;
|
|
60
|
+
/** One joined URL that keeps the base path the user configured. */
|
|
61
|
+
export declare function joinUrl(base: string, part: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Write one file.
|
|
64
|
+
*
|
|
65
|
+
* WebDAV needs none of the S3 ceremony — a `PUT` with the bytes and the auth
|
|
66
|
+
* header is the whole protocol — which is exactly why the push path can share
|
|
67
|
+
* one interface with the S3 client (`remote/write.ts`).
|
|
68
|
+
*
|
|
69
|
+
* @param baseUrl - the configured base.
|
|
70
|
+
* @param path - the file's path under it.
|
|
71
|
+
* @param bytes - what to store.
|
|
72
|
+
* @param deps - fetch, credentials and the identity to present.
|
|
73
|
+
* @param contentType - what the bytes are.
|
|
74
|
+
*/
|
|
75
|
+
export declare function writeFile(baseUrl: string, path: string, bytes: Uint8Array, deps: WebdavDeps, contentType?: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Remove one file.
|
|
78
|
+
*
|
|
79
|
+
* A 404 counts as success: the object the caller wanted gone is gone. Anything
|
|
80
|
+
* else 4xx/5xx is reported with the server's own words, because a read-only
|
|
81
|
+
* share is a real answer and "删除失败" alone would hide it.
|
|
82
|
+
*
|
|
83
|
+
* @param baseUrl - the configured base.
|
|
84
|
+
* @param path - the file's path under it.
|
|
85
|
+
* @param deps - fetch, credentials and the identity to present.
|
|
86
|
+
*/
|
|
87
|
+
export declare function deleteFile(baseUrl: string, path: string, deps: WebdavDeps): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Read one `PROPFIND` answer into files, skipping the folder entries themselves.
|
|
90
|
+
*
|
|
91
|
+
* @param xml - the response body.
|
|
92
|
+
* @param directory - the requested folder, so its own entry can be dropped.
|
|
93
|
+
* @returns the files the server listed.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseListing(xml: string, directory: string): RemoteFile[];
|
|
96
|
+
/** List the files directly inside one folder. */
|
|
97
|
+
export declare function listFolder(baseUrl: string, directory: string, deps: WebdavDeps): Promise<RemoteFile[]>;
|
|
98
|
+
/** Fetch one file's bytes, with its content type when the server offers one. */
|
|
99
|
+
export declare function readFile(path: string, deps: WebdavDeps, absoluteUrl?: string): Promise<{
|
|
100
|
+
bytes: Uint8Array;
|
|
101
|
+
contentType: string;
|
|
102
|
+
}>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the WebDAV settings live, and why they live there.
|
|
3
|
+
*
|
|
4
|
+
* URL, folder and username are *configuration*: they belong in dsh's settings
|
|
5
|
+
* service, so a deployment can ship defaults and a user can change them from a
|
|
6
|
+
* configuration surface. The password is a *secret*: it goes through the
|
|
7
|
+
* credentials service, which keeps values out of configuration files entirely.
|
|
8
|
+
*
|
|
9
|
+
* Both services are optional. Without them the plugin keeps working from its own
|
|
10
|
+
* composed values — a headless profile has no settings provider and must not
|
|
11
|
+
* break — and the panel simply says what is missing.
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
14
|
+
import z from '@deepseek-ai/schemastery';
|
|
15
|
+
import type { WebdavSettings, WebdavStatus } from '../../shared/panel-wire.js';
|
|
16
|
+
import type { Vault } from '../vault/vault.js';
|
|
17
|
+
export type { WebdavSettings, WebdavStatus } from '../../shared/panel-wire.js';
|
|
18
|
+
/** Namespace this plugin owns in the settings service. */
|
|
19
|
+
export declare const SETTINGS_NAMESPACE = "dsh-inbox-webdav";
|
|
20
|
+
/** The credential key holding the password. */
|
|
21
|
+
export declare const PASSWORD_KEY = "DSH_INBOX_WEBDAV_PASSWORD";
|
|
22
|
+
/** The credential key holding the S3 secret access key. */
|
|
23
|
+
export declare const S3_SECRET_KEY = "DSH_INBOX_S3_SECRET";
|
|
24
|
+
/** The composed defaults, so a fresh install has a sane shape. */
|
|
25
|
+
export declare const DEFAULT_SETTINGS: WebdavSettings;
|
|
26
|
+
/** The namespace's schema: every field optional, so a partial user layer is valid. */
|
|
27
|
+
export declare const WebdavSettingsSchema: z<Schemastery.ObjectS<{
|
|
28
|
+
protocol: z<"webdav" | "s3", "webdav" | "s3">;
|
|
29
|
+
baseUrl: z<string, string>;
|
|
30
|
+
directory: z<string, string>;
|
|
31
|
+
username: z<string, string>;
|
|
32
|
+
endpoint: z<string, string>;
|
|
33
|
+
bucket: z<string, string>;
|
|
34
|
+
region: z<string, string>;
|
|
35
|
+
signatureVersion: z<string, string>;
|
|
36
|
+
accessKeyId: z<string, string>;
|
|
37
|
+
userAgent: z<string, string>;
|
|
38
|
+
webdavUserAgent: z<string, string>;
|
|
39
|
+
}>, Schemastery.ObjectT<{
|
|
40
|
+
protocol: z<"webdav" | "s3", "webdav" | "s3">;
|
|
41
|
+
baseUrl: z<string, string>;
|
|
42
|
+
directory: z<string, string>;
|
|
43
|
+
username: z<string, string>;
|
|
44
|
+
endpoint: z<string, string>;
|
|
45
|
+
bucket: z<string, string>;
|
|
46
|
+
region: z<string, string>;
|
|
47
|
+
signatureVersion: z<string, string>;
|
|
48
|
+
accessKeyId: z<string, string>;
|
|
49
|
+
userAgent: z<string, string>;
|
|
50
|
+
webdavUserAgent: z<string, string>;
|
|
51
|
+
}>>;
|
|
52
|
+
/** Resolve the current settings, falling back to the composed defaults. */
|
|
53
|
+
export declare function readSettings(ctx: Context): WebdavSettings;
|
|
54
|
+
/**
|
|
55
|
+
* Declare the namespace once, at plugin load.
|
|
56
|
+
*
|
|
57
|
+
* Registering lazily on the first save looked harmless and was not: before that
|
|
58
|
+
* first save, `get()` answers undefined for an unregistered namespace, so the
|
|
59
|
+
* panel loaded *defaults* instead of the stored overrides — and the next save
|
|
60
|
+
* wrote those defaults back over the user's real values. Declaring up front is
|
|
61
|
+
* the whole point of a settings namespace.
|
|
62
|
+
*
|
|
63
|
+
* @param ctx - host context.
|
|
64
|
+
* @param base - composed defaults for this deployment.
|
|
65
|
+
*/
|
|
66
|
+
export declare function installWebdavSettings(ctx: Context, base?: WebdavSettings): void;
|
|
67
|
+
/** Read the password, or undefined when none is stored. */
|
|
68
|
+
export declare function readPassword(ctx: Context): Promise<string | undefined>;
|
|
69
|
+
/** Read the S3 secret access key, or undefined when none is stored. */
|
|
70
|
+
export declare function readS3Secret(ctx: Context): Promise<string | undefined>;
|
|
71
|
+
/** Everything the panel needs, without ever handing out the password. */
|
|
72
|
+
export declare function describeWebdav(ctx: Context): Promise<WebdavStatus>;
|
|
73
|
+
/** What a save may change. An absent password leaves the stored one alone. */
|
|
74
|
+
export interface WebdavPatch {
|
|
75
|
+
protocol?: string;
|
|
76
|
+
baseUrl?: string;
|
|
77
|
+
directory?: string;
|
|
78
|
+
username?: string;
|
|
79
|
+
/** Empty string clears the stored password; undefined leaves it. */
|
|
80
|
+
password?: string;
|
|
81
|
+
endpoint?: string;
|
|
82
|
+
bucket?: string;
|
|
83
|
+
region?: string;
|
|
84
|
+
signatureVersion?: string;
|
|
85
|
+
accessKeyId?: string;
|
|
86
|
+
/** Empty string clears the stored S3 secret; undefined leaves it. */
|
|
87
|
+
accessKeySecret?: string;
|
|
88
|
+
/**
|
|
89
|
+
* The `User-Agent` to send for this patch's protocol; empty string means the
|
|
90
|
+
* plugin's own identity. Lands in the protocol's own slot.
|
|
91
|
+
*/
|
|
92
|
+
userAgent?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The client identity configured for whichever protocol is active.
|
|
96
|
+
*
|
|
97
|
+
* Two slots, one answer: the identity is bound to the credential, and each
|
|
98
|
+
* protocol carries its own credential.
|
|
99
|
+
*
|
|
100
|
+
* @param settings - the resolved settings.
|
|
101
|
+
* @returns the `User-Agent` to send, empty when the user configured nothing.
|
|
102
|
+
*/
|
|
103
|
+
export declare function activeUserAgent(settings: WebdavSettings): string;
|
|
104
|
+
/**
|
|
105
|
+
* Accept what people actually type: a bare host is the normal case, and the
|
|
106
|
+
* scheme is the part they should not have to remember. An explicit `http://`
|
|
107
|
+
* survives, because a self-hosted endpoint on a LAN is a real setup.
|
|
108
|
+
*
|
|
109
|
+
* @param value - whatever was typed.
|
|
110
|
+
* @returns the URL to store.
|
|
111
|
+
*/
|
|
112
|
+
export declare function normalizeUrl(value: string): string;
|
|
113
|
+
/**
|
|
114
|
+
* Persist a configuration change.
|
|
115
|
+
*
|
|
116
|
+
* The password never lands in settings: it is written (or cleared) through the
|
|
117
|
+
* credentials service, and a composition without one refuses the write instead
|
|
118
|
+
* of quietly storing a secret somewhere else.
|
|
119
|
+
*
|
|
120
|
+
* @param ctx - host context.
|
|
121
|
+
* @param vault - the open vault, used for the "settings unavailable" fallback.
|
|
122
|
+
* @param base - composed defaults for the settings base layer.
|
|
123
|
+
* @param patch - what to change.
|
|
124
|
+
* @returns whether the write went through, and why not when it did not.
|
|
125
|
+
*/
|
|
126
|
+
export declare function saveWebdav(ctx: Context, vault: Vault | undefined, base: WebdavSettings, patch: WebdavPatch): Promise<{
|
|
127
|
+
ok: boolean;
|
|
128
|
+
reason?: string;
|
|
129
|
+
}>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebDAV self-test: one request that answers "is this channel usable".
|
|
3
|
+
*
|
|
4
|
+
* The S3 side needs a matrix because a gateway can refuse four different things
|
|
5
|
+
* with the same status code. WebDAV does not: a `PROPFIND` on the configured
|
|
6
|
+
* folder either works (the server answers a listing or a 404 for the path) or it
|
|
7
|
+
* does not, and the status says which. So this is deliberately one row — the
|
|
8
|
+
* panel turns it into one sentence.
|
|
9
|
+
*
|
|
10
|
+
* Read-only by construction: `PROPFIND` with no body, and nothing else.
|
|
11
|
+
*/
|
|
12
|
+
import type { ProbeRow } from '../../shared/panel-wire.js';
|
|
13
|
+
import { type WebdavDeps } from './client.js';
|
|
14
|
+
/**
|
|
15
|
+
* Ask the configured folder for its listing.
|
|
16
|
+
*
|
|
17
|
+
* @param deps - fetch, credentials and the identity to present.
|
|
18
|
+
* @param baseUrl - the remote base URL from the settings.
|
|
19
|
+
* @param directory - the folder inside it, as configured.
|
|
20
|
+
* @returns one row: what was asked, and what came back.
|
|
21
|
+
*/
|
|
22
|
+
export declare function probeWebdav(deps: WebdavDeps, baseUrl: string, directory: string): Promise<ProbeRow[]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One pull, wired for real: reads the configuration, resolves the password, and
|
|
3
|
+
* hands the whole thing to the ingest.
|
|
4
|
+
*
|
|
5
|
+
* Both callers (startup and the panel's button) go through here, so what the
|
|
6
|
+
* button does and what a restart does cannot drift apart.
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
10
|
+
import type { S3FetchLike } from '../s3/client.js';
|
|
11
|
+
import type { Vault } from '../vault/vault.js';
|
|
12
|
+
import { type PullResult } from '../remote/pull.js';
|
|
13
|
+
import type { FetchLike } from './client.js';
|
|
14
|
+
/** The global fetch, adapted to the client's injected-fetch shape. */
|
|
15
|
+
export declare const webdavFetch: FetchLike;
|
|
16
|
+
/**
|
|
17
|
+
* The same adapter for the S3 client.
|
|
18
|
+
*
|
|
19
|
+
* The body has to be forwarded here, and it *was not* until a real bucket
|
|
20
|
+
* showed it: every PUT left with an empty payload, the gateway accepted them
|
|
21
|
+
* all, and the cloud drive filled up with 0-byte objects while the push happily
|
|
22
|
+
* reported success. A fake fetch in a unit test cannot catch that — it is this
|
|
23
|
+
* one-line omission that the *adapter* had, not the client.
|
|
24
|
+
*/
|
|
25
|
+
export declare const s3Fetch: S3FetchLike;
|
|
26
|
+
/**
|
|
27
|
+
* Pull once.
|
|
28
|
+
*
|
|
29
|
+
* @param ctx - host context carrying settings and credentials.
|
|
30
|
+
* @param vault - the open vault.
|
|
31
|
+
* @param attachments - the store that owns pulled bytes.
|
|
32
|
+
* @returns the pull's outcome; never throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runPull(ctx: Context, vault: Vault, attachments: AttachmentStore | undefined): Promise<PullResult>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Stable ids shared by the host half and the browser half. */
|
|
2
|
+
/** npm package name; also the client bundle id the loader registers. */
|
|
3
|
+
export declare const PACKAGE_NAME = "@chance722/dsh-inbox";
|
|
4
|
+
/** Sidebar panel id: addresses both the `sidebar.panellist` row and the `main` key. */
|
|
5
|
+
export declare const PANEL_ID = "inbox";
|
|
6
|
+
/**
|
|
7
|
+
* The version the plugin reports, substituted from `package.json` at build time.
|
|
8
|
+
*
|
|
9
|
+
* The status tool used to print an internal milestone marker instead (`M6c`),
|
|
10
|
+
* which told the user nothing — and the assistant relays that string verbatim,
|
|
11
|
+
* so it was also the first thing they saw when they asked what the vault held.
|
|
12
|
+
* `typeof` keeps a build that forgot the substitution from throwing.
|
|
13
|
+
*/
|
|
14
|
+
export declare const VERSION: string;
|
|
15
|
+
/**
|
|
16
|
+
* What the plugin calls itself in `User-Agent` when the user configured nothing.
|
|
17
|
+
*
|
|
18
|
+
* Some object-storage gateways (中科院数据胶囊 among them) bind an access key to
|
|
19
|
+
* an application and reject any caller that does not claim to be that
|
|
20
|
+
* application, which is why this is a visible setting rather than a constant
|
|
21
|
+
* buried in a client.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_USER_AGENT = "dsh-inbox";
|