@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,492 @@
1
+ // TikTok mirror — React/TSX client. tiktok.com's own view of the twin's state: log in as the creator,
2
+ // read their profile over the Videos grid, and open a post in the vertical player. Every read is the
3
+ // twin's OWN Display API on the same origin (GET /v2/user/info/, POST /v2/video/list/); the player
4
+ // plays the posted bytes from the twin's media route. Nothing is kept but the signed-in username and
5
+ // token, in the tab's sessionStorage.
6
+ import { createRoot } from 'react-dom/client';
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
+ import {
9
+ avatarHue, avatarInitial, BASIC_FIELDS, captionOf, captionSegments, clock, compactCount, mediaSource, PROFILE_FIELDS, profileStats,
10
+ VIDEO_PAGE_SIZE, VIDEO_READ_FIELDS, type TtRow,
11
+ } from '../src/tiktok-mirror-ui.ts';
12
+
13
+ // WHERE THIS MIRROR LIVES. Served at a vendor root the base is '' (fetches are root-relative); served
14
+ // under a path prefix with a <base> tag, every read resolves inside that prefix.
15
+ const WIRE_BASE = typeof document === 'undefined' || document.querySelector('base[href]') === null ? '' : new URL('.', document.baseURI).pathname.replace(/\/$/, '');
16
+
17
+ const SESSION_HANDLE = 'tiktok-mirror.username';
18
+ const SESSION_TOKEN = 'tiktok-mirror.token';
19
+
20
+ type Session = { handle: string; token: string };
21
+ type Wire = { status: number; body: TtRow };
22
+
23
+ function readSession(): Session | null {
24
+ if (typeof sessionStorage === 'undefined') return null;
25
+ const handle = sessionStorage.getItem(SESSION_HANDLE);
26
+ const token = sessionStorage.getItem(SESSION_TOKEN);
27
+ return handle && token ? { handle, token } : null;
28
+ }
29
+
30
+ async function wire(token: string, path: string, init?: { method?: string; body?: unknown }): Promise<Wire> {
31
+ const res = await fetch(`${WIRE_BASE}${path}`, {
32
+ method: init?.method ?? 'GET',
33
+ headers: { authorization: `Bearer ${token}`, ...(init?.body !== undefined ? { 'content-type': 'application/json' } : {}) },
34
+ ...(init?.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
35
+ });
36
+ let body: TtRow = {};
37
+ try { body = await res.json() as TtRow; } catch { body = {}; }
38
+ return { status: res.status, body };
39
+ }
40
+
41
+ /** TikTok's nested error, as one sentence for the screen. */
42
+ const errorText = (w: Wire): string => String(w.body?.error?.message ?? `HTTP ${w.status}`);
43
+ const errorCode = (w: Wire): string => String(w.body?.error?.code ?? '');
44
+
45
+ /** The signed-in creator: every profile field, or — for a token without the profile/stats scopes —
46
+ * the basic ones, so the header shows what the twin returns and nothing it does not. */
47
+ async function readMe(token: string): Promise<Wire> {
48
+ const full = await wire(token, `/v2/user/info/?fields=${PROFILE_FIELDS}`);
49
+ if (full.status === 401 && errorCode(full) === 'scope_not_authorized') return wire(token, `/v2/user/info/?fields=${BASIC_FIELDS}`);
50
+ return full;
51
+ }
52
+
53
+ // ── routing: hash routes, so the shell answers every screen under any mount ─────────────────────
54
+ type Route = { page: 'login' } | { page: 'profile'; handle: string } | { page: 'video'; handle: string; id: string };
55
+
56
+ function parseRoute(hash: string): Route | null {
57
+ const parts = hash.replace(/^#\/?/, '').split('/').filter(Boolean);
58
+ if (parts.length === 0) return null;
59
+ if (parts[0] === 'login') return { page: 'login' };
60
+ const handle = decodeURIComponent(parts[0]!).replace(/^@/, '');
61
+ if (parts[1] === 'video' && parts[2]) return { page: 'video', handle, id: parts[2] };
62
+ return { page: 'profile', handle };
63
+ }
64
+ const go = (path: string) => { location.hash = `#/${path}`; };
65
+
66
+ // ── icons (vendor mirror: tiktok.com's look, drawn here) ────────────────────────────────────────
67
+ const NOTE = 'M16.6 5.82s.51.5 0 0A4.28 4.28 0 0 1 15.54 3h-3.09v12.4a2.59 2.59 0 0 1-2.59 2.5c-1.42 0-2.6-1.16-2.6-2.6 0-1.72 1.66-3.01 3.37-2.48V9.66c-3.45-.46-6.47 2.22-6.47 5.64 0 3.33 2.76 5.7 5.69 5.7 3.14 0 5.69-2.55 5.69-5.7V9.01a7.35 7.35 0 0 0 4.3 1.38V7.3s-1.88.09-3.24-1.48z';
68
+ export function Logo({ size = 34, word = true }: { size?: number; word?: boolean }) {
69
+ return (
70
+ <span className="logo">
71
+ <svg viewBox="0 0 24 24" width={size} height={size} aria-hidden="true">
72
+ <path d={NOTE} fill="#25F4EE" transform="translate(-0.9 -0.7)" />
73
+ <path d={NOTE} fill="#FE2C55" transform="translate(0.9 0.7)" />
74
+ <path d={NOTE} fill="currentColor" />
75
+ </svg>
76
+ {word ? <span className="logo-word">TikTok</span> : null}
77
+ </span>
78
+ );
79
+ }
80
+ function Icon({ d, size = 24, fill = false, className }: { d: string[]; size?: number; fill?: boolean; className?: string }) {
81
+ return (
82
+ <svg className={className} viewBox="0 0 24 24" width={size} height={size} aria-hidden="true"
83
+ fill={fill ? 'currentColor' : 'none'} stroke={fill ? 'none' : 'currentColor'} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
84
+ {d.map((p) => <path key={p} d={p} />)}
85
+ </svg>
86
+ );
87
+ }
88
+ const I_HOME = ['M3 10.5 12 3l9 7.5V21h-6v-6H9v6H3z'];
89
+ const I_EXPLORE = ['M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z', 'm16.2 7.8-2.1 6.3-6.3 2.1 2.1-6.3z'];
90
+ const I_FOLLOWING = ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2', 'M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z', 'M19 8v6', 'M22 11h-6'];
91
+ const I_FRIENDS = ['M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2', 'M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z', 'M23 21v-2a4 4 0 0 0-3-3.87', 'M16 3.13a4 4 0 0 1 0 7.75'];
92
+ const I_LIVE = ['M4 6h16v12H4z', 'm10 9 5 3-5 3z'];
93
+ const I_UPLOAD = ['M12 5v14', 'M5 12h14'];
94
+ const I_PROFILE = ['M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2', 'M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z'];
95
+ const I_PLAY = ['M7 4.5v15l12-7.5z'];
96
+ const I_PAUSE = ['M7 4h3.5v16H7z', 'M13.5 4H17v16h-3.5z'];
97
+ const I_CLOSE = ['M18 6 6 18', 'm6 6 12 12'];
98
+ const I_UP = ['m18 15-6-6-6 6'];
99
+ const I_DOWN = ['m6 9 6 6 6-6'];
100
+ const I_SHARE_ARROW = ['M14 4.5V9C7 9 3.5 13 3 19.5c2-3.5 5-5 11-5V19l7-7.25z'];
101
+ const I_HEART = ['M12 21s-7.5-4.6-9.6-9.2C.9 8.4 2.9 4.5 6.7 4.5c2.2 0 3.7 1.2 5.3 3 1.6-1.8 3.1-3 5.3-3 3.8 0 5.8 3.9 4.3 7.3C19.5 16.4 12 21 12 21z'];
102
+ const I_COMMENT = ['M12 3C6.5 3 2 6.9 2 11.7c0 2.6 1.3 4.9 3.4 6.5L4.7 22l4.6-2.4c.9.2 1.8.3 2.7.3 5.5 0 10-3.9 10-8.7S17.5 3 12 3z'];
103
+ const I_BOOKMARK = ['M6 2.5h12a1 1 0 0 1 1 1V22l-7-4.5L5 22V3.5a1 1 0 0 1 1-1z'];
104
+ const I_VERIFIED = ['M12 1.5 14.6 3.4l3.2-.2.9 3.1 2.8 1.6-1 3.1 1 3.1-2.8 1.6-.9 3.1-3.2-.2L12 22.5l-2.6-1.9-3.2.2-.9-3.1-2.8-1.6 1-3.1-1-3.1 2.8-1.6.9-3.1 3.2.2z'];
105
+ const I_CHECK = ['m7.5 12.2 3 3 6-6.4'];
106
+ const I_MUSIC = ['M9 18V5l12-2v13', 'M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z', 'M18 19a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'];
107
+ const I_VOLUME = ['M11 5 6 9H2v6h4l5 4z', 'M15.5 8.5a5 5 0 0 1 0 7', 'M19 5a10 10 0 0 1 0 14'];
108
+ const I_MUTED = ['M11 5 6 9H2v6h4l5 4z', 'm22 9-6 6', 'm16 9 6 6'];
109
+ const I_LOCK = ['M5 11h14v10H5z', 'M8 11V7a4 4 0 0 1 8 0v4'];
110
+ const I_GRID = ['M4 4h7v7H4z', 'M13 4h7v7h-7z', 'M4 13h7v7H4z', 'M13 13h7v7h-7z'];
111
+
112
+ // ── pieces ───────────────────────────────────────────────────────────────────────────────────────
113
+ export function Avatar({ user, size }: { user: TtRow | undefined; size: number }) {
114
+ return (
115
+ <div className="avatar" style={{ width: size, height: size, fontSize: size * 0.42, background: `hsl(${avatarHue(user?.union_id ?? user?.username)} 55% 45%)` }} aria-hidden="true">
116
+ {avatarInitial(user)}
117
+ </div>
118
+ );
119
+ }
120
+
121
+ function Verified({ size = 20 }: { size?: number }) {
122
+ return (
123
+ <span className="verified" title="Verified account">
124
+ <Icon d={I_VERIFIED} fill size={size} />
125
+ <Icon d={I_CHECK} size={size} className="verified-check" />
126
+ </span>
127
+ );
128
+ }
129
+
130
+ export function Caption({ text }: { text: unknown }) {
131
+ return (
132
+ <span className="caption">
133
+ {captionSegments(text).map((s, i) => s.kind === 'text' ? <span key={i}>{s.value}</span> : <strong key={i} className="tag">{s.value}</strong>)}
134
+ </span>
135
+ );
136
+ }
137
+
138
+ /** The profile header: avatar, @username (h1) with the verified tick, the nickname, the buttons a
139
+ * creator sees on their own page, the three counts (only those the twin returned) and the bio. */
140
+ export function ProfileHeader({ user, own }: { user: TtRow; own: boolean }) {
141
+ const stats = profileStats(user);
142
+ return (
143
+ <header className="profile-header">
144
+ <Avatar user={user} size={212} />
145
+ <div className="profile-info">
146
+ <div className="profile-names">
147
+ <h1 className="profile-username">{String(user.username ?? user.display_name ?? '')}</h1>
148
+ {user.is_verified === true ? <Verified /> : null}
149
+ <h2 className="profile-nickname">{String(user.display_name ?? '')}</h2>
150
+ </div>
151
+ <div className="profile-buttons">
152
+ {own ? (
153
+ <>
154
+ <button className="btn btn-red">Edit profile</button>
155
+ <button className="btn btn-gray">Promote post</button>
156
+ </>
157
+ ) : (
158
+ <>
159
+ <button className="btn btn-red">Follow</button>
160
+ <button className="btn btn-gray">Message</button>
161
+ </>
162
+ )}
163
+ </div>
164
+ {stats.length > 0 ? (
165
+ <div className="profile-stats">
166
+ {stats.map((s) => <span key={s.label} className="stat" data-stat={s.label}><strong>{s.value}</strong><span className="stat-label">{s.label}</span></span>)}
167
+ </div>
168
+ ) : null}
169
+ {typeof user.bio_description === 'string' && user.bio_description !== '' ? <p className="profile-bio">{user.bio_description}</p> : <p className="profile-bio muted">No bio yet.</p>}
170
+ </div>
171
+ </header>
172
+ );
173
+ }
174
+
175
+ /** One grid tile: the post's own first frame (the twin serves no cover image), 9:16, with the play
176
+ * count at the bottom left. A post whose bytes the twin does not hold shows its caption instead. */
177
+ export function VideoTile({ video, src, onOpen }: { video: TtRow; src: string; onOpen: () => void }) {
178
+ const [broken, setBroken] = useState(false);
179
+ return (
180
+ <div className="tile" data-video-id={String(video.id)} onClick={onOpen} role="link" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter') onOpen(); }}>
181
+ {broken ? <div className="tile-fallback"><Caption text={captionOf(video)} /></div> : (
182
+ <video className="tile-video" src={`${src}#t=0.1`} preload="metadata" muted playsInline onError={() => setBroken(true)}
183
+ onMouseEnter={(e) => { void e.currentTarget.play().catch(() => undefined); }}
184
+ onMouseLeave={(e) => { e.currentTarget.pause(); }} />
185
+ )}
186
+ {typeof video.view_count === 'number' ? (
187
+ <span className="tile-views"><Icon d={I_PLAY} size={16} />{compactCount(video.view_count)}</span>
188
+ ) : null}
189
+ </div>
190
+ );
191
+ }
192
+
193
+ export function VideoGrid({ videos, sourceOf, onOpen }: { videos: TtRow[]; sourceOf: (id: string) => string; onOpen: (id: string) => void }) {
194
+ return (
195
+ <div className="grid">
196
+ {videos.map((v) => <VideoTile key={String(v.id)} video={v} src={sourceOf(String(v.id))} onOpen={() => onOpen(String(v.id))} />)}
197
+ </div>
198
+ );
199
+ }
200
+
201
+ /** The player's overlay: @handle, the caption with its tags, and the sound line TikTok prints for a
202
+ * video's own audio ("original sound - <nickname>"). */
203
+ export function PlayerCaption({ user, video }: { user: TtRow; video: TtRow }) {
204
+ return (
205
+ <div className="player-caption">
206
+ <div className="player-author">@{String(user.username ?? user.display_name ?? '')}{user.is_verified === true ? <Verified size={16} /> : null}</div>
207
+ <div className="player-text"><Caption text={captionOf(video)} /></div>
208
+ <div className="player-sound"><Icon d={I_MUSIC} size={14} /><span>original sound - {String(user.display_name ?? user.username ?? '')}</span></div>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ /** The right-hand action rail: the creator's avatar, then like / comment / save / share, each with the
214
+ * count the Video Object carries — and no number where the Display API returns none (saves). */
215
+ export function ActionRail({ user, video }: { user: TtRow; video: TtRow }) {
216
+ const count = (raw: unknown) => (typeof raw === 'number' ? compactCount(raw) : '');
217
+ return (
218
+ <div className="rail">
219
+ <div className="rail-avatar"><Avatar user={user} size={48} /><span className="rail-plus">+</span></div>
220
+ <div className="rail-item"><span className="rail-btn"><Icon d={I_HEART} fill size={26} /></span><strong>{count(video.like_count)}</strong></div>
221
+ <div className="rail-item"><span className="rail-btn"><Icon d={I_COMMENT} fill size={26} /></span><strong>{count(video.comment_count)}</strong></div>
222
+ <div className="rail-item"><span className="rail-btn"><Icon d={I_BOOKMARK} fill size={24} /></span><strong /></div>
223
+ <div className="rail-item"><span className="rail-btn"><Icon d={I_SHARE_ARROW} fill size={26} /></span><strong>{count(video.share_count)}</strong></div>
224
+ </div>
225
+ );
226
+ }
227
+
228
+ /** The full-height vertical player: the post playing, looped, in a 9:16 frame as tall as the window,
229
+ * over a blurred copy of itself; tap to pause; the caption and the rail over it; up/down to the
230
+ * neighbouring posts; close back to the profile. */
231
+ function Player({ user, video, src, prev, next, onClose }: { user: TtRow; video: TtRow; src: string; prev?: string; next?: string; onClose: () => void }) {
232
+ const ref = useRef<HTMLVideoElement | null>(null);
233
+ const [paused, setPaused] = useState(false);
234
+ const [muted, setMuted] = useState(false);
235
+ const [time, setTime] = useState({ at: 0, of: Number(video.duration) || 0 });
236
+ const toggle = () => {
237
+ const el = ref.current;
238
+ if (!el) return;
239
+ if (el.paused) { void el.play().catch(() => undefined); setPaused(false); } else { el.pause(); setPaused(true); }
240
+ };
241
+ useEffect(() => {
242
+ const onKey = (e: KeyboardEvent) => {
243
+ if (e.key === 'Escape') onClose();
244
+ if (e.key === 'ArrowUp' && prev) go(`@${user.username}/video/${prev}`);
245
+ if (e.key === 'ArrowDown' && next) go(`@${user.username}/video/${next}`);
246
+ };
247
+ window.addEventListener('keydown', onKey);
248
+ return () => window.removeEventListener('keydown', onKey);
249
+ }, [prev, next, onClose, user.username]);
250
+ return (
251
+ <div className="player-page" data-video-id={String(video.id)}>
252
+ <video className="player-backdrop" src={src} muted autoPlay loop playsInline aria-hidden="true" />
253
+ <button className="player-close" aria-label="Close" onClick={onClose}><Icon d={I_CLOSE} size={24} /></button>
254
+ <div className="player-logo"><Logo size={30} /></div>
255
+ <div className="player-stage">
256
+ <div className="player-frame" onClick={toggle}>
257
+ <button className="player-sound-toggle" aria-label={muted ? 'Unmute' : 'Mute'} onClick={(e) => { e.stopPropagation(); setMuted(!muted); }}><Icon d={muted ? I_MUTED : I_VOLUME} size={20} /></button>
258
+ <video ref={ref} className="player-video" src={src} autoPlay loop playsInline muted={muted}
259
+ onTimeUpdate={(e) => setTime({ at: e.currentTarget.currentTime, of: e.currentTarget.duration || time.of })}
260
+ onPlay={() => setPaused(false)} onPause={() => setPaused(true)} />
261
+ {paused ? <span className="player-paused"><Icon d={I_PLAY} fill size={64} /></span> : null}
262
+ <div className="player-shade" />
263
+ <PlayerCaption user={user} video={video} />
264
+ <div className="player-bar" onClick={(e) => e.stopPropagation()}>
265
+ <button className="player-toggle" aria-label={paused ? 'Play' : 'Pause'} onClick={toggle}><Icon d={paused ? I_PLAY : I_PAUSE} fill size={18} /></button>
266
+ <div className="player-track"><div className="player-progress" style={{ width: `${time.of > 0 ? Math.min(100, (time.at / time.of) * 100) : 0}%` }} /></div>
267
+ <span className="player-time">{clock(time.at)}/{clock(time.of)}</span>
268
+ </div>
269
+ </div>
270
+ <ActionRail user={user} video={video} />
271
+ </div>
272
+ <div className="player-nav">
273
+ <button aria-label="Previous video" disabled={!prev} onClick={() => prev && go(`@${user.username}/video/${prev}`)}><Icon d={I_UP} size={26} /></button>
274
+ <button aria-label="Next video" disabled={!next} onClick={() => next && go(`@${user.username}/video/${next}`)}><Icon d={I_DOWN} size={26} /></button>
275
+ </div>
276
+ </div>
277
+ );
278
+ }
279
+
280
+ // ── the creator's videos: POST /v2/video/list/, newest first, paged on the vendor's own cursor ────
281
+ function useVideos(session: Session | null, reloadKey: number): { videos: TtRow[]; error: string | null; more: (() => void) | null; loaded: boolean } {
282
+ const [videos, setVideos] = useState<TtRow[]>([]);
283
+ const [cursor, setCursor] = useState<number | null>(null);
284
+ const [error, setError] = useState<string | null>(null);
285
+ const [loaded, setLoaded] = useState(false);
286
+ const load = useCallback(async (from: number | null, prev: TtRow[]) => {
287
+ if (!session) return;
288
+ const w = await wire(session.token, `/v2/video/list/?fields=${VIDEO_READ_FIELDS}`, { method: 'POST', body: { max_count: VIDEO_PAGE_SIZE, ...(from !== null ? { cursor: from } : {}) } });
289
+ setLoaded(true);
290
+ if (w.status !== 200) { setError(errorCode(w) === 'scope_not_authorized' ? "This token can't read the account's videos (it needs video.list)." : errorText(w)); return; }
291
+ setError(null);
292
+ const page = Array.isArray(w.body?.data?.videos) ? w.body.data.videos as TtRow[] : [];
293
+ setVideos([...prev, ...page]);
294
+ setCursor(w.body?.data?.has_more === true ? Number(w.body.data.cursor) : null);
295
+ }, [session]);
296
+ useEffect(() => { setVideos([]); setCursor(null); setLoaded(false); void load(null, []); }, [load, reloadKey]);
297
+ return { videos, error, loaded, more: cursor !== null ? () => void load(cursor, videos) : null };
298
+ }
299
+
300
+ // ── screens ──────────────────────────────────────────────────────────────────────────────────────
301
+ function SideNav({ me, active, onSignOut }: { me: TtRow | null; active: 'profile' | 'none'; onSignOut: () => void }) {
302
+ const item = (icon: string[], label: string, on = false, onClick?: () => void) => (
303
+ <button className={on ? 'nav-item active' : 'nav-item'} onClick={onClick}><Icon d={icon} size={26} /><span>{label}</span></button>
304
+ );
305
+ return (
306
+ <nav className="sidenav">
307
+ <a className="nav-logo" href={me ? `#/@${me.username}` : '#/'}><Logo /></a>
308
+ <div className="nav-search"><span>Search</span></div>
309
+ {item(I_HOME, 'For You')}
310
+ {item(I_EXPLORE, 'Explore')}
311
+ {item(I_FOLLOWING, 'Following')}
312
+ {item(I_FRIENDS, 'Friends')}
313
+ {item(I_UPLOAD, 'Upload')}
314
+ {item(I_LIVE, 'LIVE')}
315
+ {me ? (
316
+ <button className={active === 'profile' ? 'nav-item active' : 'nav-item'} onClick={() => go(`@${me.username}`)}>
317
+ <Avatar user={me} size={32} /><span>Profile</span>
318
+ </button>
319
+ ) : item(I_PROFILE, 'Profile')}
320
+ <div className="nav-foot">
321
+ {me ? <button className="nav-signout" onClick={onSignOut}>Log out @{String(me.username ?? '')}</button> : null}
322
+ <div className="muted">© 2026 TikTok</div>
323
+ </div>
324
+ </nav>
325
+ );
326
+ }
327
+
328
+ function ProfileScreen({ session, me, handle }: { session: Session; me: TtRow; handle: string }) {
329
+ const own = handle === String(me.username ?? '');
330
+ const read = useVideos(own ? session : null, 0);
331
+ const [tab, setTab] = useState<'videos' | 'reposts' | 'favorites' | 'liked'>('videos');
332
+ if (!own) {
333
+ // The Display API reads only the token's own account; another creator's page is not readable here.
334
+ return <div className="profile-missing"><h2>Couldn't find this account</h2><p className="muted">Looking for videos? Try browsing our trending creators, hashtags, and sounds.</p></div>;
335
+ }
336
+ return (
337
+ <div className="profile">
338
+ <ProfileHeader user={me} own />
339
+ <div className="profile-tabs">
340
+ <button className={tab === 'videos' ? 'ptab active' : 'ptab'} onClick={() => setTab('videos')}><Icon d={I_GRID} size={18} />Videos</button>
341
+ <button className={tab === 'reposts' ? 'ptab active' : 'ptab'} onClick={() => setTab('reposts')}>Reposts</button>
342
+ <button className={tab === 'favorites' ? 'ptab active' : 'ptab'} onClick={() => setTab('favorites')}><Icon d={I_LOCK} size={16} />Favorites</button>
343
+ <button className={tab === 'liked' ? 'ptab active' : 'ptab'} onClick={() => setTab('liked')}><Icon d={I_LOCK} size={16} />Liked</button>
344
+ {tab === 'videos' ? <div className="sort"><span className="sort-on">Latest</span><span>Popular</span><span>Oldest</span></div> : null}
345
+ </div>
346
+ {tab !== 'videos' ? <div className="empty"><h3>Nothing here yet</h3><p className="muted">This twin models the account's own posts.</p></div>
347
+ : read.error ? <div className="empty" role="alert">{read.error}</div>
348
+ : read.loaded && read.videos.length === 0 ? <div className="empty"><h3>Upload your first video</h3><p className="muted">Your videos will appear here</p></div>
349
+ : (
350
+ <>
351
+ <VideoGrid videos={read.videos} sourceOf={(id) => mediaSource(WIRE_BASE, id, session.token)} onOpen={(id) => go(`@${me.username}/video/${id}`)} />
352
+ {read.more ? <button className="show-more" onClick={read.more}>Load more</button> : null}
353
+ </>
354
+ )}
355
+ </div>
356
+ );
357
+ }
358
+
359
+ function VideoScreen({ session, me, id }: { session: Session; me: TtRow; id: string }) {
360
+ const read = useVideos(session, 0);
361
+ const at = read.videos.findIndex((v) => String(v.id) === id);
362
+ const close = useCallback(() => go(`@${me.username}`), [me.username]);
363
+ if (!read.loaded) return <div className="player-page" />;
364
+ if (at < 0) return <div className="player-page"><div className="player-missing">Video currently unavailable</div><button className="player-close" aria-label="Close" onClick={close}><Icon d={I_CLOSE} size={24} /></button></div>;
365
+ return (
366
+ <Player key={id} user={me} video={read.videos[at]!} src={mediaSource(WIRE_BASE, id, session.token)}
367
+ {...(at > 0 ? { prev: String(read.videos[at - 1]!.id) } : {})}
368
+ {...(at < read.videos.length - 1 ? { next: String(read.videos[at + 1]!.id) } : {})}
369
+ onClose={close} />
370
+ );
371
+ }
372
+
373
+ function LoginScreen({ onSignedIn }: { onSignedIn: (s: Session, me: TtRow) => void }) {
374
+ const [step, setStep] = useState<'username' | 'token'>('username');
375
+ const [handle, setHandle] = useState('');
376
+ const [token, setToken] = useState('');
377
+ const [error, setError] = useState<string | null>(null);
378
+ const [busy, setBusy] = useState(false);
379
+ const clean = handle.trim().replace(/^@/, '');
380
+ const next = () => {
381
+ if (!/^[A-Za-z0-9_.]{2,24}$/.test(clean)) { setError("Couldn't find this account"); return; }
382
+ setError(null); setStep('token');
383
+ };
384
+ const login = async () => {
385
+ setBusy(true);
386
+ const w = await readMe(token.trim());
387
+ setBusy(false);
388
+ if (w.status === 401 && errorCode(w) === 'access_token_invalid') { setError('Incorrect account or password'); return; }
389
+ if (w.status !== 200 || !w.body?.data?.user) { setError(errorText(w)); return; }
390
+ const me = w.body.data.user as TtRow;
391
+ // Is this token THIS account's? The Display API answers the token's own account, so the answer says.
392
+ if (me.username !== undefined && String(me.username) !== clean) { setError(`That token does not log in as @${clean}.`); return; }
393
+ onSignedIn({ handle: clean, token: token.trim() }, { ...me, username: me.username ?? clean });
394
+ };
395
+ return (
396
+ <div className="login-backdrop">
397
+ <div className="login">
398
+ <div className="login-logo"><Logo size={40} word={false} /></div>
399
+ {step === 'username' ? (
400
+ <form className="login-form" onSubmit={(e) => { e.preventDefault(); next(); }}>
401
+ <h1>Log in to TikTok</h1>
402
+ <div className="login-sub">Username</div>
403
+ <input autoFocus name="username" className="login-input" placeholder="Username" value={handle} onChange={(e) => setHandle(e.target.value)} autoComplete="username" />
404
+ {error ? <div className="login-error" role="alert">{error}</div> : null}
405
+ <button className="btn-login" type="submit" disabled={clean === ''}>Next</button>
406
+ <div className="muted login-note">This twin logs you in with the access token it issued for the account (<code>/_twin/tokens</code>), in place of a password.</div>
407
+ </form>
408
+ ) : (
409
+ <form className="login-form" onSubmit={(e) => { e.preventDefault(); void login(); }}>
410
+ <h1>Log in</h1>
411
+ <div className="login-sub">@{clean} <button type="button" className="link" onClick={() => { setStep('username'); setError(null); }}>Change</button></div>
412
+ <input autoFocus name="token" className="login-input" type="password" placeholder="Access token" value={token} onChange={(e) => setToken(e.target.value)} autoComplete="current-password" />
413
+ {error ? <div className="login-error" role="alert">{error}</div> : null}
414
+ <button className="btn-login" type="submit" disabled={token.trim() === '' || busy}>Log in</button>
415
+ </form>
416
+ )}
417
+ <div className="login-foot">Don't have an account? <span className="link-red">Sign up</span></div>
418
+ </div>
419
+ </div>
420
+ );
421
+ }
422
+
423
+ function App() {
424
+ const [session, setSession] = useState<Session | null>(readSession);
425
+ const [me, setMe] = useState<TtRow | null>(null);
426
+ const [route, setRoute] = useState<Route | null>(() => parseRoute(typeof location === 'undefined' ? '' : location.hash));
427
+
428
+ useEffect(() => {
429
+ const onHash = () => { setRoute(parseRoute(location.hash)); window.scrollTo(0, 0); };
430
+ // a `#/…` link is one of the mirror's own routes; under a World's <base> the browser would resolve
431
+ // it against the base and leave the shell, so it moves the hash here
432
+ const onClick = (e: MouseEvent) => {
433
+ const a = (e.target as Element | null)?.closest?.('a[href^="#/"]');
434
+ if (!a || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
435
+ e.preventDefault();
436
+ location.hash = a.getAttribute('href')!;
437
+ };
438
+ window.addEventListener('hashchange', onHash);
439
+ document.addEventListener('click', onClick, true);
440
+ return () => { window.removeEventListener('hashchange', onHash); document.removeEventListener('click', onClick, true); };
441
+ }, []);
442
+
443
+ const signOut = useCallback(() => {
444
+ sessionStorage.removeItem(SESSION_HANDLE);
445
+ sessionStorage.removeItem(SESSION_TOKEN);
446
+ setSession(null);
447
+ setMe(null);
448
+ go('login');
449
+ }, []);
450
+
451
+ // who am I: the signed-in creator, read back through the vendor's own user read
452
+ useEffect(() => {
453
+ if (!session) { setMe(null); return; }
454
+ void readMe(session.token).then((w) => {
455
+ if (w.status === 200 && w.body?.data?.user) setMe({ ...w.body.data.user, username: w.body.data.user.username ?? session.handle });
456
+ else if (w.status === 401) signOut();
457
+ });
458
+ }, [session, signOut]);
459
+
460
+ const signIn = (s: Session, user: TtRow) => {
461
+ sessionStorage.setItem(SESSION_HANDLE, s.handle);
462
+ sessionStorage.setItem(SESSION_TOKEN, s.token);
463
+ setSession(s);
464
+ setMe(user);
465
+ go(`@${s.handle}`);
466
+ };
467
+
468
+ if (!session || route?.page === 'login') {
469
+ return session && route?.page === 'login' ? <LoginRedirect handle={session.handle} /> : <LoginScreen onSignedIn={signIn} />;
470
+ }
471
+ if (!me) return <div className="loading"><Logo /></div>;
472
+ const current: Route = route ?? { page: 'profile', handle: String(me.username) };
473
+ if (current.page === 'video') return <VideoScreen session={session} me={me} id={current.id} />;
474
+ return (
475
+ <div className="layout">
476
+ <SideNav me={me} active={current.page === 'profile' && current.handle === me.username ? 'profile' : 'none'} onSignOut={signOut} />
477
+ <main className="main">
478
+ <ProfileScreen session={session} me={me} handle={current.page === 'profile' ? current.handle : String(me.username)} />
479
+ </main>
480
+ </div>
481
+ );
482
+ }
483
+
484
+ function LoginRedirect({ handle }: { handle: string }) {
485
+ useEffect(() => { go(`@${handle}`); }, [handle]);
486
+ return null;
487
+ }
488
+
489
+ if (typeof document !== 'undefined') {
490
+ const el = document.getElementById('root');
491
+ if (el) createRoot(el).render(<App />);
492
+ }
@@ -0,0 +1,18 @@
1
+ var uB=Object.create;var{getPrototypeOf:sB,defineProperty:VB,getOwnPropertyNames:yB}=Object;var dB=Object.prototype.hasOwnProperty;function pB(Q){return this[Q]}var cB,oB,JB=(Q,F,K)=>{var A=Q!=null&&typeof Q==="object";if(A){var v=F?cB??=new WeakMap:oB??=new WeakMap,E=v.get(Q);if(E)return E}K=Q!=null?uB(sB(Q)):{};let I=F||!Q||!Q.__esModule?VB(K,"default",{value:Q,enumerable:!0}):K;for(let w of yB(Q))if(!dB.call(I,w))VB(I,w,{get:pB.bind(Q,w),enumerable:!0});if(A)v.set(Q,I);return I};var vB=(Q,F)=>()=>(F||Q((F={exports:{}}).exports,F),F.exports);var SB=vB((iB,ZB)=>{(function(){function Q(B,H){Object.defineProperty(A.prototype,B,{get:function(){console.warn("%s(...) is deprecated in plain JavaScript React classes. %s",H[0],H[1])}})}function F(B){if(B===null||typeof B!=="object")return null;return B=PB&&B[PB]||B["@@iterator"],typeof B==="function"?B:null}function K(B,H){B=(B=B.constructor)&&(B.displayName||B.name)||"ReactClass";var U=B+"."+H;FB[U]||(console.error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.",H,B),FB[U]=!0)}function A(B,H,U){this.props=B,this.context=H,this.refs=OB,this.updater=U||KB}function v(){}function E(B,H,U){this.props=B,this.context=H,this.refs=OB,this.updater=U||KB}function I(){}function w(B){return""+B}function b(B){try{w(B);var H=!1}catch(X){H=!0}if(H){H=console;var U=H.error,W=typeof Symbol==="function"&&Symbol.toStringTag&&B[Symbol.toStringTag]||B.constructor.name||"Object";return U.call(H,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",W),w(B)}}function m(B){if(B==null)return null;if(typeof B==="function")return B.$$typeof===kB?null:B.displayName||B.name||null;if(typeof B==="string")return B;switch(B){case s:return"Fragment";case Z:return"Profiler";case R:return"StrictMode";case y:return"Suspense";case q:return"SuspenseList";case RB:return"Activity"}if(typeof B==="object")switch(typeof B.tag==="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),B.$$typeof){case HB:return"Portal";case P:return B.displayName||"Context";case $:return(B._context.displayName||"Context")+".Consumer";case T:var H=B.render;return B=B.displayName,B||(B=H.displayName||H.name||"",B=B!==""?"ForwardRef("+B+")":"ForwardRef"),B;case N:return H=B.displayName||null,H!==null?H:m(B.type)||"Memo";case _:H=B._payload,B=B._init;try{return m(B(H))}catch(U){}}return null}function e(B){if(B===s)return"<>";if(typeof B==="object"&&B!==null&&B.$$typeof===_)return"<...>";try{var H=m(B);return H?"<"+H+">":"<...>"}catch(U){return"<...>"}}function p(){var B=G.A;return B===null?null:B.getOwner()}function c(){return Error("react-stack-top-frame")}function o(B){if(UB.call(B,"key")){var H=Object.getOwnPropertyDescriptor(B,"key").get;if(H&&H.isReactWarning)return!1}return B.key!==void 0}function GB(B,H){function U(){TB||(TB=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",H))}U.isReactWarning=!0,Object.defineProperty(B,"key",{get:U,configurable:!0})}function i(){var B=m(this.type);return EB[B]||(EB[B]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),B=this.props.ref,B!==void 0?B:null}function l(B,H,U,W,X,M){var Y=U.ref;return B={$$typeof:r,type:B,key:H,props:U,_owner:W},(Y!==void 0?Y:null)!==null?Object.defineProperty(B,"ref",{enumerable:!1,get:i}):Object.defineProperty(B,"ref",{enumerable:!1,value:null}),B._store={},Object.defineProperty(B._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(B,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(B,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:X}),Object.defineProperty(B,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:M}),Object.freeze&&(Object.freeze(B.props),Object.freeze(B)),B}function MB(B,H){return H=l(B.type,H,B.props,B._owner,B._debugStack,B._debugTask),B._store&&(H._store.validated=B._store.validated),H}function BB(B){x(B)?B._store&&(B._store.validated=1):typeof B==="object"&&B!==null&&B.$$typeof===_&&(B._payload.status==="fulfilled"?x(B._payload.value)&&B._payload.value._store&&(B._payload.value._store.validated=1):B._store&&(B._store.validated=1))}function x(B){return typeof B==="object"&&B!==null&&B.$$typeof===r}function zB(B){var H={"=":"=0",":":"=2"};return"$"+B.replace(/[=:]/g,function(U){return H[U]})}function t(B,H){return typeof B==="object"&&B!==null&&B.key!=null?(b(B.key),zB(""+B.key)):H.toString(36)}function QB(B){switch(B.status){case"fulfilled":return B.value;case"rejected":throw B.reason;default:switch(typeof B.status==="string"?B.then(I,I):(B.status="pending",B.then(function(H){B.status==="pending"&&(B.status="fulfilled",B.value=H)},function(H){B.status==="pending"&&(B.status="rejected",B.reason=H)})),B.status){case"fulfilled":return B.value;case"rejected":throw B.reason}}throw B}function j(B,H,U,W,X){var M=typeof B;if(M==="undefined"||M==="boolean")B=null;var Y=!1;if(B===null)Y=!0;else switch(M){case"bigint":case"string":case"number":Y=!0;break;case"object":switch(B.$$typeof){case r:case HB:Y=!0;break;case _:return Y=B._init,j(Y(B._payload),H,U,W,X)}}if(Y){Y=B,X=X(Y);var O=W===""?"."+t(Y,0):W;return DB(X)?(U="",O!=null&&(U=O.replace(wB,"$&/")+"/"),j(X,H,U,"",function(V){return V})):X!=null&&(x(X)&&(X.key!=null&&(Y&&Y.key===X.key||b(X.key)),U=MB(X,U+(X.key==null||Y&&Y.key===X.key?"":(""+X.key).replace(wB,"$&/")+"/")+O),W!==""&&Y!=null&&x(Y)&&Y.key==null&&Y._store&&!Y._store.validated&&(U._store.validated=2),X=U),H.push(X)),1}if(Y=0,O=W===""?".":W+":",DB(B))for(var z=0;z<B.length;z++)W=B[z],M=O+t(W,z),Y+=j(W,H,U,M,X);else if(z=F(B),typeof z==="function")for(z===B.entries&&(_B||console.warn("Using Maps as children is not supported. Use an array of keyed ReactElements instead."),_B=!0),B=z.call(B),z=0;!(W=B.next()).done;)W=W.value,M=O+t(W,z++),Y+=j(W,H,U,M,X);else if(M==="object"){if(typeof B.then==="function")return j(QB(B),H,U,W,X);throw H=String(B),Error("Objects are not valid as a React child (found: "+(H==="[object Object]"?"object with keys {"+Object.keys(B).join(", ")+"}":H)+"). If you meant to render a collection of children, use an array instead.")}return Y}function C(B,H,U){if(B==null)return B;var W=[],X=0;return j(B,W,"","",function(M){return H.call(U,M,X++)}),W}function LB(B){if(B._status===-1){var H=B._ioInfo;H!=null&&(H.start=H.end=performance.now()),H=B._result;var U=H();if(U.then(function(X){if(B._status===0||B._status===-1){B._status=1,B._result=X;var M=B._ioInfo;M!=null&&(M.end=performance.now()),U.status===void 0&&(U.status="fulfilled",U.value=X)}},function(X){if(B._status===0||B._status===-1){B._status=2,B._result=X;var M=B._ioInfo;M!=null&&(M.end=performance.now()),U.status===void 0&&(U.status="rejected",U.reason=X)}}),H=B._ioInfo,H!=null){H.value=U;var W=U.displayName;typeof W==="string"&&(H.name=W)}B._status===-1&&(B._status=0,B._result=U)}if(B._status===1)return H=B._result,H===void 0&&console.error(`lazy: Expected the result of a dynamic import() call. Instead received: %s
2
+
3
+ Your code should look like:
4
+ const MyComponent = lazy(() => import('./MyComponent'))
5
+
6
+ Did you accidentally put curly braces around the import?`,H),"default"in H||console.error(`lazy: Expected the result of a dynamic import() call. Instead received: %s
7
+
8
+ Your code should look like:
9
+ const MyComponent = lazy(() => import('./MyComponent'))`,H),H.default;throw B._result}function J(){var B=G.H;return B===null&&console.error(`Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
10
+ 1. You might have mismatching versions of React and the renderer (such as React DOM)
11
+ 2. You might be breaking the Rules of Hooks
12
+ 3. You might have more than one copy of React in the same app
13
+ See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.`),B}function n(){G.asyncTransitions--}function S(B){if(WB===null)try{var H=("require"+Math.random()).slice(0,7);WB=(ZB&&ZB[H]).call(ZB,"timers").setImmediate}catch(U){WB=function(W){jB===!1&&(jB=!0,typeof MessageChannel>"u"&&console.error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var X=new MessageChannel;X.port1.onmessage=W,X.port2.postMessage(void 0)}}return WB(B)}function g(B){return 1<B.length&&typeof AggregateError==="function"?AggregateError(B):B[0]}function k(B,H){H!==XB-1&&console.error("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. "),XB=H}function h(B,H,U){var W=G.actQueue;if(W!==null)if(W.length!==0)try{u(W),S(function(){return h(B,H,U)});return}catch(X){G.thrownErrors.push(X)}else G.actQueue=null;0<G.thrownErrors.length?(W=g(G.thrownErrors),G.thrownErrors.length=0,U(W)):H(B)}function u(B){if(!$B){$B=!0;var H=0;try{for(;H<B.length;H++){var U=B[H];do{G.didUsePromise=!1;var W=U(!1);if(W!==null){if(G.didUsePromise){B[H]=U,B.splice(0,H);return}U=W}else break}while(1)}B.length=0}catch(X){B.splice(0,H+1),G.thrownErrors.push(X)}finally{$B=!1}}}typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());var r=Symbol.for("react.transitional.element"),HB=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),R=Symbol.for("react.strict_mode"),Z=Symbol.for("react.profiler"),$=Symbol.for("react.consumer"),P=Symbol.for("react.context"),T=Symbol.for("react.forward_ref"),y=Symbol.for("react.suspense"),q=Symbol.for("react.suspense_list"),N=Symbol.for("react.memo"),_=Symbol.for("react.lazy"),RB=Symbol.for("react.activity"),PB=Symbol.iterator,FB={},KB={isMounted:function(){return!1},enqueueForceUpdate:function(B){K(B,"forceUpdate")},enqueueReplaceState:function(B){K(B,"replaceState")},enqueueSetState:function(B){K(B,"setState")}},AB=Object.assign,OB={};Object.freeze(OB),A.prototype.isReactComponent={},A.prototype.setState=function(B,H){if(typeof B!=="object"&&typeof B!=="function"&&B!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,B,H,"setState")},A.prototype.forceUpdate=function(B){this.updater.enqueueForceUpdate(this,B,"forceUpdate")};var D={isMounted:["isMounted","Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks."],replaceState:["replaceState","Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236)."]};for(a in D)D.hasOwnProperty(a)&&Q(a,D[a]);v.prototype=A.prototype,D=E.prototype=new v,D.constructor=E,AB(D,A.prototype),D.isPureReactComponent=!0;var DB=Array.isArray,kB=Symbol.for("react.client.reference"),G={H:null,A:null,T:null,S:null,actQueue:null,asyncTransitions:0,isBatchingLegacy:!1,didScheduleLegacyUpdate:!1,didUsePromise:!1,thrownErrors:[],getCurrentStack:null,recentlyCreatedOwnerStacks:0},UB=Object.prototype.hasOwnProperty,IB=console.createTask?console.createTask:function(){return null};D={react_stack_bottom_frame:function(B){return B()}};var TB,NB,EB={},fB=D.react_stack_bottom_frame.bind(D,c)(),bB=IB(e(c)),_B=!1,wB=/\/+/g,xB=typeof reportError==="function"?reportError:function(B){if(typeof window==="object"&&typeof window.ErrorEvent==="function"){var H=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof B==="object"&&B!==null&&typeof B.message==="string"?String(B.message):String(B),error:B});if(!window.dispatchEvent(H))return}else if(typeof process==="object"&&typeof process.emit==="function"){process.emit("uncaughtException",B);return}console.error(B)},jB=!1,WB=null,XB=0,YB=!1,$B=!1,CB=typeof queueMicrotask==="function"?function(B){queueMicrotask(function(){return queueMicrotask(B)})}:S;D=Object.freeze({__proto__:null,c:function(B){return J().useMemoCache(B)}});var a={map:C,forEach:function(B,H,U){C(B,function(){H.apply(this,arguments)},U)},count:function(B){var H=0;return C(B,function(){H++}),H},toArray:function(B){return C(B,function(H){return H})||[]},only:function(B){if(!x(B))throw Error("React.Children.only expected to receive a single React element child.");return B}};iB.Activity=RB,iB.Children=a,iB.Component=A,iB.Fragment=s,iB.Profiler=Z,iB.PureComponent=E,iB.StrictMode=R,iB.Suspense=y,iB.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=G,iB.__COMPILER_RUNTIME=D,iB.act=function(B){var H=G.actQueue,U=XB;XB++;var W=G.actQueue=H!==null?H:[],X=!1;try{var M=B()}catch(z){G.thrownErrors.push(z)}if(0<G.thrownErrors.length)throw k(H,U),B=g(G.thrownErrors),G.thrownErrors.length=0,B;if(M!==null&&typeof M==="object"&&typeof M.then==="function"){var Y=M;return CB(function(){X||YB||(YB=!0,console.error("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),{then:function(z,V){X=!0,Y.then(function(d){if(k(H,U),U===0){try{u(W),S(function(){return h(d,z,V)})}catch(hB){G.thrownErrors.push(hB)}if(0<G.thrownErrors.length){var mB=g(G.thrownErrors);G.thrownErrors.length=0,V(mB)}}else z(d)},function(d){k(H,U),0<G.thrownErrors.length?(d=g(G.thrownErrors),G.thrownErrors.length=0,V(d)):V(d)})}}}var O=M;if(k(H,U),U===0&&(u(W),W.length!==0&&CB(function(){X||YB||(YB=!0,console.error("A component suspended inside an `act` scope, but the `act` call was not awaited. When testing React components that depend on asynchronous data, you must await the result:\n\nawait act(() => ...)"))}),G.actQueue=null),0<G.thrownErrors.length)throw B=g(G.thrownErrors),G.thrownErrors.length=0,B;return{then:function(z,V){X=!0,U===0?(G.actQueue=W,S(function(){return h(O,z,V)})):z(O)}}},iB.cache=function(B){return function(){return B.apply(null,arguments)}},iB.cacheSignal=function(){return null},iB.captureOwnerStack=function(){var B=G.getCurrentStack;return B===null?null:B()},iB.cloneElement=function(B,H,U){if(B===null||B===void 0)throw Error("The argument must be a React element, but you passed "+B+".");var W=AB({},B.props),X=B.key,M=B._owner;if(H!=null){var Y;B:{if(UB.call(H,"ref")&&(Y=Object.getOwnPropertyDescriptor(H,"ref").get)&&Y.isReactWarning){Y=!1;break B}Y=H.ref!==void 0}Y&&(M=p()),o(H)&&(b(H.key),X=""+H.key);for(O in H)!UB.call(H,O)||O==="key"||O==="__self"||O==="__source"||O==="ref"&&H.ref===void 0||(W[O]=H[O])}var O=arguments.length-2;if(O===1)W.children=U;else if(1<O){Y=Array(O);for(var z=0;z<O;z++)Y[z]=arguments[z+2];W.children=Y}W=l(B.type,X,W,M,B._debugStack,B._debugTask);for(X=2;X<arguments.length;X++)BB(arguments[X]);return W},iB.createContext=function(B){return B={$$typeof:P,_currentValue:B,_currentValue2:B,_threadCount:0,Provider:null,Consumer:null},B.Provider=B,B.Consumer={$$typeof:$,_context:B},B._currentRenderer=null,B._currentRenderer2=null,B},iB.createElement=function(B,H,U){for(var W=2;W<arguments.length;W++)BB(arguments[W]);W={};var X=null;if(H!=null)for(z in NB||!("__self"in H)||"key"in H||(NB=!0,console.warn("Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform")),o(H)&&(b(H.key),X=""+H.key),H)UB.call(H,z)&&z!=="key"&&z!=="__self"&&z!=="__source"&&(W[z]=H[z]);var M=arguments.length-2;if(M===1)W.children=U;else if(1<M){for(var Y=Array(M),O=0;O<M;O++)Y[O]=arguments[O+2];Object.freeze&&Object.freeze(Y),W.children=Y}if(B&&B.defaultProps)for(z in M=B.defaultProps,M)W[z]===void 0&&(W[z]=M[z]);X&&GB(W,typeof B==="function"?B.displayName||B.name||"Unknown":B);var z=1e4>G.recentlyCreatedOwnerStacks++;return l(B,X,W,p(),z?Error("react-stack-top-frame"):fB,z?IB(e(B)):bB)},iB.createRef=function(){var B={current:null};return Object.seal(B),B},iB.forwardRef=function(B){B!=null&&B.$$typeof===N?console.error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof B!=="function"?console.error("forwardRef requires a render function but was given %s.",B===null?"null":typeof B):B.length!==0&&B.length!==2&&console.error("forwardRef render functions accept exactly two parameters: props and ref. %s",B.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),B!=null&&B.defaultProps!=null&&console.error("forwardRef render functions do not support defaultProps. Did you accidentally pass a React component?");var H={$$typeof:T,render:B},U;return Object.defineProperty(H,"displayName",{enumerable:!1,configurable:!0,get:function(){return U},set:function(W){U=W,B.name||B.displayName||(Object.defineProperty(B,"name",{value:W}),B.displayName=W)}}),H},iB.isValidElement=x,iB.lazy=function(B){B={_status:-1,_result:B};var H={$$typeof:_,_payload:B,_init:LB},U={name:"lazy",start:-1,end:-1,value:null,owner:null,debugStack:Error("react-stack-top-frame"),debugTask:console.createTask?console.createTask("lazy()"):null};return B._ioInfo=U,H._debugInfo=[{awaited:U}],H},iB.memo=function(B,H){B==null&&console.error("memo: The first argument must be a component. Instead received: %s",B===null?"null":typeof B),H={$$typeof:N,type:B,compare:H===void 0?null:H};var U;return Object.defineProperty(H,"displayName",{enumerable:!1,configurable:!0,get:function(){return U},set:function(W){U=W,B.name||B.displayName||(Object.defineProperty(B,"name",{value:W}),B.displayName=W)}}),H},iB.startTransition=function(B){var H=G.T,U={};U._updatedFibers=new Set,G.T=U;try{var W=B(),X=G.S;X!==null&&X(U,W),typeof W==="object"&&W!==null&&typeof W.then==="function"&&(G.asyncTransitions++,W.then(n,n),W.then(I,xB))}catch(M){xB(M)}finally{H===null&&U._updatedFibers&&(B=U._updatedFibers.size,U._updatedFibers.clear(),10<B&&console.warn("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table.")),H!==null&&U.types!==null&&(H.types!==null&&H.types!==U.types&&console.error("We expected inner Transitions to have transferred the outer types set and that you cannot add to the outer Transition while inside the inner.This is a bug in React."),H.types=U.types),G.T=H}},iB.unstable_useCacheRefresh=function(){return J().useCacheRefresh()},iB.use=function(B){return J().use(B)},iB.useActionState=function(B,H,U){return J().useActionState(B,H,U)},iB.useCallback=function(B,H){return J().useCallback(B,H)},iB.useContext=function(B){var H=J();return B.$$typeof===$&&console.error("Calling useContext(Context.Consumer) is not supported and will cause bugs. Did you mean to call useContext(Context) instead?"),H.useContext(B)},iB.useDebugValue=function(B,H){return J().useDebugValue(B,H)},iB.useDeferredValue=function(B,H){return J().useDeferredValue(B,H)},iB.useEffect=function(B,H){return B==null&&console.warn("React Hook useEffect requires an effect callback. Did you forget to pass a callback to the hook?"),J().useEffect(B,H)},iB.useEffectEvent=function(B){return J().useEffectEvent(B)},iB.useId=function(){return J().useId()},iB.useImperativeHandle=function(B,H,U){return J().useImperativeHandle(B,H,U)},iB.useInsertionEffect=function(B,H){return B==null&&console.warn("React Hook useInsertionEffect requires an effect callback. Did you forget to pass a callback to the hook?"),J().useInsertionEffect(B,H)},iB.useLayoutEffect=function(B,H){return B==null&&console.warn("React Hook useLayoutEffect requires an effect callback. Did you forget to pass a callback to the hook?"),J().useLayoutEffect(B,H)},iB.useMemo=function(B,H){return J().useMemo(B,H)},iB.useOptimistic=function(B,H){return J().useOptimistic(B,H)},iB.useReducer=function(B,H,U){return J().useReducer(B,H,U)},iB.useRef=function(B){return J().useRef(B)},iB.useState=function(B){return J().useState(B)},iB.useSyncExternalStore=function(B,H,U){return J().useSyncExternalStore(B,H,U)},iB.useTransition=function(){return J().useTransition()},iB.version="19.2.7",typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var qB=vB((lB)=>{var f=JB(SB());(function(){function Q(R){if(R==null)return null;if(typeof R==="function")return R.$$typeof===J?null:R.displayName||R.name||null;if(typeof R==="string")return R;switch(R){case i:return"Fragment";case MB:return"Profiler";case l:return"StrictMode";case t:return"Suspense";case QB:return"SuspenseList";case LB:return"Activity"}if(typeof R==="object")switch(typeof R.tag==="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),R.$$typeof){case GB:return"Portal";case x:return R.displayName||"Context";case BB:return(R._context.displayName||"Context")+".Consumer";case zB:var Z=R.render;return R=R.displayName,R||(R=Z.displayName||Z.name||"",R=R!==""?"ForwardRef("+R+")":"ForwardRef"),R;case j:return Z=R.displayName||null,Z!==null?Z:Q(R.type)||"Memo";case C:Z=R._payload,R=R._init;try{return Q(R(Z))}catch($){}}return null}function F(R){return""+R}function K(R){try{F(R);var Z=!1}catch(T){Z=!0}if(Z){Z=console;var $=Z.error,P=typeof Symbol==="function"&&Symbol.toStringTag&&R[Symbol.toStringTag]||R.constructor.name||"Object";return $.call(Z,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",P),F(R)}}function A(R){if(R===i)return"<>";if(typeof R==="object"&&R!==null&&R.$$typeof===C)return"<...>";try{var Z=Q(R);return Z?"<"+Z+">":"<...>"}catch($){return"<...>"}}function v(){var R=n.A;return R===null?null:R.getOwner()}function E(){return Error("react-stack-top-frame")}function I(R){if(S.call(R,"key")){var Z=Object.getOwnPropertyDescriptor(R,"key").get;if(Z&&Z.isReactWarning)return!1}return R.key!==void 0}function w(R,Z){function $(){h||(h=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",Z))}$.isReactWarning=!0,Object.defineProperty(R,"key",{get:$,configurable:!0})}function b(){var R=Q(this.type);return u[R]||(u[R]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),R=this.props.ref,R!==void 0?R:null}function m(R,Z,$,P,T,y){var q=$.ref;return R={$$typeof:o,type:R,key:Z,props:$,_owner:P},(q!==void 0?q:null)!==null?Object.defineProperty(R,"ref",{enumerable:!1,get:b}):Object.defineProperty(R,"ref",{enumerable:!1,value:null}),R._store={},Object.defineProperty(R._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(R,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(R,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:T}),Object.defineProperty(R,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:y}),Object.freeze&&(Object.freeze(R.props),Object.freeze(R)),R}function e(R,Z,$,P,T,y){var q=Z.children;if(q!==void 0)if(P)if(g(q)){for(P=0;P<q.length;P++)p(q[P]);Object.freeze&&Object.freeze(q)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else p(q);if(S.call(Z,"key")){q=Q(R);var N=Object.keys(Z).filter(function(RB){return RB!=="key"});P=0<N.length?"{key: someKey, "+N.join(": ..., ")+": ...}":"{key: someKey}",s[q+P]||(N=0<N.length?"{"+N.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
14
+ let props = %s;
15
+ <%s {...props} />
16
+ React keys must be passed directly to JSX without using spread:
17
+ let props = %s;
18
+ <%s key={someKey} {...props} />`,P,q,N,q),s[q+P]=!0)}if(q=null,$!==void 0&&(K($),q=""+$),I(Z)&&(K(Z.key),q=""+Z.key),"key"in Z){$={};for(var _ in Z)_!=="key"&&($[_]=Z[_])}else $=Z;return q&&w($,typeof R==="function"?R.displayName||R.name||"Unknown":R),m(R,q,$,v(),T,y)}function p(R){c(R)?R._store&&(R._store.validated=1):typeof R==="object"&&R!==null&&R.$$typeof===C&&(R._payload.status==="fulfilled"?c(R._payload.value)&&R._payload.value._store&&(R._payload.value._store.validated=1):R._store&&(R._store.validated=1))}function c(R){return typeof R==="object"&&R!==null&&R.$$typeof===o}var o=Symbol.for("react.transitional.element"),GB=Symbol.for("react.portal"),i=Symbol.for("react.fragment"),l=Symbol.for("react.strict_mode"),MB=Symbol.for("react.profiler"),BB=Symbol.for("react.consumer"),x=Symbol.for("react.context"),zB=Symbol.for("react.forward_ref"),t=Symbol.for("react.suspense"),QB=Symbol.for("react.suspense_list"),j=Symbol.for("react.memo"),C=Symbol.for("react.lazy"),LB=Symbol.for("react.activity"),J=Symbol.for("react.client.reference"),n=f.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,S=Object.prototype.hasOwnProperty,g=Array.isArray,k=console.createTask?console.createTask:function(){return null};f={react_stack_bottom_frame:function(R){return R()}};var h,u={},r=f.react_stack_bottom_frame.bind(f,E)(),HB=k(A(E)),s={};lB.Fragment=i,lB.jsxDEV=function(R,Z,$,P){var T=1e4>n.recentlyCreatedOwnerStacks++;return e(R,Z,$,P,T?Error("react-stack-top-frame"):r,T?k(A(R)):HB)}})()});var L=JB(qB(),1);function gB(){return L.jsxDEV("span",{className:"tt-mark","aria-hidden":"true",children:"♪"},void 0,!1,void 0,this)}function pH({view:Q}){let F=Q.app.name;return L.jsxDEV("div",{className:"card",children:[L.jsxDEV("header",{className:"tt-header",children:[L.jsxDEV(gB,{},void 0,!1,void 0,this),L.jsxDEV("span",{className:"tt-wordmark",children:"TikTok"},void 0,!1,void 0,this)]},void 0,!0,void 0,this),L.jsxDEV("div",{className:"signed-in",children:L.jsxDEV("span",{className:"account-text",children:[L.jsxDEV("span",{className:"account-name",children:Q.account.displayName||Q.account.username||Q.account.unionId},void 0,!1,void 0,this),Q.account.username?L.jsxDEV("span",{className:"account-username",children:["@",Q.account.username]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this),L.jsxDEV("h1",{className:"title",children:[L.jsxDEV("strong",{className:"app-name",children:F},void 0,!1,void 0,this)," would like to access your TikTok account"]},void 0,!0,void 0,this),L.jsxDEV("form",{method:"POST",action:`${Q.origin}/_twin/consent`,className:"consent-form",children:[L.jsxDEV("input",{type:"hidden",name:"auth_request",value:Q.requestId},void 0,!1,void 0,this),L.jsxDEV("p",{className:"scope-lead",children:["This will allow ",F," to:"]},void 0,!0,void 0,this),L.jsxDEV("ul",{className:"scope-list",children:Q.scopes.map((K)=>L.jsxDEV("li",{className:"scope-row",children:[L.jsxDEV("label",{className:"scope-label",children:[L.jsxDEV("input",{className:"scope-check",type:"checkbox",name:"scope",value:K.scope,defaultChecked:!0},void 0,!1,void 0,this),L.jsxDEV("span",{className:"scope-text",children:K.label},void 0,!1,void 0,this)]},void 0,!0,void 0,this),K.known?null:L.jsxDEV("span",{className:"scope-tag scope-tag-unknown",children:"not in the twin's scope catalog"},void 0,!1,void 0,this)]},K.scope,!0,void 0,this))},void 0,!1,void 0,this),L.jsxDEV("div",{className:"actions",children:[L.jsxDEV("button",{className:"btn btn-secondary",type:"submit",name:"decision",value:"deny",children:"Cancel"},void 0,!1,void 0,this),L.jsxDEV("button",{className:"btn btn-primary",type:"submit",name:"decision",value:"allow",children:"Continue"},void 0,!1,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this),L.jsxDEV("p",{className:"legal",children:["You'll be redirected to ",L.jsxDEV("strong",{className:"redirect-host",children:Q.redirectHost},void 0,!1,void 0,this),". You can remove this app at any time from Manage app permissions in your TikTok settings."]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)}function cH({status:Q,code:F,detail:K,requestParam:A}){return L.jsxDEV("div",{className:"card card-error",children:[L.jsxDEV("header",{className:"tt-header",children:[L.jsxDEV(gB,{},void 0,!1,void 0,this),L.jsxDEV("span",{className:"tt-wordmark",children:"TikTok"},void 0,!1,void 0,this)]},void 0,!0,void 0,this),L.jsxDEV("h1",{className:"title",children:"Something went wrong"},void 0,!1,void 0,this),L.jsxDEV("p",{className:"error-detail",children:["You weren't able to give access to the app. ",K]},void 0,!0,void 0,this),L.jsxDEV("p",{className:"error-code",children:["Error ",Q,": ",F]},void 0,!0,void 0,this),A?L.jsxDEV("p",{className:"error-request",children:["Request details: ",A]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)}export{cH as ErrorPage,pH as ConsentPage};
@@ -0,0 +1,47 @@
1
+ export type ConsentAccount = {
2
+ /** The account's `union_id` — the twin's subject key for a TikTok human. */
3
+ unionId: string;
4
+ username: string;
5
+ displayName: string;
6
+ };
7
+ export type ConsentScopeRow = {
8
+ scope: string;
9
+ label: string;
10
+ product: string;
11
+ known: boolean;
12
+ };
13
+ export type ConsentApp = {
14
+ clientKey: string;
15
+ name: string;
16
+ };
17
+ export type ConsentView = {
18
+ requestId: string;
19
+ /** Where the twin is reachable — the form action posts back here. */
20
+ origin: string;
21
+ app: ConsentApp;
22
+ /** The tiktok.com session this browser is signed in as — the account that will consent. */
23
+ account: ConsentAccount;
24
+ scopes: ConsentScopeRow[];
25
+ /** Host of the validated redirect_uri, shown in the "you'll be redirected to" notice. */
26
+ redirectHost: string;
27
+ };
28
+ /** The authorization page — TikTok's consent sheet for the app named by the pending request. */
29
+ export declare function ConsentPage({ view }: {
30
+ view: ConsentView;
31
+ }): import("react").JSX.Element;
32
+ export type ErrorPageProps = {
33
+ status: number;
34
+ /** The OAuth error code the failure classifies as (e.g. `invalid_request`). */
35
+ code: string;
36
+ detail: string;
37
+ /** The offending parameter, echoed for the developer. */
38
+ requestParam?: string;
39
+ };
40
+ /**
41
+ * The authorize-endpoint error PAGE — what a browser sees when the twin cannot trust the
42
+ * redirect_uri (unknown client_key, unregistered callback) and therefore must not bounce the error
43
+ * back to it. TikTok's live page for this case was not captured, so this page states the failure
44
+ * plainly and machine-readably rather than imitating unverified vendor prose
45
+ * (`tiktok.authorize.error_page_fidelity`, todo).
46
+ */
47
+ export declare function ErrorPage({ status, code, detail, requestParam }: ErrorPageProps): import("react").JSX.Element;