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.
Files changed (153) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +196 -43
  3. package/README.zh.md +171 -42
  4. package/client/client.js +817 -0
  5. package/client/client.js.map +7 -0
  6. package/client/locale.mjs +227 -0
  7. package/client/panel-state.mjs +54 -0
  8. package/client/settings-client.mjs +389 -0
  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/examples/minimal.config.json +16 -0
  16. package/lib/binding.d.ts.map +1 -1
  17. package/lib/binding.js +2 -2
  18. package/lib/binding.js.map +1 -1
  19. package/lib/channels/dingtalk/adapter.d.ts +59 -0
  20. package/lib/channels/dingtalk/adapter.d.ts.map +1 -0
  21. package/lib/channels/dingtalk/adapter.js +123 -0
  22. package/lib/channels/dingtalk/adapter.js.map +1 -0
  23. package/lib/channels/dingtalk/i18n.d.ts +14 -0
  24. package/lib/channels/dingtalk/i18n.d.ts.map +1 -0
  25. package/lib/channels/dingtalk/i18n.js +16 -0
  26. package/lib/channels/dingtalk/i18n.js.map +1 -0
  27. package/lib/channels/dingtalk/index.d.ts +127 -0
  28. package/lib/channels/dingtalk/index.d.ts.map +1 -0
  29. package/lib/channels/dingtalk/index.js +144 -0
  30. package/lib/channels/dingtalk/index.js.map +1 -0
  31. package/lib/channels/dingtalk/message.d.ts +49 -0
  32. package/lib/channels/dingtalk/message.d.ts.map +1 -0
  33. package/lib/channels/dingtalk/message.js +50 -0
  34. package/lib/channels/dingtalk/message.js.map +1 -0
  35. package/lib/channels/dingtalk/stomp.d.ts +36 -0
  36. package/lib/channels/dingtalk/stomp.d.ts.map +1 -0
  37. package/lib/channels/dingtalk/stomp.js +107 -0
  38. package/lib/channels/dingtalk/stomp.js.map +1 -0
  39. package/lib/channels/dingtalk/stream.d.ts +53 -0
  40. package/lib/channels/dingtalk/stream.d.ts.map +1 -0
  41. package/lib/channels/dingtalk/stream.js +199 -0
  42. package/lib/channels/dingtalk/stream.js.map +1 -0
  43. package/lib/channels/dingtalk/webhook.d.ts +84 -0
  44. package/lib/channels/dingtalk/webhook.d.ts.map +1 -0
  45. package/lib/channels/dingtalk/webhook.js +143 -0
  46. package/lib/channels/dingtalk/webhook.js.map +1 -0
  47. package/lib/channels/feishu/adapter.d.ts +133 -0
  48. package/lib/channels/feishu/adapter.d.ts.map +1 -0
  49. package/lib/channels/feishu/adapter.js +662 -0
  50. package/lib/channels/feishu/adapter.js.map +1 -0
  51. package/lib/channels/feishu/i18n.d.ts +30 -0
  52. package/lib/channels/feishu/i18n.d.ts.map +1 -0
  53. package/lib/channels/feishu/i18n.js +42 -0
  54. package/lib/channels/feishu/i18n.js.map +1 -0
  55. package/lib/channels/feishu/index.d.ts +82 -0
  56. package/lib/channels/feishu/index.d.ts.map +1 -0
  57. package/lib/channels/feishu/index.js +103 -0
  58. package/lib/channels/feishu/index.js.map +1 -0
  59. package/lib/channels/feishu/onboard.d.ts +16 -0
  60. package/lib/channels/feishu/onboard.d.ts.map +1 -0
  61. package/lib/channels/feishu/onboard.js +86 -0
  62. package/lib/channels/feishu/onboard.js.map +1 -0
  63. package/lib/channels/telegram/adapter.d.ts +70 -0
  64. package/lib/channels/telegram/adapter.d.ts.map +1 -0
  65. package/lib/channels/telegram/adapter.js +460 -0
  66. package/lib/channels/telegram/adapter.js.map +1 -0
  67. package/lib/channels/telegram/client.d.ts +115 -0
  68. package/lib/channels/telegram/client.d.ts.map +1 -0
  69. package/lib/channels/telegram/client.js +198 -0
  70. package/lib/channels/telegram/client.js.map +1 -0
  71. package/lib/channels/telegram/i18n.d.ts +14 -0
  72. package/lib/channels/telegram/i18n.d.ts.map +1 -0
  73. package/lib/channels/telegram/i18n.js +18 -0
  74. package/lib/channels/telegram/i18n.js.map +1 -0
  75. package/lib/channels/telegram/index.d.ts +46 -0
  76. package/lib/channels/telegram/index.d.ts.map +1 -0
  77. package/lib/channels/telegram/index.js +54 -0
  78. package/lib/channels/telegram/index.js.map +1 -0
  79. package/lib/channels/web/adapter.d.ts +103 -0
  80. package/lib/channels/web/adapter.d.ts.map +1 -0
  81. package/lib/channels/web/adapter.js +161 -0
  82. package/lib/channels/web/adapter.js.map +1 -0
  83. package/lib/channels/web/index.d.ts +47 -0
  84. package/lib/channels/web/index.d.ts.map +1 -0
  85. package/lib/channels/web/index.js +59 -0
  86. package/lib/channels/web/index.js.map +1 -0
  87. package/lib/chat-key.d.ts +29 -0
  88. package/lib/chat-key.d.ts.map +1 -0
  89. package/lib/chat-key.js +38 -0
  90. package/lib/chat-key.js.map +1 -0
  91. package/lib/index.d.ts +78 -11
  92. package/lib/index.d.ts.map +1 -1
  93. package/lib/index.js +312 -9
  94. package/lib/index.js.map +1 -1
  95. package/lib/runner.d.ts +55 -2
  96. package/lib/runner.d.ts.map +1 -1
  97. package/lib/runner.js +158 -23
  98. package/lib/runner.js.map +1 -1
  99. package/lib/scheduler.d.ts.map +1 -1
  100. package/lib/scheduler.js +2 -2
  101. package/lib/scheduler.js.map +1 -1
  102. package/lib/service.d.ts +17 -1
  103. package/lib/service.d.ts.map +1 -1
  104. package/lib/service.js +47 -3
  105. package/lib/service.js.map +1 -1
  106. package/lib/settings/channel-runtime.d.ts +75 -0
  107. package/lib/settings/channel-runtime.d.ts.map +1 -0
  108. package/lib/settings/channel-runtime.js +165 -0
  109. package/lib/settings/channel-runtime.js.map +1 -0
  110. package/lib/settings/channels.d.ts +62 -0
  111. package/lib/settings/channels.d.ts.map +1 -0
  112. package/lib/settings/channels.js +132 -0
  113. package/lib/settings/channels.js.map +1 -0
  114. package/lib/settings/credential-store.d.ts +85 -0
  115. package/lib/settings/credential-store.d.ts.map +1 -0
  116. package/lib/settings/credential-store.js +142 -0
  117. package/lib/settings/credential-store.js.map +1 -0
  118. package/lib/settings/index.d.ts +14 -0
  119. package/lib/settings/index.d.ts.map +1 -0
  120. package/lib/settings/index.js +14 -0
  121. package/lib/settings/index.js.map +1 -0
  122. package/lib/settings/namespace.d.ts +175 -0
  123. package/lib/settings/namespace.d.ts.map +1 -0
  124. package/lib/settings/namespace.js +200 -0
  125. package/lib/settings/namespace.js.map +1 -0
  126. package/lib/settings/rpc-client.d.ts +36 -0
  127. package/lib/settings/rpc-client.d.ts.map +1 -0
  128. package/lib/settings/rpc-client.js +45 -0
  129. package/lib/settings/rpc-client.js.map +1 -0
  130. package/lib/settings/secret-disclosure.d.ts +50 -0
  131. package/lib/settings/secret-disclosure.d.ts.map +1 -0
  132. package/lib/settings/secret-disclosure.js +132 -0
  133. package/lib/settings/secret-disclosure.js.map +1 -0
  134. package/lib/settings/settings-model.d.ts +124 -0
  135. package/lib/settings/settings-model.d.ts.map +1 -0
  136. package/lib/settings/settings-model.js +118 -0
  137. package/lib/settings/settings-model.js.map +1 -0
  138. package/lib/settings/settings-rpc.d.ts +130 -0
  139. package/lib/settings/settings-rpc.d.ts.map +1 -0
  140. package/lib/settings/settings-rpc.js +233 -0
  141. package/lib/settings/settings-rpc.js.map +1 -0
  142. package/lib/settings/settings-service.d.ts +35 -0
  143. package/lib/settings/settings-service.d.ts.map +1 -0
  144. package/lib/settings/settings-service.js +171 -0
  145. package/lib/settings/settings-service.js.map +1 -0
  146. package/lib/state-dir.d.ts +28 -0
  147. package/lib/state-dir.d.ts.map +1 -0
  148. package/lib/state-dir.js +33 -0
  149. package/lib/state-dir.js.map +1 -0
  150. package/lib/stream.d.ts.map +1 -1
  151. package/lib/stream.js +3 -2
  152. package/lib/stream.js.map +1 -1
  153. 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"}