Merge upstream/dev into feature-response-headers

Resolve conflicts against upstream's refactors:

- server/db/sqlite/schema/schema.ts: adopt upstream's reindented
  sqliteTable(name, cols, indexes) form for sites/resources, re-applying
  the headers -> requestHeaders/responseHeaders split. Kept in sync with
  the Postgres schema.
- server/lib/traefik/headersMiddleware.ts: extend upstream's extracted
  buildCustomHeadersMiddleware helper to take requestHeaders and
  responseHeaders and emit both customRequestHeaders and
  customResponseHeaders.
- server/lib/traefik/getTraefikConfig.ts and
  server/private/lib/traefik/getTraefikConfig.ts: keep upstream's helper
  extraction and appendPathMatch refactor, dropping the superseded inline
  blocks.

Also carry the feature forward onto code that moved upstream:

- The resource settings UI moved from resources/proxy/[niceId]/proxy to
  resources/public/[niceId]/http, which dropped this branch's changes in
  the previous merge. Re-add the request/response header inputs there and
  rename the vestigial headers field on the tcp page.
- messages/da-DK.json is new upstream and still had the old customHeaders
  key; rename it in line with the other locales.

Per the contributing docs, versioned migrations are intentionally omitted
so maintainers can write them at release time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Julian van der Horst
2026-09-01 13:40:49 +02:00
co-authored by Claude Opus 5
694 changed files with 77422 additions and 12896 deletions
+70 -7
View File
@@ -38,7 +38,7 @@ import {
} from "@server/lib/schemas";
import { registry } from "@server/openApi";
import { OpenAPITags } from "@server/openApi";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { createCertificate } from "@server/routers/certificates/createCertificate";
import {
validateAndConstructDomain,
checkWildcardDomainConflict
@@ -249,6 +249,42 @@ const updateRawResourceBodySchema = z
registry.registerPath({
method: "post",
path: "/resource/{resourceId}",
description:
"Update a resource. Policy fields (sso, mfa, pincode, password, whitelist) update the inline policy when no shared resource policy is assigned; when a shared policy is assigned those fields override the shared policy for this resource only.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: updateResourceParamsSchema,
body: {
content: {
"application/json": {
schema: updateHttpResourceBodySchema.and(
updateRawResourceBodySchema
)
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}",
description:
"Update a resource. Policy fields (sso, mfa, pincode, password, whitelist) update the inline policy when no shared resource policy is assigned; when a shared policy is assigned those fields override the shared policy for this resource only.",
tags: [OpenAPITags.PublicResource],
@@ -318,8 +354,10 @@ export async function updateResource(
);
}
if (["http", "ssh", "rdp", "vnc"].includes(resource.mode)) {
// HANDLE UPDATING HTTP RESOURCES
if (
["http", "ssh", "rdp", "vnc", "inference"].includes(resource.mode)
) {
// HANDLE UPDATING HTTP / BROWSER / INFERENCE RESOURCES
return await updateHttpResource(
{
req,
@@ -502,6 +540,20 @@ async function updateHttpResource(
}
}
// Wildcard subdomains are not allowed for inference-mode resources
if (
resource.mode === "inference" &&
updateData.subdomain &&
updateData.subdomain.includes("*")
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Wildcard subdomains are not supported for inference-mode resources."
)
);
}
// Wildcard subdomains are a paid feature
if (updateData.subdomain && updateData.subdomain.includes("*")) {
if (!isLicensed) {
@@ -567,10 +619,23 @@ async function updateHttpResource(
logger.debug(`Full domain: ${fullDomain}`);
if (fullDomain) {
// Inference resources route through the central AI gateway
// rather than normal target-based proxying, so they're allowed
// to share a full-domain with a non-inference resource (and
// vice versa) - only conflicts within the same routing category
// are rejected. mode isn't updatable here, so `resource.mode`
// reflects the resource's actual (unchanging) routing category.
const [existingDomain] = await db
.select()
.from(resources)
.where(eq(resources.fullDomain, fullDomain));
.where(
and(
eq(resources.fullDomain, fullDomain),
resource.mode === "inference"
? ne(resources.mode, "inference")
: eq(resources.mode, "inference")
)
);
if (
existingDomain &&
@@ -636,9 +701,7 @@ async function updateHttpResource(
// Update the subdomain in the update data
updateData.subdomain = finalSubdomain;
if (build != "oss") {
await createCertificate(domainId, fullDomain, db);
}
await createCertificate(domainId, fullDomain, db);
}
let requestHeaders = undefined;