veryfront 0.1.811 → 0.1.813
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/esm/deno.js +1 -1
- package/esm/src/agent/conversation/durable-contracts.d.ts +204 -0
- package/esm/src/agent/conversation/durable-contracts.d.ts.map +1 -0
- package/esm/src/agent/conversation/durable-contracts.js +168 -0
- package/esm/src/agent/conversation/durable.d.ts +3 -198
- package/esm/src/agent/conversation/durable.d.ts.map +1 -1
- package/esm/src/agent/conversation/durable.js +2 -167
- package/esm/src/agent/runtime/agent-runtime-step.d.ts +44 -0
- package/esm/src/agent/runtime/agent-runtime-step.d.ts.map +1 -0
- package/esm/src/agent/runtime/agent-runtime-step.js +23 -0
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +40 -39
- package/esm/src/agent/runtime/model-transport.d.ts +23 -0
- package/esm/src/agent/runtime/model-transport.d.ts.map +1 -0
- package/esm/src/agent/runtime/model-transport.js +26 -0
- package/esm/src/cache/keys/builders/render.d.ts.map +1 -1
- package/esm/src/cache/keys/builders/render.js +3 -2
- package/esm/src/cache/request-cacheability.d.ts +3 -0
- package/esm/src/cache/request-cacheability.d.ts.map +1 -0
- package/esm/src/cache/request-cacheability.js +26 -0
- package/esm/src/html/hydration-script-builder/hydration-data-generator.d.ts.map +1 -1
- package/esm/src/html/hydration-script-builder/hydration-data-generator.js +1 -10
- package/esm/src/html/hydration-script-builder/templates/router.d.ts.map +1 -1
- package/esm/src/html/hydration-script-builder/templates/router.js +41 -10
- package/esm/src/modules/import-map/loader.d.ts.map +1 -1
- package/esm/src/modules/import-map/loader.js +2 -1
- package/esm/src/modules/manifest/route-module-manifest.d.ts.map +1 -1
- package/esm/src/modules/manifest/route-module-manifest.js +2 -0
- package/esm/src/modules/server/module-server.d.ts.map +1 -1
- package/esm/src/modules/server/module-server.js +27 -9
- package/esm/src/observability/request-profiler.d.ts.map +1 -1
- package/esm/src/observability/request-profiler.js +1 -0
- package/esm/src/observability/simple-metrics/index.d.ts +6 -3
- package/esm/src/observability/simple-metrics/index.d.ts.map +1 -1
- package/esm/src/observability/simple-metrics/index.js +5 -2
- package/esm/src/observability/simple-metrics/metrics-recorder.d.ts +4 -0
- package/esm/src/observability/simple-metrics/metrics-recorder.d.ts.map +1 -1
- package/esm/src/observability/simple-metrics/metrics-recorder.js +34 -0
- package/esm/src/observability/simple-metrics/metrics-state.d.ts.map +1 -1
- package/esm/src/observability/simple-metrics/metrics-state.js +24 -0
- package/esm/src/observability/simple-metrics/otel-instruments.d.ts.map +1 -1
- package/esm/src/observability/simple-metrics/otel-instruments.js +13 -0
- package/esm/src/observability/simple-metrics/types.d.ts +12 -0
- package/esm/src/observability/simple-metrics/types.d.ts.map +1 -1
- package/esm/src/platform/adapters/runtime/shared/shared-watcher.d.ts.map +1 -1
- package/esm/src/platform/adapters/runtime/shared/shared-watcher.js +1 -1
- package/esm/src/platform/compat/http/native-response.d.ts +10 -1
- package/esm/src/platform/compat/http/native-response.d.ts.map +1 -1
- package/esm/src/platform/compat/http/native-response.js +20 -14
- package/esm/src/proxy/handler.d.ts +1 -7
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +63 -189
- package/esm/src/proxy/main.js +21 -20
- package/esm/src/proxy/proxy-access-control.d.ts +41 -0
- package/esm/src/proxy/proxy-access-control.d.ts.map +1 -0
- package/esm/src/proxy/proxy-access-control.js +151 -0
- package/esm/src/proxy/proxy-error-context.d.ts +20 -0
- package/esm/src/proxy/proxy-error-context.d.ts.map +1 -0
- package/esm/src/proxy/proxy-error-context.js +35 -0
- package/esm/src/proxy/request-lifecycle.d.ts +19 -0
- package/esm/src/proxy/request-lifecycle.d.ts.map +1 -0
- package/esm/src/proxy/request-lifecycle.js +27 -0
- package/esm/src/proxy/upstream-error-response.d.ts +5 -0
- package/esm/src/proxy/upstream-error-response.d.ts.map +1 -0
- package/esm/src/proxy/upstream-error-response.js +15 -0
- package/esm/src/react/components/chat/chat/composition/chat-composer.d.ts.map +1 -1
- package/esm/src/react/components/chat/chat/composition/chat-composer.js +3 -1
- package/esm/src/release-assets/client-module-map.d.ts +3 -0
- package/esm/src/release-assets/client-module-map.d.ts.map +1 -0
- package/esm/src/release-assets/client-module-map.js +10 -0
- package/esm/src/rendering/cache/cache-coordinator.d.ts.map +1 -1
- package/esm/src/rendering/cache/cache-coordinator.js +1 -0
- package/esm/src/rendering/cache/stores/memory-store.d.ts +2 -0
- package/esm/src/rendering/cache/stores/memory-store.d.ts.map +1 -1
- package/esm/src/rendering/cache/stores/memory-store.js +2 -1
- package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/lifecycle.js +1 -0
- package/esm/src/rendering/orchestrator/module-loader/index.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/module-loader/index.js +13 -49
- package/esm/src/rendering/orchestrator/module-loader/module-cache-lookup.d.ts +25 -0
- package/esm/src/rendering/orchestrator/module-loader/module-cache-lookup.d.ts.map +1 -0
- package/esm/src/rendering/orchestrator/module-loader/module-cache-lookup.js +69 -0
- package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/pipeline.js +22 -35
- package/esm/src/rendering/orchestrator/render-result-assembly.d.ts +27 -0
- package/esm/src/rendering/orchestrator/render-result-assembly.d.ts.map +1 -0
- package/esm/src/rendering/orchestrator/render-result-assembly.js +28 -0
- package/esm/src/rendering/orchestrator/types.d.ts +2 -0
- package/esm/src/rendering/orchestrator/types.d.ts.map +1 -1
- package/esm/src/rendering/renderer.d.ts.map +1 -1
- package/esm/src/rendering/renderer.js +24 -19
- package/esm/src/rendering/shared/context-aware-cache.d.ts.map +1 -1
- package/esm/src/rendering/shared/context-aware-cache.js +1 -0
- package/esm/src/server/handlers/dev/dashboard/api.d.ts +3 -0
- package/esm/src/server/handlers/dev/dashboard/api.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/dashboard/api.js +41 -57
- package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/framework-candidates.generated.js +11 -1
- package/esm/src/server/handlers/request/module/page-data-endpoint-handler.d.ts +1 -0
- package/esm/src/server/handlers/request/module/page-data-endpoint-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/module/page-data-endpoint-handler.js +98 -8
- package/esm/src/transforms/esm/specifier-resolver.d.ts.map +1 -1
- package/esm/src/transforms/esm/specifier-resolver.js +14 -0
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
package/esm/src/proxy/handler.js
CHANGED
|
@@ -1,46 +1,12 @@
|
|
|
1
1
|
import { TokenManager } from "./token-manager.js";
|
|
2
2
|
import { parseProjectDomain } from "../server/utils/domain-parser.js";
|
|
3
|
-
import { getEnv } from "../platform/compat/process.js";
|
|
4
3
|
import { injectContext, ProxySpanNames, withSpan } from "./tracing.js";
|
|
5
4
|
import { computeContentSourceId } from "../cache/keys.js";
|
|
6
|
-
import {
|
|
5
|
+
import { checkProtectedProxyAccess } from "./proxy-access-control.js";
|
|
7
6
|
import { createLocalProjectResolver } from "./local-project-resolver.js";
|
|
8
7
|
import { isMissingCustomDomainProjectError, resolveProxyRequestToken, } from "./proxy-token-resolution.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* the registry lookup on every request. The cache is cleared implicitly when
|
|
12
|
-
* `ExtensionLoader.teardownAll()` clears the registry — the next call
|
|
13
|
-
* re-resolves (or surfaces the "install ext-auth-jwt" hint if the extension was
|
|
14
|
-
* removed).
|
|
15
|
-
*/
|
|
16
|
-
let cachedAuthProvider;
|
|
17
|
-
function getAuthProvider() {
|
|
18
|
-
if (cachedAuthProvider)
|
|
19
|
-
return cachedAuthProvider;
|
|
20
|
-
try {
|
|
21
|
-
cachedAuthProvider = resolveContract("AuthProvider");
|
|
22
|
-
return cachedAuthProvider;
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
// resolve() already throws with a helpful "Recommended: @veryfront/ext-auth-jwt"
|
|
26
|
-
// message, but the proxy is a load-bearing code path — append a concrete
|
|
27
|
-
// remediation hint that names the project-root extension directory so
|
|
28
|
-
// the user knows exactly what's missing.
|
|
29
|
-
const base = err instanceof Error ? err.message : String(err);
|
|
30
|
-
throw new Error(`${base}\nTo enable JWT verification in the proxy, install ext-auth-jwt ` +
|
|
31
|
-
`(scaffold with \`deno task cli extension init ext-auth-jwt\` or add the ` +
|
|
32
|
-
`npm package @veryfront/ext-auth-jwt).`, { cause: err });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Reset the cached AuthProvider. Intended for tests that `register()` a mock
|
|
37
|
-
* after the handler module has been imported.
|
|
38
|
-
*
|
|
39
|
-
* @internal
|
|
40
|
-
*/
|
|
41
|
-
export function __resetCachedAuthProviderForTests() {
|
|
42
|
-
cachedAuthProvider = undefined;
|
|
43
|
-
}
|
|
8
|
+
import { createProjectNotFoundProxyContext, createProxyErrorContext, createReleaseNotFoundProxyContext, } from "./proxy-error-context.js";
|
|
9
|
+
export { __resetCachedAuthProviderForTests } from "./proxy-access-control.js";
|
|
44
10
|
export const INTERNAL_PROXY_HEADERS = [
|
|
45
11
|
"x-token",
|
|
46
12
|
"x-project-slug",
|
|
@@ -75,18 +41,6 @@ function isSignedInternalControlPlaneRequest(req, url) {
|
|
|
75
41
|
}
|
|
76
42
|
return !!req.headers.get("x-token");
|
|
77
43
|
}
|
|
78
|
-
function resolveApiJwksUrl(apiBaseUrl, logger) {
|
|
79
|
-
try {
|
|
80
|
-
const normalizedBaseUrl = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
|
|
81
|
-
return new URL(".well-known/jwks.json", normalizedBaseUrl).toString();
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
logger?.error("Invalid API base URL for JWKS lookup", error, {
|
|
85
|
-
apiBaseUrl,
|
|
86
|
-
});
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
44
|
async function lookupProjectByDomain(domain, apiBaseUrl, token, logger) {
|
|
91
45
|
return withSpan(ProxySpanNames.PROXY_DOMAIN_LOOKUP, async () => {
|
|
92
46
|
const domainWithoutPort = domain.replace(/:\d+$/, "");
|
|
@@ -141,59 +95,6 @@ function parseStatusFromError(error) {
|
|
|
141
95
|
const match = message.match(/failed: (\d+)/);
|
|
142
96
|
return match ? Number(match[1]) : null;
|
|
143
97
|
}
|
|
144
|
-
async function extractUserIdFromToken(token, apiBaseUrl, log) {
|
|
145
|
-
const auth = getAuthProvider();
|
|
146
|
-
const header = auth.decode(token);
|
|
147
|
-
if (!header) {
|
|
148
|
-
log?.debug("Failed to decode JWT header");
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
const algorithm = header.alg;
|
|
152
|
-
if (algorithm === "RS256") {
|
|
153
|
-
const jwksUrl = resolveApiJwksUrl(apiBaseUrl, log);
|
|
154
|
-
if (!jwksUrl)
|
|
155
|
-
return undefined;
|
|
156
|
-
try {
|
|
157
|
-
const payload = await auth.verifyWithJwks(token, jwksUrl, {
|
|
158
|
-
algorithms: ["RS256"],
|
|
159
|
-
});
|
|
160
|
-
return payload.userId;
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
log?.debug("RS256 JWT verification failed", {
|
|
164
|
-
error: error instanceof Error ? error.message : String(error),
|
|
165
|
-
});
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (algorithm !== "HS256") {
|
|
170
|
-
log?.debug("Unsupported JWT algorithm", { algorithm: algorithm ?? null });
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
const jwtSecret = getEnv("JWT_SECRET");
|
|
174
|
-
if (!jwtSecret) {
|
|
175
|
-
log?.warn("JWT_SECRET not configured — cannot verify user token");
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
// ext-auth-jwt reads JWT_SECRET from the environment when no `secret` was
|
|
180
|
-
// passed to the extension factory; the explicit env check above is kept
|
|
181
|
-
// so callers can warn once before we attempt verification.
|
|
182
|
-
const payload = await auth.verify(token, { algorithms: ["HS256"] });
|
|
183
|
-
return payload.userId;
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
log?.debug("JWT verification failed", {
|
|
187
|
-
error: error instanceof Error ? error.message : String(error),
|
|
188
|
-
});
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
function isProjectMember(users, userId) {
|
|
193
|
-
if (!users || !userId)
|
|
194
|
-
return false;
|
|
195
|
-
return users.some((u) => u.id === userId);
|
|
196
|
-
}
|
|
197
98
|
export function createProxyHandler(options) {
|
|
198
99
|
const { config, cache, logger } = options;
|
|
199
100
|
const localProjects = config.localProjects ?? {};
|
|
@@ -213,85 +114,22 @@ export function createProxyHandler(options) {
|
|
|
213
114
|
missing.push("VERYFRONT_PROXY_API_CLIENT_SECRET");
|
|
214
115
|
return missing;
|
|
215
116
|
}
|
|
216
|
-
function makeErrorContext(base, status, message, token, redirectUrl, slug) {
|
|
217
|
-
return {
|
|
218
|
-
token,
|
|
219
|
-
projectSlug: undefined,
|
|
220
|
-
projectId: undefined,
|
|
221
|
-
environment: base.scope,
|
|
222
|
-
contentSourceId: "error",
|
|
223
|
-
localPath: undefined,
|
|
224
|
-
host: base.host,
|
|
225
|
-
parsedDomain: base.parsedDomain,
|
|
226
|
-
isLocalProject: false,
|
|
227
|
-
error: { status, message, redirectUrl, slug },
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
function makeAuthRedirectUrl(url) {
|
|
231
|
-
// Collapse leading slashes to prevent protocol-relative open redirects (e.g. "//evil.com/path")
|
|
232
|
-
const safePath = url.pathname.replace(/^\/\/+/, "/");
|
|
233
|
-
let returnPath = safePath + url.search;
|
|
234
|
-
// Ensure the return path stays within the application and is not an absolute URL.
|
|
235
|
-
// - It must start with "/".
|
|
236
|
-
// - It must not contain a scheme delimiter ("://").
|
|
237
|
-
// If it fails validation, fall back to the root path.
|
|
238
|
-
if (!returnPath.startsWith("/") || returnPath.includes("://")) {
|
|
239
|
-
returnPath = "/";
|
|
240
|
-
}
|
|
241
|
-
const isHostedProductionDeployment = url.hostname.endsWith(".production.veryfront.org") ||
|
|
242
|
-
url.hostname.endsWith(".production.veryfront.com");
|
|
243
|
-
const returnTarget = isHostedProductionDeployment ? url.toString() : returnPath;
|
|
244
|
-
return `https://veryfront.com/sign-in?from=${encodeURIComponent(returnTarget)}`;
|
|
245
|
-
}
|
|
246
|
-
function makeProjectNotFoundContext(base, message, token) {
|
|
247
|
-
return makeErrorContext(base, 404, message, token, undefined, "project-not-found");
|
|
248
|
-
}
|
|
249
|
-
async function checkProtectedAccess(req, url, matchingEnv, userToken, users, logContext) {
|
|
250
|
-
if (!matchingEnv?.protected)
|
|
251
|
-
return null;
|
|
252
|
-
if (isSignedInternalControlPlaneRequest(req, url)) {
|
|
253
|
-
logger?.debug("Allowing signed internal control-plane request through protected environment", {
|
|
254
|
-
...logContext,
|
|
255
|
-
environmentName: matchingEnv.name,
|
|
256
|
-
pathname: url.pathname,
|
|
257
|
-
});
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
if (!userToken) {
|
|
261
|
-
const redirectUrl = makeAuthRedirectUrl(url);
|
|
262
|
-
logger?.info("Protected environment requires authentication", {
|
|
263
|
-
...logContext,
|
|
264
|
-
environmentName: matchingEnv.name,
|
|
265
|
-
redirectUrl,
|
|
266
|
-
});
|
|
267
|
-
return { status: 302, message: "Authentication required", redirectUrl };
|
|
268
|
-
}
|
|
269
|
-
const userId = await extractUserIdFromToken(userToken, config.apiBaseUrl, logger);
|
|
270
|
-
if (!userId) {
|
|
271
|
-
const redirectUrl = makeAuthRedirectUrl(url);
|
|
272
|
-
logger?.info("Could not extract userId from token", {
|
|
273
|
-
...logContext,
|
|
274
|
-
environmentName: matchingEnv.name,
|
|
275
|
-
redirectUrl,
|
|
276
|
-
});
|
|
277
|
-
return { status: 302, message: "Authentication required", redirectUrl };
|
|
278
|
-
}
|
|
279
|
-
if (!isProjectMember(users, userId)) {
|
|
280
|
-
logger?.info("User is not a member of the project", {
|
|
281
|
-
...logContext,
|
|
282
|
-
environmentName: matchingEnv.name,
|
|
283
|
-
userId,
|
|
284
|
-
});
|
|
285
|
-
return { status: 403, message: "Access denied" };
|
|
286
|
-
}
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
117
|
async function resolveReleaseAndProtection(req, url, token, userToken, lookupKey, envMatcher, logContext) {
|
|
290
118
|
const lookupResult = await lookupProjectByDomain(lookupKey, config.apiBaseUrl, token, logger);
|
|
291
119
|
if (!lookupResult)
|
|
292
120
|
return { projectId: undefined, releaseId: undefined };
|
|
293
121
|
const matchingEnv = lookupResult.environments?.find(envMatcher);
|
|
294
|
-
const protectionError = await
|
|
122
|
+
const protectionError = await checkProtectedProxyAccess({
|
|
123
|
+
req,
|
|
124
|
+
url,
|
|
125
|
+
matchingEnv,
|
|
126
|
+
userToken,
|
|
127
|
+
users: lookupResult.users,
|
|
128
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
129
|
+
logger,
|
|
130
|
+
logContext,
|
|
131
|
+
isSignedInternalControlPlaneRequest: isSignedInternalControlPlaneRequest(req, url),
|
|
132
|
+
});
|
|
295
133
|
if (protectionError)
|
|
296
134
|
return { error: protectionError };
|
|
297
135
|
return {
|
|
@@ -359,10 +197,10 @@ export function createProxyHandler(options) {
|
|
|
359
197
|
if (status === 404) {
|
|
360
198
|
if (scope === "preview") {
|
|
361
199
|
logger?.info("Preview project not found", { projectSlug, host });
|
|
362
|
-
return
|
|
200
|
+
return createProjectNotFoundProxyContext(base, "Preview project not found");
|
|
363
201
|
}
|
|
364
202
|
logger?.info("Project not found", { projectSlug, host, scope });
|
|
365
|
-
return
|
|
203
|
+
return createProjectNotFoundProxyContext(base, "Project not found");
|
|
366
204
|
}
|
|
367
205
|
const message = scope === "preview"
|
|
368
206
|
? "Failed to authenticate preview request"
|
|
@@ -374,7 +212,7 @@ export function createProxyHandler(options) {
|
|
|
374
212
|
hadUserToken: !!userToken,
|
|
375
213
|
hadTokenFetchError: !!tokenFetchError,
|
|
376
214
|
});
|
|
377
|
-
return
|
|
215
|
+
return createProxyErrorContext(base, { status: 502, message });
|
|
378
216
|
}
|
|
379
217
|
if (isCustomDomain && !projectSlug) {
|
|
380
218
|
if (!token) {
|
|
@@ -382,15 +220,26 @@ export function createProxyHandler(options) {
|
|
|
382
220
|
logger?.info("Custom domain project not found during token fetch", {
|
|
383
221
|
domain: host,
|
|
384
222
|
});
|
|
385
|
-
return
|
|
223
|
+
return createProxyErrorContext(base, {
|
|
224
|
+
status: 404,
|
|
225
|
+
message: `No project configured for domain: ${host}`,
|
|
226
|
+
});
|
|
386
227
|
}
|
|
387
228
|
logger?.error("Cannot process custom domain without token", undefined, { domain: host });
|
|
388
|
-
return
|
|
229
|
+
return createProxyErrorContext(base, {
|
|
230
|
+
status: 502,
|
|
231
|
+
message: `Failed to authenticate for domain: ${host}`,
|
|
232
|
+
token,
|
|
233
|
+
});
|
|
389
234
|
}
|
|
390
235
|
const lookupResult = await lookupProjectByDomain(host, config.apiBaseUrl, token, logger);
|
|
391
236
|
if (!lookupResult) {
|
|
392
237
|
logger?.info("Custom domain not found", { domain: host });
|
|
393
|
-
return
|
|
238
|
+
return createProxyErrorContext(base, {
|
|
239
|
+
status: 404,
|
|
240
|
+
message: `No project configured for domain: ${host}`,
|
|
241
|
+
token,
|
|
242
|
+
});
|
|
394
243
|
}
|
|
395
244
|
projectSlug = lookupResult.slug;
|
|
396
245
|
projectId = lookupResult.id;
|
|
@@ -398,9 +247,24 @@ export function createProxyHandler(options) {
|
|
|
398
247
|
const matchingEnv = lookupResult.environments?.find((env) => env.domains?.some((d) => d.toLowerCase() === normalizedHost));
|
|
399
248
|
releaseId = matchingEnv?.active_release_id ?? undefined;
|
|
400
249
|
environmentId = matchingEnv?.id;
|
|
401
|
-
const protectionError = await
|
|
250
|
+
const protectionError = await checkProtectedProxyAccess({
|
|
251
|
+
req,
|
|
252
|
+
url,
|
|
253
|
+
matchingEnv,
|
|
254
|
+
userToken,
|
|
255
|
+
users: lookupResult.users,
|
|
256
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
257
|
+
logger,
|
|
258
|
+
logContext: { domain: host },
|
|
259
|
+
isSignedInternalControlPlaneRequest: isSignedInternalControlPlaneRequest(req, url),
|
|
260
|
+
});
|
|
402
261
|
if (protectionError) {
|
|
403
|
-
return
|
|
262
|
+
return createProxyErrorContext(base, {
|
|
263
|
+
status: protectionError.status,
|
|
264
|
+
message: protectionError.message,
|
|
265
|
+
token,
|
|
266
|
+
redirectUrl: protectionError.redirectUrl,
|
|
267
|
+
});
|
|
404
268
|
}
|
|
405
269
|
logger?.info("Resolved custom domain to project", {
|
|
406
270
|
domain: host,
|
|
@@ -414,7 +278,12 @@ export function createProxyHandler(options) {
|
|
|
414
278
|
const targetEnv = parsedDomain.environment.toLowerCase();
|
|
415
279
|
const resolved = await resolveReleaseAndProtection(req, url, token, userToken, projectSlug, (env) => env.name.toLowerCase() === targetEnv, { projectSlug });
|
|
416
280
|
if ("error" in resolved) {
|
|
417
|
-
return
|
|
281
|
+
return createProxyErrorContext(base, {
|
|
282
|
+
status: resolved.error.status,
|
|
283
|
+
message: resolved.error.message,
|
|
284
|
+
token,
|
|
285
|
+
redirectUrl: resolved.error.redirectUrl,
|
|
286
|
+
});
|
|
418
287
|
}
|
|
419
288
|
if (!resolved.projectId) {
|
|
420
289
|
logger?.info("Project not found after lookup", {
|
|
@@ -423,7 +292,7 @@ export function createProxyHandler(options) {
|
|
|
423
292
|
scope,
|
|
424
293
|
targetEnvName: parsedDomain.environment,
|
|
425
294
|
});
|
|
426
|
-
return
|
|
295
|
+
return createProjectNotFoundProxyContext(base, "Project not found", token);
|
|
427
296
|
}
|
|
428
297
|
projectId = resolved.projectId;
|
|
429
298
|
releaseId = resolved.releaseId;
|
|
@@ -441,11 +310,16 @@ export function createProxyHandler(options) {
|
|
|
441
310
|
// still enforce the environment's `protected` flag like other scopes.
|
|
442
311
|
const resolved = await resolveReleaseAndProtection(req, url, token, userToken, projectSlug, (env) => env.name.toLowerCase() === "preview", { projectSlug });
|
|
443
312
|
if ("error" in resolved) {
|
|
444
|
-
return
|
|
313
|
+
return createProxyErrorContext(base, {
|
|
314
|
+
status: resolved.error.status,
|
|
315
|
+
message: resolved.error.message,
|
|
316
|
+
token,
|
|
317
|
+
redirectUrl: resolved.error.redirectUrl,
|
|
318
|
+
});
|
|
445
319
|
}
|
|
446
320
|
if (!resolved.projectId) {
|
|
447
321
|
logger?.info("Preview project not found after lookup", { projectSlug, host });
|
|
448
|
-
return
|
|
322
|
+
return createProjectNotFoundProxyContext(base, "Preview project not found", token);
|
|
449
323
|
}
|
|
450
324
|
projectId = resolved.projectId;
|
|
451
325
|
environmentId = resolved.environmentId;
|
|
@@ -465,7 +339,7 @@ export function createProxyHandler(options) {
|
|
|
465
339
|
host,
|
|
466
340
|
environment: scope,
|
|
467
341
|
});
|
|
468
|
-
return
|
|
342
|
+
return createReleaseNotFoundProxyContext({ scope, host, parsedDomain }, token);
|
|
469
343
|
}
|
|
470
344
|
const contentSourceId = computeContentSourceId(isLocalProject, scope, parsedDomain.branch, releaseId);
|
|
471
345
|
return {
|
package/esm/src/proxy/main.js
CHANGED
|
@@ -35,6 +35,8 @@ import { exit, getEnv, onSignal } from "../platform/compat/process.js";
|
|
|
35
35
|
import { createHttpServer, upgradeWebSocket } from "../platform/compat/http/index.js";
|
|
36
36
|
import { createProxyErrorResponse, jsonErrorResponse } from "./error-response.js";
|
|
37
37
|
import { handleReleaseAssetRequest, isReleaseAssetPath } from "./asset-handler.js";
|
|
38
|
+
import { runProxyRequestLifecycle } from "./request-lifecycle.js";
|
|
39
|
+
import { createUpstreamFailureResponse, createUpstreamTimeoutResponse, UPSTREAM_FAILURE_STATUS, UPSTREAM_TIMEOUT_STATUS, } from "./upstream-error-response.js";
|
|
38
40
|
function getLocalProjects() {
|
|
39
41
|
const raw = getEnv("LOCAL_PROJECTS");
|
|
40
42
|
return raw ? JSON.parse(raw) : {};
|
|
@@ -231,9 +233,7 @@ function forwardToServer(req, url) {
|
|
|
231
233
|
const startTime = performance.now();
|
|
232
234
|
const requestId = dntShim.crypto.randomUUID();
|
|
233
235
|
const host = req.headers.get("host") || "";
|
|
234
|
-
const
|
|
235
|
-
const spanInfo = startServerSpan(req.method, url.pathname, parentContext);
|
|
236
|
-
const execute = async () => {
|
|
236
|
+
const execute = async (lifecycle) => {
|
|
237
237
|
try {
|
|
238
238
|
const ctx = await proxyHandler.processRequest(req, { url });
|
|
239
239
|
return runWithProxyRequestContext({
|
|
@@ -250,7 +250,7 @@ function forwardToServer(req, url) {
|
|
|
250
250
|
const ms = Math.round(performance.now() - startTime);
|
|
251
251
|
const logLevel = getProxyFailureLogLevel(ctx.error.status, req.method, url.pathname);
|
|
252
252
|
proxyLogger[logLevel](`${ctx.error.status} ${req.method} ${url.pathname}`, { ms });
|
|
253
|
-
|
|
253
|
+
lifecycle.end(ctx.error.status);
|
|
254
254
|
return createProxyErrorResponse(ctx.error);
|
|
255
255
|
}
|
|
256
256
|
const reqLogger = proxyLogger.child({
|
|
@@ -332,7 +332,6 @@ function forwardToServer(req, url) {
|
|
|
332
332
|
else {
|
|
333
333
|
reqLogger.info(`${response.status} ${req.method} ${url.pathname}`, { ms });
|
|
334
334
|
}
|
|
335
|
-
endSpan(spanInfo?.span, response.status);
|
|
336
335
|
return new Response(response.body, {
|
|
337
336
|
status: response.status,
|
|
338
337
|
statusText: response.statusText,
|
|
@@ -344,15 +343,12 @@ function forwardToServer(req, url) {
|
|
|
344
343
|
lastError = error;
|
|
345
344
|
if (error instanceof Error && error.name === "AbortError") {
|
|
346
345
|
const ms = Math.round(performance.now() - startTime);
|
|
347
|
-
proxyLogger.error(
|
|
346
|
+
proxyLogger.error(`${UPSTREAM_TIMEOUT_STATUS} ${req.method} ${url.pathname}`, {
|
|
348
347
|
ms,
|
|
349
348
|
timeoutMs: VERYFRONT_SERVER_REQUEST_TIMEOUT_MS,
|
|
350
349
|
});
|
|
351
|
-
|
|
352
|
-
return
|
|
353
|
-
error: "Gateway Timeout",
|
|
354
|
-
message: `Server request timed out after ${VERYFRONT_SERVER_REQUEST_TIMEOUT_MS}ms`,
|
|
355
|
-
});
|
|
350
|
+
lifecycle.end(UPSTREAM_TIMEOUT_STATUS, error);
|
|
351
|
+
return createUpstreamTimeoutResponse(VERYFRONT_SERVER_REQUEST_TIMEOUT_MS);
|
|
356
352
|
}
|
|
357
353
|
// Check if this is a retryable error and we have retries left
|
|
358
354
|
if (isRetryableConnectionError(error) && attempt < maxRetries) {
|
|
@@ -379,26 +375,31 @@ function forwardToServer(req, url) {
|
|
|
379
375
|
}
|
|
380
376
|
// All retries exhausted or non-retryable error
|
|
381
377
|
const ms = Math.round(performance.now() - startTime);
|
|
382
|
-
const logLevel = getProxyFailureLogLevel(
|
|
383
|
-
proxyLogger[logLevel](
|
|
384
|
-
|
|
385
|
-
return
|
|
386
|
-
error: "Proxy Error",
|
|
387
|
-
message: lastError instanceof Error ? lastError.message : "Unknown error",
|
|
388
|
-
});
|
|
378
|
+
const logLevel = getProxyFailureLogLevel(UPSTREAM_FAILURE_STATUS, req.method, url.pathname);
|
|
379
|
+
proxyLogger[logLevel](`${UPSTREAM_FAILURE_STATUS} ${req.method} ${url.pathname}`, { ms }, lastError);
|
|
380
|
+
lifecycle.end(UPSTREAM_FAILURE_STATUS, lastError);
|
|
381
|
+
return createUpstreamFailureResponse(lastError);
|
|
389
382
|
});
|
|
390
383
|
}
|
|
391
384
|
catch (error) {
|
|
392
385
|
const ms = Math.round(performance.now() - startTime);
|
|
393
386
|
proxyLogger.error(`500 ${req.method} ${url.pathname}`, { ms }, error);
|
|
394
|
-
|
|
387
|
+
lifecycle.end(500, error);
|
|
395
388
|
return jsonErrorResponse(500, {
|
|
396
389
|
error: "Internal Proxy Error",
|
|
397
390
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
398
391
|
});
|
|
399
392
|
}
|
|
400
393
|
};
|
|
401
|
-
return
|
|
394
|
+
return runProxyRequestLifecycle({
|
|
395
|
+
req,
|
|
396
|
+
url,
|
|
397
|
+
startServerSpan,
|
|
398
|
+
endSpan,
|
|
399
|
+
extractContext,
|
|
400
|
+
withContext,
|
|
401
|
+
handle: execute,
|
|
402
|
+
});
|
|
402
403
|
}
|
|
403
404
|
/**
|
|
404
405
|
* Handle stats endpoint for monitoring.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ProxyAccessControlLogger {
|
|
2
|
+
debug: (msg: string, extra?: Record<string, unknown>) => void;
|
|
3
|
+
info: (msg: string, extra?: Record<string, unknown>) => void;
|
|
4
|
+
warn: (msg: string, extra?: Record<string, unknown>) => void;
|
|
5
|
+
error: (msg: string, error?: Error, extra?: Record<string, unknown>) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface ProtectedProxyEnvironment {
|
|
8
|
+
name: string;
|
|
9
|
+
protected?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ProtectedProxyProjectUser {
|
|
12
|
+
id: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ProxyAccessError {
|
|
15
|
+
status: number;
|
|
16
|
+
message: string;
|
|
17
|
+
redirectUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Reset the cached AuthProvider. Intended for tests that `register()` a mock
|
|
21
|
+
* after the handler module has been imported.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export declare function __resetCachedAuthProviderForTests(): void;
|
|
26
|
+
export declare function extractUserIdFromToken(token: string, apiBaseUrl: string, log?: ProxyAccessControlLogger): Promise<string | undefined>;
|
|
27
|
+
export declare function buildProxyAuthRedirectUrl(url: URL): string;
|
|
28
|
+
export declare function isProjectMember(users: ProtectedProxyProjectUser[] | undefined, userId: string | undefined): boolean;
|
|
29
|
+
export declare function checkProtectedProxyAccess(input: {
|
|
30
|
+
req: Request;
|
|
31
|
+
url: URL;
|
|
32
|
+
matchingEnv: ProtectedProxyEnvironment | undefined;
|
|
33
|
+
userToken: string | undefined;
|
|
34
|
+
users: ProtectedProxyProjectUser[] | undefined;
|
|
35
|
+
apiBaseUrl: string;
|
|
36
|
+
logger?: ProxyAccessControlLogger;
|
|
37
|
+
logContext?: Record<string, unknown>;
|
|
38
|
+
isSignedInternalControlPlaneRequest: boolean;
|
|
39
|
+
extractUserIdFromToken?: (token: string, apiBaseUrl: string, log?: ProxyAccessControlLogger) => Promise<string | undefined>;
|
|
40
|
+
}): Promise<ProxyAccessError | null>;
|
|
41
|
+
//# sourceMappingURL=proxy-access-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-access-control.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/proxy-access-control.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC9D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC9E;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA2BD;;;;;GAKG;AACH,wBAAgB,iCAAiC,IAAI,IAAI,CAExD;AAiBD,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,wBAAwB,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAoD7B;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAa1D;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,yBAAyB,EAAE,GAAG,SAAS,EAC9C,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,OAAO,CAGT;AAED,wBAAsB,yBAAyB,CAAC,KAAK,EAAE;IACrD,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,GAAG,CAAC;IACT,WAAW,EAAE,yBAAyB,GAAG,SAAS,CAAC;IACnD,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,KAAK,EAAE,yBAAyB,EAAE,GAAG,SAAS,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,mCAAmC,EAAE,OAAO,CAAC;IAC7C,sBAAsB,CAAC,EAAE,CACvB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,wBAAwB,KAC3B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAClC,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA4DnC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { getEnv } from "../platform/compat/process.js";
|
|
2
|
+
import { resolve as resolveContract } from "../extensions/contracts.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cache the resolved AuthProvider at module scope so the proxy does not pay
|
|
5
|
+
* the registry lookup on every request. The cache is cleared implicitly when
|
|
6
|
+
* `ExtensionLoader.teardownAll()` clears the registry. The next call re-resolves
|
|
7
|
+
* or surfaces the install hint if the extension was removed.
|
|
8
|
+
*/
|
|
9
|
+
let cachedAuthProvider;
|
|
10
|
+
function getAuthProvider() {
|
|
11
|
+
if (cachedAuthProvider)
|
|
12
|
+
return cachedAuthProvider;
|
|
13
|
+
try {
|
|
14
|
+
cachedAuthProvider = resolveContract("AuthProvider");
|
|
15
|
+
return cachedAuthProvider;
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const base = err instanceof Error ? err.message : String(err);
|
|
19
|
+
throw new Error(`${base}\nTo enable JWT verification in the proxy, install ext-auth-jwt ` +
|
|
20
|
+
`(scaffold with \`deno task cli extension init ext-auth-jwt\` or add the ` +
|
|
21
|
+
`npm package @veryfront/ext-auth-jwt).`, { cause: err });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reset the cached AuthProvider. Intended for tests that `register()` a mock
|
|
26
|
+
* after the handler module has been imported.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export function __resetCachedAuthProviderForTests() {
|
|
31
|
+
cachedAuthProvider = undefined;
|
|
32
|
+
}
|
|
33
|
+
function resolveApiJwksUrl(apiBaseUrl, logger) {
|
|
34
|
+
try {
|
|
35
|
+
const normalizedBaseUrl = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
|
|
36
|
+
return new URL(".well-known/jwks.json", normalizedBaseUrl).toString();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
logger?.error("Invalid API base URL for JWKS lookup", error, {
|
|
40
|
+
apiBaseUrl,
|
|
41
|
+
});
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function extractUserIdFromToken(token, apiBaseUrl, log) {
|
|
46
|
+
const auth = getAuthProvider();
|
|
47
|
+
const header = auth.decode(token);
|
|
48
|
+
if (!header) {
|
|
49
|
+
log?.debug("Failed to decode JWT header");
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const algorithm = header.alg;
|
|
53
|
+
if (algorithm === "RS256") {
|
|
54
|
+
const jwksUrl = resolveApiJwksUrl(apiBaseUrl, log);
|
|
55
|
+
if (!jwksUrl)
|
|
56
|
+
return undefined;
|
|
57
|
+
try {
|
|
58
|
+
const payload = await auth.verifyWithJwks(token, jwksUrl, {
|
|
59
|
+
algorithms: ["RS256"],
|
|
60
|
+
});
|
|
61
|
+
return payload.userId;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
log?.debug("RS256 JWT verification failed", {
|
|
65
|
+
error: error instanceof Error ? error.message : String(error),
|
|
66
|
+
});
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (algorithm !== "HS256") {
|
|
71
|
+
log?.debug("Unsupported JWT algorithm", { algorithm: algorithm ?? null });
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const jwtSecret = getEnv("JWT_SECRET");
|
|
75
|
+
if (!jwtSecret) {
|
|
76
|
+
log?.warn("JWT_SECRET not configured - cannot verify user token");
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
// ext-auth-jwt reads JWT_SECRET from the environment when no `secret` was
|
|
81
|
+
// passed to the extension factory; the explicit env check above is kept
|
|
82
|
+
// so callers can warn once before attempting verification.
|
|
83
|
+
const payload = await auth.verify(token, { algorithms: ["HS256"] });
|
|
84
|
+
return payload.userId;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
log?.debug("JWT verification failed", {
|
|
88
|
+
error: error instanceof Error ? error.message : String(error),
|
|
89
|
+
});
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function buildProxyAuthRedirectUrl(url) {
|
|
94
|
+
const safePath = url.pathname.replace(/^\/\/+/, "/");
|
|
95
|
+
let returnPath = safePath + url.search;
|
|
96
|
+
if (!returnPath.startsWith("/") || returnPath.includes("://")) {
|
|
97
|
+
returnPath = "/";
|
|
98
|
+
}
|
|
99
|
+
const isHostedProductionDeployment = url.hostname.endsWith(".production.veryfront.org") ||
|
|
100
|
+
url.hostname.endsWith(".production.veryfront.com");
|
|
101
|
+
const returnTarget = isHostedProductionDeployment ? url.toString() : returnPath;
|
|
102
|
+
return `https://veryfront.com/sign-in?from=${encodeURIComponent(returnTarget)}`;
|
|
103
|
+
}
|
|
104
|
+
export function isProjectMember(users, userId) {
|
|
105
|
+
if (!users || !userId)
|
|
106
|
+
return false;
|
|
107
|
+
return users.some((u) => u.id === userId);
|
|
108
|
+
}
|
|
109
|
+
export async function checkProtectedProxyAccess(input) {
|
|
110
|
+
const { apiBaseUrl, logger, matchingEnv, url, userToken, users, } = input;
|
|
111
|
+
const logContext = input.logContext ?? {};
|
|
112
|
+
if (!matchingEnv?.protected)
|
|
113
|
+
return null;
|
|
114
|
+
if (input.isSignedInternalControlPlaneRequest) {
|
|
115
|
+
logger?.debug("Allowing signed internal control-plane request through protected environment", {
|
|
116
|
+
...logContext,
|
|
117
|
+
environmentName: matchingEnv.name,
|
|
118
|
+
pathname: url.pathname,
|
|
119
|
+
});
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (!userToken) {
|
|
123
|
+
const redirectUrl = buildProxyAuthRedirectUrl(url);
|
|
124
|
+
logger?.info("Protected environment requires authentication", {
|
|
125
|
+
...logContext,
|
|
126
|
+
environmentName: matchingEnv.name,
|
|
127
|
+
redirectUrl,
|
|
128
|
+
});
|
|
129
|
+
return { status: 302, message: "Authentication required", redirectUrl };
|
|
130
|
+
}
|
|
131
|
+
const resolveUserId = input.extractUserIdFromToken ?? extractUserIdFromToken;
|
|
132
|
+
const userId = await resolveUserId(userToken, apiBaseUrl, logger);
|
|
133
|
+
if (!userId) {
|
|
134
|
+
const redirectUrl = buildProxyAuthRedirectUrl(url);
|
|
135
|
+
logger?.info("Could not extract userId from token", {
|
|
136
|
+
...logContext,
|
|
137
|
+
environmentName: matchingEnv.name,
|
|
138
|
+
redirectUrl,
|
|
139
|
+
});
|
|
140
|
+
return { status: 302, message: "Authentication required", redirectUrl };
|
|
141
|
+
}
|
|
142
|
+
if (!isProjectMember(users, userId)) {
|
|
143
|
+
logger?.info("User is not a member of the project", {
|
|
144
|
+
...logContext,
|
|
145
|
+
environmentName: matchingEnv.name,
|
|
146
|
+
userId,
|
|
147
|
+
});
|
|
148
|
+
return { status: 403, message: "Access denied" };
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ParsedDomain } from "../server/utils/domain-parser.js";
|
|
2
|
+
import type { TokenScope } from "./token-manager.js";
|
|
3
|
+
import type { ProxyContext } from "./handler.js";
|
|
4
|
+
export type ProxyErrorSlug = "authentication-failed" | "project-not-found" | "release-not-found";
|
|
5
|
+
export interface ProxyErrorContextBase {
|
|
6
|
+
scope: TokenScope;
|
|
7
|
+
host: string;
|
|
8
|
+
parsedDomain: ParsedDomain;
|
|
9
|
+
}
|
|
10
|
+
export interface ProxyErrorContextOptions {
|
|
11
|
+
status: number;
|
|
12
|
+
message: string;
|
|
13
|
+
token?: string;
|
|
14
|
+
redirectUrl?: string;
|
|
15
|
+
slug?: ProxyErrorSlug;
|
|
16
|
+
}
|
|
17
|
+
export declare function createProxyErrorContext(base: ProxyErrorContextBase, options: ProxyErrorContextOptions): ProxyContext;
|
|
18
|
+
export declare function createProjectNotFoundProxyContext(base: ProxyErrorContextBase, message: "Preview project not found" | "Project not found", token?: string): ProxyContext;
|
|
19
|
+
export declare function createReleaseNotFoundProxyContext(base: ProxyErrorContextBase, token?: string): ProxyContext;
|
|
20
|
+
//# sourceMappingURL=proxy-error-context.d.ts.map
|