@databricks/appkit 0.61.0 → 0.62.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 +1 -0
- package/NOTICE.md +1 -0
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js.map +1 -1
- package/dist/agents/supervisor-api.d.ts.map +1 -1
- package/dist/agents/supervisor-api.js.map +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/storage/memory.js.map +1 -1
- package/dist/cache/storage/persistent.js.map +1 -1
- package/dist/cli/commands/codemod/index.js.map +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/doctor/bundle.js.map +1 -1
- package/dist/cli/commands/doctor/index.js.map +1 -1
- package/dist/cli/commands/doctor/report.js.map +1 -1
- package/dist/cli/commands/doctor/resolve-targets.js +4 -6
- package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
- package/dist/cli/commands/doctor/run.js.map +1 -1
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
- package/dist/cli/commands/plugin/index.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/client.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js.map +1 -1
- package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/cli/commands/registry/index.js.map +1 -1
- package/dist/cli/commands/registry/info.js.map +1 -1
- package/dist/cli/commands/registry/list.js.map +1 -1
- package/dist/cli/commands/registry/requirements.js.map +1 -1
- package/dist/cli/commands/registry/server-register.js.map +1 -1
- package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/lakebase/index.d.ts.map +1 -1
- package/dist/connectors/lakebase/index.js.map +1 -1
- package/dist/connectors/lakebase/pool-manager.d.ts.map +1 -1
- package/dist/connectors/lakebase/pool-manager.js.map +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/connectors/mcp/client.d.ts.map +1 -1
- package/dist/connectors/mcp/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/context/client-options.js.map +1 -1
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +1 -1
- package/dist/context/service-context.d.ts +55 -3
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js.map +1 -1
- package/dist/core/agent/build-toolkit.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/toolkit-resolver.js.map +1 -1
- package/dist/core/agent/tools/define-tool.d.ts.map +1 -1
- package/dist/core/agent/tools/define-tool.js.map +1 -1
- package/dist/core/agent/tools/tool.d.ts.map +1 -1
- package/dist/core/agent/tools/tool.js.map +1 -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.map +1 -1
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/lifecycle-manager.js.map +1 -1
- package/dist/core/plugin-context.d.ts +13 -0
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +12 -1
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/configuration.d.ts.map +1 -1
- package/dist/errors/configuration.js.map +1 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/logging/wide-event-emitter.js.map +1 -1
- package/dist/plugin/dev-reader.d.ts.map +1 -1
- package/dist/plugin/dev-reader.js.map +1 -1
- package/dist/plugin/interceptors/cache.js.map +1 -1
- package/dist/plugin/interceptors/retry.js.map +1 -1
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +2 -2
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/event-translator.js.map +1 -1
- package/dist/plugins/agents/thread-store.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/mv/cache.js.map +1 -1
- package/dist/plugins/analytics/mv/constants.js.map +1 -1
- package/dist/plugins/analytics/mv/formatters.js.map +1 -1
- package/dist/plugins/analytics/mv/metadata.js.map +1 -1
- package/dist/plugins/analytics/mv/registry.js.map +1 -1
- package/dist/plugins/analytics/mv/schemas.js.map +1 -1
- package/dist/plugins/analytics/query.js.map +1 -1
- package/dist/plugins/analytics/result-delivery.js.map +1 -1
- package/dist/plugins/files/helpers.js.map +1 -1
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/jobs/plugin.d.ts.map +1 -1
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/jobs/types.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/lakebase/types.d.ts.map +1 -1
- package/dist/plugins/server/base-server.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/react-source-loc-vite-plugin.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/plugins/serving/schema-filter.js.map +1 -1
- package/dist/plugins/serving/serving.d.ts.map +1 -1
- package/dist/plugins/serving/serving.js.map +1 -1
- package/dist/plugins/ui-variants/index.js.map +1 -1
- package/dist/registry/manifest-loader.d.ts.map +1 -1
- package/dist/registry/manifest-loader.js.map +1 -1
- package/dist/registry/resource-registry.d.ts.map +1 -1
- package/dist/registry/resource-registry.js.map +1 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry/types.js.map +1 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -1
- package/dist/shared/src/schemas/metric-source.js.map +1 -1
- package/dist/shared/src/sse/analytics.js.map +1 -1
- package/dist/shared/src/workspace-client/legacy.js.map +1 -1
- package/dist/shared/src/workspace-client/types.d.ts.map +1 -1
- package/dist/stream/sse-writer.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/instrumentations.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/telemetry/telemetry-provider.js.map +1 -1
- package/dist/telemetry/trace-sampler.js.map +1 -1
- package/dist/testing/expect-stream.d.ts +111 -0
- package/dist/testing/expect-stream.d.ts.map +1 -0
- package/dist/testing/expect-stream.js +154 -0
- package/dist/testing/expect-stream.js.map +1 -0
- package/dist/testing/fixtures.d.ts +257 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +385 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/test-plugin-context.d.ts +158 -0
- package/dist/testing/test-plugin-context.d.ts.map +1 -0
- package/dist/testing/test-plugin-context.js +139 -0
- package/dist/testing/test-plugin-context.js.map +1 -0
- package/dist/type-generator/cache.js.map +1 -1
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/mv-registry/config.js.map +1 -1
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/cache.js.map +1 -1
- package/dist/type-generator/serving/generator.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/serving/vite-plugin.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/workspace-client/legacy.js.map +1 -1
- package/dist/workspace-client/types.d.ts.map +1 -1
- package/docs/plugins/testing.md +245 -0
- package/llms.txt +1 -0
- package/package.json +15 -2
- package/sbom.cdx.json +1 -1
- package/skills/appkit-ui-variants/SKILL.md +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":[],"sources":["../../../src/core/agent/types.ts"],"sourcesContent":["import type {\n AgentAdapter,\n AgentToolDefinition,\n BasePluginConfig,\n ThreadStore,\n ToolAnnotations,\n} from \"shared\";\nimport type { GenerationParams } from \"../../agents/databricks\";\nimport type { McpHostPolicyConfig } from \"../../connectors/mcp\";\nimport type { FunctionTool } from \"./tools/function-tool\";\nimport type { HostedTool } from \"./tools/hosted-tools\";\n\n/**\n * A tool reference produced by a plugin's `.toolkit()` call. The agents plugin\n * recognizes the `__toolkitRef` brand and dispatches tool invocations through\n * `PluginContext.executeTool(req, pluginName, localName, ...)`, preserving\n * OBO (asUser) and telemetry spans.\n */\nexport interface ToolkitEntry {\n readonly __toolkitRef: true;\n pluginName: string;\n localName: string;\n def: AgentToolDefinition;\n annotations?: ToolAnnotations;\n /**\n * Whether this tool is eligible for `autoInheritTools` spreading. Mirrors\n * {@link ToolEntry.autoInheritable} from the source registry so the agents\n * plugin can filter auto-inherited tools without re-walking the provider's\n * internal registry.\n */\n autoInheritable?: boolean;\n}\n\n/**\n * Any tool an agent can invoke: inline function tools (`tool()`), hosted MCP\n * tools (`mcpServer()` / raw hosted), toolkit references from plugins\n * (`analytics().toolkit()`), or adapter-hosted Supervisor-API tools\n * (`supervisorTools.*`).\n */\nexport type AgentTool =\n | FunctionTool\n | HostedTool\n | ToolkitEntry\n | import(\"../../agents/supervisor-api\").HostedSupervisorTool;\n\nexport interface ToolkitOptions {\n /** Key prefix to prepend to each tool's local name. Defaults to `${pluginName}.`. */\n prefix?: string;\n /** Only include tools whose local name matches one of these. */\n only?: string[];\n /** Exclude tools whose local name matches one of these. */\n except?: string[];\n /** Remap specific local names to different keys (applied after prefix). */\n rename?: Record<string, string>;\n}\n\n/**\n * Minimum shape every entry in the {@link Plugins} map must expose. Core\n * plugins (analytics, files, genie, lakebase) implement this directly via\n * their `.toolkit()` method. The agents plugin and standalone `runAgent`\n * synthesize this shape for any registered plugin that doesn't implement\n * `.toolkit()` directly (falling back to `getAgentTools()` walking).\n */\nexport interface PluginToolkitProvider {\n toolkit(opts?: ToolkitOptions): Record<string, ToolkitEntry>;\n}\n\n/**\n * Plugin map passed to the function form of {@link AgentDefinition.tools}.\n * Each entry exposes a `.toolkit(opts?)` method that returns a record of\n * {@link ToolkitEntry} markers ready to be spread into a tool record.\n *\n * AppKit does not statically know which plugins the surrounding\n * `createApp` will register, so this is a plain string-keyed record.\n * Refer to plugins by the name used in `createApp({ plugins: [...] })`;\n * unknown names resolve to `undefined` at runtime.\n *\n * @example\n * ```ts\n * const support = createAgent({\n * instructions: \"...\",\n * tools(plugins) {\n * return {\n * get_weather: tool({ ... }),\n * ...plugins.analytics.toolkit(),\n * ...plugins.files.toolkit({ only: [\"uploads.read\"] }),\n * };\n * },\n * });\n * ```\n */\nexport type Plugins = Record<string, PluginToolkitProvider>;\n\n/**\n * Context passed to `baseSystemPrompt` callbacks.\n */\nexport interface PromptContext {\n agentName: string;\n pluginNames: string[];\n toolNames: string[];\n}\n\nexport type BaseSystemPromptOption =\n | false\n | string\n | ((ctx: PromptContext) => string);\n\n/**\n * Per-agent tool record. String keys map to inline tools, toolkit entries,\n * hosted tools, etc.\n */\nexport type AgentTools = Record<string, AgentTool>;\n\n/**\n * Function form of `AgentDefinition.tools`. Receives the typed\n * {@link Plugins} map and returns a tool record. Invoked exactly once at\n * setup (or once per `runAgent` call in standalone mode); the result is\n * cached as the agent's resolved tool record.\n *\n * Use the function form when an agent needs tools from registered plugins.\n * The bare object form is fine when an agent only uses inline tools.\n */\nexport type AgentToolsFn = (plugins: Plugins) => AgentTools;\n\nexport interface AgentDefinition {\n /**\n * Stable identifier for the agent. **Optional and informational** —\n * when the definition is registered via `agents: { foo: def }` (code) or\n * lives at `config/agents/<id>/agent.md` (markdown), the **registry key\n * always wins** and `name` is ignored. The agent will be reachable as\n * `foo` (or `<id>`) regardless of what this field contains.\n *\n * Set `name` when:\n * - Running standalone via `runAgent({ agent: def })`, where there is\n * no enclosing key. The runtime uses it for the agent's slot in\n * error messages and OTel spans.\n * - Building a definition that may be passed to either form and you\n * want a consistent fallback label.\n *\n * Setting `name` to a value that differs from the registry key is\n * harmless but confusing — prefer keeping them aligned or omitting `name`\n * entirely.\n */\n name?: string;\n /** System prompt body. For markdown-loaded agents this is the file body. */\n instructions: string;\n /**\n * Model adapter (or endpoint-name string sugar for\n * `DatabricksAdapter.fromServingEndpoint({ endpointName })`). Optional —\n * falls back to the plugin's `defaultModel`.\n */\n model?: AgentAdapter | Promise<AgentAdapter> | string;\n /**\n * Per-agent tool record. Key is the LLM-visible tool-call name.\n *\n * Accepts either a plain record (for agents that only use inline tools)\n * or a function `(plugins) => Record<string, AgentTool>` that receives\n * the typed {@link Plugins} map and returns a tool record (for agents\n * that pull tools from registered plugins).\n *\n * The function is invoked once at agent setup; the result is cached.\n * Don't put per-request logic in there.\n */\n tools?: AgentTools | AgentToolsFn;\n /** Sub-agents, exposed as `agent-<key>` tools on this agent. */\n agents?: Record<string, AgentDefinition>;\n /** Override the plugin's baseSystemPrompt for this agent only. */\n baseSystemPrompt?: BaseSystemPromptOption;\n maxSteps?: number;\n maxTokens?: number;\n /**\n * Optional generation parameters (`temperature`, `top_p`, `stop`,\n * `frequency_penalty`, `presence_penalty`) forwarded to the OpenAI-compatible\n * serving request body. Only set keys are sent. Applied only when AppKit\n * builds the adapter itself (string or omitted `model`); when you pass a\n * pre-built `AgentAdapter`, configure generation params on it directly.\n */\n generationParams?: GenerationParams;\n /**\n * When true, the thread used for a chat request against this agent is\n * deleted from `ThreadStore` after the stream completes (success or\n * failure). Use for stateless one-shot agents — e.g. autocomplete, where\n * each request is independent and retaining history would both poison\n * future calls and accumulate unbounded state in the default\n * `InMemoryThreadStore`. Defaults to `false`.\n */\n ephemeral?: boolean;\n}\n\n/**\n * Auto-inherit configuration. When enabled for a given agent origin, agents\n * with no explicit `tools:` declaration receive every registered ToolProvider\n * plugin tool whose author marked `autoInheritable: true`. Tools without that\n * flag — destructive, state-mutating, or privilege-sensitive — never spread\n * automatically and must be wired via `tools:` (object or function form in\n * code, `plugin:NAME` entries in markdown frontmatter).\n *\n * Defaults are `false` for both origins (safe-by-default): developers must\n * consciously opt an origin in to any auto-inherit behaviour.\n */\nexport interface AutoInheritToolsConfig {\n /** Default for agents loaded from markdown files. Default: `false`. */\n file?: boolean;\n /** Default for code-defined agents (via `agents: { foo: createAgent(...) }`). Default: `false`. */\n code?: boolean;\n}\n\nexport interface AgentsPluginConfig extends BasePluginConfig {\n /** Directory of agent packages (`<id>/agent.md` each). Default `./config/agents`. Set to `false` to disable. */\n dir?: string | false;\n /** Code-defined agents, merged with file-loaded ones (code wins on key collision). */\n agents?: Record<string, AgentDefinition>;\n /** Agent used when clients don't specify one. Defaults to the first-registered agent or the file with `default: true` frontmatter. */\n defaultAgent?: string;\n /** Default model for agents that don't specify their own (in code or frontmatter). */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library. Keys may be referenced by markdown frontmatter via `tools: [key1, key2]`. */\n tools?: Record<string, AgentTool>;\n /** Whether to auto-inherit every ToolProvider plugin's toolkit. Accepts a boolean shorthand. */\n autoInheritTools?: boolean | AutoInheritToolsConfig;\n /** Persistent thread store. Default: in-memory. */\n threadStore?: ThreadStore;\n /** Customize or disable the AppKit base system prompt. */\n baseSystemPrompt?: BaseSystemPromptOption;\n /**\n * MCP server host policy. By default only same-origin Databricks workspace\n * URLs may be used as MCP endpoints; custom hosts must be explicitly\n * allowlisted here. Workspace credentials (SP / OBO) are never forwarded\n * to non-workspace hosts.\n */\n mcp?: McpHostPolicyConfig;\n /**\n * Human-in-the-loop approval gate for mutating tool calls. When enabled\n * (the default), the agents plugin emits an `appkit.approval_pending` SSE\n * event before executing any tool whose annotation flags it as mutating —\n * `effect: \"write\" | \"update\" | \"destructive\"` (preferred) or the legacy\n * `destructive: true` boolean — and waits for a `POST /chat/approve`\n * decision from the same user who initiated the stream. A missing decision\n * after `timeoutMs` auto-denies the call.\n */\n approval?: {\n /**\n * Require human approval for tools that mutate state. Triggered by\n * `effect: \"write\" | \"update\" | \"destructive\"` (preferred) or the legacy\n * `destructive: true` boolean. Default: `true`.\n */\n requireForDestructive?: boolean;\n /** Milliseconds to wait before auto-denying. Default: 60_000. */\n timeoutMs?: number;\n };\n /**\n * Runtime resource limits applied during agent execution. Defaults are\n * tuned to protect a single-instance deployment from a misbehaving user or\n * a runaway prompt injection; tighten or relax as appropriate for the\n * deployment's scale and trust model. Request-body caps (chat message\n * size, invocations input size / length) are enforced statically by the\n * Zod schemas and are not configurable here.\n */\n limits?: {\n /**\n * Max concurrent chat streams a single user may have open. Subsequent\n * `POST /chat` requests from that user while at-limit are rejected with\n * HTTP 429. Default: `5`.\n */\n maxConcurrentStreamsPerUser?: number;\n /**\n * Max tool invocations per agent run (across the full tool-call graph,\n * including sub-agent invocations). A run that exceeds the budget is\n * aborted with a terminal error event. Default: `50`.\n */\n maxToolCalls?: number;\n /**\n * Max sub-agent recursion depth. Protects against a prompt-injected\n * agent that delegates to a sub-agent which in turn delegates back to\n * itself (directly or transitively). Default: `3`.\n */\n maxSubAgentDepth?: number;\n /**\n * Per-call timeout for tools dispatched through `PluginContext`\n * (toolkit-routed tools — analytics SQL warehouse queries, Genie\n * messages, Lakebase queries). Independent of `maxToolCalls`: the\n * budget caps how many tools fire per run, this caps how long any\n * single tool call may run. The signal handed to plugin tool\n * implementations combines this timeout with the parent stream's\n * abort signal via `AbortSignal.any`. Function and MCP tools have\n * their own timeouts in their respective adapters and ignore this\n * setting. Default: `300_000` (5 minutes) — generous enough for cold\n * SQL Warehouse round-trips and long Genie conversations.\n */\n toolCallTimeoutMs?: number;\n };\n}\n\n/** Internal tool-index entry after a tool record has been resolved to a dispatchable form. */\nexport type ResolvedToolEntry =\n | {\n source: \"toolkit\";\n pluginName: string;\n localName: string;\n def: AgentToolDefinition;\n }\n | {\n source: \"function\";\n functionTool: FunctionTool;\n def: AgentToolDefinition;\n }\n | {\n source: \"mcp\";\n mcpToolName: string;\n def: AgentToolDefinition;\n }\n | {\n source: \"subagent\";\n agentName: string;\n def: AgentToolDefinition;\n }\n | {\n /**\n * Adapter-side hosted tool (executed by the model-host, not by the\n * Node process). Today: Supervisor API hosted tools (Genie spaces,\n * UC functions, etc.). The `spec` is opaque to the agents plugin —\n * it routes the entry into `AgentInput.extensions` for the adapter\n * that declared the matching `acceptsExtensions` key. `def` is a\n * synthetic placeholder kept so the index has a uniform shape; it\n * is intentionally NOT included in the `tools` array passed to\n * `adapter.run()` (those entries are not callable functions).\n */\n source: \"hosted-supervisor\";\n spec: import(\"../../agents/supervisor-api\").SupervisorTool;\n def: AgentToolDefinition;\n };\n\nexport interface RegisteredAgent {\n name: string;\n instructions: string;\n adapter: AgentAdapter;\n toolIndex: Map<string, ResolvedToolEntry>;\n baseSystemPrompt?: BaseSystemPromptOption;\n maxSteps?: number;\n maxTokens?: number;\n /** Mirrors `AgentDefinition.generationParams`. */\n generationParams?: GenerationParams;\n /** Mirrors `AgentDefinition.ephemeral` — skip thread persistence. */\n ephemeral?: boolean;\n}\n\n/**\n * Type guard for `ToolkitEntry` — used by the agents plugin to differentiate\n * toolkit references from inline tools in a mixed `tools` record.\n */\nexport function isToolkitEntry(value: unknown): value is ToolkitEntry {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { __toolkitRef?: unknown }).__toolkitRef === true\n );\n}\n"],"mappings":";;;;;AA8VA,SAAgB,eAAe,OAAuC;AACpE,QACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAqC,iBAAiB"}
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../../src/core/agent/types.ts"],"sourcesContent":["import type {\n AgentAdapter,\n AgentToolDefinition,\n BasePluginConfig,\n ThreadStore,\n ToolAnnotations,\n} from \"shared\";\n\nimport type { GenerationParams } from \"../../agents/databricks\";\nimport type { McpHostPolicyConfig } from \"../../connectors/mcp\";\nimport type { FunctionTool } from \"./tools/function-tool\";\nimport type { HostedTool } from \"./tools/hosted-tools\";\n\n/**\n * A tool reference produced by a plugin's `.toolkit()` call. The agents plugin\n * recognizes the `__toolkitRef` brand and dispatches tool invocations through\n * `PluginContext.executeTool(req, pluginName, localName, ...)`, preserving\n * OBO (asUser) and telemetry spans.\n */\nexport interface ToolkitEntry {\n readonly __toolkitRef: true;\n pluginName: string;\n localName: string;\n def: AgentToolDefinition;\n annotations?: ToolAnnotations;\n /**\n * Whether this tool is eligible for `autoInheritTools` spreading. Mirrors\n * {@link ToolEntry.autoInheritable} from the source registry so the agents\n * plugin can filter auto-inherited tools without re-walking the provider's\n * internal registry.\n */\n autoInheritable?: boolean;\n}\n\n/**\n * Any tool an agent can invoke: inline function tools (`tool()`), hosted MCP\n * tools (`mcpServer()` / raw hosted), toolkit references from plugins\n * (`analytics().toolkit()`), or adapter-hosted Supervisor-API tools\n * (`supervisorTools.*`).\n */\nexport type AgentTool =\n | FunctionTool\n | HostedTool\n | ToolkitEntry\n | import(\"../../agents/supervisor-api\").HostedSupervisorTool;\n\nexport interface ToolkitOptions {\n /** Key prefix to prepend to each tool's local name. Defaults to `${pluginName}.`. */\n prefix?: string;\n /** Only include tools whose local name matches one of these. */\n only?: string[];\n /** Exclude tools whose local name matches one of these. */\n except?: string[];\n /** Remap specific local names to different keys (applied after prefix). */\n rename?: Record<string, string>;\n}\n\n/**\n * Minimum shape every entry in the {@link Plugins} map must expose. Core\n * plugins (analytics, files, genie, lakebase) implement this directly via\n * their `.toolkit()` method. The agents plugin and standalone `runAgent`\n * synthesize this shape for any registered plugin that doesn't implement\n * `.toolkit()` directly (falling back to `getAgentTools()` walking).\n */\nexport interface PluginToolkitProvider {\n toolkit(opts?: ToolkitOptions): Record<string, ToolkitEntry>;\n}\n\n/**\n * Plugin map passed to the function form of {@link AgentDefinition.tools}.\n * Each entry exposes a `.toolkit(opts?)` method that returns a record of\n * {@link ToolkitEntry} markers ready to be spread into a tool record.\n *\n * AppKit does not statically know which plugins the surrounding\n * `createApp` will register, so this is a plain string-keyed record.\n * Refer to plugins by the name used in `createApp({ plugins: [...] })`;\n * unknown names resolve to `undefined` at runtime.\n *\n * @example\n * ```ts\n * const support = createAgent({\n * instructions: \"...\",\n * tools(plugins) {\n * return {\n * get_weather: tool({ ... }),\n * ...plugins.analytics.toolkit(),\n * ...plugins.files.toolkit({ only: [\"uploads.read\"] }),\n * };\n * },\n * });\n * ```\n */\nexport type Plugins = Record<string, PluginToolkitProvider>;\n\n/**\n * Context passed to `baseSystemPrompt` callbacks.\n */\nexport interface PromptContext {\n agentName: string;\n pluginNames: string[];\n toolNames: string[];\n}\n\nexport type BaseSystemPromptOption =\n | false\n | string\n | ((ctx: PromptContext) => string);\n\n/**\n * Per-agent tool record. String keys map to inline tools, toolkit entries,\n * hosted tools, etc.\n */\nexport type AgentTools = Record<string, AgentTool>;\n\n/**\n * Function form of `AgentDefinition.tools`. Receives the typed\n * {@link Plugins} map and returns a tool record. Invoked exactly once at\n * setup (or once per `runAgent` call in standalone mode); the result is\n * cached as the agent's resolved tool record.\n *\n * Use the function form when an agent needs tools from registered plugins.\n * The bare object form is fine when an agent only uses inline tools.\n */\nexport type AgentToolsFn = (plugins: Plugins) => AgentTools;\n\nexport interface AgentDefinition {\n /**\n * Stable identifier for the agent. **Optional and informational** —\n * when the definition is registered via `agents: { foo: def }` (code) or\n * lives at `config/agents/<id>/agent.md` (markdown), the **registry key\n * always wins** and `name` is ignored. The agent will be reachable as\n * `foo` (or `<id>`) regardless of what this field contains.\n *\n * Set `name` when:\n * - Running standalone via `runAgent({ agent: def })`, where there is\n * no enclosing key. The runtime uses it for the agent's slot in\n * error messages and OTel spans.\n * - Building a definition that may be passed to either form and you\n * want a consistent fallback label.\n *\n * Setting `name` to a value that differs from the registry key is\n * harmless but confusing — prefer keeping them aligned or omitting `name`\n * entirely.\n */\n name?: string;\n /** System prompt body. For markdown-loaded agents this is the file body. */\n instructions: string;\n /**\n * Model adapter (or endpoint-name string sugar for\n * `DatabricksAdapter.fromServingEndpoint({ endpointName })`). Optional —\n * falls back to the plugin's `defaultModel`.\n */\n model?: AgentAdapter | Promise<AgentAdapter> | string;\n /**\n * Per-agent tool record. Key is the LLM-visible tool-call name.\n *\n * Accepts either a plain record (for agents that only use inline tools)\n * or a function `(plugins) => Record<string, AgentTool>` that receives\n * the typed {@link Plugins} map and returns a tool record (for agents\n * that pull tools from registered plugins).\n *\n * The function is invoked once at agent setup; the result is cached.\n * Don't put per-request logic in there.\n */\n tools?: AgentTools | AgentToolsFn;\n /** Sub-agents, exposed as `agent-<key>` tools on this agent. */\n agents?: Record<string, AgentDefinition>;\n /** Override the plugin's baseSystemPrompt for this agent only. */\n baseSystemPrompt?: BaseSystemPromptOption;\n maxSteps?: number;\n maxTokens?: number;\n /**\n * Optional generation parameters (`temperature`, `top_p`, `stop`,\n * `frequency_penalty`, `presence_penalty`) forwarded to the OpenAI-compatible\n * serving request body. Only set keys are sent. Applied only when AppKit\n * builds the adapter itself (string or omitted `model`); when you pass a\n * pre-built `AgentAdapter`, configure generation params on it directly.\n */\n generationParams?: GenerationParams;\n /**\n * When true, the thread used for a chat request against this agent is\n * deleted from `ThreadStore` after the stream completes (success or\n * failure). Use for stateless one-shot agents — e.g. autocomplete, where\n * each request is independent and retaining history would both poison\n * future calls and accumulate unbounded state in the default\n * `InMemoryThreadStore`. Defaults to `false`.\n */\n ephemeral?: boolean;\n}\n\n/**\n * Auto-inherit configuration. When enabled for a given agent origin, agents\n * with no explicit `tools:` declaration receive every registered ToolProvider\n * plugin tool whose author marked `autoInheritable: true`. Tools without that\n * flag — destructive, state-mutating, or privilege-sensitive — never spread\n * automatically and must be wired via `tools:` (object or function form in\n * code, `plugin:NAME` entries in markdown frontmatter).\n *\n * Defaults are `false` for both origins (safe-by-default): developers must\n * consciously opt an origin in to any auto-inherit behaviour.\n */\nexport interface AutoInheritToolsConfig {\n /** Default for agents loaded from markdown files. Default: `false`. */\n file?: boolean;\n /** Default for code-defined agents (via `agents: { foo: createAgent(...) }`). Default: `false`. */\n code?: boolean;\n}\n\nexport interface AgentsPluginConfig extends BasePluginConfig {\n /** Directory of agent packages (`<id>/agent.md` each). Default `./config/agents`. Set to `false` to disable. */\n dir?: string | false;\n /** Code-defined agents, merged with file-loaded ones (code wins on key collision). */\n agents?: Record<string, AgentDefinition>;\n /** Agent used when clients don't specify one. Defaults to the first-registered agent or the file with `default: true` frontmatter. */\n defaultAgent?: string;\n /** Default model for agents that don't specify their own (in code or frontmatter). */\n defaultModel?: AgentAdapter | Promise<AgentAdapter> | string;\n /** Ambient tool library. Keys may be referenced by markdown frontmatter via `tools: [key1, key2]`. */\n tools?: Record<string, AgentTool>;\n /** Whether to auto-inherit every ToolProvider plugin's toolkit. Accepts a boolean shorthand. */\n autoInheritTools?: boolean | AutoInheritToolsConfig;\n /** Persistent thread store. Default: in-memory. */\n threadStore?: ThreadStore;\n /** Customize or disable the AppKit base system prompt. */\n baseSystemPrompt?: BaseSystemPromptOption;\n /**\n * MCP server host policy. By default only same-origin Databricks workspace\n * URLs may be used as MCP endpoints; custom hosts must be explicitly\n * allowlisted here. Workspace credentials (SP / OBO) are never forwarded\n * to non-workspace hosts.\n */\n mcp?: McpHostPolicyConfig;\n /**\n * Human-in-the-loop approval gate for mutating tool calls. When enabled\n * (the default), the agents plugin emits an `appkit.approval_pending` SSE\n * event before executing any tool whose annotation flags it as mutating —\n * `effect: \"write\" | \"update\" | \"destructive\"` (preferred) or the legacy\n * `destructive: true` boolean — and waits for a `POST /chat/approve`\n * decision from the same user who initiated the stream. A missing decision\n * after `timeoutMs` auto-denies the call.\n */\n approval?: {\n /**\n * Require human approval for tools that mutate state. Triggered by\n * `effect: \"write\" | \"update\" | \"destructive\"` (preferred) or the legacy\n * `destructive: true` boolean. Default: `true`.\n */\n requireForDestructive?: boolean;\n /** Milliseconds to wait before auto-denying. Default: 60_000. */\n timeoutMs?: number;\n };\n /**\n * Runtime resource limits applied during agent execution. Defaults are\n * tuned to protect a single-instance deployment from a misbehaving user or\n * a runaway prompt injection; tighten or relax as appropriate for the\n * deployment's scale and trust model. Request-body caps (chat message\n * size, invocations input size / length) are enforced statically by the\n * Zod schemas and are not configurable here.\n */\n limits?: {\n /**\n * Max concurrent chat streams a single user may have open. Subsequent\n * `POST /chat` requests from that user while at-limit are rejected with\n * HTTP 429. Default: `5`.\n */\n maxConcurrentStreamsPerUser?: number;\n /**\n * Max tool invocations per agent run (across the full tool-call graph,\n * including sub-agent invocations). A run that exceeds the budget is\n * aborted with a terminal error event. Default: `50`.\n */\n maxToolCalls?: number;\n /**\n * Max sub-agent recursion depth. Protects against a prompt-injected\n * agent that delegates to a sub-agent which in turn delegates back to\n * itself (directly or transitively). Default: `3`.\n */\n maxSubAgentDepth?: number;\n /**\n * Per-call timeout for tools dispatched through `PluginContext`\n * (toolkit-routed tools — analytics SQL warehouse queries, Genie\n * messages, Lakebase queries). Independent of `maxToolCalls`: the\n * budget caps how many tools fire per run, this caps how long any\n * single tool call may run. The signal handed to plugin tool\n * implementations combines this timeout with the parent stream's\n * abort signal via `AbortSignal.any`. Function and MCP tools have\n * their own timeouts in their respective adapters and ignore this\n * setting. Default: `300_000` (5 minutes) — generous enough for cold\n * SQL Warehouse round-trips and long Genie conversations.\n */\n toolCallTimeoutMs?: number;\n };\n}\n\n/** Internal tool-index entry after a tool record has been resolved to a dispatchable form. */\nexport type ResolvedToolEntry =\n | {\n source: \"toolkit\";\n pluginName: string;\n localName: string;\n def: AgentToolDefinition;\n }\n | {\n source: \"function\";\n functionTool: FunctionTool;\n def: AgentToolDefinition;\n }\n | {\n source: \"mcp\";\n mcpToolName: string;\n def: AgentToolDefinition;\n }\n | {\n source: \"subagent\";\n agentName: string;\n def: AgentToolDefinition;\n }\n | {\n /**\n * Adapter-side hosted tool (executed by the model-host, not by the\n * Node process). Today: Supervisor API hosted tools (Genie spaces,\n * UC functions, etc.). The `spec` is opaque to the agents plugin —\n * it routes the entry into `AgentInput.extensions` for the adapter\n * that declared the matching `acceptsExtensions` key. `def` is a\n * synthetic placeholder kept so the index has a uniform shape; it\n * is intentionally NOT included in the `tools` array passed to\n * `adapter.run()` (those entries are not callable functions).\n */\n source: \"hosted-supervisor\";\n spec: import(\"../../agents/supervisor-api\").SupervisorTool;\n def: AgentToolDefinition;\n };\n\nexport interface RegisteredAgent {\n name: string;\n instructions: string;\n adapter: AgentAdapter;\n toolIndex: Map<string, ResolvedToolEntry>;\n baseSystemPrompt?: BaseSystemPromptOption;\n maxSteps?: number;\n maxTokens?: number;\n /** Mirrors `AgentDefinition.generationParams`. */\n generationParams?: GenerationParams;\n /** Mirrors `AgentDefinition.ephemeral` — skip thread persistence. */\n ephemeral?: boolean;\n}\n\n/**\n * Type guard for `ToolkitEntry` — used by the agents plugin to differentiate\n * toolkit references from inline tools in a mixed `tools` record.\n */\nexport function isToolkitEntry(value: unknown): value is ToolkitEntry {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { __toolkitRef?: unknown }).__toolkitRef === true\n );\n}\n"],"mappings":";;;;;AA+VA,SAAgB,eAAe,OAAuC;AACpE,QACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAqC,iBAAiB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appkit.d.ts","names":[],"sources":["../../src/core/appkit.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"appkit.d.ts","names":[],"sources":["../../src/core/appkit.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwXsB,SAAA,WACV,UAAA,CAAW,iBAAA,qBAAA,CAErB,MAAA;EACE,OAAA,GAAU,CAAA;EACV,SAAA,GAAY,eAAA;EACZ,KAAA,GAAQ,WAAA;EACR,MAAA,GAAS,eAAA;EACT,cAAA,IAAkB,MAAA,EAAQ,SAAA,CAAU,CAAA,aAAc,OAAA;EAClD,wBAAA;AAAA,IAED,OAAA,CAAQ,SAAA,CAAU,CAAA"}
|
package/dist/core/appkit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appkit.js","names":["#context","#pluginInstances","#setupPromises","productVersion"],"sources":["../../src/core/appkit.ts"],"sourcesContent":["import type {\n BasePlugin,\n CacheConfig,\n InputPluginMap,\n OptionalConfigPluginDef,\n PluginConstructor,\n PluginData,\n PluginMap,\n} from \"shared\";\nimport { version as productVersion } from \"../../package.json\";\nimport { CacheManager } from \"../cache\";\nimport { ServiceContext } from \"../context\";\nimport {\n isInternalTelemetryEnabled,\n TelemetryReporter,\n} from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { isPlainObject } from \"../plugin/plugin\";\nimport { uiVariants } from \"../plugins/ui-variants\";\nimport { ResourceRegistry, ResourceType } from \"../registry\";\nimport type { TelemetryConfig } from \"../telemetry\";\nimport { TelemetryManager } from \"../telemetry\";\nimport type { WorkspaceClient } from \"../workspace-client\";\nimport { LifecycleManager } from \"./lifecycle-manager\";\nimport { isToolProvider, PluginContext } from \"./plugin-context\";\n\nconst logger = createLogger(\"appkit\");\n\nexport class AppKit<TPlugins extends InputPluginMap> {\n #pluginInstances: Record<string, BasePlugin> = {};\n #setupPromises: Promise<void>[] = [];\n #context: PluginContext;\n\n private constructor(config: { plugins: TPlugins }) {\n const { plugins, ...globalConfig } = config;\n\n this.#context = new PluginContext();\n\n const pluginEntries = Object.entries(plugins);\n\n const corePlugins = pluginEntries.filter(([_, p]) => {\n return (p?.plugin?.phase ?? \"normal\") === \"core\";\n });\n const normalPlugins = pluginEntries.filter(\n ([_, p]) => (p?.plugin?.phase ?? \"normal\") === \"normal\",\n );\n const deferredPlugins = pluginEntries.filter(\n ([_, p]) => (p?.plugin?.phase ?? \"normal\") === \"deferred\",\n );\n\n for (const [name, pluginData] of corePlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n\n for (const [name, pluginData] of normalPlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n\n for (const [name, pluginData] of deferredPlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n }\n\n private createAndRegisterPlugin<T extends PluginConstructor>(\n config: Omit<{ plugins: TPlugins }, \"plugins\">,\n name: string,\n pluginData: OptionalConfigPluginDef<T>,\n extraData?: Record<string, unknown>,\n ) {\n const { plugin: Plugin, config: pluginConfig } = pluginData;\n const baseConfig = {\n ...config,\n ...Plugin.DEFAULT_CONFIG,\n ...pluginConfig,\n name,\n ...extraData,\n };\n const pluginInstance = new Plugin(baseConfig);\n\n if (typeof pluginInstance.attachContext === \"function\") {\n pluginInstance.attachContext({\n context: this.#context,\n telemetryConfig: baseConfig.telemetry,\n });\n }\n\n this.#pluginInstances[name] = pluginInstance;\n\n this.#context.registerPlugin(name, pluginInstance);\n if (isToolProvider(pluginInstance)) {\n this.#context.registerToolProvider(name, pluginInstance);\n }\n\n this.#setupPromises.push(pluginInstance.setup());\n\n const self = this;\n\n // The manifest `name` is camelCase, so it doubles as the public handle key\n // (`appkit.aiSearch`). The kebab HTTP route is derived separately.\n Object.defineProperty(this, name, {\n get() {\n const plugin = self.#pluginInstances[name];\n return self.wrapWithAsUser(plugin);\n },\n enumerable: true,\n });\n }\n\n /**\n * Binds all function properties in an exports object to the given context.\n * Recurses into plain objects to handle nested APIs (e.g., volume APIs).\n */\n private bindExportMethods(\n exports: Record<string, unknown>,\n context: BasePlugin,\n ) {\n for (const key in exports) {\n if (!Object.hasOwn(exports, key)) continue;\n const val = exports[key];\n if (typeof val === \"function\") {\n exports[key] = (val as (...args: unknown[]) => unknown).bind(context);\n } else if (isPlainObject(val)) {\n this.bindExportMethods(val as Record<string, unknown>, context);\n }\n }\n }\n\n /**\n * Wraps a plugin's exports with an `asUser` method that returns\n * a user-scoped version of the exports.\n *\n * When `exports()` returns a callable (function), it is returned as-is\n * since the plugin manages its own `asUser` per-call (e.g. files plugin).\n * When it returns a plain object, the standard `asUser` wrapper is added.\n *\n * The OBO-side wrapping lives inside `Plugin.asUser` — calling\n * `plugin.asUser(req).exports()` returns exports whose functions already\n * run inside the user's AsyncLocalStorage scope. AppKit only adapts the\n * shape; it does not own the user-context concept.\n */\n private wrapWithAsUser<T extends BasePlugin>(plugin: T) {\n // If plugin doesn't implement exports(), return empty object\n const pluginExports = plugin.exports?.() ?? {};\n\n // If exports is a function, the plugin manages its own asUser pattern\n if (typeof pluginExports === \"function\") {\n return pluginExports;\n }\n\n const objExports = pluginExports as Record<string, unknown>;\n this.bindExportMethods(objExports, plugin);\n\n // If plugin doesn't support asUser (no asUser method), return exports as-is\n if (typeof (plugin as any).asUser !== \"function\") {\n return objExports;\n }\n\n return {\n ...objExports,\n /**\n * Execute operations using the user's identity from the request.\n * Returns user-scoped exports where all methods execute with the\n * user's Databricks credentials instead of the service principal.\n */\n asUser: (req: import(\"express\").Request) =>\n (plugin as any).asUser(req).exports() as Record<string, unknown>,\n };\n }\n\n static async _createApp<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(\n config: {\n plugins?: T;\n telemetry?: TelemetryConfig;\n cache?: CacheConfig;\n client?: WorkspaceClient;\n onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;\n disableInternalTelemetry?: boolean;\n } = {},\n ): Promise<PluginMap<T>> {\n // Initialize core services\n TelemetryManager.initialize(config?.telemetry);\n await CacheManager.getInstance(config?.cache);\n\n const withDefaults = AppKit.withDefaultPlugins(config.plugins as T);\n const rawPlugins = AppKit.filterDevOnlyPlugins(withDefaults);\n\n // Collect manifest resources via registry\n const registry = new ResourceRegistry();\n registry.collectResources(rawPlugins);\n\n // Derive ServiceContext needs from what manifests declared\n const needsWarehouse = registry\n .getRequired()\n .some((r) => r.type === ResourceType.SQL_WAREHOUSE);\n await ServiceContext.initialize(\n { warehouseId: needsWarehouse },\n config?.client,\n );\n\n // Validate env vars\n registry.enforceValidation();\n\n const preparedPlugins = AppKit.preparePlugins(rawPlugins);\n const mergedConfig = {\n plugins: preparedPlugins,\n };\n\n const instance = new AppKit(mergedConfig);\n\n await Promise.all(instance.#setupPromises);\n await instance.#context.emitLifecycle(\"setup:complete\");\n\n const handle = instance as unknown as PluginMap<T>;\n\n if (config.onPluginsReady) {\n logger.debug(\"Running onPluginsReady hook\");\n await config.onPluginsReady(handle);\n logger.debug(\"onPluginsReady hook completed\");\n }\n\n if (isInternalTelemetryEnabled(config)) {\n AppKit.bootstrapInternalTelemetry();\n }\n\n const serverPlugin = instance.#pluginInstances.server;\n if (serverPlugin && typeof (serverPlugin as any).start === \"function\") {\n await (serverPlugin as any).start();\n }\n\n // Core owns graceful shutdown: install the signal handlers once every\n // plugin has started. Applies uniformly whether or not a server plugin\n // is present — server-less apps still get their telemetry flushed and\n // plugin shutdown() hooks run.\n new LifecycleManager(instance.#context).installSignalHandlers();\n\n return handle;\n }\n\n private static bootstrapInternalTelemetry(): void {\n const serviceCtx = ServiceContext.get();\n const reporter = TelemetryReporter.initialize({\n workspaceId: serviceCtx.workspaceId,\n client: serviceCtx.client,\n appId: process.env.DATABRICKS_CLIENT_ID || \"\",\n appkitVersion: productVersion,\n });\n reporter.start();\n reporter.sendStartup().catch(() => {});\n }\n\n /**\n * Injects framework-owned default plugins the app author shouldn't have to\n * register by hand. Runs before {@link filterDevOnlyPlugins}, so a default\n * that is itself `devOnly` (like `ui-variants`) is present in dev and stripped\n * in prod through the exact same guard as any user plugin — never alive in a\n * deployed app.\n *\n * Currently injects the dev-only `ui-variants` recorder that backs the\n * `<Variants>` UI picker, so neither the developer nor their coding agent has\n * to remember to add it. If the app already registered it explicitly, that\n * entry wins and no duplicate is added.\n */\n private static withDefaultPlugins<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(plugins: T): T {\n const list = (plugins ?? []) as PluginData<\n PluginConstructor,\n unknown,\n string\n >[];\n\n const defaults: PluginData<PluginConstructor, unknown, string>[] = [\n uiVariants(),\n ];\n\n const missing = defaults.filter(\n (def) => !list.some((p) => p?.name === def.name),\n );\n\n return [...list, ...missing] as T;\n }\n\n /**\n * Drops plugins whose manifest declares `devOnly: true` unless\n * `NODE_ENV === \"development\"`. Runs before resource collection so a skipped\n * plugin is never constructed, never injects routes, and has its resource\n * requirements ignored — the framework, not the app author, enforces that\n * dev-only tooling can never run in a deployed app.\n *\n * This is the primary guard; plugins that expose a mutating dev endpoint\n * should still keep an in-handler `NODE_ENV` check as fail-safe defense\n * against being mounted through a path that bypasses this filter.\n */\n private static filterDevOnlyPlugins<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(plugins: T): T {\n if (!plugins || process.env.NODE_ENV === \"development\") return plugins;\n\n return plugins.filter((pluginData) => {\n const isDevOnly = pluginData?.plugin?.manifest?.devOnly === true;\n if (isDevOnly) {\n logger.debug(\n \"Skipping dev-only plugin %s (NODE_ENV=%s)\",\n pluginData?.name ?? \"unknown\",\n process.env.NODE_ENV ?? \"<unset>\",\n );\n }\n return !isDevOnly;\n }) as T;\n }\n\n private static preparePlugins(\n plugins: PluginData<PluginConstructor, unknown, string>[],\n ) {\n const result: InputPluginMap = {};\n for (const currentPlugin of plugins) {\n result[currentPlugin.name] = {\n plugin: currentPlugin.plugin,\n config: currentPlugin.config as Record<string, unknown>,\n };\n }\n return result;\n }\n}\n\n/**\n * Bootstraps AppKit with the provided configuration.\n *\n * Initializes telemetry, cache, and service context, then registers plugins\n * in phase order (core, normal, deferred) and awaits their setup.\n * If a `onPluginsReady` callback is provided it runs after plugin setup but\n * before the server starts, giving you access to the full appkit handle\n * for registering custom routes or performing async setup.\n * The returned object maps each plugin name to its `exports()` API,\n * with an `asUser(req)` method for user-scoped execution.\n *\n * @returns A `PluginMap` keyed by plugin name with typed exports\n *\n * @example Minimal server\n * ```ts\n * import { createApp, server } from \"@databricks/appkit\";\n *\n * await createApp({\n * plugins: [server()],\n * });\n * ```\n *\n * @example Server with custom routes via onPluginsReady\n * ```ts\n * import { createApp, server, analytics } from \"@databricks/appkit\";\n *\n * await 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 */\nexport async function createApp<\n T extends PluginData<PluginConstructor, unknown, string>[],\n>(\n config: {\n plugins?: T;\n telemetry?: TelemetryConfig;\n cache?: CacheConfig;\n client?: WorkspaceClient;\n onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;\n disableInternalTelemetry?: boolean;\n } = {},\n): Promise<PluginMap<T>> {\n return AppKit._createApp(config);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"appkit.js","names":["#context","#pluginInstances","#setupPromises","productVersion"],"sources":["../../src/core/appkit.ts"],"sourcesContent":["import type {\n BasePlugin,\n CacheConfig,\n InputPluginMap,\n OptionalConfigPluginDef,\n PluginConstructor,\n PluginData,\n PluginMap,\n} from \"shared\";\n\nimport { version as productVersion } from \"../../package.json\";\nimport { CacheManager } from \"../cache\";\nimport { ServiceContext } from \"../context\";\nimport {\n isInternalTelemetryEnabled,\n TelemetryReporter,\n} from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { isPlainObject } from \"../plugin/plugin\";\nimport { uiVariants } from \"../plugins/ui-variants\";\nimport { ResourceRegistry, ResourceType } from \"../registry\";\nimport type { TelemetryConfig } from \"../telemetry\";\nimport { TelemetryManager } from \"../telemetry\";\nimport type { WorkspaceClient } from \"../workspace-client\";\nimport { LifecycleManager } from \"./lifecycle-manager\";\nimport { isToolProvider, PluginContext } from \"./plugin-context\";\n\nconst logger = createLogger(\"appkit\");\n\nexport class AppKit<TPlugins extends InputPluginMap> {\n #pluginInstances: Record<string, BasePlugin> = {};\n #setupPromises: Promise<void>[] = [];\n #context: PluginContext;\n\n private constructor(config: { plugins: TPlugins }) {\n const { plugins, ...globalConfig } = config;\n\n this.#context = new PluginContext();\n\n const pluginEntries = Object.entries(plugins);\n\n const corePlugins = pluginEntries.filter(([_, p]) => {\n return (p?.plugin?.phase ?? \"normal\") === \"core\";\n });\n const normalPlugins = pluginEntries.filter(\n ([_, p]) => (p?.plugin?.phase ?? \"normal\") === \"normal\",\n );\n const deferredPlugins = pluginEntries.filter(\n ([_, p]) => (p?.plugin?.phase ?? \"normal\") === \"deferred\",\n );\n\n for (const [name, pluginData] of corePlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n\n for (const [name, pluginData] of normalPlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n\n for (const [name, pluginData] of deferredPlugins) {\n if (pluginData) {\n this.createAndRegisterPlugin(globalConfig, name, pluginData, {\n context: this.#context,\n });\n }\n }\n }\n\n private createAndRegisterPlugin<T extends PluginConstructor>(\n config: Omit<{ plugins: TPlugins }, \"plugins\">,\n name: string,\n pluginData: OptionalConfigPluginDef<T>,\n extraData?: Record<string, unknown>,\n ) {\n const { plugin: Plugin, config: pluginConfig } = pluginData;\n const baseConfig = {\n ...config,\n ...Plugin.DEFAULT_CONFIG,\n ...pluginConfig,\n name,\n ...extraData,\n };\n const pluginInstance = new Plugin(baseConfig);\n\n if (typeof pluginInstance.attachContext === \"function\") {\n pluginInstance.attachContext({\n context: this.#context,\n telemetryConfig: baseConfig.telemetry,\n });\n }\n\n this.#pluginInstances[name] = pluginInstance;\n\n this.#context.registerPlugin(name, pluginInstance);\n if (isToolProvider(pluginInstance)) {\n this.#context.registerToolProvider(name, pluginInstance);\n }\n\n this.#setupPromises.push(pluginInstance.setup());\n\n const self = this;\n\n // The manifest `name` is camelCase, so it doubles as the public handle key\n // (`appkit.aiSearch`). The kebab HTTP route is derived separately.\n Object.defineProperty(this, name, {\n get() {\n const plugin = self.#pluginInstances[name];\n return self.wrapWithAsUser(plugin);\n },\n enumerable: true,\n });\n }\n\n /**\n * Binds all function properties in an exports object to the given context.\n * Recurses into plain objects to handle nested APIs (e.g., volume APIs).\n */\n private bindExportMethods(\n exports: Record<string, unknown>,\n context: BasePlugin,\n ) {\n for (const key in exports) {\n if (!Object.hasOwn(exports, key)) continue;\n const val = exports[key];\n if (typeof val === \"function\") {\n exports[key] = (val as (...args: unknown[]) => unknown).bind(context);\n } else if (isPlainObject(val)) {\n this.bindExportMethods(val as Record<string, unknown>, context);\n }\n }\n }\n\n /**\n * Wraps a plugin's exports with an `asUser` method that returns\n * a user-scoped version of the exports.\n *\n * When `exports()` returns a callable (function), it is returned as-is\n * since the plugin manages its own `asUser` per-call (e.g. files plugin).\n * When it returns a plain object, the standard `asUser` wrapper is added.\n *\n * The OBO-side wrapping lives inside `Plugin.asUser` — calling\n * `plugin.asUser(req).exports()` returns exports whose functions already\n * run inside the user's AsyncLocalStorage scope. AppKit only adapts the\n * shape; it does not own the user-context concept.\n */\n private wrapWithAsUser<T extends BasePlugin>(plugin: T) {\n // If plugin doesn't implement exports(), return empty object\n const pluginExports = plugin.exports?.() ?? {};\n\n // If exports is a function, the plugin manages its own asUser pattern\n if (typeof pluginExports === \"function\") {\n return pluginExports;\n }\n\n const objExports = pluginExports as Record<string, unknown>;\n this.bindExportMethods(objExports, plugin);\n\n // If plugin doesn't support asUser (no asUser method), return exports as-is\n if (typeof (plugin as any).asUser !== \"function\") {\n return objExports;\n }\n\n return {\n ...objExports,\n /**\n * Execute operations using the user's identity from the request.\n * Returns user-scoped exports where all methods execute with the\n * user's Databricks credentials instead of the service principal.\n */\n asUser: (req: import(\"express\").Request) =>\n (plugin as any).asUser(req).exports() as Record<string, unknown>,\n };\n }\n\n static async _createApp<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(\n config: {\n plugins?: T;\n telemetry?: TelemetryConfig;\n cache?: CacheConfig;\n client?: WorkspaceClient;\n onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;\n disableInternalTelemetry?: boolean;\n } = {},\n ): Promise<PluginMap<T>> {\n // Initialize core services\n TelemetryManager.initialize(config?.telemetry);\n await CacheManager.getInstance(config?.cache);\n\n const withDefaults = AppKit.withDefaultPlugins(config.plugins as T);\n const rawPlugins = AppKit.filterDevOnlyPlugins(withDefaults);\n\n // Collect manifest resources via registry\n const registry = new ResourceRegistry();\n registry.collectResources(rawPlugins);\n\n // Derive ServiceContext needs from what manifests declared\n const needsWarehouse = registry\n .getRequired()\n .some((r) => r.type === ResourceType.SQL_WAREHOUSE);\n await ServiceContext.initialize(\n { warehouseId: needsWarehouse },\n config?.client,\n );\n\n // Validate env vars\n registry.enforceValidation();\n\n const preparedPlugins = AppKit.preparePlugins(rawPlugins);\n const mergedConfig = {\n plugins: preparedPlugins,\n };\n\n const instance = new AppKit(mergedConfig);\n\n await Promise.all(instance.#setupPromises);\n await instance.#context.emitLifecycle(\"setup:complete\");\n\n const handle = instance as unknown as PluginMap<T>;\n\n if (config.onPluginsReady) {\n logger.debug(\"Running onPluginsReady hook\");\n await config.onPluginsReady(handle);\n logger.debug(\"onPluginsReady hook completed\");\n }\n\n if (isInternalTelemetryEnabled(config)) {\n AppKit.bootstrapInternalTelemetry();\n }\n\n const serverPlugin = instance.#pluginInstances.server;\n if (serverPlugin && typeof (serverPlugin as any).start === \"function\") {\n await (serverPlugin as any).start();\n }\n\n // Core owns graceful shutdown: install the signal handlers once every\n // plugin has started. Applies uniformly whether or not a server plugin\n // is present — server-less apps still get their telemetry flushed and\n // plugin shutdown() hooks run.\n new LifecycleManager(instance.#context).installSignalHandlers();\n\n return handle;\n }\n\n private static bootstrapInternalTelemetry(): void {\n const serviceCtx = ServiceContext.get();\n const reporter = TelemetryReporter.initialize({\n workspaceId: serviceCtx.workspaceId,\n client: serviceCtx.client,\n appId: process.env.DATABRICKS_CLIENT_ID || \"\",\n appkitVersion: productVersion,\n });\n reporter.start();\n reporter.sendStartup().catch(() => {});\n }\n\n /**\n * Injects framework-owned default plugins the app author shouldn't have to\n * register by hand. Runs before {@link filterDevOnlyPlugins}, so a default\n * that is itself `devOnly` (like `ui-variants`) is present in dev and stripped\n * in prod through the exact same guard as any user plugin — never alive in a\n * deployed app.\n *\n * Currently injects the dev-only `ui-variants` recorder that backs the\n * `<Variants>` UI picker, so neither the developer nor their coding agent has\n * to remember to add it. If the app already registered it explicitly, that\n * entry wins and no duplicate is added.\n */\n private static withDefaultPlugins<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(plugins: T): T {\n const list = (plugins ?? []) as PluginData<\n PluginConstructor,\n unknown,\n string\n >[];\n\n const defaults: PluginData<PluginConstructor, unknown, string>[] = [\n uiVariants(),\n ];\n\n const missing = defaults.filter(\n (def) => !list.some((p) => p?.name === def.name),\n );\n\n return [...list, ...missing] as T;\n }\n\n /**\n * Drops plugins whose manifest declares `devOnly: true` unless\n * `NODE_ENV === \"development\"`. Runs before resource collection so a skipped\n * plugin is never constructed, never injects routes, and has its resource\n * requirements ignored — the framework, not the app author, enforces that\n * dev-only tooling can never run in a deployed app.\n *\n * This is the primary guard; plugins that expose a mutating dev endpoint\n * should still keep an in-handler `NODE_ENV` check as fail-safe defense\n * against being mounted through a path that bypasses this filter.\n */\n private static filterDevOnlyPlugins<\n T extends PluginData<PluginConstructor, unknown, string>[],\n >(plugins: T): T {\n if (!plugins || process.env.NODE_ENV === \"development\") return plugins;\n\n return plugins.filter((pluginData) => {\n const isDevOnly = pluginData?.plugin?.manifest?.devOnly === true;\n if (isDevOnly) {\n logger.debug(\n \"Skipping dev-only plugin %s (NODE_ENV=%s)\",\n pluginData?.name ?? \"unknown\",\n process.env.NODE_ENV ?? \"<unset>\",\n );\n }\n return !isDevOnly;\n }) as T;\n }\n\n private static preparePlugins(\n plugins: PluginData<PluginConstructor, unknown, string>[],\n ) {\n const result: InputPluginMap = {};\n for (const currentPlugin of plugins) {\n result[currentPlugin.name] = {\n plugin: currentPlugin.plugin,\n config: currentPlugin.config as Record<string, unknown>,\n };\n }\n return result;\n }\n}\n\n/**\n * Bootstraps AppKit with the provided configuration.\n *\n * Initializes telemetry, cache, and service context, then registers plugins\n * in phase order (core, normal, deferred) and awaits their setup.\n * If a `onPluginsReady` callback is provided it runs after plugin setup but\n * before the server starts, giving you access to the full appkit handle\n * for registering custom routes or performing async setup.\n * The returned object maps each plugin name to its `exports()` API,\n * with an `asUser(req)` method for user-scoped execution.\n *\n * @returns A `PluginMap` keyed by plugin name with typed exports\n *\n * @example Minimal server\n * ```ts\n * import { createApp, server } from \"@databricks/appkit\";\n *\n * await createApp({\n * plugins: [server()],\n * });\n * ```\n *\n * @example Server with custom routes via onPluginsReady\n * ```ts\n * import { createApp, server, analytics } from \"@databricks/appkit\";\n *\n * await 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 */\nexport async function createApp<\n T extends PluginData<PluginConstructor, unknown, string>[],\n>(\n config: {\n plugins?: T;\n telemetry?: TelemetryConfig;\n cache?: CacheConfig;\n client?: WorkspaceClient;\n onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;\n disableInternalTelemetry?: boolean;\n } = {},\n): Promise<PluginMap<T>> {\n return AppKit._createApp(config);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-manager.js","names":[],"sources":["../../src/core/lifecycle-manager.ts"],"sourcesContent":["import type { BasePlugin } from \"shared\";\nimport { CacheManager } from \"../cache\";\nimport { TelemetryReporter } from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { TelemetryManager } from \"../telemetry\";\nimport type { PluginContext } from \"./plugin-context\";\n\nconst logger = createLogger(\"lifecycle\");\n\n/**\n * Owns the process's graceful-shutdown sequence.\n *\n * Created by AppKit core once every plugin has started. It is the single\n * owner of the SIGTERM/SIGINT handlers and of `process.exit`, mirroring the\n * core-owned startup in `AppKit._createApp`: core initializes telemetry,\n * cache, and the internal-telemetry reporter, and core tears them all down\n * here. Plugins participate through the generic hooks\n * (`abortActiveOperations()`, `shutdown()`, and `onLifecycle(\"shutdown\")`) —\n * they do not touch process signals or the core singletons themselves.\n */\nexport class LifecycleManager {\n /**\n * Overall graceful-shutdown budget before the process is force-exited.\n *\n * Budget arithmetic: plugin `shutdown()` hooks run concurrently and are\n * bounded by {@link PLUGIN_SHUTDOWN_TIMEOUT_MS} (10s); the lifecycle emit\n * is bounded by {@link PHASE_SHUTDOWN_TIMEOUT_MS} (2s); the cache storage\n * close and the telemetry flush run concurrently, each bounded by\n * {@link PHASE_SHUTDOWN_TIMEOUT_MS} (2s). Worst case is\n * 10s + 2s + max(2s, 2s) = 14s, leaving ~1s of margin for the remaining\n * steps (aborts) before this timer force-exits.\n */\n private static readonly SHUTDOWN_TIMEOUT_MS = 15_000;\n /**\n * Per-plugin budget for `shutdown()` hooks. Sized to cover the longest\n * built-in drain (the files plugin waits up to 10s for in-flight writes).\n */\n private static readonly PLUGIN_SHUTDOWN_TIMEOUT_MS = 10_000;\n /**\n * Budget for each non-plugin shutdown phase (the `\"shutdown\"` lifecycle\n * emit, the cache storage close, and the telemetry flush). Keeps the\n * worst-case total under {@link SHUTDOWN_TIMEOUT_MS} — see the arithmetic\n * there.\n */\n private static readonly PHASE_SHUTDOWN_TIMEOUT_MS = 2_000;\n\n /**\n * Guards against re-entrant shutdown (e.g. SIGTERM followed by SIGINT).\n * The flag set in `shutdown` must remain synchronous and first — any\n * `await` before it would open a window for a second signal to re-enter\n * the sequence.\n */\n private isShuttingDown = false;\n /**\n * Name of the shutdown phase currently in flight, so the force-exit log\n * can say where shutdown got stuck without extra bookkeeping.\n */\n private shutdownPhase = \"not started\";\n\n constructor(private readonly context: PluginContext) {}\n\n /**\n * Install the SIGTERM/SIGINT handlers that trigger {@link shutdown}.\n *\n * Uses `process.once` (not `on`) so a repeated signal cannot register the\n * handler twice; re-entrancy from a *different* signal is guarded by\n * `isShuttingDown` inside {@link shutdown}.\n */\n installSignalHandlers(): void {\n process.once(\"SIGTERM\", () => this.shutdown());\n process.once(\"SIGINT\", () => this.shutdown());\n }\n\n /**\n * Run the graceful-shutdown sequence and exit the process.\n *\n * Phases:\n * 1. stop the internal-telemetry reporter\n * 2. abort in-flight work on every plugin (cancellation only — teardown of\n * shared resources belongs in `shutdown()` so peers can still drain)\n * 3. run every plugin's `shutdown()` hook concurrently, each bounded\n * 4. emit the `\"shutdown\"` lifecycle event, bounded\n * 5. close the cache storage and flush telemetry concurrently, each bounded\n *\n * Exits 0 on completion (and on the force-exit backstop): a deliberate\n * shutdown is not a crash. Exit 1 is reserved for an unexpected error\n * thrown by the sequence itself.\n */\n async shutdown(): Promise<void> {\n // Must stay synchronous and first: any await before the flag is set\n // would let a second signal re-enter the shutdown sequence.\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n\n logger.info(\"Starting graceful shutdown...\");\n\n let exitCode = 0;\n\n // Force exit once the overall budget is spent. Exit 0 is deliberate:\n // a force-timeout still happens on a routine deploy (deliberate\n // shutdown, not a crash), and orchestrators record nonzero exits on\n // deploys as crashes. The error log below is the stuck-shutdown\n // signal instead of the exit code.\n const forceExitTimer = setTimeout(() => {\n logger.error(\n \"Graceful shutdown did NOT complete within the %dms budget (phase in flight: %s); force-exiting with code 0.\",\n LifecycleManager.SHUTDOWN_TIMEOUT_MS,\n this.shutdownPhase,\n );\n process.exit(0);\n }, LifecycleManager.SHUTDOWN_TIMEOUT_MS);\n // unref so this backstop timer never by itself keeps the process alive.\n // Any real pending teardown (OTEL export timer, DB pool sockets, the\n // still-open HTTP listener) is a ref'd handle that holds the loop open\n // until this fires; if nothing is ref'd, there is nothing left to tear\n // down and exiting early is correct.\n forceExitTimer.unref();\n\n try {\n const plugins = Array.from(this.context.getPlugins().values());\n\n // 1. stop the internal-telemetry reporter (no-op if never started).\n this.shutdownPhase = \"stopping internal telemetry reporter\";\n TelemetryReporter.getInstance()?.stop();\n\n // 2. abort active operations from plugins (in-flight executions, SSE\n // streams). Cancellation only — resource teardown (e.g. the\n // lakebase pools, the server's socket close) belongs in plugin\n // shutdown() hooks / lifecycle subscribers so other plugins can\n // still drain state through them.\n this.shutdownPhase = \"aborting active operations\";\n for (const plugin of plugins) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n\n // 3. run every plugin's shutdown() hook concurrently, each bounded\n // by a per-plugin timeout so one hung plugin cannot stall exit.\n this.shutdownPhase = \"plugin shutdown() hooks\";\n await Promise.all(\n plugins\n .filter((plugin) => typeof plugin.shutdown === \"function\")\n .map((plugin) => this.runPluginShutdown(plugin)),\n );\n\n // 4. notify lifecycle subscribers, bounded so a slow subscriber\n // cannot eat the remaining budget. The server plugin closes its\n // remaining sockets here, after other plugins have drained.\n this.shutdownPhase = \"shutdown lifecycle emit\";\n try {\n await this.raceWithTimeout(\n this.context.emitLifecycle(\"shutdown\"),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"shutdown lifecycle emit\",\n );\n } catch (err) {\n logger.error(\"Error emitting shutdown lifecycle event: %O\", err);\n }\n\n // 5. close the cache manager's storage (drains the persistent\n // Lakebase pool; no-op for in-memory storage) and flush telemetry.\n // Runs after the lifecycle emit so subscribers can still read the\n // cache. The two are independent (the flush never touches the\n // cache), so they run concurrently — each bounded so a stuck pool\n // drain or stalled OTLP export cannot eat the remaining budget.\n this.shutdownPhase = \"cache storage close + telemetry flush\";\n await Promise.all([this.closeCacheStorage(), this.flushTelemetry()]);\n\n logger.info(\"Graceful shutdown complete\");\n } catch (err) {\n // Exit 1 is reserved for an unexpected error thrown by the sequence\n // itself; every per-phase failure above is already caught and logged.\n logger.error(\"Error during graceful shutdown: %O\", err);\n exitCode = 1;\n }\n\n clearTimeout(forceExitTimer);\n process.exit(exitCode);\n }\n\n /** Close the cache storage, bounded and error-isolated. */\n private async closeCacheStorage(): Promise<void> {\n let cache: CacheManager;\n try {\n cache = CacheManager.getInstanceSync();\n } catch {\n // Cache was never initialized — nothing to close.\n return;\n }\n try {\n await this.raceWithTimeout(\n cache.close(),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"cache storage close\",\n );\n } catch (err) {\n logger.error(\"Error closing cache storage during shutdown: %O\", err);\n }\n }\n\n /** Flush and shut down the telemetry SDK, bounded and error-isolated. */\n private async flushTelemetry(): Promise<void> {\n try {\n await this.raceWithTimeout(\n TelemetryManager.getInstance().shutdown(),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"telemetry flush\",\n );\n } catch (err) {\n logger.error(\"Error flushing telemetry during shutdown: %O\", err);\n }\n }\n\n /**\n * Run a single plugin's `shutdown()` hook bounded by\n * {@link LifecycleManager.PLUGIN_SHUTDOWN_TIMEOUT_MS}. Errors and timeouts\n * are logged but never thrown so one misbehaving plugin cannot block\n * the rest of the shutdown sequence.\n */\n private async runPluginShutdown(plugin: BasePlugin): Promise<void> {\n try {\n await this.raceWithTimeout(\n plugin.shutdown?.(),\n LifecycleManager.PLUGIN_SHUTDOWN_TIMEOUT_MS,\n \"shutdown()\",\n );\n } catch (err) {\n logger.error(\"Error shutting down plugin %s: %O\", plugin.name, err);\n }\n }\n\n /**\n * Race `work` against a timeout. Rejects with a labeled error when the\n * timeout wins. A no-op rejection handler is attached to the work promise\n * before racing so a branch that rejects after the timeout already won\n * does not surface as an unhandledRejection.\n */\n private async raceWithTimeout<T>(\n work: Promise<T> | T,\n timeoutMs: number,\n label: string,\n ): Promise<T> {\n const promise = Promise.resolve(work);\n promise.catch(() => {});\n let timer: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n timer.unref();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecycle-manager.js","names":[],"sources":["../../src/core/lifecycle-manager.ts"],"sourcesContent":["import type { BasePlugin } from \"shared\";\n\nimport { CacheManager } from \"../cache\";\nimport { TelemetryReporter } from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { TelemetryManager } from \"../telemetry\";\nimport type { PluginContext } from \"./plugin-context\";\n\nconst logger = createLogger(\"lifecycle\");\n\n/**\n * Owns the process's graceful-shutdown sequence.\n *\n * Created by AppKit core once every plugin has started. It is the single\n * owner of the SIGTERM/SIGINT handlers and of `process.exit`, mirroring the\n * core-owned startup in `AppKit._createApp`: core initializes telemetry,\n * cache, and the internal-telemetry reporter, and core tears them all down\n * here. Plugins participate through the generic hooks\n * (`abortActiveOperations()`, `shutdown()`, and `onLifecycle(\"shutdown\")`) —\n * they do not touch process signals or the core singletons themselves.\n */\nexport class LifecycleManager {\n /**\n * Overall graceful-shutdown budget before the process is force-exited.\n *\n * Budget arithmetic: plugin `shutdown()` hooks run concurrently and are\n * bounded by {@link PLUGIN_SHUTDOWN_TIMEOUT_MS} (10s); the lifecycle emit\n * is bounded by {@link PHASE_SHUTDOWN_TIMEOUT_MS} (2s); the cache storage\n * close and the telemetry flush run concurrently, each bounded by\n * {@link PHASE_SHUTDOWN_TIMEOUT_MS} (2s). Worst case is\n * 10s + 2s + max(2s, 2s) = 14s, leaving ~1s of margin for the remaining\n * steps (aborts) before this timer force-exits.\n */\n private static readonly SHUTDOWN_TIMEOUT_MS = 15_000;\n /**\n * Per-plugin budget for `shutdown()` hooks. Sized to cover the longest\n * built-in drain (the files plugin waits up to 10s for in-flight writes).\n */\n private static readonly PLUGIN_SHUTDOWN_TIMEOUT_MS = 10_000;\n /**\n * Budget for each non-plugin shutdown phase (the `\"shutdown\"` lifecycle\n * emit, the cache storage close, and the telemetry flush). Keeps the\n * worst-case total under {@link SHUTDOWN_TIMEOUT_MS} — see the arithmetic\n * there.\n */\n private static readonly PHASE_SHUTDOWN_TIMEOUT_MS = 2_000;\n\n /**\n * Guards against re-entrant shutdown (e.g. SIGTERM followed by SIGINT).\n * The flag set in `shutdown` must remain synchronous and first — any\n * `await` before it would open a window for a second signal to re-enter\n * the sequence.\n */\n private isShuttingDown = false;\n /**\n * Name of the shutdown phase currently in flight, so the force-exit log\n * can say where shutdown got stuck without extra bookkeeping.\n */\n private shutdownPhase = \"not started\";\n\n constructor(private readonly context: PluginContext) {}\n\n /**\n * Install the SIGTERM/SIGINT handlers that trigger {@link shutdown}.\n *\n * Uses `process.once` (not `on`) so a repeated signal cannot register the\n * handler twice; re-entrancy from a *different* signal is guarded by\n * `isShuttingDown` inside {@link shutdown}.\n */\n installSignalHandlers(): void {\n process.once(\"SIGTERM\", () => this.shutdown());\n process.once(\"SIGINT\", () => this.shutdown());\n }\n\n /**\n * Run the graceful-shutdown sequence and exit the process.\n *\n * Phases:\n * 1. stop the internal-telemetry reporter\n * 2. abort in-flight work on every plugin (cancellation only — teardown of\n * shared resources belongs in `shutdown()` so peers can still drain)\n * 3. run every plugin's `shutdown()` hook concurrently, each bounded\n * 4. emit the `\"shutdown\"` lifecycle event, bounded\n * 5. close the cache storage and flush telemetry concurrently, each bounded\n *\n * Exits 0 on completion (and on the force-exit backstop): a deliberate\n * shutdown is not a crash. Exit 1 is reserved for an unexpected error\n * thrown by the sequence itself.\n */\n async shutdown(): Promise<void> {\n // Must stay synchronous and first: any await before the flag is set\n // would let a second signal re-enter the shutdown sequence.\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n\n logger.info(\"Starting graceful shutdown...\");\n\n let exitCode = 0;\n\n // Force exit once the overall budget is spent. Exit 0 is deliberate:\n // a force-timeout still happens on a routine deploy (deliberate\n // shutdown, not a crash), and orchestrators record nonzero exits on\n // deploys as crashes. The error log below is the stuck-shutdown\n // signal instead of the exit code.\n const forceExitTimer = setTimeout(() => {\n logger.error(\n \"Graceful shutdown did NOT complete within the %dms budget (phase in flight: %s); force-exiting with code 0.\",\n LifecycleManager.SHUTDOWN_TIMEOUT_MS,\n this.shutdownPhase,\n );\n process.exit(0);\n }, LifecycleManager.SHUTDOWN_TIMEOUT_MS);\n // unref so this backstop timer never by itself keeps the process alive.\n // Any real pending teardown (OTEL export timer, DB pool sockets, the\n // still-open HTTP listener) is a ref'd handle that holds the loop open\n // until this fires; if nothing is ref'd, there is nothing left to tear\n // down and exiting early is correct.\n forceExitTimer.unref();\n\n try {\n const plugins = Array.from(this.context.getPlugins().values());\n\n // 1. stop the internal-telemetry reporter (no-op if never started).\n this.shutdownPhase = \"stopping internal telemetry reporter\";\n TelemetryReporter.getInstance()?.stop();\n\n // 2. abort active operations from plugins (in-flight executions, SSE\n // streams). Cancellation only — resource teardown (e.g. the\n // lakebase pools, the server's socket close) belongs in plugin\n // shutdown() hooks / lifecycle subscribers so other plugins can\n // still drain state through them.\n this.shutdownPhase = \"aborting active operations\";\n for (const plugin of plugins) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n\n // 3. run every plugin's shutdown() hook concurrently, each bounded\n // by a per-plugin timeout so one hung plugin cannot stall exit.\n this.shutdownPhase = \"plugin shutdown() hooks\";\n await Promise.all(\n plugins\n .filter((plugin) => typeof plugin.shutdown === \"function\")\n .map((plugin) => this.runPluginShutdown(plugin)),\n );\n\n // 4. notify lifecycle subscribers, bounded so a slow subscriber\n // cannot eat the remaining budget. The server plugin closes its\n // remaining sockets here, after other plugins have drained.\n this.shutdownPhase = \"shutdown lifecycle emit\";\n try {\n await this.raceWithTimeout(\n this.context.emitLifecycle(\"shutdown\"),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"shutdown lifecycle emit\",\n );\n } catch (err) {\n logger.error(\"Error emitting shutdown lifecycle event: %O\", err);\n }\n\n // 5. close the cache manager's storage (drains the persistent\n // Lakebase pool; no-op for in-memory storage) and flush telemetry.\n // Runs after the lifecycle emit so subscribers can still read the\n // cache. The two are independent (the flush never touches the\n // cache), so they run concurrently — each bounded so a stuck pool\n // drain or stalled OTLP export cannot eat the remaining budget.\n this.shutdownPhase = \"cache storage close + telemetry flush\";\n await Promise.all([this.closeCacheStorage(), this.flushTelemetry()]);\n\n logger.info(\"Graceful shutdown complete\");\n } catch (err) {\n // Exit 1 is reserved for an unexpected error thrown by the sequence\n // itself; every per-phase failure above is already caught and logged.\n logger.error(\"Error during graceful shutdown: %O\", err);\n exitCode = 1;\n }\n\n clearTimeout(forceExitTimer);\n process.exit(exitCode);\n }\n\n /** Close the cache storage, bounded and error-isolated. */\n private async closeCacheStorage(): Promise<void> {\n let cache: CacheManager;\n try {\n cache = CacheManager.getInstanceSync();\n } catch {\n // Cache was never initialized — nothing to close.\n return;\n }\n try {\n await this.raceWithTimeout(\n cache.close(),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"cache storage close\",\n );\n } catch (err) {\n logger.error(\"Error closing cache storage during shutdown: %O\", err);\n }\n }\n\n /** Flush and shut down the telemetry SDK, bounded and error-isolated. */\n private async flushTelemetry(): Promise<void> {\n try {\n await this.raceWithTimeout(\n TelemetryManager.getInstance().shutdown(),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"telemetry flush\",\n );\n } catch (err) {\n logger.error(\"Error flushing telemetry during shutdown: %O\", err);\n }\n }\n\n /**\n * Run a single plugin's `shutdown()` hook bounded by\n * {@link LifecycleManager.PLUGIN_SHUTDOWN_TIMEOUT_MS}. Errors and timeouts\n * are logged but never thrown so one misbehaving plugin cannot block\n * the rest of the shutdown sequence.\n */\n private async runPluginShutdown(plugin: BasePlugin): Promise<void> {\n try {\n await this.raceWithTimeout(\n plugin.shutdown?.(),\n LifecycleManager.PLUGIN_SHUTDOWN_TIMEOUT_MS,\n \"shutdown()\",\n );\n } catch (err) {\n logger.error(\"Error shutting down plugin %s: %O\", plugin.name, err);\n }\n }\n\n /**\n * Race `work` against a timeout. Rejects with a labeled error when the\n * timeout wins. A no-op rejection handler is attached to the work promise\n * before racing so a branch that rejects after the timeout already won\n * does not surface as an unhandledRejection.\n */\n private async raceWithTimeout<T>(\n work: Promise<T> | T,\n timeoutMs: number,\n label: string,\n ): Promise<T> {\n const promise = Promise.resolve(work);\n promise.catch(() => {});\n let timer: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n timer.unref();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ToolProvider } from "../shared/src/agent.js";
|
|
2
2
|
import { BasePlugin, IAppRequest } from "../shared/src/plugin.js";
|
|
3
3
|
import "../shared/src/index.js";
|
|
4
|
+
import { ITelemetry } from "../telemetry/types.js";
|
|
5
|
+
import "../telemetry/index.js";
|
|
4
6
|
import express from "express";
|
|
5
7
|
|
|
6
8
|
//#region src/core/plugin-context.d.ts
|
|
@@ -50,6 +52,17 @@ declare class PluginContext {
|
|
|
50
52
|
private plugins;
|
|
51
53
|
private lifecycleHooks;
|
|
52
54
|
private telemetry;
|
|
55
|
+
/**
|
|
56
|
+
* @param deps.telemetry - Telemetry provider used for `executeTool` spans.
|
|
57
|
+
* Defaults to the shared `"plugin-context"` provider — the production
|
|
58
|
+
* path. Injectable so the testing kit can pass a mock provider and run
|
|
59
|
+
* `executeTool` without a live OpenTelemetry pipeline. This is the only
|
|
60
|
+
* seam the mock context needs; route buffering and the tool registry are
|
|
61
|
+
* exercised through the existing public API.
|
|
62
|
+
*/
|
|
63
|
+
constructor(deps?: {
|
|
64
|
+
telemetry?: ITelemetry;
|
|
65
|
+
});
|
|
53
66
|
/**
|
|
54
67
|
* Register a route on the root Express application.
|
|
55
68
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-context.d.ts","names":[],"sources":["../../src/core/plugin-context.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin-context.d.ts","names":[],"sources":["../../src/core/plugin-context.ts"],"mappings":";;;;;;;;UAmBU,WAAA;EACR,YAAA,CAAa,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;AAAA;;;AAZX;;;;KAqBjB,kBAAA,GAAqB,UAAA,GACxB,YAAA;EAAiB,MAAA,GAAS,GAAA,EAAK,WAAA,KAAgB,YAAA;AAAA;;;;;AAVI;;;;;;;;;KAyBhD,cAAA;;;;;;;;AAfwD;;;;;AA8B7D;cAAa,aAAA;EAAA,QACH,WAAA;EAAA,QACA,WAAA;EAAA,QACA,aAAA;EAAA,QACA,OAAA;EAAA,QACA,cAAA;EAAA,QAIA,SAAA;EAiG+B;;;;;;;;cAvF3B,IAAA;IAAQ,SAAA,GAAY,UAAA;EAAA;EAuMY;;;;;;;EA3L5C,QAAA,CACE,MAAA,UACA,IAAA,aACG,QAAA,EAAU,OAAA,CAAQ,cAAA;EAzBf;;;;;EAuCR,aAAA,CAAc,IAAA,aAAiB,QAAA,EAAU,OAAA,CAAQ,cAAA;EAhB/C;;;;;;;;;EAiCF,qBAAA,CAAsB,MAAA,EAAQ,WAAA;EAA9B;;;;;;;;EA2BA,oBAAA,CAAqB,IAAA,UAAc,MAAA,EAAQ,kBAAA;EAcJ;;;;EAAvC,cAAA,CAAe,IAAA,UAAc,QAAA,EAAU,UAAA;EAkBvC;;;;;;EARA,UAAA,CAAA,GAAc,WAAA,SAAoB,UAAA;EA8BnB;;;;EAtBf,gBAAA,CAAA,GAAoB,KAAA;IAAQ,IAAA;IAAc,QAAA,EAAU,YAAA;EAAA;EA4BjD;;;;;;;;;;;;;;EAPG,WAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,UAAA,UACA,QAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,EACT,SAAA,YACC,OAAA;EAwHoB;;;;;;;EAvEvB,WAAA,CAAY,KAAA,EAAO,cAAA,EAAgB,EAAA,eAAiB,OAAA;;;;;;;;;EAiB9C,aAAA,CAAc,KAAA,EAAO,cAAA,GAAiB,OAAA;;;;EA8B5C,cAAA,CAAA;;;;EAOA,SAAA,CAAU,IAAA;EAAA,QAIF,UAAA;EAAA,QAaA,eAAA;AAAA"}
|
|
@@ -24,7 +24,18 @@ var PluginContext = class {
|
|
|
24
24
|
toolProviders = /* @__PURE__ */ new Map();
|
|
25
25
|
plugins = /* @__PURE__ */ new Map();
|
|
26
26
|
lifecycleHooks = /* @__PURE__ */ new Map();
|
|
27
|
-
telemetry
|
|
27
|
+
telemetry;
|
|
28
|
+
/**
|
|
29
|
+
* @param deps.telemetry - Telemetry provider used for `executeTool` spans.
|
|
30
|
+
* Defaults to the shared `"plugin-context"` provider — the production
|
|
31
|
+
* path. Injectable so the testing kit can pass a mock provider and run
|
|
32
|
+
* `executeTool` without a live OpenTelemetry pipeline. This is the only
|
|
33
|
+
* seam the mock context needs; route buffering and the tool registry are
|
|
34
|
+
* exercised through the existing public API.
|
|
35
|
+
*/
|
|
36
|
+
constructor(deps = {}) {
|
|
37
|
+
this.telemetry = deps.telemetry ?? TelemetryManager.getProvider("plugin-context");
|
|
38
|
+
}
|
|
28
39
|
/**
|
|
29
40
|
* Register a route on the root Express application.
|
|
30
41
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-context.js","names":[],"sources":["../../src/core/plugin-context.ts"],"sourcesContent":["import type express from \"express\";\nimport type { BasePlugin, IAppRequest, ToolProvider } from \"shared\";\nimport { createLogger } from \"../logging/logger\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { forwardAsyncErrors } from \"../utils/safe-handler\";\n\nconst logger = createLogger(\"plugin-context\");\n\ninterface BufferedRoute {\n method: string;\n path: string;\n handlers: express.RequestHandler[];\n}\n\ninterface RouteTarget {\n addExtension(fn: (app: express.Application) => void): void;\n}\n\n/**\n * A tool-provider plugin that also exposes user-scoped execution. Plugins\n * derived from {@link Plugin} satisfy this implicitly because `asUser` lives\n * on the base class. {@link isToolProvider} narrows to this shape so\n * `executeTool` can call `asUser` without an unsafe cast.\n */\ntype ToolProviderPlugin = BasePlugin &\n ToolProvider & { asUser: (req: IAppRequest) => ToolProvider };\n\n/**\n * Lifecycle events emitted through {@link PluginContext.emitLifecycle}.\n *\n * - `\"setup:complete\"` — emitted by AppKit core after every plugin's\n * `setup()` has finished.\n * - `\"server:ready\"` — emitted when the HTTP server is listening.\n * - `\"shutdown\"` — emitted by the core lifecycle manager during graceful\n * shutdown, AFTER all plugin `shutdown()` hooks have completed. The emit is\n * bounded by a short timeout (see the lifecycle manager's shutdown budget),\n * so subscribers must not start long-running async work — finish quickly or\n * be cut off. (The server plugin subscribes here to force-close its\n * remaining sockets once peers have drained.)\n */\ntype LifecycleEvent = \"setup:complete\" | \"server:ready\" | \"shutdown\";\n\n/**\n * Mediator for inter-plugin communication.\n *\n * Created by AppKit core and passed to every plugin. Plugins request\n * capabilities from the context instead of holding direct references\n * to sibling plugin instances.\n *\n * Capabilities:\n * - Route mounting with buffering (order-independent)\n * - Typed ToolProvider registry (live, not snapshot-based)\n * - User-scoped tool execution with automatic telemetry\n * - Lifecycle hooks for plugin coordination\n */\nexport class PluginContext {\n private routeBuffer: BufferedRoute[] = [];\n private routeTarget: RouteTarget | null = null;\n private toolProviders = new Map<string, ToolProviderPlugin>();\n private plugins = new Map<string, BasePlugin>();\n private lifecycleHooks = new Map<\n LifecycleEvent,\n Set<() => void | Promise<void>>\n >();\n private telemetry = TelemetryManager.getProvider(\"plugin-context\");\n\n /**\n * Register a route on the root Express application.\n *\n * If a route target (server plugin) has registered, the route is applied\n * immediately. Otherwise it is buffered and flushed when a route target\n * becomes available.\n */\n addRoute(\n method: string,\n path: string,\n ...handlers: express.RequestHandler[]\n ): void {\n if (this.routeTarget) {\n this.applyRoute({ method, path, handlers });\n } else {\n this.routeBuffer.push({ method, path, handlers });\n }\n }\n\n /**\n * Register middleware on the root Express application.\n *\n * Same buffering semantics as `addRoute`.\n */\n addMiddleware(path: string, ...handlers: express.RequestHandler[]): void {\n if (this.routeTarget) {\n this.applyMiddleware(path, handlers);\n } else {\n this.routeBuffer.push({ method: \"use\", path, handlers });\n }\n }\n\n /**\n * Called by the server plugin to opt in as the route target.\n * Flushes all buffered routes via the server's `addExtension`.\n *\n * Only the first caller wins — subsequent calls are ignored with a warning.\n * In practice only the server plugin registers, but a misconfigured app\n * (two server plugins, or duplicate AppKit setup in tests) would otherwise\n * silently drop the first target's later extensions.\n */\n registerAsRouteTarget(target: RouteTarget): void {\n if (this.routeTarget) {\n logger.warn(\n \"registerAsRouteTarget called more than once; ignoring duplicate registration\",\n );\n return;\n }\n this.routeTarget = target;\n\n for (const route of this.routeBuffer) {\n if (route.method === \"use\") {\n this.applyMiddleware(route.path, route.handlers);\n } else {\n this.applyRoute(route);\n }\n }\n this.routeBuffer = [];\n }\n\n /**\n * Register a plugin that implements the ToolProvider interface.\n * Called by AppKit core after constructing each plugin.\n *\n * Plugin names should be unique (they are derived from `manifest.name`).\n * A duplicate registration overwrites the previous entry and emits a\n * warning so the misconfiguration is visible in startup logs.\n */\n registerToolProvider(name: string, plugin: ToolProviderPlugin): void {\n if (this.toolProviders.has(name)) {\n logger.warn(\n 'Tool provider \"%s\" registered more than once; the previous registration is being overwritten',\n name,\n );\n }\n this.toolProviders.set(name, plugin);\n }\n\n /**\n * Register a plugin instance.\n * Called by AppKit core after constructing each plugin.\n */\n registerPlugin(name: string, instance: BasePlugin): void {\n this.plugins.set(name, instance);\n }\n\n /**\n * Returns all registered plugin instances keyed by name.\n * Used by the server plugin for route injection, client config,\n * and shutdown coordination. The returned map is read-only at the\n * type level — callers must not mutate the live registry.\n */\n getPlugins(): ReadonlyMap<string, BasePlugin> {\n return this.plugins;\n }\n\n /**\n * Returns all registered ToolProvider plugins.\n * Always returns the current set — not a frozen snapshot.\n */\n getToolProviders(): Array<{ name: string; provider: ToolProvider }> {\n return Array.from(this.toolProviders.entries()).map(([name, provider]) => ({\n name,\n provider,\n }));\n }\n\n /**\n * Execute a tool on a ToolProvider plugin with automatic user scoping\n * and telemetry.\n *\n * The context:\n * 1. Resolves the plugin by name\n * 2. Calls `asUser(req)` for user-scoped execution\n * 3. Wraps the call in a telemetry span with a configurable timeout\n *\n * @param timeoutMs Per-call timeout. Defaults to 5 minutes — the floor\n * for cold SQL Warehouse round-trips, long Genie conversations, and\n * busy serverless Lakebase queries. The agents plugin overrides this\n * per-app via `agents({ limits: { toolCallTimeoutMs } })`.\n */\n async executeTool(\n req: express.Request,\n pluginName: string,\n toolName: string,\n args: unknown,\n signal?: AbortSignal,\n timeoutMs: number = 300_000,\n ): Promise<unknown> {\n const provider = this.toolProviders.get(pluginName);\n if (!provider) {\n throw new Error(\n `PluginContext: unknown plugin \"${pluginName}\". Available: ${Array.from(this.toolProviders.keys()).join(\", \")}`,\n );\n }\n\n const tracer = this.telemetry.getTracer();\n const operationName = `executeTool:${pluginName}.${toolName}`;\n\n return tracer.startActiveSpan(operationName, async (span) => {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n const combinedSignal = signal\n ? AbortSignal.any([signal, timeoutSignal])\n : timeoutSignal;\n\n try {\n const userScoped = provider.asUser(req);\n const result = await userScoped.executeAgentTool(\n toolName,\n args,\n combinedSignal,\n );\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message:\n error instanceof Error ? error.message : \"Tool execution failed\",\n });\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n throw error;\n } finally {\n span.end();\n }\n });\n }\n\n /**\n * Register a lifecycle hook callback.\n *\n * See {@link LifecycleEvent} for event semantics. In particular,\n * `\"shutdown\"` subscribers run inside a bounded shutdown phase and must\n * not start long-running async work.\n */\n onLifecycle(event: LifecycleEvent, fn: () => void | Promise<void>): void {\n let hooks = this.lifecycleHooks.get(event);\n if (!hooks) {\n hooks = new Set();\n this.lifecycleHooks.set(event, hooks);\n }\n hooks.add(fn);\n }\n\n /**\n * Emit a lifecycle event, calling all registered callbacks.\n * Errors in individual callbacks are logged but do not prevent\n * other callbacks from running.\n *\n * @internal Called by AppKit core: `setup:complete` after plugin setup,\n * and `shutdown` by the lifecycle manager during graceful shutdown.\n */\n async emitLifecycle(event: LifecycleEvent): Promise<void> {\n const hooks = this.lifecycleHooks.get(event);\n if (!hooks) return;\n\n if (\n event === \"setup:complete\" &&\n this.routeBuffer.length > 0 &&\n !this.routeTarget\n ) {\n logger.warn(\n \"%d buffered routes were never applied — no server plugin registered as route target\",\n this.routeBuffer.length,\n );\n }\n\n // Snapshot before iterating so a callback that registers a new hook for\n // the same event does not mutate the loop. ECMAScript Set iteration would\n // otherwise visit late-added entries, risking unexpected re-entry.\n for (const fn of [...hooks]) {\n try {\n await fn();\n } catch (error) {\n logger.error(\"Lifecycle hook '%s' failed: %O\", event, error);\n }\n }\n }\n\n /**\n * Returns all registered plugin names.\n */\n getPluginNames(): string[] {\n return Array.from(this.plugins.keys());\n }\n\n /**\n * Check if a plugin with the given name is registered.\n */\n hasPlugin(name: string): boolean {\n return this.plugins.has(name);\n }\n\n private applyRoute(route: BufferedRoute): void {\n if (!this.routeTarget) return;\n this.routeTarget.addExtension((app) => {\n const method = route.method.toLowerCase() as keyof express.Application;\n if (typeof app[method] === \"function\") {\n (app[method] as (...a: unknown[]) => void)(\n route.path,\n ...route.handlers.map(forwardAsyncErrors),\n );\n }\n });\n }\n\n private applyMiddleware(\n path: string,\n handlers: express.RequestHandler[],\n ): void {\n if (!this.routeTarget) return;\n this.routeTarget.addExtension((app) => {\n app.use(path, ...handlers.map(forwardAsyncErrors));\n });\n }\n}\n\n/**\n * Type guard: checks whether a plugin implements the ToolProvider interface\n * and exposes the user-scoped `asUser` helper that the {@link Plugin} base\n * class provides. Narrowing to {@link ToolProviderPlugin} lets `executeTool`\n * call `asUser` without an unsafe cast.\n */\nexport function isToolProvider(plugin: unknown): plugin is ToolProviderPlugin {\n return (\n typeof plugin === \"object\" &&\n plugin !== null &&\n \"getAgentTools\" in plugin &&\n typeof (plugin as ToolProvider).getAgentTools === \"function\" &&\n \"executeAgentTool\" in plugin &&\n typeof (plugin as ToolProvider).executeAgentTool === \"function\" &&\n \"asUser\" in plugin &&\n typeof (plugin as { asUser?: unknown }).asUser === \"function\"\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin-context.js","names":[],"sources":["../../src/core/plugin-context.ts"],"sourcesContent":["import type express from \"express\";\nimport type { BasePlugin, IAppRequest, ToolProvider } from \"shared\";\n\nimport { createLogger } from \"../logging/logger\";\nimport {\n type ITelemetry,\n SpanStatusCode,\n TelemetryManager,\n} from \"../telemetry\";\nimport { forwardAsyncErrors } from \"../utils/safe-handler\";\n\nconst logger = createLogger(\"plugin-context\");\n\ninterface BufferedRoute {\n method: string;\n path: string;\n handlers: express.RequestHandler[];\n}\n\ninterface RouteTarget {\n addExtension(fn: (app: express.Application) => void): void;\n}\n\n/**\n * A tool-provider plugin that also exposes user-scoped execution. Plugins\n * derived from {@link Plugin} satisfy this implicitly because `asUser` lives\n * on the base class. {@link isToolProvider} narrows to this shape so\n * `executeTool` can call `asUser` without an unsafe cast.\n */\ntype ToolProviderPlugin = BasePlugin &\n ToolProvider & { asUser: (req: IAppRequest) => ToolProvider };\n\n/**\n * Lifecycle events emitted through {@link PluginContext.emitLifecycle}.\n *\n * - `\"setup:complete\"` — emitted by AppKit core after every plugin's\n * `setup()` has finished.\n * - `\"server:ready\"` — emitted when the HTTP server is listening.\n * - `\"shutdown\"` — emitted by the core lifecycle manager during graceful\n * shutdown, AFTER all plugin `shutdown()` hooks have completed. The emit is\n * bounded by a short timeout (see the lifecycle manager's shutdown budget),\n * so subscribers must not start long-running async work — finish quickly or\n * be cut off. (The server plugin subscribes here to force-close its\n * remaining sockets once peers have drained.)\n */\ntype LifecycleEvent = \"setup:complete\" | \"server:ready\" | \"shutdown\";\n\n/**\n * Mediator for inter-plugin communication.\n *\n * Created by AppKit core and passed to every plugin. Plugins request\n * capabilities from the context instead of holding direct references\n * to sibling plugin instances.\n *\n * Capabilities:\n * - Route mounting with buffering (order-independent)\n * - Typed ToolProvider registry (live, not snapshot-based)\n * - User-scoped tool execution with automatic telemetry\n * - Lifecycle hooks for plugin coordination\n */\nexport class PluginContext {\n private routeBuffer: BufferedRoute[] = [];\n private routeTarget: RouteTarget | null = null;\n private toolProviders = new Map<string, ToolProviderPlugin>();\n private plugins = new Map<string, BasePlugin>();\n private lifecycleHooks = new Map<\n LifecycleEvent,\n Set<() => void | Promise<void>>\n >();\n private telemetry: ITelemetry;\n\n /**\n * @param deps.telemetry - Telemetry provider used for `executeTool` spans.\n * Defaults to the shared `\"plugin-context\"` provider — the production\n * path. Injectable so the testing kit can pass a mock provider and run\n * `executeTool` without a live OpenTelemetry pipeline. This is the only\n * seam the mock context needs; route buffering and the tool registry are\n * exercised through the existing public API.\n */\n constructor(deps: { telemetry?: ITelemetry } = {}) {\n this.telemetry =\n deps.telemetry ?? TelemetryManager.getProvider(\"plugin-context\");\n }\n\n /**\n * Register a route on the root Express application.\n *\n * If a route target (server plugin) has registered, the route is applied\n * immediately. Otherwise it is buffered and flushed when a route target\n * becomes available.\n */\n addRoute(\n method: string,\n path: string,\n ...handlers: express.RequestHandler[]\n ): void {\n if (this.routeTarget) {\n this.applyRoute({ method, path, handlers });\n } else {\n this.routeBuffer.push({ method, path, handlers });\n }\n }\n\n /**\n * Register middleware on the root Express application.\n *\n * Same buffering semantics as `addRoute`.\n */\n addMiddleware(path: string, ...handlers: express.RequestHandler[]): void {\n if (this.routeTarget) {\n this.applyMiddleware(path, handlers);\n } else {\n this.routeBuffer.push({ method: \"use\", path, handlers });\n }\n }\n\n /**\n * Called by the server plugin to opt in as the route target.\n * Flushes all buffered routes via the server's `addExtension`.\n *\n * Only the first caller wins — subsequent calls are ignored with a warning.\n * In practice only the server plugin registers, but a misconfigured app\n * (two server plugins, or duplicate AppKit setup in tests) would otherwise\n * silently drop the first target's later extensions.\n */\n registerAsRouteTarget(target: RouteTarget): void {\n if (this.routeTarget) {\n logger.warn(\n \"registerAsRouteTarget called more than once; ignoring duplicate registration\",\n );\n return;\n }\n this.routeTarget = target;\n\n for (const route of this.routeBuffer) {\n if (route.method === \"use\") {\n this.applyMiddleware(route.path, route.handlers);\n } else {\n this.applyRoute(route);\n }\n }\n this.routeBuffer = [];\n }\n\n /**\n * Register a plugin that implements the ToolProvider interface.\n * Called by AppKit core after constructing each plugin.\n *\n * Plugin names should be unique (they are derived from `manifest.name`).\n * A duplicate registration overwrites the previous entry and emits a\n * warning so the misconfiguration is visible in startup logs.\n */\n registerToolProvider(name: string, plugin: ToolProviderPlugin): void {\n if (this.toolProviders.has(name)) {\n logger.warn(\n 'Tool provider \"%s\" registered more than once; the previous registration is being overwritten',\n name,\n );\n }\n this.toolProviders.set(name, plugin);\n }\n\n /**\n * Register a plugin instance.\n * Called by AppKit core after constructing each plugin.\n */\n registerPlugin(name: string, instance: BasePlugin): void {\n this.plugins.set(name, instance);\n }\n\n /**\n * Returns all registered plugin instances keyed by name.\n * Used by the server plugin for route injection, client config,\n * and shutdown coordination. The returned map is read-only at the\n * type level — callers must not mutate the live registry.\n */\n getPlugins(): ReadonlyMap<string, BasePlugin> {\n return this.plugins;\n }\n\n /**\n * Returns all registered ToolProvider plugins.\n * Always returns the current set — not a frozen snapshot.\n */\n getToolProviders(): Array<{ name: string; provider: ToolProvider }> {\n return Array.from(this.toolProviders.entries()).map(([name, provider]) => ({\n name,\n provider,\n }));\n }\n\n /**\n * Execute a tool on a ToolProvider plugin with automatic user scoping\n * and telemetry.\n *\n * The context:\n * 1. Resolves the plugin by name\n * 2. Calls `asUser(req)` for user-scoped execution\n * 3. Wraps the call in a telemetry span with a configurable timeout\n *\n * @param timeoutMs Per-call timeout. Defaults to 5 minutes — the floor\n * for cold SQL Warehouse round-trips, long Genie conversations, and\n * busy serverless Lakebase queries. The agents plugin overrides this\n * per-app via `agents({ limits: { toolCallTimeoutMs } })`.\n */\n async executeTool(\n req: express.Request,\n pluginName: string,\n toolName: string,\n args: unknown,\n signal?: AbortSignal,\n timeoutMs: number = 300_000,\n ): Promise<unknown> {\n const provider = this.toolProviders.get(pluginName);\n if (!provider) {\n throw new Error(\n `PluginContext: unknown plugin \"${pluginName}\". Available: ${Array.from(this.toolProviders.keys()).join(\", \")}`,\n );\n }\n\n const tracer = this.telemetry.getTracer();\n const operationName = `executeTool:${pluginName}.${toolName}`;\n\n return tracer.startActiveSpan(operationName, async (span) => {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n const combinedSignal = signal\n ? AbortSignal.any([signal, timeoutSignal])\n : timeoutSignal;\n\n try {\n const userScoped = provider.asUser(req);\n const result = await userScoped.executeAgentTool(\n toolName,\n args,\n combinedSignal,\n );\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message:\n error instanceof Error ? error.message : \"Tool execution failed\",\n });\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n throw error;\n } finally {\n span.end();\n }\n });\n }\n\n /**\n * Register a lifecycle hook callback.\n *\n * See {@link LifecycleEvent} for event semantics. In particular,\n * `\"shutdown\"` subscribers run inside a bounded shutdown phase and must\n * not start long-running async work.\n */\n onLifecycle(event: LifecycleEvent, fn: () => void | Promise<void>): void {\n let hooks = this.lifecycleHooks.get(event);\n if (!hooks) {\n hooks = new Set();\n this.lifecycleHooks.set(event, hooks);\n }\n hooks.add(fn);\n }\n\n /**\n * Emit a lifecycle event, calling all registered callbacks.\n * Errors in individual callbacks are logged but do not prevent\n * other callbacks from running.\n *\n * @internal Called by AppKit core: `setup:complete` after plugin setup,\n * and `shutdown` by the lifecycle manager during graceful shutdown.\n */\n async emitLifecycle(event: LifecycleEvent): Promise<void> {\n const hooks = this.lifecycleHooks.get(event);\n if (!hooks) return;\n\n if (\n event === \"setup:complete\" &&\n this.routeBuffer.length > 0 &&\n !this.routeTarget\n ) {\n logger.warn(\n \"%d buffered routes were never applied — no server plugin registered as route target\",\n this.routeBuffer.length,\n );\n }\n\n // Snapshot before iterating so a callback that registers a new hook for\n // the same event does not mutate the loop. ECMAScript Set iteration would\n // otherwise visit late-added entries, risking unexpected re-entry.\n for (const fn of [...hooks]) {\n try {\n await fn();\n } catch (error) {\n logger.error(\"Lifecycle hook '%s' failed: %O\", event, error);\n }\n }\n }\n\n /**\n * Returns all registered plugin names.\n */\n getPluginNames(): string[] {\n return Array.from(this.plugins.keys());\n }\n\n /**\n * Check if a plugin with the given name is registered.\n */\n hasPlugin(name: string): boolean {\n return this.plugins.has(name);\n }\n\n private applyRoute(route: BufferedRoute): void {\n if (!this.routeTarget) return;\n this.routeTarget.addExtension((app) => {\n const method = route.method.toLowerCase() as keyof express.Application;\n if (typeof app[method] === \"function\") {\n (app[method] as (...a: unknown[]) => void)(\n route.path,\n ...route.handlers.map(forwardAsyncErrors),\n );\n }\n });\n }\n\n private applyMiddleware(\n path: string,\n handlers: express.RequestHandler[],\n ): void {\n if (!this.routeTarget) return;\n this.routeTarget.addExtension((app) => {\n app.use(path, ...handlers.map(forwardAsyncErrors));\n });\n }\n}\n\n/**\n * Type guard: checks whether a plugin implements the ToolProvider interface\n * and exposes the user-scoped `asUser` helper that the {@link Plugin} base\n * class provides. Narrowing to {@link ToolProviderPlugin} lets `executeTool`\n * call `asUser` without an unsafe cast.\n */\nexport function isToolProvider(plugin: unknown): plugin is ToolProviderPlugin {\n return (\n typeof plugin === \"object\" &&\n plugin !== null &&\n \"getAgentTools\" in plugin &&\n typeof (plugin as ToolProvider).getAgentTools === \"function\" &&\n \"executeAgentTool\" in plugin &&\n typeof (plugin as ToolProvider).executeAgentTool === \"function\" &&\n \"asUser\" in plugin &&\n typeof (plugin as { asUser?: unknown }).asUser === \"function\"\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configuration.d.ts","names":[],"sources":["../../src/errors/configuration.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"configuration.d.ts","names":[],"sources":["../../src/errors/configuration.ts"],"mappings":";;;;;AA4CA;;;;;;;;cAAa,kBAAA,SAA2B,WAAA;EAAA,SAC7B,IAAA;EAAA,SACA,UAAA;EAAA,SACA,WAAA;EAH6B;;;EAAA,OAQ/B,aAAA,CAAc,OAAA,WAAkB,kBAAA;EAAhC;;;EAAA,OAUA,gBAAA,CAAiB,QAAA,UAAkB,IAAA,YAAgB,kBAAA;EAAlC;;;EAAA,OAUjB,iBAAA,CACL,OAAA,UACA,OAAA,YACC,kBAAA;EAFD;;;EAAA,OAYK,sBAAA,CAAuB,KAAA,WAAgB,kBAAA;EAAhB;;;;;;;;EAAA,OAevB,mCAAA,CACL,MAAA,UACA,OAAA;IAAY,KAAA,GAAQ,KAAA;EAAA,IACnB,kBAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configuration.js","names":[],"sources":["../../src/errors/configuration.ts"],"sourcesContent":["import pc from \"picocolors\";\nimport { AppKitError } from \"./base\";\n\nfunction authSetupVerbose(): boolean {\n return (\n process.env.APPKIT_VERBOSE_AUTH_ERRORS === \"1\" ||\n process.env.APPKIT_VERBOSE_AUTH_ERRORS === \"true\"\n );\n}\n\n/** Pulls ` $ databricks ...` from SDK text when present. */\nfunction suggestedDatabricksCliCommand(detail: string): string | undefined {\n const m = detail.match(/\\$\\s*(databricks[^\\n]+)/);\n return m?.[1]?.trim();\n}\n\n/** Makes `console.error` show only the message (no stack, no extra fields). */\nfunction pinUserFacingAuthError(err: ConfigurationError): void {\n Object.defineProperty(err, \"stack\", {\n value: \"\",\n configurable: true,\n enumerable: false,\n writable: true,\n });\n Object.defineProperty(err, Symbol.for(\"nodejs.util.inspect.custom\"), {\n value: function (this: ConfigurationError): string {\n return this.message;\n },\n enumerable: false,\n configurable: true,\n });\n}\n\n/**\n * Error thrown when configuration is missing or invalid.\n * Use for missing environment variables, invalid settings, or setup issues.\n *\n * @example\n * ```typescript\n * throw new ConfigurationError(\"DATABRICKS_HOST environment variable is required\");\n * throw new ConfigurationError(\"Warehouse ID not found\", { context: { env: \"production\" } });\n * ```\n */\nexport class ConfigurationError extends AppKitError {\n readonly code = \"CONFIGURATION_ERROR\";\n readonly statusCode = 500;\n readonly isRetryable = false;\n\n /**\n * Create a configuration error for missing environment variable\n */\n static missingEnvVar(varName: string): ConfigurationError {\n return new ConfigurationError(\n `${varName} environment variable is required`,\n { context: { envVar: varName } },\n );\n }\n\n /**\n * Create a configuration error for missing resource\n */\n static resourceNotFound(resource: string, hint?: string): ConfigurationError {\n const message = hint\n ? `${resource} not found. ${hint}`\n : `${resource} not found`;\n return new ConfigurationError(message, { context: { resource } });\n }\n\n /**\n * Create a configuration error for invalid connection config\n */\n static invalidConnection(\n service: string,\n details?: string,\n ): ConfigurationError {\n const message = details\n ? `${service} connection not configured. ${details}`\n : `${service} connection not configured`;\n return new ConfigurationError(message, { context: { service } });\n }\n\n /**\n * Create a configuration error for missing connection string parameter\n */\n static missingConnectionParam(param: string): ConfigurationError {\n return new ConfigurationError(\n `Connection string must include ${param} parameter`,\n { context: { parameter: param } },\n );\n }\n\n /**\n * Databricks CLI / token auth failed while creating the workspace client.\n *\n * By default the message is short; key lines use **picocolors** when the\n * terminal supports it (also respects `NO_COLOR`). `console.error` won’t show\n * stacks or `{ code, context, … }`. Set `APPKIT_VERBOSE_AUTH_ERRORS=1` for full\n * `cause`, stack, and the raw SDK message (verbose appendix is unstyled).\n */\n static databricksAuthenticationSetupFailed(\n detail: string,\n options?: { cause?: Error },\n ): ConfigurationError {\n const verbose = authSetupVerbose();\n const host = process.env.DATABRICKS_HOST ?? \"(not set)\";\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID;\n const d = detail.trim();\n const cli = suggestedDatabricksCliCommand(d);\n\n const title = pc.bold(pc.red(\"Databricks authentication failed.\"));\n const action = cli\n ? `${pc.bold(\"Run this, then try again:\")}\\n ${pc.cyan(cli)}`\n : pc.yellow(\n \"Log in with the Databricks CLI (for example, databricks auth login for this workspace), then try again.\",\n );\n const tokenHint = pc.dim(\n \"Or set DATABRICKS_TOKEN and DATABRICKS_HOST instead of CLI-based auth.\",\n );\n\n const lines: string[] = [\n title,\n \"\",\n action,\n \"\",\n tokenHint,\n \"\",\n `${pc.green(\"DATABRICKS_HOST\")}: ${host}`,\n ];\n if (warehouseId) {\n lines.push(`${pc.green(\"DATABRICKS_WAREHOUSE_ID\")}: ${warehouseId}`);\n }\n if (verbose) {\n lines.push(\"\", d);\n }\n\n const err = new ConfigurationError(lines.join(\"\\n\"), {\n cause: verbose ? options?.cause : undefined,\n });\n\n if (!verbose) {\n pinUserFacingAuthError(err);\n }\n return err;\n }\n}\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"configuration.js","names":[],"sources":["../../src/errors/configuration.ts"],"sourcesContent":["import pc from \"picocolors\";\n\nimport { AppKitError } from \"./base\";\n\nfunction authSetupVerbose(): boolean {\n return (\n process.env.APPKIT_VERBOSE_AUTH_ERRORS === \"1\" ||\n process.env.APPKIT_VERBOSE_AUTH_ERRORS === \"true\"\n );\n}\n\n/** Pulls ` $ databricks ...` from SDK text when present. */\nfunction suggestedDatabricksCliCommand(detail: string): string | undefined {\n const m = detail.match(/\\$\\s*(databricks[^\\n]+)/);\n return m?.[1]?.trim();\n}\n\n/** Makes `console.error` show only the message (no stack, no extra fields). */\nfunction pinUserFacingAuthError(err: ConfigurationError): void {\n Object.defineProperty(err, \"stack\", {\n value: \"\",\n configurable: true,\n enumerable: false,\n writable: true,\n });\n Object.defineProperty(err, Symbol.for(\"nodejs.util.inspect.custom\"), {\n value: function (this: ConfigurationError): string {\n return this.message;\n },\n enumerable: false,\n configurable: true,\n });\n}\n\n/**\n * Error thrown when configuration is missing or invalid.\n * Use for missing environment variables, invalid settings, or setup issues.\n *\n * @example\n * ```typescript\n * throw new ConfigurationError(\"DATABRICKS_HOST environment variable is required\");\n * throw new ConfigurationError(\"Warehouse ID not found\", { context: { env: \"production\" } });\n * ```\n */\nexport class ConfigurationError extends AppKitError {\n readonly code = \"CONFIGURATION_ERROR\";\n readonly statusCode = 500;\n readonly isRetryable = false;\n\n /**\n * Create a configuration error for missing environment variable\n */\n static missingEnvVar(varName: string): ConfigurationError {\n return new ConfigurationError(\n `${varName} environment variable is required`,\n { context: { envVar: varName } },\n );\n }\n\n /**\n * Create a configuration error for missing resource\n */\n static resourceNotFound(resource: string, hint?: string): ConfigurationError {\n const message = hint\n ? `${resource} not found. ${hint}`\n : `${resource} not found`;\n return new ConfigurationError(message, { context: { resource } });\n }\n\n /**\n * Create a configuration error for invalid connection config\n */\n static invalidConnection(\n service: string,\n details?: string,\n ): ConfigurationError {\n const message = details\n ? `${service} connection not configured. ${details}`\n : `${service} connection not configured`;\n return new ConfigurationError(message, { context: { service } });\n }\n\n /**\n * Create a configuration error for missing connection string parameter\n */\n static missingConnectionParam(param: string): ConfigurationError {\n return new ConfigurationError(\n `Connection string must include ${param} parameter`,\n { context: { parameter: param } },\n );\n }\n\n /**\n * Databricks CLI / token auth failed while creating the workspace client.\n *\n * By default the message is short; key lines use **picocolors** when the\n * terminal supports it (also respects `NO_COLOR`). `console.error` won’t show\n * stacks or `{ code, context, … }`. Set `APPKIT_VERBOSE_AUTH_ERRORS=1` for full\n * `cause`, stack, and the raw SDK message (verbose appendix is unstyled).\n */\n static databricksAuthenticationSetupFailed(\n detail: string,\n options?: { cause?: Error },\n ): ConfigurationError {\n const verbose = authSetupVerbose();\n const host = process.env.DATABRICKS_HOST ?? \"(not set)\";\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID;\n const d = detail.trim();\n const cli = suggestedDatabricksCliCommand(d);\n\n const title = pc.bold(pc.red(\"Databricks authentication failed.\"));\n const action = cli\n ? `${pc.bold(\"Run this, then try again:\")}\\n ${pc.cyan(cli)}`\n : pc.yellow(\n \"Log in with the Databricks CLI (for example, databricks auth login for this workspace), then try again.\",\n );\n const tokenHint = pc.dim(\n \"Or set DATABRICKS_TOKEN and DATABRICKS_HOST instead of CLI-based auth.\",\n );\n\n const lines: string[] = [\n title,\n \"\",\n action,\n \"\",\n tokenHint,\n \"\",\n `${pc.green(\"DATABRICKS_HOST\")}: ${host}`,\n ];\n if (warehouseId) {\n lines.push(`${pc.green(\"DATABRICKS_WAREHOUSE_ID\")}: ${warehouseId}`);\n }\n if (verbose) {\n lines.push(\"\", d);\n }\n\n const err = new ConfigurationError(lines.join(\"\\n\"), {\n cause: verbose ? options?.cause : undefined,\n });\n\n if (!verbose) {\n pinUserFacingAuthError(err);\n }\n return err;\n }\n}\n"],"mappings":";;;;AAIA,SAAS,mBAA4B;AACnC,QACE,QAAQ,IAAI,+BAA+B,OAC3C,QAAQ,IAAI,+BAA+B;;;AAK/C,SAAS,8BAA8B,QAAoC;AAEzE,QADU,OAAO,MAAM,0BAA0B,GACtC,IAAI,MAAM;;;AAIvB,SAAS,uBAAuB,KAA+B;AAC7D,QAAO,eAAe,KAAK,SAAS;EAClC,OAAO;EACP,cAAc;EACd,YAAY;EACZ,UAAU;EACX,CAAC;AACF,QAAO,eAAe,KAAK,OAAO,IAAI,6BAA6B,EAAE;EACnE,OAAO,WAA4C;AACjD,UAAO,KAAK;;EAEd,YAAY;EACZ,cAAc;EACf,CAAC;;;;;;;;;;;;AAaJ,IAAa,qBAAb,MAAa,2BAA2B,YAAY;CAClD,AAAS,OAAO;CAChB,AAAS,aAAa;CACtB,AAAS,cAAc;;;;CAKvB,OAAO,cAAc,SAAqC;AACxD,SAAO,IAAI,mBACT,GAAG,QAAQ,oCACX,EAAE,SAAS,EAAE,QAAQ,SAAS,EAAE,CACjC;;;;;CAMH,OAAO,iBAAiB,UAAkB,MAAmC;AAI3E,SAAO,IAAI,mBAHK,OACZ,GAAG,SAAS,cAAc,SAC1B,GAAG,SAAS,aACuB,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;;;;;CAMnE,OAAO,kBACL,SACA,SACoB;AAIpB,SAAO,IAAI,mBAHK,UACZ,GAAG,QAAQ,8BAA8B,YACzC,GAAG,QAAQ,6BACwB,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;;;;;CAMlE,OAAO,uBAAuB,OAAmC;AAC/D,SAAO,IAAI,mBACT,kCAAkC,MAAM,aACxC,EAAE,SAAS,EAAE,WAAW,OAAO,EAAE,CAClC;;;;;;;;;;CAWH,OAAO,oCACL,QACA,SACoB;EACpB,MAAM,UAAU,kBAAkB;EAClC,MAAM,OAAO,QAAQ,IAAI,mBAAmB;EAC5C,MAAM,cAAc,QAAQ,IAAI;EAChC,MAAM,IAAI,OAAO,MAAM;EACvB,MAAM,MAAM,8BAA8B,EAAE;EAY5C,MAAM,QAAkB;GAVV,GAAG,KAAK,GAAG,IAAI,oCAAoC,CAAC;GAYhE;GAXa,MACX,GAAG,GAAG,KAAK,4BAA4B,CAAC,MAAM,GAAG,KAAK,IAAI,KAC1D,GAAG,OACD,0GACD;GASH;GARgB,GAAG,IACnB,yEACD;GAQC;GACA,GAAG,GAAG,MAAM,kBAAkB,CAAC,IAAI;GACpC;AACD,MAAI,YACF,OAAM,KAAK,GAAG,GAAG,MAAM,0BAA0B,CAAC,IAAI,cAAc;AAEtE,MAAI,QACF,OAAM,KAAK,IAAI,EAAE;EAGnB,MAAM,MAAM,IAAI,mBAAmB,MAAM,KAAK,KAAK,EAAE,EACnD,OAAO,UAAU,SAAS,QAAQ,QACnC,CAAC;AAEF,MAAI,CAAC,QACH,wBAAuB,IAAI;AAE7B,SAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","names":["createObug"],"sources":["../../src/logging/logger.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { format } from \"node:util\";\nimport { trace } from \"@opentelemetry/api\";\nimport type { NextFunction, Request, Response } from \"express\";\nimport { createDebug as createObug } from \"obug\";\nimport { DEFAULT_SAMPLING_CONFIG, shouldSample } from \"./sampling\";\nimport { WideEvent } from \"./wide-event\";\nimport { WideEventEmitter } from \"./wide-event-emitter\";\n\n/**\n * Logger interface for AppKit components\n */\ninterface Logger {\n /** Debug output (disabled by default, enable via DEBUG env var) */\n debug(message: string, ...args: unknown[]): void;\n debug(req: Request, message: string, ...args: unknown[]): void;\n\n /** Info output (always visible, for operational messages) */\n info(message: string, ...args: unknown[]): void;\n info(req: Request, message: string, ...args: unknown[]): void;\n\n /** Warning output (always visible, for degraded states) */\n warn(message: string, ...args: unknown[]): void;\n warn(req: Request, message: string, ...args: unknown[]): void;\n\n /** Error output (always visible, for failures) */\n error(message: string, ...args: unknown[]): void;\n error(req: Request, message: string, ...args: unknown[]): void;\n\n /** Get request-scoped WideEvent (from AsyncLocalStorage or explicit req) */\n event(req?: Request): WideEvent | undefined;\n}\n\n// AsyncLocalStorage for WideEvent context propagation\nconst eventStorage = new AsyncLocalStorage<WideEvent>();\n\n// WeakMap to store WideEvent per request (for explicit req usage)\nconst eventsByRequest = new WeakMap<Request, WideEvent>();\n\n// Global emitter instance\nconst emitter = new WideEventEmitter();\n\nconst MAX_REQUEST_ID_LENGTH = 128;\n\n/**\n * Sanitize a request ID from user headers\n */\nfunction sanitizeRequestId(id: string): string {\n const sanitized = id.replace(/[^a-zA-Z0-9_.-]/g, \"\");\n return sanitized.slice(0, MAX_REQUEST_ID_LENGTH);\n}\n\n/**\n * Generate a request ID from the request\n */\nfunction generateRequestId(req: Request): string {\n const existingId =\n req.headers[\"x-request-id\"] ||\n req.headers[\"x-correlation-id\"] ||\n req.headers[\"x-amzn-trace-id\"];\n\n if (existingId && typeof existingId === \"string\" && existingId.length > 0) {\n const sanitized = sanitizeRequestId(existingId);\n if (sanitized.length > 0) {\n return sanitized;\n }\n }\n\n return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n}\n\n/**\n * Create a WideEvent for a request\n */\nfunction createEventForRequest(req: Request): WideEvent {\n const requestId = generateRequestId(req);\n const wideEvent = new WideEvent(requestId);\n\n // extract path from request (strip query string)\n const rawPath = req.path || req.url || req.originalUrl;\n const path = rawPath?.split(\"?\")[0];\n wideEvent.set(\"method\", req.method).set(\"path\", path);\n\n // extract user id from request headers (sanitized)\n const rawUserId = req.headers[\"x-forwarded-user\"];\n if (rawUserId && typeof rawUserId === \"string\" && rawUserId.length > 0) {\n const userId = rawUserId.replace(/[^a-zA-Z0-9_@.-]/g, \"\").slice(0, 128);\n if (userId.length > 0) {\n wideEvent.setUser({ id: userId });\n }\n }\n\n // extract trace id from active span for distributed tracing\n const currentSpan = trace.getActiveSpan();\n const spanContext = currentSpan?.spanContext();\n if (spanContext?.traceId) {\n wideEvent.set(\"trace_id\", spanContext.traceId);\n\n const debugLogger = createObug(\"appkit:logger:event\", { useColors: true });\n debugLogger(\n \"WideEvent created: %s %s (reqId: %s, traceId: %s)\",\n req.method,\n path,\n requestId.substring(0, 8),\n spanContext.traceId.substring(0, 8),\n );\n }\n\n // Update service scope\n if (wideEvent.data.service) {\n wideEvent.data.service = {\n ...wideEvent.data.service,\n name: \"appkit\",\n };\n }\n\n return wideEvent;\n}\n\n/**\n * Setup response lifecycle handlers for WideEvent finalization\n */\nfunction setupResponseHandlers(req: Request, wideEvent: WideEvent): void {\n const res = req.res as Response | undefined;\n if (!res) return;\n\n res.once(\"finish\", () => {\n // finalize the event with status code\n const finalizedData = wideEvent.finalize(res.statusCode || 200);\n\n // emit to OpenTelemetry if sampled\n if (shouldSample(finalizedData, DEFAULT_SAMPLING_CONFIG)) {\n emitter.emit(finalizedData);\n }\n\n // clean up the WeakMap\n eventsByRequest.delete(req);\n });\n\n res.once(\"close\", () => {\n if (!res.writableFinished) {\n // request was aborted - just cleanup\n eventsByRequest.delete(req);\n }\n });\n}\n\n/**\n * Express middleware that establishes AsyncLocalStorage context for WideEvent.\n * This properly scopes the context to the entire request lifecycle using run().\n *\n * @example\n * ```typescript\n * import { wideEventMiddleware } from \"@databricks/appkit\";\n *\n * app.use(wideEventMiddleware);\n * ```\n */\nfunction _wideEventMiddleware(\n req: Request,\n _res: Response,\n next: NextFunction,\n): void {\n const wideEvent = createEventForRequest(req);\n eventsByRequest.set(req, wideEvent);\n setupResponseHandlers(req, wideEvent);\n\n // run() scopes the context to this request's entire async chain\n eventStorage.run(wideEvent, next);\n}\n\n/**\n * Get or create a WideEvent for the given request.\n * If called within wideEventMiddleware context, returns the event from AsyncLocalStorage.\n * Otherwise creates a new event for the request.\n */\nfunction getOrCreateEvent(req: Request): WideEvent {\n // first check if we already have an event\n let wideEvent = eventsByRequest.get(req);\n\n if (!wideEvent) {\n // check if we are in a middleware context\n const alsEvent = eventStorage.getStore();\n if (alsEvent) {\n // store the event in the WeakMap\n eventsByRequest.set(req, alsEvent);\n return alsEvent;\n }\n\n // no middleware context - create event directly\n wideEvent = createEventForRequest(req);\n eventsByRequest.set(req, wideEvent);\n setupResponseHandlers(req, wideEvent);\n }\n\n return wideEvent;\n}\n\n/**\n * Get current WideEvent from AsyncLocalStorage or request\n */\nfunction getCurrentEvent(req?: Request): WideEvent | undefined {\n // if req provided, use it\n if (req) {\n return getOrCreateEvent(req);\n }\n\n // otherwise, get from AsyncLocalStorage\n return eventStorage.getStore();\n}\n\n/**\n * Check if the first argument is an Express Request\n */\nfunction isRequest(arg: unknown): arg is Request {\n return (\n typeof arg === \"object\" &&\n arg !== null &&\n \"method\" in arg &&\n \"path\" in arg &&\n typeof (arg as Request).method === \"string\"\n );\n}\n\n/**\n * Create a logger instance for a specific scope\n * @param scope - The scope identifier (e.g., \"connectors:lakebase\")\n * @returns Logger instance with debug, info, warn, and error methods\n *\n * @example\n * ```typescript\n * const logger = createLogger(\"connectors:lakebase\");\n *\n * // Regular logging (no request tracking)\n * logger.debug(\"Connection established with pool size: %d\", poolSize);\n * logger.info(\"Server started on port %d\", port);\n *\n * // Request-scoped logging (tracks in WideEvent)\n * logger.debug(req, \"Processing query: %s\", queryId);\n * logger.error(req, \"Query failed: %O\", error);\n *\n * // Get WideEvent - works in route handlers (with req) or interceptors (from context)\n * const event = logger.event(req); // In route handler\n * const event = logger.event(); // In interceptor (gets from AsyncLocalStorage)\n * event?.setComponent(\"analytics\", \"executeQuery\");\n * ```\n */\nexport function createLogger(scope: string): Logger {\n const debug = createObug(`appkit:${scope}`, { useColors: true });\n const prefix = `[appkit:${scope}]`;\n\n function debugLog(reqOrMessage: Request | string, ...args: unknown[]): void {\n if (isRequest(reqOrMessage)) {\n const req = reqOrMessage;\n const message = args[0] as string;\n const logArgs = args.slice(1);\n const formatted = format(message, ...logArgs);\n\n debug(message, ...logArgs);\n getOrCreateEvent(req).addLog(\"debug\", formatted);\n } else {\n debug(reqOrMessage, ...args);\n }\n }\n\n function infoLog(reqOrMessage: Request | string, ...args: unknown[]): void {\n if (isRequest(reqOrMessage)) {\n const req = reqOrMessage;\n const message = args[0] as string;\n const logArgs = args.slice(1);\n const formatted = format(message, ...logArgs);\n\n console.log(prefix, formatted);\n getOrCreateEvent(req).addLog(\"info\", formatted);\n } else {\n console.log(prefix, format(reqOrMessage, ...args));\n }\n }\n\n function warnLog(reqOrMessage: Request | string, ...args: unknown[]): void {\n if (isRequest(reqOrMessage)) {\n const req = reqOrMessage;\n const message = args[0] as string;\n const logArgs = args.slice(1);\n const formatted = format(message, ...logArgs);\n\n console.warn(prefix, formatted);\n getOrCreateEvent(req).addLog(\"warn\", formatted);\n } else {\n console.warn(prefix, format(reqOrMessage, ...args));\n }\n }\n\n function errorLog(reqOrMessage: Request | string, ...args: unknown[]): void {\n if (isRequest(reqOrMessage)) {\n const req = reqOrMessage;\n const message = args[0] as string;\n const logArgs = args.slice(1);\n const formatted = format(message, ...logArgs);\n\n console.error(prefix, formatted);\n getOrCreateEvent(req).addLog(\"error\", formatted);\n } else {\n console.error(prefix, format(reqOrMessage, ...args));\n }\n }\n\n function event(req?: Request): WideEvent | undefined {\n return getCurrentEvent(req);\n }\n\n return {\n debug: debugLog as Logger[\"debug\"],\n info: infoLog as Logger[\"info\"],\n warn: warnLog as Logger[\"warn\"],\n error: errorLog as Logger[\"error\"],\n event,\n };\n}\n"],"mappings":";;;;;;;;;AAkCA,MAAM,eAAe,IAAI,mBAA8B;AAGvD,MAAM,kCAAkB,IAAI,SAA6B;AAGzD,MAAM,UAAU,IAAI,kBAAkB;AAEtC,MAAM,wBAAwB;;;;AAK9B,SAAS,kBAAkB,IAAoB;AAE7C,QADkB,GAAG,QAAQ,oBAAoB,GAAG,CACnC,MAAM,GAAG,sBAAsB;;;;;AAMlD,SAAS,kBAAkB,KAAsB;CAC/C,MAAM,aACJ,IAAI,QAAQ,mBACZ,IAAI,QAAQ,uBACZ,IAAI,QAAQ;AAEd,KAAI,cAAc,OAAO,eAAe,YAAY,WAAW,SAAS,GAAG;EACzE,MAAM,YAAY,kBAAkB,WAAW;AAC/C,MAAI,UAAU,SAAS,EACrB,QAAO;;AAIX,QAAO,OAAO,KAAK,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,UAAU,GAAG,EAAE;;;;;AAMxE,SAAS,sBAAsB,KAAyB;CACtD,MAAM,YAAY,kBAAkB,IAAI;CACxC,MAAM,YAAY,IAAI,UAAU,UAAU;CAI1C,MAAM,QADU,IAAI,QAAQ,IAAI,OAAO,IAAI,cACrB,MAAM,IAAI,CAAC;AACjC,WAAU,IAAI,UAAU,IAAI,OAAO,CAAC,IAAI,QAAQ,KAAK;CAGrD,MAAM,YAAY,IAAI,QAAQ;AAC9B,KAAI,aAAa,OAAO,cAAc,YAAY,UAAU,SAAS,GAAG;EACtE,MAAM,SAAS,UAAU,QAAQ,qBAAqB,GAAG,CAAC,MAAM,GAAG,IAAI;AACvE,MAAI,OAAO,SAAS,EAClB,WAAU,QAAQ,EAAE,IAAI,QAAQ,CAAC;;CAMrC,MAAM,cADc,MAAM,eAAe,EACR,aAAa;AAC9C,KAAI,aAAa,SAAS;AACxB,YAAU,IAAI,YAAY,YAAY,QAAQ;AAG9C,EADoBA,YAAW,uBAAuB,EAAE,WAAW,MAAM,CAAC,CAExE,qDACA,IAAI,QACJ,MACA,UAAU,UAAU,GAAG,EAAE,EACzB,YAAY,QAAQ,UAAU,GAAG,EAAE,CACpC;;AAIH,KAAI,UAAU,KAAK,QACjB,WAAU,KAAK,UAAU;EACvB,GAAG,UAAU,KAAK;EAClB,MAAM;EACP;AAGH,QAAO;;;;;AAMT,SAAS,sBAAsB,KAAc,WAA4B;CACvE,MAAM,MAAM,IAAI;AAChB,KAAI,CAAC,IAAK;AAEV,KAAI,KAAK,gBAAgB;EAEvB,MAAM,gBAAgB,UAAU,SAAS,IAAI,cAAc,IAAI;AAG/D,MAAI,aAAa,eAAe,wBAAwB,CACtD,SAAQ,KAAK,cAAc;AAI7B,kBAAgB,OAAO,IAAI;GAC3B;AAEF,KAAI,KAAK,eAAe;AACtB,MAAI,CAAC,IAAI,iBAEP,iBAAgB,OAAO,IAAI;GAE7B;;;;;;;AAgCJ,SAAS,iBAAiB,KAAyB;CAEjD,IAAI,YAAY,gBAAgB,IAAI,IAAI;AAExC,KAAI,CAAC,WAAW;EAEd,MAAM,WAAW,aAAa,UAAU;AACxC,MAAI,UAAU;AAEZ,mBAAgB,IAAI,KAAK,SAAS;AAClC,UAAO;;AAIT,cAAY,sBAAsB,IAAI;AACtC,kBAAgB,IAAI,KAAK,UAAU;AACnC,wBAAsB,KAAK,UAAU;;AAGvC,QAAO;;;;;AAMT,SAAS,gBAAgB,KAAsC;AAE7D,KAAI,IACF,QAAO,iBAAiB,IAAI;AAI9B,QAAO,aAAa,UAAU;;;;;AAMhC,SAAS,UAAU,KAA8B;AAC/C,QACE,OAAO,QAAQ,YACf,QAAQ,QACR,YAAY,OACZ,UAAU,OACV,OAAQ,IAAgB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;AA2BvC,SAAgB,aAAa,OAAuB;CAClD,MAAM,QAAQA,YAAW,UAAU,SAAS,EAAE,WAAW,MAAM,CAAC;CAChE,MAAM,SAAS,WAAW,MAAM;CAEhC,SAAS,SAAS,cAAgC,GAAG,MAAuB;AAC1E,MAAI,UAAU,aAAa,EAAE;GAC3B,MAAM,MAAM;GACZ,MAAM,UAAU,KAAK;GACrB,MAAM,UAAU,KAAK,MAAM,EAAE;GAC7B,MAAM,YAAY,OAAO,SAAS,GAAG,QAAQ;AAE7C,SAAM,SAAS,GAAG,QAAQ;AAC1B,oBAAiB,IAAI,CAAC,OAAO,SAAS,UAAU;QAEhD,OAAM,cAAc,GAAG,KAAK;;CAIhC,SAAS,QAAQ,cAAgC,GAAG,MAAuB;AACzE,MAAI,UAAU,aAAa,EAAE;GAC3B,MAAM,MAAM;GACZ,MAAM,UAAU,KAAK;GAErB,MAAM,YAAY,OAAO,SAAS,GADlB,KAAK,MAAM,EAAE,CACgB;AAE7C,WAAQ,IAAI,QAAQ,UAAU;AAC9B,oBAAiB,IAAI,CAAC,OAAO,QAAQ,UAAU;QAE/C,SAAQ,IAAI,QAAQ,OAAO,cAAc,GAAG,KAAK,CAAC;;CAItD,SAAS,QAAQ,cAAgC,GAAG,MAAuB;AACzE,MAAI,UAAU,aAAa,EAAE;GAC3B,MAAM,MAAM;GACZ,MAAM,UAAU,KAAK;GAErB,MAAM,YAAY,OAAO,SAAS,GADlB,KAAK,MAAM,EAAE,CACgB;AAE7C,WAAQ,KAAK,QAAQ,UAAU;AAC/B,oBAAiB,IAAI,CAAC,OAAO,QAAQ,UAAU;QAE/C,SAAQ,KAAK,QAAQ,OAAO,cAAc,GAAG,KAAK,CAAC;;CAIvD,SAAS,SAAS,cAAgC,GAAG,MAAuB;AAC1E,MAAI,UAAU,aAAa,EAAE;GAC3B,MAAM,MAAM;GACZ,MAAM,UAAU,KAAK;GAErB,MAAM,YAAY,OAAO,SAAS,GADlB,KAAK,MAAM,EAAE,CACgB;AAE7C,WAAQ,MAAM,QAAQ,UAAU;AAChC,oBAAiB,IAAI,CAAC,OAAO,SAAS,UAAU;QAEhD,SAAQ,MAAM,QAAQ,OAAO,cAAc,GAAG,KAAK,CAAC;;CAIxD,SAAS,MAAM,KAAsC;AACnD,SAAO,gBAAgB,IAAI;;AAG7B,QAAO;EACL,OAAO;EACP,MAAM;EACN,MAAM;EACN,OAAO;EACP;EACD"}
|
|
1
|
+
{"version":3,"file":"logger.js","names":["createObug"],"sources":["../../src/logging/logger.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { format } from \"node:util\";\n\nimport { trace } from \"@opentelemetry/api\";\nimport type { NextFunction, Request, Response } from \"express\";\nimport { createDebug as createObug } from \"obug\";\n\nimport { DEFAULT_SAMPLING_CONFIG, shouldSample } from \"./sampling\";\nimport { WideEvent } from \"./wide-event\";\nimport { WideEventEmitter } from \"./wide-event-emitter\";\n\n/**\n * Logger interface for AppKit components\n */\ninterface Logger {\n /** Debug output (disabled by default, enable via DEBUG env var) */\n debug(message: string, ...args: unknown[]): void;\n debug(req: Request, message: string, ...args: unknown[]): void;\n\n /** Info output (always visible, for operational messages) */\n info(message: string, ...args: unknown[]): void;\n info(req: Request, message: string, ...args: unknown[]): void;\n\n /** Warning output (always visible, for degraded states) */\n warn(message: string, ...args: unknown[]): void;\n warn(req: Request, message: string, ...args: unknown[]): void;\n\n /** Error output (always visible, for failures) */\n error(message: string, ...args: unknown[]): void;\n error(req: Request, message: string, ...args: unknown[]): void;\n\n /** Get request-scoped WideEvent (from AsyncLocalStorage or explicit req) */\n event(req?: Request): WideEvent | undefined;\n}\n\n// AsyncLocalStorage for WideEvent context propagation\nconst eventStorage = new AsyncLocalStorage<WideEvent>();\n\n// WeakMap to store WideEvent per request (for explicit req usage)\nconst eventsByRequest = new WeakMap<Request, WideEvent>();\n\n// Global emitter instance\nconst emitter = new WideEventEmitter();\n\nconst MAX_REQUEST_ID_LENGTH = 128;\n\n/**\n * Sanitize a request ID from user headers\n */\nfunction sanitizeRequestId(id: string): string {\n const sanitized = id.replace(/[^a-zA-Z0-9_.-]/g, \"\");\n return sanitized.slice(0, MAX_REQUEST_ID_LENGTH);\n}\n\n/**\n * Generate a request ID from the request\n */\nfunction generateRequestId(req: Request): string {\n const existingId =\n req.headers[\"x-request-id\"] ||\n req.headers[\"x-correlation-id\"] ||\n req.headers[\"x-amzn-trace-id\"];\n\n if (existingId && typeof existingId === \"string\" && existingId.length > 0) {\n const sanitized = sanitizeRequestId(existingId);\n if (sanitized.length > 0) {\n return sanitized;\n }\n }\n\n return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n}\n\n/**\n * Create a WideEvent for a request\n */\nfunction createEventForRequest(req: Request): WideEvent {\n const requestId = generateRequestId(req);\n const wideEvent = new WideEvent(requestId);\n\n // extract path from request (strip query string)\n const rawPath = req.path || req.url || req.originalUrl;\n const path = rawPath?.split(\"?\")[0];\n wideEvent.set(\"method\", req.method).set(\"path\", path);\n\n // extract user id from request headers (sanitized)\n const rawUserId = req.headers[\"x-forwarded-user\"];\n if (rawUserId && typeof rawUserId === \"string\" && rawUserId.length > 0) {\n const userId = rawUserId.replace(/[^a-zA-Z0-9_@.-]/g, \"\").slice(0, 128);\n if (userId.length > 0) {\n wideEvent.setUser({ id: userId });\n }\n }\n\n // extract trace id from active span for distributed tracing\n const currentSpan = trace.getActiveSpan();\n const spanContext = currentSpan?.spanContext();\n if (spanContext?.traceId) {\n wideEvent.set(\"trace_id\", spanContext.traceId);\n\n const debugLogger = createObug(\"appkit:logger:event\", { useColors: true });\n debugLogger(\n \"WideEvent created: %s %s (reqId: %s, traceId: %s)\",\n req.method,\n path,\n requestId.substring(0, 8),\n spanContext.traceId.substring(0, 8),\n );\n }\n\n // Update service scope\n if (wideEvent.data.service) {\n wideEvent.data.service = {\n ...wideEvent.data.service,\n name: \"appkit\",\n };\n }\n\n return wideEvent;\n}\n\n/**\n * Setup response lifecycle handlers for WideEvent finalization\n */\nfunction setupResponseHandlers(req: Request, wideEvent: WideEvent): void {\n const res = req.res as Response | undefined;\n if (!res) return;\n\n res.once(\"finish\", () => {\n // finalize the event with status code\n const finalizedData = wideEvent.finalize(res.statusCode || 200);\n\n // emit to OpenTelemetry if sampled\n if (shouldSample(finalizedData, DEFAULT_SAMPLING_CONFIG)) {\n emitter.emit(finalizedData);\n }\n\n // clean up the WeakMap\n eventsByRequest.delete(req);\n });\n\n res.once(\"close\", () => {\n if (!res.writableFinished) {\n // request was aborted - just cleanup\n eventsByRequest.delete(req);\n }\n });\n}\n\n/**\n * Express middleware that establishes AsyncLocalStorage context for WideEvent.\n * This properly scopes the context to the entire request lifecycle using run().\n *\n * @example\n * ```typescript\n * import { wideEventMiddleware } from \"@databricks/appkit\";\n *\n * app.use(wideEventMiddleware);\n * ```\n */\nfunction _wideEventMiddleware(\n req: Request,\n _res: Response,\n next: NextFunction,\n): void {\n const wideEvent = createEventForRequest(req);\n eventsByRequest.set(req, wideEvent);\n setupResponseHandlers(req, wideEvent);\n\n // run() scopes the context to this request's entire async chain\n eventStorage.run(wideEvent, next);\n}\n\n/**\n * Get or create a WideEvent for the given request.\n * If called within wideEventMiddleware context, returns the event from AsyncLocalStorage.\n * Otherwise creates a new event for the request.\n */\nfunction getOrCreateEvent(req: Request): WideEvent {\n // first check if we already have an event\n let wideEvent = eventsByRequest.get(req);\n\n if (!wideEvent) {\n // check if we are in a middleware context\n const alsEvent = eventStorage.getStore();\n if (alsEvent) {\n // store the event in the WeakMap\n eventsByRequest.set(req, alsEvent);\n return alsEvent;\n }\n\n // no middleware context - create event directly\n wideEvent = createEventForRequest(req);\n eventsByRequest.set(req, wideEvent);\n setupResponseHandlers(req, wideEvent);\n }\n\n return wideEvent;\n}\n\n/**\n * Get current WideEvent from AsyncLocalStorage or request\n */\nfunction getCurrentEvent(req?: Request): WideEvent | undefined {\n // if req provided, use it\n if (req) {\n return getOrCreateEvent(req);\n }\n\n // otherwise, get from AsyncLocalStorage\n return eventStorage.getStore();\n}\n\n/**\n * Check if the first argument is an Express Request\n */\nfunction isRequest(arg: unknown): arg is Request {\n return (\n typeof arg === \"object\" &&\n arg !== null &&\n \"method\" in arg &&\n \"path\" in arg &&\n typeof (arg as Request).method === \"string\"\n );\n}\n\n/**\n * Create a logger instance for a specific scope\n * @param scope - The scope identifier (e.g., \"connectors:lakebase\")\n * @returns Logger instance with debug, info, warn, and error methods\n *\n * @example\n * ```typescript\n * const logger = createLogger(\"connectors:lakebase\");\n *\n * // Regular logging (no request tracking)\n * logger.debug(\"Connection established with pool size: %d\", poolSize);\n * logger.info(\"Server started on port %d\", port);\n *\n * // Request-scoped logging (tracks in WideEvent)\n * logger.debug(req, \"Processing query: %s\", queryId);\n * logger.error(req, \"Query failed: %O\", error);\n *\n * // Get WideEvent - works in route handlers (with req) or interceptors (from context)\n * const event = logger.event(req); // In route handler\n * const event = logger.event(); // In interceptor (gets from AsyncLocalStorage)\n * event?.setComponent(\"analytics\", \"executeQuery\");\n * ```\n */\nexport function createLogger(scope: string): Logger {\n const debug = createObug(`appkit:${scope}`, { useColors: true });\n const prefix = `[appkit:${scope}]`;\n\n function debugLog(reqOrMessage: Request | string, ...args: unknown[]): void {\n if (isRequest(reqOrMessage)) {\n const req = reqOrMessage;\n const message = args[0] as string;\n const logArgs = args.slice(1);\n const formatted = format(message, ...logArgs);\n\n debug(message, ...logArgs);\n getOrCreateEvent(req).addLog(\"debug\", formatted);\n } else {\n debug(reqOrMessage, ...args);\n }\n }\n\n function infoLog(reqOrMessage: Request | string, ...args: unknown[]): void {\n if (isRequest(reqOrMessage)) {\n const req = reqOrMessage;\n const message = args[0] as string;\n const logArgs = args.slice(1);\n const formatted = format(message, ...logArgs);\n\n console.log(prefix, formatted);\n getOrCreateEvent(req).addLog(\"info\", formatted);\n } else {\n console.log(prefix, format(reqOrMessage, ...args));\n }\n }\n\n function warnLog(reqOrMessage: Request | string, ...args: unknown[]): void {\n if (isRequest(reqOrMessage)) {\n const req = reqOrMessage;\n const message = args[0] as string;\n const logArgs = args.slice(1);\n const formatted = format(message, ...logArgs);\n\n console.warn(prefix, formatted);\n getOrCreateEvent(req).addLog(\"warn\", formatted);\n } else {\n console.warn(prefix, format(reqOrMessage, ...args));\n }\n }\n\n function errorLog(reqOrMessage: Request | string, ...args: unknown[]): void {\n if (isRequest(reqOrMessage)) {\n const req = reqOrMessage;\n const message = args[0] as string;\n const logArgs = args.slice(1);\n const formatted = format(message, ...logArgs);\n\n console.error(prefix, formatted);\n getOrCreateEvent(req).addLog(\"error\", formatted);\n } else {\n console.error(prefix, format(reqOrMessage, ...args));\n }\n }\n\n function event(req?: Request): WideEvent | undefined {\n return getCurrentEvent(req);\n }\n\n return {\n debug: debugLog as Logger[\"debug\"],\n info: infoLog as Logger[\"info\"],\n warn: warnLog as Logger[\"warn\"],\n error: errorLog as Logger[\"error\"],\n event,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wide-event-emitter.js","names":[],"sources":["../../src/logging/wide-event-emitter.ts"],"sourcesContent":["import { logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport type { WideEventData } from \"./wide-event\";\n\n/**\n * Emits WideEvents to OpenTelemetry as structured logs\n */\nexport class WideEventEmitter {\n private logger = logs.getLogger(\"appkit\", \"1.0.0\");\n\n /**\n * Emit a WideEvent to OpenTelemetry.\n * Fails silently to avoid crashing the application due to observability issues.\n */\n emit(event: WideEventData): void {\n try {\n const logRecord = {\n timestamp: Date.parse(event.timestamp),\n severityNumber: this.getSeverityNumber(event),\n severityText: this.getSeverityText(event),\n body: this.createLogBody(event),\n attributes: this.createAttributes(event),\n };\n\n this.logger.emit(logRecord);\n } catch {\n // Silent fail - observability should never crash the application\n }\n }\n\n /**\n * Get OpenTelemetry severity number based on event data\n */\n private getSeverityNumber(event: WideEventData): SeverityNumber {\n // Error level\n if (event.error) {\n return SeverityNumber.ERROR;\n }\n\n // Status code based\n if (event.status_code) {\n if (event.status_code >= 500) {\n return SeverityNumber.ERROR;\n }\n if (event.status_code >= 400) {\n return SeverityNumber.WARN;\n }\n }\n\n // Check logs for errors/warnings\n if (event.logs) {\n const hasError = event.logs.some((log) => log.level === \"error\");\n if (hasError) {\n return SeverityNumber.ERROR;\n }\n\n const hasWarn = event.logs.some((log) => log.level === \"warn\");\n if (hasWarn) {\n return SeverityNumber.WARN;\n }\n }\n\n return SeverityNumber.INFO;\n }\n\n /**\n * Get severity text based on severity number\n */\n private getSeverityText(event: WideEventData): string {\n const severityNumber = this.getSeverityNumber(event);\n\n if (severityNumber >= SeverityNumber.ERROR) {\n return \"ERROR\";\n }\n if (severityNumber >= SeverityNumber.WARN) {\n return \"WARN\";\n }\n if (severityNumber >= SeverityNumber.INFO) {\n return \"INFO\";\n }\n return \"DEBUG\";\n }\n\n /**\n * Create log body from event data\n */\n private createLogBody(event: WideEventData): string {\n const parts: string[] = [];\n\n // HTTP request info\n if (event.method && event.path) {\n parts.push(`${event.method} ${event.path}`);\n }\n\n // Status code\n if (event.status_code) {\n parts.push(`→ ${event.status_code}`);\n }\n\n // Duration\n if (event.duration_ms) {\n parts.push(`(${event.duration_ms}ms)`);\n }\n\n // Component info\n if (event.component) {\n const componentStr = event.component.operation\n ? `${event.component.name}.${event.component.operation}`\n : event.component.name;\n parts.push(`[${componentStr}]`);\n }\n\n // Error message\n if (event.error) {\n parts.push(`ERROR: ${event.error.message}`);\n }\n\n return parts.join(\" \");\n }\n\n /**\n * Create OpenTelemetry attributes from event data\n */\n private createAttributes(\n event: WideEventData,\n ): Record<string, string | number | boolean | undefined> {\n const attributes: Record<string, string | number | boolean | undefined> = {\n // Request metadata\n request_id: event.request_id,\n trace_id: event.trace_id,\n\n // HTTP attributes (OpenTelemetry semantic conventions)\n \"http.method\": event.method,\n \"http.route\": event.path,\n \"http.status_code\": event.status_code,\n \"http.request.duration_ms\": event.duration_ms,\n\n // Service attributes\n \"service.name\": event.service?.name,\n \"service.version\": event.service?.version,\n \"service.region\": event.service?.region,\n \"service.deployment_id\": event.service?.deployment_id,\n \"service.node_env\": event.service?.node_env,\n\n // Component attributes\n \"component.name\": event.component?.name,\n \"component.operation\": event.component?.operation,\n\n // User attributes\n \"user.id\": event.user?.id,\n\n // Error attributes\n \"error.type\": event.error?.type,\n \"error.code\": event.error?.code,\n \"error.message\": event.error?.message,\n \"error.retriable\": event.error?.retriable,\n\n // Execution metadata\n \"execution.timeout_ms\": event.execution?.timeout_ms,\n \"execution.retry_attempts\": event.execution?.retry_attempts,\n \"execution.cache_hit\": event.execution?.cache_hit,\n \"execution.cache_key\": event.execution?.cache_key,\n \"execution.cache_deduplication\": event.execution?.cache_deduplication,\n\n // Stream metadata\n \"stream.id\": event.stream?.stream_id,\n \"stream.events_sent\": event.stream?.events_sent,\n\n // Log count\n log_count: event.logs?.length,\n };\n\n // Add custom context as attributes with scope prefix (no \"appkit\" prefix)\n if (event.context) {\n for (const [scope, scopeData] of Object.entries(event.context)) {\n for (const [key, value] of Object.entries(scopeData)) {\n // Only add primitive values\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n attributes[`${scope}.${key}`] = value;\n }\n }\n }\n }\n\n // Remove undefined values\n return Object.fromEntries(\n Object.entries(attributes).filter(([_, value]) => value !== undefined),\n );\n }\n}\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"wide-event-emitter.js","names":[],"sources":["../../src/logging/wide-event-emitter.ts"],"sourcesContent":["import { logs, SeverityNumber } from \"@opentelemetry/api-logs\";\n\nimport type { WideEventData } from \"./wide-event\";\n\n/**\n * Emits WideEvents to OpenTelemetry as structured logs\n */\nexport class WideEventEmitter {\n private logger = logs.getLogger(\"appkit\", \"1.0.0\");\n\n /**\n * Emit a WideEvent to OpenTelemetry.\n * Fails silently to avoid crashing the application due to observability issues.\n */\n emit(event: WideEventData): void {\n try {\n const logRecord = {\n timestamp: Date.parse(event.timestamp),\n severityNumber: this.getSeverityNumber(event),\n severityText: this.getSeverityText(event),\n body: this.createLogBody(event),\n attributes: this.createAttributes(event),\n };\n\n this.logger.emit(logRecord);\n } catch {\n // Silent fail - observability should never crash the application\n }\n }\n\n /**\n * Get OpenTelemetry severity number based on event data\n */\n private getSeverityNumber(event: WideEventData): SeverityNumber {\n // Error level\n if (event.error) {\n return SeverityNumber.ERROR;\n }\n\n // Status code based\n if (event.status_code) {\n if (event.status_code >= 500) {\n return SeverityNumber.ERROR;\n }\n if (event.status_code >= 400) {\n return SeverityNumber.WARN;\n }\n }\n\n // Check logs for errors/warnings\n if (event.logs) {\n const hasError = event.logs.some((log) => log.level === \"error\");\n if (hasError) {\n return SeverityNumber.ERROR;\n }\n\n const hasWarn = event.logs.some((log) => log.level === \"warn\");\n if (hasWarn) {\n return SeverityNumber.WARN;\n }\n }\n\n return SeverityNumber.INFO;\n }\n\n /**\n * Get severity text based on severity number\n */\n private getSeverityText(event: WideEventData): string {\n const severityNumber = this.getSeverityNumber(event);\n\n if (severityNumber >= SeverityNumber.ERROR) {\n return \"ERROR\";\n }\n if (severityNumber >= SeverityNumber.WARN) {\n return \"WARN\";\n }\n if (severityNumber >= SeverityNumber.INFO) {\n return \"INFO\";\n }\n return \"DEBUG\";\n }\n\n /**\n * Create log body from event data\n */\n private createLogBody(event: WideEventData): string {\n const parts: string[] = [];\n\n // HTTP request info\n if (event.method && event.path) {\n parts.push(`${event.method} ${event.path}`);\n }\n\n // Status code\n if (event.status_code) {\n parts.push(`→ ${event.status_code}`);\n }\n\n // Duration\n if (event.duration_ms) {\n parts.push(`(${event.duration_ms}ms)`);\n }\n\n // Component info\n if (event.component) {\n const componentStr = event.component.operation\n ? `${event.component.name}.${event.component.operation}`\n : event.component.name;\n parts.push(`[${componentStr}]`);\n }\n\n // Error message\n if (event.error) {\n parts.push(`ERROR: ${event.error.message}`);\n }\n\n return parts.join(\" \");\n }\n\n /**\n * Create OpenTelemetry attributes from event data\n */\n private createAttributes(\n event: WideEventData,\n ): Record<string, string | number | boolean | undefined> {\n const attributes: Record<string, string | number | boolean | undefined> = {\n // Request metadata\n request_id: event.request_id,\n trace_id: event.trace_id,\n\n // HTTP attributes (OpenTelemetry semantic conventions)\n \"http.method\": event.method,\n \"http.route\": event.path,\n \"http.status_code\": event.status_code,\n \"http.request.duration_ms\": event.duration_ms,\n\n // Service attributes\n \"service.name\": event.service?.name,\n \"service.version\": event.service?.version,\n \"service.region\": event.service?.region,\n \"service.deployment_id\": event.service?.deployment_id,\n \"service.node_env\": event.service?.node_env,\n\n // Component attributes\n \"component.name\": event.component?.name,\n \"component.operation\": event.component?.operation,\n\n // User attributes\n \"user.id\": event.user?.id,\n\n // Error attributes\n \"error.type\": event.error?.type,\n \"error.code\": event.error?.code,\n \"error.message\": event.error?.message,\n \"error.retriable\": event.error?.retriable,\n\n // Execution metadata\n \"execution.timeout_ms\": event.execution?.timeout_ms,\n \"execution.retry_attempts\": event.execution?.retry_attempts,\n \"execution.cache_hit\": event.execution?.cache_hit,\n \"execution.cache_key\": event.execution?.cache_key,\n \"execution.cache_deduplication\": event.execution?.cache_deduplication,\n\n // Stream metadata\n \"stream.id\": event.stream?.stream_id,\n \"stream.events_sent\": event.stream?.events_sent,\n\n // Log count\n log_count: event.logs?.length,\n };\n\n // Add custom context as attributes with scope prefix (no \"appkit\" prefix)\n if (event.context) {\n for (const [scope, scopeData] of Object.entries(event.context)) {\n for (const [key, value] of Object.entries(scopeData)) {\n // Only add primitive values\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n attributes[`${scope}.${key}`] = value;\n }\n }\n }\n }\n\n // Remove undefined values\n return Object.fromEntries(\n Object.entries(attributes).filter(([_, value]) => value !== undefined),\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-reader.d.ts","names":[],"sources":["../../src/plugin/dev-reader.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"dev-reader.d.ts","names":[],"sources":["../../src/plugin/dev-reader.ts"],"mappings":";;;;;KAUK,sBAAA,IACH,GAAA,EADyB,SAAA,CACF,OAAA,KACpB,gBAAA;;;;AAV0C;cAgBlC,aAAA;EAAA,eACI,QAAA;EAAA,QACP,mBAAA;EAAA,QAED,WAAA,CAAA;EAAA,OAEA,WAAA,CAAA,GAAe,aAAA;EA+BtB,oBAAA,CAAqB,MAAA,EAAQ,sBAAA;EAIvB,QAAA,CACJ,QAAA,UACA,GAAA,EANiD,SAAA,CAM1B,OAAA,GACtB,OAAA;EA+BG,OAAA,CACJ,OAAA,UACA,GAAA,EAjCQ,SAAA,CAiCe,OAAA,GACtB,OAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-reader.js","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { TunnelConnection } from \"shared\";\nimport { TunnelError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { isRemoteTunnelAllowedByEnv } from \"../plugins/server/remote-tunnel/gate\";\n\nconst logger = createLogger(\"plugin:dev-reader\");\n\ntype TunnelConnectionGetter = (\n req: import(\"express\").Request,\n) => TunnelConnection | null;\n\n/**\n * This class is used to read files from the local filesystem in dev mode\n * through the WebSocket tunnel.\n */\nexport class DevFileReader {\n private static instance: DevFileReader | null = null;\n private getTunnelForRequest: TunnelConnectionGetter | null = null;\n\n private constructor() {}\n\n static getInstance(): DevFileReader {\n if (!DevFileReader.instance) {\n DevFileReader.instance = new Proxy(new DevFileReader(), {\n /**\n * We proxy the reader to return a noop function if the remote server is disabled.\n */\n get(target, prop, receiver) {\n if (isRemoteTunnelAllowedByEnv()) {\n return Reflect.get(target, prop, receiver);\n }\n\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value === \"function\") {\n return function noop() {\n logger.debug(\"Noop: %s (remote server disabled)\", String(prop));\n return Promise.resolve(\"\");\n };\n }\n\n return value;\n },\n set(target, prop, value, receiver) {\n return Reflect.set(target, prop, value, receiver);\n },\n });\n }\n\n return DevFileReader.instance;\n }\n\n registerTunnelGetter(getter: TunnelConnectionGetter) {\n this.getTunnelForRequest = getter;\n }\n\n async readFile(\n filePath: string,\n req: import(\"express\").Request,\n ): Promise<string> {\n if (!this.getTunnelForRequest) {\n throw TunnelError.getterNotRegistered();\n }\n const tunnel = this.getTunnelForRequest(req);\n\n if (!tunnel) {\n throw TunnelError.noConnection();\n }\n\n const { ws, pendingFileReads } = tunnel;\n const requestId = randomUUID();\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFileReads.delete(requestId);\n reject(new Error(`File read timeout: ${filePath}`));\n }, 10000);\n\n pendingFileReads.set(requestId, { resolve, reject, timeout });\n\n ws.send(\n JSON.stringify({\n type: \"file:read\",\n requestId,\n path: filePath,\n }),\n );\n });\n }\n\n async readdir(\n dirPath: string,\n req: import(\"express\").Request,\n ): Promise<string[]> {\n if (!this.getTunnelForRequest) {\n throw TunnelError.getterNotRegistered();\n }\n const tunnel = this.getTunnelForRequest(req);\n\n if (!tunnel) {\n throw TunnelError.noConnection();\n }\n\n const { ws, pendingFileReads } = tunnel;\n const requestId = randomUUID();\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFileReads.delete(requestId);\n reject(new Error(`Directory read timeout: ${dirPath}`));\n }, 10000);\n\n pendingFileReads.set(requestId, {\n resolve: (data: string) => {\n try {\n const files = JSON.parse(data);\n // Validate it's an array of strings\n if (!Array.isArray(files)) {\n reject(\n new Error(\n \"Invalid directory listing format: expected array, got \" +\n typeof files,\n ),\n );\n return;\n }\n if (!files.every((f) => typeof f === \"string\")) {\n reject(\n new Error(\n \"Invalid directory listing format: expected array of strings\",\n ),\n );\n return;\n }\n resolve(files);\n } catch (error) {\n reject(\n new Error(\n `Failed to parse directory listing: ${(error as Error).message}`,\n ),\n );\n }\n },\n reject,\n timeout,\n });\n\n ws.send(\n JSON.stringify({\n type: \"dir:list\",\n requestId,\n path: dirPath,\n }),\n );\n });\n }\n}\n"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"dev-reader.js","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport type { TunnelConnection } from \"shared\";\n\nimport { TunnelError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { isRemoteTunnelAllowedByEnv } from \"../plugins/server/remote-tunnel/gate\";\n\nconst logger = createLogger(\"plugin:dev-reader\");\n\ntype TunnelConnectionGetter = (\n req: import(\"express\").Request,\n) => TunnelConnection | null;\n\n/**\n * This class is used to read files from the local filesystem in dev mode\n * through the WebSocket tunnel.\n */\nexport class DevFileReader {\n private static instance: DevFileReader | null = null;\n private getTunnelForRequest: TunnelConnectionGetter | null = null;\n\n private constructor() {}\n\n static getInstance(): DevFileReader {\n if (!DevFileReader.instance) {\n DevFileReader.instance = new Proxy(new DevFileReader(), {\n /**\n * We proxy the reader to return a noop function if the remote server is disabled.\n */\n get(target, prop, receiver) {\n if (isRemoteTunnelAllowedByEnv()) {\n return Reflect.get(target, prop, receiver);\n }\n\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value === \"function\") {\n return function noop() {\n logger.debug(\"Noop: %s (remote server disabled)\", String(prop));\n return Promise.resolve(\"\");\n };\n }\n\n return value;\n },\n set(target, prop, value, receiver) {\n return Reflect.set(target, prop, value, receiver);\n },\n });\n }\n\n return DevFileReader.instance;\n }\n\n registerTunnelGetter(getter: TunnelConnectionGetter) {\n this.getTunnelForRequest = getter;\n }\n\n async readFile(\n filePath: string,\n req: import(\"express\").Request,\n ): Promise<string> {\n if (!this.getTunnelForRequest) {\n throw TunnelError.getterNotRegistered();\n }\n const tunnel = this.getTunnelForRequest(req);\n\n if (!tunnel) {\n throw TunnelError.noConnection();\n }\n\n const { ws, pendingFileReads } = tunnel;\n const requestId = randomUUID();\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFileReads.delete(requestId);\n reject(new Error(`File read timeout: ${filePath}`));\n }, 10000);\n\n pendingFileReads.set(requestId, { resolve, reject, timeout });\n\n ws.send(\n JSON.stringify({\n type: \"file:read\",\n requestId,\n path: filePath,\n }),\n );\n });\n }\n\n async readdir(\n dirPath: string,\n req: import(\"express\").Request,\n ): Promise<string[]> {\n if (!this.getTunnelForRequest) {\n throw TunnelError.getterNotRegistered();\n }\n const tunnel = this.getTunnelForRequest(req);\n\n if (!tunnel) {\n throw TunnelError.noConnection();\n }\n\n const { ws, pendingFileReads } = tunnel;\n const requestId = randomUUID();\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFileReads.delete(requestId);\n reject(new Error(`Directory read timeout: ${dirPath}`));\n }, 10000);\n\n pendingFileReads.set(requestId, {\n resolve: (data: string) => {\n try {\n const files = JSON.parse(data);\n // Validate it's an array of strings\n if (!Array.isArray(files)) {\n reject(\n new Error(\n \"Invalid directory listing format: expected array, got \" +\n typeof files,\n ),\n );\n return;\n }\n if (!files.every((f) => typeof f === \"string\")) {\n reject(\n new Error(\n \"Invalid directory listing format: expected array of strings\",\n ),\n );\n return;\n }\n resolve(files);\n } catch (error) {\n reject(\n new Error(\n `Failed to parse directory listing: ${(error as Error).message}`,\n ),\n );\n }\n },\n reject,\n timeout,\n });\n\n ws.send(\n JSON.stringify({\n type: \"dir:list\",\n requestId,\n path: dirPath,\n }),\n );\n });\n }\n}\n"],"mappings":";;;;;;;AAQA,MAAM,SAAS,aAAa,oBAAoB;;;;;AAUhD,IAAa,gBAAb,MAAa,cAAc;CACzB,OAAe,WAAiC;CAChD,AAAQ,sBAAqD;CAE7D,AAAQ,cAAc;CAEtB,OAAO,cAA6B;AAClC,MAAI,CAAC,cAAc,SACjB,eAAc,WAAW,IAAI,MAAM,IAAI,eAAe,EAAE;GAItD,IAAI,QAAQ,MAAM,UAAU;AAC1B,QAAI,4BAA4B,CAC9B,QAAO,QAAQ,IAAI,QAAQ,MAAM,SAAS;IAG5C,MAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,SAAS;AAEjD,QAAI,OAAO,UAAU,WACnB,QAAO,SAAS,OAAO;AACrB,YAAO,MAAM,qCAAqC,OAAO,KAAK,CAAC;AAC/D,YAAO,QAAQ,QAAQ,GAAG;;AAI9B,WAAO;;GAET,IAAI,QAAQ,MAAM,OAAO,UAAU;AACjC,WAAO,QAAQ,IAAI,QAAQ,MAAM,OAAO,SAAS;;GAEpD,CAAC;AAGJ,SAAO,cAAc;;CAGvB,qBAAqB,QAAgC;AACnD,OAAK,sBAAsB;;CAG7B,MAAM,SACJ,UACA,KACiB;AACjB,MAAI,CAAC,KAAK,oBACR,OAAM,YAAY,qBAAqB;EAEzC,MAAM,SAAS,KAAK,oBAAoB,IAAI;AAE5C,MAAI,CAAC,OACH,OAAM,YAAY,cAAc;EAGlC,MAAM,EAAE,IAAI,qBAAqB;EACjC,MAAM,YAAY,YAAY;AAE9B,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,UAAU,iBAAiB;AAC/B,qBAAiB,OAAO,UAAU;AAClC,2BAAO,IAAI,MAAM,sBAAsB,WAAW,CAAC;MAClD,IAAM;AAET,oBAAiB,IAAI,WAAW;IAAE;IAAS;IAAQ;IAAS,CAAC;AAE7D,MAAG,KACD,KAAK,UAAU;IACb,MAAM;IACN;IACA,MAAM;IACP,CAAC,CACH;IACD;;CAGJ,MAAM,QACJ,SACA,KACmB;AACnB,MAAI,CAAC,KAAK,oBACR,OAAM,YAAY,qBAAqB;EAEzC,MAAM,SAAS,KAAK,oBAAoB,IAAI;AAE5C,MAAI,CAAC,OACH,OAAM,YAAY,cAAc;EAGlC,MAAM,EAAE,IAAI,qBAAqB;EACjC,MAAM,YAAY,YAAY;AAE9B,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,UAAU,iBAAiB;AAC/B,qBAAiB,OAAO,UAAU;AAClC,2BAAO,IAAI,MAAM,2BAA2B,UAAU,CAAC;MACtD,IAAM;AAET,oBAAiB,IAAI,WAAW;IAC9B,UAAU,SAAiB;AACzB,SAAI;MACF,MAAM,QAAQ,KAAK,MAAM,KAAK;AAE9B,UAAI,CAAC,MAAM,QAAQ,MAAM,EAAE;AACzB,8BACE,IAAI,MACF,2DACE,OAAO,MACV,CACF;AACD;;AAEF,UAAI,CAAC,MAAM,OAAO,MAAM,OAAO,MAAM,SAAS,EAAE;AAC9C,8BACE,IAAI,MACF,8DACD,CACF;AACD;;AAEF,cAAQ,MAAM;cACP,OAAO;AACd,6BACE,IAAI,MACF,sCAAuC,MAAgB,UACxD,CACF;;;IAGL;IACA;IACD,CAAC;AAEF,MAAG,KACD,KAAK,UAAU;IACb,MAAM;IACN;IACA,MAAM;IACP,CAAC,CACH;IACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","names":[],"sources":["../../../src/plugin/interceptors/cache.ts"],"sourcesContent":["import type { CacheConfig } from \"shared\";\nimport type { CacheManager } from \"../../cache\";\nimport type { ExecutionInterceptor, InterceptorContext } from \"./types\";\n\n// interceptor to handle caching logic\nexport class CacheInterceptor implements ExecutionInterceptor {\n constructor(\n private cacheManager: CacheManager,\n private config: CacheConfig,\n ) {}\n\n async intercept<T>(\n fn: () => Promise<T>,\n context: InterceptorContext,\n ): Promise<T> {\n // if cache disabled, ignore\n if (!this.config.enabled || !this.config.cacheKey?.length) {\n return fn();\n }\n\n const callerSignal = context.signal;\n\n // The cache may dedupe this request onto a shared in-flight execution.\n // Swap context.signal to the cache-owned shared signal for the duration\n // of fn() so the inner interceptor chain (timeout/retry/telemetry) and\n // the underlying I/O observe abort only when *all* callers have left,\n // not just this one. Without this swap, mount #1's abort under React\n // StrictMode poisons mount #2's joined inflight result.\n return this.cacheManager.getOrExecute(\n this.config.cacheKey,\n async (sharedSignal) => {\n const previousSignal = context.signal;\n context.signal = sharedSignal;\n try {\n return await fn();\n } finally {\n context.signal = previousSignal;\n }\n },\n context.userKey,\n { ttl: this.config.ttl, callerSignal },\n );\n }\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"cache.js","names":[],"sources":["../../../src/plugin/interceptors/cache.ts"],"sourcesContent":["import type { CacheConfig } from \"shared\";\n\nimport type { CacheManager } from \"../../cache\";\nimport type { ExecutionInterceptor, InterceptorContext } from \"./types\";\n\n// interceptor to handle caching logic\nexport class CacheInterceptor implements ExecutionInterceptor {\n constructor(\n private cacheManager: CacheManager,\n private config: CacheConfig,\n ) {}\n\n async intercept<T>(\n fn: () => Promise<T>,\n context: InterceptorContext,\n ): Promise<T> {\n // if cache disabled, ignore\n if (!this.config.enabled || !this.config.cacheKey?.length) {\n return fn();\n }\n\n const callerSignal = context.signal;\n\n // The cache may dedupe this request onto a shared in-flight execution.\n // Swap context.signal to the cache-owned shared signal for the duration\n // of fn() so the inner interceptor chain (timeout/retry/telemetry) and\n // the underlying I/O observe abort only when *all* callers have left,\n // not just this one. Without this swap, mount #1's abort under React\n // StrictMode poisons mount #2's joined inflight result.\n return this.cacheManager.getOrExecute(\n this.config.cacheKey,\n async (sharedSignal) => {\n const previousSignal = context.signal;\n context.signal = sharedSignal;\n try {\n return await fn();\n } finally {\n context.signal = previousSignal;\n }\n },\n context.userKey,\n { ttl: this.config.ttl, callerSignal },\n );\n }\n}\n"],"mappings":";AAMA,IAAa,mBAAb,MAA8D;CAC5D,YACE,AAAQ,cACR,AAAQ,QACR;EAFQ;EACA;;CAGV,MAAM,UACJ,IACA,SACY;AAEZ,MAAI,CAAC,KAAK,OAAO,WAAW,CAAC,KAAK,OAAO,UAAU,OACjD,QAAO,IAAI;EAGb,MAAM,eAAe,QAAQ;AAQ7B,SAAO,KAAK,aAAa,aACvB,KAAK,OAAO,UACZ,OAAO,iBAAiB;GACtB,MAAM,iBAAiB,QAAQ;AAC/B,WAAQ,SAAS;AACjB,OAAI;AACF,WAAO,MAAM,IAAI;aACT;AACR,YAAQ,SAAS;;KAGrB,QAAQ,SACR;GAAE,KAAK,KAAK,OAAO;GAAK;GAAc,CACvC"}
|