@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.
- package/README.md +25 -308
- package/business/spec/SOURCE.md +17 -0
- package/business/spec/openapi.json +1045 -0
- package/business/spec/sources.json +84 -0
- package/business/src/fetch.ts +14 -0
- package/business/src/generated/surface.gen.json +1 -0
- package/business/src/generated/ui.gen.json +1 -0
- package/business/src/index.ts +3 -0
- package/business/src/manifest.ts +38 -0
- package/business/src/semantics/index.ts +16 -0
- package/business/src/semantics/open_api.ts +142 -0
- package/business/src/semantics/shared.ts +119 -0
- package/business/src/semantics/states.ts +3 -0
- package/dist/business/spec/SOURCE.md +17 -0
- package/dist/business/spec/openapi.json +1045 -0
- package/dist/business/spec/sources.json +84 -0
- package/dist/business/src/fetch.d.ts +2 -0
- package/dist/business/src/fetch.js +13 -0
- package/dist/business/src/fetch.ts +14 -0
- package/dist/business/src/generated/surface.gen.json +1 -0
- package/dist/business/src/generated/ui.gen.json +1 -0
- package/dist/business/src/index.d.ts +2 -0
- package/dist/business/src/index.js +3 -0
- package/dist/business/src/index.ts +3 -0
- package/dist/business/src/manifest.d.ts +2 -0
- package/dist/business/src/manifest.js +33 -0
- package/dist/business/src/manifest.ts +38 -0
- package/dist/business/src/semantics/index.d.ts +2 -0
- package/dist/business/src/semantics/index.js +12 -0
- package/dist/business/src/semantics/index.ts +16 -0
- package/dist/business/src/semantics/open_api.d.ts +10 -0
- package/dist/business/src/semantics/open_api.js +162 -0
- package/dist/business/src/semantics/open_api.ts +142 -0
- package/dist/business/src/semantics/shared.d.ts +31 -0
- package/dist/business/src/semantics/shared.js +109 -0
- package/dist/business/src/semantics/shared.ts +119 -0
- package/dist/business/src/semantics/states.d.ts +8 -0
- package/dist/business/src/semantics/states.js +3 -0
- package/dist/business/src/semantics/states.ts +3 -0
- package/dist/generated/pack-facts.json +55 -0
- package/dist/journeys/coverage.json +220 -0
- package/dist/journeys/customer-life.json +2950 -0
- package/dist/journeys/customer-life.outline.md +59 -0
- package/dist/journeys/decisions.json +161 -0
- package/dist/journeys/demand.json +142 -0
- package/dist/journeys/score.json +294 -0
- package/dist/journeys/unreachable.json +13 -0
- package/dist/open/spec/SOURCE.md +20 -0
- package/dist/open/spec/openapi.json +1417 -0
- package/dist/open/spec/sources.json +124 -0
- package/dist/open/src/engine/mp4.d.ts +6 -0
- package/dist/open/src/engine/mp4.js +55 -0
- package/dist/open/src/engine/mp4.ts +48 -0
- package/dist/open/src/fetch.d.ts +2 -0
- package/dist/open/src/fetch.js +17 -0
- package/dist/open/src/fetch.ts +18 -0
- package/dist/open/src/generated/surface.gen.json +1 -0
- package/dist/open/src/generated/ui.gen.json +1 -0
- package/dist/open/src/index.d.ts +2 -0
- package/dist/open/src/index.js +3 -0
- package/dist/open/src/index.ts +3 -0
- package/dist/open/src/manifest.d.ts +2 -0
- package/dist/open/src/manifest.js +53 -0
- package/dist/open/src/manifest.ts +59 -0
- package/dist/open/src/screens/authorize.d.ts +2 -0
- package/dist/open/src/screens/authorize.js +53 -0
- package/dist/open/src/screens/authorize.ts +53 -0
- package/dist/open/src/semantics/doors.d.ts +10 -0
- package/dist/open/src/semantics/doors.js +118 -0
- package/dist/open/src/semantics/doors.ts +121 -0
- package/dist/open/src/semantics/index.d.ts +2 -0
- package/dist/open/src/semantics/index.js +16 -0
- package/dist/open/src/semantics/index.ts +20 -0
- package/dist/open/src/semantics/oauth.d.ts +2 -0
- package/dist/open/src/semantics/oauth.js +30 -0
- package/dist/open/src/semantics/oauth.ts +28 -0
- package/dist/open/src/semantics/post.d.ts +6 -0
- package/dist/open/src/semantics/post.js +66 -0
- package/dist/open/src/semantics/post.ts +60 -0
- package/dist/open/src/semantics/shared.d.ts +51 -0
- package/dist/open/src/semantics/shared.js +196 -0
- package/dist/open/src/semantics/shared.ts +197 -0
- package/dist/open/src/semantics/states.d.ts +20 -0
- package/dist/open/src/semantics/states.js +15 -0
- package/dist/open/src/semantics/states.ts +23 -0
- package/dist/open/src/semantics/user.d.ts +2 -0
- package/dist/open/src/semantics/user.js +24 -0
- package/dist/open/src/semantics/user.ts +24 -0
- package/dist/open/src/semantics/video.d.ts +4 -0
- package/dist/open/src/semantics/video.js +112 -0
- package/dist/open/src/semantics/video.ts +97 -0
- package/dist/src/cli.js +11 -34
- package/dist/src/fetch.d.ts +2 -0
- package/dist/src/fetch.js +9 -0
- package/dist/src/index.d.ts +5 -22
- package/dist/src/index.js +13 -167
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +28 -0
- package/dist/src/semantics/shared.d.ts +49 -0
- package/dist/src/semantics/shared.js +61 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +55 -0
- package/journeys/coverage.json +220 -0
- package/journeys/customer-life.json +2950 -0
- package/journeys/customer-life.outline.md +59 -0
- package/journeys/decisions.json +161 -0
- package/journeys/demand.json +142 -0
- package/journeys/score.json +294 -0
- package/journeys/unreachable.json +13 -0
- package/open/spec/SOURCE.md +20 -0
- package/open/spec/openapi.json +1417 -0
- package/open/spec/sources.json +124 -0
- package/open/src/engine/mp4.ts +48 -0
- package/open/src/fetch.ts +18 -0
- package/open/src/generated/surface.gen.json +1 -0
- package/open/src/generated/ui.gen.json +1 -0
- package/open/src/index.ts +3 -0
- package/open/src/manifest.ts +59 -0
- package/open/src/screens/authorize.ts +53 -0
- package/open/src/semantics/doors.ts +121 -0
- package/open/src/semantics/index.ts +20 -0
- package/open/src/semantics/oauth.ts +28 -0
- package/open/src/semantics/post.ts +60 -0
- package/open/src/semantics/shared.ts +197 -0
- package/open/src/semantics/states.ts +23 -0
- package/open/src/semantics/user.ts +24 -0
- package/open/src/semantics/video.ts +97 -0
- package/package.json +13 -41
- package/src/cli.ts +11 -32
- package/src/fetch.ts +10 -0
- package/src/index.ts +15 -270
- package/src/manifest.ts +33 -0
- package/src/semantics/shared.ts +78 -0
- package/src/server.ts +8 -0
- package/client/tiktok-consent.tsx +0 -154
- package/client/tiktok-mirror.css +0 -137
- package/client/tiktok-mirror.tsx +0 -497
- package/dist/client/tiktok-consent.bundle.js +0 -18
- package/dist/client/tiktok-consent.d.ts +0 -47
- package/dist/client/tiktok-consent.js +0 -20
- package/dist/client/tiktok-consent.tsx +0 -154
- package/dist/client/tiktok-mirror.bundle.js +0 -484
- package/dist/client/tiktok-mirror.css +0 -137
- package/dist/client/tiktok-mirror.d.ts +0 -42
- package/dist/client/tiktok-mirror.js +0 -325
- package/dist/client/tiktok-mirror.tsx +0 -497
- package/dist/src/tiktok-blobs.d.ts +0 -66
- package/dist/src/tiktok-blobs.js +0 -161
- package/dist/src/tiktok-budget.d.ts +0 -56
- package/dist/src/tiktok-budget.js +0 -136
- package/dist/src/tiktok-capabilities.d.ts +0 -7
- package/dist/src/tiktok-capabilities.js +0 -1855
- package/dist/src/tiktok-conformance.d.ts +0 -11
- package/dist/src/tiktok-conformance.js +0 -498
- package/dist/src/tiktok-connector.d.ts +0 -158
- package/dist/src/tiktok-connector.js +0 -600
- package/dist/src/tiktok-consent-ui.d.ts +0 -19
- package/dist/src/tiktok-consent-ui.js +0 -127
- package/dist/src/tiktok-errors.d.ts +0 -78
- package/dist/src/tiktok-errors.js +0 -175
- package/dist/src/tiktok-ids.d.ts +0 -16
- package/dist/src/tiktok-ids.js +0 -48
- package/dist/src/tiktok-media.d.ts +0 -7
- package/dist/src/tiktok-media.js +0 -86
- package/dist/src/tiktok-mirror-ui.d.ts +0 -61
- package/dist/src/tiktok-mirror-ui.js +0 -187
- package/dist/src/tiktok-pkce.d.ts +0 -25
- package/dist/src/tiktok-pkce.js +0 -56
- package/dist/src/tiktok-posting.d.ts +0 -100
- package/dist/src/tiktok-posting.js +0 -599
- package/dist/src/tiktok-sample-mp4.d.ts +0 -10
- package/dist/src/tiktok-sample-mp4.js +0 -55
- package/dist/src/tiktok-scopes.d.ts +0 -29
- package/dist/src/tiktok-scopes.js +0 -106
- package/dist/src/tiktok-server.d.ts +0 -28
- package/dist/src/tiktok-server.js +0 -99
- package/dist/src/tiktok-store.d.ts +0 -164
- package/dist/src/tiktok-store.js +0 -451
- package/dist/src/tiktok-twin.d.ts +0 -70
- package/dist/src/tiktok-twin.js +0 -1197
- package/dist/src/tiktok-user.d.ts +0 -28
- package/dist/src/tiktok-user.js +0 -174
- package/src/tiktok-blobs.ts +0 -217
- package/src/tiktok-budget.ts +0 -163
- package/src/tiktok-capabilities.ts +0 -2022
- package/src/tiktok-conformance.ts +0 -526
- package/src/tiktok-connector.ts +0 -637
- package/src/tiktok-consent-ui.ts +0 -146
- package/src/tiktok-errors.ts +0 -197
- package/src/tiktok-ids.ts +0 -51
- package/src/tiktok-journey.uitest.ts +0 -305
- package/src/tiktok-media.ts +0 -89
- package/src/tiktok-mirror-ui.ts +0 -193
- package/src/tiktok-pkce.ts +0 -61
- package/src/tiktok-posting.ts +0 -617
- package/src/tiktok-sample-mp4.ts +0 -54
- package/src/tiktok-scopes.ts +0 -122
- package/src/tiktok-server.ts +0 -110
- package/src/tiktok-store.ts +0 -543
- package/src/tiktok-twin.ts +0 -1361
- package/src/tiktok-user.ts +0 -137
package/README.md
CHANGED
|
@@ -1,310 +1,27 @@
|
|
|
1
1
|
# @volter/twin-tiktok
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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`.
|