veryfront 0.1.74 → 0.1.76

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 (122) hide show
  1. package/README.md +2 -0
  2. package/esm/cli/commands/files/command.d.ts +3 -3
  3. package/esm/cli/commands/knowledge/command.d.ts +2 -0
  4. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  5. package/esm/cli/commands/knowledge/command.js +64 -1
  6. package/esm/deno.d.ts +7 -0
  7. package/esm/deno.js +13 -6
  8. package/esm/src/data/data-fetcher.d.ts +11 -1
  9. package/esm/src/data/data-fetcher.d.ts.map +1 -1
  10. package/esm/src/data/data-fetcher.js +5 -2
  11. package/esm/src/data/index.d.ts +1 -1
  12. package/esm/src/data/index.d.ts.map +1 -1
  13. package/esm/src/data/server-data-fetcher.d.ts +14 -1
  14. package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
  15. package/esm/src/data/server-data-fetcher.js +65 -3
  16. package/esm/src/jobs/index.d.ts +34 -0
  17. package/esm/src/jobs/index.d.ts.map +1 -0
  18. package/esm/src/jobs/index.js +33 -0
  19. package/esm/src/jobs/jobs-client.d.ts +134 -0
  20. package/esm/src/jobs/jobs-client.d.ts.map +1 -0
  21. package/esm/src/jobs/jobs-client.js +218 -0
  22. package/esm/src/jobs/schemas.d.ts +1304 -0
  23. package/esm/src/jobs/schemas.d.ts.map +1 -0
  24. package/esm/src/jobs/schemas.js +159 -0
  25. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.d.ts +4 -0
  26. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.d.ts.map +1 -1
  27. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.js +12 -6
  28. package/esm/src/proxy/handler.d.ts.map +1 -1
  29. package/esm/src/proxy/handler.js +21 -21
  30. package/esm/src/rendering/orchestrator/lifecycle.d.ts +4 -0
  31. package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
  32. package/esm/src/rendering/orchestrator/lifecycle.js +8 -0
  33. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  34. package/esm/src/rendering/orchestrator/pipeline.js +6 -1
  35. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts +26 -1
  36. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts.map +1 -1
  37. package/esm/src/rendering/orchestrator/ssr-orchestrator.js +77 -1
  38. package/esm/src/routing/api/handler.d.ts.map +1 -1
  39. package/esm/src/routing/api/handler.js +6 -2
  40. package/esm/src/routing/api/route-executor.d.ts +8 -2
  41. package/esm/src/routing/api/route-executor.d.ts.map +1 -1
  42. package/esm/src/routing/api/route-executor.js +158 -3
  43. package/esm/src/security/deno-permissions.d.ts +7 -1
  44. package/esm/src/security/deno-permissions.d.ts.map +1 -1
  45. package/esm/src/security/deno-permissions.js +12 -1
  46. package/esm/src/security/sandbox/project-worker.d.ts +61 -0
  47. package/esm/src/security/sandbox/project-worker.d.ts.map +1 -0
  48. package/esm/src/security/sandbox/project-worker.js +318 -0
  49. package/esm/src/security/sandbox/worker-permissions.d.ts +30 -0
  50. package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -0
  51. package/esm/src/security/sandbox/worker-permissions.js +63 -0
  52. package/esm/src/security/sandbox/worker-pool.d.ts +87 -0
  53. package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -0
  54. package/esm/src/security/sandbox/worker-pool.js +359 -0
  55. package/esm/src/security/sandbox/worker-types.d.ts +167 -0
  56. package/esm/src/security/sandbox/worker-types.d.ts.map +1 -0
  57. package/esm/src/security/sandbox/worker-types.js +19 -0
  58. package/esm/src/server/handlers/request/internal-tasks-list.handler.d.ts +11 -0
  59. package/esm/src/server/handlers/request/internal-tasks-list.handler.d.ts.map +1 -0
  60. package/esm/src/server/handlers/request/internal-tasks-list.handler.js +72 -0
  61. package/esm/src/server/project-env/storage.d.ts +6 -0
  62. package/esm/src/server/project-env/storage.d.ts.map +1 -1
  63. package/esm/src/server/project-env/storage.js +8 -0
  64. package/esm/src/server/runtime-handler/index.d.ts +1 -1
  65. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  66. package/esm/src/server/runtime-handler/index.js +3 -0
  67. package/esm/src/server/runtime-handler/project-isolation.d.ts +5 -0
  68. package/esm/src/server/runtime-handler/project-isolation.d.ts.map +1 -1
  69. package/esm/src/server/runtime-handler/project-isolation.js +44 -0
  70. package/esm/src/server/shared/renderer/memory/pressure.d.ts +7 -0
  71. package/esm/src/server/shared/renderer/memory/pressure.d.ts.map +1 -1
  72. package/esm/src/server/shared/renderer/memory/pressure.js +7 -0
  73. package/esm/src/task/control-plane.d.ts +105 -0
  74. package/esm/src/task/control-plane.d.ts.map +1 -0
  75. package/esm/src/task/control-plane.js +52 -0
  76. package/esm/src/task/types.d.ts +6 -0
  77. package/esm/src/task/types.d.ts.map +1 -1
  78. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts +4 -4
  79. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
  80. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +15 -15
  81. package/esm/src/utils/index.d.ts +10 -1
  82. package/esm/src/utils/index.d.ts.map +1 -1
  83. package/esm/src/utils/index.js +9 -1
  84. package/esm/src/utils/logger/index.d.ts +1 -1
  85. package/esm/src/utils/logger/index.d.ts.map +1 -1
  86. package/esm/src/utils/logger/index.js +1 -1
  87. package/esm/src/utils/logger/logger.d.ts +14 -0
  88. package/esm/src/utils/logger/logger.d.ts.map +1 -1
  89. package/esm/src/utils/logger/logger.js +17 -0
  90. package/esm/src/workflow/claude-code/tool.d.ts +5 -5
  91. package/package.json +8 -1
  92. package/src/cli/commands/knowledge/command.ts +76 -1
  93. package/src/deno.js +13 -6
  94. package/src/src/data/data-fetcher.ts +18 -2
  95. package/src/src/data/index.ts +1 -1
  96. package/src/src/data/server-data-fetcher.ts +106 -3
  97. package/src/src/jobs/index.ts +85 -0
  98. package/src/src/jobs/jobs-client.ts +503 -0
  99. package/src/src/jobs/schemas.ts +202 -0
  100. package/src/src/platform/adapters/veryfront-api-client/retry-handler.ts +15 -6
  101. package/src/src/proxy/handler.ts +27 -19
  102. package/src/src/rendering/orchestrator/lifecycle.ts +11 -0
  103. package/src/src/rendering/orchestrator/pipeline.ts +7 -2
  104. package/src/src/rendering/orchestrator/ssr-orchestrator.ts +119 -0
  105. package/src/src/routing/api/handler.ts +16 -3
  106. package/src/src/routing/api/route-executor.ts +258 -1
  107. package/src/src/security/deno-permissions.ts +13 -1
  108. package/src/src/security/sandbox/project-worker.ts +416 -0
  109. package/src/src/security/sandbox/worker-permissions.ts +77 -0
  110. package/src/src/security/sandbox/worker-pool.ts +459 -0
  111. package/src/src/security/sandbox/worker-types.ts +212 -0
  112. package/src/src/server/handlers/request/internal-tasks-list.handler.ts +103 -0
  113. package/src/src/server/project-env/storage.ts +9 -0
  114. package/src/src/server/runtime-handler/index.ts +3 -0
  115. package/src/src/server/runtime-handler/project-isolation.ts +53 -0
  116. package/src/src/server/shared/renderer/memory/pressure.ts +8 -0
  117. package/src/src/task/control-plane.ts +76 -0
  118. package/src/src/task/types.ts +6 -0
  119. package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +18 -12
  120. package/src/src/utils/index.ts +11 -0
  121. package/src/src/utils/logger/index.ts +1 -0
  122. package/src/src/utils/logger/logger.ts +34 -0
@@ -0,0 +1,103 @@
1
+ import * as dntShim from "../../../../_dnt.shims.js";
2
+ import { PRIORITY_MEDIUM_API } from "../../../utils/constants/index.js";
3
+ import { ZodError } from "zod";
4
+ import {
5
+ type ControlPlaneTasksListRequest,
6
+ ControlPlaneTasksListRequestSchema,
7
+ defaultRuntimeTaskDiscoveryDeps,
8
+ listRuntimeTasks,
9
+ type RuntimeTaskDiscoveryDeps,
10
+ } from "../../../task/control-plane.js";
11
+ import {
12
+ ControlPlaneRequestError,
13
+ verifyControlPlaneRequest,
14
+ } from "../../../internal-agents/control-plane-auth.js";
15
+ import {
16
+ INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
17
+ InternalAgentRequestBodyTooLargeError,
18
+ readInternalAgentRequestBody,
19
+ } from "../../../internal-agents/request-body.js";
20
+ import { BaseHandler } from "../response/base.js";
21
+ import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
22
+
23
+ export class InternalTasksListHandler extends BaseHandler {
24
+ metadata: HandlerMetadata = {
25
+ name: "InternalTasksListHandler",
26
+ priority: PRIORITY_MEDIUM_API as HandlerPriority,
27
+ patterns: [{ pattern: "/internal/tasks/list", exact: true, method: "POST" }],
28
+ };
29
+
30
+ constructor(
31
+ private readonly deps: RuntimeTaskDiscoveryDeps = defaultRuntimeTaskDiscoveryDeps,
32
+ ) {
33
+ super();
34
+ }
35
+
36
+ async handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
37
+ if (!this.shouldHandle(req, ctx)) {
38
+ return this.continue();
39
+ }
40
+
41
+ return this.withProxyContext(ctx, async () => {
42
+ const builder = this.createResponseBuilder(ctx)
43
+ .withCORS(req, ctx.securityConfig?.cors)
44
+ .withSecurity(ctx.securityConfig ?? undefined, req);
45
+
46
+ try {
47
+ const rawBody = await readInternalAgentRequestBody(
48
+ req,
49
+ INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
50
+ );
51
+ const payload: ControlPlaneTasksListRequest = ControlPlaneTasksListRequestSchema.parse(
52
+ JSON.parse(rawBody),
53
+ );
54
+ const claims = await verifyControlPlaneRequest(req, ctx, rawBody, {
55
+ expectedSubject: payload.requestId,
56
+ expectedSurface: payload.surface,
57
+ });
58
+
59
+ if (
60
+ payload.projectId !== claims.project_id ||
61
+ (ctx.projectId !== undefined && payload.projectId !== ctx.projectId)
62
+ ) {
63
+ this.logWarn("Internal tasks list request body did not match signed claims", {
64
+ projectSlug: ctx.projectSlug,
65
+ projectId: ctx.projectId,
66
+ requestId: payload.requestId,
67
+ signedRequestId: claims.sub,
68
+ surface: payload.surface,
69
+ signedSurface: claims.surface,
70
+ });
71
+ return this.respond(builder.json({ error: "Invalid control-plane signature" }, 401));
72
+ }
73
+
74
+ const response = await listRuntimeTasks(ctx, this.deps);
75
+ return this.respond(builder.json(response, 200));
76
+ } catch (error) {
77
+ if (error instanceof InternalAgentRequestBodyTooLargeError) {
78
+ return this.respond(builder.json({ error: error.message }, error.status));
79
+ }
80
+
81
+ if (error instanceof ControlPlaneRequestError) {
82
+ this.logWarn("Internal tasks list signature verification failed", {
83
+ error: error.message,
84
+ projectSlug: ctx.projectSlug,
85
+ projectId: ctx.projectId,
86
+ });
87
+ return this.respond(builder.json({ error: error.message }, error.status));
88
+ }
89
+
90
+ if (error instanceof SyntaxError || error instanceof ZodError) {
91
+ this.logWarn("Internal tasks list request validation failed", {
92
+ error: error instanceof Error ? error.message : String(error),
93
+ projectSlug: ctx.projectSlug,
94
+ projectId: ctx.projectId,
95
+ });
96
+ return this.respond(builder.json({ error: "Invalid internal tasks request" }, 400));
97
+ }
98
+
99
+ throw error;
100
+ }
101
+ });
102
+ }
103
+ }
@@ -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;
@@ -55,6 +55,7 @@ import { MarkdownPreviewHandler } from "../handlers/preview/markdown-preview.han
55
55
  import { OpenAPIHandler } from "../handlers/request/openapi.handler.js";
56
56
  import { OpenAPIDocsHandler } from "../handlers/request/openapi-docs.handler.js";
57
57
  import { InternalAgentsListHandler } from "../handlers/request/internal-agents-list.handler.js";
58
+ import { InternalTasksListHandler } from "../handlers/request/internal-tasks-list.handler.js";
58
59
  import { AgentStreamHandler } from "../handlers/request/agent-stream.handler.js";
59
60
  import { AgentRunResumeHandler } from "../handlers/request/agent-run-resume.handler.js";
60
61
  import { AgentRunCancelHandler } from "../handlers/request/agent-run-cancel.handler.js";
@@ -131,6 +132,7 @@ export const HANDLER_NAMES = [
131
132
  "OpenAPIHandler",
132
133
  "OpenAPIDocsHandler",
133
134
  "InternalAgentsListHandler",
135
+ "InternalTasksListHandler",
134
136
  "AgentStreamHandler",
135
137
  "AgentRunResumeHandler",
136
138
  "AgentRunCancelHandler",
@@ -185,6 +187,7 @@ const handlerFactories: Record<
185
187
  OpenAPIHandler: () => new OpenAPIHandler(),
186
188
  OpenAPIDocsHandler: () => new OpenAPIDocsHandler(),
187
189
  InternalAgentsListHandler: () => new InternalAgentsListHandler(),
190
+ InternalTasksListHandler: () => new InternalTasksListHandler(),
188
191
  AgentStreamHandler: () => new AgentStreamHandler(),
189
192
  AgentRunResumeHandler: () => new AgentRunResumeHandler(),
190
193
  AgentRunCancelHandler: () => new AgentRunCancelHandler(),
@@ -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
+ }
@@ -0,0 +1,76 @@
1
+ import { ControlPlaneSurfaceSchema } from "../channels/control-plane.js";
2
+ import type { HandlerContext } from "../types/index.js";
3
+ import { z } from "zod";
4
+ import { discoverTasks, type TaskDiscoveryOptions } from "./discovery.js";
5
+
6
+ export const ControlPlaneTasksListRequestSchema = z.object({
7
+ requestId: z.string().min(1),
8
+ projectId: z.string().min(1),
9
+ surface: ControlPlaneSurfaceSchema,
10
+ });
11
+
12
+ const JsonSchemaRecordSchema = z.record(z.unknown());
13
+
14
+ export const RuntimeTaskSchema = z.object({
15
+ id: z.string().min(1),
16
+ name: z.string().min(1),
17
+ description: z.string().nullable(),
18
+ target: z.string().min(1),
19
+ sourcePath: z.string().min(1),
20
+ inputSchema: JsonSchemaRecordSchema.nullable(),
21
+ outputSchema: JsonSchemaRecordSchema.nullable(),
22
+ schedulable: z.boolean(),
23
+ });
24
+
25
+ export const RuntimeTaskListResponseSchema = z.object({
26
+ tasks: z.array(RuntimeTaskSchema),
27
+ });
28
+
29
+ export type ControlPlaneTasksListRequest = z.infer<typeof ControlPlaneTasksListRequestSchema>;
30
+ export type RuntimeTask = z.infer<typeof RuntimeTaskSchema>;
31
+ export type RuntimeTaskListResponse = z.infer<typeof RuntimeTaskListResponseSchema>;
32
+
33
+ export interface RuntimeTaskDiscoveryDeps {
34
+ discoverTasks: (options: TaskDiscoveryOptions) => ReturnType<typeof discoverTasks>;
35
+ }
36
+
37
+ export const defaultRuntimeTaskDiscoveryDeps: RuntimeTaskDiscoveryDeps = {
38
+ discoverTasks,
39
+ };
40
+
41
+ function normalizeJsonSchema(value: unknown): Record<string, unknown> | null {
42
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
43
+ return null;
44
+ }
45
+
46
+ return value as Record<string, unknown>;
47
+ }
48
+
49
+ export async function listRuntimeTasks(
50
+ ctx: HandlerContext,
51
+ deps: RuntimeTaskDiscoveryDeps = defaultRuntimeTaskDiscoveryDeps,
52
+ ): Promise<RuntimeTaskListResponse> {
53
+ const discovery = await deps.discoverTasks({
54
+ projectDir: ctx.projectDir,
55
+ adapter: ctx.adapter,
56
+ config: ctx.config,
57
+ debug: ctx.debug ?? false,
58
+ });
59
+
60
+ const tasks = discovery.tasks
61
+ .map((task) =>
62
+ RuntimeTaskSchema.parse({
63
+ id: task.id,
64
+ name: task.name,
65
+ description: task.definition.description ?? null,
66
+ target: `task:${task.id}`,
67
+ sourcePath: task.filePath,
68
+ inputSchema: normalizeJsonSchema(task.definition.inputSchema),
69
+ outputSchema: normalizeJsonSchema(task.definition.outputSchema),
70
+ schedulable: task.definition.schedulable ?? true,
71
+ })
72
+ )
73
+ .sort((left, right) => left.name.localeCompare(right.name));
74
+
75
+ return RuntimeTaskListResponseSchema.parse({ tasks });
76
+ }
@@ -26,6 +26,12 @@ export interface TaskDefinition {
26
26
  name?: string;
27
27
  /** Task description */
28
28
  description?: string;
29
+ /** Optional JSON-schema-like input contract surfaced in APIs/UIs */
30
+ inputSchema?: Record<string, unknown>;
31
+ /** Optional JSON-schema-like output contract surfaced in APIs/UIs */
32
+ outputSchema?: Record<string, unknown>;
33
+ /** Whether this task can be scheduled via cron jobs */
34
+ schedulable?: boolean;
29
35
  /** The function to execute */
30
36
  run: (ctx: TaskContext) => Promise<unknown> | unknown;
31
37
  }
@@ -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
+ }