Publishing Hub
Implementation: src/server/services/publishing.ts. UI: /studio/publishing, project → Publish.
Credential vault
Never store raw passwords. Only platform-issued tokens.
Tokens are sealed with AES-256-GCM under TOKEN_VAULT_KEY (sealToken / openToken). connectAccount refuses to store anything if no key is configured — failing loudly beats storing plaintext.
vaultStatus() reports which mode is active, and the UI shows it:
| Mode | Meaning |
|---|---|
kms | KMS_KEY_ID set — envelope encryption with rotation. Production target. |
local_key | Real AES-256-GCM at rest, but one local key. Not the Part 8.2 scheme. |
unconfigured | Connecting an account is refused. |
Calling local_key production-grade would be the kind of small lie that becomes a breach post-mortem.
Disconnect destroys the token row rather than soft-deleting it.
Platform reality
Each adapter declares readiness and an apiReality string, both shown in the UI before a creator queues a post.
| Platform | Readiness | Reality |
|---|---|---|
| YouTube | oauth_ready | Data API v3 — mature, quota-limited, audit for higher quota. Build first. |
| TikTok | requires_audit | Content Posting API needs an audited developer app. Ship inbox-draft first, direct-post after audit. |
requires_app_review | Graph API — Business/Creator accounts only. Personal accounts cannot publish via API. | |
requires_app_review | Pages well supported. Groups API heavily restricted post-2024 → share-dialog fallback. | |
| X | paid_tier | v2 posting requires a paid tier. Chunked media upload. Budget for fees. |
| Rumble | assisted_only | No public posting API. Prepare metadata + open the upload page. |
Rumble's assisted flow is a design decision, not an omission, and the UI says so. Setting expectations before someone queues twenty posts is cheaper than apologising after.
Auto-format engine
planFormat produces a per-destination plan:
- Crop — 9:16 uses
subject_track, not center-crop. A static center-crop destroys wide shots, which is most of a film. - Trim — with an explicit note when the source exceeds the platform cap.
- Safe zones — real per-aspect values (9:16 loses the bottom 20% to caption + CTA and the top 12% to the profile row).
- Captions — burned for vertical, sidecar for horizontal.
- Copy — platform-tuned title/description/hashtags, editable before posting.
- AI disclosure — always on. Where a platform exposes an API flag, it is set; where not (X, Rumble), disclosure goes in the description and the plan says which.
Publish queue
queuePost runs Stage 0 screening first — everything leaving the platform is screened even when it skips Theater review, because under each platform's API terms you are the publisher of record.
Org-shared connections set requiresOwnerApproval, so posts land in needs_approval.
executePost: Rumble returns an assisted-flow payload; every other platform requires configured credentials and returns a specific, actionable error naming the file to implement. Retries use exponential backoff capped at six hours.
Campaign planner
planCampaign produces the reviewable "MARKET THIS FILM" plan — trailer set, posters, platform-native clips, captions, and a drip schedule (anchor Monday, clip Wednesday, premiere Friday) with a total credit cost. Nothing posts until approved.
Export
Five delivery formats plus EDL, CMX3600 and SRT. Every export carries a C2PA manifest and AI-generated labelling.
Manifests are unsigned without C2PA_SIGNING_CERT, and exportManifest returns a note saying downstream verifiers will show "no valid claim signature". An unsigned manifest presented as verified provenance is worse than none.