@mastra/factory 0.4.0 → 0.5.0-alpha.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 (34) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +7 -4
  4. package/dist/factory.js.map +1 -1
  5. package/dist/integrations/base.d.ts +43 -5
  6. package/dist/integrations/base.d.ts.map +1 -1
  7. package/dist/integrations/github/webhook.d.ts +25 -5
  8. package/dist/integrations/github/webhook.d.ts.map +1 -1
  9. package/dist/integrations/github/webhook.js +13 -4
  10. package/dist/integrations/github/webhook.js.map +1 -1
  11. package/dist/integrations/platform/github/event-worker.d.ts +2 -7
  12. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  13. package/dist/integrations/platform/github/event-worker.js +1 -0
  14. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  15. package/dist/integrations/slack/connect-route.d.ts +46 -0
  16. package/dist/integrations/slack/connect-route.d.ts.map +1 -0
  17. package/dist/integrations/slack/connect-route.js +186 -0
  18. package/dist/integrations/slack/connect-route.js.map +1 -0
  19. package/dist/integrations/slack/integration.d.ts +62 -0
  20. package/dist/integrations/slack/integration.d.ts.map +1 -0
  21. package/dist/integrations/slack/integration.js +67 -0
  22. package/dist/integrations/slack/integration.js.map +1 -0
  23. package/dist/integrations/slack/slack.d.ts +273 -0
  24. package/dist/integrations/slack/slack.d.ts.map +1 -0
  25. package/dist/integrations/slack/slack.js +406 -0
  26. package/dist/integrations/slack/slack.js.map +1 -0
  27. package/dist/routes/surface.d.ts +6 -0
  28. package/dist/routes/surface.d.ts.map +1 -1
  29. package/dist/routes/surface.js +28 -1
  30. package/dist/routes/surface.js.map +1 -1
  31. package/dist/workspace.d.ts.map +1 -1
  32. package/dist/workspace.js +2 -1
  33. package/dist/workspace.js.map +1 -1
  34. package/package.json +5 -3
@@ -0,0 +1,406 @@
1
+ import { randomUUID } from "crypto";
2
+ import { createSlackAdapter } from "@mastra/slack";
3
+ import { Actions, Card, CardText, LinkButton } from "chat";
4
+ //#region src/integrations/slack/slack.ts
5
+ /**
6
+ * Adapt the source-control owner's storage handle into the
7
+ * {@link SlackSourceControl} surface. Nothing here is GitHub-specific — the
8
+ * owner is whichever integration owns source control, matched by its own
9
+ * `integrationId`. Repo resolution mirrors the factory's own
10
+ * `ensureFactoryRuleSession`: the owner's connection on the factory → its
11
+ * first linked repository → pinned branch or repo default as the base.
12
+ */
13
+ function adaptSourceControlOwner(owner) {
14
+ return {
15
+ async resolveProjectRepository({ orgId, factoryProjectId }) {
16
+ const connection = (await owner.connections.list({
17
+ orgId,
18
+ factoryProjectId
19
+ })).find((candidate) => candidate.integrationId === owner.integrationId);
20
+ if (!connection) return null;
21
+ const first = (await owner.projectRepositories.list({
22
+ orgId,
23
+ connectionId: connection.id
24
+ }))[0];
25
+ if (!first) return null;
26
+ const repository = await owner.repositories.get({
27
+ orgId,
28
+ id: first.repositoryId
29
+ });
30
+ if (!repository) return null;
31
+ return {
32
+ projectRepositoryId: first.id,
33
+ baseBranch: first.branch ?? repository.defaultBranch
34
+ };
35
+ },
36
+ getSessionForBranch: (args) => owner.sessions.getForBranch(args),
37
+ createSession: (args) => owner.sessions.create({
38
+ sessionId: randomUUID(),
39
+ ...args
40
+ })
41
+ };
42
+ }
43
+ /**
44
+ * Read the Slack team id off a raw platform payload (Events API envelope or
45
+ * slash-command body — both carry `team_id`), duck-typed to build the
46
+ * workspace-scoped account-link key.
47
+ */
48
+ function rawTeamId(rawPayload) {
49
+ if (!rawPayload || typeof rawPayload !== "object") return void 0;
50
+ const raw = rawPayload;
51
+ if (typeof raw.team_id === "string" && raw.team_id) return raw.team_id;
52
+ if (typeof raw.team === "string" && raw.team) return raw.team;
53
+ if (raw.team && typeof raw.team === "object") {
54
+ const id = raw.team.id;
55
+ if (typeof id === "string" && id) return id;
56
+ }
57
+ }
58
+ /**
59
+ * The Slack team id survives onto a normalized chat Message only on
60
+ * `message.raw` (the Slack Events API envelope).
61
+ */
62
+ function slackTeamId(message) {
63
+ return rawTeamId(message.raw);
64
+ }
65
+ /**
66
+ * Resolve the web-UI origin for links humans open in a browser (Connect card,
67
+ * session deep links). Prefers `MASTRACODE_PUBLIC_URL` — the origin auth
68
+ * cookies and OAuth redirect allow-lists are registered against — over the
69
+ * channels tunnel, which only Slack's servers need to reach.
70
+ */
71
+ function webPublicUrl() {
72
+ return process.env.MASTRACODE_PUBLIC_URL ?? process.env.MASTRACODE_CHANNELS_PUBLIC_URL;
73
+ }
74
+ /**
75
+ * Resolve the sender's account link, posting an ephemeral "connect your
76
+ * account" card (visible only to the sender) linking into the web UI's
77
+ * Slack-connect flow when they're unlinked.
78
+ */
79
+ async function resolveLinkedSender({ thread, message, accountLinks }) {
80
+ if (!accountLinks) return { status: "ungated" };
81
+ const platform = thread.adapter.name;
82
+ const externalUserId = message.author.userId;
83
+ const externalTeamId = slackTeamId(message);
84
+ const key = externalTeamId ? {
85
+ platform,
86
+ externalTeamId,
87
+ externalUserId
88
+ } : void 0;
89
+ const link = key ? await accountLinks.getAccountLink(key) : null;
90
+ if (link && key) return {
91
+ status: "linked",
92
+ link,
93
+ key
94
+ };
95
+ const publicUrl = webPublicUrl();
96
+ if (publicUrl) await thread.postEphemeral(message.author, buildConnectCard(publicUrl), { fallbackToDM: true });
97
+ return { status: "blocked" };
98
+ }
99
+ /**
100
+ * The "connect your account" card. The link is deliberately identity-free —
101
+ * `/connect/slack` sends the visitor to Connections, where "Connect Slack"
102
+ * runs the OIDC flow and Slack itself asserts the (team, user) pair.
103
+ */
104
+ function buildConnectCard(publicUrl) {
105
+ return Card({
106
+ title: "Connect your account",
107
+ children: [CardText("Connect your account to use this agent."), Actions([LinkButton({
108
+ url: `${publicUrl}/connect/slack`,
109
+ label: "Connect account"
110
+ })])]
111
+ });
112
+ }
113
+ /**
114
+ * Decide which Factory project a linked sender's run belongs to:
115
+ *
116
+ * 1. The link's `defaultFactoryProjectId`, when it still exists (a stale id —
117
+ * deleted factory — falls through as if unset).
118
+ * 2. Else, the tenant's only factory, stamped back onto the link so it shows
119
+ * up (and stays editable) in Connected Accounts settings.
120
+ * 3. Else — zero or several factories — an ephemeral "pick a default factory"
121
+ * card deep-linking to settings, and the run is blocked.
122
+ */
123
+ async function resolveFactoryForLink({ thread, message, link, key, accountLinks, projects }) {
124
+ if (!projects) return { status: "ungated" };
125
+ const orgId = link.orgId ?? "";
126
+ if (link.defaultFactoryProjectId) {
127
+ const existing = await projects.get({
128
+ orgId,
129
+ id: link.defaultFactoryProjectId
130
+ });
131
+ if (existing) return {
132
+ status: "resolved",
133
+ factoryProjectId: existing.id,
134
+ slackWorkItemsEnabled: existing.slackWorkItemsEnabled
135
+ };
136
+ }
137
+ const factories = orgId ? await projects.list({ orgId }) : [];
138
+ if (factories.length === 1) {
139
+ const only = factories[0];
140
+ await accountLinks.setDefaultFactory({
141
+ ...key,
142
+ userId: link.userId,
143
+ factoryProjectId: only.id
144
+ });
145
+ return {
146
+ status: "resolved",
147
+ factoryProjectId: only.id,
148
+ slackWorkItemsEnabled: only.slackWorkItemsEnabled
149
+ };
150
+ }
151
+ const publicUrl = webPublicUrl();
152
+ if (publicUrl) await thread.postEphemeral(message.author, Card({
153
+ title: "Pick a default factory",
154
+ children: [CardText(factories.length === 0 ? "Your account has no factory yet. Create one in the web app, then message me again." : "Your account has several factories. Pick which one Slack sessions should go to, then message me again."), Actions([LinkButton({
155
+ url: `${publicUrl}/settings/connections`,
156
+ label: "Open settings"
157
+ })])]
158
+ }), { fallbackToDM: true });
159
+ return { status: "blocked" };
160
+ }
161
+ /**
162
+ * Deterministic per-thread branch name: `slack/{threadTs}` with characters
163
+ * outside the sandbox git-ref allow-list (`[A-Za-z0-9_./-]`, and `.` for
164
+ * readability) mapped to `-`. `thread.id` is `{channelId}:{threadTs}`
165
+ * (platform-prefixed on handler threads) — the trailing segment is the ts.
166
+ */
167
+ function threadBranch(threadId) {
168
+ return `slack/${(threadId.split(":").at(-1) ?? threadId).replace(/[^A-Za-z0-9_/-]/g, "-")}`;
169
+ }
170
+ /**
171
+ * Resolve the resourceId for a NEW Slack channel thread. A linked sender whose
172
+ * factory has a repository gets a Factory user-session id — the controller
173
+ * session then materializes the repo sandbox via the factory's dynamic
174
+ * workspace (clone + PAT), the session shows up in the web Sessions list, and
175
+ * View Session deep-links land on the normal workspace route. Everything else
176
+ * (unlinked, unrouted, repo-less, or no source control) keeps the chat-only
177
+ * `defaultResourceId`.
178
+ *
179
+ * Pure lookups only — cards for unlinked/unrouted senders are the dispatch
180
+ * gate's job; this hook must never post.
181
+ */
182
+ function createChannelResourceIdResolver(deps) {
183
+ const { accountLinks, projects, sourceControl } = deps;
184
+ return async ({ platform, thread, message }) => {
185
+ const chatOnlyResourceId = `channel:${thread.id}`;
186
+ if (!accountLinks || !projects || !sourceControl) return chatOnlyResourceId;
187
+ try {
188
+ const externalTeamId = rawTeamId(message.raw);
189
+ if (!externalTeamId) return chatOnlyResourceId;
190
+ const link = await accountLinks.getAccountLink({
191
+ platform,
192
+ externalTeamId,
193
+ externalUserId: message.author.userId
194
+ });
195
+ if (!link) return chatOnlyResourceId;
196
+ const orgId = link.orgId ?? "";
197
+ let factoryProjectId;
198
+ if (link.defaultFactoryProjectId && await projects.get({
199
+ orgId,
200
+ id: link.defaultFactoryProjectId
201
+ })) factoryProjectId = link.defaultFactoryProjectId;
202
+ else if (orgId) {
203
+ const factories = await projects.list({ orgId });
204
+ if (factories.length === 1) factoryProjectId = factories[0].id;
205
+ }
206
+ if (!factoryProjectId) return chatOnlyResourceId;
207
+ const repo = await sourceControl.resolveProjectRepository({
208
+ orgId,
209
+ factoryProjectId
210
+ });
211
+ if (!repo) return chatOnlyResourceId;
212
+ const branch = threadBranch(thread.id);
213
+ const existing = await sourceControl.getSessionForBranch({
214
+ projectRepositoryId: repo.projectRepositoryId,
215
+ userId: link.userId,
216
+ branch
217
+ });
218
+ if (existing) return existing.sessionId;
219
+ return (await sourceControl.createSession({
220
+ projectRepositoryId: repo.projectRepositoryId,
221
+ orgId,
222
+ userId: link.userId,
223
+ branch,
224
+ baseBranch: repo.baseBranch
225
+ })).sessionId;
226
+ } catch (error) {
227
+ console.warn("[slack] repo-backed session resolution failed for thread", thread.id, error);
228
+ return chatOnlyResourceId;
229
+ }
230
+ };
231
+ }
232
+ /**
233
+ * Thread id for a NEW Slack channel thread. Repo-backed threads take the
234
+ * user-session id AS their thread id, matching the web convention
235
+ * (FactoryStartCoordinator seeds threads with threadId = sessionId) so
236
+ * `/workspaces/{sessionId}/threads/{sessionId}` resolves Slack-created
237
+ * sessions exactly like web-created ones — no `?resourceId=` override needed.
238
+ * Chat-only threads keep the default random id: their `channel:...`
239
+ * resourceId is a memory key, not a unique thread id.
240
+ */
241
+ const resolveChannelThreadId = ({ resourceId, defaultThreadId }) => resourceId.startsWith("channel:") ? defaultThreadId : resourceId;
242
+ /**
243
+ * The internal Mastra thread the framework created for a channel conversation.
244
+ * The handler's `thread.id` is the platform thread id (e.g. `slack:C123:ts`),
245
+ * NOT the internal UUID — the mapping lives in the stored thread's channel
246
+ * metadata.
247
+ */
248
+ async function findInternalThread(mastra, thread) {
249
+ const { threads } = await (await mastra?.getStorage()?.getStore("memory"))?.listThreads({
250
+ filter: { metadata: {
251
+ channel_platform: thread.adapter.name,
252
+ channel_externalThreadId: thread.id,
253
+ channel_externalChannelId: thread.channelId
254
+ } },
255
+ perPage: 1
256
+ }) ?? { threads: [] };
257
+ return threads[0];
258
+ }
259
+ /**
260
+ * Build the "new session" handler for mention / direct-message events. A mention or
261
+ * DM on a not-yet-subscribed thread starts a NEW session; once subscribed, later
262
+ * events are follow-ups and don't re-announce.
263
+ */
264
+ /**
265
+ * Run the account-link + factory-routing gates for one inbound message.
266
+ * Returns `null` when the run must not dispatch (a prompt card was posted
267
+ * where possible); otherwise the dispatch context — with `routed` present
268
+ * only when a linked sender resolved to a factory.
269
+ */
270
+ async function gateDispatch(thread, message, { accountLinks, projects }, ctx) {
271
+ const sender = await resolveLinkedSender({
272
+ thread,
273
+ message,
274
+ accountLinks
275
+ });
276
+ if (sender.status === "blocked") return null;
277
+ if (sender.status === "linked" && accountLinks) {
278
+ ctx.requestContext.set("user", {
279
+ id: sender.link.userId,
280
+ organizationId: sender.link.orgId
281
+ });
282
+ const route = await resolveFactoryForLink({
283
+ thread,
284
+ message,
285
+ ...sender,
286
+ accountLinks,
287
+ projects
288
+ });
289
+ if (route.status === "blocked") return null;
290
+ if (route.status === "resolved") return { routed: {
291
+ link: sender.link,
292
+ factoryProjectId: route.factoryProjectId,
293
+ slackWorkItemsEnabled: route.slackWorkItemsEnabled
294
+ } };
295
+ }
296
+ return {};
297
+ }
298
+ /**
299
+ * Upsert the Work-board card for a dispatched Slack-thread run. Keyed on the
300
+ * thread via `externalSource` — the work-items domain's unique
301
+ * `(factory_project_id, source_key)` index makes repeat messages reuse the
302
+ * same card, and `reuseMode: 'preserve'` keeps a card a human already dragged
303
+ * across stages untouched. The card lands in Building (`execute`) for every
304
+ * dispatched thread (DM or mention) — there is deliberately no per-origin
305
+ * stage split; smart routing is a follow-up.
306
+ *
307
+ * The session id / branch / threadId and the workspace deep-link are resolved
308
+ * by the caller (which already looked up the internal thread), so this helper
309
+ * just shapes and writes. Best-effort: the run is already dispatched, so a
310
+ * failure logs instead of throwing — work-item creation must never abort a Slack run.
311
+ */
312
+ async function upsertThreadWorkItem({ workItems, thread, message, link, factoryProjectId, session, url }) {
313
+ try {
314
+ const title = message.text.length > 80 ? `${message.text.slice(0, 79)}…` : message.text;
315
+ await workItems.upsert({
316
+ orgId: link.orgId ?? "",
317
+ userId: link.userId,
318
+ factoryProjectId,
319
+ reuseMode: "preserve",
320
+ input: {
321
+ title: title || "Slack thread",
322
+ externalSource: {
323
+ integrationId: thread.adapter.name,
324
+ type: "slack-thread",
325
+ externalId: thread.id,
326
+ ...url ? { url } : {}
327
+ },
328
+ stages: ["execute"],
329
+ ...session ? { sessions: { chat: session } } : {}
330
+ }
331
+ });
332
+ } catch (error) {
333
+ console.warn("[slack] work-item creation failed for thread", thread.id, error);
334
+ }
335
+ }
336
+ function createNewSessionChatHandler(deps) {
337
+ const { workItems } = deps;
338
+ return async (thread, message, defaultHandler, ctx) => {
339
+ const gate = await gateDispatch(thread, message, deps, ctx);
340
+ if (!gate) return;
341
+ const isNewSession = !await thread.isSubscribed();
342
+ await defaultHandler(thread, message);
343
+ if (!isNewSession) return;
344
+ const internalThread = await findInternalThread(ctx.mastra, thread);
345
+ if (!internalThread) {
346
+ console.warn("[onMention] no internal thread found for", thread.id);
347
+ return;
348
+ }
349
+ const isChatOnly = internalThread.resourceId.startsWith("channel:");
350
+ const workspaceSegment = isChatOnly ? "channel" : encodeURIComponent(internalThread.resourceId);
351
+ const threadPath = gate.routed ? `/factories/${encodeURIComponent(gate.routed.factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(internalThread.id)}` : `/threads/${internalThread.id}`;
352
+ const needsResourceParam = isChatOnly || !gate.routed;
353
+ const deepLink = process.env.MASTRACODE_PUBLIC_URL ? needsResourceParam ? `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}?resourceId=${encodeURIComponent(internalThread.resourceId)}` : `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}` : void 0;
354
+ if (workItems && gate.routed?.slackWorkItemsEnabled) {
355
+ const session = isChatOnly ? void 0 : {
356
+ sessionId: internalThread.resourceId,
357
+ branch: threadBranch(thread.id),
358
+ threadId: internalThread.id
359
+ };
360
+ await upsertThreadWorkItem({
361
+ workItems,
362
+ thread,
363
+ message,
364
+ link: gate.routed.link,
365
+ factoryProjectId: gate.routed.factoryProjectId,
366
+ session,
367
+ url: deepLink
368
+ });
369
+ }
370
+ if (!deepLink) return;
371
+ await thread.post(Card({
372
+ title: "New session started",
373
+ children: [Actions([LinkButton({
374
+ url: deepLink,
375
+ label: "View session"
376
+ })])]
377
+ }));
378
+ };
379
+ }
380
+ const createHandlers = (deps) => {
381
+ const newSessionChatHandler = createNewSessionChatHandler(deps);
382
+ return {
383
+ onSubscribedMessage: async (thread, message, defaultHandler, ctx) => {
384
+ if (/^aside\b/i.test(message.text)) return;
385
+ if (!await gateDispatch(thread, message, deps, ctx)) return;
386
+ await defaultHandler(thread, message);
387
+ },
388
+ onMention: newSessionChatHandler,
389
+ onDirectMessage: newSessionChatHandler
390
+ };
391
+ };
392
+ function createSlackChannelsConfig(deps) {
393
+ return {
394
+ adapters: { slack: {
395
+ adapter: createSlackAdapter(deps.slack),
396
+ toolDisplay: "hidden"
397
+ } },
398
+ handlers: createHandlers(deps),
399
+ resolveResourceId: createChannelResourceIdResolver(deps),
400
+ resolveThreadId: resolveChannelThreadId
401
+ };
402
+ }
403
+ //#endregion
404
+ export { adaptSourceControlOwner, createChannelResourceIdResolver, createHandlers, createSlackChannelsConfig, resolveChannelThreadId, resolveFactoryForLink, resolveLinkedSender, upsertThreadWorkItem };
405
+
406
+ //# sourceMappingURL=slack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack.js","names":[],"sources":["../../../src/integrations/slack/slack.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type {\n ChannelHandler,\n ChannelHandlerContext,\n ChannelHandlers,\n ResolveResourceId,\n ResolveThreadId,\n} from '@mastra/core/channels';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { createSlackAdapter } from '@mastra/slack';\nimport { Card, CardText, Actions, LinkButton } from 'chat';\n\nimport type {\n ChannelAccountLink,\n ChannelAccountLinkKey,\n ChannelIdentityStorage,\n} from '../../storage/domains/channel-identity/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { FactoryChannelsConfig } from '../base.js';\n\n// Derive the thread/message types from the core handler signature rather than\n// importing them from `chat` directly: mc-web can resolve a different `chat`\n// version than @mastra/core, and the two `Thread`/`Message` declarations are\n// structurally incompatible (private fields). Using the handler's own types\n// keeps everything on one version.\ntype HandlerThread = Parameters<ChannelHandler>[0];\ntype HandlerMessage = Parameters<ChannelHandler>[1];\n\n/** Dependencies the Slack channel handlers close over, injected from the web entry. */\ninterface SlackChannelDeps {\n /**\n * The factory's reverse-index store mapping a Slack sender to a Mastra\n * tenant. When provided, inbound messages from an unlinked sender are not\n * dispatched — the run only proceeds (with the sender's tenant stamped on\n * the request context) once they've linked their account. Unlinked senders\n * get an ephemeral \"connect your account\" card instead.\n */\n accountLinks?: ChannelIdentityStorage;\n /**\n * Factory projects domain. When provided (alongside `accountLinks`), a\n * linked sender's run must also resolve to a Factory project before it\n * dispatches: their link's default factory, else their tenant's only\n * factory (stamped back onto the link), else an ephemeral \"pick a default\n * factory\" card and no run. Unset → no factory routing (runs dispatch as\n * before).\n */\n projects?: FactoryProjectsStorage;\n /**\n * Narrow source-control surface used to make new Slack threads repo-backed:\n * when the sender is linked and their factory has a repository, the thread's\n * resourceId becomes a Factory user-session id (repo cloned on a\n * `slack/{threadTs}` branch) instead of the chat-only `channel:...` id.\n * Absent (no source-control integration registered) → chat-only sessions as\n * before.\n */\n sourceControl?: SlackSourceControl;\n /**\n * Factory work-items domain. When provided, a dispatched new-session thread\n * (DM or mention) upserts a Work-board card in Building (`execute`) carrying\n * the Slack thread as its external source and binding the repo-backed\n * session. Best-effort — a failure never blocks the run. Unset → no card.\n */\n workItems?: WorkItemsStorage;\n}\n\n/**\n * The slice of the source-control owner's storage the Slack wiring needs.\n * Structural (not the storage types themselves) so slack.ts stays decoupled\n * from the owning integration's module graph and tests can stub it.\n */\nexport interface SlackSourceControl {\n /**\n * Resolve the factory's linked repository — first repo on the factory's\n * source-control connection, the same single-repo assumption the web kickoff\n * makes.\n */\n resolveProjectRepository(args: {\n orgId: string;\n factoryProjectId: string;\n }): Promise<{ projectRepositoryId: string; baseBranch: string } | null>;\n /** Look up an existing user session for a repo branch (idempotent reuse). */\n getSessionForBranch(args: {\n projectRepositoryId: string;\n userId: string;\n branch: string;\n }): Promise<{ sessionId: string } | null>;\n /** Create the durable user-session row the workspace factory materializes. */\n createSession(args: {\n projectRepositoryId: string;\n orgId: string;\n userId: string;\n branch: string;\n baseBranch: string;\n }): Promise<{ sessionId: string }>;\n}\n\n/**\n * Structural slice of the source-control owner's storage handle\n * (`IntegrationContext.storage.sourceControlOwner`, a `SourceControlStorageHandle`)\n * the adapter reads. Structural so slack.ts stays decoupled from the storage\n * module graph and tests can stub it. The handle is bound during\n * `factory.prepare()`; all access here is lazy (request time).\n */\ninterface SourceControlOwnerSlice {\n integrationId: string;\n connections: {\n list(args: { orgId: string; factoryProjectId: string }): Promise<Array<{ id: string; integrationId: string }>>;\n };\n projectRepositories: {\n list(args: {\n orgId: string;\n connectionId: string;\n }): Promise<Array<{ id: string; repositoryId: string; branch?: string | null }>>;\n };\n repositories: {\n get(args: { orgId: string; id: string }): Promise<{ defaultBranch: string } | null>;\n };\n sessions: {\n getForBranch(args: {\n projectRepositoryId: string;\n userId: string;\n branch: string;\n }): Promise<{ sessionId: string } | null>;\n create(input: {\n sessionId: string;\n projectRepositoryId: string;\n orgId: string;\n userId: string;\n branch: string;\n baseBranch: string;\n }): Promise<{ sessionId: string }>;\n };\n}\n\n/**\n * Adapt the source-control owner's storage handle into the\n * {@link SlackSourceControl} surface. Nothing here is GitHub-specific — the\n * owner is whichever integration owns source control, matched by its own\n * `integrationId`. Repo resolution mirrors the factory's own\n * `ensureFactoryRuleSession`: the owner's connection on the factory → its\n * first linked repository → pinned branch or repo default as the base.\n */\nexport function adaptSourceControlOwner(owner: SourceControlOwnerSlice): SlackSourceControl {\n return {\n async resolveProjectRepository({ orgId, factoryProjectId }) {\n const connections = await owner.connections.list({ orgId, factoryProjectId });\n const connection = connections.find(candidate => candidate.integrationId === owner.integrationId);\n if (!connection) return null;\n const projectRepositories = await owner.projectRepositories.list({ orgId, connectionId: connection.id });\n const first = projectRepositories[0];\n if (!first) return null;\n const repository = await owner.repositories.get({ orgId, id: first.repositoryId });\n if (!repository) return null;\n return { projectRepositoryId: first.id, baseBranch: first.branch ?? repository.defaultBranch };\n },\n getSessionForBranch: args => owner.sessions.getForBranch(args),\n createSession: args => owner.sessions.create({ sessionId: randomUUID(), ...args }),\n };\n}\n\n/**\n * Read the Slack team id off a raw platform payload (Events API envelope or\n * slash-command body — both carry `team_id`), duck-typed to build the\n * workspace-scoped account-link key.\n */\nfunction rawTeamId(rawPayload: unknown): string | undefined {\n if (!rawPayload || typeof rawPayload !== 'object') return undefined;\n const raw = rawPayload as { team_id?: unknown; team?: unknown };\n if (typeof raw.team_id === 'string' && raw.team_id) return raw.team_id;\n if (typeof raw.team === 'string' && raw.team) return raw.team;\n if (raw.team && typeof raw.team === 'object') {\n const id = (raw.team as { id?: unknown }).id;\n if (typeof id === 'string' && id) return id;\n }\n return undefined;\n}\n\n/**\n * The Slack team id survives onto a normalized chat Message only on\n * `message.raw` (the Slack Events API envelope).\n */\nfunction slackTeamId(message: HandlerMessage): string | undefined {\n return rawTeamId(message.raw);\n}\n\n/**\n * Resolve the web-UI origin for links humans open in a browser (Connect card,\n * session deep links). Prefers `MASTRACODE_PUBLIC_URL` — the origin auth\n * cookies and OAuth redirect allow-lists are registered against — over the\n * channels tunnel, which only Slack's servers need to reach.\n */\nfunction webPublicUrl(): string | undefined {\n return process.env.MASTRACODE_PUBLIC_URL ?? process.env.MASTRACODE_CHANNELS_PUBLIC_URL;\n}\n\n/** Outcome of the sender-link gate for one inbound message. */\ntype LinkedSenderResult =\n /** Gating not configured — dispatch as before account linking existed. */\n | { status: 'ungated' }\n /** Sender unlinked — Connect card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** Sender linked — their tenant plus the sender key the link lives under. */\n | { status: 'linked'; link: ChannelAccountLink; key: ChannelAccountLinkKey };\n\n/**\n * Resolve the sender's account link, posting an ephemeral \"connect your\n * account\" card (visible only to the sender) linking into the web UI's\n * Slack-connect flow when they're unlinked.\n */\nexport async function resolveLinkedSender({\n thread,\n message,\n accountLinks,\n}: {\n thread: HandlerThread;\n message: HandlerMessage;\n accountLinks?: ChannelIdentityStorage;\n}): Promise<LinkedSenderResult> {\n if (!accountLinks) return { status: 'ungated' };\n const platform = thread.adapter.name;\n const externalUserId = message.author.userId;\n const externalTeamId = slackTeamId(message);\n // Without a team id we can't identify the workspace-scoped link; treat as\n // unlinked so a run never proceeds tenant-less.\n const key = externalTeamId ? { platform, externalTeamId, externalUserId } : undefined;\n const link = key ? await accountLinks.getAccountLink(key) : null;\n if (link && key) return { status: 'linked', link, key };\n\n const publicUrl = webPublicUrl();\n // A public origin is all the card needs. The link carries no identity: the\n // web app authenticates the visitor, then Slack's OIDC flow proves which\n // Slack account they control. Without an origin, still block, just no card.\n if (publicUrl) {\n await thread.postEphemeral(message.author, buildConnectCard(publicUrl), { fallbackToDM: true });\n }\n return { status: 'blocked' };\n}\n\n/**\n * The \"connect your account\" card. The link is deliberately identity-free —\n * `/connect/slack` sends the visitor to Connections, where \"Connect Slack\"\n * runs the OIDC flow and Slack itself asserts the (team, user) pair.\n */\nfunction buildConnectCard(publicUrl: string) {\n return Card({\n title: 'Connect your account',\n children: [\n CardText('Connect your account to use this agent.'),\n Actions([\n LinkButton({\n url: `${publicUrl}/connect/slack`,\n label: 'Connect account',\n }),\n ]),\n ],\n });\n}\n\n/** Outcome of factory routing for one linked sender's inbound message. */\ntype FactoryRouteResult =\n /** Factory routing not configured — dispatch without a factory. */\n | { status: 'ungated' }\n /** No factory resolved — prompt card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** The Factory project this sender's runs route to. */\n | { status: 'resolved'; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n\n/**\n * Decide which Factory project a linked sender's run belongs to:\n *\n * 1. The link's `defaultFactoryProjectId`, when it still exists (a stale id —\n * deleted factory — falls through as if unset).\n * 2. Else, the tenant's only factory, stamped back onto the link so it shows\n * up (and stays editable) in Connected Accounts settings.\n * 3. Else — zero or several factories — an ephemeral \"pick a default factory\"\n * card deep-linking to settings, and the run is blocked.\n */\nexport async function resolveFactoryForLink({\n thread,\n message,\n link,\n key,\n accountLinks,\n projects,\n}: {\n thread: HandlerThread;\n message: HandlerMessage;\n link: ChannelAccountLink;\n key: ChannelAccountLinkKey;\n accountLinks: ChannelIdentityStorage;\n projects?: FactoryProjectsStorage;\n}): Promise<FactoryRouteResult> {\n if (!projects) return { status: 'ungated' };\n // Factories are org-scoped; a personal account (no org) has none and lands\n // on the prompt below.\n const orgId = link.orgId ?? '';\n\n if (link.defaultFactoryProjectId) {\n const existing = await projects.get({ orgId, id: link.defaultFactoryProjectId });\n if (existing) {\n return {\n status: 'resolved',\n factoryProjectId: existing.id,\n slackWorkItemsEnabled: existing.slackWorkItemsEnabled,\n };\n }\n }\n\n const factories = orgId ? await projects.list({ orgId }) : [];\n if (factories.length === 1) {\n const only = factories[0]!;\n await accountLinks.setDefaultFactory({ ...key, userId: link.userId, factoryProjectId: only.id });\n return {\n status: 'resolved',\n factoryProjectId: only.id,\n slackWorkItemsEnabled: only.slackWorkItemsEnabled,\n };\n }\n\n const publicUrl = webPublicUrl();\n if (publicUrl) {\n await thread.postEphemeral(\n message.author,\n Card({\n title: 'Pick a default factory',\n children: [\n CardText(\n factories.length === 0\n ? 'Your account has no factory yet. Create one in the web app, then message me again.'\n : 'Your account has several factories. Pick which one Slack sessions should go to, then message me again.',\n ),\n Actions([\n LinkButton({\n url: `${publicUrl}/settings/connections`,\n label: 'Open settings',\n }),\n ]),\n ],\n }),\n { fallbackToDM: true },\n );\n }\n return { status: 'blocked' };\n}\n\n/**\n * Deterministic per-thread branch name: `slack/{threadTs}` with characters\n * outside the sandbox git-ref allow-list (`[A-Za-z0-9_./-]`, and `.` for\n * readability) mapped to `-`. `thread.id` is `{channelId}:{threadTs}`\n * (platform-prefixed on handler threads) — the trailing segment is the ts.\n */\nfunction threadBranch(threadId: string): string {\n const ts = threadId.split(':').at(-1) ?? threadId;\n return `slack/${ts.replace(/[^A-Za-z0-9_/-]/g, '-')}`;\n}\n\n/**\n * Resolve the resourceId for a NEW Slack channel thread. A linked sender whose\n * factory has a repository gets a Factory user-session id — the controller\n * session then materializes the repo sandbox via the factory's dynamic\n * workspace (clone + PAT), the session shows up in the web Sessions list, and\n * View Session deep-links land on the normal workspace route. Everything else\n * (unlinked, unrouted, repo-less, or no source control) keeps the chat-only\n * `defaultResourceId`.\n *\n * Pure lookups only — cards for unlinked/unrouted senders are the dispatch\n * gate's job; this hook must never post.\n */\nexport function createChannelResourceIdResolver(deps: SlackChannelDeps): ResolveResourceId {\n const { accountLinks, projects, sourceControl } = deps;\n return async ({ platform, thread, message }) => {\n // NOT the hook's `defaultResourceId`: configuring a custom resolver\n // bypasses AgentControllerChannels' own `channel:{thread.id}` derivation\n // (agent-controller-channels.ts `resolveChannelResourceId`), and the base\n // default is the per-USER memory key. Chat-only fallbacks must stay\n // per-thread, so reproduce the controller default here.\n const chatOnlyResourceId = `channel:${thread.id}`;\n if (!accountLinks || !projects || !sourceControl) return chatOnlyResourceId;\n try {\n const externalTeamId = rawTeamId(message.raw);\n if (!externalTeamId) return chatOnlyResourceId;\n const link = await accountLinks.getAccountLink({\n platform,\n externalTeamId,\n externalUserId: message.author.userId,\n });\n if (!link) return chatOnlyResourceId;\n\n // Same chain as `resolveFactoryForLink`, minus prompts/stamping: the\n // dispatch gate has already run (and stamped a lone factory) by the\n // time a new thread is created, so this is a read-only re-resolve.\n const orgId = link.orgId ?? '';\n let factoryProjectId: string | undefined;\n if (link.defaultFactoryProjectId && (await projects.get({ orgId, id: link.defaultFactoryProjectId }))) {\n factoryProjectId = link.defaultFactoryProjectId;\n } else if (orgId) {\n const factories = await projects.list({ orgId });\n if (factories.length === 1) factoryProjectId = factories[0]!.id;\n }\n if (!factoryProjectId) return chatOnlyResourceId;\n\n const repo = await sourceControl.resolveProjectRepository({ orgId, factoryProjectId });\n if (!repo) return chatOnlyResourceId;\n\n const branch = threadBranch(thread.id);\n const existing = await sourceControl.getSessionForBranch({\n projectRepositoryId: repo.projectRepositoryId,\n userId: link.userId,\n branch,\n });\n if (existing) return existing.sessionId;\n const session = await sourceControl.createSession({\n projectRepositoryId: repo.projectRepositoryId,\n orgId,\n userId: link.userId,\n branch,\n baseBranch: repo.baseBranch,\n });\n return session.sessionId;\n } catch (error) {\n // Fall back to a chat-only session rather than dropping the message.\n console.warn('[slack] repo-backed session resolution failed for thread', thread.id, error);\n return chatOnlyResourceId;\n }\n };\n}\n\n/**\n * Thread id for a NEW Slack channel thread. Repo-backed threads take the\n * user-session id AS their thread id, matching the web convention\n * (FactoryStartCoordinator seeds threads with threadId = sessionId) so\n * `/workspaces/{sessionId}/threads/{sessionId}` resolves Slack-created\n * sessions exactly like web-created ones — no `?resourceId=` override needed.\n * Chat-only threads keep the default random id: their `channel:...`\n * resourceId is a memory key, not a unique thread id.\n */\nexport const resolveChannelThreadId: ResolveThreadId = ({ resourceId, defaultThreadId }) =>\n resourceId.startsWith('channel:') ? defaultThreadId : resourceId;\n\n/**\n * The internal Mastra thread the framework created for a channel conversation.\n * The handler's `thread.id` is the platform thread id (e.g. `slack:C123:ts`),\n * NOT the internal UUID — the mapping lives in the stored thread's channel\n * metadata.\n */\nasync function findInternalThread(mastra: Mastra | undefined, thread: HandlerThread) {\n const store = await mastra?.getStorage()?.getStore('memory');\n const { threads } = (await store?.listThreads({\n filter: {\n metadata: {\n channel_platform: thread.adapter.name,\n channel_externalThreadId: thread.id,\n channel_externalChannelId: thread.channelId,\n },\n },\n perPage: 1,\n })) ?? { threads: [] };\n return threads[0];\n}\n\n/**\n * Build the \"new session\" handler for mention / direct-message events. A mention or\n * DM on a not-yet-subscribed thread starts a NEW session; once subscribed, later\n * events are follow-ups and don't re-announce.\n */\n/**\n * Run the account-link + factory-routing gates for one inbound message.\n * Returns `null` when the run must not dispatch (a prompt card was posted\n * where possible); otherwise the dispatch context — with `routed` present\n * only when a linked sender resolved to a factory.\n */\nasync function gateDispatch(\n thread: HandlerThread,\n message: HandlerMessage,\n { accountLinks, projects }: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n): Promise<{\n routed?: { link: ChannelAccountLink; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n} | null> {\n const sender = await resolveLinkedSender({ thread, message, accountLinks });\n if (sender.status === 'blocked') return null;\n // Linked senders must also route to a Factory project before a run starts.\n if (sender.status === 'linked' && accountLinks) {\n // Stamp the tenant on the run's request context — the single seam\n // `resolveCredentialStore` reads to load this sender's model credentials.\n // This belongs to the link, not to the routing: a linked sender whose\n // factory routing comes back `ungated` still exits below and dispatches, so\n // stamping only in the routed branch would silently run them on default\n // credentials.\n ctx.requestContext.set('user', { id: sender.link.userId, organizationId: sender.link.orgId });\n\n const route = await resolveFactoryForLink({ thread, message, ...sender, accountLinks, projects });\n if (route.status === 'blocked') return null;\n if (route.status === 'resolved') {\n return {\n routed: {\n link: sender.link,\n factoryProjectId: route.factoryProjectId,\n slackWorkItemsEnabled: route.slackWorkItemsEnabled,\n },\n };\n }\n }\n return {};\n}\n\n/**\n * Upsert the Work-board card for a dispatched Slack-thread run. Keyed on the\n * thread via `externalSource` — the work-items domain's unique\n * `(factory_project_id, source_key)` index makes repeat messages reuse the\n * same card, and `reuseMode: 'preserve'` keeps a card a human already dragged\n * across stages untouched. The card lands in Building (`execute`) for every\n * dispatched thread (DM or mention) — there is deliberately no per-origin\n * stage split; smart routing is a follow-up.\n *\n * The session id / branch / threadId and the workspace deep-link are resolved\n * by the caller (which already looked up the internal thread), so this helper\n * just shapes and writes. Best-effort: the run is already dispatched, so a\n * failure logs instead of throwing — work-item creation must never abort a Slack run.\n */\nexport async function upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link,\n factoryProjectId,\n session,\n url,\n}: {\n workItems: WorkItemsStorage;\n thread: HandlerThread;\n message: HandlerMessage;\n link: ChannelAccountLink;\n factoryProjectId: string;\n /**\n * The repo-backed Factory session to bind under the `chat` role, or\n * `undefined` for a chat-only thread (no Factory session to bind).\n */\n session?: { sessionId: string; branch: string; threadId: string };\n /** Workspace deep-link to the running session; omitted when no public URL. */\n url?: string;\n}): Promise<void> {\n try {\n const title = message.text.length > 80 ? `${message.text.slice(0, 79)}…` : message.text;\n\n await workItems.upsert({\n orgId: link.orgId ?? '',\n userId: link.userId,\n factoryProjectId,\n reuseMode: 'preserve',\n input: {\n title: title || 'Slack thread',\n // `integrationId` is the platform ('slack'); `type` is a single\n // constant (no DM/mention distinction); `externalId` is the stable\n // platform thread id — together they form the idempotency key.\n externalSource: {\n integrationId: thread.adapter.name,\n type: 'slack-thread',\n externalId: thread.id,\n ...(url ? { url } : {}),\n },\n stages: ['execute'],\n ...(session ? { sessions: { chat: session } } : {}),\n },\n });\n } catch (error) {\n console.warn('[slack] work-item creation failed for thread', thread.id, error);\n }\n}\n\nfunction createNewSessionChatHandler(deps: SlackChannelDeps): ChannelHandler {\n const { workItems } = deps;\n return async (thread, message, defaultHandler, ctx) => {\n // Gate on the sender having linked their Slack account to a Mastra tenant.\n // Unlinked → post the ephemeral Connect card and stop; no session/run is\n // created (which would otherwise be tenant-less and fail credential\n // resolution). This handler is the only gate — core dispatches whatever\n // reaches it — so every slot that can start a run must call it.\n const gate = await gateDispatch(thread, message, deps, ctx);\n if (!gate) return;\n\n // A mention on a not-yet-subscribed thread is a NEW session. The\n // default handler auto-subscribes, so once subscribed this is a\n // follow-up mention — don't re-announce.\n const isNewSession = !(await thread.isSubscribed());\n\n // Run the framework handler first so the internal Mastra thread and\n // controller session are created before we build the deep link.\n await defaultHandler(thread, message);\n\n if (!isNewSession) return;\n\n // The internal-thread lookup and deep-link are needed by BOTH the\n // announcement card AND work-item creation, so they run BEFORE the\n // card-only `MASTRACODE_PUBLIC_URL` gate — a deployment without a public\n // origin should still create board cards, just without a clickable link.\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n console.warn('[onMention] no internal thread found for', thread.id);\n return;\n }\n\n // When the sender routed to a factory we know exactly which workspace the\n // session belongs to — deep-link straight into it. A repo-backed thread's\n // resourceId IS the Factory user-session id, so the link lands on the same\n // route a web-started run navigates to; chat-only threads keep the literal\n // `channel` segment (the real resource rides the `?resourceId=` override).\n // Unrouted senders fall back to the factory-agnostic /threads/ redirect.\n // One predicate drives both the path segment and the query param, so the\n // two can never disagree about what the URL already carries.\n const isChatOnly = internalThread.resourceId.startsWith('channel:');\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(internalThread.resourceId);\n const threadPath = gate.routed\n ? `/factories/${encodeURIComponent(gate.routed.factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(internalThread.id)}`\n : `/threads/${internalThread.id}`;\n\n // The param is an override for a URL that can't otherwise name its\n // resource. A routed repo-backed thread already spells the resourceId out\n // as its workspace segment, so appending it again is duplication the app\n // ignores. Chat-only threads need it (their segment is the literal string\n // `channel`), and so does the unrouted fallback, which has no workspace\n // segment at all — `ChannelThreadRedirect` forwards the search through.\n //\n // One shared deep-link: the card's button and the work-item `url` read the\n // SAME value so they can never drift. Undefined without a public origin —\n // the card is then skipped, but the work item is still created (url omitted).\n const needsResourceParam = isChatOnly || !gate.routed;\n const deepLink = process.env.MASTRACODE_PUBLIC_URL\n ? needsResourceParam\n ? `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}?resourceId=${encodeURIComponent(internalThread.resourceId)}`\n : `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}`\n : undefined;\n\n // A dispatched, routed new-session thread becomes a Work-board card in\n // Building. Only routed senders (linked → factory) have the org/user/factory\n // a work item needs. Bind the repo-backed Factory session under the `chat`\n // role; a chat-only `channel:` resourceId is NOT a session id, so bind\n // nothing rather than a bad id. Best-effort (the helper swallows failures).\n if (workItems && gate.routed?.slackWorkItemsEnabled) {\n const session = isChatOnly\n ? undefined\n : { sessionId: internalThread.resourceId, branch: threadBranch(thread.id), threadId: internalThread.id };\n await upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link: gate.routed.link,\n factoryProjectId: gate.routed.factoryProjectId,\n session,\n url: deepLink,\n });\n }\n\n // The announcement card is only useful with a public origin to deep-link\n // to — otherwise the link would be `undefined/threads/...`. Without one the\n // session (and now the work item) still exist; we just skip the broken card.\n if (!deepLink) return;\n\n await thread.post(\n Card({\n title: 'New session started',\n children: [Actions([LinkButton({ url: deepLink, label: 'View session' })])],\n }),\n );\n };\n}\nexport const createHandlers = (deps: SlackChannelDeps): ChannelHandlers => {\n const newSessionChatHandler = createNewSessionChatHandler(deps);\n\n return {\n onSubscribedMessage: async (thread, message, defaultHandler, ctx) => {\n // `aside` as its own leading word lets humans talk in a subscribed\n // thread without the bot replying. Word boundary so messages that\n // merely start with \"aside...\" (e.g. \"asides can wait\") still route.\n if (/^aside\\b/i.test(message.text)) return;\n // A subscribed follow-up from an unlinked sender must not run either\n // (e.g. the link was removed mid-conversation), and it must still\n // resolve a factory (e.g. the default was cleared or its factory\n // deleted mid-conversation).\n const gate = await gateDispatch(thread, message, deps, ctx);\n if (!gate) return;\n await defaultHandler(thread, message);\n },\n onMention: newSessionChatHandler,\n onDirectMessage: newSessionChatHandler,\n };\n};\n\n/** Slack app credentials, passed in explicitly rather than read from env here. */\ninterface SlackCredentials {\n clientId?: string;\n clientSecret?: string;\n signingSecret: string;\n botToken?: string;\n}\n\nexport function createSlackChannelsConfig(deps: SlackChannelDeps & { slack: SlackCredentials }): FactoryChannelsConfig {\n return {\n adapters: {\n slack: {\n adapter: createSlackAdapter(deps.slack),\n toolDisplay: 'hidden',\n },\n },\n handlers: createHandlers(deps),\n // New linked+repo-backed threads own a Factory user-session id as their\n // resourceId, which is what makes the controller session repo-backed.\n resolveResourceId: createChannelResourceIdResolver(deps),\n resolveThreadId: resolveChannelThreadId,\n };\n}\n"],"mappings":"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"}
@@ -79,6 +79,12 @@ export declare function buildIntegrationContext(deps: Pick<FactoryApiRoutesDeps,
79
79
  rules: FactoryRules;
80
80
  factoryReady: boolean;
81
81
  domains: Pick<FactoryApiRoutesDeps['domains'], 'projects' | 'intake' | 'workItems' | 'channelIdentity'>;
82
+ /**
83
+ * Stable id of the registered source-control-owning integration (today:
84
+ * `'github'` when registered). Every call site must derive and pass it so
85
+ * `routes()`, `channels()`, and `workers()` all see the same context shape.
86
+ */
87
+ sourceControlOwnerId?: string;
82
88
  }, integrationId: string): IntegrationContext;
83
89
  /**
84
90
  * Assemble the custom `/web/*` API routes as Mastra `server.apiRoutes`:
@@ -1 +1 @@
1
- {"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../../src/routes/surface.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../../src/routes/surface.ts"],"names":[],"mappings":"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"}
@@ -111,6 +111,7 @@ function buildIntegrationContext(deps, integrationId) {
111
111
  storage: {
112
112
  generic: deps.integrationStorage.forIntegration(integrationId),
113
113
  sourceControl: deps.sourceControlStorage.forIntegration(integrationId),
114
+ ...deps.sourceControlOwnerId ? { sourceControlOwner: deps.sourceControlStorage.forIntegration(deps.sourceControlOwnerId) } : {},
114
115
  projects: deps.domains.projects,
115
116
  intake: deps.domains.intake,
116
117
  channelIdentity: deps.domains.channelIdentity
@@ -164,6 +165,29 @@ function disabledIntegrationStatusRoutes(deps, id, configured = false) {
164
165
  return [];
165
166
  }
166
167
  /**
168
+ * Stub for `GET /web/channel-accounts` when NO Slack integration is
169
+ * registered. The SPA's Connections section polls the path unconditionally;
170
+ * without a stub the SPA fallback serves HTML, which the UI can only read as
171
+ * "old server / unknown". The machine-readable reason lets it say the truth:
172
+ * the integration isn't registered.
173
+ *
174
+ * Mounted only for ABSENT slack — a registered integration owns the path via
175
+ * its connect routes (or, when the state signer is unstable, gets no routes
176
+ * at all and the UI falls back to the generic copy). Static payload, leaks
177
+ * nothing → no auth needed, same posture as the github/linear stubs.
178
+ */
179
+ function absentSlackChannelAccountsRoutes() {
180
+ return [registerApiRoute("/web/channel-accounts", {
181
+ method: "GET",
182
+ requiresAuth: false,
183
+ handler: (c) => c.json({
184
+ accounts: [],
185
+ canConnect: false,
186
+ reason: "not_registered"
187
+ })
188
+ })];
189
+ }
190
+ /**
167
191
  * Assemble the custom `/web/*` API routes as Mastra `server.apiRoutes`:
168
192
  * - fs browser routes (project picker), confined to `fsRoot`
169
193
  * - config routes (provider/API-key/model-pack/OM management)
@@ -182,7 +206,8 @@ function assembleFactoryApiRoutes(deps) {
182
206
  const context = buildIntegrationContext({
183
207
  ...deps,
184
208
  stateSigner: deps.stateSigner,
185
- emitAudit
209
+ emitAudit,
210
+ ...githubRegistration ? { sourceControlOwnerId: "github" } : {}
186
211
  }, integration.id);
187
212
  return guardIntegrationRoutes({
188
213
  ...registration,
@@ -190,6 +215,7 @@ function assembleFactoryApiRoutes(deps) {
190
215
  });
191
216
  });
192
217
  const absentStubs = ["github", "linear"].filter((id) => !registrations.some(({ integration }) => integration.id === id)).flatMap((id) => disabledIntegrationStatusRoutes(deps, id));
218
+ const slackAbsentStubs = registrations.some(({ integration }) => integration.id === "slack") ? [] : absentSlackChannelAccountsRoutes();
193
219
  const transitionService = deps.factoryReady ? deps.factoryTransitionService ?? new FactoryTransitionService({
194
220
  rules: deps.rules,
195
221
  storage: deps.domains.workItems
@@ -234,6 +260,7 @@ function assembleFactoryApiRoutes(deps) {
234
260
  }).routes(),
235
261
  ...integrationRoutes,
236
262
  ...absentStubs,
263
+ ...slackAbsentStubs,
237
264
  ...deps.intakeReady ? new IntakeRoutes({
238
265
  auth: deps.auth,
239
266
  audit: deps.audit,