@volter/twin-tiktok 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +310 -0
- package/client/tiktok-consent.tsx +154 -0
- package/client/tiktok-mirror.css +137 -0
- package/client/tiktok-mirror.tsx +492 -0
- package/dist/client/tiktok-consent.bundle.js +18 -0
- package/dist/client/tiktok-consent.d.ts +47 -0
- package/dist/client/tiktok-consent.js +20 -0
- package/dist/client/tiktok-consent.tsx +154 -0
- package/dist/client/tiktok-mirror.bundle.js +487 -0
- package/dist/client/tiktok-mirror.css +137 -0
- package/dist/client/tiktok-mirror.d.ts +42 -0
- package/dist/client/tiktok-mirror.js +315 -0
- package/dist/client/tiktok-mirror.tsx +492 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +44 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.js +167 -0
- package/dist/src/tiktok-blobs.d.ts +66 -0
- package/dist/src/tiktok-blobs.js +161 -0
- package/dist/src/tiktok-budget.d.ts +56 -0
- package/dist/src/tiktok-budget.js +136 -0
- package/dist/src/tiktok-capabilities.d.ts +7 -0
- package/dist/src/tiktok-capabilities.js +1855 -0
- package/dist/src/tiktok-conformance.d.ts +11 -0
- package/dist/src/tiktok-conformance.js +498 -0
- package/dist/src/tiktok-connector.d.ts +158 -0
- package/dist/src/tiktok-connector.js +600 -0
- package/dist/src/tiktok-consent-ui.d.ts +19 -0
- package/dist/src/tiktok-consent-ui.js +127 -0
- package/dist/src/tiktok-errors.d.ts +78 -0
- package/dist/src/tiktok-errors.js +175 -0
- package/dist/src/tiktok-ids.d.ts +16 -0
- package/dist/src/tiktok-ids.js +48 -0
- package/dist/src/tiktok-media.d.ts +7 -0
- package/dist/src/tiktok-media.js +86 -0
- package/dist/src/tiktok-mirror-ui.d.ts +49 -0
- package/dist/src/tiktok-mirror-ui.js +159 -0
- package/dist/src/tiktok-pkce.d.ts +25 -0
- package/dist/src/tiktok-pkce.js +56 -0
- package/dist/src/tiktok-posting.d.ts +100 -0
- package/dist/src/tiktok-posting.js +599 -0
- package/dist/src/tiktok-sample-mp4.d.ts +10 -0
- package/dist/src/tiktok-sample-mp4.js +55 -0
- package/dist/src/tiktok-scopes.d.ts +29 -0
- package/dist/src/tiktok-scopes.js +106 -0
- package/dist/src/tiktok-server.d.ts +28 -0
- package/dist/src/tiktok-server.js +89 -0
- package/dist/src/tiktok-store.d.ts +164 -0
- package/dist/src/tiktok-store.js +451 -0
- package/dist/src/tiktok-twin.d.ts +70 -0
- package/dist/src/tiktok-twin.js +1197 -0
- package/dist/src/tiktok-user.d.ts +28 -0
- package/dist/src/tiktok-user.js +174 -0
- package/package.json +74 -0
- package/src/cli.ts +43 -0
- package/src/index.ts +270 -0
- package/src/tiktok-blobs.ts +217 -0
- package/src/tiktok-budget.ts +163 -0
- package/src/tiktok-capabilities.ts +2022 -0
- package/src/tiktok-conformance.ts +526 -0
- package/src/tiktok-connector.ts +637 -0
- package/src/tiktok-consent-ui.ts +146 -0
- package/src/tiktok-errors.ts +197 -0
- package/src/tiktok-ids.ts +51 -0
- package/src/tiktok-journey.uitest.ts +305 -0
- package/src/tiktok-media.ts +89 -0
- package/src/tiktok-mirror-ui.ts +167 -0
- package/src/tiktok-pkce.ts +61 -0
- package/src/tiktok-posting.ts +617 -0
- package/src/tiktok-sample-mp4.ts +54 -0
- package/src/tiktok-scopes.ts +122 -0
- package/src/tiktok-server.ts +100 -0
- package/src/tiktok-store.ts +543 -0
- package/src/tiktok-twin.ts +1361 -0
- package/src/tiktok-user.ts +137 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// TIKTOK MIRROR UI — tiktok.com's own view of the twin's state: sign in as the creator, read their
|
|
2
|
+
// profile (avatar, @handle, nickname, the Following / Followers / Likes counts, bio) over the 9:16
|
|
3
|
+
// Videos grid, and open a post in the full-height vertical player with its caption and @handle. A
|
|
4
|
+
// React/TSX app bundled by Bun that renders by consuming the twin's OWN API on the same origin —
|
|
5
|
+
// `GET /v2/user/info/` and `POST /v2/video/list/`, the Display API reads any TikTok client makes —
|
|
6
|
+
// and plays the posted bytes from the twin's media route, so every screen is data-coupled to real
|
|
7
|
+
// twin state. Archetype A (passthrough), transcribed from the X and YouTube mirrors: one serving code
|
|
8
|
+
// path, so API<->UI parity cannot drift.
|
|
9
|
+
//
|
|
10
|
+
// NOTHING IS INVENTED. A count the Display API does not return (a token without user.info.stats, a
|
|
11
|
+
// persona seeded without it) is not drawn; a zero is drawn only when the twin holds a zero. Avatars
|
|
12
|
+
// the twin holds no image for (the vendor's CDN URLs a seeded persona carries) are drawn as the
|
|
13
|
+
// creator's initial.
|
|
14
|
+
//
|
|
15
|
+
// SIGN-IN. TikTok's "Log in" form (username, then the password) with the password leg replaced by the
|
|
16
|
+
// user access token the twin issued through /_twin/tokens: the mirror presents a registered token and
|
|
17
|
+
// `/v2/user/info/` says which creator it is — a token for another account is refused on the form.
|
|
18
|
+
// Both are kept in the TAB'S sessionStorage.
|
|
19
|
+
//
|
|
20
|
+
// PURE FRONTEND (R3): the mirror imports no handler and no twin internals — it MOUNTS the pack's own
|
|
21
|
+
// fetch adapter as its API backend and reads every byte of state back over the wire.
|
|
22
|
+
import { readFile } from 'node:fs/promises';
|
|
23
|
+
import { bundleClient, fileResponse, serveHttp } from '@volter/world-core';
|
|
24
|
+
import { createTikTokTwinFetch } from "./tiktok-server.js";
|
|
25
|
+
const CLIENT_ENTRY = () => new URL('../client/tiktok-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects a top-level relative import.meta.url
|
|
26
|
+
const CLIENT_CSS = () => new URL('../client/tiktok-mirror.css', import.meta.url).pathname; // lazy: same reason
|
|
27
|
+
/** The user fields the profile reads — every one the Display API gates behind the three user scopes. */
|
|
28
|
+
export const PROFILE_FIELDS = 'open_id,union_id,avatar_url,display_name,username,bio_description,is_verified,follower_count,following_count,likes_count,video_count';
|
|
29
|
+
/** The fields a token holding only user.info.basic may ask for (the header then carries no counts). */
|
|
30
|
+
export const BASIC_FIELDS = 'open_id,union_id,avatar_url,display_name';
|
|
31
|
+
/** The Video Object fields the grid and the player read. */
|
|
32
|
+
export const VIDEO_READ_FIELDS = 'id,create_time,title,video_description,duration,width,height,like_count,comment_count,share_count,view_count';
|
|
33
|
+
/** The Display API's own page maximum. */
|
|
34
|
+
export const VIDEO_PAGE_SIZE = 20;
|
|
35
|
+
/** tiktok.com's count: exact under 10,000 ("1280"), then one decimal of K or M ("45.1K", "1.4M"),
|
|
36
|
+
* the trailing ".0" dropped ("12K"). */
|
|
37
|
+
export function compactCount(raw) {
|
|
38
|
+
const n = Number(raw);
|
|
39
|
+
if (!Number.isFinite(n) || n < 0)
|
|
40
|
+
return '0';
|
|
41
|
+
const scaled = (value, unit) => `${String(Math.floor(value * 10) / 10).replace(/\.0$/, '')}${unit}`;
|
|
42
|
+
if (n >= 1e9)
|
|
43
|
+
return scaled(n / 1e9, 'B');
|
|
44
|
+
if (n >= 1e6)
|
|
45
|
+
return scaled(n / 1e6, 'M');
|
|
46
|
+
if (n >= 1e4)
|
|
47
|
+
return scaled(n / 1e3, 'K');
|
|
48
|
+
return String(Math.floor(n));
|
|
49
|
+
}
|
|
50
|
+
/** The profile header's three counts, each only when the twin returned it. */
|
|
51
|
+
export function profileStats(user) {
|
|
52
|
+
const out = [];
|
|
53
|
+
if (typeof user?.following_count === 'number')
|
|
54
|
+
out.push({ label: 'Following', value: compactCount(user.following_count) });
|
|
55
|
+
if (typeof user?.follower_count === 'number')
|
|
56
|
+
out.push({ label: 'Followers', value: compactCount(user.follower_count) });
|
|
57
|
+
if (typeof user?.likes_count === 'number')
|
|
58
|
+
out.push({ label: 'Likes', value: compactCount(user.likes_count) });
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** A caption split the way tiktok.com bolds it: #hashtags and @mentions are links, the rest text. */
|
|
62
|
+
export function captionSegments(text) {
|
|
63
|
+
const source = typeof text === 'string' ? text : '';
|
|
64
|
+
const out = [];
|
|
65
|
+
let last = 0;
|
|
66
|
+
for (const m of source.matchAll(/(#[\p{L}\p{N}_]+)|(@[A-Za-z0-9_.]{2,24})/gu)) {
|
|
67
|
+
const at = m.index ?? 0;
|
|
68
|
+
if (at > 0 && /[\p{L}\p{N}_]/u.test(source[at - 1]))
|
|
69
|
+
continue;
|
|
70
|
+
if (at > last)
|
|
71
|
+
out.push({ kind: 'text', value: source.slice(last, at) });
|
|
72
|
+
out.push({ kind: m[1] ? 'hashtag' : 'mention', value: m[0] });
|
|
73
|
+
last = at + m[0].length;
|
|
74
|
+
}
|
|
75
|
+
if (last < source.length)
|
|
76
|
+
out.push({ kind: 'text', value: source.slice(last) });
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** The caption a post shows: the Video Object's description, else its title. */
|
|
80
|
+
export function captionOf(video) {
|
|
81
|
+
const description = typeof video?.video_description === 'string' ? video.video_description : '';
|
|
82
|
+
return description !== '' ? description : typeof video?.title === 'string' ? video.title : '';
|
|
83
|
+
}
|
|
84
|
+
/** "0:03", "1:02" — the player's time readout. */
|
|
85
|
+
export function clock(seconds) {
|
|
86
|
+
const s = Math.max(0, Math.floor(Number(seconds) || 0));
|
|
87
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
|
88
|
+
}
|
|
89
|
+
/** A post's bytes on the twin's media route, relative to the wire base. The creator's token rides the
|
|
90
|
+
* URL (a <video> element sends no header), which is what a post that is not public needs. */
|
|
91
|
+
export function mediaSource(base, videoId, token) {
|
|
92
|
+
return `${base}/_twin/media/video/${encodeURIComponent(videoId)}?access_token=${encodeURIComponent(token)}`;
|
|
93
|
+
}
|
|
94
|
+
/** The avatar placeholder's letter and hue: pure functions of the account. */
|
|
95
|
+
export function avatarInitial(user) {
|
|
96
|
+
const source = String(user?.display_name ?? user?.username ?? '?').trim();
|
|
97
|
+
return (Array.from(source)[0] ?? '?').toUpperCase();
|
|
98
|
+
}
|
|
99
|
+
export function avatarHue(key) {
|
|
100
|
+
const s = String(key ?? '');
|
|
101
|
+
let h = 0;
|
|
102
|
+
for (let i = 0; i < s.length; i += 1)
|
|
103
|
+
h = (h * 31 + s.charCodeAt(i)) % 360;
|
|
104
|
+
return h;
|
|
105
|
+
}
|
|
106
|
+
const APP_SHELL = `<!doctype html>
|
|
107
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
108
|
+
<base href="/"><title>TikTok - Make Your Day</title><link rel="stylesheet" href="assets/styles.css"></head>
|
|
109
|
+
<body><div id="root"></div><script type="module" src="assets/app.js"></script></body></html>`;
|
|
110
|
+
let clientBundle = null;
|
|
111
|
+
/** Build the React/TSX mirror client to browser JS; memoized, so a pack does exactly one Bun.build. */
|
|
112
|
+
export function buildTiktokMirrorClient() {
|
|
113
|
+
if (!clientBundle) {
|
|
114
|
+
clientBundle = bundleClient(CLIENT_ENTRY())
|
|
115
|
+
.catch((error) => { clientBundle = null; throw error; });
|
|
116
|
+
}
|
|
117
|
+
return clientBundle;
|
|
118
|
+
}
|
|
119
|
+
/** Serve the TikTok mirror UI (React app) + its backing API, upload and media routes on one origin. */
|
|
120
|
+
export async function createTiktokMirrorServer(options) {
|
|
121
|
+
const twin = createTikTokTwinFetch(options);
|
|
122
|
+
const server = await serveHttp({
|
|
123
|
+
// LOOPBACK-SPECIFIC bind, as the X and YouTube mirrors: a wildcard bind on `port: 0` can be
|
|
124
|
+
// shadowed by an app already listening on 127.0.0.1 at the same port.
|
|
125
|
+
hostname: '127.0.0.1',
|
|
126
|
+
port: options.port ?? 0,
|
|
127
|
+
idleTimeout: 60,
|
|
128
|
+
async fetch(request) {
|
|
129
|
+
const url = new URL(request.url);
|
|
130
|
+
if (request.method === 'GET' && url.pathname === '/assets/app.js') {
|
|
131
|
+
try {
|
|
132
|
+
return new Response(await buildTiktokMirrorClient(), { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return new Response(String(error), { status: 500 });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (request.method === 'GET' && url.pathname === '/assets/styles.css') {
|
|
139
|
+
return fileResponse(CLIENT_CSS(), { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
140
|
+
}
|
|
141
|
+
if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '')) {
|
|
142
|
+
return new Response(APP_SHELL, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
143
|
+
}
|
|
144
|
+
// Everything else -> the twin's OWN FETCH ADAPTER: Login Kit, the Display API, Content Posting,
|
|
145
|
+
// the upload path and the media route, the same closure `createTikTokTwinServer` serves.
|
|
146
|
+
return twin(request);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const port = server.port ?? options.port ?? 0;
|
|
150
|
+
return { port, url: `http://127.0.0.1:${port}`, stop: () => server.stop(true) };
|
|
151
|
+
}
|
|
152
|
+
/** The app-shell HTML (pure). The client itself is the React app. */
|
|
153
|
+
export function tiktokMirrorHtml() {
|
|
154
|
+
return APP_SHELL;
|
|
155
|
+
}
|
|
156
|
+
/** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
|
|
157
|
+
export function tiktokMirrorStyles() {
|
|
158
|
+
return readFile(CLIENT_CSS(), 'utf8');
|
|
159
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** TikTok's `code_challenge`: the LOWERCASE HEX SHA-256 of the verifier (not base64url). */
|
|
2
|
+
export declare function tiktokCodeChallenge(verifier: string): string;
|
|
3
|
+
/** The RFC 7636 spelling, kept ONLY so a verify can prove the twin refuses it — TikTok does not
|
|
4
|
+
* accept it, and a caller that carried an X/Google-shaped challenge here must fail visibly. */
|
|
5
|
+
export declare function rfc7636Challenge(verifier: string): string;
|
|
6
|
+
/** The only `code_challenge_method` TikTok supports, per the desktop guide, verbatim: `S256`. */
|
|
7
|
+
export declare const CHALLENGE_METHOD = "S256";
|
|
8
|
+
/**
|
|
9
|
+
* Is `raw` a code_challenge_method TikTok accepts? Only `S256` is documented ("TikTok only
|
|
10
|
+
* supports S256"), and unlike X there is no `plain` fallback to model. The comparison is
|
|
11
|
+
* case-SENSITIVE: no vendor artefact shows TikTok accepting `s256`, and inventing a tolerance
|
|
12
|
+
* would be the inverse false-green (`tiktok.authorize.challenge_method_case`, todo).
|
|
13
|
+
*/
|
|
14
|
+
export declare function isSupportedChallengeMethod(raw: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Does `verifier` satisfy the challenge the code was minted against?
|
|
17
|
+
*
|
|
18
|
+
* The verifier must also be a well-formed one: "a high-entropy cryptographic random string using
|
|
19
|
+
* the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a minimum length
|
|
20
|
+
* of 43 characters and a maximum length of 128 characters" (the desktop guide, verbatim). A
|
|
21
|
+
* malformed verifier can never satisfy a challenge, so the shape check is folded in here rather
|
|
22
|
+
* than left to a caller that might forget it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function pkceVerifies(challenge: string, verifier: string): boolean;
|
|
25
|
+
export declare function isWellFormedVerifier(verifier: string): boolean;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// PKCE, TikTok's way — and TikTok's way is NOT RFC 7636's, which is the whole reason this file
|
|
2
|
+
// exists instead of a copied helper.
|
|
3
|
+
//
|
|
4
|
+
// RFC 7636 §4.2 defines `S256` as BASE64URL(SHA256(verifier)). TikTok's Login Kit for Desktop
|
|
5
|
+
// guide says instead: "Create the code challenge by hashing the code verifier using hex encoding
|
|
6
|
+
// of SHA256. Since TikTok only supports S256 as code_challenge_method, use code_challenge =
|
|
7
|
+
// SHA256(code_verifier)" — i.e. the challenge is the 64-character LOWERCASE HEX digest
|
|
8
|
+
// (developers.tiktok.com/doc/login-kit-desktop, fetched 2026-09-13). A twin that verified the
|
|
9
|
+
// base64url form would accept challenges the real vendor rejects and reject the ones it accepts,
|
|
10
|
+
// which is precisely the class of bug a twin exists to reproduce.
|
|
11
|
+
//
|
|
12
|
+
// PKCE IS OPTIONAL ON THIS VENDOR. The desktop guide requires it "for desktop apps"; the Login
|
|
13
|
+
// Kit for Web parameter table lists neither `code_challenge` nor `code_challenge_method`, and the
|
|
14
|
+
// token endpoint's own parameter table marks `code_verifier` "Required for mobile and desktop app
|
|
15
|
+
// only". Dub — a web app — sends no challenge at all. So the twin BINDS a verifier only when the
|
|
16
|
+
// authorize request carried a challenge, and requires none when it did not. Requiring PKCE the
|
|
17
|
+
// way X requires it would refuse the motivating application outright.
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
/** TikTok's `code_challenge`: the LOWERCASE HEX SHA-256 of the verifier (not base64url). */
|
|
20
|
+
export function tiktokCodeChallenge(verifier) {
|
|
21
|
+
return createHash('sha256').update(verifier).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
/** The RFC 7636 spelling, kept ONLY so a verify can prove the twin refuses it — TikTok does not
|
|
24
|
+
* accept it, and a caller that carried an X/Google-shaped challenge here must fail visibly. */
|
|
25
|
+
export function rfc7636Challenge(verifier) {
|
|
26
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
27
|
+
}
|
|
28
|
+
/** The only `code_challenge_method` TikTok supports, per the desktop guide, verbatim: `S256`. */
|
|
29
|
+
export const CHALLENGE_METHOD = 'S256';
|
|
30
|
+
/**
|
|
31
|
+
* Is `raw` a code_challenge_method TikTok accepts? Only `S256` is documented ("TikTok only
|
|
32
|
+
* supports S256"), and unlike X there is no `plain` fallback to model. The comparison is
|
|
33
|
+
* case-SENSITIVE: no vendor artefact shows TikTok accepting `s256`, and inventing a tolerance
|
|
34
|
+
* would be the inverse false-green (`tiktok.authorize.challenge_method_case`, todo).
|
|
35
|
+
*/
|
|
36
|
+
export function isSupportedChallengeMethod(raw) {
|
|
37
|
+
return raw === CHALLENGE_METHOD;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Does `verifier` satisfy the challenge the code was minted against?
|
|
41
|
+
*
|
|
42
|
+
* The verifier must also be a well-formed one: "a high-entropy cryptographic random string using
|
|
43
|
+
* the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a minimum length
|
|
44
|
+
* of 43 characters and a maximum length of 128 characters" (the desktop guide, verbatim). A
|
|
45
|
+
* malformed verifier can never satisfy a challenge, so the shape check is folded in here rather
|
|
46
|
+
* than left to a caller that might forget it.
|
|
47
|
+
*/
|
|
48
|
+
export function pkceVerifies(challenge, verifier) {
|
|
49
|
+
if (!isWellFormedVerifier(verifier))
|
|
50
|
+
return false;
|
|
51
|
+
return tiktokCodeChallenge(verifier) === challenge;
|
|
52
|
+
}
|
|
53
|
+
const VERIFIER_RE = /^[A-Za-z0-9\-._~]{43,128}$/;
|
|
54
|
+
export function isWellFormedVerifier(verifier) {
|
|
55
|
+
return VERIFIER_RE.test(verifier);
|
|
56
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type TwinResource } from '@volter/world-core';
|
|
2
|
+
import { type TikTokUpload } from './tiktok-blobs.js';
|
|
3
|
+
import { type TikTokResponse } from './tiktok-errors.js';
|
|
4
|
+
import { type Row } from './tiktok-store.js';
|
|
5
|
+
/** The vendor's own upload host, and the path its upload_url carries. */
|
|
6
|
+
export declare const UPLOAD_ORIGIN = "https://open-upload.tiktokapis.com";
|
|
7
|
+
export declare const UPLOAD_PATH = "/video";
|
|
8
|
+
/** "The upload_url is valid for one hour after issuance. The upload must be completed in this time range." */
|
|
9
|
+
export declare const UPLOAD_URL_TTL_MS: number;
|
|
10
|
+
export declare const MIN_CHUNK_BYTES: number;
|
|
11
|
+
export declare const MAX_CHUNK_BYTES: number;
|
|
12
|
+
export declare const MAX_FINAL_CHUNK_BYTES: number;
|
|
13
|
+
export declare const MAX_CHUNK_COUNT = 1000;
|
|
14
|
+
export declare const MAX_VIDEO_BYTES: number;
|
|
15
|
+
/**
|
|
16
|
+
* THE TWIN'S OWN LIMIT, below TikTok's 4 GB, and stated rather than hidden: the final chunk joins the
|
|
17
|
+
* staged chunks into ONE allocation and stores it with one put, because the kernel's blob seam takes
|
|
18
|
+
* bytes (`BlobStore.put(key, bytes)`), not a stream. So the twin accepts a video up to what it can hold
|
|
19
|
+
* whole — 512 MiB, generous for a release video — and refuses a larger init with invalid_param naming
|
|
20
|
+
* this limit (`tiktok.content_posting.large_files`, todo, is the streaming seam that would lift it).
|
|
21
|
+
*/
|
|
22
|
+
export declare const TWIN_MAX_VIDEO_BYTES: number;
|
|
23
|
+
/** "Framerate: 23–60 FPS" (media transfer guide). */
|
|
24
|
+
export declare const MIN_FRAME_RATE = 23;
|
|
25
|
+
export declare const MAX_FRAME_RATE = 60;
|
|
26
|
+
/** THE TWIN'S VALUE (TikTok publishes no processing time): how long, in World time, a finished upload
|
|
27
|
+
* stays PROCESSING_UPLOAD — two seconds, plus one per 10 MiB. */
|
|
28
|
+
export declare function processingMs(size: number): number;
|
|
29
|
+
/** Direct Post `privacy_level` values ("PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY"). */
|
|
30
|
+
export declare const PRIVACY_LEVELS: readonly ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
|
|
31
|
+
/** "Duration: Up to 10 minutes via API" (media transfer guide). An account may carry a lower
|
|
32
|
+
* `maxVideoPostDurationSec`; the reference's own example shows 300. */
|
|
33
|
+
export declare const DEFAULT_MAX_VIDEO_POST_DURATION_SEC = 600;
|
|
34
|
+
/** "Title: max 2200 UTF-16 runes" — `String.length` counts UTF-16 code units. */
|
|
35
|
+
export declare const MAX_TITLE_UTF16 = 2200;
|
|
36
|
+
/** "max 5 pending shares per 24 hours" (inbox upload reference, spam_risk_too_many_pending_share). */
|
|
37
|
+
export declare const MAX_PENDING_SHARES = 5;
|
|
38
|
+
/** The poster a Content Posting call acts for, resolved from its bearer by the router. */
|
|
39
|
+
export type PostingContext = {
|
|
40
|
+
resources: readonly TwinResource[];
|
|
41
|
+
account: Row;
|
|
42
|
+
openId: string;
|
|
43
|
+
scopes: string[];
|
|
44
|
+
/** The bearer's SHA-256 — what an upload record keeps of it. */
|
|
45
|
+
tokenHash: string;
|
|
46
|
+
clientKey: string;
|
|
47
|
+
at: string;
|
|
48
|
+
root?: string;
|
|
49
|
+
/** The twin's public base (origin plus any served-World mount path), when the request came over HTTP. */
|
|
50
|
+
origin?: string;
|
|
51
|
+
/** The vendor host the request named, when the injector or a World door forwarded it here. */
|
|
52
|
+
originalHost?: string;
|
|
53
|
+
readOnly?: boolean;
|
|
54
|
+
};
|
|
55
|
+
/** What creator_info answers for an account — every value from the account row, the reference's
|
|
56
|
+
* defaults where the row names none. */
|
|
57
|
+
export declare function creatorOptions(account: Row): {
|
|
58
|
+
privacyLevelOptions: string[];
|
|
59
|
+
commentDisabled: boolean;
|
|
60
|
+
duetDisabled: boolean;
|
|
61
|
+
stitchDisabled: boolean;
|
|
62
|
+
maxDurationSec: number;
|
|
63
|
+
};
|
|
64
|
+
export declare function creatorInfo(ctx: PostingContext): TikTokResponse;
|
|
65
|
+
/** The chunk plan a FILE_UPLOAD init names, checked against the guide's rules; a string is the refusal. */
|
|
66
|
+
export declare function checkChunkPlan(videoSize: unknown, chunkSize: unknown, count: unknown): string | null;
|
|
67
|
+
/** The byte length chunk `index` must carry under an upload's plan. */
|
|
68
|
+
export declare function chunkLength(upload: Pick<TikTokUpload, 'videoSize' | 'chunkSize' | 'totalChunkCount'>, index: number): number;
|
|
69
|
+
export declare function initUpload(ctx: PostingContext, mode: 'direct' | 'inbox', rawBody: string | undefined): Promise<TikTokResponse>;
|
|
70
|
+
export type UploadRequest = {
|
|
71
|
+
/** Always PUT: the router sends nothing else to the upload path. */
|
|
72
|
+
method: string;
|
|
73
|
+
query: URLSearchParams;
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
bytes?: Uint8Array;
|
|
76
|
+
at: string;
|
|
77
|
+
root?: string;
|
|
78
|
+
readOnly?: boolean;
|
|
79
|
+
};
|
|
80
|
+
export declare function uploadChunk(req: UploadRequest): Promise<TikTokResponse>;
|
|
81
|
+
/**
|
|
82
|
+
* The publish_id's lifecycle at World instant `at`. `publicaly_available_post_id` (the vendor's own
|
|
83
|
+
* spelling) is a list<int64>: the twin writes it as bare JSON NUMBERS, as TikTok does, so an
|
|
84
|
+
* integration that JSON.parse()s a 19-digit id into a double loses digits HERE rather than in
|
|
85
|
+
* production. It lists the post once it is public (PUBLIC_TO_EVERYONE) and complete.
|
|
86
|
+
*/
|
|
87
|
+
export declare function fetchStatus(ctx: PostingContext, rawBody: string | undefined): Promise<TikTokResponse>;
|
|
88
|
+
export declare const MEDIA_PREFIX = "/_twin/media/video/";
|
|
89
|
+
/** The most one open-ended range answers (`bytes=N-`): 8 MiB. */
|
|
90
|
+
export declare const MAX_SERVED_RANGE: number;
|
|
91
|
+
export type MediaRequest = {
|
|
92
|
+
method: string;
|
|
93
|
+
path: string;
|
|
94
|
+
query: URLSearchParams;
|
|
95
|
+
headers?: Record<string, string>;
|
|
96
|
+
at: string;
|
|
97
|
+
root?: string;
|
|
98
|
+
resources: readonly TwinResource[];
|
|
99
|
+
};
|
|
100
|
+
export declare function serveVideo(req: MediaRequest): Promise<TikTokResponse>;
|