@volter/twin-x 0.1.1 → 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.
@@ -15,6 +15,15 @@ const SESSION_TOKEN = 'x-mirror.token';
15
15
  function readSession() {
16
16
  if (typeof sessionStorage === 'undefined')
17
17
  return null;
18
+ // the World signed these screens in as the account its config names (`signIn.as`): taken once, as a sign-in would
19
+ // (off the page once taken, and not again in this tab once the person signs out)
20
+ const w = window;
21
+ const given = sessionStorage.getItem('volter.signedOut') ? undefined : w.__VOLTER_SIGN_IN__;
22
+ delete w.__VOLTER_SIGN_IN__;
23
+ if (given && !sessionStorage.getItem(SESSION_TOKEN)) {
24
+ sessionStorage.setItem(SESSION_HANDLE, given.account);
25
+ sessionStorage.setItem(SESSION_TOKEN, given.token);
26
+ }
18
27
  const handle = sessionStorage.getItem(SESSION_HANDLE);
19
28
  const token = sessionStorage.getItem(SESSION_TOKEN);
20
29
  return handle && token ? { handle, token } : null;
@@ -391,6 +400,7 @@ function App() {
391
400
  const signOut = () => {
392
401
  sessionStorage.removeItem(SESSION_HANDLE);
393
402
  sessionStorage.removeItem(SESSION_TOKEN);
403
+ sessionStorage.setItem('volter.signedOut', '1');
394
404
  setSession(null);
395
405
  setMe(null);
396
406
  go('i/flow/login');
@@ -59,3 +59,15 @@ export declare function createXMirrorServer(options: {
59
59
  export declare function xMirrorHtml(): string;
60
60
  /** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
61
61
  export declare function xMirrorStyles(): Promise<string>;
62
+ /** The account `as` names (a username, with or without its @) and a fresh token minted for it through the twin's own
63
+ * `/_twin/tokens` door, as the World's seed issues one: what X's screens sign in with when the World's config says
64
+ * `signIn: { as }` (the World hands them over, served-world.ts). A stored token (the app's own, perhaps) is never read
65
+ * back; this one carries only what the screens do: read posts and people, and post. Null when the World holds no such
66
+ * account, or the door refuses the token. */
67
+ export declare function xMirrorSignIn(as: string, ctx: {
68
+ root: string;
69
+ twin: (path: string, init?: RequestInit) => Promise<Response>;
70
+ }): Promise<{
71
+ account: string;
72
+ token: string;
73
+ } | null>;
@@ -14,11 +14,11 @@
14
14
  // PURE FRONTEND (R3): the mirror imports no handler and no twin internals — it MOUNTS the pack's
15
15
  // own fetch adapter as its API backend and reads every byte of state back over the wire.
16
16
  import { readFile } from 'node:fs/promises';
17
- import { bundleClient, fileResponse } from '@volter/world-core';
17
+ import { twinResources, bundleClient, fileResponse, filePathOf } from '@volter/world-core';
18
18
  import { serveHttp } from '@volter/world-core';
19
19
  import { createXTwinFetch } from "./x-server.js";
20
- const CLIENT_ENTRY = () => new URL('../client/x-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects a top-level relative import.meta.url
21
- const CLIENT_CSS = () => new URL('../client/x-mirror.css', import.meta.url).pathname; // lazy: same reason
20
+ const CLIENT_ENTRY = () => filePathOf(new URL('../client/x-mirror.tsx', import.meta.url)); // lazy: workerd rejects a top-level relative import.meta.url
21
+ const CLIENT_CSS = () => filePathOf(new URL('../client/x-mirror.css', import.meta.url)); // lazy: same reason
22
22
  const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
23
23
  const LONG_MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
24
24
  /** A wall-clock reading of an instant in a time zone. x.com shows the VIEWER'S local time, so the
@@ -251,3 +251,20 @@ export function xMirrorHtml() {
251
251
  export function xMirrorStyles() {
252
252
  return readFile(CLIENT_CSS(), 'utf8');
253
253
  }
254
+ /** The account `as` names (a username, with or without its @) and a fresh token minted for it through the twin's own
255
+ * `/_twin/tokens` door, as the World's seed issues one: what X's screens sign in with when the World's config says
256
+ * `signIn: { as }` (the World hands them over, served-world.ts). A stored token (the app's own, perhaps) is never read
257
+ * back; this one carries only what the screens do: read posts and people, and post. Null when the World holds no such
258
+ * account, or the door refuses the token. */
259
+ export async function xMirrorSignIn(as, ctx) {
260
+ const handle = as.replace(/^@/, '').toLowerCase();
261
+ const account = twinResources('x', ctx.root).find((r) => r.type === 'account' && String(r.username ?? '').toLowerCase() === handle);
262
+ if (!account)
263
+ return null;
264
+ const token = `volter-view-${crypto.randomUUID()}`;
265
+ const minted = await ctx.twin('/_twin/tokens', {
266
+ method: 'POST', headers: { 'content-type': 'application/json' },
267
+ body: JSON.stringify({ token, account_id: account.id, scopes: ['tweet.read', 'users.read', 'tweet.write'] }),
268
+ });
269
+ return minted.ok ? { account: String(account.username), token } : null;
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-x",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Local X (Twitter) API v2 twin (post, reply, delete, timelines, user lookup) with an x.com 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.1"
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.1",
46
+ "@volter/world-core": "2.0.2",
47
47
  "@volter/world-tooling": "0.1.0",
48
48
  "twitter-api-v2": "^1.29.1",
49
49
  "typescript": "^5.9.0"
@@ -14,12 +14,12 @@
14
14
  // PURE FRONTEND (R3): the mirror imports no handler and no twin internals — it MOUNTS the pack's
15
15
  // own fetch adapter as its API backend and reads every byte of state back over the wire.
16
16
  import { readFile } from 'node:fs/promises';
17
- import { bundleClient, fileResponse } from '@volter/world-core';
17
+ import { twinResources, bundleClient, fileResponse, filePathOf } from '@volter/world-core';
18
18
  import { serveHttp } from '@volter/world-core';
19
19
  import { createXTwinFetch } from './x-server.ts';
20
20
 
21
- const CLIENT_ENTRY = () => new URL('../client/x-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects a top-level relative import.meta.url
22
- const CLIENT_CSS = () => new URL('../client/x-mirror.css', import.meta.url).pathname; // lazy: same reason
21
+ const CLIENT_ENTRY = () => filePathOf(new URL('../client/x-mirror.tsx', import.meta.url)); // lazy: workerd rejects a top-level relative import.meta.url
22
+ const CLIENT_CSS = () => filePathOf(new URL('../client/x-mirror.css', import.meta.url)); // lazy: same reason
23
23
 
24
24
  // ---------------------------------------------------------------------------
25
25
  // Pure, dependency-free render/format helpers (importable by the React client; Bun tree-shakes
@@ -261,3 +261,20 @@ export function xMirrorHtml(): string {
261
261
  export function xMirrorStyles(): Promise<string> {
262
262
  return readFile(CLIENT_CSS(), 'utf8');
263
263
  }
264
+
265
+ /** The account `as` names (a username, with or without its @) and a fresh token minted for it through the twin's own
266
+ * `/_twin/tokens` door, as the World's seed issues one: what X's screens sign in with when the World's config says
267
+ * `signIn: { as }` (the World hands them over, served-world.ts). A stored token (the app's own, perhaps) is never read
268
+ * back; this one carries only what the screens do: read posts and people, and post. Null when the World holds no such
269
+ * account, or the door refuses the token. */
270
+ export async function xMirrorSignIn(as: string, ctx: { root: string; twin: (path: string, init?: RequestInit) => Promise<Response> }): Promise<{ account: string; token: string } | null> {
271
+ const handle = as.replace(/^@/, '').toLowerCase();
272
+ const account = twinResources('x', ctx.root).find((r) => r.type === 'account' && String(r.username ?? '').toLowerCase() === handle);
273
+ if (!account) return null;
274
+ const token = `volter-view-${crypto.randomUUID()}`;
275
+ const minted = await ctx.twin('/_twin/tokens', {
276
+ method: 'POST', headers: { 'content-type': 'application/json' },
277
+ body: JSON.stringify({ token, account_id: account.id, scopes: ['tweet.read', 'users.read', 'tweet.write'] }),
278
+ });
279
+ return minted.ok ? { account: String(account.username), token } : null;
280
+ }