@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.
Files changed (76) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +310 -0
  3. package/client/tiktok-consent.tsx +154 -0
  4. package/client/tiktok-mirror.css +137 -0
  5. package/client/tiktok-mirror.tsx +492 -0
  6. package/dist/client/tiktok-consent.bundle.js +18 -0
  7. package/dist/client/tiktok-consent.d.ts +47 -0
  8. package/dist/client/tiktok-consent.js +20 -0
  9. package/dist/client/tiktok-consent.tsx +154 -0
  10. package/dist/client/tiktok-mirror.bundle.js +487 -0
  11. package/dist/client/tiktok-mirror.css +137 -0
  12. package/dist/client/tiktok-mirror.d.ts +42 -0
  13. package/dist/client/tiktok-mirror.js +315 -0
  14. package/dist/client/tiktok-mirror.tsx +492 -0
  15. package/dist/src/cli.d.ts +2 -0
  16. package/dist/src/cli.js +44 -0
  17. package/dist/src/index.d.ts +22 -0
  18. package/dist/src/index.js +167 -0
  19. package/dist/src/tiktok-blobs.d.ts +66 -0
  20. package/dist/src/tiktok-blobs.js +161 -0
  21. package/dist/src/tiktok-budget.d.ts +56 -0
  22. package/dist/src/tiktok-budget.js +136 -0
  23. package/dist/src/tiktok-capabilities.d.ts +7 -0
  24. package/dist/src/tiktok-capabilities.js +1855 -0
  25. package/dist/src/tiktok-conformance.d.ts +11 -0
  26. package/dist/src/tiktok-conformance.js +498 -0
  27. package/dist/src/tiktok-connector.d.ts +158 -0
  28. package/dist/src/tiktok-connector.js +600 -0
  29. package/dist/src/tiktok-consent-ui.d.ts +19 -0
  30. package/dist/src/tiktok-consent-ui.js +127 -0
  31. package/dist/src/tiktok-errors.d.ts +78 -0
  32. package/dist/src/tiktok-errors.js +175 -0
  33. package/dist/src/tiktok-ids.d.ts +16 -0
  34. package/dist/src/tiktok-ids.js +48 -0
  35. package/dist/src/tiktok-media.d.ts +7 -0
  36. package/dist/src/tiktok-media.js +86 -0
  37. package/dist/src/tiktok-mirror-ui.d.ts +49 -0
  38. package/dist/src/tiktok-mirror-ui.js +159 -0
  39. package/dist/src/tiktok-pkce.d.ts +25 -0
  40. package/dist/src/tiktok-pkce.js +56 -0
  41. package/dist/src/tiktok-posting.d.ts +100 -0
  42. package/dist/src/tiktok-posting.js +599 -0
  43. package/dist/src/tiktok-sample-mp4.d.ts +10 -0
  44. package/dist/src/tiktok-sample-mp4.js +55 -0
  45. package/dist/src/tiktok-scopes.d.ts +29 -0
  46. package/dist/src/tiktok-scopes.js +106 -0
  47. package/dist/src/tiktok-server.d.ts +28 -0
  48. package/dist/src/tiktok-server.js +89 -0
  49. package/dist/src/tiktok-store.d.ts +164 -0
  50. package/dist/src/tiktok-store.js +451 -0
  51. package/dist/src/tiktok-twin.d.ts +70 -0
  52. package/dist/src/tiktok-twin.js +1197 -0
  53. package/dist/src/tiktok-user.d.ts +28 -0
  54. package/dist/src/tiktok-user.js +174 -0
  55. package/package.json +74 -0
  56. package/src/cli.ts +43 -0
  57. package/src/index.ts +270 -0
  58. package/src/tiktok-blobs.ts +217 -0
  59. package/src/tiktok-budget.ts +163 -0
  60. package/src/tiktok-capabilities.ts +2022 -0
  61. package/src/tiktok-conformance.ts +526 -0
  62. package/src/tiktok-connector.ts +637 -0
  63. package/src/tiktok-consent-ui.ts +146 -0
  64. package/src/tiktok-errors.ts +197 -0
  65. package/src/tiktok-ids.ts +51 -0
  66. package/src/tiktok-journey.uitest.ts +305 -0
  67. package/src/tiktok-media.ts +89 -0
  68. package/src/tiktok-mirror-ui.ts +167 -0
  69. package/src/tiktok-pkce.ts +61 -0
  70. package/src/tiktok-posting.ts +617 -0
  71. package/src/tiktok-sample-mp4.ts +54 -0
  72. package/src/tiktok-scopes.ts +122 -0
  73. package/src/tiktok-server.ts +100 -0
  74. package/src/tiktok-store.ts +543 -0
  75. package/src/tiktok-twin.ts +1361 -0
  76. 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).