@databricks/appkit 0.82.0 → 0.84.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/CLAUDE.md +21 -1
- package/dist/agents/databricks.d.ts +15 -3
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +25 -8
- package/dist/agents/databricks.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +6 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/agent/eval.js +1 -1
- package/dist/cli/commands/generate-types.js +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +28 -15
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/registry/add.js +3 -11
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js +1 -1
- package/dist/connectors/lakebase/index.js +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js +6 -12
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/context/caller-context.d.ts +25 -0
- package/dist/context/caller-context.d.ts.map +1 -0
- package/dist/context/caller-context.js +21 -0
- package/dist/context/caller-context.js.map +1 -0
- package/dist/context/deprecation.js +14 -0
- package/dist/context/deprecation.js.map +1 -0
- package/dist/context/execution-context.d.ts +40 -4
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js +87 -33
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +3 -1
- package/dist/context/request-scope.d.ts +2 -0
- package/dist/context/request-scope.js +39 -0
- package/dist/context/request-scope.js.map +1 -0
- package/dist/context/resource-capabilities.js +59 -0
- package/dist/context/resource-capabilities.js.map +1 -0
- package/dist/context/scoped-api.js +104 -0
- package/dist/context/scoped-api.js.map +1 -0
- package/dist/context/service-context.d.ts +18 -12
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +45 -55
- package/dist/context/service-context.js.map +1 -1
- package/dist/context/user-context.d.ts +16 -22
- package/dist/context/user-context.d.ts.map +1 -1
- package/dist/context/user-context.js +57 -3
- package/dist/context/user-context.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js +5 -2
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts +19 -6
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js +53 -17
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/types.d.ts +18 -1
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts +4 -3
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +34 -11
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/plugin-context.d.ts +14 -15
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +24 -14
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/base.d.ts +2 -2
- package/dist/errors/base.js +2 -2
- package/dist/errors/base.js.map +1 -1
- package/dist/errors/identity-expired.d.ts +14 -0
- package/dist/errors/identity-expired.d.ts.map +1 -0
- package/dist/errors/identity-expired.js +33 -0
- package/dist/errors/identity-expired.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +7 -2
- package/dist/logging/logger.js +1 -1
- package/dist/plugin/execution-result.d.ts +4 -1
- package/dist/plugin/execution-result.d.ts.map +1 -1
- package/dist/plugin/interceptors/telemetry.js +6 -4
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +14 -22
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +42 -123
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +13 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +65 -15
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/auth-mode.js +42 -0
- package/dist/plugins/agents/auth-mode.js.map +1 -0
- package/dist/plugins/agents/index.d.ts +1 -1
- package/dist/plugins/agents/mlflow.js +11 -3
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/plugins/agents/tool-dispatch.js +9 -1
- package/dist/plugins/agents/tool-dispatch.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js +4 -4
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +3 -3
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +9 -7
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/database/crud/contract.js +2 -2
- package/dist/plugins/database/crud/contract.js.map +1 -1
- package/dist/plugins/database/database.d.ts +1 -1
- package/dist/plugins/files/plugin.d.ts +21 -15
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +50 -29
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/genie/genie.d.ts +10 -1
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js +12 -3
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/genie/manifest.js +1 -0
- package/dist/plugins/jobs/plugin.d.ts +1 -1
- package/dist/plugins/jobs/plugin.js +2 -2
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +11 -18
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +16 -21
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js +1 -4
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/dev-obo-middleware.js +60 -0
- package/dist/plugins/server/dev-obo-middleware.js.map +1 -0
- package/dist/plugins/server/index.d.ts +1 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +3 -0
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/serving/manifest.js +1 -0
- package/dist/plugins/serving/serving.d.ts +1 -1
- package/dist/plugins/serving/serving.js +6 -6
- package/dist/plugins/serving/serving.js.map +1 -1
- package/dist/resources/index.d.ts +1 -0
- package/dist/resources/index.js +3 -0
- package/dist/resources/warehouse.d.ts +16 -0
- package/dist/resources/warehouse.d.ts.map +1 -0
- package/dist/resources/warehouse.js +93 -0
- package/dist/resources/warehouse.js.map +1 -0
- package/dist/schemas/manifest.d.ts +44 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js +122 -3
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/dev-obo.js +86 -0
- package/dist/shared/src/dev-obo.js.map +1 -0
- package/dist/shared/src/index.d.ts +1 -1
- package/dist/shared/src/plugin.d.ts +12 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +17 -0
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.js +53 -3
- package/dist/shared/src/schemas/manifest.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +5 -2
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/telemetry/execution-span-processor.js +21 -0
- package/dist/telemetry/execution-span-processor.js.map +1 -0
- package/dist/telemetry/telemetry-manager.js +2 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/testing/create-test-app.d.ts +2 -2
- package/dist/testing/create-test-app.js +2 -2
- package/dist/testing/create-test-app.js.map +1 -1
- package/dist/testing/fixtures.d.ts +3 -2
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +21 -14
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/reset-singletons.js +1 -1
- package/dist/testing/test-plugin-context.d.ts +3 -12
- package/dist/testing/test-plugin-context.d.ts.map +1 -1
- package/dist/testing/test-plugin-context.js +24 -16
- package/dist/testing/test-plugin-context.js.map +1 -1
- package/dist/utils/is-plain-object.js +11 -0
- package/dist/utils/is-plain-object.js.map +1 -0
- package/docs/api/appkit/Class.AppKitError.md +2 -1
- package/docs/api/appkit/Class.AuthenticationError.md +1 -1
- package/docs/api/appkit/Class.ConfigurationError.md +1 -1
- package/docs/api/appkit/Class.ConnectionError.md +1 -1
- package/docs/api/appkit/Class.DatabaseValidationError.md +1 -1
- package/docs/api/appkit/Class.ExecutionError.md +1 -1
- package/docs/api/appkit/Class.IdentityExpiredError.md +190 -0
- package/docs/api/appkit/Class.InitializationError.md +1 -1
- package/docs/api/appkit/Class.Plugin.md +9 -13
- package/docs/api/appkit/Class.ServerError.md +1 -1
- package/docs/api/appkit/Class.ServiceContext.md +169 -0
- package/docs/api/appkit/Class.TunnelError.md +1 -1
- package/docs/api/appkit/Class.ValidationError.md +1 -1
- package/docs/api/appkit/Function.createApp.md +12 -12
- package/docs/api/appkit/Function.getCallerContext.md +12 -0
- package/docs/api/appkit/Function.getCurrentActorId.md +12 -0
- package/docs/api/appkit/Function.getCurrentPrincipalKey.md +12 -0
- package/docs/api/appkit/Function.getCurrentUserId.md +14 -0
- package/docs/api/appkit/Function.getExecutionContext.md +5 -3
- package/docs/api/appkit/Function.getUserContext.md +16 -0
- package/docs/api/appkit/Function.getWarehouseId.md +20 -0
- package/docs/api/appkit/Function.isInUserContext.md +12 -0
- package/docs/api/appkit/Function.isUserContext.md +20 -0
- package/docs/api/appkit/Function.runAgent.md +1 -1
- package/docs/api/appkit/Function.runInCallerContext.md +27 -0
- package/docs/api/appkit/Function.runInUserContext.md +29 -0
- package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
- package/docs/api/appkit/Interface.AgentsPluginConfig.md +11 -0
- package/docs/api/appkit/Interface.CallerContext.md +54 -0
- package/docs/api/appkit/Interface.IndexConfig.md +1 -1
- package/docs/api/appkit/Interface.PluginManifest.md +33 -2
- package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
- package/docs/api/appkit/Interface.RunAgentInput.md +45 -1
- package/docs/api/appkit/TypeAlias.AgentAuth.md +8 -0
- package/docs/api/appkit/TypeAlias.AppKitApi.md +35 -0
- package/docs/api/appkit/TypeAlias.CallerPrincipal.md +13 -0
- package/docs/api/appkit/TypeAlias.ExecutionContext.md +9 -0
- package/docs/api/appkit/TypeAlias.ExecutionResult.md +65 -0
- package/docs/api/appkit/TypeAlias.ScopedPluginMap.md +12 -0
- package/docs/api/appkit/TypeAlias.UserContext.md +109 -0
- package/docs/api/appkit/TypeAlias.UserScopedApp.md +39 -0
- package/docs/api/appkit.md +20 -0
- package/docs/plugins/agents.md +45 -0
- package/docs/plugins/analytics.md +8 -0
- package/docs/plugins/execution-context.md +101 -50
- package/docs/plugins/lakebase.md +6 -82
- package/llms.txt +21 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-config-sanitizer.js","names":[],"sources":["../../../src/plugins/server/client-config-sanitizer.ts"],"sourcesContent":["import pc from \"picocolors\";\n\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"server:config\");\n\nconst REDACTED_CLIENT_CONFIG_VALUE = \"[redacted by appkit]\";\nconst MIN_SUBSTRING_LENGTH = 3;\nconst DISALLOWED_CLIENT_CONFIG_KEYS = new Set([\n \"__proto__\",\n \"constructor\",\n \"prototype\",\n]);\n\n/**\n * Builds a Map of non-public env var values (value -> key names)\n * and a Set of public env var values for overlap resolution.\n */\nfunction getEnvValueSets(): {\n nonPublic: Map<string, string[]>;\n publicValues: Set<string>;\n} {\n const nonPublic = new Map<string, string[]>();\n const publicValues = new Set<string>();\n for (const [key, value] of Object.entries(process.env)) {\n if (!value) continue;\n if (key.startsWith(\"PUBLIC_APPKIT_\")) {\n publicValues.add(value);\n } else {\n const existing = nonPublic.get(value);\n if (existing) {\n existing.push(key);\n } else {\n nonPublic.set(value, [key]);\n }\n }\n }\n return { nonPublic, publicValues };\n}\n\nfunction getMatchRanges(\n haystack: string,\n needle: string,\n): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n let startIndex = 0;\n\n while (startIndex < haystack.length) {\n const matchIndex = haystack.indexOf(needle, startIndex);\n if (matchIndex === -1) {\n break;\n }\n ranges.push([matchIndex, matchIndex + needle.length]);\n startIndex = matchIndex + 1;\n }\n\n return ranges;\n}\n\nfunction isSecretCoveredByPublicValue(\n value: string,\n envValue: string,\n publicValues: Set<string>,\n): boolean {\n const publicRanges = [...publicValues]\n .filter((publicValue) => publicValue.includes(envValue))\n .flatMap((publicValue) => getMatchRanges(value, publicValue));\n\n if (publicRanges.length === 0) {\n return false;\n }\n\n return getMatchRanges(value, envValue).every(([secretStart, secretEnd]) =>\n publicRanges.some(\n ([publicStart, publicEnd]) =>\n publicStart <= secretStart && publicEnd >= secretEnd,\n ),\n );\n}\n\nfunction isPlainObject(value: object): boolean {\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction invalidClientConfig(\n pluginName: string,\n path: string,\n message: string,\n): Error {\n return new Error(\n `Plugin '${pluginName}' clientConfig() ${message} at ${path}. Only JSON-serializable plain data is supported.`,\n );\n}\n\nfunction assertSafeClientConfigKey(\n pluginName: string,\n key: string,\n path: string,\n): void {\n if (DISALLOWED_CLIENT_CONFIG_KEYS.has(key)) {\n throw invalidClientConfig(\n pluginName,\n `${path}.${key}`,\n \"contains a reserved key\",\n );\n }\n}\n\nfunction validateClientConfigValue(\n pluginName: string,\n value: unknown,\n path: string,\n stack: WeakSet<object>,\n): unknown {\n if (value === null) return null;\n\n switch (typeof value) {\n case \"string\":\n case \"boolean\":\n return value;\n case \"number\":\n if (!Number.isFinite(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a non-finite number\",\n );\n }\n return value;\n case \"bigint\":\n throw invalidClientConfig(pluginName, path, \"contains a BigInt\");\n case \"undefined\":\n return undefined;\n case \"function\":\n throw invalidClientConfig(pluginName, path, \"contains a function\");\n case \"symbol\":\n throw invalidClientConfig(pluginName, path, \"contains a symbol\");\n }\n\n if (Array.isArray(value)) {\n if (stack.has(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a circular reference\",\n );\n }\n\n stack.add(value);\n const result = value.map(\n (item, index) =>\n validateClientConfigValue(\n pluginName,\n item,\n `${path}[${index}]`,\n stack,\n ) ?? null,\n );\n stack.delete(value);\n return result;\n }\n\n if (typeof value === \"object\") {\n if (!isPlainObject(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a non-plain object\",\n );\n }\n if (stack.has(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a circular reference\",\n );\n }\n\n stack.add(value);\n const result: Record<string, unknown> = {};\n for (const [key, nestedValue] of Object.entries(value)) {\n assertSafeClientConfigKey(pluginName, key, path);\n const normalizedValue = validateClientConfigValue(\n pluginName,\n nestedValue,\n `${path}.${key}`,\n stack,\n );\n if (normalizedValue !== undefined) {\n result[key] = normalizedValue;\n }\n }\n stack.delete(value);\n return result;\n }\n\n throw invalidClientConfig(pluginName, path, \"contains an unsupported value\");\n}\n\nfunction validateClientConfig(\n pluginName: string,\n config: unknown,\n): Record<string, unknown> {\n if (config === null || typeof config !== \"object\" || Array.isArray(config)) {\n throw new Error(\n `Plugin '${pluginName}' clientConfig() must return a plain object.`,\n );\n }\n\n return validateClientConfigValue(\n pluginName,\n config,\n \"clientConfig()\",\n new WeakSet(),\n ) as Record<string, unknown>;\n}\n\n/**\n * Redacts a string when it contains a non-public env var value. Exact matches\n * are caught regardless of length; substring containment requires the env value\n * to be at least MIN_SUBSTRING_LENGTH chars to avoid false positives from very\n * short values.\n */\nfunction redactLeakedString(\n value: string,\n nonPublicValues: Map<string, string[]>,\n publicValues: Set<string>,\n leakedVars: Set<string>,\n): string {\n for (const [envValue, envKeys] of nonPublicValues) {\n if (value === envValue && !publicValues.has(envValue)) {\n for (const k of envKeys) leakedVars.add(k);\n return REDACTED_CLIENT_CONFIG_VALUE;\n }\n if (\n envValue.length >= MIN_SUBSTRING_LENGTH &&\n value.includes(envValue) &&\n !isSecretCoveredByPublicValue(value, envValue, publicValues)\n ) {\n for (const k of envKeys) leakedVars.add(k);\n return REDACTED_CLIENT_CONFIG_VALUE;\n }\n }\n\n return value;\n}\n\nfunction redactLeakedValues(\n obj: unknown,\n nonPublicValues: Map<string, string[]>,\n publicValues: Set<string>,\n leakedVars: Set<string>,\n): unknown {\n if (typeof obj === \"string\") {\n return redactLeakedString(obj, nonPublicValues, publicValues, leakedVars);\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) =>\n redactLeakedValues(item, nonPublicValues, publicValues, leakedVars),\n );\n }\n\n if (obj !== null && typeof obj === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const redactedKey = redactLeakedString(\n key,\n nonPublicValues,\n publicValues,\n leakedVars,\n );\n const uniqueKey = getUniqueObjectKey(redactedKey, result);\n result[uniqueKey] = redactLeakedValues(\n value,\n nonPublicValues,\n publicValues,\n leakedVars,\n );\n }\n return result;\n }\n\n return obj;\n}\n\nfunction getUniqueObjectKey(\n key: string,\n result: Record<string, unknown>,\n): string {\n if (!Object.hasOwn(result, key)) {\n return key;\n }\n\n let suffix = 2;\n let candidate = `${key} (${suffix})`;\n while (Object.hasOwn(result, candidate)) {\n suffix += 1;\n candidate = `${key} (${suffix})`;\n }\n\n return candidate;\n}\n\n/**\n * Scans a plugin's clientConfig return value for string values that\n * match or contain non-public environment variable values. Matches are\n * replaced with \"[redacted by appkit]\" and a warning is logged.\n *\n * Only env vars prefixed with `PUBLIC_APPKIT_` are allowed through;\n * all other process.env values are treated as sensitive.\n */\nexport function sanitizeClientConfig(\n pluginName: string,\n config: unknown,\n): Record<string, unknown> {\n const validated = validateClientConfig(pluginName, config);\n const { nonPublic, publicValues } = getEnvValueSets();\n if (nonPublic.size === 0) return validated;\n\n const leakedVars = new Set<string>();\n const sanitized = redactLeakedValues(\n validated,\n nonPublic,\n publicValues,\n leakedVars,\n ) as Record<string, unknown>;\n\n if (leakedVars.size > 0) {\n const banner = formatLeakedVarsBanner(pluginName, leakedVars);\n logger.warn(\"\\n\\n%s\\n\", banner);\n }\n\n return sanitized;\n}\n\nfunction formatLeakedVarsBanner(\n pluginName: string,\n leakedVars: Set<string>,\n): string {\n const s = leakedVars.size === 1 ? \"\" : \"s\";\n const contentLines: string[] = [\n `${pc.bold(pluginName)}.clientConfig() contained ${pc.bold(String(leakedVars.size))} env var value${s}`,\n `that would have been sent to the browser. AppKit ${pc.green(\"redacted\")} them automatically.`,\n \"\",\n ...Array.from(leakedVars, (v) => ` ${pc.red(\"-\")} ${pc.yellow(v)}`),\n \"\",\n `To intentionally expose a value, set a matching ${pc.green(\"PUBLIC_APPKIT_\")} variable.`,\n `Example: ${pc.dim('PUBLIC_APPKIT_MY_VAR=\"safe-value\"')}`,\n ];\n\n // oxlint-disable-next-line no-control-regex -- stripping ANSI escape sequences requires matching the ESC control character\n const stripAnsi = (str: string) => str.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n const maxLen = Math.max(...contentLines.map((l) => stripAnsi(l).length));\n const border = pc.yellow(\"=\".repeat(maxLen + 4));\n const boxed = contentLines.map(\n (line) =>\n `${pc.yellow(\"|\")} ${line}${\" \".repeat(maxLen - stripAnsi(line).length)} ${pc.yellow(\"|\")}`,\n );\n\n return [border, ...boxed, border].join(\"\\n\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client-config-sanitizer.js","names":[],"sources":["../../../src/plugins/server/client-config-sanitizer.ts"],"sourcesContent":["import pc from \"picocolors\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { isPlainObject } from \"../../utils/is-plain-object\";\n\nconst logger = createLogger(\"server:config\");\n\nconst REDACTED_CLIENT_CONFIG_VALUE = \"[redacted by appkit]\";\nconst MIN_SUBSTRING_LENGTH = 3;\nconst DISALLOWED_CLIENT_CONFIG_KEYS = new Set([\n \"__proto__\",\n \"constructor\",\n \"prototype\",\n]);\n\n/**\n * Builds a Map of non-public env var values (value -> key names)\n * and a Set of public env var values for overlap resolution.\n */\nfunction getEnvValueSets(): {\n nonPublic: Map<string, string[]>;\n publicValues: Set<string>;\n} {\n const nonPublic = new Map<string, string[]>();\n const publicValues = new Set<string>();\n for (const [key, value] of Object.entries(process.env)) {\n if (!value) continue;\n if (key.startsWith(\"PUBLIC_APPKIT_\")) {\n publicValues.add(value);\n } else {\n const existing = nonPublic.get(value);\n if (existing) {\n existing.push(key);\n } else {\n nonPublic.set(value, [key]);\n }\n }\n }\n return { nonPublic, publicValues };\n}\n\nfunction getMatchRanges(\n haystack: string,\n needle: string,\n): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n let startIndex = 0;\n\n while (startIndex < haystack.length) {\n const matchIndex = haystack.indexOf(needle, startIndex);\n if (matchIndex === -1) {\n break;\n }\n ranges.push([matchIndex, matchIndex + needle.length]);\n startIndex = matchIndex + 1;\n }\n\n return ranges;\n}\n\nfunction isSecretCoveredByPublicValue(\n value: string,\n envValue: string,\n publicValues: Set<string>,\n): boolean {\n const publicRanges = [...publicValues]\n .filter((publicValue) => publicValue.includes(envValue))\n .flatMap((publicValue) => getMatchRanges(value, publicValue));\n\n if (publicRanges.length === 0) {\n return false;\n }\n\n return getMatchRanges(value, envValue).every(([secretStart, secretEnd]) =>\n publicRanges.some(\n ([publicStart, publicEnd]) =>\n publicStart <= secretStart && publicEnd >= secretEnd,\n ),\n );\n}\n\nfunction invalidClientConfig(\n pluginName: string,\n path: string,\n message: string,\n): Error {\n return new Error(\n `Plugin '${pluginName}' clientConfig() ${message} at ${path}. Only JSON-serializable plain data is supported.`,\n );\n}\n\nfunction assertSafeClientConfigKey(\n pluginName: string,\n key: string,\n path: string,\n): void {\n if (DISALLOWED_CLIENT_CONFIG_KEYS.has(key)) {\n throw invalidClientConfig(\n pluginName,\n `${path}.${key}`,\n \"contains a reserved key\",\n );\n }\n}\n\nfunction validateClientConfigValue(\n pluginName: string,\n value: unknown,\n path: string,\n stack: WeakSet<object>,\n): unknown {\n if (value === null) return null;\n\n switch (typeof value) {\n case \"string\":\n case \"boolean\":\n return value;\n case \"number\":\n if (!Number.isFinite(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a non-finite number\",\n );\n }\n return value;\n case \"bigint\":\n throw invalidClientConfig(pluginName, path, \"contains a BigInt\");\n case \"undefined\":\n return undefined;\n case \"function\":\n throw invalidClientConfig(pluginName, path, \"contains a function\");\n case \"symbol\":\n throw invalidClientConfig(pluginName, path, \"contains a symbol\");\n }\n\n if (Array.isArray(value)) {\n if (stack.has(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a circular reference\",\n );\n }\n\n stack.add(value);\n const result = value.map(\n (item, index) =>\n validateClientConfigValue(\n pluginName,\n item,\n `${path}[${index}]`,\n stack,\n ) ?? null,\n );\n stack.delete(value);\n return result;\n }\n\n if (typeof value === \"object\") {\n if (!isPlainObject(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a non-plain object\",\n );\n }\n if (stack.has(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a circular reference\",\n );\n }\n\n stack.add(value);\n const result: Record<string, unknown> = {};\n for (const [key, nestedValue] of Object.entries(value)) {\n assertSafeClientConfigKey(pluginName, key, path);\n const normalizedValue = validateClientConfigValue(\n pluginName,\n nestedValue,\n `${path}.${key}`,\n stack,\n );\n if (normalizedValue !== undefined) {\n result[key] = normalizedValue;\n }\n }\n stack.delete(value);\n return result;\n }\n\n throw invalidClientConfig(pluginName, path, \"contains an unsupported value\");\n}\n\nfunction validateClientConfig(\n pluginName: string,\n config: unknown,\n): Record<string, unknown> {\n if (config === null || typeof config !== \"object\" || Array.isArray(config)) {\n throw new Error(\n `Plugin '${pluginName}' clientConfig() must return a plain object.`,\n );\n }\n\n return validateClientConfigValue(\n pluginName,\n config,\n \"clientConfig()\",\n new WeakSet(),\n ) as Record<string, unknown>;\n}\n\n/**\n * Redacts a string when it contains a non-public env var value. Exact matches\n * are caught regardless of length; substring containment requires the env value\n * to be at least MIN_SUBSTRING_LENGTH chars to avoid false positives from very\n * short values.\n */\nfunction redactLeakedString(\n value: string,\n nonPublicValues: Map<string, string[]>,\n publicValues: Set<string>,\n leakedVars: Set<string>,\n): string {\n for (const [envValue, envKeys] of nonPublicValues) {\n if (value === envValue && !publicValues.has(envValue)) {\n for (const k of envKeys) leakedVars.add(k);\n return REDACTED_CLIENT_CONFIG_VALUE;\n }\n if (\n envValue.length >= MIN_SUBSTRING_LENGTH &&\n value.includes(envValue) &&\n !isSecretCoveredByPublicValue(value, envValue, publicValues)\n ) {\n for (const k of envKeys) leakedVars.add(k);\n return REDACTED_CLIENT_CONFIG_VALUE;\n }\n }\n\n return value;\n}\n\nfunction redactLeakedValues(\n obj: unknown,\n nonPublicValues: Map<string, string[]>,\n publicValues: Set<string>,\n leakedVars: Set<string>,\n): unknown {\n if (typeof obj === \"string\") {\n return redactLeakedString(obj, nonPublicValues, publicValues, leakedVars);\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) =>\n redactLeakedValues(item, nonPublicValues, publicValues, leakedVars),\n );\n }\n\n if (obj !== null && typeof obj === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const redactedKey = redactLeakedString(\n key,\n nonPublicValues,\n publicValues,\n leakedVars,\n );\n const uniqueKey = getUniqueObjectKey(redactedKey, result);\n result[uniqueKey] = redactLeakedValues(\n value,\n nonPublicValues,\n publicValues,\n leakedVars,\n );\n }\n return result;\n }\n\n return obj;\n}\n\nfunction getUniqueObjectKey(\n key: string,\n result: Record<string, unknown>,\n): string {\n if (!Object.hasOwn(result, key)) {\n return key;\n }\n\n let suffix = 2;\n let candidate = `${key} (${suffix})`;\n while (Object.hasOwn(result, candidate)) {\n suffix += 1;\n candidate = `${key} (${suffix})`;\n }\n\n return candidate;\n}\n\n/**\n * Scans a plugin's clientConfig return value for string values that\n * match or contain non-public environment variable values. Matches are\n * replaced with \"[redacted by appkit]\" and a warning is logged.\n *\n * Only env vars prefixed with `PUBLIC_APPKIT_` are allowed through;\n * all other process.env values are treated as sensitive.\n */\nexport function sanitizeClientConfig(\n pluginName: string,\n config: unknown,\n): Record<string, unknown> {\n const validated = validateClientConfig(pluginName, config);\n const { nonPublic, publicValues } = getEnvValueSets();\n if (nonPublic.size === 0) return validated;\n\n const leakedVars = new Set<string>();\n const sanitized = redactLeakedValues(\n validated,\n nonPublic,\n publicValues,\n leakedVars,\n ) as Record<string, unknown>;\n\n if (leakedVars.size > 0) {\n const banner = formatLeakedVarsBanner(pluginName, leakedVars);\n logger.warn(\"\\n\\n%s\\n\", banner);\n }\n\n return sanitized;\n}\n\nfunction formatLeakedVarsBanner(\n pluginName: string,\n leakedVars: Set<string>,\n): string {\n const s = leakedVars.size === 1 ? \"\" : \"s\";\n const contentLines: string[] = [\n `${pc.bold(pluginName)}.clientConfig() contained ${pc.bold(String(leakedVars.size))} env var value${s}`,\n `that would have been sent to the browser. AppKit ${pc.green(\"redacted\")} them automatically.`,\n \"\",\n ...Array.from(leakedVars, (v) => ` ${pc.red(\"-\")} ${pc.yellow(v)}`),\n \"\",\n `To intentionally expose a value, set a matching ${pc.green(\"PUBLIC_APPKIT_\")} variable.`,\n `Example: ${pc.dim('PUBLIC_APPKIT_MY_VAR=\"safe-value\"')}`,\n ];\n\n // oxlint-disable-next-line no-control-regex -- stripping ANSI escape sequences requires matching the ESC control character\n const stripAnsi = (str: string) => str.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n const maxLen = Math.max(...contentLines.map((l) => stripAnsi(l).length));\n const border = pc.yellow(\"=\".repeat(maxLen + 4));\n const boxed = contentLines.map(\n (line) =>\n `${pc.yellow(\"|\")} ${line}${\" \".repeat(maxLen - stripAnsi(line).length)} ${pc.yellow(\"|\")}`,\n );\n\n return [border, ...boxed, border].join(\"\\n\");\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createDevOboIdentityProvider, loadDevOboIdentityFromEnvironment } from "../../shared/src/dev-obo.js";
|
|
2
|
+
import { createLogger } from "../../logging/logger.js";
|
|
3
|
+
|
|
4
|
+
//#region src/plugins/server/dev-obo-middleware.ts
|
|
5
|
+
const logger = createLogger("server:dev-obo");
|
|
6
|
+
const loopbackHosts = new Set([
|
|
7
|
+
"localhost",
|
|
8
|
+
"127.0.0.1",
|
|
9
|
+
"[::1]"
|
|
10
|
+
]);
|
|
11
|
+
const loopbackPeers = new Set([
|
|
12
|
+
"127.0.0.1",
|
|
13
|
+
"::1",
|
|
14
|
+
"::ffff:127.0.0.1"
|
|
15
|
+
]);
|
|
16
|
+
function isLocalRequest(req) {
|
|
17
|
+
if (!loopbackPeers.has(req.socket.remoteAddress ?? "")) return false;
|
|
18
|
+
try {
|
|
19
|
+
const origin = new URL(`http://${req.headers.host}`);
|
|
20
|
+
if (!loopbackHosts.has(origin.hostname) || origin.host !== req.headers.host || Number(origin.port || 80) !== req.socket.localPort) return false;
|
|
21
|
+
if (req.headers.origin && req.headers.origin !== origin.origin) return false;
|
|
22
|
+
const site = req.headers["sec-fetch-site"];
|
|
23
|
+
return !site || site === "same-origin" || site === "none";
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Inject local user headers without changing the default execution principal. */
|
|
29
|
+
function createDevOboMiddleware() {
|
|
30
|
+
const token = process.env.DATABRICKS_TOKEN?.trim();
|
|
31
|
+
const profile = process.env.DATABRICKS_CONFIG_PROFILE?.trim();
|
|
32
|
+
if (process.env.NODE_ENV !== "development" || process.env.APPKIT_DEV_OBO === "false" || !token && !profile) return void 0;
|
|
33
|
+
const getIdentity = createDevOboIdentityProvider(() => loadDevOboIdentityFromEnvironment());
|
|
34
|
+
logger.info(`Local OBO headers enabled using ${token ? "DATABRICKS_TOKEN" : "DATABRICKS_CONFIG_PROFILE"}. Open the app on localhost. Set APPKIT_DEV_OBO=false to disable.`);
|
|
35
|
+
return async (req, res, next) => {
|
|
36
|
+
if (req.header("x-forwarded-access-token")?.trim()) {
|
|
37
|
+
next();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!isLocalRequest(req)) {
|
|
41
|
+
res.status(403).json({ error: "Local OBO emulation only accepts same-origin loopback requests." });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const identity = await getIdentity();
|
|
46
|
+
req.headers["x-forwarded-access-token"] = identity.token;
|
|
47
|
+
req.headers["x-forwarded-user"] = identity.userId;
|
|
48
|
+
delete req.headers["x-forwarded-email"];
|
|
49
|
+
if (identity.email) req.headers["x-forwarded-email"] = identity.email;
|
|
50
|
+
} catch {
|
|
51
|
+
res.status(401).json({ error: "Local OBO credentials unavailable. Verify DATABRICKS_TOKEN and DATABRICKS_HOST, or authenticate the user profile in DATABRICKS_CONFIG_PROFILE, then retry." });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
next();
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { createDevOboMiddleware };
|
|
60
|
+
//# sourceMappingURL=dev-obo-middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-obo-middleware.js","names":[],"sources":["../../../src/plugins/server/dev-obo-middleware.ts"],"sourcesContent":["import type { Request, RequestHandler } from \"express\";\nimport {\n createDevOboIdentityProvider,\n loadDevOboIdentityFromEnvironment,\n} from \"shared\";\n\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"server:dev-obo\");\nconst loopbackHosts = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\nconst loopbackPeers = new Set([\"127.0.0.1\", \"::1\", \"::ffff:127.0.0.1\"]);\n\nfunction isLocalRequest(req: Request): boolean {\n // Check the socket, not Express's proxy-aware IP or forwarded headers.\n if (!loopbackPeers.has(req.socket.remoteAddress ?? \"\")) return false;\n try {\n const origin = new URL(`http://${req.headers.host}`);\n if (\n !loopbackHosts.has(origin.hostname) ||\n origin.host !== req.headers.host ||\n Number(origin.port || 80) !== req.socket.localPort\n )\n return false;\n if (req.headers.origin && req.headers.origin !== origin.origin)\n return false;\n const site = req.headers[\"sec-fetch-site\"];\n return !site || site === \"same-origin\" || site === \"none\";\n } catch {\n return false;\n }\n}\n\n/** Inject local user headers without changing the default execution principal. */\nexport function createDevOboMiddleware(): RequestHandler | undefined {\n const token = process.env.DATABRICKS_TOKEN?.trim();\n const profile = process.env.DATABRICKS_CONFIG_PROFILE?.trim();\n if (\n process.env.NODE_ENV !== \"development\" ||\n process.env.APPKIT_DEV_OBO === \"false\" ||\n (!token && !profile)\n )\n return undefined;\n\n const getIdentity = createDevOboIdentityProvider(() =>\n loadDevOboIdentityFromEnvironment(),\n );\n logger.info(\n `Local OBO headers enabled using ${token ? \"DATABRICKS_TOKEN\" : \"DATABRICKS_CONFIG_PROFILE\"}. Open the app on localhost. Set APPKIT_DEV_OBO=false to disable.`,\n );\n\n return async (req, res, next) => {\n // Preserve explicit forwarded identities.\n if (req.header(\"x-forwarded-access-token\")?.trim()) {\n next();\n return;\n }\n if (!isLocalRequest(req)) {\n res.status(403).json({\n error:\n \"Local OBO emulation only accepts same-origin loopback requests.\",\n });\n return;\n }\n try {\n const identity = await getIdentity();\n req.headers[\"x-forwarded-access-token\"] = identity.token;\n req.headers[\"x-forwarded-user\"] = identity.userId;\n delete req.headers[\"x-forwarded-email\"];\n if (identity.email) req.headers[\"x-forwarded-email\"] = identity.email;\n } catch {\n // Never fall back to SP or expose credential-bearing CLI errors.\n res.status(401).json({\n error:\n \"Local OBO credentials unavailable. Verify DATABRICKS_TOKEN and DATABRICKS_HOST, or authenticate the user profile in DATABRICKS_CONFIG_PROFILE, then retry.\",\n });\n return;\n }\n next();\n };\n}\n"],"mappings":";;;;AAQA,MAAM,SAAS,aAAa,iBAAiB;AAC7C,MAAM,gBAAgB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAQ,CAAC;AAClE,MAAM,gBAAgB,IAAI,IAAI;CAAC;CAAa;CAAO;CAAmB,CAAC;AAEvE,SAAS,eAAe,KAAuB;AAE7C,KAAI,CAAC,cAAc,IAAI,IAAI,OAAO,iBAAiB,GAAG,CAAE,QAAO;AAC/D,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,UAAU,IAAI,QAAQ,OAAO;AACpD,MACE,CAAC,cAAc,IAAI,OAAO,SAAS,IACnC,OAAO,SAAS,IAAI,QAAQ,QAC5B,OAAO,OAAO,QAAQ,GAAG,KAAK,IAAI,OAAO,UAEzC,QAAO;AACT,MAAI,IAAI,QAAQ,UAAU,IAAI,QAAQ,WAAW,OAAO,OACtD,QAAO;EACT,MAAM,OAAO,IAAI,QAAQ;AACzB,SAAO,CAAC,QAAQ,SAAS,iBAAiB,SAAS;SAC7C;AACN,SAAO;;;;AAKX,SAAgB,yBAAqD;CACnE,MAAM,QAAQ,QAAQ,IAAI,kBAAkB,MAAM;CAClD,MAAM,UAAU,QAAQ,IAAI,2BAA2B,MAAM;AAC7D,KACE,QAAQ,IAAI,aAAa,iBACzB,QAAQ,IAAI,mBAAmB,WAC9B,CAAC,SAAS,CAAC,QAEZ,QAAO;CAET,MAAM,cAAc,mCAClB,mCAAmC,CACpC;AACD,QAAO,KACL,mCAAmC,QAAQ,qBAAqB,4BAA4B,mEAC7F;AAED,QAAO,OAAO,KAAK,KAAK,SAAS;AAE/B,MAAI,IAAI,OAAO,2BAA2B,EAAE,MAAM,EAAE;AAClD,SAAM;AACN;;AAEF,MAAI,CAAC,eAAe,IAAI,EAAE;AACxB,OAAI,OAAO,IAAI,CAAC,KAAK,EACnB,OACE,mEACH,CAAC;AACF;;AAEF,MAAI;GACF,MAAM,WAAW,MAAM,aAAa;AACpC,OAAI,QAAQ,8BAA8B,SAAS;AACnD,OAAI,QAAQ,sBAAsB,SAAS;AAC3C,UAAO,IAAI,QAAQ;AACnB,OAAI,SAAS,MAAO,KAAI,QAAQ,uBAAuB,SAAS;UAC1D;AAEN,OAAI,OAAO,IAAI,CAAC,KAAK,EACnB,OACE,8JACH,CAAC;AACF;;AAEF,QAAM"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { StreamConfig } from "../../shared/src/execute.js";
|
|
2
2
|
import { PluginPhase, TelemetryOptions, ToPlugin } from "../../shared/src/plugin.js";
|
|
3
3
|
import "../../shared/src/index.js";
|
|
4
|
+
import { PluginManifest } from "../../registry/types.js";
|
|
4
5
|
import { Plugin } from "../../plugin/plugin.js";
|
|
5
6
|
import "../../plugin/index.js";
|
|
6
|
-
import { PluginManifest } from "../../registry/types.js";
|
|
7
7
|
import { ServerConfig } from "./types.js";
|
|
8
8
|
import "../../index.js";
|
|
9
9
|
import { Server } from "node:http";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;AAsDA;;;;;;;;;;;;;;;cAAa,YAAA,SAAqB,MAAA;EAAA,OAClB,cAAA;;;;EA8JS;;;;;;EAAA,wBAnJC,uBAAA;;SAGjB,QAAA,EAfiB,cAAA;EAAA,QAgBhB,iBAAA;EAAA,QACA,MAAA;EAAA,QACA,aAAA;EAAA,QACA,sBAAA;EAFA;EAAA,QAIA,kBAAA;EAAA,UACU,MAAA,EAAQ,YAAA;EAAA,QAClB,gBAAA;EAAA,QACA,YAAA;EAFkB;;;;;EAAA,QAQlB,YAAA;EAAA,OACD,KAAA,EAAO,WAAA;cAEF,MAAA,EAAQ,YAAA;EAepB,aAAA,CAAc,IAAA,GAAM,UAAA,CAAW,MAAA;EAA/B;EAUM,KAAA,CAAA,GAAK,OAAA;EAVoB;EAa/B,SAAA,CAAA;IAAA;;;;;;gBAHW,gBAAA;;;;;;;;;;;EAiBL,KAAA,CAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,WAAA;EAoFlB;;;;;;;;EAAb,SAAA,CAAA,GAAa,MAAA;EAiCK;;;;;;;;;;;;;;EAXlB,MAAA,CAAO,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;EAsRT;;;;;;EA3QhB,YAAA,CAAa,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;;;;;;;;UAWjB,YAAA;;;;AAmYhB;;;UAhUgB,aAAA;EAAA,eA4CC,cAAA;EAoRE;;;;;EAAA,QAlQH,iBAAA;EAAA,QAqBN,cAAA;EA6OS;;;;;;;;;;;;EAlMjB,qBAAA,CAAA;;;;;;;;EAmBM,QAAA,CAAA,GAAY,OAAA;;;;;;;;;;;;UAoBJ,yBAAA;;;;;EAoBd,OAAA,CAAA;yEAIgB,GAAA,EAAK,OAAA,CAAQ,WAAA;qBA5ShB,MAAA;;;;;;;;kBAAU,gBAAA;;;;;;;;;cA+aZ,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,YAAA"}
|
|
@@ -13,6 +13,7 @@ import "../../plugin/index.js";
|
|
|
13
13
|
import { defineManifest } from "../../registry/manifest-loader.js";
|
|
14
14
|
import "../../registry/index.js";
|
|
15
15
|
import { sanitizeClientConfig } from "./client-config-sanitizer.js";
|
|
16
|
+
import { createDevOboMiddleware } from "./dev-obo-middleware.js";
|
|
16
17
|
import manifest_default from "./manifest.js";
|
|
17
18
|
import { RemoteTunnelController } from "./remote-tunnel/remote-tunnel-controller.js";
|
|
18
19
|
import { getRoutes, printRoutes } from "./utils.js";
|
|
@@ -110,6 +111,8 @@ var ServerPlugin = class ServerPlugin extends Plugin {
|
|
|
110
111
|
*/
|
|
111
112
|
async start() {
|
|
112
113
|
this.serverApplication.use(requestMetricsMiddleware);
|
|
114
|
+
const devObo = createDevOboMiddleware();
|
|
115
|
+
if (devObo) this.serverApplication.use(devObo);
|
|
113
116
|
this.serverApplication.use(express.json({
|
|
114
117
|
limit: this.config.bodyLimit ?? "1mb",
|
|
115
118
|
type: (req) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\n\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { camelToKebab } from \"shared\";\n\nimport { AppKitError, ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\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 * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /**\n * Budget for awaiting `server.close()` during shutdown. Bounded because\n * `closeAllConnections()` runs immediately before the await, so the close\n * is expected to resolve promptly; the core lifecycle manager's overall\n * force-exit timer is the ultimate backstop if it does not.\n */\n private static readonly SERVER_CLOSE_TIMEOUT_MS = 2_000;\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = defineManifest<\"server\">(manifest);\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n declare protected config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n /**\n * Resolves when `server.close()` completes. Created in\n * {@link abortActiveOperations} (which initiates the close) and awaited in\n * {@link closeRemainingConnections} after other plugins have drained.\n */\n private serverClosed?: Promise<void>;\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\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(requestMetricsMiddleware);\n this.serverApplication.use(\n express.json({\n // Express's stock 100kb default is too tight for modern apps —\n // agent chat payloads and any base64-encoded upload (e.g. the\n // dev playground's smart-dashboard \"save view\" screenshot at\n // ~105KB) blow past it instantly. Raise to 1mb by default and\n // let consumers tune via `server({ bodyLimit })` if they need\n // more headroom.\n limit: this.config.bodyLimit ?? \"1mb\",\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = 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, pluginConfigs);\n\n // Terminal error handler — registered after all routes, extensions, and\n // frontend middleware so that errors forwarded via next(err) from any of\n // them (e.g. async handler rejections forwarded by Plugin.route()) are\n // turned into JSON error responses instead of hanging the request.\n this.serverApplication.use(errorHandlerMiddleware);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\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 // Graceful shutdown is orchestrated by the core LifecycleManager, which\n // owns the process signal handlers. This plugin only contributes its\n // HTTP concerns: it aborts/initiates the socket close in\n // abortActiveOperations(), drains dev servers in shutdown(), and force-\n // closes remaining sockets in the \"shutdown\" lifecycle hook below (which\n // fires after every plugin's shutdown() has run).\n this.context?.onLifecycle(\"shutdown\", () =>\n this.closeRemainingConnections(),\n );\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\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 has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\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 * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * Note: async handlers registered directly on the app must handle their own\n * rejections — Express 4 does not forward rejected promises to the error\n * middleware. Errors passed explicitly to `next(err)` are formatted by the\n * terminal error middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\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 * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\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 plugins.values()) {\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/${camelToKebab(plugin.name)}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\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(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\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 pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\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 pluginConfigs,\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 /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n 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 /**\n * Cancel in-flight work and begin closing the HTTP server.\n *\n * Runs in the first phase of the core LifecycleManager's graceful shutdown,\n * before any plugin `shutdown()` hook. In addition to the base behavior\n * (aborting SSE streams), it stops the server accepting new connections and\n * drops idle keep-alive sockets — without `closeIdleConnections()`, a\n * connected browser would pin `server.close()` open until the force-exit\n * timer fires. The close is only *initiated* here; the returned promise is\n * awaited later in {@link closeRemainingConnections} so peer plugins can\n * still drain in-flight requests through the server first.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n\n if (this.server) {\n const server = this.server;\n this.serverClosed = new Promise((resolve) => {\n server.close(() => resolve());\n });\n server.closeIdleConnections();\n }\n }\n\n /**\n * Drain the dev-only servers as this plugin's `shutdown()` hook.\n *\n * Runs concurrently with the other plugins' hooks during graceful shutdown.\n * These are no-ops in production (neither is created), so this hook is\n * effectively dev-only.\n */\n async shutdown(): Promise<void> {\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n }\n\n /**\n * Force-close whatever sockets remain and await the server close.\n *\n * Registered on the `\"shutdown\"` lifecycle event in {@link start}, so it\n * fires after every plugin's `shutdown()` hook has run — meaning any\n * in-flight request a peer plugin was draining has already completed.\n * `closeAllConnections()` destroys the leftover sockets (aborted SSE\n * responses, keep-alive connections) synchronously so the pending\n * `server.close()` can complete; the await is bounded because the close is\n * expected to resolve promptly once the sockets are gone.\n */\n private async closeRemainingConnections(): Promise<void> {\n if (this.server) {\n this.server.closeAllConnections();\n }\n if (!this.serverClosed) return;\n\n await Promise.race([\n this.serverClosed,\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ServerPlugin.SERVER_CLOSE_TIMEOUT_MS);\n timer.unref();\n }),\n ]);\n logger.debug(\"Server closed gracefully\");\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property in the HTTP error range (e.g. `ApiError` from\n * `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n if (!(error instanceof Error) || !(\"statusCode\" in error)) return false;\n const statusCode = error.statusCode;\n return (\n typeof statusCode === \"number\" &&\n Number.isInteger(statusCode) &&\n statusCode >= 400 &&\n statusCode <= 599\n );\n}\n\n/**\n * Terminal Express error-handling middleware.\n *\n * Converts errors forwarded via `next(err)` (including async handler\n * rejections forwarded by `Plugin.route()`) into JSON error responses,\n * following the same status/message conventions as `Plugin.execute()`:\n * - errors carrying an HTTP `statusCode` (including `AppKitError`) → that\n * status; 5xx messages are masked in production to avoid leaking internals\n * - anything else → 500 with a generic message in production\n *\n * 4xx errors are expected client errors (e.g. missing auth headers) and are\n * logged at `warn` with just the message; 5xx/unknown errors are logged at\n * `error` with the full error.\n *\n * @internal Exported for unit tests.\n */\nexport function errorHandlerMiddleware(\n err: unknown,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const httpError =\n err instanceof AppKitError || hasHttpStatusCode(err) ? err : null;\n const statusCode = httpError ? httpError.statusCode : 500;\n const isClientError = statusCode < 500;\n\n if (isClientError) {\n logger.warn(\n \"Request failed for %s %s with status %d: %s\",\n req.method,\n req.originalUrl,\n statusCode,\n err instanceof Error ? err.message : String(err),\n );\n } else {\n logger.error(\n \"Unhandled error for %s %s: %O\",\n req.method,\n req.originalUrl,\n // toJSON() redacts sensitive context fields; %O on a raw AppKitError\n // would enumerate them unredacted.\n err instanceof AppKitError ? err.toJSON() : err,\n );\n }\n\n if (res.headersSent) {\n next(err);\n return;\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n const message = err instanceof Error ? err.message : \"Server error\";\n\n res.status(statusCode).json({\n error: isDev || isClientError ? message : \"Server error\",\n });\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\n\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport getPort, { portNumbers } from \"get-port\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { camelToKebab } from \"shared\";\n\nimport { AppKitError, ServerError } from \"../../errors\";\nimport { TelemetryReporter } from \"../../internal-telemetry\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport { createDevOboMiddleware } from \"./dev-obo-middleware\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/** Dev-only: try `requested` then consecutive ports (see `get-port` `portNumbers`). */\nconst devListenPortSpan = 100;\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 * The server is started automatically by `createApp` after all plugins are set up\n * and the optional `onPluginsReady` callback has run.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), analytics({})],\n * onPluginsReady(appkit) {\n * appkit.server.extend((app) => {\n * app.get(\"/custom\", (_req, res) => res.json({ ok: true }));\n * });\n * },\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /**\n * Budget for awaiting `server.close()` during shutdown. Bounded because\n * `closeAllConnections()` runs immediately before the await, so the close\n * is expected to resolve promptly; the core lifecycle manager's overall\n * force-exit timer is the ultimate backstop if it does not.\n */\n private static readonly SERVER_CLOSE_TIMEOUT_MS = 2_000;\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = defineManifest<\"server\">(manifest);\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n /** Bound listen port after optional dev-time resolution. */\n private resolvedListenPort?: number;\n declare protected config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n /**\n * Resolves when `server.close()` completes. Created in\n * {@link abortActiveOperations} (which initiates the close) and awaited in\n * {@link closeRemainingConnections} after other plugins have drained.\n */\n private serverClosed?: Promise<void>;\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n if (\"autoStart\" in config) {\n throw new ServerError(\n \"server({ autoStart }) has been removed. \" +\n \"The server is now started automatically by createApp.\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n }\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n }\n\n attachContext(deps: Parameters<Plugin[\"attachContext\"]>[0] = {}): void {\n super.attachContext(deps);\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n this.context?.registerAsRouteTarget(this);\n }\n\n /** Setup the server plugin. */\n async setup() {}\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\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(requestMetricsMiddleware);\n const devObo = createDevOboMiddleware();\n if (devObo) this.serverApplication.use(devObo);\n this.serverApplication.use(\n express.json({\n // Express's stock 100kb default is too tight for modern apps —\n // agent chat payloads and any base64-encoded upload (e.g. the\n // dev playground's smart-dashboard \"save view\" screenshot at\n // ~105KB) blow past it instantly. Raise to 1mb by default and\n // let consumers tune via `server({ bodyLimit })` if they need\n // more headroom.\n limit: this.config.bodyLimit ?? \"1mb\",\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = 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, pluginConfigs);\n\n // Terminal error handler — registered after all routes, extensions, and\n // frontend middleware so that errors forwarded via next(err) from any of\n // them (e.g. async handler rejections forwarded by Plugin.route()) are\n // turned into JSON error responses instead of hanging the request.\n this.serverApplication.use(errorHandlerMiddleware);\n\n const listenPort = await this.resolveListenPort();\n\n const server = this.serverApplication.listen(\n listenPort,\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 // Graceful shutdown is orchestrated by the core LifecycleManager, which\n // owns the process signal handlers. This plugin only contributes its\n // HTTP concerns: it aborts/initiates the socket close in\n // abortActiveOperations(), drains dev servers in shutdown(), and force-\n // closes remaining sockets in the \"shutdown\" lifecycle hook below (which\n // fires after every plugin's shutdown() has run).\n this.context?.onLifecycle(\"shutdown\", () =>\n this.closeRemainingConnections(),\n );\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\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 has not started yet.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\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 * Call this inside the `onPluginsReady` callback of `createApp` to register\n * custom Express routes or middleware before the server starts listening.\n *\n * Note: async handlers registered directly on the app must handle their own\n * rejections — Express 4 does not forward rejected promises to the error\n * middleware. Errors passed explicitly to `next(err)` are formatted by the\n * terminal error middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n */\n extend(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Register a server extension from another plugin during setup.\n * Unlike extend(), this is designed for internal plugin-to-plugin\n * coordination where extensions are registered before the server starts\n * listening — typically called by PluginContext when flushing buffered routes.\n */\n addExtension(fn: (app: express.Application) => void) {\n this.serverExtensions.push(fn);\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 * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n const plugins = this.context?.getPlugins();\n if (!plugins || plugins.size === 0) return { endpoints, pluginConfigs };\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 plugins.values()) {\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/${camelToKebab(plugin.name)}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\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(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\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 pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\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 pluginConfigs,\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 /**\n * In development, prefers {@link ServerConfig.port} / env / default (8000), then\n * scans upward using `get-port`'s `portNumbers()` on the listen host until one binds.\n * In non-development, uses config / env / default only (no fallback).\n */\n private async resolveListenPort(): Promise<number> {\n const requested = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n\n if (process.env.NODE_ENV !== \"development\") {\n this.resolvedListenPort = requested;\n return requested;\n }\n\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n const upper = Math.min(requested + devListenPortSpan - 1, 65_535);\n const port = await getPort({\n host,\n port: portNumbers(requested, upper),\n });\n this.resolvedListenPort = port;\n if (port !== requested) {\n logger.info(\"Port %d was busy, picking %d\", requested, port);\n }\n return port;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port =\n this.resolvedListenPort ??\n this.config.port ??\n 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 /**\n * Cancel in-flight work and begin closing the HTTP server.\n *\n * Runs in the first phase of the core LifecycleManager's graceful shutdown,\n * before any plugin `shutdown()` hook. In addition to the base behavior\n * (aborting SSE streams), it stops the server accepting new connections and\n * drops idle keep-alive sockets — without `closeIdleConnections()`, a\n * connected browser would pin `server.close()` open until the force-exit\n * timer fires. The close is only *initiated* here; the returned promise is\n * awaited later in {@link closeRemainingConnections} so peer plugins can\n * still drain in-flight requests through the server first.\n */\n abortActiveOperations(): void {\n super.abortActiveOperations();\n\n if (this.server) {\n const server = this.server;\n this.serverClosed = new Promise((resolve) => {\n server.close(() => resolve());\n });\n server.closeIdleConnections();\n }\n }\n\n /**\n * Drain the dev-only servers as this plugin's `shutdown()` hook.\n *\n * Runs concurrently with the other plugins' hooks during graceful shutdown.\n * These are no-ops in production (neither is created), so this hook is\n * effectively dev-only.\n */\n async shutdown(): Promise<void> {\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n }\n\n /**\n * Force-close whatever sockets remain and await the server close.\n *\n * Registered on the `\"shutdown\"` lifecycle event in {@link start}, so it\n * fires after every plugin's `shutdown()` hook has run — meaning any\n * in-flight request a peer plugin was draining has already completed.\n * `closeAllConnections()` destroys the leftover sockets (aborted SSE\n * responses, keep-alive connections) synchronously so the pending\n * `server.close()` can complete; the await is bounded because the close is\n * expected to resolve promptly once the sockets are gone.\n */\n private async closeRemainingConnections(): Promise<void> {\n if (this.server) {\n this.server.closeAllConnections();\n }\n if (!this.serverClosed) return;\n\n await Promise.race([\n this.serverClosed,\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ServerPlugin.SERVER_CLOSE_TIMEOUT_MS);\n timer.unref();\n }),\n ]);\n logger.debug(\"Server closed gracefully\");\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n /** @deprecated Server is now started automatically by createApp. */\n start() {\n throw new ServerError(\n \"server.start() has been removed. Use the onPluginsReady callback instead:\\n\\n\" +\n \" createApp({\\n\" +\n \" plugins: [server(), ...],\\n\" +\n \" onPluginsReady(appkit) {\\n\" +\n \" appkit.server.extend(...);\\n\" +\n \" },\\n\" +\n \" });\\n\\n\" +\n \"Run `npx appkit codemod on-plugins-ready --write` to auto-migrate.\",\n );\n },\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/** @internal Exported for unit tests. */\nexport function requestMetricsMiddleware(\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const startMs = Date.now();\n res.on(\"finish\", () => {\n const reporter = TelemetryReporter.getInstance();\n if (!reporter) return;\n const routePath = (req.route as { path?: string } | undefined)?.path;\n if (!routePath) return;\n const baseUrl = req.baseUrl ?? \"\";\n const template = `${baseUrl}${routePath}`;\n reporter.recordRequest(\n req.method,\n template,\n res.statusCode,\n Date.now() - startMs,\n );\n });\n next();\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property in the HTTP error range (e.g. `ApiError` from\n * `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n if (!(error instanceof Error) || !(\"statusCode\" in error)) return false;\n const statusCode = error.statusCode;\n return (\n typeof statusCode === \"number\" &&\n Number.isInteger(statusCode) &&\n statusCode >= 400 &&\n statusCode <= 599\n );\n}\n\n/**\n * Terminal Express error-handling middleware.\n *\n * Converts errors forwarded via `next(err)` (including async handler\n * rejections forwarded by `Plugin.route()`) into JSON error responses,\n * following the same status/message conventions as `Plugin.execute()`:\n * - errors carrying an HTTP `statusCode` (including `AppKitError`) → that\n * status; 5xx messages are masked in production to avoid leaking internals\n * - anything else → 500 with a generic message in production\n *\n * 4xx errors are expected client errors (e.g. missing auth headers) and are\n * logged at `warn` with just the message; 5xx/unknown errors are logged at\n * `error` with the full error.\n *\n * @internal Exported for unit tests.\n */\nexport function errorHandlerMiddleware(\n err: unknown,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n) {\n const httpError =\n err instanceof AppKitError || hasHttpStatusCode(err) ? err : null;\n const statusCode = httpError ? httpError.statusCode : 500;\n const isClientError = statusCode < 500;\n\n if (isClientError) {\n logger.warn(\n \"Request failed for %s %s with status %d: %s\",\n req.method,\n req.originalUrl,\n statusCode,\n err instanceof Error ? err.message : String(err),\n );\n } else {\n logger.error(\n \"Unhandled error for %s %s: %O\",\n req.method,\n req.originalUrl,\n // toJSON() redacts sensitive context fields; %O on a raw AppKitError\n // would enumerate them unredacted.\n err instanceof AppKitError ? err.toJSON() : err,\n );\n }\n\n if (res.headersSent) {\n next(err);\n return;\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n const message = err instanceof Error ? err.message : \"Server error\";\n\n res.status(statusCode).json({\n error: isDev || isClientError ? message : \"Server error\",\n });\n}\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ var manifest_default = {
|
|
|
5
5
|
displayName: "Model Serving Plugin (deprecated)",
|
|
6
6
|
description: "DEPRECATED: use the agents plugin instead. Authenticated proxy to Databricks Model Serving endpoints",
|
|
7
7
|
deprecated: true,
|
|
8
|
+
scopes: ["model-serving"],
|
|
8
9
|
resources: {
|
|
9
10
|
"required": [{
|
|
10
11
|
"type": "serving_endpoint",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { IAppRouter, ToPlugin } from "../../shared/src/plugin.js";
|
|
2
2
|
import "../../shared/src/index.js";
|
|
3
|
+
import { PluginManifest, ResourceRequirement } from "../../registry/types.js";
|
|
3
4
|
import { ExecutionResult } from "../../plugin/execution-result.js";
|
|
4
5
|
import { Plugin } from "../../plugin/plugin.js";
|
|
5
6
|
import "../../plugin/index.js";
|
|
6
|
-
import { PluginManifest, ResourceRequirement } from "../../registry/types.js";
|
|
7
7
|
import "../../registry/index.js";
|
|
8
8
|
import { IServingConfig, ServingEndpointMethods, ServingFactory } from "./types.js";
|
|
9
9
|
import "../../index.js";
|
|
@@ -12,8 +12,8 @@ import { invoke, stream } from "../../connectors/serving/client.js";
|
|
|
12
12
|
import { servingInvokeDefaults } from "./defaults.js";
|
|
13
13
|
import manifest_default from "./manifest.js";
|
|
14
14
|
import { filterRequestBody, loadEndpointSchemas } from "./schema-filter.js";
|
|
15
|
-
import path from "node:path";
|
|
16
15
|
import { Readable } from "node:stream";
|
|
16
|
+
import path from "node:path";
|
|
17
17
|
import { pipeline } from "node:stream/promises";
|
|
18
18
|
|
|
19
19
|
//#region src/plugins/serving/serving.ts
|
|
@@ -86,7 +86,7 @@ var ServingPlugin = class extends Plugin {
|
|
|
86
86
|
method: "post",
|
|
87
87
|
path: "/:alias/invoke",
|
|
88
88
|
handler: async (req, res) => {
|
|
89
|
-
await this.
|
|
89
|
+
await this._asUserScoped(req)._handleInvoke(req, res);
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
92
|
this.route(router, {
|
|
@@ -94,17 +94,17 @@ var ServingPlugin = class extends Plugin {
|
|
|
94
94
|
method: "post",
|
|
95
95
|
path: "/:alias/stream",
|
|
96
96
|
handler: async (req, res) => {
|
|
97
|
-
await this.
|
|
97
|
+
await this._asUserScoped(req)._handleStream(req, res);
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
100
|
} else {
|
|
101
101
|
const invokeHandler = async (req, res) => {
|
|
102
102
|
req.params.alias ??= "default";
|
|
103
|
-
await this.
|
|
103
|
+
await this._asUserScoped(req)._handleInvoke(req, res);
|
|
104
104
|
};
|
|
105
105
|
const streamHandler = async (req, res) => {
|
|
106
106
|
req.params.alias ??= "default";
|
|
107
|
-
await this.
|
|
107
|
+
await this._asUserScoped(req)._handleStream(req, res);
|
|
108
108
|
};
|
|
109
109
|
this.route(router, {
|
|
110
110
|
name: "invoke",
|
|
@@ -215,7 +215,7 @@ var ServingPlugin = class extends Plugin {
|
|
|
215
215
|
return {
|
|
216
216
|
...this.createEndpointAPI(resolved),
|
|
217
217
|
asUser: (req) => {
|
|
218
|
-
return this.
|
|
218
|
+
return this._asUserScoped(req).createEndpointAPI(resolved);
|
|
219
219
|
}
|
|
220
220
|
};
|
|
221
221
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\n\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { ResourceRequirement } from \"../../registry\";\nimport { defineManifest, ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\n/**\n * @deprecated The `serving` plugin is deprecated and will be removed in a\n * future release. Use the `agents` plugin instead.\n */\nexport class ServingPlugin extends Plugin {\n static manifest = defineManifest<\"serving\">(manifest);\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n declare protected config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n logger.warn(\n \"The `serving` plugin is deprecated and will be removed in a future release. \" +\n \"Use the `agents` plugin instead.\",\n );\n\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this.asUser(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this.asUser(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @deprecated The `serving` plugin is deprecated and will be removed in a\n * future release. Use the `agents` plugin instead.\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"serving.js","names":["manifest","servingConnector.stream","servingConnector.invoke"],"sources":["../../../src/plugins/serving/serving.ts"],"sourcesContent":["import path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport type express from \"express\";\nimport type { IAppRouter } from \"shared\";\n\nimport * as servingConnector from \"../../connectors/serving/client\";\nimport { getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging\";\nimport { type ExecutionResult, Plugin, toPlugin } from \"../../plugin\";\nimport type { ResourceRequirement } from \"../../registry\";\nimport { defineManifest, ResourceType } from \"../../registry\";\nimport { servingInvokeDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { filterRequestBody, loadEndpointSchemas } from \"./schema-filter\";\nimport type {\n EndpointConfig,\n IServingConfig,\n ServingEndpointMethods,\n ServingFactory,\n} from \"./types\";\n\nconst logger = createLogger(\"serving\");\n\nclass EndpointNotFoundError extends Error {\n constructor(alias: string) {\n super(`Unknown endpoint alias: ${alias}`);\n }\n}\n\nclass EndpointNotConfiguredError extends Error {\n constructor(alias: string, envVar: string) {\n super(\n `Endpoint '${alias}' is not configured: env var '${envVar}' is not set`,\n );\n }\n}\n\ninterface ResolvedEndpoint {\n name: string;\n}\n\n/**\n * @deprecated The `serving` plugin is deprecated and will be removed in a\n * future release. Use the `agents` plugin instead.\n */\nexport class ServingPlugin extends Plugin {\n static manifest = defineManifest<\"serving\">(manifest);\n\n protected static description =\n \"Authenticated proxy to Databricks Model Serving endpoints\";\n declare protected config: IServingConfig;\n\n private readonly endpoints: Record<string, EndpointConfig>;\n private readonly isNamedMode: boolean;\n private schemaAllowlists = new Map<string, Set<string>>();\n\n constructor(config: IServingConfig) {\n super(config);\n this.config = config;\n\n if (config.endpoints) {\n this.endpoints = config.endpoints;\n this.isNamedMode = true;\n } else {\n this.endpoints = {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n this.isNamedMode = false;\n }\n }\n\n async setup(): Promise<void> {\n logger.warn(\n \"The `serving` plugin is deprecated and will be removed in a future release. \" +\n \"Use the `agents` plugin instead.\",\n );\n\n const cacheFile = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-serving-types-cache.json\",\n );\n this.schemaAllowlists = await loadEndpointSchemas(cacheFile);\n if (this.schemaAllowlists.size > 0) {\n logger.debug(\n \"Loaded schema allowlists for %d endpoint(s)\",\n this.schemaAllowlists.size,\n );\n }\n }\n\n static getResourceRequirements(\n config: IServingConfig,\n ): ResourceRequirement[] {\n const endpoints = config.endpoints ?? {\n default: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\" },\n };\n\n return Object.entries(endpoints).map(([alias, endpointConfig]) => ({\n type: ResourceType.SERVING_ENDPOINT,\n alias: `serving-${alias}`,\n resourceKey: `serving-${alias}`,\n description: `Model Serving endpoint for \"${alias}\" inference`,\n permission: \"CAN_QUERY\" as const,\n fields: {\n name: {\n env: endpointConfig.env,\n description: `Serving endpoint name for \"${alias}\"`,\n },\n },\n required: true,\n }));\n }\n\n private resolveAndFilter(\n alias: string,\n body: Record<string, unknown>,\n ): { endpoint: ResolvedEndpoint; filteredBody: Record<string, unknown> } {\n const config = this.endpoints[alias];\n if (!config) {\n throw new EndpointNotFoundError(alias);\n }\n\n const name = process.env[config.env];\n if (!name) {\n throw new EndpointNotConfiguredError(alias, config.env);\n }\n\n const endpoint: ResolvedEndpoint = { name };\n const filteredBody = filterRequestBody(\n body,\n this.schemaAllowlists,\n alias,\n this.config.filterMode,\n );\n return { endpoint, filteredBody };\n }\n\n // All serving routes use OBO (On-Behalf-Of) by default, consistent with the\n // Genie and Files plugins. This ensures per-user CAN_QUERY permissions are enforced.\n injectRoutes(router: IAppRouter) {\n if (this.isNamedMode) {\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._asUserScoped(req)._handleInvoke(req, res);\n },\n });\n\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._asUserScoped(req)._handleStream(req, res);\n },\n });\n } else {\n // Unnamed mode: register both /invoke and /:alias/invoke patterns.\n // The type generator creates a \"default\" alias, so clients may use either URL.\n const invokeHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this._asUserScoped(req)._handleInvoke(req, res);\n };\n const streamHandler = async (\n req: express.Request,\n res: express.Response,\n ) => {\n req.params.alias ??= \"default\";\n await this._asUserScoped(req)._handleStream(req, res);\n };\n\n this.route(router, {\n name: \"invoke\",\n method: \"post\",\n path: \"/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"invoke-named\",\n method: \"post\",\n path: \"/:alias/invoke\",\n handler: invokeHandler,\n });\n this.route(router, {\n name: \"stream\",\n method: \"post\",\n path: \"/stream\",\n handler: streamHandler,\n });\n this.route(router, {\n name: \"stream-named\",\n method: \"post\",\n path: \"/:alias/stream\",\n handler: streamHandler,\n });\n }\n }\n\n async _handleInvoke(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n try {\n const result = await this.invoke(alias, rawBody);\n if (!result.ok) {\n res.status(result.status).json({ error: result.message });\n return;\n }\n res.json(result.data);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invocation failed\";\n if (err instanceof EndpointNotFoundError) {\n res.status(404).json({ error: message });\n } else if (\n err instanceof EndpointNotConfiguredError ||\n message.startsWith(\"Unknown request parameters:\")\n ) {\n res.status(400).json({ error: message });\n } else {\n res.status(502).json({ error: message });\n }\n }\n }\n\n async _handleStream(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { alias } = req.params;\n const rawBody = req.body as Record<string, unknown>;\n\n let endpoint: ResolvedEndpoint;\n let filteredBody: Record<string, unknown>;\n try {\n ({ endpoint, filteredBody } = this.resolveAndFilter(alias, rawBody));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid request\";\n const status = err instanceof EndpointNotFoundError ? 404 : 400;\n res.status(status).json({ error: message });\n return;\n }\n\n const timeout = this.config.timeout ?? 120_000;\n const workspaceClient = getWorkspaceClient();\n\n // Pipe raw SSE bytes from the upstream endpoint directly to the client.\n // No parsing/re-serialization — the upstream response is already valid SSE.\n let rawStream: ReadableStream<Uint8Array>;\n try {\n rawStream = await servingConnector.stream(\n workspaceClient,\n endpoint.name,\n filteredBody,\n );\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"Streaming request failed\";\n res.status(502).json({ error: message });\n return;\n }\n\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n res.flushHeaders();\n\n const nodeStream = Readable.fromWeb(\n rawStream as import(\"stream/web\").ReadableStream,\n );\n const abortController = new AbortController();\n const timeoutId = setTimeout(() => abortController.abort(), timeout);\n\n req.on(\"close\", () => abortController.abort());\n\n try {\n await pipeline(nodeStream, res, { signal: abortController.signal });\n } catch (err) {\n // AbortError is expected on client disconnect or timeout\n if (err instanceof Error && err.name !== \"AbortError\") {\n logger.warn(\"Stream pipe error: %s\", err.message);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async invoke(\n alias: string,\n body: Record<string, unknown>,\n ): Promise<ExecutionResult<unknown>> {\n const { endpoint, filteredBody } = this.resolveAndFilter(alias, body);\n const workspaceClient = getWorkspaceClient();\n const timeout = this.config.timeout ?? 120_000;\n\n return this.execute(\n () =>\n servingConnector.invoke(workspaceClient, endpoint.name, filteredBody),\n {\n default: {\n ...servingInvokeDefaults,\n timeout,\n },\n },\n );\n }\n\n clientConfig(): Record<string, unknown> {\n return {\n isNamedMode: this.isNamedMode,\n aliases: Object.keys(this.endpoints),\n };\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n protected createEndpointAPI(alias: string): ServingEndpointMethods {\n return {\n invoke: (body: Record<string, unknown>) => this.invoke(alias, body),\n };\n }\n\n exports(): ServingFactory {\n const resolveEndpoint = (alias?: string) => {\n const resolved = alias ?? \"default\";\n const spApi = this.createEndpointAPI(resolved);\n return {\n ...spApi,\n asUser: (req: express.Request) => {\n const userPlugin = this._asUserScoped(req) as ServingPlugin;\n return userPlugin.createEndpointAPI(resolved);\n },\n };\n };\n return resolveEndpoint as ServingFactory;\n }\n}\n\n/**\n * @deprecated The `serving` plugin is deprecated and will be removed in a\n * future release. Use the `agents` plugin instead.\n * @internal\n */\nexport const serving = toPlugin(ServingPlugin);\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuBA,MAAM,SAAS,aAAa,UAAU;AAEtC,IAAM,wBAAN,cAAoC,MAAM;CACxC,YAAY,OAAe;AACzB,QAAM,2BAA2B,QAAQ;;;AAI7C,IAAM,6BAAN,cAAyC,MAAM;CAC7C,YAAY,OAAe,QAAgB;AACzC,QACE,aAAa,MAAM,gCAAgC,OAAO,cAC3D;;;;;;;AAYL,IAAa,gBAAb,cAAmC,OAAO;CACxC,OAAO,WAAW,eAA0BA,iBAAS;CAErD,OAAiB,cACf;CAGF,AAAiB;CACjB,AAAiB;CACjB,AAAQ,mCAAmB,IAAI,KAA0B;CAEzD,YAAY,QAAwB;AAClC,QAAM,OAAO;AACb,OAAK,SAAS;AAEd,MAAI,OAAO,WAAW;AACpB,QAAK,YAAY,OAAO;AACxB,QAAK,cAAc;SACd;AACL,QAAK,YAAY,EACf,SAAS,EAAE,KAAK,oCAAoC,EACrD;AACD,QAAK,cAAc;;;CAIvB,MAAM,QAAuB;AAC3B,SAAO,KACL,+GAED;AASD,OAAK,mBAAmB,MAAM,oBAPZ,KAAK,KACrB,QAAQ,KAAK,EACb,gBACA,eACA,UACA,mCACD,CAC2D;AAC5D,MAAI,KAAK,iBAAiB,OAAO,EAC/B,QAAO,MACL,+CACA,KAAK,iBAAiB,KACvB;;CAIL,OAAO,wBACL,QACuB;EACvB,MAAM,YAAY,OAAO,aAAa,EACpC,SAAS,EAAE,KAAK,oCAAoC,EACrD;AAED,SAAO,OAAO,QAAQ,UAAU,CAAC,KAAK,CAAC,OAAO,qBAAqB;GACjE,MAAM,aAAa;GACnB,OAAO,WAAW;GAClB,aAAa,WAAW;GACxB,aAAa,+BAA+B,MAAM;GAClD,YAAY;GACZ,QAAQ,EACN,MAAM;IACJ,KAAK,eAAe;IACpB,aAAa,8BAA8B,MAAM;IAClD,EACF;GACD,UAAU;GACX,EAAE;;CAGL,AAAQ,iBACN,OACA,MACuE;EACvE,MAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,CAAC,OACH,OAAM,IAAI,sBAAsB,MAAM;EAGxC,MAAM,OAAO,QAAQ,IAAI,OAAO;AAChC,MAAI,CAAC,KACH,OAAM,IAAI,2BAA2B,OAAO,OAAO,IAAI;AAUzD,SAAO;GAAE,UAP0B,EAAE,MAAM;GAOxB,cANE,kBACnB,MACA,KAAK,kBACL,OACA,KAAK,OAAO,WACb;GACgC;;CAKnC,aAAa,QAAoB;AAC/B,MAAI,KAAK,aAAa;AACpB,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS,OAAO,KAAsB,QAA0B;AAC9D,WAAM,KAAK,cAAc,IAAI,CAAC,cAAc,KAAK,IAAI;;IAExD,CAAC;AAEF,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS,OAAO,KAAsB,QAA0B;AAC9D,WAAM,KAAK,cAAc,IAAI,CAAC,cAAc,KAAK,IAAI;;IAExD,CAAC;SACG;GAGL,MAAM,gBAAgB,OACpB,KACA,QACG;AACH,QAAI,OAAO,UAAU;AACrB,UAAM,KAAK,cAAc,IAAI,CAAC,cAAc,KAAK,IAAI;;GAEvD,MAAM,gBAAgB,OACpB,KACA,QACG;AACH,QAAI,OAAO,UAAU;AACrB,UAAM,KAAK,cAAc,IAAI,CAAC,cAAc,KAAK,IAAI;;AAGvD,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;IACV,CAAC;AACF,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;IACV,CAAC;AACF,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;IACV,CAAC;AACF,QAAK,MAAM,QAAQ;IACjB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;IACV,CAAC;;;CAIN,MAAM,cACJ,KACA,KACe;EACf,MAAM,EAAE,UAAU,IAAI;EACtB,MAAM,UAAU,IAAI;AAEpB,MAAI;GACF,MAAM,SAAS,MAAM,KAAK,OAAO,OAAO,QAAQ;AAChD,OAAI,CAAC,OAAO,IAAI;AACd,QAAI,OAAO,OAAO,OAAO,CAAC,KAAK,EAAE,OAAO,OAAO,SAAS,CAAC;AACzD;;AAEF,OAAI,KAAK,OAAO,KAAK;WACd,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,OAAI,eAAe,sBACjB,KAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;YAExC,eAAe,8BACf,QAAQ,WAAW,8BAA8B,CAEjD,KAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;OAExC,KAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;;;CAK9C,MAAM,cACJ,KACA,KACe;EACf,MAAM,EAAE,UAAU,IAAI;EACtB,MAAM,UAAU,IAAI;EAEpB,IAAI;EACJ,IAAI;AACJ,MAAI;AACF,IAAC,CAAE,UAAU,gBAAiB,KAAK,iBAAiB,OAAO,QAAQ;WAC5D,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;GACrD,MAAM,SAAS,eAAe,wBAAwB,MAAM;AAC5D,OAAI,OAAO,OAAO,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;AAC3C;;EAGF,MAAM,UAAU,KAAK,OAAO,WAAW;EACvC,MAAM,kBAAkB,oBAAoB;EAI5C,IAAI;AACJ,MAAI;AACF,eAAY,MAAMC,OAChB,iBACA,SAAS,MACT,aACD;WACM,KAAK;GACZ,MAAM,UACJ,eAAe,QAAQ,IAAI,UAAU;AACvC,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC;AACxC;;AAGF,MAAI,UAAU,gBAAgB,mCAAmC;AACjE,MAAI,UAAU,iBAAiB,yBAAyB;AACxD,MAAI,UAAU,qBAAqB,KAAK;AACxC,MAAI,cAAc;EAElB,MAAM,aAAa,SAAS,QAC1B,UACD;EACD,MAAM,kBAAkB,IAAI,iBAAiB;EAC7C,MAAM,YAAY,iBAAiB,gBAAgB,OAAO,EAAE,QAAQ;AAEpE,MAAI,GAAG,eAAe,gBAAgB,OAAO,CAAC;AAE9C,MAAI;AACF,SAAM,SAAS,YAAY,KAAK,EAAE,QAAQ,gBAAgB,QAAQ,CAAC;WAC5D,KAAK;AAEZ,OAAI,eAAe,SAAS,IAAI,SAAS,aACvC,QAAO,KAAK,yBAAyB,IAAI,QAAQ;YAE3C;AACR,gBAAa,UAAU;;;CAI3B,MAAM,OACJ,OACA,MACmC;EACnC,MAAM,EAAE,UAAU,iBAAiB,KAAK,iBAAiB,OAAO,KAAK;EACrE,MAAM,kBAAkB,oBAAoB;EAC5C,MAAM,UAAU,KAAK,OAAO,WAAW;AAEvC,SAAO,KAAK,cAERC,OAAwB,iBAAiB,SAAS,MAAM,aAAa,EACvE,EACE,SAAS;GACP,GAAG;GACH;GACD,EACF,CACF;;CAGH,eAAwC;AACtC,SAAO;GACL,aAAa,KAAK;GAClB,SAAS,OAAO,KAAK,KAAK,UAAU;GACrC;;CAGH,MAAM,WAA0B;AAC9B,OAAK,cAAc,UAAU;;CAG/B,AAAU,kBAAkB,OAAuC;AACjE,SAAO,EACL,SAAS,SAAkC,KAAK,OAAO,OAAO,KAAK,EACpE;;CAGH,UAA0B;EACxB,MAAM,mBAAmB,UAAmB;GAC1C,MAAM,WAAW,SAAS;AAE1B,UAAO;IACL,GAFY,KAAK,kBAAkB,SAAS;IAG5C,SAAS,QAAyB;AAEhC,YADmB,KAAK,cAAc,IAAI,CACxB,kBAAkB,SAAS;;IAEhD;;AAEH,SAAO;;;;;;;;AASX,MAAa,UAAU,SAAS,cAAc"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { getWarehouseId } from "./warehouse.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "../workspace-client/index.js";
|
|
2
|
+
import "../registry/types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resources/warehouse.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Get the configured SQL warehouse ID after app initialization.
|
|
7
|
+
* The warehouse is an app resource; SP and caller executions use the same binding.
|
|
8
|
+
* Deprecated user-context scopes retain support for explicit warehouse overrides.
|
|
9
|
+
*
|
|
10
|
+
* @throws ConfigurationError if no SQL warehouse was required at startup.
|
|
11
|
+
* @throws InitializationError if the app resources are not initialized.
|
|
12
|
+
*/
|
|
13
|
+
declare function getWarehouseId(): Promise<string>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { getWarehouseId };
|
|
16
|
+
//# sourceMappingURL=warehouse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warehouse.d.ts","names":[],"sources":["../../src/resources/warehouse.ts"],"mappings":";;;;;;;;;;;;iBAwJgB,cAAA,CAAA,GAAkB,OAAA"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ConfigurationError } from "../errors/configuration.js";
|
|
2
|
+
import { InitializationError } from "../errors/initialization.js";
|
|
3
|
+
import "../errors/index.js";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
|
|
6
|
+
//#region src/resources/warehouse.ts
|
|
7
|
+
let appBinding;
|
|
8
|
+
/** Owns SQL warehouse discovery and the app's immutable warehouse binding. */
|
|
9
|
+
var WarehouseResource = class {
|
|
10
|
+
/** Resolve candidates without publishing resources from a failed app startup. */
|
|
11
|
+
static async resolve(client, selection = false) {
|
|
12
|
+
const warehouseId = typeof selection === "string" ? selection : selection ? await discoverWarehouseId(client) : void 0;
|
|
13
|
+
return Object.freeze({ warehouseId: warehouseId === void 0 ? void 0 : Promise.resolve(warehouseId) });
|
|
14
|
+
}
|
|
15
|
+
/** Publish an immutable snapshot of resolved app resources. */
|
|
16
|
+
static bind(bindings) {
|
|
17
|
+
appBinding = Object.freeze({ ...bindings });
|
|
18
|
+
return appBinding;
|
|
19
|
+
}
|
|
20
|
+
static get() {
|
|
21
|
+
return appBinding;
|
|
22
|
+
}
|
|
23
|
+
/** Reset app bindings alongside the service context in tests. */
|
|
24
|
+
static reset() {
|
|
25
|
+
appBinding = void 0;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Select the app warehouse from resource values resolved by the registry. */
|
|
29
|
+
function getConfiguredWarehouseId(resources) {
|
|
30
|
+
const ids = new Set(resources.filter((resource) => resource.type === "sql_warehouse" && resource.required).flatMap((resource) => resource.values?.id ? [resource.values.id] : []));
|
|
31
|
+
if (ids.size <= 1) return ids.values().next().value;
|
|
32
|
+
if (process.env.DATABRICKS_WAREHOUSE_ID) return process.env.DATABRICKS_WAREHOUSE_ID;
|
|
33
|
+
throw new ConfigurationError("Multiple SQL warehouses are configured. Set DATABRICKS_WAREHOUSE_ID to select the app's default warehouse.");
|
|
34
|
+
}
|
|
35
|
+
async function discoverWarehouseId(client) {
|
|
36
|
+
if (process.env.DATABRICKS_WAREHOUSE_ID) return process.env.DATABRICKS_WAREHOUSE_ID;
|
|
37
|
+
const agenticMode = process.env.DATABRICKS_APPS_AGENTIC_MODE === "true" || process.env.DATABRICKS_APPS_AGENTIC_MODE === "1";
|
|
38
|
+
if (process.env.NODE_ENV === "development" && !agenticMode) {
|
|
39
|
+
const response = await client.apiClient.request({
|
|
40
|
+
path: "/api/2.0/sql/warehouses",
|
|
41
|
+
method: "GET",
|
|
42
|
+
headers: new Headers(),
|
|
43
|
+
raw: false,
|
|
44
|
+
query: { skip_cannot_use: "true" }
|
|
45
|
+
});
|
|
46
|
+
const priorities = {
|
|
47
|
+
RUNNING: 0,
|
|
48
|
+
STOPPED: 1,
|
|
49
|
+
STARTING: 2,
|
|
50
|
+
STOPPING: 3,
|
|
51
|
+
DELETED: 99,
|
|
52
|
+
DELETING: 99
|
|
53
|
+
};
|
|
54
|
+
const warehouses = (response.warehouses || []).sort((a, b) => {
|
|
55
|
+
return priorities[a.state] - priorities[b.state];
|
|
56
|
+
});
|
|
57
|
+
if (warehouses.length === 0) throw ConfigurationError.resourceNotFound("Warehouse ID", "Please configure the DATABRICKS_WAREHOUSE_ID environment variable");
|
|
58
|
+
const firstWarehouse = warehouses[0];
|
|
59
|
+
if (firstWarehouse.state === "DELETED" || firstWarehouse.state === "DELETING" || !firstWarehouse.id) throw ConfigurationError.resourceNotFound("Warehouse ID", "Please configure the DATABRICKS_WAREHOUSE_ID environment variable");
|
|
60
|
+
return firstWarehouse.id;
|
|
61
|
+
}
|
|
62
|
+
throw ConfigurationError.resourceNotFound("Warehouse ID", "Please configure the DATABRICKS_WAREHOUSE_ID environment variable");
|
|
63
|
+
}
|
|
64
|
+
const legacyResourceStorage = new AsyncLocalStorage();
|
|
65
|
+
/** @internal Keep legacy resource overrides out of execution identity. */
|
|
66
|
+
function runWithResourceBindings(bindings, fn) {
|
|
67
|
+
return legacyResourceStorage.run(bindings, fn);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the configured SQL warehouse ID after app initialization.
|
|
71
|
+
* The warehouse is an app resource; SP and caller executions use the same binding.
|
|
72
|
+
* Deprecated user-context scopes retain support for explicit warehouse overrides.
|
|
73
|
+
*
|
|
74
|
+
* @throws ConfigurationError if no SQL warehouse was required at startup.
|
|
75
|
+
* @throws InitializationError if the app resources are not initialized.
|
|
76
|
+
*/
|
|
77
|
+
function getWarehouseId() {
|
|
78
|
+
const warehouseId = captureWarehouseId()();
|
|
79
|
+
if (!warehouseId) throw ConfigurationError.resourceNotFound("Warehouse ID", "No plugin requires a SQL Warehouse. Add a sql_warehouse resource to your plugin manifest, or set DATABRICKS_WAREHOUSE_ID");
|
|
80
|
+
return warehouseId;
|
|
81
|
+
}
|
|
82
|
+
/** Capture compatibility access now so it cannot drift into a later scope. */
|
|
83
|
+
function captureWarehouseId() {
|
|
84
|
+
const binding = legacyResourceStorage.getStore() ?? WarehouseResource.get();
|
|
85
|
+
return () => {
|
|
86
|
+
if (!binding) throw InitializationError.notInitialized("WarehouseResource", "Call createApp() first");
|
|
87
|
+
return binding.warehouseId;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
export { WarehouseResource, captureWarehouseId, getConfiguredWarehouseId, getWarehouseId, runWithResourceBindings };
|
|
93
|
+
//# sourceMappingURL=warehouse.js.map
|