From 178619d43b4fc4e26907dbc374c1b7ffbd064006 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 6 Jul 2026 19:01:21 +0700 Subject: [PATCH 01/53] fix(integrations): send a single notification when a token refresh fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a token refresh fails, refreshProcess sent the refresh-error notification (with the failure cause) and then called disconnectChannel, which sends the same notification again without the cause. Every failed refresh therefore emailed the user twice: once titled "Could not refresh your channel " and once "Could not refresh your channel". Drop the disconnectChannel call from the failure branch: the refreshNeeded call two lines earlier already sets the same flag disconnectChannel would, so the only thing it added was the duplicate email. disconnectChannel itself is unchanged for its other callers. Reproduced by triggering a post on an Instagram channel holding an invalid token (refresh fails since the provider cannot refresh) — two emails arrived for the single failure. After the fix, the same scenario produces exactly one email (the one including the failure cause), and the channel is still flagged as needing reconnection. Co-Authored-By: Claude Fable 5 --- .../src/integrations/refresh.integration.service.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts b/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts index 0689bdc603..bf59c32bb1 100644 --- a/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts +++ b/libraries/nestjs-libraries/src/integrations/refresh.integration.service.ts @@ -78,6 +78,10 @@ export class RefreshIntegrationService { .catch((err) => false); if (!refresh || !refresh.accessToken) { + // informAboutRefreshError (with the failure cause) already notifies + // the user, and refreshNeeded sets the same flag disconnectChannel + // would — calling disconnectChannel here sent a second, cause-less + // copy of the same email for every failed refresh. await this._integrationService.refreshNeeded( integration.organizationId, integration.id @@ -89,11 +93,6 @@ export class RefreshIntegrationService { cause ); - await this._integrationService.disconnectChannel( - integration.organizationId, - integration - ); - return false; } From 2e807934c5c0349e0af5f4fc6935e6ea3a2349ec Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Fri, 7 Aug 2026 10:16:35 +0700 Subject: [PATCH 02/53] feat(instagram): use custom thumbnail as reel cover via cover_url When a single-video reel's media has a thumbnail set, send it as cover_url on the media container so Instagram uses it as the reel cover; media without a thumbnail keep publishing via thumb_offset exactly as before. Stories, carousel items and images are untouched. E2e-tested on a Facebook-Login Instagram channel: reels published with both JPEG and PNG covers, cover confirmed on the profile reels grid (PNG accepted despite Meta docs listing JPEG only). Standalone Instagram-Login shares this postPending, not separately e2e-tested. Co-Authored-By: Claude Fable 5 --- .../src/integrations/social/instagram.provider.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 1ab4c09155..ec33dc2878 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -670,6 +670,10 @@ export class InstagramProvider ? firstPost?.media?.length === 1 ? isStory ? `video_url=${m.path}&media_type=STORIES` + : m?.thumbnail + ? `video_url=${m.path}&media_type=REELS&cover_url=${encodeURIComponent( + m.thumbnail + )}` : `video_url=${m.path}&media_type=REELS&thumb_offset=${ m?.thumbnailTimestamp || 0 }` From 4f469c7cc8571a98745548bce87d696d9600f977 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 12 Aug 2026 13:52:55 +0700 Subject: [PATCH 03/53] fix(providers): wire the LinkedIn and Moltbook settings DTOs validatePosts runs settings-DTO validation only when the provider declares `dto`. LinkedinProvider and MoltbookProvider never did, so for linkedin, linkedin-page (inherits from LinkedinProvider) and moltbook that validation layer silently did not run: LinkedinDto and MoltbookDto existed but were unwired, and any malformed or missing settings value was accepted and stored. The dashboard never hits this (its form validates client-side with the same DTO classes), but the public API (POST /public/v1/posts, PUT /public/v1/posts/:id/settings) and the agent/MCP tools share validatePosts and were unprotected - an API caller's typo or an LLM-guessed settings value was accepted with a 200. The cost surfaced later instead: a moltbook post without submolt silently publishes to the "general" community (provider falls back to it), and a truthy non-boolean carousel value on LinkedIn fails at publish time on a post the user was told was valid. Declare `dto = LinkedinDto` on LinkedinProvider (LinkedinPageProvider inherits it) and `dto = MoltbookDto` on MoltbookProvider, so bad values are rejected at save time with a specific message (400 / {errors}). Audited all providers: these were the only unwired settings DTOs. --- .../src/integrations/social/linkedin.provider.ts | 1 + .../src/integrations/social/moltbook.provider.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 958605a50f..72dda344c0 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -58,6 +58,7 @@ type LinkedinPendingData = { export class LinkedinProvider extends SocialAbstract implements SocialProvider { identifier = 'linkedin'; name = 'LinkedIn'; + dto = LinkedinDto; oneTimeToken = true; isBetweenSteps = false; diff --git a/libraries/nestjs-libraries/src/integrations/social/moltbook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/moltbook.provider.ts index 2bc8db4e30..e4d7522ec0 100644 --- a/libraries/nestjs-libraries/src/integrations/social/moltbook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/moltbook.provider.ts @@ -6,6 +6,7 @@ import { } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import { MoltbookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/moltbook.dto'; import dayjs from 'dayjs'; import { Integration } from '@prisma/client'; @@ -15,6 +16,7 @@ export class MoltbookProvider extends SocialAbstract implements SocialProvider { override maxConcurrentJob = 100; // Moltbook: 100 requests/minute identifier = 'moltbook'; name = 'Moltbook'; + dto = MoltbookDto; isBetweenSteps = false; scopes = [] as string[]; isWeb3 = true; From e5f6fff3384386a6e53eb75ae5ecbbf6afb231db Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 19 Aug 2026 17:21:22 +0700 Subject: [PATCH 04/53] feat(mcp): accept { path, thumbnail } attachments for video covers integrationSchedulePostTool attachments now accept either a plain URL string or { path, thumbnail }, with thumbnail forwarded as the media thumbnail (e.g. Instagram Reel cover_url). The public API already accepted image[].thumbnail; this brings MCP to parity. E2e-verified: MCP call with { path, thumbnail } and the equivalent public-API request both published Reels with the custom cover. Co-Authored-By: Claude Fable 5 --- .../chat/tools/integration.schedule.post.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts index 19433a7fae..3eb5aa32a7 100644 --- a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts +++ b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts @@ -27,6 +27,19 @@ const attachmentUrl = z message: validUrlExtension.defaultMessage({} as any), }); +// Attachments are either a plain URL string or { path, thumbnail? } where +// thumbnail is an image URL (jpeg/png) used as the video's thumbnail / cover. +const attachment = z.union([ + attachmentUrl, + z.object({ + path: attachmentUrl, + thumbnail: attachmentUrl.optional(), + }), +]); + +const toMedia = (a: string | { path: string; thumbnail?: string }) => + typeof a === 'string' ? { path: a } : { path: a.path, thumbnail: a.thumbnail }; + @Injectable() export class IntegrationSchedulePostTool implements AgentToolInterface { constructor( @@ -93,8 +106,10 @@ If the tools return errors, you would need to rerun it with the right parameters "The content of the post, HTML, Each line must be wrapped in

here is the possible tags: h1, h2, h3, u, strong, li, ul, p (you can't have u and strong together)" ), attachments: z - .array(attachmentUrl) - .describe('The image of the post (URLS)'), + .array(attachment) + .describe( + 'The media of the post: either an uploaded media URL string, or { path, thumbnail } where path is the uploaded video URL and thumbnail is the path of an uploaded jpeg/png (returned by uploadFromUrlTool) used as the video thumbnail / cover (e.g. Instagram Reel cover)' + ), }) ) .describe( @@ -163,9 +178,7 @@ If the tools return errors, you would need to rerun it with the right parameters settings, value: platform.postsAndComments.map((p: any) => ({ content: p.content, - image: (p.attachments || []).map((path: string) => ({ - path, - })), + image: (p.attachments || []).map(toMedia), })), }, ] @@ -229,9 +242,9 @@ If the tools return errors, you would need to rerun it with the right parameters content: p.content, id: makeId(10), delay: 0, - image: p.attachments.map((p: any) => ({ + image: p.attachments.map((a: any) => ({ id: makeId(10), - path: p, + ...toMedia(a), })), })), }, From 78515f71cf9d6a58a93a6a862d95837cd4f35247 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Thu, 17 Sep 2026 21:45:49 +0700 Subject: [PATCH 05/53] fix(uploads): honor HTTP Range requests so local-storage video uploads work again The local uploads route ignored the Range header and answered every request with 200 and the whole file. TikTok, YouTube, LinkedIn and X now read video chunks with ranged GETs and reject anything but 206, so every video upload on STORAGE_PROVIDER=local failed with "did not return the requested byte range". Serve 206 + Content-Range from a ranged createReadStream, advertise Accept-Ranges, and return 416 for out-of-bounds ranges. Plain GETs are unchanged. Co-Authored-By: Claude Fable 5.1 --- .../(app)/api/uploads/[[...path]]/route.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts b/apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts index 7dc3d678e5..383c3211df 100644 --- a/apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts +++ b/apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts @@ -36,20 +36,43 @@ export const GET = async ( if (filePath !== base && !filePath.startsWith(base + sep)) { return new NextResponse('Not found', { status: 404 }); } - const response = createReadStream(filePath); const fileStats = statSync(filePath); const contentType = mime.getType(filePath) || 'application/octet-stream'; + + // Honor ranged requests: providers that push video in chunks (TikTok, + // YouTube, LinkedIn, X) fetch byte windows with a Range header and reject + // anything but a 206, so ignoring Range breaks their uploads. + const range = /^bytes=(\d+)-(\d*)$/.exec(request.headers.get('range') || ''); + const start = range ? Number(range[1]) : 0; + const end = range && range[2] ? Number(range[2]) : fileStats.size - 1; + + if ( + range && + (start >= fileStats.size || end >= fileStats.size || start > end) + ) { + return new NextResponse(null, { + status: 416, + headers: { 'Content-Range': `bytes */${fileStats.size}` }, + }); + } + + const response = createReadStream(filePath, range ? { start, end } : {}); const iterator = nodeStreamToIterator(response); const webStream = iteratorToStream(iterator); return new Response(webStream, { + status: range ? 206 : 200, headers: { 'Content-Type': contentType, // Set the appropriate content-type header - 'Content-Length': fileStats.size.toString(), + 'Content-Length': (end - start + 1).toString(), // Set the content-length header 'Last-Modified': fileStats.mtime.toUTCString(), // Set the last-modified header 'Cache-Control': 'public, max-age=31536000, immutable', // Example cache-control header + 'Accept-Ranges': 'bytes', + ...(range + ? { 'Content-Range': `bytes ${start}-${end}/${fileStats.size}` } + : {}), }, }); }; From c38f8edbdb03b3c68f2330ebf8c6ce723d5fc9e9 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Fri, 18 Sep 2026 16:47:29 +0700 Subject: [PATCH 06/53] chore(deps): pin the Sentry SDK to 10.56.0 to pick up the span/scope leak fix The installed SDK (10.45.0) has a memory leak in @sentry/core: spans and scopes reference each other circularly and cannot be garbage collected, and the span exporter keeps sent spans in an unbounded map. Upstream tracked it as "Memory leak with high tracesSampleRate" (getsentry/sentry-javascript#18339) and fixed it in 10.56.0 (getsentry/sentry-javascript#21242). 10.56.0 also carries the 10.49.0 fix for contexts retained through scopes on pooled connections (getsentry/sentry-javascript#20328). This matches what we saw in production: the MCP service climbed to the V8 heap limit about once a day while trace sampling was 100%, and the climb stopped when #2082 cut sampling to 20%. Sampling less only slows the leak; this fixes it at the source, for the backend, the orchestrator and the frontend server alike. Pinned exactly, not with a caret, on purpose. 10.56.0 is the first release with the fix, and later minors change behaviour we have not evaluated: 10.61.0 streams gen_ai spans by default and stops truncating AI message data, 10.71.0 enables logs by default, 10.72.0 stops reporting AI errors that propagate to the caller. The lockfile was regenerated from the committed one with only these four specifiers changed; the rest of the diff is Sentry's own dependency tree (several OpenTelemetry instrumentations are now vendored and drop out). @sentry/webpack-plugin keeps its own build-time @sentry/core copy. Behaviour changes that do ride along (10.46.0 to 10.56.0): OpenAI span attributes are renamed from openai.* to gen_ai.* (10.48.0), the NestJS instrumentation is vendored inside the SDK (10.54.0), array attribute values are sent as arrays (10.54.0). sendDefaultPii, which the frontend init uses, is deprecated in 10.57.0 and is untouched here. Testing on 10.56.0, against a Sentry development environment unless noted: - backend, orchestrator and frontend type-check; backend and orchestrator boot; production `next build` passes, also when the source-map upload fails (the next.config error handler absorbs it) - backend: HTTP transactions, console logs, an unhandled controller 500 through the Nest global filter with the organization tag, a process level crash, Sentry.metrics counters and profiler ids on spans - frontend: client init with every configured integration, user and organization tag, an uncaught browser error linked to its session replay, the crash report dialog, the feedback form, pageload spans with browser profiles - OpenAI integration against a local stub: the wrapped client works for chat.completions.create and .parse; create emits a gen_ai.chat span; .parse and images.generate are not instrumented on 10.45.0 either - MCP flows (initialize, tools/list, tools/call) unchanged Not tested: a successful source-map upload to the real project. Co-Authored-By: Claude Fable 5.1 --- package.json | 8 +- pnpm-lock.yaml | 1205 +++++++++++++++++++----------------------------- 2 files changed, 482 insertions(+), 731 deletions(-) diff --git a/package.json b/package.json index 5c87482e31..e4830c4540 100644 --- a/package.json +++ b/package.json @@ -82,10 +82,10 @@ "@pigment-css/react": "^0.0.30", "@postiz/wallets": "^0.0.1", "@prisma/client": "6.5.0", - "@sentry/nestjs": "^10.26.0", - "@sentry/nextjs": "^10.26.0", - "@sentry/profiling-node": "^10.25.0", - "@sentry/react": "^10.25.0", + "@sentry/nestjs": "10.56.0", + "@sentry/nextjs": "10.56.0", + "@sentry/profiling-node": "10.56.0", + "@sentry/react": "10.56.0", "@solana/wallet-adapter-react": "^0.15.35", "@solana/wallet-adapter-react-ui": "^0.9.35", "@stripe/react-stripe-js": "^5.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 195256bd8c..64aa6effa1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: dependencies: '@ag-ui/mastra': specifier: ^1.1.4 - version: 1.1.4(@ag-ui/client@0.0.59)(@ag-ui/core@0.0.59)(@copilotkit/runtime@1.72.0(060090e105863d9983aa04d300df3d3a))(@mastra/client-js@0.15.2(openapi-types@12.1.3)(react@19.2.4)(zod@3.25.76))(@mastra/core@1.67.0(@bufbuild/protobuf@2.11.0)(@grpc/grpc-js@1.14.3)(ai@4.3.19(react@19.2.4)(zod@3.25.76))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 1.1.4(@ag-ui/client@0.0.59)(@ag-ui/core@0.0.59)(@copilotkit/runtime@1.72.0(d8db588a33556e4390c2c1ec6b88e6cc))(@mastra/client-js@0.15.2(openapi-types@12.1.3)(react@19.2.4)(zod@3.25.76))(@mastra/core@1.67.0(@bufbuild/protobuf@2.11.0)(@grpc/grpc-js@1.14.3)(ai@4.3.19(react@19.2.4)(zod@3.25.76))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(utf-8-validate@5.0.10)(zod@3.25.76)) '@ai-sdk/openai': specifier: ^2.0.52 version: 2.0.98(zod@3.25.76) @@ -49,7 +49,7 @@ importers: version: 1.72.0(@ag-ui/core@0.0.59)(@cfworker/json-schema@4.1.1)(@types/mdast@4.0.4)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql@16.13.1)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) '@copilotkit/runtime': specifier: 1.72.0 - version: 1.72.0(060090e105863d9983aa04d300df3d3a) + version: 1.72.0(d8db588a33556e4390c2c1ec6b88e6cc) '@copilotkit/runtime-client-gql': specifier: 1.72.0 version: 1.72.0(@ag-ui/core@0.0.59)(graphql@16.13.1)(react@19.2.4) @@ -61,19 +61,19 @@ importers: version: 3.10.0(react-hook-form@7.71.2(react@19.2.4)) '@langchain/community': specifier: ^1.1.27 - version: 1.1.27(66046857593e81cf94589e5d63e1e984) + version: 1.1.27(cbe060cf4b5645a3ea17665610df96e8) '@langchain/core': specifier: ^1.1.39 - version: 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + version: 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@langchain/langgraph': specifier: ^1.2.8 - version: 1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76) + version: 1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76) '@langchain/openai': specifier: ^1.4.3 - version: 1.4.3(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + version: 1.4.3(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@langchain/tavily': specifier: ^1.2.0 - version: 1.2.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + version: 1.2.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@mantine/core': specifier: ^5.10.5 version: 5.10.5(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.2.4))(@mantine/hooks@5.10.5(react@19.2.4))(@types/react@19.1.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -144,17 +144,17 @@ importers: specifier: 6.5.0 version: 6.5.0(prisma@6.5.0(typescript@5.5.4))(typescript@5.5.4) '@sentry/nestjs': - specifier: ^10.26.0 - version: 10.45.0(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) + specifier: 10.56.0 + version: 10.56.0(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)) '@sentry/nextjs': - specifier: ^10.26.0 - version: 10.45.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.3.1(@babel/core@8.0.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(@types/node@18.16.9)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2)) + specifier: 10.56.0 + version: 10.56.0(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(next@16.3.1(@babel/core@8.0.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(@types/node@18.16.9)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2)) '@sentry/profiling-node': - specifier: ^10.25.0 - version: 10.45.0 + specifier: 10.56.0 + version: 10.56.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)) '@sentry/react': - specifier: ^10.25.0 - version: 10.45.0(react@19.2.4) + specifier: 10.56.0 + version: 10.56.0(react@19.2.4) '@solana/wallet-adapter-react': specifier: ^0.15.35 version: 0.15.39(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.5.4)(utf-8-validate@5.0.10))(bs58@6.0.0)(fastestsmallesttextencoderdecoder@1.0.22)(react-native@0.84.1(@babel/core@8.0.5)(@types/react@19.1.8)(bufferutil@4.1.0)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.5.4) @@ -3045,11 +3045,6 @@ packages: '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} - '@fastify/otel@0.17.1': - resolution: {integrity: sha512-K4wyxfUZx2ux5o+b6BtTqouYFVILohLZmSbA2tKUueJstNcBnoGPVhllCaOvbQ3ZrXdUxUC/fyrSWSCqHhdOPg==} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -5215,26 +5210,22 @@ packages: resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.207.0': - resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} - engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.208.0': resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.212.0': - resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api-logs@0.213.0': - resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==} + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + '@opentelemetry/auto-instrumentations-node@0.62.2': resolution: {integrity: sha512-Ipe6X7ddrCiRsuewyTU83IvKiSFT4piqmv9z8Ovg1E7v98pdTj1pUE6sDrHV50zl7/ypd+cONBgt+EYSZu4u9Q==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5260,6 +5251,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.11.0': + resolution: {integrity: sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.2.0': resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5350,12 +5347,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-amqplib@0.60.0': - resolution: {integrity: sha512-q/B2IvoVXRm1M00MvhnzpMN6rKYOszPXVsALi6u0ss4AYHe+TidZEtLW9N1ZhrobI1dSriHnBqqtAOZVAv07sg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-aws-lambda@0.54.1': resolution: {integrity: sha512-qm8pGSAM1mXk7unbrGktWWGJc6IFI58ZsaHJ+i420Fp5VO3Vf7GglIgaXTS8CKBrVB4LHFj3NvzJg31PtsAQcA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5386,12 +5377,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-connect@0.56.0': - resolution: {integrity: sha512-PKp+sSZ7AfzMvGgO3VCyo1inwNu+q7A1k9X88WK4PQ+S6Hp7eFk8pie+sWHDTaARovmqq5V2osav3lQej2B0nw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-cucumber@0.19.0': resolution: {integrity: sha512-99ms8kQWRuPt5lkDqbJJzD+7Tq5TMUlBZki4SA2h6CgK4ncX+tyep9XFY1e+XTBLJIWmuFMGbWqBLJ4fSKIQNQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5404,12 +5389,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-dataloader@0.30.0': - resolution: {integrity: sha512-MXHP2Q38cd2OhzEBKAIXUi9uBlPEYzF6BNJbyjUXBQ6kLaf93kRC41vNMIz0Nl5mnuwK7fDvKT+/lpx7BXRwdg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-dns@0.47.0': resolution: {integrity: sha512-775fOnewWkTF4iXMGKgwvOGqEmPrU1PZpXjjqvTrEErYBJe7Fz1WlEeUStHepyKOdld7Ghv7TOF/kE3QDctvrg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5422,12 +5401,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-express@0.61.0': - resolution: {integrity: sha512-Xdmqo9RZuZlL29Flg8QdwrrX7eW1CZ7wFQPKHyXljNymgKhN1MCsYuqQ/7uxavhSKwAl7WxkTzKhnqpUApLMvQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-fastify@0.48.0': resolution: {integrity: sha512-3zQlE/DoVfVH6/ycuTv7vtR/xib6WOa0aLFfslYcvE62z0htRu/ot8PV/zmMZfnzpTQj8S/4ULv36R6UIbpJIg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5440,36 +5413,18 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-fs@0.32.0': - resolution: {integrity: sha512-koR6apx0g0wX6RRiPpjA4AFQUQUbXrK16kq4/SZjVp7u5cffJhNkY4TnITxcGA4acGSPYAfx3NHRIv4Khn1axQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-generic-pool@0.47.0': resolution: {integrity: sha512-UfHqf3zYK+CwDwEtTjaD12uUqGGTswZ7ofLBEdQ4sEJp9GHSSJMQ2hT3pgBxyKADzUdoxQAv/7NqvL42ZI+Qbw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-generic-pool@0.56.0': - resolution: {integrity: sha512-fg+Jffs6fqrf0uQS0hom7qBFKsbtpBiBl8+Vkc63Gx8xh6pVh+FhagmiO6oM0m3vyb683t1lP7yGYq22SiDnqg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-graphql@0.51.0': resolution: {integrity: sha512-LchkOu9X5DrXAnPI1+Z06h/EH/zC7D6sA86hhPrk3evLlsJTz0grPrkL/yUJM9Ty0CL/y2HSvmWQCjbJEz/ADg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-graphql@0.61.0': - resolution: {integrity: sha512-pUiVASv6nh2XrerTvlbVHh7vKFzscpgwiQ/xvnZuAIzQ5lRjWVdRPUuXbvZJ/Yq79QsE81TZdJ7z9YsXiss1ew==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-grpc@0.203.0': resolution: {integrity: sha512-Qmjx2iwccHYRLoE4RFS46CvQE9JG9Pfeae4EPaNZjvIuJxb/pZa2R9VWzRlTehqQWpAvto/dGhtkw8Tv+o0LTg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5482,84 +5437,42 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-hapi@0.59.0': - resolution: {integrity: sha512-33wa4mEr+9+ztwdgLor1SeBu4Opz4IsmpcLETXAd3VmBrOjez8uQtrsOhPCa5Vhbm5gzDlMYTgFRLQzf8/YHFA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.203.0': resolution: {integrity: sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.213.0': - resolution: {integrity: sha512-B978Xsm5XEPGhm1P07grDoaOFLHapJPkOG9h016cJsyWWxmiLnPu2M/4Nrm7UCkHSiLnkXgC+zVGUAIahy8EEA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.51.0': resolution: {integrity: sha512-9IUws0XWCb80NovS+17eONXsw1ZJbHwYYMXiwsfR9TSurkLV5UNbRSKb9URHO+K+pIJILy9wCxvyiOneMr91Ig==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.61.0': - resolution: {integrity: sha512-hsHDadUtAFbws1YSDc1XW0svGFKiUbqv2td1Cby+UAiwvojm1NyBo/taifH0t8CuFZ0x/2SDm0iuTwrM5pnVOg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-kafkajs@0.13.0': resolution: {integrity: sha512-FPQyJsREOaGH64hcxlzTsIEQC4DYANgTwHjiB7z9lldmvua1LRMVn3/FfBlzXoqF179B0VGYviz6rn75E9wsDw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-kafkajs@0.22.0': - resolution: {integrity: sha512-wJU4IBQMUikdJAcTChLFqK5lo+flo7pahqd8DSLv7uMxsdOdAHj6RzKYAm8pPfUS6ItKYutYyuicwKaFwQKsoA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-knex@0.48.0': resolution: {integrity: sha512-V5wuaBPv/lwGxuHjC6Na2JFRjtPgstw19jTFl1B1b6zvaX8zVDYUDaR5hL7glnQtUSCMktPttQsgK4dhXpddcA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-knex@0.57.0': - resolution: {integrity: sha512-vMCSh8kolEm5rRsc+FZeTZymWmIJwc40hjIKnXH4O0Dv/gAkJJIRXCsPX5cPbe0c0j/34+PsENd0HqKruwhVYw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-koa@0.51.0': resolution: {integrity: sha512-XNLWeMTMG1/EkQBbgPYzCeBD0cwOrfnn8ao4hWgLv0fNCFQu1kCsJYygz2cvKuCs340RlnG4i321hX7R8gj3Rg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-koa@0.61.0': - resolution: {integrity: sha512-lvrfWe9ShK/D2X4brmx8ZqqeWPfRl8xekU0FCn7C1dHm5k6+rTOOi36+4fnaHAP8lig9Ux6XQ1D4RNIpPCt1WQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@opentelemetry/instrumentation-lru-memoizer@0.48.0': resolution: {integrity: sha512-KUW29wfMlTPX1wFz+NNrmE7IzN7NWZDrmFWHM/VJcmFEuQGnnBuTIdsP55CnBDxKgQ/qqYFp4udQFNtjeFosPw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-lru-memoizer@0.57.0': - resolution: {integrity: sha512-cEqpUocSKJfwDtLYTTJehRLWzkZ2eoePCxfVIgGkGkb83fMB71O+y4MvRHJPbeV2bdoWdOVrl8uO0+EynWhTEA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-memcached@0.47.0': resolution: {integrity: sha512-vXDs/l4hlWy1IepPG1S6aYiIZn+tZDI24kAzwKKJmR2QEJRL84PojmALAEJGazIOLl/VdcCPZdMb0U2K0VzojA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5572,60 +5485,30 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongodb@0.66.0': - resolution: {integrity: sha512-d7m9QnAY+4TCWI4q1QRkfrc6fo/92VwssaB1DzQfXNRvu51b78P+HJlWP7Qg6N6nkwdb9faMZNBCZJfftmszkw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongoose@0.50.0': resolution: {integrity: sha512-Am8pk1Ct951r4qCiqkBcGmPIgGhoDiFcRtqPSLbJrUZqEPUsigjtMjoWDRLG1Ki1NHgOF7D0H7d+suWz1AAizw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongoose@0.59.0': - resolution: {integrity: sha512-6/jWU+c1NgznkVLDU/2y0bXV2nJo3o9FWZ9mZ9nN6T/JBNRoMnVXZl2FdBmgH+a5MwaWLs5kmRJTP5oUVGIkPw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql2@0.50.0': resolution: {integrity: sha512-PoOMpmq73rOIE3nlTNLf3B1SyNYGsp7QXHYKmeTZZnJ2Ou7/fdURuOhWOI0e6QZ5gSem18IR1sJi6GOULBQJ9g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql2@0.59.0': - resolution: {integrity: sha512-n9/xrVCRBfG9egVbffnlU1uhr+HX0vF4GgtAB/Bvm48wpFgRidqD8msBMiym1kRYzmpWvJqTxNT47u1MkgBEdw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql@0.49.0': resolution: {integrity: sha512-QU9IUNqNsrlfE3dJkZnFHqLjlndiU39ll/YAAEvWE40sGOCi9AtOF6rmEGzJ1IswoZ3oyePV7q2MP8SrhJfVAA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql@0.59.0': - resolution: {integrity: sha512-r+V/Fh0sm7Ga8/zk/TI5H5FQRAjwr0RrpfPf8kNIehlsKf12XnvIaZi8ViZkpX0gyPEpLXqzqWD6QHlgObgzZw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.49.0': resolution: {integrity: sha512-1R/JFwdmZIk3T/cPOCkVvFQeKYzbbUvDxVH3ShXamUwBlGkdEu5QJitlRMyVNZaHkKZKWgYrBarGQsqcboYgaw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.59.0': - resolution: {integrity: sha512-tt2cFTENV8XB3D3xjhOz0q4hLc1eqkMZS5UyT9nnHF5FfYH94S2vAGdssvsMv+pFtA6/PmhPUZd4onUN1O7STg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-net@0.47.0': resolution: {integrity: sha512-csoJ++Njpf7C09JH+0HNGenuNbDZBqO1rFhMRo6s0rAmJwNh9zY3M/urzptmKlqbKnf4eH0s+CKHy/+M8fbFsQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5644,12 +5527,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.65.0': - resolution: {integrity: sha512-W0zpHEIEuyZ8zvb3njaX9AAbHgPYOsSWVOoWmv1sjVRSF6ZpBqtlxBWbU+6hhq1TFWBeWJOXZ8nZS/PUFpLJYQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pino@0.50.1': resolution: {integrity: sha512-pBbvuWiHA9iAumAuQ0SKYOXK7NRlbnVTf/qBV0nMdRnxBPrc/GZTbh0f7Y59gZfYsbCLhXLL1oRTEnS+PwS3CA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5662,12 +5539,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-redis@0.61.0': - resolution: {integrity: sha512-JnPexA034/0UJRsvH96B0erQoNOqKJZjE2ZRSw9hiTSC23LzE0nJE/u6D+xqOhgUhRnhhcPHq4MdYtmUdYTF+Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-restify@0.49.0': resolution: {integrity: sha512-tsGZZhS4mVZH7omYxw5jpsrD3LhWizqWc0PYtAnzpFUvL5ZINHE+cm57bssTQ2AK/GtZMxu9LktwCvIIf3dSmw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5698,24 +5569,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-tedious@0.32.0': - resolution: {integrity: sha512-BQS6gG8RJ1foEqfEZ+wxoqlwfCAzb1ZVG0ad8Gfe4x8T658HJCLGLd4E4NaoQd8EvPfLqOXgzGaE/2U4ytDSWA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-undici@0.14.0': resolution: {integrity: sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.7.0 - '@opentelemetry/instrumentation-undici@0.23.0': - resolution: {integrity: sha512-LL0VySzKVR2cJSFVZaTYpZl1XTpBGnfzoQPe2W7McS2267ldsaEIqtQY6VXs2KCXN0poFjze5110PIpxHDaDGg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.7.0 - '@opentelemetry/instrumentation-winston@0.48.1': resolution: {integrity: sha512-XyOuVwdziirHHYlsw+BWrvdI/ymjwnexupKA787zQQ+D5upaE/tseZxjfQa7+t4+FdVLxHICaMTmkSD4yZHpzQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5728,20 +5587,8 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.207.0': - resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation@0.212.0': - resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation@0.213.0': - resolution: {integrity: sha512-3i9NdkET/KvQomeh7UaR/F4r9P25Rx6ooALlWXPIjypcEOUxksCmVu0zA70NBJWlrMW1rPr/LRidFAflLI+s/w==} + '@opentelemetry/instrumentation@0.214.0': + resolution: {integrity: sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -5828,6 +5675,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.11.0': + resolution: {integrity: sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.2.0': resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5882,6 +5735,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.11.0': + resolution: {integrity: sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.2.0': resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5906,6 +5765,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/sdk-trace@2.11.0': + resolution: {integrity: sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.40.0': resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} engines: {node: '>=14'} @@ -6130,11 +5995,6 @@ packages: '@prisma/get-platform@6.5.0': resolution: {integrity: sha512-xYcvyJwNMg2eDptBYFqFLUCfgi+wZLcj6HDMsj0Qw0irvauG4IKmkbywnqwok0B+k+W+p+jThM2DKTSmoPCkzw==} - '@prisma/instrumentation@7.4.2': - resolution: {integrity: sha512-r9JfchJF1Ae6yAxcaLu/V1TGqBhAuSDe3mRNOssBfx1rMzfZ4fdNvrgUBwyb/TNTGXFxlH9AZix5P257x07nrg==} - peerDependencies: - '@opentelemetry/api': ^1.8 - '@project-serum/sol-wallet-adapter@0.2.6': resolution: {integrity: sha512-cpIb13aWPW8y4KzkZAPDgw+Kb+DXjCC6rZoH74MGm3I/6e/zKyGnfAuW5olb2zxonFqsYgnv7ev8MQnvSgJ3/g==} engines: {node: '>=10'} @@ -7428,165 +7288,237 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@sentry-internal/browser-utils@10.45.0': - resolution: {integrity: sha512-ZPZpeIarXKScvquGx2AfNKcYiVNDA4wegMmjyGVsTA2JPmP0TrJoO3UybJS6KGDeee8V3I3EfD/ruauMm7jOFQ==} + '@sentry-internal/browser-utils@10.56.0': + resolution: {integrity: sha512-I8tZWAFg8SZpD8BFUpglEtSTzhZjacmcThB5/Mlq/iFiiT8mBPG4ZWDWssSfmIBKvZywJZJ83uDA0+uiJU73Tw==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.45.0': - resolution: {integrity: sha512-vCSurazFVq7RUeYiM5X326jA5gOVrWYD6lYX2fbjBOMcyCEhDnveNxMT62zKkZDyNT/jyD194nz/cjntBUkyWA==} + '@sentry-internal/feedback@10.56.0': + resolution: {integrity: sha512-fkRR9JroESTIlErkht3OrH4DXKd/DbPozr2KLdX7boMo31hPu4cL9fuqzwOrwyDPRq9B4j+qEgIWB8JrTbgvmg==} engines: {node: '>=18'} - '@sentry-internal/node-cpu-profiler@2.2.0': - resolution: {integrity: sha512-oLHVYurqZfADPh5hvmQYS5qx8t0UZzT2u6+/68VXsFruQEOnYJTODKgU3BVLmemRs3WE6kCJjPeFdHVYOQGSzQ==} + '@sentry-internal/node-cpu-profiler@2.4.1': + resolution: {integrity: sha512-Wnkik+RLvRUZEB8Zx5ofgPdcC8bCSoiUUiyH6TorrLK6PBPerbjK48c2GsJf6l5Hc5GAhA3fitueVLATB0XhWQ==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.45.0': - resolution: {integrity: sha512-nvq/AocdZTuD7y0KSiWi3gVaY0s5HOFy86mC/v1kDZmT/jsBAzN5LDkk/f1FvsWma1peqQmpUqxvhC+YIW294Q==} + '@sentry-internal/replay-canvas@10.56.0': + resolution: {integrity: sha512-SDg2K0CAZT/TnhrixQGwXoi6ZsWUB+DQy3UUk0bSQm6c/5k5zFBpGOiughQN+DYsDilKREfPKmUEEnqvUjm1HQ==} engines: {node: '>=18'} - '@sentry-internal/replay@10.45.0': - resolution: {integrity: sha512-vjosRoGA1bzhVAEO1oce+CsRdd70quzBeo7WvYqpcUnoLe/Rv8qpOMqWX3j26z7XfFHMExWQNQeLxmtYOArvlw==} + '@sentry-internal/replay@10.56.0': + resolution: {integrity: sha512-DjF09hpy3TF7Km/kOZc73YJmBqcbPCxuZ5rtRs+KtVHu3Vq48xeW83qKUcFEZv20ur9UD99OAJ/gaEt//1Qbwg==} engines: {node: '>=18'} - '@sentry/babel-plugin-component-annotate@5.1.1': - resolution: {integrity: sha512-x2wEpBHwsTyTF2rWsLKJlzrRF1TTIGOfX+ngdE+Yd5DBkoS58HwQv824QOviPGQRla4/ypISqAXzjdDPL/zalg==} + '@sentry-internal/server-utils@10.56.0': + resolution: {integrity: sha512-6kuZI/vAjyVKMm1cTzc2pdUmVR4Px4etMG6wnCPyFnwEaGbUKQnTynUBFpTuo/q6Js6QBQvhLNoAnO4YsOfW4w==} + engines: {node: '>=18'} + + '@sentry/babel-plugin-component-annotate@5.3.0': + resolution: {integrity: sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==} engines: {node: '>= 18'} - '@sentry/browser@10.45.0': - resolution: {integrity: sha512-e/a8UMiQhqqv706McSIcG6XK+AoQf9INthi2pD+giZfNRTzXTdqHzUT5OIO5hg8Am6eF63nDJc+vrYNPhzs51Q==} + '@sentry/browser@10.56.0': + resolution: {integrity: sha512-80X3NmsGB6tLmfzXYdjzWWdVAdL5CRukGKLcRWIcNhgGjtskOmnzaGb93egEZGI5bUTbtONJ0oyscQ3Z9yoAtQ==} engines: {node: '>=18'} - '@sentry/bundler-plugin-core@5.1.1': - resolution: {integrity: sha512-F+itpwR9DyQR7gEkrXd2tigREPTvtF5lC8qu6e4anxXYRTui1+dVR0fXNwjpyAZMhIesLfXRN7WY7ggdj7hi0Q==} + '@sentry/bundler-plugin-core@5.3.0': + resolution: {integrity: sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q==} + engines: {node: '>= 18'} + + '@sentry/bundler-plugins@10.75.0': + resolution: {integrity: sha512-kpMO7NhvfqsobpShyW/YnNJmJpw7mPEAQorJnEzf0ZRqNvEGFjKKN19SZWbbBNdcQDvHPlRIVOC00zdXuw3y/w==} engines: {node: '>= 18'} + peerDependencies: + rollup: '>=3.2.0' + webpack: '>=5.0.0' + peerDependenciesMeta: + rollup: + optional: true + webpack: + optional: true '@sentry/cli-darwin@2.58.5': resolution: {integrity: sha512-lYrNzenZFJftfwSya7gwrHGxtE+Kob/e1sr9lmHMFOd4utDlmq0XFDllmdZAMf21fxcPRI1GL28ejZ3bId01fQ==} engines: {node: '>=10'} os: [darwin] + '@sentry/cli-darwin@2.58.6': + resolution: {integrity: sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==} + engines: {node: '>=10'} + os: [darwin] + '@sentry/cli-linux-arm64@2.58.5': resolution: {integrity: sha512-/4gywFeBqRB6tR/iGMRAJ3HRqY6Z7Yp4l8ZCbl0TDLAfHNxu7schEw4tSnm2/Hh9eNMiOVy4z58uzAWlZXAYBQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux, freebsd, android] + '@sentry/cli-linux-arm64@2.58.6': + resolution: {integrity: sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd, android] + '@sentry/cli-linux-arm@2.58.5': resolution: {integrity: sha512-KtHweSIomYL4WVDrBrYSYJricKAAzxUgX86kc6OnlikbyOhoK6Fy8Vs6vwd52P6dvWPjgrMpUYjW2M5pYXQDUw==} engines: {node: '>=10'} cpu: [arm] os: [linux, freebsd, android] + '@sentry/cli-linux-arm@2.58.6': + resolution: {integrity: sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd, android] + '@sentry/cli-linux-i686@2.58.5': resolution: {integrity: sha512-G7261dkmyxqlMdyvyP06b+RTIVzp1gZNgglj5UksxSouSUqRd/46W/2pQeOMPhloDYo9yLtCN2YFb3Mw4aUsWw==} engines: {node: '>=10'} cpu: [x86, ia32] os: [linux, freebsd, android] + '@sentry/cli-linux-i686@2.58.6': + resolution: {integrity: sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd, android] + '@sentry/cli-linux-x64@2.58.5': resolution: {integrity: sha512-rP04494RSmt86xChkQ+ecBNRYSPbyXc4u0IA7R7N1pSLCyO74e5w5Al+LnAq35cMfVbZgz5Sm0iGLjyiUu4I1g==} engines: {node: '>=10'} cpu: [x64] os: [linux, freebsd, android] + '@sentry/cli-linux-x64@2.58.6': + resolution: {integrity: sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd, android] + '@sentry/cli-win32-arm64@2.58.5': resolution: {integrity: sha512-AOJ2nCXlQL1KBaCzv38m3i2VmSHNurUpm7xVKd6yAHX+ZoVBI8VT0EgvwmtJR2TY2N2hNCC7UrgRmdUsQ152bA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] + '@sentry/cli-win32-arm64@2.58.6': + resolution: {integrity: sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + '@sentry/cli-win32-i686@2.58.5': resolution: {integrity: sha512-EsuboLSOnlrN7MMPJ1eFvfMDm+BnzOaSWl8eYhNo8W/BIrmNgpRUdBwnWn9Q2UOjJj5ZopukmsiMYtU/D7ml9g==} engines: {node: '>=10'} cpu: [x86, ia32] os: [win32] + '@sentry/cli-win32-i686@2.58.6': + resolution: {integrity: sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + '@sentry/cli-win32-x64@2.58.5': resolution: {integrity: sha512-IZf+XIMiQwj+5NzqbOQfywlOitmCV424Vtf9c+ep61AaVScUFD1TSrQbOcJJv5xGxhlxNOMNgMeZhdexdzrKZg==} engines: {node: '>=10'} cpu: [x64] os: [win32] + '@sentry/cli-win32-x64@2.58.6': + resolution: {integrity: sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + '@sentry/cli@2.58.5': resolution: {integrity: sha512-tavJ7yGUZV+z3Ct2/ZB6mg339i08sAk6HDkgqmSRuQEu2iLS5sl9HIvuXfM6xjv8fwlgFOSy++WNABNAcGHUbg==} engines: {node: '>= 10'} hasBin: true - '@sentry/core@10.45.0': - resolution: {integrity: sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q==} + '@sentry/cli@2.58.6': + resolution: {integrity: sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==} + engines: {node: '>= 10'} + hasBin: true + + '@sentry/conventions@0.16.0': + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + engines: {node: '>=14'} + + '@sentry/core@10.56.0': + resolution: {integrity: sha512-L+u1dIz5SANrmST5jhIwETtt4apILgKrylv12X4hKJU0PvZl+NorjeV/ty3MwzpKQPg6b6q6qMOSLc1rLpy3iQ==} + engines: {node: '>=18'} + + '@sentry/core@10.75.0': + resolution: {integrity: sha512-5wDQpQqjJ6RHdPR+z6Q+47XlwoxRKBv0yyB0LKHqL/1azPOQbR+nllyLmmQDcoJpaksuLSmLA1q6hFX7gjDT2w==} engines: {node: '>=18'} - '@sentry/nestjs@10.45.0': - resolution: {integrity: sha512-Puk5JNRdUMmOgN9MOcZeQ4jRR25C8R1AuujZXizBPA7iOmdesgpq3Pa6BZ5Hj4e/YZMd64D11MHEcUMINGMnRw==} + '@sentry/nestjs@10.56.0': + resolution: {integrity: sha512-tlZ52SpfWe38nNi3o51kAmLV7V1PqEH7IomsxmFQAV1cpekpEG+55MILMoSvhRonBsPQUE9jI0q/cFTtP9Au4Q==} engines: {node: '>=18'} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - '@sentry/nextjs@10.45.0': - resolution: {integrity: sha512-4LE+UvnfdOYyG8YEb/9TWaJQzMPuGLlph/iqowvsMdxaW6la+mvADiuzNTXly4QfsjeD3KIb7dKlGTqiVV0Ttw==} + '@sentry/nextjs@10.56.0': + resolution: {integrity: sha512-63zg6UawJwW4pE+rpVj+pqy08TsaQUEfYXLITNGBmPdk4ZO7mrosYeMZ+efI1LlCmaEu0/78M/kvzPIdk33SYA==} engines: {node: '>=18'} peerDependencies: next: 16.3.1 - '@sentry/node-core@10.45.0': - resolution: {integrity: sha512-KQZEvLKM344+EqXiA9HIzWbW5hzq6/9nnFUQ8niaBPoOgR9AiJhrccfIscfgb8vjkriiEtzE03OW/4h1CTgZ3Q==} + '@sentry/node-core@10.56.0': + resolution: {integrity: sha512-61lD2Wjtv5Lw2F3lJarcD0ORjR4GlVxrEd6w6Of/uF3DH73dD6K3n/3wXEeCIRfV/kgiCFIrCIq76nz0LVgE5g==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' '@opentelemetry/instrumentation': '>=0.57.1 <1' - '@opentelemetry/resources': ^1.30.1 || ^2.1.0 '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 '@opentelemetry/semantic-conventions': ^1.39.0 peerDependenciesMeta: '@opentelemetry/api': optional: true - '@opentelemetry/context-async-hooks': - optional: true '@opentelemetry/core': optional: true - '@opentelemetry/instrumentation': + '@opentelemetry/exporter-trace-otlp-http': optional: true - '@opentelemetry/resources': + '@opentelemetry/instrumentation': optional: true '@opentelemetry/sdk-trace-base': optional: true '@opentelemetry/semantic-conventions': optional: true - '@sentry/node@10.45.0': - resolution: {integrity: sha512-Kpiq9lRGnJc1ex8SwxOBl+FLQNl4Y137BydVooP7AFiAYZ6ftwHsIEF1bcYXaipHMT1YHS2bdhC2UQaaB2jkuQ==} + '@sentry/node@10.56.0': + resolution: {integrity: sha512-qvgtXHkcR4CH3fh0VEVyw4Ysc6MMiAnm727NdTTm0yU5e53erCeo2521+yfJkqmRTGiOSgwA7B5Bs+ot9j0vFQ==} engines: {node: '>=18'} - '@sentry/opentelemetry@10.45.0': - resolution: {integrity: sha512-PmuGO+p/gC3ZQ8ddOeJ5P9ApnTTm35i12Bpuyb13AckCbNSJFvG2ggZda35JQOmiFU0kKYiwkoFAa8Mvj9od3Q==} + '@sentry/opentelemetry@10.56.0': + resolution: {integrity: sha512-PtMudApHMHvttjos3b7JZ2gJ+nstHAOYE3vKPYB5o0WQO95ldiaYnpLKMCRIGZWF3Dk7ynrqqnBpn8LZLt+Mrg==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 '@opentelemetry/core': ^1.30.1 || ^2.1.0 '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 '@opentelemetry/semantic-conventions': ^1.39.0 - '@sentry/profiling-node@10.45.0': - resolution: {integrity: sha512-O1wgw4NuVRZ1c3kbjwZyZoT+iylgWsldgDZA8jhFBNRRJv7XbzJ3NVji5Ksw3W2uRNUrTfFJNC9UV3brqWom8g==} + '@sentry/profiling-node@10.56.0': + resolution: {integrity: sha512-AmcUrvqJcvl39fNzoXBCPp8iGStRSmpBYqQCPz1mlDUw/n/iWpLq5+8iHecXyT5oOST4xHjNIyb7AA/fj0Pxzw==} engines: {node: '>=18'} hasBin: true - '@sentry/react@10.45.0': - resolution: {integrity: sha512-jLezuxi4BUIU3raKyAPR5xMbQG/nhwnWmKo5p11NCbLmWzkS+lxoyDTUB4B8TAKZLfdtdkKLOn1S0tFc8vbUHw==} + '@sentry/react@10.56.0': + resolution: {integrity: sha512-HfPLyvnrydfyjRXw9Q0GMzj7w2YtEwuC9z5RrPUfarA2qpA0/J8cfGLzyFX2v0jBmA/kkj6J1uBUoSVhCTxFHg==} engines: {node: '>=18'} peerDependencies: react: 19.2.4 - '@sentry/vercel-edge@10.45.0': - resolution: {integrity: sha512-sSF+Ex5NwT60gMinLcP/JNZb3cDaIv0mL1cRjfvN6zN2ZNEw0C9rhdgxa0EdD4G6PCHQ0XnCuAMDsfJ6gnRmfA==} + '@sentry/vercel-edge@10.56.0': + resolution: {integrity: sha512-2e5rtVjLpIVYKssFcTuKsNWYJeLAnNsQCcq7Mscw1rQal5UdBgxwclw7K2SN+QHDZlKradRBdhwOU0svENOnQw==} engines: {node: '>=18'} - '@sentry/webpack-plugin@5.1.1': - resolution: {integrity: sha512-XgQg+t2aVrlQDfIiAEizqR/bsy6GtBygwgR+Kw11P/cYczj4W9PZ2IYqQEStBzHqnRTh5DbpyMcUNW2CujdA9A==} + '@sentry/webpack-plugin@5.4.0': + resolution: {integrity: sha512-J3a0BvUZ75Qxy+v/Ap3Hx4ZEcSjlPHZ/jDtxdRhXQCyNeEb8xq0uUBTI9VLtGk2eNeNucOxOEJ5ngqdNjnEH/A==} engines: {node: '>= 18'} peerDependencies: webpack: '>=5.0.0' @@ -9220,9 +9152,6 @@ packages: '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} - '@types/pg-pool@2.0.7': - resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} - '@types/pg@8.15.5': resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} @@ -11927,6 +11856,10 @@ packages: resolution: {integrity: sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + draggabilly@3.0.0: resolution: {integrity: sha512-aEs+B6prbMZQMxc9lgTpCBfyCUhRur/VFucHhIOvlvvdARTj7TcDmX/cdOUtqbjJJUh7+agyJXR5Z6IFe1MxwQ==} @@ -13474,9 +13407,6 @@ packages: import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} - import-in-the-middle@2.0.6: - resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} - import-in-the-middle@3.0.0: resolution: {integrity: sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg==} engines: {node: '>=18'} @@ -16010,9 +15940,6 @@ packages: peerDependencies: pg: '>=8.0' - pg-protocol@1.13.0: - resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} - pg-protocol@1.16.0: resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} @@ -19490,14 +19417,14 @@ snapshots: '@ag-ui/core': 0.0.59 '@ag-ui/proto': 0.0.59 - '@ag-ui/langgraph@0.0.43(@ag-ui/client@0.0.59)(@ag-ui/core@0.0.59)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@ag-ui/langgraph@0.0.43(@ag-ui/client@0.0.59)(@ag-ui/core@0.0.59)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: '@ag-ui/a2ui-toolkit': 0.0.4 '@ag-ui/client': 0.0.59 '@ag-ui/core': 0.0.59 - '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/langgraph-sdk': 1.8.8(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - langchain: 1.5.11(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/langgraph-sdk': 1.8.8(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + langchain: 1.5.11(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) partial-json: 0.1.7 rxjs: 7.8.1 transitivePeerDependencies: @@ -19511,13 +19438,13 @@ snapshots: - vue - ws - '@ag-ui/mastra@1.1.4(@ag-ui/client@0.0.59)(@ag-ui/core@0.0.59)(@copilotkit/runtime@1.72.0(060090e105863d9983aa04d300df3d3a))(@mastra/client-js@0.15.2(openapi-types@12.1.3)(react@19.2.4)(zod@3.25.76))(@mastra/core@1.67.0(@bufbuild/protobuf@2.11.0)(@grpc/grpc-js@1.14.3)(ai@4.3.19(react@19.2.4)(zod@3.25.76))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@ag-ui/mastra@1.1.4(@ag-ui/client@0.0.59)(@ag-ui/core@0.0.59)(@copilotkit/runtime@1.72.0(d8db588a33556e4390c2c1ec6b88e6cc))(@mastra/client-js@0.15.2(openapi-types@12.1.3)(react@19.2.4)(zod@3.25.76))(@mastra/core@1.67.0(@bufbuild/protobuf@2.11.0)(@grpc/grpc-js@1.14.3)(ai@4.3.19(react@19.2.4)(zod@3.25.76))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@ag-ui/a2ui-toolkit': 0.0.4 '@ag-ui/client': 0.0.59 '@ag-ui/core': 0.0.59 '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) - '@copilotkit/runtime': 1.72.0(060090e105863d9983aa04d300df3d3a) + '@copilotkit/runtime': 1.72.0(d8db588a33556e4390c2c1ec6b88e6cc) '@mastra/client-js': 0.15.2(openapi-types@12.1.3)(react@19.2.4)(zod@3.25.76) '@mastra/core': 1.67.0(@bufbuild/protobuf@2.11.0)(@grpc/grpc-js@1.14.3)(ai@4.3.19(react@19.2.4)(zod@3.25.76))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(utf-8-validate@5.0.10)(zod@3.25.76) fast-json-patch: 3.1.1 @@ -21946,13 +21873,13 @@ snapshots: - encoding - graphql - '@copilotkit/runtime@1.72.0(060090e105863d9983aa04d300df3d3a)': + '@copilotkit/runtime@1.72.0(d8db588a33556e4390c2c1ec6b88e6cc)': dependencies: '@ag-ui/a2ui-middleware': 0.0.10(@ag-ui/client@0.0.59)(rxjs@7.8.1) '@ag-ui/client': 0.0.59 '@ag-ui/core': 0.0.59 '@ag-ui/encoder': 0.0.59 - '@ag-ui/langgraph': 0.0.43(@ag-ui/client@0.0.59)(@ag-ui/core@0.0.59)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@ag-ui/langgraph': 0.0.43(@ag-ui/client@0.0.59)(@ag-ui/core@0.0.59)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@ag-ui/mcp-apps-middleware': 0.1.1(@ag-ui/client@0.0.59)(@cfworker/json-schema@4.1.1)(rxjs@7.8.1)(zod@3.25.76) '@ag-ui/mcp-middleware': 0.0.2(@ag-ui/client@0.0.59)(@cfworker/json-schema@4.1.1)(rxjs@7.8.1)(zod@3.25.76) '@ai-sdk/anthropic': 3.0.118(zod@3.25.76) @@ -21967,7 +21894,7 @@ snapshots: '@copilotkit/shared': 1.72.0(@ag-ui/core@0.0.59) '@graphql-yoga/plugin-defer-stream': 3.18.0(graphql-yoga@5.18.0(graphql@16.13.1))(graphql@16.13.1) '@hono/node-server': 1.19.11(hono@4.12.10) - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) '@remix-run/node-fetch-server': 0.13.3 '@scarf/scarf': 1.4.0 @@ -21996,13 +21923,13 @@ snapshots: zod: 3.25.76 optionalDependencies: '@anthropic-ai/sdk': 0.57.0 - '@langchain/aws': 0.1.15(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@langchain/community': 1.1.27(66046857593e81cf94589e5d63e1e984) - '@langchain/google-gauth': 0.1.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(zod@3.25.76) - '@langchain/langgraph-sdk': 1.11.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@langchain/openai': 1.4.3(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/aws': 0.1.15(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@langchain/community': 1.1.27(cbe060cf4b5645a3ea17665610df96e8) + '@langchain/google-gauth': 0.1.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(zod@3.25.76) + '@langchain/langgraph-sdk': 1.11.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@langchain/openai': 1.4.3(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) groq-sdk: 0.5.0 - langchain: 1.5.11(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + langchain: 1.5.11(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) openai: 6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@microsoft/agents-activity' @@ -22551,16 +22478,6 @@ snapshots: '@fastify/busboy@3.2.0': {} - '@fastify/otel@0.17.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - minimatch: 10.2.4 - transitivePeerDependencies: - - supports-color - '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -23523,22 +23440,22 @@ snapshots: '@kurkle/color@0.3.4': {} - '@langchain/aws@0.1.15(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@langchain/aws@0.1.15(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: '@aws-sdk/client-bedrock-agent-runtime': 3.1003.0 '@aws-sdk/client-bedrock-runtime': 3.1003.0 '@aws-sdk/client-kendra': 3.1003.0 '@aws-sdk/credential-provider-node': 3.972.17 - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) transitivePeerDependencies: - aws-crt optional: true - '@langchain/classic@1.0.27(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(cheerio@1.2.0)(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@langchain/classic@1.0.27(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(cheerio@1.2.0)(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/openai': 1.4.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/textsplitters': 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/openai': 1.4.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/textsplitters': 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) handlebars: 4.7.9 js-yaml: 4.1.1 jsonpointer: 5.0.1 @@ -23548,7 +23465,7 @@ snapshots: zod: 3.25.76 optionalDependencies: cheerio: 1.2.0 - langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) transitivePeerDependencies: - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' @@ -23556,18 +23473,18 @@ snapshots: - openai - ws - '@langchain/community@1.1.27(66046857593e81cf94589e5d63e1e984)': + '@langchain/community@1.1.27(cbe060cf4b5645a3ea17665610df96e8)': dependencies: '@browserbasehq/stagehand': 1.14.0(@playwright/test@1.58.2)(bufferutil@4.1.0)(deepmerge@4.3.1)(dotenv@16.6.1)(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(utf-8-validate@5.0.10)(zod@3.25.76) '@ibm-cloud/watsonx-ai': 1.7.9 - '@langchain/classic': 1.0.27(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(cheerio@1.2.0)(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/openai': 1.4.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/classic': 1.0.27(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(cheerio@1.2.0)(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/openai': 1.4.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) binary-extensions: 2.3.0 flat: 5.0.2 ibm-cloud-sdk-core: 5.4.8 js-yaml: 4.1.1 - langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) math-expression-evaluator: 2.0.7 openai: 6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76) uuid: 10.0.0 @@ -23604,7 +23521,7 @@ snapshots: - '@opentelemetry/sdk-trace-base' - peggy - '@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 @@ -23612,7 +23529,7 @@ snapshots: camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) mustache: 4.2.0 p-queue: 6.6.2 uuid: 11.1.0 @@ -23624,12 +23541,12 @@ snapshots: - openai - ws - '@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 js-tiktoken: 1.0.21 - langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) mustache: 4.2.0 p-queue: 6.6.2 zod: 3.25.76 @@ -23640,19 +23557,19 @@ snapshots: - openai - ws - '@langchain/google-common@0.1.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(zod@3.25.76)': + '@langchain/google-common@0.1.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(zod@3.25.76)': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) uuid: 10.0.0 zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - zod optional: true - '@langchain/google-gauth@0.1.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(zod@3.25.76)': + '@langchain/google-gauth@0.1.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(zod@3.25.76)': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/google-common': 0.1.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(zod@3.25.76) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/google-common': 0.1.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(zod@3.25.76) google-auth-library: 8.9.0 transitivePeerDependencies: - encoding @@ -23660,23 +23577,23 @@ snapshots: - zod optional: true - '@langchain/langgraph-checkpoint@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@langchain/langgraph-checkpoint@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) uuid: 10.0.0 - '@langchain/langgraph-checkpoint@1.1.5(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@langchain/langgraph-checkpoint@1.1.5(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) optional: true - '@langchain/langgraph-checkpoint@1.1.5(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@langchain/langgraph-checkpoint@1.1.5(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: - '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/langgraph-sdk@1.11.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@langchain/langgraph-sdk@1.11.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@langchain/protocol': 0.0.19 '@types/json-schema': 7.0.15 p-queue: 9.1.2 @@ -23686,9 +23603,9 @@ snapshots: react-dom: 19.2.4(react@19.2.4) optional: true - '@langchain/langgraph-sdk@1.11.0(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@langchain/langgraph-sdk@1.11.0(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@langchain/protocol': 0.0.19 '@types/json-schema': 7.0.15 p-queue: 9.1.2 @@ -23697,33 +23614,33 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@langchain/langgraph-sdk@1.8.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@langchain/langgraph-sdk@1.8.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@types/json-schema': 7.0.15 p-queue: 9.1.2 p-retry: 7.1.1 uuid: 13.0.0 optionalDependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@langchain/langgraph-sdk@1.8.8(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@langchain/langgraph-sdk@1.8.8(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@types/json-schema': 7.0.15 p-queue: 9.1.2 p-retry: 7.1.1 uuid: 13.0.0 optionalDependencies: - '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@langchain/langgraph@1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': + '@langchain/langgraph@1.2.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@langchain/langgraph-sdk': 1.8.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@langchain/langgraph-sdk': 1.8.8(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@standard-schema/spec': 1.1.0 uuid: 10.0.0 zod: 3.25.76 @@ -23735,11 +23652,11 @@ snapshots: - svelte - vue - '@langchain/langgraph@1.4.15(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76)': + '@langchain/langgraph@1.4.15(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76)': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/langgraph-checkpoint': 1.1.5(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@langchain/langgraph-sdk': 1.11.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/langgraph-checkpoint': 1.1.5(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@langchain/langgraph-sdk': 1.11.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@langchain/protocol': 0.0.19 '@standard-schema/spec': 1.1.0 zod: 3.25.76 @@ -23748,11 +23665,11 @@ snapshots: - react-dom optional: true - '@langchain/langgraph@1.4.15(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76)': + '@langchain/langgraph@1.4.15(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76)': dependencies: - '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/langgraph-checkpoint': 1.1.5(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@langchain/langgraph-sdk': 1.11.0(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/langgraph-checkpoint': 1.1.5(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@langchain/langgraph-sdk': 1.11.0(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@langchain/protocol': 0.0.19 '@standard-schema/spec': 1.1.0 zod: 3.25.76 @@ -23760,18 +23677,18 @@ snapshots: - react - react-dom - '@langchain/openai@1.4.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@langchain/openai@1.4.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) js-tiktoken: 1.0.21 openai: 6.34.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - ws - '@langchain/openai@1.4.3(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@langchain/openai@1.4.3(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) js-tiktoken: 1.0.21 openai: 6.34.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76) zod: 3.25.76 @@ -23780,14 +23697,14 @@ snapshots: '@langchain/protocol@0.0.19': {} - '@langchain/tavily@1.2.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@langchain/tavily@1.2.0(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) zod: 3.25.76 - '@langchain/textsplitters@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@langchain/textsplitters@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) js-tiktoken: 1.0.21 '@ledgerhq/devices@8.10.0': @@ -23931,8 +23848,8 @@ snapshots: '@mastra/schema-compat': 0.11.4(ai@4.3.19(react@19.2.4)(zod@3.25.76))(zod@3.25.76) '@openrouter/ai-sdk-provider-v5': '@openrouter/ai-sdk-provider@1.2.0(ai@4.3.19(react@19.2.4)(zod@3.25.76))(zod@3.25.76)' '@opentelemetry/api': 1.9.0 - '@opentelemetry/auto-instrumentations-node': 0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/auto-instrumentations-node': 0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.0)) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-trace-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) @@ -23940,7 +23857,7 @@ snapshots: '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 '@sindresorhus/slugify': 2.2.1 @@ -24834,28 +24751,22 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.207.0': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.208.0': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.212.0': + '@opentelemetry/api-logs@0.214.0': dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api-logs@0.213.0': - dependencies: - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.0': {} - '@opentelemetry/auto-instrumentations-node@0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))': + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/auto-instrumentations-node@0.62.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.0))': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-amqplib': 0.50.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-aws-lambda': 0.54.1(@opentelemetry/api@1.9.0) @@ -24921,6 +24832,16 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25048,21 +24969,12 @@ snapshots: '@opentelemetry/instrumentation-amqplib@0.50.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-amqplib@0.60.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-aws-lambda@0.54.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25075,7 +24987,7 @@ snapshots: '@opentelemetry/instrumentation-aws-sdk@0.58.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: @@ -25101,23 +25013,13 @@ snapshots: '@opentelemetry/instrumentation-connect@0.47.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 '@types/connect': 3.4.38 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-connect@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/connect': 3.4.38 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-cucumber@0.19.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25133,13 +25035,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-dataloader@0.30.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-dns@0.47.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25150,25 +25045,16 @@ snapshots: '@opentelemetry/instrumentation-express@0.52.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-express@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-fastify@0.48.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: @@ -25177,19 +25063,11 @@ snapshots: '@opentelemetry/instrumentation-fs@0.23.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-fs@0.32.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-generic-pool@0.47.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25197,13 +25075,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-generic-pool@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-graphql@0.51.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25211,13 +25082,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-graphql@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-grpc@0.203.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25229,21 +25093,12 @@ snapshots: '@opentelemetry/instrumentation-hapi@0.50.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-hapi@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-http@0.203.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25254,16 +25109,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-http@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - forwarded-parse: 2.1.2 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-ioredis@0.51.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25273,15 +25118,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-kafkajs@0.13.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25290,14 +25126,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-kafkajs@0.22.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-knex@0.48.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25306,32 +25134,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-knex@0.57.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-koa@0.51.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-koa@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-lru-memoizer@0.48.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25339,13 +25150,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-lru-memoizer@0.57.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-memcached@0.47.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25363,32 +25167,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongodb@0.66.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-mongoose@0.50.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongoose@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-mysql2@0.50.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25398,15 +25185,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql2@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-mysql@0.49.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25416,15 +25194,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/mysql': 2.15.27 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.49.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25433,14 +25202,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-net@0.47.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25461,7 +25222,7 @@ snapshots: '@opentelemetry/instrumentation-pg@0.56.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) @@ -25470,23 +25231,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.65.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - '@types/pg': 8.15.6 - '@types/pg-pool': 2.0.7 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-pino@0.50.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -25500,19 +25249,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-redis@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-restify@0.49.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: @@ -25550,32 +25290,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-tedious@0.32.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/tedious': 4.0.14 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-undici@0.14.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-undici@0.23.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-winston@0.48.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25593,28 +25315,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 - import-in-the-middle: 2.0.6 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)': dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.212.0 - import-in-the-middle: 2.0.6 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.213.0 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.214.0 import-in-the-middle: 3.0.0 require-in-the-middle: 8.0.1 transitivePeerDependencies: @@ -25677,33 +25381,33 @@ snapshots: '@opentelemetry/resource-detector-alibaba-cloud@0.31.11(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/resource-detector-aws@2.13.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/resource-detector-azure@0.10.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/resource-detector-container@0.7.11(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/resource-detector-gcp@0.37.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 gcp-metadata: 6.1.1 @@ -25717,6 +25421,12 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25796,6 +25506,22 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25824,12 +25550,19 @@ snapshots: '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/semantic-conventions@1.40.0': {} '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) '@optimize-lodash/rollup-plugin@5.1.0(rollup@4.63.3)': dependencies: @@ -26106,13 +25839,6 @@ snapshots: dependencies: '@prisma/debug': 6.5.0 - '@prisma/instrumentation@7.4.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@project-serum/sol-wallet-adapter@0.2.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.5.4)(utf-8-validate@5.0.10))': dependencies: '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.5.4)(utf-8-validate@5.0.10) @@ -27185,9 +26911,9 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-commonjs@28.0.1(rollup@4.59.0)': + '@rollup/plugin-commonjs@28.0.1(rollup@4.63.3)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@rollup/pluginutils': 5.3.0(rollup@4.63.3) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.5.0(picomatch@4.0.5) @@ -27195,7 +26921,7 @@ snapshots: magic-string: 0.30.21 picomatch: 4.0.5 optionalDependencies: - rollup: 4.59.0 + rollup: 4.63.3 '@rollup/plugin-commonjs@29.0.2(rollup@4.63.3)': dependencies: @@ -27236,17 +26962,9 @@ snapshots: optionalDependencies: rollup: 4.63.3 - '@rollup/pluginutils@5.3.0(rollup@4.59.0)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.5 - optionalDependencies: - rollup: 4.59.0 - '@rollup/pluginutils@5.3.0(rollup@4.63.3)': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 estree-walker: 2.0.2 picomatch: 4.0.5 optionalDependencies: @@ -27486,43 +27204,47 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 - '@sentry-internal/browser-utils@10.45.0': + '@sentry-internal/browser-utils@10.56.0': dependencies: - '@sentry/core': 10.45.0 + '@sentry/core': 10.56.0 - '@sentry-internal/feedback@10.45.0': + '@sentry-internal/feedback@10.56.0': dependencies: - '@sentry/core': 10.45.0 + '@sentry/core': 10.56.0 - '@sentry-internal/node-cpu-profiler@2.2.0': + '@sentry-internal/node-cpu-profiler@2.4.1': dependencies: detect-libc: 2.1.2 node-abi: 3.87.0 - '@sentry-internal/replay-canvas@10.45.0': + '@sentry-internal/replay-canvas@10.56.0': dependencies: - '@sentry-internal/replay': 10.45.0 - '@sentry/core': 10.45.0 + '@sentry-internal/replay': 10.56.0 + '@sentry/core': 10.56.0 - '@sentry-internal/replay@10.45.0': + '@sentry-internal/replay@10.56.0': dependencies: - '@sentry-internal/browser-utils': 10.45.0 - '@sentry/core': 10.45.0 + '@sentry-internal/browser-utils': 10.56.0 + '@sentry/core': 10.56.0 - '@sentry/babel-plugin-component-annotate@5.1.1': {} + '@sentry-internal/server-utils@10.56.0': + dependencies: + '@sentry/core': 10.56.0 + + '@sentry/babel-plugin-component-annotate@5.3.0': {} - '@sentry/browser@10.45.0': + '@sentry/browser@10.56.0': dependencies: - '@sentry-internal/browser-utils': 10.45.0 - '@sentry-internal/feedback': 10.45.0 - '@sentry-internal/replay': 10.45.0 - '@sentry-internal/replay-canvas': 10.45.0 - '@sentry/core': 10.45.0 + '@sentry-internal/browser-utils': 10.56.0 + '@sentry-internal/feedback': 10.56.0 + '@sentry-internal/replay': 10.56.0 + '@sentry-internal/replay-canvas': 10.56.0 + '@sentry/core': 10.56.0 - '@sentry/bundler-plugin-core@5.1.1': + '@sentry/bundler-plugin-core@5.3.0': dependencies: '@babel/core': 7.29.0 - '@sentry/babel-plugin-component-annotate': 5.1.1 + '@sentry/babel-plugin-component-annotate': 5.3.0 '@sentry/cli': 2.58.5 dotenv: 16.6.1 find-up: 5.0.0 @@ -27532,30 +27254,70 @@ snapshots: - encoding - supports-color + '@sentry/bundler-plugins@10.75.0(rollup@4.63.3)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2))': + dependencies: + '@babel/core': 7.29.0 + '@sentry/cli': 2.58.6 + '@sentry/core': 10.75.0 + dotenv: 17.4.2 + find-up: 5.0.0 + glob: 13.0.6 + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.63.3 + webpack: 5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2) + transitivePeerDependencies: + - encoding + - supports-color + '@sentry/cli-darwin@2.58.5': optional: true + '@sentry/cli-darwin@2.58.6': + optional: true + '@sentry/cli-linux-arm64@2.58.5': optional: true + '@sentry/cli-linux-arm64@2.58.6': + optional: true + '@sentry/cli-linux-arm@2.58.5': optional: true + '@sentry/cli-linux-arm@2.58.6': + optional: true + '@sentry/cli-linux-i686@2.58.5': optional: true + '@sentry/cli-linux-i686@2.58.6': + optional: true + '@sentry/cli-linux-x64@2.58.5': optional: true + '@sentry/cli-linux-x64@2.58.6': + optional: true + '@sentry/cli-win32-arm64@2.58.5': optional: true + '@sentry/cli-win32-arm64@2.58.6': + optional: true + '@sentry/cli-win32-i686@2.58.5': optional: true + '@sentry/cli-win32-i686@2.58.6': + optional: true + '@sentry/cli-win32-x64@2.58.5': optional: true + '@sentry/cli-win32-x64@2.58.6': + optional: true + '@sentry/cli@2.58.5': dependencies: https-proxy-agent: 5.0.1 @@ -27576,137 +27338,137 @@ snapshots: - encoding - supports-color - '@sentry/core@10.45.0': {} + '@sentry/cli@2.58.6': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.58.6 + '@sentry/cli-linux-arm': 2.58.6 + '@sentry/cli-linux-arm64': 2.58.6 + '@sentry/cli-linux-i686': 2.58.6 + '@sentry/cli-linux-x64': 2.58.6 + '@sentry/cli-win32-arm64': 2.58.6 + '@sentry/cli-win32-i686': 2.58.6 + '@sentry/cli-win32-x64': 2.58.6 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/conventions@0.16.0': {} + + '@sentry/core@10.56.0': {} + + '@sentry/core@10.75.0': + dependencies: + '@sentry/conventions': 0.16.0 - '@sentry/nestjs@10.45.0(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)': + '@sentry/nestjs@10.56.0(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0))': dependencies: '@nestjs/common': 11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.21(@nestjs/common@11.1.21(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.21)(@nestjs/platform-express@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-nestjs-core': 0.59.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 - '@sentry/core': 10.45.0 - '@sentry/node': 10.45.0 + '@sentry/core': 10.56.0 + '@sentry/node': 10.56.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)) transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' - supports-color - '@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@16.3.1(@babel/core@8.0.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(@types/node@18.16.9)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2))': + '@sentry/nextjs@10.56.0(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(next@16.3.1(@babel/core@8.0.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(@types/node@18.16.9)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2))': dependencies: - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.40.0 - '@rollup/plugin-commonjs': 28.0.1(rollup@4.59.0) - '@sentry-internal/browser-utils': 10.45.0 - '@sentry/bundler-plugin-core': 5.1.1 - '@sentry/core': 10.45.0 - '@sentry/node': 10.45.0 - '@sentry/opentelemetry': 10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) - '@sentry/react': 10.45.0(react@19.2.4) - '@sentry/vercel-edge': 10.45.0 - '@sentry/webpack-plugin': 5.1.1(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2)) + '@rollup/plugin-commonjs': 28.0.1(rollup@4.63.3) + '@sentry-internal/browser-utils': 10.56.0 + '@sentry/bundler-plugin-core': 5.3.0 + '@sentry/core': 10.56.0 + '@sentry/node': 10.56.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)) + '@sentry/opentelemetry': 10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry/react': 10.56.0(react@19.2.4) + '@sentry/vercel-edge': 10.56.0 + '@sentry/webpack-plugin': 5.4.0(rollup@4.63.3)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2)) next: 16.3.1(@babel/core@8.0.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(@types/node@18.16.9)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) - rollup: 4.59.0 + rollup: 4.63.3 stacktrace-parser: 0.1.11 transitivePeerDependencies: - - '@opentelemetry/context-async-hooks' - '@opentelemetry/core' + - '@opentelemetry/exporter-trace-otlp-http' - '@opentelemetry/sdk-trace-base' - encoding - react - supports-color - webpack - '@sentry/node-core@10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': + '@sentry/node-core@10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': dependencies: - '@sentry/core': 10.45.0 - '@sentry/opentelemetry': 10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry/core': 10.56.0 + '@sentry/opentelemetry': 10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) import-in-the-middle: 3.0.0 optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 - '@sentry/node@10.45.0': + '@sentry/node@10.56.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0))': dependencies: - '@fastify/otel': 0.17.1(@opentelemetry/api@1.9.0) - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-amqplib': 0.60.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dataloader': 0.30.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fs': 0.32.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-generic-pool': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-kafkajs': 0.22.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-knex': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.66.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.65.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-tedious': 0.32.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-undici': 0.23.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 - '@prisma/instrumentation': 7.4.2(@opentelemetry/api@1.9.0) - '@sentry/core': 10.45.0 - '@sentry/node-core': 10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) - '@sentry/opentelemetry': 10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry-internal/server-utils': 10.56.0 + '@sentry/core': 10.56.0 + '@sentry/node-core': 10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry/opentelemetry': 10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) import-in-the-middle: 3.0.0 transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' - supports-color - '@sentry/opentelemetry@10.45.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': + '@sentry/opentelemetry@10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 - '@sentry/core': 10.45.0 + '@sentry/core': 10.56.0 - '@sentry/profiling-node@10.45.0': + '@sentry/profiling-node@10.56.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0))': dependencies: - '@sentry-internal/node-cpu-profiler': 2.2.0 - '@sentry/core': 10.45.0 - '@sentry/node': 10.45.0 + '@sentry-internal/node-cpu-profiler': 2.4.1 + '@sentry/core': 10.56.0 + '@sentry/node': 10.56.0(@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)) transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' - supports-color - '@sentry/react@10.45.0(react@19.2.4)': + '@sentry/react@10.56.0(react@19.2.4)': dependencies: - '@sentry/browser': 10.45.0 - '@sentry/core': 10.45.0 + '@sentry/browser': 10.56.0 + '@sentry/core': 10.56.0 react: 19.2.4 - '@sentry/vercel-edge@10.45.0': + '@sentry/vercel-edge@10.56.0': dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@sentry/core': 10.45.0 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@sentry/core': 10.56.0 - '@sentry/webpack-plugin@5.1.1(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2))': + '@sentry/webpack-plugin@5.4.0(rollup@4.63.3)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2))': dependencies: - '@sentry/bundler-plugin-core': 5.1.1 - uuid: 9.0.1 + '@sentry/bundler-plugins': 10.75.0(rollup@4.63.3)(webpack@5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2)) webpack: 5.105.4(@swc/core@1.5.7(@swc/helpers@0.5.13))(esbuild@0.28.2) transitivePeerDependencies: - encoding + - rollup - supports-color '@shikijs/core@3.23.0': @@ -29648,12 +29410,12 @@ snapshots: '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/estree@1.0.8': {} @@ -29845,10 +29607,6 @@ snapshots: dependencies: '@types/pg': 8.15.6 - '@types/pg-pool@2.0.7': - dependencies: - '@types/pg': 8.15.6 - '@types/pg@8.15.5': dependencies: '@types/node': 18.16.9 @@ -29858,7 +29616,7 @@ snapshots: '@types/pg@8.15.6': dependencies: '@types/node': 18.16.9 - pg-protocol: 1.13.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 '@types/prismjs@1.26.6': {} @@ -33402,6 +33160,8 @@ snapshots: dotenv@17.4.0: {} + dotenv@17.4.2: {} + draggabilly@3.0.0: dependencies: get-size: 3.0.0 @@ -35595,13 +35355,6 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 - import-in-the-middle@2.0.6: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - import-in-the-middle@3.0.0: dependencies: acorn: 8.16.0 @@ -35830,7 +35583,7 @@ snapshots: is-reference@1.2.1: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 is-regex@1.2.1: dependencies: @@ -36654,12 +36407,12 @@ snapshots: konva@10.2.0: {} - langchain@1.5.11(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + langchain@1.5.11(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/langgraph': 1.4.15(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) - '@langchain/langgraph-checkpoint': 1.1.5(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/langgraph': 1.4.15(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) + '@langchain/langgraph-checkpoint': 1.1.5(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@opentelemetry/api' @@ -36671,12 +36424,12 @@ snapshots: - ws optional: true - langchain@1.5.11(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + langchain@1.5.11(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: - '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@langchain/langgraph': 1.4.15(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) - '@langchain/langgraph-checkpoint': 1.1.5(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/core': 1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@langchain/langgraph': 1.4.15(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) + '@langchain/langgraph-checkpoint': 1.1.5(@langchain/core@1.2.11(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + langsmith: 0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@opentelemetry/api' @@ -36687,25 +36440,25 @@ snapshots: - react-dom - ws - langsmith@0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + langsmith@0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: p-queue: 6.6.2 uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) openai: 6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76) ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - langsmith@0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + langsmith@0.5.17(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: p-queue: 6.6.2 uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) openai: 6.27.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76) ws: 8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -38834,8 +38587,6 @@ snapshots: dependencies: pg: 8.23.0 - pg-protocol@1.13.0: {} - pg-protocol@1.16.0: {} pg-types@2.2.0: From c0bd447d9a2e52bae7dd2cbcbdb6e85e61fb180a Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Fri, 18 Sep 2026 16:50:45 +0700 Subject: [PATCH 07/53] fix(frontend): report React render crashes to Sentry from the crash screen global-error.tsx only reported the error when `useVariables()` returned a Sentry DSN. This component replaces the root layout, so the variables context provider is never mounted around it and the context default (an empty DSN) is what it always got. The effect returned early every time: in a production build a React render crash showed "Application error", sent no Sentry event and opened no report dialog. It went unnoticed because development builds behave differently: there React re-throws boundary-caught errors to the global error handler, so the SDK's own handler reported them and the dialog appeared. Production React does not re-throw, so this component is the only place such an error can be reported from. Errors outside rendering (timers, promises, event handlers) were never affected. The DSN guard is removed rather than rewired. Without an initialised client `captureException` is a no-op, so self-hosted installs without a DSN behave as before. The explicit `showReportDialog` call is removed too: it never ran, and the shared `beforeSend` already opens the report dialog for every captured exception, so keeping it would request the dialog twice. Testing (production build, `next start`, headless Chrome, a temporary client component that throws during render 3 seconds after load, Sentry pointed at a local capture server): - before: no event and no dialog request reach Sentry; the session replay is the only trace of the crash - after, with a DSN: exactly one event (the render error, linked to its replay) and exactly one report dialog request - after, without a DSN: nothing is sent, no dialog request, no extra JavaScript errors - the before state was observed on SDK 10.56.0 and the fix verified on 10.45.0 (this branch); the cause is in our component, not the SDK Co-Authored-By: Claude Fable 5.1 --- apps/frontend/src/app/global-error.tsx | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/apps/frontend/src/app/global-error.tsx b/apps/frontend/src/app/global-error.tsx index db4f81ebc0..720d750d16 100644 --- a/apps/frontend/src/app/global-error.tsx +++ b/apps/frontend/src/app/global-error.tsx @@ -2,31 +2,17 @@ import * as Sentry from '@sentry/nextjs'; import NextError from 'next/error'; import { useEffect } from 'react'; -import { useVariables } from '@gitroom/react/helpers/variable.context'; export default function GlobalError({ error, }: { error: Error & { digest?: string }; }) { - const { sentryDsn } = useVariables(); - useEffect(() => { - if (!sentryDsn) { - return; - } - const eventId = Sentry.captureException(error); - Sentry.showReportDialog({ - eventId, - title: 'Something broke!', - subtitle: 'Please help us fix the issue by providing some details.', - labelComments: 'What happened?', - labelName: 'Your name', - labelEmail: 'Your email', - labelSubmit: 'Send Report', - lang: 'en', - }); - + // The variables context is not mounted here (this replaces the root + // layout), so don't gate on its DSN. Without a client this is a no-op, and + // beforeSend already opens the report dialog for captured exceptions + Sentry.captureException(error); }, [error]); return ( From d727ac0b64c3152ec36a2db3662cd2e132101bbb Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Thu, 10 Sep 2026 19:25:34 +0700 Subject: [PATCH 08/53] fix(frontend): keep the 402 response readable after the payment dialog afterRequest consumed the body to show the dialog message, so callers threw 'body stream already read' on dismiss (154 events/30d on /launches in prod). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_017g3KqiuR5TT68XdqpTRbZh --- apps/frontend/src/components/layout/layout.context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/layout/layout.context.tsx b/apps/frontend/src/components/layout/layout.context.tsx index 202da58045..876daa898e 100644 --- a/apps/frontend/src/components/layout/layout.context.tsx +++ b/apps/frontend/src/components/layout/layout.context.tsx @@ -107,7 +107,7 @@ function LayoutContextInner(params: { children: ReactNode }) { if ( await deleteDialog( ( - await response.json() + await response.clone().json() ).message, 'Move to billing', 'Payment Required' From 971aa369f3c4679a91b033be48d431be6ec83634 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Thu, 10 Sep 2026 19:38:17 +0700 Subject: [PATCH 09/53] fix(frontend): tolerate a non-JSON 402 body in the payment dialog Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_017g3KqiuR5TT68XdqpTRbZh --- apps/frontend/src/components/layout/layout.context.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/layout/layout.context.tsx b/apps/frontend/src/components/layout/layout.context.tsx index 876daa898e..8aea646eb8 100644 --- a/apps/frontend/src/components/layout/layout.context.tsx +++ b/apps/frontend/src/components/layout/layout.context.tsx @@ -107,7 +107,10 @@ function LayoutContextInner(params: { children: ReactNode }) { if ( await deleteDialog( ( - await response.clone().json() + await response + .clone() + .json() + .catch(() => ({})) ).message, 'Move to billing', 'Payment Required' From 4b933cd7914c6387606baf0cfeeffa540dff383b Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Sat, 26 Sep 2026 13:01:42 +0700 Subject: [PATCH 10/53] feat(facebook): optional title for feed video posts Facebook Page videos were uploaded with the post text as description only, so they always published without a title, and the MCP/public API settings schema for Facebook offered no way to set one. FacebookDto gets an optional title string. The provider adds it to the POST /{page-id}/videos body only when it is non-empty and the post is a feed post whose first attachment is an mp4. Stories, photo posts, text posts and background presets are untouched, and posts without a title send exactly the same request as before. The editor's Facebook settings show a title input for posts and hide it for stories. Co-Authored-By: Claude Opus 5.5 --- .../providers/facebook/facebook.provider.tsx | 14 ++++++++++---- .../dtos/posts/providers-settings/facebook.dto.ts | 8 ++++++++ .../src/integrations/social/facebook.provider.ts | 5 ++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/components/new-launch/providers/facebook/facebook.provider.tsx b/apps/frontend/src/components/new-launch/providers/facebook/facebook.provider.tsx index 00b8e1225f..b45bec5a58 100644 --- a/apps/frontend/src/components/new-launch/providers/facebook/facebook.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/facebook/facebook.provider.tsx @@ -69,10 +69,16 @@ export const FacebookSettings = () => { {postCurrentType !== 'story' && ( - + <> + + + )} {presetAvailable && ( diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/facebook.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/facebook.dto.ts index f913a95b04..0b9b606822 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/facebook.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/facebook.dto.ts @@ -1,4 +1,5 @@ import { IsIn, IsOptional, IsString, ValidateIf, IsUrl } from 'class-validator'; +import { JSONSchema } from 'class-validator-jsonschema'; // Maximum characters Facebook allows on a background ("text format") post. export const FACEBOOK_PRESET_MAX_CHARS = 130; @@ -108,4 +109,11 @@ export class FacebookDto { @IsOptional() @IsString() text_format_preset_id?: string; + + @IsOptional() + @IsString() + @JSONSchema({ + description: 'Title of the video, used only for video (non-story) posts', + }) + title?: string; } diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index c52c10b6e6..93b322f6b7 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -26,7 +26,7 @@ import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorato export const META_GRAPH_API_VERSION = 'v25.0'; @Rules( - "Facebook posts can be text only, or include photos or a video. If it's a story, it must have at least one attachment (photo or video), and each media is published as a separate story." + "Facebook posts can be text only, or include photos or a video. If it's a story, it must have at least one attachment (photo or video), and each media is published as a separate story. Video posts (not stories) can carry an optional title." ) export class FacebookProvider extends SocialAbstract implements SocialProvider { identifier = 'facebook'; @@ -845,6 +845,9 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { body: JSON.stringify({ file_url: firstPost?.media?.[0]?.path!, description: firstPost.message, + ...(firstPost?.settings?.title + ? { title: firstPost.settings.title } + : {}), published: true, }), }, From cd48401e8c7ebb9db5e76f0ef842d1d7e4fcf74b Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 28 Sep 2026 11:37:44 +0700 Subject: [PATCH 11/53] feat: canva intent --- .env.example | 4 ++++ apps/backend/src/main.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.env.example b/.env.example index 68e10256b1..d91b43bc2f 100644 --- a/.env.example +++ b/.env.example @@ -51,6 +51,10 @@ STORAGE_PROVIDER="local" # Your upload directory path if you host your files locally, otherwise Cloudflare will be used. #NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY="" +# Only needed if you run your own Postiz Canva app: its origin from the Canva +# Developer Portal (Security -> Credentials), allowed by CORS on the backend. +#CANVA_APP_ORIGIN="https://app-xxxxxxxxxxx.canva-apps.com" + # Sign in with Apple (login provider) # APPLE_BUNDLE_ID: iOS app bundle id (native mobile sign-in) # APPLE_SERVICE_ID / TEAM_ID / KEY_ID / PRIVATE_KEY: web sign-in (Services ID + .p8 key) diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index b60dc7d8ea..94d75a0e0d 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -46,6 +46,10 @@ async function start() { process.env.FRONTEND_URL, 'http://localhost:6274', ...(process.env.MAIN_URL ? [process.env.MAIN_URL] : []), + // Optional: the Canva app calls the public API from its iframe origin + ...(process.env.CANVA_APP_ORIGIN + ? [process.env.CANVA_APP_ORIGIN.trim().replace(/\/+$/, '')] + : []), ], }, }); From 7229c338c9c31b1862cd263daa8087d1174d18cd Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 28 Sep 2026 12:20:08 +0700 Subject: [PATCH 12/53] feat: schema fix --- apps/backend/src/main.ts | 7 ++++++- .../nestjs-libraries/src/database/prisma/schema.prisma | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 94d75a0e0d..7f5523bde2 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -47,8 +47,13 @@ async function start() { 'http://localhost:6274', ...(process.env.MAIN_URL ? [process.env.MAIN_URL] : []), // Optional: the Canva app calls the public API from its iframe origin + // (browsers send it lowercase, e.g. https://app-aabbcc.canva-apps.com) ...(process.env.CANVA_APP_ORIGIN - ? [process.env.CANVA_APP_ORIGIN.trim().replace(/\/+$/, '')] + ? [ + process.env.CANVA_APP_ORIGIN.trim() + .replace(/\/+$/, '') + .toLowerCase(), + ] : []), ], }, diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index fe5081bbf8..328b0db42d 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -150,6 +150,7 @@ model UserOrganization { user User @relation(fields: [userId], references: [id]) @@unique([userId, organizationId]) + @@index([organizationId]) @@index([disabled]) } From 1752382c176d72afed1dc9248b4aaf7f9a85ee7d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Mon, 28 Sep 2026 12:34:12 +0700 Subject: [PATCH 13/53] feat: force token 200 --- apps/backend/src/api/routes/oauth.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/backend/src/api/routes/oauth.controller.ts b/apps/backend/src/api/routes/oauth.controller.ts index da4db28de5..369b20f2b7 100644 --- a/apps/backend/src/api/routes/oauth.controller.ts +++ b/apps/backend/src/api/routes/oauth.controller.ts @@ -55,6 +55,8 @@ export class OAuthController { } @Post('/token') + // RFC 6749 §5.1: successful token responses are 200; strict clients (Canva) reject Nest's default 201 + @HttpCode(200) async token( @Body() body: TokenExchangeDto, @Headers('authorization') authorization?: string From c033aff59fedacd24dd7d240f92827b764fe797e Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 28 Sep 2026 12:54:44 +0700 Subject: [PATCH 14/53] fix(frontend): stay on Add Channel when the 402 dialog is cancelled Return early on a 402 instead of following the billing url in its body. --- .../components/launches/add.provider.component.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index 2a4809176b..b8db5fc1a3 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -478,11 +478,13 @@ export const AddProviderComponent: FC<{ ] .filter(Boolean) .join('&'); - const { url, err } = await ( - await fetch( - `/integrations/social/${identifier}${params ? `?${params}` : ''}` - ) - ).json(); + const response = await fetch( + `/integrations/social/${identifier}${params ? `?${params}` : ''}` + ); + if (response.status === 402) { + return; + } + const { url, err } = await response.json(); if (err) { toaster.show( t( From b7e3df35a0ba9c1a6c1761f978f0df555ad6b5ab Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 28 Sep 2026 13:29:19 +0700 Subject: [PATCH 15/53] fix(frontend): stop the web3, custom-fields and extension connect flows on a 402 Same early return as the regular flow, so none of them use the billing url from the 402 body. --- .../launches/add.provider.component.tsx | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index b8db5fc1a3..d3a620dc86 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -209,13 +209,15 @@ export const CustomVariables: FC<{ }); const submit = useCallback( async (data: FieldValues) => { - const { url } = await ( - await fetch( - `/integrations/social/${identifier}${ - onboarding ? '?onboarding=true' : '' - }` - ) - ).json(); + const response = await fetch( + `/integrations/social/${identifier}${ + onboarding ? '?onboarding=true' : '' + }` + ); + if (response.status === 402) { + return; + } + const { url } = await response.json(); modals.closeAll(); gotoUrl( `/integrations/social/${identifier}?state=${url}&code=${Buffer.from( @@ -433,13 +435,15 @@ export const AddProviderComponent: FC<{ const { component: Web3Providers } = web3List.find( (item) => item.identifier === identifier )!; - const { url } = await ( - await fetch( - `/integrations/social/${identifier}${ - onboarding ? '?onboarding=true' : '' - }` - ) - ).json(); + const response = await fetch( + `/integrations/social/${identifier}${ + onboarding ? '?onboarding=true' : '' + }` + ); + if (response.status === 402) { + return; + } + const { url } = await response.json(); modal.openModal({ title: `Add ${capitalize(identifier)}`, withCloseButton: true, @@ -611,13 +615,15 @@ export const AddProviderComponent: FC<{ ); return; } - const { url } = await ( - await fetch( - `/integrations/social/${identifier}${ - onboarding ? '?onboarding=true' : '' - }` - ) - ).json(); + const response = await fetch( + `/integrations/social/${identifier}${ + onboarding ? '?onboarding=true' : '' + }` + ); + if (response.status === 402) { + return; + } + const { url } = await response.json(); modal.closeAll(); window.location.href = `/integrations/social/${identifier}?state=${url}&code=${Buffer.from( JSON.stringify(cookieResponse.cookies) From 9dbebc2ef48c0d2309735ccb96533ee701c287e6 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 28 Sep 2026 13:57:34 +0700 Subject: [PATCH 16/53] fix(frontend): don't report a failed post save as successful Check the POST /posts response; on failure keep the editor open and show the server message (402 stays silent, the payment dialog already explained it). --- .../components/new-launch/manage.modal.tsx | 28 +++++++++++++++---- i18n.lock | 1 + .../translation/locales/ar/translation.json | 1 + .../translation/locales/bn/translation.json | 1 + .../translation/locales/de/translation.json | 1 + .../translation/locales/en/translation.json | 1 + .../translation/locales/es/translation.json | 1 + .../translation/locales/fr/translation.json | 1 + .../translation/locales/he/translation.json | 1 + .../translation/locales/it/translation.json | 1 + .../translation/locales/ja/translation.json | 1 + .../translation/locales/ko/translation.json | 1 + .../translation/locales/pt/translation.json | 1 + .../translation/locales/ru/translation.json | 1 + .../translation/locales/tr/translation.json | 1 + .../translation/locales/vi/translation.json | 1 + .../translation/locales/zh/translation.json | 1 + 17 files changed, 38 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index 9053a912fa..ef639def0b 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -437,12 +437,28 @@ export const ManageModal: FC = (props) => { } if (!dummy) { - addEditSets - ? addEditSets(data) - : await fetch('/posts', { - method: 'POST', - body: JSON.stringify(data), - }); + if (addEditSets) { + addEditSets(data); + } else { + const response = await fetch('/posts', { + method: 'POST', + body: JSON.stringify(data), + }); + + if (!response.ok) { + if (response.status !== 402) { + const { message } = await response.json().catch(() => ({})); + toaster.show( + typeof message === 'string' + ? message + : t('post_save_failed', 'Could not save the post'), + 'warning' + ); + } + setLoading(false); + return; + } + } if (!addEditSets) { mutate(); diff --git a/i18n.lock b/i18n.lock index 6f5597c7db..531159ab14 100644 --- a/i18n.lock +++ b/i18n.lock @@ -686,6 +686,7 @@ checksums: repeat_post_every_label: f3e3fd4d95a0eeaf1eab0219278ae812 add_new_tag: 911360b396ef6db7a24eb53134f0f7d2 tag_name: 9368b5a047572b6051f334af5aa76819 + post_save_failed: 164475f6751549f91a5b6e7f759fbca8 post_is_too_long: 7784c6ecca2c7e58665e83e219aff80b your_post_should_have_at_least_one_character_or_one_image: e7a0d63d2931ca540ceea25c6d4df7ec internal_edit: 04ed5d507cfed7c4f348df338c106809 diff --git a/libraries/react-shared-libraries/src/translation/locales/ar/translation.json b/libraries/react-shared-libraries/src/translation/locales/ar/translation.json index 34a1030e1f..475d3c262a 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ar/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ar/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "تكرار النشر كل", "add_new_tag": "إضافة وسم جديد", "tag_name": "الاسم", + "post_save_failed": "تعذّر حفظ المنشور", "post_is_too_long": "المنشور طويل جدًا، يرجى تعديله", "your_post_should_have_at_least_one_character_or_one_image": "يجب أن يحتوي منشورك على حرف واحد على الأقل أو صورة واحدة.", "internal_edit": "تعديل داخلي", diff --git a/libraries/react-shared-libraries/src/translation/locales/bn/translation.json b/libraries/react-shared-libraries/src/translation/locales/bn/translation.json index d52209baa8..86727b1368 100644 --- a/libraries/react-shared-libraries/src/translation/locales/bn/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/bn/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "প্রতি কতদিন পর পোস্ট পুনরাবৃত্তি হবে", "add_new_tag": "নতুন ট্যাগ যোগ করুন", "tag_name": "নাম", + "post_save_failed": "পোস্ট সংরক্ষণ করা যায়নি", "post_is_too_long": "পোস্টটি খুব বড়, অনুগ্রহ করে ঠিক করুন", "your_post_should_have_at_least_one_character_or_one_image": "আপনার পোস্টে অন্তত একটি অক্ষর বা একটি ছবি থাকতে হবে।", "internal_edit": "অভ্যন্তরীণ সম্পাদনা", diff --git a/libraries/react-shared-libraries/src/translation/locales/de/translation.json b/libraries/react-shared-libraries/src/translation/locales/de/translation.json index c9de6e6b41..5a2bf72491 100644 --- a/libraries/react-shared-libraries/src/translation/locales/de/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/de/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "Beitrag wiederholen alle", "add_new_tag": "Neues Schlagwort hinzufügen", "tag_name": "Name", + "post_save_failed": "Der Beitrag konnte nicht gespeichert werden", "post_is_too_long": "Beitrag ist zu lang, bitte korrigieren", "your_post_should_have_at_least_one_character_or_one_image": "Dein Beitrag sollte mindestens ein Zeichen oder ein Bild enthalten.", "internal_edit": "Interne Bearbeitung", diff --git a/libraries/react-shared-libraries/src/translation/locales/en/translation.json b/libraries/react-shared-libraries/src/translation/locales/en/translation.json index 290bc15df7..f4e558b0e0 100644 --- a/libraries/react-shared-libraries/src/translation/locales/en/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/en/translation.json @@ -684,6 +684,7 @@ "repeat_post_every_label": "Repeat Post Every", "add_new_tag": "Add New Tag", "tag_name": "Name", + "post_save_failed": "Could not save the post", "post_is_too_long": "post is too long, please fix it", "your_post_should_have_at_least_one_character_or_one_image": "Your post should have at least one character or one image.", "internal_edit": "Internal Edit", diff --git a/libraries/react-shared-libraries/src/translation/locales/es/translation.json b/libraries/react-shared-libraries/src/translation/locales/es/translation.json index 8645878588..b4ab4e79c8 100644 --- a/libraries/react-shared-libraries/src/translation/locales/es/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/es/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "Repetir publicación cada", "add_new_tag": "Agregar nueva etiqueta", "tag_name": "Nombre", + "post_save_failed": "No se pudo guardar la publicación", "post_is_too_long": "la publicación es demasiado larga, por favor arréglala", "your_post_should_have_at_least_one_character_or_one_image": "Tu publicación debe tener al menos un carácter o una imagen.", "internal_edit": "Edición interna", diff --git a/libraries/react-shared-libraries/src/translation/locales/fr/translation.json b/libraries/react-shared-libraries/src/translation/locales/fr/translation.json index 23d93eca05..47e5a88f61 100644 --- a/libraries/react-shared-libraries/src/translation/locales/fr/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/fr/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "Répéter la publication tous les", "add_new_tag": "Ajouter un nouveau tag", "tag_name": "Nom", + "post_save_failed": "Impossible d’enregistrer l’article", "post_is_too_long": "le message est trop long, veuillez le corriger", "your_post_should_have_at_least_one_character_or_one_image": "Votre publication doit contenir au moins un caractère ou une image.", "internal_edit": "Modification interne", diff --git a/libraries/react-shared-libraries/src/translation/locales/he/translation.json b/libraries/react-shared-libraries/src/translation/locales/he/translation.json index 0ba46e14de..5bc356d11c 100644 --- a/libraries/react-shared-libraries/src/translation/locales/he/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/he/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "חזור על הפוסט כל", "add_new_tag": "הוסף תגית חדשה", "tag_name": "שם", + "post_save_failed": "לא ניתן היה לשמור את הפוסט", "post_is_too_long": "הפוסט ארוך מדי, אנא תקן אותו", "your_post_should_have_at_least_one_character_or_one_image": "הפוסט שלך צריך לכלול לפחות תו אחד או תמונה אחת.", "internal_edit": "עריכה פנימית", diff --git a/libraries/react-shared-libraries/src/translation/locales/it/translation.json b/libraries/react-shared-libraries/src/translation/locales/it/translation.json index 63f6eaf463..52bc1b4e65 100644 --- a/libraries/react-shared-libraries/src/translation/locales/it/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/it/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "Ripeti post ogni", "add_new_tag": "Aggiungi nuovo tag", "tag_name": "Nome", + "post_save_failed": "Impossibile salvare il post", "post_is_too_long": "il post è troppo lungo, per favore correggilo", "your_post_should_have_at_least_one_character_or_one_image": "Il tuo post deve contenere almeno un carattere o un'immagine.", "internal_edit": "Modifica interna", diff --git a/libraries/react-shared-libraries/src/translation/locales/ja/translation.json b/libraries/react-shared-libraries/src/translation/locales/ja/translation.json index 870166c1c5..4acd7e1af8 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ja/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ja/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "投稿を繰り返す間隔", "add_new_tag": "新しいタグを追加", "tag_name": "名前", + "post_save_failed": "投稿を保存できませんでした", "post_is_too_long": "投稿が長すぎます。修正してください", "your_post_should_have_at_least_one_character_or_one_image": "投稿には少なくとも1文字または1枚の画像が必要です。", "internal_edit": "内部編集", diff --git a/libraries/react-shared-libraries/src/translation/locales/ko/translation.json b/libraries/react-shared-libraries/src/translation/locales/ko/translation.json index e3469e4bce..36b782817a 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ko/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ko/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "게시 반복 주기", "add_new_tag": "새 태그 추가", "tag_name": "이름", + "post_save_failed": "게시물을 저장할 수 없습니다", "post_is_too_long": "게시글이 너무 깁니다. 수정해 주세요.", "your_post_should_have_at_least_one_character_or_one_image": "게시물에는 최소한 한 글자 또는 한 이미지를 포함해야 합니다.", "internal_edit": "내부 편집", diff --git a/libraries/react-shared-libraries/src/translation/locales/pt/translation.json b/libraries/react-shared-libraries/src/translation/locales/pt/translation.json index 13e301c5ea..9fbb7a651f 100644 --- a/libraries/react-shared-libraries/src/translation/locales/pt/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/pt/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "Repetir postagem a cada", "add_new_tag": "Adicionar nova tag", "tag_name": "Nome", + "post_save_failed": "Não foi possível salvar a publicação", "post_is_too_long": "a postagem está muito longa, por favor corrija", "your_post_should_have_at_least_one_character_or_one_image": "Sua postagem deve ter pelo menos um caractere ou uma imagem.", "internal_edit": "Edição interna", diff --git a/libraries/react-shared-libraries/src/translation/locales/ru/translation.json b/libraries/react-shared-libraries/src/translation/locales/ru/translation.json index c3cb03dd79..bff1e65766 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ru/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ru/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "Повторять публикацию каждые", "add_new_tag": "Добавить новый тег", "tag_name": "Название", + "post_save_failed": "Не удалось сохранить публикацию", "post_is_too_long": "пост слишком длинный, пожалуйста, исправьте его", "your_post_should_have_at_least_one_character_or_one_image": "Ваша публикация должна содержать хотя бы один символ или одно изображение.", "internal_edit": "Внутреннее редактирование", diff --git a/libraries/react-shared-libraries/src/translation/locales/tr/translation.json b/libraries/react-shared-libraries/src/translation/locales/tr/translation.json index 2ec5eaffad..b1ba29d5ce 100644 --- a/libraries/react-shared-libraries/src/translation/locales/tr/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/tr/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "Gönderiyi Her Tekrarla", "add_new_tag": "Yeni Etiket Ekle", "tag_name": "Adı", + "post_save_failed": "Gönderi kaydedilemedi", "post_is_too_long": "gönderi çok uzun, lütfen düzeltin", "your_post_should_have_at_least_one_character_or_one_image": "Gönderinizde en az bir karakter veya bir görsel olmalıdır.", "internal_edit": "Dahili Düzenleme", diff --git a/libraries/react-shared-libraries/src/translation/locales/vi/translation.json b/libraries/react-shared-libraries/src/translation/locales/vi/translation.json index 197691d3ad..8133415cb0 100644 --- a/libraries/react-shared-libraries/src/translation/locales/vi/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/vi/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "Lặp lại bài đăng mỗi", "add_new_tag": "Thêm thẻ mới", "tag_name": "Tên", + "post_save_failed": "Không thể lưu bài viết", "post_is_too_long": "bài đăng quá dài, vui lòng chỉnh sửa", "your_post_should_have_at_least_one_character_or_one_image": "Bài đăng của bạn phải có ít nhất một ký tự hoặc một hình ảnh.", "internal_edit": "Chỉnh sửa nội bộ", diff --git a/libraries/react-shared-libraries/src/translation/locales/zh/translation.json b/libraries/react-shared-libraries/src/translation/locales/zh/translation.json index c7a089b1ba..07cf4662e9 100644 --- a/libraries/react-shared-libraries/src/translation/locales/zh/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/zh/translation.json @@ -682,6 +682,7 @@ "repeat_post_every_label": "每隔...重复发布", "add_new_tag": "添加新标签", "tag_name": "名称", + "post_save_failed": "无法保存帖子", "post_is_too_long": "帖子太长,请修改", "your_post_should_have_at_least_one_character_or_one_image": "您的帖子至少应包含一个字符或一张图片。", "internal_edit": "内部编辑", From cab42b13f5cf3391f80be4a7b211b5b5de383d3c Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 28 Sep 2026 14:03:10 +0700 Subject: [PATCH 17/53] fix(frontend): stop the Sentry report dialog on the Chrome wording of the uppy progress error #2147 ignored the Safari wording of the @uppy/aws-s3 4.3.2 progress callback crash ("undefined is not an object (evaluating 'r.progress')"), but Chrome and Edge report the same throw as "Cannot read properties of undefined (reading 'progress')", which the pattern does not match, so those users still get the "Something broke" report dialog. The throw is in the plugin's own onProgress (getFile(id).progress on a file that is no longer in state), fired from the part request's load handler after our error handler cancels the remaining files. #2145 guarded our upload-success handler, not this plugin line. Sentry CLOUD-20X, 1054 events / 33 users since 2026-09-16. Co-Authored-By: Claude Opus 5.5 --- .../src/sentry/initialize.sentry.next.basic.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/react-shared-libraries/src/sentry/initialize.sentry.next.basic.ts b/libraries/react-shared-libraries/src/sentry/initialize.sentry.next.basic.ts index fca2a33b6f..8e040cafe4 100644 --- a/libraries/react-shared-libraries/src/sentry/initialize.sentry.next.basic.ts +++ b/libraries/react-shared-libraries/src/sentry/initialize.sentry.next.basic.ts @@ -17,6 +17,7 @@ export const initializeSentryBasic = (environment: string, dsn: string, extensio /^Failed to connect to MetaMask$/i, /^MetaMask extension not found$/i, /^undefined is not an object \(evaluating '\w+\.progress'\)$/i, + /^Cannot read properties of undefined \(reading 'progress'\)$/i, ]; // Browser wallet extensions (Phantom, MetaMask, etc.) reject with a plain From 6562791eac268a7bb6c7a791e4fbd16ee29c8c89 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 28 Sep 2026 14:23:04 +0700 Subject: [PATCH 18/53] fix(uploads): clamp an over-long Range end instead of answering 416 Per RFC 7233 section 2.1, a last-byte-pos at or past the file length means the rest of the file. Clamp it and serve 206; keep 416 only for a start at or past the file size, or a start after the end. Tested on local storage against a 22MB video: before this change, bytes=16777216-25165823 (final 8MB chunk overshooting the file), bytes=22041989-99999999 and bytes=0-22041990 all returned 416. After it, all three return 206 with Content-Range clamped to the last byte and bodies byte-identical to the file on disk. In-bounds ranges still return the same 206, a start past the file still returns 416, and a plain GET still returns 200 with the full file. Co-Authored-By: Claude Opus 5.5 --- .../src/app/(app)/api/uploads/[[...path]]/route.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts b/apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts index 383c3211df..a4b093e81f 100644 --- a/apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts +++ b/apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts @@ -44,12 +44,12 @@ export const GET = async ( // anything but a 206, so ignoring Range breaks their uploads. const range = /^bytes=(\d+)-(\d*)$/.exec(request.headers.get('range') || ''); const start = range ? Number(range[1]) : 0; - const end = range && range[2] ? Number(range[2]) : fileStats.size - 1; + const end = + range && range[2] + ? Math.min(Number(range[2]), fileStats.size - 1) + : fileStats.size - 1; - if ( - range && - (start >= fileStats.size || end >= fileStats.size || start > end) - ) { + if (range && (start >= fileStats.size || start > end)) { return new NextResponse(null, { status: 416, headers: { 'Content-Range': `bytes */${fileStats.size}` }, From 29478ed6deb5677d81a3cd7f238cef40de0542df Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 28 Sep 2026 15:15:20 +0700 Subject: [PATCH 19/53] fix(media): keep the rest of the upload batch when one file fails The uploader's Uppy `error` handler called `cancelAll()` for every error. Uppy core also emits `error` when a single file fails (from its `upload-error` handler), so one failed file, e.g. a transient 5xx on `/media/create-multipart-upload`, dropped every other file of the batch: finished files were saved to the media library but never attached, the rest were cancelled, and the user got no message. Cancelling the other in-flight files is also what makes @uppy/aws-s3's progress callback throw on a removed file (Sentry CLOUD-20X). The handler now returns early while the upload is still in `currentUploads` (a single file failed); core removes the upload before emitting `error` when the whole upload fails, so that path still resets the uploader. `complete` already fires with `successful` and `failed`: it now also removes the failed files, so the next upload does not silently retry them, and shows a warning toast when any file failed. Co-Authored-By: Claude Opus 5.5 --- .../src/components/media/new.uploader.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/media/new.uploader.tsx b/apps/frontend/src/components/media/new.uploader.tsx index 187f49e989..c678467a2c 100644 --- a/apps/frontend/src/components/media/new.uploader.tsx +++ b/apps/frontend/src/components/media/new.uploader.tsx @@ -222,6 +222,10 @@ export function useUppyUploader(props: { }); }); uppy2.on('error', (result) => { + // a single file failing leaves the batch running, 'complete' reports it with the rest + if (Object.keys(uppy2.getState().currentUploads).length) { + return; + } uppy2.cancelAll(); setLocked(false); props.onEnd(); @@ -232,10 +236,20 @@ export function useUppyUploader(props: { }); uppy2.on('complete', async (result) => { console.log(result); - for (const file of [...result.successful]) { + for (const file of [...result.successful, ...(result.failed || [])]) { uppy2.removeFile(file.id); } + if (result.failed?.length) { + toast.show( + t( + 'some_files_failed_to_upload', + 'Some files failed to upload, please try again' + ), + 'warning' + ); + } + props.onEnd(); // Sort results by original add order to maintain file sequence const sortedSuccessful = [...result.successful].sort((a, b) => { From 3b36d5ed24075524452927a90e6af07bfdde9c7f Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 28 Sep 2026 15:15:53 +0700 Subject: [PATCH 20/53] chore(i18n): translate the partial upload failure toast Co-Authored-By: Claude Opus 5.5 --- i18n.lock | 1 + .../src/translation/locales/ar/translation.json | 3 ++- .../src/translation/locales/bn/translation.json | 3 ++- .../src/translation/locales/de/translation.json | 3 ++- .../src/translation/locales/en/translation.json | 3 ++- .../src/translation/locales/es/translation.json | 3 ++- .../src/translation/locales/fr/translation.json | 3 ++- .../src/translation/locales/he/translation.json | 3 ++- .../src/translation/locales/it/translation.json | 3 ++- .../src/translation/locales/ja/translation.json | 3 ++- .../src/translation/locales/ko/translation.json | 3 ++- .../src/translation/locales/pt/translation.json | 3 ++- .../src/translation/locales/ru/translation.json | 3 ++- .../src/translation/locales/tr/translation.json | 3 ++- .../src/translation/locales/vi/translation.json | 3 ++- .../src/translation/locales/zh/translation.json | 3 ++- 16 files changed, 31 insertions(+), 15 deletions(-) diff --git a/i18n.lock b/i18n.lock index 531159ab14..cfab72761d 100644 --- a/i18n.lock +++ b/i18n.lock @@ -791,3 +791,4 @@ checksums: preview_comment_detach_hint: 6fa385ae985c409b511cf7adea2f950a preview_comment_text_changed: 6a4298cc0304d150e0d150689fa049f4 preview_no_comments_yet: e9bda0977d617e8ee0079feba73b3c3a + some_files_failed_to_upload: 87c15cf17373c535da85470188df68c7 diff --git a/libraries/react-shared-libraries/src/translation/locales/ar/translation.json b/libraries/react-shared-libraries/src/translation/locales/ar/translation.json index 475d3c262a..0170e62db7 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ar/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ar/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "تعذر نشر التعليق", "preview_comment_detach_hint": "التعليقات على النص المحدد تفقد تمييزها إذا تم تعديل النص لاحقًا.", "preview_comment_text_changed": "تم تغيير النص", - "preview_no_comments_yet": "لا توجد تعليقات بعد. حدد بعض النص في المنشور للتعليق عليه، أو أضف تعليقًا عامًا." + "preview_no_comments_yet": "لا توجد تعليقات بعد. حدد بعض النص في المنشور للتعليق عليه، أو أضف تعليقًا عامًا.", + "some_files_failed_to_upload": "فشل تحميل بعض الملفات، يرجى المحاولة مرة أخرى" } diff --git a/libraries/react-shared-libraries/src/translation/locales/bn/translation.json b/libraries/react-shared-libraries/src/translation/locales/bn/translation.json index 86727b1368..61e3aeeceb 100644 --- a/libraries/react-shared-libraries/src/translation/locales/bn/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/bn/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "মন্তব্যটি পোস্ট করা যায়নি", "preview_comment_detach_hint": "নির্বাচিত টেক্সটে মন্তব্য করলে, টেক্সট সম্পাদনার পর হাইলাইট হারিয়ে যেতে পারে।", "preview_comment_text_changed": "টেক্সট পরিবর্তিত হয়েছে", - "preview_no_comments_yet": "এখনও কোনো মন্তব্য নেই। পোস্টে কিছু টেক্সট নির্বাচন করে অথবা সাধারণ মন্তব্য যোগ করে মন্তব্য করুন।" + "preview_no_comments_yet": "এখনও কোনো মন্তব্য নেই। পোস্টে কিছু টেক্সট নির্বাচন করে অথবা সাধারণ মন্তব্য যোগ করে মন্তব্য করুন।", + "some_files_failed_to_upload": "কিছু ফাইল আপলোড করতে ব্যর্থ হয়েছে, অনুগ্রহ করে আবার চেষ্টা করুন" } diff --git a/libraries/react-shared-libraries/src/translation/locales/de/translation.json b/libraries/react-shared-libraries/src/translation/locales/de/translation.json index 5a2bf72491..5e0e9912c7 100644 --- a/libraries/react-shared-libraries/src/translation/locales/de/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/de/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "Kommentar konnte nicht veröffentlicht werden", "preview_comment_detach_hint": "Kommentare zu ausgewähltem Text verlieren ihre Markierung, wenn der Text später bearbeitet wird.", "preview_comment_text_changed": "Text geändert", - "preview_no_comments_yet": "Noch keine Kommentare. Wählen Sie einen Text im Beitrag aus, um ihn zu kommentieren, oder fügen Sie einen allgemeinen Kommentar hinzu." + "preview_no_comments_yet": "Noch keine Kommentare. Wählen Sie einen Text im Beitrag aus, um ihn zu kommentieren, oder fügen Sie einen allgemeinen Kommentar hinzu.", + "some_files_failed_to_upload": "Einige Dateien konnten nicht hochgeladen werden. Bitte versuchen Sie es erneut." } diff --git a/libraries/react-shared-libraries/src/translation/locales/en/translation.json b/libraries/react-shared-libraries/src/translation/locales/en/translation.json index f4e558b0e0..e5b8f2b20b 100644 --- a/libraries/react-shared-libraries/src/translation/locales/en/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/en/translation.json @@ -788,5 +788,6 @@ "preview_comment_failed": "Could not post the comment", "preview_comment_detach_hint": "Comments on selected text lose their highlight if the text is edited later.", "preview_comment_text_changed": "Text changed", - "preview_no_comments_yet": "No comments yet. Select some text in the post to comment on it, or add a general comment." + "preview_no_comments_yet": "No comments yet. Select some text in the post to comment on it, or add a general comment.", + "some_files_failed_to_upload": "Some files failed to upload, please try again" } diff --git a/libraries/react-shared-libraries/src/translation/locales/es/translation.json b/libraries/react-shared-libraries/src/translation/locales/es/translation.json index b4ab4e79c8..445cda7df6 100644 --- a/libraries/react-shared-libraries/src/translation/locales/es/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/es/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "No se pudo publicar el comentario", "preview_comment_detach_hint": "Los comentarios sobre texto seleccionado pierden su resalte si el texto se edita más tarde.", "preview_comment_text_changed": "Texto modificado", - "preview_no_comments_yet": "Aún no hay comentarios. Selecciona texto en la publicación para comentarlo o agrega un comentario general." + "preview_no_comments_yet": "Aún no hay comentarios. Selecciona texto en la publicación para comentarlo o agrega un comentario general.", + "some_files_failed_to_upload": "Algunos archivos no se pudieron subir, por favor inténtalo de nuevo" } diff --git a/libraries/react-shared-libraries/src/translation/locales/fr/translation.json b/libraries/react-shared-libraries/src/translation/locales/fr/translation.json index 47e5a88f61..9283eb92a9 100644 --- a/libraries/react-shared-libraries/src/translation/locales/fr/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/fr/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "Impossible de publier le commentaire", "preview_comment_detach_hint": "Les commentaires sur le texte sélectionné perdent leur surlignement si le texte est modifié par la suite.", "preview_comment_text_changed": "Texte modifié", - "preview_no_comments_yet": "Aucun commentaire pour le moment. Sélectionnez du texte dans la publication pour le commenter, ou ajoutez un commentaire général." + "preview_no_comments_yet": "Aucun commentaire pour le moment. Sélectionnez du texte dans la publication pour le commenter, ou ajoutez un commentaire général.", + "some_files_failed_to_upload": "Échec du téléversement de certains fichiers, veuillez réessayer." } diff --git a/libraries/react-shared-libraries/src/translation/locales/he/translation.json b/libraries/react-shared-libraries/src/translation/locales/he/translation.json index 5bc356d11c..86fe8509b7 100644 --- a/libraries/react-shared-libraries/src/translation/locales/he/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/he/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "לא ניתן היה לפרסם את התגובה", "preview_comment_detach_hint": "תגובות על טקסט נבחר מאבדות הדגשה אם הטקסט נערך מאוחר יותר.", "preview_comment_text_changed": "הטקסט השתנה", - "preview_no_comments_yet": "עדיין אין תגובות. בחר טקסט בפוסט כדי להגיב עליו, או הוסף תגובה כללית." + "preview_no_comments_yet": "עדיין אין תגובות. בחר טקסט בפוסט כדי להגיב עליו, או הוסף תגובה כללית.", + "some_files_failed_to_upload": "חלק מהקבצים לא הועלו, אנא נסה שוב" } diff --git a/libraries/react-shared-libraries/src/translation/locales/it/translation.json b/libraries/react-shared-libraries/src/translation/locales/it/translation.json index 52bc1b4e65..0fc15d80ed 100644 --- a/libraries/react-shared-libraries/src/translation/locales/it/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/it/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "Impossibile pubblicare il commento", "preview_comment_detach_hint": "I commenti su testo selezionato perdono l'evidenziazione se il testo viene modificato successivamente.", "preview_comment_text_changed": "Testo modificato", - "preview_no_comments_yet": "Nessun commento ancora. Seleziona del testo nel post per commentarlo, oppure aggiungi un commento generale." + "preview_no_comments_yet": "Nessun commento ancora. Seleziona del testo nel post per commentarlo, oppure aggiungi un commento generale.", + "some_files_failed_to_upload": "Alcuni file non sono stati caricati, riprova" } diff --git a/libraries/react-shared-libraries/src/translation/locales/ja/translation.json b/libraries/react-shared-libraries/src/translation/locales/ja/translation.json index 4acd7e1af8..638a6eaba2 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ja/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ja/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "コメントの投稿に失敗しました", "preview_comment_detach_hint": "選択テキストへのコメントは、テキストが後から編集されるとハイライトが失われます。", "preview_comment_text_changed": "テキストが変更されました", - "preview_no_comments_yet": "まだコメントはありません。投稿内のテキストを選択してコメントするか、一般的なコメントを追加してください。" + "preview_no_comments_yet": "まだコメントはありません。投稿内のテキストを選択してコメントするか、一般的なコメントを追加してください。", + "some_files_failed_to_upload": "いくつかのファイルのアップロードに失敗しました。もう一度お試しください。" } diff --git a/libraries/react-shared-libraries/src/translation/locales/ko/translation.json b/libraries/react-shared-libraries/src/translation/locales/ko/translation.json index 36b782817a..b110b712ef 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ko/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ko/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "댓글을 게시할 수 없습니다.", "preview_comment_detach_hint": "선택한 텍스트에 단 댓글은, 텍스트가 나중에 수정되면 강조가 사라집니다.", "preview_comment_text_changed": "텍스트가 변경됨", - "preview_no_comments_yet": "아직 댓글이 없습니다. 게시물에서 텍스트를 선택해 댓글을 남기거나, 일반 댓글을 추가하세요." + "preview_no_comments_yet": "아직 댓글이 없습니다. 게시물에서 텍스트를 선택해 댓글을 남기거나, 일반 댓글을 추가하세요.", + "some_files_failed_to_upload": "일부 파일을 업로드하지 못했습니다. 다시 시도해 주세요." } diff --git a/libraries/react-shared-libraries/src/translation/locales/pt/translation.json b/libraries/react-shared-libraries/src/translation/locales/pt/translation.json index 9fbb7a651f..9d30b7e99e 100644 --- a/libraries/react-shared-libraries/src/translation/locales/pt/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/pt/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "Não foi possível publicar o comentário", "preview_comment_detach_hint": "Comentários em textos selecionados perdem o destaque se o texto for editado depois.", "preview_comment_text_changed": "Texto alterado", - "preview_no_comments_yet": "Ainda não há comentários. Selecione um trecho do post para comentar ou adicione um comentário geral." + "preview_no_comments_yet": "Ainda não há comentários. Selecione um trecho do post para comentar ou adicione um comentário geral.", + "some_files_failed_to_upload": "Alguns arquivos não foram carregados, tente novamente" } diff --git a/libraries/react-shared-libraries/src/translation/locales/ru/translation.json b/libraries/react-shared-libraries/src/translation/locales/ru/translation.json index bff1e65766..12a9349727 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ru/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ru/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "Не удалось опубликовать комментарий", "preview_comment_detach_hint": "Комментарии к выделенному тексту теряют выделение, если текст позже изменяется.", "preview_comment_text_changed": "Текст изменён", - "preview_no_comments_yet": "Пока нет ни одного комментария. Выделите текст в публикации, чтобы оставить к нему комментарий, или добавьте общий комментарий." + "preview_no_comments_yet": "Пока нет ни одного комментария. Выделите текст в публикации, чтобы оставить к нему комментарий, или добавьте общий комментарий.", + "some_files_failed_to_upload": "Не удалось загрузить некоторые файлы, пожалуйста, попробуйте еще раз" } diff --git a/libraries/react-shared-libraries/src/translation/locales/tr/translation.json b/libraries/react-shared-libraries/src/translation/locales/tr/translation.json index b1ba29d5ce..f70384f398 100644 --- a/libraries/react-shared-libraries/src/translation/locales/tr/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/tr/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "Yorum paylaşılırken bir hata oluştu", "preview_comment_detach_hint": "Seçili metindeki yorumlar, metin daha sonra değiştirilirse vurgusunu kaybeder.", "preview_comment_text_changed": "Metin değişti", - "preview_no_comments_yet": "Henüz yorum yok. Yorum yapmak için gönderide bir metin seçin veya genel bir yorum ekleyin." + "preview_no_comments_yet": "Henüz yorum yok. Yorum yapmak için gönderide bir metin seçin veya genel bir yorum ekleyin.", + "some_files_failed_to_upload": "Bazı dosyalar yüklenemedi, lütfen tekrar deneyin" } diff --git a/libraries/react-shared-libraries/src/translation/locales/vi/translation.json b/libraries/react-shared-libraries/src/translation/locales/vi/translation.json index 8133415cb0..b091ae6493 100644 --- a/libraries/react-shared-libraries/src/translation/locales/vi/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/vi/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "Không thể đăng bình luận", "preview_comment_detach_hint": "Bình luận trên đoạn văn bản được chọn sẽ mất nổi bật nếu văn bản bị chỉnh sửa sau đó.", "preview_comment_text_changed": "Nội dung đã thay đổi", - "preview_no_comments_yet": "Chưa có bình luận nào. Chọn một đoạn văn bản trong bài viết để bình luận, hoặc thêm bình luận chung." + "preview_no_comments_yet": "Chưa có bình luận nào. Chọn một đoạn văn bản trong bài viết để bình luận, hoặc thêm bình luận chung.", + "some_files_failed_to_upload": "Một số tệp không tải lên được, vui lòng thử lại" } diff --git a/libraries/react-shared-libraries/src/translation/locales/zh/translation.json b/libraries/react-shared-libraries/src/translation/locales/zh/translation.json index 07cf4662e9..bbba1bd3df 100644 --- a/libraries/react-shared-libraries/src/translation/locales/zh/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/zh/translation.json @@ -786,5 +786,6 @@ "preview_comment_failed": "无法发布评论", "preview_comment_detach_hint": "如果后续编辑了所选文本,所选文本上的评论高亮将丢失。", "preview_comment_text_changed": "文本已更改", - "preview_no_comments_yet": "还没有评论。选中帖子中的某些文本以进行评论,或添加一个总体评论。" + "preview_no_comments_yet": "还没有评论。选中帖子中的某些文本以进行评论,或添加一个总体评论。", + "some_files_failed_to_upload": "部分文件上传失败,请重试" } From a4314ea1626e1e061b19712df3c47af3b1ecde83 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 28 Sep 2026 15:49:27 +0700 Subject: [PATCH 21/53] fix(frontend): remove duplicate 'use client' directive in VK provider Co-Authored-By: Claude Opus 5.5 --- .../src/components/new-launch/providers/vk/vk.provider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/src/components/new-launch/providers/vk/vk.provider.tsx b/apps/frontend/src/components/new-launch/providers/vk/vk.provider.tsx index 36e4ce8b36..45b443142a 100644 --- a/apps/frontend/src/components/new-launch/providers/vk/vk.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/vk/vk.provider.tsx @@ -1,5 +1,4 @@ 'use client'; -'use client'; import { PostComment, From 3f21703450a90c4c5422c1db3ec618e7915e8a41 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Fri, 11 Sep 2026 08:29:57 +0700 Subject: [PATCH 22/53] fix(telegram): stop logging the post text on publish --- .../src/integrations/social/telegram.provider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts index 8aa0002002..fa5947cf0a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts @@ -217,7 +217,6 @@ export class TelegramProvider extends SocialAbstract implements SocialProvider { .replace(/<\/strong>/g, '') .replace(/

(.*?)<\/p>/g, '$1\n'); - console.log(text); const processedMedia = this.processMedia(mediaFiles); // if there's no media, bot sends a text message only From d934731cb41476464b3db2b96e14dcddd66d19dd Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Fri, 11 Sep 2026 08:26:58 +0700 Subject: [PATCH 23/53] fix(facebook): retry the temporary posting-rate block (368 / 1390008) before failing --- .../src/integrations/social/facebook.provider.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index c52c10b6e6..5abb3fefa1 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -112,9 +112,11 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { }; } + // code 368 "Temporarily blocked for policies violations" (subcode + // 1390008): Meta documents it as temporary, "wait and retry the operation" if (body.indexOf('1390008') > -1) { return { - type: 'bad-body' as const, + type: 'retry' as const, value: 'You are posting too fast, please slow down', }; } From 480ee7ee47a1eb3f009cd1da843994b1149ca0b6 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 29 Sep 2026 10:57:15 +0700 Subject: [PATCH 24/53] fix(editor): stop wiping in-flight uploads when an earlier upload finishes The post editor's onUploadSuccess called uppy.clear() after appending the media. By then the uploader hook's complete handler has already removed the finished batch's files, so clear() could only hit files of another batch that was still uploading. clear() empties Uppy state without emitting file-removed, so the aws-s3 plugin never aborts that upload: every progress event of its parts throws reading .progress of the missing file (Sentry CLOUD-20X, and CLOUD-211 on Safari) and the upload never completes, so the file is silently dropped from the post. Co-Authored-By: Claude Opus 5.5 --- apps/frontend/src/components/new-launch/editor.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 5c357b7c53..0ded20eff6 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -568,7 +568,6 @@ export const Editor: FC<{ const uppy = useUppyUploader({ onUploadSuccess: (result: any) => { appendImages(result); - uppy.clear(); }, allowedFileTypes: 'image/*,video/mp4', onStart: () => {}, From b0103f73f45383845217931b1310c8894bcc347b Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 29 Sep 2026 13:00:59 +0700 Subject: [PATCH 25/53] fix(wordpress): send a Postiz User-Agent on requests to the WordPress site The connect check, post type/category/tag lookups, media upload and post create went out with Node's default "node" User-Agent, which host firewalls can treat as unidentified bot traffic. Send the same "Postiz/1.0 (+https://postiz.com)" User-Agent the Tumblr provider uses. Co-Authored-By: Claude Opus 5.5 --- .../src/integrations/social/wordpress.provider.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/nestjs-libraries/src/integrations/social/wordpress.provider.ts b/libraries/nestjs-libraries/src/integrations/social/wordpress.provider.ts index 2f9b597847..8777a7c4b2 100644 --- a/libraries/nestjs-libraries/src/integrations/social/wordpress.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/wordpress.provider.ts @@ -15,6 +15,8 @@ import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator'; import { getSsrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher'; import { string } from 'yup'; +const WORDPRESS_USER_AGENT = 'Postiz/1.0 (+https://postiz.com)'; + export class WordpressProvider extends SocialAbstract implements SocialProvider @@ -114,6 +116,7 @@ export class WordpressProvider response = await fetch(`${domain}/wp-json/wp/v2/users/me`, { headers: { Authorization: `Basic ${auth}`, + 'User-Agent': WORDPRESS_USER_AGENT, }, // @ts-ignore - undici-only option; blocks SSRF to internal IPs dispatcher: getSsrfSafeDispatcher(), @@ -212,6 +215,7 @@ export class WordpressProvider const response = await fetch(`${body.domain}${path}`, { headers: { Authorization: `Basic ${auth}`, + 'User-Agent': WORDPRESS_USER_AGENT, }, // @ts-ignore - undici-only option; blocks SSRF to internal IPs dispatcher: getSsrfSafeDispatcher(), @@ -312,6 +316,7 @@ export class WordpressProvider .split('/') .pop()}"`, 'Content-Type': blob.type, + 'User-Agent': WORDPRESS_USER_AGENT, }, body: blob, }) @@ -334,6 +339,7 @@ export class WordpressProvider headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json', + 'User-Agent': WORDPRESS_USER_AGENT, }, method: 'POST', body: JSON.stringify({ From 881ac768f09529df9a23cb5d22d7b8f0e7936cc2 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Sun, 30 Aug 2026 11:23:18 +0700 Subject: [PATCH 26/53] fix(tiktok): send video_cover_timestamp_ms on Direct Post video init Restores the cover-frame control lost when TikTok moved from PULL_FROM_URL to FILE_UPLOAD; the field now goes in post_info per TikTok's current schema. --- .../src/integrations/social/tiktok.provider.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 63612dd2c3..5d048f69e5 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -583,6 +583,12 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { auto_add_music: firstPost.settings.autoAddMusic === 'yes', } : {}), + ...(!isPhoto && firstPost?.media?.[0]?.thumbnailTimestamp + ? { + video_cover_timestamp_ms: + firstPost?.media?.[0]?.thumbnailTimestamp, + } + : {}), }, }; } From 148cc961ca374b92246b4b26412399c1b94a4a59 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 29 Sep 2026 14:12:40 +0700 Subject: [PATCH 27/53] fix(tiktok): send a 0ms cover timestamp explicitly --- .../nestjs-libraries/src/integrations/social/tiktok.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 5d048f69e5..01ac1c0ddd 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -583,7 +583,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { auto_add_music: firstPost.settings.autoAddMusic === 'yes', } : {}), - ...(!isPhoto && firstPost?.media?.[0]?.thumbnailTimestamp + ...(!isPhoto && firstPost?.media?.[0]?.thumbnailTimestamp != null ? { video_cover_timestamp_ms: firstPost?.media?.[0]?.thumbnailTimestamp, From 3205ce10e736cf95794993b2dad887d1d1d75108 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 29 Sep 2026 14:31:08 +0700 Subject: [PATCH 28/53] Revert "feat(mcp): accept { path, thumbnail } attachments for video covers" This reverts commit e5f6fff3384386a6e53eb75ae5ecbbf6afb231db. --- .../chat/tools/integration.schedule.post.ts | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts index ec4a007ebd..9fa08a8e0a 100644 --- a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts +++ b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts @@ -27,19 +27,6 @@ const attachmentUrl = z message: validUrlExtension.defaultMessage({} as any), }); -// Attachments are either a plain URL string or { path, thumbnail? } where -// thumbnail is an image URL (jpeg/png) used as the video's thumbnail / cover. -const attachment = z.union([ - attachmentUrl, - z.object({ - path: attachmentUrl, - thumbnail: attachmentUrl.optional(), - }), -]); - -const toMedia = (a: string | { path: string; thumbnail?: string }) => - typeof a === 'string' ? { path: a } : { path: a.path, thumbnail: a.thumbnail }; - @Injectable() export class IntegrationSchedulePostTool implements AgentToolInterface { constructor( @@ -108,10 +95,8 @@ If validation fails, the result contains output.errors describing what to fix; t "The content of the post, HTML, Each line must be wrapped in

here is the possible tags: h1, h2, h3, u, strong, li, ul, p (you can't have u and strong together)" ), attachments: z - .array(attachment) - .describe( - 'The media of the post: either an uploaded media URL string, or { path, thumbnail } where path is the uploaded video URL and thumbnail is the path of an uploaded jpeg/png (returned by uploadFromUrlTool) used as the video thumbnail / cover (e.g. Instagram Reel cover)' - ), + .array(attachmentUrl) + .describe('The image of the post (URLS)'), }) ) .describe( @@ -185,7 +170,9 @@ If validation fails, the result contains output.errors describing what to fix; t settings, value: platform.postsAndComments.map((p: any) => ({ content: p.content, - image: (p.attachments || []).map(toMedia), + image: (p.attachments || []).map((path: string) => ({ + path, + })), })), }, ] @@ -257,9 +244,9 @@ If validation fails, the result contains output.errors describing what to fix; t content: p.content, id: makeId(10), delay: 0, - image: p.attachments.map((a: any) => ({ + image: p.attachments.map((p: any) => ({ id: makeId(10), - ...toMedia(a), + path: p, })), })), }, From 8e09aeb4a5d65f0bc71fb076948c66dc466fa668 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 29 Sep 2026 15:19:08 +0700 Subject: [PATCH 29/53] fix(moltbook): keep submolt optional in the settings DTO Wiring MoltbookDto made submolt required at validation, which would reject the API/MCP callers that omit it today; the provider already falls back to 'general' for a missing or empty submolt. Keep it optional and only reject values that are not strings. Co-Authored-By: Claude Fable 5.1 --- .../src/dtos/posts/providers-settings/moltbook.dto.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/moltbook.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/moltbook.dto.ts index 2d69779715..47ab88cc82 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/moltbook.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/moltbook.dto.ts @@ -1,8 +1,7 @@ -import { IsDefined, IsString, MinLength } from 'class-validator'; +import { IsOptional, IsString } from 'class-validator'; export class MoltbookDto { - @MinLength(1) - @IsDefined() + @IsOptional() @IsString() - submolt: string; + submolt?: string; } From 2e1b6ebd62f0eda0d3ab0f6724440b0d77f619fb Mon Sep 17 00:00:00 2001 From: Enno Gelhaus Date: Tue, 29 Sep 2026 14:44:55 +0200 Subject: [PATCH 30/53] feat(sponsors): add rapidproxy to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54a9d9d9d9..1b21e0a0d8 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,8 @@ | [Hostinger](https://www.hostinger.com/vps/docker/postiz?ref=postiz) | Hostinger | Hostinger is on a mission to make online success possible for anyone – from developers to aspiring bloggers and business owners | | [Virlo](https://dev.virlo.ai/?ref=postiz) | Virlo | Virlo is the #1 social media trend spotting and all-in-one GTM tool for teams leveraging short-form video | | [ChatbotX](https://chatbotx.io/?ref=postiz) | ChatbotX | The ManyChat alternative that you can self-host, white-label, and resell to your clients. Bring your own OpenClaw, Hermes, or Claude agents! | +| [RapidProxy](https://www.rapidproxy.io/?ref=postiz) | WP-PROXY-THUMBNAIL-41 | RapidProxy provides 90M+ residential IPs for social media, browser automation, and AI workflows, with smart rotation and stable sessions. From $0.55/GB; use RAPID10 for 10% off. -![Bronze Tier](https://opencollective.com/postiz/tiers/main-repository-bronze-tier.svg?avatarHeight=36&width=600&button=false) # Intro From 2ebd93e7210c304f47b1f25b87bf3659d145991a Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 29 Sep 2026 19:58:43 +0700 Subject: [PATCH 31/53] fix(frontend): close the edit modal instead of crashing when the post's channel is not loaded Opening an existing post looks its channel up in the channel list the modal was given. When the channel is missing from that list, the modal still called addOrRemoveSelectedIntegration(undefined, ...), the store appended { integration: undefined }, and the next lookup in addInternalValue read `.id` of undefined. React unmounted the page and the user got "Application error" (seen in production on 2026-09-29 as "Cannot read properties of undefined (reading 'id')" from the calendar). When the existing post's channel is not found, the modal now shows a warning toast with the existing "try to refresh the page" message and closes itself. Guarding only the lookup is not enough: the modal renders nothing without a selected channel, which left an empty overlay that Escape and outside clicks cannot close. The same missing check is added to the set branch, as the selected-channels branch already had it. This covers every caller of the modal (calendar and agent chat). Why the channel was missing from the list in the production case is not known; the channel itself was active and in the user's only organization. Co-Authored-By: Claude Fable 5.1 --- .../components/new-launch/add.edit.modal.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/new-launch/add.edit.modal.tsx b/apps/frontend/src/components/new-launch/add.edit.modal.tsx index 7de3d565d9..a7d016f475 100644 --- a/apps/frontend/src/components/new-launch/add.edit.modal.tsx +++ b/apps/frontend/src/components/new-launch/add.edit.modal.tsx @@ -9,6 +9,9 @@ import { Integrations } from '@gitroom/frontend/components/launches/calendar.con import { useShallow } from 'zustand/react/shallow'; import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data'; import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone'; +import { useModals } from '@gitroom/frontend/components/layout/new-modal'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; export interface AddEditModalProps { dummy?: boolean; @@ -68,6 +71,9 @@ export const AddEditModal: FC = (props) => { export const AddEditModalInner: FC = (props) => { const existingData = useExistingData(); + const modal = useModals(); + const toaster = useToaster(); + const t = useT(); const { addOrRemoveSelectedIntegration, selectedIntegrations, integrations } = useLaunchStore( useShallow((state) => ({ @@ -84,7 +90,9 @@ export const AddEditModalInner: FC = (props) => { const integration = integrations.find( (i) => i.id === post.integration.id ); - addOrRemoveSelectedIntegration(integration, post.settings); + if (integration) { + addOrRemoveSelectedIntegration(integration, post.settings); + } } } } @@ -93,6 +101,17 @@ export const AddEditModalInner: FC = (props) => { const integration = integrations.find( (i) => i.id === existingData.integration ); + if (!integration) { + toaster.show( + t( + 'we_are_experiencing_some_difficulty_try_to_refresh_the_page', + 'We are experiencing some difficulty, try to refresh the page' + ), + 'warning' + ); + modal.closeAll(); + return; + } addOrRemoveSelectedIntegration(integration, existingData.settings); } From 22f266aa24ca63a4c6a3bd3c766dfce079f2272e Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 29 Sep 2026 19:58:59 +0700 Subject: [PATCH 32/53] fix(media): fail the upload step when the backend returns an error fetchUploadApiEndpoint returned res.json() for every response. The app's fetch wrapper never throws on HTTP errors, and @uppy/aws-s3 treats whatever the completeMultipartUpload callback returns as a successful upload (it emits upload-success with status 200). So when /media/complete-multipart-upload answered 400 or 500, the error body became a "successful" file without `saved`, the uploader handed `undefined` to the editor, and the post preview crashed reading `.path` of it (seen in production on 2026-09-29 as "Cannot read properties of undefined (reading 'path')" and "(reading 'id')" in the post editor). The helper now throws on a non-OK response, as impersonate.tsx already does for the same reason. Uppy then marks the file as failed and aborts its multipart upload, the uploader's complete handler drops it and shows the "Some files failed to upload" warning, and the other files of the batch are still attached. This applies to all five multipart callbacks; none of them relied on reading an error body. Co-Authored-By: Claude Fable 5.1 --- libraries/react-shared-libraries/src/helpers/uppy.upload.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/react-shared-libraries/src/helpers/uppy.upload.ts b/libraries/react-shared-libraries/src/helpers/uppy.upload.ts index acece4e7d7..07dd087894 100644 --- a/libraries/react-shared-libraries/src/helpers/uppy.upload.ts +++ b/libraries/react-shared-libraries/src/helpers/uppy.upload.ts @@ -119,6 +119,11 @@ const fetchUploadApiEndpoint = async ( 'Content-Type': 'application/json', }, }); + // customFetch does not throw on HTTP errors, and Uppy treats whatever this + // returns as a successful step + if (!res.ok) { + throw new Error(await res.text().catch(() => '')); + } return res.json(); }; From 2800807930714c40b4641c489226396831e5d95f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 30 Sep 2026 10:18:56 +0700 Subject: [PATCH 33/53] redeploy --- apps/backend/src/api/routes/enterprise.controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/backend/src/api/routes/enterprise.controller.ts b/apps/backend/src/api/routes/enterprise.controller.ts index eebc630054..c14eebdb52 100644 --- a/apps/backend/src/api/routes/enterprise.controller.ts +++ b/apps/backend/src/api/routes/enterprise.controller.ts @@ -20,8 +20,7 @@ export class EnterpriseController { private verifyEnterpriseToken(params: string): T { const payload = AuthService.verifyJWT(params) as any; if ( - !payload || - typeof payload !== 'object' || + !payload || typeof payload !== 'object' || 'providerName' in payload || // login token (full User row) 'orgId' in payload || // team invite token 'expires' in payload // password reset token From 538f3e6fd8e340e2f5a48790676663cdc6fc8474 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 30 Sep 2026 11:01:22 +0700 Subject: [PATCH 34/53] fix(pinterest): map the "not permitted to access that resource" pin error Pinterest answers the pin create call with code 29 "You are not permitted to access that resource." when the connected account cannot write to the requested board (a board it does not own, a secret or group board without access). The message had no mapping, so the post failed as "Unknown Error". Map it to a bad-body error that names the board as the cause. Co-Authored-By: Claude Fable 5.1 --- .../src/integrations/social/pinterest.provider.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts index 40251c71d2..2c52e1b827 100644 --- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts @@ -144,6 +144,13 @@ export class PinterestProvider value: 'The specified board was not found. Please check the board ID.', }; } + if (body.indexOf('You are not permitted to access that resource') > -1) { + return { + type: 'bad-body' as const, + value: + 'The connected Pinterest account is not permitted to post to this board. Please check the board ID and that the account owns or can write to the board.', + }; + } if (body.indexOf('cover_image_url or cover_image_content_type') > -1) { return { type: 'bad-body' as const, From 852ac517a7fa4e4c09dd576691ec01f15fe24a61 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 30 Sep 2026 11:08:46 +0700 Subject: [PATCH 35/53] feat: more info --- .../v1/public.integrations.controller.ts | 19 +++++++++++++++++++ .../services/auth/public.auth.middleware.ts | 3 +++ .../src/user/oauth.user.id.from.request.ts | 8 ++++++++ 3 files changed, 30 insertions(+) create mode 100644 libraries/nestjs-libraries/src/user/oauth.user.id.from.request.ts diff --git a/apps/backend/src/public-api/routes/v1/public.integrations.controller.ts b/apps/backend/src/public-api/routes/v1/public.integrations.controller.ts index 6a64c0752e..99ac591814 100644 --- a/apps/backend/src/public-api/routes/v1/public.integrations.controller.ts +++ b/apps/backend/src/public-api/routes/v1/public.integrations.controller.ts @@ -17,6 +17,7 @@ import { streamUploadOptions } from '@gitroom/nestjs-libraries/upload/multer.str import { ApiTags } from '@nestjs/swagger'; import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; import { GetIncludeDeletedFromRequest } from '@gitroom/nestjs-libraries/user/include.deleted.from.request'; +import { GetOAuthUserIdFromRequest } from '@gitroom/nestjs-libraries/user/oauth.user.id.from.request'; import { Organization } from '@prisma/client'; import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; @@ -229,6 +230,24 @@ export class PublicIntegrationsController { return { connected: true }; } + // `user` is only known for OAuth app tokens; an API key belongs to the + // whole organization + @Get('/me') + async getMe( + @GetOrgFromRequest() org: Organization, + @GetOAuthUserIdFromRequest() userId?: string + ) { + Sentry.metrics.count('public_api-request', 1); + const user = userId ? await this._usersService.getPersonal(userId) : null; + + return { + organization: { id: org.id, name: org.name }, + user: user + ? { id: user.id, name: user.name, picture: user.picture?.path || null } + : null, + }; + } + @Get('/groups') async listGroups(@GetOrgFromRequest() org: Organization) { Sentry.metrics.count('public_api-request', 1); diff --git a/apps/backend/src/services/auth/public.auth.middleware.ts b/apps/backend/src/services/auth/public.auth.middleware.ts index 7cbcee3868..526757b684 100644 --- a/apps/backend/src/services/auth/public.auth.middleware.ts +++ b/apps/backend/src/services/auth/public.auth.middleware.ts @@ -44,6 +44,9 @@ export class PublicAuthMiddleware implements NestMiddleware { } org = authorization.organization; + // The user who approved the OAuth app, so /me can show who is connected + // @ts-ignore + req.oauthUserId = authorization.userId; } else { org = await this._organizationService.getOrgByApiKey(auth); if (!org) { diff --git a/libraries/nestjs-libraries/src/user/oauth.user.id.from.request.ts b/libraries/nestjs-libraries/src/user/oauth.user.id.from.request.ts new file mode 100644 index 0000000000..5f578c4a90 --- /dev/null +++ b/libraries/nestjs-libraries/src/user/oauth.user.id.from.request.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const GetOAuthUserIdFromRequest = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.oauthUserId as string | undefined; + } +); From da7f7a18787df41bc7499a0d3a4d993abafbcc81 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 30 Sep 2026 11:19:37 +0700 Subject: [PATCH 36/53] fix(tiktok): resolve the public post id for FILE_UPLOAD publish ids in post analytics The analytics resolution step only matched v_pub_url publish ids, which was every id when it was written (videos were PULL_FROM_URL). Since videos moved to FILE_UPLOAD the ids are v_pub_file~..., so the video query was sent with the publish id and returned nothing. Match any _pub_ publish id, like the business provider already does. Co-Authored-By: Claude Fable 5.1 --- .../src/integrations/social/tiktok.provider.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 01ac1c0ddd..b33744a5c6 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -1141,7 +1141,10 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { ): Promise { const today = dayjs().format('YYYY-MM-DD'); - if (postId.indexOf('v_pub_url') > -1) { + // Posts whose public id was not available yet when publishing keep the + // publish_id (v_pub_url~... / v_pub_file~...) as releaseId - resolve it + // to the public post id first. + if (postId.indexOf('_pub_') > -1) { const post = await ( await fetch( 'https://open.tiktokapis.com/v2/post/publish/status/fetch/', From 634c6da17ac1c64407d8def5184f34145835839b Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 30 Sep 2026 11:59:06 +0700 Subject: [PATCH 37/53] fix(tiktok): keep the full public post id instead of the JSON.parse-rounded value TikTok returns publicaly_available_post_id as an int64. Parsing the status response with .json() rounds it past 2^53, so every stored release id and video URL lost its last digits, and the analytics video query asked for a video that does not exist. Extract the digits from the raw body before parsing, in both checkPostStatus and postAnalytics, and send the id to the video query as a string. Co-Authored-By: Claude Fable 5.1 --- .../integrations/social/tiktok.provider.ts | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index b33744a5c6..c40bec89d0 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -448,8 +448,9 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { integration: Integration ): Promise { let post: any; + let publicPostId: string | undefined; try { - post = await ( + const raw = await ( await this.fetch( 'https://open.tiktokapis.com/v2/post/publish/status/fetch/', { @@ -466,7 +467,8 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { 0, true ) - ).json(); + ).text(); + ({ post, publicPostId } = this.parsePublishStatus(raw)); } catch (err) { if (err instanceof RefreshToken || err instanceof Disconnect) { throw err; @@ -478,7 +480,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { return { status: 'pending', pendingData }; } - const { status, publicaly_available_post_id } = post?.data || {}; + const { status } = post?.data || {}; if (status === 'SEND_TO_USER_INBOX') { return { @@ -489,16 +491,12 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { } if (status === 'PUBLISH_COMPLETE') { - // an empty array is truthy, so index it once and branch on the value - const publicPostId = publicaly_available_post_id?.[0]; - return { status: 'completed', releaseURL: !publicPostId ? `https://www.tiktok.com/@${integration.profile}` : `https://www.tiktok.com/@${integration.profile}/video/${publicPostId}`, - // TikTok returns the id as a number, releaseId in the db is a string - postId: !publicPostId ? pendingData.publishId : String(publicPostId), + postId: !publicPostId ? pendingData.publishId : publicPostId, }; } @@ -515,6 +513,15 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { return { status: 'pending', pendingData }; } + // TikTok returns publicaly_available_post_id as an int64, which JSON.parse + // rounds past 2^53 - pull the digits out of the raw body before parsing. + private parsePublishStatus(raw: string) { + const [, publicPostId] = + raw.match(/"publicaly_available_post_id"\s*:\s*\[\s*"?(\d+)/) || []; + + return { post: JSON.parse(raw), publicPostId }; + } + // UPLOAD does not publish - it only drops the media into the user's TikTok // inbox - so only an explicit UPLOAD selects it. A missing value (drafts, or // any caller that skipped the setting) publishes instead of silently landing @@ -1145,27 +1152,29 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { // publish_id (v_pub_url~... / v_pub_file~...) as releaseId - resolve it // to the public post id first. if (postId.indexOf('_pub_') > -1) { - const post = await ( - await fetch( - 'https://open.tiktokapis.com/v2/post/publish/status/fetch/', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - publish_id: postId, - }), - } - ) - ).json(); + const { publicPostId } = this.parsePublishStatus( + await ( + await fetch( + 'https://open.tiktokapis.com/v2/post/publish/status/fetch/', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + publish_id: postId, + }), + } + ) + ).text() + ); - if (!post?.data?.publicaly_available_post_id?.[0]) { + if (!publicPostId) { return []; } - postId = post.data.publicaly_available_post_id[0]; + postId = publicPostId; } try { From 80dacd71e04139bef3c97e6b363ec5e013d108bd Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 1 Sep 2026 16:18:44 +0700 Subject: [PATCH 38/53] feat(groups): rename a channel group from the channel menu Hover pencil on the group header opens a small rename modal backed by a new PUT /integrations/customers/:id (org-scoped, duplicate name rejected). --- .../src/api/routes/integrations.controller.ts | 10 ++ .../launches/launches.component.tsx | 103 +++++++++++++++++- i18n.lock | 4 + .../integrations/integration.repository.ts | 22 ++++ .../integrations/integration.service.ts | 14 +++ .../dtos/integrations/customer.name.dto.ts | 7 ++ .../translation/locales/ar/translation.json | 4 + .../translation/locales/bn/translation.json | 4 + .../translation/locales/de/translation.json | 4 + .../translation/locales/en/translation.json | 4 + .../translation/locales/es/translation.json | 4 + .../translation/locales/fr/translation.json | 4 + .../translation/locales/he/translation.json | 4 + .../translation/locales/it/translation.json | 4 + .../translation/locales/ja/translation.json | 4 + .../translation/locales/ko/translation.json | 4 + .../translation/locales/pt/translation.json | 4 + .../translation/locales/ru/translation.json | 4 + .../translation/locales/tr/translation.json | 4 + .../translation/locales/vi/translation.json | 4 + .../translation/locales/zh/translation.json | 4 + 21 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 libraries/nestjs-libraries/src/dtos/integrations/customer.name.dto.ts diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index 3b719f2a62..46acab6c4b 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -20,6 +20,7 @@ import { ApiTags } from '@nestjs/swagger'; import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto'; +import { CustomerNameDto } from '@gitroom/nestjs-libraries/dtos/integrations/customer.name.dto'; import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto'; import { Disconnect, @@ -66,6 +67,15 @@ export class IntegrationsController { return this._integrationService.customers(org.id); } + @Put('/customers/:id') + async updateCustomerName( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string, + @Body() body: CustomerNameDto + ) { + return this._integrationService.updateCustomerName(org.id, id, body.name); + } + @Put('/:id/group') async updateIntegrationGroup( @GetOrgFromRequest() org: Organization, diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index bb8d9ce6d7..51554190ba 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -30,6 +30,8 @@ import { useIntegrationList } from '@gitroom/frontend/components/launches/helper import useCookie from 'react-use-cookie'; import { Onboarding } from '@gitroom/frontend/components/onboarding/onboarding'; import { useModals } from '@gitroom/frontend/components/layout/new-modal'; +import { Input } from '@gitroom/react/form/input'; +import { Button } from '@gitroom/react/form/button'; export const SVGLine = () => { return ( @@ -111,6 +113,70 @@ export const OpenClose: FC<{ ); }; +const EditGroupNameModal: FC<{ + name: string; + id: string; + close: () => void; + resolve: (value: string) => void; +}> = (props) => { + const t = useT(); + const { close, name, id, resolve } = props; + const fetch = useFetch(); + const toaster = useToaster(); + const [groupName, setGroupName] = useState(name); + const save = useCallback(async () => { + const response = await fetch(`/integrations/customers/${id}`, { + method: 'PUT', + body: JSON.stringify({ name: groupName }), + }); + if (!response.ok) { + const { message } = await response.json().catch(() => ({} as any)); + toaster.show( + (Array.isArray(message) ? message[0] : message) || + t('could_not_save_group', 'Could not save the group.'), + 'warning' + ); + return; + } + resolve(groupName); + close(); + }, [groupName, id]); + return ( +

+ setGroupName(e.target.value)} + /> + +
+ ); +}; + +const EditPencil = () => { + return ( + + + + ); +}; + export const MenuGroupComponent: FC< MenuComponentInterface & { changeItemGroup: (id: string, group: string) => void; @@ -137,9 +203,36 @@ export const MenuGroupComponent: FC< changeItemGroup, collapsed, } = props; + const t = useT(); + const modals = useModals(); + const toaster = useToaster(); const [isOpen, setIsOpen] = useState( !!+(localStorage.getItem(group.name + '_isOpen') || '1') ); + const editGroupName = useCallback( + async (e: any) => { + e.stopPropagation(); + const val: string | undefined = await new Promise((resolve) => { + modals.openModal({ + title: t('edit_group_name', 'Edit group name'), + children: (close) => ( + + ), + }); + }); + if (!val) { + return; + } + await mutate(); + toaster.show(t('group_updated', 'Group Updated'), 'success'); + }, + [group.name, group.id, mutate, modals, t] + ); const changeOpenClose = useCallback( (e: any) => { setIsOpen(!isOpen); @@ -177,7 +270,7 @@ export const MenuGroupComponent: FC< )} {!!group.name && (
@@ -194,6 +287,14 @@ export const MenuGroupComponent: FC< > {group.name}
+ {!collapsed && ( +
+ +
+ )}
)}
Date: Wed, 30 Sep 2026 13:44:06 +0700 Subject: [PATCH 39/53] fix(groups): settle the rename promise when the modal is dismissed Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0153xf1m3VSQnd9QrMLhDikq --- apps/frontend/src/components/launches/launches.component.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index 51554190ba..03be0815db 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -215,6 +215,7 @@ export const MenuGroupComponent: FC< const val: string | undefined = await new Promise((resolve) => { modals.openModal({ title: t('edit_group_name', 'Edit group name'), + onClose: () => resolve(undefined), children: (close) => ( Date: Fri, 28 Aug 2026 09:40:38 +0700 Subject: [PATCH 40/53] docs(claude): add shared-query and persisted-data change rules --- CLAUDE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index dab2318fa1..a13a751a94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,4 +72,7 @@ const useCommunity = () => { - When you finished running, run another agents that matches the new code with the existing system code, to see that it looks similar and is not a weird pattern. - Workflows files can never be changed if they are already in origin/main, because changing a workflow will fail all its activities, instead create a new workflow with the version, and everywhere the workflow being called, change it to the new workflow version. - Workflows activities parameters cannot be changed, as it will break the workflow, if we need to change the parameters, if we need to change the parameters, we need to create a new activity with the new parameters, and then create a new workflow that uses the new activity. -- Code must always be generic, there can't be a way that a specific logic, let's say facebook or instagram, appear in a file that use a generic logic, instead, we need to edit the interface of the provider, add another function, and then generically call it from the generic code, and then implement the specific logic in the provider implementation. we can't have something like if(facebookProvider) {} inside a non facebook provider file. +- Code must always be generic, there can't be a way that a specific logic, let's say facebook or instagram, appear in a file that use a generic logic, instead, we need to edit the interface of the provider, add another function, and then generically call it from the generic code, and then implement the specific logic in the provider implementation. we can't have something like if(facebookProvider) {} inside a non facebook provider file. +- Before adding a field to a shared repository/service `select` or changing a shared method's return shape, grep for all its consumers (frontend, public API, MCP/agent tools, orchestrator, webhooks) and confirm the change is intended for each. +- Exposing a stored field to a new external surface (public API, MCP, webhooks) is a data-exposure decision — ask first, and check what existing production rows hold for that column before shipping it. +- Never silently change the meaning or format of a value persisted in an existing DB column; that affects every reader and all historical rows, so ask first. \ No newline at end of file From bcbc1195a75f3293475c36dad027ab7da1928799 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 2 Sep 2026 10:16:43 +0700 Subject: [PATCH 41/53] fix(tiktok): resolve and persist the public post id for analytics TikTok only exposes the public post id after moderation, so posts kept the publish id as releaseId and the profile URL as releaseURL, and per-post analytics returned []. Add an optional resolveReleaseId provider hook that checkPostAnalytics calls before postAnalytics, persisting the resolved id + permalink. Both TikTok providers implement it, match any publish id prefix, and read the int64 id from the raw body so it is not rounded by JSON.parse. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VcaeTzorDM71yKNf9h3Fo4 --- .../database/prisma/posts/posts.repository.ts | 18 ++++ .../database/prisma/posts/posts.service.ts | 20 ++++- .../social/social.integrations.interface.ts | 5 ++ .../social/tiktok.business.provider.ts | 85 ++++++++++++------- .../integrations/social/tiktok.provider.ts | 70 ++++++++------- 5 files changed, 135 insertions(+), 63 deletions(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index 51056e57f0..2d24173e26 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -430,6 +430,24 @@ export class PostsRepository { }); } + updateResolvedRelease( + id: string, + orgId: string, + releaseId: string, + releaseURL: string + ) { + return this._post.model.post.update({ + where: { + id, + organizationId: orgId, + }, + data: { + releaseId, + releaseURL, + }, + }); + } + async changeState(id: string, state: State, err?: any, body?: any) { const update = await this._post.model.post.update({ where: { diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 31ecc48bf3..6a9c88a0be 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -214,10 +214,28 @@ export class PostsService { // } try { + let releaseId = post.releaseId; + if (integrationProvider.resolveReleaseId) { + const resolved = await integrationProvider.resolveReleaseId( + getIntegration.token, + releaseId, + getIntegration + ); + if (resolved && resolved.postId !== releaseId) { + await this._postRepository.updateResolvedRelease( + post.id, + orgId, + resolved.postId, + resolved.releaseURL + ); + releaseId = resolved.postId; + } + } + const loadAnalytics = await integrationProvider.postAnalytics( getIntegration.internalId, getIntegration.token, - post.releaseId, + releaseId, date ); await ioRedis.set( diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index a8a29c6004..5b6a7a94a8 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -34,6 +34,11 @@ export interface IAuthenticator { postId: string, fromDate: number, ): Promise; + resolveReleaseId?( + accessToken: string, + releaseId: string, + integration: Integration + ): Promise<{ postId: string; releaseURL: string } | undefined>; // Final id + URL to persist when the stored releaseId is still a publish id, undefined when nothing to resolve (yet) changeNickname?( id: string, accessToken: string, diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.business.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.business.provider.ts index 3fdaeb3f7c..f3c78fcedb 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.business.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.business.provider.ts @@ -425,9 +425,10 @@ export class TiktokBusinessProvider pendingData: { publishId: string }, integration: Integration ): Promise { + let body: string; let post: any; try { - post = await ( + body = await ( await this.fetch( `${this.baseUrl}/business/publish/status/?business_id=${encodeURIComponent( integration.internalId @@ -442,7 +443,8 @@ export class TiktokBusinessProvider 0, true ) - ).json(); + ).text(); + post = JSON.parse(body); } catch (err) { if (err instanceof RefreshToken || err instanceof Disconnect) { throw err; @@ -469,7 +471,7 @@ export class TiktokBusinessProvider return { status: 'pending', pendingData }; } - const { status, post_ids, reason } = post?.data || {}; + const { status, reason } = post?.data || {}; if (status === 'SEND_TO_USER_INBOX') { return { @@ -482,15 +484,15 @@ export class TiktokBusinessProvider if (status === 'PUBLISH_COMPLETE') { // post_ids can lag up to 3 minutes behind PUBLISH_COMPLETE, and never // shows up at all for non-public posts - fall back to the profile URL - // and keep the share_id (postAnalytics resolves it later). - const publicPostId = post_ids?.[0]; + // and keep the share_id (resolveReleaseId fixes it later). + const publicPostId = this.publicPostId(body); return { status: 'completed', releaseURL: !publicPostId ? `https://www.tiktok.com/@${integration.profile}` : `https://www.tiktok.com/@${integration.profile}/video/${publicPostId}`, - postId: !publicPostId ? pendingData.publishId : String(publicPostId), + postId: !publicPostId ? pendingData.publishId : publicPostId, }; } @@ -929,6 +931,12 @@ export class TiktokBusinessProvider return videoListData?.data?.videos; } + // post_ids holds int64 ids that exceed Number.MAX_SAFE_INTEGER, so the id + // must be read from the raw body instead of the parsed JSON + private publicPostId(body: string) { + return body.match(/"post_ids"\s*:\s*\[\s*"?(\d+)"?/)?.[1]; + } + private throwIfTokenError(body: { code?: number }) { if (body?.code === 0) { return; @@ -1099,6 +1107,44 @@ export class TiktokBusinessProvider } } + // Posts saved before post_ids became available keep their share_id + // (v_pub_... / p_pub_...) as releaseId - resolve it to the public post id + async resolveReleaseId( + accessToken: string, + releaseId: string, + integration: Integration + ) { + if (releaseId.indexOf('_pub_') === -1) { + return undefined; + } + + const body = await ( + await this.fetch( + `${this.baseUrl}/business/publish/status/?business_id=${encodeURIComponent( + integration.internalId + )}&publish_id=${encodeURIComponent(releaseId)}`, + { + method: 'GET', + headers: { + 'Access-Token': accessToken, + }, + } + ) + ).text(); + + this.throwIfTokenError(JSON.parse(body)); + + const publicPostId = this.publicPostId(body); + if (!publicPostId) { + return undefined; + } + + return { + postId: publicPostId, + releaseURL: `https://www.tiktok.com/@${integration.profile}/video/${publicPostId}`, + }; + } + async postAnalytics( integrationId: string, accessToken: string, @@ -1107,33 +1153,6 @@ export class TiktokBusinessProvider ): Promise { const today = dayjs().format('YYYY-MM-DD'); - // Posts saved before post_ids became available keep their share_id - // (v_pub_url~... / p_pub_url~...) as releaseId - resolve it to the public - // post id first. - if (postId.indexOf('_pub_url') > -1) { - const post = await ( - await this.fetch( - `${this.baseUrl}/business/publish/status/?business_id=${encodeURIComponent( - integrationId - )}&publish_id=${encodeURIComponent(postId)}`, - { - method: 'GET', - headers: { - 'Access-Token': accessToken, - }, - } - ) - ).json(); - - this.throwIfTokenError(post); - - if (!post?.data?.post_ids?.[0]) { - return []; - } - - postId = String(post.data.post_ids[0]); - } - try { const videoQueryData = await ( await this.fetch( diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index c40bec89d0..4e7af617f6 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -491,6 +491,8 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { } if (status === 'PUBLISH_COMPLETE') { + // the id only shows up once moderation approves the post - fall back to + // the profile URL and keep the publish_id (resolveReleaseId fixes it later) return { status: 'completed', releaseURL: !publicPostId @@ -1140,6 +1142,45 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { } } + // Posts published before moderation finished keep their publish_id + // (v_pub_url~... / v_pub_file~...) as releaseId - resolve it to the video id + async resolveReleaseId( + accessToken: string, + releaseId: string, + integration: Integration + ) { + if (releaseId.indexOf('v_pub_') === -1) { + return undefined; + } + + const { publicPostId } = this.parsePublishStatus( + await ( + await this.fetch( + 'https://open.tiktokapis.com/v2/post/publish/status/fetch/', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + publish_id: releaseId, + }), + } + ) + ).text() + ); + + if (!publicPostId) { + return undefined; + } + + return { + postId: publicPostId, + releaseURL: `https://www.tiktok.com/@${integration.profile}/video/${publicPostId}`, + }; + } + async postAnalytics( integrationId: string, accessToken: string, @@ -1148,35 +1189,6 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { ): Promise { const today = dayjs().format('YYYY-MM-DD'); - // Posts whose public id was not available yet when publishing keep the - // publish_id (v_pub_url~... / v_pub_file~...) as releaseId - resolve it - // to the public post id first. - if (postId.indexOf('_pub_') > -1) { - const { publicPostId } = this.parsePublishStatus( - await ( - await fetch( - 'https://open.tiktokapis.com/v2/post/publish/status/fetch/', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - publish_id: postId, - }), - } - ) - ).text() - ); - - if (!publicPostId) { - return []; - } - - postId = publicPostId; - } - try { // Query video details using the video ID const response = await fetch( From 4637a5190be8daf59fb121944146b4a4179cffd8 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 2 Sep 2026 10:25:20 +0700 Subject: [PATCH 42/53] fix(tiktok): resolve photo publish ids too Legacy photo posts store p_pub_url~ ids, which the v_pub_ check skipped. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VcaeTzorDM71yKNf9h3Fo4 --- .../src/integrations/social/tiktok.provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 4e7af617f6..7f2e453c57 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -1143,13 +1143,13 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { } // Posts published before moderation finished keep their publish_id - // (v_pub_url~... / v_pub_file~...) as releaseId - resolve it to the video id + // (v_pub_file~... / p_pub_url~...) as releaseId - resolve it to the post id async resolveReleaseId( accessToken: string, releaseId: string, integration: Integration ) { - if (releaseId.indexOf('v_pub_') === -1) { + if (releaseId.indexOf('_pub_') === -1) { return undefined; } From 2407718ec5bf235b4cd00302e886a06e9ea8d58e Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Thu, 1 Oct 2026 11:36:56 +0700 Subject: [PATCH 43/53] docs(claude): add a rule on credentials and sensitive fields in responses Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0153xf1m3VSQnd9QrMLhDikq --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index a13a751a94..ac91bf6dc9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,4 +75,5 @@ const useCommunity = () => { - Code must always be generic, there can't be a way that a specific logic, let's say facebook or instagram, appear in a file that use a generic logic, instead, we need to edit the interface of the provider, add another function, and then generically call it from the generic code, and then implement the specific logic in the provider implementation. we can't have something like if(facebookProvider) {} inside a non facebook provider file. - Before adding a field to a shared repository/service `select` or changing a shared method's return shape, grep for all its consumers (frontend, public API, MCP/agent tools, orchestrator, webhooks) and confirm the change is intended for each. - Exposing a stored field to a new external surface (public API, MCP, webhooks) is a data-exposure decision — ask first, and check what existing production rows hold for that column before shipping it. -- Never silently change the meaning or format of a value persisted in an existing DB column; that affects every reader and all historical rows, so ask first. \ No newline at end of file +- Never silently change the meaning or format of a value persisted in an existing DB column; that affects every reader and all historical rows, so ask first. +- Never return a raw Prisma row from a model that holds credentials (`Integration`, `User` and similar) in a controller response. Repository writes on those models must use a `select` with only the fields the caller needs (like `setTimes` with `select: { id: true }`), or the controller returns nothing. This applies even where nearby code looks different. \ No newline at end of file From b6ead58bf851f07ba9af724f6fa59da0ac7e78a4 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Thu, 1 Oct 2026 23:56:03 +0700 Subject: [PATCH 44/53] fix(media): bound media reads that stall after their headers A stalled read never settles, so the activity holds its worker slot long past startToCloseTimeout. Time both buffered reads out, retry once, and let the workflow repeat the publish via post.workflow.v1.1.3. --- .../src/activities/post.activity.ts | 2 +- apps/orchestrator/src/workflows/index.ts | 1 + .../post-workflows/post.workflow.v1.1.3.ts | 740 ++++++++++++++++++ libraries/helpers/src/utils/read.or.fetch.ts | 16 - .../database/prisma/posts/posts.service.ts | 2 +- .../src/integrations/social.abstract.ts | 134 +++- .../integrations/social/linkedin.provider.ts | 7 +- .../src/integrations/social/x.provider.ts | 5 +- 8 files changed, 864 insertions(+), 43 deletions(-) create mode 100644 apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.3.ts delete mode 100644 libraries/helpers/src/utils/read.or.fetch.ts diff --git a/apps/orchestrator/src/activities/post.activity.ts b/apps/orchestrator/src/activities/post.activity.ts index 5c18c34759..47ca08da97 100644 --- a/apps/orchestrator/src/activities/post.activity.ts +++ b/apps/orchestrator/src/activities/post.activity.ts @@ -117,7 +117,7 @@ export class PostActivity { for (const post of list) { await this._temporalService.client .getRawClient() - .workflow.signalWithStart('postWorkflowV112', { + .workflow.signalWithStart('postWorkflowV113', { workflowId: `post_${post.id}`, taskQueue: 'main', signal: 'poke', diff --git a/apps/orchestrator/src/workflows/index.ts b/apps/orchestrator/src/workflows/index.ts index 45924bd7d9..4966787a0d 100644 --- a/apps/orchestrator/src/workflows/index.ts +++ b/apps/orchestrator/src/workflows/index.ts @@ -10,6 +10,7 @@ export * from './post-workflows/post.workflow.v1.0.9'; export * from './post-workflows/post.workflow.v1.1.0'; export * from './post-workflows/post.workflow.v1.1.1'; export * from './post-workflows/post.workflow.v1.1.2'; +export * from './post-workflows/post.workflow.v1.1.3'; export * from './autopost.workflow'; export * from './digest.email.workflow'; export * from './missing.post.workflow'; diff --git a/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.3.ts b/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.3.ts new file mode 100644 index 0000000000..03e1e514ae --- /dev/null +++ b/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.3.ts @@ -0,0 +1,740 @@ +import { PostActivity } from '@gitroom/orchestrator/activities/post.activity'; +import { + ActivityFailure, + ApplicationFailure, + startChild, + proxyActivities, + sleep, + defineSignal, + setHandler, +} from '@temporalio/workflow'; +import dayjs from 'dayjs'; +import { Integration } from '@prisma/client'; +import { capitalize, sortBy } from 'lodash'; +import { PostResponse } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { + TimeoutFailure, + TimeoutType, + TypedSearchAttributes, +} from '@temporalio/common'; +import { postId as postIdSearchParam } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute'; + +// The publishing activities heartbeat every 15s (withHeartbeat sends its first +// heartbeat at the first interval tick, not at entry), so a heartbeat timeout +// whose failure carries no heartbeat details means the server never received +// one: the worker never ran the activity, or it died within its first seconds. +const HEARTBEAT_TIMEOUT = 3 * 60 * 1000; + +const proxyTaskQueue = (taskQueue: string) => { + return proxyActivities({ + startToCloseTimeout: '10 minute', + taskQueue, + retry: { + maximumAttempts: 3, + backoffCoefficient: 1, + initialInterval: '2 minutes', + }, + }); +}; + +// postComment publishes through providers that can legitimately run long +// (media conversion + upload), so it gets a large time budget. The +// heartbeatTimeout exists to detect an activity that was never started: the +// activity heartbeats every 15s, so no heartbeat at all means the worker never +// ran it and nothing was published. No SDK retries: an +// automatic retry of a heartbeat timeout would run again even when the first +// attempt did publish, duplicating the comment. The workflow decides whether +// a failure is safe to retry (see handleActivityError). +const proxyCommentTaskQueue = (taskQueue: string) => { + return proxyActivities({ + startToCloseTimeout: '30 minute', + heartbeatTimeout: HEARTBEAT_TIMEOUT, + taskQueue, + retry: { + maximumAttempts: 1, + }, + }); +}; + +// checkPostStatus is a single read-only status call, so it gets a short timeout +// and fast retries - retrying it can never duplicate a post. +const proxyCheckTaskQueue = (taskQueue: string) => { + return proxyActivities({ + startToCloseTimeout: '2 minute', + taskQueue, + retry: { + maximumAttempts: 3, + backoffCoefficient: 1, + initialInterval: '10 seconds', + }, + }); +}; + +// postSocialPending / finalizePost run irreversible publishing mutations, so no +// automatic retries - a retried activity whose previous (timed-out) attempt +// still completed in the background would publish twice. The workflow retries +// deliberately, and treats timeouts as "outcome unknown". +// The heartbeatTimeout exists to detect an activity that was never started: +// both activities heartbeat every 15s, so no heartbeat at all means the +// worker never ran them and nothing was published. The workflow +// decides whether that is safe to retry (see handleActivityError); every +// other timeout still marks the post as unconfirmed. +const proxyMutationTaskQueue = (taskQueue: string) => { + return proxyActivities({ + startToCloseTimeout: '30 minute', + heartbeatTimeout: HEARTBEAT_TIMEOUT, + taskQueue, + retry: { + maximumAttempts: 1, + }, + }); +}; + +const { + getPostsList, + getPost, + inAppNotification, + changeState, + updatePost, + sendWebhooks, + isCommentable, +} = proxyActivities({ + startToCloseTimeout: '10 minute', + retry: { + maximumAttempts: 3, + backoffCoefficient: 1, + initialInterval: '2 minutes', + }, +}); + +const poke = defineSignal('poke'); + +const iterate = Array.from({ length: 5 }); + +// ~30 minutes at 20s interval (longer than the old in-activity loop, timers are +// free). Multi-item flows (stories, chunked uploads) consume several checks per +// item, so the budget must cover the largest realistic post, not one poll cycle. +const maxPendingChecks = 90; + +export async function postWorkflowV113({ + taskQueue, + postId, + organizationId, + postNow = false, +}: { + taskQueue: string; + postId: string; + organizationId: string; + postNow?: boolean; +}) { + // Dynamic task queue, for concurrency + const { + getIntegrationById, + refreshTokenWithCause, + internalPlugs, + globalPlugs, + processInternalPlug, + processPlug, + } = proxyTaskQueue(taskQueue); + + const { checkPostStatus } = proxyCheckTaskQueue(taskQueue); + + const { postComment } = proxyCommentTaskQueue(taskQueue); + + const { postSocialPending, finalizePost } = proxyMutationTaskQueue(taskQueue); + + let poked = false; + setHandler(poke, () => { + poked = true; + }); + + // get all the posts and comments to post + const firstPost = await getPost(organizationId, postId); + + // in case doesn't exists for some reason, fail it + if (!firstPost) { + await changeState(postId, 'ERROR', 'No Post'); + return; + } + + if (!postNow && firstPost.state !== 'QUEUE') { + await changeState(firstPost.id, 'ERROR', 'Already posted', [firstPost]); + return; + } + + // wait for the scheduled publish date + if (!postNow) { + await sleep( + dayjs(firstPost.publishDate).isBefore(dayjs()) + ? 0 + : dayjs(firstPost.publishDate).diff(dayjs(), 'millisecond') + ); + } + + // Captured AFTER the scheduling sleep: the repeat-post delay is + // "interval minus time spent publishing", so it must be measured from the + // publish time, not from when the workflow was started. Measuring from the + // workflow start subtracted the whole scheduling wait from the interval + // (a post scheduled further out than its interval repeated immediately). + const startTime = new Date(); + + const postsListBefore = await getPostsList(organizationId, postId); + const [post] = postsListBefore; + + if (!post) { + await changeState(postId, 'ERROR', 'No Post'); + return; + } + + // if refresh is needed from last time, let's inform the user + if (post.integration?.refreshNeeded) { + await inAppNotification( + post.organizationId, + `We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`, + `We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because you need to reconnect it. Please enable it and try again.`, + true, + false, + 'info' + ); + + await changeState( + postsListBefore[0].id, + 'ERROR', + 'Refresh channel needed', + postsListBefore + ); + return; + } + + // if it's disabled, inform the user + if (post.integration?.disabled) { + await inAppNotification( + post.organizationId, + `We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`, + `We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because it's disabled. Please enable it and try again.`, + true, + false, + 'info' + ); + + await changeState( + postsListBefore[0].id, + 'ERROR', + 'Channel disabled', + postsListBefore + ); + return; + } + + // Do we need to post comment for this social? + const toComment: boolean = + postsListBefore.length === 1 + ? false + : await isCommentable(post.integration); + + const postsList = toComment ? postsListBefore : [postsListBefore[0]]; + + // list of all the saved results + const postsResults: PostResponse[] = []; + + // Every catch block below used to repeat the same failure classification, so + // it is centralized here: detect the failure type, refresh the token when + // needed, and tell the caller what to do. + // 'retry' - the token was refreshed, or the activity never started (heartbeat + // timeout with no heartbeat), run the action again + // 'stop' - the token could not be refreshed + // 'bad-body' - the platform rejected the action + // 'timeout' - the activity timed out, its outcome is unknown + // 'unknown' - anything else (transient errors) + // + // A media read that stalled and ran out of its own retries arrives as + // 'media_stall'. Nothing was sent to the platform (the bytes never left + // storage), so the publish is simply repeated. It maps to 'retry' rather + // than 'unknown' on purpose: 'unknown' writes an ERROR on the post before + // each new attempt, and a user seeing that red state mid-retry reads it as + // "we gave up" and publishes again by hand, which is the duplicate this + // workflow exists to prevent. + const handleActivityError = async ( + err: unknown, + getIntegration?: () => Promise, + heartbeats?: boolean + ): Promise<{ + type: 'retry' | 'stop' | 'bad-body' | 'timeout' | 'unknown'; + message: string; + }> => { + if ( + err instanceof ActivityFailure && + err.cause instanceof TimeoutFailure + ) { + // The server copies the last heartbeat details it received into the + // timeout failure. None at all (undefined) means it never received a + // heartbeat: the worker never ran the activity, so nothing was published + // and it is safe to run again. Any details mean the activity ran and + // then stalled, so its outcome is unknown. This relies on withHeartbeat + // always sending a non-empty string (": entered" at least): + // a heartbeat sent with undefined details also leaves the failure + // without details. Only the callers of heartbeating activities opt in; + // no time window, so activity dispatch delay cannot skew the decision. + if ( + heartbeats && + err.cause.timeoutType === TimeoutType.HEARTBEAT && + !err.cause.lastHeartbeatDetails + ) { + return { type: 'retry', message: '' }; + } + return { type: 'timeout', message: '' }; + } + + const cause = + err instanceof ActivityFailure && err.cause instanceof ApplicationFailure + ? err.cause + : undefined; + + if (cause?.type === 'refresh_token') { + const refresh = await refreshTokenWithCause( + getIntegration ? await getIntegration() : post.integration, + cause.message || '' + ); + if (!refresh || !refresh.accessToken) { + return { type: 'stop', message: cause.message || '' }; + } + + if (!getIntegration) { + post.integration.token = refresh.accessToken; + } + + return { type: 'retry', message: cause.message || '' }; + } + + if (cause?.type === 'media_stall') { + return { type: 'retry', message: '' }; + } + + if (cause?.type === 'bad_body') { + return { type: 'bad-body', message: cause.message || '' }; + } + + return { type: 'unknown', message: '' }; + }; + + // The platform may have accepted the post but we can't confirm it was + // published - mark the error with a distinct message so the user checks the + // account before reposting manually and duplicating it. + const markUnconfirmed = async (err: any) => { + await changeState(postsList[0].id, 'ERROR', err, postsList); + await inAppNotification( + post.organizationId, + `We couldn't confirm your post on ${capitalize( + post.integration?.providerIdentifier + )}`, + `Your post was sent to ${capitalize( + post.integration?.providerIdentifier + )}, but we couldn't confirm it was published. Please check your ${ + post?.integration?.name + } account before posting again to avoid duplicates.`, + true, + false, + 'fail' + ); + }; + + // The post/comment was already accepted by the platform but returned as + // "pending": poll the read-only status check with durable timers until it + // completes. Errors are fully handled here (never rethrown), otherwise they + // would bubble to the posting retry loop and re-run the publish. + const resolvePending = async ( + pending: PostResponse + ): Promise => { + let pendingData = pending.pendingData; + let errorAttempts = 0; + let heartbeats = false; + + for (let check = 0; check < maxPendingChecks; check++) { + // only finalizePost heartbeats, so a checkPostStatus failure must never + // be classified as never started + heartbeats = false; + try { + let result = await checkPostStatus(post.integration, pendingData); + + // commit the check's state BEFORE finalizePost runs: if finalize dies + // mid-mutation, the next check must see what it had already authorized, + // so providers can detect the interrupted attempt instead of running + // the mutation again + if (result.status !== 'completed') { + pendingData = result.pendingData; + } + + // polling is done, run the remaining provider mutations + if (result.status === 'ready') { + heartbeats = true; + result = await finalizePost(post.integration, result.pendingData); + } + + if (result.status === 'completed') { + return { + id: pending.id, + postId: result.postId, + releaseURL: result.releaseURL, + status: 'success', + }; + } + + pendingData = result.pendingData; + + // a fully successful iteration proves the platform is reachable: the + // error budget bounds consecutive failures, not blips accumulated over + // a long upload + errorAttempts = 0; + } catch (err) { + const handle = await handleActivityError(err, undefined, heartbeats); + + // token refreshed, or finalize never started, check again right away + if (handle.type === 'retry') { + continue; + } + + // the token could not be refreshed while checking, but the platform + // already accepted the post - warn about a possible live post + if (handle.type === 'stop') { + await markUnconfirmed(err); + return false; + } + + // the platform explicitly failed the post, it was not published + if (handle.type === 'bad-body') { + await changeState(postsList[0].id, 'ERROR', err, postsList); + await inAppNotification( + post.organizationId, + `Error posting on ${post.integration?.providerIdentifier} for ${post?.integration?.name}`, + `An error occurred while posting on ${ + post.integration?.providerIdentifier + }${handle.message ? `: ${handle.message}` : ``}`, + true, + false, + 'fail' + ); + return false; + } + + // unknown error on a read-only check, retry a few more times + errorAttempts++; + if (errorAttempts >= iterate.length) { + break; + } + } + + // the platform is still processing, wait before the next check + await sleep('20 seconds'); + } + + // no verdict from the platform after all the checks + await markUnconfirmed('Could not confirm the post status'); + return false; + }; + + // iterate over the posts + for (let i = 0; i < postsList.length; i++) { + const before = postsResults.length; + // once the platform accepted the post, the catch below must never retry + // the publish - retrying after updatePost / notification errors would + // duplicate the post + let posted = false; + let updated = false; + // this is a small trick to repeat an action in case of token refresh + for (const _ of iterate) { + // both publish calls run heartbeating activities, but the timed-out + // status checks below must never be mistaken for never-started + let heartbeats = false; + try { + // first post the main post + if (i === 0) { + heartbeats = true; + postsResults.push( + ...(await postSocialPending(post.integration as Integration, [ + postsList[i], + ])) + ); + + // then post the comments if any + } else { + if (postsList[i].delay) { + await sleep(60000 * Math.max(0, Number(postsList[i].delay ?? 0))); + } + + heartbeats = true; + postsResults.push( + ...(await postComment( + postsResults[0].postId, + postsResults.length === 1 + ? undefined + : postsResults[i - 1].postId, + post.integration, + [postsList[i]] + )) + ); + } + + posted = true; + + // the platform accepted the post but is still processing it: resolve + // it here before marking anything, resolvePending handles its own + // errors so a failed status check can never re-run the publish above + if (postsResults[i].status === 'pending') { + let resolved: PostResponse | false = false; + try { + resolved = await resolvePending(postsResults[i]); + } catch (err) { + // never let a pending-resolution error reach the outer catch, it + // would retry the post and duplicate it. Best-effort error state, + // otherwise the post stays in QUEUE and the missing-posts sweep + // would re-publish it. + try { + await markUnconfirmed(err); + } catch (e) { + /**empty**/ + } + resolved = false; + } + if (!resolved) { + return false; + } + postsResults[i] = resolved; + } + + // mark post as successful + await updatePost( + postsList[i].id, + postsResults[i].postId, + postsResults[i].releaseURL + ); + updated = true; + + if (i === 0) { + // send notification on a sucessful post + await inAppNotification( + post.integration.organizationId, + `Your post has been published on ${capitalize( + post.integration.providerIdentifier + )}`, + `Your post has been published on ${capitalize( + post.integration.providerIdentifier + )} at ${postsResults[0].releaseURL}`, + true, + true + ); + } + + // break the current while to move to the next post + break; + } catch (err) { + // the post is already live: never re-run the publish + if (posted) { + if (!updated) { + // still marked QUEUE, record the error so the missing-posts sweep + // doesn't re-publish it + try { + await markUnconfirmed(err); + } catch (e) { + /**empty**/ + } + return false; + } + + // already marked published, a failed notification shouldn't abort + // the rest of the flow + break; + } + + const handle = await handleActivityError(err, undefined, heartbeats); + + // token refreshed, or the publish never started, repeat the action + if (handle.type === 'retry') { + continue; + } + + // the activity timed out: the platform may still complete the publish + // in the background, so never retry it + if (handle.type === 'timeout') { + try { + await markUnconfirmed(err); + } catch (e) { + /**empty**/ + } + return false; + } + + // for other errors, change state and inform the user if needed + await changeState(postsList[0].id, 'ERROR', err, postsList); + + if (handle.type === 'stop') { + return false; + } + + // specific case for bad body errors + if (handle.type === 'bad-body') { + await inAppNotification( + post.organizationId, + `Error posting${i === 0 ? ' ' : ' comments '}on ${ + post.integration?.providerIdentifier + } for ${post?.integration?.name}`, + `An error occurred while posting${i === 0 ? ' ' : ' comments '}on ${ + post.integration?.providerIdentifier + }${handle.message ? `: ${handle.message}` : ``}`, + true, + false, + 'fail' + ); + return false; + } + } + } + + if (postsResults.length === before) { + // all retries exhausted without success: record it, otherwise the post + // stays in QUEUE with no error and the missing-posts sweep re-publishes + // it. A retried publish may have run without reporting, so treat the + // outcome as unknown. + try { + await markUnconfirmed('Could not publish after several attempts'); + } catch (e) { + /**empty**/ + } + return false; + } + } + + // send webhooks for the post + await sendWebhooks( + postsResults[0].postId, + post.organizationId, + post.integration.id + ); + + // load internal plugs like repost by other users + const internalPlugsList = await internalPlugs( + post.integration, + JSON.parse(post.settings) + ); + + // load global plugs, like repost a post if it gets to a certain number of likes + const globalPlugsList = (await globalPlugs(post.integration)).reduce( + (all, current) => { + for (let i = 1; i <= current.totalRuns; i++) { + all.push({ + ...current, + delay: current.delay * i, + }); + } + + return all; + }, + [] + ); + + // Check if the post is repeatable + const repeatPost = !post.intervalInDays + ? [] + : [ + { + type: 'repeat-post', + delay: + post.intervalInDays * 24 * 60 * 60 * 1000 - + (new Date().getTime() - startTime.getTime()), + }, + ]; + + // Sort all the actions by delay, so we can process them in order + const list = sortBy( + [...internalPlugsList, ...globalPlugsList, ...repeatPost], + 'delay' + ); + + // process all the plugs in order, we are using while because in some cases we need to remove items from the list + while (list.length > 0) { + // get the next to process + const todo = list.shift(); + + // wait for the delay + await sleep(Math.max(0, Number(todo.delay ?? 0))); + + // process internal plug + if (todo.type === 'internal-plug') { + for (const _ of iterate) { + try { + await processInternalPlug({ ...todo, post: postsResults[0].postId }); + } catch (err) { + const handle = await handleActivityError(err, () => + getIntegrationById(organizationId, todo.integration) + ); + + if (handle.type === 'stop' || handle.type === 'bad-body') { + break; + } + + continue; + } + break; + } + } + + // process global plug + if (todo.type === 'global') { + for (const _ of iterate) { + try { + const process = await processPlug({ + ...todo, + postId: postsResults[0].postId, + }); + if (process) { + const toDelete = list + .reduce((all, current, index) => { + if (current.plugId === todo.plugId) { + all.push(index); + } + + return all; + }, []) + .reverse(); + + for (const index of toDelete) { + list.splice(index, 1); + } + } + } catch (err) { + const handle = await handleActivityError(err); + + if (handle.type === 'stop' || handle.type === 'bad-body') { + break; + } + + continue; + } + + break; + } + } + + // process repeat post in a new workflow, this is important so the other plugs can keep running + if (todo.type === 'repeat-post') { + await startChild(postWorkflowV113, { + parentClosePolicy: 'ABANDON', + args: [ + { + taskQueue, + postId, + organizationId, + postNow: true, + }, + ], + workflowId: `post_${post.id}_${makeId(10)}`, + typedSearchAttributes: new TypedSearchAttributes([ + { + key: postIdSearchParam, + value: postId, + }, + ]), + }); + } + } +} diff --git a/libraries/helpers/src/utils/read.or.fetch.ts b/libraries/helpers/src/utils/read.or.fetch.ts deleted file mode 100644 index 49c0ac9eef..0000000000 --- a/libraries/helpers/src/utils/read.or.fetch.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { readFileSync } from 'fs'; -import axios from 'axios'; - -export const readOrFetch = async (path: string) => { - if (path.indexOf('http') === 0) { - return ( - await axios({ - url: path, - method: 'GET', - responseType: 'arraybuffer', - }) - ).data; - } - - return readFileSync(path); -}; diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 31ecc48bf3..73463f979c 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -748,7 +748,7 @@ export class PostsService { try { await this._temporalService.client .getRawClient() - ?.workflow.start('postWorkflowV112', { + ?.workflow.start('postWorkflowV113', { workflowId: `post_${postId}`, taskQueue: 'main', workflowIdConflictPolicy: 'TERMINATE_EXISTING', diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 93c1ec2ce2..df0eef3535 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -5,16 +5,27 @@ import { PendingCheckResponse, } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { ApplicationFailure } from '@temporalio/activity'; -import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; import { setHeartbeatDetails } from '@gitroom/nestjs-libraries/temporal/temporal.heartbeat'; import { getSsrfSafeAxios, getSsrfSafeDispatcher, } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher'; +import axios from 'axios'; import sharp from 'sharp'; -import { createReadStream, statSync } from 'fs'; +import { createReadStream, readFileSync, statSync } from 'fs'; import { Readable } from 'stream'; +// Media reads answer headers in well under a second and the chunk sizes the +// providers ask for finish in under two, so a minute is only ever reached by a +// stalled transfer. Matches the AbortSignal.timeout values used for the other +// outbound media calls. +const MEDIA_READ_TIMEOUT = 60_000; + +// AbortSignal.timeout rejects with a TimeoutError, which undici may surface +// directly or wrap as the cause of the fetch rejection. +const isReadTimeout = (err: any) => + err?.name === 'TimeoutError' || err?.cause?.name === 'TimeoutError'; + export const stripQuery = (url: string) => { try { const parsed = new URL(url); @@ -88,6 +99,26 @@ export class Disconnect extends ApplicationFailure { } } +// A media read that answered its headers and then stalled, after its own +// retry was spent. Nothing has been sent to the platform at that point, so the +// workflow repeats the publish instead of failing the post. +export class MediaStall extends ApplicationFailure { + constructor(identifier: string, json: string, body: BodyInit, message = '') { + super( + truncateForTemporal(message, MAX_FAILURE_MESSAGE), + 'media_stall', + true, + [ + { + identifier, + json: truncateForTemporal(json, MAX_FAILURE_FIELD), + body: truncateForTemporal(body, MAX_FAILURE_FIELD), + }, + ] + ); + } +} + export class BadBody extends ApplicationFailure { constructor(identifier: string, json: string, body: BodyInit, message = '') { super(truncateForTemporal(message, MAX_FAILURE_MESSAGE), 'bad_body', true, [ @@ -231,11 +262,51 @@ export abstract class SocialAbstract { : path; setHeartbeatDetails(`read media ${stripQuery(url)}`); const { width = 0, height = 0 } = await sharp( - await readOrFetch(url) + await this.readOrFetch(url) ).metadata(); return { width, height }; } + // Reads the whole media into memory: used for images, which the providers + // hand straight to sharp. Lives here rather than in a helper so it can + // classify a stall like the ranged reads below do. + protected async readOrFetch(path: string, retried = false): Promise { + if (path.indexOf('http') !== 0) { + return readFileSync(path); + } + + try { + return ( + await axios({ + url: path, + method: 'GET', + responseType: 'arraybuffer', + // Same stall as the ranged reads: headers arrive fast and the body + // then stops, and without a deadline the read never settles and + // holds the activity's worker slot. Images are a few MB and arrive + // in about a second. + signal: AbortSignal.timeout(MEDIA_READ_TIMEOUT), + }) + ).data; + } catch (err: any) { + // axios reports an aborted signal as a canceled request + if (err?.code !== 'ERR_CANCELED' && !isReadTimeout(err)) { + throw err; + } + + if (!retried) { + return this.readOrFetch(path, true); + } + + throw new MediaStall( + '', + '{}', + Buffer.from('{}'), + `Media read timed out after ${MEDIA_READ_TIMEOUT}ms (${stripQuery(path)})` + ); + } + } + // Resolves the total byte size of the media without loading it into memory: // a HEAD request for remote URLs, statSync for local files. protected async mediaSize(path: string, identifier = ''): Promise { @@ -274,31 +345,58 @@ export abstract class SocialAbstract { path: string, start: number, end: number, - identifier = '' + identifier = '', + retried = false ): Promise { if (path.indexOf('http') === 0) { setHeartbeatDetails( `media chunk ${start}-${end} ${stripQuery(path)}` ); - const response = await fetch(path, { - headers: { - Range: `bytes=${start}-${end}`, - 'accept-encoding': 'identity', - }, - dispatcher: getSsrfSafeDispatcher(), - } as any); - // Anything but 206 means the server ignored the Range header: buffering - // response.body here would silently load the whole file into memory and - // upload corrupted chunks. - if (response.status !== 206) { - throw new BadBody( + try { + const response = await fetch(path, { + headers: { + Range: `bytes=${start}-${end}`, + 'accept-encoding': 'identity', + }, + dispatcher: getSsrfSafeDispatcher(), + // The store answers the headers in well under a second and then + // stops mid-body. Without a deadline that read never settles, and + // the activity keeps its worker slot until the process restarts: a + // startToCloseTimeout fails the activity on the server but does not + // stop the function, which nothing here subscribes to cancellation + // to notice. Healthy reads of these chunk sizes finish in under 2s. + signal: AbortSignal.timeout(MEDIA_READ_TIMEOUT), + } as any); + // Anything but 206 means the server ignored the Range header: buffering + // response.body here would silently load the whole file into memory and + // upload corrupted chunks. + if (response.status !== 206) { + throw new BadBody( + identifier, + '{}', + Buffer.from('{}'), + `Media server did not honor the range request (status ${response.status})` + ); + } + return Buffer.from(await response.arrayBuffer()); + } catch (err) { + if (!isReadTimeout(err)) { + throw err; + } + + // A stall is transient and nothing has been sent to the platform yet, + // so read the same range once more before giving up. + if (!retried) { + return this.mediaChunk(path, start, end, identifier, true); + } + + throw new MediaStall( identifier, '{}', Buffer.from('{}'), - `Media server did not honor the range request (status ${response.status})` + `Media read timed out after ${MEDIA_READ_TIMEOUT}ms (bytes ${start}-${end})` ); } - return Buffer.from(await response.arrayBuffer()); } return new Promise((resolve, reject) => { diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 0ec0b376a3..daaca54f81 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -9,7 +9,6 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { makeSecureId } from '@gitroom/nestjs-libraries/services/make.secure.id'; import sharp from 'sharp'; import { lookup } from 'mime-types'; -import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; import { hasExtension } from '@gitroom/helpers/utils/has.extension'; import { timer } from '@gitroom/helpers/utils/timer'; import { @@ -539,7 +538,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { // Fetch all images and get their dimensions const images = await Promise.all( firstPost.media.map(async (media) => { - const raw = await readOrFetch(media.path); + const raw = await this.readOrFetch(media.path); const image = sharp(raw, { animated: false }).toFormat('jpeg'); const { width, height } = await image.metadata(); const buffer = await image.toBuffer(); @@ -662,7 +661,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { if (isGif) { // Buffer.from keeps the return type a real Buffer regardless of what // readOrFetch yields - uploadPicture branches on Buffer.isBuffer. - return Buffer.from(await readOrFetch(mediaUrl)); + return Buffer.from(await this.readOrFetch(mediaUrl)); } const mime = lookup(mediaUrl); @@ -677,7 +676,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { // DISABLE_IMAGE_COMPRESSION=true, where the frontend no longer shrinks // uploads and full-size images reach LinkedIn directly. Do not remove it on // the assumption that the frontend compression already caps dimensions. - const pipeline = sharp(await readOrFetch(mediaUrl), { animated: false }).resize({ + const pipeline = sharp(await this.readOrFetch(mediaUrl), { animated: false }).resize({ width: 6000, height: 6000, fit: 'inside', diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 969f70d23c..782e8d8729 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -11,7 +11,6 @@ import { } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { lookup } from 'mime-types'; import sharp from 'sharp'; -import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; import { setHeartbeatDetails } from '@gitroom/nestjs-libraries/temporal/temporal.heartbeat'; import { BadBody, @@ -734,7 +733,7 @@ export class XProvider extends SocialAbstract implements SocialProvider { mediaId: await this.uploadWithRateLimitRetry(async () => asArticleImage ? client.v2.uploadMedia( - await sharp(await readOrFetch(m.path)) + await sharp(await this.readOrFetch(m.path)) .resize({ width: 1000, }) @@ -746,7 +745,7 @@ export class XProvider extends SocialAbstract implements SocialProvider { } ) : client.v2.uploadMedia( - await sharp(await readOrFetch(m.path), { + await sharp(await this.readOrFetch(m.path), { animated: lookup(m.path) === 'image/gif', }) .resize({ From a2794ab655c0d37f30b07d633aedd5c139a7bbc0 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Fri, 2 Oct 2026 06:51:46 +0700 Subject: [PATCH 45/53] fix(media): read media through the SSRF-safe axios client readOrFetch took a user-influenced path on plain axios, unlike every other media read in the class. --- .../nestjs-libraries/src/integrations/social.abstract.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index df0eef3535..5cb72a7331 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -10,7 +10,6 @@ import { getSsrfSafeAxios, getSsrfSafeDispatcher, } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher'; -import axios from 'axios'; import sharp from 'sharp'; import { createReadStream, readFileSync, statSync } from 'fs'; import { Readable } from 'stream'; @@ -277,7 +276,9 @@ export abstract class SocialAbstract { try { return ( - await axios({ + // the media path is user-influenced, so it goes through the same + // SSRF-safe client as every other outbound media read + await this.getSsrfSafeAxios()({ url: path, method: 'GET', responseType: 'arraybuffer', From 9fedac402f4c021b04a9690677b5bc94850a3689 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 2 Oct 2026 11:23:04 +0700 Subject: [PATCH 46/53] feat: responsive --- apps/frontend/AGENTS.md | 9 ++ apps/frontend/CLAUDE.md | 1 + apps/frontend/src/components/agents/agent.tsx | 30 +++-- .../billing/main.billing.component.tsx | 9 +- .../developer/developer.component.tsx | 28 ++-- .../launches/add.provider.component.tsx | 4 +- .../components/launches/calendar.context.tsx | 9 +- .../src/components/launches/calendar.tsx | 12 +- .../src/components/launches/filters.tsx | 16 +-- .../launches/helpers/date.picker.tsx | 4 +- .../launches/launches.component.tsx | 117 ++++++++++++---- .../src/components/launches/menu/menu.tsx | 9 ++ .../src/components/layout/impersonate.tsx | 10 +- .../src/components/layout/new-modal.tsx | 30 +++-- .../layout/organization.selector.tsx | 4 +- .../components/layout/settings.component.tsx | 12 +- apps/frontend/src/components/layout/title.tsx | 2 +- .../src/components/media/media.component.tsx | 24 ++-- .../src/components/new-launch/editor.tsx | 10 +- .../components/new-launch/manage.modal.tsx | 104 +++++++++++---- .../new-layout/layout.component.tsx | 77 ++++++++--- .../notifications/notification.component.tsx | 2 +- .../platform-analytics/platform.analytics.tsx | 125 +++++++++--------- apps/frontend/src/components/plugs/plug.tsx | 6 +- apps/frontend/src/components/plugs/plugs.tsx | 125 +++++++++--------- .../public-api/public.component.tsx | 24 ++-- .../third-parties/third-party.component.tsx | 12 +- .../third-party.list.component.tsx | 4 +- .../third-party.media-library.tsx | 4 +- .../third-parties/third-party.media.tsx | 4 +- 30 files changed, 522 insertions(+), 305 deletions(-) create mode 100644 apps/frontend/AGENTS.md create mode 100644 apps/frontend/CLAUDE.md diff --git a/apps/frontend/AGENTS.md b/apps/frontend/AGENTS.md new file mode 100644 index 0000000000..643577dfae --- /dev/null +++ b/apps/frontend/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/apps/frontend/CLAUDE.md b/apps/frontend/CLAUDE.md new file mode 100644 index 0000000000..43c994c2d3 --- /dev/null +++ b/apps/frontend/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/apps/frontend/src/components/agents/agent.tsx b/apps/frontend/src/components/agents/agent.tsx index af6d3af634..f304109525 100644 --- a/apps/frontend/src/components/agents/agent.tsx +++ b/apps/frontend/src/components/agents/agent.tsx @@ -107,12 +107,12 @@ export const AgentList: FC<{ onChange: (arr: any[]) => void }> = ({ return (
-
-
+
+

{t('select_channels', 'Select Channels')}

@@ -137,13 +137,17 @@ export const AgentList: FC<{ onChange: (arr: any[]) => void }> = ({
-
+
{sortedIntegrations.map((integration, index) => (
p.id === integration.id) && 'opacity-20' )} > @@ -182,7 +186,7 @@ export const AgentList: FC<{ onChange: (arr: any[]) => void }> = ({
@@ -203,7 +207,9 @@ export const Agent: FC<{ children: ReactNode }> = ({ children }) => { return ( -
{children}
+
+ {children} +
); @@ -225,11 +231,11 @@ const Threads: FC = () => {
-
-
+
+
{
-
+
{data?.threads?.map((p: any) => (
-
{feature}
+
{feature}
))}
@@ -583,8 +583,11 @@ export const MainBillingComponent: FC<{ ))}
{!!subscription?.id && ( -
-
-
+
{t('oauth_application', 'OAuth Application')} @@ -241,7 +241,7 @@ export const DeveloperComponent: FC = () => { )}
- -
+
-
+
{/* App details / edit */}
-
+
{t('oauth_application', 'OAuth Application')} @@ -404,7 +404,7 @@ export const DeveloperComponent: FC = () => { )}
-
+ {editing ? ( -
+
-
+
) : ( -
+
{app.picture?.path ? ( {
{t('redirect_url', 'Redirect URL')}
-
{app.redirectUrl}
+
{app.redirectUrl}
-
+
-
+
{t('client_id', 'Client ID')} @@ -558,7 +558,7 @@ export const DeveloperComponent: FC = () => {
{t('client_secret', 'Client Secret')}
-
+
{plaintextSecret ? ( {plaintextSecret} @@ -573,7 +573,7 @@ export const DeveloperComponent: FC = () => { )}
-
+
{plaintextSecret && ( @@ -725,7 +725,7 @@ export const AddProviderComponent: FC<{ className={clsx( isMobile ? 'flex-row h-[72px] p-[16px]' - : 'flex-col p-[10px] h-[100px] justify-center', + : 'flex-col p-[10px] h-[100px] justify-center mobile:text-[13px] mobile:leading-[1.2]', 'w-full text-[14px] rounded-[8px] bg-newTableHeader text-textColor relative items-center flex gap-[10px] cursor-pointer' )} > diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index fb5bea3621..df65ebb1f4 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -145,7 +145,14 @@ export const CalendarWeekProvider: FC<{ const [internalData, setInternalData] = useState([] as any[]); const [trendings] = useState([]); const searchParams = useSearchParams(); - const [displaySaved, setDisplaySaved] = useCookie('calendar-display', 'week'); + // A 7-column week doesn't fit a phone, so default small screens (tailwind `mobile`) to the day view + const [displaySaved, setDisplaySaved] = useCookie( + 'calendar-display', + typeof window !== 'undefined' && + window.matchMedia('(max-width: 1025px)').matches + ? 'day' + : 'week' + ); const display = searchParams.get('display') || displaySaved; // List view state diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index 11d82c57a0..0fd2a8dfff 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -309,7 +309,7 @@ export const DayView = () => {
{options.map((option) => ( -
+
{newDayjs() .utc() .startOf('day') @@ -319,7 +319,7 @@ export const DayView = () => {
{ return (
-
+
{localizedDays.map((day, index) => (
{ ))} {hours.map((hour) => ( -
+
{convertTimeFormatBasedOnLocality(hour)}
{localizedDays.map((day, indexDay) => ( @@ -465,7 +465,7 @@ export const MonthView = () => { return (
-
+
{localizedDays.map((day) => (
{integrations.map((selectedIntegrations) => (
{ }, [calendar]); return ( -
+
{!isListView && (
@@ -311,7 +311,7 @@ export const Filters = () => { />
-
+
{getDisplayText()}
@@ -350,7 +350,7 @@ export const Filters = () => {
)} {isListView && ( -
+
{ />
-
+
{t('page', 'Page')} {calendar.listPage + 1} {t('of', 'of')} {Math.max(1, calendar.listTotalPages)}
@@ -408,7 +408,7 @@ export const Filters = () => {
-
+
{listStateOptions.map((option) => (
{
{
{
@@ -48,7 +48,7 @@ export const DatePicker: FC<{ {open && (
e.stopPropagation()} - className="animate-fadeIn absolute bottom-[100%] mb-[16px] start-[50%] -translate-x-[50%] bg-sixth border border-tableBorder text-textColor rounded-[16px] z-[300] p-[16px] flex flex-col" + className="animate-fadeIn absolute bottom-[100%] mb-[16px] start-[50%] -translate-x-[50%] mobile:start-0 mobile:translate-x-0 bg-sixth border border-tableBorder text-textColor rounded-[16px] z-[300] p-[16px] flex flex-col" > { const modal = useModals(); const [reload, setReload] = useState(false); const [collapseMenu, setCollapseMenu] = useCookie('collapseMenu', '0'); + const [channelsOpen, setChannelsOpen] = useState(false); const [mode] = useCookie('mode', 'dark'); const { isLoading, data: integrations, mutate } = useIntegrationList(); @@ -626,26 +627,56 @@ export const LaunchesComponent = () => { + {channelsOpen && ( +
setChannelsOpen(false)} + /> + )}

{t('channels')}

+
setChannelsOpen(false)} + className="hidden mobile:flex text-btnText bg-btnSimple rounded-[6px] w-[28px] h-[28px] items-center justify-center cursor-pointer select-none" + > + + + +
setCollapseMenu(collapseMenu === '1' ? '0' : '1') } - className="group-[.sidebar]:rotate-[180deg] group-[.sidebar]:mx-auto text-btnText bg-btnSimple rounded-[6px] w-[24px] h-[24px] flex items-center justify-center cursor-pointer select-none" + className="group-[.sidebar]:rotate-[180deg] group-[.sidebar]:mx-auto mobile:hidden text-btnText bg-btnSimple rounded-[6px] w-[24px] h-[24px] flex items-center justify-center cursor-pointer select-none" > {
update(true)} /> -
+
{sortedIntegrations?.length > 0 && } {sortedIntegrations?.length > 0 && user?.tier?.ai && @@ -674,30 +705,31 @@ export const LaunchesComponent = () => {
- {sortedIntegrations.length === 0 && collapseMenu === '0' && ( -
-
- No channels -
- {t('no_channels', 'No channels yet')} -
-
- {t('connect_your_accounts')} + {sortedIntegrations.length === 0 && + (collapseMenu === '0' || channelsOpen) && ( +
+
+ No channels +
+ {t('no_channels', 'No channels yet')} +
+
+ {t('connect_your_accounts')} +
-
- )} + )} {menuIntegrations.map((menu) => ( { /> ))}
-
+
{billingEnabled && user?.isLifetime && (
{capitalize(user?.tier?.current || '')} tier
)} @@ -721,7 +753,40 @@ export const LaunchesComponent = () => {
-
+
+
+
setChannelsOpen(true)} + className="cursor-pointer h-[44px] px-[12px] bg-btnSimple text-btnText rounded-[8px] flex items-center gap-[8px] select-none" + > +
+ {sortedIntegrations.slice(0, 3).map((integration: any) => ( + + ))} +
+
+ {sortedIntegrations.length || t('channels')} +
+
+ {sortedIntegrations?.length > 0 ? ( + <> + + {user?.tier?.ai && billingEnabled && } + + ) : ( +
+ update(true)} /> +
+ )} +
diff --git a/apps/frontend/src/components/launches/menu/menu.tsx b/apps/frontend/src/components/launches/menu/menu.tsx index 053d276650..7859ce00f6 100644 --- a/apps/frontend/src/components/launches/menu/menu.tsx +++ b/apps/frontend/src/components/launches/menu/menu.tsx @@ -76,6 +76,7 @@ export const Menu: FC<{ if (show && menuRef.current) { const menuRect = menuRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; + const viewportWidth = window.innerWidth; const padding = 10; // Check if menu overflows bottom of viewport @@ -89,6 +90,14 @@ export const Menu: FC<{ setShow((prev) => (prev ? { ...prev, y: newY } : false)); } } + + // Check if menu overflows right of viewport (small screens) + if (menuRect.right > viewportWidth - padding) { + const newX = Math.max(padding, viewportWidth - menuRect.width - padding); + if (Math.abs(show.x - newX) > 1) { + setShow((prev) => (prev ? { ...prev, x: newX } : false)); + } + } } }, [show]); const findIntegration: any = useMemo(() => { diff --git a/apps/frontend/src/components/layout/impersonate.tsx b/apps/frontend/src/components/layout/impersonate.tsx index 215a40e927..39869b6040 100644 --- a/apps/frontend/src/components/layout/impersonate.tsx +++ b/apps/frontend/src/components/layout/impersonate.tsx @@ -1073,15 +1073,15 @@ export const Impersonate = () => { }, [data]); return (
-
+
{user?.impersonate ? ( -
+
{t('currently_impersonating', 'Currently Impersonating')}
@@ -1099,8 +1099,8 @@ export const Impersonate = () => {
) : ( -
-
+
+
-
+
@@ -183,11 +190,11 @@ export const Component: FC<{ : {} } className={clsx( - 'absolute min-w-full', + 'absolute min-w-full mobile:w-full', !modal.fullScreen ? modal.top ? '' - : 'min-h-full pt-[100px] pb-[100px]' + : 'min-h-full pt-[100px] pb-[100px] mobile:pt-[24px] mobile:pb-[24px] mobile:px-[8px]' : 'h-screen', modal.size && modal.height ? 'flex justify-center items-center' @@ -196,9 +203,10 @@ export const Component: FC<{ >
e.stopPropagation()} >
-
+
{modal.title}
{typeof modal.withCloseButton === 'undefined' || modal.withCloseButton ? (
)} @@ -106,7 +106,7 @@ export const OrganizationSelector: FC<{ asOpenSelect?: boolean }> = ({ )}
- {!asOpenSelect &&
} + {!asOpenSelect &&
} ); }; diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx index 7e2584540b..2f427d5b3e 100644 --- a/apps/frontend/src/components/layout/settings.component.tsx +++ b/apps/frontend/src/components/layout/settings.component.tsx @@ -117,20 +117,20 @@ export const SettingsPopup: FC<{ return ( <> -
-
+
+
{list.map(({ tab: tabKey, label }) => (
setTab(tabKey)} >
@@ -142,13 +142,13 @@ export const SettingsPopup: FC<{
{showLogout && ( -
+
)}
-
+
{!!getRef && ( diff --git a/apps/frontend/src/components/layout/title.tsx b/apps/frontend/src/components/layout/title.tsx index 5fa3099a61..f071a40936 100644 --- a/apps/frontend/src/components/layout/title.tsx +++ b/apps/frontend/src/components/layout/title.tsx @@ -10,5 +10,5 @@ export const Title = () => { return menuItems.find((item) => path.indexOf(item.path) > -1)?.name; }, [path]); - return

{currentTitle}

; + return

{currentTitle}

; }; diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx index 901e9e5400..4eadb0e84c 100644 --- a/apps/frontend/src/components/media/media.component.tsx +++ b/apps/frontend/src/components/media/media.component.tsx @@ -119,7 +119,7 @@ export const Pagination: FC<{ }, [current, totalPages]); return ( -
    +
    • setPage(current - 1)} > - {t('previous', 'Previous')} + {t('previous', 'Previous')}
    • {paginationItems.map((item, index) => ( @@ -162,7 +162,7 @@ export const Pagination: FC<{ aria-label="Go to next page" onClick={() => setPage(current + 1)} > - {t('next', 'Next')} + {t('next', 'Next')} @@ -420,14 +420,14 @@ export const MediaBox: FC<{
      -
      +
      (
      @@ -537,7 +537,7 @@ export const MediaBox: FC<{ .map((media: any) => (
      )} -
      {media.originalName}
      +
      + {media.originalName} +
      )}
      -
      +
      {!mediaNotAvailable && ( -
      +
      )} {!mediaNotAvailable && ( -
      +
      )} diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 0ded20eff6..83214e2a07 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -360,7 +360,7 @@ export const EditorWrapper: FC<{ return (
      0) || (!comments && index > 0)) && 'hidden' )} > -
      -
      +
      +
      {index > 0 && (
      @@ -694,7 +694,7 @@ export const Editor: FC<{ } return ( -
      +
      } toolBar={ -
      +
      {editorType !== 'none' && ( <> diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index ef639def0b..9ef635dad6 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -43,6 +43,7 @@ import { useHasScroll } from '@gitroom/frontend/components/ui/is.scroll.hook'; import { useShortlinkPreference } from '@gitroom/frontend/components/settings/shortlink-preference.component'; import dayjs from 'dayjs'; import { Button } from '@gitroom/react/form/button'; +import { useClickOutside } from '@mantine/hooks'; export const ManageModal: FC = (props) => { const t = useT(); @@ -53,6 +54,11 @@ export const ManageModal: FC = (props) => { const toaster = useToaster(); const modal = useModals(); const [showSettings, setShowSettings] = useState(false); + const [mobileTab, setMobileTab] = useState<'edit' | 'preview'>('edit'); + const [showPostNow, setShowPostNow] = useState(false); + const postNowRef = useClickOutside(() => { + setShowPostNow(false); + }); const { data: shortlinkPreferenceData } = useShortlinkPreference(); const { addEditSets, mutate, customClose, dummy } = props; @@ -483,24 +489,48 @@ export const ManageModal: FC = (props) => { ); return ( -
      -
      -
      -
      -
      +
      +
      +
      +
      +
      {t('create_post_title', 'Create Post')} +
      + +
      +
      +
      + {(['edit', 'preview'] as const).map((tab) => ( +
      setMobileTab(tab)} + className={clsx( + 'flex-1 pt-[6px] pb-[5px] cursor-pointer text-center rounded-[6px]', + mobileTab === tab && 'text-textItemFocused bg-boxFocused' + )} + > + {tab === 'edit' + ? t('edit', 'Edit') + : t('post_preview', 'Post Preview')} +
      + ))}
      -
      +
      @@ -533,7 +563,7 @@ export const ManageModal: FC = (props) => {
      = (props) => { 'text-[14px] text-textColor font-[500] relative' )} > -
      +
      = (props) => {
      -
      -
      +
      +
      {t('post_preview', 'Post Preview')}
      -
      +
      -
      -
      +
      +
      {!dummy && ( = (props) => { )}
      -
      +
      {existingData?.integration && ( )} {!addEditSets && ( -
      +
      @@ -695,7 +749,10 @@ export const ManageModal: FC = (props) => { disabled={ selectedIntegrations.length === 0 || loading || locked } - className="rounded-[8px] z-[300] disabled:cursor-not-allowed disabled:opacity-80 hidden group-hover:flex absolute bottom-[100%] -left-[12px] p-[12px] w-[206px] bg-newBgColorInner" + className={clsx( + 'rounded-[8px] z-[300] disabled:cursor-not-allowed disabled:opacity-80 absolute bottom-[100%] -left-[12px] mobile:left-0 p-[12px] mobile:px-0 w-[206px] mobile:w-full bg-newBgColorInner', + showPostNow ? 'flex' : 'hidden group-hover:flex' + )} >
      {t('post_now', 'Post Now')} @@ -708,6 +765,7 @@ export const ManageModal: FC = (props) => {
      { // Feedback icon component attaches Sentry feedback to a top-bar icon when DSN is present const searchParams = useSearchParams(); + const pathname = usePathname(); + const [menuOpen, setMenuOpen] = useState(false); + useEffect(() => { + setMenuOpen(false); + }, [pathname]); const load = useCallback(async (path: string) => { return await (await fetch(path)).json(); }, []); @@ -98,7 +103,7 @@ export const LayoutComponent = ({ children }: { children: ReactNode }) => {
      @@ -108,43 +113,81 @@ export const LayoutComponent = ({ children }: { children: ReactNode }) => { ) : ( <> -
      +
      -
      + {menuOpen && ( +
      setMenuOpen(false)} + /> + )} +
      setMenuOpen(false)} + >
      -
      +
      -
      -
      -
      +
      +
      + +
      </div> - <div className="flex gap-[20px] text-textItemBlur"> + <div className="flex gap-[20px] mobile:gap-[14px] mobile:items-center text-textItemBlur"> <StreakComponent /> - <div className="w-[1px] h-[20px] bg-blockSeparator" /> + <div className="w-[1px] h-[20px] bg-blockSeparator mobile:hidden" /> <OrganizationSelector /> <div className="hover:text-newTextColor"> <ModeComponent /> </div> - <div className="w-[1px] h-[20px] bg-blockSeparator" /> + <div className="w-[1px] h-[20px] bg-blockSeparator mobile:hidden" /> <LanguageComponent /> - <ChromeExtensionComponent /> - <div className="w-[1px] h-[20px] bg-blockSeparator" /> - <AttachToFeedbackIcon /> + <div className="contents mobile:hidden"> + <ChromeExtensionComponent /> + </div> + <div className="w-[1px] h-[20px] bg-blockSeparator mobile:hidden" /> + <div className="contents mobile:hidden"> + <AttachToFeedbackIcon /> + </div> <NotificationComponent /> </div> </div> - <div className="flex flex-1 gap-[1px]">{children}</div> + <div className="flex flex-1 min-w-0 gap-[1px] mobile:flex-col"> + {children} + </div> </div> </div> </> diff --git a/apps/frontend/src/components/notifications/notification.component.tsx b/apps/frontend/src/components/notifications/notification.component.tsx index 731b80f96b..95b51b0ebe 100644 --- a/apps/frontend/src/components/notifications/notification.component.tsx +++ b/apps/frontend/src/components/notifications/notification.component.tsx @@ -63,7 +63,7 @@ export const NotificationOpenComponent = () => { return ( <div id="notification-popup" - className="opacity-0 animate-normalFadeDown mt-[10px] absolute w-[420px] min-h-[200px] top-[100%] end-0 bg-third text-textColor rounded-[16px] flex flex-col border border-tableBorder z-[600]" + className="opacity-0 animate-normalFadeDown mt-[10px] absolute w-[420px] max-w-[calc(100vw-24px)] min-h-[200px] top-[100%] end-0 bg-third text-textColor rounded-[16px] flex flex-col border border-tableBorder z-[600]" > <div className={`p-[16px] border-b border-tableBorder font-bold`} diff --git a/apps/frontend/src/components/platform-analytics/platform.analytics.tsx b/apps/frontend/src/components/platform-analytics/platform.analytics.tsx index 67761ac53a..4234c9f639 100644 --- a/apps/frontend/src/components/platform-analytics/platform.analytics.tsx +++ b/apps/frontend/src/components/platform-analytics/platform.analytics.tsx @@ -177,12 +177,12 @@ export const PlatformAnalytics = () => { <> <div className={clsx( - 'bg-newBgColorInner p-[20px] flex flex-col gap-[15px] transition-all', + 'bg-newBgColorInner p-[20px] mobile:p-[12px] flex flex-col gap-[15px] transition-all mobile:w-full', collapseMenu === '1' ? 'group sidebar w-[100px]' : 'w-[260px]' )} > <div className="flex gap-[12px] flex-col"> - <div className="flex items-center"> + <div className="flex items-center mobile:hidden"> <h2 className="group-[.sidebar]:hidden flex-1 text-[20px] font-[500]"> {t('channels')} </h2> @@ -207,75 +207,78 @@ export const PlatformAnalytics = () => { </svg> </div> </div> - {sortedIntegrations.map((integration, index) => ( - <div - key={integration.id} - onClick={() => { - if (integration.refreshNeeded) { - toaster.show( - 'Please refresh the integration from the calendar', - 'warning' - ); - return; - } - setRefresh(true); - setTimeout(() => { - setRefresh(false); - }, 10); - setCurrent(index); - }} - className={clsx( - 'flex gap-[12px] items-center group/profile justify-center hover:bg-boxHover rounded-e-[8px]', - currentIntegration.id !== integration.id && - 'opacity-20 hover:opacity-100 cursor-pointer' - )} - > + <div className="flex gap-[15px] mobile:gap-[12px] flex-col mobile:flex-row mobile:overflow-x-auto mobile:ps-[12px] mobile:pt-[6px] mobile:pb-[6px] scrollbar scrollbar-thumb-fifth scrollbar-track-newBgColor"> + {sortedIntegrations.map((integration, index) => ( <div + key={integration.id} + onClick={() => { + if (integration.refreshNeeded) { + toaster.show( + 'Please refresh the integration from the calendar', + 'warning' + ); + return; + } + setRefresh(true); + setTimeout(() => { + setRefresh(false); + }, 10); + setCurrent(index); + }} className={clsx( - 'relative rounded-full flex justify-center items-center gap-[6px]', - integration.disabled && 'opacity-50' + 'flex gap-[12px] items-center group/profile justify-center hover:bg-boxHover rounded-e-[8px] mobile:shrink-0', + currentIntegration.id !== integration.id && + 'opacity-20 hover:opacity-100 cursor-pointer' )} > - {(integration.inBetweenSteps || integration.refreshNeeded) && ( - <div className="absolute start-0 top-0 w-[39px] h-[46px] cursor-pointer"> - <div className="bg-red-500 w-[15px] h-[15px] rounded-full start-0 -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center"> - ! + <div + className={clsx( + 'relative rounded-full flex justify-center items-center gap-[6px]', + integration.disabled && 'opacity-50' + )} + > + {(integration.inBetweenSteps || + integration.refreshNeeded) && ( + <div className="absolute start-0 top-0 w-[39px] h-[46px] cursor-pointer"> + <div className="bg-red-500 w-[15px] h-[15px] rounded-full start-0 -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center"> + ! + </div> + <div className="bg-primary/60 w-[39px] h-[46px] start-0 top-0 absolute rounded-full z-[199]" /> </div> - <div className="bg-primary/60 w-[39px] h-[46px] start-0 top-0 absolute rounded-full z-[199]" /> + )} + <div className="h-full w-[4px] -ms-[12px] rounded-s-[3px] opacity-0 group-hover/profile:opacity-100 transition-opacity"> + <SVGLine /> </div> - )} - <div className="h-full w-[4px] -ms-[12px] rounded-s-[3px] opacity-0 group-hover/profile:opacity-100 transition-opacity"> - <SVGLine /> + <ImageWithFallback + fallbackSrc={`/icons/platforms/${integration.identifier}.png`} + src={integration.picture} + className="rounded-[8px]" + alt={integration.identifier} + width={36} + height={36} + /> + <SafeImage + src={`/icons/platforms/${integration.identifier}.png`} + className="rounded-[8px] absolute z-10 bottom-[5px] -end-[5px] border border-fifth" + alt={integration.identifier} + width={18.41} + height={18.41} + /> + </div> + <div + className={clsx( + 'flex-1 whitespace-nowrap text-ellipsis overflow-hidden group-[.sidebar]:hidden mobile:hidden', + integration.disabled && 'opacity-50' + )} + > + {integration.name} </div> - <ImageWithFallback - fallbackSrc={`/icons/platforms/${integration.identifier}.png`} - src={integration.picture} - className="rounded-[8px]" - alt={integration.identifier} - width={36} - height={36} - /> - <SafeImage - src={`/icons/platforms/${integration.identifier}.png`} - className="rounded-[8px] absolute z-10 bottom-[5px] -end-[5px] border border-fifth" - alt={integration.identifier} - width={18.41} - height={18.41} - /> - </div> - <div - className={clsx( - 'flex-1 whitespace-nowrap text-ellipsis overflow-hidden group-[.sidebar]:hidden', - integration.disabled && 'opacity-50' - )} - > - {integration.name} </div> - </div> - ))} + ))} + </div> </div> </div> - <div className="bg-newBgColorInner flex-1 flex-col flex p-[20px] gap-[12px]"> + <div className="bg-newBgColorInner flex-1 flex-col flex p-[20px] mobile:p-[12px] gap-[12px]"> {!!options.length && ( <div className="flex-1 flex flex-col gap-[14px]"> <div className="max-w-[200px]"> diff --git a/apps/frontend/src/components/plugs/plug.tsx b/apps/frontend/src/components/plugs/plug.tsx index de6978ec12..91da85c779 100644 --- a/apps/frontend/src/components/plugs/plug.tsx +++ b/apps/frontend/src/components/plugs/plug.tsx @@ -198,9 +198,9 @@ export const PlugItem: FC<{ <div onClick={() => addPlug(data)} key={plug.title} - className="w-full h-[300px] rounded-[8px] bg-newTableHeader hover:bg-newTableBorder" + className="w-full h-[300px] mobile:h-auto rounded-[8px] bg-newTableHeader hover:bg-newTableBorder" > - <div key={plug.title} className="p-[16px] h-full flex flex-col flex-1"> + <div key={plug.title} className="p-[16px] h-full flex flex-col flex-1 mobile:gap-[16px]"> <div className="flex"> <div className="text-[20px] mb-[8px] flex-1">{plug.title}</div> {!!data && ( @@ -263,7 +263,7 @@ export const Plug = () => { return null; } return ( - <div className="grid grid-cols-3 gap-[30px]"> + <div className="grid grid-cols-3 tablet:grid-cols-2 mobile:!grid-cols-1 gap-[30px] mobile:gap-[16px]"> {plug.plugs.map((p) => ( <PlugItem key={p.title + '-' + plug.providerId} diff --git a/apps/frontend/src/components/plugs/plugs.tsx b/apps/frontend/src/components/plugs/plugs.tsx index 6071981b9a..e27a57df7b 100644 --- a/apps/frontend/src/components/plugs/plugs.tsx +++ b/apps/frontend/src/components/plugs/plugs.tsx @@ -122,12 +122,12 @@ export const Plugs = () => { <> <div className={clsx( - 'bg-newBgColorInner p-[20px] flex flex-col gap-[15px] transition-all', + 'bg-newBgColorInner p-[20px] mobile:p-[12px] flex flex-col gap-[15px] transition-all mobile:w-full', collapseMenu === '1' ? 'group sidebar w-[100px]' : 'w-[260px]' )} > <div className="flex gap-[12px] flex-col"> - <div className="flex items-center"> + <div className="flex items-center mobile:hidden"> <h2 className="group-[.sidebar]:hidden flex-1 text-[20px] font-[500]"> {t('channels')} </h2> @@ -152,75 +152,78 @@ export const Plugs = () => { </svg> </div> </div> - {sortedIntegrations.map((integration, index) => ( - <div - key={integration.id} - onClick={() => { - if (integration.refreshNeeded) { - toaster.show( - 'Please refresh the integration from the calendar', - 'warning' - ); - return; - } - setRefresh(true); - setTimeout(() => { - setRefresh(false); - }, 10); - setCurrent(index); - }} - className={clsx( - 'flex gap-[8px] items-center justify-center group/profile hover:bg-boxHover rounded-e-[8px]', - currentIntegration.id !== integration.id && - 'opacity-20 hover:opacity-100 cursor-pointer' - )} - > + <div className="flex gap-[15px] mobile:gap-[12px] flex-col mobile:flex-row mobile:overflow-x-auto mobile:ps-[12px] mobile:pt-[6px] mobile:pb-[6px] scrollbar scrollbar-thumb-fifth scrollbar-track-newBgColor"> + {sortedIntegrations.map((integration, index) => ( <div + key={integration.id} + onClick={() => { + if (integration.refreshNeeded) { + toaster.show( + 'Please refresh the integration from the calendar', + 'warning' + ); + return; + } + setRefresh(true); + setTimeout(() => { + setRefresh(false); + }, 10); + setCurrent(index); + }} className={clsx( - 'relative rounded-full flex justify-center items-center gap-[8px]', - integration.disabled && 'opacity-50' + 'flex gap-[8px] items-center justify-center group/profile hover:bg-boxHover rounded-e-[8px] mobile:shrink-0', + currentIntegration.id !== integration.id && + 'opacity-20 hover:opacity-100 cursor-pointer' )} > - {(integration.inBetweenSteps || integration.refreshNeeded) && ( - <div className="absolute start-0 top-0 w-[39px] h-[46px] cursor-pointer"> - <div className="bg-red-500 w-[15px] h-[15px] rounded-full start-0 -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center"> - ! + <div + className={clsx( + 'relative rounded-full flex justify-center items-center gap-[8px]', + integration.disabled && 'opacity-50' + )} + > + {(integration.inBetweenSteps || + integration.refreshNeeded) && ( + <div className="absolute start-0 top-0 w-[39px] h-[46px] cursor-pointer"> + <div className="bg-red-500 w-[15px] h-[15px] rounded-full start-0 -top-[5px] absolute z-[200] text-[10px] flex justify-center items-center"> + ! + </div> + <div className="bg-primary/60 w-[39px] h-[46px] start-0 top-0 absolute rounded-full z-[199]" /> </div> - <div className="bg-primary/60 w-[39px] h-[46px] start-0 top-0 absolute rounded-full z-[199]" /> + )} + <div className="h-full w-[4px] -ms-[12px] rounded-s-[3px] opacity-0 group-hover/profile:opacity-100 transition-opacity"> + <SVGLine /> </div> - )} - <div className="h-full w-[4px] -ms-[12px] rounded-s-[3px] opacity-0 group-hover/profile:opacity-100 transition-opacity"> - <SVGLine /> + <ImageWithFallback + fallbackSrc={`/icons/platforms/${integration.identifier}.png`} + src={integration.picture} + className="rounded-[8px]" + alt={integration.identifier} + width={36} + height={36} + /> + <SafeImage + src={`/icons/platforms/${integration.identifier}.png`} + className="rounded-[8px] absolute z-10 bottom-[5px] -end-[5px] border border-fifth" + alt={integration.identifier} + width={18.41} + height={18.41} + /> + </div> + <div + className={clsx( + 'flex-1 whitespace-nowrap text-ellipsis overflow-hidden group-[.sidebar]:hidden mobile:hidden', + integration.disabled && 'opacity-50' + )} + > + {integration.name} </div> - <ImageWithFallback - fallbackSrc={`/icons/platforms/${integration.identifier}.png`} - src={integration.picture} - className="rounded-[8px]" - alt={integration.identifier} - width={36} - height={36} - /> - <SafeImage - src={`/icons/platforms/${integration.identifier}.png`} - className="rounded-[8px] absolute z-10 bottom-[5px] -end-[5px] border border-fifth" - alt={integration.identifier} - width={18.41} - height={18.41} - /> - </div> - <div - className={clsx( - 'flex-1 whitespace-nowrap text-ellipsis overflow-hidden group-[.sidebar]:hidden', - integration.disabled && 'opacity-50' - )} - > - {integration.name} </div> - </div> - ))} + ))} + </div> </div> </div> - <div className="bg-newBgColorInner flex-1 flex-col flex p-[20px] gap-[12px]"> + <div className="bg-newBgColorInner flex-1 flex-col flex p-[20px] mobile:p-[12px] gap-[12px]"> <PlugsContext.Provider value={currentIntegrationPlug}> <Plug /> </PlugsContext.Provider> diff --git a/apps/frontend/src/components/public-api/public.component.tsx b/apps/frontend/src/components/public-api/public.component.tsx index a47f576ccf..2378eb646b 100644 --- a/apps/frontend/src/components/public-api/public.component.tsx +++ b/apps/frontend/src/components/public-api/public.component.tsx @@ -329,7 +329,7 @@ const McpSection = ({ return ( <div className="bg-newBgColorInnerInner rounded-[12px] border border-newBorder overflow-hidden"> - <div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]"> + <div className="bg-newBgColorInner px-[20px] mobile:px-[14px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px] mobile:flex-col"> <div> <div className="text-[15px] font-[600]"> {t('mcp_client_configuration', 'MCP Client Configuration')} @@ -341,7 +341,7 @@ const McpSection = ({ )} </div> </div> - <div className="flex gap-[6px] shrink-0 pt-[2px]"> + <div className="flex flex-wrap gap-[6px] shrink-0 pt-[2px]"> {billingEnabled && ( <> <a @@ -372,7 +372,7 @@ const McpSection = ({ </a> </div> </div> - <div className="p-[20px] flex flex-col gap-[16px]"> + <div className="p-[20px] mobile:p-[14px] flex flex-col gap-[16px]"> {!chatOnly && ( <div className="flex flex-col gap-[6px]"> <div className="text-[13px] font-[600] text-customColor18"> @@ -441,7 +441,7 @@ const McpSection = ({ <pre className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-[13px] whitespace-pre-wrap break-all overflow-x-auto leading-[1.6]"> {maskedConfig} </pre> - <div className="flex gap-[8px]"> + <div className="flex flex-wrap gap-[8px]"> {auth === 'apikey' && !chatOnly && ( <button type="button" @@ -571,7 +571,7 @@ const CliSection = ({ apiKey }: { apiKey: string }) => { return ( <div className="bg-newBgColorInnerInner rounded-[12px] border border-newBorder overflow-hidden"> - <div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]"> + <div className="bg-newBgColorInner px-[20px] mobile:px-[14px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px] mobile:flex-col"> <div> <div className="text-[15px] font-[600]"> {t('cli_and_skills', 'CLI & AI Skills')} @@ -583,7 +583,7 @@ const CliSection = ({ apiKey }: { apiKey: string }) => { )} </div> </div> - <div className="flex gap-[6px] shrink-0 pt-[2px]"> + <div className="flex flex-wrap gap-[6px] shrink-0 pt-[2px]"> <a className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]" href="https://docs.postiz.com/cli/introduction" @@ -594,7 +594,7 @@ const CliSection = ({ apiKey }: { apiKey: string }) => { </a> </div> </div> - <div className="p-[20px] flex flex-col gap-[16px]"> + <div className="p-[20px] mobile:p-[14px] flex flex-col gap-[16px]"> <div className="flex gap-[6px]"> {(['local', 'ci'] as const).map((m) => ( <button @@ -624,7 +624,7 @@ const CliSection = ({ apiKey }: { apiKey: string }) => { </pre> </div> ))} - <div className="flex gap-[8px]"> + <div className="flex flex-wrap gap-[8px]"> {mode === 'ci' && ( <button type="button" @@ -727,7 +727,7 @@ const PublicApiContent = () => { )} </div> <div className="bg-newBgColorInnerInner rounded-[12px] border border-newBorder overflow-hidden"> - <div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]"> + <div className="bg-newBgColorInner px-[20px] mobile:px-[14px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px] mobile:flex-col"> <div> <div className="text-[15px] font-[600]"> {t('api_key', 'API Key')} @@ -739,7 +739,7 @@ const PublicApiContent = () => { )} </div> </div> - <div className="flex gap-[6px] shrink-0 pt-[2px]"> + <div className="flex flex-wrap gap-[6px] shrink-0 pt-[2px]"> <a className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]" href="https://docs.postiz.com/public-api" @@ -758,7 +758,7 @@ const PublicApiContent = () => { </a> </div> </div> - <div className="p-[20px] flex flex-col gap-[16px]"> + <div className="p-[20px] mobile:p-[14px] flex flex-col gap-[16px]"> <div className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] flex items-center overflow-hidden"> <code className="text-[14px] flex-1 truncate"> {reveal ? ( @@ -773,7 +773,7 @@ const PublicApiContent = () => { )} </code> </div> - <div className="flex gap-[8px]"> + <div className="flex flex-wrap gap-[8px]"> <button type="button" onClick={() => setReveal(!reveal)} diff --git a/apps/frontend/src/components/third-parties/third-party.component.tsx b/apps/frontend/src/components/third-parties/third-party.component.tsx index 056c3780c3..503467a8a4 100644 --- a/apps/frontend/src/components/third-parties/third-party.component.tsx +++ b/apps/frontend/src/components/third-parties/third-party.component.tsx @@ -64,7 +64,7 @@ export const ThirdPartyMenuComponent: FC<{ {show && ( <div onClick={(e) => e.stopPropagation()} - className={`absolute top-[100%] start-0 p-[8px] px-[20px] bg-fifth flex flex-col gap-[16px] z-[100] rounded-[8px] border border-tableBorder text-nowrap`} + className={`absolute top-[100%] start-0 mobile:start-auto mobile:end-0 p-[8px] px-[20px] bg-fifth flex flex-col gap-[16px] z-[100] rounded-[8px] border border-tableBorder text-nowrap`} > <div className="flex gap-[12px] items-center" @@ -116,18 +116,18 @@ export const ThirdPartyComponent = () => { <> <div className={clsx( - 'bg-newBgColorInner p-[20px] flex flex-col gap-[15px] transition-all', + 'bg-newBgColorInner p-[20px] mobile:p-[12px] flex flex-col gap-[15px] transition-all mobile:w-full', collapseMenu === '1' ? 'group sidebar w-[100px]' : 'w-[260px]' )} > <div className="flex gap-[12px] flex-col"> <div className="flex items-center"> - <h2 className="group-[.sidebar]:hidden flex-1 text-[20px] font-[500]"> + <h2 className="group-[.sidebar]:hidden mobile:group-[.sidebar]:block flex-1 text-[20px] mobile:text-[18px] font-[500]"> {t('integrations')} </h2> <div onClick={() => setCollapseMenu(collapseMenu === '1' ? '0' : '1')} - className="group-[.sidebar]:rotate-[180deg] group-[.sidebar]:mx-auto text-btnText bg-btnSimple rounded-[6px] w-[24px] h-[24px] flex items-center justify-center cursor-pointer select-none" + className="group-[.sidebar]:rotate-[180deg] group-[.sidebar]:mx-auto mobile:hidden text-btnText bg-btnSimple rounded-[6px] w-[24px] h-[24px] flex items-center justify-center cursor-pointer select-none" > <svg xmlns="http://www.w3.org/2000/svg" @@ -184,7 +184,7 @@ export const ThirdPartyComponent = () => { // @ts-ignore role="Handle" className={clsx( - 'flex-1 whitespace-nowrap text-ellipsis overflow-hidden group-[.sidebar]:hidden' + 'flex-1 whitespace-nowrap text-ellipsis overflow-hidden group-[.sidebar]:hidden mobile:group-[.sidebar]:block' )} data-tooltip-id="tooltip" data-tooltip-content={p.title} @@ -200,7 +200,7 @@ export const ThirdPartyComponent = () => { </div> </div> </div> - <div className="bg-newBgColorInner flex-1 flex-col flex p-[20px] gap-[12px]"> + <div className="bg-newBgColorInner flex-1 flex-col flex p-[20px] mobile:p-[12px] gap-[12px]"> <ThirdPartyListComponent reload={mutate} /> </div> </> diff --git a/apps/frontend/src/components/third-parties/third-party.list.component.tsx b/apps/frontend/src/components/third-parties/third-party.list.component.tsx index 9dcaea7b25..f8d7a842c1 100644 --- a/apps/frontend/src/components/third-parties/third-party.list.component.tsx +++ b/apps/frontend/src/components/third-parties/third-party.list.component.tsx @@ -126,12 +126,12 @@ export const ThirdPartyListComponent: FC<{ reload: () => void }> = (props) => { ); return ( - <div className="grid grid-cols-4 gap-[10px] justify-items-center justify-center"> + <div className="grid grid-cols-4 tablet:grid-cols-3 mobile:!grid-cols-2 gap-[10px] justify-items-center justify-center"> {data?.map((p: any) => ( <div onClick={addApiKey(p.title, p.identifier)} key={p.identifier} - className="w-full h-full p-[20px] min-h-[100px] text-[14px] bg-newTableHeader hover:bg-newTableBorder rounded-[8px] transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer" + className="w-full h-full p-[20px] mobile:p-[14px] min-h-[100px] text-[14px] bg-newTableHeader hover:bg-newTableBorder rounded-[8px] transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer" > <div> <img diff --git a/apps/frontend/src/components/third-parties/third-party.media-library.tsx b/apps/frontend/src/components/third-parties/third-party.media-library.tsx index 9b02713f11..a0630acf5f 100644 --- a/apps/frontend/src/components/third-parties/third-party.media-library.tsx +++ b/apps/frontend/src/components/third-parties/third-party.media-library.tsx @@ -91,7 +91,7 @@ const ThirdPartyMediaLibraryBrowser: FC<{ <div className="flex-1 relative"> <div className="absolute left-0 top-0 w-full h-full overflow-x-hidden overflow-y-auto scrollbar scrollbar-thumb-newColColor scrollbar-track-newBgColorInner"> {isLoading && ( - <div className="grid grid-cols-4 gap-[8px]"> + <div className="grid grid-cols-4 mobile:grid-cols-3 gap-[8px]"> {[...new Array(8)].map((_, i) => ( <div key={i} @@ -106,7 +106,7 @@ const ThirdPartyMediaLibraryBrowser: FC<{ </div> )} {!isLoading && !!data?.results?.length && ( - <div className="grid grid-cols-4 gap-[8px]"> + <div className="grid grid-cols-4 mobile:grid-cols-3 gap-[8px]"> {data.results.map((item: any) => { const isSelected = !!selected.find((s) => s.id === item.id); return ( diff --git a/apps/frontend/src/components/third-parties/third-party.media.tsx b/apps/frontend/src/components/third-parties/third-party.media.tsx index 252409259e..c523bcdc5a 100644 --- a/apps/frontend/src/components/third-parties/third-party.media.tsx +++ b/apps/frontend/src/components/third-parties/third-party.media.tsx @@ -95,14 +95,14 @@ export const ThirdPartyPopup: FC<{ return ( <div className={clsx('flex flex-wrap flex-col gap-[10px] pt-[20px]')}> {!thirdParty && ( - <div className="grid grid-cols-4 gap-[10px] justify-items-center justify-center"> + <div className="grid grid-cols-4 mobile:grid-cols-2 gap-[10px] justify-items-center justify-center"> {thirdParties.map((p: any) => ( <div onClick={() => { setThirdParty(p); }} key={p.identifier} - className="w-full h-full p-[20px] min-h-[100px] text-[14px] bg-newTableHeader hover:bg-newTableBorder rounded-[8px] transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer" + className="w-full h-full p-[20px] mobile:p-[14px] min-h-[100px] text-[14px] bg-newTableHeader hover:bg-newTableBorder rounded-[8px] transition-all text-textColor relative flex flex-col gap-[15px] cursor-pointer" > <div> <img From 9554e9f606c532b4e081021d2c870cd2f71c19fe Mon Sep 17 00:00:00 2001 From: Gilad Resisi <giladresisi@gmail.com> Date: Fri, 2 Oct 2026 12:26:50 +0700 Subject: [PATCH 47/53] fix(media): fail a stalled media read instead of repeating the publish A stall does not clear within the workflow's retry window, so repeating the publish only re-reads the same media and re-uploads what was already sent while holding the provider's queue slot. Throw BadBody once the read's own retry is spent, which drops the need for a new workflow version. --- .../src/activities/post.activity.ts | 2 +- apps/orchestrator/src/workflows/index.ts | 1 - .../post-workflows/post.workflow.v1.1.3.ts | 740 ------------------ .../database/prisma/posts/posts.service.ts | 2 +- .../src/integrations/social.abstract.ts | 36 +- 5 files changed, 12 insertions(+), 769 deletions(-) delete mode 100644 apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.3.ts diff --git a/apps/orchestrator/src/activities/post.activity.ts b/apps/orchestrator/src/activities/post.activity.ts index 47ca08da97..5c18c34759 100644 --- a/apps/orchestrator/src/activities/post.activity.ts +++ b/apps/orchestrator/src/activities/post.activity.ts @@ -117,7 +117,7 @@ export class PostActivity { for (const post of list) { await this._temporalService.client .getRawClient() - .workflow.signalWithStart('postWorkflowV113', { + .workflow.signalWithStart('postWorkflowV112', { workflowId: `post_${post.id}`, taskQueue: 'main', signal: 'poke', diff --git a/apps/orchestrator/src/workflows/index.ts b/apps/orchestrator/src/workflows/index.ts index 4966787a0d..45924bd7d9 100644 --- a/apps/orchestrator/src/workflows/index.ts +++ b/apps/orchestrator/src/workflows/index.ts @@ -10,7 +10,6 @@ export * from './post-workflows/post.workflow.v1.0.9'; export * from './post-workflows/post.workflow.v1.1.0'; export * from './post-workflows/post.workflow.v1.1.1'; export * from './post-workflows/post.workflow.v1.1.2'; -export * from './post-workflows/post.workflow.v1.1.3'; export * from './autopost.workflow'; export * from './digest.email.workflow'; export * from './missing.post.workflow'; diff --git a/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.3.ts b/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.3.ts deleted file mode 100644 index 03e1e514ae..0000000000 --- a/apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.3.ts +++ /dev/null @@ -1,740 +0,0 @@ -import { PostActivity } from '@gitroom/orchestrator/activities/post.activity'; -import { - ActivityFailure, - ApplicationFailure, - startChild, - proxyActivities, - sleep, - defineSignal, - setHandler, -} from '@temporalio/workflow'; -import dayjs from 'dayjs'; -import { Integration } from '@prisma/client'; -import { capitalize, sortBy } from 'lodash'; -import { PostResponse } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; -import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; -import { - TimeoutFailure, - TimeoutType, - TypedSearchAttributes, -} from '@temporalio/common'; -import { postId as postIdSearchParam } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute'; - -// The publishing activities heartbeat every 15s (withHeartbeat sends its first -// heartbeat at the first interval tick, not at entry), so a heartbeat timeout -// whose failure carries no heartbeat details means the server never received -// one: the worker never ran the activity, or it died within its first seconds. -const HEARTBEAT_TIMEOUT = 3 * 60 * 1000; - -const proxyTaskQueue = (taskQueue: string) => { - return proxyActivities<PostActivity>({ - startToCloseTimeout: '10 minute', - taskQueue, - retry: { - maximumAttempts: 3, - backoffCoefficient: 1, - initialInterval: '2 minutes', - }, - }); -}; - -// postComment publishes through providers that can legitimately run long -// (media conversion + upload), so it gets a large time budget. The -// heartbeatTimeout exists to detect an activity that was never started: the -// activity heartbeats every 15s, so no heartbeat at all means the worker never -// ran it and nothing was published. No SDK retries: an -// automatic retry of a heartbeat timeout would run again even when the first -// attempt did publish, duplicating the comment. The workflow decides whether -// a failure is safe to retry (see handleActivityError). -const proxyCommentTaskQueue = (taskQueue: string) => { - return proxyActivities<PostActivity>({ - startToCloseTimeout: '30 minute', - heartbeatTimeout: HEARTBEAT_TIMEOUT, - taskQueue, - retry: { - maximumAttempts: 1, - }, - }); -}; - -// checkPostStatus is a single read-only status call, so it gets a short timeout -// and fast retries - retrying it can never duplicate a post. -const proxyCheckTaskQueue = (taskQueue: string) => { - return proxyActivities<PostActivity>({ - startToCloseTimeout: '2 minute', - taskQueue, - retry: { - maximumAttempts: 3, - backoffCoefficient: 1, - initialInterval: '10 seconds', - }, - }); -}; - -// postSocialPending / finalizePost run irreversible publishing mutations, so no -// automatic retries - a retried activity whose previous (timed-out) attempt -// still completed in the background would publish twice. The workflow retries -// deliberately, and treats timeouts as "outcome unknown". -// The heartbeatTimeout exists to detect an activity that was never started: -// both activities heartbeat every 15s, so no heartbeat at all means the -// worker never ran them and nothing was published. The workflow -// decides whether that is safe to retry (see handleActivityError); every -// other timeout still marks the post as unconfirmed. -const proxyMutationTaskQueue = (taskQueue: string) => { - return proxyActivities<PostActivity>({ - startToCloseTimeout: '30 minute', - heartbeatTimeout: HEARTBEAT_TIMEOUT, - taskQueue, - retry: { - maximumAttempts: 1, - }, - }); -}; - -const { - getPostsList, - getPost, - inAppNotification, - changeState, - updatePost, - sendWebhooks, - isCommentable, -} = proxyActivities<PostActivity>({ - startToCloseTimeout: '10 minute', - retry: { - maximumAttempts: 3, - backoffCoefficient: 1, - initialInterval: '2 minutes', - }, -}); - -const poke = defineSignal('poke'); - -const iterate = Array.from({ length: 5 }); - -// ~30 minutes at 20s interval (longer than the old in-activity loop, timers are -// free). Multi-item flows (stories, chunked uploads) consume several checks per -// item, so the budget must cover the largest realistic post, not one poll cycle. -const maxPendingChecks = 90; - -export async function postWorkflowV113({ - taskQueue, - postId, - organizationId, - postNow = false, -}: { - taskQueue: string; - postId: string; - organizationId: string; - postNow?: boolean; -}) { - // Dynamic task queue, for concurrency - const { - getIntegrationById, - refreshTokenWithCause, - internalPlugs, - globalPlugs, - processInternalPlug, - processPlug, - } = proxyTaskQueue(taskQueue); - - const { checkPostStatus } = proxyCheckTaskQueue(taskQueue); - - const { postComment } = proxyCommentTaskQueue(taskQueue); - - const { postSocialPending, finalizePost } = proxyMutationTaskQueue(taskQueue); - - let poked = false; - setHandler(poke, () => { - poked = true; - }); - - // get all the posts and comments to post - const firstPost = await getPost(organizationId, postId); - - // in case doesn't exists for some reason, fail it - if (!firstPost) { - await changeState(postId, 'ERROR', 'No Post'); - return; - } - - if (!postNow && firstPost.state !== 'QUEUE') { - await changeState(firstPost.id, 'ERROR', 'Already posted', [firstPost]); - return; - } - - // wait for the scheduled publish date - if (!postNow) { - await sleep( - dayjs(firstPost.publishDate).isBefore(dayjs()) - ? 0 - : dayjs(firstPost.publishDate).diff(dayjs(), 'millisecond') - ); - } - - // Captured AFTER the scheduling sleep: the repeat-post delay is - // "interval minus time spent publishing", so it must be measured from the - // publish time, not from when the workflow was started. Measuring from the - // workflow start subtracted the whole scheduling wait from the interval - // (a post scheduled further out than its interval repeated immediately). - const startTime = new Date(); - - const postsListBefore = await getPostsList(organizationId, postId); - const [post] = postsListBefore; - - if (!post) { - await changeState(postId, 'ERROR', 'No Post'); - return; - } - - // if refresh is needed from last time, let's inform the user - if (post.integration?.refreshNeeded) { - await inAppNotification( - post.organizationId, - `We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`, - `We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because you need to reconnect it. Please enable it and try again.`, - true, - false, - 'info' - ); - - await changeState( - postsListBefore[0].id, - 'ERROR', - 'Refresh channel needed', - postsListBefore - ); - return; - } - - // if it's disabled, inform the user - if (post.integration?.disabled) { - await inAppNotification( - post.organizationId, - `We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`, - `We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because it's disabled. Please enable it and try again.`, - true, - false, - 'info' - ); - - await changeState( - postsListBefore[0].id, - 'ERROR', - 'Channel disabled', - postsListBefore - ); - return; - } - - // Do we need to post comment for this social? - const toComment: boolean = - postsListBefore.length === 1 - ? false - : await isCommentable(post.integration); - - const postsList = toComment ? postsListBefore : [postsListBefore[0]]; - - // list of all the saved results - const postsResults: PostResponse[] = []; - - // Every catch block below used to repeat the same failure classification, so - // it is centralized here: detect the failure type, refresh the token when - // needed, and tell the caller what to do. - // 'retry' - the token was refreshed, or the activity never started (heartbeat - // timeout with no heartbeat), run the action again - // 'stop' - the token could not be refreshed - // 'bad-body' - the platform rejected the action - // 'timeout' - the activity timed out, its outcome is unknown - // 'unknown' - anything else (transient errors) - // - // A media read that stalled and ran out of its own retries arrives as - // 'media_stall'. Nothing was sent to the platform (the bytes never left - // storage), so the publish is simply repeated. It maps to 'retry' rather - // than 'unknown' on purpose: 'unknown' writes an ERROR on the post before - // each new attempt, and a user seeing that red state mid-retry reads it as - // "we gave up" and publishes again by hand, which is the duplicate this - // workflow exists to prevent. - const handleActivityError = async ( - err: unknown, - getIntegration?: () => Promise<any>, - heartbeats?: boolean - ): Promise<{ - type: 'retry' | 'stop' | 'bad-body' | 'timeout' | 'unknown'; - message: string; - }> => { - if ( - err instanceof ActivityFailure && - err.cause instanceof TimeoutFailure - ) { - // The server copies the last heartbeat details it received into the - // timeout failure. None at all (undefined) means it never received a - // heartbeat: the worker never ran the activity, so nothing was published - // and it is safe to run again. Any details mean the activity ran and - // then stalled, so its outcome is unknown. This relies on withHeartbeat - // always sending a non-empty string ("<activityType>: entered" at least): - // a heartbeat sent with undefined details also leaves the failure - // without details. Only the callers of heartbeating activities opt in; - // no time window, so activity dispatch delay cannot skew the decision. - if ( - heartbeats && - err.cause.timeoutType === TimeoutType.HEARTBEAT && - !err.cause.lastHeartbeatDetails - ) { - return { type: 'retry', message: '' }; - } - return { type: 'timeout', message: '' }; - } - - const cause = - err instanceof ActivityFailure && err.cause instanceof ApplicationFailure - ? err.cause - : undefined; - - if (cause?.type === 'refresh_token') { - const refresh = await refreshTokenWithCause( - getIntegration ? await getIntegration() : post.integration, - cause.message || '' - ); - if (!refresh || !refresh.accessToken) { - return { type: 'stop', message: cause.message || '' }; - } - - if (!getIntegration) { - post.integration.token = refresh.accessToken; - } - - return { type: 'retry', message: cause.message || '' }; - } - - if (cause?.type === 'media_stall') { - return { type: 'retry', message: '' }; - } - - if (cause?.type === 'bad_body') { - return { type: 'bad-body', message: cause.message || '' }; - } - - return { type: 'unknown', message: '' }; - }; - - // The platform may have accepted the post but we can't confirm it was - // published - mark the error with a distinct message so the user checks the - // account before reposting manually and duplicating it. - const markUnconfirmed = async (err: any) => { - await changeState(postsList[0].id, 'ERROR', err, postsList); - await inAppNotification( - post.organizationId, - `We couldn't confirm your post on ${capitalize( - post.integration?.providerIdentifier - )}`, - `Your post was sent to ${capitalize( - post.integration?.providerIdentifier - )}, but we couldn't confirm it was published. Please check your ${ - post?.integration?.name - } account before posting again to avoid duplicates.`, - true, - false, - 'fail' - ); - }; - - // The post/comment was already accepted by the platform but returned as - // "pending": poll the read-only status check with durable timers until it - // completes. Errors are fully handled here (never rethrown), otherwise they - // would bubble to the posting retry loop and re-run the publish. - const resolvePending = async ( - pending: PostResponse - ): Promise<PostResponse | false> => { - let pendingData = pending.pendingData; - let errorAttempts = 0; - let heartbeats = false; - - for (let check = 0; check < maxPendingChecks; check++) { - // only finalizePost heartbeats, so a checkPostStatus failure must never - // be classified as never started - heartbeats = false; - try { - let result = await checkPostStatus(post.integration, pendingData); - - // commit the check's state BEFORE finalizePost runs: if finalize dies - // mid-mutation, the next check must see what it had already authorized, - // so providers can detect the interrupted attempt instead of running - // the mutation again - if (result.status !== 'completed') { - pendingData = result.pendingData; - } - - // polling is done, run the remaining provider mutations - if (result.status === 'ready') { - heartbeats = true; - result = await finalizePost(post.integration, result.pendingData); - } - - if (result.status === 'completed') { - return { - id: pending.id, - postId: result.postId, - releaseURL: result.releaseURL, - status: 'success', - }; - } - - pendingData = result.pendingData; - - // a fully successful iteration proves the platform is reachable: the - // error budget bounds consecutive failures, not blips accumulated over - // a long upload - errorAttempts = 0; - } catch (err) { - const handle = await handleActivityError(err, undefined, heartbeats); - - // token refreshed, or finalize never started, check again right away - if (handle.type === 'retry') { - continue; - } - - // the token could not be refreshed while checking, but the platform - // already accepted the post - warn about a possible live post - if (handle.type === 'stop') { - await markUnconfirmed(err); - return false; - } - - // the platform explicitly failed the post, it was not published - if (handle.type === 'bad-body') { - await changeState(postsList[0].id, 'ERROR', err, postsList); - await inAppNotification( - post.organizationId, - `Error posting on ${post.integration?.providerIdentifier} for ${post?.integration?.name}`, - `An error occurred while posting on ${ - post.integration?.providerIdentifier - }${handle.message ? `: ${handle.message}` : ``}`, - true, - false, - 'fail' - ); - return false; - } - - // unknown error on a read-only check, retry a few more times - errorAttempts++; - if (errorAttempts >= iterate.length) { - break; - } - } - - // the platform is still processing, wait before the next check - await sleep('20 seconds'); - } - - // no verdict from the platform after all the checks - await markUnconfirmed('Could not confirm the post status'); - return false; - }; - - // iterate over the posts - for (let i = 0; i < postsList.length; i++) { - const before = postsResults.length; - // once the platform accepted the post, the catch below must never retry - // the publish - retrying after updatePost / notification errors would - // duplicate the post - let posted = false; - let updated = false; - // this is a small trick to repeat an action in case of token refresh - for (const _ of iterate) { - // both publish calls run heartbeating activities, but the timed-out - // status checks below must never be mistaken for never-started - let heartbeats = false; - try { - // first post the main post - if (i === 0) { - heartbeats = true; - postsResults.push( - ...(await postSocialPending(post.integration as Integration, [ - postsList[i], - ])) - ); - - // then post the comments if any - } else { - if (postsList[i].delay) { - await sleep(60000 * Math.max(0, Number(postsList[i].delay ?? 0))); - } - - heartbeats = true; - postsResults.push( - ...(await postComment( - postsResults[0].postId, - postsResults.length === 1 - ? undefined - : postsResults[i - 1].postId, - post.integration, - [postsList[i]] - )) - ); - } - - posted = true; - - // the platform accepted the post but is still processing it: resolve - // it here before marking anything, resolvePending handles its own - // errors so a failed status check can never re-run the publish above - if (postsResults[i].status === 'pending') { - let resolved: PostResponse | false = false; - try { - resolved = await resolvePending(postsResults[i]); - } catch (err) { - // never let a pending-resolution error reach the outer catch, it - // would retry the post and duplicate it. Best-effort error state, - // otherwise the post stays in QUEUE and the missing-posts sweep - // would re-publish it. - try { - await markUnconfirmed(err); - } catch (e) { - /**empty**/ - } - resolved = false; - } - if (!resolved) { - return false; - } - postsResults[i] = resolved; - } - - // mark post as successful - await updatePost( - postsList[i].id, - postsResults[i].postId, - postsResults[i].releaseURL - ); - updated = true; - - if (i === 0) { - // send notification on a sucessful post - await inAppNotification( - post.integration.organizationId, - `Your post has been published on ${capitalize( - post.integration.providerIdentifier - )}`, - `Your post has been published on ${capitalize( - post.integration.providerIdentifier - )} at ${postsResults[0].releaseURL}`, - true, - true - ); - } - - // break the current while to move to the next post - break; - } catch (err) { - // the post is already live: never re-run the publish - if (posted) { - if (!updated) { - // still marked QUEUE, record the error so the missing-posts sweep - // doesn't re-publish it - try { - await markUnconfirmed(err); - } catch (e) { - /**empty**/ - } - return false; - } - - // already marked published, a failed notification shouldn't abort - // the rest of the flow - break; - } - - const handle = await handleActivityError(err, undefined, heartbeats); - - // token refreshed, or the publish never started, repeat the action - if (handle.type === 'retry') { - continue; - } - - // the activity timed out: the platform may still complete the publish - // in the background, so never retry it - if (handle.type === 'timeout') { - try { - await markUnconfirmed(err); - } catch (e) { - /**empty**/ - } - return false; - } - - // for other errors, change state and inform the user if needed - await changeState(postsList[0].id, 'ERROR', err, postsList); - - if (handle.type === 'stop') { - return false; - } - - // specific case for bad body errors - if (handle.type === 'bad-body') { - await inAppNotification( - post.organizationId, - `Error posting${i === 0 ? ' ' : ' comments '}on ${ - post.integration?.providerIdentifier - } for ${post?.integration?.name}`, - `An error occurred while posting${i === 0 ? ' ' : ' comments '}on ${ - post.integration?.providerIdentifier - }${handle.message ? `: ${handle.message}` : ``}`, - true, - false, - 'fail' - ); - return false; - } - } - } - - if (postsResults.length === before) { - // all retries exhausted without success: record it, otherwise the post - // stays in QUEUE with no error and the missing-posts sweep re-publishes - // it. A retried publish may have run without reporting, so treat the - // outcome as unknown. - try { - await markUnconfirmed('Could not publish after several attempts'); - } catch (e) { - /**empty**/ - } - return false; - } - } - - // send webhooks for the post - await sendWebhooks( - postsResults[0].postId, - post.organizationId, - post.integration.id - ); - - // load internal plugs like repost by other users - const internalPlugsList = await internalPlugs( - post.integration, - JSON.parse(post.settings) - ); - - // load global plugs, like repost a post if it gets to a certain number of likes - const globalPlugsList = (await globalPlugs(post.integration)).reduce( - (all, current) => { - for (let i = 1; i <= current.totalRuns; i++) { - all.push({ - ...current, - delay: current.delay * i, - }); - } - - return all; - }, - [] - ); - - // Check if the post is repeatable - const repeatPost = !post.intervalInDays - ? [] - : [ - { - type: 'repeat-post', - delay: - post.intervalInDays * 24 * 60 * 60 * 1000 - - (new Date().getTime() - startTime.getTime()), - }, - ]; - - // Sort all the actions by delay, so we can process them in order - const list = sortBy( - [...internalPlugsList, ...globalPlugsList, ...repeatPost], - 'delay' - ); - - // process all the plugs in order, we are using while because in some cases we need to remove items from the list - while (list.length > 0) { - // get the next to process - const todo = list.shift(); - - // wait for the delay - await sleep(Math.max(0, Number(todo.delay ?? 0))); - - // process internal plug - if (todo.type === 'internal-plug') { - for (const _ of iterate) { - try { - await processInternalPlug({ ...todo, post: postsResults[0].postId }); - } catch (err) { - const handle = await handleActivityError(err, () => - getIntegrationById(organizationId, todo.integration) - ); - - if (handle.type === 'stop' || handle.type === 'bad-body') { - break; - } - - continue; - } - break; - } - } - - // process global plug - if (todo.type === 'global') { - for (const _ of iterate) { - try { - const process = await processPlug({ - ...todo, - postId: postsResults[0].postId, - }); - if (process) { - const toDelete = list - .reduce((all, current, index) => { - if (current.plugId === todo.plugId) { - all.push(index); - } - - return all; - }, []) - .reverse(); - - for (const index of toDelete) { - list.splice(index, 1); - } - } - } catch (err) { - const handle = await handleActivityError(err); - - if (handle.type === 'stop' || handle.type === 'bad-body') { - break; - } - - continue; - } - - break; - } - } - - // process repeat post in a new workflow, this is important so the other plugs can keep running - if (todo.type === 'repeat-post') { - await startChild(postWorkflowV113, { - parentClosePolicy: 'ABANDON', - args: [ - { - taskQueue, - postId, - organizationId, - postNow: true, - }, - ], - workflowId: `post_${post.id}_${makeId(10)}`, - typedSearchAttributes: new TypedSearchAttributes([ - { - key: postIdSearchParam, - value: postId, - }, - ]), - }); - } - } -} diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 73463f979c..31ecc48bf3 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -748,7 +748,7 @@ export class PostsService { try { await this._temporalService.client .getRawClient() - ?.workflow.start('postWorkflowV113', { + ?.workflow.start('postWorkflowV112', { workflowId: `post_${postId}`, taskQueue: 'main', workflowIdConflictPolicy: 'TERMINATE_EXISTING', diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 5cb72a7331..1b9c74358d 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -98,26 +98,6 @@ export class Disconnect extends ApplicationFailure { } } -// A media read that answered its headers and then stalled, after its own -// retry was spent. Nothing has been sent to the platform at that point, so the -// workflow repeats the publish instead of failing the post. -export class MediaStall extends ApplicationFailure { - constructor(identifier: string, json: string, body: BodyInit, message = '') { - super( - truncateForTemporal(message, MAX_FAILURE_MESSAGE), - 'media_stall', - true, - [ - { - identifier, - json: truncateForTemporal(json, MAX_FAILURE_FIELD), - body: truncateForTemporal(body, MAX_FAILURE_FIELD), - }, - ] - ); - } -} - export class BadBody extends ApplicationFailure { constructor(identifier: string, json: string, body: BodyInit, message = '') { super(truncateForTemporal(message, MAX_FAILURE_MESSAGE), 'bad_body', true, [ @@ -299,11 +279,11 @@ export abstract class SocialAbstract { return this.readOrFetch(path, true); } - throw new MediaStall( + throw new BadBody( '', - '{}', + JSON.stringify({ timeoutMs: MEDIA_READ_TIMEOUT, path: stripQuery(path) }), Buffer.from('{}'), - `Media read timed out after ${MEDIA_READ_TIMEOUT}ms (${stripQuery(path)})` + 'We could not read the media for this post, so it was not published' ); } } @@ -391,11 +371,15 @@ export abstract class SocialAbstract { return this.mediaChunk(path, start, end, identifier, true); } - throw new MediaStall( + // BadBody rather than a retryable error: the read already retried + // itself, and repeating the publish would re-read the same media and + // re-upload everything already sent while holding the provider's + // queue slot. The message is what the user is shown. + throw new BadBody( identifier, - '{}', + JSON.stringify({ timeoutMs: MEDIA_READ_TIMEOUT, start, end }), Buffer.from('{}'), - `Media read timed out after ${MEDIA_READ_TIMEOUT}ms (bytes ${start}-${end})` + 'We could not read the media for this post, so it was not published' ); } } From f5564d9f0b0208a19d040a2c85cf5df00821b25b Mon Sep 17 00:00:00 2001 From: Gilad Resisi <giladresisi@gmail.com> Date: Fri, 2 Oct 2026 15:20:23 +0700 Subject: [PATCH 48/53] fix(media): give a media read two minutes before calling it stalled The slowest healthy read measured was about eleven seconds, so a minute was already generous, but a stalled post fails on this timeout and there is no cost to being certain. --- .../nestjs-libraries/src/integrations/social.abstract.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index 1b9c74358d..472e342d7d 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -15,10 +15,10 @@ import { createReadStream, readFileSync, statSync } from 'fs'; import { Readable } from 'stream'; // Media reads answer headers in well under a second and the chunk sizes the -// providers ask for finish in under two, so a minute is only ever reached by a -// stalled transfer. Matches the AbortSignal.timeout values used for the other -// outbound media calls. -const MEDIA_READ_TIMEOUT = 60_000; +// providers ask for finish in under two, with the slowest healthy read we +// measured at about eleven, so two minutes is only ever reached by a stalled +// transfer. +const MEDIA_READ_TIMEOUT = 120_000; // AbortSignal.timeout rejects with a TimeoutError, which undici may surface // directly or wrap as the cause of the fetch rejection. From ca07a518b85b131518d34e6157fba220ce82403a Mon Sep 17 00:00:00 2001 From: Gilad Resisi <giladresisi@gmail.com> Date: Fri, 2 Oct 2026 15:50:01 +0700 Subject: [PATCH 49/53] refactor(posts): extract release resolution into resolveRelease Returns the current releaseId and releaseURL so other callers can reuse it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- .../database/prisma/posts/posts.service.ts | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 6a9c88a0be..4c63d3d728 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -156,6 +156,37 @@ export class PostsService { return this._postRepository.updateReleaseId(postId, orgId, releaseId); } + async resolveRelease( + orgId: string, + post: { + id: string; + releaseId: string; + releaseURL: string; + integration: Integration; + } + ) { + const integrationProvider = this._integrationManager.getSocialIntegration( + post.integration.providerIdentifier + ); + + const resolved = await integrationProvider.resolveReleaseId?.( + post.integration.token, + post.releaseId, + post.integration + ); + if (!resolved || resolved.postId === post.releaseId) { + return { releaseId: post.releaseId, releaseURL: post.releaseURL }; + } + + await this._postRepository.updateResolvedRelease( + post.id, + orgId, + resolved.postId, + resolved.releaseURL + ); + return { releaseId: resolved.postId, releaseURL: resolved.releaseURL }; + } + async checkPostAnalytics( orgId: string, postId: string, @@ -214,23 +245,7 @@ export class PostsService { // } try { - let releaseId = post.releaseId; - if (integrationProvider.resolveReleaseId) { - const resolved = await integrationProvider.resolveReleaseId( - getIntegration.token, - releaseId, - getIntegration - ); - if (resolved && resolved.postId !== releaseId) { - await this._postRepository.updateResolvedRelease( - post.id, - orgId, - resolved.postId, - resolved.releaseURL - ); - releaseId = resolved.postId; - } - } + const { releaseId } = await this.resolveRelease(orgId, post); const loadAnalytics = await integrationProvider.postAnalytics( getIntegration.internalId, From 876ab9e6d9545b1f9d290d449a157223b28fd34e Mon Sep 17 00:00:00 2001 From: Nevo David <me@nevos.io> Date: Fri, 2 Oct 2026 18:17:21 +0700 Subject: [PATCH 50/53] feat: self hosting connector --- .env.example | 5 + .../src/api/routes/oauth.controller.ts | 72 +++- .../src/app/(app)/oauth/authorize/page.tsx | 225 +---------- .../components/oauth/authorize.component.tsx | 277 ++++++++++++++ .../oauth/self.hosted.component.tsx | 166 ++++++++ .../onboarding/onboarding.modal.tsx | 25 +- .../public-api/public.component.tsx | 84 +++- apps/frontend/src/proxy.ts | 5 +- i18n.lock | 2 + .../src/chat/async.storage.ts | 7 + .../nestjs-libraries/src/chat/chat.module.ts | 3 +- .../src/chat/mcp.relay.service.ts | 358 ++++++++++++++++++ .../nestjs-libraries/src/chat/start.mcp.ts | 71 +++- .../database/prisma/oauth/oauth.repository.ts | 133 ++++++- .../database/prisma/oauth/oauth.service.ts | 224 ++++++++++- .../src/database/prisma/schema.prisma | 57 ++- .../src/dtos/oauth/authorize-oauth.dto.ts | 61 ++- .../translation/locales/ar/translation.json | 2 + .../translation/locales/bn/translation.json | 2 + .../translation/locales/de/translation.json | 2 + .../translation/locales/en/translation.json | 2 + .../translation/locales/es/translation.json | 2 + .../translation/locales/fr/translation.json | 2 + .../translation/locales/he/translation.json | 2 + .../translation/locales/it/translation.json | 2 + .../translation/locales/ja/translation.json | 2 + .../translation/locales/ko/translation.json | 2 + .../translation/locales/pt/translation.json | 2 + .../translation/locales/ru/translation.json | 2 + .../translation/locales/tr/translation.json | 2 + .../translation/locales/vi/translation.json | 2 + .../translation/locales/zh/translation.json | 2 + 32 files changed, 1517 insertions(+), 288 deletions(-) create mode 100644 apps/frontend/src/components/oauth/authorize.component.tsx create mode 100644 apps/frontend/src/components/oauth/self.hosted.component.tsx create mode 100644 libraries/nestjs-libraries/src/chat/mcp.relay.service.ts diff --git a/.env.example b/.env.example index d91b43bc2f..bb0c5c6230 100644 --- a/.env.example +++ b/.env.example @@ -134,6 +134,11 @@ OPENAI_OAUTH_CLIENT_ID="" # and are always accepted. Example locks web callbacks to the claude.ai # connector; add cursor.com for Cursor. # DCR_VERIFIED_DOMAINS="claude.ai,claude.com" +# Postiz Cloud only: lets people connect a self-hosted Postiz on the MCP +# consent screen ("Use self-hosted"). The MCP tool calls of that connection +# are relayed to the instance with its API key. Leave unset on self-hosted +# installs; set it on both the API and the MCP backend. +# MCP_SELF_HOSTED_RELAY=true # EVOLINK_API_KEY="" # EvoLink API key for Seedance AI video generation (https://evolink.ai) NEXT_PUBLIC_DISCORD_SUPPORT="" NEXT_PUBLIC_POLOTNO="" diff --git a/apps/backend/src/api/routes/oauth.controller.ts b/apps/backend/src/api/routes/oauth.controller.ts index 369b20f2b7..b3f0986be3 100644 --- a/apps/backend/src/api/routes/oauth.controller.ts +++ b/apps/backend/src/api/routes/oauth.controller.ts @@ -8,21 +8,32 @@ import { HttpStatus, Post, Query, + UseGuards, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { Throttle } from '@nestjs/throttler'; import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service'; import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; import { User, Organization } from '@prisma/client'; -import { AuthorizeOAuthQueryDto, ApproveOAuthDto } from '@gitroom/nestjs-libraries/dtos/oauth/authorize-oauth.dto'; +import { + AuthorizeOAuthQueryDto, + ApproveOAuthDto, + AuthorizeSelfHostedDto, +} from '@gitroom/nestjs-libraries/dtos/oauth/authorize-oauth.dto'; import { TokenExchangeDto } from '@gitroom/nestjs-libraries/dtos/oauth/token-exchange.dto'; import { RegisterClientDto } from '@gitroom/nestjs-libraries/dtos/oauth/register-client.dto'; import { extractBasicCredentials } from '@gitroom/nestjs-libraries/chat/oauth-types'; +import { McpRelayService } from '@gitroom/nestjs-libraries/chat/mcp.relay.service'; +import { ThrottlerRealIpGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider'; @ApiTags('OAuth') @Controller('/oauth') export class OAuthController { - constructor(private _oauthService: OAuthService) {} + constructor( + private _oauthService: OAuthService, + private _mcpRelayService: McpRelayService + ) {} // Dynamic Client Registration (RFC 7591), used by MCP clients like Claude @Post('/register') @@ -42,6 +53,11 @@ export class OAuthController { } ); + const selfHosted = this._oauthService.allowsSelfHosted( + app, + query.resource + ); + return { app: { name: app.name, @@ -51,9 +67,61 @@ export class OAuthController { redirectUrl: app.redirectUrl, }, state: query.state, + selfHosted, + selfHostedEmail: + selfHosted && this._oauthService.selfHostedRequiresEmail(app), }; } + // Public (the person may have no account here) and capped per client, + // since every attempt sends requests to the instance + @UseGuards(ThrottlerRealIpGuard) + @Throttle({ default: { limit: 30, ttl: 3600000 } }) + @Post('/authorize/self-hosted') + async authorizeSelfHosted(@Body() body: AuthorizeSelfHostedDto) { + const app = await this._oauthService.validateAuthorizationRequest( + body.client_id, + { + redirectUri: body.redirect_uri, + codeChallenge: body.code_challenge, + codeChallengeMethod: body.code_challenge_method, + } + ); + + const email = body.email?.trim(); + this._oauthService.validateSelfHostedRequest(app, { + resource: body.resource, + email, + }); + + const instance = await this._mcpRelayService.connect( + body.instance_url, + body.api_key + ); + + const code = await this._oauthService.createSelfHostedAuthorizationCode( + app.id, + { ...instance, email }, + app.dynamic + ? { + codeChallenge: body.code_challenge, + codeChallengeMethod: body.code_challenge_method, + redirectUri: body.redirect_uri, + } + : undefined + ); + + // Same redirect as an approved cloud authorization + const redirectUrl = new URL( + app.dynamic ? body.redirect_uri! : app.redirectUrl + ); + redirectUrl.searchParams.set('code', code); + if (body.state) { + redirectUrl.searchParams.set('state', body.state); + } + return { redirect: redirectUrl.toString() }; + } + @Post('/token') // RFC 6749 §5.1: successful token responses are 200; strict clients (Canva) reject Nest's default 201 @HttpCode(200) diff --git a/apps/frontend/src/app/(app)/oauth/authorize/page.tsx b/apps/frontend/src/app/(app)/oauth/authorize/page.tsx index 29c2ce1288..e6dbbdfb27 100644 --- a/apps/frontend/src/app/(app)/oauth/authorize/page.tsx +++ b/apps/frontend/src/app/(app)/oauth/authorize/page.tsx @@ -1,221 +1,10 @@ -'use client'; +import { OAuthAuthorize } from '@gitroom/frontend/components/oauth/authorize.component'; +import { cookies } from 'next/headers'; -import { useCallback, useEffect, useState } from 'react'; -import { useSearchParams } from 'next/navigation'; -import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; -import { Logo } from '@gitroom/frontend/components/new-layout/logo'; +export const dynamic = 'force-dynamic'; -export default function OAuthAuthorizePage() { - const searchParams = useSearchParams(); - const fetch = useFetch(); - const [appInfo, setAppInfo] = useState<any>(null); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(true); - const [submitting, setSubmitting] = useState(false); - - const clientId = searchParams.get('client_id'); - const responseType = searchParams.get('response_type'); - const state = searchParams.get('state'); - const redirectUri = searchParams.get('redirect_uri'); - const codeChallenge = searchParams.get('code_challenge'); - const codeChallengeMethod = searchParams.get('code_challenge_method'); - - useEffect(() => { - if (!clientId || !responseType) { - setError('Missing required parameters (client_id, response_type)'); - setLoading(false); - return; - } - if (responseType !== 'code') { - setError('Only response_type=code is supported'); - setLoading(false); - return; - } - - const params = new URLSearchParams({ - client_id: clientId, - response_type: responseType, - ...(state ? { state } : {}), - ...(redirectUri ? { redirect_uri: redirectUri } : {}), - ...(codeChallenge ? { code_challenge: codeChallenge } : {}), - ...(codeChallengeMethod - ? { code_challenge_method: codeChallengeMethod } - : {}), - }); - - fetch(`/oauth/authorize?${params}`) - .then((r) => r.json()) - .then((data) => { - if (data.statusCode && data.statusCode >= 400) { - setError(data.message || 'Invalid OAuth request'); - } else { - setAppInfo(data); - } - setLoading(false); - }) - .catch(() => { - setError('Failed to validate OAuth request'); - setLoading(false); - }); - }, [clientId, responseType, state, redirectUri, codeChallenge, codeChallengeMethod]); - - const handleAction = useCallback( - async (action: 'approve' | 'deny') => { - setSubmitting(true); - try { - const result = await ( - await fetch('/oauth/authorize', { - method: 'POST', - body: JSON.stringify({ - client_id: clientId, - state, - action, - ...(redirectUri ? { redirect_uri: redirectUri } : {}), - ...(codeChallenge ? { code_challenge: codeChallenge } : {}), - ...(codeChallengeMethod - ? { code_challenge_method: codeChallengeMethod } - : {}), - }), - }) - ).json(); - - if (result.redirect) { - window.location.href = result.redirect; - } - } catch { - setError('Failed to process authorization'); - setSubmitting(false); - } - }, - [clientId, state, redirectUri, codeChallenge, codeChallengeMethod] - ); - - if (loading) { - return ( - <div className="flex flex-1 items-center justify-center text-white relative overflow-hidden"> - <div className="absolute inset-0 opacity-30"> - <div className="absolute top-[20%] left-[10%] w-[300px] h-[300px] bg-[#612BD3] rounded-full blur-[120px]" /> - <div className="absolute bottom-[20%] right-[10%] w-[250px] h-[250px] bg-[#FC69FF] rounded-full blur-[120px]" /> - </div> - <div className="relative z-10 text-center"> - <div className="flex justify-center mb-[24px]"> - <Logo /> - </div> - <div className="text-[16px] text-gray-400"> - Please wait... - </div> - <div className="mt-[32px] flex justify-center"> - <div className="w-[48px] h-[48px] border-[3px] border-[#612BD3] border-t-transparent rounded-full animate-spin" /> - </div> - </div> - </div> - ); - } - - if (error) { - return ( - <div className="flex flex-1 items-center justify-center text-white relative overflow-hidden"> - <div className="absolute inset-0 opacity-30"> - <div className="absolute top-[20%] left-[10%] w-[300px] h-[300px] bg-[#612BD3] rounded-full blur-[120px]" /> - <div className="absolute bottom-[20%] right-[10%] w-[250px] h-[250px] bg-[#FC69FF] rounded-full blur-[120px]" /> - </div> - <div className="relative z-10 text-center"> - <div className="flex justify-center mb-[24px]"> - <Logo /> - </div> - <div className="w-[80px] h-[80px] mx-auto mb-[24px] rounded-full bg-red-500/20 flex items-center justify-center"> - <svg - className="w-[40px] h-[40px] text-red-500" - fill="currentColor" - viewBox="0 0 20 20" - > - <path - fillRule="evenodd" - d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" - clipRule="evenodd" - /> - </svg> - </div> - <div className="text-[28px] font-semibold mb-[12px]"> - Authorization Error - </div> - <div className="text-[16px] text-gray-400 max-w-[400px]"> - {error} - </div> - </div> - </div> - ); - } - - if (!appInfo) { - return null; - } - - return ( - <div className="flex flex-1 items-center justify-center text-white relative overflow-hidden"> - <div className="absolute inset-0 opacity-30"> - <div className="absolute top-[20%] left-[10%] w-[300px] h-[300px] bg-[#612BD3] rounded-full blur-[120px]" /> - <div className="absolute bottom-[20%] right-[10%] w-[250px] h-[250px] bg-[#FC69FF] rounded-full blur-[120px]" /> - </div> - - <div className="relative z-10 w-full max-w-[500px] mx-auto px-[20px]"> - <div className="flex justify-center mb-[32px]"> - <Logo /> - </div> - - <div className="bg-[#1A1919] rounded-[16px] p-[32px] flex flex-col gap-[24px]"> - <div className="flex flex-col items-center gap-[16px]"> - {appInfo.app.picture?.path ? ( - <img - src={appInfo.app.picture.path} - alt={appInfo.app.name} - className="w-[64px] h-[64px] rounded-full object-cover" - /> - ) : ( - <div className="w-[64px] h-[64px] rounded-full bg-[#2A2929] flex items-center justify-center text-[24px] text-gray-400"> - {appInfo.app.name?.[0]?.toUpperCase() || '?'} - </div> - )} - <h2 className="text-[24px] font-semibold text-center"> - {appInfo.app.name} - </h2> - {appInfo.app.description && ( - <div className="text-gray-400 text-center text-[14px]"> - {appInfo.app.description} - </div> - )} - </div> - - <div className="border-t border-[#2A2929] pt-[16px]"> - <div className="text-[14px] text-gray-400 mb-[12px]"> - This application is requesting access to your Postiz account. It - will be able to: - </div> - <ul className="text-[14px] list-disc list-inside space-y-[4px]"> - <li>Access your integrations and channels</li> - <li>Create and schedule posts on your behalf</li> - <li>Read your post analytics</li> - </ul> - </div> - - <div className="flex gap-[12px]"> - <button - onClick={() => handleAction('approve')} - disabled={submitting} - className="flex-1 bg-[#612BD3] hover:bg-[#7B3FF2] disabled:opacity-50 text-white rounded-[8px] py-[10px] px-[16px] text-[14px] font-semibold transition-colors" - > - Authorize - </button> - <button - onClick={() => handleAction('deny')} - disabled={submitting} - className="flex-1 bg-[#2A2929] hover:bg-[#3A3939] disabled:opacity-50 text-white rounded-[8px] py-[10px] px-[16px] text-[14px] font-semibold transition-colors" - > - Deny - </button> - </div> - </div> - </div> - </div> - ); +// signed-out visitors get the page too (sign in, or use self-hosted) +export default async function OAuthAuthorizePage() { + const get = (await cookies()).get('auth'); + return <OAuthAuthorize logged={!!get?.name} />; } diff --git a/apps/frontend/src/components/oauth/authorize.component.tsx b/apps/frontend/src/components/oauth/authorize.component.tsx new file mode 100644 index 0000000000..00556c4e01 --- /dev/null +++ b/apps/frontend/src/components/oauth/authorize.component.tsx @@ -0,0 +1,277 @@ +'use client'; + +import { FC, useCallback, useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { Logo } from '@gitroom/frontend/components/new-layout/logo'; +import { OAuthSelfHosted } from '@gitroom/frontend/components/oauth/self.hosted.component'; + +export const OAuthAuthorize: FC<{ logged: boolean }> = ({ logged }) => { + const searchParams = useSearchParams(); + const fetch = useFetch(); + const [appInfo, setAppInfo] = useState<any>(null); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [selfHosted, setSelfHosted] = useState(false); + + const clientId = searchParams.get('client_id'); + const responseType = searchParams.get('response_type'); + const state = searchParams.get('state'); + const redirectUri = searchParams.get('redirect_uri'); + const codeChallenge = searchParams.get('code_challenge'); + const codeChallengeMethod = searchParams.get('code_challenge_method'); + const resource = searchParams.get('resource'); + + useEffect(() => { + if (!clientId || !responseType) { + setError('Missing required parameters (client_id, response_type)'); + setLoading(false); + return; + } + if (responseType !== 'code') { + setError('Only response_type=code is supported'); + setLoading(false); + return; + } + + const params = new URLSearchParams({ + client_id: clientId, + response_type: responseType, + ...(state ? { state } : {}), + ...(redirectUri ? { redirect_uri: redirectUri } : {}), + ...(codeChallenge ? { code_challenge: codeChallenge } : {}), + ...(codeChallengeMethod + ? { code_challenge_method: codeChallengeMethod } + : {}), + ...(resource ? { resource } : {}), + }); + + fetch(`/oauth/authorize?${params}`) + .then((r) => r.json()) + .then((data) => { + if (data.statusCode && data.statusCode >= 400) { + setError(data.message || 'Invalid OAuth request'); + } else { + setAppInfo(data); + } + setLoading(false); + }) + .catch(() => { + setError('Failed to validate OAuth request'); + setLoading(false); + }); + }, [ + clientId, + responseType, + state, + redirectUri, + codeChallenge, + codeChallengeMethod, + resource, + ]); + + const handleAction = useCallback( + async (action: 'approve' | 'deny') => { + setSubmitting(true); + try { + const result = await ( + await fetch('/oauth/authorize', { + method: 'POST', + body: JSON.stringify({ + client_id: clientId, + state, + action, + ...(redirectUri ? { redirect_uri: redirectUri } : {}), + ...(codeChallenge ? { code_challenge: codeChallenge } : {}), + ...(codeChallengeMethod + ? { code_challenge_method: codeChallengeMethod } + : {}), + }), + }) + ).json(); + + if (result.redirect) { + window.location.href = result.redirect; + } + } catch { + setError('Failed to process authorization'); + setSubmitting(false); + } + }, + [clientId, state, redirectUri, codeChallenge, codeChallengeMethod] + ); + + // Sign in (or sign up) and land back on this same request + const signIn = useCallback(() => { + window.location.href = `/auth/login?returnUrl=${encodeURIComponent( + window.location.href + )}`; + }, []); + + if (loading) { + return ( + <div className="flex flex-1 items-center justify-center text-white relative overflow-hidden"> + <div className="absolute inset-0 opacity-30"> + <div className="absolute top-[20%] left-[10%] w-[300px] h-[300px] bg-[#612BD3] rounded-full blur-[120px]" /> + <div className="absolute bottom-[20%] right-[10%] w-[250px] h-[250px] bg-[#FC69FF] rounded-full blur-[120px]" /> + </div> + <div className="relative z-10 text-center"> + <div className="flex justify-center mb-[24px]"> + <Logo /> + </div> + <div className="text-[16px] text-gray-400"> + Please wait... + </div> + <div className="mt-[32px] flex justify-center"> + <div className="w-[48px] h-[48px] border-[3px] border-[#612BD3] border-t-transparent rounded-full animate-spin" /> + </div> + </div> + </div> + ); + } + + if (error) { + return ( + <div className="flex flex-1 items-center justify-center text-white relative overflow-hidden"> + <div className="absolute inset-0 opacity-30"> + <div className="absolute top-[20%] left-[10%] w-[300px] h-[300px] bg-[#612BD3] rounded-full blur-[120px]" /> + <div className="absolute bottom-[20%] right-[10%] w-[250px] h-[250px] bg-[#FC69FF] rounded-full blur-[120px]" /> + </div> + <div className="relative z-10 text-center"> + <div className="flex justify-center mb-[24px]"> + <Logo /> + </div> + <div className="w-[80px] h-[80px] mx-auto mb-[24px] rounded-full bg-red-500/20 flex items-center justify-center"> + <svg + className="w-[40px] h-[40px] text-red-500" + fill="currentColor" + viewBox="0 0 20 20" + > + <path + fillRule="evenodd" + d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" + clipRule="evenodd" + /> + </svg> + </div> + <div className="text-[28px] font-semibold mb-[12px]"> + Authorization Error + </div> + <div className="text-[16px] text-gray-400 max-w-[400px]"> + {error} + </div> + </div> + </div> + ); + } + + if (!appInfo) { + return null; + } + + return ( + <div className="flex flex-1 items-center justify-center text-white relative overflow-hidden"> + <div className="absolute inset-0 opacity-30"> + <div className="absolute top-[20%] left-[10%] w-[300px] h-[300px] bg-[#612BD3] rounded-full blur-[120px]" /> + <div className="absolute bottom-[20%] right-[10%] w-[250px] h-[250px] bg-[#FC69FF] rounded-full blur-[120px]" /> + </div> + + <div className="relative z-10 w-full max-w-[500px] mx-auto px-[20px]"> + <div className="flex justify-center mb-[32px]"> + <Logo /> + </div> + + <div className="bg-[#1A1919] rounded-[16px] p-[32px] flex flex-col gap-[24px]"> + <div className="flex flex-col items-center gap-[16px]"> + {appInfo.app.picture?.path ? ( + <img + src={appInfo.app.picture.path} + alt={appInfo.app.name} + className="w-[64px] h-[64px] rounded-full object-cover" + /> + ) : ( + <div className="w-[64px] h-[64px] rounded-full bg-[#2A2929] flex items-center justify-center text-[24px] text-gray-400"> + {appInfo.app.name?.[0]?.toUpperCase() || '?'} + </div> + )} + <h2 className="text-[24px] font-semibold text-center"> + {appInfo.app.name} + </h2> + {appInfo.app.description && ( + <div className="text-gray-400 text-center text-[14px]"> + {appInfo.app.description} + </div> + )} + </div> + + {selfHosted ? ( + <OAuthSelfHosted + appName={appInfo.app.name} + askEmail={!!appInfo.selfHostedEmail} + params={{ + client_id: clientId, + state, + redirect_uri: redirectUri, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + resource, + }} + onBack={() => setSelfHosted(false)} + /> + ) : ( + <> + <div className="border-t border-[#2A2929] pt-[16px]"> + <div className="text-[14px] text-gray-400 mb-[12px]"> + This application is requesting access to your Postiz + account. It will be able to: + </div> + <ul className="text-[14px] list-disc list-inside space-y-[4px]"> + <li>Access your integrations and channels</li> + <li>Create and schedule posts on your behalf</li> + <li>Read your post analytics</li> + </ul> + </div> + + {logged ? ( + <div className="flex gap-[12px]"> + <button + onClick={() => handleAction('approve')} + disabled={submitting} + className="flex-1 bg-[#612BD3] hover:bg-[#7B3FF2] disabled:opacity-50 text-white rounded-[8px] py-[10px] px-[16px] text-[14px] font-semibold transition-colors" + > + Authorize + </button> + <button + onClick={() => handleAction('deny')} + disabled={submitting} + className="flex-1 bg-[#2A2929] hover:bg-[#3A3939] disabled:opacity-50 text-white rounded-[8px] py-[10px] px-[16px] text-[14px] font-semibold transition-colors" + > + Deny + </button> + </div> + ) : ( + <button + onClick={signIn} + className="bg-[#612BD3] hover:bg-[#7B3FF2] text-white rounded-[8px] py-[10px] px-[16px] text-[14px] font-semibold transition-colors" + > + Sign in to Postiz + </button> + )} + + {!!appInfo.selfHosted && ( + <button + onClick={() => setSelfHosted(true)} + disabled={submitting} + className="text-[14px] text-gray-400 hover:text-white disabled:opacity-50 transition-colors" + > + Use self-hosted + </button> + )} + </> + )} + </div> + </div> + </div> + ); +}; diff --git a/apps/frontend/src/components/oauth/self.hosted.component.tsx b/apps/frontend/src/components/oauth/self.hosted.component.tsx new file mode 100644 index 0000000000..97c0f92ecb --- /dev/null +++ b/apps/frontend/src/components/oauth/self.hosted.component.tsx @@ -0,0 +1,166 @@ +'use client'; + +import { FC, FormEvent, useCallback, useState } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { Input } from '@gitroom/react/form/input'; + +export const OAuthSelfHosted: FC<{ + appName: string; + // clients that read the email from userinfo (ChatGPT) need one + askEmail: boolean; + params: { + client_id: string | null; + state: string | null; + redirect_uri: string | null; + code_challenge: string | null; + code_challenge_method: string | null; + resource: string | null; + }; + onBack: () => void; +}> = ({ appName, askEmail, params, onBack }) => { + const fetch = useFetch(); + const [instanceUrl, setInstanceUrl] = useState(''); + const [apiKey, setApiKey] = useState(''); + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const connect = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + setSubmitting(true); + setError(''); + try { + const response = await fetch('/oauth/authorize/self-hosted', { + method: 'POST', + body: JSON.stringify({ + client_id: params.client_id, + state: params.state, + ...(params.redirect_uri + ? { redirect_uri: params.redirect_uri } + : {}), + ...(params.code_challenge + ? { code_challenge: params.code_challenge } + : {}), + ...(params.code_challenge_method + ? { code_challenge_method: params.code_challenge_method } + : {}), + ...(params.resource ? { resource: params.resource } : {}), + instance_url: instanceUrl.trim(), + ...(apiKey.trim() ? { api_key: apiKey.trim() } : {}), + ...(askEmail && email.trim() ? { email: email.trim() } : {}), + }), + }); + const data = await response.json(); + if (data.redirect) { + window.location.href = data.redirect; + return; + } + + setError( + response.status === 429 + ? 'Too many attempts, try again later' + : (Array.isArray(data.message) ? data.message[0] : data.message) || + 'Could not connect to your instance' + ); + } catch { + setError('Could not connect to your instance'); + } + setSubmitting(false); + }, + [params, instanceUrl, apiKey, email, askEmail] + ); + + return ( + <form + onSubmit={connect} + className="border-t border-[#2A2929] pt-[16px] flex flex-col gap-[16px]" + > + <div className="flex flex-col gap-[4px]"> + <div className="text-[16px] font-semibold">Use self-hosted</div> + <div className="text-[14px] text-gray-400"> + {appName} will work with your own Postiz instance. Postiz Cloud + relays its requests, and your API key is stored encrypted. + </div> + </div> + + {/* plain text fields: a password field makes browsers autofill the + saved Postiz login (email + password) into this form */} + <Input + name="instance_url" + label="Instance URL" + disableForm={true} + removeError={true} + value={instanceUrl} + onChange={(e) => setInstanceUrl(e.target.value)} + placeholder="https://postiz.example.com" + inputMode="url" + autoComplete="off" + spellCheck={false} + /> + + <div className="flex flex-col gap-[6px]"> + <Input + name="api_key" + label="API key" + disableForm={true} + removeError={true} + value={apiKey} + onChange={(e) => setApiKey(e.target.value)} + placeholder="Settings > Developers on your instance" + autoComplete="off" + spellCheck={false} + /> + <div className="text-[12px] text-gray-400"> + You can also paste the MCP URL from Settings > Developers in the + Instance URL, it includes the key. + </div> + </div> + + {askEmail && ( + <div className="flex flex-col gap-[6px]"> + <Input + name="email" + label="Email" + type="email" + disableForm={true} + removeError={true} + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="you@example.com" + autoComplete="email" + /> + <div className="text-[12px] text-gray-400"> + {appName} uses it to identify your account. + </div> + </div> + )} + + {!!error && <div className="text-[14px] text-red-400">{error}</div>} + + <div className="flex gap-[12px]"> + <button + type="submit" + disabled={ + submitting || !instanceUrl.trim() || (askEmail && !email.trim()) + } + className="flex-1 bg-[#612BD3] hover:bg-[#7B3FF2] disabled:opacity-50 text-white rounded-[8px] py-[10px] px-[16px] text-[14px] font-semibold transition-colors" + > + {submitting ? 'Connecting...' : 'Connect'} + </button> + <button + type="button" + onClick={onBack} + disabled={submitting} + className="flex-1 bg-[#2A2929] hover:bg-[#3A3939] disabled:opacity-50 text-white rounded-[8px] py-[10px] px-[16px] text-[14px] font-semibold transition-colors" + > + Back + </button> + </div> + + <div className="text-[12px] text-gray-400 text-center"> + Your instance has to be reachable from the internet over https. + </div> + </form> + ); +}; diff --git a/apps/frontend/src/components/onboarding/onboarding.modal.tsx b/apps/frontend/src/components/onboarding/onboarding.modal.tsx index e5b81009fc..619e7cb95c 100644 --- a/apps/frontend/src/components/onboarding/onboarding.modal.tsx +++ b/apps/frontend/src/components/onboarding/onboarding.modal.tsx @@ -16,6 +16,7 @@ import { CopyButton, getMcpConfig, isChatOnlyMcpClient, + isSelfHosted, localCliSteps, McpAuth, McpClient, @@ -293,6 +294,8 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ const mcpBase = mcpUrl || backendUrl; const apiKey = user?.publicApi || ''; const available = !!apiKey && !!user?.tier?.public_api; + // the official connectors work for self-hosted installs too + const officialConnectors = billingEnabled || isSelfHosted; const { config, hint } = agent === apiTab @@ -308,22 +311,22 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ ); const connector = - agent === 'Claude' && billingEnabled + agent === 'Claude' && officialConnectors ? { href: mcpConnectorUrls.Claude, label: t('add_to_claude', 'Add to Claude'), } - : agent === 'ChatGPT' && billingEnabled + : agent === 'ChatGPT' && officialConnectors ? { href: mcpConnectorUrls.ChatGPT, label: t('add_to_chatgpt', 'Add to ChatGPT'), } - : agent === 'Cursor' && billingEnabled + : agent === 'Cursor' && officialConnectors ? { href: mcpConnectorUrls.Cursor, label: t('add_to_cursor', 'Add to Cursor'), } - : agent === 'Grok Bot' && billingEnabled + : agent === 'Grok Bot' && officialConnectors ? { href: mcpConnectorUrls['Grok Bot'], label: t('add_to_grok_bot', 'Add to Grok Bot'), @@ -435,10 +438,16 @@ const OnboardingStep2: FC<{ onBack: () => void; onNext: () => void }> = ({ {t('connector', 'Connector')} </div> <div className="text-[13px] text-customColor18 mt-[2px]"> - {t( - 'connector_onboarding_description', - 'The fastest way: add Postiz with one click, you will be asked to sign in' - )} + {isSelfHosted + ? t( + 'connector_self_hosted_description', + 'The official connector works with self-hosted Postiz too. When asked to sign in, choose "Use self-hosted" and enter {{url}} with your API key.', + { url: mcpBase, interpolation: { escapeValue: false } } + ) + : t( + 'connector_onboarding_description', + 'The fastest way: add Postiz with one click, you will be asked to sign in' + )} </div> </div> <a diff --git a/apps/frontend/src/components/public-api/public.component.tsx b/apps/frontend/src/components/public-api/public.component.tsx index 2378eb646b..43c9a27d56 100644 --- a/apps/frontend/src/components/public-api/public.component.tsx +++ b/apps/frontend/src/components/public-api/public.component.tsx @@ -21,8 +21,9 @@ export const remoteMcpClients = { 'In ChatGPT go to Settings > Connectors > Create and paste this URL.', } as const; -// Official one-click connectors listed in the assistants' directories. -// Only for the hosted Postiz (billingEnabled), they point at the public MCP server. +// Official one-click connectors listed in the assistants' directories, they +// point at the public MCP server: the hosted Postiz (billingEnabled), or a +// self-hosted install through "Use self-hosted" on its sign-in page. export const mcpConnectorUrls = { Claude: 'https://claude.ai/directory/postiz', ChatGPT: @@ -61,6 +62,17 @@ export type AnyMcpClient = RemoteMcpClient | ChatOnlyMcpClient | McpClient; // apikey: the organization API key, as a Bearer header (or inside the URL for remote clients) export type McpAuth = 'oauth' | 'apikey'; +// Only the self-hosted docker images are built with a version +export const isSelfHosted = !!process.env.NEXT_PUBLIC_VERSION; + +// The directory listings behind the "Official connector" tab +const officialConnectorClients = [ + 'Claude', + 'ChatGPT', + 'Cursor', + 'Grok Bot', +] as const; + export const getMcpOauthUrl = (mcpBase: string) => `${mcpBase}/mcp-oauth-dynamic`; @@ -305,7 +317,13 @@ const McpSection = ({ const t = useT(); const { billingEnabled } = useVariables(); const [activeClient, setActiveClient] = useState<AnyMcpClient>('Claude'); - const [auth, setAuth] = useState<McpAuth>('oauth'); + const officialConnectors = billingEnabled || isSelfHosted; + // the directory connectors come first wherever they work + const tabs: Array<'official' | McpAuth> = officialConnectors + ? ['official', 'oauth', 'apikey'] + : ['oauth', 'apikey']; + const [tab, setTab] = useState(tabs[0]); + const auth: McpAuth = tab === 'apikey' ? 'apikey' : 'oauth'; const [revealed, setRevealed] = useState(false); const { config, hint } = getMcpConfig( @@ -342,7 +360,7 @@ const McpSection = ({ </div> </div> <div className="flex flex-wrap gap-[6px] shrink-0 pt-[2px]"> - {billingEnabled && ( + {officialConnectors && ( <> <a className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]" @@ -373,25 +391,27 @@ const McpSection = ({ </div> </div> <div className="p-[20px] mobile:p-[14px] flex flex-col gap-[16px]"> - {!chatOnly && ( + {(tab === 'official' || !chatOnly) && ( <div className="flex flex-col gap-[6px]"> <div className="text-[13px] font-[600] text-customColor18"> {t('auth_method', 'Authentication')} </div> - <div className="flex gap-[6px]"> - {(['oauth', 'apikey'] as const).map((m) => ( + <div className="flex flex-wrap gap-[6px]"> + {tabs.map((m) => ( <button key={m} type="button" className={clsx( 'cursor-pointer px-[14px] h-[36px] text-[13px] font-[500] rounded-[8px] transition-colors', - auth === m + tab === m ? 'bg-[#612BD3] text-white' : 'bg-btnSimple text-customColor18 hover:bg-boxHover hover:text-textColor' )} - onClick={() => setAuth(m)} + onClick={() => setTab(m)} > - {m === 'oauth' + {m === 'official' + ? t('official_connector', 'Official connector') + : m === 'oauth' ? t('sign_in_no_api_key', 'Sign in with Postiz (no API key)') : t('api_key', 'API Key')} </button> @@ -399,6 +419,42 @@ const McpSection = ({ </div> </div> )} + {tab === 'official' ? ( + <div className="flex flex-col gap-[8px]"> + <div className="text-[12px] text-customColor18 font-[500]"> + {isSelfHosted + ? t( + 'connector_self_hosted_description', + 'The official connector works with self-hosted Postiz too. When asked to sign in, choose "Use self-hosted" and enter {{url}} with your API key.', + { url: mcpBase, interpolation: { escapeValue: false } } + ) + : t( + 'connector_onboarding_description', + 'The fastest way: add Postiz with one click, you will be asked to sign in' + )} + </div> + <div className="flex flex-wrap gap-[8px]"> + {officialConnectorClients.map((client) => ( + <a + key={client} + className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[8px]" + href={mcpConnectorUrls[client]} + target="_blank" + > + <McpClientIcon client={client} /> + {client === 'Claude' + ? t('add_to_claude', 'Add to Claude') + : client === 'ChatGPT' + ? t('add_to_chatgpt', 'Add to ChatGPT') + : client === 'Cursor' + ? t('add_to_cursor', 'Add to Cursor') + : t('add_to_grok_bot', 'Add to Grok Bot')} + </a> + ))} + </div> + </div> + ) : ( + <> <div className="flex flex-col gap-[6px]"> <div className="text-[13px] font-[600] text-customColor18"> {t('mcp_client', 'Client')} @@ -478,7 +534,7 @@ const McpSection = ({ {!isRemoteMcpClient(activeClient) && !chatOnly && ( <CopyButton text={baseUrl} label={t('copy_url', 'Copy URL')} /> )} - {activeClient === 'Claude' && billingEnabled && ( + {activeClient === 'Claude' && officialConnectors && ( <a className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]" href={mcpConnectorUrls.Claude} @@ -488,7 +544,7 @@ const McpSection = ({ {t('add_to_claude', 'Add to Claude')} </a> )} - {activeClient === 'ChatGPT' && billingEnabled && ( + {activeClient === 'ChatGPT' && officialConnectors && ( <a className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]" href={mcpConnectorUrls.ChatGPT} @@ -498,7 +554,7 @@ const McpSection = ({ {t('add_to_chatgpt', 'Add to ChatGPT')} </a> )} - {activeClient === 'Grok Bot' && billingEnabled && ( + {activeClient === 'Grok Bot' && officialConnectors && ( <a className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]" href={mcpConnectorUrls['Grok Bot']} @@ -510,6 +566,8 @@ const McpSection = ({ )} </div> </div> + </> + )} </div> </div> ); diff --git a/apps/frontend/src/proxy.ts b/apps/frontend/src/proxy.ts index 360c937e34..74dcd310d4 100644 --- a/apps/frontend/src/proxy.ts +++ b/apps/frontend/src/proxy.ts @@ -47,7 +47,10 @@ export async function proxy(request: NextRequest) { nextUrl.pathname.startsWith('/uploads/') || nextUrl.pathname.startsWith('/p/') || nextUrl.pathname.startsWith('/provider/') || - nextUrl.pathname.startsWith('/icons/') + nextUrl.pathname.startsWith('/icons/') || + // the consent screen of MCP / OAuth clients handles signed-out visitors + // itself (sign in and come back, or connect a self-hosted instance) + nextUrl.pathname.startsWith('/oauth/authorize') ) { return topResponse; } diff --git a/i18n.lock b/i18n.lock index d72791c370..1f9a81b9d9 100644 --- a/i18n.lock +++ b/i18n.lock @@ -730,6 +730,8 @@ checksums: chat_onboarding_description: 9da74037342cd6adb283e8205299b986 connector: 7abb5584501ce0e3cc51ba19f0561ca8 connector_onboarding_description: eb8027296b46bf58475f6b4925bd6351 + official_connector: cdaaab2f023cb2eaea40b666e9859442 + connector_self_hosted_description: c966620ed2df1d6ffeaa92969aaf30a6 mcp_onboarding_description: 5b33036d174222d86ac50039a3a31724 cli_onboarding_description: b4aba065e5a8410ee2c365ee4d7aa87c agent_settings_later: bbdd45a80cc5218809363348e0b03a32 diff --git a/libraries/nestjs-libraries/src/chat/async.storage.ts b/libraries/nestjs-libraries/src/chat/async.storage.ts index fff383ff13..bffed1f97f 100644 --- a/libraries/nestjs-libraries/src/chat/async.storage.ts +++ b/libraries/nestjs-libraries/src/chat/async.storage.ts @@ -1,9 +1,12 @@ // context.ts import { AsyncLocalStorage } from 'node:async_hooks'; +import type { SelfHostedInstance } from '@gitroom/nestjs-libraries/chat/mcp.relay.service'; type Ctx = { requestId: string; auth: any; // replace with your org type if you have it, e.g. Organization + // a self-hosted connection: where its tool calls are relayed to + relay?: SelfHostedInstance & { id: string }; }; const als = new AsyncLocalStorage<Ctx>(); @@ -22,4 +25,8 @@ export function getAuth<T = any>(): T | undefined { export function getRequestId(): string | undefined { return als.getStore()?.requestId; +} + +export function getRelay(): Ctx['relay'] { + return als.getStore()?.relay; } \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/chat/chat.module.ts b/libraries/nestjs-libraries/src/chat/chat.module.ts index ed5cc62da7..87f1b0ec10 100644 --- a/libraries/nestjs-libraries/src/chat/chat.module.ts +++ b/libraries/nestjs-libraries/src/chat/chat.module.ts @@ -2,10 +2,11 @@ import { Global, Module } from '@nestjs/common'; import { LoadToolsService } from '@gitroom/nestjs-libraries/chat/load.tools.service'; import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service'; import { toolList } from '@gitroom/nestjs-libraries/chat/tools/tool.list'; +import { McpRelayService } from '@gitroom/nestjs-libraries/chat/mcp.relay.service'; @Global() @Module({ - providers: [MastraService, LoadToolsService, ...toolList], + providers: [MastraService, LoadToolsService, McpRelayService, ...toolList], get exports() { return this.providers; }, diff --git a/libraries/nestjs-libraries/src/chat/mcp.relay.service.ts b/libraries/nestjs-libraries/src/chat/mcp.relay.service.ts new file mode 100644 index 0000000000..7cb0bd0a92 --- /dev/null +++ b/libraries/nestjs-libraries/src/chat/mcp.relay.service.ts @@ -0,0 +1,358 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { createTool } from '@mastra/core/tools'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { + StreamableHTTPClientTransport, + StreamableHTTPError, +} from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { + CallToolResult, + ErrorCode, + McpError, +} from '@modelcontextprotocol/sdk/types.js'; +import { getRelay } from '@gitroom/nestjs-libraries/chat/async.storage'; +import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher'; +import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator'; +import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service'; +import { LoadToolsService } from '@gitroom/nestjs-libraries/chat/load.tools.service'; + +export interface SelfHostedInstance { + mcpUrl: string; + apiKey: string; +} + +// Every Postiz MCP server since v2.19.0 (the first one with the API key /mcp +// route) has these; a server without them is not a Postiz instance +const requiredTools = ['integrationList', 'integrationSchedulePostTool']; + +// Tool results are small JSON, an instance never needs this server to hold +// more than this in memory +const maxResponseBytes = 10 * 1024 * 1024; + +// The address comes from a public endpoint: the pinned-DNS SSRF guard stays +// on whatever DISABLE_SSRF_PROTECTION says, no redirects and a cap on the body +const instanceFetch = async (url: string | URL, init?: RequestInit) => { + const response = await fetch(url, { + ...init, + redirect: 'error', + // @ts-ignore - undici option, not in lib.dom fetch types + dispatcher: ssrfSafeDispatcher, + }); + + if (Number(response.headers.get('content-length')) > maxResponseBytes) { + await response.body?.cancel(); + throw new Error('The response of the instance is too large'); + } + if (!response.body) { + return response; + } + + let received = 0; + const capped = response.body.pipeThrough( + new TransformStream<Uint8Array, Uint8Array>({ + transform(chunk, controller) { + received += chunk.byteLength; + if (received > maxResponseBytes) { + controller.error( + new Error('The response of the instance is too large') + ); + return; + } + controller.enqueue(chunk); + }, + }) + ); + + return new Response(capped, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); +}; + +// Postiz Cloud relays the MCP connection of a self-hosted Postiz: the person +// brings the instance and its API key on the consent screen ("Use +// self-hosted"), and the tool calls of that connection run on the instance +@Injectable() +export class McpRelayService { + constructor( + private _oauthService: OAuthService, + private _loadToolsService: LoadToolsService + ) {} + + // Turns what the person typed on the consent screen into the MCP endpoint + // of their instance, and checks that the API key works there + async connect( + instanceUrl: string, + apiKey?: string + ): Promise<SelfHostedInstance> { + const value = instanceUrl.trim(); + let url: URL; + try { + url = new URL( + /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}` + ); + } catch { + throw new HttpException( + 'Enter the address of your Postiz instance, for example https://postiz.example.com', + HttpStatus.BAD_REQUEST + ); + } + + // An address with credentials (an email like me@company.com reads as + // user "me" on company.com) is never sent anywhere + if (url.username || url.password) { + throw new HttpException( + 'Enter the address of your Postiz instance, for example https://postiz.example.com', + HttpStatus.BAD_REQUEST + ); + } + + // The MCP URL from Settings > Developers carries the API key: + // <backend>/mcp/<key> + const fromSettings = url.pathname.match(/^(.*?)\/mcp\/([^/]+)\/?$/); + const key = + apiKey?.trim() || (fromSettings && decodeURIComponent(fromSettings[2])); + if (!key) { + throw new HttpException( + 'Enter the API key of your Postiz instance', + HttpStatus.BAD_REQUEST + ); + } + + if (!(await isSafePublicHttpsUrl(url.origin))) { + throw new HttpException( + 'Your Postiz instance must be reachable from the internet over https', + HttpStatus.BAD_REQUEST + ); + } + + // The docker image serves the backend under /api of the same domain, + // a backend on its own domain answers on /mcp + const path = (fromSettings ? fromSettings[1] : url.pathname) + .replace(/\/+$/, '') + .replace(/\/mcp$/, ''); + const candidates = path.endsWith('/api') + ? [`${url.origin}${path}/mcp`] + : [`${url.origin}${path}/api/mcp`, `${url.origin}${path}/mcp`]; + + for (const mcpUrl of candidates) { + let tools: string[]; + try { + tools = await this.withClient({ mcpUrl, apiKey: key }, async (client) => + (await client.listTools(undefined, { timeout: 10000 })).tools.map( + (tool) => tool.name + ) + ); + } catch (err) { + if (err instanceof StreamableHTTPError && err.code === 401) { + throw new HttpException( + 'Your Postiz instance rejected this API key', + HttpStatus.BAD_REQUEST + ); + } + if (err instanceof StreamableHTTPError && err.code === 403) { + throw new HttpException( + 'Your Postiz instance refused the connection (403), make sure nothing in front of it blocks requests from Postiz Cloud', + HttpStatus.BAD_REQUEST + ); + } + // nothing that speaks MCP at this address, try the next one + continue; + } + + if (!requiredTools.every((name) => tools.includes(name))) { + throw new HttpException( + `${url.origin} does not look like a Postiz instance, or it runs a version older than v2.19.0`, + HttpStatus.BAD_REQUEST + ); + } + + // Every tool this server lists has to run on the instance, an older one + // would answer some calls with errors + const missing = Object.keys(await this._loadToolsService.loadTools()).filter( + (name) => !tools.includes(name) + ); + if (missing.length) { + throw new HttpException( + `Your Postiz instance is missing tools this connection needs (${missing.join(', ')}). Update it to the latest Postiz version and connect again`, + HttpStatus.BAD_REQUEST + ); + } + + return { mcpUrl, apiKey: key }; + } + + throw new HttpException( + `Could not find a Postiz MCP server at ${url.origin}. Paste the MCP URL from Settings > Developers of your instance, and make sure it runs Postiz v2.19.0 or newer`, + HttpStatus.BAD_REQUEST + ); + } + + // The same definitions this server serves everyone (the listing stays the + // reviewed one, and nothing an instance returns ends up in a description), + // run by the instance. Pass tools without ui:// widgets - those upload to + // this server - and _meta (a tool's widget link) is dropped too + tools(source: Record<string, any>) { + return Object.fromEntries( + Object.entries(source).map(([name, tool]) => [ + name, + createTool({ + id: tool.id, + description: tool.description, + inputSchema: tool.inputSchema, + outputSchema: tool.outputSchema, + mcp: { annotations: tool.mcp?.annotations }, + execute: async (inputData: Record<string, unknown>, context: any) => { + const instance = getRelay(); + if (!instance) { + throw new Error( + 'This connection is not linked to a self-hosted instance' + ); + } + + const result = await this.callTool( + instance, + name, + inputData, + context?.mcp?.extra?.signal + ); + const text = this.text(result); + if (!tool.outputSchema) { + return text; + } + + // The structured result is what the outputSchema describes; + // instances whose tool had no outputSchema yet answer with JSON text + let output: unknown = result.structuredContent; + if (output === undefined) { + try { + output = JSON.parse(text); + } catch { + throw new Error( + text || + `${name} returned an unexpected answer from your Postiz instance` + ); + } + } + + // an instance on another Postiz version can answer in another shape + const check = await tool.outputSchema['~standard']?.validate(output); + if (check?.issues) { + throw new Error( + `Your Postiz instance answered ${name} in a format this connection does not know, update the instance to the latest Postiz version` + ); + } + + return output; + }, + }), + ]) + ); + } + + async callTool( + instance: SelfHostedInstance & { id?: string }, + name: string, + args: Record<string, unknown>, + signal?: AbortSignal + ) { + let result: CallToolResult; + try { + result = (await this.withClient(instance, (client) => + client.callTool({ name, arguments: args }, undefined, { + signal, + // under the 100 seconds Cloudflare gives a request to the MCP host + timeout: 90000, + }) + )) as CallToolResult; + } catch (err) { + // The key was rotated (or the instance reset): the connection can never + // work again, so it goes away and the client asks to connect again + if (err instanceof StreamableHTTPError && err.code === 401 && instance.id) { + await this._oauthService.deleteSelfHostedAuthorization(instance.id); + } + throw new Error(this.describeError(instance, name, err)); + } + + if (result.isError) { + const text = this.text(result); + // an instance older than this server doesn't have every tool yet + if (text.startsWith('Unknown tool:')) { + throw new Error( + `Your Postiz instance does not have ${name} yet, update it to the latest Postiz version to use it` + ); + } + // arguments this server validated but an older instance doesn't accept + if (text.startsWith('Tool validation failed')) { + throw new Error( + `Your Postiz instance runs an older version of ${name}, update it to the latest Postiz version to use it` + ); + } + throw new Error(text || `${name} failed on your Postiz instance`); + } + + return result; + } + + private async withClient<T>( + instance: SelfHostedInstance, + run: (client: Client) => Promise<T> + ) { + const transport = new StreamableHTTPClientTransport( + new URL(instance.mcpUrl), + { + requestInit: { + headers: { Authorization: `Bearer ${instance.apiKey}` }, + }, + fetch: instanceFetch, + } + ); + const client = new Client({ name: 'Postiz Cloud', version: '1.0.0' }); + + try { + await client.connect(transport, { timeout: 10000 }); + return await run(client); + } finally { + // instances older than v2.23.0 keep a session per connection, end it + // so relayed calls don't pile them up + if (transport.sessionId) { + await transport.terminateSession().catch(() => {}); + } + await client.close().catch(() => {}); + } + } + + private text(result: CallToolResult) { + return (result.content || []) + .filter((part) => part.type === 'text') + .map((part) => (part as { text: string }).text) + .join('\n') + .trim(); + } + + // What the model (and so the person) reads when the instance can't run a + // tool - it has to say what to do next + private describeError( + instance: SelfHostedInstance, + name: string, + err: unknown + ) { + const origin = new URL(instance.mcpUrl).origin; + if (err instanceof StreamableHTTPError && err.code === 401) { + return `Your Postiz instance (${origin}) rejected the saved API key, it was probably rotated, so this connection was removed. Connect Postiz again to keep using your instance`; + } + if (err instanceof StreamableHTTPError && err.code === 403) { + return `Your Postiz instance (${origin}) refused the request (403), make sure nothing in front of it blocks requests from Postiz Cloud`; + } + if (err instanceof McpError && err.code === ErrorCode.RequestTimeout) { + return `Your Postiz instance (${origin}) did not answer ${name} in time`; + } + if (err instanceof McpError) { + return `Your Postiz instance (${origin}) could not run ${name}: ${err.message}`; + } + return `Could not reach your Postiz instance (${origin}): ${ + err instanceof Error ? err.message : String(err) + }. Make sure it is online and reachable from the internet`; + } +} diff --git a/libraries/nestjs-libraries/src/chat/start.mcp.ts b/libraries/nestjs-libraries/src/chat/start.mcp.ts index 7dfb2ff47e..b96c75d8ab 100644 --- a/libraries/nestjs-libraries/src/chat/start.mcp.ts +++ b/libraries/nestjs-libraries/src/chat/start.mcp.ts @@ -4,12 +4,16 @@ import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service'; import { LoadToolsService } from '@gitroom/nestjs-libraries/chat/load.tools.service'; import { MCPServer } from '@mastra/mcp'; import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; -import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service'; +import { + OAuthService, + selfHostedRelayEnabled, +} from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service'; import { runWithContext } from './async.storage'; import { createOAuthMiddleware } from './oauth-middleware'; import { UPLOAD_WIDGET_URI, uploadWidgetHtml } from '@gitroom/nestjs-libraries/chat/ui/upload.widget'; import { CLIPPING_WIDGET_URI, clippingWidgetHtml } from '@gitroom/nestjs-libraries/chat/ui/clipping.widget'; import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; +import { McpRelayService } from '@gitroom/nestjs-libraries/chat/mcp.relay.service'; const fixAcceptHeader = (req: Request) => { const value = 'application/json, text/event-stream'; req.headers.accept = value; @@ -34,6 +38,7 @@ export const startMcp = async (app: INestApplication) => { const organizationService = app.get(OrganizationService, { strict: false }); const oauthService = app.get(OAuthService, { strict: false }); const loadToolsService = app.get(LoadToolsService, { strict: false }); + const mcpRelayService = app.get(McpRelayService, { strict: false }); const resolveAuth = async (token: string) => { if (token.startsWith('pos_')) { @@ -44,10 +49,15 @@ export const startMcp = async (app: INestApplication) => { return organizationService.getOrgByApiKey(token); }; + // psh_ tokens: a self-hosted Postiz this server relays to (McpRelayService) + const resolveSelfHosted = (token: string) => + oauthService.getSelfHostedByAccessToken(token); + const mastra = await mastraService.mastra(); const agent = mastra.getAgent('postiz'); + const agentTools = await agent.listTools(); const tools = { - ...(await agent.listTools()), + ...agentTools, // tools that only make sense inside an MCP host (ui:// widgets) ...(await loadToolsService.loadTools(true)), }; @@ -66,9 +76,11 @@ export const startMcp = async (app: INestApplication) => { 'clippingStatusTool', 'clippingWidgetTicketTool', ]; - const claudeTools = Object.fromEntries( - Object.entries(tools).filter(([name]) => !claudeHiddenTools.includes(name)) - ) as typeof tools; + const withoutClaudeHidden = <T extends Record<string, unknown>>(source: T) => + Object.fromEntries( + Object.entries(source).filter(([name]) => !claudeHiddenTools.includes(name)) + ) as T; + const claudeTools = withoutClaudeHidden(tools); const backendUrl = process.env.NEXT_PUBLIC_OVERRIDE_BACKEND_URL || process.env.NEXT_PUBLIC_BACKEND_URL; // this runs before the backend listens: a bucket url that doesn't parse only @@ -143,6 +155,24 @@ export const startMcp = async (app: INestApplication) => { appResources: claudeAppResources, }); + // Self-hosted connections, built from the agent tools so the ui:// widgets + // (which upload to this server) are left out + let selfHostedServer: MCPServer | undefined; + let claudeSelfHostedServer: MCPServer | undefined; + if (selfHostedRelayEnabled()) { + const selfHostedTools = mcpRelayService.tools(agentTools); + selfHostedServer = new MCPServer({ + name: 'Postiz MCP', + version: '1.0.0', + tools: selfHostedTools, + }); + claudeSelfHostedServer = new MCPServer({ + name: 'Postiz MCP', + version: '1.0.0', + tools: withoutClaudeHidden(selfHostedTools), + }); + } + // Two RFC 8414 path-based issuers backed by the same endpoints and code. // /mcp-oauth-chatgpt is what the ChatGPT app submission points at: it does // not advertise a registration_endpoint, so the OpenAI builder defaults to @@ -190,7 +220,9 @@ export const startMcp = async (app: INestApplication) => { authorizationServers: [authorizationServers[authorizationServer].issuer], scopesSupported: oauthScopes, validateToken: async (token: string) => { - const org = await resolveAuth(token); + const org = token.startsWith('psh_') + ? await resolveSelfHosted(token) + : await resolveAuth(token); if (!org) { return { valid: false, error: 'invalid_token', errorDescription: 'Invalid API Key or OAuth token' }; } @@ -202,17 +234,21 @@ export const startMcp = async (app: INestApplication) => { const oauthResources: Record< string, - { middleware: ReturnType<typeof createOAuthMiddleware>; mcpServer: MCPServer } + { + middleware: ReturnType<typeof createOAuthMiddleware>; + mcpServer: MCPServer; + selfHostedServer?: MCPServer; + } > = { // ChatGPT app submission (pre-defined client credentials, no DCR) - '/mcp-oauth-chatgpt': { middleware: createResourceMiddleware('/mcp-oauth-chatgpt', '/mcp-oauth-chatgpt'), mcpServer: oauthServer }, + '/mcp-oauth-chatgpt': { middleware: createResourceMiddleware('/mcp-oauth-chatgpt', '/mcp-oauth-chatgpt'), mcpServer: oauthServer, selfHostedServer }, // Former ChatGPT path, kept for connectors that were created against it - '/mcp-oauth': { middleware: createResourceMiddleware('/mcp-oauth', '/mcp-oauth-dynamic'), mcpServer: oauthServer }, + '/mcp-oauth': { middleware: createResourceMiddleware('/mcp-oauth', '/mcp-oauth-dynamic'), mcpServer: oauthServer, selfHostedServer }, // Claude connector directory submission - '/mcp-oauth-claude': { middleware: createResourceMiddleware('/mcp-oauth-claude', '/mcp-oauth-dynamic'), mcpServer: claudeOauthServer }, + '/mcp-oauth-claude': { middleware: createResourceMiddleware('/mcp-oauth-claude', '/mcp-oauth-dynamic'), mcpServer: claudeOauthServer, selfHostedServer: claudeSelfHostedServer }, // Clients that register themselves through DCR (/oauth/register) - not // directory-reviewed, so they get the full toolset (media generation included) - '/mcp-oauth-dynamic': { middleware: createResourceMiddleware('/mcp-oauth-dynamic', '/mcp-oauth-dynamic'), mcpServer: oauthServer }, + '/mcp-oauth-dynamic': { middleware: createResourceMiddleware('/mcp-oauth-dynamic', '/mcp-oauth-dynamic'), mcpServer: oauthServer, selfHostedServer }, }; if (process.env.OPENAI_APP_CHALLANGE) { @@ -288,22 +324,25 @@ export const startMcp = async (app: INestApplication) => { } // baseUrl is the mount path that matched, e.g. /mcp-oauth-claude - const { middleware, mcpServer } = oauthResources[req.baseUrl]; + const { middleware, mcpServer, selfHostedServer } = oauthResources[req.baseUrl]; const url = new URL(req.baseUrl, process.env.NEXT_PUBLIC_BACKEND_URL); const result = await middleware(req, res, url); if (!result.proceed) return; const token = result.tokenValidation?.subject; - const auth = await resolveAuth(token!); - if (!auth) { + // a self-hosted connection has no organization here + const selfHosted = token!.startsWith('psh_'); + const relay = selfHosted ? await resolveSelfHosted(token!) : undefined; + const auth = selfHosted ? undefined : await resolveAuth(token!); + if (!auth && !(relay && selfHostedServer)) { res.status(401).json({ error: 'invalid_token', error_description: 'Could not resolve organization' }); return; } fixAcceptHeader(req); - await runWithContext({ requestId: token!, auth }, async () => { - await mcpServer.startHTTP({ + await runWithContext({ requestId: token!, auth, relay: relay || undefined }, async () => { + await (relay ? selfHostedServer! : mcpServer).startHTTP({ url: url, httpPath: url.pathname, options: { diff --git a/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.repository.ts b/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.repository.ts index e7fe30fcba..eb30f8f9a5 100644 --- a/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.repository.ts @@ -5,7 +5,8 @@ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/pris export class OAuthRepository { constructor( private _oauthApp: PrismaRepository<'oAuthApp'>, - private _oauthAuth: PrismaRepository<'oAuthAuthorization'> + private _oauthAuth: PrismaRepository<'oAuthAuthorization'>, + private _oauthSelfHosted: PrismaRepository<'oAuthSelfHostedAuthorization'> ) {} getAppByOrgId(orgId: string) { @@ -88,6 +89,7 @@ export class OAuthRepository { dynamic: true, createdAt: { lt: olderThan }, authorizations: { none: {} }, + selfHostedAuthorizations: { none: {} }, }, }); } @@ -294,7 +296,17 @@ export class OAuthRepository { }); } - revokeAllForApp(oauthAppId: string) { + async revokeAllForApp(oauthAppId: string) { + await this._oauthSelfHosted.model.oAuthSelfHostedAuthorization.updateMany({ + where: { + oauthAppId, + revokedAt: null, + }, + data: { + revokedAt: new Date(), + }, + }); + return this._oauthAuth.model.oAuthAuthorization.updateMany({ where: { oauthAppId, @@ -305,4 +317,121 @@ export class OAuthRepository { }, }); } + + // holds the instance API key, so only the id comes back + createSelfHostedAuthorization(data: { + oauthAppId: string; + mcpUrl: string; + apiKey: string; + email?: string; + authorizationCode: string; + codeExpiresAt: Date; + codeChallenge?: string; + codeChallengeMethod?: string; + redirectUri?: string; + }) { + return this._oauthSelfHosted.model.oAuthSelfHostedAuthorization.create({ + select: { + id: true, + }, + data: { + oauthAppId: data.oauthAppId, + mcpUrl: data.mcpUrl, + apiKey: data.apiKey, + email: data.email || null, + authorizationCode: data.authorizationCode, + codeExpiresAt: data.codeExpiresAt, + codeChallenge: data.codeChallenge || null, + codeChallengeMethod: data.codeChallengeMethod || null, + redirectUri: data.redirectUri || null, + }, + }); + } + + // Abandoned consent flows leave rows (and API keys) that never got a token + deleteAbandonedSelfHostedAuthorizations(olderThan: Date) { + return this._oauthSelfHosted.model.oAuthSelfHostedAuthorization.deleteMany( + { + where: { + accessToken: null, + createdAt: { lt: olderThan }, + }, + } + ); + } + + findSelfHostedByCode(encryptedCode: string) { + return this._oauthSelfHosted.model.oAuthSelfHostedAuthorization.findFirst({ + where: { + authorizationCode: encryptedCode, + revokedAt: null, + }, + select: { + id: true, + oauthAppId: true, + codeExpiresAt: true, + codeChallenge: true, + redirectUri: true, + }, + }); + } + + exchangeSelfHostedCodeForToken(id: string, encryptedToken: string) { + return this._oauthSelfHosted.model.oAuthSelfHostedAuthorization.update({ + where: { id }, + select: { + id: true, + }, + data: { + accessToken: encryptedToken, + authorizationCode: null, + codeExpiresAt: null, + codeChallenge: null, + codeChallengeMethod: null, + redirectUri: null, + }, + }); + } + + findSelfHostedByAccessToken(encryptedToken: string) { + return this._oauthSelfHosted.model.oAuthSelfHostedAuthorization.findFirst({ + where: { + accessToken: encryptedToken, + revokedAt: null, + }, + select: { + id: true, + mcpUrl: true, + apiKey: true, + }, + }); + } + + findSelfHostedUserInfo(encryptedToken: string) { + return this._oauthSelfHosted.model.oAuthSelfHostedAuthorization.findFirst({ + where: { + accessToken: encryptedToken, + revokedAt: null, + }, + select: { + id: true, + email: true, + oauthApp: { + select: { + clientId: true, + dynamic: true, + redirectUris: true, + }, + }, + }, + }); + } + + deleteSelfHostedAuthorization(id: string) { + return this._oauthSelfHosted.model.oAuthSelfHostedAuthorization.deleteMany( + { + where: { id }, + } + ); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts b/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts index 4bd1c27a9e..6e401157f2 100644 --- a/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts @@ -8,12 +8,17 @@ import { AuthService } from '@gitroom/helpers/auth/auth.service'; import { extractBearerToken } from '@gitroom/nestjs-libraries/chat/oauth-types'; import { createHash } from 'crypto'; import { OAuthApp } from '@prisma/client'; +import type { SelfHostedInstance } from '@gitroom/nestjs-libraries/chat/mcp.relay.service'; const openAiOAuthClientId = () => process.env.OPENAI_OAUTH_CLIENT_ID?.trim(); const enableOidcEmailClaims = () => Boolean(openAiOAuthClientId()); +// Postiz Cloud only, a self-hosted install has nothing to relay to +export const selfHostedRelayEnabled = () => + process.env.MCP_SELF_HOSTED_RELAY === 'true'; + // Verified-domain match: exact host or a subdomain of it (spoof-safe, the // leading dot means evilclaude.ai and claude.ai.evil.com are both rejected) const isVerifiedHost = (host: string, verifiedDomains: string[]) => @@ -259,6 +264,54 @@ export class OAuthService { ].join(' '); } + // Only MCP connections can be relayed to an instance: dynamically registered + // clients (registration is only advertised to MCP clients), the ChatGPT app, + // or a client asking for one of the /mcp-oauth resources (RFC 8707). Apps + // that use Postiz OAuth for the public API never get the option, a relayed + // token doesn't work there + allowsSelfHosted( + app: Pick<OAuthApp, 'clientId' | 'dynamic'>, + resource?: string + ) { + if (!selfHostedRelayEnabled()) { + return false; + } + if (app.dynamic || app.clientId === openAiOAuthClientId()) { + return true; + } + try { + return !!resource && new URL(resource).pathname.startsWith('/mcp-oauth'); + } catch { + return false; + } + } + + // The ChatGPT app reads the email from userinfo; a self-hosted connection + // has no cloud user to take it from, so the consent screen asks for one + selfHostedRequiresEmail(app: Pick<OAuthApp, 'clientId'>) { + return enableOidcEmailClaims() && app.clientId === openAiOAuthClientId(); + } + + // On top of validateAuthorizationRequest, for a self-hosted connection + validateSelfHostedRequest( + app: Pick<OAuthApp, 'clientId' | 'dynamic'>, + options: { resource?: string; email?: string } + ) { + if (!this.allowsSelfHosted(app, options.resource)) { + throw new HttpException( + 'This application can not connect to a self-hosted instance', + HttpStatus.BAD_REQUEST + ); + } + + if (!options.email && this.selfHostedRequiresEmail(app)) { + throw new HttpException( + 'Enter your email address', + HttpStatus.BAD_REQUEST + ); + } + } + async validateAuthorizationRequest( clientId: string, options?: { @@ -328,6 +381,40 @@ export class OAuthService { return code; } + async createSelfHostedAuthorizationCode( + oauthAppId: string, + instance: SelfHostedInstance & { email?: string }, + pkce?: { + codeChallenge?: string; + codeChallengeMethod?: string; + redirectUri?: string; + } + ) { + this._oauthRepository + .deleteAbandonedSelfHostedAuthorizations( + new Date(Date.now() - 24 * 60 * 60 * 1000) + ) + .catch(() => {}); + + // The prefix sends the code to the self-hosted table at the token + // endpoint (regular codes use the same alphabet, without an underscore) + const code = 'psc_' + makeSecureId(32); + + await this._oauthRepository.createSelfHostedAuthorization({ + oauthAppId, + mcpUrl: instance.mcpUrl, + apiKey: AuthService.fixedEncryption(instance.apiKey), + email: instance.email, + authorizationCode: AuthService.fixedEncryption(code), + codeExpiresAt: new Date(Date.now() + 10 * 60 * 1000), + codeChallenge: pkce?.codeChallenge, + codeChallengeMethod: pkce?.codeChallengeMethod, + redirectUri: pkce?.redirectUri, + }); + + return code; + } + async exchangeCodeForToken( code: string, clientId: string, @@ -359,6 +446,15 @@ export class OAuthService { } } + if (code.startsWith('psc_')) { + return this.exchangeSelfHostedCodeForToken( + app, + code, + codeVerifier, + redirectUri + ); + } + const encryptedCode = AuthService.fixedEncryption(code); const auth = await this._oauthRepository.findByCode(encryptedCode); if (!auth || auth.oauthAppId !== app.id) { @@ -368,6 +464,38 @@ export class OAuthService { ); } + this.verifyCodeGrant(auth, codeVerifier, redirectUri); + + const token = 'pos_' + makeSecureId(40); + const encryptedToken = AuthService.fixedEncryption(token); + const { + organizationId, + organization: { paymentId }, + } = await this._oauthRepository.exchangeCodeForToken( + auth.id, + encryptedToken + ); + + return { + id: organizationId, + cus: paymentId, + access_token: token, + token_type: 'bearer', + scope: this.grantedScope(app), + }; + } + + // Expiry, PKCE and redirect_uri checks of an authorization code, shared by + // cloud and self-hosted authorizations + private verifyCodeGrant( + auth: { + codeExpiresAt: Date | null; + codeChallenge: string | null; + redirectUri: string | null; + }, + codeVerifier?: string, + redirectUri?: string + ) { if (!auth.codeExpiresAt || new Date() > auth.codeExpiresAt) { throw new HttpException( { error: 'invalid_grant', error_description: 'Code has expired' }, @@ -397,26 +525,66 @@ export class OAuthService { HttpStatus.BAD_REQUEST ); } + } - const token = 'pos_' + makeSecureId(40); - const encryptedToken = AuthService.fixedEncryption(token); - const { - organizationId, - organization: { paymentId }, - } = await this._oauthRepository.exchangeCodeForToken( + // A self-hosted connection has no organization (or payment) to return, + // only the token + private async exchangeSelfHostedCodeForToken( + app: OAuthApp, + code: string, + codeVerifier?: string, + redirectUri?: string + ) { + const auth = await this._oauthRepository.findSelfHostedByCode( + AuthService.fixedEncryption(code) + ); + if (!auth || auth.oauthAppId !== app.id) { + throw new HttpException( + { error: 'invalid_grant' }, + HttpStatus.BAD_REQUEST + ); + } + + this.verifyCodeGrant(auth, codeVerifier, redirectUri); + + const token = 'psh_' + makeSecureId(40); + await this._oauthRepository.exchangeSelfHostedCodeForToken( auth.id, - encryptedToken + AuthService.fixedEncryption(token) ); return { - id: organizationId, - cus: paymentId, access_token: token, token_type: 'bearer', scope: this.grantedScope(app), }; } + // null when the relay is off or the token is unknown or revoked + async getSelfHostedByAccessToken(token: string) { + if (!selfHostedRelayEnabled()) { + return null; + } + + const authorization = + await this._oauthRepository.findSelfHostedByAccessToken( + AuthService.fixedEncryption(token) + ); + if (!authorization) { + return null; + } + + return { + id: authorization.id, + mcpUrl: authorization.mcpUrl, + apiKey: AuthService.fixedDecryption(authorization.apiKey), + }; + } + + deleteSelfHostedAuthorization(id: string) { + return this._oauthRepository.deleteSelfHostedAuthorization(id); + } + async getOrgByOAuthToken(token: string) { const encrypted = AuthService.fixedEncryption(token); return this._oauthRepository.findByAccessToken(encrypted); @@ -441,6 +609,10 @@ export class OAuthService { ); } + if (token.startsWith('psh_')) { + return this.getSelfHostedUserInfo(token); + } + const authorizationRecord = await this.getOrgByOAuthToken(token); if (!authorizationRecord) { throw new HttpException( @@ -468,6 +640,40 @@ export class OAuthService { }; } + // A self-hosted connection has no cloud user: the subject is the connection + // and the email is the one typed on the consent screen, which nobody verified + private async getSelfHostedUserInfo(token: string) { + const selfHosted = selfHostedRelayEnabled() + ? await this._oauthRepository.findSelfHostedUserInfo( + AuthService.fixedEncryption(token) + ) + : null; + if (!selfHosted) { + throw new HttpException( + { error: 'invalid_token', error_description: 'Token is invalid or revoked' }, + HttpStatus.UNAUTHORIZED + ); + } + + if (!this.allowsEmailClaims(selfHosted.oauthApp)) { + throw new HttpException( + { + error: 'insufficient_scope', + error_description: + 'This OAuth client is not authorized to access email claims', + }, + HttpStatus.FORBIDDEN + ); + } + + return { + sub: selfHosted.id, + ...(selfHosted.email + ? { email: selfHosted.email, email_verified: false } + : {}), + }; + } + async getApprovedApps(userId: string) { return this._oauthRepository.getApprovedApps(userId); } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 328b0db42d..a644164744 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -1679,23 +1679,24 @@ model mastra_workspaces { } model OAuthApp { - id String @id @default(uuid()) - organizationId String? - name String - description String? - pictureId String? - redirectUrl String - redirectUris String? - clientId String @unique - clientSecret String? - dynamic Boolean @default(false) - tokenEndpointAuthMethod String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - organization Organization? @relation(fields: [organizationId], references: [id]) - picture Media? @relation(fields: [pictureId], references: [id]) - authorizations OAuthAuthorization[] + id String @id @default(uuid()) + organizationId String? + name String + description String? + pictureId String? + redirectUrl String + redirectUris String? + clientId String @unique + clientSecret String? + dynamic Boolean @default(false) + tokenEndpointAuthMethod String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + organization Organization? @relation(fields: [organizationId], references: [id]) + picture Media? @relation(fields: [pictureId], references: [id]) + authorizations OAuthAuthorization[] + selfHostedAuthorizations OAuthSelfHostedAuthorization[] @@unique([organizationId, deletedAt]) @@index([clientId]) @@ -1731,6 +1732,28 @@ model OAuthAuthorization { @@index([revokedAt]) } +model OAuthSelfHostedAuthorization { + id String @id @default(uuid()) + oauthAppId String + mcpUrl String + apiKey String + email String? + accessToken String? + authorizationCode String? + codeExpiresAt DateTime? + codeChallenge String? + codeChallengeMethod String? + redirectUri String? + revokedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + oauthApp OAuthApp @relation(fields: [oauthAppId], references: [id]) + + @@index([accessToken]) + @@index([authorizationCode]) + @@index([oauthAppId]) +} + enum OrderStatus { PENDING ACCEPTED diff --git a/libraries/nestjs-libraries/src/dtos/oauth/authorize-oauth.dto.ts b/libraries/nestjs-libraries/src/dtos/oauth/authorize-oauth.dto.ts index 87e3e64584..bb80847c6e 100644 --- a/libraries/nestjs-libraries/src/dtos/oauth/authorize-oauth.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/oauth/authorize-oauth.dto.ts @@ -1,4 +1,11 @@ -import { IsDefined, IsIn, IsOptional, IsString } from 'class-validator'; +import { + IsDefined, + IsEmail, + IsIn, + IsOptional, + IsString, + MaxLength, +} from 'class-validator'; export class AuthorizeOAuthQueryDto { @IsString() @@ -31,6 +38,12 @@ export class AuthorizeOAuthQueryDto { @IsString() @IsOptional() scope?: string; + + // RFC 8707 resource indicator sent by MCP clients, only used to tell if + // the connection can be relayed to a self-hosted instance + @IsString() + @IsOptional() + resource?: string; } export class ApproveOAuthDto { @@ -61,3 +74,49 @@ export class ApproveOAuthDto { @IsOptional() code_challenge_method?: string; } + +export class AuthorizeSelfHostedDto { + @IsString() + @IsDefined() + client_id: string; + + @IsString() + @IsOptional() + state?: string; + + @IsString() + @IsOptional() + redirect_uri?: string; + + @IsString() + @IsOptional() + code_challenge?: string; + + // Not validated with IsIn, same as ApproveOAuthDto + @IsString() + @IsOptional() + code_challenge_method?: string; + + @IsString() + @IsOptional() + resource?: string; + + // The instance as the person knows it: its address, its backend address or + // the MCP URL from Settings > Developers (resolved in McpRelayService) + @IsString() + @IsDefined() + @MaxLength(2048) + instance_url: string; + + // Optional because the MCP URL from Settings > Developers carries the key + @IsString() + @IsOptional() + @MaxLength(512) + api_key?: string; + + // Required (in the controller) for clients that read the email from + // userinfo, like ChatGPT + @IsEmail() + @IsOptional() + email?: string; +} diff --git a/libraries/react-shared-libraries/src/translation/locales/ar/translation.json b/libraries/react-shared-libraries/src/translation/locales/ar/translation.json index 7935a7f33b..b8a6dd6e2c 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ar/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ar/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "لا حاجة لإعدادات MCP أو CLI. الصق هذا في الدردشة، سيقوم الوكيل بتثبيت Postiz CLI ويطلب منك مفتاح API الخاص بك.", "connector": "موصل", "connector_onboarding_description": "الأسرع: أضف Postiz بنقرة واحدة، سيطلب منك تسجيل الدخول", + "official_connector": "الموصل الرسمي", + "connector_self_hosted_description": "يعمل الموصل الرسمي أيضًا مع Postiz المستضاف ذاتيًا. عند طلب تسجيل الدخول، اختر \"Use self-hosted\" وأدخل {{url}} مع مفتاح API الخاص بك.", "mcp_onboarding_description": "زود وكيلك بأدوات Postiz لإنشاء وجدولة وإدارة المنشورات", "cli_onboarding_description": "ثبّت Postiz CLI والمهارة التي تعلم وكيلك كيفية استخدامها", "agent_settings_later": "المزيد من الوكلاء والتعليمات الكاملة متوفرة في الإعدادات > المطورون", diff --git a/libraries/react-shared-libraries/src/translation/locales/bn/translation.json b/libraries/react-shared-libraries/src/translation/locales/bn/translation.json index d028c353d6..4f65a827ae 100644 --- a/libraries/react-shared-libraries/src/translation/locales/bn/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/bn/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "MCP বা CLI সেটিংসের প্রয়োজন নেই। এটি চ্যাটে পেস্ট করুন, এজেন্টটি Postiz CLI ইনস্টল করবে এবং আপনার API কী চাইবে।", "connector": "কনেক্টর", "connector_onboarding_description": "সবচেয়ে দ্রুত উপায়: এক ক্লিকে Postiz যোগ করুন, আপনাকে সাইন ইন করতে বলা হবে", + "official_connector": "অফিসিয়াল কানেক্টর", + "connector_self_hosted_description": "অফিসিয়াল কানেক্টর সেলফ-হোস্টেড Postiz-এর সাথেও কাজ করে। সাইন ইন করতে বলা হলে, \"Use self-hosted\" বেছে নিন এবং আপনার API কী সহ {{url}} লিখুন।", "mcp_onboarding_description": "আপনার এজেন্টকে Postiz টুল দিন যাতে সে পোস্ট তৈরি, নির্ধারণ এবং পরিচালনা করতে পারে", "cli_onboarding_description": "Postiz CLI এবং সেই স্কিল ইনস্টল করুন যা আপনার এজেন্টকে এটি ব্যবহার করতে শেখায়", "agent_settings_later": "আরও এজেন্ট এবং সম্পূর্ণ নির্দেশিকা সেটিংস > ডেভেলপারস-এ পাওয়া যাবে", diff --git a/libraries/react-shared-libraries/src/translation/locales/de/translation.json b/libraries/react-shared-libraries/src/translation/locales/de/translation.json index 9166878e57..37a07c121e 100644 --- a/libraries/react-shared-libraries/src/translation/locales/de/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/de/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "Kein MCP oder CLI Setup nötig. Füge das in den Chat ein und der Agent installiert das Postiz CLI und fragt dich nach deinem API-Schlüssel.", "connector": "Konnektor", "connector_onboarding_description": "Am schnellsten: Füge Postiz mit einem Klick hinzu, du wirst zur Anmeldung aufgefordert.", + "official_connector": "Offizieller Connector", + "connector_self_hosted_description": "Der offizielle Connector funktioniert auch mit selbst gehostetem Postiz. Wenn du dich anmelden sollst, wähle \"Use self-hosted\" und gib {{url}} zusammen mit deinem API-Schlüssel ein.", "mcp_onboarding_description": "Gib deinem Agenten Postiz-Tools, um Beiträge zu erstellen, zu planen und zu verwalten.", "cli_onboarding_description": "Installiere das Postiz CLI und das Skill, das deinem Agenten beibringt, wie es zu nutzen ist.", "agent_settings_later": "Weitere Agenten und vollständige Anweisungen findest du unter Einstellungen > Entwickler.", diff --git a/libraries/react-shared-libraries/src/translation/locales/en/translation.json b/libraries/react-shared-libraries/src/translation/locales/en/translation.json index b7729393fa..8c860883bf 100644 --- a/libraries/react-shared-libraries/src/translation/locales/en/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/en/translation.json @@ -728,6 +728,8 @@ "chat_onboarding_description": "No MCP or CLI settings needed. Paste this into the chat, the agent installs the Postiz CLI and asks you for your API key.", "connector": "Connector", "connector_onboarding_description": "The fastest way: add Postiz with one click, you will be asked to sign in", + "official_connector": "Official connector", + "connector_self_hosted_description": "The official connector works with self-hosted Postiz too. When asked to sign in, choose \"Use self-hosted\" and enter {{url}} with your API key.", "mcp_onboarding_description": "Give your agent Postiz tools to create, schedule and manage posts", "cli_onboarding_description": "Install the Postiz CLI and the skill that teaches your agent how to use it", "agent_settings_later": "More agents and full instructions are available under Settings > Developers", diff --git a/libraries/react-shared-libraries/src/translation/locales/es/translation.json b/libraries/react-shared-libraries/src/translation/locales/es/translation.json index c5d398bf89..21f23c8148 100644 --- a/libraries/react-shared-libraries/src/translation/locales/es/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/es/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "No necesitas configuraciones de MCP ni de CLI. Pega esto en el chat, el agente instalará el CLI de Postiz y te pedirá tu clave API.", "connector": "Conector", "connector_onboarding_description": "La forma más rápida: agrega Postiz con un clic, se te pedirá iniciar sesión", + "official_connector": "Conector oficial", + "connector_self_hosted_description": "El conector oficial también funciona con Postiz autoalojado. Cuando te pida iniciar sesión, elige \"Use self-hosted\" e introduce {{url}} con tu clave de API.", "mcp_onboarding_description": "Dale a tu agente las herramientas de Postiz para crear, programar y gestionar publicaciones", "cli_onboarding_description": "Instala el CLI de Postiz y la habilidad que enseña a tu agente cómo utilizarlo", "agent_settings_later": "Más agentes y las instrucciones completas están disponibles en Configuración > Desarrolladores", diff --git a/libraries/react-shared-libraries/src/translation/locales/fr/translation.json b/libraries/react-shared-libraries/src/translation/locales/fr/translation.json index 94de4a792e..330c69db2e 100644 --- a/libraries/react-shared-libraries/src/translation/locales/fr/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/fr/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "Aucun paramétrage MCP ou CLI nécessaire. Collez ceci dans le chat, l’agent installe le CLI Postiz et vous demande votre clé API.", "connector": "Connecteur", "connector_onboarding_description": "La façon la plus rapide : ajoutez Postiz en un clic, vous serez invité à vous connecter", + "official_connector": "Connecteur officiel", + "connector_self_hosted_description": "Le connecteur officiel fonctionne aussi avec Postiz auto-hébergé. Lorsqu'on vous demande de vous connecter, choisissez \"Use self-hosted\" et saisissez {{url}} avec votre clé API.", "mcp_onboarding_description": "Donnez à votre agent des outils Postiz pour créer, programmer et gérer des publications", "cli_onboarding_description": "Installez le CLI Postiz et la compétence qui apprend à votre agent à l’utiliser", "agent_settings_later": "Plus d’agents et les instructions complètes sont disponibles dans Paramètres > Développeurs", diff --git a/libraries/react-shared-libraries/src/translation/locales/he/translation.json b/libraries/react-shared-libraries/src/translation/locales/he/translation.json index e0556c9a48..ae20db6ce2 100644 --- a/libraries/react-shared-libraries/src/translation/locales/he/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/he/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "אין צורך ב-MCP או הגדרות CLI. העתיקו את זה לצ'אט, הסוכן יתקין את Postiz CLI וישאל אתכם את מפתח ה-API שלכם.", "connector": "מחבר", "connector_onboarding_description": "הדרך המהירה ביותר: הוסיפו את Postiz בלחיצה אחת, תתבקשו להתחבר", + "official_connector": "מחבר רשמי", + "connector_self_hosted_description": "המחבר הרשמי עובד גם עם Postiz באירוח עצמי. כשתתבקש להתחבר, בחר \"Use self-hosted\" והזן את {{url}} יחד עם מפתח ה-API שלך.", "mcp_onboarding_description": "העניקו לסוכן שלכם כלים של Postiz ליצירת, תזמון וניהול פוסטים", "cli_onboarding_description": "התקינו את Postiz CLI ואת המיומנות שמלמדת את הסוכן איך להשתמש בו", "agent_settings_later": "סוכנים נוספים והוראות מלאות זמינים תחת הגדרות > מפתחים", diff --git a/libraries/react-shared-libraries/src/translation/locales/it/translation.json b/libraries/react-shared-libraries/src/translation/locales/it/translation.json index 92f5e5bfe0..5aa352e704 100644 --- a/libraries/react-shared-libraries/src/translation/locales/it/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/it/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "Nessuna impostazione MCP o CLI necessaria. Incolla questo nella chat, l'agente installerà il Postiz CLI e ti chiederà la tua API key.", "connector": "Connettore", "connector_onboarding_description": "Il modo più veloce: aggiungi Postiz con un clic, ti verrà chiesto di accedere", + "official_connector": "Connettore ufficiale", + "connector_self_hosted_description": "Il connettore ufficiale funziona anche con Postiz self-hosted. Quando ti viene chiesto di accedere, scegli \"Use self-hosted\" e inserisci {{url}} con la tua chiave API.", "mcp_onboarding_description": "Dai al tuo agente gli strumenti Postiz per creare, programmare e gestire post", "cli_onboarding_description": "Installa il Postiz CLI e la skill che insegna al tuo agente come usarlo", "agent_settings_later": "Altri agenti e istruzioni complete sono disponibili in Impostazioni > Sviluppatori", diff --git a/libraries/react-shared-libraries/src/translation/locales/ja/translation.json b/libraries/react-shared-libraries/src/translation/locales/ja/translation.json index 7883d3c1df..36c0534dc2 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ja/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ja/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "MCPやCLIの設定は不要です。これをチャットに貼り付けるだけで、エージェントがPostiz CLIをインストールし、APIキーを尋ねます。", "connector": "コネクタ", "connector_onboarding_description": "最速の方法: ワンクリックでPostizを追加、サインインが求められます。", + "official_connector": "公式コネクタ", + "connector_self_hosted_description": "公式コネクタはセルフホストの Postiz でも使えます。サインインを求められたら「Use self-hosted」を選び、{{url}} と API キーを入力してください。", "mcp_onboarding_description": "エージェントにPostizのツールを使わせて、投稿の作成やスケジュール管理をさせましょう。", "cli_onboarding_description": "Postiz CLIと、それを使い方をエージェントに教えるスキルをインストールします。", "agent_settings_later": "その他のエージェントや詳細な手順は、設定 > 開発者 から利用できます。", diff --git a/libraries/react-shared-libraries/src/translation/locales/ko/translation.json b/libraries/react-shared-libraries/src/translation/locales/ko/translation.json index 266fa2c3fd..c88a6eb3cd 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ko/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ko/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "MCP나 CLI 설정이 필요 없습니다. 채팅에 이 내용을 붙여넣으면 에이전트가 Postiz CLI를 설치하고 API 키를 입력받습니다.", "connector": "커넥터", "connector_onboarding_description": "가장 빠른 방법: 한 번의 클릭으로 Postiz를 추가하고, 로그인 요청을 받게 됩니다.", + "official_connector": "공식 커넥터", + "connector_self_hosted_description": "공식 커넥터는 자체 호스팅 Postiz에서도 작동합니다. 로그인하라는 메시지가 나타나면 \"Use self-hosted\"를 선택하고 {{url}}과 API 키를 입력하세요.", "mcp_onboarding_description": "에이전트에게 Postiz 도구를 제공해 게시물을 생성, 예약, 관리할 수 있습니다.", "cli_onboarding_description": "Postiz CLI와 에이전트에게 사용법을 알려주는 스킬을 설치하세요.", "agent_settings_later": "더 많은 에이전트와 전체 안내는 설정 > 개발자에서 볼 수 있습니다.", diff --git a/libraries/react-shared-libraries/src/translation/locales/pt/translation.json b/libraries/react-shared-libraries/src/translation/locales/pt/translation.json index 336ae9ecf0..f259f7fa72 100644 --- a/libraries/react-shared-libraries/src/translation/locales/pt/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/pt/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "Não precisa de configurações MCP ou CLI. Cole isso no chat, o agente instala o Postiz CLI e pede sua chave de API.", "connector": "Conector", "connector_onboarding_description": "A forma mais rápida: adicione o Postiz com um clique, será solicitado que você faça login", + "official_connector": "Conector oficial", + "connector_self_hosted_description": "O conector oficial também funciona com o Postiz auto-hospedado. Quando for solicitado o login, escolha \"Use self-hosted\" e informe {{url}} com sua chave de API.", "mcp_onboarding_description": "Dê ao seu agente as ferramentas Postiz para criar, agendar e gerenciar posts", "cli_onboarding_description": "Instale o Postiz CLI e a skill que ensina seu agente como usá-lo", "agent_settings_later": "Mais agentes e instruções completas estão disponíveis em Configurações > Desenvolvedores", diff --git a/libraries/react-shared-libraries/src/translation/locales/ru/translation.json b/libraries/react-shared-libraries/src/translation/locales/ru/translation.json index 4121107592..f041fdf4b4 100644 --- a/libraries/react-shared-libraries/src/translation/locales/ru/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/ru/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "Не нужны MCP или настройки CLI. Вставьте это в чат, агент установит Postiz CLI и спросит у вас API-ключ.", "connector": "Коннектор", "connector_onboarding_description": "Самый быстрый способ: добавьте Postiz одним кликом, вам предложат войти в систему", + "official_connector": "Официальный коннектор", + "connector_self_hosted_description": "Официальный коннектор работает и с Postiz на собственном сервере. Когда вас попросят войти, выберите \"Use self-hosted\" и введите {{url}} и ваш API-ключ.", "mcp_onboarding_description": "Дайте вашему агенту инструменты Postiz для создания, планирования и управления публикациями", "cli_onboarding_description": "Установите Postiz CLI и навык, который научит вашего агента им пользоваться", "agent_settings_later": "Больше агентов и полные инструкции доступны в разделе Настройки > Разработчики", diff --git a/libraries/react-shared-libraries/src/translation/locales/tr/translation.json b/libraries/react-shared-libraries/src/translation/locales/tr/translation.json index 6dc594ae5e..813cc3925f 100644 --- a/libraries/react-shared-libraries/src/translation/locales/tr/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/tr/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "MCP veya CLI ayarlarına gerek yok. Bunu sohbete yapıştırın, ajan Postiz CLI'yı yükler ve sizden API anahtarınızı ister.", "connector": "Konnektör", "connector_onboarding_description": "En hızlı yol: Postiz'i tek tıkla ekleyin, giriş yapmanız istenecektir", + "official_connector": "Resmi bağlayıcı", + "connector_self_hosted_description": "Resmi bağlayıcı, kendi sunucunuzda barındırdığınız Postiz ile de çalışır. Giriş yapmanız istendiğinde \"Use self-hosted\" seçeneğini seçin ve API anahtarınızla birlikte {{url}} adresini girin.", "mcp_onboarding_description": "Ajanınıza gönderi oluşturma, zamanlama ve yönetme araçları verin", "cli_onboarding_description": "Postiz CLI ve ajanınıza bunun nasıl kullanılacağını öğreten beceriyi kurun", "agent_settings_later": "Daha fazla ajan ve tam talimatlar Ayarlar > Geliştiriciler bölümünde mevcuttur", diff --git a/libraries/react-shared-libraries/src/translation/locales/vi/translation.json b/libraries/react-shared-libraries/src/translation/locales/vi/translation.json index b9eb25bbc2..21a367798c 100644 --- a/libraries/react-shared-libraries/src/translation/locales/vi/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/vi/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "Không cần cài đặt MCP hoặc CLI. Dán dòng này vào chat, Agent sẽ cài đặt Postiz CLI và yêu cầu API key của bạn.", "connector": "Connector", "connector_onboarding_description": "Nhanh nhất: thêm Postiz chỉ với một cú nhấp, bạn sẽ được yêu cầu đăng nhập", + "official_connector": "Trình kết nối chính thức", + "connector_self_hosted_description": "Trình kết nối chính thức cũng hoạt động với Postiz tự lưu trữ. Khi được yêu cầu đăng nhập, hãy chọn \"Use self-hosted\" và nhập {{url}} cùng khóa API của bạn.", "mcp_onboarding_description": "Cung cấp cho Agent của bạn các công cụ của Postiz để tạo, lên lịch và quản lý bài đăng", "cli_onboarding_description": "Cài đặt Postiz CLI và kỹ năng hướng dẫn Agent của bạn cách sử dụng nó", "agent_settings_later": "Thêm nhiều Agent và hướng dẫn đầy đủ có tại Cài đặt > Nhà phát triển", diff --git a/libraries/react-shared-libraries/src/translation/locales/zh/translation.json b/libraries/react-shared-libraries/src/translation/locales/zh/translation.json index 449a805b52..9b8d1b20d9 100644 --- a/libraries/react-shared-libraries/src/translation/locales/zh/translation.json +++ b/libraries/react-shared-libraries/src/translation/locales/zh/translation.json @@ -726,6 +726,8 @@ "chat_onboarding_description": "无需 MCP 或 CLI 设置。将此粘贴到聊天中,代理会安装 Postiz CLI 并向您索取 API 密钥。", "connector": "连接器", "connector_onboarding_description": "最快捷的方法:一键添加 Postiz,系统会要求您登录", + "official_connector": "官方连接器", + "connector_self_hosted_description": "官方连接器同样适用于自托管的 Postiz。在要求登录时,选择“Use self-hosted”,然后输入 {{url}} 和你的 API 密钥。", "mcp_onboarding_description": "为您的代理提供 Postiz 工具,以创建、安排和管理帖子", "cli_onboarding_description": "安装 Postiz CLI 以及教您的代理如何使用的技能", "agent_settings_later": "更多代理和完整说明可在 设置 > 开发者 下查看", From d1e4960cb83a156bdfe5bd72d025f5fd2291e3bf Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:04:03 +0700 Subject: [PATCH 51/53] fix(sync): brand the self-hosted relay MCP servers as Crove MCP The branding guard blocked the two 'Postiz MCP' server names that came with the self-hosting connector feature; they render in agent connector UIs, so they take the deployment brand like everything else. --- libraries/nestjs-libraries/src/chat/start.mcp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/chat/start.mcp.ts b/libraries/nestjs-libraries/src/chat/start.mcp.ts index b4326ac7fa..609b181f96 100644 --- a/libraries/nestjs-libraries/src/chat/start.mcp.ts +++ b/libraries/nestjs-libraries/src/chat/start.mcp.ts @@ -177,12 +177,12 @@ export const startMcp = async (app: INestApplication) => { if (selfHostedRelayEnabled()) { const selfHostedTools = mcpRelayService.tools(agentTools); selfHostedServer = new MCPServer({ - name: 'Postiz MCP', + name: 'Crove MCP', version: '1.0.0', tools: selfHostedTools, }); claudeSelfHostedServer = new MCPServer({ - name: 'Postiz MCP', + name: 'Crove MCP', version: '1.0.0', tools: withoutClaudeHidden(selfHostedTools), }); From 436dde43b1a4cc1c6b21e548e743db7aadfe854b Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:05:04 +0700 Subject: [PATCH 52/53] test(bootstrap): forceExit so the suite terminates in CI The consent suite imports oauth.controller, whose new self-hosted endpoints pull @mastra/core into the module graph. mastra keeps a background handle, so after a green run jest never exits and the CI step hung for an hour (tests themselves were 138/138 green). forceExit matches the suite's --runInBand shared-DB contract. --- tests/bootstrap.jest.cjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/bootstrap.jest.cjs b/tests/bootstrap.jest.cjs index ace7713e82..67228c1494 100644 --- a/tests/bootstrap.jest.cjs +++ b/tests/bootstrap.jest.cjs @@ -2,6 +2,10 @@ module.exports = { rootDir: '..', testEnvironment: 'node', testRegex: 'tests[\\\\/]bootstrap.*\\.spec\\.ts$', + // The consent suite imports oauth.controller, whose self-hosted endpoints + // pull @mastra/core into the module graph; mastra keeps a background handle + // so jest never exits after a green run (the CI step hung 1h on it). + forceExit: true, transform: { '^.+\\.tsx?$': [ 'ts-jest', From 3bedee5505720047a3f5e0635cc1aabc8986ea83 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:38:36 +0700 Subject: [PATCH 53/53] fix(sync): split the self-hosted authorize endpoint out of OAuthController The consent bootstrap suite imports OAuthAuthorizedController, and the self-hosted endpoint added to OAuthController pulled McpRelayService - and through it @mastra/core and ESM-only transitive deps - into the suite's module graph. On CI's fresh install @sindresorhus/slugify is ESM-only, so the suite failed to parse and the jest step went red (and, before forceExit, hung). Moving the endpoint to its own controller (OAuthSelfHostedController, same /oauth base path, registered in both MCP_ONLY and normal controller lists) breaks that import chain at the root; route paths are unchanged. --- apps/backend/src/api/api.module.ts | 3 + .../src/api/routes/oauth.controller.ts | 59 ++-------------- .../api/routes/oauth.selfhosted.controller.ts | 68 +++++++++++++++++++ 3 files changed, 75 insertions(+), 55 deletions(-) create mode 100644 apps/backend/src/api/routes/oauth.selfhosted.controller.ts diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index 34a1aee779..87d0d523ff 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -46,6 +46,7 @@ import { OAuthController, OAuthAuthorizedController, } from '@gitroom/backend/api/routes/oauth.controller'; +import { OAuthSelfHostedController } from '@gitroom/backend/api/routes/oauth.selfhosted.controller'; import { AnnouncementsController } from '@gitroom/backend/api/routes/announcements.controller'; import { AdminController } from '@gitroom/backend/api/routes/admin.controller'; import { EcosystemModule } from '@gitroom/backend/ecosystem/ecosystem.module'; @@ -88,6 +89,7 @@ const authenticatedController = [ ? [ RootController, OAuthController, + OAuthSelfHostedController, MediaWidgetController, ClippingWidgetController, ] @@ -101,6 +103,7 @@ const authenticatedController = [ EnterpriseController, NoAuthIntegrationsController, OAuthController, + OAuthSelfHostedController, MediaWidgetController, ClippingWidgetController, ...authenticatedController, diff --git a/apps/backend/src/api/routes/oauth.controller.ts b/apps/backend/src/api/routes/oauth.controller.ts index c9d705d1df..2cdf8e143a 100644 --- a/apps/backend/src/api/routes/oauth.controller.ts +++ b/apps/backend/src/api/routes/oauth.controller.ts @@ -11,7 +11,6 @@ import { Req, Res, ServiceUnavailableException, - UseGuards, } from '@nestjs/common'; import { Request, Response } from 'express'; import { BootstrapService } from '@gitroom/backend/ecosystem/bootstrap.service'; @@ -26,22 +25,16 @@ import { User, Organization } from '@prisma/client'; import { AuthorizeOAuthQueryDto, ApproveOAuthDto, - AuthorizeSelfHostedDto, } from '@gitroom/nestjs-libraries/dtos/oauth/authorize-oauth.dto'; import { TokenExchangeDto } from '@gitroom/nestjs-libraries/dtos/oauth/token-exchange.dto'; import { RegisterClientDto } from '@gitroom/nestjs-libraries/dtos/oauth/register-client.dto'; import { RevokeTokenDto } from '@gitroom/nestjs-libraries/dtos/oauth/revoke-token.dto'; import { extractBasicCredentials } from '@gitroom/nestjs-libraries/chat/oauth-types'; -import { McpRelayService } from '@gitroom/nestjs-libraries/chat/mcp.relay.service'; -import { ThrottlerRealIpGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider'; @ApiTags('OAuth') @Controller('/oauth') export class OAuthController { - constructor( - private _oauthService: OAuthService, - private _mcpRelayService: McpRelayService - ) {} + constructor(private _oauthService: OAuthService) {} // Dynamic Client Registration (RFC 7591), used by MCP clients like Claude @Post('/register') @@ -82,53 +75,9 @@ export class OAuthController { } // Public (the person may have no account here) and capped per client, - // since every attempt sends requests to the instance - @UseGuards(ThrottlerRealIpGuard) - @Throttle({ default: { limit: 30, ttl: 3600000 } }) - @Post('/authorize/self-hosted') - async authorizeSelfHosted(@Body() body: AuthorizeSelfHostedDto) { - const app = await this._oauthService.validateAuthorizationRequest( - body.client_id, - { - redirectUri: body.redirect_uri, - codeChallenge: body.code_challenge, - codeChallengeMethod: body.code_challenge_method, - } - ); - - const email = body.email?.trim(); - this._oauthService.validateSelfHostedRequest(app, { - resource: body.resource, - email, - }); - - const instance = await this._mcpRelayService.connect( - body.instance_url, - body.api_key - ); - - const code = await this._oauthService.createSelfHostedAuthorizationCode( - app.id, - { ...instance, email }, - app.dynamic - ? { - codeChallenge: body.code_challenge, - codeChallengeMethod: body.code_challenge_method, - redirectUri: body.redirect_uri, - } - : undefined - ); - - // Same redirect as an approved cloud authorization - const redirectUrl = new URL( - app.dynamic ? body.redirect_uri! : app.redirectUrl - ); - redirectUrl.searchParams.set('code', code); - if (body.state) { - redirectUrl.searchParams.set('state', body.state); - } - return { redirect: redirectUrl.toString() }; - } + // since every attempt sends requests to the instance. Moved to + // OAuthSelfHostedController (routes/oauth.selfhosted.controller.ts) so this + // file no longer pulls McpRelayService/@mastra into the consent test graph. @Post('/token') // RFC 6749 §5.1: successful token responses are 200; strict clients (Canva) reject Nest's default 201 diff --git a/apps/backend/src/api/routes/oauth.selfhosted.controller.ts b/apps/backend/src/api/routes/oauth.selfhosted.controller.ts new file mode 100644 index 0000000000..0e1c68d4f5 --- /dev/null +++ b/apps/backend/src/api/routes/oauth.selfhosted.controller.ts @@ -0,0 +1,68 @@ +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Throttle } from '@nestjs/throttler'; +import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service'; +import { AuthorizeSelfHostedDto } from '@gitroom/nestjs-libraries/dtos/oauth/authorize-oauth.dto'; +import { McpRelayService } from '@gitroom/nestjs-libraries/chat/mcp.relay.service'; +import { ThrottlerRealIpGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider'; + +// Split out of OAuthController so the bootstrap/consent test suite (which +// imports OAuthAuthorizedController) does not pull McpRelayService - and +// through it @mastra/core and ESM-only dependencies - into its module graph. +@ApiTags('OAuth') +@Controller('/oauth') +export class OAuthSelfHostedController { + constructor( + private _oauthService: OAuthService, + private _mcpRelayService: McpRelayService + ) {} + + // Public (the person may have no account here) and capped per client, + // since every attempt sends requests to the instance + @UseGuards(ThrottlerRealIpGuard) + @Throttle({ default: { limit: 30, ttl: 3600000 } }) + @Post('/authorize/self-hosted') + async authorizeSelfHosted(@Body() body: AuthorizeSelfHostedDto) { + const app = await this._oauthService.validateAuthorizationRequest( + body.client_id, + { + redirectUri: body.redirect_uri, + codeChallenge: body.code_challenge, + codeChallengeMethod: body.code_challenge_method, + } + ); + + const email = body.email?.trim(); + this._oauthService.validateSelfHostedRequest(app, { + resource: body.resource, + email, + }); + + const instance = await this._mcpRelayService.connect( + body.instance_url, + body.api_key + ); + + const code = await this._oauthService.createSelfHostedAuthorizationCode( + app.id, + { ...instance, email }, + app.dynamic + ? { + codeChallenge: body.code_challenge, + codeChallengeMethod: body.code_challenge_method, + redirectUri: body.redirect_uri, + } + : undefined + ); + + // Same redirect as an approved cloud authorization + const redirectUrl = new URL( + app.dynamic ? body.redirect_uri! : app.redirectUrl + ); + redirectUrl.searchParams.set('code', code); + if (body.state) { + redirectUrl.searchParams.set('state', body.state); + } + return { redirect: redirectUrl.toString() }; + } +}