@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
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# @volter/twin-tiktok
|
|
2
|
+
|
|
3
|
+
A local, stateful, vendor-faithful twin of **TikTok's Login Kit, Display API and Content Posting
|
|
4
|
+
API** — the tiktok.com authorization page, the full OAuth round trip (token, refresh, revoke,
|
|
5
|
+
client_credentials), `GET /v2/user/info/` + `POST /v2/video/list/` + `POST /v2/video/query/`
|
|
6
|
+
with TikTok's real field selection, scope gating and error envelopes, and a creator's video posted
|
|
7
|
+
the way TikTok takes it: `creator_info`, the Direct Post and inbox inits, the chunked `PUT` to a
|
|
8
|
+
signed upload URL, and `status/fetch` walking the publish by World time.
|
|
9
|
+
|
|
10
|
+
**First, register your app.** TikTok publishes no API that creates a developer app, so the twin
|
|
11
|
+
cannot guess your `client_key`: a world must `POST /_twin/clients` with the key, secret and
|
|
12
|
+
redirect URI its application uses *before* the authorize leg will answer anything but
|
|
13
|
+
`invalid_client`. That one seeding call is the whole setup — after it, point an unmodified TikTok
|
|
14
|
+
integration at the twin and complete a sign-in-with-TikTok flow offline.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
curl -X POST "$TIKTOK_TWIN/_twin/clients" -H 'content-type: application/json' \
|
|
18
|
+
-d '{"client_key":"'"$TIKTOK_CLIENT_ID"'","client_secret":"'"$TIKTOK_CLIENT_SECRET"'",
|
|
19
|
+
"name":"My App","redirect_uris":["https://my.app/api/auth/callback/tiktok"]}'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun run packages/twin/tiktok/src/cli.ts mirror
|
|
24
|
+
# tiktok twin (Login Kit OAuth + Display API) at http://127.0.0.1:54321
|
|
25
|
+
# authorization page: http://127.0.0.1:54321/v2/auth/authorize?client_key=…
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Five things this vendor does that its OAuth siblings do not
|
|
29
|
+
|
|
30
|
+
This pack exists next to `googleoauth` and `xidentity` because TikTok's protocol differs from both
|
|
31
|
+
in ways that break code copied from either:
|
|
32
|
+
|
|
33
|
+
1. **Scopes are COMMA-separated** (`scope=user.info.basic,user.info.profile`), not space-separated.
|
|
34
|
+
A space-separated string is one unknown scope here, and the twin refuses it as `invalid_scope`.
|
|
35
|
+
2. **Consent is GRANULAR.** The documented callback carries a `scopes` parameter — "the
|
|
36
|
+
authorization scope(s) which the user has granted" — so the granted set may be a SUBSET of what
|
|
37
|
+
was requested. The authorization page this twin serves offers a checkbox per requested scope,
|
|
38
|
+
and unchecking one narrows the token and makes the Display API refuse the field that scope gated.
|
|
39
|
+
3. **PKCE is OPTIONAL, and its challenge is HEX SHA-256.** TikTok requires PKCE for desktop apps
|
|
40
|
+
only (web apps, Dub included, send none), and when it is used the `code_challenge` is the
|
|
41
|
+
64-character hex digest — *not* RFC 7636's base64url. A helper copied from an X or Google
|
|
42
|
+
integration produces a challenge the real vendor rejects, and so does this twin.
|
|
43
|
+
4. **The client authenticates in the BODY.** `client_key` + `client_secret` form fields; TikTok
|
|
44
|
+
documents no HTTP Basic alternative, so an `Authorization` header on the token endpoint is
|
|
45
|
+
ignored rather than honoured. (Dub sends one — its callback route is shared with a Basic-auth
|
|
46
|
+
provider — and the exchange must still succeed.)
|
|
47
|
+
5. **`open_id` is PER APP, `union_id` is per human.** The same account authorizing two apps yields
|
|
48
|
+
two different `open_id`s and one `union_id`, and re-authorizing the same app yields the SAME
|
|
49
|
+
`open_id`. An app that keys its user records on `open_id` must not see a new person each login.
|
|
50
|
+
|
|
51
|
+
There are **two error envelopes**, not one: the OAuth endpoints answer the flat
|
|
52
|
+
`{error, error_description, log_id}` body and the v2 API answers the nested
|
|
53
|
+
`{data, error:{code, message, log_id}}` one — which is present on SUCCESS too, with
|
|
54
|
+
`code: "ok"`. The `log_id` is deterministic here (14 digits of UTC `yyyyMMddHHmmss` plus 20
|
|
55
|
+
uppercase hex, the vendor's own shape), because a served byte may not carry a clock or entropy.
|
|
56
|
+
|
|
57
|
+
It is a **browser-facing protocol pack** (the `googleoauth` class): the authorization page is
|
|
58
|
+
served as HTML at the vendor's real path (`https://www.tiktok.com/v2/auth/authorize`),
|
|
59
|
+
server-rendered from the twin's own kernel projection by the **same React components** the pack
|
|
60
|
+
ships — one renderer, so API↔UI parity cannot drift. The whole decision is a plain `<form>` with a
|
|
61
|
+
checkbox per scope and two submit buttons, so **no JavaScript is required to complete an OAuth
|
|
62
|
+
round trip** against this twin: the pack ships no browser bundle, the stylesheet is inlined, and
|
|
63
|
+
`curl`, redirect-following HTTP libraries and headless browsers all behave identically.
|
|
64
|
+
|
|
65
|
+
## Posting a video
|
|
66
|
+
|
|
67
|
+
The Content Posting API as its references describe it (developers.tiktok.com, fetched
|
|
68
|
+
2026-09-27), driven with nothing but `curl`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# a creator and a token that may post (the twin's own doors: TikTok has no API for either)
|
|
72
|
+
curl -X POST "$TIKTOK_TWIN/_twin/accounts" -H 'content-type: application/json' \
|
|
73
|
+
-d '{"union_id":"11111111-2222-4333-8444-555555555555","username":"volter","display_name":"Volter"}'
|
|
74
|
+
curl -X POST "$TIKTOK_TWIN/_twin/tokens" -H 'content-type: application/json' \
|
|
75
|
+
-d '{"union_id":"11111111-2222-4333-8444-555555555555","scopes":["user.info.basic","user.info.stats","video.list","video.publish"]}'
|
|
76
|
+
|
|
77
|
+
curl -X POST "$TIKTOK_TWIN/v2/post/publish/creator_info/query/" -H "Authorization: Bearer $TOKEN"
|
|
78
|
+
curl -X POST "$TIKTOK_TWIN/v2/post/publish/video/init/" -H "Authorization: Bearer $TOKEN" \
|
|
79
|
+
-H 'content-type: application/json; charset=UTF-8' \
|
|
80
|
+
-d '{"post_info":{"title":"Release #volter","privacy_level":"PUBLIC_TO_EVERYONE"},
|
|
81
|
+
"source_info":{"source":"FILE_UPLOAD","video_size":14904527,"chunk_size":5242880,"total_chunk_count":2}}'
|
|
82
|
+
# → {"data":{"publish_id":"v_pub_file~v2-1.…","upload_url":"$TIKTOK_TWIN/video/?upload_id=…&upload_token=…"}, …}
|
|
83
|
+
curl -X PUT "$UPLOAD_URL" -H 'Content-Type: video/mp4' -H 'Content-Range: bytes 0-5242879/14904527' --data-binary @chunk1 # 206
|
|
84
|
+
curl -X PUT "$UPLOAD_URL" -H 'Content-Type: video/mp4' -H 'Content-Range: bytes 5242880-14904526/14904527' --data-binary @chunk2 # 201
|
|
85
|
+
curl -X POST "$TIKTOK_TWIN/v2/post/publish/status/fetch/" -H "Authorization: Bearer $TOKEN" -d '{"publish_id":"…"}'
|
|
86
|
+
# → PROCESSING_UPLOAD, then PUBLISH_COMPLETE with "publicaly_available_post_id":[7063961975802954486]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **The chunk rules are TikTok's**: a video under 5 MB is one whole chunk; chunks are 5–64 MB;
|
|
90
|
+
`total_chunk_count` is `video_size / chunk_size` rounded down, the final chunk carrying the rest
|
|
91
|
+
(up to 128 MB); a video over 64 MB takes more than one chunk; at most 1000 chunks and 4 GB. Chunks
|
|
92
|
+
go in order: out of order is 416, a size that disagrees with the plan is 400, and each chunk answers
|
|
93
|
+
206 until the last answers 201. **The twin's own limit is lower, and says so**: it holds a video
|
|
94
|
+
whole when the last chunk lands (the kernel's blob seam stores bytes, not a stream), so an init
|
|
95
|
+
over 512 MiB is refused with `invalid_param` naming that limit.
|
|
96
|
+
- **`privacy_level` is the creator's choice from creator_info**: missing, or not one of their
|
|
97
|
+
`privacy_level_options`, is `privacy_level_option_mismatch` (403).
|
|
98
|
+
- **The upload URL is signed, not looked up.** Like TikTok's, it is a different address from the
|
|
99
|
+
API — `/video/` under the twin's public base, or `https://open-upload.tiktokapis.com/video/` when
|
|
100
|
+
the request came through a World's injector naming `open.tiktokapis.com` (the pack claims that
|
|
101
|
+
host, so the PUT routes back here) — and its `upload_token` is an HMAC over the upload and its
|
|
102
|
+
expiry, keyed by that upload's own secret drawn from entropy at init. The PUT carries no bearer; a
|
|
103
|
+
token altered by a character is 403, and so is the URL after its hour. Publish ids, upload ids and
|
|
104
|
+
post ids are drawn from entropy too, so a World cloned from another never reissues the origin's.
|
|
105
|
+
- **The lifecycle is World time**: `PROCESSING_UPLOAD` while chunks arrive and for the twin's
|
|
106
|
+
processing interval after (2 s plus 1 s per 10 MiB — TikTok publishes none), then
|
|
107
|
+
`PUBLISH_COMPLETE` (or `SEND_TO_USER_INBOX` for an inbox upload, which posts nothing to the
|
|
108
|
+
profile). The Direct Post appears in `/v2/video/list/` from that instant. What TikTok's
|
|
109
|
+
processing checks and the file's own headers can show fails the publish (`FAILED`, nothing
|
|
110
|
+
posted): bytes that are no readable video (`file_format_check_failed`), longer than the creator's
|
|
111
|
+
maximum (`duration_check_failed`), outside 23–60 FPS (`frame_rate_check_failed`) or outside
|
|
112
|
+
360–4096 px (`picture_size_check_failed`). `publicaly_available_post_id`
|
|
113
|
+
is written as bare int64 JSON numbers, as TikTok writes them — so a client that `JSON.parse`s a
|
|
114
|
+
19-digit id into a double loses digits here, not in production.
|
|
115
|
+
- **Each endpoint meters its own minute** at its reference's figure: creator_info 20, the inits 6,
|
|
116
|
+
status/fetch 30, per user token.
|
|
117
|
+
- **The bytes are the bytes.** Chunks are staged one key each and joined once, when the last lands;
|
|
118
|
+
the video is stored content-addressed on the kernel's blob seam and read back through its
|
|
119
|
+
resource-blob helpers, so a branch plays its ancestors' posts. `GET /_twin/media/video/<post id>`
|
|
120
|
+
(under the twin's public base) serves it with Range — 206, 416 past the end, an open-ended range
|
|
121
|
+
capped at 8 MiB, an inverted range ignored as RFC 9110 says (200) — and a post that is not
|
|
122
|
+
`PUBLIC_TO_EVERYONE` plays only with its creator's `access_token`.
|
|
123
|
+
- **No credential is kept in the clear.** Access, refresh and client tokens, authorization codes
|
|
124
|
+
and rate windows are the twin's `_`-prefixed bookkeeping (never deployed, never pushed), each keyed
|
|
125
|
+
by the SHA-256 of its credential; an app's client secret, a publish's token and an upload's token
|
|
126
|
+
are kept as hashes; an upload's signing secret is its own. An app registered through
|
|
127
|
+
`/_twin/clients` without a secret gets one drawn from entropy, answered once. A presented credential is hashed and
|
|
128
|
+
looked up.
|
|
129
|
+
|
|
130
|
+
The one kernel write is the finalize: `video.publish` (the video on the profile, and its `publish`
|
|
131
|
+
row) or `video.inbox_upload`. Staging is never a kernel action, so an abandoned upload leaves nothing
|
|
132
|
+
for a deploy to trip on. **Deploying** that entry to a real TikTok root performs the same flow
|
|
133
|
+
against the vendor (see *The three operations*).
|
|
134
|
+
|
|
135
|
+
## The mirror: tiktok.com over the twin's state
|
|
136
|
+
|
|
137
|
+
A creator posts and reads their profile on tiktok.com, so the pack ships tiktok.com's view of what
|
|
138
|
+
was posted — a React app over the twin's own Display API, on the twin's origin:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bun run packages/twin/tiktok/src/cli.ts mirror
|
|
142
|
+
# mirror (tiktok.com profile + player): http://127.0.0.1:54321/
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- **Log in** as tiktok.com's form asks — the username, then, in place of the password, the access
|
|
146
|
+
token the twin issued for that creator (`POST /_twin/tokens`). A token for another account is
|
|
147
|
+
refused on the form; both stay in the tab's sessionStorage.
|
|
148
|
+
- **The profile**: the avatar (the creator's initial — the twin holds no image), `@username` with the
|
|
149
|
+
verified tick, the nickname, Following / Followers / Likes **only as `/v2/user/info/` returns them**
|
|
150
|
+
(a token without `user.info.stats` or a persona seeded without counts draws none), the bio, and the
|
|
151
|
+
Videos tab's 9:16 grid of the creator's posts, newest first, each tile the post's own first frame
|
|
152
|
+
and its play count.
|
|
153
|
+
- **The player**: a post opens full height and vertical, playing its bytes from
|
|
154
|
+
`/_twin/media/video/<id>` (Range, the creator's token on the URL) over a blurred copy of itself,
|
|
155
|
+
with the `@handle`, the caption and its hashtags, the sound line, the action rail carrying the
|
|
156
|
+
Video Object's own like/comment/share counts, and up/down to the neighbouring posts.
|
|
157
|
+
|
|
158
|
+
Inside a World it is mounted at `/<org>/<world>/tiktok/mirror/`: the shell (`tiktokMirrorHtml`) has
|
|
159
|
+
`<base href="/">` and relative `assets/`, the client (`buildTiktokMirrorClient`) routes on `#/`, and
|
|
160
|
+
its reads go to the keyed wire with the browser's World session.
|
|
161
|
+
|
|
162
|
+
## Coverage
|
|
163
|
+
|
|
164
|
+
Partial and honest. The manifest (`src/tiktok-capabilities.ts`) is the **Login Kit + Display API +
|
|
165
|
+
Content Posting** denominator — the surface a user access token reaches — and it also carries
|
|
166
|
+
TikTok's other open-platform products (Research, Data Portability, Local Services, the mobile/QR
|
|
167
|
+
login variants, the photo post) and the separate business-api.tiktok.com Ads API as honest `todo`s
|
|
168
|
+
rather than silent omissions.
|
|
169
|
+
|
|
170
|
+
It currently reads **142 done / 218 covered** (76 `todo`, across 25 areas) — and the percentage is
|
|
171
|
+
flattering, so here is the magnitude of what discounts it, measured rather than hand-waved:
|
|
172
|
+
|
|
173
|
+
- **The eleven endpoints this pack serves are enumerated per BEHAVIOUR** (a parameter honoured, an
|
|
174
|
+
error produced, a field gated) — the six Login Kit + Display endpoints at about 19 rows apiece,
|
|
175
|
+
Content Posting at 14 `done` plus 8 pinning/model `todo`s — **while the product families it does
|
|
176
|
+
not serve are enumerated per OPERATION FAMILY**, about 3 rows apiece. Modelling one of them would
|
|
177
|
+
add a `done` per behaviour where a few `todo`s sit today.
|
|
178
|
+
- **16 of the 142 `done` rows never issue a request at the twin's wire at all**: three compare the
|
|
179
|
+
twin's copy of TikTok's published error/scope tables against a literal, twelve drive the
|
|
180
|
+
connector's pricing, mapping and budget over injected executors, and one checks the mirror
|
|
181
|
+
module's exports build. They are real claims, but they say nothing about what the served HTTP
|
|
182
|
+
surface does.
|
|
183
|
+
- **25 of the 142 assert an HTTP status on the flat OAuth envelope that TikTok publishes no status
|
|
184
|
+
column for.** Its error reference lists the ten `error` values and their descriptions and stops
|
|
185
|
+
there; the twin answers RFC 6749 §5.2's 400/401, and `tiktok.token.error_http_status` is the
|
|
186
|
+
`todo` that pins the live ones. If the vendor turns out to answer 200 with an error body — as
|
|
187
|
+
some OAuth implementations do — those 25 rows are asserting the wrong half of the wire.
|
|
188
|
+
|
|
189
|
+
The honest correction is to expand the unserved families as each is read from its own reference,
|
|
190
|
+
and to close the pinning `todo`s against the live vendor — never to pad the denominator now with
|
|
191
|
+
rows nobody has read the docs for.
|
|
192
|
+
|
|
193
|
+
### No first-party spec — the denominator is hand-authored
|
|
194
|
+
|
|
195
|
+
TikTok publishes **no** machine-readable specification for the open API: no OpenAPI, no SDL, no
|
|
196
|
+
Discovery document. Its reference material is prose, curl examples and hand-written tables, and its
|
|
197
|
+
one first-party generated SDK (`tiktok-business-api-sdk`) speaks the *Business/Ads* API on a
|
|
198
|
+
different host. So the denominator was authored top-down from the published references, all fetched
|
|
199
|
+
**2026-09-13**, and every capability cites the one that grounds it: Login Kit for Web and for
|
|
200
|
+
Desktop; User Access Token Management and Client Access Token Management; the Scopes Overview; Get
|
|
201
|
+
User Info; the Video Object / Video List / Video Query references; the OAuth and API-v2
|
|
202
|
+
error-handling references; and the rate-limit reference. `census.json`'s `spec` slice records that
|
|
203
|
+
trail in full.
|
|
204
|
+
|
|
205
|
+
The `todo`s are real TikTok surface this twin does not model — most notably the **photo post** and
|
|
206
|
+
**PULL_FROM_URL** (both need the developer portal's verified domains), the **webhook events page** (`authorization.removed`
|
|
207
|
+
with its reason enum, `video.publish.completed`, `video.upload.failed`,
|
|
208
|
+
`portability.download.ready`, and delivery/signature itself), the public unauthenticated
|
|
209
|
+
**oEmbed** read, **Share Kit**, the **Research API**, **Data Portability**, the **Mini Games /
|
|
210
|
+
Mini Dramas / TikTok GO** product families, and the **Business/Ads API** — plus a family of
|
|
211
|
+
**wire-pinning todos**: behaviours
|
|
212
|
+
the twin models from RFC 6749/7009 or from a widely-reported capture because no fetched official
|
|
213
|
+
artefact states them (the HTTP status of an OAuth failure, the authorization-code format and
|
|
214
|
+
lifetime, whether the live vendor always rotates a refresh token, the authorize error page, the
|
|
215
|
+
live consent sheet's affordances, the dimension the 600/minute limit is enforced on). Each such
|
|
216
|
+
`done` names its evidence boundary in the manifest and its pinning `todo`.
|
|
217
|
+
|
|
218
|
+
### What is real here
|
|
219
|
+
|
|
220
|
+
- **The complete round trip.** Authorize request → authorization page → 302 to `redirect_uri` with
|
|
221
|
+
exactly `code`, `scopes` and `state` (the documented callback parameters) → the code is
|
|
222
|
+
redeemable **exactly once** → `refresh_token` grant (rotating) → revoke kills the whole (app,
|
|
223
|
+
user) grant. One pack against one state root, so the code minted at the screen is honoured at the
|
|
224
|
+
token endpoint by construction.
|
|
225
|
+
- **`disable_auto_auth`**, modelled: with `0` and an existing grant covering the request, the page
|
|
226
|
+
is skipped and a code bounces straight back; a first authorization or a widened scope set still
|
|
227
|
+
shows it. The default (parameter absent) shows the page — bypassing the human leg is the thing
|
|
228
|
+
this pack exists to make visible.
|
|
229
|
+
- **Exact callback matching** — a trailing-slash variant is a different Redirect URI, so
|
|
230
|
+
`redirect_uri`-mismatch bugs reproduce, on the vendor's own error page, with the app never
|
|
231
|
+
reached.
|
|
232
|
+
- **The Display API reads** — `fields` selection over the documented 14 user fields and 16 video
|
|
233
|
+
fields, per-scope field gating (`scope_not_authorized`, 401), `max_count` defaulting to 10 and
|
|
234
|
+
capped at 20, millisecond cursor pagination, the 20-id query limit, and a foreign video id
|
|
235
|
+
omitted rather than 404'd.
|
|
236
|
+
- **The documented rate limit** — a one-minute sliding window at 600, enforced separately per
|
|
237
|
+
endpoint, answering HTTP 429 with `rate_limit_exceeded`. TikTok publishes no rate-limit response
|
|
238
|
+
headers, so the twin invents none.
|
|
239
|
+
- **Vendor-shaped opaque credentials** — `act.` / `rft.` / `clt.` prefixes, and an authorization
|
|
240
|
+
code carrying the characters that make the documented "URL decoded" requirement observable: the
|
|
241
|
+
raw callback text does not redeem, the decoded one does.
|
|
242
|
+
- **Fidelity over real transport** — `tiktok-sdk.integration.test.ts` drives the twin through a
|
|
243
|
+
socket with the exact calls Dub's unmodified source makes (TikTok publishes no official npm
|
|
244
|
+
client, so ADDING_A_TWIN.md §10's real-transport `fetch` alternative applies).
|
|
245
|
+
|
|
246
|
+
### What is not
|
|
247
|
+
|
|
248
|
+
The twin **authenticates nobody**: no password, no 2FA, no risk engine. The tiktok.com "signed in"
|
|
249
|
+
session is a seeded persona row — `POST /_twin/session` switches it, which is the honest local
|
|
250
|
+
equivalent of "the browser is already signed in". There is also no TikTok API that creates a
|
|
251
|
+
developer app, so a world running a real integration registers its own `client_key` through
|
|
252
|
+
`POST /_twin/clients`; the seeded `Twin Demo App` is a convenience, not a claim.
|
|
253
|
+
|
|
254
|
+
### Twin-only scaffolding (not vendor surface, not counted)
|
|
255
|
+
|
|
256
|
+
`GET /_twin/consent` (re-render a pending authorization page by request handle) and
|
|
257
|
+
`POST /_twin/consent` (the Continue/Cancel form post — TikTok's real sheet posts to an undocumented
|
|
258
|
+
internal endpoint), `POST /_twin/clients`, `POST /_twin/accounts` (also what creator_info answers:
|
|
259
|
+
`is_private`, `privacy_level_options`, the comment/duet/stitch switches,
|
|
260
|
+
`max_video_post_duration_sec`), `POST /_twin/videos` (a metadata-only video for a Display API
|
|
261
|
+
fixture; a video with its media is posted through the Content Posting API), `POST /_twin/tokens`
|
|
262
|
+
(a user access token with the named scopes — what a World issues in place of the consent leg),
|
|
263
|
+
`POST /_twin/session`, `POST /_twin/rate_limit` (arm a deterministic 429) and
|
|
264
|
+
`GET /_twin/media/video/<post id>` (a published video's bytes; TikTok's CDN is not Open API
|
|
265
|
+
surface). This list is the audit trail the conformance
|
|
266
|
+
census deliberately excludes — keep it in lockstep with `twinControl`'s branches in
|
|
267
|
+
`tiktok-twin.ts`.
|
|
268
|
+
|
|
269
|
+
## The three operations
|
|
270
|
+
|
|
271
|
+
- **pull** — `syncTikTokFromReal(execute)` observes the real account behind the operator's own user
|
|
272
|
+
token (`GET /v2/user/info/`, every modelled field) over an **injected executor**, and their
|
|
273
|
+
videos when `videos: true` (the video reads need the separate `video.list` scope, so they are
|
|
274
|
+
opt-in rather than silently skipped). Live runs use `liveTikTokExecute(accessToken)`, the ONE
|
|
275
|
+
place a real TikTok request may be issued, guarded by the fail-closed rate budget
|
|
276
|
+
(`tiktok-budget.ts`: TikTok's own 600-per-minute scheme, live-fetched 2026-09-13). The app
|
|
277
|
+
registry and the token's scope set are **not observable** (no API reads TikTok apps; TikTok has
|
|
278
|
+
no token introspection) — reasoned gaps, not fakes. Accounts are keyed by `union_id`, so one
|
|
279
|
+
human is one twin account however many apps they authorize.
|
|
280
|
+
- **write** — the default: local consent flows mint local credentials and local posts, no real calls.
|
|
281
|
+
- **perform** — a World's post crosses when its root is TikTok: `performTikTokAction` sends a
|
|
282
|
+
`video.publish` entry through the vendor's own Direct Post flow (creator_info, whose
|
|
283
|
+
`privacy_level_options` must include the entry's privacy level; `video/init` with the chunk plan
|
|
284
|
+
TikTok's rules require; each chunk `PUT` to the returned `upload_url` **presigned** — without the
|
|
285
|
+
sealed credential, on the upload host; `status/fetch` until `PUBLISH_COMPLETE`), and a
|
|
286
|
+
`video.inbox_upload` through the inbox init to `SEND_TO_USER_INBOX`. The bytes come off the blob
|
|
287
|
+
seam a chunk at a time. The pack budget wraps the executor with one ledger per sealed credential
|
|
288
|
+
(its keyed fingerprint — every World posting as one account spends one allowance), per World root
|
|
289
|
+
only when none is sealed:
|
|
290
|
+
creator_info, the init and every status read are charged (at 600 over each reference's per-minute
|
|
291
|
+
figure), an answer that arrived is never dropped, and the chunk `PUT`s are not charged — once a
|
|
292
|
+
publish has started, the budget cannot stop it. The wait reads status every 3 s and stops at
|
|
293
|
+
120 s, failing retryably — as does a 429 or 5xx from TikTok, or a chunk TikTok does not take; the
|
|
294
|
+
publish it opened and its upload URL are kept in this World's byte annex (never an entry, never a
|
|
295
|
+
changeset), so the next deploy asks that publish where it stands and resumes its missing chunks
|
|
296
|
+
instead of posting twice. It opens a fresh publish only when TikTok says the old one is gone
|
|
297
|
+
(`invalid_publish_id`, or `FAILED`): `uploaded_bytes` is a count of what arrived, and no count
|
|
298
|
+
means TikTok will not still post it. Nothing else
|
|
299
|
+
crosses: apps, users, tokens and grants have no TikTok write API, and each entry says so.
|
|
300
|
+
|
|
301
|
+
## Serving
|
|
302
|
+
|
|
303
|
+
`world-tiktok serve` starts one server for the WHOLE surface; point `www.tiktok.com` (the
|
|
304
|
+
authorization path only), `open.tiktokapis.com` (the Login Kit, Display and Content Posting paths
|
|
305
|
+
only) and `open-upload.tiktokapis.com` (`/video/` only) at it — the pack descriptor's path-scoped
|
|
306
|
+
`hosts` entries do exactly that, so an unmodelled TikTok call refuses loudly instead of landing in a
|
|
307
|
+
twin that cannot serve it. `mirror` is the same twin with the tiktok.com mirror at `/`, and prints a
|
|
308
|
+
ready-to-open authorization URL for the authorization page too; `conformance` runs the endpoint
|
|
309
|
+
census (one real probe per claimed endpoint + the hand-enumerated router surface + a reachability
|
|
310
|
+
witness per resource type).
|