@volter/twin-tiktok 0.1.36 → 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/src/index.ts CHANGED
@@ -1,270 +1,15 @@
1
- // @volter/twin-tiktok — the TikTok twin (one vendor, one package), built on the shared
2
- // @volter/world-core kernel.
3
- //
4
- // A BROWSER-FACING protocol pack (the googleoauth class): it serves TikTok's own authorization
5
- // page as HTML at www.tiktok.com's real path, completes the full Login Kit authorization-code
6
- // round trip (consent -> 302 with code+scopes+state -> redemption at the token endpoint -> refresh
7
- // -> revoke), and answers the Display API's `/v2/user/info/`, `/v2/video/list/` and
8
- // `/v2/video/query/` reads with TikTok's real field-selection, scope-gating and error envelopes, and
9
- // posts a creator's video through the Content Posting API (creator_info, the Direct Post and inbox
10
- // inits, the chunked upload to a signed upload URL, status/fetch) with the bytes on the blob seam, a
11
- // tiktok.com-style mirror over that state, and a perform that sends a World's post to TikTok.
12
- // Deliberately NON-OIDC, exactly as the vendor is: opaque `act.`/`rft.` tokens, no id_token, no
13
- // JWKS — identity comes from the Display API, and `open_id` is per-app while `union_id` is
14
- // per-human. Conformance tooling lives in @volter/world-tooling (a dev dependency), not here.
15
- import { registerPack, type TwinPack } from '@volter/world-core';
16
-
17
- export {
18
- ACCESS_TOKEN_TTL_SECONDS,
19
- API_ORIGIN,
20
- AUTH_CODE_TTL_SECONDS,
21
- AUTHORIZE_ORIGIN,
22
- CLIENT_TOKEN_TTL_SECONDS,
23
- handleTikTokTwinRequest,
24
- METERED_ENDPOINTS,
25
- RATE_LIMIT_PER_WINDOW,
26
- RATE_LIMITS,
27
- RATE_WINDOW_SECONDS,
28
- REFRESH_TOKEN_TTL_SECONDS,
29
- RESOURCE_TYPES,
30
- tiktokTwinSnapshot,
31
- USER_FIELDS,
32
- VIDEO_FIELDS,
33
- VIDEO_LIST_DEFAULT_COUNT,
34
- VIDEO_LIST_MAX_COUNT,
35
- VIDEO_QUERY_MAX_IDS,
36
- } from './tiktok-twin.ts';
37
- export type { MeteredEndpoint, TikTokRequest, TikTokResponse } from './tiktok-twin.ts';
38
- export { createTikTokConsentServer, createTikTokTwinFetch, createTikTokTwinServer, type TikTokTwinFetchOptions } from './tiktok-server.ts';
39
- export {
40
- DEFAULT_ACCOUNTS,
41
- DEFAULT_CLIENT_KEY,
42
- DEFAULT_CLIENT_SECRET,
43
- DEFAULT_VIDEOS,
44
- defaultRedirectUris,
45
- openIdFor,
46
- redirectUriAllowed,
47
- sessionAccount,
48
- } from './tiktok-store.ts';
49
- export { CHALLENGE_METHOD, isSupportedChallengeMethod, isWellFormedVerifier, pkceVerifies, rfc7636Challenge, tiktokCodeChallenge } from './tiktok-pkce.ts';
50
- export {
51
- describeScope,
52
- describeScopes,
53
- formatScopeParam,
54
- KNOWN_SCOPES,
55
- parseScopeParam,
56
- SCOPE_CATALOG,
57
- sortScopesForConsent,
58
- } from './tiktok-scopes.ts';
59
- export type { ScopeInfo, ScopeProduct } from './tiktok-scopes.ts';
60
- export { API_ERRORS, OAUTH_ERRORS } from './tiktok-errors.ts';
61
- export { logId } from './tiktok-ids.ts';
62
- export { renderUser, renderVideo, USER_FIELD_SCOPES, USER_INFO_SCOPES, VIDEO_SCOPE } from './tiktok-user.ts';
63
- export {
64
- liveTikTokExecute,
65
- mapUserInfoAccount,
66
- mapVideo,
67
- performTikTokAction,
68
- PULL_USER_FIELDS,
69
- PULL_VIDEO_FIELDS,
70
- pullTikTok,
71
- pushPendingTikTokActions,
72
- syncTikTokFromReal,
73
- syncTikTokFromRemote,
74
- tiktokExecuteOver,
75
- } from './tiktok-connector.ts';
76
- export type { LiveTikTokOptions, PullOptions, TikTokExecute } from './tiktok-connector.ts';
77
- export {
78
- budgetedTikTokExecute,
79
- MAX_PUBLISH_WAIT_MS,
80
- TikTokRetryableError,
81
- PERFORM_CHUNK_BYTES,
82
- performChunkPlan,
83
- STATUS_POLL_MS,
84
- TikTokStillProcessingError,
85
- } from './tiktok-connector.ts';
86
- export {
87
- checkChunkPlan,
88
- chunkLength,
89
- creatorOptions,
90
- DEFAULT_MAX_VIDEO_POST_DURATION_SEC,
91
- MAX_CHUNK_BYTES,
92
- MAX_CHUNK_COUNT,
93
- MAX_FINAL_CHUNK_BYTES,
94
- MAX_SERVED_RANGE,
95
- MAX_VIDEO_BYTES,
96
- MEDIA_PREFIX,
97
- MIN_CHUNK_BYTES,
98
- PRIVACY_LEVELS,
99
- processingMs,
100
- UPLOAD_ORIGIN,
101
- UPLOAD_PATH,
102
- UPLOAD_URL_TTL_MS,
103
- } from './tiktok-posting.ts';
104
- export { POSTING_ERRORS } from './tiktok-errors.ts';
105
- // The client-side rate budget — the fail-closed backstop `liveTikTokExecute` routes every live
106
- // request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
107
- // here is this vendor's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
108
- // bindings. Exported so an operator can inspect spend (`snapshot`) and so a caller can catch
109
- // `TikTokBudgetError` by type; there is deliberately no export that disables the guard.
110
- export {
111
- TIKTOK_BUDGET_BURST_CEILING,
112
- TIKTOK_BUDGET_CEILING,
113
- TIKTOK_BUDGET_MAX_RETRY_AFTER_S,
114
- TIKTOK_BUDGET_WINDOW_MS,
115
- TIKTOK_CALL_WEIGHTS,
116
- TIKTOK_RATE_BUDGET,
117
- TikTokBudget,
118
- TikTokBudgetError,
119
- tiktokBudgetPath,
120
- tiktokCallWeight,
121
- } from './tiktok-budget.ts';
122
- export type {
123
- TikTokBudgetErrorKind,
124
- TikTokBudgetOptions,
125
- TikTokBudgetReservation,
126
- TikTokBudgetSnapshot,
127
- } from './tiktok-budget.ts';
128
- export { CONSENT_CSS, consentPageHtml, errorPageHtml, tiktokConsentState } from './tiktok-consent-ui.ts';
129
- // The tiktok.com mirror (profile, grid, vertical player): its pure helpers and its server. The hosted
130
- // mirror mount reads tiktok-mirror-ui.ts directly (`tiktokMirrorHtml`, `buildTiktokMirrorClient`,
131
- // `tiktokMirrorStyles`).
132
- export { captionOf, captionSegments, compactCount, createTiktokMirrorServer, mediaSource, profileStats } from './tiktok-mirror-ui.ts';
133
- export type { ConsentAccount, ConsentScopeRow, ConsentView, ErrorPageProps } from './tiktok-consent-ui.ts';
134
-
135
- import { TIKTOK_RATE_BUDGET as RATE_BUDGET } from './tiktok-budget.ts';
136
- import { performTikTokAction, syncTikTokFromRemote } from './tiktok-connector.ts';
137
-
138
- /**
139
- * The www.tiktok.com paths THIS pack serves — the Login Kit authorization leg — as a RegExp SOURCE
140
- * for the descriptor's `hosts` path rule. www.tiktok.com is NOT an API host: it is the whole
141
- * TikTok web property (the For You feed, profiles, settings, everything). Claiming the host
142
- * outright would be the Cal.com mis-route incident with the largest possible blast radius, so this
143
- * claims EXACTLY the authorize path plus the twin-only prefix. ANCHORED, the googleoauth lesson:
144
- * an unanchored claim swallows `/v2/auth/authorize2` and friends. The optional trailing slash is
145
- * load-bearing — TikTok's own documentation writes `/v2/auth/authorize/` while Dub's URL builder
146
- * omits it, and both must route here.
147
- */
148
- const TIKTOK_AUTHORIZE_PATHS = '^/v2/auth/authorize/?$|^/_twin/';
149
-
150
- /**
151
- * The open.tiktokapis.com paths THIS pack serves: the OAuth token/revoke endpoints, the Display
152
- * API's user and video reads and the Content Posting API's four calls. open.tiktokapis.com is the
153
- * ENTIRE TikTok open platform host — Research, Data Portability, Commercial Content, Business and
154
- * the photo post besides — and nothing this pack does not model is claimed: an unmodelled TikTok
155
- * call must refuse loudly rather than land in a twin that cannot serve it.
156
- */
157
- const TIKTOK_API_PATHS = '^/v2/(?:oauth/(?:token|revoke)|user/info|video/(?:list|query)|post/publish/(?:creator_info/query|video/init|inbox/video/init|status/fetch))/?$|^/_twin/';
158
-
159
- /**
160
- * The upload host's path: the Content Posting API's upload_url is `https://open-upload.tiktokapis.com/
161
- * video/?upload_id=…&upload_token=…` (the Direct Post and inbox references' own examples). An app in a
162
- * World PUTs its chunks there, so the injector must route that host to the twin too. Nothing else on
163
- * the upload host is modelled, so nothing else is claimed.
164
- */
165
- const TIKTOK_UPLOAD_PATHS = '^/video/?$';
166
-
167
- export const pack: TwinPack = {
168
- // PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of
169
- // the real state system, registered on load. A World's POST crosses at `perform`, through the
170
- // Content Posting API (creator_info, init, the presigned chunk PUTs, status/fetch); nothing else
171
- // does: TikTok publishes no API that creates a developer app, seeds a user or grants a scope —
172
- // those are developer-portal and in-app settings actions a person takes in a browser — so
173
- // `perform` settles every other entry with that reason.
174
- protocol: '2',
175
- // The vendor's access token lives 24 hours and its Display API limit is 600/minute, so a served
176
- // world can afford a half-hourly pull; `onDemand` throttles a hand-run refresh to one a minute,
177
- // well inside that budget.
178
- refresh: { every: '30m', onDemand: { atMost: '60s' } },
179
- stateSystem: { perform: performTikTokAction, refresh: syncTikTokFromRemote },
180
- // The round trip registers a developer app through the twin's OWN door, because the vendor has no
181
- // API for it at all — which is the same reason `perform` never crosses. (The Login Kit legs
182
- // cannot serve as the round trip: the consent post answers a 302 and the harness fails any step
183
- // answering >=300.)
184
- roundTrip: {
185
- method: 'POST',
186
- path: '/_twin/clients',
187
- body: { client_key: 'awroundtripapp0001', client_secret: 'round-trip-secret', name: 'Round Trip', redirect_uris: ['https://round.trip.test/callback'] },
188
- },
189
- parityOrigin: 'http://twin',
190
- // shapeParity is NOT held, and the reason is STRUCTURAL rather than a divergence to close: the
191
- // refresh reads the ACCOUNT the credential names (`GET /v2/user/info/`), while the only write a
192
- // blind round trip can make on this vendor is registering a developer app — which TikTok
193
- // publishes no API to read back. Written and observed are different subjects by design, so there
194
- // is no pair of shapes to compare. Reaching parity would need the refresh to observe an
195
- // oauth_client, and no TikTok endpoint returns one.
196
- vendor: 'tiktok',
197
- // The SAME object tiktok-budget.ts declares at module load — one source of truth, so registering
198
- // the pack and importing the connector can never arm two different ceilings.
199
- rateBudget: RATE_BUDGET,
200
- transport: 'rest',
201
- archetype: 'crud',
202
- bin: 'world-tiktok',
203
- resources: [
204
- 'oauth_client',
205
- 'account',
206
- 'session',
207
- 'auth_request',
208
- 'grant',
209
- 'video',
210
- 'publish',
211
- ],
212
- specSource:
213
- 'developers.tiktok.com, fetched 2026-09-13 — no first-party machine-readable spec exists for '
214
- + 'the TikTok open API, so the denominator was authored top-down from the published references: '
215
- + 'Login Kit for Web (the authorize URL, its parameters and the callback parameters), User '
216
- + 'Access Token Management (token / refresh / revoke request + response tables and examples), '
217
- + 'Client Access Token Management (the client_credentials grant), Login Kit for Desktop (PKCE: '
218
- + 'hex-encoded SHA-256, S256 only), the Scopes Overview, Get User Info (the field x scope '
219
- + 'table), the Video Object / Video List / Video Query references, the OAuth error-handling '
220
- + 'reference (the ten flat `error` values) and the API v2 error-handling reference (the seven '
221
- + 'nested `error.code` values with their HTTP statuses) and rate-limit reference (600/minute, '
222
- + 'one-minute sliding window, 429 + rate_limit_exceeded); and, fetched 2026-09-27, the Content '
223
- + 'Posting references (Query Creator Info, Direct Post, Upload to inbox, Get Post Status and the '
224
- + 'Media Transfer Guide: chunk rules, the upload_url PUT with Content-Range, per-token minute limits).',
225
- description:
226
- "TikTok twin — the real tiktok.com authorization page, the full Login Kit OAuth round trip "
227
- + '(token, refresh, revoke, client_credentials) and the Display API user/video reads with '
228
- + "TikTok's own comma-separated scopes, per-app open_id, field-level scope gating and both of "
229
- + 'its error envelopes; and the Content Posting API — creator_info, Direct Post and inbox uploads '
230
- + 'through a signed upload URL in chunks, status/fetch by World time — with a tiktok.com-style '
231
- + "profile and vertical player over the posted videos, and a perform that posts a World's video "
232
- + 'to TikTok.',
233
- adoption: {
234
- // TikTok publishes NO first-party npm client for Login Kit or the Display API: its own
235
- // reference material is curl and raw fetch, which is exactly how the motivating application
236
- // (Dub) calls it. The community `tiktok` package on npm is deliberately NOT claimed — it is
237
- // not a TikTok-published client, and `adoption.sdks` is for official clients only (the bitly
238
- // precedent). Interception is therefore host-based, which is why `hosts` below is the whole
239
- // adoption story for this vendor.
240
- sdks: [],
241
- // No official Python distribution either. `tiktok-business-api-sdk` (PyPI and npm) is
242
- // first-party but speaks the BUSINESS/Ads API on business-api.tiktok.com — a different half of
243
- // the vendor that this pack does not serve, so claiming it would over-claim coverage.
244
- pypi: [],
245
- // TIKTOK_CLIENT_ID / TIKTOK_CLIENT_SECRET are exactly the pair Dub reads, and TIKTOK_CLIENT_KEY
246
- // is the vendor's own name for the same value. The `TIKTOKBUSINESS` stem is deliberately left
247
- // to the packless registry: it names the Business API this pack does not model.
248
- envStems: ['TIKTOK'],
249
- },
250
- hosts: [
251
- { host: 'www.tiktok.com', pathPattern: TIKTOK_AUTHORIZE_PATHS },
252
- { host: 'open.tiktokapis.com', pathPattern: TIKTOK_API_PATHS },
253
- { host: 'open-upload.tiktokapis.com', pathPattern: TIKTOK_UPLOAD_PATHS },
254
- ],
255
- // Dub HARDCODES both hosts — `https://open.tiktokapis.com/v2` in its TikTokClient and the
256
- // authorize/token URLs in its provider table — and TikTok publishes no official SDK with a
257
- // base-URL option, so there is no env var an app reads that a world could point at the twin.
258
- // Inventing a TIKTOK_BASE_URL would make `covers` report the world covered while the app still
259
- // talked to the real vendor; interception through the two `hosts` entries above is the whole
260
- // mechanism.
261
- endpointEnvNone:
262
- 'no app-read base-URL env exists: TikTok publishes no official SDK with a base-URL option and '
263
- + 'integrations (Dub included) hardcode www.tiktok.com and open.tiktokapis.com, so interception '
264
- + 'is the path-scoped hosts entries above.',
265
- // A browser is REDIRECTED to www.tiktok.com/v2/auth/authorize — the loader host is the consent
266
- // host and the prefix is the Login Kit path root.
267
- browserRouting: { apiPathPrefix: '/v2/auth/', loaderHost: 'https://www.tiktok.com' },
268
- };
269
- // registered at import: the kernel learns the pack's state system (protocol 2)
270
- registerPack(pack);
1
+ // @volter/twin-tiktok — the tiktok twin (Protocol 3). A fixed file (create-pack): the descriptor is the manifest's, and the
2
+ // kernel derives the vendor-backed half from the generated surface, unless the manifest declares none (vendorBacked.none;
3
+ // architecture, "The real-system adapters").
4
+ import { packOf, registerPack, type DerivedSurface } from '@volter/world-core';
5
+ import { manifest } from './manifest.ts';
6
+ import { manifest as businessManifest } from '../business/src/manifest.ts';
7
+ import businessSurface from '../business/src/generated/surface.gen.json' with { type: 'json' };
8
+ import { manifest as openManifest } from '../open/src/manifest.ts';
9
+ import openSurface from '../open/src/generated/surface.gen.json' with { type: 'json' };
10
+
11
+ export { createTiktokFetch } from './fetch.ts';
12
+ export { createTiktokTwinServer } from './server.ts';
13
+ export { manifest };
14
+
15
+ export const pack = registerPack(packOf(manifest, { "business": { manifest: businessManifest, surface: businessSurface as unknown as DerivedSurface }, "open": { manifest: openManifest, surface: openSurface as unknown as DerivedSurface } }));
@@ -0,0 +1,33 @@
1
+ // TikTok's manifest: a vendor of two APIs, each a lane (architecture, "Other wires: lanes"): the open platform
2
+ // (`open`: Login Kit's token, the Content Posting API, the Display API, the upload host and the authorize page on
3
+ // www.tiktok.com), and the API for Business (`business`, business-api.tiktok.com). Postiz calls both.
4
+ import type { VendorManifest } from '@volter/world-core';
5
+
6
+ export const manifest: VendorManifest = {
7
+ vendor: 'tiktok',
8
+ discovery: {
9
+ twinOf: "TikTok's open platform: an app's authorize page and tokens, posting videos and photos (direct and to the inbox) with their upload host and status, and a user's profile, stats and videos",
10
+ stores: "the developer site's apps and URL properties, TikTok's people, authorization codes and tokens (by their SHA-256), posts and their uploads, and videos",
11
+ },
12
+ lanes: {
13
+ routes: [
14
+ { lane: 'business', host: '^business-api\\.tiktok\\.com$' },
15
+ { lane: 'open', host: '^(open\\.tiktokapis\\.com|open-upload\\.tiktokapis\\.com|upload\\.us\\.tiktokapis\\.com|www\\.tiktok\\.com)$' },
16
+ ],
17
+ default: 'open',
18
+ },
19
+ descriptor: {
20
+ protocol: '3',
21
+ transport: 'rest',
22
+ archetype: 'crud',
23
+ bin: 'world-tiktok',
24
+ resources: ['publish', 'video'],
25
+ specSource: 'written from the API reference pages of developers.tiktok.com (open/spec/openapi.json, provenance in open/spec/SOURCE.md)',
26
+ description: "TikTok twin — Login Kit's authorize page and tokens, the Content Posting API (direct and inbox videos by chunked upload, photos pulled from verified URLs, post status) and the Display API (user info and stats, video list and query), as Postiz calls them.",
27
+ adoption: { sdks: [], envStems: ['TIKTOK'] },
28
+ hosts: [{ host: 'business-api.tiktok.com' }, { host: 'open.tiktokapis.com' }, { host: 'open-upload.tiktokapis.com' }, { host: 'upload.us.tiktokapis.com' }, { host: 'www.tiktok.com', pathPattern: '^/v2/auth/authorize' }],
29
+ endpointEnvNone: 'Postiz calls open.tiktokapis.com and sends people to www.tiktok.com; the World claims the hosts',
30
+ clientCasesNone: 'Postiz uses fetch for both TikTok APIs; no official SDK behaviour is required.',
31
+ credentialDoor: { path: '/_twin/app-credentials', body: {}, fill: { TIKTOK_CLIENT_ID: 'client_key', TIKTOK_CLIENT_SECRET: 'client_secret', TIKTOK_BUSINESS_CLIENT_ID: 'business_client_id', TIKTOK_BUSINESS_CLIENT_SECRET: 'business_client_secret' } },
32
+ },
33
+ };
@@ -0,0 +1,78 @@
1
+ // What TikTok's lanes share (a lane imports its vendor's shared helpers): the developer sites' apps, TikTok's people,
2
+ // the codes and tokens an app holds for a person (each kept by its SHA-256, drawn from ctx.secret), posts and videos,
3
+ // and the World's numbering and placeholder images.
4
+ import { placeholderPng, type HandlerContext } from '@volter/world-core';
5
+
6
+ export type Row = Record<string, unknown>;
7
+ export const APP = '_app';
8
+ export const PERSON = '_person';
9
+ export const CODE = '_code';
10
+ export const ACCESS = '_access_token';
11
+ export const REFRESH = '_refresh_token';
12
+ export const PUBLISH = 'publish';
13
+ export const VIDEO = 'video';
14
+ export const CHUNK = '_chunk';
15
+
16
+ export const sha256 = (ctx: Pick<HandlerContext, 'crypto'>, v: string): string => ctx.crypto.digest('sha256', v, 'hex');
17
+ export const seconds = (iso: string): number => Math.floor(Date.parse(iso) / 1000);
18
+ const obj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v) ? v as Row : {});
19
+ export const bodyOf = (ctx: Pick<HandlerContext, 'body'>): Row => obj(ctx.body);
20
+
21
+ // ── apps and people ────────────────────────────────────────────────────────────────────────────
22
+
23
+ /** An app's client secret, drawn from the World's secret under its id. */
24
+ export const clientSecret = async (ctx: Pick<HandlerContext, 'secret' | 'crypto'>, app: Row): Promise<string> =>
25
+ ctx.crypto.digest('sha256', await ctx.secret(`tiktok-client-secret:${String(app.id)}`), 'hex').slice(0, 32);
26
+ export const appByKey = (ctx: Pick<HandlerContext, 'rowsRaw'>, key: unknown): Row | undefined => ctx.rowsRaw(APP).find((a) => a.deleted !== true && a.client_key === key);
27
+
28
+ /** A person's id in an app ("The unique identification of the user in the current application"), and across the
29
+ * developer's apps (union_id): derived from the ids they name, both public. */
30
+ // source: https://developers.tiktok.com/doc/tiktok-api-v2-get-user-info "The unique identification of the user in the current application."
31
+ export const openIdOf = (ctx: Pick<HandlerContext, 'crypto'>, app: Row, person: Row): string => ctx.crypto.uuidFrom(`tiktok-open-id:${String(app.id)}:${String(person.id)}`);
32
+ export const unionIdOf = (ctx: Pick<HandlerContext, 'crypto'>, person: Row): string => ctx.crypto.uuidFrom(`tiktok-union-id:${String(person.id)}`);
33
+
34
+ /** A placeholder image the World's CDN serves (a person's avatar, a video's cover): the kernel's placeholder PNG. */
35
+ export const mediaUrl = (ctx: Pick<HandlerContext, 'publicBase'>, name: string): string => `${ctx.publicBase.replace(/\/$/, '')}/_twin/media/${name}.png`;
36
+ export const mediaBytes = (name: string): Uint8Array => placeholderPng({ width: 720, height: 1280 }, `tiktok:${name}`, `placeholder image from the TikTok twin: ${name}`);
37
+
38
+ // ── tokens ─────────────────────────────────────────────────────────────────────────────────────
39
+ // source: https://developers.tiktok.com/doc/oauth-user-access-token-management "It is valid for 24 hours after initial issuance."
40
+ export const ACCESS_TTL = 86400;
41
+ // source: https://developers.tiktok.com/doc/oauth-user-access-token-management "It is valid for 365 days after the initial issuance."
42
+ export const REFRESH_TTL = 31536000;
43
+
44
+ /** A credential of TikTok's form (`act.…`, `rft.…`), drawn from the World's secret under its minted id. */
45
+ export async function credential(ctx: HandlerContext, type: string, prefix: string, fields: Row): Promise<{ id: string; value: string }> {
46
+ const id = ctx.mint(type);
47
+ const value = `${prefix}.${(await ctx.secret(`tiktok:${type}:${id}`)).replace(/[^A-Za-z0-9]/g, '').slice(0, 36)}!${(await ctx.secret(`tiktok:${type}:${id}:n`)).replace(/\D/g, '').padEnd(4, '0').slice(0, 4)}`;
48
+ await ctx.record(type, { ...fields, sha256: sha256(ctx, value), created: ctx.occurredAt }, id);
49
+ return { id, value };
50
+ }
51
+ export const bySha = (ctx: Pick<HandlerContext, 'rowsRaw' | 'crypto'>, type: string, value: string): Row | undefined =>
52
+ ctx.rowsRaw(type).find((r) => r.deleted !== true && r.sha256 === sha256(ctx, value));
53
+
54
+ /** The World's next number in a sequence (an id's number, a nonce): issued from the tree over the declared
55
+ * `_seq_<what>` (ctx.issue), never a row count. */
56
+ export const next = async (ctx: Pick<HandlerContext, 'issue'>, what: 'publish' | 'video'): Promise<number> => Number(await ctx.issue(`_seq_${what}`));
57
+
58
+ /** A video's moment, the position its creator's list pages by: the World's clock in milliseconds, one past the creator's
59
+ * latest video's when the clock has not moved past it, so two videos never share a cursor. From the subject's history,
60
+ * never an accumulator. */
61
+ export const videoMoment = (ctx: Pick<HandlerContext, 'rowsRaw' | 'occurredAt'>, person: string): number =>
62
+ Math.max(Date.parse(ctx.occurredAt), ...ctx.rowsRaw(VIDEO).filter((v) => v._person === person).map((v) => Number(v._moment) + 1));
63
+
64
+ /** A creator's public videos newest first, the page before a cursor (a moment; none is the newest), and the cursor
65
+ * the page answers (its last video's moment). */
66
+ export function videoPage(ctx: Pick<HandlerContext, 'rowsRaw'>, person: string, cursor: number, max: number): { page: Row[]; cursor: number; more: boolean } {
67
+ const mine = ctx.rowsRaw(VIDEO).filter((v) => v.deleted !== true && v._person === person && v._public === true && Number(v._moment) < cursor)
68
+ .sort((a, z) => Number(z._moment) - Number(a._moment));
69
+ const page = mine.slice(0, max);
70
+ return { page, cursor: page.length ? Number(page.at(-1)!._moment) : 0, more: mine.length > max };
71
+ }
72
+
73
+ /** The privacy levels a creator may post at: "If the TikTok creator account is public, the available options are:
74
+ * PUBLIC_TO_EVERYONE MUTUAL_FOLLOW_FRIENDS SELF_ONLY"; a private one's, FOLLOWER_OF_CREATOR in place of the first. */
75
+ // source: https://developers.tiktok.com/doc/content-posting-api-reference-query-creator-info "If the TikTok creator account is public, the available options are:"
76
+ export const privacyOptions = (p: Row): string[] => (p.private === true ? ['FOLLOWER_OF_CREATOR', 'MUTUAL_FOLLOW_FRIENDS', 'SELF_ONLY'] : ['PUBLIC_TO_EVERYONE', 'MUTUAL_FOLLOW_FRIENDS', 'SELF_ONLY']);
77
+
78
+ export { publish } from "../../open/src/semantics/states.ts";
package/src/server.ts ADDED
@@ -0,0 +1,8 @@
1
+ // FETCH-FIRST (runtime contract R12b): a fixed file, the kernel's serve seam around the pack's fetch (./fetch.ts).
2
+ import { serveHttp, type PackFetchOptions } from '@volter/world-core';
3
+ import { createTiktokFetch } from './fetch.ts';
4
+
5
+ export async function createTiktokTwinServer(options: PackFetchOptions & { port?: number } = {}): Promise<{ port: number; stop: () => void }> {
6
+ const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch: createTiktokFetch(options) });
7
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
8
+ }
@@ -1,154 +0,0 @@
1
- // TikTok's AUTHORIZATION PAGE, as React — the vendor's own product UI, not a dashboard mirror.
2
- //
3
- // These components are the ONE renderer for the screen: the twin server-renders them with
4
- // `renderToStaticMarkup` to answer `GET https://www.tiktok.com/v2/auth/authorize`, the capability
5
- // verifies render the SAME exported components over the SAME projection, and the Playwright
6
- // journey drives the HTML they emit. There is no second implementation to drift from.
7
- //
8
- // NO BROWSER BUNDLE, DELIBERATELY. The whole screen is a plain `<form>`: one checkbox per
9
- // requested scope and two submit buttons. Nothing on it needs JavaScript, so this pack ships no
10
- // browser entry and no committed bundle — the served page is server-rendered markup plus an
11
- // inline stylesheet (`tiktok-consent-ui.ts`). That keeps the serve path free of a bundler
12
- // (runtime contract R12b) and free of a generated artifact to drift, and it means `curl`,
13
- // redirect-following HTTP libraries and headless browsers all complete an OAuth round trip
14
- // against this twin identically.
15
- //
16
- // WHY THE CHECKBOXES ARE THE DESIGN. TikTok's documented callback carries a `scopes` parameter —
17
- // "the authorization scope(s) which the user has granted" — and the token response's `scope` is
18
- // "the scopes the user has agreed to authorize". Both are only meaningful if the granted set can
19
- // be a SUBSET of the requested set, so the decision this page collects is per scope. LAYOUT
20
- // PROVENANCE: the app/account header, the permission list and the Cancel/Continue pair reproduce
21
- // TikTok's authorization sheet as widely screenshotted; the live DOM was not captured from a
22
- // vendor artefact, so visual fidelity is certified by the journey filmstrip review and pinned by
23
- // `tiktok.consent.optional_scope_toggles` / `tiktok.consent.button_wording` (todo), never
24
- // asserted as vendor fact.
25
-
26
- export type ConsentAccount = {
27
- /** The account's `union_id` — the twin's subject key for a TikTok human. */
28
- unionId: string;
29
- username: string;
30
- displayName: string;
31
- };
32
- // NO `avatarUrl` HERE, deliberately (§9 nit): the state builder used to carry one that nothing
33
- // rendered — a field on the view model that no pixel depends on is a data-coupling claim with
34
- // nothing behind it. The live sheet does show an avatar; serving one would mean an `<img>` at a
35
- // tiktokcdn.com URL, and a twin never reaches a vendor CDN (see TikTokMark, drawn rather than
36
- // fetched). When the avatar is worth rendering it comes back as bytes the twin serves, with a
37
- // data-coupled UI capability that asserts the seeded persona's own image — not before.
38
-
39
- export type ConsentScopeRow = {
40
- scope: string;
41
- label: string;
42
- product: string;
43
- known: boolean;
44
- };
45
-
46
- export type ConsentApp = {
47
- clientKey: string;
48
- name: string;
49
- };
50
-
51
- export type ConsentView = {
52
- requestId: string;
53
- /** Where the twin is reachable — the form action posts back here. */
54
- origin: string;
55
- app: ConsentApp;
56
- /** The tiktok.com session this browser is signed in as — the account that will consent. */
57
- account: ConsentAccount;
58
- scopes: ConsentScopeRow[];
59
- /** Host of the validated redirect_uri, shown in the "you'll be redirected to" notice. */
60
- redirectHost: string;
61
- };
62
-
63
- /** The TikTok note mark, drawn rather than fetched — a twin never reaches out to a vendor CDN. */
64
- function TikTokMark() {
65
- return (
66
- <span className="tt-mark" aria-hidden="true">
67
- ♪
68
- </span>
69
- );
70
- }
71
-
72
- /** The authorization page — TikTok's consent sheet for the app named by the pending request. */
73
- export function ConsentPage({ view }: { view: ConsentView }) {
74
- const appName = view.app.name;
75
- return (
76
- <div className="card">
77
- <header className="tt-header">
78
- <TikTokMark />
79
- <span className="tt-wordmark">TikTok</span>
80
- </header>
81
- <div className="signed-in">
82
- <span className="account-text">
83
- {/* A partially-pulled persona may lack a display name or handle; the fallback shows the
84
- REAL union id rather than an empty pill or a bare "@" (never an invented placeholder). */}
85
- <span className="account-name">{view.account.displayName || view.account.username || view.account.unionId}</span>
86
- {view.account.username ? <span className="account-username">@{view.account.username}</span> : null}
87
- </span>
88
- </div>
89
- <h1 className="title">
90
- <strong className="app-name">{appName}</strong> would like to access your TikTok account
91
- </h1>
92
- <form method="POST" action={`${view.origin}/_twin/consent`} className="consent-form">
93
- <input type="hidden" name="auth_request" value={view.requestId} />
94
- <p className="scope-lead">This will allow {appName} to:</p>
95
- <ul className="scope-list">
96
- {view.scopes.map((row) => (
97
- <li key={row.scope} className="scope-row">
98
- <label className="scope-label">
99
- <input className="scope-check" type="checkbox" name="scope" value={row.scope} defaultChecked />
100
- <span className="scope-text">{row.label}</span>
101
- </label>
102
- {row.known ? null : <span className="scope-tag scope-tag-unknown">not in the twin&apos;s scope catalog</span>}
103
- </li>
104
- ))}
105
- </ul>
106
- <div className="actions">
107
- <button className="btn btn-secondary" type="submit" name="decision" value="deny">
108
- Cancel
109
- </button>
110
- <button className="btn btn-primary" type="submit" name="decision" value="allow">
111
- Continue
112
- </button>
113
- </div>
114
- </form>
115
- <p className="legal">
116
- You&apos;ll be redirected to <strong className="redirect-host">{view.redirectHost}</strong>. You can remove this
117
- app at any time from Manage app permissions in your TikTok settings.
118
- </p>
119
- </div>
120
- );
121
- }
122
-
123
- export type ErrorPageProps = {
124
- status: number;
125
- /** The OAuth error code the failure classifies as (e.g. `invalid_request`). */
126
- code: string;
127
- detail: string;
128
- /** The offending parameter, echoed for the developer. */
129
- requestParam?: string;
130
- };
131
-
132
- /**
133
- * The authorize-endpoint error PAGE — what a browser sees when the twin cannot trust the
134
- * redirect_uri (unknown client_key, unregistered callback) and therefore must not bounce the error
135
- * back to it. TikTok's live page for this case was not captured, so this page states the failure
136
- * plainly and machine-readably rather than imitating unverified vendor prose
137
- * (`tiktok.authorize.error_page_fidelity`, todo).
138
- */
139
- export function ErrorPage({ status, code, detail, requestParam }: ErrorPageProps) {
140
- return (
141
- <div className="card card-error">
142
- <header className="tt-header">
143
- <TikTokMark />
144
- <span className="tt-wordmark">TikTok</span>
145
- </header>
146
- <h1 className="title">Something went wrong</h1>
147
- <p className="error-detail">You weren&apos;t able to give access to the app. {detail}</p>
148
- <p className="error-code">
149
- Error {status}: {code}
150
- </p>
151
- {requestParam ? <p className="error-request">Request details: {requestParam}</p> : null}
152
- </div>
153
- );
154
- }