@volter/twin-tiktok 0.1.37 → 1.0.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 (202) hide show
  1. package/README.md +25 -308
  2. package/business/spec/SOURCE.md +17 -0
  3. package/business/spec/openapi.json +1045 -0
  4. package/business/spec/sources.json +84 -0
  5. package/business/src/fetch.ts +14 -0
  6. package/business/src/generated/surface.gen.json +1 -0
  7. package/business/src/generated/ui.gen.json +1 -0
  8. package/business/src/index.ts +3 -0
  9. package/business/src/manifest.ts +38 -0
  10. package/business/src/semantics/index.ts +16 -0
  11. package/business/src/semantics/open_api.ts +142 -0
  12. package/business/src/semantics/shared.ts +119 -0
  13. package/business/src/semantics/states.ts +3 -0
  14. package/dist/business/spec/SOURCE.md +17 -0
  15. package/dist/business/spec/openapi.json +1045 -0
  16. package/dist/business/spec/sources.json +84 -0
  17. package/dist/business/src/fetch.d.ts +2 -0
  18. package/dist/business/src/fetch.js +13 -0
  19. package/dist/business/src/fetch.ts +14 -0
  20. package/dist/business/src/generated/surface.gen.json +1 -0
  21. package/dist/business/src/generated/ui.gen.json +1 -0
  22. package/dist/business/src/index.d.ts +2 -0
  23. package/dist/business/src/index.js +3 -0
  24. package/dist/business/src/index.ts +3 -0
  25. package/dist/business/src/manifest.d.ts +2 -0
  26. package/dist/business/src/manifest.js +33 -0
  27. package/dist/business/src/manifest.ts +38 -0
  28. package/dist/business/src/semantics/index.d.ts +2 -0
  29. package/dist/business/src/semantics/index.js +12 -0
  30. package/dist/business/src/semantics/index.ts +16 -0
  31. package/dist/business/src/semantics/open_api.d.ts +10 -0
  32. package/dist/business/src/semantics/open_api.js +162 -0
  33. package/dist/business/src/semantics/open_api.ts +142 -0
  34. package/dist/business/src/semantics/shared.d.ts +31 -0
  35. package/dist/business/src/semantics/shared.js +109 -0
  36. package/dist/business/src/semantics/shared.ts +119 -0
  37. package/dist/business/src/semantics/states.d.ts +8 -0
  38. package/dist/business/src/semantics/states.js +3 -0
  39. package/dist/business/src/semantics/states.ts +3 -0
  40. package/dist/generated/pack-facts.json +55 -0
  41. package/dist/journeys/coverage.json +220 -0
  42. package/dist/journeys/customer-life.json +2950 -0
  43. package/dist/journeys/customer-life.outline.md +59 -0
  44. package/dist/journeys/decisions.json +161 -0
  45. package/dist/journeys/demand.json +142 -0
  46. package/dist/journeys/score.json +294 -0
  47. package/dist/journeys/unreachable.json +13 -0
  48. package/dist/open/spec/SOURCE.md +20 -0
  49. package/dist/open/spec/openapi.json +1417 -0
  50. package/dist/open/spec/sources.json +124 -0
  51. package/dist/open/src/engine/mp4.d.ts +6 -0
  52. package/dist/open/src/engine/mp4.js +55 -0
  53. package/dist/open/src/engine/mp4.ts +48 -0
  54. package/dist/open/src/fetch.d.ts +2 -0
  55. package/dist/open/src/fetch.js +17 -0
  56. package/dist/open/src/fetch.ts +18 -0
  57. package/dist/open/src/generated/surface.gen.json +1 -0
  58. package/dist/open/src/generated/ui.gen.json +1 -0
  59. package/dist/open/src/index.d.ts +2 -0
  60. package/dist/open/src/index.js +3 -0
  61. package/dist/open/src/index.ts +3 -0
  62. package/dist/open/src/manifest.d.ts +2 -0
  63. package/dist/open/src/manifest.js +53 -0
  64. package/dist/open/src/manifest.ts +59 -0
  65. package/dist/open/src/screens/authorize.d.ts +2 -0
  66. package/dist/open/src/screens/authorize.js +53 -0
  67. package/dist/open/src/screens/authorize.ts +53 -0
  68. package/dist/open/src/semantics/doors.d.ts +10 -0
  69. package/dist/open/src/semantics/doors.js +118 -0
  70. package/dist/open/src/semantics/doors.ts +121 -0
  71. package/dist/open/src/semantics/index.d.ts +2 -0
  72. package/dist/open/src/semantics/index.js +16 -0
  73. package/dist/open/src/semantics/index.ts +20 -0
  74. package/dist/open/src/semantics/oauth.d.ts +2 -0
  75. package/dist/open/src/semantics/oauth.js +30 -0
  76. package/dist/open/src/semantics/oauth.ts +28 -0
  77. package/dist/open/src/semantics/post.d.ts +6 -0
  78. package/dist/open/src/semantics/post.js +66 -0
  79. package/dist/open/src/semantics/post.ts +60 -0
  80. package/dist/open/src/semantics/shared.d.ts +51 -0
  81. package/dist/open/src/semantics/shared.js +196 -0
  82. package/dist/open/src/semantics/shared.ts +197 -0
  83. package/dist/open/src/semantics/states.d.ts +20 -0
  84. package/dist/open/src/semantics/states.js +15 -0
  85. package/dist/open/src/semantics/states.ts +23 -0
  86. package/dist/open/src/semantics/user.d.ts +2 -0
  87. package/dist/open/src/semantics/user.js +24 -0
  88. package/dist/open/src/semantics/user.ts +24 -0
  89. package/dist/open/src/semantics/video.d.ts +4 -0
  90. package/dist/open/src/semantics/video.js +112 -0
  91. package/dist/open/src/semantics/video.ts +97 -0
  92. package/dist/src/cli.js +11 -34
  93. package/dist/src/fetch.d.ts +2 -0
  94. package/dist/src/fetch.js +9 -0
  95. package/dist/src/index.d.ts +5 -22
  96. package/dist/src/index.js +13 -167
  97. package/dist/src/manifest.d.ts +2 -0
  98. package/dist/src/manifest.js +28 -0
  99. package/dist/src/semantics/shared.d.ts +49 -0
  100. package/dist/src/semantics/shared.js +61 -0
  101. package/dist/src/server.d.ts +7 -0
  102. package/dist/src/server.js +7 -0
  103. package/generated/pack-facts.json +55 -0
  104. package/journeys/coverage.json +220 -0
  105. package/journeys/customer-life.json +2950 -0
  106. package/journeys/customer-life.outline.md +59 -0
  107. package/journeys/decisions.json +161 -0
  108. package/journeys/demand.json +142 -0
  109. package/journeys/score.json +294 -0
  110. package/journeys/unreachable.json +13 -0
  111. package/open/spec/SOURCE.md +20 -0
  112. package/open/spec/openapi.json +1417 -0
  113. package/open/spec/sources.json +124 -0
  114. package/open/src/engine/mp4.ts +48 -0
  115. package/open/src/fetch.ts +18 -0
  116. package/open/src/generated/surface.gen.json +1 -0
  117. package/open/src/generated/ui.gen.json +1 -0
  118. package/open/src/index.ts +3 -0
  119. package/open/src/manifest.ts +59 -0
  120. package/open/src/screens/authorize.ts +53 -0
  121. package/open/src/semantics/doors.ts +121 -0
  122. package/open/src/semantics/index.ts +20 -0
  123. package/open/src/semantics/oauth.ts +28 -0
  124. package/open/src/semantics/post.ts +60 -0
  125. package/open/src/semantics/shared.ts +197 -0
  126. package/open/src/semantics/states.ts +23 -0
  127. package/open/src/semantics/user.ts +24 -0
  128. package/open/src/semantics/video.ts +97 -0
  129. package/package.json +13 -41
  130. package/src/cli.ts +11 -32
  131. package/src/fetch.ts +10 -0
  132. package/src/index.ts +15 -270
  133. package/src/manifest.ts +33 -0
  134. package/src/semantics/shared.ts +78 -0
  135. package/src/server.ts +8 -0
  136. package/client/tiktok-consent.tsx +0 -154
  137. package/client/tiktok-mirror.css +0 -137
  138. package/client/tiktok-mirror.tsx +0 -497
  139. package/dist/client/tiktok-consent.bundle.js +0 -18
  140. package/dist/client/tiktok-consent.d.ts +0 -47
  141. package/dist/client/tiktok-consent.js +0 -20
  142. package/dist/client/tiktok-consent.tsx +0 -154
  143. package/dist/client/tiktok-mirror.bundle.js +0 -484
  144. package/dist/client/tiktok-mirror.css +0 -137
  145. package/dist/client/tiktok-mirror.d.ts +0 -42
  146. package/dist/client/tiktok-mirror.js +0 -325
  147. package/dist/client/tiktok-mirror.tsx +0 -497
  148. package/dist/src/tiktok-blobs.d.ts +0 -66
  149. package/dist/src/tiktok-blobs.js +0 -161
  150. package/dist/src/tiktok-budget.d.ts +0 -56
  151. package/dist/src/tiktok-budget.js +0 -136
  152. package/dist/src/tiktok-capabilities.d.ts +0 -7
  153. package/dist/src/tiktok-capabilities.js +0 -1855
  154. package/dist/src/tiktok-conformance.d.ts +0 -11
  155. package/dist/src/tiktok-conformance.js +0 -498
  156. package/dist/src/tiktok-connector.d.ts +0 -158
  157. package/dist/src/tiktok-connector.js +0 -600
  158. package/dist/src/tiktok-consent-ui.d.ts +0 -19
  159. package/dist/src/tiktok-consent-ui.js +0 -127
  160. package/dist/src/tiktok-errors.d.ts +0 -78
  161. package/dist/src/tiktok-errors.js +0 -175
  162. package/dist/src/tiktok-ids.d.ts +0 -16
  163. package/dist/src/tiktok-ids.js +0 -48
  164. package/dist/src/tiktok-media.d.ts +0 -7
  165. package/dist/src/tiktok-media.js +0 -86
  166. package/dist/src/tiktok-mirror-ui.d.ts +0 -61
  167. package/dist/src/tiktok-mirror-ui.js +0 -187
  168. package/dist/src/tiktok-pkce.d.ts +0 -25
  169. package/dist/src/tiktok-pkce.js +0 -56
  170. package/dist/src/tiktok-posting.d.ts +0 -100
  171. package/dist/src/tiktok-posting.js +0 -599
  172. package/dist/src/tiktok-sample-mp4.d.ts +0 -10
  173. package/dist/src/tiktok-sample-mp4.js +0 -55
  174. package/dist/src/tiktok-scopes.d.ts +0 -29
  175. package/dist/src/tiktok-scopes.js +0 -106
  176. package/dist/src/tiktok-server.d.ts +0 -28
  177. package/dist/src/tiktok-server.js +0 -99
  178. package/dist/src/tiktok-store.d.ts +0 -164
  179. package/dist/src/tiktok-store.js +0 -451
  180. package/dist/src/tiktok-twin.d.ts +0 -70
  181. package/dist/src/tiktok-twin.js +0 -1197
  182. package/dist/src/tiktok-user.d.ts +0 -28
  183. package/dist/src/tiktok-user.js +0 -174
  184. package/src/tiktok-blobs.ts +0 -217
  185. package/src/tiktok-budget.ts +0 -163
  186. package/src/tiktok-capabilities.ts +0 -2022
  187. package/src/tiktok-conformance.ts +0 -526
  188. package/src/tiktok-connector.ts +0 -637
  189. package/src/tiktok-consent-ui.ts +0 -146
  190. package/src/tiktok-errors.ts +0 -197
  191. package/src/tiktok-ids.ts +0 -51
  192. package/src/tiktok-journey.uitest.ts +0 -305
  193. package/src/tiktok-media.ts +0 -89
  194. package/src/tiktok-mirror-ui.ts +0 -193
  195. package/src/tiktok-pkce.ts +0 -61
  196. package/src/tiktok-posting.ts +0 -617
  197. package/src/tiktok-sample-mp4.ts +0 -54
  198. package/src/tiktok-scopes.ts +0 -122
  199. package/src/tiktok-server.ts +0 -110
  200. package/src/tiktok-store.ts +0 -543
  201. package/src/tiktok-twin.ts +0 -1361
  202. package/src/tiktok-user.ts +0 -137
package/README.md CHANGED
@@ -1,310 +1,27 @@
1
1
  # @volter/twin-tiktok
2
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).
3
+ The TikTok twin (Protocol 3), serving what Postiz calls ([journeys/demand.json](./journeys/demand.json),
4
+ [journeys/decisions.json](./journeys/decisions.json)). TikTok publishes no OpenAPI document of these APIs; the open
5
+ lane's spec is written from developers.tiktok.com's reference pages (`open/spec/SOURCE.md`).
6
+
7
+ - **The open lane** (open.tiktokapis.com, open-upload.tiktokapis.com, www.tiktok.com/v2/auth/authorize/):
8
+ - the authorize page, where a TikTok person logs in and consents; the redirect carries `code`, `scopes`, `state`;
9
+ - the token (a code once, a refresh token once; `act.`/`rft.` tokens drawn from the World's secret, kept by SHA-256);
10
+ - user info (fields gated by scope; `video_count` counted from the person's public videos) and creator info;
11
+ - posting: a video directly or to the creator's inbox by chunked upload (the chunk plan checked, the bytes kept as
12
+ blobs), photos pulled from URLs the app verified (through the World: a URL it does not serve fails the pull),
13
+ and the post's status, moved only by `src/semantics/states.ts`;
14
+ - the videos a direct post makes (the Video Object; duration and size read from the MP4's boxes, the cover a
15
+ labeled placeholder), listed newest first by cursor and queried by id.
16
+ - **The business lane** (business-api.tiktok.com, `business/spec/SOURCE.md`): Postiz's TikTok Business provider.
17
+ Every answer is HTTP 200 with TikTok's `code` (0, or 40001/40002/40102/40104/40105/40107/40110/40131):
18
+ - the business app's token (the same authorize page, a code once, a refresh token once, 24-hour access tokens);
19
+ - the account's profile and analytics (`fields` a JSON array) and its public posts' counts, paged by cursor;
20
+ - publishing a video or photos by URL: TikTok pulls them through the World, so a URL nothing in the World serves
21
+ fails the pull (FAILED, `video_pull_failed`/`photo_pull_failed`); the status moves only by `src/semantics/states.ts`;
22
+ - the locations a post may tag and the Commercial Music Library's tracks, searched and listed by genre.
23
+
24
+ The developer site's app, its redirect URIs and URL properties, TikTok's people and a video's audience are the World's
25
+ doors (`POST /_twin/app-credentials`, `/_twin/apps` with `redirect_uris` and `business_redirect_uris`, `/_twin/people`,
26
+ `/_twin/videos/{id}/stats`, `/_twin/locations`, `/_twin/tracks`). The credential door answers TIKTOK_CLIENT_ID,
27
+ TIKTOK_CLIENT_SECRET, TIKTOK_BUSINESS_CLIENT_ID and TIKTOK_BUSINESS_CLIENT_SECRET.
@@ -0,0 +1,17 @@
1
+ # TikTok API for Business v1.3: the Accounts API and Discovery
2
+
3
+ - **Why this document:** TikTok publishes no OpenAPI document of these endpoints (its SDK repository's yml files,
4
+ tiktok/tiktok-business-api-sdk @ f809c39652, are the Marketing API's). This document is written from the v1.3
5
+ documentation, one operation per endpoint. The portal (business-api.tiktok.com/portal/docs) renders in the browser;
6
+ each page's text is its docs gateway's answer, which each operation names (`externalDocs`) and the pack cites:
7
+ `https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=<v1.3>&doc_id=<page>&language=ENGLISH`
8
+ (the v1.3 key `c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b`, from the portal's own bundle). Pages: Accounts API Authentication (1738084387220481),
9
+ Authorization (1738083939371009), Get profile data (1762228399168514), Get post data (1762228421622786), Publish a
10
+ public video post (1762228496095234), Publish a photo post (1803630424390658), Get the publishing status
11
+ (1816387106635778), Get the location tags (1870580030749698), Get popular tracks from the Commercial Music Library
12
+ (1825119063013505), Get TikTok account permission scopes (1765927978092545), Appendix - Return codes
13
+ (1737172488964097), fetched 2026-09-30.
14
+ - **Its host:** business-api.tiktok.com.
15
+ - **Corrections:** none.
16
+ - **Read by:** the twin orchestrator, 2026-09-30.
17
+ - **Vendored bytes (sha256):** `openapi.json` `ad0a7a19e49fba66c359b3ef12dbe8daeb112a36e1cc0a14f0e28f94fb26d4f9`.