chore: sync with upstream Postiz (2026-09-21) - #43
Conversation
…and widget Turns a YouTube video into captioned vertical clips that land in the media library and as draft posts. Adds the Clipping / ClippingClip models, the clipping_minutes credit type, clippingWorkflow + per-clip child workflows, the /clipping REST routes, the MCP clipping tools and the ui://postiz/clipping status widget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- createClips takes the row of the clipping before looking for clips, so a timed out attempt and its retry can no longer both store a set - a clip is claimed (draftedAt off null) before its draft is created and released when creation fails, so a retry cannot draft it twice - urls are stripped from the logged processor failure - Turkish billing label reads correctly after the number Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…time createPost writes one post per channel, so a failure on a later channel left the earlier drafts in place while the released claim let a retry create them again. The claim now stays, the free slot is looked up before it is taken, and each channel is drafted on its own so one failing channel does not drop the rest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat(clipping): YouTube video clipping workflow (REST + MCP tools + widget)
# Conflicts: # .env.example # libraries/nestjs-libraries/src/chat/load.tools.service.ts # libraries/nestjs-libraries/src/chat/start.mcp.ts
There was a problem hiding this comment.
Code Review
This pull request introduces an AI-powered video clipping feature that allows users to turn long YouTube videos into short vertical clips with burned-in captions. It adds backend controllers, middleware, and database schemas, alongside Temporal workflows and activities to manage the multi-step clipping process (analysis, transcription, clipping, rendering, and drafting). Additionally, it integrates subscription-based clipping limits, UI billing updates, localized translations, and Mastra agent tools with an MCP widget. The review feedback highlights three key issues: the use of an invalid OpenAI model name (gpt-4.1) which will fail at runtime, an unsafe non-null assertion on a potentially null organization object, and a potential workflow crash from unhandled JSON parsing of clipping integrations.
| const { clips } = ( | ||
| await openai.chat.completions.parse( | ||
| { | ||
| model: 'gpt-4.1', |
There was a problem hiding this comment.
gpt-4.1 is not a valid OpenAI model name. This will cause the API call to fail with a 400 Bad Request error at runtime. Please use a valid model name such as gpt-4o or gpt-4o-mini which support structured outputs (response_format with Zod schemas).
| model: 'gpt-4.1', | |
| model: 'gpt-4o', |
| const org = await this._organizationService.getOrgByIdWithSubscription( | ||
| organizationId | ||
| ); | ||
| return (await this._subscriptionService.checkCredits(org!, CREDITS_TYPE)) | ||
| .credits; |
There was a problem hiding this comment.
Using the non-null assertion operator (org!) here is unsafe because getOrgByIdWithSubscription can return null if the organization is not found or has been deleted. This will cause a runtime TypeError. Please handle the null case safely.
| const org = await this._organizationService.getOrgByIdWithSubscription( | |
| organizationId | |
| ); | |
| return (await this._subscriptionService.checkCredits(org!, CREDITS_TYPE)) | |
| .credits; | |
| const org = await this._organizationService.getOrgByIdWithSubscription( | |
| organizationId | |
| ); | |
| if (!org) { | |
| return 0; | |
| } | |
| return (await this._subscriptionService.checkCredits(org, CREDITS_TYPE)) | |
| .credits; |
| const integrations = ( | ||
| await Promise.all( | ||
| (JSON.parse(clipping.integrations) as string[]).map((id) => | ||
| this._integrationService.getIntegrationById( | ||
| clipping.organizationId, | ||
| id | ||
| ) | ||
| ) | ||
| ) | ||
| ).filter((f) => f && !f.deletedAt && !f.disabled); |
There was a problem hiding this comment.
Parsing clipping.integrations directly with JSON.parse without a try-catch block can cause the background Temporal workflow to crash if the database contains invalid JSON or a non-array value. Wrapping it in a try-catch block makes the workflow more resilient.
let integrationIds: string[] = [];
try {
integrationIds = JSON.parse(clipping.integrations) as string[];
} catch (err) {
console.error('Failed to parse clipping integrations:', err);
}
const integrations = (
await Promise.all(
integrationIds.map((id) =>
this._integrationService.getIntegrationById(
clipping.organizationId,
id
)
)
)
).filter((f) => f && !f.deletedAt && !f.disabled);…ools The alias map in load.tools.service.ts does not register crove_post_ aliases for the clipping tools, so those claudeHiddenTools entries could never match. The real tool names stay hidden; re-add branded aliases only if the alias map gains clipping entries.
Upstream's clipping workflow added OpenaiService.pickClips referencing a module-level openai client that the fork replaced with getOpenAIClient() / getModel() for OPENAI_BASE_URL and OPENAI_MODEL_NAME support, so the auto-merge produced TS2552 (Cannot find name 'openai'). Align pickClips with the fork pattern used by every other method in this service.
…age code - clipping.service.ts: log clipping failures with fixed format strings and data as arguments instead of interpolating tainted values into the template - local.storage.ts: contain removeFile to the upload directory - resolve the requested path and refuse to unlink anything outside the upload root, so a traversal-shaped key can never delete arbitrary files Both fixes touch files introduced/changed by this upstream sync; they are deliberate fork deltas to be recorded in docs/fork-delta.md.
CodeQL js/path-injection does not model the startsWith(resolvedRoot + sep) prefix check as a validated boundary. path.relative + isAbsolute + '..' rejection is the canonical containment form: any path resolving outside the upload root yields a relative path starting with '..' or an absolute one.
What kind of change does this PR introduce?
CI/CD / Upstream sync. Manually completed version of the daily automated sync: merges upstream
gitroomhq/postiz-appmain (6 commits, head7cef69c1) intodev. Brings in the YouTube clipping workflow (REST + MCP tools + widget), MCP upload widget, Mastra 1.67 schema tables, staging-conflicts workflow fix, and the GAdvisory scope security change. Three conflicts resolved by hand:.env.example(kept the fork Temporal section and accepted the upstream Cloudflare/RUNPOD/Deepgram block),libraries/nestjs-libraries/src/chat/start.mcp.ts(kept the forkcrove_post_*hidden-tool aliases and added the three new clipping tools plus their branded aliases to the Claude-hidden list),libraries/nestjs-libraries/src/chat/load.tools.service.ts(kept the fork dynamicagentId+ brand-name description, which defaults topostizand is a superset of upstream).Why was this change needed?
The automated sync workflow aborted because the merge was not clean; the corridor must stay current so upstream fixes (provider fixes, clipping feature, security hardening) keep flowing in small, reviewable increments instead of piling up (per docs/adr/0001-upstream-sync-and-fork-delta.md).
Technical Details & Scope
Verification & Testing
git grepconfirms zero conflict markers remain in the worktree.crove_post_*alias coverage) while accepting upstream additions.QA
Checklist:
pnpm run build) - not run locally in this worktree; CI build.yml on this PR is the confirmation gate.pnpm dlx tsx scripts/branding-guard.ts) - CI branding-guard.yml on this PR is the confirmation gate.