@volter/twin-tiktok 0.1.36 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +25 -308
  2. package/business/spec/SOURCE.md +17 -0
  3. package/business/spec/openapi.json +1045 -0
  4. package/business/spec/sources.json +84 -0
  5. package/business/src/fetch.ts +14 -0
  6. package/business/src/generated/surface.gen.json +1 -0
  7. package/business/src/generated/ui.gen.json +1 -0
  8. package/business/src/index.ts +3 -0
  9. package/business/src/manifest.ts +38 -0
  10. package/business/src/semantics/index.ts +16 -0
  11. package/business/src/semantics/open_api.ts +142 -0
  12. package/business/src/semantics/shared.ts +119 -0
  13. package/business/src/semantics/states.ts +3 -0
  14. package/dist/business/spec/SOURCE.md +17 -0
  15. package/dist/business/spec/openapi.json +1045 -0
  16. package/dist/business/spec/sources.json +84 -0
  17. package/dist/business/src/fetch.d.ts +2 -0
  18. package/dist/business/src/fetch.js +13 -0
  19. package/dist/business/src/fetch.ts +14 -0
  20. package/dist/business/src/generated/surface.gen.json +1 -0
  21. package/dist/business/src/generated/ui.gen.json +1 -0
  22. package/dist/business/src/index.d.ts +2 -0
  23. package/dist/business/src/index.js +3 -0
  24. package/dist/business/src/index.ts +3 -0
  25. package/dist/business/src/manifest.d.ts +2 -0
  26. package/dist/business/src/manifest.js +33 -0
  27. package/dist/business/src/manifest.ts +38 -0
  28. package/dist/business/src/semantics/index.d.ts +2 -0
  29. package/dist/business/src/semantics/index.js +12 -0
  30. package/dist/business/src/semantics/index.ts +16 -0
  31. package/dist/business/src/semantics/open_api.d.ts +10 -0
  32. package/dist/business/src/semantics/open_api.js +162 -0
  33. package/dist/business/src/semantics/open_api.ts +142 -0
  34. package/dist/business/src/semantics/shared.d.ts +31 -0
  35. package/dist/business/src/semantics/shared.js +109 -0
  36. package/dist/business/src/semantics/shared.ts +119 -0
  37. package/dist/business/src/semantics/states.d.ts +8 -0
  38. package/dist/business/src/semantics/states.js +3 -0
  39. package/dist/business/src/semantics/states.ts +3 -0
  40. package/dist/generated/pack-facts.json +55 -0
  41. package/dist/journeys/coverage.json +220 -0
  42. package/dist/journeys/customer-life.json +2950 -0
  43. package/dist/journeys/customer-life.outline.md +59 -0
  44. package/dist/journeys/decisions.json +161 -0
  45. package/dist/journeys/demand.json +142 -0
  46. package/dist/journeys/score.json +294 -0
  47. package/dist/journeys/unreachable.json +13 -0
  48. package/dist/open/spec/SOURCE.md +20 -0
  49. package/dist/open/spec/openapi.json +1417 -0
  50. package/dist/open/spec/sources.json +124 -0
  51. package/dist/open/src/engine/mp4.d.ts +6 -0
  52. package/dist/open/src/engine/mp4.js +55 -0
  53. package/dist/open/src/engine/mp4.ts +48 -0
  54. package/dist/open/src/fetch.d.ts +2 -0
  55. package/dist/open/src/fetch.js +17 -0
  56. package/dist/open/src/fetch.ts +18 -0
  57. package/dist/open/src/generated/surface.gen.json +1 -0
  58. package/dist/open/src/generated/ui.gen.json +1 -0
  59. package/dist/open/src/index.d.ts +2 -0
  60. package/dist/open/src/index.js +3 -0
  61. package/dist/open/src/index.ts +3 -0
  62. package/dist/open/src/manifest.d.ts +2 -0
  63. package/dist/open/src/manifest.js +53 -0
  64. package/dist/open/src/manifest.ts +59 -0
  65. package/dist/open/src/screens/authorize.d.ts +2 -0
  66. package/dist/open/src/screens/authorize.js +53 -0
  67. package/dist/open/src/screens/authorize.ts +53 -0
  68. package/dist/open/src/semantics/doors.d.ts +10 -0
  69. package/dist/open/src/semantics/doors.js +118 -0
  70. package/dist/open/src/semantics/doors.ts +121 -0
  71. package/dist/open/src/semantics/index.d.ts +2 -0
  72. package/dist/open/src/semantics/index.js +16 -0
  73. package/dist/open/src/semantics/index.ts +20 -0
  74. package/dist/open/src/semantics/oauth.d.ts +2 -0
  75. package/dist/open/src/semantics/oauth.js +30 -0
  76. package/dist/open/src/semantics/oauth.ts +28 -0
  77. package/dist/open/src/semantics/post.d.ts +6 -0
  78. package/dist/open/src/semantics/post.js +66 -0
  79. package/dist/open/src/semantics/post.ts +60 -0
  80. package/dist/open/src/semantics/shared.d.ts +51 -0
  81. package/dist/open/src/semantics/shared.js +196 -0
  82. package/dist/open/src/semantics/shared.ts +197 -0
  83. package/dist/open/src/semantics/states.d.ts +20 -0
  84. package/dist/open/src/semantics/states.js +15 -0
  85. package/dist/open/src/semantics/states.ts +23 -0
  86. package/dist/open/src/semantics/user.d.ts +2 -0
  87. package/dist/open/src/semantics/user.js +24 -0
  88. package/dist/open/src/semantics/user.ts +24 -0
  89. package/dist/open/src/semantics/video.d.ts +4 -0
  90. package/dist/open/src/semantics/video.js +112 -0
  91. package/dist/open/src/semantics/video.ts +97 -0
  92. package/dist/src/cli.js +11 -34
  93. package/dist/src/fetch.d.ts +2 -0
  94. package/dist/src/fetch.js +9 -0
  95. package/dist/src/index.d.ts +5 -22
  96. package/dist/src/index.js +13 -167
  97. package/dist/src/manifest.d.ts +2 -0
  98. package/dist/src/manifest.js +28 -0
  99. package/dist/src/semantics/shared.d.ts +49 -0
  100. package/dist/src/semantics/shared.js +61 -0
  101. package/dist/src/server.d.ts +7 -0
  102. package/dist/src/server.js +7 -0
  103. package/generated/pack-facts.json +55 -0
  104. package/journeys/coverage.json +220 -0
  105. package/journeys/customer-life.json +2950 -0
  106. package/journeys/customer-life.outline.md +59 -0
  107. package/journeys/decisions.json +161 -0
  108. package/journeys/demand.json +142 -0
  109. package/journeys/score.json +294 -0
  110. package/journeys/unreachable.json +13 -0
  111. package/open/spec/SOURCE.md +20 -0
  112. package/open/spec/openapi.json +1417 -0
  113. package/open/spec/sources.json +124 -0
  114. package/open/src/engine/mp4.ts +48 -0
  115. package/open/src/fetch.ts +18 -0
  116. package/open/src/generated/surface.gen.json +1 -0
  117. package/open/src/generated/ui.gen.json +1 -0
  118. package/open/src/index.ts +3 -0
  119. package/open/src/manifest.ts +59 -0
  120. package/open/src/screens/authorize.ts +53 -0
  121. package/open/src/semantics/doors.ts +121 -0
  122. package/open/src/semantics/index.ts +20 -0
  123. package/open/src/semantics/oauth.ts +28 -0
  124. package/open/src/semantics/post.ts +60 -0
  125. package/open/src/semantics/shared.ts +197 -0
  126. package/open/src/semantics/states.ts +23 -0
  127. package/open/src/semantics/user.ts +24 -0
  128. package/open/src/semantics/video.ts +97 -0
  129. package/package.json +13 -41
  130. package/src/cli.ts +11 -32
  131. package/src/fetch.ts +10 -0
  132. package/src/index.ts +15 -270
  133. package/src/manifest.ts +33 -0
  134. package/src/semantics/shared.ts +78 -0
  135. package/src/server.ts +8 -0
  136. package/client/tiktok-consent.tsx +0 -154
  137. package/client/tiktok-mirror.css +0 -137
  138. package/client/tiktok-mirror.tsx +0 -497
  139. package/dist/client/tiktok-consent.bundle.js +0 -18
  140. package/dist/client/tiktok-consent.d.ts +0 -47
  141. package/dist/client/tiktok-consent.js +0 -20
  142. package/dist/client/tiktok-consent.tsx +0 -154
  143. package/dist/client/tiktok-mirror.bundle.js +0 -484
  144. package/dist/client/tiktok-mirror.css +0 -137
  145. package/dist/client/tiktok-mirror.d.ts +0 -42
  146. package/dist/client/tiktok-mirror.js +0 -325
  147. package/dist/client/tiktok-mirror.tsx +0 -497
  148. package/dist/src/tiktok-blobs.d.ts +0 -66
  149. package/dist/src/tiktok-blobs.js +0 -161
  150. package/dist/src/tiktok-budget.d.ts +0 -56
  151. package/dist/src/tiktok-budget.js +0 -136
  152. package/dist/src/tiktok-capabilities.d.ts +0 -7
  153. package/dist/src/tiktok-capabilities.js +0 -1855
  154. package/dist/src/tiktok-conformance.d.ts +0 -11
  155. package/dist/src/tiktok-conformance.js +0 -498
  156. package/dist/src/tiktok-connector.d.ts +0 -158
  157. package/dist/src/tiktok-connector.js +0 -600
  158. package/dist/src/tiktok-consent-ui.d.ts +0 -19
  159. package/dist/src/tiktok-consent-ui.js +0 -127
  160. package/dist/src/tiktok-errors.d.ts +0 -78
  161. package/dist/src/tiktok-errors.js +0 -175
  162. package/dist/src/tiktok-ids.d.ts +0 -16
  163. package/dist/src/tiktok-ids.js +0 -48
  164. package/dist/src/tiktok-media.d.ts +0 -7
  165. package/dist/src/tiktok-media.js +0 -86
  166. package/dist/src/tiktok-mirror-ui.d.ts +0 -61
  167. package/dist/src/tiktok-mirror-ui.js +0 -187
  168. package/dist/src/tiktok-pkce.d.ts +0 -25
  169. package/dist/src/tiktok-pkce.js +0 -56
  170. package/dist/src/tiktok-posting.d.ts +0 -100
  171. package/dist/src/tiktok-posting.js +0 -599
  172. package/dist/src/tiktok-sample-mp4.d.ts +0 -10
  173. package/dist/src/tiktok-sample-mp4.js +0 -55
  174. package/dist/src/tiktok-scopes.d.ts +0 -29
  175. package/dist/src/tiktok-scopes.js +0 -106
  176. package/dist/src/tiktok-server.d.ts +0 -28
  177. package/dist/src/tiktok-server.js +0 -99
  178. package/dist/src/tiktok-store.d.ts +0 -164
  179. package/dist/src/tiktok-store.js +0 -451
  180. package/dist/src/tiktok-twin.d.ts +0 -70
  181. package/dist/src/tiktok-twin.js +0 -1197
  182. package/dist/src/tiktok-user.d.ts +0 -28
  183. package/dist/src/tiktok-user.js +0 -174
  184. package/src/tiktok-blobs.ts +0 -217
  185. package/src/tiktok-budget.ts +0 -163
  186. package/src/tiktok-capabilities.ts +0 -2022
  187. package/src/tiktok-conformance.ts +0 -526
  188. package/src/tiktok-connector.ts +0 -637
  189. package/src/tiktok-consent-ui.ts +0 -146
  190. package/src/tiktok-errors.ts +0 -197
  191. package/src/tiktok-ids.ts +0 -51
  192. package/src/tiktok-journey.uitest.ts +0 -305
  193. package/src/tiktok-media.ts +0 -89
  194. package/src/tiktok-mirror-ui.ts +0 -193
  195. package/src/tiktok-pkce.ts +0 -61
  196. package/src/tiktok-posting.ts +0 -617
  197. package/src/tiktok-sample-mp4.ts +0 -54
  198. package/src/tiktok-scopes.ts +0 -122
  199. package/src/tiktok-server.ts +0 -110
  200. package/src/tiktok-store.ts +0 -543
  201. package/src/tiktok-twin.ts +0 -1361
  202. package/src/tiktok-user.ts +0 -137
@@ -0,0 +1,31 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ import { type Row } from '../../../src/semantics/shared.js';
3
+ export * from '../../../src/semantics/shared.js';
4
+ export type { HandlerContext };
5
+ export declare const ok: (ctx: Pick<HandlerContext, "crypto" | "occurredAt">, data: unknown) => Response;
6
+ /** A refusal: its code and message, at HTTP 200. */
7
+ export declare const refuse: (ctx: Pick<HandlerContext, "crypto" | "occurredAt">, code: number, message: string) => Response;
8
+ export declare const PARAM = 40002;
9
+ export declare const NO_PERMISSION = 40001;
10
+ /** Who a request is: the business access token its Access-Token header bears, live, with its app and its person. */
11
+ export type Caller = {
12
+ token: Row;
13
+ app: Row;
14
+ person: Row;
15
+ scopes: string[];
16
+ openId: string;
17
+ };
18
+ export declare function authorized(ctx: HandlerContext, scope?: string): Caller | Response;
19
+ /** A JSON-array query parameter (`fields`, as Postiz sends it), or the refusal. */
20
+ export declare function jsonArray(ctx: HandlerContext, name: string): string[] | undefined | Response;
21
+ export declare function appOf(ctx: HandlerContext): Promise<Row | Response>;
22
+ export declare function issue(ctx: HandlerContext, app: Row, person: Row, scope: string): Promise<Response>;
23
+ export declare const DEFAULT: string[];
24
+ export declare const profileFields: string[];
25
+ export declare const postsFields: string[];
26
+ export declare const publishObj: (v: unknown) => Row;
27
+ /** Pull each URL through the World; the bytes pulled, or undefined when one fails. */
28
+ export declare function pullAll(ctx: HandlerContext, urls: string[]): Promise<number | undefined>;
29
+ /** The video a completed pull makes (the Video Object, as the open lane makes them), public; its id. */
30
+ export declare function madeVideo(ctx: HandlerContext, publishId: string, post: Row, person: Row): Promise<string>;
31
+ export declare function publish(ctx: HandlerContext, media: 'VIDEO' | 'PHOTO', urls: string[], post: Row, draft: boolean, person: Row): Promise<Response>;
@@ -0,0 +1,109 @@
1
+ import { ACCESS, ACCESS_TTL, APP, PERSON, PUBLISH, REFRESH, REFRESH_TTL, VIDEO, bodyOf, bySha, clientSecret, credential, mediaUrl, next, openIdOf, seconds, videoMoment } from "../../../src/semantics/shared.js";
2
+ export * from "../../../src/semantics/shared.js";
3
+ /** A request's id, as the API answers it. */
4
+ const requestId = (ctx, what) => `${ctx.occurredAt.replace(/\D/g, '').slice(0, 14)}${ctx.crypto.digest('sha256', `${ctx.occurredAt}:business:${what}`, 'hex').slice(0, 18).toUpperCase()}`;
5
+ // source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Successful"
6
+ export const ok = (ctx, data) => Response.json({ code: 0, message: 'OK', request_id: requestId(ctx, JSON.stringify(data)), data });
7
+ /** A refusal: its code and message, at HTTP 200. */
8
+ export const refuse = (ctx, code, message) => Response.json({ code, message, request_id: requestId(ctx, `${code}:${message}`), data: {} });
9
+ // the codes the lane answers (Appendix - Return codes)
10
+ export const PARAM = 40002;
11
+ export const NO_PERMISSION = 40001;
12
+ // 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."
13
+ export function authorized(ctx, scope) {
14
+ const value = ctx.call.request.headers.get('access-token') ?? '';
15
+ // 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."
16
+ if (!value)
17
+ return refuse(ctx, 40104, 'The access token is empty.');
18
+ const token = bySha(ctx, ACCESS, value);
19
+ if (!token || token.kind !== 'business' || token.revoked === true)
20
+ return refuse(ctx, 40105, 'Invalid or incorrect access token.');
21
+ // 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."
22
+ if (Number(token.expires_at) <= seconds(ctx.occurredAt))
23
+ return refuse(ctx, 40102, 'The access token has expired.');
24
+ const app = ctx.row(APP, String(token.app));
25
+ const person = ctx.row(PERSON, String(token.person));
26
+ const openId = openIdOf(ctx, app, person);
27
+ const businessId = String(ctx.params.business_id ?? '');
28
+ // 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."
29
+ if (businessId !== openId)
30
+ return refuse(ctx, NO_PERMISSION, 'No permission to operate this TikTok account: business_id is not the access token\'s.');
31
+ const scopes = String(token.scope).split(',');
32
+ if (scope && !scopes.includes(scope))
33
+ return refuse(ctx, NO_PERMISSION, `No permission: the TikTok account did not grant ${scope}.`);
34
+ return { token, app, person, scopes, openId };
35
+ }
36
+ /** A JSON-array query parameter (`fields`, as Postiz sends it), or the refusal. */
37
+ export function jsonArray(ctx, name) {
38
+ const raw = new URL(ctx.call.request.url).searchParams.get(name);
39
+ if (raw === null)
40
+ return undefined;
41
+ try {
42
+ const v = JSON.parse(raw);
43
+ if (Array.isArray(v) && v.every((x) => typeof x === 'string'))
44
+ return v;
45
+ }
46
+ catch { /* the refusal below */ }
47
+ return refuse(ctx, PARAM, `${name} must be a JSON array of strings.`);
48
+ }
49
+ export async function appOf(ctx) {
50
+ const b = bodyOf(ctx);
51
+ const app = ctx.rowsRaw(APP).find((a) => a.deleted !== true && a.kind === 'business' && a.client_key === b.client_id);
52
+ // 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."
53
+ if (!app || b.client_secret !== await clientSecret(ctx, app))
54
+ return refuse(ctx, 40131, 'App ID or secret is incorrect.');
55
+ return app;
56
+ }
57
+ export async function issue(ctx, app, person, scope) {
58
+ const now = seconds(ctx.occurredAt);
59
+ const access = await credential(ctx, ACCESS, 'act', { kind: 'business', app: app.id, person: person.id, scope, expires_at: now + ACCESS_TTL, revoked: false });
60
+ 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 });
61
+ 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) });
62
+ }
63
+ // 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"
64
+ export const DEFAULT = ['display_name', 'profile_image'];
65
+ export const profileFields = ['display_name', 'profile_image', 'username', 'profile_deep_link', 'bio_description', 'is_verified', 'followers_count', 'following_count', 'total_likes', 'videos_count'];
66
+ export const postsFields = ['item_id', 'create_time', 'thumbnail_url', 'share_url', 'embed_url', 'caption', 'video_duration', 'likes', 'comments', 'shares', 'video_views'];
67
+ export const publishObj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
68
+ /** Pull each URL through the World; the bytes pulled, or undefined when one fails. */
69
+ export async function pullAll(ctx, urls) {
70
+ let n = 0;
71
+ for (const url of urls) {
72
+ try {
73
+ const r = await ctx.vendorFetch(url, { method: 'GET' });
74
+ if (!r.ok)
75
+ return undefined;
76
+ n += (await r.arrayBuffer()).byteLength;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ return n;
83
+ }
84
+ /** The video a completed pull makes (the Video Object, as the open lane makes them), public; its id. */
85
+ export async function madeVideo(ctx, publishId, post, person) {
86
+ const videoId = String(7_400_000_000_000_000_000 + (await next(ctx, 'video')) * 1024);
87
+ const caption = typeof post.caption === 'string' ? post.caption : '';
88
+ await ctx.write(VIDEO, videoId, {
89
+ 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}`,
90
+ video_description: caption.slice(0, 150), duration: 0, height: 0, width: 0, title: caption.slice(0, 150),
91
+ embed_html: `<blockquote class="tiktok-embed" cite="https://www.tiktok.com/@${String(person.username)}/video/${videoId}" data-video-id="${videoId}"></blockquote>`,
92
+ 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,
93
+ _person: person.id, _public: true, _publish: publishId,
94
+ }, 'video.publish');
95
+ return videoId;
96
+ }
97
+ export async function publish(ctx, media, urls, post, draft, person) {
98
+ const n = await next(ctx, 'publish');
99
+ const id = `${media === 'VIDEO' ? 'v' : 'p'}_pub_url~v2.${n}`;
100
+ 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');
101
+ const pulled = await pullAll(ctx, urls);
102
+ const to = pulled === undefined ? 'FAILED' : draft ? 'SEND_TO_USER_INBOX' : 'PUBLISH_COMPLETE';
103
+ const refused = ctx.legal(PUBLISH, 'status', 'publish.pull', 'PROCESSING_DOWNLOAD', to, id, 'vendor');
104
+ if (refused)
105
+ throw new Error(refused.message);
106
+ const postIds = to === 'PUBLISH_COMPLETE' && media === 'VIDEO' ? [await madeVideo(ctx, id, post, person)] : [];
107
+ await ctx.write(PUBLISH, id, { status: to, post_ids: postIds, ...(to === 'FAILED' ? { reason: media === 'VIDEO' ? 'video_pull_failed' : 'photo_pull_failed' } : {}) }, 'publish.pull');
108
+ return ok(ctx, { share_id: id });
109
+ }
@@ -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,8 @@
1
+ export declare const states: {
2
+ publish: {
3
+ state: {
4
+ status: import("@volter/world-core").StateField;
5
+ };
6
+ notState: string[];
7
+ };
8
+ };
@@ -0,0 +1,3 @@
1
+ // Both lanes import the vendor’s one publish status declaration.
2
+ import { publish } from "../../../src/semantics/shared.js";
3
+ export const states = { publish };
@@ -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,55 @@
1
+ {
2
+ "//": "GENERATED by scripts/pack-facts.ts — current pack descriptors and explicit world-runtime server backings; no prior-artifact entries are retained.",
3
+ "packs": {
4
+ "tiktok": {
5
+ "adoption": {
6
+ "sdks": [],
7
+ "envStems": [
8
+ "TIKTOK"
9
+ ]
10
+ },
11
+ "hosts": [
12
+ {
13
+ "host": "business-api.tiktok.com"
14
+ },
15
+ {
16
+ "host": "open.tiktokapis.com"
17
+ },
18
+ {
19
+ "host": "open-upload.tiktokapis.com"
20
+ },
21
+ {
22
+ "host": "upload.us.tiktokapis.com"
23
+ },
24
+ {
25
+ "host": "www.tiktok.com",
26
+ "pathPattern": "^/v2/auth/authorize"
27
+ }
28
+ ],
29
+ "archetype": "crud",
30
+ "protocol": {
31
+ "declared": "3",
32
+ "major": 3,
33
+ "standing": "current"
34
+ },
35
+ "transport": "rest",
36
+ "resources": [
37
+ "publish",
38
+ "video"
39
+ ],
40
+ "endpointEnvNone": "Postiz calls open.tiktokapis.com and sends people to www.tiktok.com; the World claims the hosts",
41
+ "credentialDoor": {
42
+ "path": "/_twin/app-credentials",
43
+ "body": {},
44
+ "fill": {
45
+ "TIKTOK_CLIENT_ID": "client_key",
46
+ "TIKTOK_CLIENT_SECRET": "client_secret",
47
+ "TIKTOK_BUSINESS_CLIENT_ID": "business_client_id",
48
+ "TIKTOK_BUSINESS_CLIENT_SECRET": "business_client_secret"
49
+ }
50
+ },
51
+ "serveExport": "createTiktokTwinServer"
52
+ }
53
+ },
54
+ "undeclared": []
55
+ }
@@ -0,0 +1,220 @@
1
+ {
2
+ "vendor": "tiktok",
3
+ "missing": 0,
4
+ "stale": [],
5
+ "code": {
6
+ "lines": 851,
7
+ "run": 849,
8
+ "recorded": 2,
9
+ "missing": 0
10
+ },
11
+ "state": {
12
+ "declared": 6,
13
+ "reached": 6,
14
+ "recorded": 0,
15
+ "missing": [],
16
+ "undeclared": []
17
+ },
18
+ "files": [
19
+ {
20
+ "file": "semantics/shared.ts",
21
+ "lines": {
22
+ "found": 39,
23
+ "hit": 39
24
+ },
25
+ "missing": []
26
+ },
27
+ {
28
+ "file": "manifest.ts",
29
+ "lines": {
30
+ "found": 28,
31
+ "hit": 28
32
+ },
33
+ "missing": []
34
+ },
35
+ {
36
+ "file": "index.ts",
37
+ "lines": {
38
+ "found": 10,
39
+ "hit": 10
40
+ },
41
+ "missing": []
42
+ },
43
+ {
44
+ "file": "server.ts",
45
+ "lines": {
46
+ "found": 4,
47
+ "hit": 2
48
+ },
49
+ "missing": []
50
+ },
51
+ {
52
+ "file": "fetch.ts",
53
+ "lines": {
54
+ "found": 6,
55
+ "hit": 6
56
+ },
57
+ "missing": []
58
+ },
59
+ {
60
+ "file": "../business/src/semantics/states.ts",
61
+ "lines": {
62
+ "found": 2,
63
+ "hit": 2
64
+ },
65
+ "missing": []
66
+ },
67
+ {
68
+ "file": "../business/src/semantics/shared.ts",
69
+ "lines": {
70
+ "found": 78,
71
+ "hit": 78
72
+ },
73
+ "missing": []
74
+ },
75
+ {
76
+ "file": "../business/src/semantics/open_api.ts",
77
+ "lines": {
78
+ "found": 105,
79
+ "hit": 105
80
+ },
81
+ "missing": []
82
+ },
83
+ {
84
+ "file": "../business/src/semantics/index.ts",
85
+ "lines": {
86
+ "found": 12,
87
+ "hit": 12
88
+ },
89
+ "missing": []
90
+ },
91
+ {
92
+ "file": "../business/src/manifest.ts",
93
+ "lines": {
94
+ "found": 27,
95
+ "hit": 27
96
+ },
97
+ "missing": []
98
+ },
99
+ {
100
+ "file": "../business/src/index.ts",
101
+ "lines": {
102
+ "found": 2,
103
+ "hit": 2
104
+ },
105
+ "missing": []
106
+ },
107
+ {
108
+ "file": "../business/src/fetch.ts",
109
+ "lines": {
110
+ "found": 10,
111
+ "hit": 10
112
+ },
113
+ "missing": []
114
+ },
115
+ {
116
+ "file": "../open/src/semantics/doors.ts",
117
+ "lines": {
118
+ "found": 81,
119
+ "hit": 81
120
+ },
121
+ "missing": []
122
+ },
123
+ {
124
+ "file": "../open/src/semantics/oauth.ts",
125
+ "lines": {
126
+ "found": 21,
127
+ "hit": 21
128
+ },
129
+ "missing": []
130
+ },
131
+ {
132
+ "file": "../open/src/semantics/video.ts",
133
+ "lines": {
134
+ "found": 76,
135
+ "hit": 76
136
+ },
137
+ "missing": []
138
+ },
139
+ {
140
+ "file": "../open/src/semantics/states.ts",
141
+ "lines": {
142
+ "found": 11,
143
+ "hit": 11
144
+ },
145
+ "missing": []
146
+ },
147
+ {
148
+ "file": "../open/src/semantics/shared.ts",
149
+ "lines": {
150
+ "found": 126,
151
+ "hit": 126
152
+ },
153
+ "missing": []
154
+ },
155
+ {
156
+ "file": "../open/src/semantics/index.ts",
157
+ "lines": {
158
+ "found": 16,
159
+ "hit": 16
160
+ },
161
+ "missing": []
162
+ },
163
+ {
164
+ "file": "../open/src/semantics/user.ts",
165
+ "lines": {
166
+ "found": 18,
167
+ "hit": 18
168
+ },
169
+ "missing": []
170
+ },
171
+ {
172
+ "file": "../open/src/semantics/post.ts",
173
+ "lines": {
174
+ "found": 39,
175
+ "hit": 39
176
+ },
177
+ "missing": []
178
+ },
179
+ {
180
+ "file": "../open/src/screens/authorize.ts",
181
+ "lines": {
182
+ "found": 39,
183
+ "hit": 39
184
+ },
185
+ "missing": []
186
+ },
187
+ {
188
+ "file": "../open/src/manifest.ts",
189
+ "lines": {
190
+ "found": 45,
191
+ "hit": 45
192
+ },
193
+ "missing": []
194
+ },
195
+ {
196
+ "file": "../open/src/index.ts",
197
+ "lines": {
198
+ "found": 2,
199
+ "hit": 2
200
+ },
201
+ "missing": []
202
+ },
203
+ {
204
+ "file": "../open/src/engine/mp4.ts",
205
+ "lines": {
206
+ "found": 40,
207
+ "hit": 40
208
+ },
209
+ "missing": []
210
+ },
211
+ {
212
+ "file": "../open/src/fetch.ts",
213
+ "lines": {
214
+ "found": 14,
215
+ "hit": 14
216
+ },
217
+ "missing": []
218
+ }
219
+ ]
220
+ }