@benqoder/beam 0.1.2 → 0.2.0
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 +308 -89
- package/dist/client.d.ts +14 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +307 -129
- package/dist/collect.d.ts +1 -47
- package/dist/collect.d.ts.map +1 -1
- package/dist/collect.js +0 -64
- package/dist/createBeam.d.ts +50 -17
- package/dist/createBeam.d.ts.map +1 -1
- package/dist/createBeam.js +265 -86
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/types.d.ts +83 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/vite.d.ts +0 -12
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +4 -10
- package/package.json +2 -2
- package/src/beam.css +2 -0
- package/dist/DrawerFrame.d.ts +0 -16
- package/dist/DrawerFrame.d.ts.map +0 -1
- package/dist/DrawerFrame.js +0 -12
- package/dist/ModalFrame.d.ts +0 -12
- package/dist/ModalFrame.d.ts.map +0 -1
- package/dist/ModalFrame.js +0 -8
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ A lightweight, declarative UI framework for building interactive web application
|
|
|
28
28
|
- **Dropdowns** - Click-outside closing, Escape key support (no server)
|
|
29
29
|
- **Collapse** - Expand/collapse with text swap (no server)
|
|
30
30
|
- **Class Toggle** - Toggle CSS classes on elements (no server)
|
|
31
|
+
- **Multi-Render** - Update multiple targets in a single action response
|
|
32
|
+
- **Async Components** - Full support for HonoX async components in `ctx.render()`
|
|
31
33
|
|
|
32
34
|
## Installation
|
|
33
35
|
|
|
@@ -47,8 +49,6 @@ export default defineConfig({
|
|
|
47
49
|
plugins: [
|
|
48
50
|
beamPlugin({
|
|
49
51
|
actions: './actions/*.tsx',
|
|
50
|
-
modals: './modals/*.tsx',
|
|
51
|
-
drawers: './drawers/*.tsx',
|
|
52
52
|
}),
|
|
53
53
|
],
|
|
54
54
|
})
|
|
@@ -123,57 +123,176 @@ export function greet(c) {
|
|
|
123
123
|
|
|
124
124
|
### Modals
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
Two ways to open modals:
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
// app/modals/confirm.tsx
|
|
130
|
-
import { ModalFrame } from '@benqoder/beam'
|
|
128
|
+
**1. `beam-modal` attribute** - Explicitly opens the action result in a modal, with optional placeholder:
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
```html
|
|
131
|
+
<!-- Shows placeholder while loading, then replaces with action result -->
|
|
132
|
+
<button beam-modal="confirmDelete" beam-data-id="123" beam-size="small"
|
|
133
|
+
beam-placeholder="<div>Loading...</div>">
|
|
134
|
+
Delete Item
|
|
135
|
+
</button>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**2. `beam-action` with `ctx.modal()`** - Action decides to return a modal:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
// app/actions/confirm.tsx
|
|
142
|
+
export function confirmDelete(ctx: BeamContext<Env>, { id }: Record<string, unknown>) {
|
|
143
|
+
return ctx.modal(
|
|
144
|
+
<div>
|
|
145
|
+
<h2>Confirm Delete</h2>
|
|
136
146
|
<p>Are you sure you want to delete item {id}?</p>
|
|
137
|
-
<button beam-action="deleteItem" beam-data-id={id} beam-close>
|
|
138
|
-
Delete
|
|
139
|
-
</button>
|
|
147
|
+
<button beam-action="deleteItem" beam-data-id={id} beam-close>Delete</button>
|
|
140
148
|
<button beam-close>Cancel</button>
|
|
141
|
-
</
|
|
142
|
-
)
|
|
149
|
+
</div>
|
|
150
|
+
, { size: 'small' })
|
|
143
151
|
}
|
|
144
152
|
```
|
|
145
153
|
|
|
154
|
+
`ctx.modal()` accepts JSX directly - no wrapper function needed. Options: `size` ('small' | 'medium' | 'large'), `spacing` (padding in pixels).
|
|
155
|
+
|
|
146
156
|
```html
|
|
147
|
-
<button beam-
|
|
148
|
-
Delete Item
|
|
149
|
-
</button>
|
|
157
|
+
<button beam-action="confirmDelete" beam-data-id="123">Delete Item</button>
|
|
150
158
|
```
|
|
151
159
|
|
|
152
160
|
### Drawers
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
Two ways to open drawers:
|
|
163
|
+
|
|
164
|
+
**1. `beam-drawer` attribute** - Explicitly opens in a drawer:
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<button beam-drawer="openCart" beam-position="right" beam-size="medium"
|
|
168
|
+
beam-placeholder="<div>Loading cart...</div>">
|
|
169
|
+
Open Cart
|
|
170
|
+
</button>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**2. `beam-action` with `ctx.drawer()`** - Action returns a drawer:
|
|
155
174
|
|
|
156
175
|
```tsx
|
|
157
|
-
// app/
|
|
158
|
-
|
|
176
|
+
// app/actions/cart.tsx
|
|
177
|
+
export function openCart(ctx: BeamContext<Env>) {
|
|
178
|
+
return ctx.drawer(
|
|
179
|
+
<div>
|
|
180
|
+
<h2>Shopping Cart</h2>
|
|
181
|
+
<div class="cart-items">{/* Cart contents */}</div>
|
|
182
|
+
<button beam-close>Close</button>
|
|
183
|
+
</div>
|
|
184
|
+
, { position: 'right', size: 'medium' })
|
|
185
|
+
}
|
|
186
|
+
```
|
|
159
187
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
`ctx.drawer()` accepts JSX directly. Options: `position` ('left' | 'right'), `size` ('small' | 'medium' | 'large'), `spacing` (padding in pixels).
|
|
189
|
+
|
|
190
|
+
```html
|
|
191
|
+
<button beam-action="openCart">Open Cart</button>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Multi-Render Array API
|
|
195
|
+
|
|
196
|
+
Update multiple targets in a single action response using `ctx.render()` with arrays:
|
|
197
|
+
|
|
198
|
+
**1. Explicit targets (comma-separated)**
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
export function refreshDashboard(ctx: BeamContext<Env>) {
|
|
202
|
+
return ctx.render(
|
|
203
|
+
[
|
|
204
|
+
<div class="stat-card">Visits: {visits}</div>,
|
|
205
|
+
<div class="stat-card">Users: {users}</div>,
|
|
206
|
+
<div class="stat-card">Revenue: ${revenue}</div>,
|
|
207
|
+
],
|
|
208
|
+
{ target: '#stats, #users, #revenue' }
|
|
167
209
|
)
|
|
168
210
|
}
|
|
169
211
|
```
|
|
170
212
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
213
|
+
**2. Auto-detect by ID (no targets needed)**
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
export function refreshDashboard(ctx: BeamContext<Env>) {
|
|
217
|
+
// Client automatically finds elements by id, beam-id, or beam-item-id
|
|
218
|
+
return ctx.render([
|
|
219
|
+
<div id="stats">Visits: {visits}</div>,
|
|
220
|
+
<div id="users">Users: {users}</div>,
|
|
221
|
+
<div id="revenue">Revenue: ${revenue}</div>,
|
|
222
|
+
])
|
|
223
|
+
}
|
|
175
224
|
```
|
|
176
225
|
|
|
226
|
+
**3. Mixed approach**
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
export function updateDashboard(ctx: BeamContext<Env>) {
|
|
230
|
+
return ctx.render(
|
|
231
|
+
[
|
|
232
|
+
<div>Header content</div>, // Uses explicit target
|
|
233
|
+
<div id="content">Main content</div>, // Auto-detected by ID
|
|
234
|
+
],
|
|
235
|
+
{ target: '#header' } // Only first item gets explicit target
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Target Resolution Order:**
|
|
241
|
+
1. Explicit target from comma-separated list (by index)
|
|
242
|
+
2. ID from the HTML fragment's root element (`id`, `beam-id`, or `beam-item-id`)
|
|
243
|
+
3. Frontend fallback (`beam-target` on the triggering element)
|
|
244
|
+
4. Skip if no target found
|
|
245
|
+
|
|
246
|
+
**Exclusion:** Use `!selector` to explicitly skip an item:
|
|
247
|
+
```tsx
|
|
248
|
+
ctx.render(
|
|
249
|
+
[<Box1 />, <Box2 />, <Box3 />],
|
|
250
|
+
{ target: '#a, !#skip, #c' } // Box2 is skipped
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Async Components
|
|
255
|
+
|
|
256
|
+
`ctx.render()` fully supports HonoX async components:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
// Async component that fetches data
|
|
260
|
+
async function UserCard({ userId }: { userId: string }) {
|
|
261
|
+
const user = await db.getUser(userId) // Async data fetch
|
|
262
|
+
return (
|
|
263
|
+
<div class="user-card">
|
|
264
|
+
<h3>{user.name}</h3>
|
|
265
|
+
<p>{user.email}</p>
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Use directly in ctx.render() - no wrapper needed
|
|
271
|
+
export function loadUser(ctx: BeamContext<Env>, { id }: Record<string, unknown>) {
|
|
272
|
+
return ctx.render(<UserCard userId={id as string} />, { target: '#user' })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Works with arrays too
|
|
276
|
+
export function loadUsers(ctx: BeamContext<Env>) {
|
|
277
|
+
return ctx.render([
|
|
278
|
+
<UserCard userId="1" />,
|
|
279
|
+
<UserCard userId="2" />,
|
|
280
|
+
<UserCard userId="3" />,
|
|
281
|
+
], { target: '#user1, #user2, #user3' })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Mixed sync and async
|
|
285
|
+
export function loadDashboard(ctx: BeamContext<Env>) {
|
|
286
|
+
return ctx.render([
|
|
287
|
+
<div>Static header</div>, // Sync
|
|
288
|
+
<UserCard userId="current" />, // Async
|
|
289
|
+
<StatsWidget />, // Async
|
|
290
|
+
])
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Async components are awaited automatically - no manual `Promise.resolve()` or helper functions needed.
|
|
295
|
+
|
|
177
296
|
---
|
|
178
297
|
|
|
179
298
|
## Attribute Reference
|
|
@@ -194,21 +313,26 @@ export function shoppingCart(c) {
|
|
|
194
313
|
| `beam-push` | Push URL to browser history after action | `beam-push="/new-url"` |
|
|
195
314
|
| `beam-replace` | Replace current URL in history | `beam-replace="?page=2"` |
|
|
196
315
|
|
|
197
|
-
### Modals
|
|
316
|
+
### Modals & Drawers
|
|
198
317
|
|
|
199
318
|
| Attribute | Description | Example |
|
|
200
319
|
|-----------|-------------|---------|
|
|
201
|
-
| `beam-modal` |
|
|
202
|
-
| `beam-
|
|
320
|
+
| `beam-modal` | Action to call and display result in modal | `beam-modal="editUser"` |
|
|
321
|
+
| `beam-drawer` | Action to call and display result in drawer | `beam-drawer="openCart"` |
|
|
322
|
+
| `beam-size` | Size for modal/drawer: `small`, `medium`, `large` | `beam-size="large"` |
|
|
323
|
+
| `beam-position` | Drawer position: `left`, `right` | `beam-position="left"` |
|
|
324
|
+
| `beam-placeholder` | HTML to show while loading | `beam-placeholder="<p>Loading...</p>"` |
|
|
325
|
+
| `beam-close` | Close the current modal/drawer when clicked | `beam-close` |
|
|
203
326
|
|
|
204
|
-
|
|
327
|
+
Modals and drawers can also be returned from `beam-action` using context helpers:
|
|
205
328
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
329
|
+
```tsx
|
|
330
|
+
// Modal with options
|
|
331
|
+
return ctx.modal(render(<MyModal />), { size: 'large', spacing: 20 })
|
|
332
|
+
|
|
333
|
+
// Drawer with options
|
|
334
|
+
return ctx.drawer(render(<MyDrawer />), { position: 'left', size: 'medium' })
|
|
335
|
+
```
|
|
212
336
|
|
|
213
337
|
### Forms
|
|
214
338
|
|
|
@@ -969,44 +1093,10 @@ Creates a Beam instance with handlers:
|
|
|
969
1093
|
import { createBeam } from '@benqoder/beam'
|
|
970
1094
|
|
|
971
1095
|
const beam = createBeam<Env>({
|
|
972
|
-
actions: { increment, decrement },
|
|
973
|
-
modals: { editUser, confirmDelete },
|
|
974
|
-
drawers: { settings, cart },
|
|
1096
|
+
actions: { increment, decrement, openModal, openCart },
|
|
975
1097
|
})
|
|
976
1098
|
```
|
|
977
1099
|
|
|
978
|
-
### ModalFrame
|
|
979
|
-
|
|
980
|
-
Wrapper component for modals:
|
|
981
|
-
|
|
982
|
-
```tsx
|
|
983
|
-
import { ModalFrame } from '@benqoder/beam'
|
|
984
|
-
|
|
985
|
-
export function myModal(c) {
|
|
986
|
-
return (
|
|
987
|
-
<ModalFrame title="Modal Title">
|
|
988
|
-
<p>Modal content</p>
|
|
989
|
-
</ModalFrame>
|
|
990
|
-
)
|
|
991
|
-
}
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
### DrawerFrame
|
|
995
|
-
|
|
996
|
-
Wrapper component for drawers:
|
|
997
|
-
|
|
998
|
-
```tsx
|
|
999
|
-
import { DrawerFrame } from '@benqoder/beam'
|
|
1000
|
-
|
|
1001
|
-
export function myDrawer(c) {
|
|
1002
|
-
return (
|
|
1003
|
-
<DrawerFrame title="Drawer Title">
|
|
1004
|
-
<p>Drawer content</p>
|
|
1005
|
-
</DrawerFrame>
|
|
1006
|
-
)
|
|
1007
|
-
}
|
|
1008
|
-
```
|
|
1009
|
-
|
|
1010
1100
|
### render
|
|
1011
1101
|
|
|
1012
1102
|
Utility to render JSX to HTML string:
|
|
@@ -1023,10 +1113,8 @@ const html = render(<div>Hello</div>)
|
|
|
1023
1113
|
|
|
1024
1114
|
```typescript
|
|
1025
1115
|
beamPlugin({
|
|
1026
|
-
// Glob
|
|
1116
|
+
// Glob pattern for action handler files (must start with '/' for virtual modules)
|
|
1027
1117
|
actions: '/app/actions/*.tsx', // default
|
|
1028
|
-
modals: '/app/modals/*.tsx', // default
|
|
1029
|
-
drawers: '/app/drawers/*.tsx', // default
|
|
1030
1118
|
})
|
|
1031
1119
|
```
|
|
1032
1120
|
|
|
@@ -1037,18 +1125,22 @@ beamPlugin({
|
|
|
1037
1125
|
### Handler Types
|
|
1038
1126
|
|
|
1039
1127
|
```typescript
|
|
1040
|
-
import type { ActionHandler,
|
|
1128
|
+
import type { ActionHandler, ActionResponse, BeamContext } from '@benqoder/beam'
|
|
1129
|
+
import { render } from '@benqoder/beam'
|
|
1041
1130
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1131
|
+
// Action that returns HTML string
|
|
1132
|
+
const myAction: ActionHandler<Env> = async (ctx, params) => {
|
|
1133
|
+
return '<div>Hello</div>'
|
|
1044
1134
|
}
|
|
1045
1135
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1136
|
+
// Action that returns ActionResponse with modal
|
|
1137
|
+
const openModal: ActionHandler<Env> = async (ctx, params) => {
|
|
1138
|
+
return ctx.modal(render(<div>Modal content</div>), { size: 'medium' })
|
|
1048
1139
|
}
|
|
1049
1140
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1141
|
+
// Action that returns ActionResponse with drawer
|
|
1142
|
+
const openDrawer: ActionHandler<Env> = async (ctx, params) => {
|
|
1143
|
+
return ctx.drawer(render(<div>Drawer content</div>), { position: 'right' })
|
|
1052
1144
|
}
|
|
1053
1145
|
```
|
|
1054
1146
|
|
|
@@ -1179,7 +1271,6 @@ Beam provides automatic session management with a simple `ctx.session` API. No b
|
|
|
1179
1271
|
```typescript
|
|
1180
1272
|
beamPlugin({
|
|
1181
1273
|
actions: '/app/actions/*.tsx',
|
|
1182
|
-
modals: '/app/modals/*.tsx',
|
|
1183
1274
|
session: true, // Enable with defaults (cookie storage)
|
|
1184
1275
|
})
|
|
1185
1276
|
```
|
|
@@ -1310,6 +1401,134 @@ ctx.session.set('cart') → adapter.set()
|
|
|
1310
1401
|
|
|
1311
1402
|
---
|
|
1312
1403
|
|
|
1404
|
+
## Security: WebSocket Authentication
|
|
1405
|
+
|
|
1406
|
+
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).
|
|
1407
|
+
|
|
1408
|
+
### The Problem
|
|
1409
|
+
|
|
1410
|
+
WebSocket connections in browsers:
|
|
1411
|
+
- **Always permit cross-site connections** (no CORS for WebSocket)
|
|
1412
|
+
- **Automatically send cookies** with the upgrade request
|
|
1413
|
+
- **Cannot use custom headers** for authentication
|
|
1414
|
+
|
|
1415
|
+
This means a malicious site could open a WebSocket to your server, and the browser would send your cookies, authenticating the attacker.
|
|
1416
|
+
|
|
1417
|
+
### The Solution: In-Band Authentication
|
|
1418
|
+
|
|
1419
|
+
Instead of relying on cookies, Beam requires clients to authenticate explicitly:
|
|
1420
|
+
|
|
1421
|
+
1. **Server generates a short-lived token** (embedded in same-origin page)
|
|
1422
|
+
2. **WebSocket connects unauthenticated** (gets `PublicApi`)
|
|
1423
|
+
3. **Client calls `authenticate(token)`** to get the full API
|
|
1424
|
+
4. **Malicious sites can't get the token** (CORS blocks page requests)
|
|
1425
|
+
|
|
1426
|
+
### Setup
|
|
1427
|
+
|
|
1428
|
+
#### 1. Enable Sessions (Required)
|
|
1429
|
+
|
|
1430
|
+
The auth token is tied to sessions:
|
|
1431
|
+
|
|
1432
|
+
```typescript
|
|
1433
|
+
// vite.config.ts
|
|
1434
|
+
beamPlugin({
|
|
1435
|
+
actions: '/app/actions/*.tsx',
|
|
1436
|
+
session: true, // Uses env.SESSION_SECRET
|
|
1437
|
+
})
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
#### 2. Use authMiddleware
|
|
1441
|
+
|
|
1442
|
+
```typescript
|
|
1443
|
+
// app/server.ts
|
|
1444
|
+
import { createApp } from 'honox/server'
|
|
1445
|
+
import { beam } from 'virtual:beam'
|
|
1446
|
+
|
|
1447
|
+
const app = createApp({
|
|
1448
|
+
init(app) {
|
|
1449
|
+
app.use('*', beam.authMiddleware()) // Generates token
|
|
1450
|
+
beam.init(app)
|
|
1451
|
+
}
|
|
1452
|
+
})
|
|
1453
|
+
|
|
1454
|
+
export default app
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
#### 3. Inject Token in Layout
|
|
1458
|
+
|
|
1459
|
+
```tsx
|
|
1460
|
+
// app/routes/_renderer.tsx
|
|
1461
|
+
import { jsxRenderer } from 'hono/jsx-renderer'
|
|
1462
|
+
|
|
1463
|
+
export default jsxRenderer((c, { children }) => {
|
|
1464
|
+
const token = c.get('beamAuthToken')
|
|
1465
|
+
|
|
1466
|
+
return (
|
|
1467
|
+
<html>
|
|
1468
|
+
<head>
|
|
1469
|
+
<meta name="beam-token" content={token} />
|
|
1470
|
+
<script type="module" src="/app/client.ts"></script>
|
|
1471
|
+
</head>
|
|
1472
|
+
<body>{children}</body>
|
|
1473
|
+
</html>
|
|
1474
|
+
)
|
|
1475
|
+
})
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
Or use the helper:
|
|
1479
|
+
|
|
1480
|
+
```tsx
|
|
1481
|
+
import { beamTokenMeta } from '@benqoder/beam'
|
|
1482
|
+
import { Raw } from 'hono/html'
|
|
1483
|
+
|
|
1484
|
+
<head>
|
|
1485
|
+
<Raw html={beamTokenMeta(c.get('beamAuthToken'))} />
|
|
1486
|
+
</head>
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
#### 4. Set Environment Variable
|
|
1490
|
+
|
|
1491
|
+
```bash
|
|
1492
|
+
# .dev.vars (local) or Cloudflare dashboard (production)
|
|
1493
|
+
SESSION_SECRET=your-secret-key-at-least-32-chars
|
|
1494
|
+
```
|
|
1495
|
+
|
|
1496
|
+
### How It Works
|
|
1497
|
+
|
|
1498
|
+
| Step | What Happens |
|
|
1499
|
+
|------|--------------|
|
|
1500
|
+
| 1. Page Load | Server generates 5-minute token, embeds in HTML |
|
|
1501
|
+
| 2. Client Connects | WebSocket opens, gets `PublicApi` (unauthenticated) |
|
|
1502
|
+
| 3. Client Authenticates | Calls `publicApi.authenticate(token)` |
|
|
1503
|
+
| 4. Server Validates | Verifies signature, expiration, session match |
|
|
1504
|
+
| 5. Server Returns | Full `BeamServer` API (authenticated) |
|
|
1505
|
+
|
|
1506
|
+
### Security Properties
|
|
1507
|
+
|
|
1508
|
+
| Attack | Result |
|
|
1509
|
+
|--------|--------|
|
|
1510
|
+
| Cross-site WebSocket | Can connect, but `authenticate()` fails (no token) |
|
|
1511
|
+
| Stolen token | Expires in 5 minutes, tied to session ID |
|
|
1512
|
+
| Replay attack | Token is single-use per session |
|
|
1513
|
+
| Token tampering | HMAC-SHA256 signature verification fails |
|
|
1514
|
+
|
|
1515
|
+
### Token Details
|
|
1516
|
+
|
|
1517
|
+
- **Algorithm**: HMAC-SHA256
|
|
1518
|
+
- **Lifetime**: 5 minutes (configurable)
|
|
1519
|
+
- **Payload**: `{ sid: sessionId, uid: userId, exp: timestamp }`
|
|
1520
|
+
- **Format**: `base64(payload).base64(signature)`
|
|
1521
|
+
|
|
1522
|
+
### Generating Tokens Manually
|
|
1523
|
+
|
|
1524
|
+
If you need to generate tokens outside the middleware:
|
|
1525
|
+
|
|
1526
|
+
```typescript
|
|
1527
|
+
const token = await beam.generateAuthToken(ctx)
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
---
|
|
1531
|
+
|
|
1313
1532
|
## Programmatic API
|
|
1314
1533
|
|
|
1315
1534
|
Call actions directly from JavaScript using `window.beam`:
|
|
@@ -1341,7 +1560,7 @@ window.beam.actionName(data?, options?) → Promise<ActionResponse>
|
|
|
1341
1560
|
// - string shorthand: treated as target selector
|
|
1342
1561
|
// - object: full options with target and swap mode
|
|
1343
1562
|
|
|
1344
|
-
// ActionResponse: { html?: string, script?: string, redirect?: string }
|
|
1563
|
+
// ActionResponse: { html?: string | string[], script?: string, redirect?: string, target?: string }
|
|
1345
1564
|
```
|
|
1346
1565
|
|
|
1347
1566
|
### Response Handling
|
package/dist/client.d.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { type RpcStub } from 'capnweb';
|
|
2
2
|
interface ActionResponse {
|
|
3
|
-
html?: string;
|
|
3
|
+
html?: string | string[];
|
|
4
4
|
script?: string;
|
|
5
5
|
redirect?: string;
|
|
6
|
+
target?: string;
|
|
7
|
+
swap?: string;
|
|
8
|
+
modal?: string | {
|
|
9
|
+
html: string;
|
|
10
|
+
size?: string;
|
|
11
|
+
spacing?: number;
|
|
12
|
+
};
|
|
13
|
+
drawer?: string | {
|
|
14
|
+
html: string;
|
|
15
|
+
position?: string;
|
|
16
|
+
size?: string;
|
|
17
|
+
spacing?: number;
|
|
18
|
+
};
|
|
6
19
|
}
|
|
7
20
|
interface BeamServer {
|
|
8
21
|
call(action: string, data?: Record<string, unknown>): Promise<ActionResponse>;
|
|
9
|
-
modal(modalId: string, data?: Record<string, unknown>): Promise<string>;
|
|
10
|
-
drawer(drawerId: string, data?: Record<string, unknown>): Promise<string>;
|
|
11
22
|
registerCallback(callback: (event: string, data: unknown) => void): Promise<void>;
|
|
12
23
|
}
|
|
13
24
|
type BeamServerStub = RpcStub<BeamServer>;
|
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,GAAG,MAAM,EAAE,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,MAAM,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACvF;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,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;AAu3BzC,iBAAS,UAAU,IAAI,IAAI,CAU1B;AAkCD,iBAAS,WAAW,IAAI,IAAI,CAU3B;AAID,iBAAS,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,SAAS,GAAG,OAAmB,GAAG,IAAI,CAsB/E;AAkrBD,iBAAS,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAUzC;AA8gBD,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;;;;;;;sBAtlEO,OAAO,CAAC,cAAc,CAAC;CA8lE5C,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"}
|