- Postiz: An alternative to: Buffer.com, Hypefury, Twitter Hunter, etc...
+ Postiz offers everything you need to manage your social media posts, build an audience, capture leads, and grow your business.
- Postiz offers everything you need to manage your social media posts, build an audience, capture leads, and grow your business.
+
## 🔌 See the leading Postiz features
From 5ff9e0b2f6f71639ea7bc52fb6d50804d9084a7b Mon Sep 17 00:00:00 2001
From: Nevo David
Date: Wed, 23 Sep 2026 00:01:18 +0700
Subject: [PATCH 24/45] feat: update readme
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 0d4ddb5ff0..9fe3c39ee8 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,8 @@
Make.com integration
+
+
Schedule posts to:
From 305f7c0d13c84aca78f98b5b4eb2ce34607b4369 Mon Sep 17 00:00:00 2001
From: Nevo David <100117126+nevo-david@users.noreply.github.com>
Date: Wed, 23 Sep 2026 10:10:13 +0700
Subject: [PATCH 25/45] Enhance README with Postiz Cloud vs Open-source section
Added a comparison between Postiz Cloud and Open-source versions, detailing features, costs, and setup differences.
---
README.md | 42 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 40 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 9fe3c39ee8..5924b50d00 100644
--- a/README.md
+++ b/README.md
@@ -133,15 +133,53 @@ To have the project up and running, please follow the [Quick Start Guide](https:
## Sponsor Postiz
-We now give a few options to Sponsor Postiz:
+We now offer a few options to sponsor Postiz:
- Just a donation: You like what we are building, and want to buy us some coffee so we can build faster.
- Main repository: Get your logo with a backlink from the main Postiz repository. Postiz has over 7M downloads and 20k views per month.
Link: https://opencollective.com/postiz
+
+
+
+## Postiz Cloud vs. Open-source
+
+[Postiz Cloud](https://postiz.com/) and Postiz self-hosted are identical.
+
+We do not "gate" features or limit the license.
+
+The main difference is the infrastructure you need to own, approval from social media providers, and deployment that might be hard at times (let your LLM deploy it)
+
+| Area | Postiz Cloud | Postiz Open-source (self-hosted) |
+|---|---|---|
+| **Cost** | Subscription per plan, 7-day free trial | Free forever (AGPL-3.0); you pay only for your own infra |
+| **Setup time** | Sign up and connect channels in minutes | Deploy with Docker / Coolify / Railway / any VPS; you configure Postgres, Redis, storage and env vars |
+| **Hosting & data** | Hosted by Postiz; data stored in our infrastructure | Runs on your own server; data never leaves your environment |
+| **Social platform apps** | Pre-approved apps for every channel, ready to use | You create your own developer apps on each platform and go through their approval (Meta, YouTube, TikTok can take weeks) |
+| **Channels** | Limited by plan tier | Unlimited, every supported provider |
+| **Posts per month** | Limited by plan tier | Unlimited |
+| **Team members** | Limited by plan tier | Unlimited |
+| **Scheduling, calendar views, cross-posting, repeated posts, post comments & delays, sets, signatures** | Included | Included |
+| **Internal & Global Plugs, RSS auto-post, customer groups** | Included per plan | Included |
+| **Analytics** | Included per plan | Included (requires your own app credentials with analytics scopes) |
+| **AI Copilot, AI images, AI videos** | Included with monthly quotas per plan; keys managed by Postiz | Available if you bring your own OpenAI (and other provider) API keys; no quota, you pay the provider |
+| **AI video clipping** | Included with monthly clipping minutes per plan | Requires your own provider keys and extra configuration |
+| **Smart Agent** | Included per plan | Available with your own LLM key |
+| **Public API & webhooks** | Included per plan | Included |
+| **Agentic surfaces (MCP, CLI, Claude / ChatGPT / Codex / OpenClaw / Cursor connectors)** | Included, hosted MCP endpoint | Included, you point the MCP / CLI at your own instance |
+| **Custom integrations** | Included per plan | Included; you can also modify the code and add providers |
+| **Updates & maintenance** | Automatic, zero downtime for you | You pull new images and run migrations yourself |
+| **Uptime, backups, security patches** | Managed by Postiz | Your responsibility |
+| **Support** | Priority support via Discord / email per plan | Community support on Discord and GitHub |
+| **Source access & customization** | No (SaaS) | Full source code, fork and modify freely under AGPL |
+| **Compliance / data residency** | Postiz-controlled regions | Any region or air-gapped environment you choose |
+
+
+
+
## Postiz Compliance
-- Postiz is an open-source, self-hosted social media scheduling tool that supports platforms like X (formerly Twitter), Bluesky, Mastodon, Discord, and others.
+- Postiz on GitHub is an open-source, self-hosted social media scheduling tool that supports platforms like X (formerly Twitter), Bluesky, Mastodon, Discord, and others.
- Postiz hosted service uses official, platform-approved OAuth flows.
- Postiz does not automate or scrape content from social media platforms.
- Postiz does not collect, store, or proxy API keys or access tokens from users.
From 38cb6a41c7c0428e3e1fa8c57c8557122c091474 Mon Sep 17 00:00:00 2001
From: Nevo David <100117126+nevo-david@users.noreply.github.com>
Date: Wed, 23 Sep 2026 10:14:08 +0700
Subject: [PATCH 26/45] Update Postiz compliance information in README
Clarify the description of the Postiz project and its compliance.
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 5924b50d00..ecd109802e 100644
--- a/README.md
+++ b/README.md
@@ -179,6 +179,7 @@ The main difference is the infrastructure you need to own, approval from social
## Postiz Compliance
+- This GitHub repository contains the open-source, self-hosted edition of Postiz. Postiz is also available as Postiz Cloud, a fully managed service at postiz.com.
- Postiz on GitHub is an open-source, self-hosted social media scheduling tool that supports platforms like X (formerly Twitter), Bluesky, Mastodon, Discord, and others.
- Postiz hosted service uses official, platform-approved OAuth flows.
- Postiz does not automate or scrape content from social media platforms.
From b90fc691c1ee6e6292ad9017db1c7210802e3554 Mon Sep 17 00:00:00 2001
From: Nevo David <100117126+nevo-david@users.noreply.github.com>
Date: Wed, 23 Sep 2026 10:20:20 +0700
Subject: [PATCH 27/45] Revise README for clarity on Postiz offerings
Updated the README to clarify the differences between Postiz Cloud and the self-hosted version, emphasizing the managed experience and deployment options.
---
README.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index ecd109802e..54a9d9d9d9 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,9 @@
- Postiz offers everything you need to manage your social media posts, build an audience, capture leads, and grow your business.
+ Postiz is a social media management platform for scheduling, automating, and analyzing your content.
+
+ Use Postiz Cloud for a fully managed experience, or deploy the open-source edition on your own infrastructure.
@@ -115,7 +117,6 @@
- Measure your work with analytics.
- Collaborate with other team members to exchange or buy posts.
- Invite your team members to collaborate, comment, and schedule posts.
-- At the moment, there is no difference between the hosted version and the self-hosted version
- Perfect for automation (API) with platforms like N8N, Make.com, Zapier, etc.
## Tech Stack
@@ -144,7 +145,7 @@ Link: https://opencollective.com/postiz
## Postiz Cloud vs. Open-source
-[Postiz Cloud](https://postiz.com/) and Postiz self-hosted are identical.
+Choose [Postiz Cloud](https://postiz.com/) for a fully managed experience, or deploy Postiz Open-source on your own infrastructure. Both provide the same core Postiz product and features.
We do not "gate" features or limit the license.
@@ -180,7 +181,6 @@ The main difference is the infrastructure you need to own, approval from social
## Postiz Compliance
- This GitHub repository contains the open-source, self-hosted edition of Postiz. Postiz is also available as Postiz Cloud, a fully managed service at postiz.com.
-- Postiz on GitHub is an open-source, self-hosted social media scheduling tool that supports platforms like X (formerly Twitter), Bluesky, Mastodon, Discord, and others.
- Postiz hosted service uses official, platform-approved OAuth flows.
- Postiz does not automate or scrape content from social media platforms.
- Postiz does not collect, store, or proxy API keys or access tokens from users.
From fa93effac5b03b815c7dec8abdac9ca0b199be88 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 23 Sep 2026 11:12:25 +0700
Subject: [PATCH 28/45] fix(frontend): stop the Sentry report dialog on the
posthog-js recorder error
posthog-js throws "Called on script loaded before session recording is
available" when its session-recording script's load event fires before the
recorder extension is registered. The frontend beforeSend hook shows the
Sentry user-report dialog for every captured exception, so users hit the
"Something broke!" form on ordinary page loads (login, launches, channel
connect) with nothing actually wrong. Sentry issue CLOUD-QP: 656 events,
91 users since March 2026; three user-feedback reports in one week.
Add the message to ignorePatterns so the event is dropped before the
dialog branch, matching the existing network-error entries.
Co-Authored-By: Claude Fable 5.1
---
.../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 fecf778ecb..b3f067d7f0 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
@@ -13,6 +13,7 @@ export const initializeSentryBasic = (environment: string, dsn: string, extensio
/^NetworkError when attempting to fetch resource\.$/i,
/^NetworkError when attempting to fetch resource\. .*/i,
/^Object captured as promise rejection with keys: code, message$/i,
+ /^Called on script loaded before session recording is available$/i,
];
// Browser wallet extensions (Phantom, MetaMask, etc.) reject with a plain
From 28730a0c1b2d49e857c61a975711af730b2c0bd0 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 18:11:14 +0700
Subject: [PATCH 29/45] fix(instagram): disconnect the channel on Meta
checkpoint errors instead of failing every post
Code-190 bodies "You cannot access the app till you log in to www.instagram.com" and "Session key is malformed" now map to disconnect, so the channel is flagged for reconnect once instead of every scheduled post failing with a misleading permissions message.
---
.../integrations/social/instagram.provider.ts | 16 +++++++++++++++-
.../social/instagram.standalone.provider.ts | 5 ++++-
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
index b9eb4a2e99..8800a5ce99 100644
--- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
@@ -103,7 +103,7 @@ export class InstagramProvider
status: number
):
| {
- type: 'refresh-token' | 'bad-body' | 'retry';
+ type: 'refresh-token' | 'bad-body' | 'retry' | 'disconnect';
value: string;
}
| undefined {
@@ -330,6 +330,20 @@ export class InstagramProvider
};
}
+ // Meta put the account behind a checkpoint: the token is still valid, so a
+ // refresh cannot help and every post fails until the user logs in on
+ // Instagram and re-connects the channel.
+ if (
+ body.indexOf('You cannot access the app till you log in to') > -1 ||
+ body.indexOf('Session key is malformed') > -1
+ ) {
+ return {
+ type: 'disconnect' as const,
+ value:
+ 'Instagram requires you to log in at instagram.com and follow its instructions before posting can resume. After that, please reconnect this channel.',
+ };
+ }
+
if (body.indexOf('190,') > -1) {
return {
type: 'bad-body' as const,
diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
index 4c3261fbd3..db023cdf58 100644
--- a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
@@ -68,7 +68,10 @@ export class InstagramStandaloneProvider
body: string,
status: number
):
- | { type: 'refresh-token' | 'bad-body' | 'retry'; value: string }
+ | {
+ type: 'refresh-token' | 'bad-body' | 'retry' | 'disconnect';
+ value: string;
+ }
| undefined {
return instagramProvider.handleErrors(body, status);
}
From 5536d8a73a9cd66d6e9b6eb72e1d259dd2800aaa Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 15:13:09 +0700
Subject: [PATCH 30/45] fix(threads): retry publish when Threads has not found
the container yet (4279009)
Threads sometimes answers threads_publish with "Media Not Found" seconds after
the container was created. Map subcode 4279009 to retry so the publish call
gets a few more attempts instead of failing the post with "Unknown Error".
---
.../src/integrations/social/threads.provider.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts
index 4538ea239e..cac4d40bc4 100644
--- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts
@@ -40,7 +40,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
override handleErrors(body: string):
| {
- type: 'refresh-token' | 'bad-body';
+ type: 'refresh-token' | 'bad-body' | 'retry';
value: string;
}
| undefined {
@@ -71,6 +71,13 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
"One of the media URLs is invalid or inaccessible, make sure it's being uploaded to Postiz first",
};
}
+ if (body.includes('4279009')) {
+ return {
+ type: 'retry',
+ value:
+ 'Threads could not find the media container yet, please try again in a few seconds',
+ };
+ }
if (body.includes('text must be at most 500 characters')) {
return {
type: 'bad-body',
From 002a341bcce2d4888fabd1cc99018f5382b1a877 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 15:10:47 +0700
Subject: [PATCH 31/45] fix(pinterest): make the numeric board id mapping
actually match
The match string carried 8 literal backslashes while Pinterest's response
carries 4, so the mapping never fired and posts failed with "Unknown Error".
Match on backslash-free fragments of the message instead.
---
.../src/integrations/social/pinterest.provider.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
index 7f6354b280..40251c71d2 100644
--- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
@@ -128,7 +128,10 @@ export class PinterestProvider
'Pinterest was unable to reach the URL provided. Please check the link and try again.',
};
}
- if (body.indexOf(`does not match '^\\\\\\\\\\\\\\\\d+$'`) > -1) {
+ if (
+ body.indexOf("does not match '^") > -1 &&
+ body.indexOf("d+$'") > -1
+ ) {
return {
type: 'bad-body' as const,
value:
From 6fb5df417eb87efbd0f0c3e9a6ab54f648e319a9 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 23 Sep 2026 14:41:10 +0700
Subject: [PATCH 32/45] fix(reddit): retry the submit after a RATELIMIT
rejection instead of failing
Reddit answers a rate-limited /api/submit with HTTP 200 and an errors
array. finalizePost treated every entry as a terminal BadBody, so a
temporary rate limit failed the whole post.
A RATELIMIT entry means nothing was submitted, so finalizePost now
clears the armed marker and returns pending: the next status check
re-arms the same subreddit and submits it again once the window has
passed. Every other rejection still throws the non-retryable BadBody.
Verified against real Reddit: with the rate-limit payload injected into
the first submit only, the following check re-armed and the resubmit
published; a non-RATELIMIT rejection still throws BadBody.
Co-Authored-By: Claude Fable 5.1
---
.../src/integrations/social/reddit.provider.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
index 201f00769e..a1a6c219d5 100644
--- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
@@ -520,6 +520,14 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
// Reddit rejects submissions with a 200 and an errors array: surface the
// real reason instead of failing later with an unknown outcome.
if (all?.json?.errors?.length) {
+ // A rate limit is a refusal, nothing was submitted: disarm the marker so
+ // the next check re-arms this subreddit and submits it again once the
+ // window has passed, instead of failing the whole post.
+ if (all.json.errors.some((e: any[]) => e?.[0] === 'RATELIMIT')) {
+ data.armed = undefined;
+ return { status: 'pending', pendingData: data };
+ }
+
throw new BadBody(
this.identifier,
JSON.stringify(all),
From 70499563d200fef9394a6e6f9f81f64107319557 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 15:00:24 +0700
Subject: [PATCH 33/45] fix(facebook): map five recurring Graph API rejections
instead of "Unknown Error"
Adds handleErrors branches for 190/459 (checkpoint), 190/492 (no Page role),
the 190 missing pages_* permissions body, 100/33 object does not exist, and
Facebook's HTML outage page, so users see the reason and the right outcome
(fail, reconnect, or retry) instead of "Unknown Error".
---
.../integrations/social/facebook.provider.ts | 39 ++++++++++++++++++-
1 file changed, 38 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 fff046d021..fa911f328c 100644
--- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts
@@ -64,7 +64,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
status: number
):
| {
- type: 'refresh-token' | 'bad-body';
+ type: 'refresh-token' | 'bad-body' | 'retry';
value: string;
}
| undefined {
@@ -229,6 +229,43 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
value: 'Facebook return: No permission to publish the video',
};
}
+ if (body.indexOf('"error_subcode":459') > -1) {
+ return {
+ type: 'bad-body' as const,
+ value:
+ 'Facebook is asking you to resolve a security check. Log in at facebook.com, complete it, then try again',
+ };
+ }
+ if (body.indexOf('"error_subcode":492') > -1) {
+ return {
+ type: 'bad-body' as const,
+ value:
+ 'Your Facebook user no longer has a role on this Page. Ask a Page admin to grant you a role, then reconnect the channel',
+ };
+ }
+ if (body.indexOf('must be granted before impersonating') > -1) {
+ return {
+ type: 'refresh-token' as const,
+ value:
+ 'Facebook Page permissions are missing, please reconnect the channel and allow all permissions',
+ };
+ }
+ if (
+ body.indexOf('"error_subcode":33') > -1 &&
+ body.indexOf('does not exist') > -1
+ ) {
+ return {
+ type: 'bad-body' as const,
+ value:
+ 'The Facebook Page or post this was targeting no longer exists, please reconnect the channel and schedule again',
+ };
+ }
+ if (body.indexOf('Sorry, something went wrong') > -1) {
+ return {
+ type: 'retry' as const,
+ value: 'Facebook is temporarily unavailable, please try again later',
+ };
+ }
if (body.indexOf('490') > -1) {
return {
type: 'refresh-token' as const,
From 8b5999b8d1c4bbbf0047bdab3775a03ed493496a Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 15:15:35 +0700
Subject: [PATCH 34/45] fix(facebook): match error subcodes on a word boundary
"error_subcode":33 as a substring also matched 330 and 331; the same held for
459 and 492. Test the three subcodes with a trailing word boundary instead.
---
.../src/integrations/social/facebook.provider.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts
index fa911f328c..33c69599b7 100644
--- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts
@@ -229,14 +229,14 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
value: 'Facebook return: No permission to publish the video',
};
}
- if (body.indexOf('"error_subcode":459') > -1) {
+ if (/"error_subcode":459\b/.test(body)) {
return {
type: 'bad-body' as const,
value:
'Facebook is asking you to resolve a security check. Log in at facebook.com, complete it, then try again',
};
}
- if (body.indexOf('"error_subcode":492') > -1) {
+ if (/"error_subcode":492\b/.test(body)) {
return {
type: 'bad-body' as const,
value:
@@ -251,7 +251,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
};
}
if (
- body.indexOf('"error_subcode":33') > -1 &&
+ /"error_subcode":33\b/.test(body) &&
body.indexOf('does not exist') > -1
) {
return {
From 9fbaf550633b69a48f6800ec0c7c2ae19c8a4050 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 23 Sep 2026 14:52:24 +0700
Subject: [PATCH 35/45] fix(reddit): only retry when RATELIMIT is the sole
submit error
Reddit can return several error entries at once. A terminal error next
to RATELIMIT would reject every resubmit, so retrying burns the whole
pending budget before the user sees the real reason. Retry only when
every entry is RATELIMIT.
Co-Authored-By: Claude Fable 5.1
---
.../nestjs-libraries/src/integrations/social/reddit.provider.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
index a1a6c219d5..6c2ea6fd58 100644
--- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
@@ -523,7 +523,7 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
// A rate limit is a refusal, nothing was submitted: disarm the marker so
// the next check re-arms this subreddit and submits it again once the
// window has passed, instead of failing the whole post.
- if (all.json.errors.some((e: any[]) => e?.[0] === 'RATELIMIT')) {
+ if (all.json.errors.every((e: any[]) => e?.[0] === 'RATELIMIT')) {
data.armed = undefined;
return { status: 'pending', pendingData: data };
}
From cd899eaad67188557d5ef528b92b658044bfe5ba Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 17:04:50 +0700
Subject: [PATCH 36/45] fix(pinterest): validate board as a numeric id and map
the board-name rejection
Reject a board name in the settings DTO at scheduling time and fix the over-escaped handleErrors match so Pinterest's board-id rejection is curated instead of surfacing as Unknown Error.
---
.../chat/tools/integration.schedule.post.ts | 2 +-
.../src/chat/tools/post.settings.tool.ts | 2 +-
.../posts/providers-settings/pinterest.dto.ts | 18 +++++++++++++++---
3 files changed, 17 insertions(+), 5 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 39d11cbd7d..d7b670b2c8 100644
--- a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts
+++ b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts
@@ -110,7 +110,7 @@ If validation fails, the result contains output.errors describing what to fix; t
value: z
.any()
.describe(
- 'Value of the key, always prefer the id then label if possible'
+ 'Value of the key, always prefer the id then label if possible. When the settings schema says a field is an id, pass the id returned by the channel tools, never the display label'
),
})
)
diff --git a/libraries/nestjs-libraries/src/chat/tools/post.settings.tool.ts b/libraries/nestjs-libraries/src/chat/tools/post.settings.tool.ts
index e3812b2203..99a95d3cb5 100644
--- a/libraries/nestjs-libraries/src/chat/tools/post.settings.tool.ts
+++ b/libraries/nestjs-libraries/src/chat/tools/post.settings.tool.ts
@@ -41,7 +41,7 @@ If validation fails, the result contains output.errors describing what to fix; t
value: z
.any()
.describe(
- 'New value of the key, always prefer the id then label if possible'
+ 'New value of the key, always prefer the id then label if possible. When the settings schema says a field is an id, pass the id returned by the channel tools, never the display label'
),
})
)
diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/pinterest.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/pinterest.dto.ts
index aed79c8055..81779dfec9 100644
--- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/pinterest.dto.ts
+++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/pinterest.dto.ts
@@ -1,5 +1,12 @@
import {
- IsDefined, IsOptional, IsString, IsUrl, MaxLength, MinLength, ValidateIf
+ IsDefined,
+ IsOptional,
+ IsString,
+ IsUrl,
+ Matches,
+ MaxLength,
+ MinLength,
+ ValidateIf,
} from 'class-validator';
import { JSONSchema } from 'class-validator-jsonschema';
@@ -27,8 +34,13 @@ export class PinterestSettingsDto {
@MinLength(1, {
message: 'Board is required',
})
- @JSONSchema({
- description: 'board must be an id',
+ @Matches(/^\d+$/, {
+ message:
+ 'Board must be the numeric board id (use the boards list of the channel to find it), not the board name',
+ })
+ @JSONSchema({
+ description:
+ 'The numeric id of the board (from the boards list of the channel), not the board name',
})
board: string;
}
From 0a2fc9607238b16eba432cb5132067aea73ccb57 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 13:34:51 +0700
Subject: [PATCH 37/45] fix(lemmy): surface Lemmy API errors instead of
crashing on post_view
Route post, comment and login through this.fetch with a handleErrors mapping so Lemmy rejections fail once with a readable message (rate limits retry, bad credentials flag the channel); the login failure is rebuilt without the request body so the stored password is never persisted.
---
.../src/integrations/social/lemmy.provider.ts | 129 ++++++++++++++----
1 file changed, 103 insertions(+), 26 deletions(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts b/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts
index 94bf8e085d..9f25a86e45 100644
--- a/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts
@@ -6,6 +6,8 @@ import {
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeSecureId } from '@gitroom/nestjs-libraries/services/make.secure.id';
import {
+ BadBody,
+ RefreshToken,
SocialAbstract,
ValidityMedia,
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
@@ -28,6 +30,65 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
}
dto = LemmySettingsDto;
+ override handleErrors(
+ body: string,
+ status: number
+ ):
+ | { type: 'refresh-token' | 'bad-body' | 'retry'; value: string }
+ | undefined {
+ if (body.includes('rate_limit_error')) {
+ return {
+ type: 'retry',
+ value: 'Lemmy rate limit reached, please try again later',
+ };
+ }
+
+ if (body.includes('not_logged_in') || body.includes('incorrect_login')) {
+ return {
+ type: 'refresh-token',
+ value: 'Lemmy session is no longer valid, please reconnect the channel',
+ };
+ }
+
+ if (body.includes('site_ban') || body.includes('"error":"banned"')) {
+ return {
+ type: 'bad-body',
+ value: 'This account is banned on the Lemmy instance',
+ };
+ }
+
+ if (body.includes('couldnt_find_community')) {
+ return {
+ type: 'bad-body',
+ value:
+ 'The selected Lemmy community no longer exists, please pick another one',
+ };
+ }
+
+ if (body.includes('blocked_url')) {
+ return {
+ type: 'bad-body',
+ value: 'The Lemmy instance blocks the URL in this post',
+ };
+ }
+
+ if (body.includes('"error":"deleted"')) {
+ return {
+ type: 'bad-body',
+ value: 'The selected Lemmy community or post was deleted',
+ };
+ }
+
+ if (body.includes('"error":"locked"')) {
+ return {
+ type: 'bad-body',
+ value: 'This Lemmy post is locked, comments cannot be added',
+ };
+ }
+
+ return undefined;
+ }
+
override async checkValidity(
items: Array
): Promise {
@@ -149,20 +210,46 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
AuthService.fixedDecryption(integration.customInstanceDetails!)
);
- const { jwt } = await (
- await fetch(body.service + '/api/v3/user/login', {
- // @ts-ignore - undici-only option; blocks SSRF to internal IPs
- dispatcher: getSsrfSafeDispatcher(),
- body: JSON.stringify({
- username_or_email: body.identifier,
- password: body.password,
- }),
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- })
- ).json();
+ const options = {
+ // @ts-ignore - undici-only option; blocks SSRF to internal IPs
+ dispatcher: getSsrfSafeDispatcher(),
+ body: JSON.stringify({
+ username_or_email: body.identifier,
+ password: body.password,
+ }),
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ let login: Response;
+ try {
+ login = await this.fetch(body.service + '/api/v3/user/login', options);
+ } catch (err) {
+ // The request body holds the stored password, so the failure is rebuilt
+ // without it before it reaches the Temporal history and the Errors table.
+ const json = (err as any).details?.[0]?.json || '{}';
+ if (err instanceof BadBody) {
+ throw new BadBody(
+ this.identifier,
+ json,
+ {} as BodyInit,
+ err.message || 'Unknown Error'
+ );
+ }
+ if (err instanceof RefreshToken) {
+ throw new RefreshToken(
+ this.identifier,
+ json,
+ {} as BodyInit,
+ err.message || 'Unknown Error'
+ );
+ }
+ throw err;
+ }
+
+ const { jwt } = await login.json();
return { jwt, service: body.service };
}
@@ -179,18 +266,8 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
const valueArray: PostResponse[] = [];
for (const lemmy of firstPost.settings.subreddit) {
- console.log({
- community_id: +lemmy.value.id,
- name: lemmy.value.title,
- body: firstPost.message,
- ...(lemmy.value.url ? { url: lemmy.value.url } : {}),
- ...(firstPost.media?.length
- ? { custom_thumbnail: firstPost.media[0].path }
- : {}),
- nsfw: false,
- });
const { post_view } = await (
- await fetch(service + '/api/v3/post', {
+ await this.fetch(service + '/api/v3/post', {
// @ts-ignore - undici-only option; blocks SSRF to internal IPs
dispatcher: getSsrfSafeDispatcher(),
body: JSON.stringify({
@@ -253,7 +330,7 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
for (const singlePostId of postIds) {
const { comment_view } = await (
- await fetch(service + '/api/v3/comment', {
+ await this.fetch(service + '/api/v3/comment', {
// @ts-ignore - undici-only option; blocks SSRF to internal IPs
dispatcher: getSsrfSafeDispatcher(),
body: JSON.stringify({
From 1ae4a3b78e0ab5860e82f6cc45279e8f4eda9f81 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 10:10:27 +0700
Subject: [PATCH 38/45] fix(x): readable message when Too Many Requests retries
are exhausted
---
.../nestjs-libraries/src/integrations/social/x.provider.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
index 51512fbdf2..3dad170402 100644
--- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
@@ -161,6 +161,12 @@ export class XProvider extends SocialAbstract implements SocialProvider {
value: 'X is currently unavailable, please try again later',
};
}
+ if (body.includes('Too Many Requests')) {
+ return {
+ type: 'retry',
+ value: 'X rate limit reached, please try again later',
+ };
+ }
if (body.includes('maximum of one cashtag')) {
return {
type: 'bad-body',
From 96bca725519587b145502838ab6d9b3a2fb973c8 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Tue, 8 Sep 2026 12:20:41 +0700
Subject: [PATCH 39/45] fix(vk): surface VK API errors instead of marking posts
completed
VK answers HTTP 200 with { error } on failures, so wall.post/wall.createComment rejections were stored as completed with postId undefined. Map code 5 to RefreshToken, 6/9/29 to a retryable error, everything else to BadBody with VK's error_msg.
---
.../src/integrations/social/vk.provider.ts | 31 +++++++++++++++++--
1 file changed, 28 insertions(+), 3 deletions(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/vk.provider.ts b/libraries/nestjs-libraries/src/integrations/social/vk.provider.ts
index 3ef8c950fc..f9db95efff 100644
--- a/libraries/nestjs-libraries/src/integrations/social/vk.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/vk.provider.ts
@@ -6,7 +6,11 @@ import {
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeSecureId } from '@gitroom/nestjs-libraries/services/make.secure.id';
import dayjs from 'dayjs';
-import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
+import {
+ BadBody,
+ RefreshToken,
+ SocialAbstract,
+} from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { createHash, randomBytes } from 'crypto';
import FormDataNew from 'form-data';
import mime from 'mime-types';
@@ -229,6 +233,23 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
);
}
+ // VK answers HTTP 200 with { error } instead of { response } on failures,
+ // so this.fetch never sees them and the post used to be marked completed.
+ private checkApiError(all: any) {
+ if (!all?.error) {
+ return;
+ }
+ const json = JSON.stringify(all);
+ const message = all.error.error_msg || 'VK rejected the request';
+ if (all.error.error_code === 5) {
+ throw new RefreshToken(this.identifier, json, Buffer.from('{}'), message);
+ }
+ if ([6, 9, 29].includes(all.error.error_code)) {
+ throw new Error(message);
+ }
+ throw new BadBody(this.identifier, json, Buffer.from('{}'), message);
+ }
+
async post(
userId: string,
accessToken: string,
@@ -249,7 +270,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
);
}
- const { response } = await (
+ const all = await (
await this.fetch(
`https://api.vk.com/method/wall.post?v=5.251&access_token=${accessToken}&client_id=${process.env.VK_ID}`,
{
@@ -258,6 +279,8 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
}
)
).json();
+ this.checkApiError(all);
+ const { response } = all;
return [
{
@@ -293,7 +316,7 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
);
}
- const { response } = await (
+ const all = await (
await this.fetch(
`https://api.vk.com/method/wall.createComment?v=5.251&access_token=${accessToken}&client_id=${process.env.VK_ID}`,
{
@@ -302,6 +325,8 @@ export class VkProvider extends SocialAbstract implements SocialProvider {
}
)
).json();
+ this.checkApiError(all);
+ const { response } = all;
return [
{
From 935c80a8e1aa7819ada0b9b293ad6ac93a3610ee Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 10:10:27 +0700
Subject: [PATCH 40/45] fix(x): mark the channel for reconnect on Unauthorized
media upload errors
twitter-api-v2 401s reach handleErrors via runInConcurrent with status
200, so the generic 401 rule never fired and the post failed as Unknown
Error without flagging the channel.
---
.../nestjs-libraries/src/integrations/social/x.provider.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
index 51512fbdf2..834a832670 100644
--- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
@@ -232,6 +232,12 @@ export class XProvider extends SocialAbstract implements SocialProvider {
'The video you are trying to post is longer than 2 minutes, which is not allowed for this account',
};
}
+ if (body.includes('"title":"Unauthorized"')) {
+ return {
+ type: 'refresh-token',
+ value: 'X rejected the connected account, please reconnect your account',
+ };
+ }
return undefined;
}
From e475160bc3328fe48c9004007741e2911c491725 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 23 Sep 2026 15:27:16 +0700
Subject: [PATCH 41/45] fix(lemmy): route community search through this.fetch
A non-2xx from /api/v3/search threw a TypeError on communities.map; it now fails through the same handleErrors mapping as post and comment.
---
.../nestjs-libraries/src/integrations/social/lemmy.provider.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts b/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts
index 9f25a86e45..726559491b 100644
--- a/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/lemmy.provider.ts
@@ -382,7 +382,7 @@ export class LemmyProvider extends SocialAbstract implements SocialProvider {
const { jwt, service } = await this.getJwtAndService(integration);
const { communities } = await (
- await fetch(
+ await this.fetch(
service + `/api/v3/search?type_=Communities&sort=Active&q=${data.word}`,
{
// @ts-ignore - undici-only option; blocks SSRF to internal IPs
From f477777ee0087de6ff563e909ae912f1c2ad3ca8 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Wed, 9 Sep 2026 10:10:27 +0700
Subject: [PATCH 42/45] fix(x): map remaining known error responses to readable
messages
Locked account, crypto addresses, invalid media ids, 10-minute video,
Premium-only articles and empty tweet now map to bad-body with a
message instead of Unknown Error.
---
.../src/integrations/social/x.provider.ts | 45 +++++++++++++++++++
1 file changed, 45 insertions(+)
diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
index 51512fbdf2..baff596fe3 100644
--- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
@@ -232,6 +232,51 @@ export class XProvider extends SocialAbstract implements SocialProvider {
'The video you are trying to post is longer than 2 minutes, which is not allowed for this account',
};
}
+ if (
+ body.includes(
+ 'This user is not allowed to post a video longer than 10 minutes'
+ )
+ ) {
+ return {
+ type: 'bad-body',
+ value:
+ 'The video you are trying to post is longer than 10 minutes, which is not allowed for this account',
+ };
+ }
+ if (body.includes('Your account is temporarily locked')) {
+ return {
+ type: 'bad-body',
+ value:
+ 'Your X account is temporarily locked, log in to x.com to unlock it and then try again',
+ };
+ }
+ if (body.includes('Crypto addresses are prohibited')) {
+ return {
+ type: 'bad-body',
+ value:
+ 'X does not allow crypto addresses in posts for the first 7 days after connecting the account',
+ };
+ }
+ if (body.includes('Your media IDs are invalid')) {
+ return {
+ type: 'bad-body',
+ value:
+ 'X rejected the attached media, please re-upload the media and try again',
+ };
+ }
+ if (body.includes('not authorized to create or publish articles')) {
+ return {
+ type: 'bad-body',
+ value: 'Publishing articles on X requires an X Premium subscription',
+ };
+ }
+ if (body.includes('Please include either text or media in your Tweet')) {
+ return {
+ type: 'bad-body',
+ value:
+ 'One of the posts in this thread has no text or media, please add some text or remove it',
+ };
+ }
return undefined;
}
From a63b28bf371dcf687e6cfef3ebe07534bff1e8e5 Mon Sep 17 00:00:00 2001
From: Gilad Resisi
Date: Mon, 7 Sep 2026 18:24:33 +0700
Subject: [PATCH 43/45] fix(dribbble): map 4xx shot rejections to non-retryable
BadBody
Dribbble 4xx responses on shot creation were plain AxiosErrors: the
workflow retried them to exhaustion and recorded "Could not publish
after several attempts", and the response body with Dribbble's actual
rejection reason was discarded. Map 4xx (except 429) to BadBody with
the response body persisted in the failure details.
Co-Authored-By: Claude Fable 5
Claude-Session: https://claude.ai/code/session_0188yxtbWi6wtfJg7ydjUpfG
---
.../integrations/social/dribbble.provider.ts | 34 ++++++++++++++-----
1 file changed, 25 insertions(+), 9 deletions(-)
diff --git a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts
index 08f26e0930..dc325e09b6 100644
--- a/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/dribbble.provider.ts
@@ -8,6 +8,7 @@ import {
import { makeSecureId } from '@gitroom/nestjs-libraries/services/make.secure.id';
import FormData from 'form-data';
import {
+ BadBody,
SocialAbstract,
ValidityMedia,
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
@@ -180,16 +181,31 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider {
formData.append('title', postDetails[0].settings.title);
formData.append('description', postDetails[0].message);
- const data2 = await this.getSsrfSafeAxios().post(
- 'https://api.dribbble.com/v2/shots',
- formData,
- {
- headers: {
- ...formData.getHeaders(),
- Authorization: `Bearer ${accessToken}`,
- },
+ let data2;
+ try {
+ data2 = await this.getSsrfSafeAxios().post(
+ 'https://api.dribbble.com/v2/shots',
+ formData,
+ {
+ headers: {
+ ...formData.getHeaders(),
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ );
+ } catch (err: any) {
+ const status = err?.response?.status;
+ if (status >= 400 && status < 500 && status !== 429) {
+ throw new BadBody(
+ this.identifier,
+ JSON.stringify(err?.response?.data ?? {}),
+ '{}',
+ err?.response?.data?.message ||
+ `Dribbble rejected the shot with status ${status}`
+ );
}
- );
+ throw err;
+ }
const location = data2.headers['location'];
const newId = location.split('/').at(-1);
From 38a06efd0d665741864a85ca656e1ab1be8c4294 Mon Sep 17 00:00:00 2001
From: JOY <5027251+JOY@users.noreply.github.com>
Date: Thu, 24 Sep 2026 01:44:23 +0700
Subject: [PATCH 44/45] fix(sync): restore makeId import and remove upstream
connector URL table
- reddit.provider.ts: the fork's broker-based generateAuthUrl uses makeId(32)
for the state; the upstream merge replaced the import with makeSecureId.
- public.component.tsx: drop the upstream mcpConnectorUrls table
(claude.ai/directory/postiz etc.) entirely - it routes users to the
upstream cloud and the fork gates connector cards via brandConfig URLs
(fail closed). Onboarding modal's unused import removed.
---
.../src/components/onboarding/onboarding.modal.tsx | 1 -
.../src/components/public-api/public.component.tsx | 13 ++++---------
.../src/integrations/social/reddit.provider.ts | 1 +
3 files changed, 5 insertions(+), 10 deletions(-)
diff --git a/apps/frontend/src/components/onboarding/onboarding.modal.tsx b/apps/frontend/src/components/onboarding/onboarding.modal.tsx
index becaaa2040..d1c5a5d5f6 100644
--- a/apps/frontend/src/components/onboarding/onboarding.modal.tsx
+++ b/apps/frontend/src/components/onboarding/onboarding.modal.tsx
@@ -21,7 +21,6 @@ import {
McpAuth,
McpClient,
mcpClients,
- mcpConnectorUrls,
} from '@gitroom/frontend/components/public-api/public.component';
import { McpClientIcon } from '@gitroom/frontend/components/public-api/mcp.client.icons';
diff --git a/apps/frontend/src/components/public-api/public.component.tsx b/apps/frontend/src/components/public-api/public.component.tsx
index db6d2cbd7e..f9009ce23f 100644
--- a/apps/frontend/src/components/public-api/public.component.tsx
+++ b/apps/frontend/src/components/public-api/public.component.tsx
@@ -22,15 +22,10 @@ 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.
-export const mcpConnectorUrls = {
- Claude: 'https://claude.ai/directory/postiz',
- ChatGPT:
- 'https://chatgpt.com/plugins/plugin_asdk_app_6aaaf1a529808191a2a15fde824bb013',
- Cursor: 'https://cursor.com/marketplace/postiz',
- 'Grok Bot': 'https://x.ai/bot/plugin/58737848',
-} as const;
+// The upstream one-click connector directory URLs (claude.ai/directory,
+// chatgpt.com/plugins, cursor.com/marketplace) are deliberately NOT
+// reproduced here: they route this deployment's users to the upstream cloud.
+// Connector cards are brand-gated via brandConfig URLs instead.
// Clients with no MCP or CLI settings: you paste instructions into the chat,
// the agent installs the CLI itself and asks you for the API key.
diff --git a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
index c097d293e4..34f630d0ee 100644
--- a/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/reddit.provider.ts
@@ -6,6 +6,7 @@ import {
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeSecureId } from '@gitroom/nestjs-libraries/services/make.secure.id';
+import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto';
import { timer } from '@gitroom/helpers/utils/timer';
import {
From ec7194cdf8b8fcca685b6fd0ad6894946793fb07 Mon Sep 17 00:00:00 2001
From: JOY <5027251+JOY@users.noreply.github.com>
Date: Thu, 24 Sep 2026 01:51:28 +0700
Subject: [PATCH 45/45] fix(sync): restore getMcpOauthUrl import used by
getCursorInstallUrl
---
apps/frontend/src/components/onboarding/onboarding.modal.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/apps/frontend/src/components/onboarding/onboarding.modal.tsx b/apps/frontend/src/components/onboarding/onboarding.modal.tsx
index d1c5a5d5f6..ae20e4836a 100644
--- a/apps/frontend/src/components/onboarding/onboarding.modal.tsx
+++ b/apps/frontend/src/components/onboarding/onboarding.modal.tsx
@@ -16,6 +16,7 @@ import {
AnyMcpClient,
CopyButton,
getMcpConfig,
+ getMcpOauthUrl,
isChatOnlyMcpClient,
localCliSteps,
McpAuth,