dsh-home-hosted 0.9.0 → 0.9.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.
- package/docs/PANEL.md +11 -5
- package/lib/index.js +85 -67
- package/lib/index.js.map +4 -4
- package/lib/types/home-hosted/config-file.d.ts +40 -0
- package/package.json +1 -1
package/docs/PANEL.md
CHANGED
|
@@ -69,11 +69,17 @@ a config it created before any panel had run was unstartable on 0.9. It now stam
|
|
|
69
69
|
every file it **creates**, reading the number from the pinned release rather than hard-coding it — a
|
|
70
70
|
fixed `2` would break again at the next bump, from the other side.
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
An existing file that does not already declare **the** schema the pinned panel reads is refused, in
|
|
73
|
+
both directions: below it needs `home-hosted migrate`, above it was written by a newer home-hosted
|
|
74
|
+
than this plugin pins. Stamping over either would claim a migration that never ran — measured, a
|
|
75
|
+
schema-2 stamp over an old `notifications.telegram` block makes 0.9 report the key as unrecognized and
|
|
76
|
+
ignore it, so the person's notifications end up silently off — and writing on regardless would leave
|
|
77
|
+
this plugin editing a panel that cannot boot while every call reports success. Lifting or downgrading
|
|
78
|
+
a config is `home-hosted migrate`'s job, and the refusal names the remedy for the direction it found.
|
|
79
|
+
|
|
80
|
+
The page says so **before** a write is attempted: a config the panel cannot boot is reported as the
|
|
81
|
+
panel's error, so the root is not shown as healthy until the first call fails. The entries are still
|
|
82
|
+
listed — the file is readable, it is only un-bootable — so nothing looks lost while you migrate it.
|
|
77
83
|
|
|
78
84
|
The floor is `0.9.0` for the same reason the pin is: a 0.8.x panel reads a schema-2 stamp as a *newer*
|
|
79
85
|
release's file and refuses that in turn, so no stamp satisfies both and there is no version-conditional
|
package/lib/index.js
CHANGED
|
@@ -109,6 +109,63 @@ var DEFAULT_SETTINGS = {
|
|
|
109
109
|
cli: { prefer: "pinned" }
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// src/rpc.ts
|
|
113
|
+
function answer(endpoint, result) {
|
|
114
|
+
return Response.json({ v: RPC_VERSION, endpoint, result });
|
|
115
|
+
}
|
|
116
|
+
function registerRpc(ctx, service) {
|
|
117
|
+
ctx.inject(["connection"], (scoped) => {
|
|
118
|
+
const connection = scoped.connection;
|
|
119
|
+
if (connection?.fetch?.register === void 0)
|
|
120
|
+
return;
|
|
121
|
+
scoped.effect(() => {
|
|
122
|
+
let dispose;
|
|
123
|
+
try {
|
|
124
|
+
dispose = connection.fetch.register({
|
|
125
|
+
// The exact-route registry is keyed by the full request path, so the
|
|
126
|
+
// `/api` prefix belongs here; the page composes the same URL from
|
|
127
|
+
// API_BASE + RPC_PATH.
|
|
128
|
+
path: `/api${RPC_PATH}`,
|
|
129
|
+
methods: ["POST"],
|
|
130
|
+
requestBody: "buffered",
|
|
131
|
+
fetch: async (request) => {
|
|
132
|
+
let body;
|
|
133
|
+
try {
|
|
134
|
+
body = await request.json();
|
|
135
|
+
} catch {
|
|
136
|
+
return Response.json({ error: "the request body is not JSON" }, { status: 400 });
|
|
137
|
+
}
|
|
138
|
+
if (body === null || typeof body !== "object" || body.v !== RPC_VERSION) {
|
|
139
|
+
return Response.json(
|
|
140
|
+
{ error: `this page and the host disagree on the protocol version (expected ${String(RPC_VERSION)})` },
|
|
141
|
+
{ status: 409 }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const value = await service.call(body.endpoint, body.payload);
|
|
146
|
+
return answer(body.endpoint, { ok: true, value });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const code = error?.code;
|
|
149
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
150
|
+
const detail = error?.detail;
|
|
151
|
+
return answer(body.endpoint, { ok: false, error: { code: typeof code === "string" ? code : "INTERNAL", message: message2, ...detail === void 0 ? {} : { detail } } });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const logger = scoped.logger;
|
|
157
|
+
logger?.warn?.(`[dsh-home-hosted] could not register ${RPC_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
return () => {
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return () => {
|
|
162
|
+
void Promise.resolve(dispose()).catch(() => {
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
}, "dsh-home-hosted: rpc route");
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
112
169
|
// src/service.ts
|
|
113
170
|
import { Service } from "@deepseek-ai/cordis";
|
|
114
171
|
import fs18 from "node:fs";
|
|
@@ -3176,9 +3233,12 @@ function readConfigSchemaFrom(manifestPath) {
|
|
|
3176
3233
|
// src/home-hosted/config-file.ts
|
|
3177
3234
|
var ConfigLayoutError = class extends Error {
|
|
3178
3235
|
name = "ConfigLayoutError";
|
|
3236
|
+
/** The wire code a caller reports; the root's layout is a readable config problem, not a crash. */
|
|
3237
|
+
code = "CONFIG_UNREADABLE";
|
|
3179
3238
|
};
|
|
3180
3239
|
var ConfigPatchError = class extends Error {
|
|
3181
3240
|
name = "ConfigPatchError";
|
|
3241
|
+
code = "INVALID_PATCH";
|
|
3182
3242
|
};
|
|
3183
3243
|
function readConfig(home, workspace = DEFAULT_WORKSPACE) {
|
|
3184
3244
|
const refusal = migrationRefusal(home);
|
|
@@ -3208,17 +3268,30 @@ function writeConfig(home, raw, writtenBy = "dsh-home-hosted", workspace = DEFAU
|
|
|
3208
3268
|
writeFileAtomic(file, `${JSON.stringify(next, null, 2)}
|
|
3209
3269
|
`);
|
|
3210
3270
|
}
|
|
3271
|
+
function schemaRefusal(meta, where, creating) {
|
|
3272
|
+
const pinned = panelConfigSchema();
|
|
3273
|
+
if (pinned === null)
|
|
3274
|
+
return null;
|
|
3275
|
+
const declared = isRecord(meta) && typeof meta.schema === "number" ? meta.schema : null;
|
|
3276
|
+
if (declared === null) {
|
|
3277
|
+
return creating ? null : `${where} carries no meta.schema, so its config shape is unknown; run \`home-hosted migrate\` before this plugin writes it`;
|
|
3278
|
+
}
|
|
3279
|
+
if (declared === pinned)
|
|
3280
|
+
return null;
|
|
3281
|
+
return declared < pinned ? `${where} is on config schema ${declared}, which home-hosted ${pinned} refuses to boot; run \`home-hosted migrate\` before this plugin writes it` : `${where} is on config schema ${declared}, written by a newer home-hosted than this plugin's ${pinned}; update home-hosted, or pin this plugin to the release that wrote it`;
|
|
3282
|
+
}
|
|
3211
3283
|
function stampedMeta(existing, writtenBy, where, file) {
|
|
3212
|
-
const
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3284
|
+
const onDisk = fs12.existsSync(file) ? readJson(file) : null;
|
|
3285
|
+
const diskMeta = isRecord(onDisk) && isRecord(onDisk.meta) ? onDisk.meta : {};
|
|
3286
|
+
const meta = { ...diskMeta, ...isRecord(existing) ? existing : {} };
|
|
3287
|
+
if (onDisk === null) {
|
|
3288
|
+
const pinned = panelConfigSchema();
|
|
3289
|
+
if (pinned !== null)
|
|
3290
|
+
meta.schema = pinned;
|
|
3291
|
+
} else {
|
|
3292
|
+
const refusal = schemaRefusal(meta, where, false);
|
|
3293
|
+
if (refusal !== null)
|
|
3294
|
+
throw new ConfigLayoutError(refusal);
|
|
3222
3295
|
}
|
|
3223
3296
|
meta.writtenBy = writtenBy;
|
|
3224
3297
|
return meta;
|
|
@@ -6461,6 +6534,8 @@ var HomeHostedService = class extends Service {
|
|
|
6461
6534
|
servers = read.servers;
|
|
6462
6535
|
if (read.error !== null)
|
|
6463
6536
|
lastError ??= read.error;
|
|
6537
|
+
const file = readConfig(this.options.home, this.managedWorkspace());
|
|
6538
|
+
lastError ??= schemaRefusal(file.raw?.meta, `servers.config.json in ${this.managedWorkspace()}`, !file.exists);
|
|
6464
6539
|
}
|
|
6465
6540
|
return {
|
|
6466
6541
|
defaultEntryId: this.options.defaultEntryId,
|
|
@@ -6694,63 +6769,6 @@ var HomeHostedService = class extends Service {
|
|
|
6694
6769
|
}
|
|
6695
6770
|
};
|
|
6696
6771
|
|
|
6697
|
-
// src/rpc.ts
|
|
6698
|
-
function answer(endpoint, result) {
|
|
6699
|
-
return Response.json({ v: RPC_VERSION, endpoint, result });
|
|
6700
|
-
}
|
|
6701
|
-
function registerRpc(ctx, service) {
|
|
6702
|
-
ctx.inject(["connection"], (scoped) => {
|
|
6703
|
-
const connection = scoped.connection;
|
|
6704
|
-
if (connection?.fetch?.register === void 0)
|
|
6705
|
-
return;
|
|
6706
|
-
scoped.effect(() => {
|
|
6707
|
-
let dispose;
|
|
6708
|
-
try {
|
|
6709
|
-
dispose = connection.fetch.register({
|
|
6710
|
-
// The exact-route registry is keyed by the full request path, so the
|
|
6711
|
-
// `/api` prefix belongs here; the page composes the same URL from
|
|
6712
|
-
// API_BASE + RPC_PATH.
|
|
6713
|
-
path: `/api${RPC_PATH}`,
|
|
6714
|
-
methods: ["POST"],
|
|
6715
|
-
requestBody: "buffered",
|
|
6716
|
-
fetch: async (request) => {
|
|
6717
|
-
let body;
|
|
6718
|
-
try {
|
|
6719
|
-
body = await request.json();
|
|
6720
|
-
} catch {
|
|
6721
|
-
return Response.json({ error: "the request body is not JSON" }, { status: 400 });
|
|
6722
|
-
}
|
|
6723
|
-
if (body === null || typeof body !== "object" || body.v !== RPC_VERSION) {
|
|
6724
|
-
return Response.json(
|
|
6725
|
-
{ error: `this page and the host disagree on the protocol version (expected ${String(RPC_VERSION)})` },
|
|
6726
|
-
{ status: 409 }
|
|
6727
|
-
);
|
|
6728
|
-
}
|
|
6729
|
-
try {
|
|
6730
|
-
const value = await service.call(body.endpoint, body.payload);
|
|
6731
|
-
return answer(body.endpoint, { ok: true, value });
|
|
6732
|
-
} catch (error) {
|
|
6733
|
-
const code = error instanceof HomeHostedError ? error.code : "INTERNAL";
|
|
6734
|
-
const message2 = error instanceof Error ? error.message : String(error);
|
|
6735
|
-
const detail = error?.detail;
|
|
6736
|
-
return answer(body.endpoint, { ok: false, error: { code, message: message2, ...detail === void 0 ? {} : { detail } } });
|
|
6737
|
-
}
|
|
6738
|
-
}
|
|
6739
|
-
});
|
|
6740
|
-
} catch (error) {
|
|
6741
|
-
const logger = scoped.logger;
|
|
6742
|
-
logger?.warn?.(`[dsh-home-hosted] could not register ${RPC_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
6743
|
-
return () => {
|
|
6744
|
-
};
|
|
6745
|
-
}
|
|
6746
|
-
return () => {
|
|
6747
|
-
void Promise.resolve(dispose()).catch(() => {
|
|
6748
|
-
});
|
|
6749
|
-
};
|
|
6750
|
-
}, "dsh-home-hosted: rpc route");
|
|
6751
|
-
});
|
|
6752
|
-
}
|
|
6753
|
-
|
|
6754
6772
|
// src/home-hosted/instances-notice.ts
|
|
6755
6773
|
var INSTANCES_CONTEXT_ORDER = 130;
|
|
6756
6774
|
function instancesContextText(instances, enabled) {
|