Source path: app/Services/Publishers/TikTokPublisher.php
Extracted from supplied specification. Review before copying into application.

<?php
namespace App\Services\Publishers;

use App\Models\PostTarget;
use App\Models\SocialAccount;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class TikTokPublisher extends BasePublisher
{
    private const API = 'https://open.tiktokapis.com/v2';

    public function discoverAccounts(string $t): array
    {
        $u = Http::withToken($t)->get(self::API . '/user/info/', ['fields' => 'open_id,display_name'])->throw()->json('data.user');
        return [['id' => $u['open_id'], 'name' => $u['display_name'] ?? 'TikTok user']];
    }

    public function refreshToken(SocialAccount $a): void
    {
        $c = config('social.tiktok');
        $r = Http::asForm()->post($c['token_url'], [
            'client_key' => $c['client_id'], 'client_secret' => $c['client_secret'],
            'grant_type' => 'refresh_token', 'refresh_token' => $a->refresh_token,
        ])->throw();
        $this->saveTokens($a, $r->json());
    }

    public function publish(PostTarget $t): array
    {
        $a = $t->socialAccount;
        $tok = $this->token($a);
        $video = $this->media($t)->firstWhere('type', 'video');
        if (!$video) throw new \RuntimeException('TikTok publisher here supports video only.');

        // Unaudited apps are limited to private (SELF_ONLY) posts; use what the creator is allowed.
        $info = Http::withToken($tok)->post(self::API . '/post/publish/creator_info/query/')->throw()->json('data');
        $options = $info['privacy_level_options'] ?? ['SELF_ONLY'];
        $wanted = config('social.tiktok.privacy');
        $privacy = in_array($wanted, $options) ? $wanted : $options[0];

        $f = $this->tmp($video);
        try {
            $size = filesize($f);
            if ($size > 64 * 1024 * 1024) throw new \RuntimeException('Video over 64MB needs chunked upload (not implemented).');

            $init = Http::withToken($tok)->post(self::API . '/post/publish/video/init/', [
                'post_info' => [
                    'title' => Str::limit($t->post->caption, 2200, ''),
                    'privacy_level' => $privacy,
                ],
                'source_info' => [
                    'source' => 'FILE_UPLOAD', 'video_size' => $size, 'chunk_size' => $size, 'total_chunk_count' => 1,
                ],
            ])->throw()->json('data');

            Http::timeout(300)->withHeaders([
                'Content-Range' => 'bytes 0-' . ($size - 1) . "/$size",
            ])->withBody(file_get_contents($f), 'video/mp4')->put($init['upload_url'])->throw();
        } finally { @unlink($f); }

        $publishId = $init['publish_id'];
        for ($i = 0; $i < 30; $i++) {
            sleep(5);
            $s = Http::withToken($tok)->post(self::API . '/post/publish/status/fetch/', ['publish_id' => $publishId])->throw()->json('data');
            if (($s['status'] ?? '') === 'PUBLISH_COMPLETE') {
                return ['id' => $publishId, 'url' => null];
            }
            if (($s['status'] ?? '') === 'FAILED') throw new \RuntimeException('TikTok: ' . ($s['fail_reason'] ?? 'failed'));
        }
        return ['id' => $publishId, 'url' => null]; // still processing; accepted by TikTok
    }
}
YouTube
