@nexttylabs/echo 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/package.json +2 -1
- package/proxy.ts +0 -74
- package/vercel.json +4 -0
- package/app/api/internal/domain-lookup/route.ts +0 -67
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @nexttylabs/echo
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5cfdeb7: feat: support editing user profile
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 7dba561: remove legacy changeset files
|
|
12
|
+
- e3c4e1c: fix workflow errors
|
|
13
|
+
- daf9abb: first release
|
|
14
|
+
|
|
15
|
+
## 0.3.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 5cfdeb7: feat: support editing user profile
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 7dba561: remove legacy changeset files
|
|
24
|
+
- e3c4e1c: fix workflow errors
|
|
25
|
+
- daf9abb: first release
|
|
26
|
+
|
|
3
27
|
## 0.2.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/package.json
CHANGED
package/proxy.ts
CHANGED
|
@@ -30,19 +30,6 @@ const publicRoutes = ["/login", "/register", "/invite", "/invite/", "/api/auth",
|
|
|
30
30
|
const protectedRoutes = ["/dashboard", "/feedback", "/settings"];
|
|
31
31
|
const LOCALE_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
|
|
32
32
|
|
|
33
|
-
// Primary app hosts (custom domains will not match these)
|
|
34
|
-
const APP_HOSTS = new Set([
|
|
35
|
-
"localhost",
|
|
36
|
-
"localhost:3000",
|
|
37
|
-
"127.0.0.1:3000",
|
|
38
|
-
// Add production domains when deployed
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
// Simple in-memory cache for domain lookups
|
|
42
|
-
const domainCache = new Map<string, { orgSlug: string; projectSlug: string } | null>();
|
|
43
|
-
const CACHE_TTL = 60 * 1000; // 1 minute
|
|
44
|
-
const cacheTimestamps = new Map<string, number>();
|
|
45
|
-
|
|
46
33
|
function generateRequestId(): string {
|
|
47
34
|
return crypto.randomUUID();
|
|
48
35
|
}
|
|
@@ -86,44 +73,6 @@ function isAuthenticated(req: NextRequest): boolean {
|
|
|
86
73
|
return testAuth === '1';
|
|
87
74
|
}
|
|
88
75
|
|
|
89
|
-
async function lookupCustomDomain(hostname: string, requestUrl: string): Promise<{ orgSlug: string; projectSlug: string } | null> {
|
|
90
|
-
const now = Date.now();
|
|
91
|
-
const cachedResult = domainCache.get(hostname);
|
|
92
|
-
const cacheTime = cacheTimestamps.get(hostname);
|
|
93
|
-
|
|
94
|
-
if (cachedResult !== undefined && cacheTime && now - cacheTime < CACHE_TTL) {
|
|
95
|
-
return cachedResult;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const lookupUrl = new URL("/api/internal/domain-lookup", requestUrl);
|
|
100
|
-
lookupUrl.searchParams.set("domain", hostname);
|
|
101
|
-
|
|
102
|
-
const response = await fetch(lookupUrl, {
|
|
103
|
-
headers: {
|
|
104
|
-
"x-middleware-secret": process.env.MIDDLEWARE_SECRET || "",
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (response.ok) {
|
|
109
|
-
const data = await response.json();
|
|
110
|
-
if (data.orgSlug && data.projectSlug) {
|
|
111
|
-
const result = { orgSlug: data.orgSlug, projectSlug: data.projectSlug };
|
|
112
|
-
domainCache.set(hostname, result);
|
|
113
|
-
cacheTimestamps.set(hostname, now);
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
domainCache.set(hostname, null);
|
|
119
|
-
cacheTimestamps.set(hostname, now);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.error("Domain lookup failed:", error);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
76
|
export async function proxy(req: NextRequest) {
|
|
128
77
|
const startTime = Date.now();
|
|
129
78
|
const reqId = req.headers.get("x-request-id") || generateRequestId();
|
|
@@ -135,29 +84,6 @@ export async function proxy(req: NextRequest) {
|
|
|
135
84
|
console.log(`[${reqId}] ${req.method} ${req.nextUrl.pathname}`);
|
|
136
85
|
|
|
137
86
|
const pathname = req.nextUrl.pathname;
|
|
138
|
-
const hostname = req.headers.get("host") || "";
|
|
139
|
-
const hostnameWithoutPort = hostname.split(":")[0];
|
|
140
|
-
|
|
141
|
-
// Custom domain routing - check if this is a custom domain request
|
|
142
|
-
if (!APP_HOSTS.has(hostname) && !APP_HOSTS.has(hostnameWithoutPort)) {
|
|
143
|
-
// Skip API routes and static assets
|
|
144
|
-
if (!pathname.startsWith("/api/") && !pathname.startsWith("/_next/") && !pathname.includes(".")) {
|
|
145
|
-
const domainInfo = await lookupCustomDomain(hostname, req.url);
|
|
146
|
-
if (domainInfo) {
|
|
147
|
-
const url = req.nextUrl.clone();
|
|
148
|
-
url.pathname = `/portal/${domainInfo.orgSlug}/${domainInfo.projectSlug}${pathname === "/" ? "" : pathname}`;
|
|
149
|
-
|
|
150
|
-
const response = NextResponse.rewrite(url, {
|
|
151
|
-
request: { headers: requestHeaders },
|
|
152
|
-
});
|
|
153
|
-
maybeSetLocaleCookie(req, response, pathname);
|
|
154
|
-
response.headers.set("x-request-id", reqId);
|
|
155
|
-
const duration = Date.now() - startTime;
|
|
156
|
-
console.log(`[${reqId}] ${response.status} ${duration}ms (rewrite)`);
|
|
157
|
-
return response;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
87
|
|
|
162
88
|
const isPublic = isRouteMatch(pathname, publicRoutes);
|
|
163
89
|
const isProtected = isRouteMatch(pathname, protectedRoutes);
|
package/vercel.json
ADDED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Echo Team
|
|
3
|
-
*
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
* (at your option) any later version.
|
|
8
|
-
*
|
|
9
|
-
* This program is distributed in the hope that it will be useful,
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
13
|
-
*
|
|
14
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
19
|
-
import { db } from "@/lib/db";
|
|
20
|
-
import { organizationSettings, organizations } from "@/lib/db/schema";
|
|
21
|
-
import { eq } from "drizzle-orm";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Internal API for domain lookup (used by middleware)
|
|
25
|
-
* GET /api/internal/domain-lookup?domain=feedback.acme.com
|
|
26
|
-
*/
|
|
27
|
-
export async function GET(req: NextRequest) {
|
|
28
|
-
// Verify middleware secret to prevent external access
|
|
29
|
-
const secret = req.headers.get("x-middleware-secret");
|
|
30
|
-
if (secret !== process.env.MIDDLEWARE_SECRET) {
|
|
31
|
-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const domain = req.nextUrl.searchParams.get("domain");
|
|
35
|
-
if (!domain) {
|
|
36
|
-
return NextResponse.json({ error: "Domain required" }, { status: 400 });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!db) {
|
|
40
|
-
return NextResponse.json({ error: "Database not configured" }, { status: 500 });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const [organization] = await db
|
|
45
|
-
.select({
|
|
46
|
-
organizationId: organizations.id,
|
|
47
|
-
orgSlug: organizations.slug,
|
|
48
|
-
})
|
|
49
|
-
.from(organizationSettings)
|
|
50
|
-
.innerJoin(organizations, eq(organizationSettings.organizationId, organizations.id))
|
|
51
|
-
.where(eq(organizationSettings.customDomain, domain))
|
|
52
|
-
.limit(1);
|
|
53
|
-
|
|
54
|
-
if (!organization) {
|
|
55
|
-
return NextResponse.json({ found: false });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return NextResponse.json({
|
|
59
|
-
found: true,
|
|
60
|
-
orgSlug: organization.orgSlug,
|
|
61
|
-
organizationId: organization.organizationId,
|
|
62
|
-
});
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.error("Domain lookup error:", error);
|
|
65
|
-
return NextResponse.json({ error: "Lookup failed" }, { status: 500 });
|
|
66
|
-
}
|
|
67
|
-
}
|