diff --git a/config/db/.gitignore b/config/db/.gitignore new file mode 100644 index 000000000..9d4b1bb9c --- /dev/null +++ b/config/db/.gitignore @@ -0,0 +1 @@ +*-journal diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 10204713a..a44d1948f 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -2351,7 +2351,7 @@ }, "scale": { "title": "Скала", - "description": "Предприятие, 50 потребители, 50 сайта и приоритетна поддръжка." + "description": "Функции за корпоративни клиенти, 50 потребители, 100 сайта и приоритетна поддръжка." } }, "personalUseOnly": "Само за лична употреба (безплатен лиценз - без проверка)", diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 5b7122867..3a797e564 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -2351,7 +2351,7 @@ }, "scale": { "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í)", diff --git a/messages/de-DE.json b/messages/de-DE.json index 5edc95cbc..2b5e92865 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/en-US.json b/messages/en-US.json index 7578574b6..d1ce572bd 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2351,7 +2351,7 @@ }, "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)", @@ -2824,9 +2824,9 @@ "streamingHttpWebhookTitle": "HTTP Webhook", "streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.", "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", - "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.", "streamingFailedToLoad": "Failed to load destinations", "streamingUnexpectedError": "An unexpected error occurred.", @@ -2849,7 +2849,7 @@ "httpDestNamePlaceholder": "My HTTP destination", "httpDestUrlLabel": "Destination URL", "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)", "httpDestAuthTitle": "Authentication", "httpDestAuthDescription": "Choose how requests to your endpoint are authenticated.", diff --git a/messages/es-ES.json b/messages/es-ES.json index 72251ffba..34c4cc970 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 8ede738ec..6b2efec27 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/it-IT.json b/messages/it-IT.json index 5e0f13a7e..6a771b5a3 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/ko-KR.json b/messages/ko-KR.json index ccf1f2ca8..b444d9f4d 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -2351,7 +2351,7 @@ }, "scale": { "title": "스케일", - "description": "기업 기능, 50명의 사용자, 50개의 사이트, 우선 지원." + "description": "기업 기능, 50명의 사용자, 100개의 사이트, 그리고 우선 지원." } }, "personalUseOnly": "개인용으로만 사용 (무료 라이선스 - 결제 없음)", diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 8e864f5b7..91593503a 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/nl-NL.json b/messages/nl-NL.json index d7d64abc1..987e08419 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/pl-PL.json b/messages/pl-PL.json index e58aafda1..eb4b4af2f 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 8b36732d3..a16101e43 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 12a285100..279f8b1a8 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -2351,7 +2351,7 @@ }, "scale": { "title": "Масштаб", - "description": "Функции предприятия, 50 пользователей, 50 сайтов, а также приоритетная поддержка." + "description": "Функции корпоративного уровня, 50 пользователей, 100 сайтов и приоритетная поддержка." } }, "personalUseOnly": "Только для личного использования (бесплатная лицензия - без оформления на кассе)", diff --git a/messages/tr-TR.json b/messages/tr-TR.json index f13f6588b..e38b93ca8 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -2351,7 +2351,7 @@ }, "scale": { "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)", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 4d5d96d7e..761f39524 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -2351,7 +2351,7 @@ }, "scale": { "title": "缩放比例", - "description": "企业特征、50个用户、50个站点和优先支持。" + "description": "企业功能,50个用户,100个站点,以及优先支持。" } }, "personalUseOnly": "仅限个人使用(免费许可 - 无需结账)", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index bde3e9aec..acc3bb17f 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -1080,6 +1080,7 @@ export type ResourceWhitelist = InferSelectModel; export type VersionMigration = InferSelectModel; export type ResourceRule = InferSelectModel; export type Domain = InferSelectModel; +export type DnsRecord = InferSelectModel; export type SupporterKey = InferSelectModel; export type Idp = InferSelectModel; export type ApiKey = InferSelectModel; diff --git a/server/lib/billing/licenses.ts b/server/lib/billing/licenses.ts index 3fecb32b5..ff942d11b 100644 --- a/server/lib/billing/licenses.ts +++ b/server/lib/billing/licenses.ts @@ -9,8 +9,8 @@ export type LicensePriceSet = { export const licensePriceSet: LicensePriceSet = { // Free license matches the freeLimitSet - [LicenseId.SMALL_LICENSE]: "price_1SxKHiD3Ee2Ir7WmvtEh17A8", - [LicenseId.BIG_LICENSE]: "price_1SxKHiD3Ee2Ir7WmMUiP0H6Y" + [LicenseId.SMALL_LICENSE]: "price_1TMJzmD3Ee2Ir7Wm05NlGImT", + [LicenseId.BIG_LICENSE]: "price_1TMJzzD3Ee2Ir7WmzJw9TerS" }; export const licensePriceSetSandbox: LicensePriceSet = { diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index a40142526..8e87cd769 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -217,7 +217,7 @@ export async function handleSubscriptionCreated( subscriptionPriceId === priceSet[LicenseId.BIG_LICENSE] ) { numUsers = 50; - numSites = 50; + numSites = 100; } else { logger.error( `Unknown price ID ${subscriptionPriceId} for subscription ${subscription.id}` diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 0bf798509..f5d69857d 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -29,65 +29,9 @@ import { } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import NodeCache from "node-cache"; -import semver from "semver"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -const olmVersionCache = new NodeCache({ stdTTL: 3600 }); - -async function getLatestOlmVersion(): Promise { - try { - const cachedVersion = olmVersionCache.get("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({ 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( - (client) => { - const OlmWithUpdate: OlmWithUpdateAvailable = { ...client }; - // Initially set to false, will be updated if version check succeeds - OlmWithUpdate.olmUpdateAvailable = false; - return OlmWithUpdate; - } - ); + // const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map( + // (client) => { + // const OlmWithUpdate: OlmWithUpdateAvailable = { ...client }; + // // Initially set to false, will be updated if version check succeeds + // OlmWithUpdate.olmUpdateAvailable = false; + // return OlmWithUpdate; + // } + // ); // Try to get the latest version, but don't block if it fails - try { - const latestOlVersion = await latestOlVersionPromise; + // try { + // const latestOlmVersion = await latestOlVersionPromise; - if (latestOlVersion) { - olmsWithUpdates.forEach((client) => { - try { - client.olmUpdateAvailable = semver.lt( - client.olmVersion ? client.olmVersion : "", - latestOlVersion - ); - } catch (error) { - client.olmUpdateAvailable = false; - } - }); - } - } catch (error) { - // Log the error but don't let it block the response - logger.warn( - "Failed to check for OLM updates, continuing without update info:", - error - ); - } + // if (latestOlVersion) { + // olmsWithUpdates.forEach((client) => { + // try { + // client.olmUpdateAvailable = semver.lt( + // client.olmVersion ? client.olmVersion : "", + // latestOlVersion + // ); + // } catch (error) { + // client.olmUpdateAvailable = false; + // } + // }); + // } + // } catch (error) { + // // Log the error but don't let it block the response + // logger.warn( + // "Failed to check for OLM updates, continuing without update info:", + // error + // ); + // } return response(res, { data: { - clients: olmsWithUpdates, + clients: clientsWithSites, pagination: { total: totalCount, page, diff --git a/server/routers/client/listUserDevices.ts b/server/routers/client/listUserDevices.ts index 0ae31165a..d793faf09 100644 --- a/server/routers/client/listUserDevices.ts +++ b/server/routers/client/listUserDevices.ts @@ -30,65 +30,10 @@ import { } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import NodeCache from "node-cache"; import semver from "semver"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -const olmVersionCache = new NodeCache({ stdTTL: 3600 }); - -async function getLatestOlmVersion(): Promise { - try { - const cachedVersion = olmVersionCache.get("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({ orgId: z.string() }); @@ -453,29 +398,30 @@ export async function listUserDevices( } ); - // Try to get the latest version, but don't block if it fails - try { - const latestOlmVersion = await getLatestOlmVersion(); + // REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW + // // Try to get the latest version, but don't block if it fails + // try { + // const latestOlmVersion = await getLatestOlmVersion(); - if (latestOlmVersion) { - olmsWithUpdates.forEach((client) => { - try { - client.olmUpdateAvailable = semver.lt( - client.olmVersion ? client.olmVersion : "", - latestOlmVersion - ); - } catch (error) { - client.olmUpdateAvailable = false; - } - }); - } - } catch (error) { - // Log the error but don't let it block the response - logger.warn( - "Failed to check for OLM updates, continuing without update info:", - error - ); - } + // if (latestOlmVersion) { + // olmsWithUpdates.forEach((client) => { + // try { + // client.olmUpdateAvailable = semver.lt( + // client.olmVersion ? client.olmVersion : "", + // latestOlmVersion + // ); + // } catch (error) { + // client.olmUpdateAvailable = false; + // } + // }); + // } + // } catch (error) { + // // Log the error but don't let it block the response + // logger.warn( + // "Failed to check for OLM updates, continuing without update info:", + // error + // ); + // } return response(res, { data: { diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 6f085d74d..b65182908 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -21,6 +21,11 @@ import semver from "semver"; import { z } from "zod"; 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 { try { const cachedVersion = await cache.get("latestNewtVersion"); @@ -29,7 +34,7 @@ async function getLatestNewtVersion(): Promise { } 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( "https://api.github.com/repos/fosrl/newt/tags", @@ -44,18 +49,46 @@ async function getLatestNewtVersion(): Promise { logger.warn( `Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}` ); - return null; + return staleNewtVersion; } let tags = await response.json(); if (!Array.isArray(tags) || tags.length === 0) { 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(); + 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; - await cache.set("latestNewtVersion", latestVersion, 3600); + staleNewtVersion = latestVersion; + await cache.set("cache:latestNewtVersion", latestVersion, 3600); return latestVersion; } catch (error: any) { @@ -73,7 +106,7 @@ async function getLatestNewtVersion(): Promise { error.message || error ); } - return null; + return staleNewtVersion; } }