diff --git a/packages/eve/src/public/agents/vercel.test.ts b/packages/eve/src/public/agents/vercel.test.ts index 9979fd9d9..43ecca5b2 100644 --- a/packages/eve/src/public/agents/vercel.test.ts +++ b/packages/eve/src/public/agents/vercel.test.ts @@ -7,6 +7,7 @@ vi.mock("#compiled/@vercel/oidc/index.js", () => ({ import { getVercelOidcToken } from "#compiled/@vercel/oidc/index.js"; import { VERCEL_TRUSTED_OIDC_IDP_TOKEN_HEADER } from "#client/types.js"; +import { normalizeChannelDirectedRemote } from "#execution/channel-directed-remote.js"; import { defineVercelBranchAgent } from "#public/agents/vercel.js"; describe("defineVercelBranchAgent", () => { @@ -34,6 +35,9 @@ describe("defineVercelBranchAgent", () => { [VERCEL_TRUSTED_OIDC_IDP_TOKEN_HEADER]: "oidc-token", }, }); + await expect(normalizeChannelDirectedRemote(agent)).resolves.toMatchObject({ + credentialsStepId: "eve:vercel-branch-agent//credentials", + }); }); it("requires a branch", () => { diff --git a/packages/eve/src/public/agents/vercel.ts b/packages/eve/src/public/agents/vercel.ts index 8657b615e..9d4d3f95d 100644 --- a/packages/eve/src/public/agents/vercel.ts +++ b/packages/eve/src/public/agents/vercel.ts @@ -32,14 +32,17 @@ export function defineVercelBranchAgent(input: VercelBranchAgentInput): RemoteAg const branch = rawBranch.trim(); if (!branch) throw new Error("defineVercelBranchAgent requires a non-empty branch."); - return defineRemoteAgent({ - ...remote, - auth: vercelDeploymentOidc(), + const agent = Object.assign(defineRemoteAgent(remote), { auth: vercelDeploymentOidc }); + Object.defineProperty(agent, "__eveResolveRemoteAgentCredentials", { + value: vercelDeploymentOidc, }); + return agent; } -function vercelDeploymentOidc(): OutboundAuthFn { - return async () => { +const VERCEL_BRANCH_AGENT_CREDENTIALS_STEP_ID = "eve:vercel-branch-agent//credentials"; + +const vercelDeploymentOidc = Object.assign( + async () => { const token = await getVercelOidcToken(); return { headers: { @@ -47,5 +50,12 @@ function vercelDeploymentOidc(): OutboundAuthFn { [VERCEL_TRUSTED_OIDC_IDP_TOKEN_HEADER]: token, }, }; - }; -} + }, + { stepId: VERCEL_BRANCH_AGENT_CREDENTIALS_STEP_ID }, +) satisfies OutboundAuthFn & { readonly stepId: string }; + +const stepRegistryKey = Symbol.for("@workflow/core//registeredSteps"); +const globalRegistry = globalThis as Record | undefined>; +const stepRegistry = globalRegistry[stepRegistryKey] ?? new Map(); +globalRegistry[stepRegistryKey] = stepRegistry; +stepRegistry.set(VERCEL_BRANCH_AGENT_CREDENTIALS_STEP_ID, vercelDeploymentOidc); diff --git a/packages/eve/src/public/channels/slack/index.ts b/packages/eve/src/public/channels/slack/index.ts index 3d227dbfc..ceb16fc8c 100644 --- a/packages/eve/src/public/channels/slack/index.ts +++ b/packages/eve/src/public/channels/slack/index.ts @@ -67,6 +67,12 @@ export { export { defaultSlackAuth } from "#public/channels/slack/defaults.js"; +export { + slackUserGroupMentions, + withoutSlackUserGroupMention, + type SlackUserGroupMention, +} from "#public/channels/slack/user-groups.js"; + export { describeActionRequest, describeActionRequests, diff --git a/packages/eve/src/public/channels/slack/user-groups.test.ts b/packages/eve/src/public/channels/slack/user-groups.test.ts new file mode 100644 index 000000000..32f8a8a49 --- /dev/null +++ b/packages/eve/src/public/channels/slack/user-groups.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import { + slackUserGroupMentions, + withoutSlackUserGroupMention, +} from "#public/channels/slack/user-groups.js"; + +describe("slackUserGroupMentions", () => { + it("returns unique opaque group ids in mention order", () => { + expect( + slackUserGroupMentions("Ask then . "), + ).toEqual([{ id: "S123" }, { id: "S456" }]); + }); +}); + +describe("withoutSlackUserGroupMention", () => { + it("removes only the selected group mention", () => { + expect( + withoutSlackUserGroupMention(" check ", "S123"), + ).toBe("check "); + }); +}); diff --git a/packages/eve/src/public/channels/slack/user-groups.ts b/packages/eve/src/public/channels/slack/user-groups.ts new file mode 100644 index 000000000..690e6155e --- /dev/null +++ b/packages/eve/src/public/channels/slack/user-groups.ts @@ -0,0 +1,34 @@ +const USER_GROUP_MENTION = /]+)?>/gu; + +/** One Slack user-group mention found in message text. */ +export interface SlackUserGroupMention { + readonly id: string; +} + +/** + * Returns unique Slack user-group ids mentioned in text in first-mention + * order. The parser deliberately retains Slack's opaque id rather than a + * mutable display handle so channel-owned registries can verify ownership. + */ +export function slackUserGroupMentions(text: string): readonly SlackUserGroupMention[] { + const seen = new Set(); + const mentions: SlackUserGroupMention[] = []; + for (const match of text.matchAll(USER_GROUP_MENTION)) { + const id = match[1]; + if (id === undefined || seen.has(id)) continue; + seen.add(id); + mentions.push({ id }); + } + return mentions; +} + +/** + * Removes one recognized user-group mention using the same whitespace + * normalization callers use for an empty Slack app mention. + */ +export function withoutSlackUserGroupMention(text: string, userGroupId: string): string { + return text + .replace(USER_GROUP_MENTION, (mention, id: string) => (id === userGroupId ? "" : mention)) + .replace(/\s+/gu, " ") + .trim(); +}