@dbx-tools/appkit-mastra 0.5.1 → 0.6.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/src/server.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  type MastraPluginConfig,
30
30
  type User,
31
31
  } from "./config.ts";
32
+ import { requestUserEmail, requestUserId } from "./identity.ts";
32
33
  import { resolveFeedbackEnabled } from "./mlflow.ts";
33
34
 
34
35
  import {
@@ -44,6 +45,21 @@ import {
44
45
  */
45
46
  const INVALID_TRACE_ID = "0".repeat(32);
46
47
 
48
+ /**
49
+ * Who a turn is ATTRIBUTED to, independent of which Databricks credential runs
50
+ * its calls. Supplied by the HTTP middleware from the forwarded user headers so
51
+ * that in `service-principal` mode - where every request shares the app SP's
52
+ * client - the memory thread, per-user cache namespace, and trace metadata
53
+ * still key off the real caller. Ignored in an OBO (user) context, whose own
54
+ * user id / email are authoritative.
55
+ */
56
+ export interface AttributedIdentity {
57
+ /** Forwarded `x-forwarded-user` id, used when the client is the service principal. */
58
+ userId?: string | undefined;
59
+ /** Forwarded `x-forwarded-email`, used for trace metadata under the service principal. */
60
+ email?: string | undefined;
61
+ }
62
+
47
63
  /**
48
64
  * Stamp the AppKit user (plus the resource id and trace metadata) onto
49
65
  * `requestContext`.
@@ -56,27 +72,43 @@ const INVALID_TRACE_ID = "0".repeat(32);
56
72
  * callers build a context with {@link createRequestContext} instead of a
57
73
  * request.
58
74
  *
75
+ * The Databricks credential always comes from the ambient execution context;
76
+ * `attributed` only changes WHO the turn is attributed to when that context is
77
+ * the service principal (see {@link AttributedIdentity}).
78
+ *
59
79
  * Idempotent: returns immediately when the user and resource id are already
60
80
  * present, so the middleware can call it over a context another layer stamped.
61
81
  */
62
- export async function stampRequestContextUser(requestContext: RequestContext): Promise<void> {
82
+ export async function stampRequestContextUser(
83
+ requestContext: RequestContext,
84
+ attributed: AttributedIdentity = {},
85
+ ): Promise<void> {
63
86
  if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
64
87
  const executionContext = getExecutionContext();
65
- const user: User = {
66
- id: executionContextUserId(executionContext),
67
- executionContext,
68
- };
88
+ const isUserContext = "isUserContext" in executionContext;
89
+ // The Databricks CREDENTIAL is always the ambient execution context's client
90
+ // (OBO in a user context, the service principal otherwise). WHO the turn is
91
+ // attributed to can differ: in `service-principal` mode every request shares
92
+ // the SP client, but the forwarded user id must still key the memory thread
93
+ // and the per-user cache namespace so two callers don't share a conversation.
94
+ // Prefer the OBO context's own user id, then the forwarded id, then the SP id.
95
+ const id =
96
+ (isUserContext ? executionContextUserId(executionContext) : attributed.userId) ??
97
+ executionContextUserId(executionContext);
98
+ const user: User = { id, executionContext };
69
99
  requestContext.set(MASTRA_USER_KEY, user);
70
100
  requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
71
101
  // AppKit's `UserContext` surfaces display name / email only on
72
102
  // OBO requests. Service-context calls (background tasks, server
73
103
  // start-up) leave these undefined and we skip the stamp so
74
- // downstream trace metadata stays absent rather than empty.
104
+ // downstream trace metadata stays absent rather than empty. In
105
+ // `service-principal` mode the forwarded email still identifies the caller,
106
+ // so it is used for trace metadata even though the client is the SP.
75
107
  let userName: string | undefined;
76
- let email: string | undefined;
77
- if ("isUserContext" in executionContext) {
108
+ let email: string | undefined = attributed.email;
109
+ if (isUserContext) {
78
110
  userName = executionContext.userName;
79
- email = executionContext.userEmail;
111
+ email = executionContext.userEmail ?? email;
80
112
  } else if (process.env.NODE_ENV === "development") {
81
113
  const currentUser = await executionContext.client.currentUser.me();
82
114
  userName = currentUser?.userName;
@@ -142,7 +174,7 @@ export class MastraServer extends MastraServerExpress {
142
174
  super.registerAuthMiddleware();
143
175
  this.app.use(async (req, res, next) => {
144
176
  const requestContext = res.locals.requestContext! as RequestContext;
145
- await this.configureRequestContextUser(requestContext);
177
+ await this.configureRequestContextUser(req, requestContext);
146
178
  this.configureRequestContextThreadId(req, res, requestContext);
147
179
  this.configureRequestContextModelOverride(req, requestContext);
148
180
  this.configureRequestContextRequestId(req, res, requestContext);
@@ -167,8 +199,14 @@ export class MastraServer extends MastraServerExpress {
167
199
  });
168
200
  }
169
201
 
170
- async configureRequestContextUser(requestContext: RequestContext) {
171
- await stampRequestContextUser(requestContext);
202
+ async configureRequestContextUser(req: express.Request, requestContext: RequestContext) {
203
+ // Pass the forwarded caller identity so `service-principal` mode still keys
204
+ // memory / cache / traces per user even though the client is the app SP.
205
+ // In an OBO context these are ignored in favor of the context's own user.
206
+ await stampRequestContextUser(requestContext, {
207
+ userId: requestUserId(req),
208
+ email: requestUserEmail(req),
209
+ });
172
210
  }
173
211
 
174
212
  /**