@benqoder/beam 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -1310,6 +1310,135 @@ ctx.session.set('cart') → adapter.set()
1310
1310
 
1311
1311
  ---
1312
1312
 
1313
+ ## Security: WebSocket Authentication
1314
+
1315
+ Beam uses **in-band authentication** to prevent Cross-Site WebSocket Hijacking (CSWSH) attacks. This is the pattern recommended by [capnweb](https://github.com/nickelsworth/capnweb).
1316
+
1317
+ ### The Problem
1318
+
1319
+ WebSocket connections in browsers:
1320
+ - **Always permit cross-site connections** (no CORS for WebSocket)
1321
+ - **Automatically send cookies** with the upgrade request
1322
+ - **Cannot use custom headers** for authentication
1323
+
1324
+ This means a malicious site could open a WebSocket to your server, and the browser would send your cookies, authenticating the attacker.
1325
+
1326
+ ### The Solution: In-Band Authentication
1327
+
1328
+ Instead of relying on cookies, Beam requires clients to authenticate explicitly:
1329
+
1330
+ 1. **Server generates a short-lived token** (embedded in same-origin page)
1331
+ 2. **WebSocket connects unauthenticated** (gets `PublicApi`)
1332
+ 3. **Client calls `authenticate(token)`** to get the full API
1333
+ 4. **Malicious sites can't get the token** (CORS blocks page requests)
1334
+
1335
+ ### Setup
1336
+
1337
+ #### 1. Enable Sessions (Required)
1338
+
1339
+ The auth token is tied to sessions:
1340
+
1341
+ ```typescript
1342
+ // vite.config.ts
1343
+ beamPlugin({
1344
+ actions: '/app/actions/*.tsx',
1345
+ modals: '/app/modals/*.tsx',
1346
+ session: true, // Uses env.SESSION_SECRET
1347
+ })
1348
+ ```
1349
+
1350
+ #### 2. Use authMiddleware
1351
+
1352
+ ```typescript
1353
+ // app/server.ts
1354
+ import { createApp } from 'honox/server'
1355
+ import { beam } from 'virtual:beam'
1356
+
1357
+ const app = createApp({
1358
+ init(app) {
1359
+ app.use('*', beam.authMiddleware()) // Generates token
1360
+ beam.init(app)
1361
+ }
1362
+ })
1363
+
1364
+ export default app
1365
+ ```
1366
+
1367
+ #### 3. Inject Token in Layout
1368
+
1369
+ ```tsx
1370
+ // app/routes/_renderer.tsx
1371
+ import { jsxRenderer } from 'hono/jsx-renderer'
1372
+
1373
+ export default jsxRenderer((c, { children }) => {
1374
+ const token = c.get('beamAuthToken')
1375
+
1376
+ return (
1377
+ <html>
1378
+ <head>
1379
+ <meta name="beam-token" content={token} />
1380
+ <script type="module" src="/app/client.ts"></script>
1381
+ </head>
1382
+ <body>{children}</body>
1383
+ </html>
1384
+ )
1385
+ })
1386
+ ```
1387
+
1388
+ Or use the helper:
1389
+
1390
+ ```tsx
1391
+ import { beamTokenMeta } from '@benqoder/beam'
1392
+ import { Raw } from 'hono/html'
1393
+
1394
+ <head>
1395
+ <Raw html={beamTokenMeta(c.get('beamAuthToken'))} />
1396
+ </head>
1397
+ ```
1398
+
1399
+ #### 4. Set Environment Variable
1400
+
1401
+ ```bash
1402
+ # .dev.vars (local) or Cloudflare dashboard (production)
1403
+ SESSION_SECRET=your-secret-key-at-least-32-chars
1404
+ ```
1405
+
1406
+ ### How It Works
1407
+
1408
+ | Step | What Happens |
1409
+ |------|--------------|
1410
+ | 1. Page Load | Server generates 5-minute token, embeds in HTML |
1411
+ | 2. Client Connects | WebSocket opens, gets `PublicApi` (unauthenticated) |
1412
+ | 3. Client Authenticates | Calls `publicApi.authenticate(token)` |
1413
+ | 4. Server Validates | Verifies signature, expiration, session match |
1414
+ | 5. Server Returns | Full `BeamServer` API (authenticated) |
1415
+
1416
+ ### Security Properties
1417
+
1418
+ | Attack | Result |
1419
+ |--------|--------|
1420
+ | Cross-site WebSocket | Can connect, but `authenticate()` fails (no token) |
1421
+ | Stolen token | Expires in 5 minutes, tied to session ID |
1422
+ | Replay attack | Token is single-use per session |
1423
+ | Token tampering | HMAC-SHA256 signature verification fails |
1424
+
1425
+ ### Token Details
1426
+
1427
+ - **Algorithm**: HMAC-SHA256
1428
+ - **Lifetime**: 5 minutes (configurable)
1429
+ - **Payload**: `{ sid: sessionId, uid: userId, exp: timestamp }`
1430
+ - **Format**: `base64(payload).base64(signature)`
1431
+
1432
+ ### Generating Tokens Manually
1433
+
1434
+ If you need to generate tokens outside the middleware:
1435
+
1436
+ ```typescript
1437
+ const token = await beam.generateAuthToken(ctx)
1438
+ ```
1439
+
1440
+ ---
1441
+
1313
1442
  ## Programmatic API
1314
1443
 
1315
1444
  Call actions directly from JavaScript using `window.beam`:
package/dist/client.d.ts CHANGED
@@ -3,6 +3,8 @@ interface ActionResponse {
3
3
  html?: string;
4
4
  script?: string;
5
5
  redirect?: string;
6
+ target?: string;
7
+ swap?: string;
6
8
  }
7
9
  interface BeamServer {
8
10
  call(action: string, data?: Record<string, unknown>): Promise<ActionResponse>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AAkB9D,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAGD,UAAU,UAAU;IAClB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC7E,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACzE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClF;AAGD,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAiuBzC,iBAAS,UAAU,IAAI,IAAI,CAU1B;AA2CD,iBAAS,WAAW,IAAI,IAAI,CAU3B;AAID,iBAAS,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,SAAS,GAAG,OAAmB,GAAG,IAAI,CAsB/E;AAwqBD,iBAAS,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAUzC;AAogBD,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAMD,iBAAS,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0B9D;AAGD,QAAA,MAAM,SAAS;;;;;;;sBAr7DO,OAAO,CAAC,cAAc,CAAC;CA67D5C,CAAA;AAGD,KAAK,YAAY,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;AAE/G,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,EAAE,OAAO,SAAS,GAAG;YACvB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAA;SAC/B,CAAA;KACF;CACF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AA8B9D,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAGD,UAAU,UAAU;IAClB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC7E,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACzE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClF;AAQD,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAivBzC,iBAAS,UAAU,IAAI,IAAI,CAU1B;AA2CD,iBAAS,WAAW,IAAI,IAAI,CAU3B;AAID,iBAAS,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,SAAS,GAAG,OAAmB,GAAG,IAAI,CAsB/E;AAwqBD,iBAAS,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAUzC;AAogBD,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAMD,iBAAS,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0B9D;AAGD,QAAA,MAAM,SAAS;;;;;;;sBAz7DO,OAAO,CAAC,cAAc,CAAC;CAi8D5C,CAAA;AAGD,KAAK,YAAY,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;AAE/G,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,EAAE,OAAO,SAAS,GAAG;YACvB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAA;SAC/B,CAAA;KACF;CACF"}
package/dist/client.js CHANGED
@@ -7,12 +7,23 @@ import { newWebSocketRpcSession } from 'capnweb';
7
7
  // - Bidirectional RPC (server can call client callbacks)
8
8
  // - Automatic reconnection
9
9
  // - Type-safe method calls
10
+ //
11
+ // SECURITY: Implements in-band authentication pattern
12
+ // - WebSocket connections start unauthenticated (PublicApi)
13
+ // - Client must call authenticate(token) to get AuthenticatedApi
14
+ // - Token is obtained from same-origin page (prevents CSWSH attacks)
10
15
  // Get endpoint from meta tag or default to /beam
11
16
  // Usage: <meta name="beam-endpoint" content="/custom-endpoint">
12
17
  function getEndpoint() {
13
18
  const meta = document.querySelector('meta[name="beam-endpoint"]');
14
19
  return meta?.getAttribute('content') ?? '/beam';
15
20
  }
21
+ // Get auth token from meta tag
22
+ // Usage: <meta name="beam-token" content="...">
23
+ function getAuthToken() {
24
+ const meta = document.querySelector('meta[name="beam-token"]');
25
+ return meta?.getAttribute('content') ?? '';
26
+ }
16
27
  let isOnline = navigator.onLine;
17
28
  let rpcSession = null;
18
29
  let connectingPromise = null;
@@ -38,27 +49,36 @@ function connect() {
38
49
  if (rpcSession) {
39
50
  return Promise.resolve(rpcSession);
40
51
  }
41
- connectingPromise = new Promise((resolve, reject) => {
52
+ connectingPromise = (async () => {
42
53
  try {
43
54
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
44
55
  const endpoint = getEndpoint();
45
56
  const url = `${protocol}//${location.host}${endpoint}`;
46
- // Create capnweb RPC session with BeamServer type
47
- const session = newWebSocketRpcSession(url);
57
+ // Get auth token from page (proves same-origin access)
58
+ const token = getAuthToken();
59
+ if (!token) {
60
+ throw new Error('No auth token found. Ensure <meta name="beam-token" content="..."> is set.');
61
+ }
62
+ // Create capnweb RPC session - starts with PublicBeamServer
63
+ const publicSession = newWebSocketRpcSession(url);
64
+ // Authenticate to get the full BeamServer API
65
+ // This is the capnweb in-band authentication pattern
66
+ // @ts-ignore - capnweb stub methods are dynamically typed
67
+ const authenticatedSession = await publicSession.authenticate(token);
48
68
  // Register client callback for bidirectional communication
49
69
  // @ts-ignore - capnweb stub methods are dynamically typed
50
- session.registerCallback?.(handleServerEvent)?.catch?.(() => {
70
+ authenticatedSession.registerCallback?.(handleServerEvent)?.catch?.(() => {
51
71
  // Server may not support callbacks, that's ok
52
72
  });
53
- rpcSession = session;
73
+ rpcSession = authenticatedSession;
54
74
  connectingPromise = null;
55
- resolve(session);
75
+ return authenticatedSession;
56
76
  }
57
77
  catch (err) {
58
78
  connectingPromise = null;
59
- reject(err);
79
+ throw err;
60
80
  }
61
- });
81
+ })();
62
82
  return connectingPromise;
63
83
  }
64
84
  async function ensureConnected() {
@@ -452,8 +472,8 @@ function parseOobSwaps(html) {
452
472
  }
453
473
  // ============ RPC WRAPPER ============
454
474
  async function rpc(action, data, el) {
455
- const targetSelector = el.getAttribute('beam-target');
456
- const swapMode = el.getAttribute('beam-swap') || 'morph';
475
+ const frontendTarget = el.getAttribute('beam-target');
476
+ const frontendSwap = el.getAttribute('beam-swap') || 'morph';
457
477
  const opt = optimistic(el);
458
478
  const placeholder = showPlaceholder(el);
459
479
  setLoading(el, true, action, data);
@@ -464,6 +484,9 @@ async function rpc(action, data, el) {
464
484
  location.href = response.redirect;
465
485
  return;
466
486
  }
487
+ // Server target/swap override frontend values
488
+ const targetSelector = response.target || frontendTarget;
489
+ const swapMode = response.swap || frontendSwap;
467
490
  // Handle HTML (if present)
468
491
  if (response.html && targetSelector) {
469
492
  const target = $(targetSelector);
@@ -1826,11 +1849,14 @@ window.beam = new Proxy(beamUtils, {
1826
1849
  const opts = typeof options === 'string'
1827
1850
  ? { target: options }
1828
1851
  : (options || {});
1852
+ // Server target/swap override frontend options
1853
+ const targetSelector = response.target || opts.target;
1854
+ const swapMode = response.swap || opts.swap || 'morph';
1829
1855
  // Handle HTML swap if target provided
1830
- if (response.html && opts.target) {
1831
- const targetEl = document.querySelector(opts.target);
1856
+ if (response.html && targetSelector) {
1857
+ const targetEl = document.querySelector(targetSelector);
1832
1858
  if (targetEl) {
1833
- swap(targetEl, response.html, opts.swap || 'morph');
1859
+ swap(targetEl, response.html, swapMode);
1834
1860
  }
1835
1861
  }
1836
1862
  // Execute script if present
@@ -1,5 +1,5 @@
1
1
  import { RpcTarget } from 'capnweb';
2
- import type { ActionHandler, ActionResponse, ModalHandler, DrawerHandler, BeamConfig, BeamInstance, BeamContext, BeamSession } from './types';
2
+ import type { ActionHandler, ActionResponse, ModalHandler, DrawerHandler, BeamConfig, BeamInstance, BeamContext, BeamSession, SessionConfig } from './types';
3
3
  /**
4
4
  * Session implementation using KV storage.
5
5
  * Exported for users who need custom storage adapter.
@@ -100,5 +100,52 @@ declare class BeamServer<TEnv extends object> extends RpcTarget {
100
100
  * ```
101
101
  */
102
102
  export declare function createBeam<TEnv extends object = object>(config: BeamConfig<TEnv>): BeamInstance<TEnv>;
103
- export { BeamServer };
103
+ /**
104
+ * Public Beam RPC Server - initial unauthenticated API
105
+ *
106
+ * This follows the capnweb in-band authentication pattern:
107
+ * - WebSocket connections start with this unauthenticated API
108
+ * - Client calls authenticate(token) to get the authenticated BeamServer
109
+ * - This prevents Cross-Site WebSocket Hijacking (CSWSH) attacks
110
+ */
111
+ declare class PublicBeamServer<TEnv extends object> extends RpcTarget {
112
+ private secret;
113
+ private sessionConfig;
114
+ private env;
115
+ private request;
116
+ private actions;
117
+ private modals;
118
+ private drawers;
119
+ private auth;
120
+ constructor(secret: string, sessionConfig: SessionConfig<TEnv> | undefined, env: TEnv, request: Request, actions: Record<string, ActionHandler<TEnv>>, modals: Record<string, ModalHandler<TEnv>>, drawers: Record<string, DrawerHandler<TEnv>>, auth: ((request: Request, env: TEnv) => Promise<import('./types').BeamUser | null>) | undefined);
121
+ /**
122
+ * Authenticate with a token and return the authenticated API
123
+ * This is the only method available on the public API
124
+ */
125
+ authenticate(token: string): Promise<BeamServer<TEnv>>;
126
+ }
127
+ /**
128
+ * Generate the auth token meta tag HTML for in-band WebSocket authentication.
129
+ * Call this in your layout/page to inject the token.
130
+ *
131
+ * @example
132
+ * ```tsx
133
+ * // app/routes/_renderer.tsx
134
+ * import { beamTokenMeta } from '@benqoder/beam'
135
+ *
136
+ * export default defineRenderer((c, { Layout, children }) => {
137
+ * const token = c.get('beamAuthToken')
138
+ * return (
139
+ * <html>
140
+ * <head>
141
+ * <RawHTML>{beamTokenMeta(token)}</RawHTML>
142
+ * </head>
143
+ * <body>{children}</body>
144
+ * </html>
145
+ * )
146
+ * })
147
+ * ```
148
+ */
149
+ export declare function beamTokenMeta(token: string): string;
150
+ export { BeamServer, PublicBeamServer };
104
151
  //# sourceMappingURL=createBeam.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createBeam.d.ts","sourceRoot":"","sources":["../src/createBeam.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"createBeam.d.ts","sourceRoot":"","sources":["../src/createBeam.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,43 @@
1
1
  import { getSignedCookie, setSignedCookie } from 'hono/cookie';
2
2
  import { RpcTarget, newWorkersRpcResponse } from 'capnweb';
3
+ /** Default token lifetime: 5 minutes */
4
+ const DEFAULT_TOKEN_LIFETIME = 5 * 60 * 1000;
5
+ /**
6
+ * Sign an auth token payload using HMAC-SHA256
7
+ */
8
+ async function signToken(payload, secret) {
9
+ const encoder = new TextEncoder();
10
+ const data = JSON.stringify(payload);
11
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
12
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
13
+ const sigBase64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
14
+ return `${btoa(data)}.${sigBase64}`;
15
+ }
16
+ /**
17
+ * Verify and decode an auth token
18
+ */
19
+ async function verifyToken(token, secret) {
20
+ try {
21
+ const [dataBase64, sigBase64] = token.split('.');
22
+ if (!dataBase64 || !sigBase64)
23
+ return null;
24
+ const data = atob(dataBase64);
25
+ const encoder = new TextEncoder();
26
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
27
+ const signature = Uint8Array.from(atob(sigBase64), c => c.charCodeAt(0));
28
+ const valid = await crypto.subtle.verify('HMAC', key, signature, encoder.encode(data));
29
+ if (!valid)
30
+ return null;
31
+ const payload = JSON.parse(data);
32
+ // Check expiration
33
+ if (payload.exp < Date.now())
34
+ return null;
35
+ return payload;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
3
41
  /**
4
42
  * Session implementation using KV storage.
5
43
  * Exported for users who need custom storage adapter.
@@ -122,9 +160,19 @@ function createBeamContext(base) {
122
160
  script: (code) => ({ script: code }),
123
161
  render: (html, options) => {
124
162
  if (html instanceof Promise) {
125
- return html.then((resolved) => ({ html: resolved, script: options?.script }));
163
+ return html.then((resolved) => ({
164
+ html: resolved,
165
+ script: options?.script,
166
+ target: options?.target,
167
+ swap: options?.swap,
168
+ }));
126
169
  }
127
- return { html, script: options?.script };
170
+ return {
171
+ html,
172
+ script: options?.script,
173
+ target: options?.target,
174
+ swap: options?.swap,
175
+ };
128
176
  },
129
177
  redirect: (url) => ({ redirect: url }),
130
178
  };
@@ -323,8 +371,22 @@ export function createBeam(config) {
323
371
  request: c.req.raw,
324
372
  session,
325
373
  });
374
+ // Generate auth token for in-band WebSocket authentication
375
+ const secret = sessionConfig?.secretEnvKey
376
+ ? c.env[sessionConfig.secretEnvKey]
377
+ : sessionConfig?.secret;
378
+ let authToken = '';
379
+ if (secret && sessionId) {
380
+ const tokenPayload = {
381
+ sid: sessionId,
382
+ uid: user?.id ?? null,
383
+ exp: Date.now() + DEFAULT_TOKEN_LIFETIME,
384
+ };
385
+ authToken = await signToken(tokenPayload, secret);
386
+ }
326
387
  // Set in Hono context for use by routes
327
388
  c.set('beam', ctx);
389
+ c.set('beamAuthToken', authToken);
328
390
  await next();
329
391
  // If using cookie session and data was modified, save it back to cookie
330
392
  if (cookieSession && cookieSession.isDirty() && sessionConfig) {
@@ -341,10 +403,46 @@ export function createBeam(config) {
341
403
  }
342
404
  };
343
405
  },
406
+ /**
407
+ * Generate a short-lived auth token for in-band WebSocket authentication.
408
+ * Use this when you need to generate a token outside of the authMiddleware.
409
+ *
410
+ * @example
411
+ * ```typescript
412
+ * const token = await beam.generateAuthToken(ctx)
413
+ * // Embed in page: <meta name="beam-token" content="${token}">
414
+ * ```
415
+ */
416
+ async generateAuthToken(ctx) {
417
+ if (!sessionConfig) {
418
+ throw new Error('Session config is required for auth token generation');
419
+ }
420
+ const secret = sessionConfig.secretEnvKey
421
+ ? ctx.env[sessionConfig.secretEnvKey]
422
+ : sessionConfig.secret;
423
+ if (!secret) {
424
+ throw new Error('Session secret is required for auth token generation');
425
+ }
426
+ // Get session ID from request cookies
427
+ const sessionId = parseSessionFromRequest(ctx.request, cookieName);
428
+ if (!sessionId) {
429
+ throw new Error('No session found - ensure authMiddleware is used');
430
+ }
431
+ const tokenPayload = {
432
+ sid: sessionId,
433
+ uid: ctx.user?.id ?? null,
434
+ exp: Date.now() + DEFAULT_TOKEN_LIFETIME,
435
+ };
436
+ return signToken(tokenPayload, secret);
437
+ },
344
438
  /**
345
439
  * Init function for HonoX createApp().
346
440
  * Registers the WebSocket RPC endpoint using capnweb.
347
441
  *
442
+ * SECURITY: Uses in-band authentication pattern to prevent CSWSH attacks.
443
+ * WebSocket connections start unauthenticated, client must call authenticate(token)
444
+ * with a valid token obtained from a same-origin page request.
445
+ *
348
446
  * @example
349
447
  * ```typescript
350
448
  * const app = createApp({
@@ -362,60 +460,121 @@ export function createBeam(config) {
362
460
  if (upgradeHeader !== 'websocket') {
363
461
  return c.text('Expected WebSocket', 426);
364
462
  }
365
- // Try to get context from middleware, otherwise resolve fresh
366
- let ctx;
367
- const existingCtx = c.var.beam;
368
- if (existingCtx) {
369
- ctx = existingCtx;
463
+ // Get the session secret for token verification
464
+ const secret = sessionConfig?.secretEnvKey
465
+ ? c.env[sessionConfig.secretEnvKey]
466
+ : sessionConfig?.secret;
467
+ if (!secret) {
468
+ return c.text('Session secret is required for secure WebSocket connections', 500);
370
469
  }
371
- else {
372
- // Resolve auth
373
- const user = auth ? await auth(c.req.raw, c.env) : null;
374
- // Resolve session for WebSocket (cookie storage is read-only in WebSocket context)
375
- let session;
376
- if (sessionConfig) {
377
- const sessionId = parseSessionFromRequest(c.req.raw, cookieName);
378
- if (sessionId) {
379
- // Use custom storage factory if provided
380
- if (sessionConfig.storageFactory) {
381
- session = sessionConfig.storageFactory(sessionId, c.env);
382
- }
383
- else {
384
- // Default: cookie-based session (read-only - can't set cookies in WebSocket)
385
- const existingData = parseSessionDataFromRequest(c.req.raw);
386
- session = new CookieSession(existingData);
387
- }
388
- }
389
- else {
390
- // No session cookie - provide noop (rare edge case)
391
- session = {
392
- get: async () => null,
393
- set: async () => { },
394
- delete: async () => { },
395
- };
396
- }
397
- }
398
- else {
399
- session = {
400
- get: async () => null,
401
- set: async () => { },
402
- delete: async () => { },
403
- };
404
- }
405
- ctx = createBeamContext({
406
- env: c.env,
407
- user,
408
- request: c.req.raw,
409
- session,
410
- });
411
- }
412
- // Create BeamServer instance with capnweb RpcTarget
413
- const server = new BeamServer(ctx, actions, modals, drawers);
470
+ // Create PublicBeamServer - client must authenticate to get full API
471
+ const server = new PublicBeamServer(secret, sessionConfig, c.env, c.req.raw, actions, modals, drawers, auth);
414
472
  // Use capnweb to handle the RPC connection
415
473
  return newWorkersRpcResponse(c.req.raw, server);
416
474
  });
417
475
  },
418
476
  };
419
477
  }
478
+ /**
479
+ * Public Beam RPC Server - initial unauthenticated API
480
+ *
481
+ * This follows the capnweb in-band authentication pattern:
482
+ * - WebSocket connections start with this unauthenticated API
483
+ * - Client calls authenticate(token) to get the authenticated BeamServer
484
+ * - This prevents Cross-Site WebSocket Hijacking (CSWSH) attacks
485
+ */
486
+ class PublicBeamServer extends RpcTarget {
487
+ secret;
488
+ sessionConfig;
489
+ env;
490
+ request;
491
+ actions;
492
+ modals;
493
+ drawers;
494
+ auth;
495
+ constructor(secret, sessionConfig, env, request, actions, modals, drawers, auth) {
496
+ super();
497
+ this.secret = secret;
498
+ this.sessionConfig = sessionConfig;
499
+ this.env = env;
500
+ this.request = request;
501
+ this.actions = actions;
502
+ this.modals = modals;
503
+ this.drawers = drawers;
504
+ this.auth = auth;
505
+ }
506
+ /**
507
+ * Authenticate with a token and return the authenticated API
508
+ * This is the only method available on the public API
509
+ */
510
+ async authenticate(token) {
511
+ // Verify the token
512
+ const payload = await verifyToken(token, this.secret);
513
+ if (!payload) {
514
+ throw new Error('Invalid or expired auth token');
515
+ }
516
+ // Resolve auth (user info is embedded in token, but we re-resolve for fresh data)
517
+ const user = this.auth ? await this.auth(this.request, this.env) : null;
518
+ // Resolve session
519
+ let session;
520
+ if (this.sessionConfig) {
521
+ const cookieName = this.sessionConfig.cookieName ?? 'beam_sid';
522
+ const sessionId = parseSessionFromRequest(this.request, cookieName);
523
+ // Verify session ID matches token
524
+ if (sessionId !== payload.sid) {
525
+ throw new Error('Session mismatch');
526
+ }
527
+ if (sessionId && this.sessionConfig.storageFactory) {
528
+ session = this.sessionConfig.storageFactory(sessionId, this.env);
529
+ }
530
+ else if (sessionId) {
531
+ const existingData = parseSessionDataFromRequest(this.request);
532
+ session = new CookieSession(existingData);
533
+ }
534
+ else {
535
+ session = { get: async () => null, set: async () => { }, delete: async () => { } };
536
+ }
537
+ }
538
+ else {
539
+ session = { get: async () => null, set: async () => { }, delete: async () => { } };
540
+ }
541
+ // Create authenticated context
542
+ const ctx = createBeamContext({
543
+ env: this.env,
544
+ user,
545
+ request: this.request,
546
+ session,
547
+ });
548
+ // Return the authenticated BeamServer
549
+ return new BeamServer(ctx, this.actions, this.modals, this.drawers);
550
+ }
551
+ }
552
+ /**
553
+ * Generate the auth token meta tag HTML for in-band WebSocket authentication.
554
+ * Call this in your layout/page to inject the token.
555
+ *
556
+ * @example
557
+ * ```tsx
558
+ * // app/routes/_renderer.tsx
559
+ * import { beamTokenMeta } from '@benqoder/beam'
560
+ *
561
+ * export default defineRenderer((c, { Layout, children }) => {
562
+ * const token = c.get('beamAuthToken')
563
+ * return (
564
+ * <html>
565
+ * <head>
566
+ * <RawHTML>{beamTokenMeta(token)}</RawHTML>
567
+ * </head>
568
+ * <body>{children}</body>
569
+ * </html>
570
+ * )
571
+ * })
572
+ * ```
573
+ */
574
+ export function beamTokenMeta(token) {
575
+ // Escape any quotes in the token for safe HTML embedding
576
+ const escapedToken = token.replace(/"/g, '&quot;');
577
+ return `<meta name="beam-token" content="${escapedToken}">`;
578
+ }
420
579
  // Export BeamServer for advanced usage (e.g., extending with custom methods)
421
- export { BeamServer };
580
+ export { BeamServer, PublicBeamServer };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { createBeam, KVSession, CookieSession } from './createBeam';
1
+ export { createBeam, KVSession, CookieSession, beamTokenMeta } from './createBeam';
2
2
  export { render } from './render';
3
3
  export { ModalFrame } from './ModalFrame';
4
4
  export { DrawerFrame } from './DrawerFrame';
5
5
  export { collectActions, collectModals, collectDrawers, collectHandlers, } from './collect';
6
- export type { ActionHandler, ModalHandler, DrawerHandler, BeamConfig, BeamInstance, BeamInitOptions, BeamUser, BeamContext, BeamVariables, AuthResolver, BeamSession, SessionConfig, SessionStorageFactory, } from './types';
6
+ export type { ActionHandler, ModalHandler, DrawerHandler, BeamConfig, BeamInstance, BeamInitOptions, BeamUser, BeamContext, BeamVariables, AuthResolver, BeamSession, SessionConfig, SessionStorageFactory, AuthTokenPayload, } from './types';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAG3C,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,WAAW,CAAA;AAGlB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,WAAW,EACX,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,qBAAqB,GACtB,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAClF,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAG3C,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,WAAW,CAAA;AAGlB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,WAAW,EACX,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,SAAS,CAAA"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Main server-side exports for @benqoder/beam
2
- export { createBeam, KVSession, CookieSession } from './createBeam';
2
+ export { createBeam, KVSession, CookieSession, beamTokenMeta } from './createBeam';
3
3
  export { render } from './render';
4
4
  export { ModalFrame } from './ModalFrame';
5
5
  export { DrawerFrame } from './DrawerFrame';
package/dist/types.d.ts CHANGED
@@ -24,6 +24,10 @@ export interface BeamSession {
24
24
  export interface RenderOptions {
25
25
  /** JavaScript to execute on client after rendering */
26
26
  script?: string;
27
+ /** CSS selector for target element (overrides frontend target) */
28
+ target?: string;
29
+ /** Swap mode: 'morph' | 'replace' | 'append' | 'prepend' | 'delete' */
30
+ swap?: string;
27
31
  }
28
32
  /**
29
33
  * Context passed to all handlers
@@ -54,6 +58,25 @@ export interface BeamContext<TEnv = object> {
54
58
  * Auth resolver function - user provides this to extract user from request
55
59
  */
56
60
  export type AuthResolver<TEnv = object> = (request: Request, env: TEnv) => Promise<BeamUser | null>;
61
+ /**
62
+ * Auth token payload - signed and short-lived
63
+ * Used for secure in-band WebSocket authentication
64
+ */
65
+ export interface AuthTokenPayload {
66
+ /** Session ID */
67
+ sid: string;
68
+ /** User ID (null for guest) */
69
+ uid: string | null;
70
+ /** Expiration timestamp (ms) */
71
+ exp: number;
72
+ }
73
+ /**
74
+ * Auth token configuration
75
+ */
76
+ export interface AuthTokenConfig {
77
+ /** Token lifetime in milliseconds (default: 5 minutes) */
78
+ tokenLifetime?: number;
79
+ }
57
80
  /**
58
81
  * Response type for actions - can include HTML and/or script to execute
59
82
  */
@@ -64,6 +87,10 @@ export interface ActionResponse {
64
87
  script?: string;
65
88
  /** URL to redirect to (optional) */
66
89
  redirect?: string;
90
+ /** CSS selector for target element (optional - overrides frontend target) */
91
+ target?: string;
92
+ /** Swap mode: 'morph' | 'replace' | 'append' | 'prepend' | 'delete' (optional) */
93
+ swap?: string;
67
94
  }
68
95
  /**
69
96
  * Type for action handlers - receives context and data, returns ActionResponse
@@ -128,6 +155,8 @@ export interface BeamInitOptions {
128
155
  */
129
156
  export interface BeamVariables<TEnv = object> {
130
157
  beam: BeamContext<TEnv>;
158
+ /** Short-lived auth token for in-band WebSocket authentication */
159
+ beamAuthToken: string;
131
160
  }
132
161
  /**
133
162
  * The Beam instance returned by createBeam
@@ -147,5 +176,12 @@ export interface BeamInstance<TEnv extends object = object> {
147
176
  Bindings: TEnv;
148
177
  Variables: BeamVariables<TEnv>;
149
178
  }>;
179
+ /**
180
+ * Generate a short-lived auth token for in-band WebSocket authentication.
181
+ * This token should be embedded in the page and used by the client to authenticate.
182
+ * @param ctx - The Beam context (from authMiddleware)
183
+ * @returns A signed, short-lived token string
184
+ */
185
+ generateAuthToken: (ctx: BeamContext<TEnv>) => Promise<string>;
150
186
  }
151
187
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benqoder/beam",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
@@ -51,7 +51,7 @@
51
51
  }
52
52
  },
53
53
  "devDependencies": {
54
- "@cloudflare/workers-types": "^4.20241127.0",
54
+ "@cloudflare/workers-types": "^4.20260124.0",
55
55
  "capnweb": "^0.4.0",
56
56
  "hono": "^4.6.0",
57
57
  "honox": "^0.1.0",