Skip to content

chore(sync): promote upstream v1.1.1, RevenueCat billing, and OpenAI-compatible support to main - #29

Merged
JOY (JOY) merged 26 commits into
mainfrom
dev
Sep 2, 2026
Merged

chore(sync): promote upstream v1.1.1, RevenueCat billing, and OpenAI-compatible support to main#29
JOY (JOY) merged 26 commits into
mainfrom
dev

Conversation

@JOY

@JOY JOY (JOY) commented Sep 2, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

Feature & Upstream Synchronization

Why was this change needed?

Promotes verified dev branch changes to main:

  1. Upstream Postiz Synchronization: Merged latest features from gitroomhq/postiz-app main branch:
    • Temporal Post Workflow v1.1.1: Added activity retries on heartbeat timeouts when activities fail to start.
    • In-App Purchase & Billing Sync: Added RevenueCat provider (apps/backend/src/api/routes/payment.controller.ts, libraries/nestjs-libraries/src/services/payment/) supporting mobile app subscription webhooks and Stripe synchronization.
    • AI Video Provider Migration: Migrated Veo3 provider to Seedance / EvoLink (libraries/nestjs-libraries/src/videos/seedance/).
    • MCP Video Status Tooling: Added video.status.tool.ts and enhanced image/video generator MCP tools.
  2. OpenAI-Compatible Gateway Support: Configured OPENAI_BASE_URL, OPENAI_MODEL_NAME, OPENAI_IMAGE_MODEL across AI services (OpenaiService, CopilotController, AgentGraphService, AutopostService).
  3. Database Schema & Prisma Client Alignment: Added provider field to Subscription model with default("stripe"), validated with Prisma 6.5.0.

Technical Details & Scope

  • libraries/nestjs-libraries/src/database/prisma/schema.prisma: Added provider column to Subscription.
  • 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

  • Backend TypeScript build (pnpm --filter ./apps/backend run build): Passed 0 errors.
  • Orchestrator build (pnpm --filter ./apps/orchestrator run build): Passed 0 errors.
  • Extension build (pnpm run build:extension): Passed 0 errors.
  • Branding Guard (pnpm dlx tsx scripts/branding-guard.ts): Passed 100%.

QA

  1. Run pnpm run build across monorepo to verify clean compilation.
  2. Run pnpm run prisma-generate and ensure client types are up to date.
  3. Verify live health endpoints https://post.crove.com/api/health and https://beta-post.crove.com/api/health return HTTP 200 OK.

Checklist:

  • My code follows the project's code style and architectural conventions.
  • Local build passes (pnpm run build).
  • Branding guard validation passes (pnpm dlx tsx scripts/branding-guard.ts).
  • Tests and typecheck have been verified without errors.
  • Documentation has been updated (if applicable).
  • No secrets or sensitive credentials are included in this PR.
  • I have filled in the QA / Verification section above with real steps to verify this change.

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/:provider webhooks, PaymentService routing in BillingController/UsersController, subscription rows tagged with a provider field, POST /billing/sync for 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 via startGenerateVideo / getGenerateVideoStatus and videoStatusTool). 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 Temporal patched('reminder') branch.

Smaller but notable: MCP adds /mcp-oauth-dynamic and 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.

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>
…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
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@JOY
JOY (JOY) merged commit 616bac5 into main Sep 2, 2026
19 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
const check = this.checkDiscount(organization);
const check = await this.checkDiscount(organization);

if (e instanceof HttpException) {
throw e;
}
throw new HttpException(e, 500);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
const { middleware, mcpServer } = oauthResources[req.baseUrl];
const resource = oauthResources[req.baseUrl];
if (!resource) {
next();
return;
}
const { middleware, mcpServer } = resource;

Comment on lines +45 to +65
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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();

Comment on lines +85 to +93
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants