dsh-connect 0.8.0 → 0.9.2
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/README.i18n.yaml +2 -2
- package/README.md +236 -18
- package/README.zh.md +185 -18
- package/client/client.js +517 -84
- package/client/client.js.map +4 -4
- package/client/locale.mjs +227 -0
- package/client/panel-state.mjs +54 -0
- package/client/settings-client.mjs +282 -61
- package/docs/images/settings-advanced-en.png +0 -0
- package/docs/images/settings-advanced-zh.png +0 -0
- package/docs/images/settings-defaults-en.png +0 -0
- package/docs/images/settings-defaults-zh.png +0 -0
- package/docs/images/settings-overview-en.png +0 -0
- package/docs/images/settings-overview-zh.png +0 -0
- package/lib/binding.d.ts.map +1 -1
- package/lib/binding.js +2 -2
- package/lib/binding.js.map +1 -1
- package/lib/channels/dingtalk/index.d.ts +49 -49
- package/lib/channels/dingtalk/index.d.ts.map +1 -1
- package/lib/channels/dingtalk/message.d.ts.map +1 -1
- package/lib/channels/dingtalk/message.js +7 -0
- package/lib/channels/dingtalk/message.js.map +1 -1
- package/lib/channels/feishu/adapter.d.ts +14 -13
- package/lib/channels/feishu/adapter.d.ts.map +1 -1
- package/lib/channels/feishu/adapter.js +122 -56
- package/lib/channels/feishu/adapter.js.map +1 -1
- package/lib/channels/feishu/i18n.d.ts +2 -0
- package/lib/channels/feishu/i18n.d.ts.map +1 -1
- package/lib/channels/feishu/i18n.js +2 -0
- package/lib/channels/feishu/i18n.js.map +1 -1
- package/lib/channels/feishu/index.d.ts +48 -27
- package/lib/channels/feishu/index.d.ts.map +1 -1
- package/lib/channels/feishu/index.js +32 -6
- package/lib/channels/feishu/index.js.map +1 -1
- package/lib/channels/telegram/adapter.d.ts.map +1 -1
- package/lib/channels/telegram/adapter.js +7 -1
- package/lib/channels/telegram/adapter.js.map +1 -1
- package/lib/channels/telegram/index.d.ts +13 -13
- package/lib/channels/telegram/index.d.ts.map +1 -1
- package/lib/channels/web/adapter.d.ts +3 -1
- package/lib/channels/web/adapter.d.ts.map +1 -1
- package/lib/channels/web/adapter.js +3 -1
- package/lib/channels/web/adapter.js.map +1 -1
- package/lib/channels/web/index.d.ts +5 -5
- package/lib/channels/web/index.d.ts.map +1 -1
- package/lib/chat-key.d.ts +29 -0
- package/lib/chat-key.d.ts.map +1 -0
- package/lib/chat-key.js +38 -0
- package/lib/chat-key.js.map +1 -0
- package/lib/index.d.ts +57 -63
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +330 -55
- package/lib/index.js.map +1 -1
- package/lib/interaction.d.ts +90 -50
- package/lib/interaction.d.ts.map +1 -1
- package/lib/interaction.js +233 -272
- package/lib/interaction.js.map +1 -1
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +7 -1
- package/lib/retry.js.map +1 -1
- package/lib/runner.d.ts +54 -0
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +158 -23
- package/lib/runner.js.map +1 -1
- package/lib/scheduler.d.ts.map +1 -1
- package/lib/scheduler.js +2 -2
- package/lib/scheduler.js.map +1 -1
- package/lib/service.d.ts +16 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +46 -4
- package/lib/service.js.map +1 -1
- package/lib/settings/channel-runtime.d.ts +75 -0
- package/lib/settings/channel-runtime.d.ts.map +1 -0
- package/lib/settings/channel-runtime.js +165 -0
- package/lib/settings/channel-runtime.js.map +1 -0
- package/lib/settings/channels.d.ts +11 -0
- package/lib/settings/channels.d.ts.map +1 -1
- package/lib/settings/channels.js +31 -0
- package/lib/settings/channels.js.map +1 -1
- package/lib/settings/credential-store.d.ts +52 -6
- package/lib/settings/credential-store.d.ts.map +1 -1
- package/lib/settings/credential-store.js +92 -18
- package/lib/settings/credential-store.js.map +1 -1
- package/lib/settings/legacy-import.d.ts +190 -0
- package/lib/settings/legacy-import.d.ts.map +1 -0
- package/lib/settings/legacy-import.js +373 -0
- package/lib/settings/legacy-import.js.map +1 -0
- package/lib/settings/namespace.d.ts +263 -0
- package/lib/settings/namespace.d.ts.map +1 -0
- package/lib/settings/namespace.js +287 -0
- package/lib/settings/namespace.js.map +1 -0
- package/lib/settings/secret-disclosure.d.ts +50 -0
- package/lib/settings/secret-disclosure.d.ts.map +1 -0
- package/lib/settings/secret-disclosure.js +132 -0
- package/lib/settings/secret-disclosure.js.map +1 -0
- package/lib/settings/settings-model.d.ts +17 -0
- package/lib/settings/settings-model.d.ts.map +1 -1
- package/lib/settings/settings-model.js +18 -1
- package/lib/settings/settings-model.js.map +1 -1
- package/lib/settings/settings-rpc.d.ts +71 -3
- package/lib/settings/settings-rpc.d.ts.map +1 -1
- package/lib/settings/settings-rpc.js +167 -24
- package/lib/settings/settings-rpc.js.map +1 -1
- package/lib/settings/settings-service.d.ts +22 -1
- package/lib/settings/settings-service.d.ts.map +1 -1
- package/lib/settings/settings-service.js +145 -8
- package/lib/settings/settings-service.js.map +1 -1
- package/lib/state-dir.d.ts +28 -0
- package/lib/state-dir.d.ts.map +1 -0
- package/lib/state-dir.js +33 -0
- package/lib/state-dir.js.map +1 -0
- package/lib/stream.d.ts.map +1 -1
- package/lib/stream.js +3 -2
- package/lib/stream.js.map +1 -1
- package/lib/types.d.ts +14 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +13 -10
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `dsh-connect` user-settings seam.
|
|
3
|
+
*
|
|
4
|
+
* DSH 0.2 replaced the plugin-facing settings API. The old `ctx.settings`
|
|
5
|
+
* namespace contract (`installSection(owner, ns, schema, entry, hooks)` backed by
|
|
6
|
+
* a `$DSH_HOME/settings.yaml` section keyed by the plugin *name*) is gone; what
|
|
7
|
+
* is left is `SettingsForms`, which edits a plugin's **profile entry** and
|
|
8
|
+
* hot-commits the fields that plugin declared `volatile`:
|
|
9
|
+
*
|
|
10
|
+
* - **Reads come from our own config.** A field declared with
|
|
11
|
+
* `z.…volatile()` arrives in `apply()` as a *reference* (`{ get() }`) rather
|
|
12
|
+
* than a value, and the loader updates that reference in place when the
|
|
13
|
+
* profile entry changes — no remount, no `apply()` re-entry. So there is no
|
|
14
|
+
* read API to call: the live state is `refs.get()`, and this module's
|
|
15
|
+
* `materializeConfig()` turns the ref-carrying config into a plain snapshot.
|
|
16
|
+
* - **Writes go through `SettingsForms.replace(ns, section)`**, where `ns` is
|
|
17
|
+
* the profile entry id (`connect`) — *not* the package name. `replace` resets
|
|
18
|
+
* every live field to its inherited value and then applies the supplied ones,
|
|
19
|
+
* which is exactly "this is the user's complete visible config" — and why a
|
|
20
|
+
* *partial* section is a destructive write: a field the caller omits is
|
|
21
|
+
* reset, not preserved. Callers that mean to preserve something must merge
|
|
22
|
+
* it in first ({@link mergeSections}).
|
|
23
|
+
* - **`volatileForm(schema)` must not be empty**, or `replace` refuses the
|
|
24
|
+
* write with `Plugin entry "connect" has no volatile fields`. That is why
|
|
25
|
+
* `paneConfigFields()` exists and must keep declaring the pane's field set.
|
|
26
|
+
*
|
|
27
|
+
* **Only volatile *leaves* may be declared — never a volatile container.**
|
|
28
|
+
* `projectForm(form, base)` projects the *base* layer down to volatile fields
|
|
29
|
+
* before the write is merged: if `feishu` itself were volatile it would be
|
|
30
|
+
* copied whole into the settings document, `appSecret` and all. Declaring the
|
|
31
|
+
* leaves keeps every non-pane key (`appSecret`, `appId`, …) in the raw
|
|
32
|
+
* remainder, where `strip()` leaves ordinary config untouched. This is the same
|
|
33
|
+
* hazard `sectionOf()` guards on the write path, from the other side: an
|
|
34
|
+
* undeclared key in a *submitted* section must not reach the document either.
|
|
35
|
+
*
|
|
36
|
+
* Secrets therefore never appear in either declared surface: they live in the
|
|
37
|
+
* DSH credential store (`ctx.credentials`), which is where onboarding and the
|
|
38
|
+
* `FEISHU_*`-style env layering already put them, and `settings`-side documents
|
|
39
|
+
* are plain files users are invited to paste into bug reports.
|
|
40
|
+
*
|
|
41
|
+
* @module dsh-connect/settings/namespace
|
|
42
|
+
*/
|
|
43
|
+
import z from "@deepseek-ai/schemastery";
|
|
44
|
+
import { type ChannelName, type LoggerLike } from "./channels.js";
|
|
45
|
+
/**
|
|
46
|
+
* The section key the pre-0.2 harness read from `$DSH_HOME/settings.yaml`, keyed
|
|
47
|
+
* by plugin *name*. Nothing writes it any more; `legacy-import.ts` reads it once
|
|
48
|
+
* so an upgrading user's pane settings survive the switch to the new store.
|
|
49
|
+
*/
|
|
50
|
+
export declare const LEGACY_CONNECT_SECTION = "dsh-connect";
|
|
51
|
+
/**
|
|
52
|
+
* The automatic-page policy for our entry. `auto: false` stops the host's
|
|
53
|
+
* Plugins page from generating a second, generic form for `connect`: this
|
|
54
|
+
* plugin ships its own client pane (`client/settings-client.mjs`), and two
|
|
55
|
+
* editors writing the same fields through different UIs is how a save silently
|
|
56
|
+
* reverts. Register it with `settings.configure(policy, ctx.fiber)` — the
|
|
57
|
+
* `owner` argument matters, see `index.ts`.
|
|
58
|
+
*/
|
|
59
|
+
export declare const CONNECT_PRESENTATION: Record<string, unknown>;
|
|
60
|
+
/** A config reference: a stable handle whose value the loader replaces in place. */
|
|
61
|
+
export interface ConfigRef<T = unknown> {
|
|
62
|
+
/** The current immutable snapshot; `undefined` for a field absent from the config. */
|
|
63
|
+
get(): T;
|
|
64
|
+
}
|
|
65
|
+
/** Whether a parsed config value is a volatile reference rather than plain data. */
|
|
66
|
+
export declare function isConfigRef(value: unknown): value is ConfigRef;
|
|
67
|
+
/**
|
|
68
|
+
* Copy a ref-carrying config into plain data: every reference replaced by its
|
|
69
|
+
* current snapshot, recursively, so callers can hand the result to code that
|
|
70
|
+
* only understands plain objects (the adapters, the settings pane, tests).
|
|
71
|
+
*
|
|
72
|
+
* The copy is *fresh* rather than the snapshot itself, deliberately: snapshots
|
|
73
|
+
* are deeply frozen, and both `ChannelRuntime` (which spreads a channel's
|
|
74
|
+
* config into a new object) and the pane (which holds a form to edit) want
|
|
75
|
+
* mutable data. Cycles in ordinary config are tolerated via `seen`; a snapshot
|
|
76
|
+
* cannot contain references (the loader commits plain data), but the walk
|
|
77
|
+
* treats them uniformly so it does not matter.
|
|
78
|
+
*
|
|
79
|
+
* **A key that materializes to `undefined` is dropped, not copied.** Every
|
|
80
|
+
* declared volatile leaf is present in the parsed config even when the profile
|
|
81
|
+
* never set it — `z.object({transport: z.any().volatile()})` resolves an absent
|
|
82
|
+
* `transport` to a reference to `undefined` — so a faithful copy would hand the
|
|
83
|
+
* adapters `{transport: undefined, language: undefined, …}`. That is not the
|
|
84
|
+
* same as absent: `ChannelRuntime` resolves a channel as
|
|
85
|
+
* `{...channelDefaults, ...overrides}`, and since `language` is a key of *both*
|
|
86
|
+
* tables, a channel's own `language: undefined` would mask the shared
|
|
87
|
+
* `channelDefaults.language` every other channel still gets. Config here is
|
|
88
|
+
* JSON-shaped (a profile entry, a YAML document, the settings RPC), where
|
|
89
|
+
* `undefined` can only ever mean "this key was not set", so dropping it restores
|
|
90
|
+
* exactly the shape a plain `z.any()` field used to produce.
|
|
91
|
+
*/
|
|
92
|
+
export declare function materializeConfig<T>(value: T): T;
|
|
93
|
+
/**
|
|
94
|
+
* The volatile half of the plugin `Config`, derived from the same field tables
|
|
95
|
+
* the settings pane renders. Deriving rather than restating them is the point:
|
|
96
|
+
* a field added to `CHANNEL_CONFIG_FIELDS` becomes user-editable *and*
|
|
97
|
+
* hot-appliable in one edit, and a field the pane offers can never be missing
|
|
98
|
+
* from the schema (a write of an undeclared path is refused by the host).
|
|
99
|
+
*
|
|
100
|
+
* Every leaf is `z.any()` on purpose. A stricter node would let a stale value
|
|
101
|
+
* in a profile — a `webhookPort` written as a string, a `dmMode` from a version
|
|
102
|
+
* that spelled it differently — abort config validation and keep the bridge
|
|
103
|
+
* from loading at all. The pane's own `coerceConfigValue` is what keeps types
|
|
104
|
+
* honest; the schema's job here is only to say "these paths are live-editable".
|
|
105
|
+
*
|
|
106
|
+
* `channels` is a loose `z.array(z.string())` for the same reason, and it is
|
|
107
|
+
* safe because `ChannelRuntime` skips a name it has no adapter for (with a log
|
|
108
|
+
* line) instead of throwing. Its explicit `.default([...CHANNELS])` is load
|
|
109
|
+
* bearing, not cosmetic: a volatile *array* resolves an absent key to `[]`
|
|
110
|
+
* rather than `undefined`, so a profile that never set `channels` would
|
|
111
|
+
* otherwise materialize an empty list and start no adapter at all, where the
|
|
112
|
+
* documented default — and the pre-volatile behaviour — is every channel. A
|
|
113
|
+
* default also keeps `replace()`'s reset target honest: an omitted `channels`
|
|
114
|
+
* in a section resets to all channels, not to none. `[...CHANNELS]` rather than
|
|
115
|
+
* `CHANNELS` so the schema cannot hand out the module's own array.
|
|
116
|
+
*/
|
|
117
|
+
export declare function paneConfigFields(): Record<string, z<any>>;
|
|
118
|
+
/** Channel-agnostic defaults section of the namespace. */
|
|
119
|
+
export type ConnectSectionDefaults = Record<string, unknown> & {
|
|
120
|
+
language?: string;
|
|
121
|
+
notifyLevel?: string;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The pane-editable slice of the config: the whole of what this plugin stores
|
|
125
|
+
* on the user's behalf. Every field is optional — a user who only ever edits
|
|
126
|
+
* `channels` leaves the rest absent, and each adapter applies its own default
|
|
127
|
+
* for anything missing.
|
|
128
|
+
*/
|
|
129
|
+
export interface ConnectSection {
|
|
130
|
+
channels?: ChannelName[];
|
|
131
|
+
channelDefaults?: ConnectSectionDefaults;
|
|
132
|
+
feishu?: Record<string, unknown>;
|
|
133
|
+
telegram?: Record<string, unknown>;
|
|
134
|
+
dingtalk?: Record<string, unknown>;
|
|
135
|
+
web?: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
export interface SectionOptions {
|
|
138
|
+
/**
|
|
139
|
+
* Materialize `channels` even when the source omits the key. True (the
|
|
140
|
+
* default) for the *write* path: there an absent key means "the caller did
|
|
141
|
+
* not speak to the channel list", and a section without `channels` handed to
|
|
142
|
+
* `replace()` is a reset — reading it as "activate nothing" would switch
|
|
143
|
+
* every adapter off. False for the legacy import, where an absent key must
|
|
144
|
+
* stay absent so the profile's own `channels` keeps applying.
|
|
145
|
+
*
|
|
146
|
+
* Note this is about the *key being absent*, not about an empty array: a user
|
|
147
|
+
* who unchecks every channel saves `channels: []`, which is an explicit
|
|
148
|
+
* "none" and is left alone.
|
|
149
|
+
*/
|
|
150
|
+
defaultChannels?: boolean;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Project a config onto the pane's field set: declared, non-secret keys only.
|
|
154
|
+
*
|
|
155
|
+
* Two callers, one guard. For a **read** the input is a materialized snapshot
|
|
156
|
+
* of our own config (a profile may legitimately carry `appSecret`, and it must
|
|
157
|
+
* not travel out through the snapshot the pane receives); for a **write** the
|
|
158
|
+
* input is untrusted JSON from the settings RPC, and an undeclared key would
|
|
159
|
+
* otherwise be preserved into the profile by schemastery's passthrough. Both
|
|
160
|
+
* are the same projection, so both get the same answer.
|
|
161
|
+
*
|
|
162
|
+
* `channels` is emitted unconditionally on the write path because an absent
|
|
163
|
+
* array resolves to `[]` (schemastery's array default), which would read as
|
|
164
|
+
* "activate nothing". Unknown channel names are dropped rather than refused:
|
|
165
|
+
* a name this version does not know is not a reason to reject the whole save.
|
|
166
|
+
*/
|
|
167
|
+
export declare function sectionOf(config: unknown, options?: SectionOptions): ConnectSection;
|
|
168
|
+
/**
|
|
169
|
+
* Layer one section over another: for each channel and for the shared defaults,
|
|
170
|
+
* the override's own keys win and the base's surviving keys are kept.
|
|
171
|
+
*
|
|
172
|
+
* This exists because of how `replace()` treats an omission — see the module
|
|
173
|
+
* doc. A caller that has a section in force and only wants to change part of it
|
|
174
|
+
* must merge, never pass the part alone: `feishu: { transport }` resets every
|
|
175
|
+
* other declared `feishu` field, and a section without `channels` resets the
|
|
176
|
+
* channel list itself, which switches every adapter off.
|
|
177
|
+
*/
|
|
178
|
+
export declare function mergeSections(base: ConnectSection, override: ConnectSection): ConnectSection;
|
|
179
|
+
/**
|
|
180
|
+
* The slice of `SettingsForms` this module calls. Declared structurally so the
|
|
181
|
+
* plugin keeps no dependency on `@deepseek-ai/dsh-settings` (a `dsh-base` row we
|
|
182
|
+
* cannot import at build time): a missing method degrades to "no live store"
|
|
183
|
+
* rather than a crash, and the settings service falls back to its JSON file.
|
|
184
|
+
*/
|
|
185
|
+
export interface SettingsProviderLike {
|
|
186
|
+
/**
|
|
187
|
+
* Register this plugin instance's page policy. `owner` must be the plugin's
|
|
188
|
+
* own fiber — the default is the *service's* fiber, which would make the
|
|
189
|
+
* policy invisible to this instance's page and leak on every reload.
|
|
190
|
+
* @returns Disposer; register it with the plugin's effects.
|
|
191
|
+
*/
|
|
192
|
+
configure?(presentation: Record<string, unknown>, owner?: unknown): () => void;
|
|
193
|
+
/**
|
|
194
|
+
* Reset every live field to its inherited value, then apply `section`;
|
|
195
|
+
* ordinary (non-live) config is preserved.
|
|
196
|
+
*
|
|
197
|
+
* `expectedRevision` is deliberately omitted by every caller here: `write`
|
|
198
|
+
* only enforces the revision check when it is defined, and we read our state
|
|
199
|
+
* from config references rather than from `describe()`, so there is no
|
|
200
|
+
* revision to round-trip. Two concurrent editors of the pane would still both
|
|
201
|
+
* land, last write winning — the same behaviour the JSON store had.
|
|
202
|
+
*/
|
|
203
|
+
replace?(ns: string, section: unknown, expectedRevision?: number): Promise<unknown>;
|
|
204
|
+
}
|
|
205
|
+
/** A live handle onto the profile entry: read the effective section, write a new one. */
|
|
206
|
+
export interface LiveConnectSection {
|
|
207
|
+
/** The section in force right now, read from the config references. */
|
|
208
|
+
read(): ConnectSection;
|
|
209
|
+
/**
|
|
210
|
+
* Replace the user's section with the declared-key projection of `config`,
|
|
211
|
+
* then hand the new section to `onChange`. Rejects if the host refuses the
|
|
212
|
+
* write (a non-volatile path, a missing entry) — the caller is expected to
|
|
213
|
+
* surface that rather than swallow it.
|
|
214
|
+
*/
|
|
215
|
+
write(config: unknown): Promise<void>;
|
|
216
|
+
}
|
|
217
|
+
/** Outcome of wiring the seam. */
|
|
218
|
+
export interface InstallConnectSectionResult {
|
|
219
|
+
/** True when a write handle exists, i.e. a save can land and take effect. */
|
|
220
|
+
live: boolean;
|
|
221
|
+
/** The section in force at install time. */
|
|
222
|
+
section: ConnectSection;
|
|
223
|
+
/** Read/write onto the profile entry; absent when the seam is unavailable. */
|
|
224
|
+
handle?: LiveConnectSection;
|
|
225
|
+
}
|
|
226
|
+
export interface InstallConnectSectionOptions<Ctx extends LoggerLike> {
|
|
227
|
+
/** The plugin's own context: `logger` for diagnostics, and the fallback owner. */
|
|
228
|
+
owner: Ctx;
|
|
229
|
+
settings: SettingsProviderLike | undefined;
|
|
230
|
+
/** The ref-carrying config this plugin was applied with (see `materializeConfig`). */
|
|
231
|
+
config: unknown;
|
|
232
|
+
/** The profile entry id (`connect`), from the loader entry. */
|
|
233
|
+
ns: string | undefined;
|
|
234
|
+
/** Called with the in-force section at install and after every successful write. */
|
|
235
|
+
onChange: (section: ConnectSection) => void;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Wire the plugin's config references to the host's settings service.
|
|
239
|
+
*
|
|
240
|
+
* Never throws. A host without `SettingsForms`, a context where the entry id
|
|
241
|
+
* cannot be resolved (a bare unit-test context, or a plugin mounted outside the
|
|
242
|
+
* loader) and a refused `replace` all degrade to `live: false` with one warning
|
|
243
|
+
* line — a settings integration that cannot work must never be the reason a
|
|
244
|
+
* bridge fails to start.
|
|
245
|
+
*/
|
|
246
|
+
export declare function installConnectSection<Ctx extends LoggerLike>(options: InstallConnectSectionOptions<Ctx>): InstallConnectSectionResult;
|
|
247
|
+
declare module "@deepseek-ai/cordis" {
|
|
248
|
+
interface Events {
|
|
249
|
+
/**
|
|
250
|
+
* Dispatched to the owning fiber when the loader commits changed volatile
|
|
251
|
+
* config paths **in place**, without remounting the plugin — a settings-pane
|
|
252
|
+
* save, or an edit to a running profile's patch. The payload is the changed
|
|
253
|
+
* paths; a consumer re-reads the references it was applied with
|
|
254
|
+
* ({@link materializeConfig}) rather than being told the new values.
|
|
255
|
+
*
|
|
256
|
+
* Declared here because cordis's `on()` typechecks event names against this
|
|
257
|
+
* interface and nothing this plugin depends on names the event — an
|
|
258
|
+
* undeclared name would be a type error at the only call site (`index.ts`).
|
|
259
|
+
*/
|
|
260
|
+
"loader/volatile-update"(paths: readonly (readonly string[])[]): void;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=namespace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespace.d.ts","sourceRoot":"","sources":["../../src/settings/namespace.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `dsh-connect` user-settings seam.
|
|
3
|
+
*
|
|
4
|
+
* DSH 0.2 replaced the plugin-facing settings API. The old `ctx.settings`
|
|
5
|
+
* namespace contract (`installSection(owner, ns, schema, entry, hooks)` backed by
|
|
6
|
+
* a `$DSH_HOME/settings.yaml` section keyed by the plugin *name*) is gone; what
|
|
7
|
+
* is left is `SettingsForms`, which edits a plugin's **profile entry** and
|
|
8
|
+
* hot-commits the fields that plugin declared `volatile`:
|
|
9
|
+
*
|
|
10
|
+
* - **Reads come from our own config.** A field declared with
|
|
11
|
+
* `z.…volatile()` arrives in `apply()` as a *reference* (`{ get() }`) rather
|
|
12
|
+
* than a value, and the loader updates that reference in place when the
|
|
13
|
+
* profile entry changes — no remount, no `apply()` re-entry. So there is no
|
|
14
|
+
* read API to call: the live state is `refs.get()`, and this module's
|
|
15
|
+
* `materializeConfig()` turns the ref-carrying config into a plain snapshot.
|
|
16
|
+
* - **Writes go through `SettingsForms.replace(ns, section)`**, where `ns` is
|
|
17
|
+
* the profile entry id (`connect`) — *not* the package name. `replace` resets
|
|
18
|
+
* every live field to its inherited value and then applies the supplied ones,
|
|
19
|
+
* which is exactly "this is the user's complete visible config" — and why a
|
|
20
|
+
* *partial* section is a destructive write: a field the caller omits is
|
|
21
|
+
* reset, not preserved. Callers that mean to preserve something must merge
|
|
22
|
+
* it in first ({@link mergeSections}).
|
|
23
|
+
* - **`volatileForm(schema)` must not be empty**, or `replace` refuses the
|
|
24
|
+
* write with `Plugin entry "connect" has no volatile fields`. That is why
|
|
25
|
+
* `paneConfigFields()` exists and must keep declaring the pane's field set.
|
|
26
|
+
*
|
|
27
|
+
* **Only volatile *leaves* may be declared — never a volatile container.**
|
|
28
|
+
* `projectForm(form, base)` projects the *base* layer down to volatile fields
|
|
29
|
+
* before the write is merged: if `feishu` itself were volatile it would be
|
|
30
|
+
* copied whole into the settings document, `appSecret` and all. Declaring the
|
|
31
|
+
* leaves keeps every non-pane key (`appSecret`, `appId`, …) in the raw
|
|
32
|
+
* remainder, where `strip()` leaves ordinary config untouched. This is the same
|
|
33
|
+
* hazard `sectionOf()` guards on the write path, from the other side: an
|
|
34
|
+
* undeclared key in a *submitted* section must not reach the document either.
|
|
35
|
+
*
|
|
36
|
+
* Secrets therefore never appear in either declared surface: they live in the
|
|
37
|
+
* DSH credential store (`ctx.credentials`), which is where onboarding and the
|
|
38
|
+
* `FEISHU_*`-style env layering already put them, and `settings`-side documents
|
|
39
|
+
* are plain files users are invited to paste into bug reports.
|
|
40
|
+
*
|
|
41
|
+
* @module dsh-connect/settings/namespace
|
|
42
|
+
*/
|
|
43
|
+
import z from "@deepseek-ai/schemastery";
|
|
44
|
+
import { CHANNELS } from "./channels.js";
|
|
45
|
+
import { CHANNEL_CONFIG_FIELDS, CHANNEL_DEFAULT_FIELDS } from "./settings-model.js";
|
|
46
|
+
/**
|
|
47
|
+
* The section key the pre-0.2 harness read from `$DSH_HOME/settings.yaml`, keyed
|
|
48
|
+
* by plugin *name*. Nothing writes it any more; `legacy-import.ts` reads it once
|
|
49
|
+
* so an upgrading user's pane settings survive the switch to the new store.
|
|
50
|
+
*/
|
|
51
|
+
export const LEGACY_CONNECT_SECTION = "dsh-connect";
|
|
52
|
+
/**
|
|
53
|
+
* The automatic-page policy for our entry. `auto: false` stops the host's
|
|
54
|
+
* Plugins page from generating a second, generic form for `connect`: this
|
|
55
|
+
* plugin ships its own client pane (`client/settings-client.mjs`), and two
|
|
56
|
+
* editors writing the same fields through different UIs is how a save silently
|
|
57
|
+
* reverts. Register it with `settings.configure(policy, ctx.fiber)` — the
|
|
58
|
+
* `owner` argument matters, see `index.ts`.
|
|
59
|
+
*/
|
|
60
|
+
export const CONNECT_PRESENTATION = { auto: false };
|
|
61
|
+
/**
|
|
62
|
+
* The shared cross-ESM volatile protocol (`cosmokit`'s `Symbol.for` key).
|
|
63
|
+
*
|
|
64
|
+
* Looked up by symbol rather than by importing `@deepseek-ai/cosmokit`: the
|
|
65
|
+
* host may hand us refs created by *its* copy of the library, and `Symbol.for`
|
|
66
|
+
* is the one identifier that is identical across copies. Sniffing the key (as
|
|
67
|
+
* cosmokit's own `isVolatile` does) is more forgiving than `instanceof`, which
|
|
68
|
+
* would fail across an ESM/CJS duplicate.
|
|
69
|
+
*/
|
|
70
|
+
const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
|
|
71
|
+
/** Whether a parsed config value is a volatile reference rather than plain data. */
|
|
72
|
+
export function isConfigRef(value) {
|
|
73
|
+
return typeof value === "object" && value !== null && VOLATILE_WRITE in value;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Copy a ref-carrying config into plain data: every reference replaced by its
|
|
77
|
+
* current snapshot, recursively, so callers can hand the result to code that
|
|
78
|
+
* only understands plain objects (the adapters, the settings pane, tests).
|
|
79
|
+
*
|
|
80
|
+
* The copy is *fresh* rather than the snapshot itself, deliberately: snapshots
|
|
81
|
+
* are deeply frozen, and both `ChannelRuntime` (which spreads a channel's
|
|
82
|
+
* config into a new object) and the pane (which holds a form to edit) want
|
|
83
|
+
* mutable data. Cycles in ordinary config are tolerated via `seen`; a snapshot
|
|
84
|
+
* cannot contain references (the loader commits plain data), but the walk
|
|
85
|
+
* treats them uniformly so it does not matter.
|
|
86
|
+
*
|
|
87
|
+
* **A key that materializes to `undefined` is dropped, not copied.** Every
|
|
88
|
+
* declared volatile leaf is present in the parsed config even when the profile
|
|
89
|
+
* never set it — `z.object({transport: z.any().volatile()})` resolves an absent
|
|
90
|
+
* `transport` to a reference to `undefined` — so a faithful copy would hand the
|
|
91
|
+
* adapters `{transport: undefined, language: undefined, …}`. That is not the
|
|
92
|
+
* same as absent: `ChannelRuntime` resolves a channel as
|
|
93
|
+
* `{...channelDefaults, ...overrides}`, and since `language` is a key of *both*
|
|
94
|
+
* tables, a channel's own `language: undefined` would mask the shared
|
|
95
|
+
* `channelDefaults.language` every other channel still gets. Config here is
|
|
96
|
+
* JSON-shaped (a profile entry, a YAML document, the settings RPC), where
|
|
97
|
+
* `undefined` can only ever mean "this key was not set", so dropping it restores
|
|
98
|
+
* exactly the shape a plain `z.any()` field used to produce.
|
|
99
|
+
*/
|
|
100
|
+
export function materializeConfig(value) {
|
|
101
|
+
return walk(value, new WeakMap());
|
|
102
|
+
}
|
|
103
|
+
function walk(value, seen) {
|
|
104
|
+
if (value === null || typeof value !== "object")
|
|
105
|
+
return value;
|
|
106
|
+
if (isConfigRef(value))
|
|
107
|
+
return walk(value.get(), seen);
|
|
108
|
+
const known = seen.get(value);
|
|
109
|
+
if (known !== undefined)
|
|
110
|
+
return known;
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
const out = [];
|
|
113
|
+
seen.set(value, out);
|
|
114
|
+
for (const item of value)
|
|
115
|
+
out.push(walk(item, seen));
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
const out = {};
|
|
119
|
+
seen.set(value, out);
|
|
120
|
+
for (const [key, child] of Object.entries(value)) {
|
|
121
|
+
const materialized = walk(child, seen);
|
|
122
|
+
if (materialized !== undefined)
|
|
123
|
+
out[key] = materialized;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The volatile half of the plugin `Config`, derived from the same field tables
|
|
129
|
+
* the settings pane renders. Deriving rather than restating them is the point:
|
|
130
|
+
* a field added to `CHANNEL_CONFIG_FIELDS` becomes user-editable *and*
|
|
131
|
+
* hot-appliable in one edit, and a field the pane offers can never be missing
|
|
132
|
+
* from the schema (a write of an undeclared path is refused by the host).
|
|
133
|
+
*
|
|
134
|
+
* Every leaf is `z.any()` on purpose. A stricter node would let a stale value
|
|
135
|
+
* in a profile — a `webhookPort` written as a string, a `dmMode` from a version
|
|
136
|
+
* that spelled it differently — abort config validation and keep the bridge
|
|
137
|
+
* from loading at all. The pane's own `coerceConfigValue` is what keeps types
|
|
138
|
+
* honest; the schema's job here is only to say "these paths are live-editable".
|
|
139
|
+
*
|
|
140
|
+
* `channels` is a loose `z.array(z.string())` for the same reason, and it is
|
|
141
|
+
* safe because `ChannelRuntime` skips a name it has no adapter for (with a log
|
|
142
|
+
* line) instead of throwing. Its explicit `.default([...CHANNELS])` is load
|
|
143
|
+
* bearing, not cosmetic: a volatile *array* resolves an absent key to `[]`
|
|
144
|
+
* rather than `undefined`, so a profile that never set `channels` would
|
|
145
|
+
* otherwise materialize an empty list and start no adapter at all, where the
|
|
146
|
+
* documented default — and the pre-volatile behaviour — is every channel. A
|
|
147
|
+
* default also keeps `replace()`'s reset target honest: an omitted `channels`
|
|
148
|
+
* in a section resets to all channels, not to none. `[...CHANNELS]` rather than
|
|
149
|
+
* `CHANNELS` so the schema cannot hand out the module's own array.
|
|
150
|
+
*/
|
|
151
|
+
export function paneConfigFields() {
|
|
152
|
+
const fields = {
|
|
153
|
+
channels: z.array(z.string()).default([...CHANNELS]).volatile(),
|
|
154
|
+
};
|
|
155
|
+
const defaults = {};
|
|
156
|
+
for (const field of CHANNEL_DEFAULT_FIELDS)
|
|
157
|
+
defaults[field.key] = z.any().volatile();
|
|
158
|
+
fields.channelDefaults = z.object(defaults);
|
|
159
|
+
for (const name of CHANNELS) {
|
|
160
|
+
const channel = {};
|
|
161
|
+
for (const field of CHANNEL_CONFIG_FIELDS[name])
|
|
162
|
+
channel[field.key] = z.any().volatile();
|
|
163
|
+
if (Object.keys(channel).length > 0)
|
|
164
|
+
fields[name] = z.object(channel);
|
|
165
|
+
}
|
|
166
|
+
return fields;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Project a config onto the pane's field set: declared, non-secret keys only.
|
|
170
|
+
*
|
|
171
|
+
* Two callers, one guard. For a **read** the input is a materialized snapshot
|
|
172
|
+
* of our own config (a profile may legitimately carry `appSecret`, and it must
|
|
173
|
+
* not travel out through the snapshot the pane receives); for a **write** the
|
|
174
|
+
* input is untrusted JSON from the settings RPC, and an undeclared key would
|
|
175
|
+
* otherwise be preserved into the profile by schemastery's passthrough. Both
|
|
176
|
+
* are the same projection, so both get the same answer.
|
|
177
|
+
*
|
|
178
|
+
* `channels` is emitted unconditionally on the write path because an absent
|
|
179
|
+
* array resolves to `[]` (schemastery's array default), which would read as
|
|
180
|
+
* "activate nothing". Unknown channel names are dropped rather than refused:
|
|
181
|
+
* a name this version does not know is not a reason to reject the whole save.
|
|
182
|
+
*/
|
|
183
|
+
export function sectionOf(config, options = {}) {
|
|
184
|
+
const source = (config !== null && typeof config === "object" ? config : {});
|
|
185
|
+
const section = {};
|
|
186
|
+
const rawChannels = source.channels;
|
|
187
|
+
if (Array.isArray(rawChannels)) {
|
|
188
|
+
section.channels = rawChannels.filter((name) => CHANNELS.includes(name));
|
|
189
|
+
}
|
|
190
|
+
else if (options.defaultChannels !== false) {
|
|
191
|
+
section.channels = [...CHANNELS];
|
|
192
|
+
}
|
|
193
|
+
const defaults = {};
|
|
194
|
+
const rawDefaults = (source.channelDefaults ?? {});
|
|
195
|
+
for (const field of CHANNEL_DEFAULT_FIELDS) {
|
|
196
|
+
if (rawDefaults[field.key] !== undefined)
|
|
197
|
+
defaults[field.key] = rawDefaults[field.key];
|
|
198
|
+
}
|
|
199
|
+
if (Object.keys(defaults).length > 0)
|
|
200
|
+
section.channelDefaults = defaults;
|
|
201
|
+
for (const name of CHANNELS) {
|
|
202
|
+
const raw = (source[name] ?? {});
|
|
203
|
+
const projected = {};
|
|
204
|
+
for (const field of CHANNEL_CONFIG_FIELDS[name]) {
|
|
205
|
+
if (raw[field.key] !== undefined)
|
|
206
|
+
projected[field.key] = raw[field.key];
|
|
207
|
+
}
|
|
208
|
+
if (Object.keys(projected).length > 0)
|
|
209
|
+
section[name] = projected;
|
|
210
|
+
}
|
|
211
|
+
return section;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Layer one section over another: for each channel and for the shared defaults,
|
|
215
|
+
* the override's own keys win and the base's surviving keys are kept.
|
|
216
|
+
*
|
|
217
|
+
* This exists because of how `replace()` treats an omission — see the module
|
|
218
|
+
* doc. A caller that has a section in force and only wants to change part of it
|
|
219
|
+
* must merge, never pass the part alone: `feishu: { transport }` resets every
|
|
220
|
+
* other declared `feishu` field, and a section without `channels` resets the
|
|
221
|
+
* channel list itself, which switches every adapter off.
|
|
222
|
+
*/
|
|
223
|
+
export function mergeSections(base, override) {
|
|
224
|
+
const merged = {};
|
|
225
|
+
const channels = override.channels ?? base.channels;
|
|
226
|
+
if (channels !== undefined)
|
|
227
|
+
merged.channels = [...channels];
|
|
228
|
+
const defaults = { ...base.channelDefaults, ...override.channelDefaults };
|
|
229
|
+
if (Object.keys(defaults).length > 0)
|
|
230
|
+
merged.channelDefaults = defaults;
|
|
231
|
+
for (const name of CHANNELS) {
|
|
232
|
+
const fields = { ...base[name], ...override[name] };
|
|
233
|
+
if (Object.keys(fields).length > 0)
|
|
234
|
+
merged[name] = fields;
|
|
235
|
+
}
|
|
236
|
+
return merged;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Wire the plugin's config references to the host's settings service.
|
|
240
|
+
*
|
|
241
|
+
* Never throws. A host without `SettingsForms`, a context where the entry id
|
|
242
|
+
* cannot be resolved (a bare unit-test context, or a plugin mounted outside the
|
|
243
|
+
* loader) and a refused `replace` all degrade to `live: false` with one warning
|
|
244
|
+
* line — a settings integration that cannot work must never be the reason a
|
|
245
|
+
* bridge fails to start.
|
|
246
|
+
*/
|
|
247
|
+
export function installConnectSection(options) {
|
|
248
|
+
const { settings, config, ns, onChange } = options;
|
|
249
|
+
const warn = (message) => {
|
|
250
|
+
options.owner.logger?.warn?.(`connect: ${message}`);
|
|
251
|
+
};
|
|
252
|
+
const read = () => sectionOf(materializeConfig(config));
|
|
253
|
+
const section = read();
|
|
254
|
+
const replace = settings?.replace?.bind(settings);
|
|
255
|
+
if (replace === undefined || ns === undefined) {
|
|
256
|
+
// Two different situations, one outcome. No settings service at all is the
|
|
257
|
+
// normal bare-context case and needs no line. A *present* service with no
|
|
258
|
+
// resolvable entry id is a real misconfiguration: the pane would silently
|
|
259
|
+
// stop persisting, so say so once.
|
|
260
|
+
if (settings !== undefined && ns === undefined) {
|
|
261
|
+
warn("settings service is present but this plugin's profile entry id could not be resolved; " +
|
|
262
|
+
"the settings pane will fall back to its state file");
|
|
263
|
+
}
|
|
264
|
+
onChange(section);
|
|
265
|
+
return { live: false, section };
|
|
266
|
+
}
|
|
267
|
+
const handle = {
|
|
268
|
+
read,
|
|
269
|
+
write: async (value) => {
|
|
270
|
+
// Materialize first so the write path is shape-agnostic: handed this
|
|
271
|
+
// plugin's own ref-carrying `config` (a plausible caller mistake), a raw
|
|
272
|
+
// `sectionOf` would see `channels` as a reference rather than an array,
|
|
273
|
+
// read it as absent and project *every* channel — turning an unrelated
|
|
274
|
+
// save into "enable all adapters". Cheap, and it is a save, not a hot path.
|
|
275
|
+
await replace(ns, sectionOf(materializeConfig(value)));
|
|
276
|
+
// The loader commits the volatile paths into these references and emits
|
|
277
|
+
// `loader/volatile-update`; reconciling here as well makes a write take
|
|
278
|
+
// effect even on a host that does not dispatch it. A repeat apply of the
|
|
279
|
+
// same values is a no-op (`ChannelRuntime` diffs before it restarts
|
|
280
|
+
// anything), so the belt is free.
|
|
281
|
+
onChange(read());
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
onChange(section);
|
|
285
|
+
return { live: true, section, handle };
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=namespace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespace.js","sourceRoot":"","sources":["../../src/settings/namespace.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a stored secret the settings pane is allowed to show.
|
|
3
|
+
*
|
|
4
|
+
* The user has to be able to *confirm* what they configured — a pane that only
|
|
5
|
+
* ever says 「已配置」 cannot tell a correct appId from a typo — but a browser
|
|
6
|
+
* tab (or a screenshot of one) must never hold a usable credential. The two are
|
|
7
|
+
* reconciled here: the host masks the value **before it crosses the wire**, so
|
|
8
|
+
* what the pane receives is already unusable, and `maskSecret` is the only
|
|
9
|
+
* place that decides how much survives.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately pure and dependency-free: the host (`settings-service`) masks
|
|
12
|
+
* with it and the client bundle imports the same compiled module for the input
|
|
13
|
+
* `type`, so the two can never drift into disagreeing about which keys are
|
|
14
|
+
* confidential.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-connect/settings/secret-disclosure
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* - `full` — shown verbatim. For identifiers, not authenticators.
|
|
20
|
+
* - `mask` — head and tail survive, the middle does not.
|
|
21
|
+
* - `url` — a URL whose *query* carries the token, so only that part is masked.
|
|
22
|
+
*/
|
|
23
|
+
export type Disclosure = "full" | "mask" | "url";
|
|
24
|
+
/**
|
|
25
|
+
* Per-config-key policy, keyed by the channel config key (`CREDENTIAL`-style
|
|
26
|
+
* keys are unique across channels: no channel has two fields that share a name
|
|
27
|
+
* but want different treatment).
|
|
28
|
+
*
|
|
29
|
+
* `appId` / `clientId` are `full` on purpose. They are **identifiers**: they
|
|
30
|
+
* appear in the URL or body of every outbound API call, are readable in the
|
|
31
|
+
* vendor's own console, and grant nothing without the paired secret. Masking
|
|
32
|
+
* them would cost the user the one thing this pane is for — checking that the
|
|
33
|
+
* id they pasted is the one they meant — and buy no secrecy at all.
|
|
34
|
+
*/
|
|
35
|
+
export declare const SECRET_DISCLOSURE: Record<string, Disclosure>;
|
|
36
|
+
/** Policy for a config key; unknown keys are treated as confidential. */
|
|
37
|
+
export declare function disclosureOf(configKey: string): Disclosure;
|
|
38
|
+
/** True when the pane should render this key as a password input. */
|
|
39
|
+
export declare function isMaskedSecret(configKey: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The display preview for one stored secret. Never returns the input: for
|
|
42
|
+
* `mask` and `url` the returned string is lossy by construction, so a caller
|
|
43
|
+
* that echoes it to a browser cannot leak a usable credential.
|
|
44
|
+
*
|
|
45
|
+
* Surrounding whitespace is trimmed first — a pasted value commonly carries a
|
|
46
|
+
* trailing newline, and the mask should describe the stored value, not the
|
|
47
|
+
* paste.
|
|
48
|
+
*/
|
|
49
|
+
export declare function maskSecret(configKey: string, value: string): string;
|
|
50
|
+
//# sourceMappingURL=secret-disclosure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-disclosure.d.ts","sourceRoot":"","sources":["../../src/settings/secret-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAEjD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAQvD,CAAC;AAkBH,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAE1D;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEzD;AAqDD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAWnE"}
|