dsh-zen-remote 2.2.9 → 2.3.1

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 (149) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/docs/interface.md +1 -1
  4. package/docs/remote-access.md +20 -0
  5. package/lan-gate.mjs +11 -1
  6. package/lib/adapters/dsh-better-sidebar/index.js +6 -2
  7. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  8. package/lib/adapters/dsh-better-sidebar/methods.js +3 -0
  9. package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -1
  10. package/lib/admin-routes.js +33 -2
  11. package/lib/admin-routes.js.map +1 -1
  12. package/lib/client.js +3289 -6
  13. package/lib/config.js +70 -0
  14. package/lib/config.js.map +1 -1
  15. package/lib/index.js +232 -2
  16. package/lib/index.js.map +1 -1
  17. package/lib/intercept.js +43 -3
  18. package/lib/intercept.js.map +1 -1
  19. package/lib/lan-gate-server.cjs +29 -3
  20. package/lib/merge-streams.js +42 -12
  21. package/lib/merge-streams.js.map +1 -1
  22. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +1 -1
  23. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  24. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -1
  25. package/lib/types/admin-routes.d.ts +13 -0
  26. package/lib/types/admin-routes.d.ts.map +1 -1
  27. package/lib/types/client/index.d.ts.map +1 -1
  28. package/lib/types/client/locales.d.ts +204 -1
  29. package/lib/types/client/locales.d.ts.map +1 -1
  30. package/lib/types/client/wizard/GatewayWizard.d.ts +29 -0
  31. package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -0
  32. package/lib/types/client/wizard/WizardCard.d.ts +30 -0
  33. package/lib/types/client/wizard/WizardCard.d.ts.map +1 -0
  34. package/lib/types/client/wizard/cards/MigrateCard.d.ts +15 -0
  35. package/lib/types/client/wizard/cards/MigrateCard.d.ts.map +1 -0
  36. package/lib/types/client/wizard/cards/PortForwardCard.d.ts +15 -0
  37. package/lib/types/client/wizard/cards/PortForwardCard.d.ts.map +1 -0
  38. package/lib/types/client/wizard/cards/ProxyCard.d.ts +19 -0
  39. package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -0
  40. package/lib/types/client/wizard/cards/TokenCard.d.ts +20 -0
  41. package/lib/types/client/wizard/cards/TokenCard.d.ts.map +1 -0
  42. package/lib/types/client/wizard/register-wizard.d.ts +51 -0
  43. package/lib/types/client/wizard/register-wizard.d.ts.map +1 -0
  44. package/lib/types/client/wizard/wizard-css.d.ts +10 -0
  45. package/lib/types/client/wizard/wizard-css.d.ts.map +1 -0
  46. package/lib/types/client-data/qr.d.ts +20 -0
  47. package/lib/types/client-data/qr.d.ts.map +1 -0
  48. package/lib/types/client-data/settings-form.d.ts +7 -2
  49. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  50. package/lib/types/client-data/wizard.d.ts +247 -0
  51. package/lib/types/client-data/wizard.d.ts.map +1 -0
  52. package/lib/types/config.d.ts +27 -0
  53. package/lib/types/config.d.ts.map +1 -1
  54. package/lib/types/index.d.ts.map +1 -1
  55. package/lib/types/intercept.d.ts.map +1 -1
  56. package/lib/types/merge-streams.d.ts.map +1 -1
  57. package/lib/types/wizard/binary-io.d.ts +111 -0
  58. package/lib/types/wizard/binary-io.d.ts.map +1 -0
  59. package/lib/types/wizard/binary.d.ts +118 -0
  60. package/lib/types/wizard/binary.d.ts.map +1 -0
  61. package/lib/types/wizard/caddy-process.d.ts +101 -0
  62. package/lib/types/wizard/caddy-process.d.ts.map +1 -0
  63. package/lib/types/wizard/caddy.d.ts +53 -0
  64. package/lib/types/wizard/caddy.d.ts.map +1 -0
  65. package/lib/types/wizard/cloudflare.d.ts +112 -0
  66. package/lib/types/wizard/cloudflare.d.ts.map +1 -0
  67. package/lib/types/wizard/cloudflared.d.ts +71 -0
  68. package/lib/types/wizard/cloudflared.d.ts.map +1 -0
  69. package/lib/types/wizard/probe.d.ts +66 -0
  70. package/lib/types/wizard/probe.d.ts.map +1 -0
  71. package/lib/types/wizard/proxy-snippets.d.ts +60 -0
  72. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -0
  73. package/lib/types/wizard/routes.d.ts +152 -0
  74. package/lib/types/wizard/routes.d.ts.map +1 -0
  75. package/lib/types/wizard/source-detect.d.ts +53 -0
  76. package/lib/types/wizard/source-detect.d.ts.map +1 -0
  77. package/lib/types/wizard/status.d.ts +115 -0
  78. package/lib/types/wizard/status.d.ts.map +1 -0
  79. package/lib/types/wizard/token-store.d.ts +27 -0
  80. package/lib/types/wizard/token-store.d.ts.map +1 -0
  81. package/lib/types/wizard/tunnel-orchestrator.d.ts +90 -0
  82. package/lib/types/wizard/tunnel-orchestrator.d.ts.map +1 -0
  83. package/lib/types/wizard/verify.d.ts +97 -0
  84. package/lib/types/wizard/verify.d.ts.map +1 -0
  85. package/lib/wizard/binary-io.js +386 -0
  86. package/lib/wizard/binary-io.js.map +1 -0
  87. package/lib/wizard/binary.js +138 -0
  88. package/lib/wizard/binary.js.map +1 -0
  89. package/lib/wizard/caddy-process.js +162 -0
  90. package/lib/wizard/caddy-process.js.map +1 -0
  91. package/lib/wizard/caddy.js +80 -0
  92. package/lib/wizard/caddy.js.map +1 -0
  93. package/lib/wizard/cloudflare.js +334 -0
  94. package/lib/wizard/cloudflare.js.map +1 -0
  95. package/lib/wizard/cloudflared.js +158 -0
  96. package/lib/wizard/cloudflared.js.map +1 -0
  97. package/lib/wizard/probe.js +118 -0
  98. package/lib/wizard/probe.js.map +1 -0
  99. package/lib/wizard/proxy-snippets.js +112 -0
  100. package/lib/wizard/proxy-snippets.js.map +1 -0
  101. package/lib/wizard/routes.js +462 -0
  102. package/lib/wizard/routes.js.map +1 -0
  103. package/lib/wizard/source-detect.js +77 -0
  104. package/lib/wizard/source-detect.js.map +1 -0
  105. package/lib/wizard/status.js +94 -0
  106. package/lib/wizard/status.js.map +1 -0
  107. package/lib/wizard/token-store.js +71 -0
  108. package/lib/wizard/token-store.js.map +1 -0
  109. package/lib/wizard/tunnel-orchestrator.js +155 -0
  110. package/lib/wizard/tunnel-orchestrator.js.map +1 -0
  111. package/lib/wizard/verify.js +171 -0
  112. package/lib/wizard/verify.js.map +1 -0
  113. package/package.json +37 -37
  114. package/scripts/check-settings-form.mjs +5 -4
  115. package/scripts/check-wizard-ui.mjs +690 -0
  116. package/src/adapters/dsh-better-sidebar/index.ts +6 -2
  117. package/src/adapters/dsh-better-sidebar/methods.ts +3 -0
  118. package/src/admin-routes.ts +44 -2
  119. package/src/client/index.tsx +9 -0
  120. package/src/client/locales.ts +410 -2
  121. package/src/client/wizard/GatewayWizard.tsx +2220 -0
  122. package/src/client/wizard/WizardCard.tsx +81 -0
  123. package/src/client/wizard/cards/MigrateCard.tsx +42 -0
  124. package/src/client/wizard/cards/PortForwardCard.tsx +37 -0
  125. package/src/client/wizard/cards/ProxyCard.tsx +56 -0
  126. package/src/client/wizard/cards/TokenCard.tsx +81 -0
  127. package/src/client/wizard/register-wizard.ts +99 -0
  128. package/src/client/wizard/wizard-css.ts +948 -0
  129. package/src/client-data/qr.ts +284 -0
  130. package/src/client-data/settings-form.ts +12 -3
  131. package/src/client-data/wizard.ts +432 -0
  132. package/src/config.ts +87 -0
  133. package/src/index.ts +227 -2
  134. package/src/intercept.ts +43 -3
  135. package/src/merge-streams.ts +41 -12
  136. package/src/wizard/binary-io.ts +449 -0
  137. package/src/wizard/binary.ts +177 -0
  138. package/src/wizard/caddy-process.ts +228 -0
  139. package/src/wizard/caddy.ts +97 -0
  140. package/src/wizard/cloudflare.ts +413 -0
  141. package/src/wizard/cloudflared.ts +196 -0
  142. package/src/wizard/probe.ts +161 -0
  143. package/src/wizard/proxy-snippets.ts +128 -0
  144. package/src/wizard/routes.ts +537 -0
  145. package/src/wizard/source-detect.ts +87 -0
  146. package/src/wizard/status.ts +180 -0
  147. package/src/wizard/token-store.ts +73 -0
  148. package/src/wizard/tunnel-orchestrator.ts +197 -0
  149. package/src/wizard/verify.ts +236 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * WizardCard (GW-10): the modal popup for steps the user must perform
3
+ * ELSEWHERE — creating the Cloudflare token, configuring their own
4
+ * reverse proxy. Numbered steps in the page locale, copy-to-clipboard for
5
+ * the value or snippet at hand, Esc / backdrop / button to close. Scoped
6
+ * under `[data-zen-remote="wizard-card"]`.
7
+ *
8
+ * Deliberately NO react-dom portal: `react-dom` is not a seed word of the
9
+ * host browser module table (GW-fix-4 — `check-client-externals` gates the
10
+ * allowlist), so the overlay renders IN PLACE as a `position: fixed`
11
+ * layer. That keeps the plugin version-agnostic; the z-index floor of
12
+ * 1000 clears the Plugins page chrome, and no ancestor of the detail page
13
+ * establishes a containing block that could clip it.
14
+ */
15
+
16
+ import { useEffect, useRef, useState } from 'react'
17
+ import type { ReactNode } from 'react'
18
+
19
+ export interface WizardCardProps {
20
+ title: string
21
+ /** The card's body — numbered steps and/or a config snippet. */
22
+ children: ReactNode
23
+ /** The copy button's payload; absent means no copy button. */
24
+ copyText?: string | null
25
+ copyLabel: string
26
+ copiedLabel: string
27
+ closeLabel: string
28
+ /** Optional primary action button rendered alongside the close button. */
29
+ primaryAction?: ReactNode
30
+ onClose: () => void
31
+ }
32
+
33
+ export function WizardCard({ title, children, copyText, copyLabel, copiedLabel, closeLabel, primaryAction, onClose }: WizardCardProps): ReactNode {
34
+ const [copied, setCopied] = useState(false)
35
+ const copiedTimer = useRef<ReturnType<typeof setTimeout>>()
36
+
37
+ useEffect(() => {
38
+ const onKey = (event: KeyboardEvent): void => {
39
+ if (event.key === 'Escape') onClose()
40
+ }
41
+ document.addEventListener('keydown', onKey)
42
+ return () => {
43
+ document.removeEventListener('keydown', onKey)
44
+ if (copiedTimer.current !== undefined) clearTimeout(copiedTimer.current)
45
+ }
46
+ }, [onClose])
47
+
48
+ const copy = async (): Promise<void> => {
49
+ if (copyText === undefined || copyText === null || copyText === '') return
50
+ try {
51
+ await navigator.clipboard.writeText(copyText)
52
+ setCopied(true)
53
+ if (copiedTimer.current !== undefined) clearTimeout(copiedTimer.current)
54
+ copiedTimer.current = setTimeout(() => setCopied(false), 2000)
55
+ } catch {
56
+ // Clipboard denial (permissions, non-secure context): the snippet is
57
+ // selectable text anyway.
58
+ }
59
+ }
60
+
61
+ return (
62
+ <div data-zen-remote="wizard-card" onClick={onClose}>
63
+ <div className="zr-wizard-cardPanel" onClick={(event) => event.stopPropagation()}>
64
+ <div className="zr-wizard-cardHeader">
65
+ <p className="zr-wizard-cardTitle">{title}</p>
66
+ <button type="button" className="zr-wizard-cardCloseIcon" aria-label={closeLabel} onClick={onClose}>×</button>
67
+ </div>
68
+ <div className="zr-wizard-cardBody">
69
+ {children}
70
+ </div>
71
+ <div className="zr-wizard-cardActions">
72
+ <button type="button" className="zr-wizard-btn" onClick={onClose}>{closeLabel}</button>
73
+ {copyText !== undefined && copyText !== null && copyText !== '' ? (
74
+ <button type="button" className="zr-wizard-btn" onClick={() => { void copy() }}>{copied ? copiedLabel : copyLabel}</button>
75
+ ) : null}
76
+ {primaryAction}
77
+ </div>
78
+ </div>
79
+ </div>
80
+ )
81
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * MigrateCard (GW-11): the six-step domain-to-Cloudflare walkthrough for
3
+ * the tunnel path's precondition, with the 「重新检查」 button that closes
4
+ * the card so the user re-runs the form's own check.
5
+ */
6
+
7
+ import type { ReactNode } from 'react'
8
+ import type { WizardT } from './TokenCard.tsx'
9
+ import { WizardCard } from '../WizardCard.tsx'
10
+
11
+ export interface MigrateCardProps {
12
+ t: WizardT
13
+ /** Closes the card so the caller can re-run its precondition check. */
14
+ onRecheck: () => void
15
+ onClose: () => void
16
+ }
17
+
18
+ const STEPS = ['migrateStep1', 'migrateStep2', 'migrateStep3', 'migrateStep4', 'migrateStep5', 'migrateStep6'] as const
19
+
20
+ export function MigrateCard({ t, onRecheck, onClose }: MigrateCardProps): ReactNode {
21
+ return (
22
+ <WizardCard
23
+ title={t('wizard.card.migrateTitle')}
24
+ copyText={null}
25
+ copyLabel={t('wizard.card.copy')}
26
+ copiedLabel={t('wizard.card.copied')}
27
+ closeLabel={t('wizard.card.close')}
28
+ primaryAction={(
29
+ <button type="button" className="zr-wizard-btn zr-wizard-btnPrimary" onClick={onRecheck}>
30
+ {t('wizard.card.migrateRecheck')}
31
+ </button>
32
+ )}
33
+ onClose={onClose}
34
+ >
35
+ <div className="zr-wizard-cardSteps">
36
+ {STEPS.map((key, index) => (
37
+ <p key={key} className="zr-wizard-cardStep">{`${index + 1}. ${t(`wizard.card.${key}`)}`}</p>
38
+ ))}
39
+ </div>
40
+ </WizardCard>
41
+ )
42
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * PortForwardCard (GW-11): the router port-forwarding steps for path A on
3
+ * a non-443 HTTPS port — the certificate rides DNS-01 (no inbound needed
4
+ * for issuance), but the HTTPS service port itself must be reachable.
5
+ */
6
+
7
+ import type { ReactNode } from 'react'
8
+ import type { WizardT } from './TokenCard.tsx'
9
+ import { WizardCard } from '../WizardCard.tsx'
10
+
11
+ export interface PortForwardCardProps {
12
+ t: WizardT
13
+ /** The HTTPS port to forward. */
14
+ port: number
15
+ onClose: () => void
16
+ }
17
+
18
+ const STEPS = ['portForwardStep1', 'portForwardStep2', 'portForwardStep3', 'portForwardStep4'] as const
19
+
20
+ export function PortForwardCard({ t, port, onClose }: PortForwardCardProps): ReactNode {
21
+ return (
22
+ <WizardCard
23
+ title={t('wizard.card.portForwardTitle')}
24
+ copyText={null}
25
+ copyLabel={t('wizard.card.copy')}
26
+ copiedLabel={t('wizard.card.copied')}
27
+ closeLabel={t('wizard.card.close')}
28
+ onClose={onClose}
29
+ >
30
+ <div className="zr-wizard-cardSteps">
31
+ {STEPS.map((key, index) => (
32
+ <p key={key} className="zr-wizard-cardStep">{`${index + 1}. ${t(`wizard.card.${key}`, { port: String(port) })}`}</p>
33
+ ))}
34
+ </div>
35
+ </WizardCard>
36
+ )
37
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * ProxyCard (GW-11): the reverse-proxy snippets for the user's OWN proxy,
3
+ * tabbed nginx / Caddy / Lucky, with the snippet copy button and the
4
+ * 「我配好了,检查」 button that hands control back to the wizard's
5
+ * verification step.
6
+ */
7
+
8
+ import { useState } from 'react'
9
+ import type { ReactNode } from 'react'
10
+ import { caddySnippetText, luckyStepTexts, nginxSnippetText, SNIPPET_KINDS, type SnippetKind } from '../../../client-data/wizard.ts'
11
+ import type { WizardT } from './TokenCard.tsx'
12
+ import { WizardCard } from '../WizardCard.tsx'
13
+
14
+ export interface ProxyCardProps {
15
+ t: WizardT
16
+ /** Snippet inputs — the domain as typed, the gateway's ACTUAL port. */
17
+ domain: string
18
+ upstreamPort: number
19
+ /** 「我配好了,检查」: closes the card and starts the verification. */
20
+ onCheck: () => void
21
+ onClose: () => void
22
+ }
23
+
24
+ export function ProxyCard({ t, domain, upstreamPort, onCheck, onClose }: ProxyCardProps): ReactNode {
25
+ const [tab, setTab] = useState<SnippetKind>('nginx')
26
+ const input = { domain, upstreamHost: '127.0.0.1', upstreamPort }
27
+ const text = tab === 'nginx'
28
+ ? nginxSnippetText(input)
29
+ : tab === 'caddy' ? caddySnippetText(input) : (luckyStepTexts(input) ?? []).join('\n')
30
+ return (
31
+ <WizardCard
32
+ title={t('wizard.card.proxyTitle')}
33
+ copyText={text}
34
+ copyLabel={t('wizard.card.copy')}
35
+ copiedLabel={t('wizard.card.copied')}
36
+ closeLabel={t('wizard.card.close')}
37
+ primaryAction={(
38
+ <button type="button" className="zr-wizard-btn zr-wizard-btnPrimary" onClick={() => { onCheck(); onClose() }}>
39
+ {t('wizard.card.proxyCheck')}
40
+ </button>
41
+ )}
42
+ onClose={onClose}
43
+ >
44
+ <div className="zr-wizard-cardTabs">
45
+ {SNIPPET_KINDS.map((kind) => (
46
+ <button key={kind} type="button" className="zr-wizard-cardTab" data-selected={tab === kind} onClick={() => setTab(kind)}>
47
+ {kind}
48
+ </button>
49
+ ))}
50
+ </div>
51
+ {text === null
52
+ ? <p className="zr-wizard-cardStep">{t('wizard.badDomain')}</p>
53
+ : <pre className="zr-wizard-cardCode">{text}</pre>}
54
+ </WizardCard>
55
+ )
56
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * TokenCard (GW-11): the Cloudflare API-token creation walkthrough — eight
3
+ * numbered steps, the permission checklist as a small table, and a
4
+ * paste-in box that hands the token back to the wizard's form (`onUse`).
5
+ * The token never persists in component state beyond the box and never
6
+ * leaves this machine except inside the wizard's own POST.
7
+ */
8
+
9
+ import { useState } from 'react'
10
+ import type { ReactNode } from 'react'
11
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
12
+ import { NS } from '../../locales.ts'
13
+ import { WizardCard } from '../WizardCard.tsx'
14
+
15
+ /** The wizard's translate function, straight from the shared locale contract. */
16
+ export type WizardT = PropsLocale<typeof NS>['t']
17
+
18
+ export interface TokenCardProps {
19
+ t: WizardT
20
+ /** Hands the pasted token back to the wizard's form field. */
21
+ onUse: (token: string) => void
22
+ onClose: () => void
23
+ }
24
+
25
+ const STEPS = ['tokenStep1', 'tokenStep2', 'tokenStep3', 'tokenStep4', 'tokenStep5', 'tokenStep6', 'tokenStep7', 'tokenStep8'] as const
26
+
27
+ export function TokenCard({ t, onUse, onClose }: TokenCardProps): ReactNode {
28
+ const [pasted, setPasted] = useState('')
29
+ return (
30
+ <WizardCard
31
+ title={t('wizard.card.tokenTitle')}
32
+ copyText={null}
33
+ copyLabel={t('wizard.card.copy')}
34
+ copiedLabel={t('wizard.card.copied')}
35
+ closeLabel={t('wizard.card.close')}
36
+ primaryAction={(
37
+ <button type="button" className="zr-wizard-btn zr-wizard-btnPrimary" disabled={pasted.trim() === ''} onClick={() => onUse(pasted.trim())}>
38
+ {t('wizard.card.tokenUse')}
39
+ </button>
40
+ )}
41
+ onClose={onClose}
42
+ >
43
+ <div className="zr-wizard-cardSteps">
44
+ {STEPS.map((key, index) => (
45
+ <p key={key} className="zr-wizard-cardStep">{`${index + 1}. ${t(`wizard.card.${key}`)}`}</p>
46
+ ))}
47
+ </div>
48
+ <p className="zr-wizard-cardSubhead">{t('wizard.card.tokenPerms')}</p>
49
+ <table className="zr-wizard-cardTable">
50
+ <tbody>
51
+ <tr>
52
+ <td className="zr-wizard-cardPerm">{t('wizard.card.tokenPermDns')}</td>
53
+ <td className="zr-wizard-cardWhy">{t('wizard.card.tokenPermDnsWhy')}</td>
54
+ </tr>
55
+ <tr>
56
+ <td className="zr-wizard-cardPerm">{t('wizard.card.tokenPermTunnel')}</td>
57
+ <td className="zr-wizard-cardWhy">{t('wizard.card.tokenPermTunnelWhy')}</td>
58
+ </tr>
59
+ <tr>
60
+ <td className="zr-wizard-cardPerm">{t('wizard.card.tokenPermAccount')}</td>
61
+ <td className="zr-wizard-cardWhy">{t('wizard.card.tokenPermAccountWhy')}</td>
62
+ </tr>
63
+ <tr>
64
+ <td className="zr-wizard-cardPerm">{t('wizard.card.tokenPermZoneRead')}</td>
65
+ <td className="zr-wizard-cardWhy">{t('wizard.card.tokenPermZoneReadWhy')}</td>
66
+ </tr>
67
+ </tbody>
68
+ </table>
69
+ <div className="zr-wizard-field">
70
+ <label className="zr-wizard-label" htmlFor="zr-wizard-token-paste">{t('wizard.card.tokenPaste')}</label>
71
+ <input
72
+ id="zr-wizard-token-paste"
73
+ className="zr-wizard-input"
74
+ type="password"
75
+ value={pasted}
76
+ onChange={(event) => setPasted(event.target.value)}
77
+ />
78
+ </div>
79
+ </WizardCard>
80
+ )
81
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Wizard registration for the plugin DETAIL page — the piece that must
3
+ * run BEFORE apply's desktop-shell gate (same discipline as
4
+ * register-settings.ts, and for the same reason: the desktop app IS the
5
+ * main server, and the wizard is exactly where it is most useful).
6
+ *
7
+ * Registered on `plugins.bundle.config` (keyed by `'dsh-zen-remote'`), which
8
+ * `PackageDetail` renders as the FIRST section at the top of the bundle
9
+ * detail page (above `RowsSection`「插件组件」and `SourceSection`「来源」),
10
+ * and which `RowDetail` (the component detail page) never renders.
11
+ *
12
+ * `configForms` never joins the top-level inject: a composition without
13
+ * the settings service keeps the whole browser half loadable, and the lazy
14
+ * `ctx.inject(['configForms'], …)` below simply never fires there — a
15
+ * composition that cannot stage row edits has no use for a wizard that
16
+ * writes rows.
17
+ */
18
+ import type { ClientContext } from '../compat/types.ts'
19
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
20
+ // Type-only: the renderer declares `ctx.slots` on the cordis Context.
21
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
22
+ import { DEVICE_TOKEN_FIELD, ZenRemoteSettingsForm } from '../../client-data/settings-form.ts'
23
+ import { NS } from '../locales.ts'
24
+ import { SETTINGS_ENTRY_ID } from '../settings/register-settings.ts'
25
+ import { WIZARD_CSS } from './wizard-css.ts'
26
+ import type { GatewayWizard } from './GatewayWizard.tsx'
27
+
28
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
29
+ interface SlotMap {
30
+ /**
31
+ * One bundle's top configuration block on the plugin DETAIL page — a
32
+ * keyed seat (`key = pkg.name`) rendered at the top of `PackageDetail`
33
+ * above the component rows and source sections, and absent from
34
+ * `RowDetail` (structural mirror of the plugin-manager's declaration).
35
+ */
36
+ 'plugins.bundle.config': {
37
+ kind: 'keyed'
38
+ scope: 'root'
39
+ owner: { readonly view: 'page' }
40
+ }
41
+ }
42
+ }
43
+
44
+ /** `plugins.bundle.config` key: the bundle's package name. */
45
+ export const WIZARD_BUNDLE_KEY = 'dsh-zen-remote'
46
+
47
+ export interface SaveRowFieldsOptions {
48
+ /** Field names to remove (`op: 'unset'`) in the same atomic mutation. */
49
+ unset?: readonly string[]
50
+ }
51
+
52
+ /**
53
+ * Register the wizard stylesheet and (lazily, only where the settings
54
+ * service exists) the bundle detail-page top block.
55
+ * @param ctx - client root context.
56
+ * @param section - the block component; a parameter so this module stays
57
+ * JSX-free and importable under Node's type stripping.
58
+ */
59
+ export function registerGatewayWizard(ctx: ClientContext, section: typeof GatewayWizard): void {
60
+ ctx.effect(() => {
61
+ const tag = document.createElement('style')
62
+ tag.dataset.plugin = 'dsh-zen-remote-wizard'
63
+ tag.textContent = WIZARD_CSS
64
+ document.head.appendChild(tag)
65
+ return () => { tag.remove() }
66
+ }, 'dsh-zen-remote: wizard styles')
67
+
68
+ ctx.inject(['configForms'], (formsCtx) => {
69
+ const scope = formsCtx.configForms.get(SETTINGS_ENTRY_ID)
70
+ // Follow the describe sidecar when available so client-mode pairing can
71
+ // see whether `deviceToken` is configured (degrades to false in minimal
72
+ // test harnesses that omit `describe`).
73
+ const describe = typeof formsCtx.configForms.describe === 'function' ? formsCtx.configForms.describe() : undefined
74
+ const secretConfigured = (): boolean => {
75
+ const namespaces = describe?.getSnapshot().view?.namespaces ?? []
76
+ const row = namespaces.find((candidate) => candidate.ns === SETTINGS_ENTRY_ID)
77
+ return (row?.secrets ?? []).some(
78
+ (secret) => secret.path.length === 1 && secret.path[0] === DEVICE_TOKEN_FIELD && secret.set,
79
+ )
80
+ }
81
+ const config = new ZenRemoteSettingsForm(scope, secretConfigured)
82
+ const offDescribe = describe !== undefined ? describe.subscribe(() => { config.refresh() }) : () => {}
83
+ formsCtx.effect(() => () => { offDescribe(); config.dispose() }, 'dsh-zen-remote: wizard form')
84
+ const saveRowFields = async (fields: Record<string, unknown>, options?: SaveRowFieldsOptions): Promise<void> => {
85
+ const snapshot = scope.getSnapshot()
86
+ const ops = [
87
+ ...Object.entries(fields).map(([key, value]) => ({ op: 'set' as const, path: [key], value })),
88
+ ...(options?.unset ?? []).map((key) => ({ op: 'unset' as const, path: [key] })),
89
+ ]
90
+ await scope.mutate(ops, snapshot.revision)
91
+ }
92
+ formsCtx.effect(() => formsCtx.slots.inject('plugins.bundle.config', () => formsCtx.slots.register({
93
+ name: 'plugins.bundle.config',
94
+ key: WIZARD_BUNDLE_KEY,
95
+ locale: NS,
96
+ inject: () => ({ config, saveRowFields }),
97
+ }, section)), 'dsh-zen-remote: wizard section')
98
+ })
99
+ }