@volter/twin-instagram 0.1.0 → 0.1.2
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/dist/client/instagram-mirror.bundle.js +170 -173
- package/dist/client/instagram-mirror.js +10 -0
- package/dist/src/instagram-mirror-ui.d.ts +12 -0
- package/dist/src/instagram-mirror-ui.js +20 -3
- package/dist/src/instagram-twin.js +3 -3
- package/package.json +3 -3
- package/src/instagram-mirror-ui.ts +20 -3
- package/src/instagram-twin.ts +3 -3
|
@@ -14,6 +14,15 @@ const SESSION_TOKEN = 'instagram-mirror.token';
|
|
|
14
14
|
function readSession() {
|
|
15
15
|
if (typeof sessionStorage === 'undefined')
|
|
16
16
|
return null;
|
|
17
|
+
// the World signed these screens in as the account its config names (`signIn.as`): taken once, as a log-in would
|
|
18
|
+
// (off the page once taken, and not again in this tab once the person signs out)
|
|
19
|
+
const w = window;
|
|
20
|
+
const given = sessionStorage.getItem('volter.signedOut') ? undefined : w.__VOLTER_SIGN_IN__;
|
|
21
|
+
delete w.__VOLTER_SIGN_IN__;
|
|
22
|
+
if (given && !sessionStorage.getItem(SESSION_TOKEN)) {
|
|
23
|
+
sessionStorage.setItem(SESSION_USER, given.account);
|
|
24
|
+
sessionStorage.setItem(SESSION_TOKEN, given.token);
|
|
25
|
+
}
|
|
17
26
|
const username = sessionStorage.getItem(SESSION_USER);
|
|
18
27
|
const token = sessionStorage.getItem(SESSION_TOKEN);
|
|
19
28
|
return username && token ? { username, token } : null;
|
|
@@ -242,6 +251,7 @@ function App() {
|
|
|
242
251
|
const signOut = () => {
|
|
243
252
|
sessionStorage.removeItem(SESSION_USER);
|
|
244
253
|
sessionStorage.removeItem(SESSION_TOKEN);
|
|
254
|
+
sessionStorage.setItem('volter.signedOut', '1');
|
|
245
255
|
setSession(null);
|
|
246
256
|
};
|
|
247
257
|
if (!session)
|
|
@@ -55,3 +55,15 @@ export declare function createInstagramMirrorServer(options: {
|
|
|
55
55
|
export declare function instagramMirrorHtml(): string;
|
|
56
56
|
/** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
|
|
57
57
|
export declare function instagramMirrorStyles(): Promise<string>;
|
|
58
|
+
/** The professional account `as` names (a username, with or without its @) and a token minted for it now: what
|
|
59
|
+
* Instagram's screens sign in with when the World's config says `signIn: { as }` (the World hands them over,
|
|
60
|
+
* served-world.ts). The account is read from the twin's state; the token is a fresh one, seeded through the twin's
|
|
61
|
+
* own `/_twin/tokens` exactly as a World's seed does (tokens are held only as their SHA-256, never read back), with
|
|
62
|
+
* the read permissions the screens use. Null when the World holds no such account, or the twin refuses the token. */
|
|
63
|
+
export declare function instagramMirrorSignIn(as: string, ctx: {
|
|
64
|
+
root: string;
|
|
65
|
+
twin: (path: string, init?: RequestInit) => Promise<Response>;
|
|
66
|
+
}): Promise<{
|
|
67
|
+
account: string;
|
|
68
|
+
token: string;
|
|
69
|
+
} | null>;
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
// PURE FRONTEND (R3): the mirror imports no handler and no twin internals — it MOUNTS the pack's own
|
|
20
20
|
// fetch adapter as its API backend and reads every byte of state back over the wire.
|
|
21
21
|
import { readFile } from 'node:fs/promises';
|
|
22
|
-
import { bundleClient, fileResponse, serveHttp } from '@volter/world-core';
|
|
22
|
+
import { filePathOf, bundleClient, fileResponse, ownFields, serveHttp, twinResources } from '@volter/world-core';
|
|
23
23
|
import { createInstagramTwinFetch } from "./instagram-server.js";
|
|
24
|
-
const CLIENT_ENTRY = () => new URL('../client/instagram-mirror.tsx', import.meta.url)
|
|
25
|
-
const CLIENT_CSS = () => new URL('../client/instagram-mirror.css', import.meta.url)
|
|
24
|
+
const CLIENT_ENTRY = () => filePathOf(new URL('../client/instagram-mirror.tsx', import.meta.url)); // lazy: workerd rejects a top-level relative import.meta.url
|
|
25
|
+
const CLIENT_CSS = () => filePathOf(new URL('../client/instagram-mirror.css', import.meta.url));
|
|
26
26
|
/** The version the mirror's reads pin — the twin's newest. */
|
|
27
27
|
export const MIRROR_VERSION = 'v26.0';
|
|
28
28
|
/** The account fields the profile header reads. */
|
|
@@ -156,3 +156,20 @@ export function instagramMirrorHtml() {
|
|
|
156
156
|
export function instagramMirrorStyles() {
|
|
157
157
|
return readFile(CLIENT_CSS(), 'utf8');
|
|
158
158
|
}
|
|
159
|
+
/** The professional account `as` names (a username, with or without its @) and a token minted for it now: what
|
|
160
|
+
* Instagram's screens sign in with when the World's config says `signIn: { as }` (the World hands them over,
|
|
161
|
+
* served-world.ts). The account is read from the twin's state; the token is a fresh one, seeded through the twin's
|
|
162
|
+
* own `/_twin/tokens` exactly as a World's seed does (tokens are held only as their SHA-256, never read back), with
|
|
163
|
+
* the read permissions the screens use. Null when the World holds no such account, or the twin refuses the token. */
|
|
164
|
+
export async function instagramMirrorSignIn(as, ctx) {
|
|
165
|
+
const username = as.replace(/^@/, '').toLowerCase();
|
|
166
|
+
const user = twinResources('instagram', ctx.root).find((r) => r.type === 'ig_user' && String(ownFields(r).username ?? '').toLowerCase() === username);
|
|
167
|
+
if (!user)
|
|
168
|
+
return null;
|
|
169
|
+
const token = crypto.randomUUID();
|
|
170
|
+
const res = await ctx.twin('/_twin/tokens', {
|
|
171
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ token, user: String(user.id), permissions: ['instagram_basic', 'pages_read_engagement', 'pages_show_list'] }),
|
|
173
|
+
});
|
|
174
|
+
return res.ok ? { account: String(ownFields(user).username), token } : null;
|
|
175
|
+
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// that writes. The one exception is a container's id, drawn from entropy (as LinkedIn's asset ids): a
|
|
38
38
|
// container is staging a branch inherits from its ancestors' annex, where an ordinal or a digest could
|
|
39
39
|
// collide between a parent and a child; the determinism replay captures it and never compares it.
|
|
40
|
-
import { applyTwinWrite, applyTwinWriteAtomic, blobDigest, ownFields, projectResources, resolveSubjectId, subjectAliases } from '@volter/world-core';
|
|
40
|
+
import { applyTwinWrite, applyTwinWriteAtomic, blobDigest, ownFields, projectResources, resolveSubjectId, subjectAliases, worldNow } from '@volter/world-core';
|
|
41
41
|
import { graphError, invalidParam, invalidToken, missingParam, noToken, ok, permissionDenied, readOnlyRefusal, ruploadFailure, ruploadOk, unknownPath, unmodeled, unsupported, } from "./instagram-errors.js";
|
|
42
42
|
import { clearChunks, CONTAINER_LIFETIME_MS, containerStatus, isNumericId, listContainers, MAX_REEL_BYTES, putChunk, putInstagramBlob, readChunks, readContainer, reelSpecViolation, sniffImage, sniffVideo, withContainer, writeContainer, } from "./instagram-media.js";
|
|
43
43
|
const SERVICE = 'instagram';
|
|
@@ -155,8 +155,8 @@ function resources(root) {
|
|
|
155
155
|
}
|
|
156
156
|
const ofType = (all, type) => all.filter((r) => r.type === type);
|
|
157
157
|
const fields = (r) => ownFields(r);
|
|
158
|
-
const nowMs = (req) => Date.parse(req.occurredAt ??
|
|
159
|
-
const at = (req) => req.occurredAt ??
|
|
158
|
+
const nowMs = (req) => Date.parse(req.occurredAt ?? worldNow());
|
|
159
|
+
const at = (req) => req.occurredAt ?? worldNow();
|
|
160
160
|
/** Meta's timestamp format: `2019-09-26T22:36:43+0000`. */
|
|
161
161
|
export function igTimestamp(iso) {
|
|
162
162
|
return `${new Date(iso).toISOString().slice(0, 19)}+0000`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-instagram",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Local Instagram twin (a professional account's Reels: resumable containers, the rupload upload, status_code, media_publish) with an instagram.com profile mirror, built on @volter/world-core.",
|
|
5
5
|
"author": "Volter (https://github.com/volter-ai)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@volter/world-core": "2.0.
|
|
39
|
+
"@volter/world-core": "2.0.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/bun": "^1.2.20",
|
|
43
43
|
"@types/node": "^24.0.0",
|
|
44
44
|
"@types/react": "^19.2.17",
|
|
45
45
|
"@types/react-dom": "^19.2.3",
|
|
46
|
-
"@volter/world-core": "2.0.
|
|
46
|
+
"@volter/world-core": "2.0.2",
|
|
47
47
|
"@volter/world-tooling": "0.1.0",
|
|
48
48
|
"typescript": "^5.9.0"
|
|
49
49
|
},
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
// PURE FRONTEND (R3): the mirror imports no handler and no twin internals — it MOUNTS the pack's own
|
|
20
20
|
// fetch adapter as its API backend and reads every byte of state back over the wire.
|
|
21
21
|
import { readFile } from 'node:fs/promises';
|
|
22
|
-
import { bundleClient, fileResponse, serveHttp } from '@volter/world-core';
|
|
22
|
+
import { filePathOf, bundleClient, fileResponse, ownFields, serveHttp, twinResources } from '@volter/world-core';
|
|
23
23
|
import { createInstagramTwinFetch } from './instagram-server.ts';
|
|
24
24
|
|
|
25
|
-
const CLIENT_ENTRY = () => new URL('../client/instagram-mirror.tsx', import.meta.url)
|
|
26
|
-
const CLIENT_CSS = () => new URL('../client/instagram-mirror.css', import.meta.url)
|
|
25
|
+
const CLIENT_ENTRY = () => filePathOf(new URL('../client/instagram-mirror.tsx', import.meta.url)); // lazy: workerd rejects a top-level relative import.meta.url
|
|
26
|
+
const CLIENT_CSS = () => filePathOf(new URL('../client/instagram-mirror.css', import.meta.url));
|
|
27
27
|
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
// Pure, dependency-free render/format helpers (importable by the React client; Bun tree-shakes the
|
|
@@ -171,3 +171,20 @@ export function instagramMirrorHtml(): string {
|
|
|
171
171
|
export function instagramMirrorStyles(): Promise<string> {
|
|
172
172
|
return readFile(CLIENT_CSS(), 'utf8');
|
|
173
173
|
}
|
|
174
|
+
|
|
175
|
+
/** The professional account `as` names (a username, with or without its @) and a token minted for it now: what
|
|
176
|
+
* Instagram's screens sign in with when the World's config says `signIn: { as }` (the World hands them over,
|
|
177
|
+
* served-world.ts). The account is read from the twin's state; the token is a fresh one, seeded through the twin's
|
|
178
|
+
* own `/_twin/tokens` exactly as a World's seed does (tokens are held only as their SHA-256, never read back), with
|
|
179
|
+
* the read permissions the screens use. Null when the World holds no such account, or the twin refuses the token. */
|
|
180
|
+
export async function instagramMirrorSignIn(as: string, ctx: { root: string; twin: (path: string, init?: RequestInit) => Promise<Response> }): Promise<{ account: string; token: string } | null> {
|
|
181
|
+
const username = as.replace(/^@/, '').toLowerCase();
|
|
182
|
+
const user = twinResources('instagram', ctx.root).find((r) => r.type === 'ig_user' && String(ownFields(r).username ?? '').toLowerCase() === username);
|
|
183
|
+
if (!user) return null;
|
|
184
|
+
const token = crypto.randomUUID();
|
|
185
|
+
const res = await ctx.twin('/_twin/tokens', {
|
|
186
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
187
|
+
body: JSON.stringify({ token, user: String(user.id), permissions: ['instagram_basic', 'pages_read_engagement', 'pages_show_list'] }),
|
|
188
|
+
});
|
|
189
|
+
return res.ok ? { account: String(ownFields(user).username), token } : null;
|
|
190
|
+
}
|
package/src/instagram-twin.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// that writes. The one exception is a container's id, drawn from entropy (as LinkedIn's asset ids): a
|
|
38
38
|
// container is staging a branch inherits from its ancestors' annex, where an ordinal or a digest could
|
|
39
39
|
// collide between a parent and a child; the determinism replay captures it and never compares it.
|
|
40
|
-
import { applyTwinWrite, applyTwinWriteAtomic, blobDigest, ownFields, projectResources, resolveSubjectId, subjectAliases } from '@volter/world-core';
|
|
40
|
+
import { applyTwinWrite, applyTwinWriteAtomic, blobDigest, ownFields, projectResources, resolveSubjectId, subjectAliases, worldNow } from '@volter/world-core';
|
|
41
41
|
import {
|
|
42
42
|
graphError,
|
|
43
43
|
invalidParam,
|
|
@@ -211,8 +211,8 @@ function resources(root?: string): Resource[] {
|
|
|
211
211
|
}
|
|
212
212
|
const ofType = (all: Resource[], type: string): Resource[] => all.filter((r) => r.type === type);
|
|
213
213
|
const fields = (r: Resource): Record<string, any> => ownFields(r as never) as Record<string, any>;
|
|
214
|
-
const nowMs = (req: InstagramRequest): number => Date.parse(req.occurredAt ??
|
|
215
|
-
const at = (req: InstagramRequest): string => req.occurredAt ??
|
|
214
|
+
const nowMs = (req: InstagramRequest): number => Date.parse(req.occurredAt ?? worldNow());
|
|
215
|
+
const at = (req: InstagramRequest): string => req.occurredAt ?? worldNow();
|
|
216
216
|
|
|
217
217
|
/** Meta's timestamp format: `2019-09-26T22:36:43+0000`. */
|
|
218
218
|
export function igTimestamp(iso: string): string {
|