@benqoder/beam 0.1.1 → 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 +129 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +39 -13
- package/dist/createBeam.d.ts +49 -2
- package/dist/createBeam.d.ts.map +1 -1
- package/dist/createBeam.js +210 -51
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
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
package/dist/client.d.ts.map
CHANGED
|
@@ -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;
|
|
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 =
|
|
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
|
-
//
|
|
47
|
-
const
|
|
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
|
-
|
|
70
|
+
authenticatedSession.registerCallback?.(handleServerEvent)?.catch?.(() => {
|
|
51
71
|
// Server may not support callbacks, that's ok
|
|
52
72
|
});
|
|
53
|
-
rpcSession =
|
|
73
|
+
rpcSession = authenticatedSession;
|
|
54
74
|
connectingPromise = null;
|
|
55
|
-
|
|
75
|
+
return authenticatedSession;
|
|
56
76
|
}
|
|
57
77
|
catch (err) {
|
|
58
78
|
connectingPromise = null;
|
|
59
|
-
|
|
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
|
|
456
|
-
const
|
|
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 &&
|
|
1831
|
-
const targetEl = document.querySelector(
|
|
1856
|
+
if (response.html && targetSelector) {
|
|
1857
|
+
const targetEl = document.querySelector(targetSelector);
|
|
1832
1858
|
if (targetEl) {
|
|
1833
|
-
swap(targetEl, response.html,
|
|
1859
|
+
swap(targetEl, response.html, swapMode);
|
|
1834
1860
|
}
|
|
1835
1861
|
}
|
|
1836
1862
|
// Execute script if present
|
package/dist/createBeam.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/createBeam.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createBeam.d.ts","sourceRoot":"","sources":["../src/createBeam.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAyB,MAAM,SAAS,CAAA;AAC1D,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,WAAW,EAEX,WAAW,
|
|
1
|
+
{"version":3,"file":"createBeam.d.ts","sourceRoot":"","sources":["../src/createBeam.ts"],"names":[],"mappings":"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"}
|
package/dist/createBeam.js
CHANGED
|
@@ -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) => ({
|
|
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 {
|
|
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
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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, '"');
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAEnD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,6BAA6B;IAC7B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAChD,2BAA2B;IAC3B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,gCAAgC;IAChC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;
|
|
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.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"capnweb": "^0.4.0",
|
|
43
43
|
"hono": "^4.0.0",
|
|
44
44
|
"honox": "^0.1.0",
|
|
45
|
-
"idiomorph": "^0.
|
|
45
|
+
"idiomorph": "^0.7.0",
|
|
46
46
|
"vite": "^5.0.0 || ^6.0.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@cloudflare/workers-types": "^4.
|
|
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",
|
|
58
|
-
"idiomorph": "^0.
|
|
58
|
+
"idiomorph": "^0.7.4",
|
|
59
59
|
"typescript": "^5.0.0",
|
|
60
60
|
"vite": "^6.0.0"
|
|
61
61
|
}
|