@databricks/appkit 0.34.1 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CLAUDE.md +3 -0
  2. package/README.md +3 -3
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/cache/index.js +1 -1
  5. package/dist/connectors/index.js +2 -0
  6. package/dist/connectors/lakebase/index.d.ts +2 -0
  7. package/dist/connectors/lakebase/index.d.ts.map +1 -1
  8. package/dist/connectors/lakebase/index.js +2 -0
  9. package/dist/connectors/lakebase/index.js.map +1 -1
  10. package/dist/connectors/lakebase/pool-manager.d.ts +54 -0
  11. package/dist/connectors/lakebase/pool-manager.d.ts.map +1 -0
  12. package/dist/connectors/lakebase/pool-manager.js +77 -0
  13. package/dist/connectors/lakebase/pool-manager.js.map +1 -0
  14. package/dist/connectors/lakebase/routing-pool.d.ts +22 -0
  15. package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -0
  16. package/dist/connectors/lakebase/routing-pool.js +48 -0
  17. package/dist/connectors/lakebase/routing-pool.js.map +1 -0
  18. package/dist/context/execution-context.js +9 -1
  19. package/dist/context/execution-context.js.map +1 -1
  20. package/dist/context/service-context.d.ts.map +1 -1
  21. package/dist/context/service-context.js +4 -1
  22. package/dist/context/service-context.js.map +1 -1
  23. package/dist/context/user-context.d.ts +4 -0
  24. package/dist/context/user-context.d.ts.map +1 -1
  25. package/dist/context/user-context.js.map +1 -1
  26. package/dist/core/appkit.d.ts.map +1 -1
  27. package/dist/core/appkit.js +24 -4
  28. package/dist/core/appkit.js.map +1 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +5 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/plugin/interceptors/telemetry.js +1 -1
  33. package/dist/plugin/plugin.d.ts.map +1 -1
  34. package/dist/plugin/plugin.js +12 -4
  35. package/dist/plugin/plugin.js.map +1 -1
  36. package/dist/plugins/files/plugin.js +1 -1
  37. package/dist/plugins/jobs/plugin.js +1 -1
  38. package/dist/plugins/lakebase/lakebase.d.ts +40 -14
  39. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  40. package/dist/plugins/lakebase/lakebase.js +91 -21
  41. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  42. package/dist/plugins/serving/serving.js +1 -1
  43. package/docs/api/appkit/Function.createLakebasePoolManager.md +36 -0
  44. package/docs/api/appkit/Interface.LakebasePool.md +84 -0
  45. package/docs/api/appkit/Interface.LakebasePoolManager.md +101 -0
  46. package/docs/api/appkit.md +3 -0
  47. package/docs/development/llm-guide.md +0 -1
  48. package/docs/plugins/execution-context.md +6 -0
  49. package/docs/plugins/lakebase.md +112 -6
  50. package/llms.txt +3 -0
  51. package/package.json +1 -1
  52. package/sbom.cdx.json +1 -1
package/CLAUDE.md CHANGED
@@ -83,6 +83,7 @@ npx @databricks/appkit docs <query>
83
83
  - [Function: createAgent()](./docs/api/appkit/Function.createAgent.md): Pure factory for agent definitions. Returns the passed-in definition after
84
84
  - [Function: createApp()](./docs/api/appkit/Function.createApp.md): Bootstraps AppKit with the provided configuration.
85
85
  - [Function: createLakebasePool()](./docs/api/appkit/Function.createLakebasePool.md): Create a Lakebase pool with appkit's logger integration.
86
+ - [Function: createLakebasePoolManager()](./docs/api/appkit/Function.createLakebasePoolManager.md): Create a pool manager that maintains per-key Lakebase connection pools.
86
87
  - [Function: defineTool()](./docs/api/appkit/Function.defineTool.md): Defines a single tool entry for a plugin's internal registry.
87
88
  - [Function: executeFromRegistry()](./docs/api/appkit/Function.executeFromRegistry.md): Validates tool-call arguments against the entry's schema and invokes its
88
89
  - [Function: extractServingEndpoints()](./docs/api/appkit/Function.extractServingEndpoints.md): Extract serving endpoint config from a server file by AST-parsing it.
@@ -128,7 +129,9 @@ npx @databricks/appkit docs <query>
128
129
  - [Interface: JobAPI](./docs/api/appkit/Interface.JobAPI.md): User-facing API for a single configured job.
129
130
  - [Interface: JobConfig](./docs/api/appkit/Interface.JobConfig.md): Per-job configuration options.
130
131
  - [Interface: JobsConnectorConfig](./docs/api/appkit/Interface.JobsConnectorConfig.md): Properties
132
+ - [Interface: LakebasePool](./docs/api/appkit/Interface.LakebasePool.md): Subset of pg.Pool exposed by the Lakebase plugin.
131
133
  - [Interface: LakebasePoolConfig](./docs/api/appkit/Interface.LakebasePoolConfig.md): Configuration for creating a Lakebase connection pool
134
+ - [Interface: LakebasePoolManager](./docs/api/appkit/Interface.LakebasePoolManager.md): Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId).
132
135
  - [Interface: McpConnectAllResult](./docs/api/appkit/Interface.McpConnectAllResult.md): Per-endpoint outcome of AppKitMcpClient.connectAll. Callers (the
133
136
  - [Interface: Message](./docs/api/appkit/Interface.Message.md): Properties
134
137
  - [Interface: PluginManifest<TName>](./docs/api/appkit/Interface.PluginManifest.md): Plugin manifest that declares metadata and resource requirements.
package/README.md CHANGED
@@ -27,13 +27,13 @@ AppKit's power comes from its plugin system. Each plugin adds a focused capabili
27
27
 
28
28
  ## Getting started
29
29
 
30
- Follow the [Getting Started](https://databricks.github.io/appkit/docs/) guide to get started with AppKit.
30
+ Follow the [Getting Started](https://www.databricks.com/devhub/docs/appkit/v0/) guide to get started with AppKit.
31
31
 
32
- 🤖 For AI/code assistants, see the [AI-assisted development](https://databricks.github.io/appkit/docs/development/ai-assisted-development) guide.
32
+ 🤖 For AI/code assistants, see the [AI-assisted development](https://www.databricks.com/devhub/docs/appkit/v0/development/ai-assisted-development) guide.
33
33
 
34
34
  ## Documentation
35
35
 
36
- 📖 For full AppKit documentation, visit the [AppKit Documentation](https://databricks.github.io/appkit/) website.
36
+ 📖 For full AppKit documentation, visit the [AppKit Documentation](https://www.databricks.com/devhub/docs/appkit/v0/) website.
37
37
 
38
38
  ## Contributing
39
39
 
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.34.1";
3
+ var version = "0.35.1";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -1,9 +1,9 @@
1
1
  import { createLogger } from "../logging/logger.js";
2
- import { createLakebasePool } from "../connectors/lakebase/index.js";
3
2
  import { AppKitError } from "../errors/base.js";
4
3
  import { ExecutionError } from "../errors/execution.js";
5
4
  import { InitializationError } from "../errors/initialization.js";
6
5
  import { init_errors } from "../errors/index.js";
6
+ import { createLakebasePool } from "../connectors/lakebase/index.js";
7
7
  import { TelemetryManager } from "../telemetry/telemetry-manager.js";
8
8
  import { SpanStatusCode } from "../telemetry/index.js";
9
9
  import { deepMerge } from "../utils/merge.js";
@@ -1,3 +1,5 @@
1
+ import { createLakebasePoolManager } from "./lakebase/pool-manager.js";
2
+ import { RoutingPool } from "./lakebase/routing-pool.js";
1
3
  import { RequestedClaimsPermissionSet, createLakebasePool, generateDatabaseCredential, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup, getWorkspaceClient } from "./lakebase/index.js";
2
4
  import { FILES_MAX_READ_SIZE, SAFE_INLINE_CONTENT_TYPES, contentTypeFromPath, isSafeInlineContentType, isTextContentType, validateCustomContentTypes } from "./files/defaults.js";
3
5
  import { FilesConnector } from "./files/client.js";
@@ -1,3 +1,5 @@
1
+ import { LakebasePoolManager, createLakebasePoolManager } from "./pool-manager.js";
2
+ import { LakebasePool } from "./routing-pool.js";
1
3
  import { DatabaseCredential, GenerateDatabaseCredentialRequest, LakebasePoolConfig, LakebasePoolConfig as LakebasePoolConfig$1, RequestedClaims, RequestedClaimsPermissionSet, RequestedResource, generateDatabaseCredential, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup, getWorkspaceClient } from "@databricks/lakebase";
2
4
  import { Pool } from "pg";
3
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/connectors/lakebase/index.ts"],"mappings":";;;;;;AAcA;;;;;iBAAgB,oBAAA,CAAmB,MAAA,GAAS,OAAA,CAAQ,kBAAA,IAAsB,IAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/connectors/lakebase/index.ts"],"mappings":";;;;;;;;;;AAcA;;;iBAAgB,oBAAA,CAAmB,MAAA,GAAS,OAAA,CAAQ,kBAAA,IAAsB,IAAA"}
@@ -1,4 +1,6 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
+ import { createLakebasePoolManager } from "./pool-manager.js";
3
+ import { RoutingPool } from "./routing-pool.js";
2
4
  import { RequestedClaimsPermissionSet, createLakebasePool, generateDatabaseCredential, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup, getWorkspaceClient } from "@databricks/lakebase";
3
5
 
4
6
  //#region src/connectors/lakebase/index.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["createLakebasePool","createLakebasePoolBase"],"sources":["../../../src/connectors/lakebase/index.ts"],"sourcesContent":["import {\n createLakebasePool as createLakebasePoolBase,\n type LakebasePoolConfig,\n} from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\nimport { createLogger } from \"../../logging/logger\";\n\n/**\n * Create a Lakebase pool with appkit's logger integration.\n * Telemetry automatically uses appkit's OpenTelemetry configuration via global registry.\n *\n * @param config - Lakebase pool configuration\n * @returns PostgreSQL pool with appkit integration\n */\nexport function createLakebasePool(config?: Partial<LakebasePoolConfig>): Pool {\n const logger = createLogger(\"connectors:lakebase\");\n\n return createLakebasePoolBase({\n ...config,\n logger,\n });\n}\n\n// Re-export everything else from lakebase\nexport {\n type DatabaseCredential,\n type GenerateDatabaseCredentialRequest,\n generateDatabaseCredential,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n getWorkspaceClient,\n type LakebasePoolConfig,\n type RequestedClaims,\n RequestedClaimsPermissionSet,\n type RequestedResource,\n} from \"@databricks/lakebase\";\n"],"mappings":";;;;;;;;;;;AAcA,SAAgBA,qBAAmB,QAA4C;CAC7E,MAAM,SAAS,aAAa,sBAAsB;AAElD,QAAOC,mBAAuB;EAC5B,GAAG;EACH;EACD,CAAC"}
1
+ {"version":3,"file":"index.js","names":["createLakebasePool","createLakebasePoolBase"],"sources":["../../../src/connectors/lakebase/index.ts"],"sourcesContent":["import {\n createLakebasePool as createLakebasePoolBase,\n type LakebasePoolConfig,\n} from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\nimport { createLogger } from \"../../logging/logger\";\n\n/**\n * Create a Lakebase pool with appkit's logger integration.\n * Telemetry automatically uses appkit's OpenTelemetry configuration via global registry.\n *\n * @param config - Lakebase pool configuration\n * @returns PostgreSQL pool with appkit integration\n */\nexport function createLakebasePool(config?: Partial<LakebasePoolConfig>): Pool {\n const logger = createLogger(\"connectors:lakebase\");\n\n return createLakebasePoolBase({\n ...config,\n logger,\n });\n}\n\n// Re-export everything else from lakebase\nexport {\n type DatabaseCredential,\n type GenerateDatabaseCredentialRequest,\n generateDatabaseCredential,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n getWorkspaceClient,\n type LakebasePoolConfig,\n type RequestedClaims,\n RequestedClaimsPermissionSet,\n type RequestedResource,\n} from \"@databricks/lakebase\";\n\nexport {\n createLakebasePoolManager,\n type LakebasePoolManager,\n} from \"./pool-manager\";\n\nexport { type LakebasePool, RoutingPool } from \"./routing-pool\";\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgBA,qBAAmB,QAA4C;CAC7E,MAAM,SAAS,aAAa,sBAAsB;AAElD,QAAOC,mBAAuB;EAC5B,GAAG;EACH;EACD,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { LakebasePoolConfig } from "@databricks/lakebase";
2
+ import { Pool } from "pg";
3
+
4
+ //#region src/connectors/lakebase/pool-manager.d.ts
5
+ /**
6
+ * Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId).
7
+ *
8
+ * Used for On-Behalf-Of (OBO) scenarios where each user needs their own pool
9
+ * with their own OAuth token refresh, enabling features like Row-Level Security.
10
+ */
11
+ interface LakebasePoolManager {
12
+ /**
13
+ * Get an existing pool or create a new one for the given key.
14
+ * When creating, merges `perPoolConfig` with the base config passed to the factory.
15
+ *
16
+ * If `tokenFingerprint` is provided and differs from the cached pool's
17
+ * fingerprint, the stale pool is closed and a fresh one is created with
18
+ * the new config (including the updated `workspaceClient`).
19
+ */
20
+ getPool(key: string, perPoolConfig: Partial<LakebasePoolConfig>, tokenFingerprint?: string): Pool;
21
+ /** Check whether a pool exists for the given key. */
22
+ hasPool(key: string): boolean;
23
+ /** Close and remove a specific pool. */
24
+ closePool(key: string): Promise<void>;
25
+ /** Close all managed pools and stop cleanup (for graceful shutdown). */
26
+ closeAll(): Promise<void>;
27
+ /** Number of active pools. */
28
+ readonly size: number;
29
+ }
30
+ /**
31
+ * Create a pool manager that maintains per-key Lakebase connection pools.
32
+ *
33
+ * Each pool is created via `createLakebasePool` with the base config merged
34
+ * with per-pool overrides (e.g. a user's `workspaceClient` and `user`).
35
+ *
36
+ * A periodic cleanup removes empty Pool objects (where all connections have
37
+ * been closed by pg's built-in `idleTimeoutMillis`) from the internal Map.
38
+ *
39
+ * @example OBO usage
40
+ * ```typescript
41
+ * const poolManager = createLakebasePoolManager();
42
+ *
43
+ * // In a route handler:
44
+ * const userPool = poolManager.getPool(userName, {
45
+ * workspaceClient: new WorkspaceClient({ token: userToken, host, authType: "pat" }),
46
+ * user: userName,
47
+ * });
48
+ * const result = await userPool.query("SELECT * FROM products");
49
+ * ```
50
+ */
51
+ declare function createLakebasePoolManager(baseConfig?: Partial<LakebasePoolConfig>): LakebasePoolManager;
52
+ //#endregion
53
+ export { LakebasePoolManager, createLakebasePoolManager };
54
+ //# sourceMappingURL=pool-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool-manager.d.ts","names":[],"sources":["../../../src/connectors/lakebase/pool-manager.ts"],"mappings":";;;;;;AAaA;;;;UAAiB,mBAAA;EAaZ;;;;;;;;EAJH,OAAA,CACE,GAAA,UACA,aAAA,EAAe,OAAA,CAAQ,kBAAA,GACvB,gBAAA,YACC,IAAA;EAFD;EAKF,OAAA,CAAQ,GAAA;EAHL;EAMH,SAAA,CAAU,GAAA,WAAc,OAAA;EAHhB;EAMR,QAAA,IAAY,OAAA;EAHF;EAAA,SAMD,IAAA;AAAA;;;;;AAwBX;;;;;;;;;;;;;;;;;iBAAgB,yBAAA,CACd,UAAA,GAAa,OAAA,CAAQ,kBAAA,IACpB,mBAAA"}
@@ -0,0 +1,77 @@
1
+ import { createLakebasePool } from "./index.js";
2
+
3
+ //#region src/connectors/lakebase/pool-manager.ts
4
+ /** Interval for removing empty (connectionless) pools from the Map. */
5
+ const CLEANUP_INTERVAL_MS = 300 * 1e3;
6
+ /**
7
+ * Create a pool manager that maintains per-key Lakebase connection pools.
8
+ *
9
+ * Each pool is created via `createLakebasePool` with the base config merged
10
+ * with per-pool overrides (e.g. a user's `workspaceClient` and `user`).
11
+ *
12
+ * A periodic cleanup removes empty Pool objects (where all connections have
13
+ * been closed by pg's built-in `idleTimeoutMillis`) from the internal Map.
14
+ *
15
+ * @example OBO usage
16
+ * ```typescript
17
+ * const poolManager = createLakebasePoolManager();
18
+ *
19
+ * // In a route handler:
20
+ * const userPool = poolManager.getPool(userName, {
21
+ * workspaceClient: new WorkspaceClient({ token: userToken, host, authType: "pat" }),
22
+ * user: userName,
23
+ * });
24
+ * const result = await userPool.query("SELECT * FROM products");
25
+ * ```
26
+ */
27
+ function createLakebasePoolManager(baseConfig) {
28
+ const entries = /* @__PURE__ */ new Map();
29
+ const cleanupTimer = setInterval(() => {
30
+ for (const [key, entry] of entries) if (entry.pool.totalCount === 0) {
31
+ entry.pool.end().catch(() => {});
32
+ entries.delete(key);
33
+ }
34
+ }, CLEANUP_INTERVAL_MS);
35
+ cleanupTimer.unref();
36
+ return {
37
+ getPool(key, perPoolConfig, tokenFingerprint) {
38
+ const existing = entries.get(key);
39
+ if (existing) {
40
+ if (!(tokenFingerprint && existing.tokenFingerprint && tokenFingerprint !== existing.tokenFingerprint)) return existing.pool;
41
+ existing.pool.end().catch(() => {});
42
+ }
43
+ const pool = createLakebasePool({
44
+ ...baseConfig,
45
+ ...perPoolConfig
46
+ });
47
+ entries.set(key, {
48
+ pool,
49
+ tokenFingerprint
50
+ });
51
+ return pool;
52
+ },
53
+ hasPool(key) {
54
+ return entries.has(key);
55
+ },
56
+ async closePool(key) {
57
+ const entry = entries.get(key);
58
+ if (entry) {
59
+ await entry.pool.end();
60
+ entries.delete(key);
61
+ }
62
+ },
63
+ async closeAll() {
64
+ clearInterval(cleanupTimer);
65
+ const endPromises = [...entries.values()].map((e) => e.pool.end());
66
+ await Promise.all(endPromises);
67
+ entries.clear();
68
+ },
69
+ get size() {
70
+ return entries.size;
71
+ }
72
+ };
73
+ }
74
+
75
+ //#endregion
76
+ export { createLakebasePoolManager };
77
+ //# sourceMappingURL=pool-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool-manager.js","names":[],"sources":["../../../src/connectors/lakebase/pool-manager.ts"],"sourcesContent":["import type { LakebasePoolConfig } from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\nimport { createLakebasePool } from \"./index\";\n\n/** Interval for removing empty (connectionless) pools from the Map. */\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId).\n *\n * Used for On-Behalf-Of (OBO) scenarios where each user needs their own pool\n * with their own OAuth token refresh, enabling features like Row-Level Security.\n */\nexport interface LakebasePoolManager {\n /**\n * Get an existing pool or create a new one for the given key.\n * When creating, merges `perPoolConfig` with the base config passed to the factory.\n *\n * If `tokenFingerprint` is provided and differs from the cached pool's\n * fingerprint, the stale pool is closed and a fresh one is created with\n * the new config (including the updated `workspaceClient`).\n */\n getPool(\n key: string,\n perPoolConfig: Partial<LakebasePoolConfig>,\n tokenFingerprint?: string,\n ): Pool;\n\n /** Check whether a pool exists for the given key. */\n hasPool(key: string): boolean;\n\n /** Close and remove a specific pool. */\n closePool(key: string): Promise<void>;\n\n /** Close all managed pools and stop cleanup (for graceful shutdown). */\n closeAll(): Promise<void>;\n\n /** Number of active pools. */\n readonly size: number;\n}\n\n/**\n * Create a pool manager that maintains per-key Lakebase connection pools.\n *\n * Each pool is created via `createLakebasePool` with the base config merged\n * with per-pool overrides (e.g. a user's `workspaceClient` and `user`).\n *\n * A periodic cleanup removes empty Pool objects (where all connections have\n * been closed by pg's built-in `idleTimeoutMillis`) from the internal Map.\n *\n * @example OBO usage\n * ```typescript\n * const poolManager = createLakebasePoolManager();\n *\n * // In a route handler:\n * const userPool = poolManager.getPool(userName, {\n * workspaceClient: new WorkspaceClient({ token: userToken, host, authType: \"pat\" }),\n * user: userName,\n * });\n * const result = await userPool.query(\"SELECT * FROM products\");\n * ```\n */\nexport function createLakebasePoolManager(\n baseConfig?: Partial<LakebasePoolConfig>,\n): LakebasePoolManager {\n interface PoolEntry {\n pool: Pool;\n tokenFingerprint?: string;\n }\n\n const entries = new Map<string, PoolEntry>();\n\n // Periodically remove empty Pool objects from the Map.\n // pg.Pool's idleTimeoutMillis closes idle connections automatically;\n // this just cleans up the Map entries once all connections are gone.\n const cleanupTimer = setInterval(() => {\n for (const [key, entry] of entries) {\n if (entry.pool.totalCount === 0) {\n entry.pool.end().catch(() => {});\n entries.delete(key);\n }\n }\n }, CLEANUP_INTERVAL_MS);\n cleanupTimer.unref();\n\n return {\n getPool(\n key: string,\n perPoolConfig: Partial<LakebasePoolConfig>,\n tokenFingerprint?: string,\n ): Pool {\n const existing = entries.get(key);\n\n if (existing) {\n // When the caller provides a fingerprint that differs from the\n // cached one, the underlying OBO token has rotated. The pool's\n // password callback holds a stale WorkspaceClient (authType: \"pat\",\n // static token) that will fail once the Lakebase Postgres token\n // needs refreshing. Drain the old pool and create a fresh one.\n const stale =\n tokenFingerprint &&\n existing.tokenFingerprint &&\n tokenFingerprint !== existing.tokenFingerprint;\n\n if (!stale) return existing.pool;\n\n existing.pool.end().catch(() => {});\n }\n\n // Safe without locking: createLakebasePool is synchronous and Node.js\n // is single-threaded, so no preemption between get() and set().\n const pool = createLakebasePool({ ...baseConfig, ...perPoolConfig });\n entries.set(key, { pool, tokenFingerprint });\n return pool;\n },\n\n hasPool(key: string): boolean {\n return entries.has(key);\n },\n\n async closePool(key: string): Promise<void> {\n const entry = entries.get(key);\n if (entry) {\n await entry.pool.end();\n entries.delete(key);\n }\n },\n\n async closeAll(): Promise<void> {\n clearInterval(cleanupTimer);\n const endPromises = [...entries.values()].map((e) => e.pool.end());\n await Promise.all(endPromises);\n entries.clear();\n },\n\n get size() {\n return entries.size;\n },\n };\n}\n"],"mappings":";;;;AAKA,MAAM,sBAAsB,MAAS;;;;;;;;;;;;;;;;;;;;;;AAyDrC,SAAgB,0BACd,YACqB;CAMrB,MAAM,0BAAU,IAAI,KAAwB;CAK5C,MAAM,eAAe,kBAAkB;AACrC,OAAK,MAAM,CAAC,KAAK,UAAU,QACzB,KAAI,MAAM,KAAK,eAAe,GAAG;AAC/B,SAAM,KAAK,KAAK,CAAC,YAAY,GAAG;AAChC,WAAQ,OAAO,IAAI;;IAGtB,oBAAoB;AACvB,cAAa,OAAO;AAEpB,QAAO;EACL,QACE,KACA,eACA,kBACM;GACN,MAAM,WAAW,QAAQ,IAAI,IAAI;AAEjC,OAAI,UAAU;AAWZ,QAAI,EAJF,oBACA,SAAS,oBACT,qBAAqB,SAAS,kBAEpB,QAAO,SAAS;AAE5B,aAAS,KAAK,KAAK,CAAC,YAAY,GAAG;;GAKrC,MAAM,OAAO,mBAAmB;IAAE,GAAG;IAAY,GAAG;IAAe,CAAC;AACpE,WAAQ,IAAI,KAAK;IAAE;IAAM;IAAkB,CAAC;AAC5C,UAAO;;EAGT,QAAQ,KAAsB;AAC5B,UAAO,QAAQ,IAAI,IAAI;;EAGzB,MAAM,UAAU,KAA4B;GAC1C,MAAM,QAAQ,QAAQ,IAAI,IAAI;AAC9B,OAAI,OAAO;AACT,UAAM,MAAM,KAAK,KAAK;AACtB,YAAQ,OAAO,IAAI;;;EAIvB,MAAM,WAA0B;AAC9B,iBAAc,aAAa;GAC3B,MAAM,cAAc,CAAC,GAAG,QAAQ,QAAQ,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,KAAK,CAAC;AAClE,SAAM,QAAQ,IAAI,YAAY;AAC9B,WAAQ,OAAO;;EAGjB,IAAI,OAAO;AACT,UAAO,QAAQ;;EAElB"}
@@ -0,0 +1,22 @@
1
+ import "../../context/user-context.js";
2
+ import { Pool, PoolClient, QueryResult, QueryResultRow } from "pg";
3
+
4
+ //#region src/connectors/lakebase/routing-pool.d.ts
5
+ /**
6
+ * Subset of `pg.Pool` exposed by the Lakebase plugin.
7
+ *
8
+ * RoutingPool does not extend EventEmitter — event listener methods
9
+ * like `on('error', ...)` are not available. Use `query()`, `connect()`,
10
+ * and `end()` for all pool operations.
11
+ */
12
+ interface LakebasePool {
13
+ query<T extends QueryResultRow = any>(text: string, values?: unknown[]): Promise<QueryResult<T>>;
14
+ connect(): Promise<PoolClient>;
15
+ end(): Promise<void>;
16
+ readonly totalCount: number;
17
+ readonly idleCount: number;
18
+ readonly waitingCount: number;
19
+ }
20
+ //#endregion
21
+ export { LakebasePool };
22
+ //# sourceMappingURL=routing-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-pool.d.ts","names":[],"sources":["../../../src/connectors/lakebase/routing-pool.ts"],"mappings":";;;;;;AAWA;;;;;UAAiB,YAAA;EACf,KAAA,WAAgB,cAAA,QACd,IAAA,UACA,MAAA,eACC,OAAA,CAAQ,WAAA,CAAY,CAAA;EACvB,OAAA,IAAW,OAAA,CAAQ,UAAA;EACnB,GAAA,IAAO,OAAA;EAAA,SACE,UAAA;EAAA,SACA,SAAA;EAAA,SACA,YAAA;AAAA"}
@@ -0,0 +1,48 @@
1
+ import { getUserContext, init_execution_context } from "../../context/execution-context.js";
2
+
3
+ //#region src/connectors/lakebase/routing-pool.ts
4
+ init_execution_context();
5
+ /**
6
+ * A `pg.Pool`-like wrapper that routes queries to the appropriate pool
7
+ * based on the current execution context.
8
+ *
9
+ * When called inside `runInUserContext()` (set up by `Plugin.asUser(req)`),
10
+ * queries route to the per-user pool returned by `resolveUserPool`.
11
+ * Otherwise, queries route to the service-principal pool.
12
+ *
13
+ * This enables OBO (On-Behalf-Of) without custom `asUser()` overrides —
14
+ * the base class sets up AsyncLocalStorage context, and the RoutingPool
15
+ * reads it transparently.
16
+ */
17
+ var RoutingPool = class {
18
+ constructor(spPool, resolveUserPool) {
19
+ this.spPool = spPool;
20
+ this.resolveUserPool = resolveUserPool;
21
+ }
22
+ activePool() {
23
+ const userCtx = getUserContext();
24
+ return userCtx ? this.resolveUserPool(userCtx) : this.spPool;
25
+ }
26
+ query(text, values) {
27
+ return this.activePool().query(text, values);
28
+ }
29
+ connect() {
30
+ return this.activePool().connect();
31
+ }
32
+ async end() {
33
+ await this.spPool.end();
34
+ }
35
+ get totalCount() {
36
+ return this.spPool.totalCount;
37
+ }
38
+ get idleCount() {
39
+ return this.spPool.idleCount;
40
+ }
41
+ get waitingCount() {
42
+ return this.spPool.waitingCount;
43
+ }
44
+ };
45
+
46
+ //#endregion
47
+ export { RoutingPool };
48
+ //# sourceMappingURL=routing-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-pool.js","names":[],"sources":["../../../src/connectors/lakebase/routing-pool.ts"],"sourcesContent":["import type { Pool, PoolClient, QueryResult, QueryResultRow } from \"pg\";\nimport { getUserContext } from \"../../context/execution-context\";\nimport type { UserContext } from \"../../context/user-context\";\n\n/**\n * Subset of `pg.Pool` exposed by the Lakebase plugin.\n *\n * RoutingPool does not extend EventEmitter — event listener methods\n * like `on('error', ...)` are not available. Use `query()`, `connect()`,\n * and `end()` for all pool operations.\n */\nexport interface LakebasePool {\n query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>>;\n connect(): Promise<PoolClient>;\n end(): Promise<void>;\n readonly totalCount: number;\n readonly idleCount: number;\n readonly waitingCount: number;\n}\n\n/**\n * A `pg.Pool`-like wrapper that routes queries to the appropriate pool\n * based on the current execution context.\n *\n * When called inside `runInUserContext()` (set up by `Plugin.asUser(req)`),\n * queries route to the per-user pool returned by `resolveUserPool`.\n * Otherwise, queries route to the service-principal pool.\n *\n * This enables OBO (On-Behalf-Of) without custom `asUser()` overrides —\n * the base class sets up AsyncLocalStorage context, and the RoutingPool\n * reads it transparently.\n */\nexport class RoutingPool implements LakebasePool {\n constructor(\n private spPool: Pool,\n private resolveUserPool: (ctx: UserContext) => Pool,\n ) {}\n\n private activePool(): Pool {\n const userCtx = getUserContext();\n return userCtx ? this.resolveUserPool(userCtx) : this.spPool;\n }\n\n query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n return this.activePool().query<T>(text, values);\n }\n\n connect(): Promise<PoolClient> {\n return this.activePool().connect();\n }\n\n async end(): Promise<void> {\n await this.spPool.end();\n }\n\n get totalCount() {\n return this.spPool.totalCount;\n }\n get idleCount() {\n return this.spPool.idleCount;\n }\n get waitingCount() {\n return this.spPool.waitingCount;\n }\n}\n"],"mappings":";;;wBACiE;;;;;;;;;;;;;AAkCjE,IAAa,cAAb,MAAiD;CAC/C,YACE,AAAQ,QACR,AAAQ,iBACR;EAFQ;EACA;;CAGV,AAAQ,aAAmB;EACzB,MAAM,UAAU,gBAAgB;AAChC,SAAO,UAAU,KAAK,gBAAgB,QAAQ,GAAG,KAAK;;CAGxD,MACE,MACA,QACyB;AACzB,SAAO,KAAK,YAAY,CAAC,MAAS,MAAM,OAAO;;CAGjD,UAA+B;AAC7B,SAAO,KAAK,YAAY,CAAC,SAAS;;CAGpC,MAAM,MAAqB;AACzB,QAAM,KAAK,OAAO,KAAK;;CAGzB,IAAI,aAAa;AACf,SAAO,KAAK,OAAO;;CAErB,IAAI,YAAY;AACd,SAAO,KAAK,OAAO;;CAErB,IAAI,eAAe;AACjB,SAAO,KAAK,OAAO"}
@@ -66,6 +66,14 @@ function getWorkspaceId() {
66
66
  function isInUserContext() {
67
67
  return executionContextStorage.getStore() !== void 0;
68
68
  }
69
+ /**
70
+ * Get the user context if one is active, otherwise `undefined`.
71
+ * Unlike `getExecutionContext()`, this does not require `ServiceContext`
72
+ * to be initialized and never throws.
73
+ */
74
+ function getUserContext() {
75
+ return executionContextStorage.getStore();
76
+ }
69
77
  var executionContextStorage;
70
78
  var init_execution_context = __esmMin((() => {
71
79
  init_errors();
@@ -76,5 +84,5 @@ var init_execution_context = __esmMin((() => {
76
84
 
77
85
  //#endregion
78
86
  init_execution_context();
79
- export { getCurrentUserId, getExecutionContext, getWarehouseId, getWorkspaceClient, getWorkspaceId, init_execution_context, isInUserContext, runInUserContext };
87
+ export { getCurrentUserId, getExecutionContext, getUserContext, getWarehouseId, getWorkspaceClient, getWorkspaceId, init_execution_context, isInUserContext, runInUserContext };
80
88
  //# sourceMappingURL=execution-context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"execution-context.js","names":[],"sources":["../../src/context/execution-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { ConfigurationError } from \"../errors\";\nimport { ServiceContext } from \"./service-context\";\nimport {\n type ExecutionContext,\n isUserContext,\n type UserContext,\n} from \"./user-context\";\n\n/**\n * AsyncLocalStorage for execution context.\n * Used to pass user context through the call stack without explicit parameters.\n */\nconst executionContextStorage = new AsyncLocalStorage<UserContext>();\n\n/**\n * Run a function in the context of a user.\n * All calls within the function will have access to the user context.\n *\n * @param userContext - The user context to use\n * @param fn - The function to run\n * @returns The result of the function\n */\nexport function runInUserContext<T>(userContext: UserContext, fn: () => T): T {\n return executionContextStorage.run(userContext, fn);\n}\n\n/**\n * Get the current execution context.\n *\n * - If running inside a user context (via asUser), returns the user context\n * - Otherwise, returns the service context\n *\n * @throws Error if ServiceContext is not initialized\n */\nexport function getExecutionContext(): ExecutionContext {\n const userContext = executionContextStorage.getStore();\n if (userContext) {\n return userContext;\n }\n return ServiceContext.get();\n}\n\n/**\n * Get the current user ID for cache keying and telemetry.\n *\n * Returns the user ID if in user context, otherwise the service user ID.\n */\nexport function getCurrentUserId(): string {\n const ctx = getExecutionContext();\n if (isUserContext(ctx)) {\n return ctx.userId;\n }\n return ctx.serviceUserId;\n}\n\n/**\n * Get the WorkspaceClient for the current execution context.\n */\nexport function getWorkspaceClient() {\n return getExecutionContext().client;\n}\n\n/**\n * Get the warehouse ID promise.\n */\nexport function getWarehouseId(): Promise<string> {\n const ctx = getExecutionContext();\n if (!ctx.warehouseId) {\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n \"No plugin requires a SQL Warehouse. Add a sql_warehouse resource to your plugin manifest, or set DATABRICKS_WAREHOUSE_ID\",\n );\n }\n return ctx.warehouseId;\n}\n\n/**\n * Get the workspace ID promise.\n */\nexport function getWorkspaceId(): Promise<string> {\n return getExecutionContext().workspaceId;\n}\n\n/**\n * Check if currently running in a user context.\n */\nexport function isInUserContext(): boolean {\n const ctx = executionContextStorage.getStore();\n return ctx !== undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAuBA,SAAgB,iBAAoB,aAA0B,IAAgB;AAC5E,QAAO,wBAAwB,IAAI,aAAa,GAAG;;;;;;;;;;AAWrD,SAAgB,sBAAwC;CACtD,MAAM,cAAc,wBAAwB,UAAU;AACtD,KAAI,YACF,QAAO;AAET,QAAO,eAAe,KAAK;;;;;;;AAQ7B,SAAgB,mBAA2B;CACzC,MAAM,MAAM,qBAAqB;AACjC,KAAI,cAAc,IAAI,CACpB,QAAO,IAAI;AAEb,QAAO,IAAI;;;;;AAMb,SAAgB,qBAAqB;AACnC,QAAO,qBAAqB,CAAC;;;;;AAM/B,SAAgB,iBAAkC;CAChD,MAAM,MAAM,qBAAqB;AACjC,KAAI,CAAC,IAAI,YACP,OAAM,mBAAmB,iBACvB,gBACA,2HACD;AAEH,QAAO,IAAI;;;;;AAMb,SAAgB,iBAAkC;AAChD,QAAO,qBAAqB,CAAC;;;;;AAM/B,SAAgB,kBAA2B;AAEzC,QADY,wBAAwB,UAAU,KAC/B;;;;cAxF8B;uBACI;oBAK3B;CAMlB,0BAA0B,IAAI,mBAAgC"}
1
+ {"version":3,"file":"execution-context.js","names":[],"sources":["../../src/context/execution-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { ConfigurationError } from \"../errors\";\nimport { ServiceContext } from \"./service-context\";\nimport {\n type ExecutionContext,\n isUserContext,\n type UserContext,\n} from \"./user-context\";\n\n/**\n * AsyncLocalStorage for execution context.\n * Used to pass user context through the call stack without explicit parameters.\n */\nconst executionContextStorage = new AsyncLocalStorage<UserContext>();\n\n/**\n * Run a function in the context of a user.\n * All calls within the function will have access to the user context.\n *\n * @param userContext - The user context to use\n * @param fn - The function to run\n * @returns The result of the function\n */\nexport function runInUserContext<T>(userContext: UserContext, fn: () => T): T {\n return executionContextStorage.run(userContext, fn);\n}\n\n/**\n * Get the current execution context.\n *\n * - If running inside a user context (via asUser), returns the user context\n * - Otherwise, returns the service context\n *\n * @throws Error if ServiceContext is not initialized\n */\nexport function getExecutionContext(): ExecutionContext {\n const userContext = executionContextStorage.getStore();\n if (userContext) {\n return userContext;\n }\n return ServiceContext.get();\n}\n\n/**\n * Get the current user ID for cache keying and telemetry.\n *\n * Returns the user ID if in user context, otherwise the service user ID.\n */\nexport function getCurrentUserId(): string {\n const ctx = getExecutionContext();\n if (isUserContext(ctx)) {\n return ctx.userId;\n }\n return ctx.serviceUserId;\n}\n\n/**\n * Get the WorkspaceClient for the current execution context.\n */\nexport function getWorkspaceClient() {\n return getExecutionContext().client;\n}\n\n/**\n * Get the warehouse ID promise.\n */\nexport function getWarehouseId(): Promise<string> {\n const ctx = getExecutionContext();\n if (!ctx.warehouseId) {\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n \"No plugin requires a SQL Warehouse. Add a sql_warehouse resource to your plugin manifest, or set DATABRICKS_WAREHOUSE_ID\",\n );\n }\n return ctx.warehouseId;\n}\n\n/**\n * Get the workspace ID promise.\n */\nexport function getWorkspaceId(): Promise<string> {\n return getExecutionContext().workspaceId;\n}\n\n/**\n * Check if currently running in a user context.\n */\nexport function isInUserContext(): boolean {\n const ctx = executionContextStorage.getStore();\n return ctx !== undefined;\n}\n\n/**\n * Get the user context if one is active, otherwise `undefined`.\n * Unlike `getExecutionContext()`, this does not require `ServiceContext`\n * to be initialized and never throws.\n */\nexport function getUserContext(): UserContext | undefined {\n return executionContextStorage.getStore();\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAuBA,SAAgB,iBAAoB,aAA0B,IAAgB;AAC5E,QAAO,wBAAwB,IAAI,aAAa,GAAG;;;;;;;;;;AAWrD,SAAgB,sBAAwC;CACtD,MAAM,cAAc,wBAAwB,UAAU;AACtD,KAAI,YACF,QAAO;AAET,QAAO,eAAe,KAAK;;;;;;;AAQ7B,SAAgB,mBAA2B;CACzC,MAAM,MAAM,qBAAqB;AACjC,KAAI,cAAc,IAAI,CACpB,QAAO,IAAI;AAEb,QAAO,IAAI;;;;;AAMb,SAAgB,qBAAqB;AACnC,QAAO,qBAAqB,CAAC;;;;;AAM/B,SAAgB,iBAAkC;CAChD,MAAM,MAAM,qBAAqB;AACjC,KAAI,CAAC,IAAI,YACP,OAAM,mBAAmB,iBACvB,gBACA,2HACD;AAEH,QAAO,IAAI;;;;;AAMb,SAAgB,iBAAkC;AAChD,QAAO,qBAAqB,CAAC;;;;;AAM/B,SAAgB,kBAA2B;AAEzC,QADY,wBAAwB,UAAU,KAC/B;;;;;;;AAQjB,SAAgB,iBAA0C;AACxD,QAAO,wBAAwB,UAAU;;;;cAjGI;uBACI;oBAK3B;CAMlB,0BAA0B,IAAI,mBAAgC"}
@@ -1 +1 @@
1
- {"version":3,"file":"service-context.d.ts","names":[],"sources":["../../src/context/service-context.ts"],"mappings":";;;;;;AAsBA;;UAAiB,mBAAA;EAEP;EAAR,MAAA,EAAQ,eAAA;EAMK;EAJb,aAAA;EAIoB;EAFpB,WAAA,GAAc,OAAA;EAJN;EAMR,WAAA,EAAa,OAAA;AAAA"}
1
+ {"version":3,"file":"service-context.d.ts","names":[],"sources":["../../src/context/service-context.ts"],"mappings":";;;;;;AAuBA;;UAAiB,mBAAA;EAEP;EAAR,MAAA,EAAQ,eAAA;EAMK;EAJb,aAAA;EAIoB;EAFpB,WAAA,GAAc,OAAA;EAJN;EAMR,WAAA,EAAa,OAAA;AAAA"}
@@ -4,6 +4,7 @@ import { ConfigurationError } from "../errors/configuration.js";
4
4
  import { InitializationError } from "../errors/initialization.js";
5
5
  import { init_errors } from "../errors/index.js";
6
6
  import { name, version } from "../appkit/package.js";
7
+ import { createHash } from "node:crypto";
7
8
  import { ConfigError, WorkspaceClient } from "@databricks/sdk-experimental";
8
9
  import { coerce } from "semver";
9
10
 
@@ -59,7 +60,7 @@ var init_service_context = __esmMin((() => {
59
60
  * @param userName - Optional user name
60
61
  * @throws Error if token is not provided
61
62
  */
62
- static createUserContext(token, userId, userName) {
63
+ static createUserContext(token, userId, userName, userEmail) {
63
64
  if (!token) throw AuthenticationError.missingToken("user token");
64
65
  const host = process.env.DATABRICKS_HOST;
65
66
  if (!host) throw ConfigurationError.missingEnvVar("DATABRICKS_HOST");
@@ -72,6 +73,8 @@ var init_service_context = __esmMin((() => {
72
73
  }, getClientOptions()),
73
74
  userId,
74
75
  userName,
76
+ userEmail,
77
+ tokenFingerprint: createHash("sha256").update(token).digest("hex").slice(0, 16),
75
78
  warehouseId: serviceCtx.warehouseId,
76
79
  workspaceId: serviceCtx.workspaceId,
77
80
  isUserContext: true
@@ -1 +1 @@
1
- {"version":3,"file":"service-context.js","names":["productName","productVersion"],"sources":["../../src/context/service-context.ts"],"sourcesContent":["import {\n type ClientOptions,\n ConfigError,\n type sql,\n WorkspaceClient,\n} from \"@databricks/sdk-experimental\";\nimport { coerce } from \"semver\";\nimport {\n name as productName,\n version as productVersion,\n} from \"../../package.json\";\nimport {\n AuthenticationError,\n ConfigurationError,\n InitializationError,\n} from \"../errors\";\nimport type { UserContext } from \"./user-context\";\n\n/**\n * Service context holds the service principal client and shared resources.\n * This is initialized once at app startup and shared across all requests.\n */\nexport interface ServiceContextState {\n /** WorkspaceClient authenticated as the service principal */\n client: WorkspaceClient;\n /** The service principal's user ID */\n serviceUserId: string;\n /** Promise that resolves to the warehouse ID (only present when a plugin requires `SQL_WAREHOUSE` resource) */\n warehouseId?: Promise<string>;\n /** Promise that resolves to the workspace ID */\n workspaceId: Promise<string>;\n}\n\nfunction getClientOptions(): ClientOptions {\n const isDev = process.env.NODE_ENV === \"development\";\n const semver = coerce(productVersion);\n const normalizedVersion = (semver?.version ??\n productVersion) as ClientOptions[\"productVersion\"];\n\n return {\n product: productName,\n productVersion: normalizedVersion,\n ...(isDev && { userAgentExtra: { mode: \"dev\" } }),\n };\n}\n\n/**\n * ServiceContext is a singleton that manages the service principal's\n * WorkspaceClient and shared resources like warehouse/workspace IDs.\n *\n * It's initialized once at app startup and provides the foundation\n * for both service principal and user context execution.\n */\nexport class ServiceContext {\n private static instance: ServiceContextState | null = null;\n private static initPromise: Promise<ServiceContextState> | null = null;\n\n /**\n * Initialize the service context. Should be called once at app startup.\n * Safe to call multiple times - will return the same instance.\n *\n * @param options - Which shared resources to resolve (derived from plugin manifests).\n * @param client - Optional pre-configured WorkspaceClient to use instead\n * of creating one from environment credentials.\n */\n static async initialize(\n options?: { warehouseId?: boolean },\n client?: WorkspaceClient,\n ): Promise<ServiceContextState> {\n if (ServiceContext.instance) {\n return ServiceContext.instance;\n }\n\n if (ServiceContext.initPromise) {\n return ServiceContext.initPromise;\n }\n\n ServiceContext.initPromise = ServiceContext.createContext(options, client);\n ServiceContext.instance = await ServiceContext.initPromise;\n return ServiceContext.instance;\n }\n\n /**\n * Get the initialized service context.\n * @throws Error if not initialized\n */\n static get(): ServiceContextState {\n if (!ServiceContext.instance) {\n throw InitializationError.notInitialized(\n \"ServiceContext\",\n \"Call ServiceContext.initialize() first\",\n );\n }\n return ServiceContext.instance;\n }\n\n /**\n * Check if the service context has been initialized.\n */\n static isInitialized(): boolean {\n return ServiceContext.instance !== null;\n }\n\n /**\n * Create a user context from request headers.\n *\n * @param token - The user's access token from x-forwarded-access-token header\n * @param userId - The user's ID from x-forwarded-user header\n * @param userName - Optional user name\n * @throws Error if token is not provided\n */\n static createUserContext(\n token: string,\n userId: string,\n userName?: string,\n ): UserContext {\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n const host = process.env.DATABRICKS_HOST;\n if (!host) {\n throw ConfigurationError.missingEnvVar(\"DATABRICKS_HOST\");\n }\n\n const serviceCtx = ServiceContext.get();\n\n // Create user client with the OAuth token from Databricks Apps\n // Note: We use authType: \"pat\" because the token is passed as a Bearer token\n // just like a PAT, even though it's technically an OAuth token\n const userClient = new WorkspaceClient(\n {\n token,\n host,\n authType: \"pat\",\n },\n getClientOptions(),\n );\n\n return {\n client: userClient,\n userId,\n userName,\n warehouseId: serviceCtx.warehouseId,\n workspaceId: serviceCtx.workspaceId,\n isUserContext: true,\n };\n }\n\n /**\n * Get the client options for WorkspaceClient.\n * Exposed for testing purposes.\n */\n static getClientOptions(): ClientOptions {\n return getClientOptions();\n }\n\n private static async createContext(\n options?: { warehouseId?: boolean },\n client?: WorkspaceClient,\n ): Promise<ServiceContextState> {\n try {\n const wsClient = client ?? new WorkspaceClient({}, getClientOptions());\n\n const [resolvedWorkspaceId, currentUser, resolvedWarehouseId] =\n await Promise.all([\n ServiceContext.getWorkspaceId(wsClient),\n wsClient.currentUser.me(),\n options?.warehouseId\n ? ServiceContext.getWarehouseId(wsClient)\n : Promise.resolve(undefined as string | undefined),\n ]);\n\n if (!currentUser.id) {\n throw ConfigurationError.resourceNotFound(\"Service user ID\");\n }\n\n const warehouseId =\n options?.warehouseId && resolvedWarehouseId !== undefined\n ? Promise.resolve(resolvedWarehouseId)\n : undefined;\n\n return {\n client: wsClient,\n serviceUserId: currentUser.id,\n warehouseId,\n workspaceId: Promise.resolve(resolvedWorkspaceId),\n };\n } catch (e) {\n if (e instanceof ConfigError) {\n throw ConfigurationError.databricksAuthenticationSetupFailed(\n e.baseMessage,\n { cause: e },\n );\n }\n throw e;\n }\n }\n\n private static async getWorkspaceId(\n client: WorkspaceClient,\n ): Promise<string> {\n if (process.env.DATABRICKS_WORKSPACE_ID) {\n return process.env.DATABRICKS_WORKSPACE_ID;\n }\n\n const response = (await client.apiClient.request({\n path: \"/api/2.0/preview/scim/v2/Me\",\n method: \"GET\",\n headers: new Headers(),\n raw: false,\n query: {},\n responseHeaders: [\"x-databricks-org-id\"],\n })) as { \"x-databricks-org-id\": string };\n\n if (!response[\"x-databricks-org-id\"]) {\n throw ConfigurationError.resourceNotFound(\"Workspace ID\");\n }\n\n return response[\"x-databricks-org-id\"];\n }\n\n private static async getWarehouseId(\n client: WorkspaceClient,\n ): Promise<string> {\n if (process.env.DATABRICKS_WAREHOUSE_ID) {\n return process.env.DATABRICKS_WAREHOUSE_ID;\n }\n\n if (process.env.NODE_ENV === \"development\") {\n const response = (await client.apiClient.request({\n path: \"/api/2.0/sql/warehouses\",\n method: \"GET\",\n headers: new Headers(),\n raw: false,\n query: {\n skip_cannot_use: \"true\",\n },\n })) as { warehouses: sql.EndpointInfo[] };\n\n const priorities: Record<sql.State, number> = {\n RUNNING: 0,\n STOPPED: 1,\n STARTING: 2,\n STOPPING: 3,\n DELETED: 99,\n DELETING: 99,\n };\n\n const warehouses = (response.warehouses || []).sort((a, b) => {\n return (\n priorities[a.state as sql.State] - priorities[b.state as sql.State]\n );\n });\n\n if (response.warehouses.length === 0) {\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n \"Please configure the DATABRICKS_WAREHOUSE_ID environment variable\",\n );\n }\n\n const firstWarehouse = warehouses[0];\n if (\n firstWarehouse.state === \"DELETED\" ||\n firstWarehouse.state === \"DELETING\" ||\n !firstWarehouse.id\n ) {\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n \"Please configure the DATABRICKS_WAREHOUSE_ID environment variable\",\n );\n }\n\n return firstWarehouse.id;\n }\n\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n \"Please configure the DATABRICKS_WAREHOUSE_ID environment variable\",\n );\n }\n\n /**\n * Reset the service context. Only for testing purposes.\n */\n static reset(): void {\n ServiceContext.instance = null;\n ServiceContext.initPromise = null;\n }\n}\n"],"mappings":";;;;;;;;;;AAiCA,SAAS,mBAAkC;CACzC,MAAM,QAAQ,QAAQ,IAAI,aAAa;AAKvC,QAAO;EACL,SAASA;EACT,gBANa,OAAOC,QAAe,EACF,WACjCA;EAKA,GAAI,SAAS,EAAE,gBAAgB,EAAE,MAAM,OAAO,EAAE;EACjD;;;;cA5BgB;CAsCN,iBAAb,MAAa,eAAe;EAC1B,OAAe,WAAuC;EACtD,OAAe,cAAmD;;;;;;;;;EAUlE,aAAa,WACX,SACA,QAC8B;AAC9B,OAAI,eAAe,SACjB,QAAO,eAAe;AAGxB,OAAI,eAAe,YACjB,QAAO,eAAe;AAGxB,kBAAe,cAAc,eAAe,cAAc,SAAS,OAAO;AAC1E,kBAAe,WAAW,MAAM,eAAe;AAC/C,UAAO,eAAe;;;;;;EAOxB,OAAO,MAA2B;AAChC,OAAI,CAAC,eAAe,SAClB,OAAM,oBAAoB,eACxB,kBACA,yCACD;AAEH,UAAO,eAAe;;;;;EAMxB,OAAO,gBAAyB;AAC9B,UAAO,eAAe,aAAa;;;;;;;;;;EAWrC,OAAO,kBACL,OACA,QACA,UACa;AACb,OAAI,CAAC,MACH,OAAM,oBAAoB,aAAa,aAAa;GAGtD,MAAM,OAAO,QAAQ,IAAI;AACzB,OAAI,CAAC,KACH,OAAM,mBAAmB,cAAc,kBAAkB;GAG3D,MAAM,aAAa,eAAe,KAAK;AAcvC,UAAO;IACL,QAViB,IAAI,gBACrB;KACE;KACA;KACA,UAAU;KACX,EACD,kBAAkB,CACnB;IAIC;IACA;IACA,aAAa,WAAW;IACxB,aAAa,WAAW;IACxB,eAAe;IAChB;;;;;;EAOH,OAAO,mBAAkC;AACvC,UAAO,kBAAkB;;EAG3B,aAAqB,cACnB,SACA,QAC8B;AAC9B,OAAI;IACF,MAAM,WAAW,UAAU,IAAI,gBAAgB,EAAE,EAAE,kBAAkB,CAAC;IAEtE,MAAM,CAAC,qBAAqB,aAAa,uBACvC,MAAM,QAAQ,IAAI;KAChB,eAAe,eAAe,SAAS;KACvC,SAAS,YAAY,IAAI;KACzB,SAAS,cACL,eAAe,eAAe,SAAS,GACvC,QAAQ,QAAQ,OAAgC;KACrD,CAAC;AAEJ,QAAI,CAAC,YAAY,GACf,OAAM,mBAAmB,iBAAiB,kBAAkB;IAG9D,MAAM,cACJ,SAAS,eAAe,wBAAwB,SAC5C,QAAQ,QAAQ,oBAAoB,GACpC;AAEN,WAAO;KACL,QAAQ;KACR,eAAe,YAAY;KAC3B;KACA,aAAa,QAAQ,QAAQ,oBAAoB;KAClD;YACM,GAAG;AACV,QAAI,aAAa,YACf,OAAM,mBAAmB,oCACvB,EAAE,aACF,EAAE,OAAO,GAAG,CACb;AAEH,UAAM;;;EAIV,aAAqB,eACnB,QACiB;AACjB,OAAI,QAAQ,IAAI,wBACd,QAAO,QAAQ,IAAI;GAGrB,MAAM,WAAY,MAAM,OAAO,UAAU,QAAQ;IAC/C,MAAM;IACN,QAAQ;IACR,SAAS,IAAI,SAAS;IACtB,KAAK;IACL,OAAO,EAAE;IACT,iBAAiB,CAAC,sBAAsB;IACzC,CAAC;AAEF,OAAI,CAAC,SAAS,uBACZ,OAAM,mBAAmB,iBAAiB,eAAe;AAG3D,UAAO,SAAS;;EAGlB,aAAqB,eACnB,QACiB;AACjB,OAAI,QAAQ,IAAI,wBACd,QAAO,QAAQ,IAAI;AAGrB,OAAI,QAAQ,IAAI,aAAa,eAAe;IAC1C,MAAM,WAAY,MAAM,OAAO,UAAU,QAAQ;KAC/C,MAAM;KACN,QAAQ;KACR,SAAS,IAAI,SAAS;KACtB,KAAK;KACL,OAAO,EACL,iBAAiB,QAClB;KACF,CAAC;IAEF,MAAM,aAAwC;KAC5C,SAAS;KACT,SAAS;KACT,UAAU;KACV,UAAU;KACV,SAAS;KACT,UAAU;KACX;IAED,MAAM,cAAc,SAAS,cAAc,EAAE,EAAE,MAAM,GAAG,MAAM;AAC5D,YACE,WAAW,EAAE,SAAsB,WAAW,EAAE;MAElD;AAEF,QAAI,SAAS,WAAW,WAAW,EACjC,OAAM,mBAAmB,iBACvB,gBACA,oEACD;IAGH,MAAM,iBAAiB,WAAW;AAClC,QACE,eAAe,UAAU,aACzB,eAAe,UAAU,cACzB,CAAC,eAAe,GAEhB,OAAM,mBAAmB,iBACvB,gBACA,oEACD;AAGH,WAAO,eAAe;;AAGxB,SAAM,mBAAmB,iBACvB,gBACA,oEACD;;;;;EAMH,OAAO,QAAc;AACnB,kBAAe,WAAW;AAC1B,kBAAe,cAAc"}
1
+ {"version":3,"file":"service-context.js","names":["productName","productVersion"],"sources":["../../src/context/service-context.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n type ClientOptions,\n ConfigError,\n type sql,\n WorkspaceClient,\n} from \"@databricks/sdk-experimental\";\nimport { coerce } from \"semver\";\nimport {\n name as productName,\n version as productVersion,\n} from \"../../package.json\";\nimport {\n AuthenticationError,\n ConfigurationError,\n InitializationError,\n} from \"../errors\";\nimport type { UserContext } from \"./user-context\";\n\n/**\n * Service context holds the service principal client and shared resources.\n * This is initialized once at app startup and shared across all requests.\n */\nexport interface ServiceContextState {\n /** WorkspaceClient authenticated as the service principal */\n client: WorkspaceClient;\n /** The service principal's user ID */\n serviceUserId: string;\n /** Promise that resolves to the warehouse ID (only present when a plugin requires `SQL_WAREHOUSE` resource) */\n warehouseId?: Promise<string>;\n /** Promise that resolves to the workspace ID */\n workspaceId: Promise<string>;\n}\n\nfunction getClientOptions(): ClientOptions {\n const isDev = process.env.NODE_ENV === \"development\";\n const semver = coerce(productVersion);\n const normalizedVersion = (semver?.version ??\n productVersion) as ClientOptions[\"productVersion\"];\n\n return {\n product: productName,\n productVersion: normalizedVersion,\n ...(isDev && { userAgentExtra: { mode: \"dev\" } }),\n };\n}\n\n/**\n * ServiceContext is a singleton that manages the service principal's\n * WorkspaceClient and shared resources like warehouse/workspace IDs.\n *\n * It's initialized once at app startup and provides the foundation\n * for both service principal and user context execution.\n */\nexport class ServiceContext {\n private static instance: ServiceContextState | null = null;\n private static initPromise: Promise<ServiceContextState> | null = null;\n\n /**\n * Initialize the service context. Should be called once at app startup.\n * Safe to call multiple times - will return the same instance.\n *\n * @param options - Which shared resources to resolve (derived from plugin manifests).\n * @param client - Optional pre-configured WorkspaceClient to use instead\n * of creating one from environment credentials.\n */\n static async initialize(\n options?: { warehouseId?: boolean },\n client?: WorkspaceClient,\n ): Promise<ServiceContextState> {\n if (ServiceContext.instance) {\n return ServiceContext.instance;\n }\n\n if (ServiceContext.initPromise) {\n return ServiceContext.initPromise;\n }\n\n ServiceContext.initPromise = ServiceContext.createContext(options, client);\n ServiceContext.instance = await ServiceContext.initPromise;\n return ServiceContext.instance;\n }\n\n /**\n * Get the initialized service context.\n * @throws Error if not initialized\n */\n static get(): ServiceContextState {\n if (!ServiceContext.instance) {\n throw InitializationError.notInitialized(\n \"ServiceContext\",\n \"Call ServiceContext.initialize() first\",\n );\n }\n return ServiceContext.instance;\n }\n\n /**\n * Check if the service context has been initialized.\n */\n static isInitialized(): boolean {\n return ServiceContext.instance !== null;\n }\n\n /**\n * Create a user context from request headers.\n *\n * @param token - The user's access token from x-forwarded-access-token header\n * @param userId - The user's ID from x-forwarded-user header\n * @param userName - Optional user name\n * @throws Error if token is not provided\n */\n static createUserContext(\n token: string,\n userId: string,\n userName?: string,\n userEmail?: string,\n ): UserContext {\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n const host = process.env.DATABRICKS_HOST;\n if (!host) {\n throw ConfigurationError.missingEnvVar(\"DATABRICKS_HOST\");\n }\n\n const serviceCtx = ServiceContext.get();\n\n // Create user client with the OAuth token from Databricks Apps\n // Note: We use authType: \"pat\" because the token is passed as a Bearer token\n // just like a PAT, even though it's technically an OAuth token\n const userClient = new WorkspaceClient(\n {\n token,\n host,\n authType: \"pat\",\n },\n getClientOptions(),\n );\n\n const tokenFingerprint = createHash(\"sha256\")\n .update(token)\n .digest(\"hex\")\n .slice(0, 16);\n\n return {\n client: userClient,\n userId,\n userName,\n userEmail,\n tokenFingerprint,\n warehouseId: serviceCtx.warehouseId,\n workspaceId: serviceCtx.workspaceId,\n isUserContext: true,\n };\n }\n\n /**\n * Get the client options for WorkspaceClient.\n * Exposed for testing purposes.\n */\n static getClientOptions(): ClientOptions {\n return getClientOptions();\n }\n\n private static async createContext(\n options?: { warehouseId?: boolean },\n client?: WorkspaceClient,\n ): Promise<ServiceContextState> {\n try {\n const wsClient = client ?? new WorkspaceClient({}, getClientOptions());\n\n const [resolvedWorkspaceId, currentUser, resolvedWarehouseId] =\n await Promise.all([\n ServiceContext.getWorkspaceId(wsClient),\n wsClient.currentUser.me(),\n options?.warehouseId\n ? ServiceContext.getWarehouseId(wsClient)\n : Promise.resolve(undefined as string | undefined),\n ]);\n\n if (!currentUser.id) {\n throw ConfigurationError.resourceNotFound(\"Service user ID\");\n }\n\n const warehouseId =\n options?.warehouseId && resolvedWarehouseId !== undefined\n ? Promise.resolve(resolvedWarehouseId)\n : undefined;\n\n return {\n client: wsClient,\n serviceUserId: currentUser.id,\n warehouseId,\n workspaceId: Promise.resolve(resolvedWorkspaceId),\n };\n } catch (e) {\n if (e instanceof ConfigError) {\n throw ConfigurationError.databricksAuthenticationSetupFailed(\n e.baseMessage,\n { cause: e },\n );\n }\n throw e;\n }\n }\n\n private static async getWorkspaceId(\n client: WorkspaceClient,\n ): Promise<string> {\n if (process.env.DATABRICKS_WORKSPACE_ID) {\n return process.env.DATABRICKS_WORKSPACE_ID;\n }\n\n const response = (await client.apiClient.request({\n path: \"/api/2.0/preview/scim/v2/Me\",\n method: \"GET\",\n headers: new Headers(),\n raw: false,\n query: {},\n responseHeaders: [\"x-databricks-org-id\"],\n })) as { \"x-databricks-org-id\": string };\n\n if (!response[\"x-databricks-org-id\"]) {\n throw ConfigurationError.resourceNotFound(\"Workspace ID\");\n }\n\n return response[\"x-databricks-org-id\"];\n }\n\n private static async getWarehouseId(\n client: WorkspaceClient,\n ): Promise<string> {\n if (process.env.DATABRICKS_WAREHOUSE_ID) {\n return process.env.DATABRICKS_WAREHOUSE_ID;\n }\n\n if (process.env.NODE_ENV === \"development\") {\n const response = (await client.apiClient.request({\n path: \"/api/2.0/sql/warehouses\",\n method: \"GET\",\n headers: new Headers(),\n raw: false,\n query: {\n skip_cannot_use: \"true\",\n },\n })) as { warehouses: sql.EndpointInfo[] };\n\n const priorities: Record<sql.State, number> = {\n RUNNING: 0,\n STOPPED: 1,\n STARTING: 2,\n STOPPING: 3,\n DELETED: 99,\n DELETING: 99,\n };\n\n const warehouses = (response.warehouses || []).sort((a, b) => {\n return (\n priorities[a.state as sql.State] - priorities[b.state as sql.State]\n );\n });\n\n if (response.warehouses.length === 0) {\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n \"Please configure the DATABRICKS_WAREHOUSE_ID environment variable\",\n );\n }\n\n const firstWarehouse = warehouses[0];\n if (\n firstWarehouse.state === \"DELETED\" ||\n firstWarehouse.state === \"DELETING\" ||\n !firstWarehouse.id\n ) {\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n \"Please configure the DATABRICKS_WAREHOUSE_ID environment variable\",\n );\n }\n\n return firstWarehouse.id;\n }\n\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n \"Please configure the DATABRICKS_WAREHOUSE_ID environment variable\",\n );\n }\n\n /**\n * Reset the service context. Only for testing purposes.\n */\n static reset(): void {\n ServiceContext.instance = null;\n ServiceContext.initPromise = null;\n }\n}\n"],"mappings":"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"}
@@ -12,6 +12,10 @@ interface UserContext {
12
12
  userId: string;
13
13
  /** The user's name (from request headers) */
14
14
  userName?: string;
15
+ /** The user's email (from `x-forwarded-email` header) */
16
+ userEmail?: string;
17
+ /** Truncated SHA-256 hash of the user's OBO token, used to detect token rotation */
18
+ tokenFingerprint?: string;
15
19
  /** Promise that resolves to the warehouse ID (inherited from service context, only present when a plugin requires `SQL_WAREHOUSE` resource) */
16
20
  warehouseId?: Promise<string>;
17
21
  /** Promise that resolves to the workspace ID (inherited from service context) */
@@ -1 +1 @@
1
- {"version":3,"file":"user-context.d.ts","names":[],"sources":["../../src/context/user-context.ts"],"mappings":";;;;;AAMA;;UAAiB,WAAA;EAEP;EAAR,MAAA,EAAQ,mBAAA;EAQK;EANb,MAAA;EAMoB;EAJpB,QAAA;EAJQ;EAMR,WAAA,GAAc,OAAA;EAFd;EAIA,WAAA,EAAa,OAAA;EAFC;EAId,aAAA;AAAA;;;;KAMU,gBAAA,GAAmB,mBAAA,GAAsB,WAAA"}
1
+ {"version":3,"file":"user-context.d.ts","names":[],"sources":["../../src/context/user-context.ts"],"mappings":";;;;;AAMA;;UAAiB,WAAA;EAEP;EAAR,MAAA,EAAQ,mBAAA;EAYK;EAVb,MAAA;EAUoB;EARpB,QAAA;EAJQ;EAMR,SAAA;EAFA;EAIA,gBAAA;EAAA;EAEA,WAAA,GAAc,OAAA;EAAA;EAEd,WAAA,EAAa,OAAA;EAAA;EAEb,aAAA;AAAA;;AAMF;;KAAY,gBAAA,GAAmB,mBAAA,GAAsB,WAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"user-context.js","names":[],"sources":["../../src/context/user-context.ts"],"sourcesContent":["import type { ServiceContextState } from \"./service-context\";\n\n/**\n * User execution context extends the service context with user-specific data.\n * Created on-demand when asUser(req) is called.\n */\nexport interface UserContext {\n /** WorkspaceClient authenticated as the user */\n client: ServiceContextState[\"client\"];\n /** The user's ID (from request headers) */\n userId: string;\n /** The user's name (from request headers) */\n userName?: string;\n /** Promise that resolves to the warehouse ID (inherited from service context, only present when a plugin requires `SQL_WAREHOUSE` resource) */\n warehouseId?: Promise<string>;\n /** Promise that resolves to the workspace ID (inherited from service context) */\n workspaceId: Promise<string>;\n /** Flag indicating this is a user context */\n isUserContext: true;\n}\n\n/**\n * Execution context can be either service or user context.\n */\nexport type ExecutionContext = ServiceContextState | UserContext;\n\n/**\n * Check if an execution context is a user context.\n */\nexport function isUserContext(ctx: ExecutionContext): ctx is UserContext {\n return \"isUserContext\" in ctx && ctx.isUserContext === true;\n}\n"],"mappings":";;;;;;AA6BA,SAAgB,cAAc,KAA2C;AACvE,QAAO,mBAAmB,OAAO,IAAI,kBAAkB"}
1
+ {"version":3,"file":"user-context.js","names":[],"sources":["../../src/context/user-context.ts"],"sourcesContent":["import type { ServiceContextState } from \"./service-context\";\n\n/**\n * User execution context extends the service context with user-specific data.\n * Created on-demand when asUser(req) is called.\n */\nexport interface UserContext {\n /** WorkspaceClient authenticated as the user */\n client: ServiceContextState[\"client\"];\n /** The user's ID (from request headers) */\n userId: string;\n /** The user's name (from request headers) */\n userName?: string;\n /** The user's email (from `x-forwarded-email` header) */\n userEmail?: string;\n /** Truncated SHA-256 hash of the user's OBO token, used to detect token rotation */\n tokenFingerprint?: string;\n /** Promise that resolves to the warehouse ID (inherited from service context, only present when a plugin requires `SQL_WAREHOUSE` resource) */\n warehouseId?: Promise<string>;\n /** Promise that resolves to the workspace ID (inherited from service context) */\n workspaceId: Promise<string>;\n /** Flag indicating this is a user context */\n isUserContext: true;\n}\n\n/**\n * Execution context can be either service or user context.\n */\nexport type ExecutionContext = ServiceContextState | UserContext;\n\n/**\n * Check if an execution context is a user context.\n */\nexport function isUserContext(ctx: ExecutionContext): ctx is UserContext {\n return \"isUserContext\" in ctx && ctx.isUserContext === true;\n}\n"],"mappings":";;;;;;AAiCA,SAAgB,cAAc,KAA2C;AACvE,QAAO,mBAAmB,OAAO,IAAI,kBAAkB"}
@@ -1 +1 @@
1
- {"version":3,"file":"appkit.d.ts","names":[],"sources":["../../src/core/appkit.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2TsB,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"}
1
+ {"version":3,"file":"appkit.d.ts","names":[],"sources":["../../src/core/appkit.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkWsB,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"}
@@ -1,13 +1,15 @@
1
1
  import { createLogger } from "../logging/logger.js";
2
+ import { version } from "../appkit/package.js";
3
+ import { ServiceContext } from "../context/service-context.js";
4
+ import { runInUserContext } from "../context/execution-context.js";
2
5
  import { TelemetryManager } from "../telemetry/telemetry-manager.js";
3
6
  import "../telemetry/index.js";
4
7
  import { CacheManager } from "../cache/index.js";
5
- import { version } from "../appkit/package.js";
6
- import { ServiceContext } from "../context/service-context.js";
7
8
  import { init_context } from "../context/index.js";
8
9
  import { isInternalTelemetryEnabled } from "../internal-telemetry/config.js";
9
10
  import { TelemetryReporter } from "../internal-telemetry/reporter.js";
10
11
  import "../internal-telemetry/index.js";
12
+ import { USER_CONTEXT_SYMBOL } from "../plugin/plugin.js";
11
13
  import { ResourceType } from "../registry/types.generated.js";
12
14
  import { ResourceRegistry } from "../registry/resource-registry.js";
13
15
  import "../registry/index.js";
@@ -73,6 +75,22 @@ var AppKit = class AppKit {
73
75
  }
74
76
  }
75
77
  /**
78
+ * Wraps all function properties in an exports object so they run
79
+ * inside the given user context (via AsyncLocalStorage).
80
+ * This ensures RoutingPool and other context-aware code sees the
81
+ * user identity even though the function was obtained outside the proxy.
82
+ */
83
+ wrapExportsInUserContext(exports, userContext) {
84
+ for (const key in exports) {
85
+ if (!Object.hasOwn(exports, key)) continue;
86
+ const val = exports[key];
87
+ if (typeof val === "function") {
88
+ const fn = val;
89
+ exports[key] = (...args) => runInUserContext(userContext, () => fn(...args));
90
+ } else if (AppKit.isPlainObject(val)) this.wrapExportsInUserContext(val, userContext);
91
+ }
92
+ }
93
+ /**
76
94
  * Wraps a plugin's exports with an `asUser` method that returns
77
95
  * a user-scoped version of the exports.
78
96
  *
@@ -90,8 +108,10 @@ var AppKit = class AppKit {
90
108
  ...objExports,
91
109
  asUser: (req) => {
92
110
  const userPlugin = plugin.asUser(req);
93
- const userExports = userPlugin.exports?.() ?? {};
94
- this.bindExportMethods(userExports, userPlugin);
111
+ const userContext = userPlugin[USER_CONTEXT_SYMBOL];
112
+ const userExports = plugin.exports?.() ?? {};
113
+ if (userContext) this.wrapExportsInUserContext(userExports, userContext);
114
+ else this.bindExportMethods(userExports, userPlugin);
95
115
  return userExports;
96
116
  }
97
117
  };