@volter/twin-instagram 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.
- package/LICENSE +202 -0
- package/README.md +56 -0
- package/dist/client/instagram-mirror.bundle.js +321 -0
- package/dist/client/instagram-mirror.d.ts +58 -0
- package/dist/client/instagram-mirror.js +257 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +30 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +71 -0
- package/dist/src/instagram-budget.d.ts +44 -0
- package/dist/src/instagram-budget.js +112 -0
- package/dist/src/instagram-capabilities.d.ts +4 -0
- package/dist/src/instagram-capabilities.js +1249 -0
- package/dist/src/instagram-conformance.d.ts +8 -0
- package/dist/src/instagram-conformance.js +44 -0
- package/dist/src/instagram-connector.d.ts +79 -0
- package/dist/src/instagram-connector.js +437 -0
- package/dist/src/instagram-errors.d.ts +33 -0
- package/dist/src/instagram-errors.js +73 -0
- package/dist/src/instagram-media.d.ts +134 -0
- package/dist/src/instagram-media.js +388 -0
- package/dist/src/instagram-mirror-ui.d.ts +57 -0
- package/dist/src/instagram-mirror-ui.js +158 -0
- package/dist/src/instagram-server.d.ts +14 -0
- package/dist/src/instagram-server.js +146 -0
- package/dist/src/instagram-twin.d.ts +52 -0
- package/dist/src/instagram-twin.js +884 -0
- package/package.json +57 -0
- package/src/cli.ts +28 -0
- package/src/index.ts +117 -0
- package/src/instagram-budget.ts +130 -0
- package/src/instagram-capabilities.ts +1191 -0
- package/src/instagram-conformance.ts +58 -0
- package/src/instagram-connector.ts +400 -0
- package/src/instagram-errors.ts +89 -0
- package/src/instagram-media.ts +403 -0
- package/src/instagram-mirror-ui.ts +173 -0
- package/src/instagram-server.ts +146 -0
- package/src/instagram-twin.ts +859 -0
|
@@ -0,0 +1,1249 @@
|
|
|
1
|
+
// THE INSTAGRAM PACK'S CAPABILITY MANIFEST — the real vendor surface as the DENOMINATOR, not a list
|
|
2
|
+
// of what got built. The denominator is the Instagram Platform as a professional account uses it to
|
|
3
|
+
// publish and read its own content (developers.facebook.com/docs/instagram-platform, Graph API v26.0,
|
|
4
|
+
// read 2026-09-27): content publishing (images, Reels, videos, carousels, stories; hosted and
|
|
5
|
+
// resumable uploads), the IG User and IG Media nodes and their edges, comments, insights, hashtag
|
|
6
|
+
// search, mentions, business discovery, the publishing limit, webhooks and the Instagram Login token
|
|
7
|
+
// surface. The Marketing API's Instagram ads, the Messenger Platform's Instagram messaging and Threads
|
|
8
|
+
// are other Meta products this manifest does not enumerate beyond one line each.
|
|
9
|
+
//
|
|
10
|
+
// Every `done` below drives the handler the fetch adapter wraps (`handleInstagramTwinRequest`) on a
|
|
11
|
+
// fresh root with a pinned instant, and asserts VALUES and the vendor's refusals — never a status
|
|
12
|
+
// alone. The mirror's `done`s render the mirror's OWN components over what the handler served.
|
|
13
|
+
import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { checkCapabilities, verifyBoundary } from '@volter/world-tooling';
|
|
17
|
+
import { blobDigest, buildRemoteExecute, deployableEntries, observeResource, pendingActions, projectResources, RefusedWriteError, validateRemoteOrigin } from '@volter/world-core';
|
|
18
|
+
import { createElement } from 'react';
|
|
19
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
20
|
+
import { handleInstagramTwinRequest, tokenDigest } from "./instagram-twin.js";
|
|
21
|
+
import { budgetedInstagramExecute, MAX_STATUS_WAIT_MS, performInstagramAction as performServed, performInstagramActionWithin, syncInstagramFromRemote, UPLOAD_CHUNK_BYTES, uploadReel, uploadTarget } from "./instagram-connector.js";
|
|
22
|
+
import { INSTAGRAM_CALL_WEIGHTS, INSTAGRAM_RATE_BUDGET, InstagramBudget, InstagramBudgetError, instagramCallWeight } from "./instagram-budget.js";
|
|
23
|
+
import { captionSegments, compactCount, createInstagramMirrorServer, instagramMirrorStyles, mediaQuery, profileFromRead, reelRoute, timeAgo } from "./instagram-mirror-ui.js";
|
|
24
|
+
import { listContainers, readContainer, writeContainer } from "./instagram-media.js";
|
|
25
|
+
import { ProfileHeader, ProfileScreen, ReelsGrid, ReelViewer } from "../client/instagram-mirror.js";
|
|
26
|
+
const IG = '17841405822304914';
|
|
27
|
+
const OTHER = '17841400000000777';
|
|
28
|
+
const TOKEN = 'ig-user-token-0001';
|
|
29
|
+
const OTHER_TOKEN = 'ig-other-token-0002';
|
|
30
|
+
const PERMS = ['instagram_basic', 'instagram_content_publish', 'pages_read_engagement', 'instagram_manage_contents'];
|
|
31
|
+
const AUTH = { authorization: `Bearer ${TOKEN}` };
|
|
32
|
+
const OAUTH = { authorization: `OAuth ${TOKEN}` };
|
|
33
|
+
const AT = '2026-09-27T12:00:00.000Z';
|
|
34
|
+
/** Ten seconds before AT: a Reel uploaded then has finished processing (PROCESSING_MS) by AT. */
|
|
35
|
+
const EARLY = '2026-09-27T11:59:50.000Z';
|
|
36
|
+
const LATER = (ms) => new Date(Date.parse(AT) + ms).toISOString();
|
|
37
|
+
function handleFor(root, at = AT) {
|
|
38
|
+
return (step) => handleInstagramTwinRequest({
|
|
39
|
+
method: step.m, path: step.p, root, occurredAt: step.at ?? at, readOnly: step.readOnly,
|
|
40
|
+
headers: { ...(step.form !== undefined ? { 'content-type': 'application/x-www-form-urlencoded' } : { 'content-type': 'application/json' }), ...(step.headers ?? AUTH) },
|
|
41
|
+
...(step.form !== undefined ? { body: step.form } : step.b === undefined ? {} : { body: JSON.stringify(step.b) }),
|
|
42
|
+
...(step.bytes ? { bytes: step.bytes } : {}),
|
|
43
|
+
...(step.base ? { publicBase: step.base } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function seed(h, opts = {}) {
|
|
47
|
+
const id = opts.id ?? IG;
|
|
48
|
+
await h({ m: 'POST', p: '/_twin/users', b: { id, username: opts.username ?? 'metricsaurus', name: 'Metricsaurus', biography: 'Dino data crunching app', website: 'http://www.metricsaurus.com/', followers_count: 12480, follows_count: 87 }, headers: {} });
|
|
49
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: opts.token ?? TOKEN, user: id, permissions: opts.permissions ?? PERMS }, headers: {} });
|
|
50
|
+
}
|
|
51
|
+
/** A throwaway world with the account and the token the Facebook Login leg would have minted. */
|
|
52
|
+
async function withWorld(fn, opts = {}) {
|
|
53
|
+
const root = mkdtempSync(join(tmpdir(), 'instagram-cap-'));
|
|
54
|
+
const h = handleFor(root);
|
|
55
|
+
try {
|
|
56
|
+
return await verifyBoundary('instagram.withWorld', async () => { await seed(h, opts); return fn(h, root); });
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
rmSync(root, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const bodyOf = (r) => r.body;
|
|
63
|
+
const err = (r) => r.body?.error ?? {};
|
|
64
|
+
// ── media fixtures: the smallest bytes the twin reads as a Reel ─────────────────────────────────
|
|
65
|
+
function box(type, ...payload) {
|
|
66
|
+
const inner = concat(...payload);
|
|
67
|
+
const out = new Uint8Array(8 + inner.length);
|
|
68
|
+
new DataView(out.buffer).setUint32(0, out.length);
|
|
69
|
+
out.set(new TextEncoder().encode(type), 4);
|
|
70
|
+
out.set(inner, 8);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function concat(...parts) {
|
|
74
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
75
|
+
let at = 0;
|
|
76
|
+
for (const p of parts) {
|
|
77
|
+
out.set(p, at);
|
|
78
|
+
at += p.length;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function u32s(...values) {
|
|
83
|
+
const out = new Uint8Array(values.length * 4);
|
|
84
|
+
values.forEach((v, i) => new DataView(out.buffer).setUint32(i * 4, v >>> 0));
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** An ISO BMFF file's boxes for a Reel: ftyp, moov (mvhd; a video trak with tkhd, mdhd, hdlr, stsd, stts;
|
|
88
|
+
* optionally a sound trak) and mdat — before or after moov. Nothing in it decodes; the twin reads boxes. */
|
|
89
|
+
function mp4(o = {}) {
|
|
90
|
+
const ms = o.ms ?? 5000;
|
|
91
|
+
const w = o.w ?? 1080;
|
|
92
|
+
const hgt = o.h ?? 1920;
|
|
93
|
+
const fps = o.fps ?? 30;
|
|
94
|
+
const scale = 15360;
|
|
95
|
+
const mvhd = new Uint8Array(100);
|
|
96
|
+
new DataView(mvhd.buffer).setUint32(12, 1000);
|
|
97
|
+
new DataView(mvhd.buffer).setUint32(16, ms);
|
|
98
|
+
const tkhd = new Uint8Array(84);
|
|
99
|
+
new DataView(tkhd.buffer).setUint32(76, w << 16);
|
|
100
|
+
new DataView(tkhd.buffer).setUint32(80, hgt << 16);
|
|
101
|
+
const mdhd = (ts, dur) => box('mdhd', u32s(0, 0, 0, ts, dur, 0));
|
|
102
|
+
const hdlr = (t) => box('hdlr', u32s(0, 0), new TextEncoder().encode(t), new Uint8Array(13));
|
|
103
|
+
const frames = Math.round((ms / 1000) * fps);
|
|
104
|
+
const videoEntry = box(o.codec ?? 'avc1', new Uint8Array(78));
|
|
105
|
+
const video = box('trak', box('tkhd', tkhd), box('mdia', mdhd(scale, Math.round((ms / 1000) * scale)), hdlr('vide'), box('minf', box('stbl', box('stsd', u32s(0, 1), videoEntry), box('stts', u32s(0, 1, frames, Math.round(scale / fps)))))));
|
|
106
|
+
const tracks = [video];
|
|
107
|
+
const audio = o.audio === undefined ? { codec: 'mp4a', hz: 48000, ch: 2 } : o.audio;
|
|
108
|
+
if (audio) {
|
|
109
|
+
const entry = new Uint8Array(28);
|
|
110
|
+
new DataView(entry.buffer).setUint16(16, audio.ch);
|
|
111
|
+
new DataView(entry.buffer).setUint32(24, audio.hz * 65536);
|
|
112
|
+
tracks.push(box('trak', box('tkhd', new Uint8Array(84)), box('mdia', mdhd(audio.hz, Math.round((ms / 1000) * audio.hz)), hdlr('soun'), box('minf', box('stbl', box('stsd', u32s(0, 1), box(audio.codec, entry)))))));
|
|
113
|
+
}
|
|
114
|
+
const pad = new Uint8Array(o.pad ?? 4096);
|
|
115
|
+
for (let i = 0; i < pad.length; i += 1)
|
|
116
|
+
pad[i] = (i * 31 + 7) % 251;
|
|
117
|
+
const ftyp = box('ftyp', new TextEncoder().encode(o.brand ?? 'isom'), u32s(512), new TextEncoder().encode('isomiso2avc1mp41'));
|
|
118
|
+
const moov = box('moov', box('mvhd', mvhd), ...tracks);
|
|
119
|
+
const mdat = box('mdat', pad);
|
|
120
|
+
return o.moovFirst === false ? concat(ftyp, mdat, moov) : concat(ftyp, moov, mdat);
|
|
121
|
+
}
|
|
122
|
+
/** A container, opened at `at`. */
|
|
123
|
+
async function container(h, extra = {}, at = EARLY, user = IG) {
|
|
124
|
+
const r = await h({ m: 'POST', p: `/v26.0/${user}/media`, b: { media_type: 'REELS', upload_type: 'resumable', ...extra }, at });
|
|
125
|
+
if (r.status !== 200)
|
|
126
|
+
throw new Error(`container answered ${r.status}: ${JSON.stringify(r.body)}`);
|
|
127
|
+
return String(bodyOf(r).id);
|
|
128
|
+
}
|
|
129
|
+
/** The bytes, in one rupload POST, at `at`. */
|
|
130
|
+
async function upload(h, id, bytes, at = EARLY, headers = OAUTH) {
|
|
131
|
+
return h({ m: 'POST', p: `/ig-api-upload/v26.0/${id}`, bytes, headers: { ...headers, offset: '0', file_size: String(bytes.length) }, at });
|
|
132
|
+
}
|
|
133
|
+
/** A Reel through the whole flow: container and upload ten seconds before AT, published at AT. */
|
|
134
|
+
async function publishReel(h, extra = {}, bytes = mp4()) {
|
|
135
|
+
const id = await container(h, extra);
|
|
136
|
+
const up = await upload(h, id, bytes);
|
|
137
|
+
if (up.status !== 200)
|
|
138
|
+
throw new Error(`upload answered ${up.status}: ${JSON.stringify(up.body)}`);
|
|
139
|
+
const r = await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: id } });
|
|
140
|
+
if (r.status !== 200)
|
|
141
|
+
throw new Error(`publish answered ${r.status}: ${JSON.stringify(r.body)}`);
|
|
142
|
+
return String(bodyOf(r).id);
|
|
143
|
+
}
|
|
144
|
+
function vendorExecutor(vendorRoot, calls, opts = {}) {
|
|
145
|
+
// the vendor's clock moves 3 s per call, so a Reel it received is FINISHED two reads later
|
|
146
|
+
let clock = Date.parse('2026-09-27T13:00:00.000Z');
|
|
147
|
+
return async (request) => {
|
|
148
|
+
clock += opts.step ?? 3_000;
|
|
149
|
+
const absolute = /^https:\/\//.test(request.path);
|
|
150
|
+
if (absolute && new URL(request.path).hostname !== 'rupload.facebook.com')
|
|
151
|
+
throw new Error(`the kernel would refuse ${request.path}`);
|
|
152
|
+
const path = absolute ? new URL(request.path).pathname + new URL(request.path).search : request.path;
|
|
153
|
+
const own = Object.keys(request.headers ?? {}).some((k) => k.toLowerCase() === 'authorization');
|
|
154
|
+
const headers = { ...(request.headers ?? {}), authorization: 'OAuth vendor-token-0002' };
|
|
155
|
+
calls.push({ method: request.method, path: absolute ? request.path : path, absolute, own, ...(request.headers?.offset !== undefined ? { offset: request.headers.offset } : {}) });
|
|
156
|
+
const body = request.body;
|
|
157
|
+
const res = await handleInstagramTwinRequest({
|
|
158
|
+
method: request.method, path, headers, root: vendorRoot, occurredAt: new Date(clock).toISOString(), ...(opts.base ? { publicBase: opts.base } : {}),
|
|
159
|
+
...(body instanceof Uint8Array ? { bytes: body } : body !== undefined ? { body } : {}),
|
|
160
|
+
});
|
|
161
|
+
// an HTTP server's own header, as the vendor's server stamps it: the vendor's instant
|
|
162
|
+
return { status: res.status, headers: { ...(res.headers ?? {}), date: new Date(clock).toUTCString() }, body: res.body === null || res.body === undefined || res.body === '' ? '' : JSON.stringify(res.body) };
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function withVendor(fn) {
|
|
166
|
+
const localRoot = mkdtempSync(join(tmpdir(), 'instagram-cap-local-'));
|
|
167
|
+
const vendorRoot = mkdtempSync(join(tmpdir(), 'instagram-cap-vendor-'));
|
|
168
|
+
try {
|
|
169
|
+
return await verifyBoundary('instagram.withVendor', async () => {
|
|
170
|
+
const local = handleFor(localRoot);
|
|
171
|
+
await seed(local);
|
|
172
|
+
// the vendor holds the same account (a real IG user id); its token is the root's sealed credential
|
|
173
|
+
await seed(handleFor(vendorRoot), { token: 'vendor-token-0002' });
|
|
174
|
+
return fn(local, localRoot, vendorRoot);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
rmSync(localRoot, { recursive: true, force: true });
|
|
179
|
+
rmSync(vendorRoot, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const entryFor = (root, subjectId, operation) => pendingActions('instagram', root).find((a) => a.subject?.id === subjectId && a.operation === operation);
|
|
183
|
+
const ctxFor = (root) => ({ resolve: (_t, id) => id, root, service: 'instagram' });
|
|
184
|
+
/** A throwaway ledger per verify: a verify never spends the operator's Instagram allowance. */
|
|
185
|
+
const scratchBudget = () => new InstagramBudget({ path: join(mkdtempSync(join(tmpdir(), 'instagram-budget-')), 'ledger.json') });
|
|
186
|
+
const noWait = async (_ms) => undefined;
|
|
187
|
+
const performInstagramAction = (execute, action, ctx, wait = noWait, now) => performInstagramActionWithin(scratchBudget(), execute, action, ctx, wait, now);
|
|
188
|
+
/** Every file under a directory, as text — what a push or a clone of the World would carry. */
|
|
189
|
+
function allText(dir) {
|
|
190
|
+
let out = '';
|
|
191
|
+
for (const name of readdirSync(dir)) {
|
|
192
|
+
const p = join(dir, name);
|
|
193
|
+
if (statSync(p).isDirectory())
|
|
194
|
+
out += allText(p);
|
|
195
|
+
else
|
|
196
|
+
out += readFileSync(p).toString('latin1');
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
const done = (id, area, title, dimension, tier, verify) => ({ id, area, title, dimension, tier, expected: 'done', verify });
|
|
201
|
+
const todo = (id, area, title, dimension, tier) => ({ id, area, title, dimension, tier, expected: 'todo' });
|
|
202
|
+
export const INSTAGRAM_AREAS = ['versioning', 'auth', 'users', 'containers', 'upload', 'publish', 'media', 'limits', 'publishing', 'comments', 'insights', 'discovery', 'webhooks', 'login', 'errors', 'twin_control', 'connector', 'rate_limit', 'mirror'];
|
|
203
|
+
export const INSTAGRAM_CAPABILITIES = [
|
|
204
|
+
// ── versioning ──────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
done('instagram.versioning.versions_served', 'versioning', 'The changelog\'s usable versions (v21.0 … v26.0 on 2026-09-27) and an unversioned path are served; an expired version defaults to the oldest usable at the request\'s own instant (v20.0 → v21.0 now, v21.0 → v22.0 once v21.0 expires 2027-01-21); a version the table does not hold (v27.0, v26.1) is an unknown path, on the Graph hosts and the upload host', 'api', 'core', () => withWorld(async (h) => {
|
|
206
|
+
const ok = [];
|
|
207
|
+
for (const v of ['/v21.0', '/v24.0', '/v26.0', '/v20.0', '/v2.0', ''])
|
|
208
|
+
ok.push(bodyOf(await h({ m: 'GET', p: `${v}/${IG}?fields=username` })).username === 'metricsaurus');
|
|
209
|
+
const future = await h({ m: 'GET', p: `/v27.0/${IG}` });
|
|
210
|
+
const minor = await h({ m: 'GET', p: `/v26.1/${IG}` });
|
|
211
|
+
const uri = bodyOf(await h({ m: 'POST', p: `/v20.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable' } })).uri;
|
|
212
|
+
const later = bodyOf(await h({ m: 'POST', p: `/v21.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable' }, at: '2027-02-01T00:00:00.000Z' })).uri;
|
|
213
|
+
const up = await h({ m: 'POST', p: '/ig-api-upload/v27.0/812345678901234567', bytes: mp4(), headers: { ...OAUTH, offset: '0', file_size: '1' } });
|
|
214
|
+
return ok.every(Boolean) && future.status === 400 && err(future).code === 2500 && minor.status === 400 && /\/ig-api-upload\/v21\.0\//.test(String(uri)) && /\/ig-api-upload\/v22\.0\//.test(String(later))
|
|
215
|
+
&& up.status === 400 && bodyOf(up).debug_info?.message?.includes('Invalid upload path');
|
|
216
|
+
})),
|
|
217
|
+
// ── auth ──────────────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
done('instagram.auth.token_required', 'auth', 'No token is refused (code 104); a token the twin never issued is 190 Invalid OAuth access token', 'api', 'core', () => withWorld(async (h) => {
|
|
219
|
+
const none = await h({ m: 'GET', p: `/v26.0/${IG}`, headers: {} });
|
|
220
|
+
const bad = await h({ m: 'GET', p: `/v26.0/${IG}`, headers: { authorization: 'Bearer not-a-real-token' } });
|
|
221
|
+
return none.status === 400 && err(none).code === 104 && err(none).type === 'OAuthException' && bad.status === 400 && err(bad).code === 190;
|
|
222
|
+
})),
|
|
223
|
+
done('instagram.auth.token_carriers', 'auth', 'The token rides Authorization: Bearer, Authorization: OAuth, the access_token query parameter or a form body\'s access_token', 'api', 'core', () => withWorld(async (h) => {
|
|
224
|
+
const bearer = await h({ m: 'GET', p: `/v26.0/${IG}?fields=username` });
|
|
225
|
+
const oauth = await h({ m: 'GET', p: `/v26.0/${IG}?fields=username`, headers: OAUTH });
|
|
226
|
+
const query = await h({ m: 'GET', p: `/v26.0/${IG}?fields=username&access_token=${TOKEN}`, headers: {} });
|
|
227
|
+
const form = await h({ m: 'POST', p: `/v26.0/${IG}/media`, form: `media_type=REELS&upload_type=resumable&access_token=${TOKEN}`, headers: {} });
|
|
228
|
+
return [bearer, oauth, query].every((r) => bodyOf(r).username === 'metricsaurus') && form.status === 200 && /^\d{18}$/.test(String(bodyOf(form).id));
|
|
229
|
+
})),
|
|
230
|
+
done('instagram.auth.permission_gate', 'auth', "A request needs every permission of its reference's Requirements row: a container instagram_basic + instagram_content_publish + pages_read_engagement, a publish instagram_basic + instagram_content_publish, a read instagram_basic + pages_read_engagement; short of one it is (#10) and nothing is staged or published", 'api', 'core', () => withWorld(async (h, root) => {
|
|
231
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'read-only-token-1', user: IG, permissions: ['instagram_basic', 'pages_read_engagement'] }, headers: {} });
|
|
232
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'publish-no-pages-1', user: IG, permissions: ['instagram_basic', 'instagram_content_publish'] }, headers: {} });
|
|
233
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'publish-only-token', user: IG, permissions: ['instagram_content_publish'] }, headers: {} });
|
|
234
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'basic-only-token-1', user: IG, permissions: ['instagram_basic'] }, headers: {} });
|
|
235
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'ig-login-token-01', user: IG, permissions: ['instagram_business_basic', 'instagram_business_content_publish'] }, headers: {} });
|
|
236
|
+
const reel = (t) => h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable' }, headers: { authorization: `Bearer ${t}` } });
|
|
237
|
+
const noPublish = await reel('read-only-token-1');
|
|
238
|
+
const noPages = await reel('publish-no-pages-1');
|
|
239
|
+
const read = await h({ m: 'GET', p: `/v26.0/${IG}?fields=username`, headers: { authorization: 'Bearer publish-only-token' } });
|
|
240
|
+
const id = await container(h);
|
|
241
|
+
await upload(h, id, mp4());
|
|
242
|
+
const pubRead = await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: id }, headers: { authorization: 'Bearer read-only-token-1' } });
|
|
243
|
+
const pubOnly = await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: id }, headers: { authorization: 'Bearer publish-only-token' } });
|
|
244
|
+
const reads = await Promise.all([`/v26.0/${IG}?fields=username`, `/v26.0/${IG}/media`, `/v26.0/${id}?fields=status_code`].map((p) => h({ m: 'GET', p, headers: { authorization: 'Bearer basic-only-token-1' } })));
|
|
245
|
+
const igLogin = await reel('ig-login-token-01');
|
|
246
|
+
const igLoginRead = await h({ m: 'GET', p: `/v26.0/${IG}?fields=username`, headers: { authorization: 'Bearer ig-login-token-01' } });
|
|
247
|
+
const pubOk = await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: id }, headers: { authorization: 'Bearer publish-no-pages-1' } });
|
|
248
|
+
return [noPublish, noPages, read, pubRead, pubOnly, ...reads, igLogin].every((r) => r.status === 400 && err(r).code === 10) && /Facebook Login for Business/.test(String(err(igLogin).message))
|
|
249
|
+
&& bodyOf(igLoginRead).username === 'metricsaurus' && (await listContainers(root)).length === 1 && pubOk.status === 200 && /^9\d{17}$/.test(String(bodyOf(pubOk).id));
|
|
250
|
+
})),
|
|
251
|
+
done('instagram.auth.own_account_only', 'auth', "A token reads and writes only its own account: another account's node is Unsupported get request (100/33), creating on it is (#10)", 'api', 'core', () => withWorld(async (h) => {
|
|
252
|
+
await seed(h, { id: OTHER, username: 'other.account', token: OTHER_TOKEN });
|
|
253
|
+
const read = await h({ m: 'GET', p: `/v26.0/${OTHER}?fields=username` });
|
|
254
|
+
const create = await h({ m: 'POST', p: `/v26.0/${OTHER}/media`, b: { media_type: 'REELS', upload_type: 'resumable' } });
|
|
255
|
+
const own = await h({ m: 'GET', p: `/v26.0/${OTHER}?fields=username`, headers: { authorization: `Bearer ${OTHER_TOKEN}` } });
|
|
256
|
+
return read.status === 400 && err(read).code === 100 && err(read).error_subcode === 33 && create.status === 400 && err(create).code === 10 && bodyOf(own).username === 'other.account';
|
|
257
|
+
})),
|
|
258
|
+
done('instagram.auth.token_held_as_digest', 'auth', "A seeded token never lands in the World in the clear: the log, the tree and the annex hold only its SHA-256, under the `_token` bookkeeping type that is never deployed", 'api', 'core', () => withWorld(async (h, root) => {
|
|
259
|
+
await publishReel(h);
|
|
260
|
+
const text = allText(root);
|
|
261
|
+
const rows = projectResources('instagram', root);
|
|
262
|
+
const deployable = deployableEntries('instagram', root);
|
|
263
|
+
const pending = pendingActions('instagram', root);
|
|
264
|
+
return !text.includes(TOKEN) && rows.some((r) => r.type === '_token' && r.id === tokenDigest(TOKEN)) && tokenDigest(TOKEN) === blobDigest(new TextEncoder().encode(TOKEN))
|
|
265
|
+
&& pending.some((a) => a.subject.type === '_token') && deployable.length > 0 && deployable.every((a) => a.subject.type !== '_token') && !JSON.stringify(pending).includes(TOKEN);
|
|
266
|
+
})),
|
|
267
|
+
// ── the IG User node ─────────────────────────────────────────────────────────────────────────
|
|
268
|
+
done('instagram.users.get_fields', 'users', "GET /{ig-user-id} answers the fields asked for (the reference sample's biography, id, username, website), and only id when none are asked", 'api', 'core', () => withWorld(async (h) => {
|
|
269
|
+
const sample = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}?fields=biography%2Cid%2Cusername%2Cwebsite` }));
|
|
270
|
+
const bare = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}` }));
|
|
271
|
+
const counts = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}?fields=name,followers_count,follows_count,media_count,has_profile_pic` }));
|
|
272
|
+
return JSON.stringify(sample) === JSON.stringify({ biography: 'Dino data crunching app', id: IG, username: 'metricsaurus', website: 'http://www.metricsaurus.com/' })
|
|
273
|
+
&& JSON.stringify(bare) === JSON.stringify({ id: IG }) && counts.followers_count === 12480 && counts.follows_count === 87 && counts.media_count === 0 && counts.has_profile_pic === false && counts.name === 'Metricsaurus';
|
|
274
|
+
})),
|
|
275
|
+
done('instagram.users.nonexisting_field', 'users', 'A field the IG User does not have is (#100) Tried accessing nonexisting field; a documented one the twin does not model is refused by name', 'api', 'common', () => withWorld(async (h) => {
|
|
276
|
+
const bogus = await h({ m: 'GET', p: `/v26.0/${IG}?fields=username,favorite_color` });
|
|
277
|
+
const gap = await h({ m: 'GET', p: `/v26.0/${IG}?fields=legacy_instagram_user_id` });
|
|
278
|
+
return bogus.status === 400 && /nonexisting field \(favorite_color\) on node type \(IGUser\)/.test(String(err(bogus).message)) && gap.status === 422 && /\[twin gap\]/.test(String(err(gap).message));
|
|
279
|
+
})),
|
|
280
|
+
done('instagram.users.me', 'users', "GET /me answers the token's own account, so a client needs no stored user id", 'api', 'core', () => withWorld(async (h) => {
|
|
281
|
+
const me = bodyOf(await h({ m: 'GET', p: '/v26.0/me?fields=id,username' }));
|
|
282
|
+
await seed(h, { id: OTHER, username: 'other.account', token: OTHER_TOKEN });
|
|
283
|
+
const them = bodyOf(await h({ m: 'GET', p: '/v26.0/me?fields=id,username', headers: { authorization: `Bearer ${OTHER_TOKEN}` } }));
|
|
284
|
+
return me.id === IG && me.username === 'metricsaurus' && them.id === OTHER && them.username === 'other.account';
|
|
285
|
+
})),
|
|
286
|
+
done('instagram.users.media_count_from_state', 'users', 'media_count is the live media the account holds: a publish raises it and a delete lowers it', 'api', 'common', () => withWorld(async (h) => {
|
|
287
|
+
const a = await publishReel(h);
|
|
288
|
+
await publishReel(h, { caption: 'second' }, mp4({ ms: 6000 }));
|
|
289
|
+
const two = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}?fields=media_count` })).media_count;
|
|
290
|
+
await h({ m: 'DELETE', p: `/v26.0/${a}` });
|
|
291
|
+
const one = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}?fields=media_count` })).media_count;
|
|
292
|
+
return two === 2 && one === 1;
|
|
293
|
+
})),
|
|
294
|
+
done('instagram.users.profile_picture', 'users', "A seeded profile picture answers profile_picture_url on the twin's base, whose bytes are the picture, and has_profile_pic true", 'api', 'common', async () => {
|
|
295
|
+
const root = mkdtempSync(join(tmpdir(), 'instagram-cap-pic-'));
|
|
296
|
+
try {
|
|
297
|
+
const { createInstagramTwinFetch } = await import("./instagram-server.js");
|
|
298
|
+
const fetcher = createInstagramTwinFetch({ root });
|
|
299
|
+
const png = concat(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), box('IHDR', u32s(64, 64), new Uint8Array([8, 2, 0, 0, 0])), new Uint8Array(4));
|
|
300
|
+
const json = { 'content-type': 'application/json' };
|
|
301
|
+
await fetcher(new Request('http://twin.test/_twin/users', { method: 'POST', headers: json, body: JSON.stringify({ id: IG, username: 'metricsaurus', profile_picture: btoa(String.fromCharCode(...png)) }) }));
|
|
302
|
+
await fetcher(new Request('http://twin.test/_twin/tokens', { method: 'POST', headers: json, body: JSON.stringify({ token: TOKEN, user: IG, permissions: PERMS }) }));
|
|
303
|
+
const me = await (await fetcher(new Request(`http://twin.test/v26.0/${IG}?fields=profile_picture_url,has_profile_pic`, { headers: AUTH }))).json();
|
|
304
|
+
const url = new URL(String(me.profile_picture_url));
|
|
305
|
+
const got = await fetcher(new Request(`http://twin.test${url.pathname}`));
|
|
306
|
+
const bytes = new Uint8Array(await got.arrayBuffer());
|
|
307
|
+
const forged = await fetcher(new Request(`http://twin.test/v/t51.2885-19/${'0'.repeat(64)}.png`));
|
|
308
|
+
return url.origin === 'http://twin.test' && me.has_profile_pic === true && got.status === 200 && got.headers.get('content-type') === 'image/png'
|
|
309
|
+
&& bytes.length === png.length && bytes.every((b, i) => b === png[i]) && forged.status === 404;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
rmSync(root, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
}),
|
|
315
|
+
todo('instagram.users.business_discovery', 'discovery', "business_discovery: another professional account's public fields and media by username", 'api', 'common'),
|
|
316
|
+
todo('instagram.users.stories', 'users', 'The stories edge (an account\'s live stories)', 'api', 'common'),
|
|
317
|
+
todo('instagram.users.tags', 'users', 'The tags edge (media the account is tagged in)', 'api', 'niche'),
|
|
318
|
+
todo('instagram.users.mentioned_media', 'users', 'mentioned_media and mentioned_comment (media and comments that @mention the account)', 'api', 'niche'),
|
|
319
|
+
todo('instagram.users.live_media', 'users', 'The live_media edge (live broadcasts)', 'api', 'niche'),
|
|
320
|
+
todo('instagram.users.collaboration', 'users', 'collaboration_invites and collaborative_media', 'api', 'niche'),
|
|
321
|
+
todo('instagram.users.threads', 'users', 'connected_threads_user and instagram_backed_threads_user', 'api', 'niche'),
|
|
322
|
+
todo('instagram.users.shopping', 'users', 'shopping_product_tag_eligibility, available_catalogs and catalog_product_search', 'api', 'niche'),
|
|
323
|
+
todo('instagram.users.ads_edges', 'users', 'agencies, authorized_adaccounts and upcoming_events', 'api', 'niche'),
|
|
324
|
+
// ── containers ───────────────────────────────────────────────────────────────────────────────
|
|
325
|
+
done('instagram.containers.create_resumable_reel', 'containers', 'POST /{ig-user-id}/media with media_type=REELS and upload_type=resumable answers {id, uri}: the uri is the rupload host\'s /ig-api-upload/{version}/{id}, and the container reads IN_PROGRESS', 'api', 'core', () => withWorld(async (h, root) => {
|
|
326
|
+
const r = bodyOf(await h({ m: 'POST', p: `/v24.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', caption: 'hello', share_to_feed: false, thumb_offset: 1000, audio_name: 'Volter theme' } }));
|
|
327
|
+
const status = bodyOf(await h({ m: 'GET', p: `/v26.0/${r.id}?fields=status_code` }));
|
|
328
|
+
const stored = await readContainer(String(r.id), root);
|
|
329
|
+
return /^8\d{17}$/.test(String(r.id)) && r.uri === `https://rupload.facebook.com/ig-api-upload/v24.0/${r.id}` && status.status_code === 'IN_PROGRESS'
|
|
330
|
+
&& stored?.caption === 'hello' && stored.share_to_feed === false && stored.thumb_offset === 1000 && stored.audio_name === 'Volter theme' && Object.keys(r).length === 2;
|
|
331
|
+
})),
|
|
332
|
+
done('instagram.containers.uri_under_public_base', 'containers', "Served by a twin, the container's uri is minted under the twin's own public base, so the upload reaches the twin", 'api', 'core', () => withWorld(async (h) => {
|
|
333
|
+
const r = bodyOf(await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable' }, base: 'http://127.0.0.1:4400/acme/world/instagram' }));
|
|
334
|
+
return r.uri === `http://127.0.0.1:4400/acme/world/instagram/ig-api-upload/v26.0/${r.id}`;
|
|
335
|
+
})),
|
|
336
|
+
done('instagram.containers.media_type_values', 'containers', 'An unknown media_type is 2207023; no media_type and no image_url asks for image_url; image, carousel, story and VIDEO containers are refused by name', 'api', 'core', () => withWorld(async (h, root) => {
|
|
337
|
+
const unknown = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'GIF' } });
|
|
338
|
+
const bare = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: {} });
|
|
339
|
+
const gaps = await Promise.all([{ image_url: 'https://x.example/a.jpg' }, { media_type: 'CAROUSEL' }, { media_type: 'STORIES' }, { media_type: 'VIDEO' }].map((b) => h({ m: 'POST', p: `/v26.0/${IG}/media`, b })));
|
|
340
|
+
return unknown.status === 400 && err(unknown).error_subcode === 2207023 && bare.status === 400 && /image_url is required/.test(String(err(bare).message))
|
|
341
|
+
&& gaps.every((g) => g.status === 422 && /\[twin gap\]/.test(String(err(g).message))) && (await listContainers(root)).length === 0;
|
|
342
|
+
})),
|
|
343
|
+
done('instagram.containers.upload_type', 'containers', 'A Reel without upload_type needs video_url (a hosted video is refused by name: Meta would fetch it); upload_type must be the lowercase resumable; a resumable session takes no video_url', 'api', 'core', () => withWorld(async (h) => {
|
|
344
|
+
const bare = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS' } });
|
|
345
|
+
const hosted = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', video_url: 'https://www.example.com/videos/bronz-fonz.mp4' } });
|
|
346
|
+
const upper = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'RESUMABLE' } });
|
|
347
|
+
const both = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', video_url: 'https://x.example/v.mp4' } });
|
|
348
|
+
return bare.status === 400 && /video_url is required/.test(String(err(bare).message)) && hosted.status === 422 && /video_url/.test(String(err(hosted).message))
|
|
349
|
+
&& upper.status === 400 && err(upper).code === 100 && /lowercase string resumable/.test(String(err(upper).message)) && both.status === 400 && err(both).code === 100 && /video_url is not taken/.test(String(err(both).message));
|
|
350
|
+
})),
|
|
351
|
+
done('instagram.containers.caption_limits', 'containers', 'A caption over 2200 characters is 36004/2207010, over 20 @ tags 100/2207040, over 30 hashtags (#100)', 'api', 'common', () => withWorld(async (h) => {
|
|
352
|
+
const long = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', caption: 'x'.repeat(2201) } });
|
|
353
|
+
const edge = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', caption: 'x'.repeat(2200) } });
|
|
354
|
+
const tags = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', caption: Array.from({ length: 21 }, (_, i) => `@user${i}`).join(' ') } });
|
|
355
|
+
const hashes = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', caption: Array.from({ length: 31 }, (_, i) => `#tag${i}`).join(' ') } });
|
|
356
|
+
return long.status === 400 && err(long).code === 36004 && err(long).error_subcode === 2207010 && edge.status === 200
|
|
357
|
+
&& tags.status === 400 && err(tags).error_subcode === 2207040 && hashes.status === 400 && err(hashes).code === 100;
|
|
358
|
+
})),
|
|
359
|
+
done('instagram.containers.params_validated', 'containers', 'share_to_feed and is_ai_generated take true/false, cover_url an http(s) URL, thumb_offset a non-negative integer (1/2207057); alt_text is refused on a Reel', 'api', 'common', () => withWorld(async (h) => {
|
|
360
|
+
const r = (b) => h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', ...b } });
|
|
361
|
+
const share = await r({ share_to_feed: 'maybe' });
|
|
362
|
+
const ai = await r({ is_ai_generated: 'sure' });
|
|
363
|
+
const cover = await r({ cover_url: 'ftp://x.example/c.jpg' });
|
|
364
|
+
const offset = await r({ thumb_offset: -1 });
|
|
365
|
+
const alt = await r({ alt_text: 'a dino' });
|
|
366
|
+
const good = await r({ share_to_feed: 'false', is_ai_generated: true, cover_url: 'https://www.example.com/cover.jpg', thumb_offset: '0' });
|
|
367
|
+
return /share_to_feed must be true or false/.test(String(err(share).message)) && /is_ai_generated must be true or false/.test(String(err(ai).message)) && /cover_url must be an http\(s\) URL/.test(String(err(cover).message))
|
|
368
|
+
&& err(offset).code === 1 && err(offset).error_subcode === 2207057 && /Reels and stories are not supported/.test(String(err(alt).message)) && good.status === 200;
|
|
369
|
+
})),
|
|
370
|
+
done('instagram.containers.unmodeled_params_refused', 'containers', 'collaborators, user_tags, location_id, trial_params and product_tags on a Reel are refused by name, and no container is staged', 'api', 'common', () => withWorld(async (h, root) => {
|
|
371
|
+
const refused = await Promise.all([{ collaborators: ['a'] }, { user_tags: [] }, { location_id: '1' }, { trial_params: { graduation_strategy: 'MANUAL' } }, { product_tags: [] }]
|
|
372
|
+
.map((b) => h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', ...b } })));
|
|
373
|
+
return refused.every((r) => r.status === 422 && /\[twin gap\]/.test(String(err(r).message))) && (await listContainers(root)).length === 0;
|
|
374
|
+
})),
|
|
375
|
+
done('instagram.containers.status_lifecycle', 'containers', 'status_code: IN_PROGRESS before the bytes and for five seconds of world time after them, then FINISHED; PUBLISHED once published; EXPIRED 24 hours after creation if not; reading never moves it', 'api', 'core', () => withWorld(async (h) => {
|
|
376
|
+
const s = async (id, at) => bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=status_code`, at })).status_code;
|
|
377
|
+
const a = await container(h, {}, AT);
|
|
378
|
+
const empty = await s(a, AT);
|
|
379
|
+
await upload(h, a, mp4(), LATER(1_000));
|
|
380
|
+
const processing = [await s(a, LATER(5_999)), await s(a, LATER(5_999))];
|
|
381
|
+
const finished = await s(a, LATER(6_000));
|
|
382
|
+
await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: a }, at: LATER(7_000) });
|
|
383
|
+
const published = await s(a, LATER(8_000));
|
|
384
|
+
const b = await container(h, {}, AT);
|
|
385
|
+
const expired = await s(b, LATER(24 * 3_600_000));
|
|
386
|
+
return empty === 'IN_PROGRESS' && processing.every((x) => x === 'IN_PROGRESS') && finished === 'FINISHED' && published === 'PUBLISHED' && expired === 'EXPIRED';
|
|
387
|
+
})),
|
|
388
|
+
done('instagram.containers.error_on_spec_violation', 'containers', "A file outside the Reels specification leaves the container ERROR with the subcode on `status`: moov after mdat, under 3 s, over 1920 columns, 15 FPS, not H.264/HEVC, not AAC, 64 kHz audio, an average bitrate over 25 Mbps (2207026); a thumb_offset past the end (2207057) — unless a cover_url is set, which Meta uses instead", 'api', 'core', () => withWorld(async (h) => {
|
|
389
|
+
const verdict = async (bytes, extra = {}) => {
|
|
390
|
+
const id = await container(h, extra);
|
|
391
|
+
await upload(h, id, bytes);
|
|
392
|
+
return bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=status_code,status` }));
|
|
393
|
+
};
|
|
394
|
+
const bad = await Promise.all([mp4({ moovFirst: false }), mp4({ ms: 2_000 }), mp4({ w: 2160, h: 3840 }), mp4({ fps: 15 }), mp4({ codec: 'vp09' }), mp4({ audio: { codec: 'Opus', hz: 48000, ch: 2 } }), mp4({ audio: { codec: 'mp4a', hz: 64000, ch: 2 } }), mp4({ ms: 3_000, pad: 10_000_000 }), new TextEncoder().encode('not a video at all')].map((b) => verdict(b)));
|
|
395
|
+
const offset = await verdict(mp4({ ms: 5000 }), { thumb_offset: 5000 });
|
|
396
|
+
const covered = await verdict(mp4({ ms: 5000 }), { thumb_offset: 5000, cover_url: 'https://www.example.com/cover.jpg' });
|
|
397
|
+
const fast = await verdict(mp4({ ms: 5_000, pad: 10_000_000 }));
|
|
398
|
+
return bad.every((v) => v.status_code === 'ERROR' && v.status === '2207026') && offset.status_code === 'ERROR' && offset.status === '2207057' && covered.status_code === 'FINISHED' && fast.status_code === 'FINISHED';
|
|
399
|
+
})),
|
|
400
|
+
done('instagram.containers.accepts_the_spec', 'containers', 'Files inside the specification finish: MP4 or MOV, H.264 or HEVC, 23-60 FPS, silent or AAC mono/stereo up to 48 kHz, 3 s to 15 min, a thumb_offset inside the Reel', 'api', 'core', () => withWorld(async (h) => {
|
|
401
|
+
const fine = [mp4(), mp4({ brand: 'qt ', codec: 'hvc1', fps: 60 }), mp4({ fps: 23.976, audio: null }), mp4({ ms: 3_000, audio: { codec: 'mp4a', hz: 44100, ch: 1 } }), mp4({ ms: 15 * 60_000, w: 720, h: 1280 })];
|
|
402
|
+
const out = [];
|
|
403
|
+
for (const b of fine) {
|
|
404
|
+
const id = await container(h, { thumb_offset: 2999 });
|
|
405
|
+
await upload(h, id, b);
|
|
406
|
+
out.push(String(bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=status_code` })).status_code));
|
|
407
|
+
}
|
|
408
|
+
return out.length === 5 && out.every((s) => s === 'FINISHED');
|
|
409
|
+
})),
|
|
410
|
+
done('instagram.containers.daily_quota', 'containers', 'An account that created 400 containers in the last 24 hours is refused a 401st (code 9 — the twin\'s own code: the Error Codes table gives none for this limit), and may create again once the window moves', 'api', 'common', () => withWorld(async (h, root) => {
|
|
411
|
+
const at = Date.parse(AT) - 3_600_000;
|
|
412
|
+
for (let i = 0; i < 400; i += 1) {
|
|
413
|
+
const c = { id: String(700000000000000000n + BigInt(i)), owner: IG, media_type: 'REELS', upload_type: 'resumable', share_to_feed: true, created_ms: at, received: 0 };
|
|
414
|
+
await writeContainer(c, root);
|
|
415
|
+
}
|
|
416
|
+
const refused = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable' } });
|
|
417
|
+
const later = await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable' }, at: LATER(23 * 3_600_000 + 1) });
|
|
418
|
+
return refused.status === 400 && err(refused).code === 9 && /400 containers/.test(String(err(refused).message)) && later.status === 200 && (await listContainers(root)).length === 401;
|
|
419
|
+
})),
|
|
420
|
+
todo('instagram.containers.copyright_check_status', 'containers', "copyright_check_status on a container (matches_found, status)", 'api', 'niche'),
|
|
421
|
+
todo('instagram.containers.edit_lists', 'containers', 'The "no edit lists" and closed-GOP / progressive / 4:2:0 Reels requirements (the twin reads no edts/elst box or bitstream; most encoders write an edit list, so refusing one unread would refuse files Meta takes)', 'api', 'common'),
|
|
422
|
+
todo('instagram.media.frame_thumbnail', 'media', "A Reel's thumbnail cut from its frame at thumb_offset (the twin decodes nothing and answers a stand-in poster when no cover_url is set)", 'api', 'niche'),
|
|
423
|
+
// ── the rupload host ─────────────────────────────────────────────────────────────────────────
|
|
424
|
+
done('instagram.upload.single_post', 'upload', 'POST rupload /ig-api-upload/{version}/{container} with Authorization: OAuth, offset 0 and file_size carries the whole file: {"success":true,"message":"Upload successful."}, the bytes held content-addressed', 'api', 'core', () => withWorld(async (h, root) => {
|
|
425
|
+
const bytes = mp4();
|
|
426
|
+
const id = await container(h);
|
|
427
|
+
const up = await upload(h, id, bytes);
|
|
428
|
+
const noVersion = await container(h);
|
|
429
|
+
const up2 = await h({ m: 'POST', p: `/ig-api-upload/${noVersion}`, bytes, headers: { ...OAUTH, offset: '0', file_size: String(bytes.length) } });
|
|
430
|
+
const c = await readContainer(id, root);
|
|
431
|
+
return up.status === 200 && JSON.stringify(up.body) === JSON.stringify({ success: true, message: 'Upload successful.' }) && up2.status === 200
|
|
432
|
+
&& c?.sha256 === blobDigest(bytes) && c.size === bytes.length && c.received === bytes.length && c.video?.width === 1080 && c.video.height === 1920 && c.video.durationMs === 5000;
|
|
433
|
+
})),
|
|
434
|
+
done('instagram.upload.resume_from_offset', 'upload', "An upload may arrive in pieces, each POST's offset the first byte it carries: a piece is held (its answer carries no `success`), a POST at any other offset is refused, and the POST that completes the file answers the documented success with the same bytes held", 'api', 'core', () => withWorld(async (h, root) => {
|
|
435
|
+
const bytes = mp4({ pad: 50_000 });
|
|
436
|
+
const id = await container(h);
|
|
437
|
+
const hdr = (offset) => ({ ...OAUTH, offset: String(offset), file_size: String(bytes.length) });
|
|
438
|
+
const first = await h({ m: 'POST', p: `/ig-api-upload/v26.0/${id}`, bytes: bytes.subarray(0, 20_000), headers: hdr(0) });
|
|
439
|
+
const wrong = await h({ m: 'POST', p: `/ig-api-upload/v26.0/${id}`, bytes: bytes.subarray(20_000), headers: hdr(10_000) });
|
|
440
|
+
const status = bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=status_code`, at: LATER(60_000) })).status_code;
|
|
441
|
+
const rest = await h({ m: 'POST', p: `/ig-api-upload/v26.0/${id}`, bytes: bytes.subarray(20_000), headers: hdr(20_000) });
|
|
442
|
+
const c = await readContainer(id, root);
|
|
443
|
+
return first.status === 200 && bodyOf(first).success === undefined && bodyOf(first).offset === 20_000 && wrong.status === 400 && bodyOf(wrong).debug_info?.retriable === false && /resume at offset 20000/.test(String(bodyOf(wrong).debug_info?.message))
|
|
444
|
+
&& status === 'IN_PROGRESS' && JSON.stringify(rest.body) === JSON.stringify({ success: true, message: 'Upload successful.' }) && c?.sha256 === blobDigest(bytes);
|
|
445
|
+
})),
|
|
446
|
+
todo('instagram.upload.offset_discovery', 'upload', "How the rupload host tells a client the bytes it holds after an interrupted POST (undocumented for rupload; the twin answers a GET with the Graph Resumable Upload guide's file_offset, as its own shape)", 'api', 'common'),
|
|
447
|
+
done('instagram.upload.oauth_scheme', 'upload', 'The upload host takes the same token as the Graph API — as Authorization: OAuth (its documented scheme), Bearer or access_token; no token, an unknown one, one without the publishing permissions or another account\'s is "unauthorized user request" and nothing is stored', 'api', 'core', () => withWorld(async (h, root) => {
|
|
448
|
+
await seed(h, { id: OTHER, username: 'other.account', token: OTHER_TOKEN });
|
|
449
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'read-only-token-1', user: IG, permissions: ['instagram_basic', 'pages_read_engagement'] }, headers: {} });
|
|
450
|
+
const bytes = mp4();
|
|
451
|
+
const id = await container(h);
|
|
452
|
+
const carriers = [{}, { authorization: 'OAuth not-a-real-token' }, { authorization: 'OAuth read-only-token-1' }];
|
|
453
|
+
const refused = await Promise.all(carriers.map((a) => upload(h, id, bytes, EARLY, a)));
|
|
454
|
+
const foreign = await upload(h, id, bytes, EARLY, { authorization: `OAuth ${OTHER_TOKEN}` });
|
|
455
|
+
const untouched = (await readContainer(id, root))?.received === 0;
|
|
456
|
+
const byOAuth = await upload(h, id, bytes);
|
|
457
|
+
const bearer = await container(h);
|
|
458
|
+
const byBearer = await upload(h, bearer, bytes, EARLY, AUTH);
|
|
459
|
+
const query = await container(h);
|
|
460
|
+
const byQuery = await h({ m: 'POST', p: `/ig-api-upload/v26.0/${query}?access_token=${TOKEN}`, bytes, headers: { offset: '0', file_size: String(bytes.length) } });
|
|
461
|
+
return refused.every((r) => r.status === 400 && bodyOf(r).debug_info?.retriable === false && /unauthorized user request/.test(String(bodyOf(r).debug_info?.message)))
|
|
462
|
+
&& foreign.status === 400 && /Invalid container id/.test(String(bodyOf(foreign).debug_info?.message)) && untouched && [byOAuth, byBearer, byQuery].every((r) => bodyOf(r).success === true);
|
|
463
|
+
})),
|
|
464
|
+
done('instagram.upload.headers_validated', 'upload', 'file_size is required and at most 300MB (the twin\'s own bound), fixed by the first POST; bytes may not run past it; an empty body and a second upload of a finished file are refused', 'api', 'core', () => withWorld(async (h) => {
|
|
465
|
+
const bytes = mp4();
|
|
466
|
+
const id = await container(h);
|
|
467
|
+
const post = (b, headers) => h({ m: 'POST', p: `/ig-api-upload/v26.0/${id}`, bytes: b, headers: { ...OAUTH, ...headers } });
|
|
468
|
+
const said = (r) => String(bodyOf(r).debug_info?.message ?? '');
|
|
469
|
+
const noSize = await post(bytes, { offset: '0' });
|
|
470
|
+
const huge = await post(bytes, { offset: '0', file_size: '300000001' });
|
|
471
|
+
const past = await post(bytes, { offset: '0', file_size: String(bytes.length - 1) });
|
|
472
|
+
const empty = await post(new Uint8Array(), { offset: '0', file_size: String(bytes.length) });
|
|
473
|
+
await post(bytes.subarray(0, 100), { offset: '0', file_size: String(bytes.length) });
|
|
474
|
+
const resized = await post(bytes.subarray(100), { offset: '100', file_size: String(bytes.length + 5) });
|
|
475
|
+
const done1 = await post(bytes.subarray(100), { offset: '100', file_size: String(bytes.length) });
|
|
476
|
+
const again = await post(bytes, { offset: '0', file_size: String(bytes.length) });
|
|
477
|
+
return /file_size header must be/.test(said(noSize)) && /over the Reels maximum of 300MB/.test(said(huge)) && /run past file_size/.test(said(past)) && /carries no bytes/.test(said(empty))
|
|
478
|
+
&& /differs from the/.test(said(resized)) && bodyOf(done1).success === true && /already received its whole file/.test(said(again));
|
|
479
|
+
})),
|
|
480
|
+
done('instagram.upload.expired_container', 'upload', "An upload to a container 24 hours old is refused, and a GET of an unknown container's offset is refused", 'api', 'common', () => withWorld(async (h) => {
|
|
481
|
+
const id = await container(h, {}, AT);
|
|
482
|
+
const late = await upload(h, id, mp4(), LATER(24 * 3_600_000));
|
|
483
|
+
const unknown = await h({ m: 'GET', p: '/ig-api-upload/v26.0/812345678901234567', headers: OAUTH });
|
|
484
|
+
return late.status === 400 && /expired/.test(String(bodyOf(late).debug_info?.message)) && unknown.status === 400;
|
|
485
|
+
})),
|
|
486
|
+
done('instagram.upload.file_url_refused', 'upload', 'Uploading from a hosted file_url (Meta fetching it) is refused by name, never answered as a success', 'api', 'common', () => withWorld(async (h, root) => {
|
|
487
|
+
const id = await container(h);
|
|
488
|
+
const r = await h({ m: 'POST', p: `/ig-api-upload/v26.0/${id}`, headers: { ...OAUTH, file_url: 'https://example_hosted_video.com/v.mp4' } });
|
|
489
|
+
return r.status === 422 && /\[twin gap\]/.test(String(err(r).message)) && (await readContainer(id, root))?.received === 0;
|
|
490
|
+
})),
|
|
491
|
+
// ── media_publish ────────────────────────────────────────────────────────────────────────────
|
|
492
|
+
done('instagram.publish.reel', 'publish', 'media_publish with a FINISHED container answers {id}; the media reads media_type VIDEO, media_product_type REELS, the caption, timestamp, shortcode, the /reel/ permalink, is_shared_to_feed, owner and username', 'api', 'core', () => withWorld(async (h) => {
|
|
493
|
+
const id = await publishReel(h, { caption: 'Out now #release @volter', share_to_feed: false });
|
|
494
|
+
const m = bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=id,media_type,media_product_type,caption,timestamp,shortcode,permalink,is_shared_to_feed,owner,username,comments_count,like_count` }));
|
|
495
|
+
return /^9\d{17}$/.test(id) && m.id === id && m.media_type === 'VIDEO' && m.media_product_type === 'REELS' && m.caption === 'Out now #release @volter'
|
|
496
|
+
&& m.timestamp === '2026-09-27T12:00:00+0000' && /^[A-Za-z0-9_-]{11}$/.test(m.shortcode) && m.permalink === `https://www.instagram.com/reel/${m.shortcode}/`
|
|
497
|
+
&& m.is_shared_to_feed === false && m.owner?.id === IG && m.username === 'metricsaurus' && m.comments_count === 0 && m.like_count === 0;
|
|
498
|
+
})),
|
|
499
|
+
done('instagram.publish.refusals', 'publish', 'Publishing a container IN_PROGRESS is 9007/2207027, in ERROR -1/2207032, EXPIRED -2/2207020; an unknown, another account\'s or an already-published container is 24/2207008; no creation_id is (#100)', 'api', 'core', () => withWorld(async (h) => {
|
|
500
|
+
const pub = (id, at = AT) => h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: id }, at });
|
|
501
|
+
const waiting = await container(h, {}, AT);
|
|
502
|
+
const failed = await container(h);
|
|
503
|
+
await upload(h, failed, mp4({ ms: 1000 }));
|
|
504
|
+
const old = await container(h, {}, AT);
|
|
505
|
+
const done1 = await container(h);
|
|
506
|
+
await upload(h, done1, mp4());
|
|
507
|
+
await pub(done1);
|
|
508
|
+
await seed(h, { id: OTHER, username: 'other.account', token: OTHER_TOKEN });
|
|
509
|
+
const theirs = String(bodyOf(await h({ m: 'POST', p: `/v26.0/${OTHER}/media`, b: { media_type: 'REELS', upload_type: 'resumable' }, headers: { authorization: `Bearer ${OTHER_TOKEN}` } })).id);
|
|
510
|
+
const r = [await pub(waiting), await pub(failed), await pub(old, LATER(24 * 3_600_000)), await pub('812345678901234567'), await pub(theirs), await pub(done1)];
|
|
511
|
+
const none = await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: {} });
|
|
512
|
+
const codes = r.map((x) => `${err(x).code}/${err(x).error_subcode}`);
|
|
513
|
+
return JSON.stringify(codes) === JSON.stringify(['9007/2207027', '-1/2207032', '-2/2207020', '24/2207008', '24/2207008', '24/2207008']) && none.status === 400 && err(none).code === 100;
|
|
514
|
+
})),
|
|
515
|
+
done('instagram.publish.daily_limit', 'publish', "An account that API-published 50 posts in a moving 24 hours is refused a 51st with 9/2207042 — a deleted one still counts, a Reel pulled by a refresh (perhaps posted in the app) does not — and may publish once the oldest leaves the window", 'api', 'core', () => withWorld(async (h, root) => {
|
|
516
|
+
const ids = [];
|
|
517
|
+
for (let i = 0; i < 49; i += 1)
|
|
518
|
+
ids.push(await publishReel(h, { caption: `n${i}` }));
|
|
519
|
+
await h({ m: 'DELETE', p: `/v26.0/${ids[0]}` });
|
|
520
|
+
for (let i = 0; i < 5; i += 1) {
|
|
521
|
+
observeResource('instagram', { type: 'media', id: String(17900000000000000n + BigInt(i)), fields: { media_type: 'VIDEO', media_product_type: 'REELS', owner: { id: IG }, timestamp: '2026-09-27T11:00:00+0000' } }, { root, at: '2026-09-27T11:30:00.000Z' });
|
|
522
|
+
}
|
|
523
|
+
const fiftieth = await publishReel(h, { caption: 'fiftieth' });
|
|
524
|
+
const id = await container(h);
|
|
525
|
+
await upload(h, id, mp4());
|
|
526
|
+
const refused = await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: id } });
|
|
527
|
+
// a day on, the fifty have left the moving window (a fresh container: the refused one has expired)
|
|
528
|
+
const next = await container(h, {}, LATER(23 * 3_600_000));
|
|
529
|
+
await upload(h, next, mp4(), LATER(23 * 3_600_000));
|
|
530
|
+
const later = await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: next }, at: LATER(24 * 3_600_000) });
|
|
531
|
+
return /^9\d{17}$/.test(fiftieth) && refused.status === 400 && err(refused).code === 9 && err(refused).error_subcode === 2207042 && later.status === 200;
|
|
532
|
+
})),
|
|
533
|
+
done('instagram.publish.ids_minted_from_state', 'publish', 'Media ids are a digest of the write, 18 digits starting 9 (above the real id space): two fresh worlds replaying the same writes mint the same id, and a delete-then-publish never reissues one', 'api', 'core', async () => {
|
|
534
|
+
const run = async () => {
|
|
535
|
+
const root = mkdtempSync(join(tmpdir(), 'instagram-cap-ids-'));
|
|
536
|
+
try {
|
|
537
|
+
const h = handleFor(root);
|
|
538
|
+
await seed(h);
|
|
539
|
+
const first = await publishReel(h, { caption: 'same' });
|
|
540
|
+
await h({ m: 'DELETE', p: `/v26.0/${first}` });
|
|
541
|
+
const second = await publishReel(h, { caption: 'same' });
|
|
542
|
+
return [first, second];
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
rmSync(root, { recursive: true, force: true });
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const [a, b] = [await run(), await run()];
|
|
549
|
+
return a[0] !== a[1] && /^9\d{17}$/.test(a[0]) && /^9\d{17}$/.test(a[1]) && JSON.stringify(a) === JSON.stringify(b);
|
|
550
|
+
}),
|
|
551
|
+
// ── the IG Media node and the media edge ─────────────────────────────────────────────────────
|
|
552
|
+
done('instagram.media.list_newest_first', 'media', "GET /{ig-user-id}/media lists the account's media newest first, id only by default, the fields asked for otherwise", 'api', 'core', () => withWorld(async (h) => {
|
|
553
|
+
const a = await publishReel(h, { caption: 'first' });
|
|
554
|
+
const bId = await container(h, { caption: 'second' }, LATER(1_000));
|
|
555
|
+
await upload(h, bId, mp4(), LATER(1_000));
|
|
556
|
+
const b = String(bodyOf(await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: bId }, at: LATER(60_000) })).id);
|
|
557
|
+
const bare = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media`, at: LATER(61_000) }));
|
|
558
|
+
const rich = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media?fields=caption,media_product_type,username`, at: LATER(61_000) }));
|
|
559
|
+
return JSON.stringify(bare.data) === JSON.stringify([{ id: b }, { id: a }]) && rich.data[0].caption === 'second' && rich.data[1].caption === 'first'
|
|
560
|
+
&& rich.data.every((m) => m.media_product_type === 'REELS' && m.username === 'metricsaurus');
|
|
561
|
+
})),
|
|
562
|
+
done('instagram.media.list_paging', 'media', 'limit pages the list with cursors and a next link that carries no token; after= reads the next page; a cursor the twin never issued is (#100); since/until are refused by name', 'api', 'common', () => withWorld(async (h) => {
|
|
563
|
+
const ids = [await publishReel(h, { caption: '1' }), await publishReel(h, { caption: '2' }, mp4({ ms: 4000 })), await publishReel(h, { caption: '3' }, mp4({ ms: 6000 }))];
|
|
564
|
+
const p1 = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media?limit=2&access_token=${TOKEN}`, headers: {} }));
|
|
565
|
+
const p2 = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media?limit=2&after=${encodeURIComponent(p1.paging.cursors.after)}` }));
|
|
566
|
+
const bad = await h({ m: 'GET', p: `/v26.0/${IG}/media?after=bm90LWEtY3Vyc29y` });
|
|
567
|
+
const since = await h({ m: 'GET', p: `/v26.0/${IG}/media?since=1700000000` });
|
|
568
|
+
const all = [...p1.data, ...p2.data].map((m) => m.id);
|
|
569
|
+
return p1.data.length === 2 && typeof p1.paging.next === 'string' && !p1.paging.next.includes(TOKEN) && /after=/.test(p1.paging.next) && p2.data.length === 1 && p2.paging.next === undefined
|
|
570
|
+
&& new Set(all).size === 3 && ids.every((i) => all.includes(i)) && bad.status === 400 && since.status === 422;
|
|
571
|
+
})),
|
|
572
|
+
done('instagram.media.get_fields', 'media', 'GET /{ig-media-id} answers id alone by default, the fields asked for, media_url and thumbnail_url on the twin\'s base; a field the node lacks is (#100), a documented one unmodelled is refused by name', 'api', 'core', () => withWorld(async (h) => {
|
|
573
|
+
const id = await publishReel(h);
|
|
574
|
+
const bare = bodyOf(await h({ m: 'GET', p: `/v26.0/${id}` }));
|
|
575
|
+
const urls = bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=media_url,thumbnail_url`, base: 'http://127.0.0.1:4400/w/instagram' }));
|
|
576
|
+
const bogus = await h({ m: 'GET', p: `/v26.0/${id}?fields=likes_by_dinos` });
|
|
577
|
+
const views = await h({ m: 'GET', p: `/v26.0/${id}?fields=view_count` });
|
|
578
|
+
return JSON.stringify(bare) === JSON.stringify({ id }) && urls.media_url === `http://127.0.0.1:4400/w/instagram/o1/v/t16/f2/m86/${blobDigest(mp4())}.mp4`
|
|
579
|
+
&& /\/v\/t51\.71878-15\/[0-9a-f]{64}_poster\.svg$/.test(urls.thumbnail_url) && bogus.status === 400 && views.status === 422;
|
|
580
|
+
})),
|
|
581
|
+
done('instagram.media.cover_url_is_thumbnail', 'media', "A Reel created with cover_url answers that URL as its thumbnail_url (the twin fetches nothing, so it is not rehosted)", 'api', 'common', () => withWorld(async (h) => {
|
|
582
|
+
const id = await publishReel(h, { cover_url: 'https://www.example.com/cover.jpg' });
|
|
583
|
+
return bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=thumbnail_url` })).thumbnail_url === 'https://www.example.com/cover.jpg';
|
|
584
|
+
})),
|
|
585
|
+
done('instagram.media.bytes_served_with_range', 'media', "media_url serves the uploaded bytes on the twin's own base: the whole file identical, Range 206, an open range capped at 8 MiB, suffix ranges, 416 past the end, 404 for a digest no live media names", 'api', 'core', async () => {
|
|
586
|
+
const root = mkdtempSync(join(tmpdir(), 'instagram-cap-bytes-'));
|
|
587
|
+
try {
|
|
588
|
+
const { createInstagramTwinFetch } = await import("./instagram-server.js");
|
|
589
|
+
const fetcher = createInstagramTwinFetch({ root });
|
|
590
|
+
const h = handleFor(root);
|
|
591
|
+
await seed(h);
|
|
592
|
+
const bytes = mp4({ pad: 9 * 1024 * 1024 });
|
|
593
|
+
const id = await publishReel(h, {}, bytes);
|
|
594
|
+
const url = new URL(String(bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=media_url`, base: 'http://twin.test' })).media_url));
|
|
595
|
+
const get = (range) => fetcher(new Request(`http://twin.test${url.pathname}`, { headers: range ? { range } : {} }));
|
|
596
|
+
const whole = new Uint8Array(await (await get()).arrayBuffer());
|
|
597
|
+
const part = await get('bytes=100-199');
|
|
598
|
+
const partBytes = new Uint8Array(await part.arrayBuffer());
|
|
599
|
+
const open = await get('bytes=10-');
|
|
600
|
+
const suffix = await get('bytes=-50');
|
|
601
|
+
const past = await get(`bytes=${bytes.length}-`);
|
|
602
|
+
const other = await fetcher(new Request(`http://twin.test/o1/v/t16/f2/m86/${'1'.repeat(64)}.mp4`));
|
|
603
|
+
await h({ m: 'DELETE', p: `/v26.0/${id}` });
|
|
604
|
+
const deleted = await get();
|
|
605
|
+
return whole.length === bytes.length && whole.every((b, i) => b === bytes[i]) && part.status === 206 && part.headers.get('content-range') === `bytes 100-199/${bytes.length}`
|
|
606
|
+
&& partBytes.every((b, i) => b === bytes[100 + i]) && open.status === 206 && open.headers.get('content-range') === `bytes 10-${10 + 8 * 1024 * 1024 - 1}/${bytes.length}`
|
|
607
|
+
&& suffix.status === 206 && Number(suffix.headers.get('content-length')) === 50 && past.status === 416 && other.status === 404 && deleted.status === 404;
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
rmSync(root, { recursive: true, force: true });
|
|
611
|
+
}
|
|
612
|
+
}),
|
|
613
|
+
done('instagram.media.delete', 'media', "DELETE /{ig-media-id} answers {success, deleted_id}, the media leaves every read; it needs instagram_manage_contents, and another account's media is refused", 'api', 'common', () => withWorld(async (h) => {
|
|
614
|
+
const id = await publishReel(h);
|
|
615
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'no-manage-token', user: IG, permissions: ['instagram_basic', 'instagram_content_publish'] }, headers: {} });
|
|
616
|
+
const denied = await h({ m: 'DELETE', p: `/v26.0/${id}`, headers: { authorization: 'Bearer no-manage-token' } });
|
|
617
|
+
await seed(h, { id: OTHER, username: 'other.account', token: OTHER_TOKEN });
|
|
618
|
+
const foreign = await h({ m: 'DELETE', p: `/v26.0/${id}`, headers: { authorization: `Bearer ${OTHER_TOKEN}` } });
|
|
619
|
+
const del = bodyOf(await h({ m: 'DELETE', p: `/v26.0/${id}` }));
|
|
620
|
+
const gone = await h({ m: 'GET', p: `/v26.0/${id}` });
|
|
621
|
+
const list = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media` }));
|
|
622
|
+
return denied.status === 400 && err(denied).code === 10 && foreign.status === 400 && del.success === true && del.deleted_id === id && gone.status === 400 && err(gone).error_subcode === 33 && list.data.length === 0;
|
|
623
|
+
})),
|
|
624
|
+
done('instagram.media.kernel_tombstone', 'media', 'A media the kernel tombstones (an observed `deleted`) is gone from the node and the list', 'api', 'common', () => withWorld(async (h, root) => {
|
|
625
|
+
const id = '17900000000000077';
|
|
626
|
+
const f = { media_type: 'VIDEO', media_product_type: 'REELS', owner: { id: IG }, caption: 'pulled', timestamp: '2026-09-27T09:00:00+0000', media_url: 'https://scontent.cdninstagram.com/o1/v/x.mp4' };
|
|
627
|
+
observeResource('instagram', { type: 'media', id, fields: f }, { root, at: '2026-09-27T11:00:00.000Z' });
|
|
628
|
+
const before = bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=caption,media_url` }));
|
|
629
|
+
observeResource('instagram', { type: 'media', id, fields: { ...f, deleted: true } }, { root, at: '2026-09-27T11:30:00.000Z' });
|
|
630
|
+
const after = await h({ m: 'GET', p: `/v26.0/${id}` });
|
|
631
|
+
const list = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media` }));
|
|
632
|
+
return before.caption === 'pulled' && before.media_url === f.media_url && after.status === 400 && list.data.length === 0;
|
|
633
|
+
})),
|
|
634
|
+
todo('instagram.media.comment_enabled', 'media', 'Enable or disable comments on a media (POST /{ig-media-id}?comment_enabled=)', 'api', 'common'),
|
|
635
|
+
todo('instagram.media.engagement_counts', 'media', 'view_count, reposts_count, saved_count, shares_count and the total_* counts', 'api', 'common'),
|
|
636
|
+
todo('instagram.media.children', 'media', 'The children edge of a carousel album', 'api', 'niche'),
|
|
637
|
+
todo('instagram.media.copyright_check_information', 'media', 'copyright_check_information on a published video', 'api', 'niche'),
|
|
638
|
+
todo('instagram.media.media_audio_type', 'media', 'media_audio_type (MUSIC or ORIGINAL_SOUND) and the audio_name rename', 'api', 'niche'),
|
|
639
|
+
todo('instagram.media.boost', 'media', 'boost_ads_list and boost_eligibility_info', 'api', 'niche'),
|
|
640
|
+
// ── the publishing limit ─────────────────────────────────────────────────────────────────────
|
|
641
|
+
done('instagram.limits.content_publishing_limit', 'limits', "GET /{ig-user-id}/content_publishing_limit answers {data:[{quota_usage}]} by default — the API publishes in the last 24 hours, a deleted one included, a pulled Reel not — and config {quota_total 50, quota_duration 86400} when asked (or for the reference example's rate_limit_settings); since counts from a timestamp no older than 24 hours", 'api', 'core', () => withWorld(async (h, root) => {
|
|
642
|
+
const a = await publishReel(h);
|
|
643
|
+
await publishReel(h, { caption: 'two' });
|
|
644
|
+
await h({ m: 'DELETE', p: `/v26.0/${a}` });
|
|
645
|
+
for (const id of ['17900000000000008', '17900000000000009'])
|
|
646
|
+
observeResource('instagram', { type: 'media', id, fields: { media_type: 'VIDEO', owner: { id: IG }, timestamp: '2026-09-27T11:59:00+0000' } }, { root, at: '2026-09-27T11:59:30.000Z' });
|
|
647
|
+
const plain = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/content_publishing_limit`, at: LATER(1_000) }));
|
|
648
|
+
const full = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/content_publishing_limit?fields=quota_usage,config`, at: LATER(1_000) }));
|
|
649
|
+
const example = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/content_publishing_limit?fields=quota_usage,rate_limit_settings`, at: LATER(1_000) }));
|
|
650
|
+
const since = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/content_publishing_limit?since=${Math.floor(Date.parse(AT) / 1000) + 1}`, at: LATER(5_000) }));
|
|
651
|
+
const old = await h({ m: 'GET', p: `/v26.0/${IG}/content_publishing_limit?since=${Math.floor(Date.parse(AT) / 1000) - 2 * 86_400}` });
|
|
652
|
+
const config = { quota_total: 50, quota_duration: 86400 };
|
|
653
|
+
return JSON.stringify(plain) === JSON.stringify({ data: [{ quota_usage: 2 }] }) && JSON.stringify(full) === JSON.stringify({ data: [{ quota_usage: 2, config }] })
|
|
654
|
+
&& JSON.stringify(example) === JSON.stringify({ data: [{ quota_usage: 2, config }] }) && since.data[0].quota_usage === 0 && old.status === 400;
|
|
655
|
+
})),
|
|
656
|
+
// ── the rest of content publishing (the denominator) ─────────────────────────────────────────
|
|
657
|
+
todo('instagram.publishing.image_posts', 'publishing', 'Single image posts (image_url containers: JPEG, 8 MB, 4:5 to 1.91:1, alt_text)', 'api', 'core'),
|
|
658
|
+
todo('instagram.publishing.video_url_reels', 'publishing', 'Reels from a hosted video_url (Meta fetching the file), and the rupload file_url header', 'api', 'common'),
|
|
659
|
+
todo('instagram.publishing.carousels', 'publishing', 'Carousel posts (CAROUSEL containers of up to 10 image/video children)', 'api', 'common'),
|
|
660
|
+
todo('instagram.publishing.stories', 'publishing', 'Image and video stories (STORIES containers)', 'api', 'common'),
|
|
661
|
+
todo('instagram.publishing.trial_reels', 'publishing', 'Trial reels (trial_params graduation_strategy MANUAL / SS_PERFORMANCE)', 'api', 'niche'),
|
|
662
|
+
todo('instagram.publishing.tags_and_collaborators', 'publishing', 'user_tags, collaborators and location_id on a post', 'api', 'common'),
|
|
663
|
+
todo('instagram.publishing.product_tags', 'publishing', 'product_tags (shopping) on image and video posts', 'api', 'niche'),
|
|
664
|
+
todo('instagram.publishing.branded_content', 'publishing', 'Partnership ads labels (branded_content_sponsor_ids, is_paid_partnership)', 'api', 'niche'),
|
|
665
|
+
todo('instagram.publishing.cover_fetch', 'publishing', "Fetching a Reel's cover_url and holding it to the cover spec (JPEG, 8 MB), rehosted on the CDN", 'api', 'niche'),
|
|
666
|
+
todo('instagram.comments.read', 'comments', 'The comments edge on a media, and comment replies', 'api', 'common'),
|
|
667
|
+
todo('instagram.comments.moderate', 'comments', 'Reply to, hide and delete comments', 'api', 'common'),
|
|
668
|
+
todo('instagram.insights.media', 'insights', 'Media insights (reach, plays, saved, shares, total_interactions for Reels)', 'api', 'common'),
|
|
669
|
+
todo('instagram.insights.account', 'insights', 'Account insights (reach, profile views, follower demographics)', 'api', 'common'),
|
|
670
|
+
todo('instagram.discovery.hashtag_search', 'discovery', 'Hashtag search (ig_hashtag_search, top_media, recent_media)', 'api', 'niche'),
|
|
671
|
+
todo('instagram.webhooks.subscriptions', 'webhooks', 'Webhooks for comments, mentions, story insights and live comments', 'connector', 'common'),
|
|
672
|
+
todo('instagram.login.oauth', 'login', 'Business Login for Instagram: the authorize page, the code-for-token exchange, long-lived tokens and their refresh', 'api', 'common'),
|
|
673
|
+
todo('instagram.messaging', 'login', 'Instagram messaging through the Messenger Platform (a separate Meta product, one line in this denominator)', 'api', 'niche'),
|
|
674
|
+
// ── errors ───────────────────────────────────────────────────────────────────────────────────
|
|
675
|
+
done('instagram.errors.envelope_shape', 'errors', 'Every Graph refusal is {error:{message, type, code, is_transient, fbtrace_id}}, and the same refusal answers the same fbtrace_id', 'api', 'common', () => withWorld(async (h) => {
|
|
676
|
+
const rs = [await h({ m: 'GET', p: `/v26.0/${IG}`, headers: {} }), await h({ m: 'GET', p: '/v26.0/17841499999999999' }), await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'GIF' } })];
|
|
677
|
+
const again = await h({ m: 'GET', p: '/v26.0/17841499999999999' });
|
|
678
|
+
return rs.every((r) => { const e = err(r); return typeof e.message === 'string' && typeof e.type === 'string' && typeof e.code === 'number' && e.is_transient === false && /^[A-Za-z0-9_-]{11}$/.test(e.fbtrace_id); })
|
|
679
|
+
&& err(again).fbtrace_id === err(rs[1]).fbtrace_id;
|
|
680
|
+
})),
|
|
681
|
+
done('instagram.errors.unknown_node_and_edge', 'errors', "An unknown node is Unsupported get request (100/33); an unknown edge is a nonexisting field; a documented edge the twin does not model is refused by name; a deeper path is Unknown path components (2500)", 'api', 'core', () => withWorld(async (h) => {
|
|
682
|
+
const node = await h({ m: 'GET', p: '/v26.0/17841499999999999' });
|
|
683
|
+
const edge = await h({ m: 'GET', p: `/v26.0/${IG}/dinosaurs` });
|
|
684
|
+
const stories = await h({ m: 'GET', p: `/v26.0/${IG}/stories` });
|
|
685
|
+
const deep = await h({ m: 'GET', p: `/v26.0/${IG}/media/extra` });
|
|
686
|
+
const post = await h({ m: 'POST', p: `/v26.0/${IG}`, b: {} });
|
|
687
|
+
return err(node).code === 100 && err(node).error_subcode === 33 && /nonexisting field \(dinosaurs\)/.test(String(err(edge).message)) && stories.status === 422 && err(deep).code === 2500 && err(post).error_subcode === 33;
|
|
688
|
+
})),
|
|
689
|
+
done('instagram.errors.malformed_body', 'errors', 'A JSON body that is not an object is (#100), and nothing is staged', 'api', 'common', () => withWorld(async (h, root) => {
|
|
690
|
+
const r = await handleInstagramTwinRequest({ method: 'POST', path: `/v26.0/${IG}/media`, headers: { ...AUTH, 'content-type': 'application/json' }, body: '[1,2', root, occurredAt: AT });
|
|
691
|
+
return r.status === 400 && r.body.error?.code === 100 && (await listContainers(root)).length === 0;
|
|
692
|
+
})),
|
|
693
|
+
done('instagram.errors.read_only', 'errors', 'A read-only twin refuses every write — a container, an upload, a publish, a delete, a seed — with 405 and still serves reads', 'api', 'common', () => withWorld(async (h) => {
|
|
694
|
+
const id = await publishReel(h);
|
|
695
|
+
const c = await container(h);
|
|
696
|
+
const w = [
|
|
697
|
+
await h({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable' }, readOnly: true }),
|
|
698
|
+
await h({ m: 'POST', p: `/ig-api-upload/v26.0/${c}`, bytes: mp4(), headers: { ...OAUTH, offset: '0', file_size: '1' }, readOnly: true }),
|
|
699
|
+
await h({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: c }, readOnly: true }),
|
|
700
|
+
await h({ m: 'DELETE', p: `/v26.0/${id}`, readOnly: true }),
|
|
701
|
+
await h({ m: 'POST', p: '/_twin/users', b: { id: '1', username: 'x' }, headers: {}, readOnly: true }),
|
|
702
|
+
];
|
|
703
|
+
const read = await h({ m: 'GET', p: `/v26.0/${id}?fields=caption`, readOnly: true });
|
|
704
|
+
return w.every((r) => r.status === 405) && read.status === 200;
|
|
705
|
+
})),
|
|
706
|
+
todo('instagram.errors.message_wording', 'errors', "Meta's exact message wording where the Error Codes table gives none (the token errors, the parameter errors)", 'api', 'niche'),
|
|
707
|
+
todo('instagram.errors.rate_limited', 'errors', 'Business Use Case throttling answered as codes 4 / 17 / 32 / 613 with X-Business-Use-Case-Usage headers', 'api', 'common'),
|
|
708
|
+
// ── the twin control plane ───────────────────────────────────────────────────────────────────
|
|
709
|
+
done('instagram.twin_control.seed_and_validate', 'twin_control', 'A world seeds an account and a token; malformed seeds (a non-numeric id, a bad or taken username, a token for an unknown account, an undocumented permission) are refused', 'api', 'common', () => withWorld(async (h) => {
|
|
710
|
+
const bad = await Promise.all([
|
|
711
|
+
h({ m: 'POST', p: '/_twin/users', b: { id: 'abc', username: 'x' }, headers: {} }),
|
|
712
|
+
h({ m: 'POST', p: '/_twin/users', b: { id: '1', username: 'Has Spaces' }, headers: {} }),
|
|
713
|
+
h({ m: 'POST', p: '/_twin/users', b: { id: '2', username: 'metricsaurus' }, headers: {} }),
|
|
714
|
+
h({ m: 'POST', p: '/_twin/users', b: { id: '3', username: 'pic', profile_picture: btoa('not an image') }, headers: {} }),
|
|
715
|
+
h({ m: 'POST', p: '/_twin/tokens', b: { token: 'token-for-nobody', user: '999', permissions: [] }, headers: {} }),
|
|
716
|
+
h({ m: 'POST', p: '/_twin/tokens', b: { token: 'token-bad-perm', user: IG, permissions: ['instagram_everything'] }, headers: {} }),
|
|
717
|
+
]);
|
|
718
|
+
const good = await h({ m: 'POST', p: '/_twin/users', b: { id: '17841400000000999', username: 'new.one' }, headers: {} });
|
|
719
|
+
return bad.every((r) => r.status === 400 && err(r).code === 100) && good.status === 200;
|
|
720
|
+
})),
|
|
721
|
+
// ── connector (the real state system, over an in-process vendor) ─────────────────────────────
|
|
722
|
+
done('instagram.connector.perform_reel', 'connector', "performInstagramAction crosses a Reel as Meta documents it — the container, the bytes to the rupload uri as an absolute URL on the declared host, the status wait, media_publish — and the vendor holds the Reel with byte-identical video", 'connector', 'core', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
723
|
+
const bytes = mp4({ pad: 20_000 });
|
|
724
|
+
const media = await (async () => { const id = await container(local, { caption: 'Release day #volter', share_to_feed: true, thumb_offset: 1500, audio_name: 'Volter theme' }); await upload(local, id, bytes); return String(bodyOf(await local({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: id } })).id); })();
|
|
725
|
+
const calls = [];
|
|
726
|
+
const waits = [];
|
|
727
|
+
const out = await performInstagramAction(vendorExecutor(vendorRoot, calls), entryFor(localRoot, media, 'instagram.media.publish'), ctxFor(localRoot), async (ms) => { waits.push(ms); });
|
|
728
|
+
const vendor = handleFor(vendorRoot, '2026-09-27T14:00:00.000Z');
|
|
729
|
+
const vAuth = { authorization: 'Bearer vendor-token-0002' };
|
|
730
|
+
const got = bodyOf(await vendor({ m: 'GET', p: `/v26.0/${out.externalId}?fields=caption,media_product_type,permalink,is_shared_to_feed,media_url`, headers: vAuth }));
|
|
731
|
+
const sha = /\/([0-9a-f]{64})\.mp4$/.exec(String(got.media_url))?.[1] ?? '';
|
|
732
|
+
const up = calls.find((c) => c.path.includes('/ig-api-upload/'));
|
|
733
|
+
const vc = await readContainer(String(out.data?._vendor_container), vendorRoot);
|
|
734
|
+
return /^9\d{17}$/.test(out.externalId) && out.externalId !== media && out.url === got.permalink && got.caption === 'Release day #volter' && got.media_product_type === 'REELS'
|
|
735
|
+
&& got.is_shared_to_feed === true && sha === blobDigest(bytes) && up?.absolute === true && up.path.startsWith('https://rupload.facebook.com/ig-api-upload/v26.0/') && up.offset === '0'
|
|
736
|
+
&& calls.every((c) => !c.own) && JSON.stringify(waits) === JSON.stringify([60_000]) && vc?.thumb_offset === 1500 && vc.audio_name === 'Volter theme'
|
|
737
|
+
&& JSON.stringify(calls.map((c) => c.method)) === JSON.stringify(['POST', 'POST', 'GET', 'GET', 'POST', 'GET']);
|
|
738
|
+
})),
|
|
739
|
+
done('instagram.connector.anchored_upload_for_a_twin_vendor', 'connector', "A twin standing as the vendor mints its rupload uri under its own base — the root's origin — so the upload goes as the anchored /ig-api-upload/… path the root's base prefixes", 'connector', 'core', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
740
|
+
const media = await publishReel(local);
|
|
741
|
+
const calls = [];
|
|
742
|
+
const out = await performInstagramAction(vendorExecutor(vendorRoot, calls, { base: 'http://127.0.0.1:4400/acme/reality/instagram' }), entryFor(localRoot, media, 'instagram.media.publish'), ctxFor(localRoot));
|
|
743
|
+
const up = calls.find((c) => c.path.includes('/ig-api-upload/'));
|
|
744
|
+
return /^\d+$/.test(out.externalId) && up?.absolute === false && /^\/ig-api-upload\/v26\.0\/\d+$/.test(up.path)
|
|
745
|
+
&& uploadTarget('https://rupload.facebook.com/ig-api-upload/v26.0/1', '1') === 'https://rupload.facebook.com/ig-api-upload/v26.0/1'
|
|
746
|
+
&& uploadTarget(undefined, '5') === 'https://rupload.facebook.com/ig-api-upload/v26.0/5'
|
|
747
|
+
&& ['https://rupload.facebook.com:8443/ig-api-upload/v26.0/1', 'http://rupload.facebook.com/ig-api-upload/v26.0/1', 'https://u:p@rupload.facebook.com/ig-api-upload/v26.0/1'].every((u) => { try {
|
|
748
|
+
uploadTarget(u, '1');
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
return true;
|
|
753
|
+
} });
|
|
754
|
+
})),
|
|
755
|
+
done('instagram.connector.retry_keeps_the_container', 'connector', "A perform whose publish failed keeps the vendor's container; its retry publishes that container with no second container and no second upload", 'connector', 'core', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
756
|
+
const media = await publishReel(local);
|
|
757
|
+
const entry = entryFor(localRoot, media, 'instagram.media.publish');
|
|
758
|
+
const calls = [];
|
|
759
|
+
const inner = vendorExecutor(vendorRoot, calls);
|
|
760
|
+
let fail = true;
|
|
761
|
+
const flaky = async (request) => {
|
|
762
|
+
if (request.path.endsWith('/media_publish') && fail) {
|
|
763
|
+
fail = false;
|
|
764
|
+
return { status: 500, headers: {}, body: '{"error":{"message":"An unknown error occurred","type":"OAuthException","code":1,"is_transient":true,"fbtrace_id":"x"}}' };
|
|
765
|
+
}
|
|
766
|
+
return inner(request);
|
|
767
|
+
};
|
|
768
|
+
let first = '';
|
|
769
|
+
// the local clock eight days AHEAD of the vendor's: the kept container's lifetime is counted locally
|
|
770
|
+
const ahead = () => Date.parse('2026-10-05T13:00:00.000Z');
|
|
771
|
+
try {
|
|
772
|
+
await performInstagramAction(flaky, entry, ctxFor(localRoot), noWait, ahead);
|
|
773
|
+
}
|
|
774
|
+
catch (e) {
|
|
775
|
+
first = String(e.message);
|
|
776
|
+
}
|
|
777
|
+
const before = calls.filter((c) => c.path.endsWith('/media') && c.method === 'POST').length;
|
|
778
|
+
const out = await performInstagramAction(flaky, entry, ctxFor(localRoot), noWait, ahead);
|
|
779
|
+
const containers = calls.filter((c) => c.path.endsWith('/media') && c.method === 'POST').length;
|
|
780
|
+
const uploads = calls.filter((c) => c.path.includes('/ig-api-upload/')).length;
|
|
781
|
+
const listed = bodyOf(await handleFor(vendorRoot, '2026-09-27T14:00:00.000Z')({ m: 'GET', p: `/v26.0/${IG}/media`, headers: { authorization: 'Bearer vendor-token-0002' } }));
|
|
782
|
+
return /HTTP 500/.test(first) && before === 1 && containers === 1 && uploads === 1 && listed.data.length === 1 && listed.data[0].id === out.externalId;
|
|
783
|
+
})),
|
|
784
|
+
done('instagram.connector.status_wait_capped', 'connector', 'A container Meta keeps IN_PROGRESS is read at 0, 60 and 120 seconds and no longer; the perform then fails retryably (a plain failure, not a refusal) and keeps the container for the retry', 'connector', 'core', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
785
|
+
const media = await publishReel(local);
|
|
786
|
+
const entry = entryFor(localRoot, media, 'instagram.media.publish');
|
|
787
|
+
const inner = vendorExecutor(vendorRoot, []);
|
|
788
|
+
let reads = 0;
|
|
789
|
+
const slow = async (request) => {
|
|
790
|
+
const res = await inner(request);
|
|
791
|
+
if (request.method === 'GET' && /fields=status_code/.test(request.path)) {
|
|
792
|
+
reads += 1;
|
|
793
|
+
return { ...res, body: JSON.stringify({ ...JSON.parse(res.body), status_code: 'IN_PROGRESS' }) };
|
|
794
|
+
}
|
|
795
|
+
return res;
|
|
796
|
+
};
|
|
797
|
+
const waits = [];
|
|
798
|
+
let error;
|
|
799
|
+
try {
|
|
800
|
+
await performInstagramAction(slow, entry, ctxFor(localRoot), async (ms) => { waits.push(ms); });
|
|
801
|
+
}
|
|
802
|
+
catch (e) {
|
|
803
|
+
error = e;
|
|
804
|
+
}
|
|
805
|
+
const { readPerformContainer } = await import("./instagram-media.js");
|
|
806
|
+
const kept = await readPerformContainer(entry.id, localRoot);
|
|
807
|
+
return reads === 3 && JSON.stringify(waits) === JSON.stringify([60_000, 60_000]) && waits.reduce((a, b) => a + b, 0) === MAX_STATUS_WAIT_MS
|
|
808
|
+
&& error instanceof Error && error.name === 'Error' && /still IN_PROGRESS after 120s; kept/.test(error.message) && kept !== null;
|
|
809
|
+
})),
|
|
810
|
+
done('instagram.connector.lost_publish_recovered', 'connector', "A publish that reached Meta but whose answer was lost is not published twice: the retry reads the container PUBLISHED and lands the one Reel with this caption (every @ ignored, as Meta strips them) since the container opened on the vendor's clock; a caption-less Reel or no single match is refused, and a media list that cannot be read fails retryably — never a blocked queue", 'connector', 'core', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
811
|
+
const media = await publishReel(local, { caption: 'the one that got lost, thanks @volter' });
|
|
812
|
+
const entry = entryFor(localRoot, media, 'instagram.media.publish');
|
|
813
|
+
const inner = vendorExecutor(vendorRoot, []);
|
|
814
|
+
let lose = true;
|
|
815
|
+
// Meta strips the @ from captions for a non-admin app user: the vendor answers the list that way
|
|
816
|
+
const lossy = async (request) => {
|
|
817
|
+
const res = await inner(request);
|
|
818
|
+
if (lose && request.path.endsWith('/media_publish')) {
|
|
819
|
+
lose = false;
|
|
820
|
+
return { status: 502, headers: {}, body: '{"error":{"message":"Bad gateway","type":"OAuthException","code":2,"is_transient":true,"fbtrace_id":"x"}}' };
|
|
821
|
+
}
|
|
822
|
+
if (request.method === 'GET' && /\/media\?/.test(request.path))
|
|
823
|
+
return { ...res, body: res.body.replace(/@/g, '') };
|
|
824
|
+
return res;
|
|
825
|
+
};
|
|
826
|
+
// the local clock two hours AHEAD of the vendor's: only the vendor's own instant finds the Reel
|
|
827
|
+
const ahead = () => Date.parse('2026-09-27T15:00:00.000Z');
|
|
828
|
+
let first = '';
|
|
829
|
+
try {
|
|
830
|
+
await performInstagramAction(lossy, entry, ctxFor(localRoot), noWait, ahead);
|
|
831
|
+
}
|
|
832
|
+
catch (e) {
|
|
833
|
+
first = String(e.message);
|
|
834
|
+
}
|
|
835
|
+
const out = await performInstagramAction(lossy, entry, ctxFor(localRoot), noWait, ahead);
|
|
836
|
+
const listed = bodyOf(await handleFor(vendorRoot, '2026-09-27T14:00:00.000Z')({ m: 'GET', p: `/v26.0/${IG}/media?fields=id,caption`, headers: { authorization: 'Bearer vendor-token-0002' } })).data;
|
|
837
|
+
const { writePerformContainer } = await import("./instagram-media.js");
|
|
838
|
+
const keep = () => writePerformContainer(entry.id, { container: '812345678901234567', sha256: blobDigest(mp4()), created_ms: Date.now(), uploaded: mp4().length, target: 'https://rupload.facebook.com/ig-api-upload/v26.0/812345678901234567' }, localRoot);
|
|
839
|
+
const published = (list) => async (request) => /fields=status_code/.test(request.path) ? { status: 200, headers: {}, body: '{"id":"1","status_code":"PUBLISHED"}' }
|
|
840
|
+
: list && /\/media\?/.test(request.path) ? list(request) : { status: 200, headers: {}, body: '{}' };
|
|
841
|
+
// no single match: refused, never a second publish
|
|
842
|
+
await keep();
|
|
843
|
+
let refused;
|
|
844
|
+
try {
|
|
845
|
+
await performInstagramAction(published(async () => ({ status: 200, headers: {}, body: '{"data":[]}' })), entry, ctxFor(localRoot));
|
|
846
|
+
}
|
|
847
|
+
catch (e) {
|
|
848
|
+
refused = e;
|
|
849
|
+
}
|
|
850
|
+
// the list cannot be read: a retryable failure, the Reel not given up
|
|
851
|
+
await keep();
|
|
852
|
+
let unreadable;
|
|
853
|
+
try {
|
|
854
|
+
await performInstagramAction(published(async () => ({ status: 503, headers: {}, body: '{"error":{"message":"x","type":"OAuthException","code":2,"fbtrace_id":"x"}}' })), entry, ctxFor(localRoot));
|
|
855
|
+
}
|
|
856
|
+
catch (e) {
|
|
857
|
+
unreadable = e;
|
|
858
|
+
}
|
|
859
|
+
// a caption-less Reel: refused without guessing
|
|
860
|
+
const bare = await publishReel(local, {}, mp4({ ms: 7000 }));
|
|
861
|
+
const bareEntry = entryFor(localRoot, bare, 'instagram.media.publish');
|
|
862
|
+
await writePerformContainer(bareEntry.id, { container: '812345678901234568', sha256: blobDigest(mp4({ ms: 7000 })), created_ms: Date.now(), uploaded: mp4({ ms: 7000 }).length, target: 'https://rupload.facebook.com/ig-api-upload/v26.0/812345678901234568' }, localRoot);
|
|
863
|
+
let blind;
|
|
864
|
+
try {
|
|
865
|
+
await performInstagramAction(published(async () => ({ status: 200, headers: {}, body: JSON.stringify({ data: [{ id: '17900000000000001', media_product_type: 'REELS', timestamp: '2026-09-27T13:00:00+0000' }] }) })), bareEntry, ctxFor(localRoot));
|
|
866
|
+
}
|
|
867
|
+
catch (e) {
|
|
868
|
+
blind = e;
|
|
869
|
+
}
|
|
870
|
+
return /HTTP 502/.test(first) && listed.length === 1 && out.externalId === listed[0].id
|
|
871
|
+
&& refused instanceof RefusedWriteError && /PUBLISHED at Meta/.test(refused.message) && unreadable instanceof Error && !(unreadable instanceof RefusedWriteError) && /HTTP 503/.test(unreadable.message)
|
|
872
|
+
&& blind instanceof RefusedWriteError && /caption-less/.test(blind.message);
|
|
873
|
+
})),
|
|
874
|
+
done('instagram.connector.container_error_is_retryable', 'connector', "A container Meta leaves in ERROR for its own reasons (2207001) fails the perform retryably and drops the container, so the retry uploads afresh; an ERROR that names the FILE (2207026, 2207057) refuses the entry, since the same bytes fail again", 'connector', 'core', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
875
|
+
const media = await publishReel(local);
|
|
876
|
+
const entry = entryFor(localRoot, media, 'instagram.media.publish');
|
|
877
|
+
const { readPerformContainer } = await import("./instagram-media.js");
|
|
878
|
+
const failing = (status) => {
|
|
879
|
+
const inner = vendorExecutor(vendorRoot, []);
|
|
880
|
+
return async (request) => {
|
|
881
|
+
const res = await inner(request);
|
|
882
|
+
if (request.method === 'GET' && /fields=status_code/.test(request.path))
|
|
883
|
+
return { ...res, body: JSON.stringify({ id: '1', status_code: 'ERROR', status }) };
|
|
884
|
+
return res;
|
|
885
|
+
};
|
|
886
|
+
};
|
|
887
|
+
let transient;
|
|
888
|
+
let spec;
|
|
889
|
+
try {
|
|
890
|
+
await performInstagramAction(failing('2207001'), entry, ctxFor(localRoot));
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
transient = e;
|
|
894
|
+
}
|
|
895
|
+
const keptAfterTransient = await readPerformContainer(entry.id, localRoot);
|
|
896
|
+
try {
|
|
897
|
+
await performInstagramAction(failing('2207026'), entry, ctxFor(localRoot));
|
|
898
|
+
}
|
|
899
|
+
catch (e) {
|
|
900
|
+
spec = e;
|
|
901
|
+
}
|
|
902
|
+
let offset;
|
|
903
|
+
try {
|
|
904
|
+
await performInstagramAction(failing('2207057'), entry, ctxFor(localRoot));
|
|
905
|
+
}
|
|
906
|
+
catch (e) {
|
|
907
|
+
offset = e;
|
|
908
|
+
}
|
|
909
|
+
return offset instanceof RefusedWriteError && /2207057/.test(offset.message) && transient instanceof Error && !(transient instanceof RefusedWriteError) && /2207001/.test(transient.message) && keptAfterTransient === null
|
|
910
|
+
&& spec instanceof RefusedWriteError && /2207026/.test(spec.message) && (await readPerformContainer(entry.id, localRoot)) === null;
|
|
911
|
+
})),
|
|
912
|
+
done('instagram.connector.upload_in_chunks_resumes', 'connector', "A Reel crosses in 8 MiB chunks (offsets 0, 8 MiB, 16 MiB), the container kept from its opening and the confirmed offset after each chunk; a chunk that failed before reaching the host is resumed at that offset on the retry, with no second container", 'connector', 'core', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
913
|
+
const bytes = mp4({ ms: 15_000, pad: 17 * 1024 * 1024 });
|
|
914
|
+
const media = await publishReel(local, {}, bytes);
|
|
915
|
+
const entry = entryFor(localRoot, media, 'instagram.media.publish');
|
|
916
|
+
const calls = [];
|
|
917
|
+
const inner = vendorExecutor(vendorRoot, calls);
|
|
918
|
+
let dropped = false;
|
|
919
|
+
const flaky = async (request) => {
|
|
920
|
+
if (!dropped && request.headers?.offset === String(UPLOAD_CHUNK_BYTES)) {
|
|
921
|
+
dropped = true;
|
|
922
|
+
return { status: 503, headers: {}, body: JSON.stringify({ debug_info: { retriable: true, type: 'ProcessingFailedError', message: 'x' } }) };
|
|
923
|
+
}
|
|
924
|
+
return inner(request);
|
|
925
|
+
};
|
|
926
|
+
let first = '';
|
|
927
|
+
try {
|
|
928
|
+
await performInstagramAction(flaky, entry, ctxFor(localRoot));
|
|
929
|
+
}
|
|
930
|
+
catch (e) {
|
|
931
|
+
first = String(e.message);
|
|
932
|
+
}
|
|
933
|
+
const out = await performInstagramAction(flaky, entry, ctxFor(localRoot));
|
|
934
|
+
const offsets = calls.filter((c) => c.offset !== undefined).map((c) => Number(c.offset));
|
|
935
|
+
const containers = calls.filter((c) => c.method === 'POST' && /\/media$/.test(c.path)).length;
|
|
936
|
+
const got = bodyOf(await handleFor(vendorRoot, '2026-09-27T14:00:00.000Z')({ m: 'GET', p: `/v26.0/${out.externalId}?fields=media_url`, headers: { authorization: 'Bearer vendor-token-0002' } }));
|
|
937
|
+
return /HTTP 503/.test(first) && containers === 1 && JSON.stringify(offsets) === JSON.stringify([0, UPLOAD_CHUNK_BYTES, 2 * UPLOAD_CHUNK_BYTES])
|
|
938
|
+
&& String(got.media_url).endsWith(`${blobDigest(bytes)}.mp4`);
|
|
939
|
+
})),
|
|
940
|
+
done('instagram.connector.resume_refused_opens_a_new_container', 'connector', "A chunk the host received but whose answer was lost leaves the kept offset behind the host's: the resumed POST is refused, the kept container is dropped, and the next retry uploads to a new container — one Reel published", 'connector', 'common', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
941
|
+
const bytes = mp4({ ms: 15_000, pad: 9 * 1024 * 1024 });
|
|
942
|
+
const media = await publishReel(local, {}, bytes);
|
|
943
|
+
const entry = entryFor(localRoot, media, 'instagram.media.publish');
|
|
944
|
+
const calls = [];
|
|
945
|
+
const inner = vendorExecutor(vendorRoot, calls);
|
|
946
|
+
let lost = false;
|
|
947
|
+
const flaky = async (request) => {
|
|
948
|
+
const res = await inner(request);
|
|
949
|
+
if (!lost && request.headers?.offset === '0') {
|
|
950
|
+
lost = true;
|
|
951
|
+
return { status: 504, headers: {}, body: '{"debug_info":{"retriable":true,"type":"x","message":"gateway timeout"}}' };
|
|
952
|
+
}
|
|
953
|
+
return res;
|
|
954
|
+
};
|
|
955
|
+
const errors = [];
|
|
956
|
+
for (let i = 0; i < 2; i += 1) {
|
|
957
|
+
try {
|
|
958
|
+
await performInstagramAction(flaky, entry, ctxFor(localRoot));
|
|
959
|
+
}
|
|
960
|
+
catch (e) {
|
|
961
|
+
errors.push(String(e.message));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const out = await performInstagramAction(flaky, entry, ctxFor(localRoot));
|
|
965
|
+
const containers = calls.filter((c) => c.method === 'POST' && /\/media$/.test(c.path)).length;
|
|
966
|
+
const listed = bodyOf(await handleFor(vendorRoot, '2026-09-27T14:00:00.000Z')({ m: 'GET', p: `/v26.0/${IG}/media`, headers: { authorization: 'Bearer vendor-token-0002' } })).data;
|
|
967
|
+
return errors.length === 2 && /HTTP 504/.test(errors[0]) && /after a refused resume/.test(errors[1]) && containers === 2 && listed.length === 1 && listed[0].id === out.externalId;
|
|
968
|
+
})),
|
|
969
|
+
done('instagram.connector.final_chunk_answer_lost_keeps_the_container', 'connector', "When the LAST chunk reached the host but its answer was lost, the resumed POST is refused and the container reads processing done: the perform goes on with that container — no second container, no second upload", 'connector', 'common', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
970
|
+
const bytes = mp4({ ms: 15_000, pad: 9 * 1024 * 1024 });
|
|
971
|
+
const media = await publishReel(local, {}, bytes);
|
|
972
|
+
const entry = entryFor(localRoot, media, 'instagram.media.publish');
|
|
973
|
+
const calls = [];
|
|
974
|
+
// one second of vendor time per call: the resumed POST's status read finds Meta still PROCESSING the whole file
|
|
975
|
+
const inner = vendorExecutor(vendorRoot, calls, { step: 1_000 });
|
|
976
|
+
let lost = false;
|
|
977
|
+
const flaky = async (request) => {
|
|
978
|
+
const res = await inner(request);
|
|
979
|
+
if (!lost && request.headers?.offset === String(UPLOAD_CHUNK_BYTES)) {
|
|
980
|
+
lost = true;
|
|
981
|
+
return { status: 504, headers: {}, body: '{"debug_info":{"retriable":true,"type":"x","message":"gateway timeout"}}' };
|
|
982
|
+
}
|
|
983
|
+
return res;
|
|
984
|
+
};
|
|
985
|
+
let first = '';
|
|
986
|
+
try {
|
|
987
|
+
await performInstagramAction(flaky, entry, ctxFor(localRoot));
|
|
988
|
+
}
|
|
989
|
+
catch (e) {
|
|
990
|
+
first = String(e.message);
|
|
991
|
+
}
|
|
992
|
+
const out = await performInstagramAction(flaky, entry, ctxFor(localRoot));
|
|
993
|
+
const containers = calls.filter((c) => c.method === 'POST' && /\/media$/.test(c.path)).length;
|
|
994
|
+
const listed = bodyOf(await handleFor(vendorRoot, '2026-09-27T14:00:00.000Z')({ m: 'GET', p: `/v26.0/${IG}/media`, headers: { authorization: 'Bearer vendor-token-0002' } })).data;
|
|
995
|
+
return /HTTP 504/.test(first) && containers === 1 && listed.length === 1 && listed[0].id === out.externalId;
|
|
996
|
+
})),
|
|
997
|
+
done('instagram.connector.kept_target_revalidated', 'connector', "A kept upload target read back from the annex must be what the vendor could have answered (Meta's upload host over plain https, or the anchored path for this container): a record naming another host is dropped and a fresh container opened, so the credential never follows it", 'connector', 'common', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
998
|
+
const bytes = mp4();
|
|
999
|
+
const media = await publishReel(local, {}, bytes);
|
|
1000
|
+
const entry = entryFor(localRoot, media, 'instagram.media.publish');
|
|
1001
|
+
const { writePerformContainer } = await import("./instagram-media.js");
|
|
1002
|
+
const calls = [];
|
|
1003
|
+
let opened = 0;
|
|
1004
|
+
for (const target of ['https://graph.instagram.com/ig-api-upload/v26.0/812345678901234567', 'https://rupload.facebook.com/ig-api-upload/v26.0/899999999999999999']) {
|
|
1005
|
+
await writePerformContainer(entry.id, { container: '812345678901234567', sha256: blobDigest(bytes), created_ms: Date.now(), uploaded: 0, target }, localRoot);
|
|
1006
|
+
const before = calls.length;
|
|
1007
|
+
try {
|
|
1008
|
+
await performInstagramAction(vendorExecutor(vendorRoot, calls), entry, ctxFor(localRoot));
|
|
1009
|
+
}
|
|
1010
|
+
catch { /* a second publish of one entry is not the point */ }
|
|
1011
|
+
opened += calls.slice(before).filter((c) => c.method === 'POST' && /\/media$/.test(c.path)).length;
|
|
1012
|
+
}
|
|
1013
|
+
return opened === 2 && calls.every((c) => !c.path.includes('graph.instagram.com') && !c.path.includes('899999999999999999'));
|
|
1014
|
+
})),
|
|
1015
|
+
done('instagram.connector.perform_delete', 'connector', 'A landed delete crosses as DELETE /{ig-media-id} against the resolved vendor id', 'connector', 'core', () => withVendor(async (local, localRoot, vendorRoot) => {
|
|
1016
|
+
const media = await publishReel(local);
|
|
1017
|
+
const exec = vendorExecutor(vendorRoot, []);
|
|
1018
|
+
const created = await performInstagramAction(exec, entryFor(localRoot, media, 'instagram.media.publish'), ctxFor(localRoot));
|
|
1019
|
+
await local({ m: 'DELETE', p: `/v26.0/${media}` });
|
|
1020
|
+
const out = await performInstagramAction(exec, entryFor(localRoot, media, 'instagram.media.delete'), { ...ctxFor(localRoot), resolve: (_t, id) => (id === media ? created.externalId : id) });
|
|
1021
|
+
const gone = await handleFor(vendorRoot, '2026-09-27T14:00:00.000Z')({ m: 'GET', p: `/v26.0/${created.externalId}`, headers: { authorization: 'Bearer vendor-token-0002' } });
|
|
1022
|
+
return out.externalId === created.externalId && out.data?.deleted === true && gone.status === 400 && err(gone).error_subcode === 33;
|
|
1023
|
+
})),
|
|
1024
|
+
done('instagram.connector.perform_twin_records_cross_nothing', 'connector', "A seeded account or token is the twin's own record and performs as nothing; a token seed is bookkeeping and is never deployable at all", 'connector', 'common', () => withVendor(async (_local, localRoot, vendorRoot) => {
|
|
1025
|
+
const calls = [];
|
|
1026
|
+
const seeds = deployableEntries('instagram', localRoot).filter((a) => String(a.operation).startsWith('instagram.twin.'));
|
|
1027
|
+
for (const s of seeds) {
|
|
1028
|
+
const out = await performInstagramAction(vendorExecutor(vendorRoot, calls), s, ctxFor(localRoot));
|
|
1029
|
+
if (out.data?.performed !== false)
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
return seeds.length === 1 && seeds[0].operation === 'instagram.twin.seed_user' && calls.length === 0;
|
|
1033
|
+
})),
|
|
1034
|
+
done('instagram.connector.budget_keyed_by_credential', 'connector', 'The served perform charges one ledger per sealed credential: two World roots with one credential spend one allowance', 'connector', 'core', async () => {
|
|
1035
|
+
const credential = `verify-${crypto.randomUUID()}`;
|
|
1036
|
+
const roots = [mkdtempSync(join(tmpdir(), 'instagram-cap-cred-a-')), mkdtempSync(join(tmpdir(), 'instagram-cap-cred-b-'))];
|
|
1037
|
+
try {
|
|
1038
|
+
const answering = async (r) => ({ status: 200, headers: {}, body: r.method === 'DELETE' ? '{"success":true}' : '{}' });
|
|
1039
|
+
for (const root of roots) {
|
|
1040
|
+
const h = handleFor(root);
|
|
1041
|
+
await seed(h);
|
|
1042
|
+
observeResource('instagram', { type: 'media', id: '17900000000000001', fields: { media_type: 'VIDEO', owner: { id: IG }, timestamp: '2026-09-27T09:00:00+0000' } }, { root, at: '2026-09-27T10:00:00.000Z' });
|
|
1043
|
+
await h({ m: 'DELETE', p: '/v26.0/17900000000000001' });
|
|
1044
|
+
await performServed(answering, entryFor(root, '17900000000000001', 'instagram.media.delete'), { ...ctxFor(root), credential });
|
|
1045
|
+
}
|
|
1046
|
+
// both deletes (8 units each) are on the credential's ledger, and on neither root's
|
|
1047
|
+
return new InstagramBudget({ token: credential }).spend() === 16 && new InstagramBudget({ root: roots[0] }).spend() === 0;
|
|
1048
|
+
}
|
|
1049
|
+
finally {
|
|
1050
|
+
for (const r of roots)
|
|
1051
|
+
rmSync(r, { recursive: true, force: true });
|
|
1052
|
+
}
|
|
1053
|
+
}),
|
|
1054
|
+
done('instagram.connector.refresh_observes_account_and_media', 'connector', "syncInstagramFromRemote observes the credential's account (/me) and every media it holds, paged, into a World that had none; a second refresh leaves one copy of each", 'connector', 'core', () => withVendor(async (_local, _localRoot, vendorRoot) => {
|
|
1055
|
+
const vendor = handleFor(vendorRoot);
|
|
1056
|
+
const vAuth = { authorization: 'Bearer vendor-token-0002' };
|
|
1057
|
+
const made = [];
|
|
1058
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1059
|
+
const bytes = mp4({ ms: 4000 + i * 1000 });
|
|
1060
|
+
const c = String(bodyOf(await vendor({ m: 'POST', p: `/v26.0/${IG}/media`, b: { media_type: 'REELS', upload_type: 'resumable', caption: `on instagram.com ${i}` }, headers: vAuth, at: EARLY })).id);
|
|
1061
|
+
await vendor({ m: 'POST', p: `/ig-api-upload/v26.0/${c}`, bytes, headers: { authorization: 'OAuth vendor-token-0002', offset: '0', file_size: String(bytes.length) }, at: EARLY });
|
|
1062
|
+
made.push(String(bodyOf(await vendor({ m: 'POST', p: `/v26.0/${IG}/media_publish`, b: { creation_id: c }, headers: vAuth, at: LATER(i * 1000) })).id));
|
|
1063
|
+
}
|
|
1064
|
+
await vendor({ m: 'POST', p: '/_twin/users', b: { id: IG, username: 'metricsaurus', name: 'Named On Instagram', followers_count: 99 }, headers: {} });
|
|
1065
|
+
// a World that has never seen the account: what it knows of it is what the refresh observed
|
|
1066
|
+
const pulled = mkdtempSync(join(tmpdir(), 'instagram-cap-pulled-'));
|
|
1067
|
+
try {
|
|
1068
|
+
const exec = vendorExecutor(vendorRoot, [], { base: 'https://graph.facebook.com' });
|
|
1069
|
+
const r1 = await syncInstagramFromRemote(exec, { root: pulled, occurredAt: '2026-09-27T14:00:00.000Z', budget: scratchBudget() });
|
|
1070
|
+
const r2 = await syncInstagramFromRemote(exec, { root: pulled, occurredAt: '2026-09-27T14:10:00.000Z', budget: scratchBudget() });
|
|
1071
|
+
const h = handleFor(pulled);
|
|
1072
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: TOKEN, user: IG, permissions: PERMS }, headers: {} });
|
|
1073
|
+
const me = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}?fields=username,name,followers_count,media_count` }));
|
|
1074
|
+
const got = bodyOf(await h({ m: 'GET', p: `/v26.0/${made[0]}?fields=caption,media_url` }));
|
|
1075
|
+
const listed = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media` })).data;
|
|
1076
|
+
return r1.media === 3 && r2.media === 3 && me.username === 'metricsaurus' && me.name === 'Named On Instagram' && me.followers_count === 99 && me.media_count === 3
|
|
1077
|
+
&& got.caption === 'on instagram.com 0' && /^https:\/\/graph\.facebook\.com\/o1\//.test(String(got.media_url)) && listed.length === 3 && made.every((m) => listed.some((l) => l.id === m));
|
|
1078
|
+
}
|
|
1079
|
+
finally {
|
|
1080
|
+
rmSync(pulled, { recursive: true, force: true });
|
|
1081
|
+
}
|
|
1082
|
+
})),
|
|
1083
|
+
done('instagram.connector.refresh_refusal_throws', 'connector', 'A refused refresh read throws rather than folding an empty account', 'connector', 'core', async () => {
|
|
1084
|
+
const refusing = async () => ({ status: 400, headers: {}, body: JSON.stringify({ error: { message: 'Invalid OAuth access token', type: 'OAuthException', code: 190, fbtrace_id: 'x' } }) });
|
|
1085
|
+
const emptyBody = async (r) => ({ status: 200, headers: {}, body: /\/me\?/.test(r.path) ? JSON.stringify({ id: IG, username: 'metricsaurus' }) : '{}' });
|
|
1086
|
+
const root = mkdtempSync(join(tmpdir(), 'instagram-cap-refuse-'));
|
|
1087
|
+
try {
|
|
1088
|
+
const messages = [];
|
|
1089
|
+
for (const exec of [refusing, emptyBody]) {
|
|
1090
|
+
try {
|
|
1091
|
+
await syncInstagramFromRemote(exec, { root, budget: scratchBudget() });
|
|
1092
|
+
messages.push('no throw');
|
|
1093
|
+
}
|
|
1094
|
+
catch (e) {
|
|
1095
|
+
messages.push(String(e.message));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return /refused IG User GET me: HTTP 400/.test(messages[0]) && /with no data/.test(messages[1]);
|
|
1099
|
+
}
|
|
1100
|
+
finally {
|
|
1101
|
+
rmSync(root, { recursive: true, force: true });
|
|
1102
|
+
}
|
|
1103
|
+
}),
|
|
1104
|
+
done('instagram.connector.declared_host_executor', 'connector', "Through the kernel's own executor (buildRemoteExecute for an https root with this pack's hosts): the rupload upload goes to rupload.facebook.com WITH the sealed credential; an undeclared host, a rupload path outside /ig-api-upload/ and a userinfo URL are refused before any request", 'connector', 'core', async () => {
|
|
1105
|
+
const { pack } = await import("./index.js");
|
|
1106
|
+
const seen = [];
|
|
1107
|
+
const realFetch = globalThis.fetch;
|
|
1108
|
+
globalThis.fetch = (async (input, init) => {
|
|
1109
|
+
const req = new Request(input, init);
|
|
1110
|
+
seen.push({ url: req.url, auth: req.headers.get('authorization'), offset: req.headers.get('offset') });
|
|
1111
|
+
return new Response(JSON.stringify({ success: true, message: 'Upload successful.' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
1112
|
+
});
|
|
1113
|
+
try {
|
|
1114
|
+
const exec = buildRemoteExecute(validateRemoteOrigin('https://graph.facebook.com'), { headers: { authorization: 'OAuth sealed-credential-0003' } }, undefined, pack.hosts);
|
|
1115
|
+
await uploadReel(exec, 'https://rupload.facebook.com/ig-api-upload/v26.0/812345678901234567', new Uint8Array([1, 2, 3]));
|
|
1116
|
+
const refused = [];
|
|
1117
|
+
for (const url of ['https://evil.example/ig-api-upload/v26.0/1', 'https://rupload.facebook.com/other/1', 'https://user:pw@rupload.facebook.com/ig-api-upload/v26.0/1']) {
|
|
1118
|
+
try {
|
|
1119
|
+
await uploadReel(exec, url, new Uint8Array([1]));
|
|
1120
|
+
}
|
|
1121
|
+
catch (e) {
|
|
1122
|
+
refused.push(String(e.message));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return seen.length === 1 && seen[0].url === 'https://rupload.facebook.com/ig-api-upload/v26.0/812345678901234567' && seen[0].auth === 'OAuth sealed-credential-0003' && seen[0].offset === '0'
|
|
1126
|
+
&& refused.length === 3 && /not a host this vendor's pack declares/.test(refused[0]) && /not a host this vendor's pack declares/.test(refused[1]) && /userinfo/.test(refused[2]);
|
|
1127
|
+
}
|
|
1128
|
+
finally {
|
|
1129
|
+
globalThis.fetch = realFetch;
|
|
1130
|
+
}
|
|
1131
|
+
}),
|
|
1132
|
+
todo('instagram.connector.refresh_deletions', 'connector', 'A refresh that tombstones media deleted on instagram.com (the media list read in full as a complete scope)', 'connector', 'common'),
|
|
1133
|
+
todo('instagram.connector.refresh_bytes', 'connector', "Refresh a pulled Reel's bytes onto the blob seam, so a pulled Reel previews offline", 'connector', 'niche'),
|
|
1134
|
+
// ── the rate budget (the fail-closed backstop on the perform and refresh paths) ──────────────
|
|
1135
|
+
done('instagram.rate_limit.budget_fails_closed', 'rate_limit', "The budgeted executor refuses BEFORE calling once the declared burst is spent: a second publish in a minute is refused with the fake's call count unchanged", 'connector', 'core', async () => {
|
|
1136
|
+
let calls = 0;
|
|
1137
|
+
const fake = async () => { calls += 1; return { status: 200, headers: {}, body: '{"id":"1"}' }; };
|
|
1138
|
+
const exec = budgetedInstagramExecute(fake, scratchBudget());
|
|
1139
|
+
let refusedAt = -1;
|
|
1140
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1141
|
+
try {
|
|
1142
|
+
await exec({ method: 'POST', path: `/v26.0/${IG}/media_publish`, body: '{}' });
|
|
1143
|
+
}
|
|
1144
|
+
catch (error) {
|
|
1145
|
+
if (error instanceof InstagramBudgetError) {
|
|
1146
|
+
refusedAt = i;
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return refusedAt === 1 && calls === 1;
|
|
1153
|
+
}),
|
|
1154
|
+
done('instagram.rate_limit.prices', 'rate_limit', 'A publish costs 100, a container 16, a delete 8, an 8 MiB rupload chunk 1 on Meta\'s host or anchored, a read 1', 'connector', 'core', () => instagramCallWeight('POST', `/v26.0/${IG}/media_publish`) === 100 && instagramCallWeight('POST', `/v26.0/${IG}/media`) === 16 && instagramCallWeight('DELETE', '/v26.0/1790000') === 8
|
|
1155
|
+
&& instagramCallWeight('POST', 'https://rupload.facebook.com/ig-api-upload/v26.0/8123') === 1 && instagramCallWeight('POST', '/ig-api-upload/v26.0/8123') === 1
|
|
1156
|
+
&& instagramCallWeight('GET', '/v26.0/8123?fields=status_code') === 1 && instagramCallWeight('GET', `/v26.0/${IG}/media?limit=100`) === 1 && instagramCallWeight('POST', '/v26.0/1/unknown') === 6),
|
|
1157
|
+
done('instagram.rate_limit.declaration_is_the_descriptor', 'rate_limit', 'The pack descriptor arms the SAME declaration the adapters charge; spent flat out for a day it stays under 50 publishes and 400 containers, and its burst admits one whole perform of the largest (300 MB) Reel', 'connector', 'core', async () => {
|
|
1158
|
+
const { pack } = await import("./index.js");
|
|
1159
|
+
const d = INSTAGRAM_RATE_BUDGET;
|
|
1160
|
+
const chunks = Math.ceil(300_000_000 / UPLOAD_CHUNK_BYTES);
|
|
1161
|
+
const perform = INSTAGRAM_CALL_WEIGHTS.container + chunks * INSTAGRAM_CALL_WEIGHTS.upload + 3 * INSTAGRAM_CALL_WEIGHTS.read + INSTAGRAM_CALL_WEIGHTS.publish;
|
|
1162
|
+
return pack.rateBudget === d && d.windowMs === 3_600_000 && Math.floor(d.ceiling / INSTAGRAM_CALL_WEIGHTS.publish) * 24 <= 50
|
|
1163
|
+
&& Math.floor(d.ceiling / INSTAGRAM_CALL_WEIGHTS.container) * 24 <= 400 && (d.burstCeiling ?? 0) >= perform && (d.burstCeiling ?? 0) / d.defaultWeight <= 30 && chunks === 36;
|
|
1164
|
+
}),
|
|
1165
|
+
todo('instagram.rate_limit.buc_headers', 'rate_limit', "Settle the ledger from Meta's X-Business-Use-Case-Usage / X-App-Usage headers rather than only 429s", 'connector', 'common'),
|
|
1166
|
+
// ══ MIRROR (dimension 'ui' — each DATA-COUPLED: seed through the handler, read the SAME route the
|
|
1167
|
+
// screen reads, render the mirror's OWN component over it) ═════════════════════════════════════
|
|
1168
|
+
done('instagram.mirror.profile_header', 'mirror', "The profile header renders the /me read: avatar, username, the posts / followers / following counts, name, bio and website", 'ui', 'core', () => withWorld(async (h) => {
|
|
1169
|
+
await publishReel(h);
|
|
1170
|
+
const me = bodyOf(await h({ m: 'GET', p: '/v26.0/me?fields=id,username,name,biography,website,followers_count,follows_count,media_count,profile_picture_url' }));
|
|
1171
|
+
const html = renderToStaticMarkup(createElement(ProfileHeader, { profile: profileFromRead(me) }));
|
|
1172
|
+
return html.includes('>metricsaurus</h2>') && html.includes('<span class="count">1</span> post<') && html.includes('<span class="count">12.4K</span> followers')
|
|
1173
|
+
&& html.includes('<span class="count">87</span> following') && html.includes('Dino data crunching app') && html.includes('metricsaurus.com') && html.includes('avatar-empty');
|
|
1174
|
+
})),
|
|
1175
|
+
done('instagram.mirror.counts_only_when_held', 'mirror', 'An account that holds no follower or following count shows no such count — never "0 followers"', 'ui', 'common', () => withWorld(async (h) => {
|
|
1176
|
+
await h({ m: 'POST', p: '/_twin/users', b: { id: OTHER, username: 'sparse.account' }, headers: {} });
|
|
1177
|
+
await h({ m: 'POST', p: '/_twin/tokens', b: { token: OTHER_TOKEN, user: OTHER, permissions: PERMS }, headers: {} });
|
|
1178
|
+
const me = bodyOf(await h({ m: 'GET', p: '/v26.0/me?fields=id,username,followers_count,follows_count,media_count', headers: { authorization: `Bearer ${OTHER_TOKEN}` } }));
|
|
1179
|
+
const html = renderToStaticMarkup(createElement(ProfileHeader, { profile: profileFromRead(me) }));
|
|
1180
|
+
return html.includes('>sparse.account</h2>') && !/follower/.test(html) && !/following/.test(html) && html.includes('<span class="count">0</span> posts');
|
|
1181
|
+
})),
|
|
1182
|
+
done('instagram.mirror.reels_grid', 'mirror', "The Reels grid renders the media list: a 9:16 tile per Reel linking to its /reel/<shortcode>/ route, the Reel's own first frame (the twin's thumbnail is a stand-in) or its cover image, a play glyph and no view count it does not hold", 'ui', 'core', () => withWorld(async (h) => {
|
|
1183
|
+
await publishReel(h, { caption: 'first' });
|
|
1184
|
+
await publishReel(h, { caption: 'covered', cover_url: 'https://www.example.com/cover.jpg' }, mp4({ ms: 7000 }));
|
|
1185
|
+
const list = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media?fields=id,media_product_type,media_url,thumbnail_url,shortcode,caption,like_count,comments_count` })).data;
|
|
1186
|
+
const html = renderToStaticMarkup(createElement(ReelsGrid, { reels: list, shape: 'reels' }));
|
|
1187
|
+
const plain = list.find((m) => m.caption === 'first');
|
|
1188
|
+
return html.includes('grid grid-reels') && list.every((m) => html.includes(`href="${reelRoute(String(m.shortcode))}"`) && html.includes(`data-media-id="${m.id}"`))
|
|
1189
|
+
&& html.includes(`src="${String(plain.media_url)}#t=0.1"`) && html.includes('src="https://www.example.com/cover.jpg"')
|
|
1190
|
+
&& (html.match(/class="tile-views"><svg[^>]*>.*?<\/svg><\/span>/g) ?? []).length === 2 && /\.grid-reels \.tile \{ aspect-ratio: 9 \/ 16; \}/.test(await instagramMirrorStyles());
|
|
1191
|
+
})),
|
|
1192
|
+
done('instagram.mirror.posts_tab_honours_share_to_feed', 'mirror', "The profile screen's Posts tab shows only Reels shared to the feed (share_to_feed false keeps a Reel in the Reels tab alone; a pulled Reel whose node omits the field stays), from the same media read the grid makes", 'ui', 'core', () => withWorld(async (h, root) => {
|
|
1193
|
+
await publishReel(h, { caption: 'in the feed' });
|
|
1194
|
+
await publishReel(h, { caption: 'reels tab only', share_to_feed: false }, mp4({ ms: 6000 }));
|
|
1195
|
+
observeResource('instagram', { type: 'media', id: '17900000000000031', fields: { media_type: 'VIDEO', media_product_type: 'REELS', owner: { id: IG }, caption: 'pulled', shortcode: 'PULLEDCODE1', timestamp: '2026-09-27T09:00:00+0000', media_url: 'https://scontent.cdninstagram.com/o1/v/p.mp4' } }, { root, at: '2026-09-27T10:00:00.000Z' });
|
|
1196
|
+
const me = bodyOf(await h({ m: 'GET', p: '/v26.0/me?fields=id,username,media_count' }));
|
|
1197
|
+
const list = bodyOf(await h({ m: 'GET', p: mediaQuery(IG) })).data;
|
|
1198
|
+
const account = { profile: profileFromRead(me), reels: list, error: null };
|
|
1199
|
+
const posts = renderToStaticMarkup(createElement(ProfileScreen, { account, route: { page: 'profile', tab: 'posts' } }));
|
|
1200
|
+
const reels = renderToStaticMarkup(createElement(ProfileScreen, { account, route: { page: 'profile', tab: 'reels' } }));
|
|
1201
|
+
const id = (c) => String(list.find((m) => m.caption === c)?.id);
|
|
1202
|
+
return list.length === 3 && posts.includes('grid grid-posts') && posts.includes(`data-media-id="${id('in the feed')}"`) && posts.includes(`data-media-id="${id('pulled')}"`)
|
|
1203
|
+
&& !posts.includes(`data-media-id="${id('reels tab only')}"`) && reels.includes('grid grid-reels') && ['in the feed', 'pulled', 'reels tab only'].every((c) => reels.includes(`data-media-id="${id(c)}"`));
|
|
1204
|
+
})),
|
|
1205
|
+
done('instagram.mirror.reel_viewer', 'mirror', "The Reel viewer renders one Reel full height: its media_url playing (autoplay, muted, looped), the username and avatar, the caption with #tags and @mentions as links, the age from the World's clock, and links to the neighbouring Reels", 'ui', 'core', () => withWorld(async (h) => {
|
|
1206
|
+
await publishReel(h, { caption: 'Out now #release with @volter' });
|
|
1207
|
+
const me = bodyOf(await h({ m: 'GET', p: '/v26.0/me?fields=id,username,profile_picture_url,media_count' }));
|
|
1208
|
+
const reel = bodyOf(await h({ m: 'GET', p: `/v26.0/${IG}/media?fields=id,media_product_type,media_url,caption,timestamp,shortcode,like_count,comments_count` })).data[0];
|
|
1209
|
+
const html = renderToStaticMarkup(createElement(ReelViewer, { reel, profile: profileFromRead(me), nowMs: Date.parse(AT) + 3 * 3600_000, next: 'NEXTCODE123' }));
|
|
1210
|
+
return html.includes(`src="${String(reel.media_url)}"`) && /<video[^>]*autoplay=""[^>]*loop=""[^>]*muted=""/i.test(html) && html.includes('>metricsaurus</a>') && html.includes('class="ig-link">#release<')
|
|
1211
|
+
&& html.includes('class="ig-link">@volter<') && html.includes('>3h<') && html.includes(`href="${reelRoute('NEXTCODE123')}"`) && !html.includes('Previous reel');
|
|
1212
|
+
})),
|
|
1213
|
+
done('instagram.mirror.format_helpers', 'mirror', "The mirror's own formatters: instagram.com counts (1,234 / 12.4K / 1.2M), post ages, and caption segments", 'ui', 'common', () => withWorld(async (h) => {
|
|
1214
|
+
const id = await publishReel(h, { caption: 'Ship it #twin, thanks @volter.ai! email me a@b.c' });
|
|
1215
|
+
const m = bodyOf(await h({ m: 'GET', p: `/v26.0/${id}?fields=caption,timestamp` }));
|
|
1216
|
+
const segs = captionSegments(m.caption);
|
|
1217
|
+
const now = Date.parse(AT);
|
|
1218
|
+
return segs.some((s) => s.kind === 'hashtag' && s.value === '#twin') && segs.some((s) => s.kind === 'mention' && s.value === '@volter.ai') && !segs.some((s) => s.value === '@b.c')
|
|
1219
|
+
&& segs.map((s) => s.value).join('') === m.caption && timeAgo(m.timestamp, now + 90_000) === '1m' && timeAgo(m.timestamp, now + 2 * 86_400_000) === '2d'
|
|
1220
|
+
&& timeAgo(m.timestamp, now + 60 * 86_400_000) === 'September 27' && compactCount(1234) === '1,234' && compactCount(12480) === '12.4K' && compactCount(1_250_000) === '1.2M' && compactCount(10_000) === '10K';
|
|
1221
|
+
})),
|
|
1222
|
+
done('instagram.mirror.serves_the_vendor_api_on_its_own_origin', 'mirror', 'The mirror origin serves the REAL Instagram handler — one serving code path — with its shell (<base href="/">, relative assets) and the token-gated Graph API', 'ui', 'core', async () => {
|
|
1223
|
+
const root = mkdtempSync(join(tmpdir(), 'instagram-cap-mirror-'));
|
|
1224
|
+
const server = await createInstagramMirrorServer({ root, port: 0 });
|
|
1225
|
+
try {
|
|
1226
|
+
const json = { 'content-type': 'application/json' };
|
|
1227
|
+
await fetch(`${server.url}/_twin/users`, { method: 'POST', headers: json, body: JSON.stringify({ id: IG, username: 'metricsaurus' }) });
|
|
1228
|
+
await fetch(`${server.url}/_twin/tokens`, { method: 'POST', headers: json, body: JSON.stringify({ token: TOKEN, user: IG, permissions: PERMS }) });
|
|
1229
|
+
const created = await (await fetch(`${server.url}/v26.0/${IG}/media`, { method: 'POST', headers: { ...json, ...AUTH }, body: JSON.stringify({ media_type: 'REELS', upload_type: 'resumable' }) })).json();
|
|
1230
|
+
const direct = await handleInstagramTwinRequest({ method: 'GET', path: `/v26.0/${created.id}?fields=status_code`, headers: AUTH, root });
|
|
1231
|
+
const me = await (await fetch(`${server.url}/v26.0/me?fields=username`, { headers: AUTH })).json();
|
|
1232
|
+
const anon = await fetch(`${server.url}/v26.0/me`);
|
|
1233
|
+
const shell = await (await fetch(`${server.url}/`)).text();
|
|
1234
|
+
return String(created.uri).startsWith(`${server.url}/ig-api-upload/`) && direct.body.status_code === 'IN_PROGRESS' && me.username === 'metricsaurus' && anon.status === 400
|
|
1235
|
+
&& shell.includes('<div id="root">') && shell.includes('<base href="/">') && shell.includes('href="assets/styles.css"') && shell.includes('src="assets/app.js"');
|
|
1236
|
+
}
|
|
1237
|
+
finally {
|
|
1238
|
+
server.stop();
|
|
1239
|
+
rmSync(root, { recursive: true, force: true });
|
|
1240
|
+
}
|
|
1241
|
+
}),
|
|
1242
|
+
todo('instagram.mirror.image_posts', 'mirror', 'Image and carousel posts in the Posts grid and a post page (needs image posts)', 'ui', 'common'),
|
|
1243
|
+
todo('instagram.mirror.comments', 'mirror', "A Reel's comments panel (needs the comments edge)", 'ui', 'common'),
|
|
1244
|
+
todo('instagram.mirror.view_counts', 'mirror', 'View counts on the Reels grid (needs view_count)', 'ui', 'common'),
|
|
1245
|
+
todo('instagram.mirror.compose', 'mirror', "instagram.com's Create flow (upload a Reel, caption, share) writing through the container / rupload / media_publish routes", 'ui', 'common'),
|
|
1246
|
+
];
|
|
1247
|
+
export async function instagramCapabilities() {
|
|
1248
|
+
return checkCapabilities('instagram', INSTAGRAM_CAPABILITIES);
|
|
1249
|
+
}
|