dsh-home-hosted 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -19
- package/docs/DESIGN.md +20 -0
- package/lib/client.js +89 -6
- package/lib/index.js +107 -7
- package/lib/index.js.map +3 -3
- package/lib/types/home-hosted/entries.d.ts +18 -4
- package/lib/types/home-hosted/resolve.d.ts +3 -1
- package/lib/types/home-hosted/web-notice.d.ts +18 -0
- package/lib/types/service.d.ts +4 -0
- package/lib/types/shared/contracts.d.ts +12 -0
- package/package.json +18 -4
package/lib/index.js
CHANGED
|
@@ -39,6 +39,8 @@ var DEFAULT_SETTINGS = {
|
|
|
39
39
|
// Every tool on by default; the session's own permission mode is what gates them.
|
|
40
40
|
agentTools: { enabled: true, allow: [...AGENT_TOOL_NAMES] },
|
|
41
41
|
panel: { port: null },
|
|
42
|
+
authNotice: true,
|
|
43
|
+
uiStyle: "detailed",
|
|
42
44
|
cli: { prefer: "pinned" }
|
|
43
45
|
};
|
|
44
46
|
|
|
@@ -2179,18 +2181,20 @@ function defaultIntent(id, platform = process.platform) {
|
|
|
2179
2181
|
id,
|
|
2180
2182
|
autostart: true,
|
|
2181
2183
|
onPortConflict: defaultOnPortConflict(platform),
|
|
2182
|
-
stopKillPortHolders: true
|
|
2184
|
+
stopKillPortHolders: true,
|
|
2185
|
+
persistent: true
|
|
2183
2186
|
};
|
|
2184
2187
|
}
|
|
2185
|
-
function ownedPatch(intent, live) {
|
|
2188
|
+
function ownedPatch(intent, live, options = {}) {
|
|
2186
2189
|
const stop = live?.stop ?? {};
|
|
2187
2190
|
return {
|
|
2188
2191
|
autostart: intent.autostart,
|
|
2189
2192
|
onPortConflict: intent.onPortConflict,
|
|
2193
|
+
...options.persistent === false ? {} : { persistent: intent.persistent },
|
|
2190
2194
|
stop: { ...stop, killPortHolders: intent.stopKillPortHolders }
|
|
2191
2195
|
};
|
|
2192
2196
|
}
|
|
2193
|
-
function ownedDrift(live, intent) {
|
|
2197
|
+
function ownedDrift(live, intent, options = {}) {
|
|
2194
2198
|
if (live === null)
|
|
2195
2199
|
return ["missing entry"];
|
|
2196
2200
|
const drift = [];
|
|
@@ -2198,6 +2202,8 @@ function ownedDrift(live, intent) {
|
|
|
2198
2202
|
drift.push("autostart");
|
|
2199
2203
|
if (live.onPortConflict !== intent.onPortConflict)
|
|
2200
2204
|
drift.push("onPortConflict");
|
|
2205
|
+
if (options.persistent !== false && (live.persistent ?? false) !== intent.persistent)
|
|
2206
|
+
drift.push("persistent");
|
|
2201
2207
|
const killPortHolders = live.stop?.killPortHolders;
|
|
2202
2208
|
if (killPortHolders !== intent.stopKillPortHolders)
|
|
2203
2209
|
drift.push("stop.killPortHolders");
|
|
@@ -2209,6 +2215,7 @@ function snapshotOwned(live) {
|
|
|
2209
2215
|
id: live.id,
|
|
2210
2216
|
autostart: typeof live.autostart === "boolean" ? live.autostart : false,
|
|
2211
2217
|
onPortConflict: isOnPortConflict(live.onPortConflict) ? live.onPortConflict : "block",
|
|
2218
|
+
persistent: live.persistent === true,
|
|
2212
2219
|
stop: { killPortHolders: typeof killPortHolders === "boolean" ? killPortHolders : false }
|
|
2213
2220
|
};
|
|
2214
2221
|
}
|
|
@@ -2218,6 +2225,7 @@ function restorePatch(live, snapshot) {
|
|
|
2218
2225
|
return {
|
|
2219
2226
|
autostart: snapshot?.autostart ?? false,
|
|
2220
2227
|
onPortConflict: snapshot?.onPortConflict ?? "block",
|
|
2228
|
+
persistent: snapshot?.persistent === true,
|
|
2221
2229
|
stop: {
|
|
2222
2230
|
...stop,
|
|
2223
2231
|
killPortHolders: typeof previous.killPortHolders === "boolean" ? previous.killPortHolders : false
|
|
@@ -2230,9 +2238,10 @@ import { createRequire } from "node:module";
|
|
|
2230
2238
|
import fs9 from "node:fs";
|
|
2231
2239
|
import path9 from "node:path";
|
|
2232
2240
|
import process6 from "node:process";
|
|
2233
|
-
var EXPECTED_RANGE = "^0.6.
|
|
2241
|
+
var EXPECTED_RANGE = "^0.6.4";
|
|
2234
2242
|
var MIN_SUPPORTED_VERSION = "0.4.1";
|
|
2235
2243
|
var MIN_KILL_VERSION = "0.6.0";
|
|
2244
|
+
var MIN_PERSISTENT_VERSION = "0.6.3";
|
|
2236
2245
|
function pinnedManifestPath() {
|
|
2237
2246
|
try {
|
|
2238
2247
|
return createRequire(import.meta.url).resolve("home-hosted/package.json");
|
|
@@ -2988,6 +2997,13 @@ var HomeHostedService = class extends Service {
|
|
|
2988
2997
|
// -------------------------------------------------------------------------
|
|
2989
2998
|
// Panel
|
|
2990
2999
|
// -------------------------------------------------------------------------
|
|
3000
|
+
/** The panel page showing this session's log, where its tokenised URL is. */
|
|
3001
|
+
panelLogUrl() {
|
|
3002
|
+
const url = this.runtime()?.url;
|
|
3003
|
+
if (url === void 0 || url.length === 0)
|
|
3004
|
+
return null;
|
|
3005
|
+
return `${url.replace(/\/+$/, "")}/logs?server=${encodeURIComponent(this.options.defaultEntryId)}`;
|
|
3006
|
+
}
|
|
2991
3007
|
/** The port the panel's config holds, without touching a running panel. */
|
|
2992
3008
|
configuredPort() {
|
|
2993
3009
|
const control = readConfig(this.options.home).raw?.control;
|
|
@@ -3101,7 +3117,7 @@ var HomeHostedService = class extends Service {
|
|
|
3101
3117
|
snapshots[intent.id] = live === null ? { id: intent.id } : snapshotOwned(live.config);
|
|
3102
3118
|
this.saveSnapshots(snapshots);
|
|
3103
3119
|
}
|
|
3104
|
-
const patch = ownedPatch(intent, live?.config ?? null);
|
|
3120
|
+
const patch = ownedPatch(intent, live?.config ?? null, { persistent: await this.supportsPersistent() });
|
|
3105
3121
|
const client = await this.tryClient();
|
|
3106
3122
|
if (client !== null) {
|
|
3107
3123
|
if (live !== null) {
|
|
@@ -3153,6 +3169,11 @@ var HomeHostedService = class extends Service {
|
|
|
3153
3169
|
const { resolution } = await this.cli();
|
|
3154
3170
|
return resolution.status.version;
|
|
3155
3171
|
}
|
|
3172
|
+
/** Whether the panel whose schema parses our write knows `persistent`. */
|
|
3173
|
+
async supportsPersistent() {
|
|
3174
|
+
const version = await this.configVersion();
|
|
3175
|
+
return version !== null && compareVersions(version, MIN_PERSISTENT_VERSION) >= 0;
|
|
3176
|
+
}
|
|
3156
3177
|
async assertPolicySupported(intent) {
|
|
3157
3178
|
if (intent.onPortConflict !== "kill")
|
|
3158
3179
|
return;
|
|
@@ -3180,7 +3201,8 @@ var HomeHostedService = class extends Service {
|
|
|
3180
3201
|
id: raw.id,
|
|
3181
3202
|
autostart: raw.autostart === true,
|
|
3182
3203
|
onPortConflict: isOnPortConflict(requested) ? requested : fallback.onPortConflict,
|
|
3183
|
-
stopKillPortHolders: raw.stopKillPortHolders ?? fallback.stopKillPortHolders
|
|
3204
|
+
stopKillPortHolders: raw.stopKillPortHolders ?? fallback.stopKillPortHolders,
|
|
3205
|
+
persistent: raw.persistent ?? fallback.persistent
|
|
3184
3206
|
};
|
|
3185
3207
|
await this.assertPolicySupported(intent);
|
|
3186
3208
|
await this.writeOwned(intent);
|
|
@@ -3318,6 +3340,7 @@ var HomeHostedService = class extends Service {
|
|
|
3318
3340
|
const settings = this.options.settings.get();
|
|
3319
3341
|
const live = await this.liveEntries();
|
|
3320
3342
|
const snapshots = this.snapshots();
|
|
3343
|
+
const persistent = await this.supportsPersistent();
|
|
3321
3344
|
const ids = /* @__PURE__ */ new Set([this.options.defaultEntryId, ...settings.entries.map((entry) => entry.id)]);
|
|
3322
3345
|
return [...ids].map((id) => {
|
|
3323
3346
|
const intent = this.options.settings.intentFor(id);
|
|
@@ -3326,7 +3349,7 @@ var HomeHostedService = class extends Service {
|
|
|
3326
3349
|
intent,
|
|
3327
3350
|
exists: entry !== null,
|
|
3328
3351
|
managed: snapshots[id] !== void 0,
|
|
3329
|
-
drift: entry === null ? ["missing entry"] : ownedDrift(entry.config, intent),
|
|
3352
|
+
drift: entry === null ? ["missing entry"] : ownedDrift(entry.config, intent, { persistent }),
|
|
3330
3353
|
live: entry?.view ?? null,
|
|
3331
3354
|
snapshot: snapshots[id] ?? null
|
|
3332
3355
|
};
|
|
@@ -3680,6 +3703,76 @@ function registerRpc(ctx, service) {
|
|
|
3680
3703
|
});
|
|
3681
3704
|
}
|
|
3682
3705
|
|
|
3706
|
+
// src/home-hosted/web-notice.ts
|
|
3707
|
+
var STOCK_UNAUTHORIZED = "dsh web authentication required; reopen the URL printed by dsh web.";
|
|
3708
|
+
function authNoticeBody(logUrl) {
|
|
3709
|
+
if (logUrl === null)
|
|
3710
|
+
return `${STOCK_UNAUTHORIZED}
|
|
3711
|
+
`;
|
|
3712
|
+
return `${STOCK_UNAUTHORIZED}
|
|
3713
|
+
home-hosted: the URL with its token is in the dsh log at ${logUrl}. An authentication plugin can sign you in seamlessly instead.
|
|
3714
|
+
`;
|
|
3715
|
+
}
|
|
3716
|
+
function wrap(auth, logUrlOf) {
|
|
3717
|
+
const original = auth.writeUnauthorized.bind(auth);
|
|
3718
|
+
auth.writeUnauthorized = (req, res) => {
|
|
3719
|
+
const response = res;
|
|
3720
|
+
const end = response.end;
|
|
3721
|
+
if (typeof end !== "function" || response.writableEnded === true) {
|
|
3722
|
+
original(req, res);
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
let restored = false;
|
|
3726
|
+
const restore = () => {
|
|
3727
|
+
if (restored) return;
|
|
3728
|
+
restored = true;
|
|
3729
|
+
try {
|
|
3730
|
+
response.end = end;
|
|
3731
|
+
} catch {
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3734
|
+
try {
|
|
3735
|
+
response.end = (...args) => {
|
|
3736
|
+
restore();
|
|
3737
|
+
const [chunk, ...rest] = args;
|
|
3738
|
+
let body = chunk;
|
|
3739
|
+
if (typeof chunk === "string" && chunk.trim() === STOCK_UNAUTHORIZED) {
|
|
3740
|
+
try {
|
|
3741
|
+
body = authNoticeBody(logUrlOf());
|
|
3742
|
+
} catch {
|
|
3743
|
+
body = chunk;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
return end.call(response, body, ...rest);
|
|
3747
|
+
};
|
|
3748
|
+
} catch {
|
|
3749
|
+
original(req, res);
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
try {
|
|
3753
|
+
response.once?.("finish", restore);
|
|
3754
|
+
response.once?.("close", restore);
|
|
3755
|
+
} catch {
|
|
3756
|
+
}
|
|
3757
|
+
original(req, res);
|
|
3758
|
+
};
|
|
3759
|
+
return () => {
|
|
3760
|
+
auth.writeUnauthorized = original;
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
function registerAuthNotice(ctx, logUrlOf) {
|
|
3764
|
+
ctx.inject(["connection"], (scoped) => {
|
|
3765
|
+
scoped.effect(() => {
|
|
3766
|
+
const connection = scoped.get("connection");
|
|
3767
|
+
const auth = connection?.browserAuth;
|
|
3768
|
+
if (typeof auth?.writeUnauthorized !== "function")
|
|
3769
|
+
return () => {
|
|
3770
|
+
};
|
|
3771
|
+
return wrap(auth, logUrlOf);
|
|
3772
|
+
}, "dsh-home-hosted: sign-in notice");
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3683
3776
|
// src/settings.ts
|
|
3684
3777
|
function knownTool(value) {
|
|
3685
3778
|
return typeof value === "string" && AGENT_TOOL_NAMES.includes(value);
|
|
@@ -3689,6 +3782,8 @@ function normalizeIntent(value, fallbackId) {
|
|
|
3689
3782
|
return {
|
|
3690
3783
|
id: base.id,
|
|
3691
3784
|
autostart: typeof value?.autostart === "boolean" ? value.autostart : base.autostart,
|
|
3785
|
+
// Absent means a stored intent from before persistence existed: it wants it.
|
|
3786
|
+
persistent: typeof value?.persistent === "boolean" ? value.persistent : base.persistent,
|
|
3692
3787
|
// Only a policy the panel's own schema parses may reach the config it boots from.
|
|
3693
3788
|
onPortConflict: isOnPortConflict(value?.onPortConflict) ? value.onPortConflict : base.onPortConflict,
|
|
3694
3789
|
stopKillPortHolders: typeof value?.stopKillPortHolders === "boolean" ? value.stopKillPortHolders : base.stopKillPortHolders
|
|
@@ -3735,6 +3830,8 @@ function normalize(raw, fallbackEntryId) {
|
|
|
3735
3830
|
// and a value it cannot parse stops it booting at all.
|
|
3736
3831
|
port: typeof raw?.panel?.port === "number" && Number.isInteger(raw.panel.port) && raw.panel.port >= 1 && raw.panel.port <= 65535 ? raw.panel.port : null
|
|
3737
3832
|
},
|
|
3833
|
+
authNotice: raw?.authNotice === void 0 ? DEFAULT_SETTINGS.authNotice : raw.authNotice === true,
|
|
3834
|
+
uiStyle: raw?.uiStyle === "compact" ? "compact" : "detailed",
|
|
3738
3835
|
agentTools: {
|
|
3739
3836
|
// Absent means the default (on), and so does the pair the previous
|
|
3740
3837
|
// release wrote for it; any other explicit false stays false.
|
|
@@ -3770,6 +3867,8 @@ var SettingsStore = class {
|
|
|
3770
3867
|
manageDsh: patch.manageDsh ?? this.current.manageDsh,
|
|
3771
3868
|
entries: patch.entries ?? this.current.entries,
|
|
3772
3869
|
panel: { ...this.current.panel, ...patch.panel ?? {} },
|
|
3870
|
+
authNotice: patch.authNotice ?? this.current.authNotice,
|
|
3871
|
+
uiStyle: patch.uiStyle ?? this.current.uiStyle,
|
|
3773
3872
|
agentTools: { ...this.current.agentTools, ...patch.agentTools ?? {} },
|
|
3774
3873
|
cli: { ...this.current.cli, ...patch.cli ?? {} }
|
|
3775
3874
|
}, this.fallbackEntryId);
|
|
@@ -3989,6 +4088,7 @@ function apply(ctx, config) {
|
|
|
3989
4088
|
});
|
|
3990
4089
|
registerRpc(ctx, service);
|
|
3991
4090
|
registerAgentTools(ctx, service, settings);
|
|
4091
|
+
registerAuthNotice(ctx, () => settings.get().authNotice ? service.panelLogUrl() : null);
|
|
3992
4092
|
ctx.effect(() => {
|
|
3993
4093
|
const timer = setTimeout(() => {
|
|
3994
4094
|
void service.reconcile().catch(() => {
|