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,329 @@
|
|
|
1
|
+
// Inline cross-runtime getEnv to avoid dependency on src/platform/compat (not copied in Docker)
|
|
2
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
3
|
+
|
|
4
|
+
function getEnv(key: string): string | undefined {
|
|
5
|
+
// Deno
|
|
6
|
+
if (typeof dntShim.Deno !== "undefined" && dntShim.Deno.env?.get) {
|
|
7
|
+
return dntShim.Deno.env.get(key);
|
|
8
|
+
}
|
|
9
|
+
// Node.js / Bun
|
|
10
|
+
const nodeProcess = (dntShim.dntGlobalThis as { process?: { env?: Record<string, string> } }).process;
|
|
11
|
+
return nodeProcess?.env?.[key];
|
|
12
|
+
}
|
|
13
|
+
import { getTraceContext } from "./tracing.js";
|
|
14
|
+
|
|
15
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
16
|
+
|
|
17
|
+
// Log level configuration
|
|
18
|
+
const MIN_LOG_LEVEL: LogLevel = ((): LogLevel => {
|
|
19
|
+
const level = getEnv("LOG_LEVEL")?.toLowerCase();
|
|
20
|
+
if (level === "debug" || level === "info" || level === "warn" || level === "error") {
|
|
21
|
+
return level;
|
|
22
|
+
}
|
|
23
|
+
return "info"; // Default: suppress debug logs
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
const TAG_WIDTH = 10;
|
|
27
|
+
|
|
28
|
+
const LEVEL_GLYPHS: Record<LogLevel, string> = {
|
|
29
|
+
debug: "·",
|
|
30
|
+
info: "●",
|
|
31
|
+
warn: "▲",
|
|
32
|
+
error: "✖",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const ANSI = {
|
|
36
|
+
reset: "\u001b[0m",
|
|
37
|
+
dim: "\u001b[2m",
|
|
38
|
+
gray: "\u001b[90m",
|
|
39
|
+
red: "\u001b[31m",
|
|
40
|
+
green: "\u001b[32m",
|
|
41
|
+
yellow: "\u001b[33m",
|
|
42
|
+
cyan: "\u001b[36m",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const LEVEL_COLORS: Record<LogLevel, string> = {
|
|
46
|
+
debug: ANSI.gray,
|
|
47
|
+
info: ANSI.green,
|
|
48
|
+
warn: ANSI.yellow,
|
|
49
|
+
error: ANSI.red,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function padTag(tag: string): string {
|
|
53
|
+
if (tag.length >= TAG_WIDTH) return tag.slice(0, TAG_WIDTH);
|
|
54
|
+
return tag.padEnd(TAG_WIDTH, " ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatTimestamp(date: Date = new Date()): string {
|
|
58
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
59
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isTty(): boolean {
|
|
63
|
+
try {
|
|
64
|
+
if (typeof dntShim.Deno !== "undefined" && typeof dntShim.Deno.stdout?.isTerminal === "function") {
|
|
65
|
+
return dntShim.Deno.stdout.isTerminal();
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stdout = (dntShim.dntGlobalThis as { process?: { stdout?: { isTTY?: boolean } } }).process?.stdout;
|
|
72
|
+
return stdout?.isTTY ?? false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shouldUseColor(): boolean {
|
|
76
|
+
const noColor = getEnv("NO_COLOR");
|
|
77
|
+
const forceColor = getEnv("FORCE_COLOR");
|
|
78
|
+
const logColor = getEnv("LOG_COLOR");
|
|
79
|
+
if (forceColor === "0" || logColor === "0") return false;
|
|
80
|
+
if (noColor !== undefined) return false;
|
|
81
|
+
if (getEnv("CI") !== undefined) return false;
|
|
82
|
+
if (forceColor || logColor === "1" || logColor === "true") return true;
|
|
83
|
+
return isTty();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function colorize(text: string, color: string | undefined, enable: boolean): string {
|
|
87
|
+
if (!enable || !color) return text;
|
|
88
|
+
return `${color}${text}${ANSI.reset}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeText(value: string): string {
|
|
92
|
+
return value.replace(/\s+/g, " ");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function truncateText(value: string, maxLength = 80): string {
|
|
96
|
+
if (value.length <= maxLength) return value;
|
|
97
|
+
return `${value.slice(0, maxLength - 1)}…`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatValue(value: unknown): string {
|
|
101
|
+
if (typeof value === "string") {
|
|
102
|
+
const trimmed = normalizeText(value);
|
|
103
|
+
if (/\s/.test(trimmed)) return JSON.stringify(trimmed);
|
|
104
|
+
return trimmed;
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
107
|
+
if (value === null) return "null";
|
|
108
|
+
if (value === undefined) return "undefined";
|
|
109
|
+
let text = "";
|
|
110
|
+
try {
|
|
111
|
+
text = JSON.stringify(value) ?? String(value);
|
|
112
|
+
} catch {
|
|
113
|
+
text = String(value);
|
|
114
|
+
}
|
|
115
|
+
return truncateText(normalizeText(text));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatErrorText(error: LogEntry["error"]): string {
|
|
119
|
+
if (!error) return "";
|
|
120
|
+
const text = `${error.name}: ${error.message}`;
|
|
121
|
+
return truncateText(normalizeText(text), 120);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Prefix width: timestamp(8) + gap(2) + tag(10) + space(1) + glyph(1) + space(1) = 23
|
|
125
|
+
const PREFIX_WIDTH = 23;
|
|
126
|
+
|
|
127
|
+
function formatContextText(
|
|
128
|
+
context: Record<string, unknown>,
|
|
129
|
+
error: LogEntry["error"] | undefined,
|
|
130
|
+
enableColor: boolean,
|
|
131
|
+
): string {
|
|
132
|
+
const entries = Object.entries(context).map(([key, value]) => `${key}=${formatValue(value)}`);
|
|
133
|
+
if (error) {
|
|
134
|
+
entries.push(`err=${formatErrorText(error)}`);
|
|
135
|
+
}
|
|
136
|
+
if (entries.length === 0) return "";
|
|
137
|
+
const text = entries.join(" ");
|
|
138
|
+
// Put context on new line, indented to align with message
|
|
139
|
+
const indent = " ".repeat(PREFIX_WIDTH);
|
|
140
|
+
return `\n${indent}${colorize(text, ANSI.dim, enableColor)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatTextLine(
|
|
144
|
+
level: LogLevel,
|
|
145
|
+
message: string,
|
|
146
|
+
context: Record<string, unknown> | undefined,
|
|
147
|
+
error: LogEntry["error"] | undefined,
|
|
148
|
+
): string {
|
|
149
|
+
const enableColor = shouldUseColor();
|
|
150
|
+
const timestamp = colorize(formatTimestamp(), ANSI.dim, enableColor);
|
|
151
|
+
const tag = colorize(padTag("PROXY"), ANSI.cyan, enableColor);
|
|
152
|
+
const glyph = colorize(LEVEL_GLYPHS[level], LEVEL_COLORS[level], enableColor);
|
|
153
|
+
const contextText = formatContextText(context ?? {}, error, enableColor);
|
|
154
|
+
return `${timestamp} ${tag} ${glyph} ${message}${contextText}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface LogEntry {
|
|
158
|
+
timestamp: string;
|
|
159
|
+
level: LogLevel;
|
|
160
|
+
service: string;
|
|
161
|
+
message: string;
|
|
162
|
+
traceId?: string;
|
|
163
|
+
spanId?: string;
|
|
164
|
+
context?: Record<string, unknown>;
|
|
165
|
+
error?: {
|
|
166
|
+
name: string;
|
|
167
|
+
message: string;
|
|
168
|
+
stack?: string;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isProduction(): boolean {
|
|
173
|
+
return getEnv("NODE_ENV") === "production";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getLogFormat(): "json" | "text" {
|
|
177
|
+
const format = getEnv("LOG_FORMAT");
|
|
178
|
+
if (format === "json" || format === "text") return format;
|
|
179
|
+
return isProduction() ? "json" : "text";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const LOG_LEVEL_ORDER: Record<LogLevel, number> = {
|
|
183
|
+
debug: 0,
|
|
184
|
+
info: 1,
|
|
185
|
+
warn: 2,
|
|
186
|
+
error: 3,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
function serializeError(err: unknown): LogEntry["error"] | undefined {
|
|
190
|
+
if (err instanceof Error) {
|
|
191
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
192
|
+
}
|
|
193
|
+
if (err !== undefined && err !== null) {
|
|
194
|
+
return { name: "UnknownError", message: String(err) };
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
class ProxyLogger {
|
|
200
|
+
private format = getLogFormat();
|
|
201
|
+
|
|
202
|
+
private log(
|
|
203
|
+
level: LogLevel,
|
|
204
|
+
message: string,
|
|
205
|
+
context?: Record<string, unknown>,
|
|
206
|
+
error?: unknown,
|
|
207
|
+
): void {
|
|
208
|
+
// Filter by minimum log level
|
|
209
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[MIN_LOG_LEVEL]) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (this.format === "json") {
|
|
213
|
+
const traceCtx = getTraceContext();
|
|
214
|
+
const entry: LogEntry = {
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
level,
|
|
217
|
+
service: "proxy",
|
|
218
|
+
message,
|
|
219
|
+
...(traceCtx.traceId && { traceId: traceCtx.traceId, spanId: traceCtx.spanId }),
|
|
220
|
+
};
|
|
221
|
+
if (context && Object.keys(context).length > 0) {
|
|
222
|
+
entry.context = context;
|
|
223
|
+
}
|
|
224
|
+
const serializedError = serializeError(error);
|
|
225
|
+
if (serializedError) {
|
|
226
|
+
entry.error = serializedError;
|
|
227
|
+
}
|
|
228
|
+
console.log(JSON.stringify(entry));
|
|
229
|
+
} else {
|
|
230
|
+
const serializedError = serializeError(error);
|
|
231
|
+
console.log(formatTextLine(level, message, context, serializedError));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
debug(message: string, context?: Record<string, unknown>): void {
|
|
236
|
+
this.log("debug", message, context);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
info(message: string, context?: Record<string, unknown>): void {
|
|
240
|
+
this.log("info", message, context);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
warn(message: string, context?: Record<string, unknown>): void {
|
|
244
|
+
this.log("warn", message, context);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
error(message: string, error?: unknown): void;
|
|
248
|
+
error(
|
|
249
|
+
message: string,
|
|
250
|
+
context: Record<string, unknown>,
|
|
251
|
+
error?: unknown,
|
|
252
|
+
): void;
|
|
253
|
+
error(
|
|
254
|
+
message: string,
|
|
255
|
+
contextOrError?: Record<string, unknown> | unknown,
|
|
256
|
+
error?: unknown,
|
|
257
|
+
): void {
|
|
258
|
+
if (contextOrError instanceof Error || error !== undefined) {
|
|
259
|
+
const ctx = contextOrError instanceof Error
|
|
260
|
+
? undefined
|
|
261
|
+
: contextOrError as Record<string, unknown>;
|
|
262
|
+
const err = contextOrError instanceof Error ? contextOrError : error;
|
|
263
|
+
this.log("error", message, ctx, err);
|
|
264
|
+
} else {
|
|
265
|
+
this.log("error", message, contextOrError as Record<string, unknown>);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create a child logger with bound context.
|
|
271
|
+
*/
|
|
272
|
+
child(context: Record<string, unknown>): ChildProxyLogger {
|
|
273
|
+
return new ChildProxyLogger(this, context);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
class ChildProxyLogger {
|
|
278
|
+
constructor(
|
|
279
|
+
private parent: ProxyLogger,
|
|
280
|
+
private boundContext: Record<string, unknown>,
|
|
281
|
+
) {}
|
|
282
|
+
|
|
283
|
+
private merge(ctx?: Record<string, unknown>): Record<string, unknown> {
|
|
284
|
+
return { ...this.boundContext, ...ctx };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
debug(message: string, context?: Record<string, unknown>): void {
|
|
288
|
+
this.parent.debug(message, this.merge(context));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
info(message: string, context?: Record<string, unknown>): void {
|
|
292
|
+
this.parent.info(message, this.merge(context));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
warn(message: string, context?: Record<string, unknown>): void {
|
|
296
|
+
this.parent.warn(message, this.merge(context));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
error(message: string, error?: unknown): void;
|
|
300
|
+
error(
|
|
301
|
+
message: string,
|
|
302
|
+
context: Record<string, unknown>,
|
|
303
|
+
error?: unknown,
|
|
304
|
+
): void;
|
|
305
|
+
error(
|
|
306
|
+
message: string,
|
|
307
|
+
contextOrError?: Record<string, unknown> | unknown,
|
|
308
|
+
error?: unknown,
|
|
309
|
+
): void {
|
|
310
|
+
if (contextOrError instanceof Error || error !== undefined) {
|
|
311
|
+
const ctx = contextOrError instanceof Error
|
|
312
|
+
? this.boundContext
|
|
313
|
+
: this.merge(contextOrError as Record<string, unknown>);
|
|
314
|
+
const err = contextOrError instanceof Error ? contextOrError : error;
|
|
315
|
+
this.parent.error(message, ctx, err);
|
|
316
|
+
} else {
|
|
317
|
+
this.parent.error(
|
|
318
|
+
message,
|
|
319
|
+
this.merge(contextOrError as Record<string, unknown>),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
child(context: Record<string, unknown>): ChildProxyLogger {
|
|
325
|
+
return new ChildProxyLogger(this.parent, this.merge(context));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const proxyLogger = new ProxyLogger();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Client for Veryfront API - client credentials flow.
|
|
3
|
+
*/
|
|
4
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import { injectContext, ProxySpanNames, withSpan } from "./tracing.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
10
|
+
|
|
11
|
+
export interface TokenResponse {
|
|
12
|
+
access_token: string;
|
|
13
|
+
token_type: "Bearer";
|
|
14
|
+
expires_in?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface OAuthTokenConfig {
|
|
18
|
+
apiBaseUrl: string;
|
|
19
|
+
clientId: string;
|
|
20
|
+
clientSecret: string;
|
|
21
|
+
projectSlug?: string;
|
|
22
|
+
customDomain?: string;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function fetchOAuthToken(
|
|
27
|
+
config: OAuthTokenConfig,
|
|
28
|
+
): Promise<TokenResponse> {
|
|
29
|
+
return await withSpan(
|
|
30
|
+
ProxySpanNames.OAUTH_TOKEN_REQUEST,
|
|
31
|
+
async () => {
|
|
32
|
+
const url = `${config.apiBaseUrl}/auth/token`;
|
|
33
|
+
const urlObj = new URL(url);
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeoutId = dntShim.setTimeout(
|
|
36
|
+
() => controller.abort(),
|
|
37
|
+
config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const headers = new dntShim.Headers({ "Content-Type": "application/json" });
|
|
42
|
+
injectContext(headers);
|
|
43
|
+
|
|
44
|
+
const response = await withSpan(
|
|
45
|
+
ProxySpanNames.HTTP_CLIENT_FETCH,
|
|
46
|
+
() =>
|
|
47
|
+
dntShim.fetch(url, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers,
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
grant_type: "client_credentials",
|
|
52
|
+
client_id: config.clientId,
|
|
53
|
+
client_secret: config.clientSecret,
|
|
54
|
+
...(config.projectSlug && { project_slug: config.projectSlug }),
|
|
55
|
+
...(config.customDomain && { custom_domain: config.customDomain }),
|
|
56
|
+
}),
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
}),
|
|
59
|
+
{
|
|
60
|
+
"http.method": "POST",
|
|
61
|
+
"http.url": url,
|
|
62
|
+
"http.host": urlObj.host,
|
|
63
|
+
"oauth.grant_type": "client_credentials",
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
69
|
+
throw new Error(
|
|
70
|
+
`OAuth token request failed: ${response.status} - ${errorText}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return response.json();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`OAuth token request timed out after ${config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(timeoutId);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"oauth.project_slug": config.projectSlug || "",
|
|
88
|
+
"oauth.custom_domain": config.customDomain || "",
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Manager for OAuth Token Caching
|
|
3
|
+
*
|
|
4
|
+
* Manages OAuth tokens with automatic refresh before expiry.
|
|
5
|
+
* Caches tokens per scope (preview/production) and project.
|
|
6
|
+
* Supports pluggable cache backends (memory, Redis).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { fetchOAuthToken, type TokenResponse } from "./oauth-client.js";
|
|
10
|
+
import type { TokenCache, TokenCacheEntry } from "./cache/types.js";
|
|
11
|
+
import { MemoryCache } from "./cache/memory-cache.js";
|
|
12
|
+
import { ProxySpanNames, withSpan } from "./tracing.js";
|
|
13
|
+
|
|
14
|
+
export type TokenScope = "preview" | "production";
|
|
15
|
+
|
|
16
|
+
export interface OAuthConfig {
|
|
17
|
+
apiBaseUrl: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
previewClientId: string;
|
|
21
|
+
previewClientSecret: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TokenManagerOptions {
|
|
25
|
+
cache?: TokenCache;
|
|
26
|
+
refreshBuffer?: number; // ms before expiry to trigger refresh
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class TokenManager {
|
|
30
|
+
private cache: TokenCache;
|
|
31
|
+
private pendingRequests = new Map<string, Promise<string>>();
|
|
32
|
+
private refreshBuffer: number;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private config: OAuthConfig,
|
|
36
|
+
options: TokenManagerOptions = {},
|
|
37
|
+
) {
|
|
38
|
+
this.cache = options.cache ?? new MemoryCache();
|
|
39
|
+
this.refreshBuffer = options.refreshBuffer ?? 2 * 60 * 1000; // 2 minutes before expiry
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a valid token for the given scope and project.
|
|
44
|
+
* Returns cached token if valid, otherwise fetches a new one.
|
|
45
|
+
* Can use either projectSlug or customDomain to identify the project.
|
|
46
|
+
*/
|
|
47
|
+
async getToken(
|
|
48
|
+
scope: TokenScope,
|
|
49
|
+
projectSlug?: string,
|
|
50
|
+
customDomain?: string,
|
|
51
|
+
): Promise<string> {
|
|
52
|
+
return await withSpan(
|
|
53
|
+
ProxySpanNames.PROXY_TOKEN_FETCH,
|
|
54
|
+
async () => {
|
|
55
|
+
const cacheKey = this.getCacheKey(scope, projectSlug || customDomain);
|
|
56
|
+
const cached = await this.cache.get(cacheKey);
|
|
57
|
+
|
|
58
|
+
if (cached && this.isTokenValid(cached)) {
|
|
59
|
+
return cached.token;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Prevent duplicate concurrent requests for the same token
|
|
63
|
+
const pending = this.pendingRequests.get(cacheKey);
|
|
64
|
+
if (pending) {
|
|
65
|
+
return pending;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tokenPromise = this.fetchAndCacheToken(scope, projectSlug, customDomain);
|
|
69
|
+
this.pendingRequests.set(cacheKey, tokenPromise);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
return await tokenPromise;
|
|
73
|
+
} finally {
|
|
74
|
+
this.pendingRequests.delete(cacheKey);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"proxy.token_scope": scope,
|
|
79
|
+
"proxy.project_slug": projectSlug || "",
|
|
80
|
+
"proxy.custom_domain": customDomain || "",
|
|
81
|
+
"proxy.cache_key": this.getCacheKey(scope, projectSlug || customDomain),
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Invalidate a cached token, forcing refresh on next request.
|
|
88
|
+
*/
|
|
89
|
+
async invalidateToken(scope: TokenScope, projectSlug?: string): Promise<void> {
|
|
90
|
+
const cacheKey = this.getCacheKey(scope, projectSlug);
|
|
91
|
+
await this.cache.delete(cacheKey);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Clear all cached tokens.
|
|
96
|
+
*/
|
|
97
|
+
async clearCache(): Promise<void> {
|
|
98
|
+
await this.cache.clear();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get cache statistics.
|
|
103
|
+
*/
|
|
104
|
+
async getStats(): Promise<{ hits: number; misses: number; size: number; type: string }> {
|
|
105
|
+
return await this.cache.stats();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Close the cache connection (for Redis cleanup).
|
|
110
|
+
*/
|
|
111
|
+
async close(): Promise<void> {
|
|
112
|
+
await this.cache.close();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private getCacheKey(scope: TokenScope, projectSlug?: string): string {
|
|
116
|
+
return `${scope}:${projectSlug || "global"}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private isTokenValid(cached: TokenCacheEntry): boolean {
|
|
120
|
+
return Date.now() + this.refreshBuffer < cached.expiresAt;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async fetchAndCacheToken(
|
|
124
|
+
scope: TokenScope,
|
|
125
|
+
projectSlug?: string,
|
|
126
|
+
customDomain?: string,
|
|
127
|
+
): Promise<string> {
|
|
128
|
+
const clientId = scope === "preview" ? this.config.previewClientId : this.config.clientId;
|
|
129
|
+
const clientSecret = scope === "preview"
|
|
130
|
+
? this.config.previewClientSecret
|
|
131
|
+
: this.config.clientSecret;
|
|
132
|
+
|
|
133
|
+
const response = await fetchOAuthToken({
|
|
134
|
+
apiBaseUrl: this.config.apiBaseUrl,
|
|
135
|
+
clientId,
|
|
136
|
+
clientSecret,
|
|
137
|
+
projectSlug,
|
|
138
|
+
customDomain,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const expiresAt = this.calculateExpiresAt(response);
|
|
142
|
+
|
|
143
|
+
await this.cache.set(this.getCacheKey(scope, projectSlug || customDomain), {
|
|
144
|
+
token: response.access_token,
|
|
145
|
+
expiresAt,
|
|
146
|
+
scope,
|
|
147
|
+
projectSlug: projectSlug || customDomain,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return response.access_token;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private calculateExpiresAt(response: TokenResponse): number {
|
|
154
|
+
if (response.expires_in) {
|
|
155
|
+
return Date.now() + response.expires_in * 1000;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Try to decode JWT and get exp claim
|
|
159
|
+
try {
|
|
160
|
+
const [, payload] = response.access_token.split(".");
|
|
161
|
+
if (payload) {
|
|
162
|
+
const decoded = JSON.parse(atob(payload));
|
|
163
|
+
if (decoded.exp) {
|
|
164
|
+
return decoded.exp * 1000;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Fall through to default
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Default to 1 hour
|
|
172
|
+
return Date.now() + 3600 * 1000;
|
|
173
|
+
}
|
|
174
|
+
}
|