@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,2 @@
1
+ import { type DerivedFetch, type PackFetchOptions } from '@volter/world-core';
2
+ export declare function createBusinessLaneFetch(options?: PackFetchOptions): DerivedFetch;
@@ -0,0 +1,13 @@
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 } from '@volter/world-core';
4
+ import surface from './generated/surface.gen.json' with { type: 'json' };
5
+ import { manifest } from "./manifest.js";
6
+ import { businessSemantics } from "./semantics/index.js";
7
+ export function createBusinessLaneFetch(options = {}) {
8
+ return createPackFetch({
9
+ manifest,
10
+ surface: surface,
11
+ handlers: businessSemantics,
12
+ }, options);
13
+ }
@@ -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,2 @@
1
+ export { createBusinessLaneFetch } from './fetch.js';
2
+ export { manifest } from './manifest.js';
@@ -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.js";
3
+ export { manifest } from "./manifest.js";
@@ -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,2 @@
1
+ import type { DerivedManifest } from '@volter/world-core';
2
+ export declare const manifest: DerivedManifest;
@@ -0,0 +1,33 @@
1
+ import { states } from "./semantics/states.js";
2
+ export const manifest = {
3
+ vendor: 'tiktok',
4
+ // the vendor's one store: the open lane's apps, people, codes, posts and videos are these too
5
+ service: 'tiktok',
6
+ body: { json: 'always' },
7
+ ids: { template: '{uuid}' },
8
+ time: 'unix',
9
+ // "Every Business API endpoint (except OAuth) ... wraps the payload in {code, message, data}" (Postiz reads it so):
10
+ // a refusal is a non-zero code, answered HTTP 200
11
+ // 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"
12
+ error: { code: '{code}', message: '{message}', request_id: '{id}', data: {} },
13
+ errorsAnsweredAs: { status: 200, below: 500 },
14
+ readOnly: { status: 200, message: 'This twin was started read-only; writes are refused.' },
15
+ notFound: { status: 200, code: 40008, message: "The interface (endpoint) has not been implemented." },
16
+ // 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."
17
+ // Where documentation stops: token errors do not establish an authentication gate before unknown-route resolution.
18
+ gap: { status: 200, code: 40008, message: "The interface (endpoint) has not been implemented." },
19
+ deleted: {},
20
+ resources: {
21
+ publish: { idPrefix: '', ...states.publish },
22
+ video: { idPrefix: '' },
23
+ _access_token: { idPrefix: 'act_', ids: '{prefix}{n}' },
24
+ _refresh_token: { idPrefix: 'rft_', ids: '{prefix}{n}' },
25
+ _seq_publish: { idPrefix: '', ids: '{n}' },
26
+ _seq_video: { idPrefix: '', ids: '{n}' },
27
+ },
28
+ unmodeled: ['ttUserOauth2Revoke', 'ttUserTokenInfoGet'],
29
+ discovery: {
30
+ 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",
31
+ 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",
32
+ },
33
+ };
@@ -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,2 @@
1
+ import type { Handler } from '@volter/world-core';
2
+ export declare const businessSemantics: Record<string, Handler>;
@@ -0,0 +1,12 @@
1
+ import * as open_api from "./open_api.js";
2
+ export const businessSemantics = {
3
+ businessGet: open_api.businessGet,
4
+ businessPhotoPublish: open_api.businessPhotoPublish,
5
+ businessPublishLocation: open_api.businessPublishLocation,
6
+ businessPublishStatus: open_api.businessPublishStatus,
7
+ businessVideoList: open_api.businessVideoList,
8
+ businessVideoPublish: open_api.businessVideoPublish,
9
+ discoveryCmlTrendingList: open_api.discoveryCmlTrendingList,
10
+ ttUserOauth2RefreshToken: open_api.ttUserOauth2RefreshToken,
11
+ ttUserOauth2Token: open_api.ttUserOauth2Token,
12
+ };
@@ -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,10 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ export declare function ttUserOauth2Token(ctx: HandlerContext): Promise<Response>;
3
+ export declare function ttUserOauth2RefreshToken(ctx: HandlerContext): Promise<Response>;
4
+ export declare function businessGet(ctx: HandlerContext): Promise<Response>;
5
+ export declare function businessVideoList(ctx: HandlerContext): Promise<Response>;
6
+ export declare function businessVideoPublish(ctx: HandlerContext): Promise<Response>;
7
+ export declare function businessPhotoPublish(ctx: HandlerContext): Promise<Response>;
8
+ export declare function businessPublishStatus(ctx: HandlerContext): Promise<Response>;
9
+ export declare function businessPublishLocation(ctx: HandlerContext): Promise<Response>;
10
+ export declare function discoveryCmlTrendingList(ctx: HandlerContext): Promise<Response>;
@@ -0,0 +1,162 @@
1
+ import { CODE, DEFAULT, NO_PERMISSION, PARAM, PERSON, PUBLISH, REFRESH, VIDEO, appOf, authorized, bodyOf, bySha, issue, jsonArray, mediaUrl, ok, postsFields, privacyOptions, profileFields, publish, publishObj, refuse, seconds, videoPage } from "./shared.js";
2
+ // 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."
3
+ export async function ttUserOauth2Token(ctx) {
4
+ const app = await appOf(ctx);
5
+ if (app instanceof Response)
6
+ return app;
7
+ const b = bodyOf(ctx);
8
+ if (b.grant_type !== 'authorization_code')
9
+ 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))
13
+ return refuse(ctx, 40110, 'Invalid authorization code. The auth_code is canceled, already used or expired.');
14
+ if (typeof b.redirect_uri === 'string' && b.redirect_uri !== code.redirect_uri)
15
+ return refuse(ctx, PARAM, 'redirect_uri does not match the one the code was issued for.');
16
+ await ctx.record(CODE, { ...code, used: true }, String(code.id));
17
+ return issue(ctx, app, ctx.row(PERSON, String(code.person)), String(code.scope));
18
+ }
19
+ // 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"
20
+ export async function ttUserOauth2RefreshToken(ctx) {
21
+ const app = await appOf(ctx);
22
+ if (app instanceof Response)
23
+ return app;
24
+ const b = bodyOf(ctx);
25
+ if (b.grant_type !== 'refresh_token')
26
+ return refuse(ctx, PARAM, 'grant_type must be refresh_token.');
27
+ const held = typeof b.refresh_token === 'string' ? bySha(ctx, REFRESH, b.refresh_token) : undefined;
28
+ // 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."
29
+ if (!held || held.used === true || held.kind !== 'business' || held.app !== app.id || Number(held.expires_at) <= seconds(ctx.occurredAt))
30
+ return refuse(ctx, 40107, 'Invalid or incorrect refresh token.');
31
+ await ctx.record(REFRESH, { ...held, used: true }, String(held.id));
32
+ return issue(ctx, app, ctx.row(PERSON, String(held.person)), String(held.scope));
33
+ }
34
+ // 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"
35
+ export async function businessGet(ctx) {
36
+ const c = authorized(ctx);
37
+ if (c instanceof Response)
38
+ return c;
39
+ const asked = jsonArray(ctx, 'fields');
40
+ if (asked instanceof Response)
41
+ return asked;
42
+ const fields = asked ?? DEFAULT;
43
+ const unknown = fields.filter((f) => !profileFields.includes(f));
44
+ if (unknown.length)
45
+ return refuse(ctx, PARAM, `Invalid fields: ${unknown.join(', ')}`);
46
+ const p = c.person;
47
+ const all = {
48
+ 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)}`,
49
+ 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),
50
+ total_likes: Number(p.likes_count ?? 0),
51
+ // the account's public videos, counted from the set
52
+ videos_count: ctx.rowsRaw(VIDEO).filter((v) => v.deleted !== true && v._person === p.id && v._public === true).length,
53
+ };
54
+ return ok(ctx, Object.fromEntries(fields.map((f) => [f, all[f]])));
55
+ }
56
+ // 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"
57
+ export async function businessVideoList(ctx) {
58
+ const c = authorized(ctx);
59
+ if (c instanceof Response)
60
+ return c;
61
+ const asked = jsonArray(ctx, 'fields');
62
+ if (asked instanceof Response)
63
+ return asked;
64
+ const fields = asked ?? ['item_id'];
65
+ if (!fields.includes('item_id') || fields.some((f) => !postsFields.includes(f)))
66
+ return refuse(ctx, PARAM, 'fields must include item_id and name only the supported fields.');
67
+ const q = new URL(ctx.call.request.url).searchParams;
68
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228421622786&language=ENGLISH "Maximum value"
69
+ const max = q.get('max_count') === null ? 10 : Number(q.get('max_count'));
70
+ if (!Number.isInteger(max) || max < 1 || max > 20)
71
+ return refuse(ctx, PARAM, 'max_count must be between 1 and 20.');
72
+ const cursor = q.get('cursor') === null ? Infinity : Number(q.get('cursor'));
73
+ const { page, cursor: after, more } = videoPage(ctx, String(c.person.id), cursor, max);
74
+ const post = (v) => {
75
+ const all = {
76
+ 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,
77
+ video_duration: v.duration, likes: v.like_count, comments: v.comment_count, shares: v.share_count, video_views: v.view_count,
78
+ };
79
+ return Object.fromEntries(fields.map((f) => [f, all[f]]));
80
+ };
81
+ return ok(ctx, { videos: page.map(post), cursor: after, has_more: more });
82
+ }
83
+ // 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"
84
+ export async function businessVideoPublish(ctx) {
85
+ const c = authorized(ctx, 'video.publish');
86
+ if (c instanceof Response)
87
+ return c;
88
+ const b = bodyOf(ctx);
89
+ if (typeof b.video_url !== 'string' || !/^https?:\/\//.test(b.video_url))
90
+ return refuse(ctx, PARAM, 'video_url must be an http(s) URL.');
91
+ if (b.post_info === undefined || typeof b.post_info !== 'object')
92
+ return refuse(ctx, PARAM, 'post_info is required.');
93
+ const post = publishObj(b.post_info);
94
+ if (typeof post.is_brand_organic !== 'boolean' || typeof post.is_branded_content !== 'boolean')
95
+ return refuse(ctx, PARAM, 'post_info.is_brand_organic and post_info.is_branded_content are required.');
96
+ return publish(ctx, 'VIDEO', [b.video_url], post, post.upload_to_draft === true, c.person);
97
+ }
98
+ // 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."
99
+ export async function businessPhotoPublish(ctx) {
100
+ const c = authorized(ctx, 'video.publish');
101
+ if (c instanceof Response)
102
+ return c;
103
+ const b = bodyOf(ctx);
104
+ const images = Array.isArray(b.photo_images) ? b.photo_images.map(String) : [];
105
+ if (images.length < 1 || images.length > 35)
106
+ return refuse(ctx, PARAM, 'photo_images must hold 1 to 35 URLs.');
107
+ const cover = Number(b.photo_cover_index ?? 0);
108
+ if (!Number.isInteger(cover) || cover < 0 || cover >= images.length)
109
+ return refuse(ctx, PARAM, 'photo_cover_index must index photo_images.');
110
+ const post = publishObj(b.post_info);
111
+ const draft = post.is_draft === true;
112
+ if (typeof post.is_brand_organic !== 'boolean' || typeof post.is_branded_content !== 'boolean')
113
+ return refuse(ctx, PARAM, 'post_info.is_brand_organic and post_info.is_branded_content are required.');
114
+ // 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)."
115
+ if (typeof post.title === 'string' && post.title.length > 90)
116
+ return refuse(ctx, PARAM, 'post_info.title is over 90 characters.');
117
+ if (!draft && !privacyOptions(c.person).includes(String(post.privacy_level)))
118
+ return refuse(ctx, PARAM, 'post_info.privacy_level must be one of the account\'s privacy levels.');
119
+ return publish(ctx, 'PHOTO', images, post, draft, c.person);
120
+ }
121
+ // 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."
122
+ // post_ids are "Returned only when" the status is PUBLISH_COMPLETE, the reason when FAILED
123
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1816387106635778&language=ENGLISH "Returned only when"
124
+ export async function businessPublishStatus(ctx) {
125
+ const c = authorized(ctx);
126
+ if (c instanceof Response)
127
+ return c;
128
+ const id = String(ctx.params.publish_id ?? '');
129
+ const p = ctx.row(PUBLISH, id);
130
+ if (!p || p.deleted === true)
131
+ return refuse(ctx, PARAM, 'publish_id does not exist.');
132
+ if (p._person !== c.person.id)
133
+ return refuse(ctx, NO_PERMISSION, 'No permission to read this publishing task.');
134
+ return ok(ctx, { status: p.status, ...(p.status === 'PUBLISH_COMPLETE' ? { post_ids: p.post_ids ?? [] } : {}), ...(p.status === 'FAILED' ? { reason: p.reason } : {}) });
135
+ }
136
+ // 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."
137
+ export async function businessPublishLocation(ctx) {
138
+ const c = authorized(ctx);
139
+ if (c instanceof Response)
140
+ return c;
141
+ const q = String(ctx.params.search_query ?? '');
142
+ if (!q || q.length > 100)
143
+ return refuse(ctx, PARAM, 'search_query must be 1 to 100 characters.');
144
+ const words = q.toLowerCase().split(/\s+/).filter(Boolean);
145
+ const found = ctx.rowsRaw('_location').filter((l) => words.every((w) => `${String(l.location_name)} ${String(l.location_address)}`.toLowerCase().includes(w)));
146
+ return ok(ctx, { locations: found.map((l) => ({ location_id: l.id, location_name: l.location_name, location_address: l.location_address })) });
147
+ }
148
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH "Commercial Music Library"
149
+ export async function discoveryCmlTrendingList(ctx) {
150
+ const c = authorized(ctx);
151
+ if (c instanceof Response)
152
+ return c;
153
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH "Default value"
154
+ const genre = typeof ctx.params.genre === 'string' && ctx.params.genre.toUpperCase() !== 'ALL' ? ctx.params.genre.toUpperCase() : undefined;
155
+ const tracks = ctx.rowsRaw('_track').filter((t) => !genre || t.genre === genre);
156
+ return ok(ctx, {
157
+ list: tracks.map((t) => ({
158
+ 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,
159
+ full_duration_song_clip: { song_clip_id: t.song_clip_id, duration: t.duration, preview_url: t.preview_url },
160
+ })),
161
+ });
162
+ }
@@ -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
+ }