@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.
@@ -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).pathname; // lazy: workerd rejects a top-level relative import.meta.url
25
- const CLIENT_CSS = () => new URL('../client/instagram-mirror.css', import.meta.url).pathname;
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 ?? new Date().toISOString());
159
- const at = (req) => req.occurredAt ?? new Date().toISOString();
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.0",
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.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.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).pathname; // lazy: workerd rejects a top-level relative import.meta.url
26
- const CLIENT_CSS = () => new URL('../client/instagram-mirror.css', import.meta.url).pathname;
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
+ }
@@ -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 ?? new Date().toISOString());
215
- const at = (req: InstagramRequest): string => req.occurredAt ?? new Date().toISOString();
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 {