Conversation
Replace the kie.ai submit/poll calls in the veo3 video provider with
EvoLink's async API (POST /v1/videos/generations, GET /v1/tasks/{id}),
gated by a new EVOLINK_API_KEY env var. Same Veo 3.1 Fast model, 8s
with audio, same DTO/identifier/UI. Thrown errors now carry EvoLink's
error code so content-policy failures map to the existing 422 safety
response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNtigh3UmZMbvyPsqrjxfn
Co-authored-by: egelhaus <156946629+egelhaus@users.noreply.github.com>
…r selection fields
…o feat/evolink-video-provider
…ties on heartbeat timeout Restores a 3 minute heartbeatTimeout on postSocialPending, finalizePost and postComment and retries at the workflow level when the heartbeat timeout fires with no heartbeat ever received, which means the worker never ran the activity. Comment proxy drops SDK retries; exhausted retries now mark the post unconfirmed instead of leaving it in QUEUE.
…ut-retry feat(orchestrator): post workflow v1.1.1 retries never-started activities on heartbeat timeout
…eCat, Seedance video provider)
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_81785b2f-0051-43d4-a44e-97b41ddb9d11) |
There was a problem hiding this comment.
Code Review
This pull request refactors the billing and payment architecture to support multiple providers (Stripe and RevenueCat) via a unified PaymentService, introduces asynchronous video generation workflows using Temporal, replaces the Veo3 video provider with Seedance (Evolink), and updates OAuth client registration to support private-use schemes. Key feedback includes a critical bug in StripeService where an asynchronous checkDiscount call is missing an await, potential runtime crashes in start.mcp.ts due to unsafe destructuring from oauthResources, poor error serialization in PaymentController when throwing raw error objects, and missing response.ok checks before parsing JSON in the new Seedance video provider.
| const check = this.checkDiscount(customer); | ||
| async applyDiscount(organization: Organization) { | ||
| const customer = organization.paymentId; | ||
| const check = this.checkDiscount(organization); |
There was a problem hiding this comment.
The checkDiscount method is an asynchronous function that returns a Promise<boolean>. Calling it without await means check will be a Promise object, which is always truthy in JavaScript. As a result, the if (!check) guard will never be entered, and the discount will be applied even if checkDiscount resolves to false. Please add await to resolve the promise.
| const check = this.checkDiscount(organization); | |
| const check = await this.checkDiscount(organization); |
| if (e instanceof HttpException) { | ||
| throw e; | ||
| } | ||
| throw new HttpException(e, 500); |
There was a problem hiding this comment.
Passing a raw Error or unknown object e directly as the first argument to HttpException can lead to poor serialization (such as an empty object {} or a blank message) because Error properties like message are non-enumerable. It is better to extract the error message or pass a user-friendly generic message.
| throw new HttpException(e, 500); | |
| throw new HttpException((e as Error)?.message || 'Internal server error', 500); |
|
|
||
| const url = new URL('/mcp-oauth-claude', process.env.NEXT_PUBLIC_BACKEND_URL); | ||
| // baseUrl is the mount path that matched, e.g. /mcp-oauth-claude | ||
| const { middleware, mcpServer } = oauthResources[req.baseUrl]; |
There was a problem hiding this comment.
Destructuring middleware and mcpServer directly from oauthResources[req.baseUrl] assumes that req.baseUrl will always match one of the keys in oauthResources. If there is any mismatch (e.g., due to trailing slashes or routing differences), oauthResources[req.baseUrl] will be undefined, causing a runtime TypeError and crashing the request. It is safer to add a guard check first.
| const { middleware, mcpServer } = oauthResources[req.baseUrl]; | |
| const resource = oauthResources[req.baseUrl]; | |
| if (!resource) { | |
| next(); | |
| return; | |
| } | |
| const { middleware, mcpServer } = resource; |
| const value = await ( | ||
| await fetch('https://api.evolink.ai/v1/videos/generations', { | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${process.env.EVOLINK_API_KEY}`, | ||
| }, | ||
| method: 'POST', | ||
| signal: AbortSignal.timeout(30000), | ||
| body: JSON.stringify({ | ||
| model: imageUrls.length | ||
| ? 'seedance-2.0-fast-reference-to-video' | ||
| : 'seedance-2.0-fast-text-to-video', | ||
| prompt: customParams.prompt, | ||
| ...(imageUrls.length ? { image_urls: imageUrls } : {}), | ||
| aspect_ratio: output === 'horizontal' ? '16:9' : '9:16', | ||
| duration: 8, | ||
| quality: '720p', | ||
| generate_audio: true, | ||
| }), | ||
| }) | ||
| ).json(); |
There was a problem hiding this comment.
Calling .json() directly on the fetch promise without checking response.ok can cause the application to throw a SyntaxError (e.g., "Unexpected token < in JSON") if the Evolink API fails and returns a non-JSON error page (such as a Cloudflare HTML error page). It is safer to check response.ok first and handle HTTP errors gracefully.
const response = await fetch('https://api.evolink.ai/v1/videos/generations', {
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + process.env.EVOLINK_API_KEY,
},
method: 'POST',
signal: AbortSignal.timeout(30000),
body: JSON.stringify({
model: imageUrls.length
? 'seedance-2.0-fast-reference-to-video'
: 'seedance-2.0-fast-text-to-video',
prompt: customParams.prompt,
...(imageUrls.length ? { image_urls: imageUrls } : {}),
aspect_ratio: output === 'horizontal' ? '16:9' : '9:16',
duration: 8,
quality: '720p',
generate_audio: true,
}),
});
if (!response.ok) {
throw new Error('Evolink API failed with status ' + response.status + ': ' + (await response.text()));
}
const value = await response.json();| const data = await ( | ||
| await fetch('https://api.evolink.ai/v1/tasks/' + taskId, { | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${process.env.EVOLINK_API_KEY}`, | ||
| }, | ||
| signal: AbortSignal.timeout(30000), | ||
| }) | ||
| ).json(); |
There was a problem hiding this comment.
Similar to the initial generation request, calling .json() directly on the task status fetch without checking response.ok can lead to unhandled JSON parsing errors if the API returns an HTML error page. Please check response.ok first.
const response = await fetch('https://api.evolink.ai/v1/tasks/' + taskId, {
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + process.env.EVOLINK_API_KEY,
},
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error('Evolink task status check failed with status ' + response.status);
}
const data = await response.json();
What kind of change does this PR introduce?
Feature & Upstream Synchronization
Why was this change needed?
Promotes verified
devbranch changes tomain:gitroomhq/postiz-appmain branch:apps/backend/src/api/routes/payment.controller.ts,libraries/nestjs-libraries/src/services/payment/) supporting mobile app subscription webhooks and Stripe synchronization.libraries/nestjs-libraries/src/videos/seedance/).video.status.tool.tsand enhanced image/video generator MCP tools.OPENAI_BASE_URL,OPENAI_MODEL_NAME,OPENAI_IMAGE_MODELacross AI services (OpenaiService,CopilotController,AgentGraphService,AutopostService).providerfield toSubscriptionmodel withdefault("stripe"), validated with Prisma 6.5.0.Technical Details & Scope
libraries/nestjs-libraries/src/database/prisma/schema.prisma: Addedprovidercolumn toSubscription.apps/orchestrator/src/workflows/post-workflows/post.workflow.v1.1.1.ts: Heartbeat timeout recovery.libraries/nestjs-libraries/src/services/payment/: Abstracted multi-provider payment service (Stripe & RevenueCat)..env.example: Full 15-section reference with sample environment variables.Verification & Testing
pnpm --filter ./apps/backend run build): Passed 0 errors.pnpm --filter ./apps/orchestrator run build): Passed 0 errors.pnpm run build:extension): Passed 0 errors.pnpm dlx tsx scripts/branding-guard.ts): Passed 100%.QA
pnpm run buildacross monorepo to verify clean compilation.pnpm run prisma-generateand ensure client types are up to date.https://post.crove.com/api/healthandhttps://beta-post.crove.com/api/healthreturn HTTP 200 OK.Checklist:
pnpm run build).pnpm dlx tsx scripts/branding-guard.ts).Note
High Risk
Changes span subscription lifecycle (Stripe + RevenueCat webhooks), Temporal post publishing (duplicate-post risk), and credit-consuming async video jobs—areas that directly affect billing and production content.
Overview
This PR refactors billing behind a provider abstraction so web Stripe and mobile RevenueCat share one path: generic
POST /payment/:providerwebhooks,PaymentServicerouting inBillingController/UsersController, subscription rows tagged with aproviderfield,POST /billing/syncfor app restore, and the billing UI blocking web checkout when the active sub is mobile-managed.AI video moves from synchronous MCP calls to Temporal (
generateVideoWorkflow, job polling viastartGenerateVideo/getGenerateVideoStatusandvideoStatusTool). The Veo3/Kie integration is replaced by Seedance/EvoLink (EVOLINK_API_KEY), with related frontend provider rename and image-slides/fal/elevenlabs hardening (timeouts, clearer errors).Publishing reliability bumps to
postWorkflowV111: heartbeat-timeout detection, safer pending-post resolution, and repeat-post delay measured after the schedule sleep. Streak emails change under a Temporalpatched('reminder')branch.Smaller but notable: MCP adds
/mcp-oauth-dynamicand unified OAuth resource middleware; DCR accepts private-use redirect URIs; ChatGPT submission/test cases updated; LinkedIn post analytics and X timeline pagination fixes; admin impersonation search enriched; PR/CLAUDE QA checklist rules tightened.Reviewed by Cursor Bugbot for commit bfbba88. Configure here.