@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
@@ -1,161 +0,0 @@
1
- // Content-addressed BYTE storage for the TikTok twin — a posted video's real media.
2
- //
3
- // Transcribed from the YouTube pack's byte annex (youtube-blobs.ts), which took it from Slack's: bytes
4
- // ride the KERNEL'S BLOB SEAM (`getActiveBlobStore()`, runtime contract R11) — the filesystem locally,
5
- // object storage inside a served namespace — under this service's own `resources` dir, which
6
- // `scrubService('tiktok')` already owns. Keys (relative to that dir): blobs/sha256/<ab>/<hex>. The
7
- // kernel projection references a blob by digest only (`_content_sha256` on a video or a draft), so the
8
- // event log stays small however large the media is.
9
- //
10
- // READS GO THROUGH THE KERNEL'S RESOURCE-BLOB HELPERS (`readResourceBlobRange`, `resourceBlobSize`): a
11
- // branch inherits its ancestors' videos in the projection, so it must inherit their bytes too, and
12
- // those helpers look at the branch and then its retained ancestors. They read a RANGE natively, so
13
- // serving a seek, or sending one upload chunk, never loads the whole video.
14
- //
15
- // AN UPLOAD IS STAGING, and staging is NOT a kernel action: the upload_url the init hands back is a
16
- // place to put bytes, not an event in the creator's history. An upload is a pointer record plus its
17
- // chunks, each chunk its own key, joined ONCE when the final chunk lands; only that finalize is a
18
- // kernel write. An upload that is abandoned leaves no pending action to wedge a later deploy, and its
19
- // upload_url stops answering after TikTok's own hour.
20
- import { join } from 'node:path';
21
- import { blobDigest, getActiveBlobStore, readResourceBlobRange, resourceBlobSize, worldPaths } from '@volter/world-core';
22
- const SERVICE = 'tiktok';
23
- const DIGEST = /^[0-9a-f]{64}$/;
24
- const UPLOAD_ID = /^[0-9]{1,24}$/;
25
- function resources(root) {
26
- return worldPaths(SERVICE, root).resources;
27
- }
28
- function blobKey(sha256) {
29
- return join('blobs', 'sha256', sha256.slice(0, 2), sha256);
30
- }
31
- /** Store bytes content-addressed in this branch's annex; identical content is a no-op write. */
32
- export async function putTikTokBlob(bytes, root) {
33
- const sha256 = blobDigest(bytes);
34
- const key = join(resources(root), blobKey(sha256));
35
- if (!(await getActiveBlobStore().exists(key)))
36
- await getActiveBlobStore().put(key, bytes);
37
- return { sha256, size: bytes.length };
38
- }
39
- /** A stored blob's size, here or at an ancestor; null when absent or the digest is malformed. */
40
- export async function tikTokBlobSize(sha256, root) {
41
- if (typeof sha256 !== 'string' || !DIGEST.test(sha256))
42
- return null;
43
- return resourceBlobSize(SERVICE, blobKey(sha256), root);
44
- }
45
- /** Bytes start..endInclusive of a stored blob, here or at an ancestor, read as a range. */
46
- export async function readTikTokBlobRange(sha256, start, endInclusive, root) {
47
- if (typeof sha256 !== 'string' || !DIGEST.test(sha256))
48
- return null;
49
- return readResourceBlobRange(SERVICE, blobKey(sha256), start, endInclusive, root);
50
- }
51
- function uploadDir(root) {
52
- return join(resources(root), 'uploads');
53
- }
54
- function assertUploadId(id) {
55
- if (!UPLOAD_ID.test(id))
56
- throw new Error(`invalid TikTok upload id: ${JSON.stringify(id)}`);
57
- return id;
58
- }
59
- function uploadKey(id, root) {
60
- return join(uploadDir(root), `${assertUploadId(id)}.json`);
61
- }
62
- function publishKey(publishId, root) {
63
- return join(uploadDir(root), 'by-publish', `${blobDigest(new TextEncoder().encode(publishId))}.json`);
64
- }
65
- function chunkDir(id, root) {
66
- return join(uploadDir(root), assertUploadId(id));
67
- }
68
- /** A chunk's key carries its first byte, zero-padded so the seam's sorted listing is byte order. */
69
- function chunkKey(id, offset, root) {
70
- return join(chunkDir(id, root), `${String(offset).padStart(16, '0')}.part`);
71
- }
72
- async function readJson(key) {
73
- try {
74
- const stored = await getActiveBlobStore().get(key);
75
- return stored === null ? null : JSON.parse(new TextDecoder().decode(stored));
76
- }
77
- catch {
78
- return null;
79
- }
80
- }
81
- export async function writeUpload(upload, root) {
82
- const bytes = new TextEncoder().encode(JSON.stringify(upload));
83
- await getActiveBlobStore().put(uploadKey(upload.uploadId, root), bytes);
84
- await getActiveBlobStore().put(publishKey(upload.publishId, root), new TextEncoder().encode(JSON.stringify({ uploadId: upload.uploadId })));
85
- }
86
- export async function readUpload(id, root) {
87
- if (!UPLOAD_ID.test(id))
88
- return null;
89
- const upload = await readJson(uploadKey(id, root));
90
- return upload && typeof upload.uploadId === 'string' ? upload : null;
91
- }
92
- /** The upload a publish_id names, while it is staging (before its final chunk lands). */
93
- export async function readUploadByPublishId(publishId, root) {
94
- const pointer = await readJson(publishKey(publishId, root));
95
- return pointer ? readUpload(pointer.uploadId, root) : null;
96
- }
97
- /** The upload's chunks in byte order, with their offsets and sizes. */
98
- async function chunks(id, root) {
99
- const prefix = `${chunkDir(id, root)}/`;
100
- const out = [];
101
- for (const key of await getActiveBlobStore().list(prefix)) {
102
- const m = /(\d{16})\.part$/.exec(key);
103
- if (!m)
104
- continue;
105
- out.push({ key, offset: Number(m[1]), size: (await getActiveBlobStore().size(key)) ?? 0 });
106
- }
107
- return out.sort((a, b) => a.offset - b.offset);
108
- }
109
- /** Bytes an upload holds so far: the contiguous run from 0. */
110
- export async function stagedSize(id, root) {
111
- let held = 0;
112
- for (const c of await chunks(id, root)) {
113
- if (c.offset !== held)
114
- break;
115
- held += c.size;
116
- }
117
- return held;
118
- }
119
- /** Stage one chunk at its first byte — its own key; nothing already staged is read or rewritten. */
120
- export async function stageChunk(id, offset, bytes, root) {
121
- if (bytes.length > 0)
122
- await getActiveBlobStore().put(chunkKey(id, offset, root), bytes);
123
- }
124
- /** Join an upload's chunks, once, into the video's bytes. */
125
- export async function joinStaged(id, root) {
126
- const parts = await chunks(id, root);
127
- const total = parts.reduce((n, c) => n + c.size, 0);
128
- const out = new Uint8Array(total);
129
- let at = 0;
130
- for (const c of parts) {
131
- const bytes = await getActiveBlobStore().get(c.key);
132
- if (bytes === null)
133
- continue;
134
- out.set(bytes, at);
135
- at += bytes.length;
136
- }
137
- return out.subarray(0, at);
138
- }
139
- /** Drop a finalized upload's chunks (the bytes now live content-addressed); the record stays, so a
140
- * final PUT whose answer was lost can be asked again and status/fetch still finds the publish. */
141
- export async function clearStaged(id, root) {
142
- try {
143
- await getActiveBlobStore().remove(`${chunkDir(id, root)}/`);
144
- }
145
- catch { /* never staged */ }
146
- }
147
- function performKey(actionId, root) {
148
- return join(resources(root), 'perform-publishes', `${blobDigest(new TextEncoder().encode(actionId))}.json`);
149
- }
150
- export async function readPerformPublish(actionId, root) {
151
- return readJson(performKey(actionId, root));
152
- }
153
- export async function writePerformPublish(actionId, record, root) {
154
- await getActiveBlobStore().put(performKey(actionId, root), new TextEncoder().encode(JSON.stringify(record)));
155
- }
156
- export async function clearPerformPublish(actionId, root) {
157
- try {
158
- await getActiveBlobStore().remove(performKey(actionId, root));
159
- }
160
- catch { /* none */ }
161
- }
@@ -1,56 +0,0 @@
1
- import { RateBudget, type RateBudgetDeclaration, type RateBudgetOptions, type RateBudgetReservation, type RateBudgetSnapshot } from '@volter/world-core';
2
- /** Rolling window, in ms — TikTok's own one-minute sliding window. */
3
- export declare const TIKTOK_BUDGET_WINDOW_MS = 60000;
4
- /** Weighted units allowed inside one window — TikTok's own published per-endpoint figure, applied
5
- * across ALL endpoints at once (strictly tighter than the vendor's own per-endpoint allowance). */
6
- export declare const TIKTOK_BUDGET_CEILING = 600;
7
- /** The window IS the kernel's burst sub-window, so this is also the per-minute burst bound. */
8
- export declare const TIKTOK_BUDGET_BURST_CEILING = 600;
9
- /** Seconds. A `Retry-After` above this means the client is throttled hard — fail loudly, don't sleep. */
10
- export declare const TIKTOK_BUDGET_MAX_RETRY_AFTER_S = 300;
11
- /** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is documented vs judged. */
12
- export declare const TIKTOK_CALL_WEIGHTS: {
13
- /** The three Display API reads — 600/min is the PUBLISHED limit; weight 1 transcribes it. */
14
- readonly displayRead: 1;
15
- /** `POST /v2/oauth/revoke/` — disconnects an app from a REAL human's TikTok account. Priced as a
16
- * human blast radius, not a throttle (a judgement call, stated as one). */
17
- readonly revoke: 60;
18
- /** Everything else (the token endpoint): no published scalar limit -> priced conservatively. */
19
- readonly other: 5;
20
- /** `POST /v2/post/publish/creator_info/query/` — "limited to 20 requests per minute". */
21
- readonly creatorInfo: 30;
22
- /** `POST /v2/post/publish/video/init/` and `/inbox/video/init/` — "limited to 6 requests per minute". */
23
- readonly postInit: 100;
24
- /** `POST /v2/post/publish/status/fetch/` — "limited to 30 requests per minute". */
25
- readonly postStatus: 20;
26
- };
27
- /** THE PACK'S DECLARATION — pure data, the only TikTok-specific thing in the whole budget. */
28
- export declare const TIKTOK_RATE_BUDGET: RateBudgetDeclaration;
29
- /**
30
- * Price one call. The key is `"<METHOD> <path>"` with the query string split off. NORMALIZED
31
- * (upper-cased method, collapsed trailing slash) because the anchored rules are otherwise trivially
32
- * evaded by input variation — and on THIS vendor the trailing slash is not hypothetical: every
33
- * documented TikTok path carries one (`/v2/oauth/revoke/`), so a rule anchored on the bare form
34
- * would price the vendor's OWN spelling of the destructive call at the default weight.
35
- */
36
- export declare function tiktokCallWeight(method: string, path: string): number;
37
- /** Where this vendor's ledger lives. Token-keyed and cwd-independent by default; pass `root` to
38
- * opt into world-scoped accounting. */
39
- export declare function tiktokBudgetPath(opts?: {
40
- root?: string;
41
- token?: string;
42
- } | string): string;
43
- /** Construction options. The vendor is fixed; everything else may only TIGHTEN. */
44
- export type TikTokBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
45
- /**
46
- * This vendor's budget — the shared kernel guard bound to this declaration. A real subclass, not
47
- * an alias, so `budget instanceof TikTokBudget` means "a budget that accounts against TIKTOK's
48
- * ledger under TIKTOK's ceiling".
49
- */
50
- export declare class TikTokBudget extends RateBudget {
51
- constructor(opts?: TikTokBudgetOptions);
52
- }
53
- export { RateBudgetError as TikTokBudgetError } from '@volter/world-core';
54
- export type { RateBudgetErrorKind as TikTokBudgetErrorKind } from '@volter/world-core';
55
- export type TikTokBudgetReservation = RateBudgetReservation;
56
- export type TikTokBudgetSnapshot = RateBudgetSnapshot;
@@ -1,136 +0,0 @@
1
- // TikTok's CLIENT-SIDE RATE BUDGET — this pack's DECLARATION (the numbers) plus the thin typed
2
- // bindings `liveTikTokExecute` uses. The MECHANISM — the durable token-keyed ledger, the rolling
3
- // window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a corrupt ledger —
4
- // lives ONCE in the vendor-agnostic kernel (`@volter/world-core` -> `rateBudget.ts`).
5
- //
6
- // ── HOW THE CEILING WAS CHOSEN (live-fetched first-party figures) ───────────────────────────────
7
- // TikTok PUBLISHES per-endpoint rate limits. From https://developers.tiktok.com/doc/tiktok-api-v2-rate-limit
8
- // (fetched 2026-09-13): "Request rate calculation is based on a one minute sliding window", the
9
- // table gives `/v2/user/info/`, `/v2/video/list/` and `/v2/video/query/` a limit of **600** each,
10
- // and "If the number of requests exceeds the threshold, new requests will be throttled and a
11
- // response will be returned with HTTP status 429 and error code rate_limit_exceeded."
12
- //
13
- // So the window is the vendor's own ONE MINUTE and the ceiling is the vendor's own 600, with each
14
- // Display API read at weight 1 — the declaration REPRODUCES the published scheme for the endpoints
15
- // the connector reads (the github-budget precedent: when the vendor's scheme is already a windowed
16
- // budget, transcribe it). Two deliberate tightenings on top, both stated rather than hidden:
17
- //
18
- // • The vendor's 600 is PER ENDPOINT; this ledger is ONE budget across all of them, so 600
19
- // shared is strictly tighter than the vendor's 1800 across the three.
20
- // • The OAuth endpoints (`/v2/oauth/token/`, `/v2/oauth/revoke/`) publish NO scalar limit on
21
- // that page. That absence is stated here rather than dressed up, and those calls are priced
22
- // ABOVE the documented read so the undocumented surface is the conservative one.
23
- //
24
- // The window is already the kernel's 60s burst sub-window, so the ceiling IS the burst and the
25
- // kernel refuses a `burstCeiling` here. 600 units/minute out-bursts the kernel fallback's 30
26
- // calls/minute, which is licensed by the hand-written `burstAnchor` in this pack's gate.ts —
27
- // the vendor's own documented per-minute allowance, with its source.
28
- import { declareRateBudget, rateBudgetPath, rateBudgetWeight, RateBudget, } from '@volter/world-core';
29
- const VENDOR = 'tiktok';
30
- /** Rolling window, in ms — TikTok's own one-minute sliding window. */
31
- export const TIKTOK_BUDGET_WINDOW_MS = 60_000;
32
- /** Weighted units allowed inside one window — TikTok's own published per-endpoint figure, applied
33
- * across ALL endpoints at once (strictly tighter than the vendor's own per-endpoint allowance). */
34
- export const TIKTOK_BUDGET_CEILING = 600;
35
- /** The window IS the kernel's burst sub-window, so this is also the per-minute burst bound. */
36
- export const TIKTOK_BUDGET_BURST_CEILING = TIKTOK_BUDGET_CEILING;
37
- /** Seconds. A `Retry-After` above this means the client is throttled hard — fail loudly, don't sleep. */
38
- export const TIKTOK_BUDGET_MAX_RETRY_AFTER_S = 300;
39
- /** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is documented vs judged. */
40
- export const TIKTOK_CALL_WEIGHTS = {
41
- /** The three Display API reads — 600/min is the PUBLISHED limit; weight 1 transcribes it. */
42
- displayRead: 1,
43
- /** `POST /v2/oauth/revoke/` — disconnects an app from a REAL human's TikTok account. Priced as a
44
- * human blast radius, not a throttle (a judgement call, stated as one). */
45
- revoke: 60,
46
- /** Everything else (the token endpoint): no published scalar limit -> priced conservatively. */
47
- other: 5,
48
- // The Content Posting calls publish their OWN per-token minute allowance on each reference page
49
- // (fetched 2026-09-27), and each is priced so that the endpoint alone can never exceed it inside
50
- // this 600-unit minute: weight = 600 / the published per-minute figure.
51
- /** `POST /v2/post/publish/creator_info/query/` — "limited to 20 requests per minute". */
52
- creatorInfo: 30,
53
- /** `POST /v2/post/publish/video/init/` and `/inbox/video/init/` — "limited to 6 requests per minute". */
54
- postInit: 100,
55
- /** `POST /v2/post/publish/status/fetch/` — "limited to 30 requests per minute". */
56
- postStatus: 20,
57
- };
58
- /** THE PACK'S DECLARATION — pure data, the only TikTok-specific thing in the whole budget. */
59
- export const TIKTOK_RATE_BUDGET = {
60
- windowMs: TIKTOK_BUDGET_WINDOW_MS,
61
- ceiling: TIKTOK_BUDGET_CEILING,
62
- defaultWeight: TIKTOK_CALL_WEIGHTS.other,
63
- maxRetryAfterSeconds: TIKTOK_BUDGET_MAX_RETRY_AFTER_S,
64
- rules: [
65
- { match: '^GET /v2/user/info$', weight: TIKTOK_CALL_WEIGHTS.displayRead },
66
- { match: '^POST /v2/video/list$', weight: TIKTOK_CALL_WEIGHTS.displayRead },
67
- { match: '^POST /v2/video/query$', weight: TIKTOK_CALL_WEIGHTS.displayRead },
68
- { match: '^POST /v2/oauth/revoke$', weight: TIKTOK_CALL_WEIGHTS.revoke },
69
- { match: '^POST /v2/post/publish/creator_info/query$', weight: TIKTOK_CALL_WEIGHTS.creatorInfo },
70
- { match: '^POST /v2/post/publish/video/init$', weight: TIKTOK_CALL_WEIGHTS.postInit },
71
- { match: '^POST /v2/post/publish/inbox/video/init$', weight: TIKTOK_CALL_WEIGHTS.postInit },
72
- { match: '^POST /v2/post/publish/status/fetch$', weight: TIKTOK_CALL_WEIGHTS.postStatus },
73
- ],
74
- reason: 'TikTok publishes per-endpoint rate limits (https://developers.tiktok.com/doc/tiktok-api-v2-rate-limit, '
75
- + 'fetched 2026-09-13): "Request rate calculation is based on a one minute sliding window", the '
76
- + 'table lists /v2/user/info/, /v2/video/list/ and /v2/video/query/ at 600 each, and exceeding a '
77
- + 'limit answers "HTTP status 429 and error code rate_limit_exceeded". This declaration '
78
- + "transcribes that scheme: the window is the vendor's own one minute, the ceiling is the "
79
- + "vendor's own 600, and each Display API read costs 1. It is tighter than the vendor in one "
80
- + 'respect, deliberately: the published 600 is PER ENDPOINT while this is ONE ledger shared '
81
- + 'across all three, so the twin spends at a third of the vendor allowance in the worst case. '
82
- + 'The OAuth token and revoke endpoints publish NO scalar limit on that page; they are priced '
83
- + 'ABOVE the documented read (defaultWeight 5) so the undocumented surface is the conservative '
84
- + 'one. POST /v2/oauth/revoke/ costs 60 because it is the one destructive call here — it '
85
- + "disconnects an app from a real human's TikTok account — which is a human blast radius, not a "
86
- + 'throttle; that price is a judgement call, stated as one. The Content Posting references each '
87
- + 'publish a per-token minute allowance (fetched 2026-09-27): creator_info 20, the Direct Post and '
88
- + 'inbox inits 6, status/fetch 30; each is priced 600 divided by its figure (30, 100, 20), so no '
89
- + 'one of them can exceed its own allowance inside this minute. The chunk PUTs to the upload URL '
90
- + 'are not charged: they go to the upload host on a presigned URL, and once a publish has started '
91
- + 'the budget cannot stop it. The vendor publishes no rate-limit response headers, so the 429 / '
92
- + 'Retry-After cooldown is the only backstop and is armed from whatever the vendor does send.',
93
- };
94
- // Declared at module load, so merely importing this module (which `tiktok-connector.ts` does) is
95
- // enough to arm the real ceiling.
96
- declareRateBudget(VENDOR, TIKTOK_RATE_BUDGET);
97
- /**
98
- * Price one call. The key is `"<METHOD> <path>"` with the query string split off. NORMALIZED
99
- * (upper-cased method, collapsed trailing slash) because the anchored rules are otherwise trivially
100
- * evaded by input variation — and on THIS vendor the trailing slash is not hypothetical: every
101
- * documented TikTok path carries one (`/v2/oauth/revoke/`), so a rule anchored on the bare form
102
- * would price the vendor's OWN spelling of the destructive call at the default weight.
103
- */
104
- export function tiktokCallWeight(method, path) {
105
- const { bare, query } = splitQuery(path);
106
- return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
107
- }
108
- function splitQuery(path) {
109
- const at = path.indexOf('?');
110
- const query = {};
111
- if (at !== -1)
112
- for (const [k, v] of new URLSearchParams(path.slice(at + 1)))
113
- query[k] = v;
114
- const raw = at === -1 ? path : path.slice(0, at);
115
- const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
116
- return { bare, query };
117
- }
118
- /** Where this vendor's ledger lives. Token-keyed and cwd-independent by default; pass `root` to
119
- * opt into world-scoped accounting. */
120
- export function tiktokBudgetPath(opts = {}) {
121
- const o = typeof opts === 'string' ? { root: opts } : opts;
122
- // VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through must not
123
- // redirect this pack's ledger.
124
- return rateBudgetPath({ ...o, vendor: VENDOR });
125
- }
126
- /**
127
- * This vendor's budget — the shared kernel guard bound to this declaration. A real subclass, not
128
- * an alias, so `budget instanceof TikTokBudget` means "a budget that accounts against TIKTOK's
129
- * ledger under TIKTOK's ceiling".
130
- */
131
- export class TikTokBudget extends RateBudget {
132
- constructor(opts = {}) {
133
- super({ ...opts, vendor: VENDOR });
134
- }
135
- }
136
- export { RateBudgetError as TikTokBudgetError } from '@volter/world-core';
@@ -1,7 +0,0 @@
1
- import { type CapabilityReport, type CapabilitySpec } from '@volter/world-tooling';
2
- export declare const TIKTOK_CAPABILITIES: CapabilitySpec[];
3
- /** Committed area census (TWIN-87/F1) — enumerated TOP-DOWN from the vendor's own product nav
4
- * (Login Kit, Display API, Content Posting, Research, Data Portability, Local Services, Business)
5
- * plus this pack's structural areas, not derived from the manifest (which would be a tautology). */
6
- export declare const TIKTOK_AREAS: readonly ["authorize", "business", "client_credentials", "conformance", "connector", "consent", "content_posting", "errors", "local_services", "login_kit", "mini_apps", "oembed", "portability", "rate", "readonly", "refresh", "research", "revoke", "scopes", "share_kit", "token", "ui", "user_info", "video", "webhooks"];
7
- export declare function tiktokCapabilities(): Promise<CapabilityReport>;