@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
@@ -0,0 +1,84 @@
1
+ {
2
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH": {
3
+ "status": 200,
4
+ "checked": "2026-10-03",
5
+ "quotes": {
6
+ "These return codes take precedence over HTTP status codes": true,
7
+ "The interface (endpoint) has not been implemented.": true,
8
+ "Invalid authorization code.": true,
9
+ "Invalid or incorrect refresh token.": true,
10
+ "Successful": true,
11
+ "Invalid or incorrect access token.": true,
12
+ "The access token is empty.": true,
13
+ "The access token has expired.": true,
14
+ "No permission to perform the related operation.": true,
15
+ "App ID or secret is incorrect.": true
16
+ }
17
+ },
18
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1738084387220481&language=ENGLISH": {
19
+ "status": 200,
20
+ "checked": "2026-10-03",
21
+ "quotes": {
22
+ "The code is valid for 10 minutes and can only be used once.": true,
23
+ "Use this endpoint to renew a TikTok account access token by": true
24
+ }
25
+ },
26
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228399168514&language=ENGLISH": {
27
+ "status": 200,
28
+ "checked": "2026-10-03",
29
+ "quotes": {
30
+ "Requested fields. If not set, returns the": true,
31
+ "Use this endpoint to access detailed analytics and insights about a TikTok account": true
32
+ }
33
+ },
34
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228421622786&language=ENGLISH": {
35
+ "status": 200,
36
+ "checked": "2026-10-03",
37
+ "quotes": {
38
+ "you need to at least include": true,
39
+ "Maximum value": true
40
+ }
41
+ },
42
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228496095234&language=ENGLISH": {
43
+ "status": 200,
44
+ "checked": "2026-10-03",
45
+ "quotes": {
46
+ "This field is required, even if you don't want to use any of the additional parameters": true
47
+ }
48
+ },
49
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1803630424390658&language=ENGLISH": {
50
+ "status": 200,
51
+ "checked": "2026-10-03",
52
+ "quotes": {
53
+ "A list of up to 35 publicly accessible HTTP(s) URLs for the photo content to be published.": true,
54
+ "Length limit: 90 runes (UTF-16 code units).": true
55
+ }
56
+ },
57
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1816387106635778&language=ENGLISH": {
58
+ "status": 200,
59
+ "checked": "2026-10-03",
60
+ "quotes": {
61
+ "The publishing status of the TikTok post.": true,
62
+ "Returned only when": true
63
+ }
64
+ },
65
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH": {
66
+ "status": 200,
67
+ "checked": "2026-10-03",
68
+ "quotes": {
69
+ "Commercial Music Library": true,
70
+ "Default value": true
71
+ }
72
+ },
73
+ "https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1870580030749698&language=ENGLISH": {
74
+ "status": 200,
75
+ "checked": "2026-10-03",
76
+ "quotes": {
77
+ "Length limit: 100 characters.": true
78
+ }
79
+ },
80
+ "https://developers.tiktok.com/doc/content-posting-api-reference-get-video-status": {
81
+ "status": 200,
82
+ "checked": "2026-10-03"
83
+ }
84
+ }
@@ -0,0 +1,14 @@
1
+ // GENERATED by scripts/create-pack.ts (--init, --index): a fixed file. The kernel serves the discovery door, the clock,
2
+ // the World's doors, the vendor's screens and the API from the pack's parts (architecture, "The pack contract").
3
+ import { createPackFetch, type DerivedFetch, type DerivedSurface, type PackFetchOptions } from '@volter/world-core';
4
+ import surface from './generated/surface.gen.json' with { type: 'json' };
5
+ import { manifest } from './manifest.ts';
6
+ import { businessSemantics } from './semantics/index.ts';
7
+
8
+ export function createBusinessLaneFetch(options: PackFetchOptions = {}): DerivedFetch {
9
+ return createPackFetch({
10
+ manifest,
11
+ surface: surface as DerivedSurface,
12
+ handlers: businessSemantics,
13
+ }, options);
14
+ }
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","spec":"openapi.json","format":"openapi","version":"v1.3","resources":[],"operations":[{"id":"businessGet","method":"get","path":"/open_api/v1.3/business/get/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"fields","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"businessPhotoPublish","method":"post","path":"/open_api/v1.3/business/photo/publish/","pathParams":[],"query":[],"body":[{"name":"business_id","type":"string","required":false},{"name":"photo_cover_index","type":"integer","required":false},{"name":"photo_images","type":"array","required":false},{"name":"post_info","type":"object","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"businessPublishLocation","method":"get","path":"/open_api/v1.3/business/publish/location/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"search_query","type":"string","required":true}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"businessPublishStatus","method":"get","path":"/open_api/v1.3/business/publish/status/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"publish_id","type":"string","required":true}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"businessVideoList","method":"get","path":"/open_api/v1.3/business/video/list/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"cursor","type":"integer","required":false},{"name":"fields","type":"string","required":false},{"name":"max_count","type":"integer","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"businessVideoPublish","method":"post","path":"/open_api/v1.3/business/video/publish/","pathParams":[],"query":[],"body":[{"name":"business_id","type":"string","required":false},{"name":"custom_thumbnail_url","type":"string","required":false},{"name":"post_info","type":"object","required":false},{"name":"video_url","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"discoveryCmlTrendingList","method":"get","path":"/open_api/v1.3/discovery/cml/trending_list/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"genre","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"ttUserOauth2RefreshToken","method":"post","path":"/open_api/v1.3/tt_user/oauth2/refresh_token/","pathParams":[],"query":[],"body":[{"name":"client_id","type":"string","required":false},{"name":"client_secret","type":"string","required":false},{"name":"grant_type","type":"string","required":false},{"name":"refresh_token","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"ttUserOauth2Revoke","method":"post","path":"/open_api/v1.3/tt_user/oauth2/revoke/","pathParams":[],"query":[],"body":[{"name":"access_token","type":"string","required":false},{"name":"client_id","type":"string","required":false},{"name":"client_secret","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"ttUserOauth2Token","method":"post","path":"/open_api/v1.3/tt_user/oauth2/token/","pathParams":[],"query":[],"body":[{"name":"auth_code","type":"string","required":false},{"name":"client_id","type":"string","required":false},{"name":"client_secret","type":"string","required":false},{"name":"grant_type","type":"string","required":false},{"name":"redirect_uri","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"ttUserTokenInfoGet","method":"post","path":"/open_api/v1.3/tt_user/token_info/get/","pathParams":[],"query":[],"body":[{"name":"access_token","type":"string","required":false},{"name":"app_id","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"}]}
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","resources":{"publish":{"actions":[],"states":{"status":["FAILED","PROCESSING_DOWNLOAD","PROCESSING_UPLOAD","PUBLISH_COMPLETE","SEND_TO_USER_INBOX"]},"embeds":{}},"video":{"actions":[],"states":{},"embeds":{}},"_access_token":{"actions":[],"states":{},"embeds":{}},"_refresh_token":{"actions":[],"states":{},"embeds":{}},"_seq_publish":{"actions":[],"states":{},"embeds":{}},"_seq_video":{"actions":[],"states":{},"embeds":{}}}}
@@ -0,0 +1,3 @@
1
+ // The business lane of the tiktok pack: the vendor's manifest routes its requests here.
2
+ export { createBusinessLaneFetch } from './fetch.ts';
3
+ export { manifest } from './manifest.ts';
@@ -0,0 +1,38 @@
1
+ // The business lane's manifest: the vendor facts its spec (../spec, written from the API for Business v1.3 pages) does
2
+ // not carry. Served is what Postiz's TikTok Business provider calls (../../journeys/demand.json,
3
+ // ../../journeys/decisions.json); revoking and the token's scopes are `unmodeled`.
4
+ import type { DerivedManifest } from '@volter/world-core';
5
+ import { states } from './semantics/states.ts';
6
+
7
+ export const manifest: DerivedManifest = {
8
+ vendor: 'tiktok',
9
+ // the vendor's one store: the open lane's apps, people, codes, posts and videos are these too
10
+ service: 'tiktok',
11
+ body: { json: 'always' },
12
+ ids: { template: '{uuid}' },
13
+ time: 'unix',
14
+ // "Every Business API endpoint (except OAuth) ... wraps the payload in {code, message, data}" (Postiz reads it so):
15
+ // a refusal is a non-zero code, answered HTTP 200
16
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "These return codes take precedence over HTTP status codes"
17
+ error: { code: '{code}', message: '{message}', request_id: '{id}', data: {} },
18
+ errorsAnsweredAs: { status: 200, below: 500 },
19
+ readOnly: { status: 200, message: 'This twin was started read-only; writes are refused.' },
20
+ notFound: { status: 200, code: 40008, message: "The interface (endpoint) has not been implemented." },
21
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "The interface (endpoint) has not been implemented."
22
+ // Where documentation stops: token errors do not establish an authentication gate before unknown-route resolution.
23
+ gap: { status: 200, code: 40008, message: "The interface (endpoint) has not been implemented." },
24
+ deleted: {},
25
+ resources: {
26
+ publish: { idPrefix: '', ...states.publish },
27
+ video: { idPrefix: '' },
28
+ _access_token: { idPrefix: 'act_', ids: '{prefix}{n}' },
29
+ _refresh_token: { idPrefix: 'rft_', ids: '{prefix}{n}' },
30
+ _seq_publish: { idPrefix: '', ids: '{n}' },
31
+ _seq_video: { idPrefix: '', ids: '{n}' },
32
+ },
33
+ unmodeled: ['ttUserOauth2Revoke', 'ttUserTokenInfoGet'],
34
+ discovery: {
35
+ twinOf: "TikTok's API for Business v1.3 as a TikTok account's owner uses it: its tokens, its profile, its posts, publishing videos and photos by URL, the publishing status, location tags and the Commercial Music Library's popular tracks",
36
+ stores: "the vendor's one store (the open lane's): apps, people, codes and tokens, posts and videos, and the World's location tags and tracks",
37
+ },
38
+ };
@@ -0,0 +1,16 @@
1
+ // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
2
+ // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
+ import type { Handler } from '@volter/world-core';
4
+ import * as open_api from './open_api.ts';
5
+
6
+ export const businessSemantics: Record<string, Handler> = {
7
+ businessGet: open_api.businessGet,
8
+ businessPhotoPublish: open_api.businessPhotoPublish,
9
+ businessPublishLocation: open_api.businessPublishLocation,
10
+ businessPublishStatus: open_api.businessPublishStatus,
11
+ businessVideoList: open_api.businessVideoList,
12
+ businessVideoPublish: open_api.businessVideoPublish,
13
+ discoveryCmlTrendingList: open_api.discoveryCmlTrendingList,
14
+ ttUserOauth2RefreshToken: open_api.ttUserOauth2RefreshToken,
15
+ ttUserOauth2Token: open_api.ttUserOauth2Token,
16
+ };
@@ -0,0 +1,142 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ import { ACCESS, ACCESS_TTL, APP, CODE, DEFAULT, NO_PERMISSION, PARAM, PERSON, PUBLISH, REFRESH, REFRESH_TTL, VIDEO, appOf, authorized, bodyOf, bySha, clientSecret, credential, issue, jsonArray, madeVideo, mediaUrl, next, ok, openIdOf, postsFields, privacyOptions, profileFields, publish, publishObj, pullAll, refuse, seconds, type Row, videoMoment, videoPage } from './shared.ts';
3
+
4
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1738084387220481&language=ENGLISH "The code is valid for 10 minutes and can only be used once."
5
+ export async function ttUserOauth2Token(ctx: HandlerContext): Promise<Response> {
6
+ const app = await appOf(ctx);
7
+ if (app instanceof Response) return app;
8
+ const b = bodyOf(ctx);
9
+ if (b.grant_type !== 'authorization_code') return refuse(ctx, PARAM, 'grant_type must be authorization_code.');
10
+ const code = typeof b.auth_code === 'string' ? bySha(ctx, CODE, b.auth_code) : undefined;
11
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Invalid authorization code."
12
+ if (!code || code.used === true || code.app !== app.id || Number(code.expires_at) <= seconds(ctx.occurredAt)) return refuse(ctx, 40110, 'Invalid authorization code. The auth_code is canceled, already used or expired.');
13
+ if (typeof b.redirect_uri === 'string' && b.redirect_uri !== code.redirect_uri) return refuse(ctx, PARAM, 'redirect_uri does not match the one the code was issued for.');
14
+ await ctx.record(CODE, { ...code, used: true }, String(code.id));
15
+ return issue(ctx, app, ctx.row(PERSON, String(code.person))!, String(code.scope));
16
+ }
17
+
18
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1738084387220481&language=ENGLISH "Use this endpoint to renew a TikTok account access token by"
19
+ export async function ttUserOauth2RefreshToken(ctx: HandlerContext): Promise<Response> {
20
+ const app = await appOf(ctx);
21
+ if (app instanceof Response) return app;
22
+ const b = bodyOf(ctx);
23
+ if (b.grant_type !== 'refresh_token') return refuse(ctx, PARAM, 'grant_type must be refresh_token.');
24
+ const held = typeof b.refresh_token === 'string' ? bySha(ctx, REFRESH, b.refresh_token) : undefined;
25
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Invalid or incorrect refresh token."
26
+ if (!held || held.used === true || held.kind !== 'business' || held.app !== app.id || Number(held.expires_at) <= seconds(ctx.occurredAt)) return refuse(ctx, 40107, 'Invalid or incorrect refresh token.');
27
+ await ctx.record(REFRESH, { ...held, used: true }, String(held.id));
28
+ return issue(ctx, app, ctx.row(PERSON, String(held.person))!, String(held.scope));
29
+ }
30
+
31
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228399168514&language=ENGLISH "Use this endpoint to access detailed analytics and insights about a TikTok account"
32
+ export async function businessGet(ctx: HandlerContext): Promise<Response> {
33
+ const c = authorized(ctx);
34
+ if (c instanceof Response) return c;
35
+ const asked = jsonArray(ctx, 'fields');
36
+ if (asked instanceof Response) return asked;
37
+ const fields = asked ?? DEFAULT;
38
+ const unknown = fields.filter((f) => !profileFields.includes(f));
39
+ if (unknown.length) return refuse(ctx, PARAM, `Invalid fields: ${unknown.join(', ')}`);
40
+ const p = c.person;
41
+ const all: Row = {
42
+ display_name: p.display_name, profile_image: mediaUrl(ctx, `avatar-${String(p.id)}`), username: p.username, profile_deep_link: `https://www.tiktok.com/@${String(p.username)}`,
43
+ bio_description: p.bio_description ?? '', is_verified: p.is_verified === true, followers_count: Number(p.follower_count ?? 0), following_count: Number(p.following_count ?? 0),
44
+ total_likes: Number(p.likes_count ?? 0),
45
+ // the account's public videos, counted from the set
46
+ videos_count: ctx.rowsRaw(VIDEO).filter((v) => v.deleted !== true && v._person === p.id && v._public === true).length,
47
+ };
48
+ return ok(ctx, Object.fromEntries(fields.map((f) => [f, all[f]])));
49
+ }
50
+
51
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228421622786&language=ENGLISH "you need to at least include"
52
+ export async function businessVideoList(ctx: HandlerContext): Promise<Response> {
53
+ const c = authorized(ctx);
54
+ if (c instanceof Response) return c;
55
+ const asked = jsonArray(ctx, 'fields');
56
+ if (asked instanceof Response) return asked;
57
+ const fields = asked ?? ['item_id'];
58
+ if (!fields.includes('item_id') || fields.some((f) => !postsFields.includes(f))) return refuse(ctx, PARAM, 'fields must include item_id and name only the supported fields.');
59
+ const q = new URL(ctx.call.request.url).searchParams;
60
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228421622786&language=ENGLISH "Maximum value"
61
+ const max = q.get('max_count') === null ? 10 : Number(q.get('max_count'));
62
+ if (!Number.isInteger(max) || max < 1 || max > 20) return refuse(ctx, PARAM, 'max_count must be between 1 and 20.');
63
+ const cursor = q.get('cursor') === null ? Infinity : Number(q.get('cursor'));
64
+ const { page, cursor: after, more } = videoPage(ctx, String(c.person.id), cursor, max);
65
+ const post = (v: Row): Row => {
66
+ const all: Row = {
67
+ item_id: v.id, create_time: v.create_time, thumbnail_url: v.cover_image_url, share_url: v.share_url, embed_url: v.embed_link, caption: v.video_description,
68
+ video_duration: v.duration, likes: v.like_count, comments: v.comment_count, shares: v.share_count, video_views: v.view_count,
69
+ };
70
+ return Object.fromEntries(fields.map((f) => [f, all[f]]));
71
+ };
72
+ return ok(ctx, { videos: page.map(post), cursor: after, has_more: more });
73
+ }
74
+
75
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228496095234&language=ENGLISH "This field is required, even if you don't want to use any of the additional parameters"
76
+ export async function businessVideoPublish(ctx: HandlerContext): Promise<Response> {
77
+ const c = authorized(ctx, 'video.publish');
78
+ if (c instanceof Response) return c;
79
+ const b = bodyOf(ctx);
80
+ if (typeof b.video_url !== 'string' || !/^https?:\/\//.test(b.video_url)) return refuse(ctx, PARAM, 'video_url must be an http(s) URL.');
81
+ if (b.post_info === undefined || typeof b.post_info !== 'object') return refuse(ctx, PARAM, 'post_info is required.');
82
+ const post = publishObj(b.post_info);
83
+ if (typeof post.is_brand_organic !== 'boolean' || typeof post.is_branded_content !== 'boolean') return refuse(ctx, PARAM, 'post_info.is_brand_organic and post_info.is_branded_content are required.');
84
+ return publish(ctx, 'VIDEO', [b.video_url], post, post.upload_to_draft === true, c.person);
85
+ }
86
+
87
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1803630424390658&language=ENGLISH "A list of up to 35 publicly accessible HTTP(s) URLs for the photo content to be published."
88
+ export async function businessPhotoPublish(ctx: HandlerContext): Promise<Response> {
89
+ const c = authorized(ctx, 'video.publish');
90
+ if (c instanceof Response) return c;
91
+ const b = bodyOf(ctx);
92
+ const images = Array.isArray(b.photo_images) ? (b.photo_images as unknown[]).map(String) : [];
93
+ if (images.length < 1 || images.length > 35) return refuse(ctx, PARAM, 'photo_images must hold 1 to 35 URLs.');
94
+ const cover = Number(b.photo_cover_index ?? 0);
95
+ if (!Number.isInteger(cover) || cover < 0 || cover >= images.length) return refuse(ctx, PARAM, 'photo_cover_index must index photo_images.');
96
+ const post = publishObj(b.post_info);
97
+ const draft = post.is_draft === true;
98
+ if (typeof post.is_brand_organic !== 'boolean' || typeof post.is_branded_content !== 'boolean') return refuse(ctx, PARAM, 'post_info.is_brand_organic and post_info.is_branded_content are required.');
99
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1803630424390658&language=ENGLISH "Length limit: 90 runes (UTF-16 code units)."
100
+ if (typeof post.title === 'string' && post.title.length > 90) return refuse(ctx, PARAM, 'post_info.title is over 90 characters.');
101
+ if (!draft && !privacyOptions(c.person).includes(String(post.privacy_level))) return refuse(ctx, PARAM, 'post_info.privacy_level must be one of the account\'s privacy levels.');
102
+ return publish(ctx, 'PHOTO', images, post, draft, c.person);
103
+ }
104
+
105
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1816387106635778&language=ENGLISH "The publishing status of the TikTok post."
106
+ // post_ids are "Returned only when" the status is PUBLISH_COMPLETE, the reason when FAILED
107
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1816387106635778&language=ENGLISH "Returned only when"
108
+ export async function businessPublishStatus(ctx: HandlerContext): Promise<Response> {
109
+ const c = authorized(ctx);
110
+ if (c instanceof Response) return c;
111
+ const id = String(ctx.params.publish_id ?? '');
112
+ const p = ctx.row(PUBLISH, id);
113
+ if (!p || p.deleted === true) return refuse(ctx, PARAM, 'publish_id does not exist.');
114
+ if (p._person !== c.person.id) return refuse(ctx, NO_PERMISSION, 'No permission to read this publishing task.');
115
+ return ok(ctx, { status: p.status, ...(p.status === 'PUBLISH_COMPLETE' ? { post_ids: p.post_ids ?? [] } : {}), ...(p.status === 'FAILED' ? { reason: p.reason } : {}) });
116
+ }
117
+
118
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1870580030749698&language=ENGLISH "Length limit: 100 characters."
119
+ export async function businessPublishLocation(ctx: HandlerContext): Promise<Response> {
120
+ const c = authorized(ctx);
121
+ if (c instanceof Response) return c;
122
+ const q = String(ctx.params.search_query ?? '');
123
+ if (!q || q.length > 100) return refuse(ctx, PARAM, 'search_query must be 1 to 100 characters.');
124
+ const words = q.toLowerCase().split(/\s+/).filter(Boolean);
125
+ const found = ctx.rowsRaw('_location').filter((l) => words.every((w) => `${String(l.location_name)} ${String(l.location_address)}`.toLowerCase().includes(w)));
126
+ return ok(ctx, { locations: found.map((l) => ({ location_id: l.id, location_name: l.location_name, location_address: l.location_address })) });
127
+ }
128
+
129
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH "Commercial Music Library"
130
+ export async function discoveryCmlTrendingList(ctx: HandlerContext): Promise<Response> {
131
+ const c = authorized(ctx);
132
+ if (c instanceof Response) return c;
133
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH "Default value"
134
+ const genre = typeof ctx.params.genre === 'string' && ctx.params.genre.toUpperCase() !== 'ALL' ? ctx.params.genre.toUpperCase() : undefined;
135
+ const tracks = ctx.rowsRaw('_track').filter((t) => !genre || t.genre === genre);
136
+ return ok(ctx, {
137
+ list: tracks.map((t) => ({
138
+ commercial_music_id: t.id, commercial_music_name: t.name, artist: t.artist, thumbnail_url: t.thumbnail_url, duration: t.duration, preview_url: t.preview_url, genre: t.genre,
139
+ full_duration_song_clip: { song_clip_id: t.song_clip_id, duration: t.duration, preview_url: t.preview_url },
140
+ })),
141
+ });
142
+ }
@@ -0,0 +1,119 @@
1
+ // What every family of the business lane shares beyond the vendor's (../../../src/semantics/shared.ts): the API for
2
+ // Business's envelope (`{code, message, request_id, data}`, a refusal a non-zero code at HTTP 200) and who a request is
3
+ // (its Access-Token, a business app's, and the business_id it names).
4
+ import type { HandlerContext } from '@volter/world-core';
5
+ import { ACCESS, ACCESS_TTL, APP, CODE, PERSON, PUBLISH, REFRESH, REFRESH_TTL, VIDEO, bodyOf, bySha, clientSecret, credential, mediaUrl, next, openIdOf, privacyOptions, seconds, type Row, videoMoment, videoPage } from '../../../src/semantics/shared.ts';
6
+
7
+ export * from '../../../src/semantics/shared.ts';
8
+ export type { HandlerContext };
9
+
10
+ /** A request's id, as the API answers it. */
11
+ const requestId = (ctx: Pick<HandlerContext, 'crypto' | 'occurredAt'>, what: string): string =>
12
+ `${ctx.occurredAt.replace(/\D/g, '').slice(0, 14)}${ctx.crypto.digest('sha256', `${ctx.occurredAt}:business:${what}`, 'hex').slice(0, 18).toUpperCase()}`;
13
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Successful"
14
+ export const ok = (ctx: Pick<HandlerContext, 'crypto' | 'occurredAt'>, data: unknown): Response =>
15
+ Response.json({ code: 0, message: 'OK', request_id: requestId(ctx, JSON.stringify(data)), data });
16
+ /** A refusal: its code and message, at HTTP 200. */
17
+ export const refuse = (ctx: Pick<HandlerContext, 'crypto' | 'occurredAt'>, code: number, message: string): Response =>
18
+ Response.json({ code, message, request_id: requestId(ctx, `${code}:${message}`), data: {} });
19
+
20
+ // the codes the lane answers (Appendix - Return codes)
21
+ export const PARAM = 40002;
22
+ export const NO_PERMISSION = 40001;
23
+
24
+ /** Who a request is: the business access token its Access-Token header bears, live, with its app and its person. */
25
+ export type Caller = { token: Row; app: Row; person: Row; scopes: string[]; openId: string };
26
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Invalid or incorrect access token."
27
+ export function authorized(ctx: HandlerContext, scope?: string): Caller | Response {
28
+ const value = ctx.call.request.headers.get('access-token') ?? '';
29
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "The access token is empty."
30
+ if (!value) return refuse(ctx, 40104, 'The access token is empty.');
31
+ const token = bySha(ctx, ACCESS, value);
32
+ if (!token || token.kind !== 'business' || token.revoked === true) return refuse(ctx, 40105, 'Invalid or incorrect access token.');
33
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "The access token has expired."
34
+ if (Number(token.expires_at) <= seconds(ctx.occurredAt)) return refuse(ctx, 40102, 'The access token has expired.');
35
+ const app = ctx.row(APP, String(token.app))!;
36
+ const person = ctx.row(PERSON, String(token.person))!;
37
+ const openId = openIdOf(ctx, app, person);
38
+ const businessId = String(ctx.params.business_id ?? '');
39
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "No permission to perform the related operation."
40
+ if (businessId !== openId) return refuse(ctx, NO_PERMISSION, 'No permission to operate this TikTok account: business_id is not the access token\'s.');
41
+ const scopes = String(token.scope).split(',');
42
+ if (scope && !scopes.includes(scope)) return refuse(ctx, NO_PERMISSION, `No permission: the TikTok account did not grant ${scope}.`);
43
+ return { token, app, person, scopes, openId };
44
+ }
45
+
46
+ /** A JSON-array query parameter (`fields`, as Postiz sends it), or the refusal. */
47
+ export function jsonArray(ctx: HandlerContext, name: string): string[] | undefined | Response {
48
+ const raw = new URL(ctx.call.request.url).searchParams.get(name);
49
+ if (raw === null) return undefined;
50
+ try {
51
+ const v = JSON.parse(raw) as unknown;
52
+ if (Array.isArray(v) && v.every((x) => typeof x === 'string')) return v as string[];
53
+ } catch { /* the refusal below */ }
54
+ return refuse(ctx, PARAM, `${name} must be a JSON array of strings.`);
55
+ }
56
+
57
+ export async function appOf(ctx: HandlerContext): Promise<Row | Response> {
58
+ const b = bodyOf(ctx);
59
+ const app = ctx.rowsRaw(APP).find((a) => a.deleted !== true && a.kind === 'business' && a.client_key === b.client_id);
60
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "App ID or secret is incorrect."
61
+ if (!app || b.client_secret !== await clientSecret(ctx, app)) return refuse(ctx, 40131, 'App ID or secret is incorrect.');
62
+ return app;
63
+ }
64
+
65
+ export async function issue(ctx: HandlerContext, app: Row, person: Row, scope: string): Promise<Response> {
66
+ const now = seconds(ctx.occurredAt);
67
+ const access = await credential(ctx, ACCESS, 'act', { kind: 'business', app: app.id, person: person.id, scope, expires_at: now + ACCESS_TTL, revoked: false });
68
+ const refresh = await credential(ctx, REFRESH, 'rft', { kind: 'business', app: app.id, person: person.id, scope, access: access.id, expires_at: now + REFRESH_TTL, used: false });
69
+ return ok(ctx, { access_token: access.value, token_type: 'Bearer', scope, expires_in: ACCESS_TTL, refresh_token: refresh.value, refresh_token_expires_in: REFRESH_TTL, open_id: openIdOf(ctx, app, person) });
70
+ }
71
+
72
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228399168514&language=ENGLISH "Requested fields. If not set, returns the"
73
+ export const DEFAULT = ['display_name', 'profile_image'];
74
+
75
+ export const profileFields = ['display_name', 'profile_image', 'username', 'profile_deep_link', 'bio_description', 'is_verified', 'followers_count', 'following_count', 'total_likes', 'videos_count'];
76
+
77
+ export const postsFields = ['item_id', 'create_time', 'thumbnail_url', 'share_url', 'embed_url', 'caption', 'video_duration', 'likes', 'comments', 'shares', 'video_views'];
78
+
79
+ export const publishObj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v) ? v as Row : {});
80
+
81
+ /** Pull each URL through the World; the bytes pulled, or undefined when one fails. */
82
+ export async function pullAll(ctx: HandlerContext, urls: string[]): Promise<number | undefined> {
83
+ let n = 0;
84
+ for (const url of urls) {
85
+ try {
86
+ const r = await ctx.vendorFetch(url, { method: 'GET' });
87
+ if (!r.ok) return undefined;
88
+ n += (await r.arrayBuffer()).byteLength;
89
+ } catch { return undefined; }
90
+ }
91
+ return n;
92
+ }
93
+
94
+ /** The video a completed pull makes (the Video Object, as the open lane makes them), public; its id. */
95
+ export async function madeVideo(ctx: HandlerContext, publishId: string, post: Row, person: Row): Promise<string> {
96
+ const videoId = String(7_400_000_000_000_000_000 + (await next(ctx, 'video')) * 1024);
97
+ const caption = typeof post.caption === 'string' ? post.caption : '';
98
+ await ctx.write(VIDEO, videoId, {
99
+ id: videoId, create_time: seconds(ctx.occurredAt), _moment: videoMoment(ctx, String(person.id)), cover_image_url: mediaUrl(ctx, `cover-${videoId}`), share_url: `https://www.tiktok.com/@${String(person.username)}/video/${videoId}`,
100
+ video_description: caption.slice(0, 150), duration: 0, height: 0, width: 0, title: caption.slice(0, 150),
101
+ embed_html: `<blockquote class="tiktok-embed" cite="https://www.tiktok.com/@${String(person.username)}/video/${videoId}" data-video-id="${videoId}"></blockquote>`,
102
+ embed_link: `https://www.tiktok.com/player/v1/${videoId}`, like_count: 0, comment_count: 0, share_count: 0, view_count: 0, is_aigc: post.is_ai_generated === true,
103
+ _person: person.id, _public: true, _publish: publishId,
104
+ }, 'video.publish');
105
+ return videoId;
106
+ }
107
+
108
+ export async function publish(ctx: HandlerContext, media: 'VIDEO' | 'PHOTO', urls: string[], post: Row, draft: boolean, person: Row): Promise<Response> {
109
+ const n = await next(ctx, 'publish');
110
+ const id = `${media === 'VIDEO' ? 'v' : 'p'}_pub_url~v2.${n}`;
111
+ await ctx.write(PUBLISH, id, { status: 'PROCESSING_DOWNLOAD', post_ids: [], _person: person.id, _media: media, _urls: urls, _post_info: post, _draft: draft, _lane: 'business' }, media === 'VIDEO' ? 'businessVideoPublish' : 'businessPhotoPublish');
112
+ const pulled = await pullAll(ctx, urls);
113
+ const to = pulled === undefined ? 'FAILED' : draft ? 'SEND_TO_USER_INBOX' : 'PUBLISH_COMPLETE';
114
+ const refused = ctx.legal(PUBLISH, 'status', 'publish.pull', 'PROCESSING_DOWNLOAD', to, id, 'vendor');
115
+ if (refused) throw new Error(refused.message);
116
+ const postIds = to === 'PUBLISH_COMPLETE' && media === 'VIDEO' ? [await madeVideo(ctx, id, post, person)] : [];
117
+ await ctx.write(PUBLISH, id, { status: to, post_ids: postIds, ...(to === 'FAILED' ? { reason: media === 'VIDEO' ? 'video_pull_failed' : 'photo_pull_failed' } : {}) }, 'publish.pull');
118
+ return ok(ctx, { share_id: id });
119
+ }
@@ -0,0 +1,3 @@
1
+ // Both lanes import the vendor’s one publish status declaration.
2
+ import { publish } from '../../../src/semantics/shared.ts';
3
+ export const states = { publish };
@@ -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`.