fix(dsh): preserve exact host validation with serializable forms

This commit is contained in:
jinli.yl 2026-09-30 14:41:16 +08:00
parent 75c9e23f72
commit c723544ba1
2 changed files with 82 additions and 44 deletions

View file

@ -3,31 +3,19 @@ import z from "@deepseek-ai/schemastery";
import { validTimezone, validateDailyCron } from "./scheduling.js";
import type { ReMeConfig, ReMeConfigInput } from "./types.js";
// DSH sends volatile schemas to the browser as JSON, so these checks must serialize without callbacks.
const HTTP_ENDPOINT =
/^https?:\/\/(?:\[[0-9a-fA-F:.]+\]|[^\s/?#:@]+)(?::\d{1,5})?(?:[/?#][^\s]*)?$/;
const DAILY_CRON = /^(?:[0-5]?\d)\s+(?:[01]?\d|2[0-3])\s+\*\s+\*\s+\*$/;
const TIMEZONES = [
...Intl.supportedValuesOf("timeZone"),
"UTC",
"Etc/UTC",
"GMT",
"Etc/GMT",
];
const TIMEZONE = new RegExp(`^(?:${TIMEZONES.map(escapeRegExp).join("|")})$`);
const DEFAULT_ENDPOINT =
process.env.REME_URL ||
`http://${process.env.REME_HOST || "127.0.0.1"}:${
process.env.REME_PORT || "2333"
}`;
const DEFAULT_DREAM_CRON = process.env.REME_DSH_DREAM_CRON || "0 23 * * *";
export const Config = z.object({
endpoint: z
.string()
.pattern(HTTP_ENDPOINT)
.description("ReMe HTTP service URL")
.default(
process.env.REME_URL ||
`http://${process.env.REME_HOST || "127.0.0.1"}:${
process.env.REME_PORT || "2333"
}`,
)
.volatile(),
endpoint: checkedString(
DEFAULT_ENDPOINT,
"ReMe HTTP service URL",
assertEndpoint,
),
requestTimeoutMs: z.natural().min(1000).max(120000).default(10000).volatile(),
backgroundTimeoutMs: z
.natural()
@ -39,27 +27,47 @@ export const Config = z.object({
autoMemoryEnabled: z.boolean().default(true).volatile(),
autoMemoryInterval: z.natural().min(1).max(1000).default(5).volatile(),
autoDreamEnabled: z.boolean().default(true).volatile(),
dreamCron: z
.string()
.pattern(DAILY_CRON)
.description("Daily cron in the workspace timezone")
.default(process.env.REME_DSH_DREAM_CRON || "0 23 * * *")
.volatile(),
dreamCron: checkedString(
DEFAULT_DREAM_CRON,
"Daily cron in the workspace timezone",
validateDailyCron,
),
dreamHint: z.string().default("").volatile(),
dreamIntervalMs: z.natural().max(2147483647).default(0),
rootAgentsOnly: z.boolean().default(true).volatile(),
language: z.union(["en", "zh"]).default("en").volatile(),
searchLimit: z.natural().min(1).max(50).default(5).volatile(),
timezone: z
.string()
.pattern(TIMEZONE)
.default("Asia/Shanghai")
.description("IANA timezone matching the ReMe workspace")
.volatile(),
timezone: checkedString(
"Asia/Shanghai",
"IANA timezone matching the ReMe workspace",
(value) => {
if (!validTimezone(value))
throw new TypeError(`Invalid ReMe timezone: ${value}`);
},
),
});
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
function checkedString(
defaultValue: string,
description: string,
check: (value: string) => void,
) {
const form = z
.string()
.description(description)
.default(defaultValue)
.volatile();
const host = z
.transform(z.string(), (value) => {
check(value);
return value;
})
.description(description)
.default(defaultValue)
.volatile();
// DSH sends Config.toJSON() to the browser; keep Host validation while serializing a plain form field.
host.toJSON = () => form.toJSON();
return host;
}
const DEFAULT_CONFIG: Readonly<ReMeConfig> = Object.freeze({

View file

@ -30,10 +30,24 @@ test("exports a Cordis schema that rejects invalid configuration", async () => {
for (const input of [
{ endpoint: "not-a-url" },
{ endpoint: "http://localhost:bad-port" },
{ endpoint: "http://localhost:65536" },
{ endpoint: "http://[:::]" },
{ endpoint: "http://999.0.0.1" },
{ dreamCron: "every night" },
{ timezone: "Mars/Olympus" },
]) {
assert.ok(Config["~standard"].validate(input).issues?.length);
assert.throws(() => Config["~standard"].validate(input));
}
for (const timezone of [
"US/Pacific",
"US/Eastern",
"Etc/GMT+8",
"Etc/GMT-8",
]) {
const result = Config["~standard"].validate({ timezone });
assert.equal(result.issues, undefined);
assert.equal(result.value.timezone.get(), timezone);
}
});
@ -51,9 +65,10 @@ test("volatile settings survive Host projection and browser schema rehydration",
fields.map(([name]) => [name, defaults[name].get()]),
);
assert.equal(browserForm["~standard"].validate(values).issues, undefined);
assert.ok(
browserForm["~standard"].validate({ ...values, dreamCron: "bad" }).issues
?.length,
assert.equal(
browserForm["~standard"].validate({ ...values, timezone: "US/Pacific" })
.issues,
undefined,
);
});
@ -116,28 +131,43 @@ test("Loader keeps the previous live values when an update fails schema validati
const id = await ctx.loader.create({
id: "reme-memory",
name: "cordis:remeTest",
config: { endpoint: "http://valid.test", autoDreamEnabled: false },
config: {
endpoint: "http://valid.test",
autoMemoryEnabled: false,
autoDreamEnabled: false,
timezone: "US/Pacific",
},
});
const entry = ctx.loader.resolve(id);
await entry.fiber.await();
for (const patch of [
{ endpoint: "not-a-url" },
{ endpoint: "http://localhost:65536" },
{ endpoint: "http://999.0.0.1" },
{ dreamCron: "every night" },
{ timezone: "Mars/Olympus" },
]) {
await entry.update({
config: {
endpoint: "http://valid.test",
autoMemoryEnabled: true,
autoDreamEnabled: false,
timezone: "US/Pacific",
...patch,
},
});
assert.equal(live.endpoint.get(), "http://valid.test");
assert.equal(live.autoMemoryEnabled.get(), false);
assert.equal(live.dreamCron.get(), "0 23 * * *");
assert.equal(live.timezone.get(), "Asia/Shanghai");
assert.equal(live.timezone.get(), "US/Pacific");
}
await entry.update({
config: { endpoint: "http://updated.test", autoDreamEnabled: false },
config: {
endpoint: "http://updated.test",
autoMemoryEnabled: false,
autoDreamEnabled: false,
timezone: "US/Pacific",
},
});
assert.equal(live.endpoint.get(), "http://updated.test");
} finally {