@volter/twin-tiktok 0.1.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 (76) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +310 -0
  3. package/client/tiktok-consent.tsx +154 -0
  4. package/client/tiktok-mirror.css +137 -0
  5. package/client/tiktok-mirror.tsx +492 -0
  6. package/dist/client/tiktok-consent.bundle.js +18 -0
  7. package/dist/client/tiktok-consent.d.ts +47 -0
  8. package/dist/client/tiktok-consent.js +20 -0
  9. package/dist/client/tiktok-consent.tsx +154 -0
  10. package/dist/client/tiktok-mirror.bundle.js +487 -0
  11. package/dist/client/tiktok-mirror.css +137 -0
  12. package/dist/client/tiktok-mirror.d.ts +42 -0
  13. package/dist/client/tiktok-mirror.js +315 -0
  14. package/dist/client/tiktok-mirror.tsx +492 -0
  15. package/dist/src/cli.d.ts +2 -0
  16. package/dist/src/cli.js +44 -0
  17. package/dist/src/index.d.ts +22 -0
  18. package/dist/src/index.js +167 -0
  19. package/dist/src/tiktok-blobs.d.ts +66 -0
  20. package/dist/src/tiktok-blobs.js +161 -0
  21. package/dist/src/tiktok-budget.d.ts +56 -0
  22. package/dist/src/tiktok-budget.js +136 -0
  23. package/dist/src/tiktok-capabilities.d.ts +7 -0
  24. package/dist/src/tiktok-capabilities.js +1855 -0
  25. package/dist/src/tiktok-conformance.d.ts +11 -0
  26. package/dist/src/tiktok-conformance.js +498 -0
  27. package/dist/src/tiktok-connector.d.ts +158 -0
  28. package/dist/src/tiktok-connector.js +600 -0
  29. package/dist/src/tiktok-consent-ui.d.ts +19 -0
  30. package/dist/src/tiktok-consent-ui.js +127 -0
  31. package/dist/src/tiktok-errors.d.ts +78 -0
  32. package/dist/src/tiktok-errors.js +175 -0
  33. package/dist/src/tiktok-ids.d.ts +16 -0
  34. package/dist/src/tiktok-ids.js +48 -0
  35. package/dist/src/tiktok-media.d.ts +7 -0
  36. package/dist/src/tiktok-media.js +86 -0
  37. package/dist/src/tiktok-mirror-ui.d.ts +49 -0
  38. package/dist/src/tiktok-mirror-ui.js +159 -0
  39. package/dist/src/tiktok-pkce.d.ts +25 -0
  40. package/dist/src/tiktok-pkce.js +56 -0
  41. package/dist/src/tiktok-posting.d.ts +100 -0
  42. package/dist/src/tiktok-posting.js +599 -0
  43. package/dist/src/tiktok-sample-mp4.d.ts +10 -0
  44. package/dist/src/tiktok-sample-mp4.js +55 -0
  45. package/dist/src/tiktok-scopes.d.ts +29 -0
  46. package/dist/src/tiktok-scopes.js +106 -0
  47. package/dist/src/tiktok-server.d.ts +28 -0
  48. package/dist/src/tiktok-server.js +89 -0
  49. package/dist/src/tiktok-store.d.ts +164 -0
  50. package/dist/src/tiktok-store.js +451 -0
  51. package/dist/src/tiktok-twin.d.ts +70 -0
  52. package/dist/src/tiktok-twin.js +1197 -0
  53. package/dist/src/tiktok-user.d.ts +28 -0
  54. package/dist/src/tiktok-user.js +174 -0
  55. package/package.json +74 -0
  56. package/src/cli.ts +43 -0
  57. package/src/index.ts +270 -0
  58. package/src/tiktok-blobs.ts +217 -0
  59. package/src/tiktok-budget.ts +163 -0
  60. package/src/tiktok-capabilities.ts +2022 -0
  61. package/src/tiktok-conformance.ts +526 -0
  62. package/src/tiktok-connector.ts +637 -0
  63. package/src/tiktok-consent-ui.ts +146 -0
  64. package/src/tiktok-errors.ts +197 -0
  65. package/src/tiktok-ids.ts +51 -0
  66. package/src/tiktok-journey.uitest.ts +305 -0
  67. package/src/tiktok-media.ts +89 -0
  68. package/src/tiktok-mirror-ui.ts +167 -0
  69. package/src/tiktok-pkce.ts +61 -0
  70. package/src/tiktok-posting.ts +617 -0
  71. package/src/tiktok-sample-mp4.ts +54 -0
  72. package/src/tiktok-scopes.ts +122 -0
  73. package/src/tiktok-server.ts +100 -0
  74. package/src/tiktok-store.ts +543 -0
  75. package/src/tiktok-twin.ts +1361 -0
  76. package/src/tiktok-user.ts +137 -0
@@ -0,0 +1,137 @@
1
+ // The two FIELD CATALOGS the TikTok open API selects over, and the projections that render them.
2
+ //
3
+ // Both are `fields`-driven: a caller names the fields it wants in a query parameter and gets back
4
+ // exactly those, which means the field list itself is vendor surface — a wrong name here is a
5
+ // twin that accepts what TikTok refuses.
6
+ //
7
+ // • USER_FIELDS — the Get User Info reference's own table
8
+ // (developers.tiktok.com/docs/en/tiktok-api-v2-get-user-info, fetched 2026-09-13), including
9
+ // the SCOPE each field is gated behind. That gating is the reason the table carries a scope
10
+ // column at all: asking for `username` with only `user.info.basic` granted is the documented
11
+ // `scope_not_authorized` case, and it is exactly the bug a Login Kit integration hits when the
12
+ // user unchecks a permission on the consent sheet.
13
+ //
14
+ // • VIDEO_FIELDS — the Video Object reference's own table
15
+ // (developers.tiktok.com/doc/tiktok-api-v2-video-object), which `/v2/video/list/` and
16
+ // `/v2/video/query/` both select over. Every field there is gated behind the single
17
+ // `video.list` scope.
18
+ import type { Row } from './tiktok-store.ts';
19
+
20
+ export const USER_INFO_SCOPES = ['user.info.basic', 'user.info.profile', 'user.info.stats'] as const;
21
+
22
+ /** Field -> the scope that gates it, in the reference table's own order. */
23
+ export const USER_FIELD_SCOPES: Record<string, string> = {
24
+ open_id: 'user.info.basic',
25
+ union_id: 'user.info.basic',
26
+ avatar_url: 'user.info.basic',
27
+ avatar_url_100: 'user.info.basic',
28
+ avatar_large_url: 'user.info.basic',
29
+ display_name: 'user.info.basic',
30
+ bio_description: 'user.info.profile',
31
+ profile_deep_link: 'user.info.profile',
32
+ is_verified: 'user.info.profile',
33
+ username: 'user.info.profile',
34
+ follower_count: 'user.info.stats',
35
+ following_count: 'user.info.stats',
36
+ likes_count: 'user.info.stats',
37
+ video_count: 'user.info.stats',
38
+ };
39
+
40
+ export const USER_FIELDS: readonly string[] = Object.keys(USER_FIELD_SCOPES);
41
+
42
+ /**
43
+ * Render an account row as the vendor's User object for a requested field set.
44
+ *
45
+ * `open_id` is APP-SCOPED and therefore comes from the grant, not from the account — that is the
46
+ * whole distinction TikTok draws between `open_id` and `union_id`. A modelled field whose value
47
+ * this persona genuinely lacks is OMITTED rather than served as a placeholder (the connector's
48
+ * "invent nothing" rule, applied at the read side too).
49
+ */
50
+ export function renderUser(account: Row, openId: string, fields: readonly string[]): Record<string, unknown> {
51
+ const out: Record<string, unknown> = {};
52
+ const put = (key: string, value: unknown) => {
53
+ if (value !== undefined && value !== null) out[key] = value;
54
+ };
55
+ for (const field of fields) {
56
+ switch (field) {
57
+ case 'open_id': put(field, openId); break;
58
+ case 'union_id': put(field, account.id); break;
59
+ case 'avatar_url': put(field, account.avatarUrl); break;
60
+ case 'avatar_url_100': put(field, account.avatarUrl100); break;
61
+ case 'avatar_large_url': put(field, account.avatarLargeUrl); break;
62
+ case 'display_name': put(field, account.displayName); break;
63
+ case 'bio_description': put(field, account.bioDescription); break;
64
+ case 'profile_deep_link': put(field, account.profileDeepLink); break;
65
+ case 'is_verified': put(field, typeof account.isVerified === 'boolean' ? account.isVerified : undefined); break;
66
+ case 'username': put(field, account.username); break;
67
+ case 'follower_count': put(field, account.followerCount); break;
68
+ case 'following_count': put(field, account.followingCount); break;
69
+ case 'likes_count': put(field, account.likesCount); break;
70
+ case 'video_count': put(field, account.videoCount); break;
71
+ default: break;
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /** The Video Object's fields, verbatim from the vendor's own reference table. */
78
+ export const VIDEO_FIELDS: readonly string[] = [
79
+ 'id',
80
+ 'create_time',
81
+ 'cover_image_url',
82
+ 'share_url',
83
+ 'video_description',
84
+ 'duration',
85
+ 'height',
86
+ 'width',
87
+ 'title',
88
+ 'embed_html',
89
+ 'embed_link',
90
+ 'like_count',
91
+ 'comment_count',
92
+ 'share_count',
93
+ 'view_count',
94
+ 'is_aigc',
95
+ ];
96
+
97
+ /** The single scope every Display API video read is gated behind. */
98
+ export const VIDEO_SCOPE = 'video.list';
99
+
100
+ /**
101
+ * Render a video row for a requested field set.
102
+ *
103
+ * The four LINK fields are DERIVED, deterministically, from the stored row — TikTok composes them
104
+ * the same way (`share_url` is the canonical `/@handle/video/<id>` permalink and `embed_link` the
105
+ * `/embed/v2/<id>` player). Deriving keeps them consistent with the id and the owner's handle
106
+ * instead of storing four strings that could drift apart; the exact CDN host of `cover_image_url`
107
+ * is a twin host, not the vendor's, and `tiktok.video.link_shapes` (todo) pins the live forms.
108
+ */
109
+ export function renderVideo(video: Row, ownerUsername: string, fields: readonly string[]): Record<string, unknown> {
110
+ const shareUrl = `https://www.tiktok.com/@${ownerUsername}/video/${video.id}`;
111
+ const out: Record<string, unknown> = {};
112
+ const put = (key: string, value: unknown) => {
113
+ if (value !== undefined && value !== null) out[key] = value;
114
+ };
115
+ for (const field of fields) {
116
+ switch (field) {
117
+ case 'id': put(field, video.id); break;
118
+ case 'create_time': put(field, video.createTime); break;
119
+ case 'cover_image_url': put(field, `https://p16-sign.tiktokcdn-us.com/twin/${video.id}~tplv-cover.jpeg`); break;
120
+ case 'share_url': put(field, shareUrl); break;
121
+ case 'video_description': put(field, video.videoDescription); break;
122
+ case 'duration': put(field, video.duration); break;
123
+ case 'height': put(field, video.height); break;
124
+ case 'width': put(field, video.width); break;
125
+ case 'title': put(field, video.title); break;
126
+ case 'embed_html': put(field, `<blockquote class="tiktok-embed" cite="${shareUrl}" data-video-id="${video.id}"></blockquote>`); break;
127
+ case 'embed_link': put(field, `https://www.tiktok.com/embed/v2/${video.id}`); break;
128
+ case 'like_count': put(field, video.likeCount); break;
129
+ case 'comment_count': put(field, video.commentCount); break;
130
+ case 'share_count': put(field, video.shareCount); break;
131
+ case 'view_count': put(field, video.viewCount); break;
132
+ case 'is_aigc': put(field, video.isAigc === true); break;
133
+ default: break;
134
+ }
135
+ }
136
+ return out;
137
+ }