veryfront 0.0.82 → 0.0.84
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/README.md +18 -17
- package/esm/deno.js +1 -1
- package/esm/proxy/cache/index.d.ts +41 -0
- package/esm/proxy/cache/index.d.ts.map +1 -0
- package/esm/proxy/cache/index.js +75 -0
- package/esm/proxy/cache/memory-cache.d.ts +18 -0
- package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
- package/esm/proxy/cache/memory-cache.js +100 -0
- package/esm/proxy/cache/redis-cache.d.ts +27 -0
- package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
- package/esm/proxy/cache/redis-cache.js +183 -0
- package/esm/proxy/cache/resilient-cache.d.ts +44 -0
- package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
- package/esm/proxy/cache/resilient-cache.js +178 -0
- package/esm/proxy/cache/types.d.ts +65 -0
- package/esm/proxy/cache/types.d.ts.map +1 -0
- package/esm/proxy/cache/types.js +7 -0
- package/esm/proxy/handler.d.ts +81 -0
- package/esm/proxy/handler.d.ts.map +1 -0
- package/esm/proxy/handler.js +417 -0
- package/esm/proxy/logger.d.ts +29 -0
- package/esm/proxy/logger.d.ts.map +1 -0
- package/esm/proxy/logger.js +258 -0
- package/esm/proxy/oauth-client.d.ts +15 -0
- package/esm/proxy/oauth-client.d.ts.map +1 -0
- package/esm/proxy/oauth-client.js +52 -0
- package/esm/proxy/token-manager.d.ts +59 -0
- package/esm/proxy/token-manager.d.ts.map +1 -0
- package/esm/proxy/token-manager.js +125 -0
- package/esm/proxy/tracing.d.ts +39 -0
- package/esm/proxy/tracing.d.ts.map +1 -0
- package/esm/proxy/tracing.js +194 -0
- package/esm/src/cache/backend.d.ts +2 -0
- package/esm/src/cache/backend.d.ts.map +1 -1
- package/esm/src/cache/backend.js +2 -0
- package/esm/src/cache/cache-key-builder.d.ts +0 -4
- package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
- package/esm/src/cache/cache-key-builder.js +0 -6
- package/esm/src/cache/multi-tier.d.ts +0 -29
- package/esm/src/cache/multi-tier.d.ts.map +1 -1
- package/esm/src/cache/multi-tier.js +0 -26
- package/esm/src/cli/app/actions.d.ts +26 -0
- package/esm/src/cli/app/actions.d.ts.map +1 -0
- package/esm/src/cli/app/actions.js +152 -0
- package/esm/src/cli/app/components/inline-input.d.ts +35 -0
- package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
- package/esm/src/cli/app/components/inline-input.js +220 -0
- package/esm/src/cli/app/components/list-select.d.ts +69 -0
- package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
- package/esm/src/cli/app/components/list-select.js +137 -0
- package/esm/src/cli/app/index.d.ts +45 -0
- package/esm/src/cli/app/index.d.ts.map +1 -0
- package/esm/src/cli/app/index.js +1252 -0
- package/esm/src/cli/app/state.d.ts +122 -0
- package/esm/src/cli/app/state.d.ts.map +1 -0
- package/esm/src/cli/app/state.js +232 -0
- package/esm/src/cli/app/views/dashboard.d.ts +19 -0
- package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
- package/esm/src/cli/app/views/dashboard.js +178 -0
- package/esm/src/cli/commands/dev.js +2 -2
- package/esm/src/cli/commands/new.js +1 -1
- package/esm/src/cli/index/command-router.d.ts.map +1 -1
- package/esm/src/cli/index/command-router.js +9 -39
- package/esm/src/cli/index/start-handler.d.ts +3 -0
- package/esm/src/cli/index/start-handler.d.ts.map +1 -0
- package/esm/src/cli/index/start-handler.js +145 -0
- package/esm/src/cli/mcp/index.d.ts +11 -0
- package/esm/src/cli/mcp/index.d.ts.map +1 -0
- package/esm/src/cli/mcp/index.js +10 -0
- package/esm/src/cli/ui/tui.js +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +34 -13
- package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
- package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
- package/esm/src/server/context/cache-invalidation.js +4 -0
- package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
- package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
- package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache.js +139 -64
- package/esm/src/utils/index.d.ts +1 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +1 -1
- package/package.json +2 -1
- package/src/deno.js +1 -1
- package/src/proxy/cache/index.ts +93 -0
- package/src/proxy/cache/memory-cache.ts +120 -0
- package/src/proxy/cache/redis-cache.ts +203 -0
- package/src/proxy/cache/resilient-cache.ts +205 -0
- package/src/proxy/cache/types.ts +72 -0
- package/src/proxy/handler.ts +593 -0
- package/src/proxy/logger.ts +329 -0
- package/src/proxy/oauth-client.ts +91 -0
- package/src/proxy/token-manager.ts +174 -0
- package/src/proxy/tracing.ts +237 -0
- package/src/src/cache/backend.ts +3 -0
- package/src/src/cache/cache-key-builder.ts +0 -9
- package/src/src/cache/multi-tier.ts +0 -41
- package/src/src/cli/app/actions.ts +190 -0
- package/src/src/cli/app/components/inline-input.ts +255 -0
- package/src/src/cli/app/components/list-select.ts +215 -0
- package/src/src/cli/app/index.ts +1471 -0
- package/src/src/cli/app/state.ts +385 -0
- package/src/src/cli/app/views/dashboard.ts +212 -0
- package/src/src/cli/commands/dev.ts +2 -2
- package/src/src/cli/commands/new.ts +1 -1
- package/src/src/cli/index/command-router.ts +9 -40
- package/src/src/cli/index/start-handler.ts +195 -0
- package/src/src/cli/mcp/index.ts +11 -0
- package/src/src/cli/ui/tui.ts +1 -1
- package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
- package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +38 -14
- package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
- package/src/src/server/context/cache-invalidation.ts +4 -0
- package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
- package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
- package/src/src/transforms/esm/http-cache.ts +148 -73
- package/src/src/utils/index.ts +0 -1
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy Handler - Core Logic
|
|
3
|
+
*
|
|
4
|
+
* Extracted proxy logic that can be used in:
|
|
5
|
+
* - Split mode: Standalone proxy server (proxy/main.ts)
|
|
6
|
+
* - Combined mode: Request interceptor in renderer process
|
|
7
|
+
*
|
|
8
|
+
* Handles:
|
|
9
|
+
* - Domain parsing (subdomain to project slug)
|
|
10
|
+
* - OAuth token management
|
|
11
|
+
* - Local project detection
|
|
12
|
+
* - User auth token extraction from cookies
|
|
13
|
+
*/
|
|
14
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import { TokenManager, type TokenScope } from "./token-manager.js";
|
|
18
|
+
import { type ParsedDomain, parseProjectDomain } from "../src/server/utils/domain-parser.js";
|
|
19
|
+
import type { TokenCache } from "./cache/types.js";
|
|
20
|
+
import { createFileSystem } from "../src/platform/compat/fs.js";
|
|
21
|
+
import { cwd } from "../src/platform/compat/process.js";
|
|
22
|
+
import { join } from "../src/platform/compat/path/index.js";
|
|
23
|
+
import { injectContext, ProxySpanNames, withSpan } from "./tracing.js";
|
|
24
|
+
import { computeContentSourceId } from "../src/cache/keys.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Domain lookup result from API.
|
|
28
|
+
* Uses GET /projects/{domain} which returns full project data.
|
|
29
|
+
*/
|
|
30
|
+
interface DomainLookupResult {
|
|
31
|
+
id: string;
|
|
32
|
+
slug: string;
|
|
33
|
+
name: string;
|
|
34
|
+
environments?: Array<{
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
domains?: string[];
|
|
38
|
+
active_release_id?: string | null;
|
|
39
|
+
protected?: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Look up project info by custom domain.
|
|
45
|
+
* Uses GET /projects/{domain} to resolve project slug when request comes via custom domain.
|
|
46
|
+
*/
|
|
47
|
+
async function lookupProjectByDomain(
|
|
48
|
+
domain: string,
|
|
49
|
+
apiBaseUrl: string,
|
|
50
|
+
token: string,
|
|
51
|
+
logger?: ProxyLogger,
|
|
52
|
+
): Promise<DomainLookupResult | null> {
|
|
53
|
+
return await withSpan(
|
|
54
|
+
ProxySpanNames.PROXY_DOMAIN_LOOKUP,
|
|
55
|
+
async () => {
|
|
56
|
+
const domainWithoutPort = domain.replace(/:\d+$/, "");
|
|
57
|
+
const url = `${apiBaseUrl}/projects/${encodeURIComponent(domainWithoutPort)}`;
|
|
58
|
+
const urlObj = new URL(url);
|
|
59
|
+
|
|
60
|
+
logger?.debug("Looking up project by domain", { domain, url });
|
|
61
|
+
|
|
62
|
+
const headers = new dntShim.Headers({
|
|
63
|
+
Authorization: `Bearer ${token}`,
|
|
64
|
+
Accept: "application/json",
|
|
65
|
+
});
|
|
66
|
+
injectContext(headers);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const response = await withSpan(
|
|
70
|
+
ProxySpanNames.HTTP_CLIENT_FETCH,
|
|
71
|
+
() => dntShim.fetch(url, { headers }),
|
|
72
|
+
{
|
|
73
|
+
"http.method": "GET",
|
|
74
|
+
"http.url": url,
|
|
75
|
+
"http.host": urlObj.host,
|
|
76
|
+
"proxy.domain_lookup": domain,
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
// Consume response body to prevent resource leak
|
|
82
|
+
await response.body?.cancel();
|
|
83
|
+
if (response.status !== 404) {
|
|
84
|
+
logger?.error("Domain lookup API error", undefined, {
|
|
85
|
+
domain,
|
|
86
|
+
status: response.status,
|
|
87
|
+
statusText: response.statusText,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await response.json() as DomainLookupResult;
|
|
94
|
+
logger?.debug("Domain lookup successful", {
|
|
95
|
+
domain,
|
|
96
|
+
projectSlug: result.slug,
|
|
97
|
+
environments: result.environments?.map((e) => e.name),
|
|
98
|
+
});
|
|
99
|
+
return result;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger?.error("Domain lookup failed", error as Error, { domain });
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{ "proxy.domain": domain },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ProxyConfig {
|
|
110
|
+
apiBaseUrl: string;
|
|
111
|
+
clientId: string;
|
|
112
|
+
clientSecret: string;
|
|
113
|
+
previewClientId: string;
|
|
114
|
+
previewClientSecret: string;
|
|
115
|
+
apiToken?: string; // Fallback token when OAuth credentials not available
|
|
116
|
+
localProjects?: Record<string, string>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ProxyContext {
|
|
120
|
+
token?: string;
|
|
121
|
+
projectSlug?: string;
|
|
122
|
+
projectId?: string;
|
|
123
|
+
releaseId?: string;
|
|
124
|
+
branchId?: string;
|
|
125
|
+
branchName?: string;
|
|
126
|
+
environment: "preview" | "production";
|
|
127
|
+
contentSourceId: string;
|
|
128
|
+
localPath?: string;
|
|
129
|
+
host: string;
|
|
130
|
+
parsedDomain: ParsedDomain;
|
|
131
|
+
isLocalProject: boolean;
|
|
132
|
+
/** Error if request cannot be processed (e.g., custom domain not found) */
|
|
133
|
+
error?: { status: number; message: string; redirectUrl?: string };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ProxyLogger {
|
|
137
|
+
debug: (msg: string, extra?: Record<string, unknown>) => void;
|
|
138
|
+
info: (msg: string, extra?: Record<string, unknown>) => void;
|
|
139
|
+
warn: (msg: string, extra?: Record<string, unknown>) => void;
|
|
140
|
+
error: (msg: string, error?: Error, extra?: Record<string, unknown>) => void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ProxyHandlerOptions {
|
|
144
|
+
config: ProxyConfig;
|
|
145
|
+
cache?: TokenCache;
|
|
146
|
+
logger?: ProxyLogger;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Determine the OAuth scope based on the parsed domain environment.
|
|
151
|
+
*/
|
|
152
|
+
function getScope(environment: string | null): TokenScope {
|
|
153
|
+
return environment === "preview" ? "preview" : "production";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract user auth token from cookie header.
|
|
158
|
+
*/
|
|
159
|
+
function extractUserToken(cookieHeader: string): string | undefined {
|
|
160
|
+
const authTokenMatch = cookieHeader.match(/(?:^|;\s*)authToken=([^;]+)/);
|
|
161
|
+
return authTokenMatch?.[1] ? decodeURIComponent(authTokenMatch[1]) : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a proxy handler that processes requests and returns context.
|
|
166
|
+
*
|
|
167
|
+
* This is the core proxy logic, usable in both split and combined modes.
|
|
168
|
+
*/
|
|
169
|
+
export function createProxyHandler(options: ProxyHandlerOptions) {
|
|
170
|
+
const { config, cache, logger } = options;
|
|
171
|
+
const localProjects = config.localProjects ?? {};
|
|
172
|
+
|
|
173
|
+
// Dynamic project discovery - check if project exists in common directories
|
|
174
|
+
const fs = createFileSystem();
|
|
175
|
+
async function findLocalProject(slug: string): Promise<string | undefined> {
|
|
176
|
+
// First check the static map
|
|
177
|
+
if (localProjects[slug]) {
|
|
178
|
+
return localProjects[slug];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Dynamically check common project directories - parallelized for performance
|
|
182
|
+
const projectDirs = ["projects", "data/projects", "examples"];
|
|
183
|
+
const basePath = cwd();
|
|
184
|
+
|
|
185
|
+
// Check all directories in parallel
|
|
186
|
+
const candidatePaths = projectDirs.map((dir) => join(basePath, dir, slug));
|
|
187
|
+
const existsResults = await Promise.all(
|
|
188
|
+
candidatePaths.map(async (projectPath) => {
|
|
189
|
+
try {
|
|
190
|
+
const exists = await fs.exists(projectPath);
|
|
191
|
+
return exists ? projectPath : null;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// For each existing path, check app/pages/components in parallel
|
|
199
|
+
for (const projectPath of existsResults) {
|
|
200
|
+
if (!projectPath) continue;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const [hasApp, hasPages, hasComponents] = await Promise.all([
|
|
204
|
+
fs.exists(join(projectPath, "app")),
|
|
205
|
+
fs.exists(join(projectPath, "pages")),
|
|
206
|
+
fs.exists(join(projectPath, "components")),
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
if (hasApp || hasPages || hasComponents) {
|
|
210
|
+
// Cache for future requests
|
|
211
|
+
localProjects[slug] = projectPath;
|
|
212
|
+
logger?.debug("Dynamically discovered local project", {
|
|
213
|
+
slug,
|
|
214
|
+
projectPath,
|
|
215
|
+
});
|
|
216
|
+
return projectPath;
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Directory check failed, continue
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Create token manager
|
|
226
|
+
const tokenManager = new TokenManager(
|
|
227
|
+
{
|
|
228
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
229
|
+
clientId: config.clientId,
|
|
230
|
+
clientSecret: config.clientSecret,
|
|
231
|
+
previewClientId: config.previewClientId,
|
|
232
|
+
previewClientSecret: config.previewClientSecret,
|
|
233
|
+
},
|
|
234
|
+
{ cache },
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate configuration and return missing credentials.
|
|
239
|
+
*/
|
|
240
|
+
function validateConfig(): string[] {
|
|
241
|
+
const missing: string[] = [];
|
|
242
|
+
if (!config.clientId) {
|
|
243
|
+
missing.push("API_CLIENT_ID_VERYFRONT_RENDERER_PROXY");
|
|
244
|
+
}
|
|
245
|
+
if (!config.clientSecret) {
|
|
246
|
+
missing.push("API_CLIENT_SECRET_VERYFRONT_RENDERER_PROXY");
|
|
247
|
+
}
|
|
248
|
+
return missing;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function processRequest(req: dntShim.Request): Promise<ProxyContext> {
|
|
252
|
+
const host = req.headers.get("host") || "";
|
|
253
|
+
const parsedDomain = parseProjectDomain(host);
|
|
254
|
+
const scope = getScope(parsedDomain.environment);
|
|
255
|
+
let projectSlug = parsedDomain.slug || undefined;
|
|
256
|
+
let projectId: string | undefined;
|
|
257
|
+
let releaseId: string | undefined;
|
|
258
|
+
const isCustomDomain = !projectSlug && !parsedDomain.isVeryfrontDomain;
|
|
259
|
+
|
|
260
|
+
// Handle veryfront domain without project slug (e.g., veryfront.me:8080)
|
|
261
|
+
// Return a no-project context so the projects page can be served
|
|
262
|
+
if (!projectSlug && parsedDomain.isVeryfrontDomain && !isCustomDomain) {
|
|
263
|
+
return {
|
|
264
|
+
token: undefined,
|
|
265
|
+
projectSlug: undefined,
|
|
266
|
+
projectId: undefined,
|
|
267
|
+
environment: "preview",
|
|
268
|
+
contentSourceId: "no-project",
|
|
269
|
+
localPath: undefined,
|
|
270
|
+
host,
|
|
271
|
+
parsedDomain,
|
|
272
|
+
isLocalProject: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const localPath = projectSlug ? await findLocalProject(projectSlug) : undefined;
|
|
277
|
+
const isLocalProject = !!localPath;
|
|
278
|
+
|
|
279
|
+
logger?.debug("Processing request", {
|
|
280
|
+
host,
|
|
281
|
+
projectSlug,
|
|
282
|
+
environment: scope,
|
|
283
|
+
isLocalProject,
|
|
284
|
+
isCustomDomain,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const makeErrorContext = (
|
|
288
|
+
status: number,
|
|
289
|
+
message: string,
|
|
290
|
+
token?: string,
|
|
291
|
+
redirectUrl?: string,
|
|
292
|
+
): ProxyContext => ({
|
|
293
|
+
token,
|
|
294
|
+
projectSlug: undefined,
|
|
295
|
+
projectId: undefined,
|
|
296
|
+
environment: scope,
|
|
297
|
+
contentSourceId: "error",
|
|
298
|
+
localPath: undefined,
|
|
299
|
+
host,
|
|
300
|
+
parsedDomain,
|
|
301
|
+
isLocalProject: false,
|
|
302
|
+
error: { status, message, redirectUrl },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
let token: string | undefined;
|
|
306
|
+
// Extract user auth token from cookies (used for preview scope and protected env check)
|
|
307
|
+
const cookieHeader = req.headers.get("cookie") || "";
|
|
308
|
+
const userToken = extractUserToken(cookieHeader);
|
|
309
|
+
|
|
310
|
+
if (isLocalProject) {
|
|
311
|
+
logger?.debug("Local project, skipping token fetch", { localPath });
|
|
312
|
+
} else {
|
|
313
|
+
if (scope === "preview" && userToken) {
|
|
314
|
+
token = userToken;
|
|
315
|
+
logger?.debug("Using user auth token for preview");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!token && config.clientId && config.clientSecret) {
|
|
319
|
+
const customDomain = !projectSlug ? host : undefined;
|
|
320
|
+
if (projectSlug || customDomain) {
|
|
321
|
+
try {
|
|
322
|
+
token = await tokenManager.getToken(
|
|
323
|
+
scope,
|
|
324
|
+
projectSlug,
|
|
325
|
+
customDomain,
|
|
326
|
+
);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
logger?.error("Token fetch failed", error as Error, {
|
|
329
|
+
projectSlug,
|
|
330
|
+
customDomain,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!token && config.apiToken) {
|
|
337
|
+
token = config.apiToken;
|
|
338
|
+
logger?.debug("Using static API token fallback");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (isCustomDomain && !projectSlug) {
|
|
342
|
+
// Custom domain: lookup project by domain
|
|
343
|
+
if (!token) {
|
|
344
|
+
logger?.error(
|
|
345
|
+
"Cannot process custom domain without token",
|
|
346
|
+
undefined,
|
|
347
|
+
{ domain: host },
|
|
348
|
+
);
|
|
349
|
+
return makeErrorContext(
|
|
350
|
+
502,
|
|
351
|
+
`Failed to authenticate for domain: ${host}`,
|
|
352
|
+
token,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const lookupResult = await lookupProjectByDomain(
|
|
357
|
+
host,
|
|
358
|
+
config.apiBaseUrl,
|
|
359
|
+
token,
|
|
360
|
+
logger,
|
|
361
|
+
);
|
|
362
|
+
if (lookupResult) {
|
|
363
|
+
projectSlug = lookupResult.slug;
|
|
364
|
+
projectId = lookupResult.id;
|
|
365
|
+
|
|
366
|
+
// Find matching environment for this domain and extract active release
|
|
367
|
+
const normalizedHost = host.toLowerCase().replace(/:\d+$/, "");
|
|
368
|
+
const matchingEnv = lookupResult.environments?.find((env) =>
|
|
369
|
+
env.domains?.some((d) => d.toLowerCase() === normalizedHost)
|
|
370
|
+
);
|
|
371
|
+
if (matchingEnv?.active_release_id) {
|
|
372
|
+
releaseId = matchingEnv.active_release_id;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check if environment is protected and user is not authenticated
|
|
376
|
+
if (matchingEnv?.protected && !userToken) {
|
|
377
|
+
const originalUrl = req.url;
|
|
378
|
+
const redirectUrl = `https://veryfront.com/sign-in?from=${
|
|
379
|
+
encodeURIComponent(originalUrl)
|
|
380
|
+
}`;
|
|
381
|
+
logger?.info("Protected environment requires authentication", {
|
|
382
|
+
domain: host,
|
|
383
|
+
environmentName: matchingEnv.name,
|
|
384
|
+
redirectUrl,
|
|
385
|
+
});
|
|
386
|
+
return makeErrorContext(
|
|
387
|
+
302,
|
|
388
|
+
"Authentication required",
|
|
389
|
+
token,
|
|
390
|
+
redirectUrl,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
logger?.info("Resolved custom domain to project", {
|
|
395
|
+
domain: host,
|
|
396
|
+
projectSlug,
|
|
397
|
+
projectId,
|
|
398
|
+
releaseId,
|
|
399
|
+
environmentName: matchingEnv?.name,
|
|
400
|
+
});
|
|
401
|
+
} else {
|
|
402
|
+
logger?.error("Custom domain not found", undefined, { domain: host });
|
|
403
|
+
return makeErrorContext(
|
|
404
|
+
404,
|
|
405
|
+
`No project configured for domain: ${host}`,
|
|
406
|
+
token,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
} else if (
|
|
410
|
+
projectSlug && scope === "production" && token &&
|
|
411
|
+
parsedDomain.environment
|
|
412
|
+
) {
|
|
413
|
+
// Veryfront domain in non-preview mode: lookup project by slug to get releaseId
|
|
414
|
+
// This handles production, staging, and other non-preview environments
|
|
415
|
+
const lookupResult = await lookupProjectByDomain(
|
|
416
|
+
projectSlug,
|
|
417
|
+
config.apiBaseUrl,
|
|
418
|
+
token,
|
|
419
|
+
logger,
|
|
420
|
+
);
|
|
421
|
+
if (lookupResult) {
|
|
422
|
+
projectId = lookupResult.id;
|
|
423
|
+
|
|
424
|
+
// Find environment matching the parsed domain's environment (e.g., "staging", "production")
|
|
425
|
+
const matchingEnv = lookupResult.environments?.find((env) =>
|
|
426
|
+
env.name.toLowerCase() === parsedDomain.environment!.toLowerCase()
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if (matchingEnv?.active_release_id) {
|
|
430
|
+
releaseId = matchingEnv.active_release_id;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Check if environment is protected and user is not authenticated
|
|
434
|
+
if (matchingEnv?.protected && !userToken) {
|
|
435
|
+
const originalUrl = req.url;
|
|
436
|
+
const redirectUrl = `https://veryfront.com/sign-in?from=${
|
|
437
|
+
encodeURIComponent(originalUrl)
|
|
438
|
+
}`;
|
|
439
|
+
logger?.info("Protected environment requires authentication", {
|
|
440
|
+
projectSlug,
|
|
441
|
+
environmentName: matchingEnv.name,
|
|
442
|
+
redirectUrl,
|
|
443
|
+
});
|
|
444
|
+
return makeErrorContext(
|
|
445
|
+
302,
|
|
446
|
+
"Authentication required",
|
|
447
|
+
token,
|
|
448
|
+
redirectUrl,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
logger?.info("Resolved veryfront domain to project", {
|
|
453
|
+
projectSlug,
|
|
454
|
+
projectId,
|
|
455
|
+
releaseId,
|
|
456
|
+
targetEnvName: parsedDomain.environment,
|
|
457
|
+
environmentName: matchingEnv?.name,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Error early if remote production but no releaseId
|
|
464
|
+
if (
|
|
465
|
+
scope === "production" && projectSlug && !releaseId && !isLocalProject
|
|
466
|
+
) {
|
|
467
|
+
logger?.error("Missing releaseId in production", undefined, {
|
|
468
|
+
projectSlug,
|
|
469
|
+
projectId,
|
|
470
|
+
host,
|
|
471
|
+
environment: scope,
|
|
472
|
+
});
|
|
473
|
+
return makeErrorContext(
|
|
474
|
+
502,
|
|
475
|
+
`Missing releaseId for production project: ${projectSlug}`,
|
|
476
|
+
token,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Compute contentSourceId using the single source of truth
|
|
481
|
+
const contentSourceId = computeContentSourceId(
|
|
482
|
+
isLocalProject,
|
|
483
|
+
scope,
|
|
484
|
+
parsedDomain.branch,
|
|
485
|
+
releaseId,
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
token,
|
|
490
|
+
projectSlug,
|
|
491
|
+
projectId,
|
|
492
|
+
releaseId,
|
|
493
|
+
contentSourceId,
|
|
494
|
+
environment: scope,
|
|
495
|
+
localPath,
|
|
496
|
+
host,
|
|
497
|
+
parsedDomain,
|
|
498
|
+
isLocalProject,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get token for API proxy requests.
|
|
504
|
+
*/
|
|
505
|
+
async function getTokenForApi(req: dntShim.Request): Promise<string | undefined> {
|
|
506
|
+
const host = req.headers.get("host") || "";
|
|
507
|
+
const parsedDomain = parseProjectDomain(host);
|
|
508
|
+
const scope = getScope(parsedDomain.environment);
|
|
509
|
+
const projectSlug = parsedDomain.slug || undefined;
|
|
510
|
+
|
|
511
|
+
// Try user token first for preview
|
|
512
|
+
if (scope === "preview") {
|
|
513
|
+
const cookieHeader = req.headers.get("cookie") || "";
|
|
514
|
+
const userToken = extractUserToken(cookieHeader);
|
|
515
|
+
if (userToken) return userToken;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Fall back to OAuth (requires projectSlug or customDomain for project-scoped tokens)
|
|
519
|
+
if (config.clientId && config.clientSecret) {
|
|
520
|
+
const customDomain = !projectSlug ? host : undefined;
|
|
521
|
+
if (projectSlug || customDomain) {
|
|
522
|
+
try {
|
|
523
|
+
return await tokenManager.getToken(scope, projectSlug, customDomain);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
logger?.error("Token fetch failed for API", error as Error, {
|
|
526
|
+
projectSlug,
|
|
527
|
+
customDomain,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Fall back to static API token
|
|
534
|
+
if (config.apiToken) {
|
|
535
|
+
return config.apiToken;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get token manager stats for monitoring.
|
|
543
|
+
*/
|
|
544
|
+
async function getStats() {
|
|
545
|
+
return await tokenManager.getStats();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Close the token manager and clean up resources.
|
|
550
|
+
*/
|
|
551
|
+
async function close() {
|
|
552
|
+
await tokenManager.close();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
processRequest,
|
|
557
|
+
getTokenForApi,
|
|
558
|
+
getStats,
|
|
559
|
+
close,
|
|
560
|
+
validateConfig,
|
|
561
|
+
localProjects,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export type ProxyHandler = ReturnType<typeof createProxyHandler>;
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Inject proxy context into request headers for the renderer.
|
|
569
|
+
* Used by both split mode (proxy/main.ts) and combined mode (scripts/server.ts).
|
|
570
|
+
*/
|
|
571
|
+
export function injectContextHeaders(req: dntShim.Request, ctx: ProxyContext): dntShim.Request {
|
|
572
|
+
const headers = new dntShim.Headers(req.headers);
|
|
573
|
+
|
|
574
|
+
if (ctx.token) headers.set("x-token", ctx.token);
|
|
575
|
+
headers.set("x-project-slug", ctx.projectSlug || "");
|
|
576
|
+
headers.set("x-environment", ctx.environment);
|
|
577
|
+
headers.set("x-content-source-id", ctx.contentSourceId);
|
|
578
|
+
headers.set("x-forwarded-host", ctx.host);
|
|
579
|
+
if (ctx.localPath) headers.set("x-project-path", ctx.localPath);
|
|
580
|
+
|
|
581
|
+
// Forward project/branch context for logging
|
|
582
|
+
if (ctx.projectId) headers.set("x-project-id", ctx.projectId);
|
|
583
|
+
if (ctx.releaseId) headers.set("x-release-id", ctx.releaseId);
|
|
584
|
+
if (ctx.branchId) headers.set("x-branch-id", ctx.branchId);
|
|
585
|
+
if (ctx.branchName) headers.set("x-branch-name", ctx.branchName);
|
|
586
|
+
|
|
587
|
+
return new dntShim.Request(req.url, {
|
|
588
|
+
method: req.method,
|
|
589
|
+
headers,
|
|
590
|
+
body: req.body,
|
|
591
|
+
redirect: "manual",
|
|
592
|
+
});
|
|
593
|
+
}
|