dsh-connect 0.7.2 → 0.9.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/README.i18n.yaml +2 -2
- package/README.md +196 -43
- package/README.zh.md +171 -42
- package/client/client.js +817 -0
- package/client/client.js.map +7 -0
- package/client/locale.mjs +227 -0
- package/client/panel-state.mjs +54 -0
- package/client/settings-client.mjs +389 -0
- 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/examples/minimal.config.json +16 -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/adapter.d.ts +59 -0
- package/lib/channels/dingtalk/adapter.d.ts.map +1 -0
- package/lib/channels/dingtalk/adapter.js +123 -0
- package/lib/channels/dingtalk/adapter.js.map +1 -0
- package/lib/channels/dingtalk/i18n.d.ts +14 -0
- package/lib/channels/dingtalk/i18n.d.ts.map +1 -0
- package/lib/channels/dingtalk/i18n.js +16 -0
- package/lib/channels/dingtalk/i18n.js.map +1 -0
- package/lib/channels/dingtalk/index.d.ts +127 -0
- package/lib/channels/dingtalk/index.d.ts.map +1 -0
- package/lib/channels/dingtalk/index.js +144 -0
- package/lib/channels/dingtalk/index.js.map +1 -0
- package/lib/channels/dingtalk/message.d.ts +49 -0
- package/lib/channels/dingtalk/message.d.ts.map +1 -0
- package/lib/channels/dingtalk/message.js +50 -0
- package/lib/channels/dingtalk/message.js.map +1 -0
- package/lib/channels/dingtalk/stomp.d.ts +36 -0
- package/lib/channels/dingtalk/stomp.d.ts.map +1 -0
- package/lib/channels/dingtalk/stomp.js +107 -0
- package/lib/channels/dingtalk/stomp.js.map +1 -0
- package/lib/channels/dingtalk/stream.d.ts +53 -0
- package/lib/channels/dingtalk/stream.d.ts.map +1 -0
- package/lib/channels/dingtalk/stream.js +199 -0
- package/lib/channels/dingtalk/stream.js.map +1 -0
- package/lib/channels/dingtalk/webhook.d.ts +84 -0
- package/lib/channels/dingtalk/webhook.d.ts.map +1 -0
- package/lib/channels/dingtalk/webhook.js +143 -0
- package/lib/channels/dingtalk/webhook.js.map +1 -0
- package/lib/channels/feishu/adapter.d.ts +133 -0
- package/lib/channels/feishu/adapter.d.ts.map +1 -0
- package/lib/channels/feishu/adapter.js +662 -0
- package/lib/channels/feishu/adapter.js.map +1 -0
- package/lib/channels/feishu/i18n.d.ts +30 -0
- package/lib/channels/feishu/i18n.d.ts.map +1 -0
- package/lib/channels/feishu/i18n.js +42 -0
- package/lib/channels/feishu/i18n.js.map +1 -0
- package/lib/channels/feishu/index.d.ts +82 -0
- package/lib/channels/feishu/index.d.ts.map +1 -0
- package/lib/channels/feishu/index.js +103 -0
- package/lib/channels/feishu/index.js.map +1 -0
- package/lib/channels/feishu/onboard.d.ts +16 -0
- package/lib/channels/feishu/onboard.d.ts.map +1 -0
- package/lib/channels/feishu/onboard.js +86 -0
- package/lib/channels/feishu/onboard.js.map +1 -0
- package/lib/channels/telegram/adapter.d.ts +70 -0
- package/lib/channels/telegram/adapter.d.ts.map +1 -0
- package/lib/channels/telegram/adapter.js +460 -0
- package/lib/channels/telegram/adapter.js.map +1 -0
- package/lib/channels/telegram/client.d.ts +115 -0
- package/lib/channels/telegram/client.d.ts.map +1 -0
- package/lib/channels/telegram/client.js +198 -0
- package/lib/channels/telegram/client.js.map +1 -0
- package/lib/channels/telegram/i18n.d.ts +14 -0
- package/lib/channels/telegram/i18n.d.ts.map +1 -0
- package/lib/channels/telegram/i18n.js +18 -0
- package/lib/channels/telegram/i18n.js.map +1 -0
- package/lib/channels/telegram/index.d.ts +46 -0
- package/lib/channels/telegram/index.d.ts.map +1 -0
- package/lib/channels/telegram/index.js +54 -0
- package/lib/channels/telegram/index.js.map +1 -0
- package/lib/channels/web/adapter.d.ts +103 -0
- package/lib/channels/web/adapter.d.ts.map +1 -0
- package/lib/channels/web/adapter.js +161 -0
- package/lib/channels/web/adapter.js.map +1 -0
- package/lib/channels/web/index.d.ts +47 -0
- package/lib/channels/web/index.d.ts.map +1 -0
- package/lib/channels/web/index.js +59 -0
- package/lib/channels/web/index.js.map +1 -0
- 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 +78 -11
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +312 -9
- package/lib/index.js.map +1 -1
- package/lib/runner.d.ts +55 -2
- 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 +17 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +47 -3
- 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 +62 -0
- package/lib/settings/channels.d.ts.map +1 -0
- package/lib/settings/channels.js +132 -0
- package/lib/settings/channels.js.map +1 -0
- package/lib/settings/credential-store.d.ts +85 -0
- package/lib/settings/credential-store.d.ts.map +1 -0
- package/lib/settings/credential-store.js +142 -0
- package/lib/settings/credential-store.js.map +1 -0
- package/lib/settings/index.d.ts +14 -0
- package/lib/settings/index.d.ts.map +1 -0
- package/lib/settings/index.js +14 -0
- package/lib/settings/index.js.map +1 -0
- package/lib/settings/namespace.d.ts +175 -0
- package/lib/settings/namespace.d.ts.map +1 -0
- package/lib/settings/namespace.js +200 -0
- package/lib/settings/namespace.js.map +1 -0
- package/lib/settings/rpc-client.d.ts +36 -0
- package/lib/settings/rpc-client.d.ts.map +1 -0
- package/lib/settings/rpc-client.js +45 -0
- package/lib/settings/rpc-client.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 +124 -0
- package/lib/settings/settings-model.d.ts.map +1 -0
- package/lib/settings/settings-model.js +118 -0
- package/lib/settings/settings-model.js.map +1 -0
- package/lib/settings/settings-rpc.d.ts +130 -0
- package/lib/settings/settings-rpc.d.ts.map +1 -0
- package/lib/settings/settings-rpc.js +233 -0
- package/lib/settings/settings-rpc.js.map +1 -0
- package/lib/settings/settings-service.d.ts +35 -0
- package/lib/settings/settings-service.d.ts.map +1 -0
- package/lib/settings/settings-service.js +171 -0
- package/lib/settings/settings-service.js.map +1 -0
- 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/package.json +56 -12
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side RPC helper for the dsh-connect web settings pane.
|
|
3
|
+
*
|
|
4
|
+
* The frontend talks to the host over the `/dsh-connect` channel; the channel
|
|
5
|
+
* returns `{ ok, value }`/`{ ok:false, error }` envelopes (see `settings-rpc.js`).
|
|
6
|
+
* This helper unwraps the envelope and throws a typed error on failure, so the
|
|
7
|
+
* UI can catch it and render a message. Pure + dependency-free for easy testing.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-connect/settings/rpc-client
|
|
10
|
+
*/
|
|
11
|
+
/** A typed RPC failure, surfaced to the settings UI. */
|
|
12
|
+
export class RpcError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "RpcError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Invoke one endpoint and unwrap the envelope (throws `RpcError` on failure). */
|
|
21
|
+
export function callRpc(rpcCall, endpoint, payload, signal) {
|
|
22
|
+
return rpcCall(endpoint, payload, signal).then((res) => {
|
|
23
|
+
if (res.ok)
|
|
24
|
+
return res.value;
|
|
25
|
+
const code = res.error?.code ?? "settings-failed";
|
|
26
|
+
throw new RpcError(code, res.error?.message ?? code);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Load the current settings snapshot via `settings.get`. */
|
|
30
|
+
export function loadSettings(rpcCall) {
|
|
31
|
+
return callRpc(rpcCall, "settings.get", {});
|
|
32
|
+
}
|
|
33
|
+
/** Persist the settings via `settings.save`. */
|
|
34
|
+
export function saveSettings(rpcCall, config) {
|
|
35
|
+
return callRpc(rpcCall, "settings.save", config);
|
|
36
|
+
}
|
|
37
|
+
/** Persist channel secrets to the credential store via `credentials.save`. */
|
|
38
|
+
export function saveCredentials(rpcCall, channel, values) {
|
|
39
|
+
return callRpc(rpcCall, "credentials.save", { channel, values });
|
|
40
|
+
}
|
|
41
|
+
/** Ask the host for a fresh status snapshot via `settings.status`. */
|
|
42
|
+
export function loadStatus(rpcCall) {
|
|
43
|
+
return callRpc(rpcCall, "settings.status", {});
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=rpc-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc-client.js","sourceRoot":"","sources":["../../src/settings/rpc-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAWH,wDAAwD;AACxD,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,IAAI,CAAS;IACtB,YAAY,IAAY,EAAE,OAAe;QACvC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,kFAAkF;AAClF,MAAM,UAAU,OAAO,CACrB,OAAgB,EAChB,QAAgB,EAChB,OAAiB,EACjB,MAAoB;IAEpB,OAAO,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;QACrD,IAAI,GAAG,CAAC,EAAE;YAAE,OAAO,GAAG,CAAC,KAAU,CAAC;QAClC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,EAAE,IAAI,IAAI,iBAAiB,CAAC;QAClD,MAAM,IAAI,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,EAAE,OAAO,IAAI,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC3C,OAAO,OAAO,CAAmB,OAAO,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;AAChE,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,YAAY,CAAC,OAAgB,EAAE,MAA+B;IAC5E,OAAO,OAAO,CAAmB,OAAO,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC;AACrE,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,eAAe,CAC7B,OAAgB,EAChB,OAAe,EACf,MAA8B;IAE9B,OAAO,OAAO,CAAmB,OAAO,EAAE,kBAAkB,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;AACrF,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,UAAU,CAAC,OAAgB;IACzC,OAAO,OAAO,CAAmB,OAAO,EAAE,iBAAiB,EAAE,EAAE,CAAC,CAAC;AACnE,CAAC"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
* Per-config-key policy, keyed by the channel config key (`CREDENTIAL`-style
|
|
20
|
+
* keys are unique across channels: no channel has two fields that share a name
|
|
21
|
+
* but want different treatment).
|
|
22
|
+
*
|
|
23
|
+
* `appId` / `clientId` are `full` on purpose. They are **identifiers**: they
|
|
24
|
+
* appear in the URL or body of every outbound API call, are readable in the
|
|
25
|
+
* vendor's own console, and grant nothing without the paired secret. Masking
|
|
26
|
+
* them would cost the user the one thing this pane is for — checking that the
|
|
27
|
+
* id they pasted is the one they meant — and buy no secrecy at all.
|
|
28
|
+
*/
|
|
29
|
+
export const SECRET_DISCLOSURE = Object.freeze({
|
|
30
|
+
appId: "full",
|
|
31
|
+
clientId: "full",
|
|
32
|
+
appSecret: "mask",
|
|
33
|
+
clientSecret: "mask",
|
|
34
|
+
botToken: "mask",
|
|
35
|
+
secret: "mask",
|
|
36
|
+
webhookUrl: "url",
|
|
37
|
+
});
|
|
38
|
+
/** Query parameters whose *value* is a credential and must not be echoed. */
|
|
39
|
+
const SECRET_QUERY_PARAM = /(secret|token|key|sign)/i;
|
|
40
|
+
/** Characters kept visible on each end of a masked value. */
|
|
41
|
+
const EDGE = 4;
|
|
42
|
+
/**
|
|
43
|
+
* Below this length there is no middle to hide — showing 4+4 of a 10-character
|
|
44
|
+
* value would print it in full — so short values collapse to a fixed-width
|
|
45
|
+
* mask, which also avoids leaking the length.
|
|
46
|
+
*/
|
|
47
|
+
const MIN_MASKABLE = 12;
|
|
48
|
+
/** The width of the all-bullets mask used for values too short to split. */
|
|
49
|
+
const BULLETS = "••••••";
|
|
50
|
+
/** Policy for a config key; unknown keys are treated as confidential. */
|
|
51
|
+
export function disclosureOf(configKey) {
|
|
52
|
+
return SECRET_DISCLOSURE[configKey] ?? "mask";
|
|
53
|
+
}
|
|
54
|
+
/** True when the pane should render this key as a password input. */
|
|
55
|
+
export function isMaskedSecret(configKey) {
|
|
56
|
+
return disclosureOf(configKey) !== "full";
|
|
57
|
+
}
|
|
58
|
+
/** `head…tail`, or the bullets mask when the value is too short to split. */
|
|
59
|
+
function maskMiddle(value) {
|
|
60
|
+
if (value.length < MIN_MASKABLE)
|
|
61
|
+
return BULLETS;
|
|
62
|
+
return value.slice(0, EDGE) + "…" + value.slice(-EDGE);
|
|
63
|
+
}
|
|
64
|
+
/** Escape a query-parameter name for use inside a `RegExp`. */
|
|
65
|
+
function escapeForRegExp(text) {
|
|
66
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Mask the credential-bearing query parameters of a URL, keeping the origin,
|
|
70
|
+
* path and the innocent parameters readable.
|
|
71
|
+
*
|
|
72
|
+
* Masking such a URL end-to-end would defeat the purpose: DingTalk's webhook is
|
|
73
|
+
* `https://oapi.dingtalk.com/robot/send?access_token=…`, so `https…bcde` tells
|
|
74
|
+
* the user nothing about whether they pasted the right robot. Falls back to the
|
|
75
|
+
* plain mask when the value isn't a parseable URL (a bare token, a typo, an
|
|
76
|
+
* empty-ish string).
|
|
77
|
+
*
|
|
78
|
+
* The masking is a surgical replace on the original text rather than a
|
|
79
|
+
* re-serialize through `URLSearchParams`, which would percent-encode the `…`
|
|
80
|
+
* into `%E2%80%A6` and render the preview unreadable.
|
|
81
|
+
*/
|
|
82
|
+
function maskUrl(value) {
|
|
83
|
+
let url;
|
|
84
|
+
try {
|
|
85
|
+
url = new URL(value);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return maskMiddle(value);
|
|
89
|
+
}
|
|
90
|
+
let masked = value;
|
|
91
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
92
|
+
if (!SECRET_QUERY_PARAM.test(key))
|
|
93
|
+
continue;
|
|
94
|
+
const pattern = new RegExp(`([?&]${escapeForRegExp(key)}=)([^&#]*)`, "g");
|
|
95
|
+
masked = masked.replace(pattern, (whole, prefix, held) => {
|
|
96
|
+
// Tolerate a percent-encoded token: decode to measure it, mask the
|
|
97
|
+
// decoded form, and splice the readable result back into the raw text.
|
|
98
|
+
let decoded = held;
|
|
99
|
+
try {
|
|
100
|
+
decoded = decodeURIComponent(held);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* not encoded — use as-is */
|
|
104
|
+
}
|
|
105
|
+
return decoded.length === 0 ? whole : prefix + maskMiddle(decoded);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return masked;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The display preview for one stored secret. Never returns the input: for
|
|
112
|
+
* `mask` and `url` the returned string is lossy by construction, so a caller
|
|
113
|
+
* that echoes it to a browser cannot leak a usable credential.
|
|
114
|
+
*
|
|
115
|
+
* Surrounding whitespace is trimmed first — a pasted value commonly carries a
|
|
116
|
+
* trailing newline, and the mask should describe the stored value, not the
|
|
117
|
+
* paste.
|
|
118
|
+
*/
|
|
119
|
+
export function maskSecret(configKey, value) {
|
|
120
|
+
const trimmed = value.trim();
|
|
121
|
+
if (trimmed.length === 0)
|
|
122
|
+
return "";
|
|
123
|
+
switch (disclosureOf(configKey)) {
|
|
124
|
+
case "full":
|
|
125
|
+
return trimmed;
|
|
126
|
+
case "url":
|
|
127
|
+
return maskUrl(trimmed);
|
|
128
|
+
default:
|
|
129
|
+
return maskMiddle(trimmed);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=secret-disclosure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-disclosure.js","sourceRoot":"","sources":["../../src/settings/secret-disclosure.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ChannelName } from "./channels.js";
|
|
2
|
+
import type { SettingsSnapshot } from "./settings-rpc.js";
|
|
3
|
+
/** Secret config-keys per channel, for rendering secret inputs. */
|
|
4
|
+
export declare const CHANNEL_SECRET_FIELDS: Record<ChannelName, string[]>;
|
|
5
|
+
/**
|
|
6
|
+
* A non-secret, user-editable per-channel config field. `kind` drives how the
|
|
7
|
+
* pane renders the control and how `coerceConfigValue` normalizes the raw input
|
|
8
|
+
* (text → string, number → finite number, boolean → false on empty, select →
|
|
9
|
+
* one of `options`).
|
|
10
|
+
*/
|
|
11
|
+
export interface ConfigField {
|
|
12
|
+
key: string;
|
|
13
|
+
kind: 'text' | 'number' | 'boolean' | 'select';
|
|
14
|
+
options?: string[];
|
|
15
|
+
/** Human label; defaults to the key if absent. */
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Non-secret per-channel fields the pane exposes, mirroring the keys used by
|
|
20
|
+
* each channel adapter config (see `examples/profile-cordis.patch.yml`). Order
|
|
21
|
+
* is the render order; keep secrets separate (`CHANNEL_SECRET_FIELDS`).
|
|
22
|
+
*/
|
|
23
|
+
export declare const CHANNEL_CONFIG_FIELDS: {
|
|
24
|
+
readonly feishu: readonly [{
|
|
25
|
+
readonly key: "transport";
|
|
26
|
+
readonly kind: "select";
|
|
27
|
+
readonly options: ["websocket", "webhook"];
|
|
28
|
+
readonly label: "transport";
|
|
29
|
+
}, {
|
|
30
|
+
readonly key: "requireMention";
|
|
31
|
+
readonly kind: "boolean";
|
|
32
|
+
readonly label: "requireMention";
|
|
33
|
+
}, {
|
|
34
|
+
readonly key: "dmMode";
|
|
35
|
+
readonly kind: "select";
|
|
36
|
+
readonly options: ["open", "allowlist", "pair", "disabled"];
|
|
37
|
+
readonly label: "dmMode";
|
|
38
|
+
}, {
|
|
39
|
+
readonly key: "language";
|
|
40
|
+
readonly kind: "select";
|
|
41
|
+
readonly options: ["zh", "en"];
|
|
42
|
+
readonly label: "language";
|
|
43
|
+
}, {
|
|
44
|
+
readonly key: "webhookPort";
|
|
45
|
+
readonly kind: "number";
|
|
46
|
+
readonly label: "webhookPort";
|
|
47
|
+
}, {
|
|
48
|
+
readonly key: "webhookPath";
|
|
49
|
+
readonly kind: "text";
|
|
50
|
+
readonly label: "webhookPath";
|
|
51
|
+
}];
|
|
52
|
+
readonly telegram: readonly [{
|
|
53
|
+
readonly key: "requireMention";
|
|
54
|
+
readonly kind: "boolean";
|
|
55
|
+
readonly label: "requireMention";
|
|
56
|
+
}, {
|
|
57
|
+
readonly key: "language";
|
|
58
|
+
readonly kind: "select";
|
|
59
|
+
readonly options: ["zh", "en"];
|
|
60
|
+
readonly label: "language";
|
|
61
|
+
}, {
|
|
62
|
+
readonly key: "pollingTimeoutSeconds";
|
|
63
|
+
readonly kind: "number";
|
|
64
|
+
readonly label: "pollingTimeoutSeconds";
|
|
65
|
+
}, {
|
|
66
|
+
readonly key: "baseUrl";
|
|
67
|
+
readonly kind: "text";
|
|
68
|
+
readonly label: "baseUrl";
|
|
69
|
+
}];
|
|
70
|
+
readonly dingtalk: readonly [{
|
|
71
|
+
readonly key: "language";
|
|
72
|
+
readonly kind: "select";
|
|
73
|
+
readonly options: ["zh", "en"];
|
|
74
|
+
readonly label: "language";
|
|
75
|
+
}, {
|
|
76
|
+
readonly key: "defaultAt";
|
|
77
|
+
readonly kind: "text";
|
|
78
|
+
readonly label: "defaultAt";
|
|
79
|
+
}];
|
|
80
|
+
readonly web: readonly [{
|
|
81
|
+
readonly key: "pollIntervalMs";
|
|
82
|
+
readonly kind: "number";
|
|
83
|
+
readonly label: "pollIntervalMs";
|
|
84
|
+
}];
|
|
85
|
+
};
|
|
86
|
+
/** Channel-agnostic keys applied to every channel that doesn't set its own. */
|
|
87
|
+
export declare const CHANNEL_DEFAULT_FIELDS: readonly ConfigField[];
|
|
88
|
+
/** Normalize a raw form input for a field `kind` (empty → undefined = not saved). */
|
|
89
|
+
export declare function coerceConfigValue(kind: ConfigField['kind'], raw: unknown): unknown;
|
|
90
|
+
/** The edit form the pane works on. */
|
|
91
|
+
export interface SettingsForm {
|
|
92
|
+
channels: ChannelName[];
|
|
93
|
+
channelDefaults: Record<string, unknown>;
|
|
94
|
+
channelConfigs: Record<string, Record<string, unknown>>;
|
|
95
|
+
/** Secret inputs the user is *typing*: written on save, never seeded by a read. */
|
|
96
|
+
secrets: Record<string, Record<string, string>>;
|
|
97
|
+
/**
|
|
98
|
+
* Whether a value is already stored for a secret key, for the placeholder.
|
|
99
|
+
* Separate from `secrets` because the two have opposite lifetimes: presence
|
|
100
|
+
* comes from the snapshot and the typed values start empty and are cleared
|
|
101
|
+
* after a save — a read must never be able to populate an input.
|
|
102
|
+
*/
|
|
103
|
+
secretPresence: Record<string, Record<string, boolean>>;
|
|
104
|
+
/**
|
|
105
|
+
* Host-masked previews of the stored secrets, so the user can confirm what
|
|
106
|
+
* they configured. Rendered as read-only text and **never** fed into
|
|
107
|
+
* `secrets`: a masked value is not a credential, and treating one as a value
|
|
108
|
+
* to save would overwrite a working secret with its own mask.
|
|
109
|
+
*/
|
|
110
|
+
secretPreviews: Record<string, Record<string, string>>;
|
|
111
|
+
settingsStatePath?: string;
|
|
112
|
+
/** True when the section lives in the settings namespace: a save is immediate and durable. */
|
|
113
|
+
live: boolean;
|
|
114
|
+
}
|
|
115
|
+
/** Build the initial form from a snapshot. */
|
|
116
|
+
export declare function snapshotToForm(snapshot: SettingsSnapshot): SettingsForm;
|
|
117
|
+
/** Build the non-secret config to persist via `settings.save`. */
|
|
118
|
+
export declare function buildConfigSave(form: SettingsForm): Record<string, unknown>;
|
|
119
|
+
/** Build the `credentials.save` payloads for every channel with secret values. */
|
|
120
|
+
export declare function buildCredentialSaves(form: SettingsForm): {
|
|
121
|
+
channel: string;
|
|
122
|
+
values: Record<string, string>;
|
|
123
|
+
}[];
|
|
124
|
+
//# sourceMappingURL=settings-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-model.d.ts","sourceRoot":"","sources":["../../src/settings/settings-model.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,EAE7B,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC/C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsB8B,CAAC;AAEjE,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,EAAE,SAAS,WAAW,EAGxD,CAAC;AAEF,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAclF;AAED,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,8FAA8F;IAC9F,IAAI,EAAE,OAAO,CAAC;CACf;AAED,8CAA8C;AAC9C,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,GAAG,YAAY,CAoBvE;AASD,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAU3E;AAED,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,EAAE,CAM9G"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side settings model: pure, dependency-free helpers that map between the
|
|
3
|
+
* host snapshot and the edit form, and build the save payloads. Kept separate from
|
|
4
|
+
* the React component so the client settings logic is unit-testable anywhere.
|
|
5
|
+
* The host-only secret-ref mapping lives on the server; the client only carries
|
|
6
|
+
* field names.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-connect/settings/settings-model
|
|
9
|
+
*/
|
|
10
|
+
import { CHANNEL_SECRET_KEYS } from "./credential-store.js";
|
|
11
|
+
/** Secret config-keys per channel, for rendering secret inputs. */
|
|
12
|
+
export const CHANNEL_SECRET_FIELDS = Object.fromEntries(Object.entries(CHANNEL_SECRET_KEYS).map(([ch, map]) => [ch, Object.keys(map ?? {})]));
|
|
13
|
+
/**
|
|
14
|
+
* Non-secret per-channel fields the pane exposes, mirroring the keys used by
|
|
15
|
+
* each channel adapter config (see `examples/profile-cordis.patch.yml`). Order
|
|
16
|
+
* is the render order; keep secrets separate (`CHANNEL_SECRET_FIELDS`).
|
|
17
|
+
*/
|
|
18
|
+
export const CHANNEL_CONFIG_FIELDS = {
|
|
19
|
+
feishu: [
|
|
20
|
+
{ key: 'transport', kind: 'select', options: ['websocket', 'webhook'], label: 'transport' },
|
|
21
|
+
{ key: 'requireMention', kind: 'boolean', label: 'requireMention' },
|
|
22
|
+
{ key: 'dmMode', kind: 'select', options: ['open', 'allowlist', 'pair', 'disabled'], label: 'dmMode' },
|
|
23
|
+
{ key: 'language', kind: 'select', options: ['zh', 'en'], label: 'language' },
|
|
24
|
+
{ key: 'webhookPort', kind: 'number', label: 'webhookPort' },
|
|
25
|
+
{ key: 'webhookPath', kind: 'text', label: 'webhookPath' },
|
|
26
|
+
],
|
|
27
|
+
telegram: [
|
|
28
|
+
{ key: 'requireMention', kind: 'boolean', label: 'requireMention' },
|
|
29
|
+
{ key: 'language', kind: 'select', options: ['zh', 'en'], label: 'language' },
|
|
30
|
+
{ key: 'pollingTimeoutSeconds', kind: 'number', label: 'pollingTimeoutSeconds' },
|
|
31
|
+
{ key: 'baseUrl', kind: 'text', label: 'baseUrl' },
|
|
32
|
+
],
|
|
33
|
+
dingtalk: [
|
|
34
|
+
{ key: 'language', kind: 'select', options: ['zh', 'en'], label: 'language' },
|
|
35
|
+
{ key: 'defaultAt', kind: 'text', label: 'defaultAt' },
|
|
36
|
+
],
|
|
37
|
+
web: [
|
|
38
|
+
{ key: 'pollIntervalMs', kind: 'number', label: 'pollIntervalMs' },
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
/** Channel-agnostic keys applied to every channel that doesn't set its own. */
|
|
42
|
+
export const CHANNEL_DEFAULT_FIELDS = [
|
|
43
|
+
{ key: 'language', kind: 'select', options: ['zh', 'en'], label: 'language' },
|
|
44
|
+
{ key: 'notifyLevel', kind: 'select', options: ['full', 'important', 'result'], label: 'notifyLevel' },
|
|
45
|
+
];
|
|
46
|
+
/** Normalize a raw form input for a field `kind` (empty → undefined = not saved). */
|
|
47
|
+
export function coerceConfigValue(kind, raw) {
|
|
48
|
+
if (raw === undefined || raw === null || raw === '')
|
|
49
|
+
return undefined;
|
|
50
|
+
switch (kind) {
|
|
51
|
+
case 'number': {
|
|
52
|
+
const n = Number(raw);
|
|
53
|
+
return Number.isFinite(n) ? n : undefined;
|
|
54
|
+
}
|
|
55
|
+
case 'boolean':
|
|
56
|
+
return typeof raw === 'boolean' ? raw : raw === true || raw === 'true';
|
|
57
|
+
case 'select':
|
|
58
|
+
return String(raw);
|
|
59
|
+
default:
|
|
60
|
+
return String(raw);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Build the initial form from a snapshot. */
|
|
64
|
+
export function snapshotToForm(snapshot) {
|
|
65
|
+
const config = snapshot.config ?? {};
|
|
66
|
+
const channels = (snapshot.enabled ?? []);
|
|
67
|
+
const channelConfigs = {};
|
|
68
|
+
for (const ch of channels)
|
|
69
|
+
channelConfigs[ch] = (config[ch] ?? {});
|
|
70
|
+
return {
|
|
71
|
+
channels,
|
|
72
|
+
channelDefaults: (config.channelDefaults ?? {}),
|
|
73
|
+
channelConfigs,
|
|
74
|
+
// Deliberately empty even though `secretPreviews` carries something: an
|
|
75
|
+
// input is a place to *type a new* secret, and prefilling it with the mask
|
|
76
|
+
// would either overwrite the stored secret with its own preview on save, or
|
|
77
|
+
// train the user to think a masked value is the real one. The preview is
|
|
78
|
+
// rendered beside the input instead.
|
|
79
|
+
secrets: {},
|
|
80
|
+
secretPresence: (snapshot.secrets ?? {}),
|
|
81
|
+
secretPreviews: (snapshot.secretPreviews ?? {}),
|
|
82
|
+
settingsStatePath: config.settingsStatePath,
|
|
83
|
+
live: snapshot.live === true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Drop keys whose value is `undefined`/`null` so cleared inputs aren't persisted. */
|
|
87
|
+
function stripEmpty(obj) {
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const [k, v] of Object.entries(obj))
|
|
90
|
+
if (v !== undefined && v !== null)
|
|
91
|
+
out[k] = v;
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** Build the non-secret config to persist via `settings.save`. */
|
|
95
|
+
export function buildConfigSave(form) {
|
|
96
|
+
const config = { channels: form.channels };
|
|
97
|
+
const defaults = stripEmpty(form.channelDefaults ?? {});
|
|
98
|
+
if (Object.keys(defaults).length > 0)
|
|
99
|
+
config.channelDefaults = defaults;
|
|
100
|
+
for (const [ch, cfg] of Object.entries(form.channelConfigs ?? {})) {
|
|
101
|
+
const clean = stripEmpty(cfg ?? {});
|
|
102
|
+
if (Object.keys(clean).length > 0)
|
|
103
|
+
config[ch] = clean;
|
|
104
|
+
}
|
|
105
|
+
if (form.settingsStatePath)
|
|
106
|
+
config.settingsStatePath = form.settingsStatePath;
|
|
107
|
+
return config;
|
|
108
|
+
}
|
|
109
|
+
/** Build the `credentials.save` payloads for every channel with secret values. */
|
|
110
|
+
export function buildCredentialSaves(form) {
|
|
111
|
+
const out = [];
|
|
112
|
+
for (const [ch, values] of Object.entries(form.secrets ?? {})) {
|
|
113
|
+
if (values && Object.keys(values).length > 0)
|
|
114
|
+
out.push({ channel: ch, values });
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=settings-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-model.js","sourceRoot":"","sources":["../../src/settings/settings-model.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side RPC facade for the dsh-connect web settings pane.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors dsh-im's host RPC pattern: a plugin registers a single channel on
|
|
5
|
+
* `ctx.connection.rpc.handle(...)` and a handler dispatches dotted endpoints,
|
|
6
|
+
* returning `{ ok: true, value }` on success or `{ ok: false, error }` on
|
|
7
|
+
* failure (see dsh-im `plugin-src/host/update-rpc.mjs`).
|
|
8
|
+
*
|
|
9
|
+
* Endpoints:
|
|
10
|
+
* - `settings.get` — current non-secret config + enabled channels + credential presence.
|
|
11
|
+
* - `settings.save` — persist non-secret config (payload must be non-empty).
|
|
12
|
+
* - `credentials.save` — persist secrets to the DSH credential store (payload
|
|
13
|
+
* `{ channel, values }`, values are {configKey: secret}).
|
|
14
|
+
* - `settings.status` — fresh status snapshot.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-connect/settings/settings-rpc
|
|
17
|
+
*/
|
|
18
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
19
|
+
/** RPC channel the settings pane calls. */
|
|
20
|
+
export declare const SETTINGS_RPC_CHANNEL = "/dsh-connect";
|
|
21
|
+
/** Endpoints the settings pane can invoke on the channel. */
|
|
22
|
+
export declare const SETTINGS_ENDPOINTS: readonly string[];
|
|
23
|
+
/** A settings pane `settings.get`/status snapshot. */
|
|
24
|
+
export interface SettingsSnapshot {
|
|
25
|
+
/** The current plugin config (non-secret fields). */
|
|
26
|
+
config: Record<string, unknown>;
|
|
27
|
+
/** The channel names currently enabled. */
|
|
28
|
+
enabled: string[];
|
|
29
|
+
/** Per-channel credential presence (true = stored & set). */
|
|
30
|
+
credentials: Record<string, boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Per-channel, per-config-key secret **presence** (true = a value is stored
|
|
33
|
+
* for that key) — never the values themselves. The pane uses it to tell an
|
|
34
|
+
* untouched input («已配置») from an empty one.
|
|
35
|
+
*
|
|
36
|
+
* This used to echo the values, which put an `appSecret` into browser state
|
|
37
|
+
* and every screenshot of the settings page. What leaves the host now is
|
|
38
|
+
* either this boolean or the masked {@link secretPreviews} below — a usable
|
|
39
|
+
* secret still leaves exactly once, when the adapter consumes it
|
|
40
|
+
* (`injectSecrets`).
|
|
41
|
+
*/
|
|
42
|
+
secrets?: Record<string, Record<string, boolean>>;
|
|
43
|
+
/**
|
|
44
|
+
* Per-channel, per-config-key **display preview**, for the user to confirm
|
|
45
|
+
* what they configured without being able to copy it back out.
|
|
46
|
+
*
|
|
47
|
+
* Already masked on the host by `maskSecret` (`./secret-disclosure.js`), so
|
|
48
|
+
* it is lossy by construction: identifiers such as an `appId` come through
|
|
49
|
+
* whole, an `appSecret` keeps only its first and last four characters, and a
|
|
50
|
+
* `webhookUrl` keeps its origin and path with the `access_token` in the query
|
|
51
|
+
* masked. A key is present here only when a value is stored for it, which
|
|
52
|
+
* makes an empty entry the same statement as `secrets[key] === false`.
|
|
53
|
+
*
|
|
54
|
+
* **Display-only.** The pane renders it as text; it must never be written
|
|
55
|
+
* back as a credential, and `buildCredentialSaves` ignores it — the secret
|
|
56
|
+
* inputs are still write-only and still start blank.
|
|
57
|
+
*/
|
|
58
|
+
secretPreviews?: Record<string, Record<string, string>>;
|
|
59
|
+
/**
|
|
60
|
+
* True when the `dsh-connect` settings namespace is live, i.e. this config is
|
|
61
|
+
* stored in `$DSH_HOME/settings.yaml` and a save takes effect immediately.
|
|
62
|
+
* False means the legacy JSON state file is behind this pane: the settings
|
|
63
|
+
* path control still applies, and a save needs a restart.
|
|
64
|
+
*/
|
|
65
|
+
live?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** The service backing the RPC; supplied by the web-settings integration. */
|
|
68
|
+
export interface SettingsService {
|
|
69
|
+
get(): Promise<SettingsSnapshot>;
|
|
70
|
+
save(config: Record<string, unknown>): Promise<SettingsSnapshot>;
|
|
71
|
+
/** Persist channel secrets ({configKey: secret}) to the credential store. */
|
|
72
|
+
saveCredentials?(channel: string, values: Record<string, string>): Promise<SettingsSnapshot>;
|
|
73
|
+
status(): Promise<SettingsSnapshot>;
|
|
74
|
+
}
|
|
75
|
+
/** Build the channel handler for a settings service. */
|
|
76
|
+
export declare function createSettingsRpcHandler(service: SettingsService): (endpoint: string, payload: unknown, signal?: {
|
|
77
|
+
aborted?: boolean;
|
|
78
|
+
}) => Promise<{
|
|
79
|
+
ok: boolean;
|
|
80
|
+
value: SettingsSnapshot;
|
|
81
|
+
error?: undefined;
|
|
82
|
+
} | {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
error: {
|
|
85
|
+
code: any;
|
|
86
|
+
message: any;
|
|
87
|
+
};
|
|
88
|
+
value?: undefined;
|
|
89
|
+
}>;
|
|
90
|
+
/** The Host/Origin + browser-auth fence exposed by the host `connection` service. */
|
|
91
|
+
interface ConnectionFence {
|
|
92
|
+
/** HTTP status to reject with, or `undefined` to allow the request through. */
|
|
93
|
+
requestRejection?(request: IncomingMessage): number | undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build the raw HTTP handler for {@link SETTINGS_RPC_CHANNEL}. Mirrors the
|
|
97
|
+
* host's own Connection RPC adapter (method/content-type/envelope checks plus
|
|
98
|
+
* the Host/Origin and browser-auth fence) so the pane sees the same protocol
|
|
99
|
+
* and the same failure codes as a first-party channel.
|
|
100
|
+
*/
|
|
101
|
+
export declare function createSettingsHttpHandler(service: SettingsService, connection?: ConnectionFence): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Mount the settings RPC channel on the host webserver.
|
|
104
|
+
*
|
|
105
|
+
* Mounts a `prefix` route for {@link SETTINGS_RPC_CHANNEL} directly on the
|
|
106
|
+
* `webServer` host service — the same thing the host's own
|
|
107
|
+
* `connection.rpc.handle()` does internally (`owner.webServer.register(route)`
|
|
108
|
+
* with the `/api` fence), and the same thing `dsh-api-gateway` does for its
|
|
109
|
+
* WebSocket route.
|
|
110
|
+
*
|
|
111
|
+
* Why not `connection.rpc.handle()`: the host's `HostConnectionService` is
|
|
112
|
+
* context-tracked, so its `get rpc()` captures `this.ctx`. cordis resolves that
|
|
113
|
+
* read to a *shadow* of the service's construction context, and service
|
|
114
|
+
* resolution then walks **that** fiber chain — which never contains the
|
|
115
|
+
* `webServer` provider (a sibling fiber). The call therefore dies with
|
|
116
|
+
* `cannot get property "webServer" without inject`, the route is never mounted,
|
|
117
|
+
* and every `settings.*` call falls through to the static fallback (HTTP 405),
|
|
118
|
+
* leaving the pane stuck on "加载中". Injecting `webServer` into the calling
|
|
119
|
+
* scope — or declaring it in the plugin's `inject` — does not help, because the
|
|
120
|
+
* shadow overrides the caller's fiber.
|
|
121
|
+
*
|
|
122
|
+
* @param ctx - an injection scope holding `connection` and `webServer`.
|
|
123
|
+
* @param options - the settings service backing the channel.
|
|
124
|
+
* @returns a disposer removing the route (no-op if the host has no webserver).
|
|
125
|
+
*/
|
|
126
|
+
export declare function installSettingsRpc(ctx: unknown, options: {
|
|
127
|
+
service: SettingsService;
|
|
128
|
+
}): () => void;
|
|
129
|
+
export {};
|
|
130
|
+
//# sourceMappingURL=settings-rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-rpc.d.ts","sourceRoot":"","sources":["../../src/settings/settings-rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AAEnD,6DAA6D;AAC7D,eAAO,MAAM,kBAAkB,mBAA0F,CAAC;AAU1H,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,2CAA2C;IAC3C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClD;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,GAAG,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACjC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACjE,6EAA6E;IAC7E,eAAe,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC7F,MAAM,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACrC;AAeD,wDAAwD;AACxD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,eAAe,IACjD,UAAU,MAAM,EAAE,SAAS,OAAO,EAAE,SAAS;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;;;;;;;GAoBjF;AAmBD,qFAAqF;AACrF,UAAU,eAAe;IACvB,+EAA+E;IAC/E,gBAAgB,CAAC,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAAC;CACjE;AA+ED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,eAAe,IAGpD,KAAK,eAAe,EAAE,KAAK,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC,CAgEpG;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,GAAG,MAAM,IAAI,CAiBlG"}
|