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.
Files changed (117) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +236 -18
  3. package/README.zh.md +185 -18
  4. package/client/client.js +517 -84
  5. package/client/client.js.map +4 -4
  6. package/client/locale.mjs +227 -0
  7. package/client/panel-state.mjs +54 -0
  8. package/client/settings-client.mjs +282 -61
  9. package/docs/images/settings-advanced-en.png +0 -0
  10. package/docs/images/settings-advanced-zh.png +0 -0
  11. package/docs/images/settings-defaults-en.png +0 -0
  12. package/docs/images/settings-defaults-zh.png +0 -0
  13. package/docs/images/settings-overview-en.png +0 -0
  14. package/docs/images/settings-overview-zh.png +0 -0
  15. package/lib/binding.d.ts.map +1 -1
  16. package/lib/binding.js +2 -2
  17. package/lib/binding.js.map +1 -1
  18. package/lib/channels/dingtalk/index.d.ts +49 -49
  19. package/lib/channels/dingtalk/index.d.ts.map +1 -1
  20. package/lib/channels/dingtalk/message.d.ts.map +1 -1
  21. package/lib/channels/dingtalk/message.js +7 -0
  22. package/lib/channels/dingtalk/message.js.map +1 -1
  23. package/lib/channels/feishu/adapter.d.ts +14 -13
  24. package/lib/channels/feishu/adapter.d.ts.map +1 -1
  25. package/lib/channels/feishu/adapter.js +122 -56
  26. package/lib/channels/feishu/adapter.js.map +1 -1
  27. package/lib/channels/feishu/i18n.d.ts +2 -0
  28. package/lib/channels/feishu/i18n.d.ts.map +1 -1
  29. package/lib/channels/feishu/i18n.js +2 -0
  30. package/lib/channels/feishu/i18n.js.map +1 -1
  31. package/lib/channels/feishu/index.d.ts +48 -27
  32. package/lib/channels/feishu/index.d.ts.map +1 -1
  33. package/lib/channels/feishu/index.js +32 -6
  34. package/lib/channels/feishu/index.js.map +1 -1
  35. package/lib/channels/telegram/adapter.d.ts.map +1 -1
  36. package/lib/channels/telegram/adapter.js +7 -1
  37. package/lib/channels/telegram/adapter.js.map +1 -1
  38. package/lib/channels/telegram/index.d.ts +13 -13
  39. package/lib/channels/telegram/index.d.ts.map +1 -1
  40. package/lib/channels/web/adapter.d.ts +3 -1
  41. package/lib/channels/web/adapter.d.ts.map +1 -1
  42. package/lib/channels/web/adapter.js +3 -1
  43. package/lib/channels/web/adapter.js.map +1 -1
  44. package/lib/channels/web/index.d.ts +5 -5
  45. package/lib/channels/web/index.d.ts.map +1 -1
  46. package/lib/chat-key.d.ts +29 -0
  47. package/lib/chat-key.d.ts.map +1 -0
  48. package/lib/chat-key.js +38 -0
  49. package/lib/chat-key.js.map +1 -0
  50. package/lib/index.d.ts +57 -63
  51. package/lib/index.d.ts.map +1 -1
  52. package/lib/index.js +330 -55
  53. package/lib/index.js.map +1 -1
  54. package/lib/interaction.d.ts +90 -50
  55. package/lib/interaction.d.ts.map +1 -1
  56. package/lib/interaction.js +233 -272
  57. package/lib/interaction.js.map +1 -1
  58. package/lib/retry.d.ts.map +1 -1
  59. package/lib/retry.js +7 -1
  60. package/lib/retry.js.map +1 -1
  61. package/lib/runner.d.ts +54 -0
  62. package/lib/runner.d.ts.map +1 -1
  63. package/lib/runner.js +158 -23
  64. package/lib/runner.js.map +1 -1
  65. package/lib/scheduler.d.ts.map +1 -1
  66. package/lib/scheduler.js +2 -2
  67. package/lib/scheduler.js.map +1 -1
  68. package/lib/service.d.ts +16 -0
  69. package/lib/service.d.ts.map +1 -1
  70. package/lib/service.js +46 -4
  71. package/lib/service.js.map +1 -1
  72. package/lib/settings/channel-runtime.d.ts +75 -0
  73. package/lib/settings/channel-runtime.d.ts.map +1 -0
  74. package/lib/settings/channel-runtime.js +165 -0
  75. package/lib/settings/channel-runtime.js.map +1 -0
  76. package/lib/settings/channels.d.ts +11 -0
  77. package/lib/settings/channels.d.ts.map +1 -1
  78. package/lib/settings/channels.js +31 -0
  79. package/lib/settings/channels.js.map +1 -1
  80. package/lib/settings/credential-store.d.ts +52 -6
  81. package/lib/settings/credential-store.d.ts.map +1 -1
  82. package/lib/settings/credential-store.js +92 -18
  83. package/lib/settings/credential-store.js.map +1 -1
  84. package/lib/settings/legacy-import.d.ts +190 -0
  85. package/lib/settings/legacy-import.d.ts.map +1 -0
  86. package/lib/settings/legacy-import.js +373 -0
  87. package/lib/settings/legacy-import.js.map +1 -0
  88. package/lib/settings/namespace.d.ts +263 -0
  89. package/lib/settings/namespace.d.ts.map +1 -0
  90. package/lib/settings/namespace.js +287 -0
  91. package/lib/settings/namespace.js.map +1 -0
  92. package/lib/settings/secret-disclosure.d.ts +50 -0
  93. package/lib/settings/secret-disclosure.d.ts.map +1 -0
  94. package/lib/settings/secret-disclosure.js +132 -0
  95. package/lib/settings/secret-disclosure.js.map +1 -0
  96. package/lib/settings/settings-model.d.ts +17 -0
  97. package/lib/settings/settings-model.d.ts.map +1 -1
  98. package/lib/settings/settings-model.js +18 -1
  99. package/lib/settings/settings-model.js.map +1 -1
  100. package/lib/settings/settings-rpc.d.ts +71 -3
  101. package/lib/settings/settings-rpc.d.ts.map +1 -1
  102. package/lib/settings/settings-rpc.js +167 -24
  103. package/lib/settings/settings-rpc.js.map +1 -1
  104. package/lib/settings/settings-service.d.ts +22 -1
  105. package/lib/settings/settings-service.d.ts.map +1 -1
  106. package/lib/settings/settings-service.js +145 -8
  107. package/lib/settings/settings-service.js.map +1 -1
  108. package/lib/state-dir.d.ts +28 -0
  109. package/lib/state-dir.d.ts.map +1 -0
  110. package/lib/state-dir.js +33 -0
  111. package/lib/state-dir.js.map +1 -0
  112. package/lib/stream.d.ts.map +1 -1
  113. package/lib/stream.js +3 -2
  114. package/lib/stream.js.map +1 -1
  115. package/lib/types.d.ts +14 -1
  116. package/lib/types.d.ts.map +1 -1
  117. 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"}