@databricks/appkit 0.1.4 → 0.2.0
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/AGENTS.md +89 -12
- package/CLAUDE.md +89 -12
- package/NOTICE.md +4 -0
- package/README.md +21 -15
- package/bin/appkit-lint.js +129 -0
- package/dist/analytics/analytics.d.ts +33 -8
- package/dist/analytics/analytics.d.ts.map +1 -1
- package/dist/analytics/analytics.js +67 -27
- package/dist/analytics/analytics.js.map +1 -1
- package/dist/analytics/defaults.js.map +1 -1
- package/dist/analytics/query.js +12 -6
- package/dist/analytics/query.js.map +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +7 -5
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/defaults.js.map +1 -1
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +25 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/storage/memory.js.map +1 -1
- package/dist/cache/storage/persistent.js +12 -6
- package/dist/cache/storage/persistent.js.map +1 -1
- package/dist/connectors/lakebase/client.js +31 -21
- package/dist/connectors/lakebase/client.js.map +1 -1
- package/dist/connectors/lakebase/defaults.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js +68 -28
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/defaults.js.map +1 -1
- package/dist/context/execution-context.js +75 -0
- package/dist/context/execution-context.js.map +1 -0
- package/dist/context/index.js +27 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/service-context.js +154 -0
- package/dist/context/service-context.js.map +1 -0
- package/dist/context/user-context.js +15 -0
- package/dist/context/user-context.js.map +1 -0
- package/dist/core/appkit.d.ts +3 -0
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +7 -0
- package/dist/core/appkit.js.map +1 -1
- package/dist/errors/authentication.d.ts +38 -0
- package/dist/errors/authentication.d.ts.map +1 -0
- package/dist/errors/authentication.js +48 -0
- package/dist/errors/authentication.js.map +1 -0
- package/dist/errors/base.d.ts +58 -0
- package/dist/errors/base.d.ts.map +1 -0
- package/dist/errors/base.js +70 -0
- package/dist/errors/base.js.map +1 -0
- package/dist/errors/configuration.d.ts +38 -0
- package/dist/errors/configuration.d.ts.map +1 -0
- package/dist/errors/configuration.js +45 -0
- package/dist/errors/configuration.js.map +1 -0
- package/dist/errors/connection.d.ts +42 -0
- package/dist/errors/connection.d.ts.map +1 -0
- package/dist/errors/connection.js +54 -0
- package/dist/errors/connection.js.map +1 -0
- package/dist/errors/execution.d.ts +42 -0
- package/dist/errors/execution.d.ts.map +1 -0
- package/dist/errors/execution.js +51 -0
- package/dist/errors/execution.js.map +1 -0
- package/dist/errors/index.js +28 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/initialization.d.ts +34 -0
- package/dist/errors/initialization.d.ts.map +1 -0
- package/dist/errors/initialization.js +42 -0
- package/dist/errors/initialization.js.map +1 -0
- package/dist/errors/server.d.ts +38 -0
- package/dist/errors/server.d.ts.map +1 -0
- package/dist/errors/server.js +45 -0
- package/dist/errors/server.js.map +1 -0
- package/dist/errors/tunnel.d.ts +38 -0
- package/dist/errors/tunnel.d.ts.map +1 -0
- package/dist/errors/tunnel.js +51 -0
- package/dist/errors/tunnel.js.map +1 -0
- package/dist/errors/validation.d.ts +36 -0
- package/dist/errors/validation.d.ts.map +1 -0
- package/dist/errors/validation.js +45 -0
- package/dist/errors/validation.js.map +1 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/logging/logger.js +179 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/sampling.js +56 -0
- package/dist/logging/sampling.js.map +1 -0
- package/dist/logging/wide-event-emitter.js +108 -0
- package/dist/logging/wide-event-emitter.js.map +1 -0
- package/dist/logging/wide-event.js +167 -0
- package/dist/logging/wide-event.js.map +1 -0
- package/dist/plugin/dev-reader.d.ts.map +1 -1
- package/dist/plugin/dev-reader.js +8 -3
- package/dist/plugin/dev-reader.js.map +1 -1
- package/dist/plugin/interceptors/cache.js.map +1 -1
- package/dist/plugin/interceptors/retry.js +10 -2
- package/dist/plugin/interceptors/retry.js.map +1 -1
- package/dist/plugin/interceptors/telemetry.js +24 -9
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/interceptors/timeout.js +4 -0
- package/dist/plugin/interceptors/timeout.js.map +1 -1
- package/dist/plugin/plugin.d.ts +38 -4
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +86 -5
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugin/to-plugin.d.ts +4 -0
- package/dist/plugin/to-plugin.d.ts.map +1 -1
- package/dist/plugin/to-plugin.js +3 -0
- package/dist/plugin/to-plugin.js.map +1 -1
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +25 -21
- package/dist/server/index.js.map +1 -1
- package/dist/server/remote-tunnel/remote-tunnel-controller.js +4 -2
- package/dist/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
- package/dist/server/remote-tunnel/remote-tunnel-manager.js +10 -8
- package/dist/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/server/utils.js.map +1 -1
- package/dist/server/vite-dev-server.js +8 -5
- package/dist/server/vite-dev-server.js.map +1 -1
- package/dist/shared/src/sql/helpers.js.map +1 -1
- package/dist/stream/arrow-stream-processor.js +13 -6
- package/dist/stream/arrow-stream-processor.js.map +1 -1
- package/dist/stream/buffers.js +5 -1
- package/dist/stream/buffers.js.map +1 -1
- package/dist/stream/sse-writer.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +47 -36
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/stream-registry.js.map +1 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/index.d.ts +2 -2
- package/dist/telemetry/index.js +2 -2
- package/dist/telemetry/instrumentations.js +14 -10
- package/dist/telemetry/instrumentations.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js +8 -6
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/telemetry/trace-sampler.js +33 -0
- package/dist/telemetry/trace-sampler.js.map +1 -0
- package/dist/type-generator/index.js +4 -2
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/query-registry.js +4 -2
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/types.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +5 -3
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/utils/env-validator.js +5 -5
- package/dist/utils/env-validator.js.map +1 -1
- package/dist/utils/merge.js +1 -5
- package/dist/utils/merge.js.map +1 -1
- package/dist/utils/path-exclusions.js +66 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/dist/utils/vite-config-merge.js +1 -5
- package/dist/utils/vite-config-merge.js.map +1 -1
- package/llms.txt +89 -12
- package/package.json +6 -1
- package/dist/utils/databricks-client-middleware.d.ts +0 -17
- package/dist/utils/databricks-client-middleware.d.ts.map +0 -1
- package/dist/utils/databricks-client-middleware.js +0 -117
- package/dist/utils/databricks-client-middleware.js.map +0 -1
- package/dist/utils/index.js +0 -26
- package/dist/utils/index.js.map +0 -1
package/dist/plugin/plugin.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { normalizeTelemetryOptions } from "../telemetry/config.js";
|
|
2
|
+
import { createLogger } from "../logging/logger.js";
|
|
2
3
|
import { TelemetryManager } from "../telemetry/telemetry-manager.js";
|
|
3
4
|
import "../telemetry/index.js";
|
|
5
|
+
import { AuthenticationError } from "../errors/authentication.js";
|
|
6
|
+
import { init_errors } from "../errors/index.js";
|
|
4
7
|
import { validateEnv } from "../utils/env-validator.js";
|
|
5
8
|
import { deepMerge } from "../utils/merge.js";
|
|
6
|
-
import {
|
|
9
|
+
import { ServiceContext } from "../context/service-context.js";
|
|
10
|
+
import { getCurrentUserId, runInUserContext } from "../context/execution-context.js";
|
|
11
|
+
import { init_context } from "../context/index.js";
|
|
7
12
|
import { AppManager } from "../app/index.js";
|
|
8
13
|
import { CacheManager } from "../cache/index.js";
|
|
9
14
|
import { StreamManager } from "../stream/stream-manager.js";
|
|
@@ -15,7 +20,24 @@ import { TelemetryInterceptor } from "./interceptors/telemetry.js";
|
|
|
15
20
|
import { TimeoutInterceptor } from "./interceptors/timeout.js";
|
|
16
21
|
|
|
17
22
|
//#region src/plugin/plugin.ts
|
|
18
|
-
|
|
23
|
+
init_context();
|
|
24
|
+
init_errors();
|
|
25
|
+
const logger = createLogger("plugin");
|
|
26
|
+
/**
|
|
27
|
+
* Methods that should not be proxied by asUser().
|
|
28
|
+
* These are lifecycle/internal methods that don't make sense
|
|
29
|
+
* to execute in a user context.
|
|
30
|
+
*/
|
|
31
|
+
const EXCLUDED_FROM_PROXY = new Set([
|
|
32
|
+
"setup",
|
|
33
|
+
"shutdown",
|
|
34
|
+
"validateEnv",
|
|
35
|
+
"injectRoutes",
|
|
36
|
+
"getEndpoints",
|
|
37
|
+
"abortActiveOperations",
|
|
38
|
+
"asUser",
|
|
39
|
+
"constructor"
|
|
40
|
+
]);
|
|
19
41
|
var Plugin = class {
|
|
20
42
|
static {
|
|
21
43
|
this.phase = "normal";
|
|
@@ -23,7 +45,6 @@ var Plugin = class {
|
|
|
23
45
|
constructor(config) {
|
|
24
46
|
this.config = config;
|
|
25
47
|
this.isReady = false;
|
|
26
|
-
this.requiresDatabricksClient = false;
|
|
27
48
|
this.registeredEndpoints = {};
|
|
28
49
|
this.name = config.name ?? "plugin";
|
|
29
50
|
this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);
|
|
@@ -44,18 +65,77 @@ var Plugin = class {
|
|
|
44
65
|
abortActiveOperations() {
|
|
45
66
|
this.streamManager.abortAll();
|
|
46
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Execute operations using the user's identity from the request.
|
|
70
|
+
*
|
|
71
|
+
* Returns a scoped instance of this plugin where all method calls
|
|
72
|
+
* will execute with the user's Databricks credentials instead of
|
|
73
|
+
* the service principal.
|
|
74
|
+
*
|
|
75
|
+
* @param req - The Express request containing the user token in headers
|
|
76
|
+
* @returns A scoped plugin instance that executes as the user
|
|
77
|
+
* @throws Error if user token is not available in request headers
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // In route handler - execute query as the requesting user
|
|
82
|
+
* router.post('/users/me/query/:key', async (req, res) => {
|
|
83
|
+
* const result = await this.asUser(req).query(req.params.key)
|
|
84
|
+
* res.json(result)
|
|
85
|
+
* })
|
|
86
|
+
*
|
|
87
|
+
* // Mixed execution in same handler
|
|
88
|
+
* router.post('/dashboard', async (req, res) => {
|
|
89
|
+
* const [systemData, userData] = await Promise.all([
|
|
90
|
+
* this.getSystemStats(), // Service principal
|
|
91
|
+
* this.asUser(req).getUserPreferences(), // User context
|
|
92
|
+
* ])
|
|
93
|
+
* res.json({ systemData, userData })
|
|
94
|
+
* })
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
asUser(req) {
|
|
98
|
+
const token = req.headers["x-forwarded-access-token"];
|
|
99
|
+
const userId = req.headers["x-forwarded-user"];
|
|
100
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
101
|
+
if (!token && isDev) {
|
|
102
|
+
logger.warn("asUser() called without user token in development mode. Using service principal.");
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
if (!token) throw AuthenticationError.missingToken("user token");
|
|
106
|
+
if (!userId && !isDev) throw AuthenticationError.missingUserId();
|
|
107
|
+
const effectiveUserId = userId || "dev-user";
|
|
108
|
+
const userContext = ServiceContext.createUserContext(token, effectiveUserId);
|
|
109
|
+
return this.createUserContextProxy(userContext);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Creates a proxy that wraps method calls in a user context.
|
|
113
|
+
* This allows all plugin methods to automatically use the user's
|
|
114
|
+
* Databricks credentials.
|
|
115
|
+
*/
|
|
116
|
+
createUserContextProxy(userContext) {
|
|
117
|
+
return new Proxy(this, { get: (target, prop, receiver) => {
|
|
118
|
+
const value = Reflect.get(target, prop, receiver);
|
|
119
|
+
if (typeof value !== "function") return value;
|
|
120
|
+
if (typeof prop === "string" && EXCLUDED_FROM_PROXY.has(prop)) return value;
|
|
121
|
+
return (...args) => {
|
|
122
|
+
return runInUserContext(userContext, () => value.apply(target, args));
|
|
123
|
+
};
|
|
124
|
+
} });
|
|
125
|
+
}
|
|
47
126
|
async executeStream(res, fn, options, userKey) {
|
|
48
127
|
const { stream: streamConfig, default: defaultConfig, user: userConfig } = options;
|
|
49
128
|
const executeConfig = this._buildExecutionConfig({
|
|
50
129
|
default: defaultConfig,
|
|
51
130
|
user: userConfig
|
|
52
131
|
});
|
|
132
|
+
const effectiveUserKey = userKey ?? getCurrentUserId();
|
|
53
133
|
const self = this;
|
|
54
134
|
const asyncWrapperFn = async function* (streamSignal) {
|
|
55
135
|
const context = {
|
|
56
136
|
signal: streamSignal,
|
|
57
137
|
metadata: /* @__PURE__ */ new Map(),
|
|
58
|
-
userKey
|
|
138
|
+
userKey: effectiveUserKey
|
|
59
139
|
};
|
|
60
140
|
const interceptors = self._buildInterceptors(executeConfig);
|
|
61
141
|
const wrappedFn = async () => {
|
|
@@ -70,9 +150,10 @@ var Plugin = class {
|
|
|
70
150
|
async execute(fn, options, userKey) {
|
|
71
151
|
const executeConfig = this._buildExecutionConfig(options);
|
|
72
152
|
const interceptors = this._buildInterceptors(executeConfig);
|
|
153
|
+
const effectiveUserKey = userKey ?? getCurrentUserId();
|
|
73
154
|
const context = {
|
|
74
155
|
metadata: /* @__PURE__ */ new Map(),
|
|
75
|
-
userKey
|
|
156
|
+
userKey: effectiveUserKey
|
|
76
157
|
};
|
|
77
158
|
try {
|
|
78
159
|
return await this._executeWithInterceptors(fn, interceptors, context);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["config: TConfig","context: ExecutionContext","interceptors: ExecutionInterceptor[]"],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge, validateEnv } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionContext,\n ExecutionInterceptor,\n} from \"./interceptors/types\";\n\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry: ITelemetry;\n protected abstract envVars: string[];\n\n /** If the plugin requires the Databricks client to be set in the request context */\n requiresDatabricksClient = false;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n static phase: PluginPhase = \"normal\";\n name: string;\n\n constructor(protected config: TConfig) {\n this.name = config.name ?? \"plugin\";\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.streamManager = new StreamManager();\n this.cache = CacheManager.getInstanceSync();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n\n this.isReady = true;\n }\n\n validateEnv() {\n validateEnv(this.envVars);\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n const self = this;\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: ExecutionContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: userKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors\n const result = await self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client\n await this.streamManager.stream(res, asyncWrapperFn, streamConfig);\n }\n\n // single sync execution with interceptors\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey: string,\n ): Promise<T | undefined> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n const context: ExecutionContext = {\n metadata: new Map(),\n userKey: userKey,\n };\n\n try {\n return await this._executeWithInterceptors(fn, interceptors, context);\n } catch (_error) {\n // production-safe, don't crash sdk\n return undefined;\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n this.registerEndpoint(name, `/api/${this.name}${path}`);\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n // Only add telemetry interceptor if traces are enabled\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: ExecutionContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport {\n getCurrentUserId,\n runInUserContext,\n ServiceContext,\n type UserContext,\n} from \"../context\";\nimport { AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge, validateEnv } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"validateEnv\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"abortActiveOperations\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry: ITelemetry;\n protected abstract envVars: string[];\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n static phase: PluginPhase = \"normal\";\n name: string;\n\n constructor(protected config: TConfig) {\n this.name = config.name ?? \"plugin\";\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.streamManager = new StreamManager();\n this.cache = CacheManager.getInstanceSync();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n\n this.isReady = true;\n }\n\n validateEnv() {\n validateEnv(this.envVars);\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n *\n * Returns a scoped instance of this plugin where all method calls\n * will execute with the user's Databricks credentials instead of\n * the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A scoped plugin instance that executes as the user\n * @throws Error if user token is not available in request headers\n *\n * @example\n * ```typescript\n * // In route handler - execute query as the requesting user\n * router.post('/users/me/query/:key', async (req, res) => {\n * const result = await this.asUser(req).query(req.params.key)\n * res.json(result)\n * })\n *\n * // Mixed execution in same handler\n * router.post('/dashboard', async (req, res) => {\n * const [systemData, userData] = await Promise.all([\n * this.getSystemStats(), // Service principal\n * this.asUser(req).getUserPreferences(), // User context\n * ])\n * res.json({ systemData, userData })\n * })\n * ```\n */\n asUser(req: express.Request): this {\n const token = req.headers[\"x-forwarded-access-token\"] as string;\n const userId = req.headers[\"x-forwarded-user\"] as string;\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, fall back to service principal\n // since there's no user token available\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Using service principal.\",\n );\n\n return this;\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n );\n\n // Return a proxy that wraps method calls in user context\n return this.createUserContextProxy(userContext);\n }\n\n /**\n * Creates a proxy that wraps method calls in a user context.\n * This allows all plugin methods to automatically use the user's\n * Databricks credentials.\n */\n private createUserContextProxy(userContext: UserContext): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") {\n return value;\n }\n\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop)) {\n return value;\n }\n\n return (...args: unknown[]) => {\n return runInUserContext(userContext, () => value.apply(target, args));\n };\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors\n const result = await self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client\n await this.streamManager.stream(res, asyncWrapperFn, streamConfig);\n }\n\n // single sync execution with interceptors\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<T | undefined> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n return await this._executeWithInterceptors(fn, interceptors, context);\n } catch (_error) {\n // production-safe, don't crash sdk\n return undefined;\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n this.registerEndpoint(name, `/api/${this.name}${path}`);\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-plugin.d.ts","names":[],"sources":["../../src/plugin/to-plugin.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"to-plugin.d.ts","names":[],"sources":["../../src/plugin/to-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;AAKA;AAAwB,iBAAR,QAAQ,CAAA,CAAA,EAAA,CAAA,EAAA,UAAA,MAAA,CAAA,CAAA,MAAA,EACd,CADc,EAAA,IAAA,EAEhB,CAFgB,CAAA,EAGrB,QAHqB,CAGZ,CAHY,EAGT,CAHS,EAGN,CAHM,CAAA"}
|
package/dist/plugin/to-plugin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-plugin.js","names":[],"sources":["../../src/plugin/to-plugin.ts"],"sourcesContent":["import type { PluginData, ToPlugin } from \"shared\";\n\nexport function toPlugin<T, U, N extends string>(\n plugin: T,\n name: N,\n): ToPlugin<T, U, N> {\n return (config: U = {} as U): PluginData<T, U, N> => ({\n plugin: plugin as T,\n config: config as U,\n name,\n });\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"to-plugin.js","names":[],"sources":["../../src/plugin/to-plugin.ts"],"sourcesContent":["import type { PluginData, ToPlugin } from \"shared\";\n\n/**\n * @internal\n */\nexport function toPlugin<T, U, N extends string>(\n plugin: T,\n name: N,\n): ToPlugin<T, U, N> {\n return (config: U = {} as U): PluginData<T, U, N> => ({\n plugin: plugin as T,\n config: config as U,\n name,\n });\n}\n"],"mappings":";;;;AAKA,SAAgB,SACd,QACA,MACmB;AACnB,SAAQ,SAAY,EAAE,MAAgC;EAC5C;EACA;EACR;EACD"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAkCA;;;;;;;;;;;;AAAwC,cAA3B,YAAA,SAAqB,MAAA,CAAM;EA0S3B,OAAA,cAGZ,EAAA;IAAA,SAAA,EAAA,OAAA;IAHkB,IAAA,EAAA,MAAA;IAAA,IAAA,EAAA,MAAA;;EAAA,IAAA,EAAA,QAAA;;;;;;oBA7RS;;gBAEZ;sBAEM;;WAaT;;;;;;;;;gBAAA;;;;;;;;;;;;WA0BI,QAAQ,OAAA,CAAQ;;;;;;;;;eA8ClB;;;;;;;;mBAmBI,OAAA,CAAQ;;;;;;;;;;;;;;;;;;;;;;cAiLd,QAAM,gBAAA,cAAA"}
|
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { instrumentations } from "../telemetry/instrumentations.js";
|
|
2
|
+
import { createLogger } from "../logging/logger.js";
|
|
2
3
|
import "../telemetry/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { ServerError } from "../errors/server.js";
|
|
5
|
+
import { init_errors } from "../errors/index.js";
|
|
5
6
|
import { Plugin } from "../plugin/plugin.js";
|
|
6
7
|
import { toPlugin } from "../plugin/to-plugin.js";
|
|
7
8
|
import "../plugin/index.js";
|
|
@@ -15,8 +16,9 @@ import dotenv from "dotenv";
|
|
|
15
16
|
import express from "express";
|
|
16
17
|
|
|
17
18
|
//#region src/server/index.ts
|
|
18
|
-
|
|
19
|
+
init_errors();
|
|
19
20
|
dotenv.config({ path: path.resolve(process.cwd(), "./.env") });
|
|
21
|
+
const logger = createLogger("server");
|
|
20
22
|
/**
|
|
21
23
|
* Server plugin for the AppKit.
|
|
22
24
|
*
|
|
@@ -81,9 +83,9 @@ var ServerPlugin = class ServerPlugin extends Plugin {
|
|
|
81
83
|
this.remoteTunnelController = new RemoteTunnelController(this.devFileReader);
|
|
82
84
|
this.serverApplication.use(this.remoteTunnelController.middleware);
|
|
83
85
|
await this.setupFrontend(endpoints);
|
|
84
|
-
const server
|
|
85
|
-
this.server = server
|
|
86
|
-
this.remoteTunnelController.setServer(server
|
|
86
|
+
const server = this.serverApplication.listen(this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port, this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host, () => this.logStartupInfo());
|
|
87
|
+
this.server = server;
|
|
88
|
+
this.remoteTunnelController.setServer(server);
|
|
87
89
|
process.on("SIGTERM", () => this._gracefulShutdown());
|
|
88
90
|
process.on("SIGINT", () => this._gracefulShutdown());
|
|
89
91
|
if (process.env.NODE_ENV === "development") {
|
|
@@ -101,8 +103,8 @@ var ServerPlugin = class ServerPlugin extends Plugin {
|
|
|
101
103
|
* @returns {HTTPServer} The server instance.
|
|
102
104
|
*/
|
|
103
105
|
getServer() {
|
|
104
|
-
if (this.shouldAutoStart()) throw
|
|
105
|
-
if (!this.server) throw
|
|
106
|
+
if (this.shouldAutoStart()) throw ServerError.autoStartConflict("get server");
|
|
107
|
+
if (!this.server) throw ServerError.notStarted();
|
|
106
108
|
return this.server;
|
|
107
109
|
}
|
|
108
110
|
/**
|
|
@@ -113,7 +115,7 @@ var ServerPlugin = class ServerPlugin extends Plugin {
|
|
|
113
115
|
* @throws {Error} If autoStart is true.
|
|
114
116
|
*/
|
|
115
117
|
extend(fn) {
|
|
116
|
-
if (this.shouldAutoStart()) throw
|
|
118
|
+
if (this.shouldAutoStart()) throw ServerError.autoStartConflict("extend server");
|
|
117
119
|
this.serverExtensions.push(fn);
|
|
118
120
|
return this;
|
|
119
121
|
}
|
|
@@ -134,7 +136,6 @@ var ServerPlugin = class ServerPlugin extends Plugin {
|
|
|
134
136
|
if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;
|
|
135
137
|
if (plugin?.injectRoutes && typeof plugin.injectRoutes === "function") {
|
|
136
138
|
const router = express.Router();
|
|
137
|
-
if (plugin.requiresDatabricksClient) router.use(await databricksClientMiddleware());
|
|
138
139
|
plugin.injectRoutes(router);
|
|
139
140
|
const basePath = `/api/${plugin.name}`;
|
|
140
141
|
this.serverApplication.use(basePath, router);
|
|
@@ -175,7 +176,7 @@ var ServerPlugin = class ServerPlugin extends Plugin {
|
|
|
175
176
|
for (const p of staticPaths) {
|
|
176
177
|
const fullPath = path.resolve(cwd, p);
|
|
177
178
|
if (fs.existsSync(path.resolve(fullPath, "index.html"))) {
|
|
178
|
-
|
|
179
|
+
logger.debug("Static files: serving from %s", fullPath);
|
|
179
180
|
return fullPath;
|
|
180
181
|
}
|
|
181
182
|
}
|
|
@@ -185,38 +186,41 @@ var ServerPlugin = class ServerPlugin extends Plugin {
|
|
|
185
186
|
const hasExplicitStaticPath = this.config.staticPath !== void 0;
|
|
186
187
|
const port = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;
|
|
187
188
|
const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;
|
|
188
|
-
|
|
189
|
-
if (hasExplicitStaticPath)
|
|
190
|
-
else if (isDev)
|
|
191
|
-
else
|
|
189
|
+
logger.info("Server running on http://%s:%d", host, port);
|
|
190
|
+
if (hasExplicitStaticPath) logger.info("Mode: static (%s)", this.config.staticPath);
|
|
191
|
+
else if (isDev) logger.info("Mode: development (Vite HMR)");
|
|
192
|
+
else logger.info("Mode: production (static)");
|
|
192
193
|
const remoteServerController = this.remoteTunnelController;
|
|
193
|
-
if (!remoteServerController)
|
|
194
|
-
else
|
|
194
|
+
if (!remoteServerController) logger.debug("Remote tunnel: disabled (controller not initialized)");
|
|
195
|
+
else logger.debug("Remote tunnel: %s; %s", remoteServerController.isAllowedByEnv() ? "allowed" : "blocked", remoteServerController.isActive() ? "active" : "inactive");
|
|
195
196
|
}
|
|
196
197
|
async _gracefulShutdown() {
|
|
197
|
-
|
|
198
|
+
logger.info("Starting graceful shutdown...");
|
|
198
199
|
if (this.viteDevServer) await this.viteDevServer.close();
|
|
199
200
|
if (this.remoteTunnelController) this.remoteTunnelController.cleanup();
|
|
200
201
|
if (this.config.plugins) {
|
|
201
202
|
for (const plugin of Object.values(this.config.plugins)) if (plugin.abortActiveOperations) try {
|
|
202
203
|
plugin.abortActiveOperations();
|
|
203
204
|
} catch (err) {
|
|
204
|
-
|
|
205
|
+
logger.error("Error aborting operations for plugin %s: %O", plugin.name, err);
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
if (this.server) {
|
|
208
209
|
this.server.close(() => {
|
|
209
|
-
|
|
210
|
+
logger.debug("Server closed gracefully");
|
|
210
211
|
process.exit(0);
|
|
211
212
|
});
|
|
212
213
|
setTimeout(() => {
|
|
213
|
-
|
|
214
|
+
logger.debug("Force shutdown after timeout");
|
|
214
215
|
process.exit(1);
|
|
215
216
|
}, 15e3);
|
|
216
217
|
} else process.exit(0);
|
|
217
218
|
}
|
|
218
219
|
};
|
|
219
220
|
const EXCLUDED_PLUGINS = [ServerPlugin.name];
|
|
221
|
+
/**
|
|
222
|
+
* @internal
|
|
223
|
+
*/
|
|
220
224
|
const server = toPlugin(ServerPlugin, "server");
|
|
221
225
|
|
|
222
226
|
//#endregion
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["server","endpoints: PluginEndpoints"],"sources":["../../src/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport type { PluginPhase } from \"shared\";\nimport { Plugin, toPlugin } from \"../plugin\";\nimport { instrumentations } from \"../telemetry\";\nimport { databricksClientMiddleware } from \"../utils\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { type PluginEndpoints, getRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), telemetryExamples(), analytics({})],\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n autoStart: true,\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n public name = \"server\" as const;\n public envVars: string[] = [];\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n }\n\n /** Setup the server plugin. */\n async setup() {\n if (this.shouldAutoStart()) {\n await this.start();\n }\n }\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /** Check if the server should auto start. */\n shouldAutoStart() {\n return this.config.autoStart;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(express.json());\n\n const endpoints = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints);\n\n const server = this.serverApplication.listen(\n this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n console.dir(allRoutes, { depth: null });\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server is not started or autoStart is true.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (this.shouldAutoStart()) {\n throw new Error(\"Cannot get server when autoStart is true.\");\n }\n\n if (!this.server) {\n throw new Error(\n \"Server not started. Please start the server first by calling the start() method.\",\n );\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n * @throws {Error} If autoStart is true.\n */\n extend(fn: (app: express.Application) => void) {\n if (this.shouldAutoStart()) {\n throw new Error(\"Cannot extend server when autoStart is true.\");\n }\n\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints.\n */\n private async extendRoutes(): Promise<PluginEndpoints> {\n const endpoints: PluginEndpoints = {};\n\n if (!this.config.plugins) return endpoints;\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of Object.values(this.config.plugins)) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n // add databricks client middleware to the router if the plugin needs the request context\n if (plugin.requiresDatabricksClient)\n router.use(await databricksClientMiddleware());\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n // Collect named endpoints from the plugin\n endpoints[plugin.name] = plugin.getEndpoints();\n }\n }\n\n return endpoints;\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(endpoints: PluginEndpoints) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(this.serverApplication, endpoints);\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n console.log(`Static files: serving from ${fullPath}`);\n return fullPath;\n }\n }\n return undefined;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n console.log(`Server running on http://${host}:${port}`);\n\n if (hasExplicitStaticPath) {\n console.log(`Mode: static (${this.config.staticPath})`);\n } else if (isDev) {\n console.log(\"Mode: development (Vite HMR)\");\n } else {\n console.log(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n console.log(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n console.log(\n `Remote tunnel: ${\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\"\n }; ${remoteServerController.isActive() ? \"active\" : \"inactive\"}`,\n );\n }\n }\n\n private async _gracefulShutdown() {\n console.log(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n // 1. abort active operations from plugins\n if (this.config.plugins) {\n for (const plugin of Object.values(this.config.plugins)) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n console.error(\n `Error aborting operations for plugin ${plugin.name}:`,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n console.log(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n console.log(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n}\n\nconst EXCLUDED_PLUGINS = [ServerPlugin.name];\n\nexport const server = toPlugin<typeof ServerPlugin, ServerConfig, \"server\">(\n ServerPlugin,\n \"server\",\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport type { PluginPhase } from \"shared\";\nimport { ServerError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { Plugin, toPlugin } from \"../plugin\";\nimport { instrumentations } from \"../telemetry\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), telemetryExamples(), analytics({})],\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n autoStart: true,\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n public name = \"server\" as const;\n public envVars: string[] = [];\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n }\n\n /** Setup the server plugin. */\n async setup() {\n if (this.shouldAutoStart()) {\n await this.start();\n }\n }\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /** Check if the server should auto start. */\n shouldAutoStart() {\n return this.config.autoStart;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(express.json());\n\n const endpoints = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints);\n\n const server = this.serverApplication.listen(\n this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n console.dir(allRoutes, { depth: null });\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server is not started or autoStart is true.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"get server\");\n }\n\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n * @throws {Error} If autoStart is true.\n */\n extend(fn: (app: express.Application) => void) {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"extend server\");\n }\n\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints.\n */\n private async extendRoutes(): Promise<PluginEndpoints> {\n const endpoints: PluginEndpoints = {};\n\n if (!this.config.plugins) return endpoints;\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of Object.values(this.config.plugins)) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n // Collect named endpoints from the plugin\n endpoints[plugin.name] = plugin.getEndpoints();\n }\n }\n\n return endpoints;\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(endpoints: PluginEndpoints) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(this.serverApplication, endpoints);\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n // 1. abort active operations from plugins\n if (this.config.plugins) {\n for (const plugin of Object.values(this.config.plugins)) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n}\n\nconst EXCLUDED_PLUGINS = [ServerPlugin.name];\n\n/**\n * @internal\n */\nexport const server = toPlugin<typeof ServerPlugin, ServerConfig, \"server\">(\n ServerPlugin,\n \"server\",\n);\n"],"mappings":"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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createLogger } from "../../logging/logger.js";
|
|
1
2
|
import { hasDevQuery, isLocalDev, isRemoteTunnelAllowedByEnv, isRemoteTunnelAssetRequest } from "./gate.js";
|
|
2
3
|
|
|
3
4
|
//#region src/server/remote-tunnel/remote-tunnel-controller.ts
|
|
5
|
+
const logger = createLogger("server:remote-tunnel:controller");
|
|
4
6
|
/**
|
|
5
7
|
* Controller for the remote tunnel
|
|
6
8
|
*
|
|
@@ -72,7 +74,7 @@ var RemoteTunnelController = class {
|
|
|
72
74
|
const remoteTunnelManager = new (await (import("./remote-tunnel-manager.js"))).RemoteTunnelManager(this.devFileReader);
|
|
73
75
|
this.manager = remoteTunnelManager;
|
|
74
76
|
this.maybeSetupWebSocket();
|
|
75
|
-
|
|
77
|
+
logger.debug("RemoteTunnel: initialized (on-demand)");
|
|
76
78
|
return remoteTunnelManager;
|
|
77
79
|
})();
|
|
78
80
|
return this.initPromise;
|
|
@@ -91,7 +93,7 @@ var RemoteTunnelController = class {
|
|
|
91
93
|
this.manager.setServer(this.server);
|
|
92
94
|
this.manager.setupWebSocket();
|
|
93
95
|
this.wsReady = true;
|
|
94
|
-
|
|
96
|
+
logger.debug("RemoteTunnel: web socket setup complete");
|
|
95
97
|
}
|
|
96
98
|
};
|
|
97
99
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-tunnel-controller.js","names":[],"sources":["../../../src/server/remote-tunnel/remote-tunnel-controller.ts"],"sourcesContent":["import type { Server as HTTPServer } from \"node:http\";\nimport type express from \"express\";\nimport type { DevFileReader } from \"../../plugin/dev-reader\";\nimport {\n hasDevQuery,\n isLocalDev,\n isRemoteTunnelAllowedByEnv,\n isRemoteTunnelAssetRequest,\n} from \"./gate\";\nimport type { RemoteTunnelManager } from \"./remote-tunnel-manager\";\n\n/**\n * Controller for the remote tunnel\n *\n * - Reads files from the dev file reader\n * - Manages the remote tunnel manager\n * - Sets up the web socket\n * - Cleans up the remote tunnel\n */\nexport class RemoteTunnelController {\n private devFileReader: DevFileReader;\n private server?: HTTPServer;\n private manager: RemoteTunnelManager | null;\n private initPromise: Promise<RemoteTunnelManager | null> | null;\n private wsReady: boolean;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.manager = null;\n this.initPromise = null;\n this.wsReady = false;\n }\n\n /**\n * Set the server instance\n * @param server\n */\n setServer(server: HTTPServer) {\n this.server = server;\n this.maybeSetupWebSocket();\n }\n\n /** Check if the remote tunnel is active */\n isActive(): boolean {\n return this.manager != null;\n }\n\n /** Check if the remote tunnel is allowed by the environment */\n isAllowedByEnv(): boolean {\n return isRemoteTunnelAllowedByEnv();\n }\n\n /**\n * Middleware for the remote tunnel\n * - Hard blocks in local dev\n * - Blocks when not allowed by env\n * - Handles dev query and asset requests\n * @param req - the request\n * @param res - the response\n * @param next - the next function\n * @returns the next function\n */\n middleware: express.RequestHandler = async (req, res, next) => {\n // hard blocker in local dev\n if (isLocalDev()) return next();\n\n // if not allowed by env, block\n if (!this.isAllowedByEnv()) return next();\n\n const wantsDev = hasDevQuery(req);\n const wantsRemoteAssets = isRemoteTunnelAssetRequest(req);\n\n // if not wants dev and not wants remote assets, skip\n if (!wantsDev && !wantsRemoteAssets) return next();\n\n const manager = await this.getOrInitManager();\n // if no manager, skip\n if (!manager) return next();\n\n // dev query present, let manager handle it\n if (wantsDev) {\n return manager.devModeMiddleware()(req, res, next);\n }\n\n // otherwise, handle vite asset fetch paths through the tunnel\n try {\n await manager.assetMiddleware()(req, res);\n } catch (error) {\n next(error);\n }\n };\n\n /** Cleanup the remote tunnel */\n cleanup() {\n try {\n this.manager?.cleanup();\n } finally {\n this.manager = null;\n this.initPromise = null;\n this.wsReady = false;\n }\n }\n\n /**\n * Get or initialize the remote tunnel manager\n * - If the manager is already initialized, return it\n * - If the manager is not initialized, initialize it\n * - If the manager is not allowed by the environment, return null\n * @returns the remote tunnel manager\n */\n private async getOrInitManager(): Promise<RemoteTunnelManager | null> {\n if (this.manager) return this.manager;\n if (this.initPromise) return await this.initPromise;\n\n this.initPromise = (async () => {\n // double check gate\n if (isLocalDev() || !isRemoteTunnelAllowedByEnv()) return null;\n const mod = await import(\"./remote-tunnel-manager\");\n const remoteTunnelManager = new mod.RemoteTunnelManager(\n this.devFileReader,\n );\n this.manager = remoteTunnelManager;\n\n // attach server + ws setup\n this.maybeSetupWebSocket();\n\n
|
|
1
|
+
{"version":3,"file":"remote-tunnel-controller.js","names":[],"sources":["../../../src/server/remote-tunnel/remote-tunnel-controller.ts"],"sourcesContent":["import type { Server as HTTPServer } from \"node:http\";\nimport type express from \"express\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { DevFileReader } from \"../../plugin/dev-reader\";\nimport {\n hasDevQuery,\n isLocalDev,\n isRemoteTunnelAllowedByEnv,\n isRemoteTunnelAssetRequest,\n} from \"./gate\";\nimport type { RemoteTunnelManager } from \"./remote-tunnel-manager\";\n\nconst logger = createLogger(\"server:remote-tunnel:controller\");\n\n/**\n * Controller for the remote tunnel\n *\n * - Reads files from the dev file reader\n * - Manages the remote tunnel manager\n * - Sets up the web socket\n * - Cleans up the remote tunnel\n */\nexport class RemoteTunnelController {\n private devFileReader: DevFileReader;\n private server?: HTTPServer;\n private manager: RemoteTunnelManager | null;\n private initPromise: Promise<RemoteTunnelManager | null> | null;\n private wsReady: boolean;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.manager = null;\n this.initPromise = null;\n this.wsReady = false;\n }\n\n /**\n * Set the server instance\n * @param server\n */\n setServer(server: HTTPServer) {\n this.server = server;\n this.maybeSetupWebSocket();\n }\n\n /** Check if the remote tunnel is active */\n isActive(): boolean {\n return this.manager != null;\n }\n\n /** Check if the remote tunnel is allowed by the environment */\n isAllowedByEnv(): boolean {\n return isRemoteTunnelAllowedByEnv();\n }\n\n /**\n * Middleware for the remote tunnel\n * - Hard blocks in local dev\n * - Blocks when not allowed by env\n * - Handles dev query and asset requests\n * @param req - the request\n * @param res - the response\n * @param next - the next function\n * @returns the next function\n */\n middleware: express.RequestHandler = async (req, res, next) => {\n // hard blocker in local dev\n if (isLocalDev()) return next();\n\n // if not allowed by env, block\n if (!this.isAllowedByEnv()) return next();\n\n const wantsDev = hasDevQuery(req);\n const wantsRemoteAssets = isRemoteTunnelAssetRequest(req);\n\n // if not wants dev and not wants remote assets, skip\n if (!wantsDev && !wantsRemoteAssets) return next();\n\n const manager = await this.getOrInitManager();\n // if no manager, skip\n if (!manager) return next();\n\n // dev query present, let manager handle it\n if (wantsDev) {\n return manager.devModeMiddleware()(req, res, next);\n }\n\n // otherwise, handle vite asset fetch paths through the tunnel\n try {\n await manager.assetMiddleware()(req, res);\n } catch (error) {\n next(error);\n }\n };\n\n /** Cleanup the remote tunnel */\n cleanup() {\n try {\n this.manager?.cleanup();\n } finally {\n this.manager = null;\n this.initPromise = null;\n this.wsReady = false;\n }\n }\n\n /**\n * Get or initialize the remote tunnel manager\n * - If the manager is already initialized, return it\n * - If the manager is not initialized, initialize it\n * - If the manager is not allowed by the environment, return null\n * @returns the remote tunnel manager\n */\n private async getOrInitManager(): Promise<RemoteTunnelManager | null> {\n if (this.manager) return this.manager;\n if (this.initPromise) return await this.initPromise;\n\n this.initPromise = (async () => {\n // double check gate\n if (isLocalDev() || !isRemoteTunnelAllowedByEnv()) return null;\n const mod = await import(\"./remote-tunnel-manager\");\n const remoteTunnelManager = new mod.RemoteTunnelManager(\n this.devFileReader,\n );\n this.manager = remoteTunnelManager;\n\n // attach server + ws setup\n this.maybeSetupWebSocket();\n\n logger.debug(\"RemoteTunnel: initialized (on-demand)\");\n return remoteTunnelManager;\n })();\n\n return this.initPromise;\n }\n\n /**\n * Setup the web socket\n * - If the server is not set, return\n * - If the manager is not set, return\n * - If the web socket is already setup, return\n * - Setup the web socket\n */\n private maybeSetupWebSocket() {\n if (!this.server) return;\n if (!this.manager) return;\n if (this.wsReady) return;\n\n this.manager.setServer(this.server);\n this.manager.setupWebSocket();\n this.wsReady = true;\n\n logger.debug(\"RemoteTunnel: web socket setup complete\");\n }\n}\n"],"mappings":";;;;AAYA,MAAM,SAAS,aAAa,kCAAkC;;;;;;;;;AAU9D,IAAa,yBAAb,MAAoC;CAOlC,YAAY,eAA8B;oBAoCL,OAAO,KAAK,KAAK,SAAS;AAE7D,OAAI,YAAY,CAAE,QAAO,MAAM;AAG/B,OAAI,CAAC,KAAK,gBAAgB,CAAE,QAAO,MAAM;GAEzC,MAAM,WAAW,YAAY,IAAI;GACjC,MAAM,oBAAoB,2BAA2B,IAAI;AAGzD,OAAI,CAAC,YAAY,CAAC,kBAAmB,QAAO,MAAM;GAElD,MAAM,UAAU,MAAM,KAAK,kBAAkB;AAE7C,OAAI,CAAC,QAAS,QAAO,MAAM;AAG3B,OAAI,SACF,QAAO,QAAQ,mBAAmB,CAAC,KAAK,KAAK,KAAK;AAIpD,OAAI;AACF,UAAM,QAAQ,iBAAiB,CAAC,KAAK,IAAI;YAClC,OAAO;AACd,SAAK,MAAM;;;AA7Db,OAAK,gBAAgB;AACrB,OAAK,UAAU;AACf,OAAK,cAAc;AACnB,OAAK,UAAU;;;;;;CAOjB,UAAU,QAAoB;AAC5B,OAAK,SAAS;AACd,OAAK,qBAAqB;;;CAI5B,WAAoB;AAClB,SAAO,KAAK,WAAW;;;CAIzB,iBAA0B;AACxB,SAAO,4BAA4B;;;CA4CrC,UAAU;AACR,MAAI;AACF,QAAK,SAAS,SAAS;YACf;AACR,QAAK,UAAU;AACf,QAAK,cAAc;AACnB,QAAK,UAAU;;;;;;;;;;CAWnB,MAAc,mBAAwD;AACpE,MAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,MAAI,KAAK,YAAa,QAAO,MAAM,KAAK;AAExC,OAAK,eAAe,YAAY;AAE9B,OAAI,YAAY,IAAI,CAAC,4BAA4B,CAAE,QAAO;GAE1D,MAAM,sBAAsB,KADhB,OAAM,OAAO,gCACW,oBAClC,KAAK,cACN;AACD,QAAK,UAAU;AAGf,QAAK,qBAAqB;AAE1B,UAAO,MAAM,wCAAwC;AACrD,UAAO;MACL;AAEJ,SAAO,KAAK;;;;;;;;;CAUd,AAAQ,sBAAsB;AAC5B,MAAI,CAAC,KAAK,OAAQ;AAClB,MAAI,CAAC,KAAK,QAAS;AACnB,MAAI,KAAK,QAAS;AAElB,OAAK,QAAQ,UAAU,KAAK,OAAO;AACnC,OAAK,QAAQ,gBAAgB;AAC7B,OAAK,UAAU;AAEf,SAAO,MAAM,0CAA0C"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from "../../logging/logger.js";
|
|
1
2
|
import { REMOTE_TUNNEL_ASSET_PREFIXES } from "./gate.js";
|
|
2
3
|
import { generateTunnelIdFromEmail, getConfigScript, parseCookies } from "../utils.js";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -10,6 +11,7 @@ import { WebSocketServer } from "ws";
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
12
13
|
const MAX_ASSET_FETCH_TIMEOUT = 6e4;
|
|
14
|
+
const logger = createLogger("server:remote-tunnel");
|
|
13
15
|
/**
|
|
14
16
|
* Remote tunnel manager for the AppKit.
|
|
15
17
|
*
|
|
@@ -75,7 +77,7 @@ var RemoteTunnelManager = class {
|
|
|
75
77
|
});
|
|
76
78
|
ws.send(JSON.stringify(request));
|
|
77
79
|
}).catch((err) => {
|
|
78
|
-
|
|
80
|
+
logger.error("Failed to fetch %s: %s", path$1, err.message);
|
|
79
81
|
return {
|
|
80
82
|
status: 504,
|
|
81
83
|
body: Buffer.from(""),
|
|
@@ -165,13 +167,13 @@ var RemoteTunnelManager = class {
|
|
|
165
167
|
if (!tunnel) return;
|
|
166
168
|
if (isBinary) {
|
|
167
169
|
if (!tunnel.waitingForBinaryBody) {
|
|
168
|
-
|
|
170
|
+
logger.debug("Received binary message but no requestId is waiting for body");
|
|
169
171
|
return;
|
|
170
172
|
}
|
|
171
173
|
const requestId = tunnel.waitingForBinaryBody;
|
|
172
174
|
const pending = tunnel.pendingFetches.get(requestId);
|
|
173
175
|
if (!pending || !pending.metadata) {
|
|
174
|
-
|
|
176
|
+
logger.debug("Received binary message but pending fetch not found");
|
|
175
177
|
tunnel.waitingForBinaryBody = null;
|
|
176
178
|
return;
|
|
177
179
|
}
|
|
@@ -192,10 +194,10 @@ var RemoteTunnelManager = class {
|
|
|
192
194
|
tunnel.pendingRequests.delete(data.viewer);
|
|
193
195
|
if (data.approved) {
|
|
194
196
|
tunnel.approvedViewers.add(data.viewer);
|
|
195
|
-
|
|
197
|
+
logger.debug("✅ Approved %s for tunnel %s", data.viewer, tunnelId);
|
|
196
198
|
} else {
|
|
197
199
|
tunnel.rejectedViewers.add(data.viewer);
|
|
198
|
-
|
|
200
|
+
logger.debug("❌ Denied %s for tunnel %s", data.viewer, tunnelId);
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
203
|
} else if (data.type === "fetch:response:meta") {
|
|
@@ -225,7 +227,7 @@ var RemoteTunnelManager = class {
|
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
} catch (e) {
|
|
228
|
-
|
|
230
|
+
logger.error("Failed to parse WebSocket message: %O", e);
|
|
229
231
|
}
|
|
230
232
|
});
|
|
231
233
|
ws.send(JSON.stringify({
|
|
@@ -255,7 +257,7 @@ var RemoteTunnelManager = class {
|
|
|
255
257
|
if (!approvedViewers.has(email)) return browserWs.close(1008, "Not approved");
|
|
256
258
|
browserWs.on("message", (msg) => {
|
|
257
259
|
const hmrStart = Date.now();
|
|
258
|
-
|
|
260
|
+
logger.debug("browser -> cli browserWS message: %s", msg.toString());
|
|
259
261
|
cliWs.send(JSON.stringify({
|
|
260
262
|
type: "hmr:message",
|
|
261
263
|
body: msg.toString(),
|
|
@@ -268,7 +270,7 @@ var RemoteTunnelManager = class {
|
|
|
268
270
|
const data = JSON.parse(msg.toString());
|
|
269
271
|
if (data.type === "hmr:message") browserWs.send(data.body);
|
|
270
272
|
} catch {
|
|
271
|
-
|
|
273
|
+
logger.error("Failed to parse CLI message for HMR: %s", msg.toString().substring(0, 100));
|
|
272
274
|
}
|
|
273
275
|
};
|
|
274
276
|
cliWs.on("message", cliHandler);
|