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,258 @@
|
|
|
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
|
+
function getEnv(key) {
|
|
4
|
+
// Deno
|
|
5
|
+
if (typeof dntShim.Deno !== "undefined" && dntShim.Deno.env?.get) {
|
|
6
|
+
return dntShim.Deno.env.get(key);
|
|
7
|
+
}
|
|
8
|
+
// Node.js / Bun
|
|
9
|
+
const nodeProcess = dntShim.dntGlobalThis.process;
|
|
10
|
+
return nodeProcess?.env?.[key];
|
|
11
|
+
}
|
|
12
|
+
import { getTraceContext } from "./tracing.js";
|
|
13
|
+
// Log level configuration
|
|
14
|
+
const MIN_LOG_LEVEL = (() => {
|
|
15
|
+
const level = getEnv("LOG_LEVEL")?.toLowerCase();
|
|
16
|
+
if (level === "debug" || level === "info" || level === "warn" || level === "error") {
|
|
17
|
+
return level;
|
|
18
|
+
}
|
|
19
|
+
return "info"; // Default: suppress debug logs
|
|
20
|
+
})();
|
|
21
|
+
const TAG_WIDTH = 10;
|
|
22
|
+
const LEVEL_GLYPHS = {
|
|
23
|
+
debug: "·",
|
|
24
|
+
info: "●",
|
|
25
|
+
warn: "▲",
|
|
26
|
+
error: "✖",
|
|
27
|
+
};
|
|
28
|
+
const ANSI = {
|
|
29
|
+
reset: "\u001b[0m",
|
|
30
|
+
dim: "\u001b[2m",
|
|
31
|
+
gray: "\u001b[90m",
|
|
32
|
+
red: "\u001b[31m",
|
|
33
|
+
green: "\u001b[32m",
|
|
34
|
+
yellow: "\u001b[33m",
|
|
35
|
+
cyan: "\u001b[36m",
|
|
36
|
+
};
|
|
37
|
+
const LEVEL_COLORS = {
|
|
38
|
+
debug: ANSI.gray,
|
|
39
|
+
info: ANSI.green,
|
|
40
|
+
warn: ANSI.yellow,
|
|
41
|
+
error: ANSI.red,
|
|
42
|
+
};
|
|
43
|
+
function padTag(tag) {
|
|
44
|
+
if (tag.length >= TAG_WIDTH)
|
|
45
|
+
return tag.slice(0, TAG_WIDTH);
|
|
46
|
+
return tag.padEnd(TAG_WIDTH, " ");
|
|
47
|
+
}
|
|
48
|
+
function formatTimestamp(date = new Date()) {
|
|
49
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
50
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
51
|
+
}
|
|
52
|
+
function isTty() {
|
|
53
|
+
try {
|
|
54
|
+
if (typeof dntShim.Deno !== "undefined" && typeof dntShim.Deno.stdout?.isTerminal === "function") {
|
|
55
|
+
return dntShim.Deno.stdout.isTerminal();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// ignore
|
|
60
|
+
}
|
|
61
|
+
const stdout = dntShim.dntGlobalThis.process?.stdout;
|
|
62
|
+
return stdout?.isTTY ?? false;
|
|
63
|
+
}
|
|
64
|
+
function shouldUseColor() {
|
|
65
|
+
const noColor = getEnv("NO_COLOR");
|
|
66
|
+
const forceColor = getEnv("FORCE_COLOR");
|
|
67
|
+
const logColor = getEnv("LOG_COLOR");
|
|
68
|
+
if (forceColor === "0" || logColor === "0")
|
|
69
|
+
return false;
|
|
70
|
+
if (noColor !== undefined)
|
|
71
|
+
return false;
|
|
72
|
+
if (getEnv("CI") !== undefined)
|
|
73
|
+
return false;
|
|
74
|
+
if (forceColor || logColor === "1" || logColor === "true")
|
|
75
|
+
return true;
|
|
76
|
+
return isTty();
|
|
77
|
+
}
|
|
78
|
+
function colorize(text, color, enable) {
|
|
79
|
+
if (!enable || !color)
|
|
80
|
+
return text;
|
|
81
|
+
return `${color}${text}${ANSI.reset}`;
|
|
82
|
+
}
|
|
83
|
+
function normalizeText(value) {
|
|
84
|
+
return value.replace(/\s+/g, " ");
|
|
85
|
+
}
|
|
86
|
+
function truncateText(value, maxLength = 80) {
|
|
87
|
+
if (value.length <= maxLength)
|
|
88
|
+
return value;
|
|
89
|
+
return `${value.slice(0, maxLength - 1)}…`;
|
|
90
|
+
}
|
|
91
|
+
function formatValue(value) {
|
|
92
|
+
if (typeof value === "string") {
|
|
93
|
+
const trimmed = normalizeText(value);
|
|
94
|
+
if (/\s/.test(trimmed))
|
|
95
|
+
return JSON.stringify(trimmed);
|
|
96
|
+
return trimmed;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
99
|
+
return String(value);
|
|
100
|
+
if (value === null)
|
|
101
|
+
return "null";
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return "undefined";
|
|
104
|
+
let text = "";
|
|
105
|
+
try {
|
|
106
|
+
text = JSON.stringify(value) ?? String(value);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
text = String(value);
|
|
110
|
+
}
|
|
111
|
+
return truncateText(normalizeText(text));
|
|
112
|
+
}
|
|
113
|
+
function formatErrorText(error) {
|
|
114
|
+
if (!error)
|
|
115
|
+
return "";
|
|
116
|
+
const text = `${error.name}: ${error.message}`;
|
|
117
|
+
return truncateText(normalizeText(text), 120);
|
|
118
|
+
}
|
|
119
|
+
// Prefix width: timestamp(8) + gap(2) + tag(10) + space(1) + glyph(1) + space(1) = 23
|
|
120
|
+
const PREFIX_WIDTH = 23;
|
|
121
|
+
function formatContextText(context, error, enableColor) {
|
|
122
|
+
const entries = Object.entries(context).map(([key, value]) => `${key}=${formatValue(value)}`);
|
|
123
|
+
if (error) {
|
|
124
|
+
entries.push(`err=${formatErrorText(error)}`);
|
|
125
|
+
}
|
|
126
|
+
if (entries.length === 0)
|
|
127
|
+
return "";
|
|
128
|
+
const text = entries.join(" ");
|
|
129
|
+
// Put context on new line, indented to align with message
|
|
130
|
+
const indent = " ".repeat(PREFIX_WIDTH);
|
|
131
|
+
return `\n${indent}${colorize(text, ANSI.dim, enableColor)}`;
|
|
132
|
+
}
|
|
133
|
+
function formatTextLine(level, message, context, error) {
|
|
134
|
+
const enableColor = shouldUseColor();
|
|
135
|
+
const timestamp = colorize(formatTimestamp(), ANSI.dim, enableColor);
|
|
136
|
+
const tag = colorize(padTag("PROXY"), ANSI.cyan, enableColor);
|
|
137
|
+
const glyph = colorize(LEVEL_GLYPHS[level], LEVEL_COLORS[level], enableColor);
|
|
138
|
+
const contextText = formatContextText(context ?? {}, error, enableColor);
|
|
139
|
+
return `${timestamp} ${tag} ${glyph} ${message}${contextText}`;
|
|
140
|
+
}
|
|
141
|
+
function isProduction() {
|
|
142
|
+
return getEnv("NODE_ENV") === "production";
|
|
143
|
+
}
|
|
144
|
+
function getLogFormat() {
|
|
145
|
+
const format = getEnv("LOG_FORMAT");
|
|
146
|
+
if (format === "json" || format === "text")
|
|
147
|
+
return format;
|
|
148
|
+
return isProduction() ? "json" : "text";
|
|
149
|
+
}
|
|
150
|
+
const LOG_LEVEL_ORDER = {
|
|
151
|
+
debug: 0,
|
|
152
|
+
info: 1,
|
|
153
|
+
warn: 2,
|
|
154
|
+
error: 3,
|
|
155
|
+
};
|
|
156
|
+
function serializeError(err) {
|
|
157
|
+
if (err instanceof Error) {
|
|
158
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
159
|
+
}
|
|
160
|
+
if (err !== undefined && err !== null) {
|
|
161
|
+
return { name: "UnknownError", message: String(err) };
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
class ProxyLogger {
|
|
166
|
+
format = getLogFormat();
|
|
167
|
+
log(level, message, context, error) {
|
|
168
|
+
// Filter by minimum log level
|
|
169
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[MIN_LOG_LEVEL]) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (this.format === "json") {
|
|
173
|
+
const traceCtx = getTraceContext();
|
|
174
|
+
const entry = {
|
|
175
|
+
timestamp: new Date().toISOString(),
|
|
176
|
+
level,
|
|
177
|
+
service: "proxy",
|
|
178
|
+
message,
|
|
179
|
+
...(traceCtx.traceId && { traceId: traceCtx.traceId, spanId: traceCtx.spanId }),
|
|
180
|
+
};
|
|
181
|
+
if (context && Object.keys(context).length > 0) {
|
|
182
|
+
entry.context = context;
|
|
183
|
+
}
|
|
184
|
+
const serializedError = serializeError(error);
|
|
185
|
+
if (serializedError) {
|
|
186
|
+
entry.error = serializedError;
|
|
187
|
+
}
|
|
188
|
+
console.log(JSON.stringify(entry));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const serializedError = serializeError(error);
|
|
192
|
+
console.log(formatTextLine(level, message, context, serializedError));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
debug(message, context) {
|
|
196
|
+
this.log("debug", message, context);
|
|
197
|
+
}
|
|
198
|
+
info(message, context) {
|
|
199
|
+
this.log("info", message, context);
|
|
200
|
+
}
|
|
201
|
+
warn(message, context) {
|
|
202
|
+
this.log("warn", message, context);
|
|
203
|
+
}
|
|
204
|
+
error(message, contextOrError, error) {
|
|
205
|
+
if (contextOrError instanceof Error || error !== undefined) {
|
|
206
|
+
const ctx = contextOrError instanceof Error
|
|
207
|
+
? undefined
|
|
208
|
+
: contextOrError;
|
|
209
|
+
const err = contextOrError instanceof Error ? contextOrError : error;
|
|
210
|
+
this.log("error", message, ctx, err);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.log("error", message, contextOrError);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Create a child logger with bound context.
|
|
218
|
+
*/
|
|
219
|
+
child(context) {
|
|
220
|
+
return new ChildProxyLogger(this, context);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
class ChildProxyLogger {
|
|
224
|
+
parent;
|
|
225
|
+
boundContext;
|
|
226
|
+
constructor(parent, boundContext) {
|
|
227
|
+
this.parent = parent;
|
|
228
|
+
this.boundContext = boundContext;
|
|
229
|
+
}
|
|
230
|
+
merge(ctx) {
|
|
231
|
+
return { ...this.boundContext, ...ctx };
|
|
232
|
+
}
|
|
233
|
+
debug(message, context) {
|
|
234
|
+
this.parent.debug(message, this.merge(context));
|
|
235
|
+
}
|
|
236
|
+
info(message, context) {
|
|
237
|
+
this.parent.info(message, this.merge(context));
|
|
238
|
+
}
|
|
239
|
+
warn(message, context) {
|
|
240
|
+
this.parent.warn(message, this.merge(context));
|
|
241
|
+
}
|
|
242
|
+
error(message, contextOrError, error) {
|
|
243
|
+
if (contextOrError instanceof Error || error !== undefined) {
|
|
244
|
+
const ctx = contextOrError instanceof Error
|
|
245
|
+
? this.boundContext
|
|
246
|
+
: this.merge(contextOrError);
|
|
247
|
+
const err = contextOrError instanceof Error ? contextOrError : error;
|
|
248
|
+
this.parent.error(message, ctx, err);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
this.parent.error(message, this.merge(contextOrError));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
child(context) {
|
|
255
|
+
return new ChildProxyLogger(this.parent, this.merge(context));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export const proxyLogger = new ProxyLogger();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface TokenResponse {
|
|
2
|
+
access_token: string;
|
|
3
|
+
token_type: "Bearer";
|
|
4
|
+
expires_in?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface OAuthTokenConfig {
|
|
7
|
+
apiBaseUrl: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
clientSecret: string;
|
|
10
|
+
projectSlug?: string;
|
|
11
|
+
customDomain?: string;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function fetchOAuthToken(config: OAuthTokenConfig): Promise<TokenResponse>;
|
|
15
|
+
//# sourceMappingURL=oauth-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-client.d.ts","sourceRoot":"","sources":["../../src/proxy/oauth-client.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,QAAQ,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,aAAa,CAAC,CA+DxB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Client for Veryfront API - client credentials flow.
|
|
3
|
+
*/
|
|
4
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
5
|
+
import { injectContext, ProxySpanNames, withSpan } from "./tracing.js";
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
7
|
+
export async function fetchOAuthToken(config) {
|
|
8
|
+
return await withSpan(ProxySpanNames.OAUTH_TOKEN_REQUEST, async () => {
|
|
9
|
+
const url = `${config.apiBaseUrl}/auth/token`;
|
|
10
|
+
const urlObj = new URL(url);
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timeoutId = dntShim.setTimeout(() => controller.abort(), config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
13
|
+
try {
|
|
14
|
+
const headers = new dntShim.Headers({ "Content-Type": "application/json" });
|
|
15
|
+
injectContext(headers);
|
|
16
|
+
const response = await withSpan(ProxySpanNames.HTTP_CLIENT_FETCH, () => dntShim.fetch(url, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers,
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
grant_type: "client_credentials",
|
|
21
|
+
client_id: config.clientId,
|
|
22
|
+
client_secret: config.clientSecret,
|
|
23
|
+
...(config.projectSlug && { project_slug: config.projectSlug }),
|
|
24
|
+
...(config.customDomain && { custom_domain: config.customDomain }),
|
|
25
|
+
}),
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
}), {
|
|
28
|
+
"http.method": "POST",
|
|
29
|
+
"http.url": url,
|
|
30
|
+
"http.host": urlObj.host,
|
|
31
|
+
"oauth.grant_type": "client_credentials",
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
35
|
+
throw new Error(`OAuth token request failed: ${response.status} - ${errorText}`);
|
|
36
|
+
}
|
|
37
|
+
return response.json();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
41
|
+
throw new Error(`OAuth token request timed out after ${config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`);
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timeoutId);
|
|
47
|
+
}
|
|
48
|
+
}, {
|
|
49
|
+
"oauth.project_slug": config.projectSlug || "",
|
|
50
|
+
"oauth.custom_domain": config.customDomain || "",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
import type { TokenCache } from "./cache/types.js";
|
|
9
|
+
export type TokenScope = "preview" | "production";
|
|
10
|
+
export interface OAuthConfig {
|
|
11
|
+
apiBaseUrl: string;
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
previewClientId: string;
|
|
15
|
+
previewClientSecret: string;
|
|
16
|
+
}
|
|
17
|
+
export interface TokenManagerOptions {
|
|
18
|
+
cache?: TokenCache;
|
|
19
|
+
refreshBuffer?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class TokenManager {
|
|
22
|
+
private config;
|
|
23
|
+
private cache;
|
|
24
|
+
private pendingRequests;
|
|
25
|
+
private refreshBuffer;
|
|
26
|
+
constructor(config: OAuthConfig, options?: TokenManagerOptions);
|
|
27
|
+
/**
|
|
28
|
+
* Get a valid token for the given scope and project.
|
|
29
|
+
* Returns cached token if valid, otherwise fetches a new one.
|
|
30
|
+
* Can use either projectSlug or customDomain to identify the project.
|
|
31
|
+
*/
|
|
32
|
+
getToken(scope: TokenScope, projectSlug?: string, customDomain?: string): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Invalidate a cached token, forcing refresh on next request.
|
|
35
|
+
*/
|
|
36
|
+
invalidateToken(scope: TokenScope, projectSlug?: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Clear all cached tokens.
|
|
39
|
+
*/
|
|
40
|
+
clearCache(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Get cache statistics.
|
|
43
|
+
*/
|
|
44
|
+
getStats(): Promise<{
|
|
45
|
+
hits: number;
|
|
46
|
+
misses: number;
|
|
47
|
+
size: number;
|
|
48
|
+
type: string;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Close the cache connection (for Redis cleanup).
|
|
52
|
+
*/
|
|
53
|
+
close(): Promise<void>;
|
|
54
|
+
private getCacheKey;
|
|
55
|
+
private isTokenValid;
|
|
56
|
+
private fetchAndCacheToken;
|
|
57
|
+
private calculateExpiresAt;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=token-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/proxy/token-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,kBAAkB,CAAC;AAIpE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,YAAY;IAMrB,OAAO,CAAC,MAAM;IALhB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,eAAe,CAAsC;IAC7D,OAAO,CAAC,aAAa,CAAS;gBAGpB,MAAM,EAAE,WAAW,EAC3B,OAAO,GAAE,mBAAwB;IAMnC;;;;OAIG;IACG,QAAQ,CACZ,KAAK,EAAE,UAAU,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC;IAmClB;;OAEG;IACG,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7E;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAIvF;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,YAAY;YAIN,kBAAkB;IA8BhC,OAAO,CAAC,kBAAkB;CAqB3B"}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
import { fetchOAuthToken } from "./oauth-client.js";
|
|
9
|
+
import { MemoryCache } from "./cache/memory-cache.js";
|
|
10
|
+
import { ProxySpanNames, withSpan } from "./tracing.js";
|
|
11
|
+
export class TokenManager {
|
|
12
|
+
config;
|
|
13
|
+
cache;
|
|
14
|
+
pendingRequests = new Map();
|
|
15
|
+
refreshBuffer;
|
|
16
|
+
constructor(config, options = {}) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.cache = options.cache ?? new MemoryCache();
|
|
19
|
+
this.refreshBuffer = options.refreshBuffer ?? 2 * 60 * 1000; // 2 minutes before expiry
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get a valid token for the given scope and project.
|
|
23
|
+
* Returns cached token if valid, otherwise fetches a new one.
|
|
24
|
+
* Can use either projectSlug or customDomain to identify the project.
|
|
25
|
+
*/
|
|
26
|
+
async getToken(scope, projectSlug, customDomain) {
|
|
27
|
+
return await withSpan(ProxySpanNames.PROXY_TOKEN_FETCH, async () => {
|
|
28
|
+
const cacheKey = this.getCacheKey(scope, projectSlug || customDomain);
|
|
29
|
+
const cached = await this.cache.get(cacheKey);
|
|
30
|
+
if (cached && this.isTokenValid(cached)) {
|
|
31
|
+
return cached.token;
|
|
32
|
+
}
|
|
33
|
+
// Prevent duplicate concurrent requests for the same token
|
|
34
|
+
const pending = this.pendingRequests.get(cacheKey);
|
|
35
|
+
if (pending) {
|
|
36
|
+
return pending;
|
|
37
|
+
}
|
|
38
|
+
const tokenPromise = this.fetchAndCacheToken(scope, projectSlug, customDomain);
|
|
39
|
+
this.pendingRequests.set(cacheKey, tokenPromise);
|
|
40
|
+
try {
|
|
41
|
+
return await tokenPromise;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
this.pendingRequests.delete(cacheKey);
|
|
45
|
+
}
|
|
46
|
+
}, {
|
|
47
|
+
"proxy.token_scope": scope,
|
|
48
|
+
"proxy.project_slug": projectSlug || "",
|
|
49
|
+
"proxy.custom_domain": customDomain || "",
|
|
50
|
+
"proxy.cache_key": this.getCacheKey(scope, projectSlug || customDomain),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Invalidate a cached token, forcing refresh on next request.
|
|
55
|
+
*/
|
|
56
|
+
async invalidateToken(scope, projectSlug) {
|
|
57
|
+
const cacheKey = this.getCacheKey(scope, projectSlug);
|
|
58
|
+
await this.cache.delete(cacheKey);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clear all cached tokens.
|
|
62
|
+
*/
|
|
63
|
+
async clearCache() {
|
|
64
|
+
await this.cache.clear();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get cache statistics.
|
|
68
|
+
*/
|
|
69
|
+
async getStats() {
|
|
70
|
+
return await this.cache.stats();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Close the cache connection (for Redis cleanup).
|
|
74
|
+
*/
|
|
75
|
+
async close() {
|
|
76
|
+
await this.cache.close();
|
|
77
|
+
}
|
|
78
|
+
getCacheKey(scope, projectSlug) {
|
|
79
|
+
return `${scope}:${projectSlug || "global"}`;
|
|
80
|
+
}
|
|
81
|
+
isTokenValid(cached) {
|
|
82
|
+
return Date.now() + this.refreshBuffer < cached.expiresAt;
|
|
83
|
+
}
|
|
84
|
+
async fetchAndCacheToken(scope, projectSlug, customDomain) {
|
|
85
|
+
const clientId = scope === "preview" ? this.config.previewClientId : this.config.clientId;
|
|
86
|
+
const clientSecret = scope === "preview"
|
|
87
|
+
? this.config.previewClientSecret
|
|
88
|
+
: this.config.clientSecret;
|
|
89
|
+
const response = await fetchOAuthToken({
|
|
90
|
+
apiBaseUrl: this.config.apiBaseUrl,
|
|
91
|
+
clientId,
|
|
92
|
+
clientSecret,
|
|
93
|
+
projectSlug,
|
|
94
|
+
customDomain,
|
|
95
|
+
});
|
|
96
|
+
const expiresAt = this.calculateExpiresAt(response);
|
|
97
|
+
await this.cache.set(this.getCacheKey(scope, projectSlug || customDomain), {
|
|
98
|
+
token: response.access_token,
|
|
99
|
+
expiresAt,
|
|
100
|
+
scope,
|
|
101
|
+
projectSlug: projectSlug || customDomain,
|
|
102
|
+
});
|
|
103
|
+
return response.access_token;
|
|
104
|
+
}
|
|
105
|
+
calculateExpiresAt(response) {
|
|
106
|
+
if (response.expires_in) {
|
|
107
|
+
return Date.now() + response.expires_in * 1000;
|
|
108
|
+
}
|
|
109
|
+
// Try to decode JWT and get exp claim
|
|
110
|
+
try {
|
|
111
|
+
const [, payload] = response.access_token.split(".");
|
|
112
|
+
if (payload) {
|
|
113
|
+
const decoded = JSON.parse(atob(payload));
|
|
114
|
+
if (decoded.exp) {
|
|
115
|
+
return decoded.exp * 1000;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Fall through to default
|
|
121
|
+
}
|
|
122
|
+
// Default to 1 hour
|
|
123
|
+
return Date.now() + 3600 * 1000;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry OTLP tracing for proxy.
|
|
3
|
+
* Env: OTEL_TRACES_ENABLED, OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS
|
|
4
|
+
*/
|
|
5
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
6
|
+
import type { Context, Span } from "@opentelemetry/api";
|
|
7
|
+
export declare function initializeOTLP(): Promise<void>;
|
|
8
|
+
export declare function shutdownOTLP(): Promise<void>;
|
|
9
|
+
export declare function isOTLPEnabled(): boolean;
|
|
10
|
+
export declare function extractContext(headers: dntShim.Headers): Context | undefined;
|
|
11
|
+
export declare function injectContext(headers: dntShim.Headers): void;
|
|
12
|
+
export declare function startServerSpan(method: string, path: string, parentContext?: Context): {
|
|
13
|
+
span: Span;
|
|
14
|
+
context: Context;
|
|
15
|
+
} | null;
|
|
16
|
+
export declare function endSpan(span: Span | undefined, statusCode: number, error?: Error): void;
|
|
17
|
+
export declare function withContext<T>(spanContext: Context, fn: () => Promise<T>): Promise<T>;
|
|
18
|
+
export declare function getTraceContext(): {
|
|
19
|
+
traceId?: string;
|
|
20
|
+
spanId?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Span names for proxy tracing.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ProxySpanNames: {
|
|
26
|
+
readonly PROXY_REQUEST: "proxy.request";
|
|
27
|
+
readonly PROXY_PROCESS: "proxy.process";
|
|
28
|
+
readonly PROXY_TOKEN_FETCH: "proxy.token_fetch";
|
|
29
|
+
readonly PROXY_DOMAIN_LOOKUP: "proxy.domain_lookup";
|
|
30
|
+
readonly OAUTH_TOKEN_REQUEST: "oauth.token_request";
|
|
31
|
+
readonly HTTP_CLIENT_FETCH: "http.client.fetch";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Execute an async function within a tracing span.
|
|
35
|
+
* If tracing is disabled, executes the function directly.
|
|
36
|
+
*/
|
|
37
|
+
export declare function withSpan<T>(name: string, fn: () => Promise<T>, attributes?: Record<string, string | number | boolean>): Promise<T>;
|
|
38
|
+
export { initializeOTLP as initializeOTLPWithApis };
|
|
39
|
+
//# sourceMappingURL=tracing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/proxy/tracing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAG5C,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAU,MAAM,oBAAoB,CAAC;AAsDhE,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAiDpD;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CASlD;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,GAAG,SAAS,CAS5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,GAAG,IAAI,CAS5D;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,aAAa,CAAC,EAAE,OAAO,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAKzC;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAUvF;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAGrF;AAED,wBAAgB,eAAe,IAAI;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAMvE;AAED;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;;CAOjB,CAAC;AAEX;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAC9B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GACrD,OAAO,CAAC,CAAC,CAAC,CA6BZ;AAED,OAAO,EAAE,cAAc,IAAI,sBAAsB,EAAE,CAAC"}
|