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 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
- A file that already exists **without** a stamp is refused instead of stamped: its content may still
73
- need lifting, and stamping it would claim a migration that never ran and silently strand what the step
74
- rewrites. Measured: a schema-2 stamp over an old `notifications.telegram` block makes 0.9 report the
75
- key as unrecognized and ignore it, so the person's notifications end up silently off. Lifting a config
76
- is `home-hosted migrate`'s job, and the refusal names it.
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 meta = isRecord(existing) ? { ...existing } : {};
3213
- if (typeof meta.schema !== "number") {
3214
- if (fs12.existsSync(file)) {
3215
- throw new ConfigLayoutError(
3216
- `${where} carries no meta.schema, so its config shape is unknown; run \`home-hosted migrate\` before this plugin writes it`
3217
- );
3218
- }
3219
- const schema = panelConfigSchema();
3220
- if (schema !== null)
3221
- meta.schema = schema;
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) {