Merge pull request #2862 from fosrl/dev

1.17.3
This commit is contained in:
Owen Schwartz
2026-04-14 20:33:04 -07:00
committed by GitHub
22 changed files with 117 additions and 191 deletions
+1
View File
@@ -0,0 +1 @@
*-journal
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Скала", "title": "Скала",
"description": "Предприятие, 50 потребители, 50 сайта и приоритетна поддръжка." "description": "Функции за корпоративни клиенти, 50 потребители, 100 сайта и приоритетна поддръжка."
} }
}, },
"personalUseOnly": "Само за лична употреба (безплатен лиценз - без проверка)", "personalUseOnly": "Само за лична употреба (безплатен лиценз - без проверка)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Měřítko", "title": "Měřítko",
"description": "Podnikové funkce, 50 uživatelů, 50 míst a prioritní podpory." "description": "Podnikové funkce, 50 uživatelů, 100 stránek a prioritní podpora."
} }
}, },
"personalUseOnly": "Pouze pro osobní použití (zdarma licence - bez ověření)", "personalUseOnly": "Pouze pro osobní použití (zdarma licence - bez ověření)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Maßstab", "title": "Maßstab",
"description": "Enterprise Features, 50 Benutzer, 50 Sites und Prioritätsunterstützung." "description": "Unternehmensmerkmale, 50 Benutzer, 100 Standorte und prioritärer Support."
} }
}, },
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz - kein Checkout)", "personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz - kein Checkout)",
+4 -4
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Scale", "title": "Scale",
"description": "Enterprise features, 50 users, 50 sites, and priority support." "description": "Enterprise features, 50 users, 100 sites, and priority support."
} }
}, },
"personalUseOnly": "Personal use only (free license - no checkout)", "personalUseOnly": "Personal use only (free license - no checkout)",
@@ -2824,9 +2824,9 @@
"streamingHttpWebhookTitle": "HTTP Webhook", "streamingHttpWebhookTitle": "HTTP Webhook",
"streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.", "streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.",
"streamingS3Title": "Amazon S3", "streamingS3Title": "Amazon S3",
"streamingS3Description": "Stream events to an S3-compatible object storage bucket. Coming soon.", "streamingS3Description": "Stream events to an S3-compatible object storage bucket. Contact support to enable this destination.",
"streamingDatadogTitle": "Datadog", "streamingDatadogTitle": "Datadog",
"streamingDatadogDescription": "Forward events directly to your Datadog account. Coming soon.", "streamingDatadogDescription": "Forward events directly to your Datadog account. Contact support to enable this destination.",
"streamingTypePickerDescription": "Choose a destination type to get started.", "streamingTypePickerDescription": "Choose a destination type to get started.",
"streamingFailedToLoad": "Failed to load destinations", "streamingFailedToLoad": "Failed to load destinations",
"streamingUnexpectedError": "An unexpected error occurred.", "streamingUnexpectedError": "An unexpected error occurred.",
@@ -2849,7 +2849,7 @@
"httpDestNamePlaceholder": "My HTTP destination", "httpDestNamePlaceholder": "My HTTP destination",
"httpDestUrlLabel": "Destination URL", "httpDestUrlLabel": "Destination URL",
"httpDestUrlErrorHttpRequired": "URL must use http or https", "httpDestUrlErrorHttpRequired": "URL must use http or https",
"httpDestUrlErrorHttpsRequired": "HTTPS is required on cloud deployments", "httpDestUrlErrorHttpsRequired": "HTTPS is required",
"httpDestUrlErrorInvalid": "Enter a valid URL (e.g. https://example.com/webhook)", "httpDestUrlErrorInvalid": "Enter a valid URL (e.g. https://example.com/webhook)",
"httpDestAuthTitle": "Authentication", "httpDestAuthTitle": "Authentication",
"httpDestAuthDescription": "Choose how requests to your endpoint are authenticated.", "httpDestAuthDescription": "Choose how requests to your endpoint are authenticated.",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Escala", "title": "Escala",
"description": "Características de la empresa, 50 usuarios, 50 sitios y soporte prioritario." "description": "Funcionalidades empresariales, 50 usuarios, 100 sitios y soporte prioritario."
} }
}, },
"personalUseOnly": "Solo uso personal (licencia gratuita - sin salida)", "personalUseOnly": "Solo uso personal (licencia gratuita - sin salida)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Échelle", "title": "Échelle",
"description": "Fonctionnalités d'entreprise, 50 utilisateurs, 50 sites et une prise en charge prioritaire." "description": "Fonctionnalités d'entreprise, 50 utilisateurs, 100 sites et support prioritaire."
} }
}, },
"personalUseOnly": "Usage personnel uniquement (licence gratuite - pas de validation)", "personalUseOnly": "Usage personnel uniquement (licence gratuite - pas de validation)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Scala", "title": "Scala",
"description": "Funzionalità aziendali, 50 utenti, 50 siti e supporto prioritario." "description": "Funzionalità aziendali, 50 utenti, 100 siti e supporto prioritario."
} }
}, },
"personalUseOnly": "Uso personale esclusivo (licenza gratuita - nessun pagamento)", "personalUseOnly": "Uso personale esclusivo (licenza gratuita - nessun pagamento)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "스케일", "title": "스케일",
"description": "기업 기능, 50명의 사용자, 50개의 사이트, 우선 지원." "description": "기업 기능, 50명의 사용자, 100개의 사이트, 그리고 우선 지원."
} }
}, },
"personalUseOnly": "개인용으로만 사용 (무료 라이선스 - 결제 없음)", "personalUseOnly": "개인용으로만 사용 (무료 라이선스 - 결제 없음)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Skala", "title": "Skala",
"description": "Enterprise features, 50 brukere, 50 nettsteder og prioritetsstøtte." "description": "Funksjoner for bedrifter, 50 brukere, 100 nettsteder og prioritert support."
} }
}, },
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen kasse)", "personalUseOnly": "Kun personlig bruk (gratis lisens - ingen kasse)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Schaal", "title": "Schaal",
"description": "Enterprise functies, 50 gebruikers, 50 sites en prioriteit ondersteuning." "description": "Enterprise-functies, 50 gebruikers, 100 sites en prioritaire ondersteuning."
} }
}, },
"personalUseOnly": "Alleen voor persoonlijk gebruik (gratis licentie - geen afrekening)", "personalUseOnly": "Alleen voor persoonlijk gebruik (gratis licentie - geen afrekening)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Skala", "title": "Skala",
"description": "Cechy przedsiębiorstw, 50 użytkowników, 50 obiektów i wsparcie priorytetowe." "description": "Funkcje dla przedsiębiorstw, 50 użytkowników, 100 witryn i priorytetowe wsparcie."
} }
}, },
"personalUseOnly": "Tylko do użytku osobistego (darmowa licencja - bez płatności)", "personalUseOnly": "Tylko do użytku osobistego (darmowa licencja - bez płatności)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Escala", "title": "Escala",
"description": "Recursos de empresa, 50 usuários, 50 sites e apoio prioritário." "description": "Recursos empresariais, 50 usuários, 100 sites, e suporte prioritário."
} }
}, },
"personalUseOnly": "Uso pessoal apenas (licença gratuita - sem checkout)", "personalUseOnly": "Uso pessoal apenas (licença gratuita - sem checkout)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Масштаб", "title": "Масштаб",
"description": "Функции предприятия, 50 пользователей, 50 сайтов, а также приоритетная поддержка." "description": "Функции корпоративного уровня, 50 пользователей, 100 сайтов и приоритетная поддержка."
} }
}, },
"personalUseOnly": "Только для личного использования (бесплатная лицензия - без оформления на кассе)", "personalUseOnly": "Только для личного использования (бесплатная лицензия - без оформления на кассе)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "Ölçek", "title": "Ölçek",
"description": "Kurumsal özellikler, 50 kullanıcı, 50 site ve öncelikli destek." "description": "Kurumsal özellikler, 50 kullanıcı, 100 site ve öncelikli destek."
} }
}, },
"personalUseOnly": "Kişisel kullanım için (ücretsiz lisans - ödeme yok)", "personalUseOnly": "Kişisel kullanım için (ücretsiz lisans - ödeme yok)",
+1 -1
View File
@@ -2351,7 +2351,7 @@
}, },
"scale": { "scale": {
"title": "缩放比例", "title": "缩放比例",
"description": "企业特征、50个用户、50个站点优先支持。" "description": "企业功能,50个用户100个站点,以及优先支持。"
} }
}, },
"personalUseOnly": "仅限个人使用(免费许可 - 无需结账)", "personalUseOnly": "仅限个人使用(免费许可 - 无需结账)",
+1
View File
@@ -1080,6 +1080,7 @@ export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
export type VersionMigration = InferSelectModel<typeof versionMigrations>; export type VersionMigration = InferSelectModel<typeof versionMigrations>;
export type ResourceRule = InferSelectModel<typeof resourceRules>; export type ResourceRule = InferSelectModel<typeof resourceRules>;
export type Domain = InferSelectModel<typeof domains>; export type Domain = InferSelectModel<typeof domains>;
export type DnsRecord = InferSelectModel<typeof dnsRecords>;
export type SupporterKey = InferSelectModel<typeof supporterKey>; export type SupporterKey = InferSelectModel<typeof supporterKey>;
export type Idp = InferSelectModel<typeof idp>; export type Idp = InferSelectModel<typeof idp>;
export type ApiKey = InferSelectModel<typeof apiKeys>; export type ApiKey = InferSelectModel<typeof apiKeys>;
+2 -2
View File
@@ -9,8 +9,8 @@ export type LicensePriceSet = {
export const licensePriceSet: LicensePriceSet = { export const licensePriceSet: LicensePriceSet = {
// Free license matches the freeLimitSet // Free license matches the freeLimitSet
[LicenseId.SMALL_LICENSE]: "price_1SxKHiD3Ee2Ir7WmvtEh17A8", [LicenseId.SMALL_LICENSE]: "price_1TMJzmD3Ee2Ir7Wm05NlGImT",
[LicenseId.BIG_LICENSE]: "price_1SxKHiD3Ee2Ir7WmMUiP0H6Y" [LicenseId.BIG_LICENSE]: "price_1TMJzzD3Ee2Ir7WmzJw9TerS"
}; };
export const licensePriceSetSandbox: LicensePriceSet = { export const licensePriceSetSandbox: LicensePriceSet = {
@@ -217,7 +217,7 @@ export async function handleSubscriptionCreated(
subscriptionPriceId === priceSet[LicenseId.BIG_LICENSE] subscriptionPriceId === priceSet[LicenseId.BIG_LICENSE]
) { ) {
numUsers = 50; numUsers = 50;
numSites = 50; numSites = 100;
} else { } else {
logger.error( logger.error(
`Unknown price ID ${subscriptionPriceId} for subscription ${subscription.id}` `Unknown price ID ${subscriptionPriceId} for subscription ${subscription.id}`
+32 -87
View File
@@ -29,65 +29,9 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import NodeCache from "node-cache";
import semver from "semver";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
async function getLatestOlmVersion(): Promise<string | null> {
try {
const cachedVersion = olmVersionCache.get<string>("latestOlmVersion");
if (cachedVersion) {
return cachedVersion;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1500);
const response = await fetch(
"https://api.github.com/repos/fosrl/olm/tags",
{
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
logger.warn(
`Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}`
);
return null;
}
let tags = await response.json();
if (!Array.isArray(tags) || tags.length === 0) {
logger.warn("No tags found for Olm repository");
return null;
}
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name;
olmVersionCache.set("latestOlmVersion", latestVersion, 3600);
return latestVersion;
} catch (error: any) {
if (error.name === "AbortError") {
logger.warn("Request to fetch latest Olm version timed out (1.5s)");
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
logger.warn("Connection timeout while fetching latest Olm version");
} else {
logger.warn(
"Error fetching latest Olm version:",
error.message || error
);
}
return null;
}
}
const listClientsParamsSchema = z.strictObject({ const listClientsParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
}); });
@@ -413,44 +357,45 @@ export async function listClients(
}; };
}); });
const latestOlVersionPromise = getLatestOlmVersion(); // REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
// const latestOlmVersionPromise = getLatestOlmVersion();
const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map( // const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map(
(client) => { // (client) => {
const OlmWithUpdate: OlmWithUpdateAvailable = { ...client }; // const OlmWithUpdate: OlmWithUpdateAvailable = { ...client };
// Initially set to false, will be updated if version check succeeds // // Initially set to false, will be updated if version check succeeds
OlmWithUpdate.olmUpdateAvailable = false; // OlmWithUpdate.olmUpdateAvailable = false;
return OlmWithUpdate; // return OlmWithUpdate;
} // }
); // );
// Try to get the latest version, but don't block if it fails // Try to get the latest version, but don't block if it fails
try { // try {
const latestOlVersion = await latestOlVersionPromise; // const latestOlmVersion = await latestOlVersionPromise;
if (latestOlVersion) { // if (latestOlVersion) {
olmsWithUpdates.forEach((client) => { // olmsWithUpdates.forEach((client) => {
try { // try {
client.olmUpdateAvailable = semver.lt( // client.olmUpdateAvailable = semver.lt(
client.olmVersion ? client.olmVersion : "", // client.olmVersion ? client.olmVersion : "",
latestOlVersion // latestOlVersion
); // );
} catch (error) { // } catch (error) {
client.olmUpdateAvailable = false; // client.olmUpdateAvailable = false;
} // }
}); // });
} // }
} catch (error) { // } catch (error) {
// Log the error but don't let it block the response // // Log the error but don't let it block the response
logger.warn( // logger.warn(
"Failed to check for OLM updates, continuing without update info:", // "Failed to check for OLM updates, continuing without update info:",
error // error
); // );
} // }
return response<ListClientsResponse>(res, { return response<ListClientsResponse>(res, {
data: { data: {
clients: olmsWithUpdates, clients: clientsWithSites,
pagination: { pagination: {
total: totalCount, total: totalCount,
page, page,
+23 -77
View File
@@ -30,65 +30,10 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import NodeCache from "node-cache";
import semver from "semver"; import semver from "semver";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
async function getLatestOlmVersion(): Promise<string | null> {
try {
const cachedVersion = olmVersionCache.get<string>("latestOlmVersion");
if (cachedVersion) {
return cachedVersion;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1500);
const response = await fetch(
"https://api.github.com/repos/fosrl/olm/tags",
{
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
logger.warn(
`Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}`
);
return null;
}
let tags = await response.json();
if (!Array.isArray(tags) || tags.length === 0) {
logger.warn("No tags found for Olm repository");
return null;
}
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name;
olmVersionCache.set("latestOlmVersion", latestVersion, 3600);
return latestVersion;
} catch (error: any) {
if (error.name === "AbortError") {
logger.warn("Request to fetch latest Olm version timed out (1.5s)");
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
logger.warn("Connection timeout while fetching latest Olm version");
} else {
logger.warn(
"Error fetching latest Olm version:",
error.message || error
);
}
return null;
}
}
const listUserDevicesParamsSchema = z.strictObject({ const listUserDevicesParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
}); });
@@ -453,29 +398,30 @@ export async function listUserDevices(
} }
); );
// Try to get the latest version, but don't block if it fails // REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
try { // // Try to get the latest version, but don't block if it fails
const latestOlmVersion = await getLatestOlmVersion(); // try {
// const latestOlmVersion = await getLatestOlmVersion();
if (latestOlmVersion) { // if (latestOlmVersion) {
olmsWithUpdates.forEach((client) => { // olmsWithUpdates.forEach((client) => {
try { // try {
client.olmUpdateAvailable = semver.lt( // client.olmUpdateAvailable = semver.lt(
client.olmVersion ? client.olmVersion : "", // client.olmVersion ? client.olmVersion : "",
latestOlmVersion // latestOlmVersion
); // );
} catch (error) { // } catch (error) {
client.olmUpdateAvailable = false; // client.olmUpdateAvailable = false;
} // }
}); // });
} // }
} catch (error) { // } catch (error) {
// Log the error but don't let it block the response // // Log the error but don't let it block the response
logger.warn( // logger.warn(
"Failed to check for OLM updates, continuing without update info:", // "Failed to check for OLM updates, continuing without update info:",
error // error
); // );
} // }
return response<ListUserDevicesResponse>(res, { return response<ListUserDevicesResponse>(res, {
data: { data: {
+39 -6
View File
@@ -21,6 +21,11 @@ import semver from "semver";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
// Stale-while-revalidate: keeps the last successfully fetched version so that
// a transient network failure / timeout does not flip every site back to
// newtUpdateAvailable: false.
let staleNewtVersion: string | null = null;
async function getLatestNewtVersion(): Promise<string | null> { async function getLatestNewtVersion(): Promise<string | null> {
try { try {
const cachedVersion = await cache.get<string>("latestNewtVersion"); const cachedVersion = await cache.get<string>("latestNewtVersion");
@@ -29,7 +34,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
} }
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1500); // Reduced timeout to 1.5 seconds const timeoutId = setTimeout(() => controller.abort(), 1500);
const response = await fetch( const response = await fetch(
"https://api.github.com/repos/fosrl/newt/tags", "https://api.github.com/repos/fosrl/newt/tags",
@@ -44,18 +49,46 @@ async function getLatestNewtVersion(): Promise<string | null> {
logger.warn( logger.warn(
`Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}` `Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}`
); );
return null; return staleNewtVersion;
} }
let tags = await response.json(); let tags = await response.json();
if (!Array.isArray(tags) || tags.length === 0) { if (!Array.isArray(tags) || tags.length === 0) {
logger.warn("No tags found for Newt repository"); logger.warn("No tags found for Newt repository");
return null; return staleNewtVersion;
} }
tags = tags.filter((version) => !version.name.includes("rc"));
// Remove release-candidates, then sort descending by semver so that
// duplicate tags (e.g. "1.10.3" and "v1.10.3") and any ordering quirks
// from the GitHub API do not cause an older tag to be selected.
tags = tags.filter((tag: any) => !tag.name.includes("rc"));
tags.sort((a: any, b: any) => {
const va = semver.coerce(a.name);
const vb = semver.coerce(b.name);
if (!va && !vb) return 0;
if (!va) return 1;
if (!vb) return -1;
return semver.rcompare(va, vb);
});
// Deduplicate: keep only the first (highest) entry per normalised version
const seen = new Set<string>();
tags = tags.filter((tag: any) => {
const normalised = semver.coerce(tag.name)?.version;
if (!normalised || seen.has(normalised)) return false;
seen.add(normalised);
return true;
});
if (tags.length === 0) {
logger.warn("No valid semver tags found for Newt repository");
return staleNewtVersion;
}
const latestVersion = tags[0].name; const latestVersion = tags[0].name;
await cache.set("latestNewtVersion", latestVersion, 3600); staleNewtVersion = latestVersion;
await cache.set("cache:latestNewtVersion", latestVersion, 3600);
return latestVersion; return latestVersion;
} catch (error: any) { } catch (error: any) {
@@ -73,7 +106,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
error.message || error error.message || error
); );
} }
return null; return staleNewtVersion;
} }
} }