@riceawa/dsh-lan-gateway 0.5.5 → 0.6.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.md +28 -18
- package/lib/client.js +38 -26
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +86 -4
- package/lib/index.js +107 -45
- package/package.json +26 -14
- package/src/client/index.ts +39 -29
- package/src/client/lan-gateway-card.tsx +41 -27
- package/src/index.ts +155 -65
package/lib/index.js
CHANGED
|
@@ -1897,33 +1897,91 @@ const name = "dsh-lan-gateway";
|
|
|
1897
1897
|
/** Requires the web server service (binds before this row's apply runs) and the tool registry. */
|
|
1898
1898
|
const inject = ["webServer", "tools"];
|
|
1899
1899
|
/**
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1900
|
+
* Schemastery configuration validated by the Loader.
|
|
1901
|
+
*
|
|
1902
|
+
* Every field is `.volatile()`, which is what lets the Settings service write
|
|
1903
|
+
* it: 0.1.7 projects only volatile fields into forms and refuses an edit to any
|
|
1904
|
+
* other path (`not volatile`). The mark also changes the runtime shape — a
|
|
1905
|
+
* volatile field arrives as a reference (see `ConfigRefs`), never as the plain
|
|
1906
|
+
* value the rest of this file expects — so read it through `readConfig`.
|
|
1904
1907
|
*/
|
|
1905
|
-
const NS = "lan-gateway";
|
|
1906
|
-
/** Schemastery configuration validated by the Loader. */
|
|
1907
1908
|
const Config = z.object({
|
|
1908
|
-
enabled: z.boolean().default(false),
|
|
1909
|
-
gatewayPort: z.natural().min(1).max(65535).default(3081),
|
|
1910
|
-
dshTargetPort: z.natural().min(1).max(65535),
|
|
1911
|
-
lanCidrs: z.array(String).default([...DEFAULT_LAN_CIDR_STRINGS]),
|
|
1912
|
-
lanPasswordless: z.boolean().default(false),
|
|
1913
|
-
authRequired: z.boolean().default(true),
|
|
1914
|
-
cookieMaxAgeDays: z.natural().min(1).max(365).default(7),
|
|
1915
|
-
cookieName: z.string().default("dsh_gw_auth"),
|
|
1916
|
-
tlsEnabled: z.boolean().default(false),
|
|
1917
|
-
tlsMode: z.union([z.const("self-signed"), z.const("custom")]).default("self-signed"),
|
|
1918
|
-
tlsCertPath: z.string(),
|
|
1919
|
-
tlsKeyPath: z.string(),
|
|
1920
|
-
tlsSelfSignedHosts: z.string().default("localhost"),
|
|
1921
|
-
tlsCertMaxAgeDays: z.natural().min(1).max(3650).default(825),
|
|
1922
|
-
allowInsecurePlaintext: z.boolean().default(false),
|
|
1923
|
-
trustedTerminator: z.string(),
|
|
1924
|
-
secureCookies: z.boolean()
|
|
1909
|
+
enabled: z.boolean().default(false).volatile(),
|
|
1910
|
+
gatewayPort: z.natural().min(1).max(65535).default(3081).volatile(),
|
|
1911
|
+
dshTargetPort: z.natural().min(1).max(65535).volatile(),
|
|
1912
|
+
lanCidrs: z.array(String).default([...DEFAULT_LAN_CIDR_STRINGS]).volatile(),
|
|
1913
|
+
lanPasswordless: z.boolean().default(false).volatile(),
|
|
1914
|
+
authRequired: z.boolean().default(true).volatile(),
|
|
1915
|
+
cookieMaxAgeDays: z.natural().min(1).max(365).default(7).volatile(),
|
|
1916
|
+
cookieName: z.string().default("dsh_gw_auth").volatile(),
|
|
1917
|
+
tlsEnabled: z.boolean().default(false).volatile(),
|
|
1918
|
+
tlsMode: z.union([z.const("self-signed"), z.const("custom")]).default("self-signed").volatile(),
|
|
1919
|
+
tlsCertPath: z.string().volatile(),
|
|
1920
|
+
tlsKeyPath: z.string().volatile(),
|
|
1921
|
+
tlsSelfSignedHosts: z.string().default("localhost").volatile(),
|
|
1922
|
+
tlsCertMaxAgeDays: z.natural().min(1).max(3650).default(825).volatile(),
|
|
1923
|
+
allowInsecurePlaintext: z.boolean().default(false).volatile(),
|
|
1924
|
+
trustedTerminator: z.string().volatile(),
|
|
1925
|
+
secureCookies: z.boolean().volatile()
|
|
1925
1926
|
});
|
|
1926
1927
|
/**
|
|
1928
|
+
* Unwrap the config references into the plain values every other function in
|
|
1929
|
+
* this file reads. Called on each access rather than once, because a settings
|
|
1930
|
+
* write updates the references in place.
|
|
1931
|
+
* @param refs - the config object handed to `apply`.
|
|
1932
|
+
* @returns one detached plain snapshot.
|
|
1933
|
+
*/
|
|
1934
|
+
function readConfig(refs) {
|
|
1935
|
+
const dshTargetPort = refs.dshTargetPort?.get();
|
|
1936
|
+
const authRequired = refs.authRequired?.get();
|
|
1937
|
+
const tlsCertPath = refs.tlsCertPath?.get();
|
|
1938
|
+
const tlsKeyPath = refs.tlsKeyPath?.get();
|
|
1939
|
+
const tlsSelfSignedHosts = refs.tlsSelfSignedHosts?.get();
|
|
1940
|
+
const trustedTerminator = refs.trustedTerminator?.get();
|
|
1941
|
+
const secureCookies = refs.secureCookies?.get();
|
|
1942
|
+
return {
|
|
1943
|
+
enabled: refs.enabled.get(),
|
|
1944
|
+
gatewayPort: refs.gatewayPort.get(),
|
|
1945
|
+
lanCidrs: [...refs.lanCidrs.get()],
|
|
1946
|
+
lanPasswordless: refs.lanPasswordless.get(),
|
|
1947
|
+
cookieMaxAgeDays: refs.cookieMaxAgeDays.get(),
|
|
1948
|
+
cookieName: refs.cookieName.get(),
|
|
1949
|
+
tlsEnabled: refs.tlsEnabled.get(),
|
|
1950
|
+
tlsMode: refs.tlsMode.get(),
|
|
1951
|
+
tlsCertMaxAgeDays: refs.tlsCertMaxAgeDays.get(),
|
|
1952
|
+
allowInsecurePlaintext: refs.allowInsecurePlaintext.get(),
|
|
1953
|
+
...dshTargetPort !== void 0 ? { dshTargetPort } : {},
|
|
1954
|
+
...authRequired !== void 0 ? { authRequired } : {},
|
|
1955
|
+
...tlsCertPath !== void 0 ? { tlsCertPath } : {},
|
|
1956
|
+
...tlsKeyPath !== void 0 ? { tlsKeyPath } : {},
|
|
1957
|
+
...tlsSelfSignedHosts !== void 0 ? { tlsSelfSignedHosts } : {},
|
|
1958
|
+
...trustedTerminator !== void 0 ? { trustedTerminator } : {},
|
|
1959
|
+
...secureCookies !== void 0 ? { secureCookies } : {}
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Build the reference-shaped config `apply` receives, exactly as the Loader
|
|
1964
|
+
* builds it. Exported for tests that drive `apply` directly.
|
|
1965
|
+
* @param raw - a config object; missing fields take their schema defaults.
|
|
1966
|
+
* @returns one reference per volatile field.
|
|
1967
|
+
*/
|
|
1968
|
+
function configRefs(raw) {
|
|
1969
|
+
return Config(raw);
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Validate a raw config object the way the Loader does, and unwrap it.
|
|
1973
|
+
*
|
|
1974
|
+
* `Config` marks every field volatile, so a validation hands the values back as
|
|
1975
|
+
* references (typed deeply-readonly by schemastery); this returns the plain
|
|
1976
|
+
* shape the rest of the file reads. Used to judge a config the Settings card is
|
|
1977
|
+
* about to save, before it is persisted.
|
|
1978
|
+
* @param raw - a config object; missing fields take their schema defaults.
|
|
1979
|
+
* @returns the validated plain config.
|
|
1980
|
+
*/
|
|
1981
|
+
function validateConfig(raw) {
|
|
1982
|
+
return readConfig(configRefs(raw));
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1927
1985
|
* The fail-closed problems that prevent a config from enabling the listener.
|
|
1928
1986
|
* Returns every problem (not just the first) so the operator sees the full
|
|
1929
1987
|
* migration at once. Exported for tests.
|
|
@@ -2101,16 +2159,18 @@ function apply(ctx, config) {
|
|
|
2101
2159
|
let connectionGeneration = 0;
|
|
2102
2160
|
/** Builds a fresh shared-session relay for a dsh port, once the base supports sessions. */
|
|
2103
2161
|
let makeRelay;
|
|
2104
|
-
/**
|
|
2105
|
-
let configSource = () => config;
|
|
2106
|
-
/** Whether writes go to the settings section rather than staying in memory. */
|
|
2162
|
+
/** Whether the settings service is attached, so writes reach the profile entry. */
|
|
2107
2163
|
let settingsAttached = false;
|
|
2108
|
-
/** The settings scope for the `lan-gateway` namespace, while one is attached. */
|
|
2109
|
-
let settingsScope;
|
|
2110
2164
|
/**
|
|
2111
|
-
*
|
|
2112
|
-
*
|
|
2113
|
-
*
|
|
2165
|
+
* This plugin's own Loader entry id. dsh 0.1.7 addresses a settings write by
|
|
2166
|
+
* the *entry id* — the `lan-gateway` namespace this plugin used to register
|
|
2167
|
+
* with is gone along with `settingsScope`.
|
|
2168
|
+
*/
|
|
2169
|
+
let settingsEntryId;
|
|
2170
|
+
/**
|
|
2171
|
+
* The settings service, for the one write a merge patch cannot express: a key
|
|
2172
|
+
* must be *removed* to re-inherit the composition layer, and only its
|
|
2173
|
+
* path-addressed `mutate` can unset one.
|
|
2114
2174
|
*/
|
|
2115
2175
|
let settingsProvider;
|
|
2116
2176
|
/**
|
|
@@ -2121,7 +2181,7 @@ function apply(ctx, config) {
|
|
|
2121
2181
|
let lifecycle = Promise.resolve();
|
|
2122
2182
|
/** Set by the dispose hook; a start that completes after it must undo itself. */
|
|
2123
2183
|
let disposed = false;
|
|
2124
|
-
const effective = () =>
|
|
2184
|
+
const effective = () => readConfig(config);
|
|
2125
2185
|
/** Queue one lifecycle action behind every action already running. */
|
|
2126
2186
|
const enqueue = (reason, action) => {
|
|
2127
2187
|
lifecycle = lifecycle.then(action).catch((error) => {
|
|
@@ -2198,26 +2258,26 @@ function apply(ctx, config) {
|
|
|
2198
2258
|
}
|
|
2199
2259
|
});
|
|
2200
2260
|
};
|
|
2201
|
-
/** Record the run intent where it will survive: the
|
|
2261
|
+
/** Record the run intent where it will survive: the profile entry, or memory. */
|
|
2202
2262
|
const setRunIntent = async (enabled) => {
|
|
2203
|
-
if (settingsAttached &&
|
|
2204
|
-
await
|
|
2263
|
+
if (settingsAttached && settingsProvider !== void 0 && settingsEntryId !== void 0) {
|
|
2264
|
+
await settingsProvider.update(settingsEntryId, { enabled });
|
|
2205
2265
|
return;
|
|
2206
2266
|
}
|
|
2207
2267
|
manualOverride = enabled;
|
|
2208
2268
|
};
|
|
2209
2269
|
ctx.inject(["settings"], (sctx) => {
|
|
2210
|
-
const
|
|
2211
|
-
|
|
2270
|
+
const entryId = ctx.fiber.entry?.options.id;
|
|
2271
|
+
if (entryId === void 0) return;
|
|
2272
|
+
settingsEntryId = entryId;
|
|
2212
2273
|
settingsProvider = sctx.settings;
|
|
2213
2274
|
settingsAttached = true;
|
|
2214
|
-
|
|
2215
|
-
sctx.effect(() =>
|
|
2275
|
+
sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber));
|
|
2276
|
+
sctx.effect(() => ctx.on("loader/volatile-update", () => {
|
|
2216
2277
|
syncGateway("settings change");
|
|
2217
2278
|
}));
|
|
2218
2279
|
sctx.effect(() => () => {
|
|
2219
|
-
|
|
2220
|
-
settingsScope = void 0;
|
|
2280
|
+
settingsEntryId = void 0;
|
|
2221
2281
|
settingsProvider = void 0;
|
|
2222
2282
|
settingsAttached = false;
|
|
2223
2283
|
syncGateway("settings detach");
|
|
@@ -2282,12 +2342,14 @@ function apply(ctx, config) {
|
|
|
2282
2342
|
send(400, { error: "body must be a config object" });
|
|
2283
2343
|
return;
|
|
2284
2344
|
}
|
|
2285
|
-
|
|
2345
|
+
const settings = settingsProvider;
|
|
2346
|
+
const entryId = settingsEntryId;
|
|
2347
|
+
if (settings === void 0 || entryId === void 0) {
|
|
2286
2348
|
send(409, { error: "settings service unavailable — edit the profile patch (cordis.patch.yml) instead" });
|
|
2287
2349
|
return;
|
|
2288
2350
|
}
|
|
2289
2351
|
const { patch, clear, unknown } = buildConfigPatch(submitted);
|
|
2290
|
-
const candidate =
|
|
2352
|
+
const candidate = validateConfig({
|
|
2291
2353
|
...effective(),
|
|
2292
2354
|
...patch
|
|
2293
2355
|
});
|
|
@@ -2306,7 +2368,7 @@ function apply(ctx, config) {
|
|
|
2306
2368
|
op: "unset",
|
|
2307
2369
|
path: [key]
|
|
2308
2370
|
}))];
|
|
2309
|
-
if (ops.length > 0) await
|
|
2371
|
+
if (ops.length > 0) await settings.mutate(entryId, ops);
|
|
2310
2372
|
await syncGateway("config route save");
|
|
2311
2373
|
const next = { ...snapshot() };
|
|
2312
2374
|
if (unknown.length > 0) next["ignored"] = unknown;
|
|
@@ -2434,4 +2496,4 @@ function apply(ctx, config) {
|
|
|
2434
2496
|
}, "dsh-lan-gateway: listener lifecycle");
|
|
2435
2497
|
}
|
|
2436
2498
|
//#endregion
|
|
2437
|
-
export { Config, apply, buildConfigPatch, gatewayStartProblems, inject, isTrustedConfigRequest, name, resolveSecureCookies };
|
|
2499
|
+
export { Config, apply, buildConfigPatch, configRefs, gatewayStartProblems, inject, isTrustedConfigRequest, name, readConfig, resolveSecureCookies, validateConfig };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riceawa/dsh-lan-gateway",
|
|
3
3
|
"description": "LAN/internet reverse-proxy gateway for the DeepSeek Harness web GUI: binds 0.0.0.0 and forwards to the loopback dsh web server. Default-deny: every source (loopback, LAN, internet) must sign in with an HMAC session cookie unless lanPasswordless is explicitly enabled; against dsh >= 0.1.2-rc.1 the gateway relays one shared upstream browser session, so the harness's own authorization still gates every request. Fail-closed start guard (password required, plaintext needs an explicit opt-in), session revocation by epoch (password changes and secret rotation kill cookies and live WebSockets), same-site/Origin fence on HTTP and WebSocket upgrades, optional TLS (auto self-signed or user-supplied certs), and a Settings → Plugins card for live adjustment of port, CIDRs, auth, and TLS. Includes an insecure-origin UUID shim client bundle: on gateway-served plain-HTTP origins browsers lack crypto.randomUUID, so the client half patches a getRandomValues-backed randomUUID onto the Crypto prototype, fixing workspace open over LAN without touching DSH source.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -42,22 +42,34 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
46
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
47
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
48
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
45
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
46
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
48
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "4.0.
|
|
52
|
-
"@deepseek-ai/
|
|
51
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
52
|
+
"@deepseek-ai/cordis-plugin-loader": "1.0.5",
|
|
53
|
+
"@deepseek-ai/cosmokit": "1.8.5",
|
|
54
|
+
"@deepseek-ai/dsh-brand": "0.1.7-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-invariants": "0.1.7-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.1.7-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-user-approval": "0.1.7-rc.2",
|
|
53
63
|
"@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.1.7-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-llm": "0.1.7-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-scope": "0.1.7-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-settings": "0.1.7-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-util-values": "0.1.7-rc.2",
|
|
72
|
+
"@deepseek-ai/schemastery": "3.18.4",
|
|
61
73
|
"@types/node": "^22.0.0",
|
|
62
74
|
"@types/react": "~18.3.1",
|
|
63
75
|
"react": "^18.2.0",
|
package/src/client/index.ts
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
* installs a getRandomValues-backed `randomUUID` on the Crypto prototype at
|
|
8
8
|
* module scope. With TLS enabled the origin is secure and the shim is a
|
|
9
9
|
* no-op.
|
|
10
|
-
* 2. Settings card: registers the LAN gateway card into the official
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* adjustable from the GUI.
|
|
10
|
+
* 2. Settings card: registers the LAN gateway card into the official Plugins
|
|
11
|
+
* page (`plugins.item` slot) so port, CIDRs, auth, and TLS stay adjustable
|
|
12
|
+
* from the GUI.
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
/** RFC 4122 v4 UUID from crypto.getRandomValues (available on insecure origins). */
|
|
@@ -62,12 +61,25 @@ export function installRandomUuidShim(): boolean {
|
|
|
62
61
|
installRandomUuidShim()
|
|
63
62
|
|
|
64
63
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
65
|
-
|
|
64
|
+
// Type-only, and never bundled: the Plugins page's slot contract (`plugins.item`)
|
|
65
|
+
// and the settings domain's `configForms` service both resolve from the web
|
|
66
|
+
// shell's frozen module table. Importing them is what subjects the registration
|
|
67
|
+
// below to the platform's own contract instead of a local copy that drifts the
|
|
68
|
+
// next time upstream renames a slot.
|
|
69
|
+
import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
|
|
70
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
71
|
+
import { LanGatewayCard, cardTitle } from './lan-gateway-card.tsx'
|
|
66
72
|
|
|
67
73
|
export const name = 'dsh-lan-gateway'
|
|
68
74
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The profile entry id this plugin's bundle patch composes it under, and the
|
|
77
|
+
* settings namespace dsh ≥ 0.1.7 addresses every write by.
|
|
78
|
+
*/
|
|
79
|
+
const ENTRY_ID = 'dsh-lan-gateway'
|
|
80
|
+
|
|
81
|
+
/** The slots service the card rides, and the settings mirror the gate reads. */
|
|
82
|
+
export const inject = ['slots', 'configForms']
|
|
71
83
|
|
|
72
84
|
/**
|
|
73
85
|
* Mount the settings card and the UUID shim.
|
|
@@ -76,28 +88,26 @@ export const inject = ['slots']
|
|
|
76
88
|
export function apply(ctx: ClientContext): void {
|
|
77
89
|
installRandomUuidShim()
|
|
78
90
|
|
|
79
|
-
// The card rides the official Plugins
|
|
80
|
-
//
|
|
81
|
-
//
|
|
91
|
+
// The card rides the official Plugins page. Like ModLens it registers with no
|
|
92
|
+
// inject face and fetches its own loopback config route, so it depends on no
|
|
93
|
+
// settings, locale, or connection service.
|
|
82
94
|
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
95
|
+
// dsh 0.1.7 replaced the namespace-keyed `settings.plugin.item` slot with the
|
|
96
|
+
// list slot `plugins.item`, which is where a host-plane plugin's own
|
|
97
|
+
// configuration page belongs ("one companion package per host-plane
|
|
98
|
+
// namespace"); the page renders the contribution as the card's one-liner and,
|
|
99
|
+
// once opened, as the body of the plugin's own page. Registering into the
|
|
100
|
+
// retired slot left the card invisible on 0.1.7.
|
|
89
101
|
//
|
|
90
|
-
// `
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}, LanGatewayCard)
|
|
102
|
-
})
|
|
102
|
+
// `whileServed` keeps the entry off the page until the Host's settings mirror
|
|
103
|
+
// serves this plugin's entry. That is the one gate worth having: without a
|
|
104
|
+
// Loader entry there is nothing to write to, and the card would appear only
|
|
105
|
+
// to fail every save with the route's 409.
|
|
106
|
+
ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () =>
|
|
107
|
+
ctx.slots.inject('plugins.item', () => ctx.slots.register({
|
|
108
|
+
name: 'plugins.item',
|
|
109
|
+
id: ENTRY_ID,
|
|
110
|
+
order: 30,
|
|
111
|
+
label: () => cardTitle(),
|
|
112
|
+
}, LanGatewayCard))))
|
|
103
113
|
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The lan-gateway settings card
|
|
3
|
-
*
|
|
2
|
+
* The lan-gateway settings card, rendered by the official DSH Plugins page
|
|
3
|
+
* through its `plugins.item` slot.
|
|
4
4
|
*
|
|
5
5
|
* ModLens-style: the card carries NO injected services. It reads and writes
|
|
6
6
|
* the loopback-only `/lan-gateway/config` host route (the browser never sees
|
|
7
|
-
* the settings seam or any secret), so the
|
|
8
|
-
* the `slots` service
|
|
7
|
+
* the settings seam or any secret), so the only platform service it needs is
|
|
8
|
+
* the `slots` service every plugin already has.
|
|
9
9
|
*
|
|
10
10
|
* @module @riceawa/dsh-lan-gateway/client/card
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { useEffect, useState, type ChangeEvent, type ReactNode } from 'react'
|
|
14
14
|
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
15
|
+
// Type-only. The Plugins page owns the `plugins.item` contract, and its own
|
|
16
|
+
// doc says a registrant merges that contract with `import type` instead of
|
|
17
|
+
// importing the package at runtime. Taking the contract from its owner is also
|
|
18
|
+
// what turns the next upstream rename of this slot into a compile error here,
|
|
19
|
+
// rather than a card that quietly stops rendering.
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
|
|
15
21
|
import {
|
|
16
22
|
FIELDS,
|
|
17
23
|
TRISTATE_OPTIONS,
|
|
@@ -22,23 +28,12 @@ import {
|
|
|
22
28
|
} from '../config-fields.ts'
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* real; this only restores the compile-time table.
|
|
31
|
+
* Props the renderer binds for this card. The Plugins page asks for either the
|
|
32
|
+
* card's one-liner (`summary`) or the body of its own page (`page`), and draws
|
|
33
|
+
* the page's title, icon, and crumb itself. The card needs no injected face —
|
|
34
|
+
* it fetches its own route.
|
|
30
35
|
*/
|
|
31
|
-
|
|
32
|
-
interface SlotMap {
|
|
33
|
-
/** One plugin's card inside the plugin configuration section. */
|
|
34
|
-
'settings.plugin.item': { kind: 'keyed'; scope: 'root'; owner: { children?: never } }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Props the renderer binds for this card (unused — the card is self-loading).
|
|
40
|
-
*/
|
|
41
|
-
export type LanGatewayCardProps = PropsRuntime<'settings.plugin.item'>
|
|
36
|
+
export type LanGatewayCardProps = PropsRuntime<'plugins.item'>
|
|
42
37
|
|
|
43
38
|
/**
|
|
44
39
|
* The card's field table and value codecs live in `config-fields.ts`, shared
|
|
@@ -192,6 +187,15 @@ function labels(): Labels {
|
|
|
192
187
|
return lang.startsWith('zh') ? LABELS.zh : LABELS.en
|
|
193
188
|
}
|
|
194
189
|
|
|
190
|
+
/**
|
|
191
|
+
* The card's title in the browser's language, for the Plugins page's list
|
|
192
|
+
* entry. A thunk so the label follows the page's locale without re-registering.
|
|
193
|
+
* @returns the localized card title.
|
|
194
|
+
*/
|
|
195
|
+
export function cardTitle(): string {
|
|
196
|
+
return labels().title
|
|
197
|
+
}
|
|
198
|
+
|
|
195
199
|
/* ------------------------------------------------------------------ */
|
|
196
200
|
/* Card */
|
|
197
201
|
/* ------------------------------------------------------------------ */
|
|
@@ -199,10 +203,14 @@ function labels(): Labels {
|
|
|
199
203
|
/**
|
|
200
204
|
* Render the LAN gateway card. Self-loading: fetches the config route on
|
|
201
205
|
* mount, posts the edited config on save.
|
|
202
|
-
*
|
|
203
|
-
*
|
|
206
|
+
*
|
|
207
|
+
* `view` swaps between the card's one-liner and its page body, so the branch
|
|
208
|
+
* sits after the hooks: the Plugins page re-renders one contribution under the
|
|
209
|
+
* other view when the card is opened.
|
|
210
|
+
* @param props - the view the Plugins page is asking for.
|
|
211
|
+
* @returns the one-liner, the card, or nothing while the route is unreachable.
|
|
204
212
|
*/
|
|
205
|
-
export function LanGatewayCard(
|
|
213
|
+
export function LanGatewayCard(props: LanGatewayCardProps): ReactNode {
|
|
206
214
|
const t = labels()
|
|
207
215
|
const [open, setOpen] = useState(false)
|
|
208
216
|
const [route, setRoute] = useState<RouteState | null>(null)
|
|
@@ -225,6 +233,12 @@ export function LanGatewayCard(_props: LanGatewayCardProps): ReactNode {
|
|
|
225
233
|
return () => { cancelled = true }
|
|
226
234
|
}, [])
|
|
227
235
|
|
|
236
|
+
// The Plugins page lists this plugin as one card and opens its own page on
|
|
237
|
+
// demand: `summary` is the one-liner the list shows, `page` the body. The
|
|
238
|
+
// hooks above run for both views, because the same contribution flips
|
|
239
|
+
// between them.
|
|
240
|
+
if (props.view === 'summary') return t.description
|
|
241
|
+
|
|
228
242
|
// A remote browser reaches this card through the gateway, which answers 403
|
|
229
243
|
// for the plugin's own prefix by design, so the route is unreachable exactly
|
|
230
244
|
// where a user is most likely to go looking for the setting. Rendering
|
|
@@ -232,7 +246,7 @@ export function LanGatewayCard(_props: LanGatewayCardProps): ReactNode {
|
|
|
232
246
|
// a broken one; say what is wrong and where the card does work instead.
|
|
233
247
|
if (loadFailed) {
|
|
234
248
|
return (
|
|
235
|
-
<
|
|
249
|
+
<div style={styles.card}>
|
|
236
250
|
<div style={styles.header}>
|
|
237
251
|
<span style={styles.headerTop}>
|
|
238
252
|
<span style={styles.name}>{t.title}</span>
|
|
@@ -242,7 +256,7 @@ export function LanGatewayCard(_props: LanGatewayCardProps): ReactNode {
|
|
|
242
256
|
<div style={styles.body}>
|
|
243
257
|
<p style={styles.hint}>{t.readOnly}</p>
|
|
244
258
|
</div>
|
|
245
|
-
</
|
|
259
|
+
</div>
|
|
246
260
|
)
|
|
247
261
|
}
|
|
248
262
|
if (route === null) return null
|
|
@@ -407,7 +421,7 @@ export function LanGatewayCard(_props: LanGatewayCardProps): ReactNode {
|
|
|
407
421
|
const statusLine = `${route.running ? t.running : t.stopped} · ${t.tls}: ${route.tls} · :${route.port}`
|
|
408
422
|
|
|
409
423
|
return (
|
|
410
|
-
<
|
|
424
|
+
<div style={open ? { ...styles.card, ...styles.cardOpen } : styles.card}>
|
|
411
425
|
<button
|
|
412
426
|
type="button"
|
|
413
427
|
style={styles.header}
|
|
@@ -449,7 +463,7 @@ export function LanGatewayCard(_props: LanGatewayCardProps): ReactNode {
|
|
|
449
463
|
</div>
|
|
450
464
|
)
|
|
451
465
|
: null}
|
|
452
|
-
</
|
|
466
|
+
</div>
|
|
453
467
|
)
|
|
454
468
|
}
|
|
455
469
|
|