merge: upstream v2.25.0 (87 commits - self-hosting connector, responsive UI, media hardening) - #67
Conversation
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 <provider> channel <cause>" and once "Could not refresh your <provider> 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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.
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 <noreply@anthropic.com>
…s 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 <noreply@anthropic.com>
…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 gitroomhq#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 <noreply@anthropic.com>
…creen 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 <noreply@anthropic.com>
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g3KqiuR5TT68XdqpTRbZh
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017g3KqiuR5TT68XdqpTRbZh
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 <noreply@anthropic.com>
Return early on a 402 instead of following the billing url in its body.
…ws on a 402 Same early return as the regular flow, so none of them use the billing url from the 402 body.
…2-body-read-main fix(frontend): keep the 402 response readable and stay on Add Channel after cancel
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).
… the uppy progress error gitroomhq#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. gitroomhq#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 <noreply@anthropic.com>
…cess-on-error fix(frontend): don't report a failed post save as successful
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 <noreply@anthropic.com>
…nge-main fix(uploads): honor HTTP Range requests so local-storage video uploads work again
…py-progress-chrome fix(frontend): stop the Sentry report dialog on the Chrome wording of the uppy progress error
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tch-on-file-error fix(media): keep the rest of the upload batch when one file fails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-client fix(frontend): remove duplicate 'use client' directive in VK provider
Hover pencil on the group header opens a small rename modal backed by a new PUT /integrations/customers/:id (org-scoped, duplicate name rejected).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153xf1m3VSQnd9QrMLhDikq
…-main feat(groups): rename a channel group from the channel menu
…h-error-email fix(integrations): send a single notification when a token refresh fails
…ed-data-rules-main docs(claude): add shared-query and persisted-data change rules
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VcaeTzorDM71yKNf9h3Fo4
Legacy photo posts store p_pub_url~ ids, which the v_pub_ check skipped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VcaeTzorDM71yKNf9h3Fo4
…nses Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153xf1m3VSQnd9QrMLhDikq
…edential-rows docs(claude): add a rule on credentials and sensitive fields in responses
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.
readOrFetch took a user-influenced path on plain axios, unlike every other media read in the class.
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.
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.
fix(media): bound media reads that stall after their headers
Returns the current releaseId and releaseURL so other callers can reuse it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ost-id-main fix(tiktok): resolve and persist the public post id for analytics
Self-hosting connector (MCP relay for self-hosted installs), responsive UI, channel group rename, Canva intent, Instagram reel cover thumbnail, TikTok publish/analytics id fixes, media pipeline hardening (SSRF-safe reads, stalled-read timeouts, Range requests, batch failure isolation), Facebook posting-rate retry, Sentry 10.56.0 span/scope leak pin. Conflicts resolved: - .env.example / CLAUDE.md: union (Crove blocks + upstream additions) - pnpm-lock.yaml: upstream side, regenerated against merged package.json - main.billing.component: keep showPortalAndCancel gating (Crove) and add upstream's responsive button classes - oauth.controller: union imports for the self-hosted endpoints (backend plumbing, inert without env) - onboarding.modal / public.component / oauth authorize page: took the Crove side wholesale - these are the brand-gated upstream-shared UI surfaces the branding guard protects (official connector directory URLs route users to the upstream cloud) Schema: new OAuthSelfHostedAuthorization table + OAuthApp organizationId index. Beta DB gets the idempotent DDL at deploy; the shared prod DB goes through the DOS-Me ledger before the prod deploy (rule 9).
| await this.getSsrfSafeAxios()({ | ||
| 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), | ||
| }) |
| 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]); |
| 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]); |
| 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]); |
| 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]); |
| 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]); |
| setSubmitting(false); | ||
| } | ||
| }, | ||
| [clientId, state, redirectUri, codeChallenge, codeChallengeMethod] |
| }, [ | ||
| clientId, | ||
| responseType, | ||
| state, | ||
| redirectUri, | ||
| codeChallenge, | ||
| codeChallengeMethod, | ||
| resource, | ||
| ]); |
| <img | ||
| src={ | ||
| mode === 'dark' | ||
| ? '/no-channels.svg' | ||
| : '/no-channels-colors.svg' | ||
| } | ||
| alt="No channels" | ||
| className="mx-auto min-w-[100%]" | ||
| /> |
| await mutate(); | ||
| toaster.show(t('group_updated', 'Group Updated'), 'success'); | ||
| }, | ||
| [group.name, group.id, mutate, modals, t] |
| } | ||
| resolve(groupName); | ||
| close(); | ||
| }, [groupName, id]); |
There was a problem hiding this comment.
Code Review
This pull request introduces support for connecting and relaying Model Context Protocol (MCP) and OAuth requests to self-hosted Postiz instances, alongside various mobile responsiveness and UI enhancements across the frontend. Key feedback includes addressing a potential hydration mismatch in the calendar context by avoiding dynamic window checks during render, replacing blocking synchronous file reads (readFileSync) with asynchronous alternatives (promises.readFile) in the social integrations abstract class, and adding optional chaining to prevent runtime errors when integrations are undefined during loading states.
| // 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; |
There was a problem hiding this comment.
Initializing the cookie default value dynamically using window.matchMedia during render will cause a Hydration Mismatch in Next.js. The server-rendered HTML (which defaults to 'week' because window is undefined) will not match the client-rendered HTML on mobile devices (which defaults to 'day').
To fix this, initialize the cookie with a static default value (e.g., 'week'), and use a useEffect hook to dynamically switch to 'day' on mount if the user is on a mobile device and no cookie has been set yet.
| // 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; | |
| const [displaySaved, setDisplaySaved] = useCookie('calendar-display', 'week'); | |
| const display = searchParams.get('display') || displaySaved; | |
| useEffect(() => { | |
| if (window.matchMedia('(max-width: 1025px)').matches) { | |
| const hasCookie = document.cookie.includes('calendar-display='); | |
| if (!hasCookie) { | |
| setDisplaySaved('day'); | |
| } | |
| } | |
| }, [setDisplaySaved]); |
| } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher'; | ||
| import sharp from 'sharp'; | ||
| import { createReadStream, statSync } from 'fs'; | ||
| import { createReadStream, readFileSync, statSync } from 'fs'; |
| // classify a stall like the ranged reads below do. | ||
| protected async readOrFetch(path: string, retried = false): Promise<Buffer> { | ||
| if (path.indexOf('http') !== 0) { | ||
| return readFileSync(path); |
| const integration = integrations.find( | ||
| (i) => i.id === post.integration.id | ||
| ); |
There was a problem hiding this comment.
If integrations is undefined or null (e.g., during the initial loading state), calling .find() on it will throw a runtime error. Use optional chaining integrations?.find to safely handle this case.
| const integration = integrations.find( | |
| (i) => i.id === post.integration.id | |
| ); | |
| const integration = integrations?.find( | |
| (i) => i.id === post.integration.id | |
| ); |
|
Reviewer disposition (verified finding re: Range handling in the uploads route): accepted-as-is, not patched. The verified claim is that the Range regex does not match multi-range headers, so a multi-range request is served a full-file 200 instead of 206/416. That is RFC 7233-compliant behavior (a server MAY ignore Range and return 200; every mainstream upload client handles it), it is upstream's shipped-and-tested v2.25.0 code, and patching upstream logic in a sync PR is against the standing no-shared-rebuilds directive. If a real client ever breaks on this, a targeted follow-up patch (take first range, 206) is the documented remediation. The two security claims (authorizeSelfHosted public endpoint, public.auth.middleware userId) were verified-dismissed by the critic - no authorization bypass; apiKey validation happens in McpRelayService and userId is signature-bound. |
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.
Upstream sync 374fb20..8e42f09 (= tag v2.25.0), 87 commits since upstream-sync-20260926 (PR #65).
Features: self-hosting connector (relay a self-hosted Postiz to Postiz Cloud MCP), responsive UI pass, channel group rename from menu, Canva intent, Instagram custom thumbnail as reel cover, MCP video cover attachments.
Fixes: media pipeline hardening (SSRF-safe reads via axios client, stalled-read 2min timeout + fail instead of repeating publish, HTTP Range honored/clamped, batch upload failure isolation, upload-step error surfacing), TikTok publish/analytics id resolution, Facebook temporary posting-rate retry (368/1390008), Telegram no longer logs post text, Sentry pinned 10.56.0 (span/scope leak), editor no longer wipes in-flight uploads, web3/custom-fields/extension connect flows stop on 402.
Conflicts resolved:
.env.example+CLAUDE.md: union (Crove blocks + upstream additions).pnpm-lock.yaml: upstream side regenerated against the merged package.json.main.billing.component: keep Crove'sshowPortalAndCancelplan gating, add upstream's responsive button classes.oauth.controller: union imports (self-hosted endpoint plumbing, inert without env).onboarding.modal/public.component/oauth/authorize/page: took the CROVE side wholesale - these are the brand-gated upstream-shared UI surfaces (official connector directory URLs route users to the upstream cloud; the branding guard exists to keep them out). Upstream's self-hosted connector UI is a Postiz-Cloud branding surface and is intentionally not shipped here.Schema (deploy gating): new
OAuthSelfHostedAuthorizationtable +OAuthApp.organizationIdindex. Beta DB gets the idempotent DDL at deploy; the shared prod DB goes through the DOS-Me ledger before the prod deploy (rule 9). Migration SQL follows via the DOS-Me handoff.📌 TL;DR
This PR introduces a self-hosted MCP relay authorization flow, adds HTTP Range request support to the upload API to fix video uploads, and implements extensive mobile responsiveness improvements across the dashboard, calendar, and developer tools.
🎯 Type of Change
🔍 Changes Walkthrough
.env.exampleapps/backend/src/api/routes/enterprise.controller.tsapps/backend/src/api/routes/integrations.controller.tsPUT /customers/:idendpoint to update customer names.apps/backend/src/api/routes/oauth.controller.tsPOST /authorize/self-hostedfor MCP relay connections, added throttling, and fixed token endpoint status code to 200 for Canva compatibility.apps/backend/src/main.tsCANVA_APP_ORIGINto the CORS allowed origins list.apps/backend/src/public-api/routes/v1/public.integrations.controller.tsGET /meendpoint to return organization and user details for OAuth app tokens.apps/backend/src/services/auth/public.auth.middleware.tsuserIdfrom OAuth authorization to the request object for the new/meendpoint.apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.tsapps/frontend/src/app/global-error.tsxapps/frontend/src/components/agents/agent.tsxapps/frontend/src/components/billing/main.billing.component.tsxapps/frontend/src/components/developer/developer.component.tsxapps/frontend/src/components/launches/add.provider.component.tsx402 Payment Requiredresponses during integration connection and adjusted grid layout for mobile.apps/frontend/src/components/launches/calendar.context.tsxapps/frontend/src/components/launches/calendar.tsxapps/frontend/src/components/launches/filters.tsxapps/frontend/src/components/launches/helpers/date.picker.tsxapps/frontend/src/components/launches/launches.component.tsxapps/frontend/src/components/launches/menu/menu.tsxapps/frontend/src/components/layout/impersonate.tsx📊 Architectural Flow
sequenceDiagram participant Client as MCP Client (e.g., Claude) participant Backend as Postiz Backend participant Relay as MCP Relay Service participant SelfHosted as Self-Hosted Instance Client->>Backend: POST /oauth/authorize/self-hosted Note right of Client: Includes instance_url, api_key, client_id Backend->>Backend: Validate Authorization Request Backend->>Relay: connect(instance_url, api_key) Relay->>SelfHosted: Verify Connection SelfHosted-->>Relay: Success Relay-->>Backend: Instance Metadata Backend->>Backend: Create Self-Hosted Authorization Code Backend-->>Client: Redirect with Code Client->>Backend: POST /oauth/token Note right of Client: Exchange Code for Token Backend-->>Client: Access Token (200 OK)