veryfront 0.1.74 → 0.1.75

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.
Files changed (86) hide show
  1. package/esm/cli/commands/knowledge/command.d.ts +2 -0
  2. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  3. package/esm/cli/commands/knowledge/command.js +64 -1
  4. package/esm/deno.d.ts +2 -0
  5. package/esm/deno.js +3 -1
  6. package/esm/src/data/data-fetcher.d.ts +11 -1
  7. package/esm/src/data/data-fetcher.d.ts.map +1 -1
  8. package/esm/src/data/data-fetcher.js +5 -2
  9. package/esm/src/data/index.d.ts +1 -1
  10. package/esm/src/data/index.d.ts.map +1 -1
  11. package/esm/src/data/server-data-fetcher.d.ts +14 -1
  12. package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
  13. package/esm/src/data/server-data-fetcher.js +49 -3
  14. package/esm/src/rendering/orchestrator/lifecycle.d.ts +4 -0
  15. package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
  16. package/esm/src/rendering/orchestrator/lifecycle.js +8 -0
  17. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  18. package/esm/src/rendering/orchestrator/pipeline.js +6 -1
  19. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts +26 -1
  20. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts.map +1 -1
  21. package/esm/src/rendering/orchestrator/ssr-orchestrator.js +77 -1
  22. package/esm/src/routing/api/handler.d.ts.map +1 -1
  23. package/esm/src/routing/api/handler.js +6 -2
  24. package/esm/src/routing/api/route-executor.d.ts +8 -2
  25. package/esm/src/routing/api/route-executor.d.ts.map +1 -1
  26. package/esm/src/routing/api/route-executor.js +131 -3
  27. package/esm/src/security/deno-permissions.d.ts +6 -0
  28. package/esm/src/security/deno-permissions.d.ts.map +1 -1
  29. package/esm/src/security/deno-permissions.js +10 -0
  30. package/esm/src/security/sandbox/project-worker.d.ts +61 -0
  31. package/esm/src/security/sandbox/project-worker.d.ts.map +1 -0
  32. package/esm/src/security/sandbox/project-worker.js +318 -0
  33. package/esm/src/security/sandbox/worker-permissions.d.ts +30 -0
  34. package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -0
  35. package/esm/src/security/sandbox/worker-permissions.js +60 -0
  36. package/esm/src/security/sandbox/worker-pool.d.ts +87 -0
  37. package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -0
  38. package/esm/src/security/sandbox/worker-pool.js +356 -0
  39. package/esm/src/security/sandbox/worker-types.d.ts +165 -0
  40. package/esm/src/security/sandbox/worker-types.d.ts.map +1 -0
  41. package/esm/src/security/sandbox/worker-types.js +17 -0
  42. package/esm/src/server/project-env/storage.d.ts +6 -0
  43. package/esm/src/server/project-env/storage.d.ts.map +1 -1
  44. package/esm/src/server/project-env/storage.js +8 -0
  45. package/esm/src/server/runtime-handler/project-isolation.d.ts +5 -0
  46. package/esm/src/server/runtime-handler/project-isolation.d.ts.map +1 -1
  47. package/esm/src/server/runtime-handler/project-isolation.js +44 -0
  48. package/esm/src/server/shared/renderer/memory/pressure.d.ts +7 -0
  49. package/esm/src/server/shared/renderer/memory/pressure.d.ts.map +1 -1
  50. package/esm/src/server/shared/renderer/memory/pressure.js +7 -0
  51. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts +4 -4
  52. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
  53. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +15 -15
  54. package/esm/src/utils/index.d.ts +10 -1
  55. package/esm/src/utils/index.d.ts.map +1 -1
  56. package/esm/src/utils/index.js +9 -1
  57. package/esm/src/utils/logger/index.d.ts +1 -1
  58. package/esm/src/utils/logger/index.d.ts.map +1 -1
  59. package/esm/src/utils/logger/index.js +1 -1
  60. package/esm/src/utils/logger/logger.d.ts +14 -0
  61. package/esm/src/utils/logger/logger.d.ts.map +1 -1
  62. package/esm/src/utils/logger/logger.js +17 -0
  63. package/esm/src/workflow/claude-code/tool.d.ts +5 -5
  64. package/package.json +4 -1
  65. package/src/cli/commands/knowledge/command.ts +76 -1
  66. package/src/deno.js +3 -1
  67. package/src/src/data/data-fetcher.ts +18 -2
  68. package/src/src/data/index.ts +1 -1
  69. package/src/src/data/server-data-fetcher.ts +78 -3
  70. package/src/src/rendering/orchestrator/lifecycle.ts +11 -0
  71. package/src/src/rendering/orchestrator/pipeline.ts +7 -2
  72. package/src/src/rendering/orchestrator/ssr-orchestrator.ts +119 -0
  73. package/src/src/routing/api/handler.ts +16 -3
  74. package/src/src/routing/api/route-executor.ts +222 -1
  75. package/src/src/security/deno-permissions.ts +11 -0
  76. package/src/src/security/sandbox/project-worker.ts +416 -0
  77. package/src/src/security/sandbox/worker-permissions.ts +74 -0
  78. package/src/src/security/sandbox/worker-pool.ts +451 -0
  79. package/src/src/security/sandbox/worker-types.ts +209 -0
  80. package/src/src/server/project-env/storage.ts +9 -0
  81. package/src/src/server/runtime-handler/project-isolation.ts +53 -0
  82. package/src/src/server/shared/renderer/memory/pressure.ts +8 -0
  83. package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +18 -12
  84. package/src/src/utils/index.ts +11 -0
  85. package/src/src/utils/logger/index.ts +1 -0
  86. package/src/src/utils/logger/logger.ts +34 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Worker Isolation Types
3
+ *
4
+ * Shared type definitions for the worker isolation system.
5
+ * Used by both the main process and worker script.
6
+ *
7
+ * @module security/sandbox/worker-types
8
+ */
9
+
10
+ /**
11
+ * Serialized request data that can cross the Worker boundary via postMessage.
12
+ * We cannot send a full Request object (it's not structured-cloneable),
13
+ * so we extract the essential fields.
14
+ */
15
+ export interface SerializedRequest {
16
+ url: string;
17
+ method: string;
18
+ headers: [string, string][];
19
+ body: Uint8Array | null;
20
+ }
21
+
22
+ /**
23
+ * Serialized API context for Pages Router routes.
24
+ */
25
+ export interface SerializedPagesContext {
26
+ url: string;
27
+ method: string;
28
+ headers: [string, string][];
29
+ body: Uint8Array | null;
30
+ params: Record<string, string | string[]>;
31
+ cookies: Record<string, string>;
32
+ }
33
+
34
+ /**
35
+ * Serialized response data that can cross the Worker boundary.
36
+ */
37
+ export interface SerializedResponse {
38
+ status: number;
39
+ statusText: string;
40
+ headers: [string, string][];
41
+ body: Uint8Array | null;
42
+ }
43
+
44
+ /**
45
+ * Serialized error for cross-boundary transport.
46
+ */
47
+ export interface SerializedError {
48
+ message: string;
49
+ name: string;
50
+ stack?: string;
51
+ /** RFC 9457 fields if the error originated from VFError */
52
+ type?: string;
53
+ status?: number;
54
+ detail?: string;
55
+ }
56
+
57
+ /**
58
+ * Serialized DataContext for data fetcher isolation.
59
+ * Request and URL are not structured-cloneable, so we serialize them.
60
+ */
61
+ export interface SerializedDataContext {
62
+ params: Record<string, string | string[]>;
63
+ /** URLSearchParams.toString() */
64
+ query: string;
65
+ request: SerializedRequest;
66
+ /** URL.toString() */
67
+ url: string;
68
+ }
69
+
70
+ /**
71
+ * Serialized DataResult — plain JSON, fully structured-cloneable.
72
+ */
73
+ export interface SerializedDataResult {
74
+ props?: unknown;
75
+ redirect?: { destination: string; permanent?: boolean };
76
+ notFound?: boolean;
77
+ revalidate?: number | false;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Worker Request / Response Protocol
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export type WorkerRequest =
85
+ | ExecuteAppRouteRequest
86
+ | ExecutePagesRouteRequest
87
+ | FetchDataRequest
88
+ | RenderSSRRequest;
89
+
90
+ export interface ExecuteAppRouteRequest {
91
+ type: "execute-app-route";
92
+ id: string;
93
+ modulePath: string;
94
+ method: string;
95
+ request: SerializedRequest;
96
+ params: Record<string, string | string[]>;
97
+ projectDir: string;
98
+ /** Per-project env var overlay for multi-tenant proxy mode */
99
+ projectEnv?: Record<string, string>;
100
+ }
101
+
102
+ export interface ExecutePagesRouteRequest {
103
+ type: "execute-pages-route";
104
+ id: string;
105
+ modulePath: string;
106
+ method: string;
107
+ context: SerializedPagesContext;
108
+ projectDir: string;
109
+ /** Per-project env var overlay for multi-tenant proxy mode */
110
+ projectEnv?: Record<string, string>;
111
+ }
112
+
113
+ export interface FetchDataRequest {
114
+ type: "fetch-data";
115
+ id: string;
116
+ modulePath: string;
117
+ context: SerializedDataContext;
118
+ }
119
+
120
+ export interface RenderSSRRequest {
121
+ type: "render-ssr";
122
+ id: string;
123
+ /** Temp file path for the page component module */
124
+ pageModulePath: string;
125
+ /** Ordered layout module temp paths (innermost → outermost) */
126
+ layoutModulePaths: string[];
127
+ /** Page component props (JSON-serializable) */
128
+ pageProps: Record<string, unknown>;
129
+ /** Layout props keyed by layout index (matching layoutModulePaths order) */
130
+ layoutProps: Record<string, unknown>[];
131
+ /** Rendering delivery mode */
132
+ delivery: "string" | "stream";
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Streaming SSR Protocol
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export interface WorkerStreamChunk {
140
+ type: "stream-chunk";
141
+ id: string;
142
+ chunk: Uint8Array;
143
+ }
144
+
145
+ export interface WorkerStreamEnd {
146
+ type: "stream-end";
147
+ id: string;
148
+ }
149
+
150
+ export type WorkerResponse =
151
+ | WorkerResultResponse
152
+ | WorkerDataResultResponse
153
+ | WorkerSSRResultResponse
154
+ | WorkerErrorResponse;
155
+
156
+ export interface WorkerSSRResultResponse {
157
+ type: "ssr-result";
158
+ id: string;
159
+ html: string;
160
+ }
161
+
162
+ export interface WorkerResultResponse {
163
+ type: "result";
164
+ id: string;
165
+ response: SerializedResponse;
166
+ }
167
+
168
+ export interface WorkerDataResultResponse {
169
+ type: "data-result";
170
+ id: string;
171
+ result: SerializedDataResult;
172
+ }
173
+
174
+ export interface WorkerErrorResponse {
175
+ type: "error";
176
+ id: string;
177
+ error: SerializedError;
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Worker Pool Configuration
182
+ // ---------------------------------------------------------------------------
183
+
184
+ export interface WorkerPoolConfig {
185
+ /** Maximum number of concurrent workers (default: 20) */
186
+ maxPoolSize: number;
187
+ /** Idle timeout before evicting a worker (default: 300_000 = 5 minutes) */
188
+ idleTimeoutMs: number;
189
+ /** Per-request timeout inside the worker (default: 30_000) */
190
+ requestTimeoutMs: number;
191
+ /** Health check interval (default: 30_000) */
192
+ healthCheckIntervalMs: number;
193
+ /** Maximum requests before recycling a worker (default: 1000) */
194
+ maxRequestsPerWorker: number;
195
+ /** Maximum age of a worker in ms before recycling (default: 600_000 = 10 minutes) */
196
+ maxWorkerAgeMs: number;
197
+ /** Per-worker memory budget in MB (default: 64). Workers exceeding this are evicted. */
198
+ memoryBudgetMb: number;
199
+ }
200
+
201
+ export const DEFAULT_WORKER_POOL_CONFIG: WorkerPoolConfig = {
202
+ maxPoolSize: 20,
203
+ idleTimeoutMs: 300_000,
204
+ requestTimeoutMs: 30_000,
205
+ healthCheckIntervalMs: 30_000,
206
+ maxRequestsPerWorker: 1_000,
207
+ maxWorkerAgeMs: 600_000,
208
+ memoryBudgetMb: 64,
209
+ };
@@ -38,6 +38,15 @@ export function isProjectEnvActive(): boolean {
38
38
  return projectEnvStorage.getStore() !== undefined;
39
39
  }
40
40
 
41
+ /**
42
+ * Get a snapshot of the current project env overlay.
43
+ * Returns undefined if no overlay is active.
44
+ * Used to forward env vars to isolated workers in proxy mode.
45
+ */
46
+ export function getProjectEnvSnapshot(): Record<string, string> | undefined {
47
+ return projectEnvStorage.getStore();
48
+ }
49
+
41
50
  // Register on globalThis so process.ts can access without circular imports.
42
51
  // process.ts is low-level (platform/compat), project-env is high-level (server/).
43
52
  (dntShim.dntGlobalThis as Record<string, unknown>).__vfProjectEnvGetter = getProjectEnv;
@@ -1,6 +1,10 @@
1
1
  import * as dntShim from "../../../_dnt.shims.js";
2
2
  import { serverLogger } from "../../utils/index.js";
3
3
  import { getEnvNumber, unrefTimer } from "../../platform/compat/process.js";
4
+ import {
5
+ getWorkerPool,
6
+ isWorkerIsolationEnabled,
7
+ } from "../../security/sandbox/worker-pool.js";
4
8
 
5
9
  const logger = serverLogger.component("project-isolation");
6
10
 
@@ -156,6 +160,46 @@ export class ProjectIsolationManager {
156
160
  });
157
161
  }
158
162
 
163
+ /**
164
+ * Record a worker crash for a project. This counts as a failure
165
+ * toward the circuit breaker threshold and evicts the worker.
166
+ */
167
+ recordWorkerCrash(projectSlug: string | undefined): void {
168
+ if (!projectSlug) return;
169
+
170
+ const state = this.getOrCreateState(projectSlug);
171
+ const now = Date.now();
172
+ state.failures.push(now);
173
+
174
+ state.failures = state.failures.filter(
175
+ (t) => now - t < this.config.failureWindowMs,
176
+ );
177
+
178
+ logger.warn("Worker crash recorded", {
179
+ projectSlug,
180
+ recentFailures: state.failures.length,
181
+ });
182
+
183
+ // Evict the crashed worker from the pool
184
+ if (isWorkerIsolationEnabled()) {
185
+ try {
186
+ getWorkerPool().evictWorker(projectSlug);
187
+ } catch {
188
+ // Pool may not be initialized
189
+ }
190
+ }
191
+
192
+ if (state.failures.length < this.config.circuitBreakerThreshold) return;
193
+
194
+ state.circuitOpenedAt = now;
195
+ logger.error("Circuit opened due to worker crashes", {
196
+ projectSlug,
197
+ recentFailures: state.failures.length,
198
+ threshold: this.config.circuitBreakerThreshold,
199
+ resetAfterMs: this.config.circuitResetTimeMs,
200
+ });
201
+ }
202
+
159
203
  getStats(): Record<
160
204
  string,
161
205
  {
@@ -193,6 +237,15 @@ export class ProjectIsolationManager {
193
237
  shutdown(): void {
194
238
  if (this.cleanupInterval) clearInterval(this.cleanupInterval);
195
239
  this.projects.clear();
240
+
241
+ // Shut down the worker pool if isolation is enabled
242
+ if (isWorkerIsolationEnabled()) {
243
+ try {
244
+ getWorkerPool().shutdown();
245
+ } catch {
246
+ // Pool may not be initialized
247
+ }
248
+ }
196
249
  }
197
250
  }
198
251
 
@@ -59,3 +59,11 @@ export function shouldRejectDueToMemory(): boolean {
59
59
  rendererLog.warn("Rejecting request - memory critical", { heapUsedPercent });
60
60
  return true;
61
61
  }
62
+
63
+ /**
64
+ * Get current memory pressure level for use by the worker pool
65
+ * to decide whether to evict idle workers.
66
+ */
67
+ export function getMemoryPressureLevel(): MemoryPressureLevel {
68
+ return getMemoryPressure().level;
69
+ }
@@ -20,6 +20,7 @@ import {
20
20
  export async function tryReadWithExtensions(
21
21
  fs: ReturnType<typeof createFileSystem>,
22
22
  basePath: string,
23
+ existsFn: (path: string) => Promise<boolean> = exists,
23
24
  ): Promise<{ sourcePath: string; content: string } | null> {
24
25
  // Try all extensions, including .src versions for embedded sources
25
26
  const allExtensions = [
@@ -30,7 +31,7 @@ export async function tryReadWithExtensions(
30
31
  for (const ext of allExtensions) {
31
32
  const sourcePath = basePath + ext;
32
33
  try {
33
- if (await exists(sourcePath)) {
34
+ if (await existsFn(sourcePath)) {
34
35
  const content = await fs.readTextFile(sourcePath);
35
36
  return { sourcePath, content };
36
37
  }
@@ -47,6 +48,7 @@ export async function tryReadWithExtensions(
47
48
  export async function resolveFrameworkFile(
48
49
  vfModulePath: string,
49
50
  fs: ReturnType<typeof createFileSystem>,
51
+ existsFn: (path: string) => Promise<boolean> = exists,
50
52
  ): Promise<{ sourcePath: string; content: string } | null> {
51
53
  const pathWithoutPrefix = vfModulePath
52
54
  .replace(/^\/_vf_modules\//, "")
@@ -78,7 +80,7 @@ export async function resolveFrameworkFile(
78
80
  fullPath: pathWithPrefixDir,
79
81
  });
80
82
 
81
- const withPrefix = await tryReadWithExtensions(fs, pathWithPrefixDir);
83
+ const withPrefix = await tryReadWithExtensions(fs, pathWithPrefixDir, existsFn);
82
84
  if (withPrefix) {
83
85
  logger.debug(`${LOG_PREFIX} Found with prefix`, { sourcePath: withPrefix.sourcePath });
84
86
  return withPrefix;
@@ -93,7 +95,7 @@ export async function resolveFrameworkFile(
93
95
  fullPath: pathWithoutPrefixDir,
94
96
  });
95
97
 
96
- const withoutPrefix = await tryReadWithExtensions(fs, pathWithoutPrefixDir);
98
+ const withoutPrefix = await tryReadWithExtensions(fs, pathWithoutPrefixDir, existsFn);
97
99
  if (withoutPrefix) {
98
100
  logger.debug(`${LOG_PREFIX} Found without prefix`, { sourcePath: withoutPrefix.sourcePath });
99
101
  return withoutPrefix;
@@ -118,7 +120,10 @@ export async function resolveFrameworkFile(
118
120
  * then falls back to regular src/. This matches resolveFrameworkFile's behavior
119
121
  * and ensures consistent path resolution for cycle detection.
120
122
  */
121
- export async function resolveVeryfrontSourcePath(specifier: string): Promise<string | null> {
123
+ export async function resolveVeryfrontSourcePath(
124
+ specifier: string,
125
+ existsFn: (path: string) => Promise<boolean> = exists,
126
+ ): Promise<string | null> {
122
127
  if (!specifier.startsWith("#veryfront/")) return null;
123
128
 
124
129
  const mappedTarget = resolveInternalModuleTarget(specifier);
@@ -143,13 +148,13 @@ export async function resolveVeryfrontSourcePath(specifier: string): Promise<str
143
148
  // Try exact path with .src suffix first (for embedded sources)
144
149
  try {
145
150
  const srcPath = basePath + ".src";
146
- if (await exists(srcPath)) return srcPath;
151
+ if (await existsFn(srcPath)) return srcPath;
147
152
  } catch (_) {
148
153
  /* expected: file may not exist at this path */
149
154
  }
150
155
  // Try exact path
151
156
  try {
152
- if (await exists(basePath)) return basePath;
157
+ if (await existsFn(basePath)) return basePath;
153
158
  } catch (_) {
154
159
  /* expected: file may not exist at this path */
155
160
  }
@@ -166,7 +171,7 @@ export async function resolveVeryfrontSourcePath(specifier: string): Promise<str
166
171
  for (const ext of allExtensions) {
167
172
  const pathWithExt = basePath + ext;
168
173
  try {
169
- if (await exists(pathWithExt)) return pathWithExt;
174
+ if (await existsFn(pathWithExt)) return pathWithExt;
170
175
  } catch (_) {
171
176
  /* expected: file may not exist at this path */
172
177
  }
@@ -176,7 +181,7 @@ export async function resolveVeryfrontSourcePath(specifier: string): Promise<str
176
181
  for (const ext of allExtensions) {
177
182
  const indexPath = join(basePath, "index" + ext);
178
183
  try {
179
- if (await exists(indexPath)) return indexPath;
184
+ if (await existsFn(indexPath)) return indexPath;
180
185
  } catch (_) {
181
186
  /* expected: file may not exist at this path */
182
187
  }
@@ -197,6 +202,7 @@ export async function resolveRelativeFrameworkImport(
197
202
  specifier: string,
198
203
  fromSourcePath: string,
199
204
  _fs: ReturnType<typeof createFileSystem>,
205
+ existsFn: (path: string) => Promise<boolean> = exists,
200
206
  ): Promise<string | null> {
201
207
  const fromDir = fromSourcePath.substring(0, fromSourcePath.lastIndexOf("/"));
202
208
  const parts = fromDir.split("/").filter(Boolean);
@@ -219,7 +225,7 @@ export async function resolveRelativeFrameworkImport(
219
225
  if (/\.(tsx?|jsx?|mjs)$/.test(specifier)) {
220
226
  // Try exact path first
221
227
  try {
222
- if (await exists(basePath)) return basePath;
228
+ if (await existsFn(basePath)) return basePath;
223
229
  } catch (_) {
224
230
  /* expected: file may not exist at this path */
225
231
  }
@@ -227,7 +233,7 @@ export async function resolveRelativeFrameworkImport(
227
233
  // Try with .src suffix for embedded sources
228
234
  try {
229
235
  const srcPath = basePath + ".src";
230
- if (await exists(srcPath)) return srcPath;
236
+ if (await existsFn(srcPath)) return srcPath;
231
237
  } catch (_) {
232
238
  /* expected: file may not exist at this path */
233
239
  }
@@ -245,7 +251,7 @@ export async function resolveRelativeFrameworkImport(
245
251
  for (const ext of allExtensions) {
246
252
  const pathWithExt = basePath + ext;
247
253
  try {
248
- if (await exists(pathWithExt)) return pathWithExt;
254
+ if (await existsFn(pathWithExt)) return pathWithExt;
249
255
  } catch (_) {
250
256
  /* expected: file may not exist at this path */
251
257
  }
@@ -255,7 +261,7 @@ export async function resolveRelativeFrameworkImport(
255
261
  for (const ext of allExtensions) {
256
262
  const indexPath = join(basePath, "index" + ext);
257
263
  try {
258
- if (await exists(indexPath)) return indexPath;
264
+ if (await existsFn(indexPath)) return indexPath;
259
265
  } catch (_) {
260
266
  /* expected: file may not exist at this path */
261
267
  }
@@ -3,7 +3,16 @@
3
3
  * (breakpoints, timeouts, HTTP codes), hashing, memoization, and feature flags.
4
4
  *
5
5
  * @module utils
6
+ *
7
+ * @example Structured logging
8
+ * ```ts
9
+ * import { serverLogger } from "veryfront/utils";
10
+ *
11
+ * serverLogger.info("Booting server", { project_id: "proj_123" });
12
+ * ```
6
13
  */
14
+ import "../../_dnt.polyfills.js";
15
+
7
16
 
8
17
  export {
9
18
  type GlobalWithBun,
@@ -17,11 +26,13 @@ export {
17
26
  export {
18
27
  agentLogger,
19
28
  bundlerLogger,
29
+ createJobUserLogger,
20
30
  logger,
21
31
  refreshLoggerConfig,
22
32
  rendererLogger,
23
33
  serverLogger,
24
34
  } from "./logger/index.js";
35
+ export type { Logger } from "./logger/index.js";
25
36
 
26
37
  export {
27
38
  BREAKPOINT_LG,
@@ -12,6 +12,7 @@ export {
12
12
  agentLogger,
13
13
  bundlerLogger,
14
14
  cliLogger,
15
+ createJobUserLogger,
15
16
  createRequestLogger,
16
17
  getBaseLogger,
17
18
  getDefaultLevel,
@@ -60,6 +60,12 @@ export interface LogEntry {
60
60
  release_id?: string;
61
61
  branch_id?: string;
62
62
  branch_name?: string;
63
+ job_id?: string;
64
+ batch_id?: string;
65
+ job_target?: string;
66
+ task?: string;
67
+ event_kind?: string;
68
+ user_visible?: string;
63
69
  // Duration for timed operations
64
70
  /** @deprecated Use `duration_ms` instead. Kept for Grafana dashboard transition. Planned removal after Grafana dashboard migration is complete. */
65
71
  durationMs?: number;
@@ -314,6 +320,12 @@ class ConsoleLogger implements Logger {
314
320
  extractToEntryField(entry, mergedContext, "release_id", (v) => String(v));
315
321
  extractToEntryField(entry, mergedContext, "branch_id", (v) => String(v));
316
322
  extractToEntryField(entry, mergedContext, "branch_name", (v) => String(v));
323
+ extractToEntryField(entry, mergedContext, "job_id", (v) => String(v));
324
+ extractToEntryField(entry, mergedContext, "batch_id", (v) => String(v));
325
+ extractToEntryField(entry, mergedContext, "job_target", (v) => String(v));
326
+ extractToEntryField(entry, mergedContext, "task", (v) => String(v));
327
+ extractToEntryField(entry, mergedContext, "event_kind", (v) => String(v));
328
+ extractToEntryField(entry, mergedContext, "user_visible", (v) => String(v));
317
329
  extractToEntryField(entry, mergedContext, "duration_ms", (v) => Number(v));
318
330
 
319
331
  // Emit snake_case aliases for camelCase fields (transition period)
@@ -568,3 +580,25 @@ export function createRequestLogger(
568
580
  ): Logger {
569
581
  return baseLogger.child(requestContext);
570
582
  }
583
+
584
+ export function createJobUserLogger(
585
+ baseLogger: Logger,
586
+ jobContext: {
587
+ projectId: string;
588
+ jobId: string;
589
+ task: string;
590
+ batchId?: string | null;
591
+ jobTarget?: string | null;
592
+ eventKind?: string;
593
+ },
594
+ ): Logger {
595
+ return baseLogger.child({
596
+ project_id: jobContext.projectId,
597
+ job_id: jobContext.jobId,
598
+ ...(jobContext.batchId ? { batch_id: jobContext.batchId } : {}),
599
+ ...(jobContext.jobTarget ? { job_target: jobContext.jobTarget } : {}),
600
+ task: jobContext.task,
601
+ event_kind: jobContext.eventKind ?? "job_user_log",
602
+ user_visible: "true",
603
+ });
604
+ }