mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
fix(chat): use the claimed Cloud origin for connector URLs (#13680)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Chat connectors publish callback URLs and links to the board. > - Cloud can assign a warm instance its final origin after the server starts. > - The signed runtime identity already tracks that change. > - The chat service kept a copy of the startup origin and continued to publish it. > - This pull request resolves the trusted origin when it creates each URL. > - New connector setup uses the claimed hostname without a server restart. ## Linked Issues or Issue Description **What happened?** Chat setup in a claimed warm instance used its old pool hostname in provider callbacks. Account confirmation and task links could also use the old hostname. **Expected behavior** Chat URLs follow the signed canonical origin after the claim. An explicit webhook ingress override still applies only to provider callbacks. Self-hosted URL precedence stays the same. **Steps to reproduce** 1. Construct the chat service with a pool origin. 2. Apply the Cloud claim without restarting the service. 3. Open Slack setup or create an account-linking intent. 4. Observe the startup hostname in the returned URL. **Paperclip version or commit** Reproduced on master at `9335b7db1`. **Deployment mode** Paperclip Cloud warm-instance claim. Related: #12766 introduced the signed canonical runtime identity. ## What Changed - Resolve the signed Cloud origin when building chat setup, account confirmation, and task URLs. - Use the same callback origin for Telegram registration and GitHub webhook recovery. - Preserve explicit webhook ingress and self-hosted configuration precedence. - Add regression coverage for existing and new endpoints across Slack, GitHub, Teams, and Telegram. - Document the origin precedence and the need to update callbacks already saved at a provider. ## Verification - Reproduced both new regression cases against the original code. - Full chat integration and signed Cloud identity suites: 1,015 tests passed after the production-code correction. - Five origin and ingress cases passed after review additions, including Telegram registration and GitHub webhook repair after a live claim. - Focused origin, ingress, callback, task-link safety, and tenant-isolation checks: 67 passed. - `pnpm -r typecheck` and `pnpm build` passed. Server typecheck and compilation passed again after the task-link validation correction. - A broad local `pnpm test:run` started before the correction was stopped after the final-commit CI suite passed. It is not counted as a passing local run. - Final-commit CI: 54 successful checks; two optional Storybook checks skipped. Greptile: 5/5 with all review threads resolved. - No live deployment or Slack app mutation was performed. ## Risks - Cloud chat URLs now follow the signed runtime identity. Request host headers cannot set this value. - An explicit webhook ingress override still takes precedence for callbacks. - Existing Slack app settings are external state. Operators must replace an old callback URL in Slack. - This change does not deploy the app or change gateway ingress policy. No schema migration is required. ## Model Used OpenAI Codex (GPT-6), with repository search, code execution, and automated tests. The exact model ID and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c1f6c3310a
commit
20d26117c9
3 files changed
+153
-26
No files matched your search
@@ -47,6 +47,13 @@ only `POST /api/chat-webhooks/*` from that host. Provider signatures still gate
|
||||
ingress; this variable does not expose routes or grant provider access.
|
||||
Never forward the private `local_trusted` board through a public tunnel.
|
||||
|
||||
In Paperclip Cloud, chat callback URLs and account-linking URLs follow the
|
||||
instance's signed canonical origin after a warm instance is claimed, without
|
||||
requiring a restart. An explicit `PAPERCLIP_CHAT_WEBHOOK_PUBLIC_URL` still takes
|
||||
precedence for provider callbacks only; board links follow the claimed origin.
|
||||
Existing provider-side callback settings must be updated if they were created
|
||||
with an old URL.
|
||||
|
||||
Task links in external messages require an externally safe HTTPS board URL.
|
||||
Local/private board URLs are omitted with instructions to open the task in
|
||||
Paperclip; the public webhook host is never substituted for the board. Identity
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import * as cloudRuntimeIdentity from "../services/cloud-runtime-identity.js";
|
||||
import {
|
||||
createHash,
|
||||
createHmac,
|
||||
@@ -1765,6 +1766,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
| "githubWebhookReplayBarrier"
|
||||
| "githubWebhookResponseBudgetMs"
|
||||
| "publicBaseUrl"
|
||||
| "webhookPublicBaseUrl"
|
||||
| "scheduleDeferredWork"
|
||||
| "setupSecretActivityLogger"
|
||||
| "setupSecretCredentialPersistBarrier"
|
||||
@@ -12755,6 +12757,117 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
await service.shutdown();
|
||||
});
|
||||
|
||||
it.each([undefined, "https://ingress.example"])(
|
||||
"uses a live Cloud claim after chat service startup (ingress: %s)",
|
||||
async (webhookPublicBaseUrl) => {
|
||||
const poolOrigin = "https://pool-fixture.staging.paperclip.app";
|
||||
const claimedOrigin = "https://claimed-fixture.staging.paperclip.app";
|
||||
const canonicalOrigin = vi.spyOn(cloudRuntimeIdentity, "runtimeCanonicalOrigin").mockReturnValue(null);
|
||||
let context: ReturnType<typeof createService> | undefined;
|
||||
try {
|
||||
const fixture = await seedCompany();
|
||||
context = createService(new FakeChatSdkRuntime(), fakeSlackFetch(), {
|
||||
publicBaseUrl: poolOrigin,
|
||||
webhookPublicBaseUrl,
|
||||
});
|
||||
const { service } = context;
|
||||
const endpoints = [];
|
||||
for (const provider of ["slack", "github", "microsoft-teams", "telegram"] as const) {
|
||||
endpoints.push(await service.create(fixture.companyId, {
|
||||
provider,
|
||||
assignedAgentId: fixture.assignedAgentId,
|
||||
}, "owner-user"));
|
||||
}
|
||||
expect(endpoints[0].setup.webhookUrl).toBe(
|
||||
`${webhookPublicBaseUrl ?? poolOrigin}/api/chat-webhooks/${endpoints[0].publicId}/slack`,
|
||||
);
|
||||
|
||||
// The warm process is already serving; applying a signed claim changes
|
||||
// the trusted identity provider without constructing another service.
|
||||
canonicalOrigin.mockReturnValue(claimedOrigin);
|
||||
for (const endpoint of endpoints) {
|
||||
const updated = await service.get(endpoint.id);
|
||||
const callback = `${webhookPublicBaseUrl ?? claimedOrigin}/api/chat-webhooks/${endpoint.publicId}/${endpoint.provider}`;
|
||||
expect(updated.setup).toMatchObject(endpoint.provider === "microsoft-teams"
|
||||
? { messagingEndpoint: callback }
|
||||
: { webhookUrl: callback });
|
||||
const [principal] = await db.insert(chatExternalPrincipals).values({
|
||||
companyId: fixture.companyId,
|
||||
provider: endpoint.provider,
|
||||
providerAccountId: "",
|
||||
externalId: randomUUID(),
|
||||
kind: "user",
|
||||
isBot: false,
|
||||
}).returning();
|
||||
const intent = await service.createLinkIntent(endpoint.id, principal.id, 1800);
|
||||
expect(intent.confirmationUrl).toMatch(
|
||||
/^https:\/\/claimed-fixture\.staging\.paperclip\.app\/chat-identity\/confirm\?token=/,
|
||||
);
|
||||
}
|
||||
const fresh = await service.create(fixture.companyId, {
|
||||
provider: "slack",
|
||||
assignedAgentId: fixture.assignedAgentId,
|
||||
}, "owner-user");
|
||||
expect(fresh.setup.webhookUrl).toBe(
|
||||
`${webhookPublicBaseUrl ?? claimedOrigin}/api/chat-webhooks/${fresh.publicId}/slack`,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await context?.service.shutdown();
|
||||
} finally {
|
||||
canonicalOrigin.mockRestore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([undefined, "https://ingress.example"])(
|
||||
"registers provider callbacks after a live Cloud claim (ingress: %s)",
|
||||
async (webhookPublicBaseUrl) => {
|
||||
const poolOrigin = "https://pool-fixture.staging.paperclip.app";
|
||||
const claimedOrigin = "https://claimed-fixture.staging.paperclip.app";
|
||||
const canonicalOrigin = vi.spyOn(cloudRuntimeIdentity, "runtimeCanonicalOrigin").mockReturnValue(null);
|
||||
let telegram: ReturnType<typeof createService> | undefined;
|
||||
let github: Awaited<ReturnType<typeof configuredGitHubEndpoint>> | undefined;
|
||||
try {
|
||||
const fixture = await seedCompany();
|
||||
const telegramRequests: Array<Record<string, unknown>> = [];
|
||||
const telegramFetch = fakeTelegramFetch();
|
||||
telegram = createService(new FakeChatSdkRuntime(), (async (input, init) => {
|
||||
if (String(input).endsWith("/setWebhook")) {
|
||||
telegramRequests.push(JSON.parse(String(init?.body)));
|
||||
}
|
||||
return telegramFetch(input);
|
||||
}) as typeof globalThis.fetch, { publicBaseUrl: poolOrigin, webhookPublicBaseUrl });
|
||||
const endpoint = await telegram.service.create(fixture.companyId, {
|
||||
provider: "telegram",
|
||||
assignedAgentId: fixture.assignedAgentId,
|
||||
}, "owner-user");
|
||||
github = await configuredGitHubEndpoint(fixture, { publicBaseUrl: poolOrigin, webhookPublicBaseUrl });
|
||||
expect(github.webhookSyncRequests).toHaveLength(0);
|
||||
|
||||
canonicalOrigin.mockReturnValue(claimedOrigin);
|
||||
await telegram.service.configure(endpoint.id, {
|
||||
action: "configure",
|
||||
credentials: { botToken: "123456:telegram-cloud-claim-test" },
|
||||
}, "owner-user");
|
||||
expect(telegramRequests).toEqual([expect.objectContaining({
|
||||
url: `${webhookPublicBaseUrl ?? claimedOrigin}/api/chat-webhooks/${endpoint.publicId}/telegram`,
|
||||
})]);
|
||||
await github.service.configure(github.endpoint.id, { action: "reconnect" }, "owner-user");
|
||||
expect(github.webhookSyncRequests).toEqual([expect.objectContaining({
|
||||
url: `${webhookPublicBaseUrl ?? claimedOrigin}/api/chat-webhooks/${github.endpoint.publicId}/github`,
|
||||
})]);
|
||||
} finally {
|
||||
try {
|
||||
await Promise.all([telegram?.service.shutdown(), github?.service.shutdown()]);
|
||||
} finally {
|
||||
canonicalOrigin.mockRestore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("separates verified webhook ingress from board identity links for every webhook provider", async () => {
|
||||
const fixture = await seedCompany();
|
||||
for (const publicBaseUrl of [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { runtimeCanonicalOrigin } from "./cloud-runtime-identity.js";
|
||||
import { takePhotonCompanion } from "./photon/attachments.js";
|
||||
import { writePhotonCheckpoint } from "./photon/receiver.js";
|
||||
import { PhotonState } from "./photon/state.js";
|
||||
@@ -2979,10 +2980,16 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
>();
|
||||
const persistence = createChatSdkStatePersistence(db);
|
||||
const fetchImpl = options.fetch ?? globalThis.fetch;
|
||||
const publicBaseUrl = absoluteBaseUrl(options.publicBaseUrl);
|
||||
const webhookPublicBaseUrl =
|
||||
parseChatWebhookPublicBaseUrl(options.webhookPublicBaseUrl) ??
|
||||
publicBaseUrl;
|
||||
const configuredPublicBaseUrl = absoluteBaseUrl(options.publicBaseUrl);
|
||||
const configuredWebhookPublicBaseUrl = parseChatWebhookPublicBaseUrl(options.webhookPublicBaseUrl);
|
||||
// A warm Cloud instance is constructed before it receives its signed claim.
|
||||
// Resolve its live identity when producing URLs, not once at service startup.
|
||||
// An explicit webhook ingress remains separate from board/identity links.
|
||||
const getPublicBaseUrl = () => runtimeCanonicalOrigin() ?? configuredPublicBaseUrl;
|
||||
// Task links must validate the original configured URL before normalization
|
||||
// can remove credentials or other evidence that makes it unsafe to publish.
|
||||
const getTaskBaseUrl = () => runtimeCanonicalOrigin() ?? options.publicBaseUrl;
|
||||
const getWebhookPublicBaseUrl = () => configuredWebhookPublicBaseUrl ?? getPublicBaseUrl();
|
||||
const issuesSvc = issueService(db);
|
||||
const secrets = secretService(db);
|
||||
const questionResponses = questionResponseDeliveryService(db, {
|
||||
@@ -5829,7 +5836,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
setup: {
|
||||
...providerSetupState(
|
||||
endpoint,
|
||||
webhookPublicBaseUrl,
|
||||
getWebhookPublicBaseUrl(),
|
||||
row.assignedAgentName,
|
||||
),
|
||||
...(endpoint.provider === "github"
|
||||
@@ -9460,7 +9467,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
) {
|
||||
throw unprocessable("Unsupported chat endpoint setup action");
|
||||
}
|
||||
if (!webhookPublicBaseUrl && endpoint.provider !== "discord" && endpoint.provider !== "imessage-photon") {
|
||||
if (!getWebhookPublicBaseUrl() && endpoint.provider !== "discord" && endpoint.provider !== "imessage-photon") {
|
||||
throw unprocessable(
|
||||
`A public HTTPS Paperclip URL is required before connecting ${PROVIDER_LABELS[endpoint.provider]}`,
|
||||
);
|
||||
@@ -9468,7 +9475,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
if (
|
||||
endpoint.provider === "telegram" &&
|
||||
(input.action === "configure" || input.action === "reconnect") &&
|
||||
!isSupportedTelegramWebhookBaseUrl(webhookPublicBaseUrl)
|
||||
!isSupportedTelegramWebhookBaseUrl(getWebhookPublicBaseUrl())
|
||||
) {
|
||||
throw unprocessable(
|
||||
"Telegram webhooks require PAPERCLIP_CHAT_WEBHOOK_PUBLIC_URL to use HTTPS on port 443, 80, 88, or 8443",
|
||||
@@ -9774,7 +9781,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
await resyncGitHubAppWebhook({
|
||||
fetch: fetchImpl,
|
||||
appToken: githubAppJwt(credentials.appId, credentials.privateKey),
|
||||
webhookUrl: `${webhookPublicBaseUrl}/api/chat-webhooks/${endpoint.publicId}/github`,
|
||||
webhookUrl: `${getWebhookPublicBaseUrl()}/api/chat-webhooks/${endpoint.publicId}/github`,
|
||||
webhookSecret: credentials.webhookSecret,
|
||||
});
|
||||
await auditWebhookSync("chat_endpoint.webhook_synced");
|
||||
@@ -9786,8 +9793,8 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
waitForDiscordOwnership: next.endpoint.provider === "discord",
|
||||
});
|
||||
|
||||
if (endpoint.provider === "telegram" && webhookPublicBaseUrl) {
|
||||
const webhookUrl = `${webhookPublicBaseUrl}/api/chat-webhooks/${endpoint.publicId}/telegram`;
|
||||
if (endpoint.provider === "telegram" && getWebhookPublicBaseUrl()) {
|
||||
const webhookUrl = `${getWebhookPublicBaseUrl()}/api/chat-webhooks/${endpoint.publicId}/telegram`;
|
||||
const infoResponse = await fetchImpl(
|
||||
`https://api.telegram.org/bot${encodeURIComponent(credentials.botToken)}/getWebhookInfo`,
|
||||
{ signal: AbortSignal.timeout(PROVIDER_CREDENTIAL_CHECK_TIMEOUT_MS) },
|
||||
@@ -23290,8 +23297,8 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
await tx.update(chatActions).set({ payload: {
|
||||
version: 1, channelId: event.event.channel.id, userId: event.event.user.userId, identityLinkHash: tokenHash,
|
||||
} }).where(eq(chatActions.id, inserted[0].id));
|
||||
const url = `${publicBaseUrl}/chat-identity/confirm?token=${encodeURIComponent(token)}`;
|
||||
notice = publicBaseUrl
|
||||
const url = `${getPublicBaseUrl()}/chat-identity/confirm?token=${encodeURIComponent(token)}`;
|
||||
notice = getPublicBaseUrl()
|
||||
? `[Connect your Paperclip account](${url}) — sign in and confirm this Slack identity. This private link expires in 15 minutes and works once. You can also confirm in the setup wizard. No agent work has started.`
|
||||
: "Return to the Paperclip setup wizard to confirm your Slack account. No agent work has started.";
|
||||
}
|
||||
@@ -24737,7 +24744,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
fence.generation !== context.generation ||
|
||||
fence.credentialFingerprint !== context.credentialFingerprint ||
|
||||
fence.webhookUrl !==
|
||||
`${webhookPublicBaseUrl}/api/chat-webhooks/${currentEndpoint.publicId}/github` ||
|
||||
`${getWebhookPublicBaseUrl()}/api/chat-webhooks/${currentEndpoint.publicId}/github` ||
|
||||
!original?.payload?.comment ||
|
||||
original.event !== eventType ||
|
||||
incoming?.action !== "created" ||
|
||||
@@ -25409,7 +25416,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
limit = 5,
|
||||
onlyEndpointId?: string,
|
||||
) {
|
||||
if (shuttingDown || !webhookPublicBaseUrl?.startsWith("https://")) return 0;
|
||||
if (shuttingDown || !getWebhookPublicBaseUrl()?.startsWith("https://")) return 0;
|
||||
const now = new Date();
|
||||
const rows = await db
|
||||
.select({ endpoint: chatEndpoints })
|
||||
@@ -25442,7 +25449,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
// the superseded epoch was rate limited for several hours.
|
||||
sql`${chatSdkState.value}->>'generation' is distinct from coalesce(${chatEndpoints.setup}->>'runtimeGeneration', '0')`,
|
||||
sql`${chatSdkState.value}->>'appId' is distinct from ${chatEndpoints.botExternalId}`,
|
||||
sql`${chatSdkState.value}->>'webhookUrl' is distinct from (${webhookPublicBaseUrl} || '/api/chat-webhooks/' || ${chatEndpoints.publicId} || '/github')`,
|
||||
sql`${chatSdkState.value}->>'webhookUrl' is distinct from (${getWebhookPublicBaseUrl()} || '/api/chat-webhooks/' || ${chatEndpoints.publicId} || '/github')`,
|
||||
sql`(${chatSdkState.value}->>'nextScanAt')::timestamptz <= ${now.toISOString()}::timestamptz`,
|
||||
),
|
||||
),
|
||||
@@ -25453,7 +25460,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
const record = await endpointRecord(endpoint.id);
|
||||
if (!record || !record.endpoint.botExternalId) continue;
|
||||
const context = runtimeContextForRecord(record);
|
||||
const webhookUrl = `${webhookPublicBaseUrl}/api/chat-webhooks/${endpoint.publicId}/github`;
|
||||
const webhookUrl = `${getWebhookPublicBaseUrl()}/api/chat-webhooks/${endpoint.publicId}/github`;
|
||||
const scope = { companyId: endpoint.companyId, endpointId: endpoint.id };
|
||||
const stored = await persistence.read(scope, GITHUB_RECOVERY_STATE_KEY);
|
||||
const previous = githubRecoveryWindow(stored?.value);
|
||||
@@ -26388,7 +26395,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
errorCode: "slack_session_stopped",
|
||||
issueId: issue.id,
|
||||
milestone: "failed",
|
||||
publicBaseUrl,
|
||||
publicBaseUrl: getPublicBaseUrl(),
|
||||
}),
|
||||
}),
|
||||
principalId: principal.id,
|
||||
@@ -26506,7 +26513,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
},
|
||||
},
|
||||
);
|
||||
await enqueueChatRunMilestones(db, { publicBaseUrl });
|
||||
await enqueueChatRunMilestones(db, { publicBaseUrl: getPublicBaseUrl() });
|
||||
const authoritativeRun = await db
|
||||
.select({ status: heartbeatRuns.status })
|
||||
.from(heartbeatRuns)
|
||||
@@ -27944,7 +27951,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
});
|
||||
const path = `/chat-identity/confirm?token=${encodeURIComponent(token)}`;
|
||||
return {
|
||||
confirmationUrl: publicBaseUrl ? `${publicBaseUrl}${path}` : path,
|
||||
confirmationUrl: getPublicBaseUrl() ? `${getPublicBaseUrl()}${path}` : path,
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -32213,12 +32220,12 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
if (
|
||||
endpoint.provider !== "telegram" ||
|
||||
!endpoint.botExternalId ||
|
||||
!webhookPublicBaseUrl
|
||||
!getWebhookPublicBaseUrl()
|
||||
)
|
||||
return null;
|
||||
const webhookUrlSha256 = createHash("sha256")
|
||||
.update(
|
||||
`${webhookPublicBaseUrl}/api/chat-webhooks/${endpoint.publicId}/telegram`,
|
||||
`${getWebhookPublicBaseUrl()}/api/chat-webhooks/${endpoint.publicId}/telegram`,
|
||||
)
|
||||
.digest("hex");
|
||||
return {
|
||||
@@ -32468,7 +32475,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
String(identity.id) !== scope.botUserId
|
||||
)
|
||||
throw reject();
|
||||
const url = `${webhookPublicBaseUrl}/api/chat-webhooks/${endpoint.publicId}/telegram`;
|
||||
const url = `${getWebhookPublicBaseUrl()}/api/chat-webhooks/${endpoint.publicId}/telegram`;
|
||||
const plan = telegramStopSubscriptionPlan(
|
||||
await request("getWebhookInfo"),
|
||||
url,
|
||||
@@ -32947,7 +32954,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
}
|
||||
} else if (files.length === 0) {
|
||||
const taskUrl = safeChatTaskUrl(
|
||||
options.publicBaseUrl,
|
||||
getTaskBaseUrl(),
|
||||
input.publication.issueId,
|
||||
);
|
||||
if (
|
||||
@@ -33105,7 +33112,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
interaction,
|
||||
questionIndex: nextQuestion ? Number(nextQuestion[2]) : 0,
|
||||
taskUrl: safeChatTaskUrl(
|
||||
options.publicBaseUrl,
|
||||
getTaskBaseUrl(),
|
||||
input.publication.issueId,
|
||||
),
|
||||
assertCurrent: promptGuard,
|
||||
@@ -33393,7 +33400,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
? typeof prepared.payload.taskUrl === "string"
|
||||
? safeChatTaskUrl(prepared.payload.taskUrl, publication.issueId)
|
||||
: null
|
||||
: safeChatTaskUrl(options.publicBaseUrl, publication.issueId);
|
||||
: safeChatTaskUrl(getTaskBaseUrl(), publication.issueId);
|
||||
if (
|
||||
prepared &&
|
||||
(prepared.kind !== "github_omission_navigation" ||
|
||||
@@ -35245,7 +35252,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
noticeEndpoint.provider === attachmentFailure.provider);
|
||||
if (providerCanSendTextNotice && noticeConversation) {
|
||||
const taskUrl = safeChatTaskUrl(
|
||||
options.publicBaseUrl,
|
||||
getTaskBaseUrl(),
|
||||
publication.issueId,
|
||||
);
|
||||
const noticeText =
|
||||
|
||||
Reference in new issue
Block a user