veryfront 0.1.810 → 0.1.812
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.d.ts +3 -0
- package/esm/deno.js +5 -2
- 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/factory.d.ts.map +1 -1
- package/esm/src/agent/factory.js +18 -6
- package/esm/src/agent/hosted/veryfront-cloud-agent-service.d.ts.map +1 -1
- package/esm/src/agent/hosted/veryfront-cloud-agent-service.js +1 -0
- package/esm/src/agent/types.d.ts +8 -0
- package/esm/src/agent/types.d.ts.map +1 -1
- package/esm/src/config/schemas/config.schema.d.ts +12 -0
- package/esm/src/config/schemas/config.schema.d.ts.map +1 -1
- package/esm/src/config/schemas/config.schema.js +12 -0
- package/esm/src/discovery/discovery-engine.d.ts.map +1 -1
- package/esm/src/discovery/discovery-engine.js +6 -1
- package/esm/src/discovery/handlers/index.d.ts +1 -0
- package/esm/src/discovery/handlers/index.d.ts.map +1 -1
- package/esm/src/discovery/handlers/index.js +1 -0
- package/esm/src/discovery/handlers/work-handler.d.ts +7 -0
- package/esm/src/discovery/handlers/work-handler.d.ts.map +1 -0
- package/esm/src/discovery/handlers/work-handler.js +19 -0
- package/esm/src/discovery/import-rewriter.d.ts +1 -1
- package/esm/src/discovery/import-rewriter.d.ts.map +1 -1
- package/esm/src/discovery/import-rewriter.js +1 -0
- package/esm/src/discovery/project-discovery-config.d.ts +2 -0
- package/esm/src/discovery/project-discovery-config.d.ts.map +1 -1
- package/esm/src/discovery/project-discovery-config.js +2 -0
- package/esm/src/discovery/transpiler.d.ts.map +1 -1
- package/esm/src/discovery/transpiler.js +2 -0
- package/esm/src/discovery/types.d.ts +3 -0
- package/esm/src/discovery/types.d.ts.map +1 -1
- 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 +1 -0
- package/esm/src/observability/request-profiler.d.ts.map +1 -1
- package/esm/src/observability/request-profiler.js +6 -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 +24 -160
- 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/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/rendering/cache/cache-coordinator.d.ts +3 -0
- package/esm/src/rendering/cache/cache-coordinator.d.ts.map +1 -1
- package/esm/src/rendering/cache/cache-coordinator.js +27 -2
- 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/pipeline.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/pipeline.js +13 -25
- 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/renderer.d.ts.map +1 -1
- package/esm/src/rendering/renderer.js +3 -1
- package/esm/src/rendering/shared/context-aware-cache.d.ts +3 -0
- package/esm/src/rendering/shared/context-aware-cache.d.ts.map +1 -1
- package/esm/src/rendering/shared/context-aware-cache.js +25 -2
- package/esm/src/server/dev-server/server.d.ts.map +1 -1
- package/esm/src/server/dev-server/server.js +4 -1
- 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/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/esm/src/work/factory.d.ts +4 -0
- package/esm/src/work/factory.d.ts.map +1 -0
- package/esm/src/work/factory.js +51 -0
- package/esm/src/work/index.d.ts +27 -0
- package/esm/src/work/index.d.ts.map +1 -0
- package/esm/src/work/index.js +25 -0
- package/esm/src/work/prompt-augmentation.d.ts +6 -0
- package/esm/src/work/prompt-augmentation.d.ts.map +1 -0
- package/esm/src/work/prompt-augmentation.js +30 -0
- package/esm/src/work/registry.d.ts +10 -0
- package/esm/src/work/registry.d.ts.map +1 -0
- package/esm/src/work/registry.js +23 -0
- package/esm/src/work/types.d.ts +30 -0
- package/esm/src/work/types.d.ts.map +1 -0
- package/esm/src/work/types.js +1 -0
- package/package.json +5 -1
package/esm/src/proxy/handler.js
CHANGED
|
@@ -1,46 +1,11 @@
|
|
|
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
|
-
* Cache the resolved AuthProvider at module scope so the proxy does not pay
|
|
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
|
+
export { __resetCachedAuthProviderForTests } from "./proxy-access-control.js";
|
|
44
9
|
export const INTERNAL_PROXY_HEADERS = [
|
|
45
10
|
"x-token",
|
|
46
11
|
"x-project-slug",
|
|
@@ -75,18 +40,6 @@ function isSignedInternalControlPlaneRequest(req, url) {
|
|
|
75
40
|
}
|
|
76
41
|
return !!req.headers.get("x-token");
|
|
77
42
|
}
|
|
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
43
|
async function lookupProjectByDomain(domain, apiBaseUrl, token, logger) {
|
|
91
44
|
return withSpan(ProxySpanNames.PROXY_DOMAIN_LOOKUP, async () => {
|
|
92
45
|
const domainWithoutPort = domain.replace(/:\d+$/, "");
|
|
@@ -141,59 +94,6 @@ function parseStatusFromError(error) {
|
|
|
141
94
|
const match = message.match(/failed: (\d+)/);
|
|
142
95
|
return match ? Number(match[1]) : null;
|
|
143
96
|
}
|
|
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
97
|
export function createProxyHandler(options) {
|
|
198
98
|
const { config, cache, logger } = options;
|
|
199
99
|
const localProjects = config.localProjects ?? {};
|
|
@@ -227,71 +127,25 @@ export function createProxyHandler(options) {
|
|
|
227
127
|
error: { status, message, redirectUrl, slug },
|
|
228
128
|
};
|
|
229
129
|
}
|
|
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
130
|
function makeProjectNotFoundContext(base, message, token) {
|
|
247
131
|
return makeErrorContext(base, 404, message, token, undefined, "project-not-found");
|
|
248
132
|
}
|
|
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
133
|
async function resolveReleaseAndProtection(req, url, token, userToken, lookupKey, envMatcher, logContext) {
|
|
290
134
|
const lookupResult = await lookupProjectByDomain(lookupKey, config.apiBaseUrl, token, logger);
|
|
291
135
|
if (!lookupResult)
|
|
292
136
|
return { projectId: undefined, releaseId: undefined };
|
|
293
137
|
const matchingEnv = lookupResult.environments?.find(envMatcher);
|
|
294
|
-
const protectionError = await
|
|
138
|
+
const protectionError = await checkProtectedProxyAccess({
|
|
139
|
+
req,
|
|
140
|
+
url,
|
|
141
|
+
matchingEnv,
|
|
142
|
+
userToken,
|
|
143
|
+
users: lookupResult.users,
|
|
144
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
145
|
+
logger,
|
|
146
|
+
logContext,
|
|
147
|
+
isSignedInternalControlPlaneRequest: isSignedInternalControlPlaneRequest(req, url),
|
|
148
|
+
});
|
|
295
149
|
if (protectionError)
|
|
296
150
|
return { error: protectionError };
|
|
297
151
|
return {
|
|
@@ -398,7 +252,17 @@ export function createProxyHandler(options) {
|
|
|
398
252
|
const matchingEnv = lookupResult.environments?.find((env) => env.domains?.some((d) => d.toLowerCase() === normalizedHost));
|
|
399
253
|
releaseId = matchingEnv?.active_release_id ?? undefined;
|
|
400
254
|
environmentId = matchingEnv?.id;
|
|
401
|
-
const protectionError = await
|
|
255
|
+
const protectionError = await checkProtectedProxyAccess({
|
|
256
|
+
req,
|
|
257
|
+
url,
|
|
258
|
+
matchingEnv,
|
|
259
|
+
userToken,
|
|
260
|
+
users: lookupResult.users,
|
|
261
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
262
|
+
logger,
|
|
263
|
+
logContext: { domain: host },
|
|
264
|
+
isSignedInternalControlPlaneRequest: isSignedInternalControlPlaneRequest(req, url),
|
|
265
|
+
});
|
|
402
266
|
if (protectionError) {
|
|
403
267
|
return makeErrorContext(base, protectionError.status, protectionError.message, token, protectionError.redirectUrl);
|
|
404
268
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-composer.d.ts","sourceRoot":"","sources":["../../../../../../../src/src/react/components/chat/chat/composition/chat-composer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,KAAK,MAAM,kCAAkC,CAAC;AAI1D,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,yBAAyB,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAEvE,uCAAuC;AACvC,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KAAK,IAAI,CAAC;IACjF,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC;IAGlB,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAGxC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrC,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,kBAAkB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAG1C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IAEzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,4BAA4B;AAC5B,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"chat-composer.d.ts","sourceRoot":"","sources":["../../../../../../../src/src/react/components/chat/chat/composition/chat-composer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,KAAK,MAAM,kCAAkC,CAAC;AAI1D,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,yBAAyB,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAEvE,uCAAuC;AACvC,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KAAK,IAAI,CAAC;IACjF,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC;IAGlB,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAGxC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrC,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,kBAAkB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAG1C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IAEzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,4BAA4B;AAC5B,eAAO,MAAM,YAAY,0FA4MxB,CAAC"}
|
|
@@ -14,6 +14,8 @@ import { downloadMarkdown } from "../utils/export.js";
|
|
|
14
14
|
export const ChatComposer = React.forwardRef(function ChatComposer({ input, onChange, onSubmit, isLoading, placeholder = "Type a message...", theme, stop, onVoice, isListening = false, transcript, models, model, onModelChange, onAttach, onSelectAttachment, attachAccept, attachments, onRemoveAttachment, showExport = false, messages, className, children, }, ref) {
|
|
15
15
|
const fileInputRef = React.useRef(null);
|
|
16
16
|
const [attachmentMenuOpen, setAttachmentMenuOpen] = React.useState(false);
|
|
17
|
+
const inputPlaceholder = isListening ? "Listening..." : placeholder;
|
|
18
|
+
const inputLabel = inputPlaceholder || "Message";
|
|
17
19
|
return (React.createElement("div", { ref: ref, className: cn("flex-shrink-0 pb-6 pt-2", className) },
|
|
18
20
|
React.createElement("div", { className: "mx-auto w-full max-w-3xl px-4" },
|
|
19
21
|
children && (React.createElement("div", { className: "flex flex-wrap items-center gap-1.5 pb-3" }, children)),
|
|
@@ -57,7 +59,7 @@ export const ChatComposer = React.forwardRef(function ChatComposer({ input, onCh
|
|
|
57
59
|
setAttachmentMenuOpen(false);
|
|
58
60
|
onSelectAttachment();
|
|
59
61
|
} }, "Select document")))))),
|
|
60
|
-
React.createElement(InputBox, { value: isListening ? transcript || input : input, onChange: onChange, onSubmit: () => onSubmit?.(), placeholder:
|
|
62
|
+
React.createElement(InputBox, { value: isListening ? transcript || input : input, onChange: onChange, onSubmit: () => onSubmit?.(), placeholder: inputPlaceholder, "aria-label": inputLabel, disabled: isLoading || isListening, multiline: true, className: cn("min-h-9 min-w-0 flex-1 py-2 text-[15px] leading-normal text-[var(--foreground)] placeholder:text-[var(--input-placeholder)]", theme?.input) }),
|
|
61
63
|
React.createElement("div", { className: "flex items-center gap-2" },
|
|
62
64
|
models && models.length > 0 && onModelChange && (React.createElement(ModelSelector, { models: models, value: model, onChange: onModelChange, disabled: isLoading })),
|
|
63
65
|
showExport && messages && messages.length > 0 && (React.createElement("button", { type: "button", onClick: () => downloadMarkdown(messages), className: "size-9 flex items-center justify-center rounded-full text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--foreground)]/5 transition-colors shrink-0", "aria-label": "Export conversation", title: "Export as Markdown" },
|
|
@@ -23,11 +23,14 @@ export interface CacheCoordinatorOptions {
|
|
|
23
23
|
*/
|
|
24
24
|
contentSourceId?: string;
|
|
25
25
|
}
|
|
26
|
+
export type CacheLookupStatus = "hit" | "miss" | "expired";
|
|
26
27
|
export interface CacheLookupResult {
|
|
27
28
|
cachedResult?: RenderResult;
|
|
28
29
|
depAwareSlug: string;
|
|
29
30
|
moduleCacheKey: string;
|
|
30
31
|
cachedModule?: RenderResult["pageModule"];
|
|
32
|
+
cacheStatus: CacheLookupStatus;
|
|
33
|
+
lookupDurationMs: number;
|
|
31
34
|
}
|
|
32
35
|
export declare class CacheCoordinator {
|
|
33
36
|
private store;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-coordinator.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/cache/cache-coordinator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAoB,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-coordinator.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/cache/cache-coordinator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAoB,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAQnF,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAE3D,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC;IAC1C,WAAW,EAAE,iBAAiB,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwB;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,OAAO,GAAE,uBAA4B;IA0BjD;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAKrB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0CvE,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B7E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAIzB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5C;;;OAGG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAShC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,aAAa;CAkBtB"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { rendererLogger as logger } from "../../utils/index.js";
|
|
2
2
|
import { MemoryCacheStore } from "./stores/index.js";
|
|
3
3
|
import { withSpan } from "../../observability/tracing/otlp-setup.js";
|
|
4
|
+
import { markRequestProfilePhase } from "../../observability/request-profiler.js";
|
|
5
|
+
import { metrics } from "../../observability/simple-metrics/index.js";
|
|
4
6
|
/** Default TTL for cache entries (5 minutes) */
|
|
5
7
|
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1_000;
|
|
6
8
|
export class CacheCoordinator {
|
|
@@ -27,6 +29,7 @@ export class CacheCoordinator {
|
|
|
27
29
|
new MemoryCacheStore({
|
|
28
30
|
maxEntries: options.memory?.maxEntries,
|
|
29
31
|
ttlMs: options.memory?.ttlMs ?? this.ttlMs,
|
|
32
|
+
enforceStoreTtl: false,
|
|
30
33
|
});
|
|
31
34
|
}
|
|
32
35
|
/**
|
|
@@ -41,19 +44,33 @@ export class CacheCoordinator {
|
|
|
41
44
|
checkCache(slug, cacheKey) {
|
|
42
45
|
const key = this.buildCacheKey(slug, cacheKey);
|
|
43
46
|
return withSpan("cache.checkCache", async () => {
|
|
47
|
+
const lookupStart = performance.now();
|
|
44
48
|
const cached = await this.store.get(key);
|
|
45
49
|
if (!cached) {
|
|
46
|
-
|
|
50
|
+
const lookupDurationMs = roundDurationMs(performance.now() - lookupStart);
|
|
51
|
+
recordCacheLookup("miss", lookupDurationMs);
|
|
52
|
+
return { depAwareSlug: slug, moduleCacheKey: key, cacheStatus: "miss", lookupDurationMs };
|
|
47
53
|
}
|
|
48
54
|
if (this.isExpired(cached)) {
|
|
49
55
|
await this.store.delete(key);
|
|
50
|
-
|
|
56
|
+
const lookupDurationMs = roundDurationMs(performance.now() - lookupStart);
|
|
57
|
+
recordCacheLookup("expired", lookupDurationMs);
|
|
58
|
+
return {
|
|
59
|
+
depAwareSlug: slug,
|
|
60
|
+
moduleCacheKey: key,
|
|
61
|
+
cacheStatus: "expired",
|
|
62
|
+
lookupDurationMs,
|
|
63
|
+
};
|
|
51
64
|
}
|
|
65
|
+
const lookupDurationMs = roundDurationMs(performance.now() - lookupStart);
|
|
66
|
+
recordCacheLookup("hit", lookupDurationMs);
|
|
52
67
|
return {
|
|
53
68
|
cachedResult: this.hydrateResult(cached),
|
|
54
69
|
depAwareSlug: slug,
|
|
55
70
|
moduleCacheKey: key,
|
|
56
71
|
cachedModule: cached.result.pageModule,
|
|
72
|
+
cacheStatus: "hit",
|
|
73
|
+
lookupDurationMs,
|
|
57
74
|
};
|
|
58
75
|
}, { "cache.slug": slug, "cache.key": key, "cache.projectId": this.projectId ?? "unknown" });
|
|
59
76
|
}
|
|
@@ -128,3 +145,11 @@ export class CacheCoordinator {
|
|
|
128
145
|
};
|
|
129
146
|
}
|
|
130
147
|
}
|
|
148
|
+
function roundDurationMs(value) {
|
|
149
|
+
return Math.round(value * 100) / 100;
|
|
150
|
+
}
|
|
151
|
+
function recordCacheLookup(status, durationMs) {
|
|
152
|
+
markRequestProfilePhase("render.cache_lookup", durationMs);
|
|
153
|
+
markRequestProfilePhase(`render.cache_${status}`);
|
|
154
|
+
metrics.recordCacheGet(status === "hit");
|
|
155
|
+
}
|
|
@@ -10,6 +10,8 @@ interface CacheLike<K, V> {
|
|
|
10
10
|
export interface MemoryCacheStoreOptions {
|
|
11
11
|
maxEntries?: number;
|
|
12
12
|
ttlMs?: number;
|
|
13
|
+
/** Disable store-level TTL when callers classify payload expiration themselves. */
|
|
14
|
+
enforceStoreTtl?: boolean;
|
|
13
15
|
/** Optional cache implementation for testing */
|
|
14
16
|
cache?: CacheLike<string, CachePayload>;
|
|
15
17
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../../../../src/src/rendering/cache/stores/memory-store.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI7E,UAAU,SAAS,CAAC,CAAC,EAAE,CAAC;IACtB,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC;IACrB,IAAI,IAAI,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,CAAC,IAAI,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,KAAK,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACzC;AAED,qBAAa,gBAAiB,YAAW,UAAU;IACjD,OAAO,CAAC,KAAK,CAAkC;gBAEnC,OAAO,GAAE,uBAA4B;
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../../../../src/src/rendering/cache/stores/memory-store.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI7E,UAAU,SAAS,CAAC,CAAC,EAAE,CAAC;IACtB,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC;IACrB,IAAI,IAAI,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,CAAC,IAAI,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,gDAAgD;IAChD,KAAK,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACzC;AAED,qBAAa,gBAAiB,YAAW,UAAU;IACjD,OAAO,CAAC,KAAK,CAAkC;gBAEnC,OAAO,GAAE,uBAA4B;IAcjD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAInD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAa/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAKxB,IAAI,IAAI,MAAM;IAMd,QAAQ,IAAI,eAAe;CAG5B"}
|
|
@@ -18,9 +18,10 @@ export class MemoryCacheStore {
|
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
const disableIntervals = isLruIntervalDisabled();
|
|
21
|
+
const enforceStoreTtl = options.enforceStoreTtl ?? true;
|
|
21
22
|
this.cache = new LRUCache({
|
|
22
23
|
maxEntries: options.maxEntries ?? DEFAULT_MAX_ENTRIES,
|
|
23
|
-
ttlMs: disableIntervals ? undefined : options.ttlMs,
|
|
24
|
+
ttlMs: disableIntervals || !enforceStoreTtl ? undefined : options.ttlMs,
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
get(key) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/orchestrator/lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AASrD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AASxD,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,oBAAoB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,gBAAgB,CAAC;CACjE;AAED,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,cAAc,EAAE,mBAAmB,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,eAAe,EAAE,eAAe,CAAC;IACjC,eAAe,EAAE,eAAe,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,eAAe,CAAC;CAClC;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,CAAmB;IACpC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,eAAe,CAAC,CAAgD;gBAE5D,OAAO,EAAE,gBAAgB;IAS/B,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/orchestrator/lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AASrD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AASxD,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,oBAAoB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,gBAAgB,CAAC;CACjE;AAED,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,cAAc,EAAE,mBAAmB,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,eAAe,EAAE,eAAe,CAAC;IACjC,eAAe,EAAE,eAAe,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,eAAe,CAAC;CAClC;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,CAAmB;IACpC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,eAAe,CAAC,CAAgD;gBAE5D,OAAO,EAAE,gBAAgB;IAS/B,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAsJ7C,gBAAgB,CACd,UAAU,EAAE,CACV,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,QAAQ,CAAC,EAAE,MAAM,KACd,OAAO,CAAC,SAAS,CAAC,GACtB,IAAI;IAcP,WAAW,IAAI,gBAAgB;IAazB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC;IAc3C,cAAc,IAAI,IAAI;IAWtB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAS5B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/orchestrator/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAsBH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/orchestrator/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAsBH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA6ChF,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACxE,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,wBAAwB,CAAC;IAC3C,YAAY,EAAE,YAAY,CAAC;IAC3B,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,eAAe,EAAE,eAAe,CAAC;IACjC,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE,aAAa,GAAG,YAAY,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,OAAO,qBAAqB,EAAE,sBAAsB,CAAC;CAC1E;AA0BD,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,kBAAkB,CAAqB;gBAEnC,MAAM,EAAE,oBAAoB;IAaxC;;;OAGG;IACH,gBAAgB,IAAI,IAAI;IAKxB,OAAO,CAAC,UAAU;YAIJ,0BAA0B;IAaxC;;;;;;;;;OASG;YACW,qBAAqB;IAyDnC;;;OAGG;YACW,mBAAmB;IAiGjC,OAAO,CAAC,uBAAuB;IAkCzB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IA8O9E,+EAA+E;IACzE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAqFzE,kBAAkB;YA2ClB,cAAc;IAW5B,OAAO,CAAC,uBAAuB;YAYjB,kBAAkB;IA8EhC,OAAO,CAAC,kBAAkB;YAMZ,uBAAuB;IAmBrC;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;CAetB"}
|