veryfront 0.1.216 → 0.1.217

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 (160) hide show
  1. package/esm/cli/commands/generate/integration-generator-helpers.d.ts +35 -0
  2. package/esm/cli/commands/generate/integration-generator-helpers.d.ts.map +1 -0
  3. package/esm/cli/commands/generate/integration-generator-helpers.js +123 -0
  4. package/esm/cli/commands/generate/integration-generator.d.ts +1 -2
  5. package/esm/cli/commands/generate/integration-generator.d.ts.map +1 -1
  6. package/esm/cli/commands/generate/integration-generator.js +1 -123
  7. package/esm/cli/commands/knowledge/command-helpers.d.ts +48 -0
  8. package/esm/cli/commands/knowledge/command-helpers.d.ts.map +1 -0
  9. package/esm/cli/commands/knowledge/command-helpers.js +216 -0
  10. package/esm/cli/commands/knowledge/command.d.ts +11 -16
  11. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  12. package/esm/cli/commands/knowledge/command.js +25 -212
  13. package/esm/cli/mcp/remote-file-tool-helpers.d.ts +5 -0
  14. package/esm/cli/mcp/remote-file-tool-helpers.d.ts.map +1 -0
  15. package/esm/cli/mcp/remote-file-tool-helpers.js +22 -0
  16. package/esm/cli/mcp/remote-file-tools.d.ts.map +1 -1
  17. package/esm/cli/mcp/remote-file-tools.js +1 -22
  18. package/esm/cli/templates/integration-loader-helpers.d.ts +8 -0
  19. package/esm/cli/templates/integration-loader-helpers.d.ts.map +1 -0
  20. package/esm/cli/templates/integration-loader-helpers.js +29 -0
  21. package/esm/cli/templates/integration-loader.d.ts.map +1 -1
  22. package/esm/cli/templates/integration-loader.js +5 -21
  23. package/esm/cli/templates/manifest.d.ts +1 -0
  24. package/esm/cli/templates/manifest.js +2 -1
  25. package/esm/deno.js +2 -2
  26. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  27. package/esm/src/agent/runtime/index.js +48 -90
  28. package/esm/src/chat/ag-ui-helpers.d.ts +17 -0
  29. package/esm/src/chat/ag-ui-helpers.d.ts.map +1 -0
  30. package/esm/src/chat/ag-ui-helpers.js +111 -0
  31. package/esm/src/chat/ag-ui.d.ts.map +1 -1
  32. package/esm/src/chat/ag-ui.js +1 -111
  33. package/esm/src/config/schemas/config.schema.d.ts +1 -0
  34. package/esm/src/config/schemas/config.schema.d.ts.map +1 -1
  35. package/esm/src/config/schemas/config.schema.js +11 -0
  36. package/esm/src/config/schemas/index.d.ts +11 -1
  37. package/esm/src/config/schemas/index.d.ts.map +1 -1
  38. package/esm/src/errors/error-registry-helpers.d.ts +6 -0
  39. package/esm/src/errors/error-registry-helpers.d.ts.map +1 -0
  40. package/esm/src/errors/error-registry-helpers.js +9 -0
  41. package/esm/src/errors/error-registry.d.ts.map +1 -1
  42. package/esm/src/errors/error-registry.js +4 -3
  43. package/esm/src/extensions/factory-loader.d.ts +29 -0
  44. package/esm/src/extensions/factory-loader.d.ts.map +1 -0
  45. package/esm/src/extensions/factory-loader.js +63 -0
  46. package/esm/src/extensions/index.d.ts +16 -0
  47. package/esm/src/extensions/index.d.ts.map +1 -1
  48. package/esm/src/extensions/index.js +16 -0
  49. package/esm/src/extensions/orchestrate.d.ts +54 -0
  50. package/esm/src/extensions/orchestrate.d.ts.map +1 -0
  51. package/esm/src/extensions/orchestrate.js +116 -0
  52. package/esm/src/html/styles-builder/plugin-loader.d.ts.map +1 -1
  53. package/esm/src/html/styles-builder/plugin-loader.js +23 -0
  54. package/esm/src/html/styles-builder/tailwind-plugin-allowlist.d.ts +29 -0
  55. package/esm/src/html/styles-builder/tailwind-plugin-allowlist.d.ts.map +1 -0
  56. package/esm/src/html/styles-builder/tailwind-plugin-allowlist.js +48 -0
  57. package/esm/src/modules/react-loader/ssr-module-loader/loader-helpers.d.ts +15 -0
  58. package/esm/src/modules/react-loader/ssr-module-loader/loader-helpers.d.ts.map +1 -0
  59. package/esm/src/modules/react-loader/ssr-module-loader/loader-helpers.js +22 -0
  60. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts +0 -2
  61. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  62. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +4 -24
  63. package/esm/src/platform/adapters/fs/veryfront/adapter-helpers.d.ts +19 -0
  64. package/esm/src/platform/adapters/fs/veryfront/adapter-helpers.d.ts.map +1 -0
  65. package/esm/src/platform/adapters/fs/veryfront/adapter-helpers.js +26 -0
  66. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts.map +1 -1
  67. package/esm/src/platform/adapters/fs/veryfront/adapter.js +4 -20
  68. package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.d.ts +1 -0
  69. package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.d.ts.map +1 -1
  70. package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.js +3 -0
  71. package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts.map +1 -1
  72. package/esm/src/platform/adapters/fs/veryfront/read-operations.js +4 -7
  73. package/esm/src/platform/adapters/fs/veryfront/websocket-manager-helpers.d.ts +28 -0
  74. package/esm/src/platform/adapters/fs/veryfront/websocket-manager-helpers.d.ts.map +1 -0
  75. package/esm/src/platform/adapters/fs/veryfront/websocket-manager-helpers.js +41 -0
  76. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +1 -4
  77. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
  78. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +7 -40
  79. package/esm/src/provider/runtime-loader/tool-input-status.d.ts +17 -0
  80. package/esm/src/provider/runtime-loader/tool-input-status.d.ts.map +1 -0
  81. package/esm/src/provider/runtime-loader/tool-input-status.js +155 -0
  82. package/esm/src/provider/runtime-loader.d.ts +2 -3
  83. package/esm/src/provider/runtime-loader.d.ts.map +1 -1
  84. package/esm/src/provider/runtime-loader.js +2 -155
  85. package/esm/src/rendering/orchestrator/pipeline-helpers.d.ts +8 -0
  86. package/esm/src/rendering/orchestrator/pipeline-helpers.d.ts.map +1 -0
  87. package/esm/src/rendering/orchestrator/pipeline-helpers.js +20 -0
  88. package/esm/src/rendering/orchestrator/pipeline.d.ts +0 -3
  89. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  90. package/esm/src/rendering/orchestrator/pipeline.js +4 -22
  91. package/esm/src/routing/api/module-loader/loader-helpers.d.ts +10 -0
  92. package/esm/src/routing/api/module-loader/loader-helpers.d.ts.map +1 -0
  93. package/esm/src/routing/api/module-loader/loader-helpers.js +62 -0
  94. package/esm/src/routing/api/module-loader/loader.d.ts +1 -1
  95. package/esm/src/routing/api/module-loader/loader.d.ts.map +1 -1
  96. package/esm/src/routing/api/module-loader/loader.js +2 -60
  97. package/esm/src/server/bootstrap.d.ts +22 -2
  98. package/esm/src/server/bootstrap.d.ts.map +1 -1
  99. package/esm/src/server/bootstrap.js +67 -5
  100. package/esm/src/server/dev-ui/manifest.d.ts +2 -0
  101. package/esm/src/server/dev-ui/manifest.js +3 -1
  102. package/esm/src/server/handlers/request/api/project-discovery.d.ts.map +1 -1
  103. package/esm/src/server/handlers/request/api/project-discovery.js +14 -8
  104. package/esm/src/server/production-server.js +1 -1
  105. package/esm/src/server/services/rsc/endpoints/rsc-bundles.generated.d.ts.map +1 -1
  106. package/esm/src/server/services/rsc/endpoints/rsc-bundles.generated.js +2 -2
  107. package/esm/src/studio/bridge/bridge-bundle.generated.d.ts.map +1 -1
  108. package/esm/src/studio/bridge/bridge-bundle.generated.js +1 -1
  109. package/esm/src/tool/index.d.ts +1 -1
  110. package/esm/src/tool/index.d.ts.map +1 -1
  111. package/esm/src/tool/types.d.ts +20 -0
  112. package/esm/src/tool/types.d.ts.map +1 -1
  113. package/esm/src/utils/version-constant.d.ts +1 -1
  114. package/esm/src/utils/version-constant.js +1 -1
  115. package/package.json +1 -1
  116. package/src/cli/commands/generate/integration-generator-helpers.ts +185 -0
  117. package/src/cli/commands/generate/integration-generator.ts +12 -168
  118. package/src/cli/commands/knowledge/command-helpers.ts +295 -0
  119. package/src/cli/commands/knowledge/command.ts +33 -259
  120. package/src/cli/mcp/remote-file-tool-helpers.ts +27 -0
  121. package/src/cli/mcp/remote-file-tools.ts +6 -28
  122. package/src/cli/templates/integration-loader-helpers.ts +49 -0
  123. package/src/cli/templates/integration-loader.ts +10 -28
  124. package/src/cli/templates/manifest.js +2 -1
  125. package/src/deno.js +2 -2
  126. package/src/src/agent/runtime/index.ts +77 -94
  127. package/src/src/chat/ag-ui-helpers.ts +139 -0
  128. package/src/src/chat/ag-ui.ts +11 -139
  129. package/src/src/config/schemas/config.schema.ts +11 -0
  130. package/src/src/config/schemas/index.ts +15 -1
  131. package/src/src/errors/error-registry-helpers.ts +23 -0
  132. package/src/src/errors/error-registry.ts +8 -3
  133. package/src/src/extensions/factory-loader.ts +76 -0
  134. package/src/src/extensions/index.ts +20 -0
  135. package/src/src/extensions/orchestrate.ts +184 -0
  136. package/src/src/html/styles-builder/plugin-loader.ts +32 -0
  137. package/src/src/html/styles-builder/tailwind-plugin-allowlist.ts +51 -0
  138. package/src/src/modules/react-loader/ssr-module-loader/loader-helpers.ts +37 -0
  139. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +8 -39
  140. package/src/src/platform/adapters/fs/veryfront/adapter-helpers.ts +43 -0
  141. package/src/src/platform/adapters/fs/veryfront/adapter.ts +8 -22
  142. package/src/src/platform/adapters/fs/veryfront/read-operations-helpers.ts +4 -0
  143. package/src/src/platform/adapters/fs/veryfront/read-operations.ts +4 -7
  144. package/src/src/platform/adapters/fs/veryfront/websocket-manager-helpers.ts +73 -0
  145. package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +29 -44
  146. package/src/src/provider/runtime-loader/tool-input-status.ts +210 -0
  147. package/src/src/provider/runtime-loader.ts +6 -203
  148. package/src/src/rendering/orchestrator/pipeline-helpers.ts +35 -0
  149. package/src/src/rendering/orchestrator/pipeline.ts +8 -35
  150. package/src/src/routing/api/module-loader/loader-helpers.ts +68 -0
  151. package/src/src/routing/api/module-loader/loader.ts +8 -65
  152. package/src/src/server/bootstrap.ts +88 -7
  153. package/src/src/server/dev-ui/manifest.js +3 -1
  154. package/src/src/server/handlers/request/api/project-discovery.ts +19 -8
  155. package/src/src/server/production-server.ts +1 -1
  156. package/src/src/server/services/rsc/endpoints/rsc-bundles.generated.ts +2 -2
  157. package/src/src/studio/bridge/bridge-bundle.generated.ts +1 -1
  158. package/src/src/tool/index.ts +1 -0
  159. package/src/src/tool/types.ts +21 -0
  160. package/src/src/utils/version-constant.ts +1 -1
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Dynamic factory loader.
3
+ *
4
+ * Loads an extension factory from a filesystem path by dynamic import,
5
+ * invokes it, and wraps the result as a `ResolvedExtension`.
6
+ *
7
+ * @module extensions/factory-loader
8
+ */
9
+
10
+ import { isAbsolute, toFileUrl } from "../../deps/jsr.io/@std/path/1.1.4/mod.js";
11
+ import { EXTENSION_VALIDATION_ERROR } from "./errors.js";
12
+ import type { Extension, ExtensionFactory, ExtensionSource, ResolvedExtension } from "./types.js";
13
+
14
+ /**
15
+ * Dynamically import an extension factory from `path` and resolve it.
16
+ *
17
+ * `path` may be either an absolute filesystem path (for project and
18
+ * local-file sources) or a bare module specifier (for `package` source).
19
+ * Absolute paths are converted to `file://` URLs; bare specifiers are
20
+ * passed through so the runtime's module resolver can find them.
21
+ *
22
+ * The module must `export default` an `ExtensionFactory` (a function that
23
+ * returns an `Extension`). On any error — missing default export, default
24
+ * export that is not a function, factory throw, or import failure — this
25
+ * throws `EXTENSION_VALIDATION_ERROR` with a `detail` field that names the
26
+ * path and what went wrong.
27
+ *
28
+ * @param path Absolute filesystem path or bare module specifier.
29
+ * @param source Where the extension was discovered (drives merge priority).
30
+ * @param config Optional config forwarded to the factory.
31
+ */
32
+ export async function loadExtensionFactory(
33
+ path: string,
34
+ source: ExtensionSource,
35
+ config?: unknown,
36
+ ): Promise<ResolvedExtension> {
37
+ const specifier = isAbsolute(path) ? toFileUrl(path).href : path;
38
+ let mod: { default?: unknown };
39
+ try {
40
+ mod = await import(specifier);
41
+ } catch (err) {
42
+ throw EXTENSION_VALIDATION_ERROR.create({
43
+ detail: `Failed to import extension at "${path}": ${
44
+ err instanceof Error ? err.message : String(err)
45
+ }`,
46
+ cause: err,
47
+ });
48
+ }
49
+
50
+ const factory = mod.default;
51
+ if (factory === undefined || factory === null) {
52
+ throw EXTENSION_VALIDATION_ERROR.create({
53
+ detail: `Extension at "${path}" has no default export`,
54
+ });
55
+ }
56
+
57
+ if (typeof factory !== "function") {
58
+ throw EXTENSION_VALIDATION_ERROR.create({
59
+ detail: `Extension at "${path}" default export is not a function (got ${typeof factory})`,
60
+ });
61
+ }
62
+
63
+ let extension: Extension;
64
+ try {
65
+ extension = (factory as ExtensionFactory)(config);
66
+ } catch (err) {
67
+ throw EXTENSION_VALIDATION_ERROR.create({
68
+ detail: `Extension factory at "${path}" threw during invocation: ${
69
+ err instanceof Error ? err.message : String(err)
70
+ }`,
71
+ cause: err,
72
+ });
73
+ }
74
+
75
+ return { extension, source, origin: path };
76
+ }
@@ -6,6 +6,19 @@
6
6
  * helpers consumers need to load, validate, and orchestrate extensions.
7
7
  *
8
8
  * @module extensions
9
+ * @example
10
+ * ```ts
11
+ * import { orchestrateExtensions } from "veryfront/extensions";
12
+ *
13
+ * const loader = await orchestrateExtensions({
14
+ * projectDir: Deno.cwd(),
15
+ * config,
16
+ * logger,
17
+ * });
18
+ *
19
+ * // Later, on shutdown:
20
+ * await loader.teardownAll();
21
+ * ```
9
22
  */
10
23
 
11
24
  // Core types
@@ -40,6 +53,13 @@ export {
40
53
  // Loader
41
54
  export { ExtensionLoader } from "./loader.js";
42
55
 
56
+ // Factory loader (dynamic-import of an extension factory)
57
+ export { loadExtensionFactory } from "./factory-loader.js";
58
+
59
+ // Orchestrator (discover → load → merge → setup)
60
+ export type { OrchestrateOptions } from "./orchestrate.js";
61
+ export { orchestrateExtensions } from "./orchestrate.js";
62
+
43
63
  // Validation
44
64
  export type { ConflictInfo } from "./validation.js";
45
65
  export { detectConflicts, validateExtension } from "./validation.js";
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Extension orchestration pipeline.
3
+ *
4
+ * Discovers, loads, merges, sorts, and runs setup for every extension
5
+ * contributed by the four sources (config, package, project, local-file).
6
+ * Invoked once by `bootstrap()` after config resolution.
7
+ *
8
+ * @module extensions/orchestrate
9
+ */
10
+
11
+ import { basename, dirname } from "../../deps/jsr.io/@std/path/1.1.4/mod.js";
12
+ import * as defaultDiscovery from "./discovery.js";
13
+ import { loadExtensionFactory as defaultLoadFactory } from "./factory-loader.js";
14
+ import { ExtensionLoader } from "./loader.js";
15
+ import type {
16
+ Extension,
17
+ ExtensionConfigEntry,
18
+ ExtensionLogger,
19
+ ExtensionSource,
20
+ ResolvedExtension,
21
+ } from "./types.js";
22
+
23
+ /**
24
+ * Options for `orchestrateExtensions`.
25
+ *
26
+ * The `discovery` and `loadFactory` fields are test seams — they are not
27
+ * part of the stable public API and default to the real implementations.
28
+ */
29
+ export interface OrchestrateOptions {
30
+ projectDir: string;
31
+ config: { extensions?: ExtensionConfigEntry[] };
32
+ logger: ExtensionLogger;
33
+ /** @internal Override discovery functions in tests. */
34
+ discovery?: {
35
+ discoverPackageExtensions: typeof defaultDiscovery.discoverPackageExtensions;
36
+ discoverProjectExtensions: typeof defaultDiscovery.discoverProjectExtensions;
37
+ discoverLocalExtensions: typeof defaultDiscovery.discoverLocalExtensions;
38
+ mergeExtensions: typeof defaultDiscovery.mergeExtensions;
39
+ };
40
+ /** @internal Override factory loading in tests. */
41
+ loadFactory?: typeof defaultLoadFactory;
42
+ }
43
+
44
+ function isDisableDirective(
45
+ entry: ExtensionConfigEntry,
46
+ ): entry is { name: string; enabled: false } {
47
+ return (
48
+ typeof entry === "object" &&
49
+ entry !== null &&
50
+ "enabled" in entry &&
51
+ (entry as { enabled: unknown }).enabled === false
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Extract the extension name from a project-extension path.
57
+ *
58
+ * Project extensions live under `<baseDir>/extensions/<name>/...`. Discovery
59
+ * emits either `<baseDir>/extensions/<name>/src/index.ts` or
60
+ * `<baseDir>/extensions/<name>/index.ts`. This walks parent directories until
61
+ * it finds the ancestor whose parent is `extensions/`.
62
+ */
63
+ function projectExtensionNameFromPath(path: string): string | undefined {
64
+ let current = dirname(path);
65
+ // Safety limit in case of a malformed path that never reaches a root.
66
+ for (let i = 0; i < 8; i++) {
67
+ const parent = dirname(current);
68
+ if (basename(parent) === "extensions") {
69
+ return basename(current);
70
+ }
71
+ if (parent === current) return undefined;
72
+ current = parent;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * Run the full extension pipeline against a resolved project config.
79
+ *
80
+ * Pipeline:
81
+ * 1. Split `config.extensions` into resolved entries and disable directives.
82
+ * 2. Discover extensions from package, project, and local sources.
83
+ * 3. Skip loading factories for package- and project-source extensions whose
84
+ * names appear in the disable set (local-file names are not reliable
85
+ * pre-load and are filtered after `mergeExtensions`).
86
+ * 4. Dynamic-import factories for every remaining discovered path.
87
+ * 5. Merge sources honoring priority `config > package > project > local-file`.
88
+ * 6. Construct an `ExtensionLoader` and run `setupAll`.
89
+ *
90
+ * On factory error during `setup()`, `ExtensionLoader.setupAll` performs
91
+ * partial rollback internally. The error is re-thrown unchanged so callers
92
+ * can surface the extension name to the user.
93
+ */
94
+ export async function orchestrateExtensions(
95
+ options: OrchestrateOptions,
96
+ ): Promise<ExtensionLoader> {
97
+ const { projectDir, config, logger } = options;
98
+ const disc = options.discovery ?? defaultDiscovery;
99
+ const loadFactory = options.loadFactory ?? defaultLoadFactory;
100
+
101
+ const configEntries = Array.isArray(config.extensions) ? config.extensions : [];
102
+ const disables: Array<{ name: string; enabled: false }> = [];
103
+ const configResolved: ResolvedExtension[] = [];
104
+
105
+ for (const entry of configEntries) {
106
+ if (isDisableDirective(entry)) {
107
+ disables.push(entry);
108
+ } else {
109
+ configResolved.push({
110
+ extension: entry as Extension,
111
+ source: "config",
112
+ origin: "veryfront.config",
113
+ });
114
+ }
115
+ }
116
+
117
+ // Build the disabled-names set early so we can skip dynamic imports for
118
+ // extensions the user has explicitly turned off. A factory whose module
119
+ // fails to import or invoke would otherwise take down bootstrap even
120
+ // though the user asked for it to be disabled.
121
+ const disabledNames = new Set(disables.map((d) => d.name));
122
+
123
+ const [packageHits, projectPaths, localPaths] = await Promise.all([
124
+ disc.discoverPackageExtensions(projectDir),
125
+ disc.discoverProjectExtensions(projectDir),
126
+ disc.discoverLocalExtensions(projectDir),
127
+ ]);
128
+
129
+ // Package hits carry the package name directly — filter before loading.
130
+ const enabledPackageNames = packageHits
131
+ .map((hit) => hit.packageName)
132
+ .filter((name) => !disabledNames.has(name));
133
+
134
+ // Project paths have the shape `<projectDir>/extensions/<name>/src/index.ts`
135
+ // (or `<projectDir>/extensions/<name>/index.ts`). `mergeExtensions` is the
136
+ // safety net for any path whose name cannot be derived.
137
+ const enabledProjectPaths = projectPaths.filter((path) => {
138
+ const name = projectExtensionNameFromPath(path);
139
+ return name === undefined || !disabledNames.has(name);
140
+ });
141
+
142
+ // Local-file paths cannot be reliably filtered pre-load: the filename
143
+ // (`foo.extension.ts`) is not guaranteed to match the extension name
144
+ // declared by the factory. `mergeExtensions` applies the post-hoc filter.
145
+ const packageResolved = await loadAllFactories(
146
+ enabledPackageNames,
147
+ "package",
148
+ loadFactory,
149
+ );
150
+ const projectResolved = await loadAllFactories(
151
+ enabledProjectPaths,
152
+ "project",
153
+ loadFactory,
154
+ );
155
+ const localResolved = await loadAllFactories(
156
+ localPaths,
157
+ "local-file",
158
+ loadFactory,
159
+ );
160
+
161
+ const merged = disc.mergeExtensions(
162
+ configResolved,
163
+ packageResolved,
164
+ projectResolved,
165
+ localResolved,
166
+ disables,
167
+ );
168
+
169
+ const loader = new ExtensionLoader(logger);
170
+ await loader.setupAll(merged, config as Record<string, unknown>);
171
+ return loader;
172
+ }
173
+
174
+ async function loadAllFactories(
175
+ paths: string[],
176
+ source: ExtensionSource,
177
+ loadFactory: typeof defaultLoadFactory,
178
+ ): Promise<ResolvedExtension[]> {
179
+ const resolved: ResolvedExtension[] = [];
180
+ for (const path of paths) {
181
+ resolved.push(await loadFactory(path, source));
182
+ }
183
+ return resolved;
184
+ }
@@ -21,9 +21,34 @@ import {
21
21
  VeryfrontError,
22
22
  } from "../../errors/index.js";
23
23
  import { getTailwindPluginBundleUrl } from "../../build/binary-plugin-includes.js";
24
+ import {
25
+ bareName,
26
+ PACKAGE_SPEC_RE,
27
+ TAILWIND_PLUGIN_ALLOWLIST,
28
+ } from "./tailwind-plugin-allowlist.js";
24
29
 
25
30
  const logger = serverLogger.component("tailwind");
26
31
 
32
+ /**
33
+ * Enforce the Tailwind plugin allowlist (VULN-FS-1).
34
+ *
35
+ * Called at the top of every entry point that can load third-party plugin
36
+ * code. Rejects anything that is not a syntactically valid npm package
37
+ * specifier, and anything whose bare name is not on the allowlist.
38
+ */
39
+ function assertPluginAllowed(spec: string): void {
40
+ if (!PACKAGE_SPEC_RE.test(spec)) {
41
+ throw new Error(`Invalid Tailwind plugin specifier: ${spec}`);
42
+ }
43
+ const name = bareName(spec);
44
+ if (!TAILWIND_PLUGIN_ALLOWLIST.has(name)) {
45
+ throw new Error(
46
+ `Package "${name}" is not on the Tailwind plugin allowlist. ` +
47
+ `See src/html/styles-builder/tailwind-plugin-allowlist.ts.`,
48
+ );
49
+ }
50
+ }
51
+
27
52
  // Provide localStorage shim for plugins that use util-deprecate (which checks localStorage)
28
53
  // This prevents "LocalStorage is not supported in this context" errors in Deno.
29
54
  try {
@@ -109,6 +134,8 @@ async function importBundledModule(code: string): Promise<unknown> {
109
134
  * dynamic imports from URLs. Fetches bundled code, rewrites imports, loads via temp file.
110
135
  */
111
136
  export async function loadModuleFromEsmSh(packageName: string): Promise<unknown> {
137
+ assertPluginAllowed(packageName);
138
+
112
139
  const stubUrl = getTailwindPluginBundleUrl(packageName);
113
140
  logger.debug("Fetching esm.sh stub", { url: stubUrl });
114
141
 
@@ -173,6 +200,11 @@ export async function loadPlugin(
173
200
  pluginCache: Map<string, unknown>,
174
201
  pluginErrors: Map<string, Error>,
175
202
  ): Promise<unknown> {
203
+ // Enforce the allowlist before consulting any caches so a disallowed id can
204
+ // never be served from a pre-seeded or stale cache entry — defence-in-depth
205
+ // against future changes that might pre-populate these maps.
206
+ assertPluginAllowed(id);
207
+
176
208
  const cachedError = pluginErrors.get(id);
177
209
  if (cachedError) throw cachedError;
178
210
 
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Allowlist of Tailwind CSS plugin package names that may be dynamically loaded
3
+ * via `loadPlugin` / `loadModuleFromEsmSh`.
4
+ *
5
+ * Tailwind v4 stylesheets can request plugins through the `@plugin "..."`
6
+ * directive. Without restriction, this turns arbitrary project CSS into a
7
+ * remote-code-execution vector because the loader fetches and imports code
8
+ * from https://esm.sh. Only the packages listed here may be loaded.
9
+ *
10
+ * @module html/styles-builder/tailwind-plugin-allowlist
11
+ */
12
+
13
+ export const TAILWIND_PLUGIN_ALLOWLIST: ReadonlySet<string> = new Set([
14
+ // Bundled into the compiled binary (see src/build/binary-plugin-includes.ts).
15
+ // Must stay in sync with BINARY_TAILWIND_PLUGIN_PACKAGES — an invariant
16
+ // enforced by tailwind-plugin-allowlist.test.ts.
17
+ "@tailwindcss/typography",
18
+ "@tailwindcss/forms",
19
+ "tailwindcss-animate",
20
+ "tailwind-scrollbar-hide",
21
+ "daisyui",
22
+ // Allowlisted but NOT bundled: loaded from esm.sh on first use. Adding an
23
+ // entry here is arbitrary-code-exec surface; review each one explicitly.
24
+ "@tailwindcss/aspect-ratio",
25
+ "@tailwindcss/container-queries",
26
+ ]);
27
+
28
+ /**
29
+ * Matches npm package specifiers, optionally scoped and optionally suffixed
30
+ * with an `@version` range. Deliberately restrictive: ASCII only, no path
31
+ * separators, no whitespace, no control characters.
32
+ */
33
+ export const PACKAGE_SPEC_RE = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(?:@[\w.+-]+)?$/i;
34
+
35
+ /**
36
+ * Return the bare package name (without any `@version` suffix) for a spec.
37
+ *
38
+ * Examples:
39
+ * bareName("pkg") -> "pkg"
40
+ * bareName("pkg@1.0.0") -> "pkg"
41
+ * bareName("@scope/pkg") -> "@scope/pkg"
42
+ * bareName("@scope/pkg@1.0") -> "@scope/pkg"
43
+ */
44
+ export function bareName(spec: string): string {
45
+ if (spec.startsWith("@")) {
46
+ const idx = spec.indexOf("@", 1);
47
+ return idx === -1 ? spec : spec.slice(0, idx);
48
+ }
49
+ const idx = spec.indexOf("@");
50
+ return idx === -1 ? spec : spec.slice(0, idx);
51
+ }
@@ -0,0 +1,37 @@
1
+ import { createError, toError } from "../../../errors/veryfront-error.js";
2
+
3
+ const MISSING_HTTP_BUNDLE_PATTERN = /veryfront-http-bundle\/http-([a-f0-9]+)\.mjs/;
4
+
5
+ export type TransformCapacityErrorMode = "plain" | "build";
6
+
7
+ export type ImportErrorClassification =
8
+ | { type: "http-bundle-missing"; hash: string; message: string }
9
+ | { type: "module-not-found"; message: string }
10
+ | { type: "unknown"; message: string };
11
+
12
+ export function classifyImportError(importError: unknown): ImportErrorClassification {
13
+ const message = importError instanceof Error ? importError.message : String(importError);
14
+ const bundleMatch = message.match(MISSING_HTTP_BUNDLE_PATTERN);
15
+ if (bundleMatch?.[1]) {
16
+ return { type: "http-bundle-missing", hash: bundleMatch[1], message };
17
+ }
18
+ if (message.includes("Cannot find module") || message.includes("Module not found")) {
19
+ return { type: "module-not-found", message };
20
+ }
21
+ return { type: "unknown", message };
22
+ }
23
+
24
+ export function createTransformCapacityError(
25
+ mode: TransformCapacityErrorMode,
26
+ message: string,
27
+ filePath: string,
28
+ ): Error {
29
+ if (mode === "plain") return new Error(message);
30
+ return toError(
31
+ createError({
32
+ type: "build",
33
+ message,
34
+ context: { file: filePath, phase: "transform" },
35
+ }),
36
+ );
37
+ }
@@ -22,6 +22,11 @@ import { rendererLogger } from "../../../utils/index.js";
22
22
  import { withSpan } from "../../../observability/tracing/otlp-setup.js";
23
23
  import { SpanNames } from "../../../observability/tracing/span-names.js";
24
24
  import { extractComponent } from "../extract-component.js";
25
+ import {
26
+ classifyImportError,
27
+ createTransformCapacityError,
28
+ type TransformCapacityErrorMode,
29
+ } from "./loader-helpers.js";
25
30
  import {
26
31
  getMaxConcurrentTransforms,
27
32
  IN_PROGRESS_WAIT_TIMEOUT_MS,
@@ -58,15 +63,6 @@ import { ensureMdxModuleDependencies } from "../../../transforms/mdx/esm-module-
58
63
 
59
64
  const logger = rendererLogger.component("ssr-module-loader");
60
65
 
61
- const MISSING_HTTP_BUNDLE_PATTERN = /veryfront-http-bundle\/http-([a-f0-9]+)\.mjs/;
62
-
63
- type TransformCapacityErrorMode = "plain" | "build";
64
-
65
- type ImportErrorClassification =
66
- | { type: "http-bundle-missing"; hash: string; message: string }
67
- | { type: "module-not-found"; message: string }
68
- | { type: "unknown"; message: string };
69
-
70
66
  /**
71
67
  * SSR Module Loader with Redis Support.
72
68
  *
@@ -89,33 +85,6 @@ export class SSRModuleLoader {
89
85
  );
90
86
  }
91
87
 
92
- private classifyImportError(importError: unknown): ImportErrorClassification {
93
- const message = importError instanceof Error ? importError.message : String(importError);
94
- const bundleMatch = message.match(MISSING_HTTP_BUNDLE_PATTERN);
95
- if (bundleMatch?.[1]) {
96
- return { type: "http-bundle-missing", hash: bundleMatch[1], message };
97
- }
98
- if (message.includes("Cannot find module") || message.includes("Module not found")) {
99
- return { type: "module-not-found", message };
100
- }
101
- return { type: "unknown", message };
102
- }
103
-
104
- private createTransformCapacityError(
105
- mode: TransformCapacityErrorMode,
106
- message: string,
107
- filePath: string,
108
- ): Error {
109
- if (mode === "plain") return new Error(message);
110
- return toError(
111
- createError({
112
- type: "build",
113
- message,
114
- context: { file: filePath, phase: "transform" },
115
- }),
116
- );
117
- }
118
-
119
88
  private async withTransformCapacity<T>(
120
89
  filePath: string,
121
90
  mode: TransformCapacityErrorMode,
@@ -127,7 +96,7 @@ export class SSRModuleLoader {
127
96
  let semaphoreAcquired = false;
128
97
 
129
98
  if (!await tryAcquireTransformSlot(projectId, TRANSFORM_ACQUIRE_TIMEOUT_MS)) {
130
- throw this.createTransformCapacityError(
99
+ throw createTransformCapacityError(
131
100
  mode,
132
101
  `Project ${projectId} at transform capacity. Consider reducing page complexity or request rate.`,
133
102
  filePath,
@@ -138,7 +107,7 @@ export class SSRModuleLoader {
138
107
  if (semaphore) {
139
108
  semaphoreAcquired = await semaphore.tryAcquire(TRANSFORM_ACQUIRE_TIMEOUT_MS);
140
109
  if (!semaphoreAcquired) {
141
- throw this.createTransformCapacityError(
110
+ throw createTransformCapacityError(
142
111
  mode,
143
112
  `Transform capacity exceeded (${semaphore.waiting} waiting). Service is overloaded.`,
144
113
  filePath,
@@ -189,7 +158,7 @@ export class SSRModuleLoader {
189
158
  { "ssr.file": fileName },
190
159
  )) as Record<string, unknown>;
191
160
  } catch (importError) {
192
- const classifiedError = this.classifyImportError(importError);
161
+ const classifiedError = classifyImportError(importError);
193
162
 
194
163
  if (classifiedError.type === "http-bundle-missing") {
195
164
  const hash = classifiedError.hash;
@@ -0,0 +1,43 @@
1
+ import type { VeryfrontAPIConfig } from "../../veryfront-api-client/types.js";
2
+ import type { FileCacheOptions } from "../cache/types.js";
3
+ import type { ContentSource, FSAdapterConfig } from "./types.js";
4
+
5
+ export const DEFAULT_MAX_RETRIES = 3;
6
+ export const DEFAULT_INITIAL_RETRY_DELAY_MS = 1_000;
7
+ export const DEFAULT_MAX_RETRY_DELAY_MS = 10_000;
8
+ export const DEFAULT_CACHE_TTL_MS = 60_000;
9
+ export const DEFAULT_CACHE_MAX_ENTRIES = 1_000;
10
+ export const DEFAULT_CACHE_MAX_MEMORY_BYTES = 100 * 1024 * 1024;
11
+
12
+ type VeryfrontConfigOverrides = NonNullable<FSAdapterConfig["veryfront"]>;
13
+ type RetryOverrides = VeryfrontConfigOverrides["retry"];
14
+ type CacheOverrides = VeryfrontConfigOverrides["cache"];
15
+
16
+ export function buildRetryConfig(
17
+ retry?: RetryOverrides,
18
+ ): NonNullable<VeryfrontAPIConfig["retry"]> {
19
+ return {
20
+ maxRetries: DEFAULT_MAX_RETRIES,
21
+ initialDelay: DEFAULT_INITIAL_RETRY_DELAY_MS,
22
+ maxDelay: DEFAULT_MAX_RETRY_DELAY_MS,
23
+ ...retry,
24
+ };
25
+ }
26
+
27
+ export function buildFileCacheOptions(
28
+ cache?: CacheOverrides,
29
+ ): FileCacheOptions {
30
+ return {
31
+ enabled: true,
32
+ ttl: DEFAULT_CACHE_TTL_MS,
33
+ maxSize: DEFAULT_CACHE_MAX_ENTRIES,
34
+ maxMemory: DEFAULT_CACHE_MAX_MEMORY_BYTES,
35
+ ...cache,
36
+ };
37
+ }
38
+
39
+ export function shouldBackgroundPregenerateStyles(
40
+ contentContext: { sourceType: ContentSource["type"] } | null,
41
+ ): boolean {
42
+ return contentContext?.sourceType !== "branch";
43
+ }
@@ -13,7 +13,6 @@ import type { FileInfo, ResolveFileOptions } from "../../base.js";
13
13
  import { VeryfrontApiClient } from "../../veryfront-api-client/index.js";
14
14
  import type { Project } from "../../veryfront-api-client/index.js";
15
15
  import { FileCache } from "../cache/file-cache.js";
16
- import type { FileCacheOptions } from "../cache/types.js";
17
16
  import { PathNormalizer } from "./path-normalizer.js";
18
17
  import { ReadOperations } from "./read-operations.js";
19
18
  import { DirectoryOperations } from "./directory-operations.js";
@@ -28,16 +27,14 @@ import {
28
27
  summarizeFileList,
29
28
  toClientContext,
30
29
  } from "./adapter-content-context.js";
30
+ import {
31
+ buildFileCacheOptions,
32
+ buildRetryConfig,
33
+ shouldBackgroundPregenerateStyles,
34
+ } from "./adapter-helpers.js";
31
35
 
32
36
  const logger = baseLogger.component("veryfront-fs-adapter");
33
37
 
34
- const DEFAULT_MAX_RETRIES = 3;
35
- const DEFAULT_INITIAL_RETRY_DELAY_MS = 1_000;
36
- const DEFAULT_MAX_RETRY_DELAY_MS = 10_000;
37
- const DEFAULT_CACHE_TTL_MS = 60_000;
38
- const DEFAULT_CACHE_MAX_ENTRIES = 1_000;
39
- const DEFAULT_CACHE_MAX_MEMORY_BYTES = 100 * 1024 * 1024;
40
-
41
38
  export class VeryfrontFSAdapter implements FSAdapter {
42
39
  private client: VeryfrontApiClient;
43
40
  private cache: FileCache;
@@ -126,12 +123,7 @@ export class VeryfrontFSAdapter implements FSAdapter {
126
123
  this.contentSource = vf.contentSource ?? { type: "branch", branch: "main" };
127
124
  this.proxyMode = vf.proxyMode ?? false;
128
125
 
129
- const retryConfig = {
130
- maxRetries: DEFAULT_MAX_RETRIES,
131
- initialDelay: DEFAULT_INITIAL_RETRY_DELAY_MS,
132
- maxDelay: DEFAULT_MAX_RETRY_DELAY_MS,
133
- ...vf.retry,
134
- };
126
+ const retryConfig = buildRetryConfig(vf.retry);
135
127
 
136
128
  this.client = new VeryfrontApiClient({
137
129
  apiBaseUrl: this.apiBaseUrl,
@@ -142,13 +134,7 @@ export class VeryfrontFSAdapter implements FSAdapter {
142
134
  retry: retryConfig,
143
135
  });
144
136
 
145
- const cacheConfig: FileCacheOptions = {
146
- enabled: true,
147
- ttl: DEFAULT_CACHE_TTL_MS,
148
- maxSize: DEFAULT_CACHE_MAX_ENTRIES,
149
- maxMemory: DEFAULT_CACHE_MAX_MEMORY_BYTES,
150
- ...vf.cache,
151
- };
137
+ const cacheConfig = buildFileCacheOptions(vf.cache);
152
138
 
153
139
  this.cache = new FileCache(cacheConfig);
154
140
  this.normalizer = new PathNormalizer(config.projectDir);
@@ -396,7 +382,7 @@ export class VeryfrontFSAdapter implements FSAdapter {
396
382
  // Branch previews should recover the last registered stylesheet artifact on
397
383
  // cold starts before rebuilding CSS locally. Live edit pokes still
398
384
  // pregenerate through the WebSocket path after branch content changes.
399
- return this.contentContext?.sourceType !== "branch";
385
+ return shouldBackgroundPregenerateStyles(this.contentContext);
400
386
  }
401
387
 
402
388
  private scheduleFileListWarmup(reason: string, cacheKey?: string): void {
@@ -80,6 +80,10 @@ export function getResolvedCacheKey(
80
80
  return `${cacheKeyPrefix}:${normalizedResolvedPath}`;
81
81
  }
82
82
 
83
+ export function buildContentPreview(content: string, max = 80): string {
84
+ return content.length > max ? `${content.slice(0, max)}...` : content;
85
+ }
86
+
83
87
  export function buildExtensionCandidatePaths(basePath: string): string[] {
84
88
  return READ_OPERATION_EXTENSION_PRIORITY.map((ext) => `${basePath}${ext}`);
85
89
  }