From 8e1905a695add77d18ca8a2e16cd4c40437bbca9 Mon Sep 17 00:00:00 2001
From: Mustafa <104644957+Blacks-Army@users.noreply.github.com>
Date: Sun, 12 Apr 2026 20:19:32 +0200
Subject: [PATCH 001/107] Exclude local/private/CGNAT IPs from COUNTRY=ALL and
ASN=ALL/AS0 geo-blocking rules
---
server/routers/badger/verifySession.ts | 57 ++++++++++++++++++++++----
1 file changed, 50 insertions(+), 7 deletions(-)
diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts
index e2e5f6766..d3c110728 100644
--- a/server/routers/badger/verifySession.ts
+++ b/server/routers/badger/verifySession.ts
@@ -1003,7 +1003,11 @@ async function checkRules(
isIpInCidr(clientIp, rule.value)
) {
return rule.action as any;
- } else if (clientIp && rule.match == "IP" && clientIp == rule.value) {
+ } else if (
+ clientIp &&
+ rule.match == "IP" &&
+ clientIp == rule.value
+ ) {
return rule.action as any;
} else if (
path &&
@@ -1013,16 +1017,35 @@ async function checkRules(
return rule.action as any;
} else if (
clientIp &&
- rule.match == "COUNTRY" &&
- (await isIpInGeoIP(ipCC, rule.value))
+ rule.match == "COUNTRY"
) {
- return rule.action as any;
+ // COUNTRY=ALL should not affect local/private/CGNAT addresses.
+ if (
+ rule.value.toUpperCase() === "ALL" &&
+ isLocalOrCarrierGradeNatIp(clientIp)
+ ) {
+ continue;
+ }
+
+ if (await isIpInGeoIP(ipCC, rule.value)) {
+ return rule.action as any;
+ }
} else if (
clientIp &&
- rule.match == "ASN" &&
- (await isIpInAsn(ipAsn, rule.value))
+ rule.match == "ASN"
) {
- return rule.action as any;
+ // ASN=ALL/AS0 should not affect local/private/CGNAT addresses.
+ if (
+ (rule.value.toUpperCase() === "ALL" ||
+ rule.value.toUpperCase() === "AS0") &&
+ isLocalOrCarrierGradeNatIp(clientIp)
+ ) {
+ continue;
+ }
+
+ if (await isIpInAsn(ipAsn, rule.value)) {
+ return rule.action as any;
+ }
} else if (
clientIp &&
rule.match == "REGION" &&
@@ -1184,6 +1207,26 @@ async function isIpInGeoIP(
return ipCountryCode?.toUpperCase() === checkCountryCode.toUpperCase();
}
+function isLocalOrCarrierGradeNatIp(ip: string): boolean {
+ const localAndCgnatCidrs = [
+ "10.0.0.0/8",
+ "172.16.0.0/12",
+ "192.168.0.0/16",
+ "100.64.0.0/10",
+ "127.0.0.0/8",
+ "169.254.0.0/16",
+ "::1/128",
+ "fc00::/7",
+ "fe80::/10"
+ ];
+
+ try {
+ return localAndCgnatCidrs.some((cidr) => isIpInCidr(ip, cidr));
+ } catch {
+ return false;
+ }
+}
+
async function isIpInAsn(
ipAsn: number | undefined,
checkAsn: string
From 8685cf42087c9b47958c20f2d92393d6cecd73dd Mon Sep 17 00:00:00 2001
From: Milo Schwartz
Date: Wed, 29 Apr 2026 02:03:18 -0400
Subject: [PATCH 002/107] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index a4bfb9fe8..562b35d40 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@
-Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
+Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
## Installation
From 81972dbb73d250843a8c5b0cba1889ea36d2c51f Mon Sep 17 00:00:00 2001
From: Owen
Date: Thu, 30 Apr 2026 10:56:12 -0700
Subject: [PATCH 003/107] Add name to migration
Fixes #2943
---
server/setup/scriptsPg/1.18.0.ts | 11 ++++++++++-
server/setup/scriptsSqlite/1.18.0.ts | 13 +++++++++++--
2 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/server/setup/scriptsPg/1.18.0.ts b/server/setup/scriptsPg/1.18.0.ts
index df22faa2d..88b2fb5bc 100644
--- a/server/setup/scriptsPg/1.18.0.ts
+++ b/server/setup/scriptsPg/1.18.0.ts
@@ -16,6 +16,9 @@ export default async function migration() {
thc."targetId",
t."siteId",
s."orgId",
+ r."name" AS "resourceName",
+ t."ip",
+ t."port",
thc."hcEnabled",
thc."hcPath",
thc."hcScheme",
@@ -33,13 +36,17 @@ export default async function migration() {
thc."hcTlsServerName"
FROM "targetHealthCheck" thc
JOIN "targets" t ON thc."targetId" = t."targetId"
- JOIN "sites" s ON t."siteId" = s."siteId"`
+ JOIN "sites" s ON t."siteId" = s."siteId"
+ JOIN "resources" r ON t."resourceId" = r."resourceId"`
);
const existingHealthChecks = healthChecksQuery.rows as {
targetHealthCheckId: number;
targetId: number;
siteId: number;
orgId: string;
+ resourceName: string;
+ ip: string;
+ port: number;
hcEnabled: boolean;
hcPath: string | null;
hcScheme: string | null;
@@ -385,6 +392,7 @@ export default async function migration() {
"targetId",
"orgId",
"siteId",
+ "name",
"hcEnabled",
"hcPath",
"hcScheme",
@@ -405,6 +413,7 @@ export default async function migration() {
${hc.targetId},
${hc.orgId},
${hc.siteId},
+ ${`Resource ${hc.resourceName} - ${hc.ip}:${hc.port}`},
${hc.hcEnabled},
${hc.hcPath},
${hc.hcScheme},
diff --git a/server/setup/scriptsSqlite/1.18.0.ts b/server/setup/scriptsSqlite/1.18.0.ts
index 49ee8c450..a5078e2d3 100644
--- a/server/setup/scriptsSqlite/1.18.0.ts
+++ b/server/setup/scriptsSqlite/1.18.0.ts
@@ -22,6 +22,9 @@ export default async function migration() {
thc."targetId",
t."siteId",
s."orgId",
+ r."name" AS "resourceName",
+ t."ip",
+ t."port",
thc."hcEnabled",
thc."hcPath",
thc."hcScheme",
@@ -39,13 +42,17 @@ export default async function migration() {
thc."hcTlsServerName"
FROM 'targetHealthCheck' thc
JOIN 'targets' t ON thc."targetId" = t."targetId"
- JOIN 'sites' s ON t."siteId" = s."siteId"`
+ JOIN 'sites' s ON t."siteId" = s."siteId"
+ JOIN 'resources' r ON t."resourceId" = r."resourceId"`
)
.all() as {
targetHealthCheckId: number;
targetId: number;
siteId: number;
orgId: string;
+ resourceName: string;
+ ip: string;
+ port: number;
hcEnabled: number;
hcPath: string | null;
hcScheme: string | null;
@@ -392,6 +399,7 @@ export default async function migration() {
"targetId",
"orgId",
"siteId",
+ "name",
"hcEnabled",
"hcPath",
"hcScheme",
@@ -407,7 +415,7 @@ export default async function migration() {
"hcStatus",
"hcHealth",
"hcTlsServerName"
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
const insertAll = db.transaction(() => {
@@ -417,6 +425,7 @@ export default async function migration() {
hc.targetId,
hc.orgId,
hc.siteId,
+ `Resource ${hc.resourceName} - ${hc.ip}:${hc.port}`,
hc.hcEnabled,
hc.hcPath,
hc.hcScheme,
From d3e4d8cda88e3c90cb1e9c66f743a70b01e2f694 Mon Sep 17 00:00:00 2001
From: Owen
Date: Thu, 30 Apr 2026 11:39:37 -0700
Subject: [PATCH 004/107] Fix pr blueprints not picking up site
---
server/lib/blueprints/clientResources.ts | 71 ++++++++++++------------
1 file changed, 34 insertions(+), 37 deletions(-)
diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts
index 1b2ec2ef7..21476b580 100644
--- a/server/lib/blueprints/clientResources.ts
+++ b/server/lib/blueprints/clientResources.ts
@@ -131,41 +131,22 @@ export async function updateClientResources(
: [];
const allSites: { siteId: number }[] = [];
+
if (resourceData.site) {
- let siteSingle;
- const resourceSiteId = resourceData.site;
-
- if (resourceSiteId) {
- // Look up site by niceId
- [siteSingle] = await trx
- .select({ siteId: sites.siteId })
- .from(sites)
- .where(
- and(
- eq(sites.niceId, resourceSiteId),
- eq(sites.orgId, orgId)
- )
+ // Look up site by niceId
+ const [siteSingle] = await trx
+ .select({ siteId: sites.siteId })
+ .from(sites)
+ .where(
+ and(
+ eq(sites.niceId, resourceData.site),
+ eq(sites.orgId, orgId)
)
- .limit(1);
- } else if (siteId) {
- // Use the provided siteId directly, but verify it belongs to the org
- [siteSingle] = await trx
- .select({ siteId: sites.siteId })
- .from(sites)
- .where(
- and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
- )
- .limit(1);
- } else {
- throw new Error(`Target site is required`);
+ )
+ .limit(1);
+ if (siteSingle) {
+ allSites.push(siteSingle);
}
-
- if (!siteSingle) {
- throw new Error(
- `Site not found: ${resourceSiteId} in org ${orgId}`
- );
- }
- allSites.push(siteSingle);
}
if (resourceData.sites) {
@@ -180,15 +161,31 @@ export async function updateClientResources(
)
)
.limit(1);
- if (!site) {
- throw new Error(
- `Site not found: ${siteId} in org ${orgId}`
- );
+ if (site) {
+ allSites.push(site);
}
- allSites.push(site);
}
}
+ if (siteId && allSites.length === 0) {
+ // only add if there are not provided sites
+ // Use the provided siteId directly, but verify it belongs to the org
+ const [siteSingle] = await trx
+ .select({ siteId: sites.siteId })
+ .from(sites)
+ .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
+ .limit(1);
+ if (siteSingle) {
+ allSites.push(siteSingle);
+ }
+ }
+
+ if (allSites.length === 0) {
+ throw new Error(
+ `No valid sites found for private private resource ${resourceNiceId} in org ${orgId}`
+ );
+ }
+
if (existingResource) {
let domainInfo:
| { subdomain: string | null; domainId: string }
From 416e124c021d52bc8abd7a7263990165e0211167 Mon Sep 17 00:00:00 2001
From: Owen
Date: Thu, 30 Apr 2026 11:53:55 -0700
Subject: [PATCH 005/107] Rotate the secret on the new things using it
---
cli/commands/rotateServerSecret.ts | 134 ++++++++++++++++++++++++++++-
1 file changed, 133 insertions(+), 1 deletion(-)
diff --git a/cli/commands/rotateServerSecret.ts b/cli/commands/rotateServerSecret.ts
index d3828f0e5..afac262b2 100644
--- a/cli/commands/rotateServerSecret.ts
+++ b/cli/commands/rotateServerSecret.ts
@@ -1,5 +1,5 @@
import { CommandModule } from "yargs";
-import { db, idpOidcConfig, licenseKey } from "@server/db";
+import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions } from "@server/db";
import { encrypt, decrypt } from "@server/lib/crypto";
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { eq } from "drizzle-orm";
@@ -129,9 +129,15 @@ export const rotateServerSecret: CommandModule<
console.log("\nReading encrypted data from database...");
const idpConfigs = await db.select().from(idpOidcConfig);
const licenseKeys = await db.select().from(licenseKey);
+ const certs = await db.select().from(certificates);
+ const streamingDestinations = await db.select().from(eventStreamingDestinations);
+ const webhookActions = await db.select().from(alertWebhookActions);
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
console.log(`Found ${licenseKeys.length} license key(s)`);
+ console.log(`Found ${certs.length} certificate(s)`);
+ console.log(`Found ${streamingDestinations.length} event streaming destination(s)`);
+ console.log(`Found ${webhookActions.length} alert webhook action(s)`);
// Prepare all decrypted and re-encrypted values
console.log("\nDecrypting and re-encrypting values...");
@@ -149,8 +155,27 @@ export const rotateServerSecret: CommandModule<
encryptedInstanceId: string;
};
+ type CertUpdate = {
+ certId: number;
+ encryptedCertFile: string | null;
+ encryptedKeyFile: string | null;
+ };
+
+ type StreamingDestinationUpdate = {
+ destinationId: number;
+ encryptedConfig: string;
+ };
+
+ type WebhookActionUpdate = {
+ webhookActionId: number;
+ encryptedConfig: string;
+ };
+
const idpUpdates: IdpUpdate[] = [];
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
+ const certUpdates: CertUpdate[] = [];
+ const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
+ const webhookActionUpdates: WebhookActionUpdate[] = [];
// Process idpOidcConfig entries
for (const idpConfig of idpConfigs) {
@@ -217,6 +242,70 @@ export const rotateServerSecret: CommandModule<
}
}
+ // Process certificate entries
+ for (const cert of certs) {
+ try {
+ const encryptedCertFile = cert.certFile
+ ? encrypt(decrypt(cert.certFile, oldSecret), newSecret)
+ : null;
+ const encryptedKeyFile = cert.keyFile
+ ? encrypt(decrypt(cert.keyFile, oldSecret), newSecret)
+ : null;
+
+ certUpdates.push({
+ certId: cert.certId,
+ encryptedCertFile,
+ encryptedKeyFile
+ });
+ } catch (error) {
+ console.error(
+ `Error processing certificate ${cert.certId} (${cert.domain}):`,
+ error
+ );
+ throw error;
+ }
+ }
+
+ // Process eventStreamingDestinations entries
+ for (const dest of streamingDestinations) {
+ try {
+ const decryptedConfig = decrypt(dest.config, oldSecret);
+ const encryptedConfig = encrypt(decryptedConfig, newSecret);
+
+ streamingDestinationUpdates.push({
+ destinationId: dest.destinationId,
+ encryptedConfig
+ });
+ } catch (error) {
+ console.error(
+ `Error processing event streaming destination ${dest.destinationId}:`,
+ error
+ );
+ throw error;
+ }
+ }
+
+ // Process alertWebhookActions entries
+ for (const webhook of webhookActions) {
+ try {
+ if (webhook.config == null) continue;
+
+ const decryptedConfig = decrypt(webhook.config, oldSecret);
+ const encryptedConfig = encrypt(decryptedConfig, newSecret);
+
+ webhookActionUpdates.push({
+ webhookActionId: webhook.webhookActionId,
+ encryptedConfig
+ });
+ } catch (error) {
+ console.error(
+ `Error processing alert webhook action ${webhook.webhookActionId}:`,
+ error
+ );
+ throw error;
+ }
+ }
+
// Perform all database updates in a single transaction
console.log("\nUpdating database in transaction...");
await db.transaction(async (trx) => {
@@ -250,10 +339,50 @@ export const rotateServerSecret: CommandModule<
instanceId: update.encryptedInstanceId
});
}
+
+ // Update certificate entries
+ for (const update of certUpdates) {
+ await trx
+ .update(certificates)
+ .set({
+ certFile: update.encryptedCertFile,
+ keyFile: update.encryptedKeyFile
+ })
+ .where(eq(certificates.certId, update.certId));
+ }
+
+ // Update event streaming destination entries
+ for (const update of streamingDestinationUpdates) {
+ await trx
+ .update(eventStreamingDestinations)
+ .set({ config: update.encryptedConfig })
+ .where(
+ eq(
+ eventStreamingDestinations.destinationId,
+ update.destinationId
+ )
+ );
+ }
+
+ // Update alert webhook action entries
+ for (const update of webhookActionUpdates) {
+ await trx
+ .update(alertWebhookActions)
+ .set({ config: update.encryptedConfig })
+ .where(
+ eq(
+ alertWebhookActions.webhookActionId,
+ update.webhookActionId
+ )
+ );
+ }
});
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
+ console.log(`Rotated ${certUpdates.length} certificate(s)`);
+ console.log(`Rotated ${streamingDestinationUpdates.length} event streaming destination(s)`);
+ console.log(`Rotated ${webhookActionUpdates.length} alert webhook action(s)`);
// Update config file with new secret
console.log("\nUpdating config file...");
@@ -270,6 +399,9 @@ export const rotateServerSecret: CommandModule<
console.log(`\nSummary:`);
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
console.log(` - License keys: ${licenseKeyUpdates.length}`);
+ console.log(` - Certificates: ${certUpdates.length}`);
+ console.log(` - Event streaming destinations: ${streamingDestinationUpdates.length}`);
+ console.log(` - Alert webhook actions: ${webhookActionUpdates.length}`);
console.log(
`\n IMPORTANT: Restart the server for the new secret to take effect.`
);
From 68f551273204758d9ac3935399180a356b0b616e Mon Sep 17 00:00:00 2001
From: Owen
Date: Thu, 30 Apr 2026 14:00:32 -0700
Subject: [PATCH 006/107] Handle messaging in the background; dont time out
---
.../siteResource/createSiteResource.ts | 21 +++++++++---
.../siteResource/updateSiteResource.ts | 32 +++++++++++++------
2 files changed, 39 insertions(+), 14 deletions(-)
diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts
index 0da48d160..01f7a0d9c 100644
--- a/server/routers/siteResource/createSiteResource.ts
+++ b/server/routers/siteResource/createSiteResource.ts
@@ -496,11 +496,6 @@ export async function createSiteResource(
);
}
}
-
- await rebuildClientAssociationsFromSiteResource(
- newSiteResource,
- trx
- ); // we need to call this because we added to the admin role
});
if (!newSiteResource) {
@@ -526,6 +521,22 @@ export async function createSiteResource(
await createCertificate(domainId, fullDomain, db);
}
+ // Run in the background after the response is sent. Wrapped in its
+ // own transaction so it always executes on the primary — avoiding any
+ // replica-lag issues while still allowing the HTTP response to return
+ // early.
+ db.transaction(async (trx) => {
+ await rebuildClientAssociationsFromSiteResource(
+ newSiteResource!,
+ trx
+ );
+ }).catch((err) => {
+ logger.error(
+ `Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
+ err
+ );
+ });
+
return response(res, {
data: newSiteResource,
success: true,
diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts
index d0efa0cf4..8a3f93326 100644
--- a/server/routers/siteResource/updateSiteResource.ts
+++ b/server/routers/siteResource/updateSiteResource.ts
@@ -431,9 +431,6 @@ export async function updateSiteResource(
})
.returning();
- // wait some time to allow for messages to be handled
- await new Promise((resolve) => setTimeout(resolve, 750));
-
const sshPamSet =
isLicensedSshPam &&
(authDaemonPort !== undefined ||
@@ -556,11 +553,6 @@ export async function updateSiteResource(
}))
);
}
-
- await rebuildClientAssociationsFromSiteResource(
- updatedSiteResource,
- trx
- );
} else {
// Update the site resource
const sshPamSet =
@@ -690,7 +682,24 @@ export async function updateSiteResource(
}
logger.info(`Updated site resource ${siteResourceId}`);
+ }
+ });
+ // Background: wait for removal messages to propagate, then rebuild
+ // associations for the re-created resource. Own transaction ensures
+ // execution on the primary against fully committed state.
+ (async () => {
+ await db.transaction(async (trx) => {
+ if (!updatedSiteResource) {
+ throw new Error("No updated resource found after update");
+ }
+ if (sitesChanged) {
+ await new Promise((resolve) => setTimeout(resolve, 750));
+ await rebuildClientAssociationsFromSiteResource(
+ updatedSiteResource,
+ trx
+ );
+ }
await handleMessagingForUpdatedSiteResource(
existingSiteResource,
updatedSiteResource,
@@ -700,7 +709,12 @@ export async function updateSiteResource(
})),
trx
);
- }
+ });
+ })().catch((err) => {
+ logger.error(
+ `Error rebuilding client associations for site resource ${updatedSiteResource?.siteResourceId}:`,
+ err
+ );
});
return response(res, {
From 54d2d689c1d0334c931bd548ffa60a494dd8f34d Mon Sep 17 00:00:00 2001
From: Owen
Date: Thu, 30 Apr 2026 14:38:03 -0700
Subject: [PATCH 007/107] Run messaging for delete in the background as well
---
.../siteResource/deleteSiteResource.ts | 21 +++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts
index df43d5c25..7dbb111ad 100644
--- a/server/routers/siteResource/deleteSiteResource.ts
+++ b/server/routers/siteResource/deleteSiteResource.ts
@@ -63,17 +63,26 @@ export async function deleteSiteResource(
);
}
- await db.transaction(async (trx) => {
- // Delete the site resource
- const [removedSiteResource] = await trx
- .delete(siteResources)
- .where(eq(siteResources.siteResourceId, siteResourceId))
- .returning();
+ // Delete the site resource
+ const [removedSiteResource] = await db
+ .delete(siteResources)
+ .where(eq(siteResources.siteResourceId, siteResourceId))
+ .returning();
+ // Run in the background after the response is sent. Wrapped in its
+ // own transaction so it always executes on the primary — avoiding any
+ // replica-lag issues while still allowing the HTTP response to return
+ // early.
+ db.transaction(async (trx) => {
await rebuildClientAssociationsFromSiteResource(
removedSiteResource,
trx
);
+ }).catch((err) => {
+ logger.error(
+ `Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
+ err
+ );
});
logger.info(`Deleted site resource ${siteResourceId}`);
From db6e60d0a3ea205b546db2f8ebed0e5af22f3ba8 Mon Sep 17 00:00:00 2001
From: Owen
Date: Fri, 1 May 2026 10:48:09 -0700
Subject: [PATCH 008/107] Adjust language
---
server/emails/templates/NotifyTrialExpiring.tsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/server/emails/templates/NotifyTrialExpiring.tsx b/server/emails/templates/NotifyTrialExpiring.tsx
index 7cd6d30ac..7c712e278 100644
--- a/server/emails/templates/NotifyTrialExpiring.tsx
+++ b/server/emails/templates/NotifyTrialExpiring.tsx
@@ -64,7 +64,7 @@ export const NotifyTrialExpiring = ({
Some features and resources may now be
- restricted or disconnected. To restore full
+ restricted. To restore full
access and continue using all the features
you had during your trial, please upgrade to
a paid plan.
@@ -85,7 +85,7 @@ export const NotifyTrialExpiring = ({
{orgName} will end on{" "}
{trialEndsAt}
{isLastDay
- ? " — that's tomorrow!"
+ ? " - that's tomorrow!"
: `, in ${daysRemaining} days`}
.
@@ -93,8 +93,7 @@ export const NotifyTrialExpiring = ({
After your trial ends, your account will be
moved to the free plan and some
- functionality may be restricted or your
- sites may disconnect.
+ functionality may be restricted.
From 3dfd7e8a43992948ea78bfaefcaf09d69387eb3a Mon Sep 17 00:00:00 2001
From: Owen
Date: Fri, 1 May 2026 11:47:14 -0700
Subject: [PATCH 009/107] Update limits
---
server/lib/billing/limitSet.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/server/lib/billing/limitSet.ts b/server/lib/billing/limitSet.ts
index ae9a18ffe..e45ae637d 100644
--- a/server/lib/billing/limitSet.ts
+++ b/server/lib/billing/limitSet.ts
@@ -25,7 +25,7 @@ export const tier1LimitSet: LimitSet = {
export const tier2LimitSet: LimitSet = {
[FeatureId.USERS]: {
- value: 100,
+ value: 50,
description: "Team limit"
},
[FeatureId.SITES]: {
@@ -48,7 +48,7 @@ export const tier2LimitSet: LimitSet = {
export const tier3LimitSet: LimitSet = {
[FeatureId.USERS]: {
- value: 500,
+ value: 250,
description: "Business limit"
},
[FeatureId.SITES]: {
From 53e096f7cb6d459b635c1001de9897633f13981a Mon Sep 17 00:00:00 2001
From: Owen
Date: Fri, 1 May 2026 15:01:48 -0700
Subject: [PATCH 010/107] Allow deleting account with trial
---
server/private/lib/billing/getOrgTierData.ts | 15 +++++++++------
server/routers/auth/deleteMyAccount.ts | 5 +++--
2 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/server/private/lib/billing/getOrgTierData.ts b/server/private/lib/billing/getOrgTierData.ts
index 1dc9f83a4..9df9b3b74 100644
--- a/server/private/lib/billing/getOrgTierData.ts
+++ b/server/private/lib/billing/getOrgTierData.ts
@@ -19,12 +19,13 @@ import { eq, and, ne } from "drizzle-orm";
export async function getOrgTierData(
orgId: string
-): Promise<{ tier: Tier | null; active: boolean }> {
+): Promise<{ tier: Tier | null; active: boolean; isTrial: boolean }> {
let tier: Tier | null = null;
let active = false;
+ let isTrial = false;
if (build !== "saas") {
- return { tier, active };
+ return { tier, active, isTrial };
}
try {
@@ -35,7 +36,7 @@ export async function getOrgTierData(
.limit(1);
if (!org) {
- return { tier, active };
+ return { tier, active, isTrial };
}
let orgIdToUse = org.orgId;
@@ -44,7 +45,7 @@ export async function getOrgTierData(
logger.warn(
`Org ${orgId} is not a billing org and does not have a billingOrgId`
);
- return { tier, active };
+ return { tier, active, isTrial };
}
orgIdToUse = org.billingOrgId;
}
@@ -57,7 +58,7 @@ export async function getOrgTierData(
.limit(1);
if (!customer) {
- return { tier, active };
+ return { tier, active, isTrial };
}
// Query for active subscriptions that are not license type
@@ -84,11 +85,13 @@ export async function getOrgTierData(
tier = subscription.type;
active = true;
}
+
+ isTrial = subscription.trial ?? false;
}
} catch (error) {
// If org not found or error occurs, return null tier and inactive
// This is acceptable behavior as per the function signature
}
- return { tier, active };
+ return { tier, active, isTrial };
}
diff --git a/server/routers/auth/deleteMyAccount.ts b/server/routers/auth/deleteMyAccount.ts
index b824e582b..07bdf883d 100644
--- a/server/routers/auth/deleteMyAccount.ts
+++ b/server/routers/auth/deleteMyAccount.ts
@@ -104,8 +104,9 @@ export async function deleteMyAccount(
(r) => r.isBillingOrg && r.isOwner
)?.orgId;
if (primaryOrgId) {
- const { tier, active } = await getOrgTierData(primaryOrgId);
- if (active && tier) {
+ const { tier, active, isTrial } =
+ await getOrgTierData(primaryOrgId);
+ if (active && tier && !isTrial) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
From 4524bdc094feb46897fa78fe206f8ff91f379082 Mon Sep 17 00:00:00 2001
From: Owen
Date: Fri, 1 May 2026 15:42:38 -0700
Subject: [PATCH 011/107] Add http cert syncing for use with the controller
---
server/private/lib/acmeCertSync.ts | 247 ++++++++++++++++++--
server/private/lib/readConfigFile.ts | 327 +++++++++++++--------------
2 files changed, 396 insertions(+), 178 deletions(-)
diff --git a/server/private/lib/acmeCertSync.ts b/server/private/lib/acmeCertSync.ts
index 06b427955..beeeef39d 100644
--- a/server/private/lib/acmeCertSync.ts
+++ b/server/private/lib/acmeCertSync.ts
@@ -274,6 +274,216 @@ function detectWildcard(
return { wildcard: false, wildcardSan: null };
}
+interface HttpCert {
+ wildcard: boolean;
+ altName: string;
+ certName: string;
+ commonName: string;
+ certFile: string;
+ keyFile: string;
+}
+
+async function syncAcmeCertsFromHttp(endpoint: string): Promise {
+ let response: Response;
+ try {
+ response = await fetch(endpoint);
+ } catch (err) {
+ logger.debug(
+ `acmeCertSync: could not reach HTTP endpoint ${endpoint}: ${err}`
+ );
+ return;
+ }
+
+ if (!response.ok) {
+ logger.debug(
+ `acmeCertSync: HTTP endpoint returned status ${response.status}`
+ );
+ return;
+ }
+
+ let httpCerts: HttpCert[];
+ try {
+ httpCerts = await response.json();
+ } catch (err) {
+ logger.debug(
+ `acmeCertSync: could not parse JSON from HTTP endpoint: ${err}`
+ );
+ return;
+ }
+
+ if (!Array.isArray(httpCerts) || httpCerts.length === 0) {
+ logger.debug(
+ `acmeCertSync: no certificates returned from HTTP endpoint`
+ );
+ return;
+ }
+
+ for (const cert of httpCerts) {
+ const domain = cert?.certName;
+
+ if (!domain || typeof domain !== "string") {
+ logger.debug(
+ `acmeCertSync: skipping HTTP cert with missing certName`
+ );
+ continue;
+ }
+
+ const certPem = cert.certFile;
+ const keyPem = cert.keyFile;
+
+ if (!certPem?.trim() || !keyPem?.trim()) {
+ logger.debug(
+ `acmeCertSync: skipping HTTP cert for ${domain} - empty certFile or keyFile`
+ );
+ continue;
+ }
+
+ const firstCertPemForValidation = extractFirstCert(certPem);
+ if (!firstCertPemForValidation) {
+ logger.debug(
+ `acmeCertSync: skipping HTTP cert for ${domain} - no PEM certificate block found`
+ );
+ continue;
+ }
+
+ let validatedX509: crypto.X509Certificate;
+ try {
+ validatedX509 = new crypto.X509Certificate(
+ firstCertPemForValidation
+ );
+ } catch (err) {
+ logger.debug(
+ `acmeCertSync: skipping HTTP cert for ${domain} - invalid X.509 certificate: ${err}`
+ );
+ continue;
+ }
+
+ try {
+ crypto.createPrivateKey(keyPem);
+ } catch (err) {
+ logger.debug(
+ `acmeCertSync: skipping HTTP cert for ${domain} - invalid private key: ${err}`
+ );
+ continue;
+ }
+
+ const wildcard = cert.wildcard ?? false;
+
+ const existing = await db
+ .select()
+ .from(certificates)
+ .where(eq(certificates.domain, domain))
+ .limit(1);
+
+ let oldCertPem: string | null = null;
+ let oldKeyPem: string | null = null;
+
+ if (existing.length > 0 && existing[0].certFile) {
+ try {
+ const storedCertPem = decrypt(
+ existing[0].certFile,
+ config.getRawConfig().server.secret!
+ );
+ const wildcardUnchanged = existing[0].wildcard === wildcard;
+ if (storedCertPem === certPem && wildcardUnchanged) {
+ continue;
+ }
+ oldCertPem = storedCertPem;
+ if (existing[0].keyFile) {
+ try {
+ oldKeyPem = decrypt(
+ existing[0].keyFile,
+ config.getRawConfig().server.secret!
+ );
+ } catch (keyErr) {
+ logger.debug(
+ `acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
+ );
+ }
+ }
+ } catch (err) {
+ logger.debug(
+ `acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
+ );
+ }
+ }
+
+ let expiresAt: number | null = null;
+ try {
+ expiresAt = Math.floor(
+ new Date(validatedX509.validTo).getTime() / 1000
+ );
+ } catch (err) {
+ logger.debug(
+ `acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
+ );
+ }
+
+ const encryptedCert = encrypt(
+ certPem,
+ config.getRawConfig().server.secret!
+ );
+ const encryptedKey = encrypt(
+ keyPem,
+ config.getRawConfig().server.secret!
+ );
+ const now = Math.floor(Date.now() / 1000);
+
+ const domainId = await findDomainId(domain);
+ if (domainId) {
+ logger.debug(
+ `acmeCertSync: resolved domainId "${domainId}" for HTTP cert domain "${domain}"`
+ );
+ } else {
+ logger.debug(
+ `acmeCertSync: no matching domain record found for HTTP cert domain "${domain}"`
+ );
+ }
+
+ if (existing.length > 0) {
+ logger.debug(
+ `acmeCertSync: updating existing certificate (HTTP) for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
+ );
+ await db
+ .update(certificates)
+ .set({
+ certFile: encryptedCert,
+ keyFile: encryptedKey,
+ status: "valid",
+ expiresAt,
+ updatedAt: now,
+ wildcard,
+ ...(domainId !== null && { domainId })
+ })
+ .where(eq(certificates.domain, domain));
+
+ await pushCertUpdateToAffectedNewts(
+ domain,
+ domainId,
+ oldCertPem,
+ oldKeyPem
+ );
+ } else {
+ logger.debug(
+ `acmeCertSync: inserting new certificate (HTTP) for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
+ );
+ await db.insert(certificates).values({
+ domain,
+ domainId,
+ certFile: encryptedCert,
+ keyFile: encryptedKey,
+ status: "valid",
+ expiresAt,
+ createdAt: now,
+ updatedAt: now,
+ wildcard
+ });
+
+ await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
+ }
+ }
+}
+
async function syncAcmeCerts(acmeJsonPath: string): Promise {
let raw: string;
try {
@@ -389,11 +599,7 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise {
const existing = await db
.select()
.from(certificates)
- .where(
- and(
- eq(certificates.domain, domain)
- )
- )
+ .where(and(eq(certificates.domain, domain)))
.limit(1);
let oldCertPem: string | null = null;
@@ -408,7 +614,7 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise {
const wildcardUnchanged = existing[0].wildcard === wildcard;
if (storedCertPem === certPem && wildcardUnchanged) {
// logger.debug(
- // `acmeCertSync: cert for ${domain} is unchanged, skipping`
+ // `acmeCertSync: cert for ${domain} is unchanged, skipping`
// );
continue;
}
@@ -547,19 +753,32 @@ export function initAcmeCertSync(): void {
privateConfigData.acme?.acme_json_path ??
"config/letsencrypt/acme.json";
const intervalMs = privateConfigData.acme?.sync_interval_ms ?? 5000;
+ const httpEndpoint = privateConfigData.acme?.acme_http_endpoint;
logger.debug(
`acmeCertSync: starting ACME cert sync from "${acmeJsonPath}" across all resolvers every ${intervalMs}ms`
);
+ if (httpEndpoint) {
+ logger.debug(
+ `acmeCertSync: also syncing from HTTP endpoint "${httpEndpoint}" every ${intervalMs}ms`
+ );
+ }
+
+ const runSync = () => {
+ if (httpEndpoint) {
+ syncAcmeCertsFromHttp(httpEndpoint).catch((err) => {
+ logger.error(`acmeCertSync: error during HTTP sync: ${err}`);
+ });
+ } else {
+ // only run the file-based sync if the HTTP endpoint is not configured, to avoid doubling up
+ syncAcmeCerts(acmeJsonPath).catch((err) => {
+ logger.error(`acmeCertSync: error during sync: ${err}`);
+ });
+ }
+ };
// Run immediately on init, then on the configured interval
- syncAcmeCerts(acmeJsonPath).catch((err) => {
- logger.error(`acmeCertSync: error during initial sync: ${err}`);
- });
+ runSync();
- setInterval(() => {
- syncAcmeCerts(acmeJsonPath).catch((err) => {
- logger.error(`acmeCertSync: error during sync: ${err}`);
- });
- }, intervalMs);
+ setInterval(runSync, intervalMs);
}
diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts
index 056624159..63ca0b068 100644
--- a/server/private/lib/readConfigFile.ts
+++ b/server/private/lib/readConfigFile.ts
@@ -21,173 +21,172 @@ import { getEnvOrYaml } from "@server/lib/getEnvOrYaml";
const portSchema = z.number().positive().gt(0).lte(65535);
-export const privateConfigSchema = z.object({
- app: z
- .object({
- region: z.string().optional().default("default"),
- base_domain: z.string().optional(),
- identity_provider_mode: z.enum(["global", "org"]).optional()
- })
- .optional()
- .default({
- region: "default"
- }),
- server: z
- .object({
- reo_client_id: z
- .string()
- .optional()
- .transform(getEnvOrYaml("REO_CLIENT_ID")),
- fossorial_api: z
- .string()
- .optional()
- .default("https://api.fossorial.io"),
- fossorial_api_key: z
- .string()
- .optional()
- .transform(getEnvOrYaml("FOSSORIAL_API_KEY"))
- })
- .optional()
- .prefault({}),
- redis: z
- .object({
- host: z.string(),
- port: portSchema,
- password: z
- .string()
- .optional()
- .transform(getEnvOrYaml("REDIS_PASSWORD")),
- db: z.int().nonnegative().optional().default(0),
- replicas: z
- .array(
- z.object({
- host: z.string(),
- port: portSchema,
- password: z.string().optional(),
- db: z.int().nonnegative().optional().default(0)
+export const privateConfigSchema = z
+ .object({
+ app: z
+ .object({
+ region: z.string().optional().default("default"),
+ base_domain: z.string().optional(),
+ identity_provider_mode: z.enum(["global", "org"]).optional()
+ })
+ .optional()
+ .default({
+ region: "default"
+ }),
+ server: z
+ .object({
+ reo_client_id: z
+ .string()
+ .optional()
+ .transform(getEnvOrYaml("REO_CLIENT_ID")),
+ fossorial_api: z
+ .string()
+ .optional()
+ .default("https://api.fossorial.io"),
+ fossorial_api_key: z
+ .string()
+ .optional()
+ .transform(getEnvOrYaml("FOSSORIAL_API_KEY"))
+ })
+ .optional()
+ .prefault({}),
+ redis: z
+ .object({
+ host: z.string(),
+ port: portSchema,
+ password: z
+ .string()
+ .optional()
+ .transform(getEnvOrYaml("REDIS_PASSWORD")),
+ db: z.int().nonnegative().optional().default(0),
+ replicas: z
+ .array(
+ z.object({
+ host: z.string(),
+ port: portSchema,
+ password: z.string().optional(),
+ db: z.int().nonnegative().optional().default(0)
+ })
+ )
+ .optional(),
+ tls: z
+ .object({
+ rejectUnauthorized: z.boolean().optional().default(true)
})
- )
- .optional(),
- tls: z
- .object({
- rejectUnauthorized: z
- .boolean()
- .optional()
- .default(true)
- })
- .optional()
- })
- .optional(),
- gerbil: z
- .object({
- local_exit_node_reachable_at: z
- .string()
- .optional()
- .default("http://gerbil:3004")
- })
- .optional()
- .prefault({}),
- flags: z
- .object({
- enable_redis: z.boolean().optional().default(false),
- use_pangolin_dns: z.boolean().optional().default(false),
- use_org_only_idp: z.boolean().optional(),
- enable_acme_cert_sync: z.boolean().optional().default(true)
- })
- .optional()
- .prefault({}),
- acme: z
- .object({
- acme_json_path: z
- .string()
- .optional()
- .default("config/letsencrypt/acme.json"),
- sync_interval_ms: z.number().optional().default(5000)
- })
- .optional(),
- branding: z
- .object({
- app_name: z.string().optional(),
- background_image_path: z.string().optional(),
- colors: z
- .object({
- light: colorsSchema.optional(),
- dark: colorsSchema.optional()
- })
- .optional(),
- logo: z
- .object({
- light_path: z.string().optional(),
- dark_path: z.string().optional(),
- auth_page: z
- .object({
- width: z.number().optional(),
- height: z.number().optional()
- })
- .optional(),
- navbar: z
- .object({
- width: z.number().optional(),
- height: z.number().optional()
- })
- .optional()
- })
- .optional(),
- footer: z
- .array(
- z.object({
- text: z.string(),
- href: z.string().optional()
+ .optional()
+ })
+ .optional(),
+ gerbil: z
+ .object({
+ local_exit_node_reachable_at: z
+ .string()
+ .optional()
+ .default("http://gerbil:3004")
+ })
+ .optional()
+ .prefault({}),
+ flags: z
+ .object({
+ enable_redis: z.boolean().optional().default(false),
+ use_pangolin_dns: z.boolean().optional().default(false),
+ use_org_only_idp: z.boolean().optional(),
+ enable_acme_cert_sync: z.boolean().optional().default(true)
+ })
+ .optional()
+ .prefault({}),
+ acme: z
+ .object({
+ acme_json_path: z
+ .string()
+ .optional()
+ .default("config/letsencrypt/acme.json"),
+ acme_http_endpoint: z.string().optional(),
+ sync_interval_ms: z.number().optional().default(5000)
+ })
+ .optional(),
+ branding: z
+ .object({
+ app_name: z.string().optional(),
+ background_image_path: z.string().optional(),
+ colors: z
+ .object({
+ light: colorsSchema.optional(),
+ dark: colorsSchema.optional()
})
- )
- .optional(),
- hide_auth_layout_footer: z.boolean().optional().default(false),
- login_page: z
- .object({
- subtitle_text: z.string().optional()
- })
- .optional(),
- signup_page: z
- .object({
- subtitle_text: z.string().optional()
- })
- .optional(),
- resource_auth_page: z
- .object({
- show_logo: z.boolean().optional(),
- hide_powered_by: z.boolean().optional(),
- title_text: z.string().optional(),
- subtitle_text: z.string().optional()
- })
- .optional(),
- emails: z
- .object({
- signature: z.string().optional(),
- colors: z
- .object({
- primary: z.string().optional()
+ .optional(),
+ logo: z
+ .object({
+ light_path: z.string().optional(),
+ dark_path: z.string().optional(),
+ auth_page: z
+ .object({
+ width: z.number().optional(),
+ height: z.number().optional()
+ })
+ .optional(),
+ navbar: z
+ .object({
+ width: z.number().optional(),
+ height: z.number().optional()
+ })
+ .optional()
+ })
+ .optional(),
+ footer: z
+ .array(
+ z.object({
+ text: z.string(),
+ href: z.string().optional()
})
- .optional()
- })
- .optional()
- })
- .optional(),
- stripe: z
- .object({
- secret_key: z
- .string()
- .optional()
- .transform(getEnvOrYaml("STRIPE_SECRET_KEY")),
- webhook_secret: z
- .string()
- .optional()
- .transform(getEnvOrYaml("STRIPE_WEBHOOK_SECRET")),
- // s3Bucket: z.string(),
- // s3Region: z.string().default("us-east-1"),
- // localFilePath: z.string().optional()
- })
- .optional()
-})
+ )
+ .optional(),
+ hide_auth_layout_footer: z.boolean().optional().default(false),
+ login_page: z
+ .object({
+ subtitle_text: z.string().optional()
+ })
+ .optional(),
+ signup_page: z
+ .object({
+ subtitle_text: z.string().optional()
+ })
+ .optional(),
+ resource_auth_page: z
+ .object({
+ show_logo: z.boolean().optional(),
+ hide_powered_by: z.boolean().optional(),
+ title_text: z.string().optional(),
+ subtitle_text: z.string().optional()
+ })
+ .optional(),
+ emails: z
+ .object({
+ signature: z.string().optional(),
+ colors: z
+ .object({
+ primary: z.string().optional()
+ })
+ .optional()
+ })
+ .optional()
+ })
+ .optional(),
+ stripe: z
+ .object({
+ secret_key: z
+ .string()
+ .optional()
+ .transform(getEnvOrYaml("STRIPE_SECRET_KEY")),
+ webhook_secret: z
+ .string()
+ .optional()
+ .transform(getEnvOrYaml("STRIPE_WEBHOOK_SECRET"))
+ // s3Bucket: z.string(),
+ // s3Region: z.string().default("us-east-1"),
+ // localFilePath: z.string().optional()
+ })
+ .optional()
+ })
.transform((data) => {
// this to maintain backwards compatibility with the old config file
const identityProviderMode = data.app?.identity_provider_mode;
From 4651f19c53e8993de2a5032e86ec3a43cc8b454d Mon Sep 17 00:00:00 2001
From: Owen
Date: Fri, 1 May 2026 16:06:13 -0700
Subject: [PATCH 012/107] Support acme_json_path as a directory of acme file
Fixes #2961
---
server/private/lib/acmeCertSync.ts | 65 +++++++++++++++++++++++++++---
1 file changed, 60 insertions(+), 5 deletions(-)
diff --git a/server/private/lib/acmeCertSync.ts b/server/private/lib/acmeCertSync.ts
index beeeef39d..adf87eed8 100644
--- a/server/private/lib/acmeCertSync.ts
+++ b/server/private/lib/acmeCertSync.ts
@@ -12,6 +12,7 @@
*/
import fs from "fs";
+import path from "path";
import crypto from "crypto";
import {
certificates,
@@ -484,12 +485,34 @@ async function syncAcmeCertsFromHttp(endpoint: string): Promise {
}
}
+function findAcmeJsonFiles(dirPath: string): string[] {
+ const results: string[] = [];
+ let entries: fs.Dirent[];
+ try {
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
+ } catch (err) {
+ logger.warn(
+ `acmeCertSync: could not read directory "${dirPath}": ${err}`
+ );
+ return results;
+ }
+ for (const entry of entries) {
+ const fullPath = path.join(dirPath, entry.name);
+ if (entry.isDirectory()) {
+ results.push(...findAcmeJsonFiles(fullPath));
+ } else if (entry.isFile() && entry.name === "acme.json") {
+ results.push(fullPath);
+ }
+ }
+ return results;
+}
+
async function syncAcmeCerts(acmeJsonPath: string): Promise {
let raw: string;
try {
raw = fs.readFileSync(acmeJsonPath, "utf8");
} catch (err) {
- logger.debug(`acmeCertSync: could not read ${acmeJsonPath}: ${err}`);
+ logger.warn(`acmeCertSync: could not read "${acmeJsonPath}": ${err}`);
return;
}
@@ -497,7 +520,9 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise {
try {
acmeJson = JSON.parse(raw);
} catch (err) {
- logger.debug(`acmeCertSync: could not parse acme.json: ${err}`);
+ logger.warn(
+ `acmeCertSync: could not parse "${acmeJsonPath}" as JSON: ${err}`
+ );
return;
}
@@ -771,9 +796,39 @@ export function initAcmeCertSync(): void {
});
} else {
// only run the file-based sync if the HTTP endpoint is not configured, to avoid doubling up
- syncAcmeCerts(acmeJsonPath).catch((err) => {
- logger.error(`acmeCertSync: error during sync: ${err}`);
- });
+ let stat: fs.Stats | null = null;
+ try {
+ stat = fs.statSync(acmeJsonPath);
+ } catch (err) {
+ logger.warn(
+ `acmeCertSync: cannot stat path "${acmeJsonPath}": ${err}`
+ );
+ return;
+ }
+
+ if (stat.isDirectory()) {
+ const files = findAcmeJsonFiles(acmeJsonPath);
+ if (files.length === 0) {
+ logger.debug(
+ `acmeCertSync: no acme.json files found in directory "${acmeJsonPath}"`
+ );
+ return;
+ }
+ logger.debug(
+ `acmeCertSync: found ${files.length} acme.json file(s) in directory "${acmeJsonPath}"`
+ );
+ for (const file of files) {
+ syncAcmeCerts(file).catch((err) => {
+ logger.error(
+ `acmeCertSync: error during sync of "${file}": ${err}`
+ );
+ });
+ }
+ } else {
+ syncAcmeCerts(acmeJsonPath).catch((err) => {
+ logger.error(`acmeCertSync: error during sync: ${err}`);
+ });
+ }
}
};
From f8b85d4b4e1f830b473b06d42a380659355485e3 Mon Sep 17 00:00:00 2001
From: miloschwartz
Date: Fri, 1 May 2026 16:13:54 -0700
Subject: [PATCH 013/107] fix sidebar product updates spacing
---
src/components/ProductUpdates.tsx | 83 +++++++++++++++++--------------
1 file changed, 47 insertions(+), 36 deletions(-)
diff --git a/src/components/ProductUpdates.tsx b/src/components/ProductUpdates.tsx
index b1da32a93..0d88853a7 100644
--- a/src/components/ProductUpdates.tsx
+++ b/src/components/ProductUpdates.tsx
@@ -81,10 +81,10 @@ export default function ProductUpdates({
const showNewVersionPopup = Boolean(
latestVersion &&
- valid(latestVersion) &&
- valid(currentVersion) &&
- ignoredVersionUpdate !== latestVersion &&
- gt(latestVersion, currentVersion)
+ valid(latestVersion) &&
+ valid(currentVersion) &&
+ ignoredVersionUpdate !== latestVersion &&
+ gt(latestVersion, currentVersion)
);
const filteredUpdates = data.updates.filter(
@@ -103,40 +103,51 @@ export default function ProductUpdates({
)}
>
- {filteredUpdates.length > 1 && (
-
0 && (
+
+ {filteredUpdates.length > 1 && (
+
+
+
+ {showNewVersionPopup
+ ? t("productUpdateMoreInfo", {
+ noOfUpdates:
+ filteredUpdates.length
+ })
+ : t("productUpdateInfo", {
+ noOfUpdates:
+ filteredUpdates.length
+ })}
+
+
)}
- >
-
-
- {showNewVersionPopup
- ? t("productUpdateMoreInfo", {
- noOfUpdates: filteredUpdates.length
- })
- : t("productUpdateInfo", {
- noOfUpdates: filteredUpdates.length
- })}
-
-
+
0}
+ onDimissAll={() =>
+ setProductUpdatesRead([
+ ...productUpdatesRead,
+ ...filteredUpdates.map(
+ (update) => update.id
+ )
+ ])
+ }
+ onDimiss={(id) =>
+ setProductUpdatesRead([
+ ...productUpdatesRead,
+ id
+ ])
+ }
+ />
+
)}
- 0}
- onDimissAll={() =>
- setProductUpdatesRead([
- ...productUpdatesRead,
- ...filteredUpdates.map((update) => update.id)
- ])
- }
- onDimiss={(id) =>
- setProductUpdatesRead([...productUpdatesRead, id])
- }
- />
Date: Fri, 1 May 2026 16:26:45 -0700
Subject: [PATCH 014/107] show newt version on site
---
messages/en-US.json | 1 +
server/routers/site/getSite.ts | 12 ++-
src/components/SiteInfoCard.tsx | 178 +++++++++++++++++++++-----------
3 files changed, 128 insertions(+), 63 deletions(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index eb4d3ae3c..c3e062edb 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Secret",
+ "newtVersion": "Version",
"architecture": "Architecture",
"sites": "Sites",
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address internal resources using the peer IP.",
diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts
index 45d49abe6..a16547b8d 100644
--- a/server/routers/site/getSite.ts
+++ b/server/routers/site/getSite.ts
@@ -42,9 +42,12 @@ async function query(siteId?: number, niceId?: string, orgId?: string) {
}
}
-export type GetSiteResponse = NonNullable<
- Awaited>
->["sites"] & { newtId: string | null };
+type SiteQueryRow = NonNullable>>;
+
+export type GetSiteResponse = SiteQueryRow["sites"] & {
+ newtId: string | null;
+ newtVersion: string | null;
+};
registry.registerPath({
method: "get",
@@ -100,7 +103,8 @@ export async function getSite(
const data: GetSiteResponse = {
...site.sites,
- newtId: site.newt ? site.newt.newtId : null
+ newtId: site.newt ? site.newt.newtId : null,
+ newtVersion: site.newt?.version ?? null
};
return response(res, {
diff --git a/src/components/SiteInfoCard.tsx b/src/components/SiteInfoCard.tsx
index 56492ff54..91a924e58 100644
--- a/src/components/SiteInfoCard.tsx
+++ b/src/components/SiteInfoCard.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Alert, AlertDescription } from "@/components/ui/alert";
import { useSiteContext } from "@app/hooks/useSiteContext";
import {
InfoSection,
@@ -9,77 +9,137 @@ import {
InfoSectionTitle
} from "@app/components/InfoSection";
import { useTranslations } from "next-intl";
-import { useEnvContext } from "@app/hooks/useEnvContext";
type SiteInfoCardProps = {};
-export default function SiteInfoCard({}: SiteInfoCardProps) {
- const { site, updateSite } = useSiteContext();
- const t = useTranslations();
- const { env } = useEnvContext();
+function formatPublicEndpoint(endpoint: string) {
+ return endpoint.includes(":")
+ ? endpoint.substring(0, endpoint.lastIndexOf(":"))
+ : endpoint;
+}
- const getConnectionTypeString = (type: string) => {
- if (type === "newt") {
- return "Newt";
- } else if (type === "wireguard") {
- return "WireGuard";
- } else if (type === "local") {
- return t("local");
- } else {
- return t("unknown");
- }
- };
+export default function SiteInfoCard({}: SiteInfoCardProps) {
+ const { site } = useSiteContext();
+ const t = useTranslations();
+
+ const identifierSection = (
+
+ {t("identifier")}
+ {site.niceId}
+
+ );
+
+ const statusSection = (
+
+ {t("status")}
+
+ {site.online ? (
+
+ ) : (
+
+ )}
+
+
+ );
+
+ const endpointSection = site.endpoint ? (
+
+ {t("publicIpEndpoint")}
+
+ {formatPublicEndpoint(site.endpoint)}
+
+
+ ) : null;
+
+ if (site.type === "newt") {
+ return (
+
+
+
+ {identifierSection}
+ {statusSection}
+
+
+ {t("connectionType")}
+
+ Newt
+
+
+
+ {t("newtVersion")}
+
+
+ {site.newtVersion
+ ? `v${site.newtVersion}`
+ : "-"}
+
+
+ {endpointSection}
+
+
+
+ );
+ }
+
+ if (site.type === "wireguard") {
+ return (
+
+
+
+ {identifierSection}
+ {statusSection}
+
+
+ {t("connectionType")}
+
+ WireGuard
+
+ {endpointSection}
+
+
+
+ );
+ }
+
+ if (site.type === "local") {
+ return (
+
+
+
+ {identifierSection}
+
+
+ {t("connectionType")}
+
+
+ {t("local")}
+
+
+ {endpointSection}
+
+
+
+ );
+ }
return (
-
-
- {t("identifier")}
- {site.niceId}
-
- {(site.type == "newt" || site.type == "wireguard") && (
- <>
-
-
- {t("status")}
-
-
- {site.online ? (
-
- ) : (
-
- )}
-
-
- >
- )}
+
+ {identifierSection}
{t("connectionType")}
-
- {getConnectionTypeString(site.type)}
-
+ {t("unknown")}
- {site.endpoint && (
-
-
- {t("publicIpEndpoint")}
-
-
- {site.endpoint.includes(":")
- ? site.endpoint.substring(0, site.endpoint.lastIndexOf(":"))
- : site.endpoint}
-
-
- )}
+ {endpointSection}
From 22116373e3b314c18e766d651c5ad8b7818b3a78 Mon Sep 17 00:00:00 2001
From: miloschwartz
Date: Fri, 1 May 2026 16:33:40 -0700
Subject: [PATCH 015/107] increase target site selector width
---
src/components/resource-target-address-item.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/components/resource-target-address-item.tsx b/src/components/resource-target-address-item.tsx
index 08ce40dd3..abd4ed45b 100644
--- a/src/components/resource-target-address-item.tsx
+++ b/src/components/resource-target-address-item.tsx
@@ -113,10 +113,10 @@ export function ResourceTargetAddressItem({
? selectedSite?.name
: t("siteSelect")}
-
+
-
+
-
);
From b907850344cc6418b59c524426efc3860d821d3b Mon Sep 17 00:00:00 2001
From: Owen
Date: Fri, 1 May 2026 16:35:13 -0700
Subject: [PATCH 016/107] Add missing heading
---
messages/en-US.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index c3e062edb..6e1947b3e 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -3202,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Wildcard subdomains are not allowed.",
"domainPickerWildcardCertWarning": "Wildcard resources may require additional configuration to work properly.",
"domainPickerWildcardCertWarningLink": "Learn more",
- "health": "Health"
+ "health": "Health",
+ "domainPendingErrorTitle": "Verification Issue"
}
From fbf95c536311efe4eb88921cf9f67e419b4f947f Mon Sep 17 00:00:00 2001
From: Owen
Date: Fri, 1 May 2026 16:51:17 -0700
Subject: [PATCH 017/107] Start creating ns one level down
---
.../routers/certificates/createCertificate.ts | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/server/private/routers/certificates/createCertificate.ts b/server/private/routers/certificates/createCertificate.ts
index 60ca2072a..048b92352 100644
--- a/server/private/routers/certificates/createCertificate.ts
+++ b/server/private/routers/certificates/createCertificate.ts
@@ -79,7 +79,7 @@ export async function createCertificate(
let domainToWrite = domain;
if (
- domainRecord.type == "wildcard" &&
+ domainRecord.type == "wildcard" && // this is to fix the wildcard certs for traefik in self hosted NOT ON THE CLOUD
domainRecord.preferWildcardCert &&
!domain.startsWith("*.")
) {
@@ -89,6 +89,16 @@ export async function createCertificate(
domainToWrite = parts.slice(1).join(".");
domainToWrite = `*.${domainToWrite}`;
}
+ } else if (domainRecord.type == "ns") {
+ // first if we have a * in the domain for this case we dont want to include it because it will mess with the cert generator so remove it
+ if (domain.startsWith("*.")) {
+ domain = domain.slice(2);
+ }
+
+ const parts = domain.split(".");
+ if (parts.length > 2) {
+ domainToWrite = parts.slice(1).join(".");
+ }
}
// No cert found, create a new one in pending state
From 805e6f856a8e0d3e4880da7318bea5d92a3785c2 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:14 -0700
Subject: [PATCH 018/107] New translations en-us.json (French)
[ci skip]
---
messages/fr-FR.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/fr-FR.json b/messages/fr-FR.json
index 9dbb4797e..d2b68f4c6 100644
--- a/messages/fr-FR.json
+++ b/messages/fr-FR.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Secrète",
+ "newtVersion": "Version",
"architecture": "Architecture",
"sites": "Nœuds",
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser des ressources internes en utilisant l'adresse IP du pair.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Les sous-domaines Joker ne sont pas autorisés.",
"domainPickerWildcardCertWarning": "Les ressources Joker peuvent nécessiter une configuration supplémentaire pour fonctionner correctement.",
"domainPickerWildcardCertWarningLink": "En savoir plus",
- "health": "Santé"
+ "health": "Santé",
+ "domainPendingErrorTitle": "Verification Issue"
}
From 5073507b90cf752b9aa3ad38994ae036a9912780 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:16 -0700
Subject: [PATCH 019/107] New translations en-us.json (Bulgarian)
[ci skip]
---
messages/bg-BG.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/bg-BG.json b/messages/bg-BG.json
index 0b743adbc..3e3fe3ac5 100644
--- a/messages/bg-BG.json
+++ b/messages/bg-BG.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Крайна точка",
"newtId": "Идентификационен номер",
"newtSecretKey": "Секретен ключ",
+ "newtVersion": "Version",
"architecture": "Архитектура",
"sites": "Сайтове",
"siteWgAnyClients": "Използвайте клиент на WireGuard, за да се свържете. Ще трябва да използвате вътрешните ресурси чрез IP адреса на връстника.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Уайлдкард подсайтове не са позволени.",
"domainPickerWildcardCertWarning": "Ресурсите с уайлдкард може да изискват допълнителна конфигурация за правилна работа.",
"domainPickerWildcardCertWarningLink": "Научете повече",
- "health": "Здраве"
+ "health": "Здраве",
+ "domainPendingErrorTitle": "Verification Issue"
}
From 0bc3276ee278633bfbf5d388dcdaadab8f552ab4 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:18 -0700
Subject: [PATCH 020/107] New translations en-us.json (Czech)
[ci skip]
---
messages/cs-CZ.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json
index 309d52a90..7761b7113 100644
--- a/messages/cs-CZ.json
+++ b/messages/cs-CZ.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Tajný klíč",
+ "newtVersion": "Version",
"architecture": "Architektura",
"sites": "Stránky",
"siteWgAnyClients": "K připojení použijte jakéhokoli klienta WireGuard. Budete muset řešit interní zdroje pomocí klientské IP adresy.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Zástupné poddomény nejsou povoleny.",
"domainPickerWildcardCertWarning": "Zástupné zdroje mohou vyžadovat dodatečnou konfiguraci pro správnou funkci.",
"domainPickerWildcardCertWarningLink": "Zjistit více",
- "health": "Zdraví"
+ "health": "Zdraví",
+ "domainPendingErrorTitle": "Verification Issue"
}
From 5eac131d2e6f3b24b85366d95433552b0359d4f8 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:21 -0700
Subject: [PATCH 021/107] New translations en-us.json (German)
[ci skip]
---
messages/de-DE.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/de-DE.json b/messages/de-DE.json
index 74247798f..e9b333651 100644
--- a/messages/de-DE.json
+++ b/messages/de-DE.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpunkt",
"newtId": "ID",
"newtSecretKey": "Geheimnis",
+ "newtVersion": "Version",
"architecture": "Architektur",
"sites": "Standorte",
"siteWgAnyClients": "Verwenden Sie jeden WireGuard-Client um sich zu verbinden. Sie müssen interne Ressourcen über die Peer-IP ansprechen.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Wildcard-Subdomains sind nicht erlaubt.",
"domainPickerWildcardCertWarning": "Wildcard-Ressourcen erfordern möglicherweise zusätzliche Konfigurationen, um ordnungsgemäß zu funktionieren.",
"domainPickerWildcardCertWarningLink": "Mehr erfahren",
- "health": "Gesundheit"
+ "health": "Gesundheit",
+ "domainPendingErrorTitle": "Verification Issue"
}
From c5a77192394918985fb036a8ef8f95f56f3f050f Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:22 -0700
Subject: [PATCH 022/107] New translations en-us.json (Italian)
[ci skip]
---
messages/it-IT.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/it-IT.json b/messages/it-IT.json
index 9e810c259..bd680c4cb 100644
--- a/messages/it-IT.json
+++ b/messages/it-IT.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Segreto",
+ "newtVersion": "Version",
"architecture": "Architettura",
"sites": "Siti",
"siteWgAnyClients": "Usa qualsiasi client WireGuard per connetterti. Dovrai indirizzare le risorse interne utilizzando l'IP del peer.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "I sottodomini wildcard non sono permessi.",
"domainPickerWildcardCertWarning": "Le risorse wildcard potrebbero richiedere configurazioni aggiuntive per funzionare correttamente.",
"domainPickerWildcardCertWarningLink": "Scopri di più",
- "health": "Salute"
+ "health": "Salute",
+ "domainPendingErrorTitle": "Verification Issue"
}
From 190074ea0c9c2b4f85ffb115135925f8c5812862 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:24 -0700
Subject: [PATCH 023/107] New translations en-us.json (Korean)
[ci skip]
---
messages/ko-KR.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/ko-KR.json b/messages/ko-KR.json
index e98fc65fa..5aee0037c 100644
--- a/messages/ko-KR.json
+++ b/messages/ko-KR.json
@@ -763,6 +763,7 @@
"newtEndpoint": "엔드포인트",
"newtId": "ID",
"newtSecretKey": "비밀",
+ "newtVersion": "Version",
"architecture": "아키텍처",
"sites": "사이트",
"siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 내부 리소스에 접근해야 합니다.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "와일드카드 서브도메인은 허용되지 않습니다.",
"domainPickerWildcardCertWarning": "와일드카드 리소스는 올바르게 작동하려면 추가 구성이 필요할 수 있습니다.",
"domainPickerWildcardCertWarningLink": "자세히 알아보기",
- "health": "건강"
+ "health": "건강",
+ "domainPendingErrorTitle": "Verification Issue"
}
From d43b3176f5d1f1492bc99e581a065e4a63c21180 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:26 -0700
Subject: [PATCH 024/107] New translations en-us.json (Dutch)
[ci skip]
---
messages/nl-NL.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/nl-NL.json b/messages/nl-NL.json
index 09096c424..cfae244eb 100644
--- a/messages/nl-NL.json
+++ b/messages/nl-NL.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Geheim",
+ "newtVersion": "Version",
"architecture": "Architectuur",
"sites": "Sites",
"siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je zult interne bronnen moeten aanspreken met behulp van de peer IP.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Wildcard-subdomeinen zijn niet toegestaan.",
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
"domainPickerWildcardCertWarningLink": "Meer informatie",
- "health": "Gezondheid"
+ "health": "Gezondheid",
+ "domainPendingErrorTitle": "Verification Issue"
}
From aec0aed2111ae4ef07b3bbc6b7e578a32890729f Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:28 -0700
Subject: [PATCH 025/107] New translations en-us.json (Polish)
[ci skip]
---
messages/pl-PL.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/pl-PL.json b/messages/pl-PL.json
index 38bbea59a..e5a93b9fd 100644
--- a/messages/pl-PL.json
+++ b/messages/pl-PL.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Sekret",
+ "newtVersion": "Version",
"architecture": "Architektura",
"sites": "Witryny",
"siteWgAnyClients": "Użyj dowolnego klienta WireGuard, aby się połączyć. Będziesz musiał przekierować wewnętrzne zasoby za pomocą adresu IP.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Uniwersalne subdomeny nie są dozwolone.",
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
- "health": "Zdrowie"
+ "health": "Zdrowie",
+ "domainPendingErrorTitle": "Verification Issue"
}
From 49232e32bfb46e97084d4d7ce69c31a5bb10442c Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:30 -0700
Subject: [PATCH 026/107] New translations en-us.json (Portuguese)
[ci skip]
---
messages/pt-PT.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/pt-PT.json b/messages/pt-PT.json
index 2cd442720..85eecb65d 100644
--- a/messages/pt-PT.json
+++ b/messages/pt-PT.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Chave Secreta",
+ "newtVersion": "Version",
"architecture": "Arquitetura",
"sites": "sites",
"siteWgAnyClients": "Use qualquer cliente do WireGuard para se conectar. Você terá que endereçar recursos internos usando o IP de pares.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Subdomínios curinga não são permitidos.",
"domainPickerWildcardCertWarning": "Recursos curinga podem exigir configurações adicionais para funcionarem corretamente.",
"domainPickerWildcardCertWarningLink": "Saiba mais",
- "health": "Saúde"
+ "health": "Saúde",
+ "domainPendingErrorTitle": "Verification Issue"
}
From 6685afdcf9230bb6f6033db1501572e3a423e99d Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:32 -0700
Subject: [PATCH 027/107] New translations en-us.json (Russian)
[ci skip]
---
messages/ru-RU.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/ru-RU.json b/messages/ru-RU.json
index 4899a3f97..1a93b823a 100644
--- a/messages/ru-RU.json
+++ b/messages/ru-RU.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Секретный ключ",
+ "newtVersion": "Version",
"architecture": "Архитектура",
"sites": "Сайты",
"siteWgAnyClients": "Для подключения используйте любой клиент WireGuard. Вы должны будете адресовать внутренние ресурсы, используя IP адрес пира.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Wildcard поддомены не допускаются.",
"domainPickerWildcardCertWarning": "Wildcard ресурсы могут потребовать дополнительной настройки для правильной работы.",
"domainPickerWildcardCertWarningLink": "Узнать больше",
- "health": "Состояние"
+ "health": "Состояние",
+ "domainPendingErrorTitle": "Verification Issue"
}
From eb40b04b43753446fc2ea2a204e73e72edff8870 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:33 -0700
Subject: [PATCH 028/107] New translations en-us.json (Turkish)
[ci skip]
---
messages/tr-TR.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/tr-TR.json b/messages/tr-TR.json
index 4d36aebba..0ac815b50 100644
--- a/messages/tr-TR.json
+++ b/messages/tr-TR.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Uç Nokta",
"newtId": "Kimlik",
"newtSecretKey": "Gizli",
+ "newtVersion": "Version",
"architecture": "Mimari",
"sites": "Siteler",
"siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklara eş IP adresini kullanarak erişmeniz gerekecek.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Genel alt alanlara izin verilmiyor.",
"domainPickerWildcardCertWarning": "Genel kaynaklar düzgün çalışmak için ek yapılandırma gerektirebilir.",
"domainPickerWildcardCertWarningLink": "Daha fazla bilgi",
- "health": "Sağlık"
+ "health": "Sağlık",
+ "domainPendingErrorTitle": "Verification Issue"
}
From 0a1fe1b72569379e6f55582d0ca40beffeb37513 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:35 -0700
Subject: [PATCH 029/107] New translations en-us.json (Chinese Simplified)
[ci skip]
---
messages/zh-CN.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/zh-CN.json b/messages/zh-CN.json
index aa7ad9ed0..b94895795 100644
--- a/messages/zh-CN.json
+++ b/messages/zh-CN.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "密钥",
+ "newtVersion": "Version",
"architecture": "架构",
"sites": "站点",
"siteWgAnyClients": "使用任何 WireGuard 客户端连接。您必须使用对等IP解决内部资源问题。",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "不允许使用通配符子域。",
"domainPickerWildcardCertWarning": "通配符资源可能需要额外配置才能正常工作。",
"domainPickerWildcardCertWarningLink": "了解更多",
- "health": "健康"
+ "health": "健康",
+ "domainPendingErrorTitle": "Verification Issue"
}
From e94fc6bc659adefeca32fe42a022346647d5c3b6 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:37 -0700
Subject: [PATCH 030/107] New translations en-us.json (Norwegian Bokmal)
[ci skip]
---
messages/nb-NO.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/nb-NO.json b/messages/nb-NO.json
index d6c674801..7a2d5f89e 100644
--- a/messages/nb-NO.json
+++ b/messages/nb-NO.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Sikkerhetsnøkkel",
+ "newtVersion": "Version",
"architecture": "Arkitektur",
"sites": "Områder",
"siteWgAnyClients": "Bruk hvilken som helst WireGuard klient til å koble til. Du må adressere interne ressurser ved hjelp av peer IP.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "Jokertegnsubdomener er ikke tillatt.",
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
"domainPickerWildcardCertWarningLink": "Lær mer",
- "health": "Helse"
+ "health": "Helse",
+ "domainPendingErrorTitle": "Verification Issue"
}
From 498f586eebe388b200880f2549a82ce9acf64adb Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 16:57:38 -0700
Subject: [PATCH 031/107] New translations en-us.json (Spanish)
[ci skip]
---
messages/es-ES.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/messages/es-ES.json b/messages/es-ES.json
index ea5e33b25..93499d639 100644
--- a/messages/es-ES.json
+++ b/messages/es-ES.json
@@ -763,6 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Secreto",
+ "newtVersion": "Version",
"architecture": "Arquitectura",
"sites": "Sitios",
"siteWgAnyClients": "Usa cualquier cliente de Wirex para conectarte. Tendrás que dirigirte a los recursos internos usando la IP de compañeros.",
@@ -3201,5 +3202,6 @@
"domainPickerWildcardSubdomainNotAllowed": "No se permiten subdominios comodín.",
"domainPickerWildcardCertWarning": "Los recursos comodín pueden requerir configuración adicional para funcionar correctamente.",
"domainPickerWildcardCertWarningLink": "Más información",
- "health": "Salud"
+ "health": "Salud",
+ "domainPendingErrorTitle": "Verification Issue"
}
From d47faeced1fa3eaf3d6380d4f4bd215595abcebd Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:23 -0700
Subject: [PATCH 032/107] New translations en-us.json (French)
[ci skip]
---
messages/fr-FR.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/messages/fr-FR.json b/messages/fr-FR.json
index d2b68f4c6..e443246fc 100644
--- a/messages/fr-FR.json
+++ b/messages/fr-FR.json
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Les ressources Joker peuvent nécessiter une configuration supplémentaire pour fonctionner correctement.",
"domainPickerWildcardCertWarningLink": "En savoir plus",
"health": "Santé",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Problème de vérification"
}
From 6ad06e6faff3ff890ee8b62f52d0f8ba3a162624 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:25 -0700
Subject: [PATCH 033/107] New translations en-us.json (Bulgarian)
[ci skip]
---
messages/bg-BG.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/bg-BG.json b/messages/bg-BG.json
index 3e3fe3ac5..9efa9e394 100644
--- a/messages/bg-BG.json
+++ b/messages/bg-BG.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Крайна точка",
"newtId": "Идентификационен номер",
"newtSecretKey": "Секретен ключ",
- "newtVersion": "Version",
+ "newtVersion": "Версия",
"architecture": "Архитектура",
"sites": "Сайтове",
"siteWgAnyClients": "Използвайте клиент на WireGuard, за да се свържете. Ще трябва да използвате вътрешните ресурси чрез IP адреса на връстника.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Ресурсите с уайлдкард може да изискват допълнителна конфигурация за правилна работа.",
"domainPickerWildcardCertWarningLink": "Научете повече",
"health": "Здраве",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Проблем при проверка"
}
From 0fe2b24f6bc4b86ef2aeaf58e46bf71647771776 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:27 -0700
Subject: [PATCH 034/107] New translations en-us.json (Czech)
[ci skip]
---
messages/cs-CZ.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json
index 7761b7113..3e1965f89 100644
--- a/messages/cs-CZ.json
+++ b/messages/cs-CZ.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Tajný klíč",
- "newtVersion": "Version",
+ "newtVersion": "Verze",
"architecture": "Architektura",
"sites": "Stránky",
"siteWgAnyClients": "K připojení použijte jakéhokoli klienta WireGuard. Budete muset řešit interní zdroje pomocí klientské IP adresy.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Zástupné zdroje mohou vyžadovat dodatečnou konfiguraci pro správnou funkci.",
"domainPickerWildcardCertWarningLink": "Zjistit více",
"health": "Zdraví",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Problém s ověřením"
}
From f175ac774fda3751e52a80a2de3c10f64357319c Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:29 -0700
Subject: [PATCH 035/107] New translations en-us.json (German)
[ci skip]
---
messages/de-DE.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/messages/de-DE.json b/messages/de-DE.json
index e9b333651..0109a011b 100644
--- a/messages/de-DE.json
+++ b/messages/de-DE.json
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Wildcard-Ressourcen erfordern möglicherweise zusätzliche Konfigurationen, um ordnungsgemäß zu funktionieren.",
"domainPickerWildcardCertWarningLink": "Mehr erfahren",
"health": "Gesundheit",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Verifizierungsproblem"
}
From 4d6cea5fcd6775f5f60df4430b29928de7e15ba4 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:31 -0700
Subject: [PATCH 036/107] New translations en-us.json (Italian)
[ci skip]
---
messages/it-IT.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/it-IT.json b/messages/it-IT.json
index bd680c4cb..4b9dc2717 100644
--- a/messages/it-IT.json
+++ b/messages/it-IT.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Segreto",
- "newtVersion": "Version",
+ "newtVersion": "Versione",
"architecture": "Architettura",
"sites": "Siti",
"siteWgAnyClients": "Usa qualsiasi client WireGuard per connetterti. Dovrai indirizzare le risorse interne utilizzando l'IP del peer.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Le risorse wildcard potrebbero richiedere configurazioni aggiuntive per funzionare correttamente.",
"domainPickerWildcardCertWarningLink": "Scopri di più",
"health": "Salute",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Problema di Verifica"
}
From 6deefcd00381b8fa6195794b7347197519eee867 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:33 -0700
Subject: [PATCH 037/107] New translations en-us.json (Korean)
[ci skip]
---
messages/ko-KR.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/ko-KR.json b/messages/ko-KR.json
index 5aee0037c..2dd09f853 100644
--- a/messages/ko-KR.json
+++ b/messages/ko-KR.json
@@ -763,7 +763,7 @@
"newtEndpoint": "엔드포인트",
"newtId": "ID",
"newtSecretKey": "비밀",
- "newtVersion": "Version",
+ "newtVersion": "버전",
"architecture": "아키텍처",
"sites": "사이트",
"siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 내부 리소스에 접근해야 합니다.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "와일드카드 리소스는 올바르게 작동하려면 추가 구성이 필요할 수 있습니다.",
"domainPickerWildcardCertWarningLink": "자세히 알아보기",
"health": "건강",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "확인 문제"
}
From 8ba5b43569e5f4b6cdac1239e64a7c012a29cda3 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:35 -0700
Subject: [PATCH 038/107] New translations en-us.json (Dutch)
[ci skip]
---
messages/nl-NL.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/messages/nl-NL.json b/messages/nl-NL.json
index cfae244eb..bd01045bb 100644
--- a/messages/nl-NL.json
+++ b/messages/nl-NL.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Geheim",
- "newtVersion": "Version",
+ "newtVersion": "Versie",
"architecture": "Architectuur",
"sites": "Sites",
"siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je zult interne bronnen moeten aanspreken met behulp van de peer IP.",
@@ -3169,7 +3169,7 @@
"publicIpEndpoint": "Eindpunt",
"lastTriggeredAt": "Laatste Trigger",
"reject": "Afwijzen",
- "uptimeDaysAgo": "{count} days ago",
+ "uptimeDaysAgo": "{count} dagen geleden",
"uptimeToday": "Vandaag",
"uptimeNoDataAvailable": "Geen gegevens beschikbaar",
"uptimeSuffix": "werktijd",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
"domainPickerWildcardCertWarningLink": "Meer informatie",
"health": "Gezondheid",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Verificatieprobleem"
}
From 1fd2a0fae29f59a05160e4448e42829a8f5d7662 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:37 -0700
Subject: [PATCH 039/107] New translations en-us.json (Polish)
[ci skip]
---
messages/pl-PL.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/pl-PL.json b/messages/pl-PL.json
index e5a93b9fd..b8182232e 100644
--- a/messages/pl-PL.json
+++ b/messages/pl-PL.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Sekret",
- "newtVersion": "Version",
+ "newtVersion": "Wersja",
"architecture": "Architektura",
"sites": "Witryny",
"siteWgAnyClients": "Użyj dowolnego klienta WireGuard, aby się połączyć. Będziesz musiał przekierować wewnętrzne zasoby za pomocą adresu IP.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
"health": "Zdrowie",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Problem z weryfikacją"
}
From c3dc0bd0155dea8907f4099021a4a6e38ad9b887 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:39 -0700
Subject: [PATCH 040/107] New translations en-us.json (Portuguese)
[ci skip]
---
messages/pt-PT.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/pt-PT.json b/messages/pt-PT.json
index 85eecb65d..d7824263e 100644
--- a/messages/pt-PT.json
+++ b/messages/pt-PT.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Chave Secreta",
- "newtVersion": "Version",
+ "newtVersion": "Versão",
"architecture": "Arquitetura",
"sites": "sites",
"siteWgAnyClients": "Use qualquer cliente do WireGuard para se conectar. Você terá que endereçar recursos internos usando o IP de pares.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Recursos curinga podem exigir configurações adicionais para funcionarem corretamente.",
"domainPickerWildcardCertWarningLink": "Saiba mais",
"health": "Saúde",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Problema de Verificação"
}
From f43baaaf1f66b6377e7f5b07d42a07bdcd36ceb0 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:41 -0700
Subject: [PATCH 041/107] New translations en-us.json (Russian)
[ci skip]
---
messages/ru-RU.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/ru-RU.json b/messages/ru-RU.json
index 1a93b823a..2d9811c65 100644
--- a/messages/ru-RU.json
+++ b/messages/ru-RU.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Секретный ключ",
- "newtVersion": "Version",
+ "newtVersion": "Версия",
"architecture": "Архитектура",
"sites": "Сайты",
"siteWgAnyClients": "Для подключения используйте любой клиент WireGuard. Вы должны будете адресовать внутренние ресурсы, используя IP адрес пира.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Wildcard ресурсы могут потребовать дополнительной настройки для правильной работы.",
"domainPickerWildcardCertWarningLink": "Узнать больше",
"health": "Состояние",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Проблема с подтверждением"
}
From a882619eafdaa4edcb438329bc610fcd64373bcc Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:43 -0700
Subject: [PATCH 042/107] New translations en-us.json (Turkish)
[ci skip]
---
messages/tr-TR.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/tr-TR.json b/messages/tr-TR.json
index 0ac815b50..f2c56eeb5 100644
--- a/messages/tr-TR.json
+++ b/messages/tr-TR.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Uç Nokta",
"newtId": "Kimlik",
"newtSecretKey": "Gizli",
- "newtVersion": "Version",
+ "newtVersion": "Sürüm",
"architecture": "Mimari",
"sites": "Siteler",
"siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklara eş IP adresini kullanarak erişmeniz gerekecek.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Genel kaynaklar düzgün çalışmak için ek yapılandırma gerektirebilir.",
"domainPickerWildcardCertWarningLink": "Daha fazla bilgi",
"health": "Sağlık",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Doğrulama Sorunu"
}
From dd1e681a9c1c901d4b5bdec2b9284a229c89288b Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:45 -0700
Subject: [PATCH 043/107] New translations en-us.json (Chinese Simplified)
[ci skip]
---
messages/zh-CN.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/zh-CN.json b/messages/zh-CN.json
index b94895795..fb7874750 100644
--- a/messages/zh-CN.json
+++ b/messages/zh-CN.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "密钥",
- "newtVersion": "Version",
+ "newtVersion": "版本",
"architecture": "架构",
"sites": "站点",
"siteWgAnyClients": "使用任何 WireGuard 客户端连接。您必须使用对等IP解决内部资源问题。",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "通配符资源可能需要额外配置才能正常工作。",
"domainPickerWildcardCertWarningLink": "了解更多",
"health": "健康",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "验证问题"
}
From 441d4bce6e8969877d166843090af4074fe2e6eb Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:47 -0700
Subject: [PATCH 044/107] New translations en-us.json (Norwegian Bokmal)
[ci skip]
---
messages/nb-NO.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/nb-NO.json b/messages/nb-NO.json
index 7a2d5f89e..318c31bca 100644
--- a/messages/nb-NO.json
+++ b/messages/nb-NO.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Sikkerhetsnøkkel",
- "newtVersion": "Version",
+ "newtVersion": "Versjon",
"architecture": "Arkitektur",
"sites": "Områder",
"siteWgAnyClients": "Bruk hvilken som helst WireGuard klient til å koble til. Du må adressere interne ressurser ved hjelp av peer IP.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
"domainPickerWildcardCertWarningLink": "Lær mer",
"health": "Helse",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Verifiseringsproblem"
}
From a9019cfb231cc9efa982e3812334d51f337976b2 Mon Sep 17 00:00:00 2001
From: Owen Schwartz
Date: Fri, 1 May 2026 17:00:49 -0700
Subject: [PATCH 045/107] New translations en-us.json (Spanish)
[ci skip]
---
messages/es-ES.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/messages/es-ES.json b/messages/es-ES.json
index 93499d639..f1db6ad90 100644
--- a/messages/es-ES.json
+++ b/messages/es-ES.json
@@ -763,7 +763,7 @@
"newtEndpoint": "Endpoint",
"newtId": "ID",
"newtSecretKey": "Secreto",
- "newtVersion": "Version",
+ "newtVersion": "Versión",
"architecture": "Arquitectura",
"sites": "Sitios",
"siteWgAnyClients": "Usa cualquier cliente de Wirex para conectarte. Tendrás que dirigirte a los recursos internos usando la IP de compañeros.",
@@ -3203,5 +3203,5 @@
"domainPickerWildcardCertWarning": "Los recursos comodín pueden requerir configuración adicional para funcionar correctamente.",
"domainPickerWildcardCertWarningLink": "Más información",
"health": "Salud",
- "domainPendingErrorTitle": "Verification Issue"
+ "domainPendingErrorTitle": "Problema de verificación"
}
From cab8be1a9a9792a44cb19f3ee24b1de456b46042 Mon Sep 17 00:00:00 2001
From: miloschwartz
Date: Fri, 1 May 2026 17:33:57 -0700
Subject: [PATCH 046/107] add new screenshots
---
public/screenshots/hero.png | Bin 602015 -> 635670 bytes
public/screenshots/private-resources.png | Bin 582722 -> 572953 bytes
public/screenshots/public-resources.png | Bin 602015 -> 635670 bytes
public/screenshots/sites.png | Bin 2473392 -> 587443 bytes
public/screenshots/user-devices.png | Bin 444519 -> 419639 bytes
public/screenshots/users.png | Bin 280481 -> 528904 bytes
6 files changed, 0 insertions(+), 0 deletions(-)
diff --git a/public/screenshots/hero.png b/public/screenshots/hero.png
index 918dd755dd894dbc568aa3f58ef2afdc3c9d8ccf..8d758b2604ccc38933b8384e48bc84909f82f890 100644
GIT binary patch
literal 635670
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