@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.
- package/dist/client/x-mirror.bundle.js +173 -176
- package/dist/client/x-mirror.js +10 -0
- package/dist/src/x-mirror-ui.d.ts +12 -0
- package/dist/src/x-mirror-ui.js +20 -3
- package/package.json +3 -3
- package/src/x-mirror-ui.ts +20 -3
package/dist/client/x-mirror.js
CHANGED
|
@@ -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>;
|
package/dist/src/x-mirror-ui.js
CHANGED
|
@@ -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)
|
|
21
|
-
const CLIENT_CSS = () => new URL('../client/x-mirror.css', import.meta.url)
|
|
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.
|
|
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.
|
|
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
|
"twitter-api-v2": "^1.29.1",
|
|
49
49
|
"typescript": "^5.9.0"
|
package/src/x-mirror-ui.ts
CHANGED
|
@@ -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)
|
|
22
|
-
const CLIENT_CSS = () => new URL('../client/x-mirror.css', import.meta.url)
|
|
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
|
+
}
|