@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,28 @@
|
|
|
1
|
+
import type { Row } from './tiktok-store.js';
|
|
2
|
+
export declare const USER_INFO_SCOPES: readonly ["user.info.basic", "user.info.profile", "user.info.stats"];
|
|
3
|
+
/** Field -> the scope that gates it, in the reference table's own order. */
|
|
4
|
+
export declare const USER_FIELD_SCOPES: Record<string, string>;
|
|
5
|
+
export declare const USER_FIELDS: readonly string[];
|
|
6
|
+
/**
|
|
7
|
+
* Render an account row as the vendor's User object for a requested field set.
|
|
8
|
+
*
|
|
9
|
+
* `open_id` is APP-SCOPED and therefore comes from the grant, not from the account — that is the
|
|
10
|
+
* whole distinction TikTok draws between `open_id` and `union_id`. A modelled field whose value
|
|
11
|
+
* this persona genuinely lacks is OMITTED rather than served as a placeholder (the connector's
|
|
12
|
+
* "invent nothing" rule, applied at the read side too).
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderUser(account: Row, openId: string, fields: readonly string[]): Record<string, unknown>;
|
|
15
|
+
/** The Video Object's fields, verbatim from the vendor's own reference table. */
|
|
16
|
+
export declare const VIDEO_FIELDS: readonly string[];
|
|
17
|
+
/** The single scope every Display API video read is gated behind. */
|
|
18
|
+
export declare const VIDEO_SCOPE = "video.list";
|
|
19
|
+
/**
|
|
20
|
+
* Render a video row for a requested field set.
|
|
21
|
+
*
|
|
22
|
+
* The four LINK fields are DERIVED, deterministically, from the stored row — TikTok composes them
|
|
23
|
+
* the same way (`share_url` is the canonical `/@handle/video/<id>` permalink and `embed_link` the
|
|
24
|
+
* `/embed/v2/<id>` player). Deriving keeps them consistent with the id and the owner's handle
|
|
25
|
+
* instead of storing four strings that could drift apart; the exact CDN host of `cover_image_url`
|
|
26
|
+
* is a twin host, not the vendor's, and `tiktok.video.link_shapes` (todo) pins the live forms.
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderVideo(video: Row, ownerUsername: string, fields: readonly string[]): Record<string, unknown>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export const USER_INFO_SCOPES = ['user.info.basic', 'user.info.profile', 'user.info.stats'];
|
|
2
|
+
/** Field -> the scope that gates it, in the reference table's own order. */
|
|
3
|
+
export const USER_FIELD_SCOPES = {
|
|
4
|
+
open_id: 'user.info.basic',
|
|
5
|
+
union_id: 'user.info.basic',
|
|
6
|
+
avatar_url: 'user.info.basic',
|
|
7
|
+
avatar_url_100: 'user.info.basic',
|
|
8
|
+
avatar_large_url: 'user.info.basic',
|
|
9
|
+
display_name: 'user.info.basic',
|
|
10
|
+
bio_description: 'user.info.profile',
|
|
11
|
+
profile_deep_link: 'user.info.profile',
|
|
12
|
+
is_verified: 'user.info.profile',
|
|
13
|
+
username: 'user.info.profile',
|
|
14
|
+
follower_count: 'user.info.stats',
|
|
15
|
+
following_count: 'user.info.stats',
|
|
16
|
+
likes_count: 'user.info.stats',
|
|
17
|
+
video_count: 'user.info.stats',
|
|
18
|
+
};
|
|
19
|
+
export const USER_FIELDS = Object.keys(USER_FIELD_SCOPES);
|
|
20
|
+
/**
|
|
21
|
+
* Render an account row as the vendor's User object for a requested field set.
|
|
22
|
+
*
|
|
23
|
+
* `open_id` is APP-SCOPED and therefore comes from the grant, not from the account — that is the
|
|
24
|
+
* whole distinction TikTok draws between `open_id` and `union_id`. A modelled field whose value
|
|
25
|
+
* this persona genuinely lacks is OMITTED rather than served as a placeholder (the connector's
|
|
26
|
+
* "invent nothing" rule, applied at the read side too).
|
|
27
|
+
*/
|
|
28
|
+
export function renderUser(account, openId, fields) {
|
|
29
|
+
const out = {};
|
|
30
|
+
const put = (key, value) => {
|
|
31
|
+
if (value !== undefined && value !== null)
|
|
32
|
+
out[key] = value;
|
|
33
|
+
};
|
|
34
|
+
for (const field of fields) {
|
|
35
|
+
switch (field) {
|
|
36
|
+
case 'open_id':
|
|
37
|
+
put(field, openId);
|
|
38
|
+
break;
|
|
39
|
+
case 'union_id':
|
|
40
|
+
put(field, account.id);
|
|
41
|
+
break;
|
|
42
|
+
case 'avatar_url':
|
|
43
|
+
put(field, account.avatarUrl);
|
|
44
|
+
break;
|
|
45
|
+
case 'avatar_url_100':
|
|
46
|
+
put(field, account.avatarUrl100);
|
|
47
|
+
break;
|
|
48
|
+
case 'avatar_large_url':
|
|
49
|
+
put(field, account.avatarLargeUrl);
|
|
50
|
+
break;
|
|
51
|
+
case 'display_name':
|
|
52
|
+
put(field, account.displayName);
|
|
53
|
+
break;
|
|
54
|
+
case 'bio_description':
|
|
55
|
+
put(field, account.bioDescription);
|
|
56
|
+
break;
|
|
57
|
+
case 'profile_deep_link':
|
|
58
|
+
put(field, account.profileDeepLink);
|
|
59
|
+
break;
|
|
60
|
+
case 'is_verified':
|
|
61
|
+
put(field, typeof account.isVerified === 'boolean' ? account.isVerified : undefined);
|
|
62
|
+
break;
|
|
63
|
+
case 'username':
|
|
64
|
+
put(field, account.username);
|
|
65
|
+
break;
|
|
66
|
+
case 'follower_count':
|
|
67
|
+
put(field, account.followerCount);
|
|
68
|
+
break;
|
|
69
|
+
case 'following_count':
|
|
70
|
+
put(field, account.followingCount);
|
|
71
|
+
break;
|
|
72
|
+
case 'likes_count':
|
|
73
|
+
put(field, account.likesCount);
|
|
74
|
+
break;
|
|
75
|
+
case 'video_count':
|
|
76
|
+
put(field, account.videoCount);
|
|
77
|
+
break;
|
|
78
|
+
default: break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/** The Video Object's fields, verbatim from the vendor's own reference table. */
|
|
84
|
+
export const VIDEO_FIELDS = [
|
|
85
|
+
'id',
|
|
86
|
+
'create_time',
|
|
87
|
+
'cover_image_url',
|
|
88
|
+
'share_url',
|
|
89
|
+
'video_description',
|
|
90
|
+
'duration',
|
|
91
|
+
'height',
|
|
92
|
+
'width',
|
|
93
|
+
'title',
|
|
94
|
+
'embed_html',
|
|
95
|
+
'embed_link',
|
|
96
|
+
'like_count',
|
|
97
|
+
'comment_count',
|
|
98
|
+
'share_count',
|
|
99
|
+
'view_count',
|
|
100
|
+
'is_aigc',
|
|
101
|
+
];
|
|
102
|
+
/** The single scope every Display API video read is gated behind. */
|
|
103
|
+
export const VIDEO_SCOPE = 'video.list';
|
|
104
|
+
/**
|
|
105
|
+
* Render a video row for a requested field set.
|
|
106
|
+
*
|
|
107
|
+
* The four LINK fields are DERIVED, deterministically, from the stored row — TikTok composes them
|
|
108
|
+
* the same way (`share_url` is the canonical `/@handle/video/<id>` permalink and `embed_link` the
|
|
109
|
+
* `/embed/v2/<id>` player). Deriving keeps them consistent with the id and the owner's handle
|
|
110
|
+
* instead of storing four strings that could drift apart; the exact CDN host of `cover_image_url`
|
|
111
|
+
* is a twin host, not the vendor's, and `tiktok.video.link_shapes` (todo) pins the live forms.
|
|
112
|
+
*/
|
|
113
|
+
export function renderVideo(video, ownerUsername, fields) {
|
|
114
|
+
const shareUrl = `https://www.tiktok.com/@${ownerUsername}/video/${video.id}`;
|
|
115
|
+
const out = {};
|
|
116
|
+
const put = (key, value) => {
|
|
117
|
+
if (value !== undefined && value !== null)
|
|
118
|
+
out[key] = value;
|
|
119
|
+
};
|
|
120
|
+
for (const field of fields) {
|
|
121
|
+
switch (field) {
|
|
122
|
+
case 'id':
|
|
123
|
+
put(field, video.id);
|
|
124
|
+
break;
|
|
125
|
+
case 'create_time':
|
|
126
|
+
put(field, video.createTime);
|
|
127
|
+
break;
|
|
128
|
+
case 'cover_image_url':
|
|
129
|
+
put(field, `https://p16-sign.tiktokcdn-us.com/twin/${video.id}~tplv-cover.jpeg`);
|
|
130
|
+
break;
|
|
131
|
+
case 'share_url':
|
|
132
|
+
put(field, shareUrl);
|
|
133
|
+
break;
|
|
134
|
+
case 'video_description':
|
|
135
|
+
put(field, video.videoDescription);
|
|
136
|
+
break;
|
|
137
|
+
case 'duration':
|
|
138
|
+
put(field, video.duration);
|
|
139
|
+
break;
|
|
140
|
+
case 'height':
|
|
141
|
+
put(field, video.height);
|
|
142
|
+
break;
|
|
143
|
+
case 'width':
|
|
144
|
+
put(field, video.width);
|
|
145
|
+
break;
|
|
146
|
+
case 'title':
|
|
147
|
+
put(field, video.title);
|
|
148
|
+
break;
|
|
149
|
+
case 'embed_html':
|
|
150
|
+
put(field, `<blockquote class="tiktok-embed" cite="${shareUrl}" data-video-id="${video.id}"></blockquote>`);
|
|
151
|
+
break;
|
|
152
|
+
case 'embed_link':
|
|
153
|
+
put(field, `https://www.tiktok.com/embed/v2/${video.id}`);
|
|
154
|
+
break;
|
|
155
|
+
case 'like_count':
|
|
156
|
+
put(field, video.likeCount);
|
|
157
|
+
break;
|
|
158
|
+
case 'comment_count':
|
|
159
|
+
put(field, video.commentCount);
|
|
160
|
+
break;
|
|
161
|
+
case 'share_count':
|
|
162
|
+
put(field, video.shareCount);
|
|
163
|
+
break;
|
|
164
|
+
case 'view_count':
|
|
165
|
+
put(field, video.viewCount);
|
|
166
|
+
break;
|
|
167
|
+
case 'is_aigc':
|
|
168
|
+
put(field, video.isAigc === true);
|
|
169
|
+
break;
|
|
170
|
+
default: break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@volter/twin-tiktok",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local TikTok twin — the real tiktok.com authorization page, the full Login Kit OAuth round trip (token, refresh, revoke, client_credentials) and the Display API user/video reads with TikTok's own comma-separated scopes, per-app open_id, field-level scope gating and both of its error envelopes, so an unmodified TikTok integration completes sign-in-with-TikTok against it. Built on @volter/world-core.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"twin",
|
|
7
|
+
"local",
|
|
8
|
+
"mock",
|
|
9
|
+
"mirror",
|
|
10
|
+
"simulator",
|
|
11
|
+
"fixtures",
|
|
12
|
+
"testing",
|
|
13
|
+
"oauth",
|
|
14
|
+
"oauth2",
|
|
15
|
+
"tiktok",
|
|
16
|
+
"login-kit",
|
|
17
|
+
"display-api",
|
|
18
|
+
"identity",
|
|
19
|
+
"sign-in-with-tiktok"
|
|
20
|
+
],
|
|
21
|
+
"author": "Volter (https://github.com/volter-ai)",
|
|
22
|
+
"license": "Apache-2.0",
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"client",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"!**/*.test.ts",
|
|
29
|
+
"!**/*.test.tsx",
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/volter-ai/twin.git",
|
|
35
|
+
"directory": "packages/twin/tiktok"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/tiktok#readme",
|
|
38
|
+
"type": "module",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/src/index.d.ts",
|
|
42
|
+
"default": "./dist/src/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"bin": {
|
|
46
|
+
"world-tiktok": "dist/src/cli.js"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "bun test src/*.test.ts",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"build": "node ../../../scripts/publish/build.mjs",
|
|
52
|
+
"prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
|
|
53
|
+
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"react": "^19.2.7",
|
|
57
|
+
"react-dom": "^19.2.7"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@volter/world-core": "2.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/bun": "^1.2.20",
|
|
64
|
+
"@types/node": "^24.0.0",
|
|
65
|
+
"@types/react": "^19.2.17",
|
|
66
|
+
"@types/react-dom": "^19.2.3",
|
|
67
|
+
"@volter/world-core": "2.0.0",
|
|
68
|
+
"@volter/world-tooling": "0.1.0",
|
|
69
|
+
"typescript": "^5.9.0"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=22.3"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// world-tiktok CLI: serve the TikTok twin, or run conformance.
|
|
3
|
+
//
|
|
4
|
+
// `serve` starts the twin. `mirror` starts the same twin with tiktok.com's profile and player in
|
|
5
|
+
// front of it (tiktok-mirror-ui.ts: the shell at `/`, everything else the twin's own fetch adapter),
|
|
6
|
+
// and prints both screens an operator asks to "see": the mirror, and a ready-to-open authorize URL
|
|
7
|
+
// for the authorization page the twin serves at the vendor's own path.
|
|
8
|
+
import { hasFlag, optionValue } from '@volter/world-core/args';
|
|
9
|
+
import { keepProcessAlive } from '@volter/world-core/lifecycle';
|
|
10
|
+
import { createTikTokTwinServer } from './tiktok-server.ts';
|
|
11
|
+
import { createTiktokMirrorServer } from './tiktok-mirror-ui.ts';
|
|
12
|
+
import { DEFAULT_CLIENT_KEY, defaultRedirectUris } from './tiktok-store.ts';
|
|
13
|
+
|
|
14
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
15
|
+
const port = Number(optionValue(rest, '--port', '0')) || undefined;
|
|
16
|
+
const root = optionValue(rest, '--root') || undefined;
|
|
17
|
+
const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
|
|
18
|
+
|
|
19
|
+
if (cmd === 'serve' || cmd === 'mirror') {
|
|
20
|
+
const options = { readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) };
|
|
21
|
+
const s = cmd === 'mirror' ? await createTiktokMirrorServer(options) : await createTikTokTwinServer(options);
|
|
22
|
+
const origin = `http://127.0.0.1:${s.port}`;
|
|
23
|
+
process.stdout.write(`tiktok twin (Login Kit OAuth + Display API + Content Posting)${readOnly ? ' [read-only]' : ''} at ${origin}\n`);
|
|
24
|
+
if (cmd === 'mirror') {
|
|
25
|
+
process.stdout.write(`mirror (tiktok.com profile + player): ${origin}/\n`);
|
|
26
|
+
const url = new URL(`${origin}/v2/auth/authorize`);
|
|
27
|
+
url.searchParams.set('client_key', DEFAULT_CLIENT_KEY);
|
|
28
|
+
url.searchParams.set('response_type', 'code');
|
|
29
|
+
url.searchParams.set('redirect_uri', defaultRedirectUris(origin)[0]!);
|
|
30
|
+
url.searchParams.set('scope', 'user.info.basic,user.info.profile,video.list');
|
|
31
|
+
url.searchParams.set('state', 'twin-demo-state');
|
|
32
|
+
process.stdout.write(`authorization page: ${url.toString()}\n`);
|
|
33
|
+
}
|
|
34
|
+
await keepProcessAlive();
|
|
35
|
+
} else if (cmd === 'conformance') {
|
|
36
|
+
// dev-only; lazy so the bin runs without @volter/world-tooling in the runtime graph
|
|
37
|
+
const { checkTikTokConformance } = await import('./tiktok-conformance.ts');
|
|
38
|
+
const report = await checkTikTokConformance({ ...(root ? { root } : {}) });
|
|
39
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
40
|
+
if (!report.ok) process.exitCode = 1;
|
|
41
|
+
} else {
|
|
42
|
+
process.stdout.write('Usage: world-tiktok serve|mirror|conformance [--port N] [--root DIR] [--read-only]\n');
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// @volter/twin-tiktok — the TikTok twin (one vendor, one package), built on the shared
|
|
2
|
+
// @volter/world-core kernel.
|
|
3
|
+
//
|
|
4
|
+
// A BROWSER-FACING protocol pack (the googleoauth class): it serves TikTok's own authorization
|
|
5
|
+
// page as HTML at www.tiktok.com's real path, completes the full Login Kit authorization-code
|
|
6
|
+
// round trip (consent -> 302 with code+scopes+state -> redemption at the token endpoint -> refresh
|
|
7
|
+
// -> revoke), and answers the Display API's `/v2/user/info/`, `/v2/video/list/` and
|
|
8
|
+
// `/v2/video/query/` reads with TikTok's real field-selection, scope-gating and error envelopes, and
|
|
9
|
+
// posts a creator's video through the Content Posting API (creator_info, the Direct Post and inbox
|
|
10
|
+
// inits, the chunked upload to a signed upload URL, status/fetch) with the bytes on the blob seam, a
|
|
11
|
+
// tiktok.com-style mirror over that state, and a perform that sends a World's post to TikTok.
|
|
12
|
+
// Deliberately NON-OIDC, exactly as the vendor is: opaque `act.`/`rft.` tokens, no id_token, no
|
|
13
|
+
// JWKS — identity comes from the Display API, and `open_id` is per-app while `union_id` is
|
|
14
|
+
// per-human. Conformance tooling lives in @volter/world-tooling (a dev dependency), not here.
|
|
15
|
+
import { registerPack, type TwinPack } from '@volter/world-core';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
ACCESS_TOKEN_TTL_SECONDS,
|
|
19
|
+
API_ORIGIN,
|
|
20
|
+
AUTH_CODE_TTL_SECONDS,
|
|
21
|
+
AUTHORIZE_ORIGIN,
|
|
22
|
+
CLIENT_TOKEN_TTL_SECONDS,
|
|
23
|
+
handleTikTokTwinRequest,
|
|
24
|
+
METERED_ENDPOINTS,
|
|
25
|
+
RATE_LIMIT_PER_WINDOW,
|
|
26
|
+
RATE_LIMITS,
|
|
27
|
+
RATE_WINDOW_SECONDS,
|
|
28
|
+
REFRESH_TOKEN_TTL_SECONDS,
|
|
29
|
+
RESOURCE_TYPES,
|
|
30
|
+
tiktokTwinSnapshot,
|
|
31
|
+
USER_FIELDS,
|
|
32
|
+
VIDEO_FIELDS,
|
|
33
|
+
VIDEO_LIST_DEFAULT_COUNT,
|
|
34
|
+
VIDEO_LIST_MAX_COUNT,
|
|
35
|
+
VIDEO_QUERY_MAX_IDS,
|
|
36
|
+
} from './tiktok-twin.ts';
|
|
37
|
+
export type { MeteredEndpoint, TikTokRequest, TikTokResponse } from './tiktok-twin.ts';
|
|
38
|
+
export { createTikTokConsentServer, createTikTokTwinFetch, createTikTokTwinServer, type TikTokTwinFetchOptions } from './tiktok-server.ts';
|
|
39
|
+
export {
|
|
40
|
+
DEFAULT_ACCOUNTS,
|
|
41
|
+
DEFAULT_CLIENT_KEY,
|
|
42
|
+
DEFAULT_CLIENT_SECRET,
|
|
43
|
+
DEFAULT_VIDEOS,
|
|
44
|
+
defaultRedirectUris,
|
|
45
|
+
openIdFor,
|
|
46
|
+
redirectUriAllowed,
|
|
47
|
+
sessionAccount,
|
|
48
|
+
} from './tiktok-store.ts';
|
|
49
|
+
export { CHALLENGE_METHOD, isSupportedChallengeMethod, isWellFormedVerifier, pkceVerifies, rfc7636Challenge, tiktokCodeChallenge } from './tiktok-pkce.ts';
|
|
50
|
+
export {
|
|
51
|
+
describeScope,
|
|
52
|
+
describeScopes,
|
|
53
|
+
formatScopeParam,
|
|
54
|
+
KNOWN_SCOPES,
|
|
55
|
+
parseScopeParam,
|
|
56
|
+
SCOPE_CATALOG,
|
|
57
|
+
sortScopesForConsent,
|
|
58
|
+
} from './tiktok-scopes.ts';
|
|
59
|
+
export type { ScopeInfo, ScopeProduct } from './tiktok-scopes.ts';
|
|
60
|
+
export { API_ERRORS, OAUTH_ERRORS } from './tiktok-errors.ts';
|
|
61
|
+
export { logId } from './tiktok-ids.ts';
|
|
62
|
+
export { renderUser, renderVideo, USER_FIELD_SCOPES, USER_INFO_SCOPES, VIDEO_SCOPE } from './tiktok-user.ts';
|
|
63
|
+
export {
|
|
64
|
+
liveTikTokExecute,
|
|
65
|
+
mapUserInfoAccount,
|
|
66
|
+
mapVideo,
|
|
67
|
+
performTikTokAction,
|
|
68
|
+
PULL_USER_FIELDS,
|
|
69
|
+
PULL_VIDEO_FIELDS,
|
|
70
|
+
pullTikTok,
|
|
71
|
+
pushPendingTikTokActions,
|
|
72
|
+
syncTikTokFromReal,
|
|
73
|
+
syncTikTokFromRemote,
|
|
74
|
+
tiktokExecuteOver,
|
|
75
|
+
} from './tiktok-connector.ts';
|
|
76
|
+
export type { LiveTikTokOptions, PullOptions, TikTokExecute } from './tiktok-connector.ts';
|
|
77
|
+
export {
|
|
78
|
+
budgetedTikTokExecute,
|
|
79
|
+
MAX_PUBLISH_WAIT_MS,
|
|
80
|
+
TikTokRetryableError,
|
|
81
|
+
PERFORM_CHUNK_BYTES,
|
|
82
|
+
performChunkPlan,
|
|
83
|
+
STATUS_POLL_MS,
|
|
84
|
+
TikTokStillProcessingError,
|
|
85
|
+
} from './tiktok-connector.ts';
|
|
86
|
+
export {
|
|
87
|
+
checkChunkPlan,
|
|
88
|
+
chunkLength,
|
|
89
|
+
creatorOptions,
|
|
90
|
+
DEFAULT_MAX_VIDEO_POST_DURATION_SEC,
|
|
91
|
+
MAX_CHUNK_BYTES,
|
|
92
|
+
MAX_CHUNK_COUNT,
|
|
93
|
+
MAX_FINAL_CHUNK_BYTES,
|
|
94
|
+
MAX_SERVED_RANGE,
|
|
95
|
+
MAX_VIDEO_BYTES,
|
|
96
|
+
MEDIA_PREFIX,
|
|
97
|
+
MIN_CHUNK_BYTES,
|
|
98
|
+
PRIVACY_LEVELS,
|
|
99
|
+
processingMs,
|
|
100
|
+
UPLOAD_ORIGIN,
|
|
101
|
+
UPLOAD_PATH,
|
|
102
|
+
UPLOAD_URL_TTL_MS,
|
|
103
|
+
} from './tiktok-posting.ts';
|
|
104
|
+
export { POSTING_ERRORS } from './tiktok-errors.ts';
|
|
105
|
+
// The client-side rate budget — the fail-closed backstop `liveTikTokExecute` routes every live
|
|
106
|
+
// request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
|
|
107
|
+
// here is this vendor's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
|
|
108
|
+
// bindings. Exported so an operator can inspect spend (`snapshot`) and so a caller can catch
|
|
109
|
+
// `TikTokBudgetError` by type; there is deliberately no export that disables the guard.
|
|
110
|
+
export {
|
|
111
|
+
TIKTOK_BUDGET_BURST_CEILING,
|
|
112
|
+
TIKTOK_BUDGET_CEILING,
|
|
113
|
+
TIKTOK_BUDGET_MAX_RETRY_AFTER_S,
|
|
114
|
+
TIKTOK_BUDGET_WINDOW_MS,
|
|
115
|
+
TIKTOK_CALL_WEIGHTS,
|
|
116
|
+
TIKTOK_RATE_BUDGET,
|
|
117
|
+
TikTokBudget,
|
|
118
|
+
TikTokBudgetError,
|
|
119
|
+
tiktokBudgetPath,
|
|
120
|
+
tiktokCallWeight,
|
|
121
|
+
} from './tiktok-budget.ts';
|
|
122
|
+
export type {
|
|
123
|
+
TikTokBudgetErrorKind,
|
|
124
|
+
TikTokBudgetOptions,
|
|
125
|
+
TikTokBudgetReservation,
|
|
126
|
+
TikTokBudgetSnapshot,
|
|
127
|
+
} from './tiktok-budget.ts';
|
|
128
|
+
export { CONSENT_CSS, consentPageHtml, errorPageHtml, tiktokConsentState } from './tiktok-consent-ui.ts';
|
|
129
|
+
// The tiktok.com mirror (profile, grid, vertical player): its pure helpers and its server. The hosted
|
|
130
|
+
// mirror mount reads tiktok-mirror-ui.ts directly (`tiktokMirrorHtml`, `buildTiktokMirrorClient`,
|
|
131
|
+
// `tiktokMirrorStyles`).
|
|
132
|
+
export { captionOf, captionSegments, compactCount, createTiktokMirrorServer, mediaSource, profileStats } from './tiktok-mirror-ui.ts';
|
|
133
|
+
export type { ConsentAccount, ConsentScopeRow, ConsentView, ErrorPageProps } from './tiktok-consent-ui.ts';
|
|
134
|
+
|
|
135
|
+
import { TIKTOK_RATE_BUDGET as RATE_BUDGET } from './tiktok-budget.ts';
|
|
136
|
+
import { performTikTokAction, syncTikTokFromRemote } from './tiktok-connector.ts';
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The www.tiktok.com paths THIS pack serves — the Login Kit authorization leg — as a RegExp SOURCE
|
|
140
|
+
* for the descriptor's `hosts` path rule. www.tiktok.com is NOT an API host: it is the whole
|
|
141
|
+
* TikTok web property (the For You feed, profiles, settings, everything). Claiming the host
|
|
142
|
+
* outright would be the Cal.com mis-route incident with the largest possible blast radius, so this
|
|
143
|
+
* claims EXACTLY the authorize path plus the twin-only prefix. ANCHORED, the googleoauth lesson:
|
|
144
|
+
* an unanchored claim swallows `/v2/auth/authorize2` and friends. The optional trailing slash is
|
|
145
|
+
* load-bearing — TikTok's own documentation writes `/v2/auth/authorize/` while Dub's URL builder
|
|
146
|
+
* omits it, and both must route here.
|
|
147
|
+
*/
|
|
148
|
+
const TIKTOK_AUTHORIZE_PATHS = '^/v2/auth/authorize/?$|^/_twin/';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The open.tiktokapis.com paths THIS pack serves: the OAuth token/revoke endpoints, the Display
|
|
152
|
+
* API's user and video reads and the Content Posting API's four calls. open.tiktokapis.com is the
|
|
153
|
+
* ENTIRE TikTok open platform host — Research, Data Portability, Commercial Content, Business and
|
|
154
|
+
* the photo post besides — and nothing this pack does not model is claimed: an unmodelled TikTok
|
|
155
|
+
* call must refuse loudly rather than land in a twin that cannot serve it.
|
|
156
|
+
*/
|
|
157
|
+
const TIKTOK_API_PATHS = '^/v2/(?:oauth/(?:token|revoke)|user/info|video/(?:list|query)|post/publish/(?:creator_info/query|video/init|inbox/video/init|status/fetch))/?$|^/_twin/';
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The upload host's path: the Content Posting API's upload_url is `https://open-upload.tiktokapis.com/
|
|
161
|
+
* video/?upload_id=…&upload_token=…` (the Direct Post and inbox references' own examples). An app in a
|
|
162
|
+
* World PUTs its chunks there, so the injector must route that host to the twin too. Nothing else on
|
|
163
|
+
* the upload host is modelled, so nothing else is claimed.
|
|
164
|
+
*/
|
|
165
|
+
const TIKTOK_UPLOAD_PATHS = '^/video/?$';
|
|
166
|
+
|
|
167
|
+
export const pack: TwinPack = {
|
|
168
|
+
// PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of
|
|
169
|
+
// the real state system, registered on load. A World's POST crosses at `perform`, through the
|
|
170
|
+
// Content Posting API (creator_info, init, the presigned chunk PUTs, status/fetch); nothing else
|
|
171
|
+
// does: TikTok publishes no API that creates a developer app, seeds a user or grants a scope —
|
|
172
|
+
// those are developer-portal and in-app settings actions a person takes in a browser — so
|
|
173
|
+
// `perform` settles every other entry with that reason.
|
|
174
|
+
protocol: '2',
|
|
175
|
+
// The vendor's access token lives 24 hours and its Display API limit is 600/minute, so a served
|
|
176
|
+
// world can afford a half-hourly pull; `onDemand` throttles a hand-run refresh to one a minute,
|
|
177
|
+
// well inside that budget.
|
|
178
|
+
refresh: { every: '30m', onDemand: { atMost: '60s' } },
|
|
179
|
+
stateSystem: { perform: performTikTokAction, refresh: syncTikTokFromRemote },
|
|
180
|
+
// The round trip registers a developer app through the twin's OWN door, because the vendor has no
|
|
181
|
+
// API for it at all — which is the same reason `perform` never crosses. (The Login Kit legs
|
|
182
|
+
// cannot serve as the round trip: the consent post answers a 302 and the harness fails any step
|
|
183
|
+
// answering >=300.)
|
|
184
|
+
roundTrip: {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
path: '/_twin/clients',
|
|
187
|
+
body: { client_key: 'awroundtripapp0001', client_secret: 'round-trip-secret', name: 'Round Trip', redirect_uris: ['https://round.trip.test/callback'] },
|
|
188
|
+
},
|
|
189
|
+
parityOrigin: 'http://twin',
|
|
190
|
+
// shapeParity is NOT held, and the reason is STRUCTURAL rather than a divergence to close: the
|
|
191
|
+
// refresh reads the ACCOUNT the credential names (`GET /v2/user/info/`), while the only write a
|
|
192
|
+
// blind round trip can make on this vendor is registering a developer app — which TikTok
|
|
193
|
+
// publishes no API to read back. Written and observed are different subjects by design, so there
|
|
194
|
+
// is no pair of shapes to compare. Reaching parity would need the refresh to observe an
|
|
195
|
+
// oauth_client, and no TikTok endpoint returns one.
|
|
196
|
+
vendor: 'tiktok',
|
|
197
|
+
// The SAME object tiktok-budget.ts declares at module load — one source of truth, so registering
|
|
198
|
+
// the pack and importing the connector can never arm two different ceilings.
|
|
199
|
+
rateBudget: RATE_BUDGET,
|
|
200
|
+
transport: 'rest',
|
|
201
|
+
archetype: 'crud',
|
|
202
|
+
bin: 'world-tiktok',
|
|
203
|
+
resources: [
|
|
204
|
+
'oauth_client',
|
|
205
|
+
'account',
|
|
206
|
+
'session',
|
|
207
|
+
'auth_request',
|
|
208
|
+
'grant',
|
|
209
|
+
'video',
|
|
210
|
+
'publish',
|
|
211
|
+
],
|
|
212
|
+
specSource:
|
|
213
|
+
'developers.tiktok.com, fetched 2026-09-13 — no first-party machine-readable spec exists for '
|
|
214
|
+
+ 'the TikTok open API, so the denominator was authored top-down from the published references: '
|
|
215
|
+
+ 'Login Kit for Web (the authorize URL, its parameters and the callback parameters), User '
|
|
216
|
+
+ 'Access Token Management (token / refresh / revoke request + response tables and examples), '
|
|
217
|
+
+ 'Client Access Token Management (the client_credentials grant), Login Kit for Desktop (PKCE: '
|
|
218
|
+
+ 'hex-encoded SHA-256, S256 only), the Scopes Overview, Get User Info (the field x scope '
|
|
219
|
+
+ 'table), the Video Object / Video List / Video Query references, the OAuth error-handling '
|
|
220
|
+
+ 'reference (the ten flat `error` values) and the API v2 error-handling reference (the seven '
|
|
221
|
+
+ 'nested `error.code` values with their HTTP statuses) and rate-limit reference (600/minute, '
|
|
222
|
+
+ 'one-minute sliding window, 429 + rate_limit_exceeded); and, fetched 2026-09-27, the Content '
|
|
223
|
+
+ 'Posting references (Query Creator Info, Direct Post, Upload to inbox, Get Post Status and the '
|
|
224
|
+
+ 'Media Transfer Guide: chunk rules, the upload_url PUT with Content-Range, per-token minute limits).',
|
|
225
|
+
description:
|
|
226
|
+
"TikTok twin — the real tiktok.com authorization page, the full Login Kit OAuth round trip "
|
|
227
|
+
+ '(token, refresh, revoke, client_credentials) and the Display API user/video reads with '
|
|
228
|
+
+ "TikTok's own comma-separated scopes, per-app open_id, field-level scope gating and both of "
|
|
229
|
+
+ 'its error envelopes; and the Content Posting API — creator_info, Direct Post and inbox uploads '
|
|
230
|
+
+ 'through a signed upload URL in chunks, status/fetch by World time — with a tiktok.com-style '
|
|
231
|
+
+ "profile and vertical player over the posted videos, and a perform that posts a World's video "
|
|
232
|
+
+ 'to TikTok.',
|
|
233
|
+
adoption: {
|
|
234
|
+
// TikTok publishes NO first-party npm client for Login Kit or the Display API: its own
|
|
235
|
+
// reference material is curl and raw fetch, which is exactly how the motivating application
|
|
236
|
+
// (Dub) calls it. The community `tiktok` package on npm is deliberately NOT claimed — it is
|
|
237
|
+
// not a TikTok-published client, and `adoption.sdks` is for official clients only (the bitly
|
|
238
|
+
// precedent). Interception is therefore host-based, which is why `hosts` below is the whole
|
|
239
|
+
// adoption story for this vendor.
|
|
240
|
+
sdks: [],
|
|
241
|
+
// No official Python distribution either. `tiktok-business-api-sdk` (PyPI and npm) is
|
|
242
|
+
// first-party but speaks the BUSINESS/Ads API on business-api.tiktok.com — a different half of
|
|
243
|
+
// the vendor that this pack does not serve, so claiming it would over-claim coverage.
|
|
244
|
+
pypi: [],
|
|
245
|
+
// TIKTOK_CLIENT_ID / TIKTOK_CLIENT_SECRET are exactly the pair Dub reads, and TIKTOK_CLIENT_KEY
|
|
246
|
+
// is the vendor's own name for the same value. The `TIKTOKBUSINESS` stem is deliberately left
|
|
247
|
+
// to the packless registry: it names the Business API this pack does not model.
|
|
248
|
+
envStems: ['TIKTOK'],
|
|
249
|
+
},
|
|
250
|
+
hosts: [
|
|
251
|
+
{ host: 'www.tiktok.com', pathPattern: TIKTOK_AUTHORIZE_PATHS },
|
|
252
|
+
{ host: 'open.tiktokapis.com', pathPattern: TIKTOK_API_PATHS },
|
|
253
|
+
{ host: 'open-upload.tiktokapis.com', pathPattern: TIKTOK_UPLOAD_PATHS },
|
|
254
|
+
],
|
|
255
|
+
// Dub HARDCODES both hosts — `https://open.tiktokapis.com/v2` in its TikTokClient and the
|
|
256
|
+
// authorize/token URLs in its provider table — and TikTok publishes no official SDK with a
|
|
257
|
+
// base-URL option, so there is no env var an app reads that a world could point at the twin.
|
|
258
|
+
// Inventing a TIKTOK_BASE_URL would make `covers` report the world covered while the app still
|
|
259
|
+
// talked to the real vendor; interception through the two `hosts` entries above is the whole
|
|
260
|
+
// mechanism.
|
|
261
|
+
endpointEnvNone:
|
|
262
|
+
'no app-read base-URL env exists: TikTok publishes no official SDK with a base-URL option and '
|
|
263
|
+
+ 'integrations (Dub included) hardcode www.tiktok.com and open.tiktokapis.com, so interception '
|
|
264
|
+
+ 'is the path-scoped hosts entries above.',
|
|
265
|
+
// A browser is REDIRECTED to www.tiktok.com/v2/auth/authorize — the loader host is the consent
|
|
266
|
+
// host and the prefix is the Login Kit path root.
|
|
267
|
+
browserRouting: { apiPathPrefix: '/v2/auth/', loaderHost: 'https://www.tiktok.com' },
|
|
268
|
+
};
|
|
269
|
+
// registered at import: the kernel learns the pack's state system (protocol 2)
|
|
270
|
+
registerPack(pack);
|