Compare commits

..

10 Commits

Author SHA1 Message Date
Owen Schwartz
9fe4f78269 Merge pull request #2857 from fosrl/dev
Proxy targets returns an array
2026-04-13 20:57:15 -07:00
Owen
03d95874e6 Proxy targets returns an array 2026-04-13 20:44:35 -07:00
Milo Schwartz
bd3d6994c1 Merge pull request #2856 from fosrl/update-readme
fix image
2026-04-13 20:29:36 -07:00
miloschwartz
5fd78817a8 fix image 2026-04-13 20:28:05 -07:00
Owen Schwartz
72bc125f84 Merge pull request #2854 from fosrl/dev
Rename script
2026-04-13 16:23:39 -07:00
Owen
5d51af4330 Rename script 2026-04-13 16:22:53 -07:00
Owen Schwartz
b18ea66def Merge pull request #2853 from fosrl/dev
1.17.1-s.1
2026-04-13 12:28:08 -07:00
Owen
93998f9fd5 Fix ts issue 2026-04-13 12:27:29 -07:00
Owen
c554e69514 Fill the width 2026-04-13 12:11:15 -07:00
Owen
a6e10e55cc Handle grandfather on the front end 2026-04-13 12:08:30 -07:00
14 changed files with 83 additions and 73 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 KiB

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 KiB

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 KiB

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -591,7 +591,7 @@ export function generateSubnetProxyTargetV2(
pubKey: string | null; pubKey: string | null;
subnet: string | null; subnet: string | null;
}[] }[]
): SubnetProxyTargetV2 | undefined { ): SubnetProxyTargetV2[] | undefined {
if (clients.length === 0) { if (clients.length === 0) {
logger.debug( logger.debug(
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.` `No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
@@ -599,7 +599,7 @@ export function generateSubnetProxyTargetV2(
return; return;
} }
let target: SubnetProxyTargetV2 | null = null; let targets: SubnetProxyTargetV2[] = [];
const portRange = [ const portRange = [
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
@@ -614,52 +614,54 @@ export function generateSubnetProxyTargetV2(
if (ipSchema.safeParse(destination).success) { if (ipSchema.safeParse(destination).success) {
destination = `${destination}/32`; destination = `${destination}/32`;
target = { targets.push({
sourcePrefixes: [], sourcePrefixes: [],
destPrefix: destination, destPrefix: destination,
portRange, portRange,
disableIcmp, disableIcmp,
resourceId: siteResource.siteResourceId, resourceId: siteResource.siteResourceId
}; });
} }
if (siteResource.alias && siteResource.aliasAddress) { if (siteResource.alias && siteResource.aliasAddress) {
// also push a match for the alias address // also push a match for the alias address
target = { targets.push({
sourcePrefixes: [], sourcePrefixes: [],
destPrefix: `${siteResource.aliasAddress}/32`, destPrefix: `${siteResource.aliasAddress}/32`,
rewriteTo: destination, rewriteTo: destination,
portRange, portRange,
disableIcmp, disableIcmp,
resourceId: siteResource.siteResourceId, resourceId: siteResource.siteResourceId
}; });
} }
} else if (siteResource.mode == "cidr") { } else if (siteResource.mode == "cidr") {
target = { targets.push({
sourcePrefixes: [], sourcePrefixes: [],
destPrefix: siteResource.destination, destPrefix: siteResource.destination,
portRange, portRange,
disableIcmp, disableIcmp,
resourceId: siteResource.siteResourceId, resourceId: siteResource.siteResourceId
}; });
} }
if (!target) { if (targets.length == 0) {
return; return;
} }
for (const clientSite of clients) { for (const target of targets) {
if (!clientSite.subnet) { for (const clientSite of clients) {
logger.debug( if (!clientSite.subnet) {
`Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.` logger.debug(
); `Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.`
continue; );
continue;
}
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
// add client prefix to source prefixes
target.sourcePrefixes.push(clientPrefix);
} }
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
// add client prefix to source prefixes
target.sourcePrefixes.push(clientPrefix);
} }
// print a nice representation of the targets // print a nice representation of the targets
@@ -667,36 +669,34 @@ export function generateSubnetProxyTargetV2(
// `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}` // `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}`
// ); // );
return target; return targets;
} }
/** /**
* Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1) * Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1)
* by expanding each source prefix into its own target entry. * by expanding each source prefix into its own target entry.
* @param targetV2 - The v2 target to convert * @param targetV2 - The v2 target to convert
* @returns Array of v1 SubnetProxyTarget objects * @returns Array of v1 SubnetProxyTarget objects
*/ */
export function convertSubnetProxyTargetsV2ToV1( export function convertSubnetProxyTargetsV2ToV1(
targetsV2: SubnetProxyTargetV2[] targetsV2: SubnetProxyTargetV2[]
): SubnetProxyTarget[] { ): SubnetProxyTarget[] {
return targetsV2.flatMap((targetV2) => return targetsV2.flatMap((targetV2) =>
targetV2.sourcePrefixes.map((sourcePrefix) => ({ targetV2.sourcePrefixes.map((sourcePrefix) => ({
sourcePrefix, sourcePrefix,
destPrefix: targetV2.destPrefix, destPrefix: targetV2.destPrefix,
...(targetV2.disableIcmp !== undefined && { ...(targetV2.disableIcmp !== undefined && {
disableIcmp: targetV2.disableIcmp disableIcmp: targetV2.disableIcmp
}), }),
...(targetV2.rewriteTo !== undefined && { ...(targetV2.rewriteTo !== undefined && {
rewriteTo: targetV2.rewriteTo rewriteTo: targetV2.rewriteTo
}), }),
...(targetV2.portRange !== undefined && { ...(targetV2.portRange !== undefined && {
portRange: targetV2.portRange portRange: targetV2.portRange
}) })
})) }))
); );
} }
// Custom schema for validating port range strings // Custom schema for validating port range strings
// Format: "80,443,8000-9000" or "*" for all ports, or empty string // Format: "80,443,8000-9000" or "*" for all ports, or empty string

View File

@@ -661,16 +661,16 @@ async function handleSubnetProxyTargetUpdates(
); );
if (addedClients.length > 0) { if (addedClients.length > 0) {
const targetToAdd = generateSubnetProxyTargetV2( const targetsToAdd = generateSubnetProxyTargetV2(
siteResource, siteResource,
addedClients addedClients
); );
if (targetToAdd) { if (targetsToAdd) {
proxyJobs.push( proxyJobs.push(
addSubnetProxyTargets( addSubnetProxyTargets(
newt.newtId, newt.newtId,
[targetToAdd], targetsToAdd,
newt.version newt.version
) )
); );
@@ -698,16 +698,16 @@ async function handleSubnetProxyTargetUpdates(
); );
if (removedClients.length > 0) { if (removedClients.length > 0) {
const targetToRemove = generateSubnetProxyTargetV2( const targetsToRemove = generateSubnetProxyTargetV2(
siteResource, siteResource,
removedClients removedClients
); );
if (targetToRemove) { if (targetsToRemove) {
proxyJobs.push( proxyJobs.push(
removeSubnetProxyTargets( removeSubnetProxyTargets(
newt.newtId, newt.newtId,
[targetToRemove], targetsToRemove,
newt.version newt.version
) )
); );
@@ -1164,7 +1164,7 @@ async function handleMessagesForClientResources(
} }
for (const resource of resources) { for (const resource of resources) {
const target = generateSubnetProxyTargetV2(resource, [ const targets = generateSubnetProxyTargetV2(resource, [
{ {
clientId: client.clientId, clientId: client.clientId,
pubKey: client.pubKey, pubKey: client.pubKey,
@@ -1172,11 +1172,11 @@ async function handleMessagesForClientResources(
} }
]); ]);
if (target) { if (targets) {
proxyJobs.push( proxyJobs.push(
addSubnetProxyTargets( addSubnetProxyTargets(
newt.newtId, newt.newtId,
[target], targets,
newt.version newt.version
) )
); );
@@ -1241,7 +1241,7 @@ async function handleMessagesForClientResources(
} }
for (const resource of resources) { for (const resource of resources) {
const target = generateSubnetProxyTargetV2(resource, [ const targets = generateSubnetProxyTargetV2(resource, [
{ {
clientId: client.clientId, clientId: client.clientId,
pubKey: client.pubKey, pubKey: client.pubKey,
@@ -1249,11 +1249,11 @@ async function handleMessagesForClientResources(
} }
]); ]);
if (target) { if (targets) {
proxyJobs.push( proxyJobs.push(
removeSubnetProxyTargets( removeSubnetProxyTargets(
newt.newtId, newt.newtId,
[target], targets,
newt.version newt.version
) )
); );

View File

@@ -168,13 +168,13 @@ export async function buildClientConfigurationForNewtClient(
) )
); );
const resourceTarget = generateSubnetProxyTargetV2( const resourceTargets = generateSubnetProxyTargetV2(
resource, resource,
resourceClients resourceClients
); );
if (resourceTarget) { if (resourceTargets) {
targetsToSend.push(resourceTarget); targetsToSend.push(...resourceTargets);
} }
} }

View File

@@ -618,11 +618,11 @@ export async function handleMessagingForUpdatedSiteResource(
// Only update targets on newt if destination changed // Only update targets on newt if destination changed
if (destinationChanged || portRangesChanged) { if (destinationChanged || portRangesChanged) {
const oldTarget = generateSubnetProxyTargetV2( const oldTargets = generateSubnetProxyTargetV2(
existingSiteResource, existingSiteResource,
mergedAllClients mergedAllClients
); );
const newTarget = generateSubnetProxyTargetV2( const newTargets = generateSubnetProxyTargetV2(
updatedSiteResource, updatedSiteResource,
mergedAllClients mergedAllClients
); );
@@ -630,8 +630,8 @@ export async function handleMessagingForUpdatedSiteResource(
await updateTargets( await updateTargets(
newt.newtId, newt.newtId,
{ {
oldTargets: oldTarget ? [oldTarget] : [], oldTargets: oldTargets ? oldTargets : [],
newTargets: newTarget ? [newTarget] : [] newTargets: newTargets ? newTargets : []
}, },
newt.version newt.version
); );

View File

@@ -21,7 +21,8 @@ async function queryUser(userId: string) {
serverAdmin: users.serverAdmin, serverAdmin: users.serverAdmin,
idpName: idp.name, idpName: idp.name,
idpId: users.idpId, idpId: users.idpId,
locale: users.locale locale: users.locale,
dateCreated: users.dateCreated
}) })
.from(users) .from(users)
.leftJoin(idp, eq(users.idpId, idp.idpId)) .leftJoin(idp, eq(users.idpId, idp.idpId))

View File

@@ -64,7 +64,8 @@ export async function myDevice(
serverAdmin: users.serverAdmin, serverAdmin: users.serverAdmin,
idpName: idp.name, idpName: idp.name,
idpId: users.idpId, idpId: users.idpId,
locale: users.locale locale: users.locale,
dateCreated: users.dateCreated
}) })
.from(users) .from(users)
.leftJoin(idp, eq(users.idpId, idp.idpId)) .leftJoin(idp, eq(users.idpId, idp.idpId))

View File

@@ -491,6 +491,10 @@ export default function BillingPage() {
const currentPlanId = getCurrentPlanId(); const currentPlanId = getCurrentPlanId();
const visiblePlanOptions = planOptions.filter(
(plan) => plan.id !== "home" || currentPlanId === "home"
);
// Check if subscription is in a problematic state that requires attention // Check if subscription is in a problematic state that requires attention
const hasProblematicSubscription = (): boolean => { const hasProblematicSubscription = (): boolean => {
if (!tierSubscription?.subscription) return false; if (!tierSubscription?.subscription) return false;
@@ -803,8 +807,8 @@ export default function BillingPage() {
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
{/* Plan Cards Grid */} {/* Plan Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4"> <div className={cn("grid grid-cols-1 gap-4", visiblePlanOptions.length === 5 ? "md:grid-cols-5" : "md:grid-cols-4")}>
{planOptions.filter((plan) => plan.id !== "home" || currentPlanId === "home").map((plan) => { {visiblePlanOptions.map((plan) => {
const isCurrentPlan = plan.id === currentPlanId; const isCurrentPlan = plan.id === currentPlanId;
const planAction = getPlanAction(plan); const planAction = getPlanAction(plan);

View File

@@ -49,6 +49,7 @@ import { usePaidStatus } from "@/hooks/usePaidStatus";
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { toUnicode } from "punycode"; import { toUnicode } from "punycode";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useUserContext } from "@app/hooks/useUserContext";
type AvailableOption = { type AvailableOption = {
domainNamespaceId: string; domainNamespaceId: string;
@@ -97,10 +98,16 @@ export default function DomainPicker({
warnOnProvidedDomain = false warnOnProvidedDomain = false
}: DomainPickerProps) { }: DomainPickerProps) {
const { env } = useEnvContext(); const { env } = useEnvContext();
const { user } = useUserContext();
const api = createApiClient({ env }); const api = createApiClient({ env });
const t = useTranslations(); const t = useTranslations();
const { hasSaasSubscription } = usePaidStatus(); const { hasSaasSubscription } = usePaidStatus();
const requiresPaywall =
build === "saas" &&
!hasSaasSubscription(tierMatrix[TierFeature.DomainNamespaces]) &&
new Date(user.dateCreated) > new Date("2026-04-13");
const { data = [], isLoading: loadingDomains } = useQuery( const { data = [], isLoading: loadingDomains } = useQuery(
orgQueries.domains({ orgId }) orgQueries.domains({ orgId })
); );
@@ -656,6 +663,7 @@ export default function DomainPicker({
}) })
} }
className="mx-2 rounded-md" className="mx-2 rounded-md"
disabled={requiresPaywall}
> >
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3"> <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
<Zap className="h-4 w-4 text-primary" /> <Zap className="h-4 w-4 text-primary" />
@@ -696,11 +704,7 @@ export default function DomainPicker({
</div> </div>
</div> </div>
{build === "saas" && {requiresPaywall && !hideFreeDomain && (
!hasSaasSubscription(
tierMatrix[TierFeature.DomainNamespaces]
) &&
!hideFreeDomain && (
<Card className="mt-3 border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden"> <Card className="mt-3 border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden">
<CardContent className="py-3 px-4"> <CardContent className="py-3 px-4">
<div className="flex items-center gap-2.5 text-sm text-muted-foreground"> <div className="flex items-center gap-2.5 text-sm text-muted-foreground">