@longstoryshort/vtt-sdk 0.3.0 → 0.5.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 CHANGED
@@ -1,15 +1,15 @@
1
1
  # @longstoryshort/vtt-sdk
2
2
 
3
- Embed a [longstoryshort.app](https://longstoryshort.app) character sheet in any virtual tabletop (VTT) via iframe and postMessage.
3
+ Embed a [longstoryshort.app](https://longstoryshort.app) character sheet in any virtual tabletop via iframe and postMessage.
4
4
 
5
5
  ## How it works
6
6
 
7
- The SDK implements a postMessage-based protocol that lets a VTT extension (the "bridge") embed the LSS sheet in a nested iframe and receive dice rolls, manifests, and other events — without ever reading the sheet's DOM, cookies, or auth token.
7
+ The sheet and your VTT run at different origins. They communicate only via `window.postMessage` your code never reads the sheet's DOM, cookies, or auth token.
8
8
 
9
9
  ```
10
- OBR / Foundry / … ──► [bridge page, FOREIGN origin]
11
- embeds iframe ↓ ← trust boundary (postMessage only)
12
- └► [LSS sheet, lss origin]
10
+ Your page (your origin)
11
+ └── LSS sheet iframe (longstoryshort.app)
12
+ └── postMessage ──→ your page
13
13
  ```
14
14
 
15
15
  ## Installation
@@ -18,7 +18,7 @@ OBR / Foundry / … ──► [bridge page, FOREIGN origin]
18
18
  npm install @longstoryshort/vtt-sdk
19
19
  ```
20
20
 
21
- The Owlbear adapter is an optional peer dependency:
21
+ For Owlbear Rodeo, also install the peer dependency:
22
22
 
23
23
  ```sh
24
24
  npm install @owlbear-rodeo/sdk
@@ -28,82 +28,64 @@ npm install @owlbear-rodeo/sdk
28
28
 
29
29
  | Import | Contents |
30
30
  |--------|----------|
31
- | `@longstoryshort/vtt-sdk` | Core: types, `createRollBridge`, `createSheetClient`, `createBridgeSheetSource`, `formatRollMessage` |
32
- | `@longstoryshort/vtt-sdk/owlbear` | `OwlbearAdapter`, `syncObrref`, constants |
31
+ | `@longstoryshort/vtt-sdk` | Core: protocol types, `createBridgeSheetSource`, `createSheetClient`, `formatRollMessage`, `SHEET_IFRAME_SANDBOX` |
32
+ | `@longstoryshort/vtt-sdk/owlbear` | `OwlbearAdapter`, `ObrAdapter`, OBR bootstrap helpers |
33
33
 
34
- ## Quick start — bridge side
34
+ ## Quick start
35
35
 
36
36
  ```ts
37
- import { createRollBridge, createBridgeSheetSource } from '@longstoryshort/vtt-sdk';
38
- import { OwlbearAdapter } from '@longstoryshort/vtt-sdk/owlbear';
37
+ import { createBridgeSheetSource, SHEET_IFRAME_SANDBOX, formatRollMessage, rollVariant } from '@longstoryshort/vtt-sdk';
39
38
 
40
- const adapter = new OwlbearAdapter();
39
+ // Embed the sheet
40
+ const iframe = document.createElement('iframe');
41
+ iframe.src = 'https://longstoryshort.app/iframe/characters/list/';
42
+ iframe.setAttribute('sandbox', SHEET_IFRAME_SANDBOX);
43
+ iframe.setAttribute('allow', 'clipboard-write');
44
+ iframe.style.cssText = 'border:none;width:100%;height:100vh;display:block';
45
+ document.body.appendChild(iframe);
46
+
47
+ // Receive rolls
41
48
  const source = createBridgeSheetSource({
42
- iframe: document.getElementById('sheet-frame') as HTMLIFrameElement,
49
+ iframe,
43
50
  allowedOrigins: ['https://longstoryshort.app'],
44
51
  });
45
52
 
46
- const dispose = createRollBridge(source, adapter, {
47
- messages: {
48
- connected: '🎲 Sheet connected',
49
- labelHint: 'Select exactly one token to place a roll label',
50
- },
51
- });
52
-
53
- // later:
54
- dispose();
55
- source.dispose();
56
- ```
57
-
58
- ## Quick start — sheet side
59
-
60
- ```ts
61
- import { createSheetClient } from '@longstoryshort/vtt-sdk';
62
-
63
- const client = createSheetClient();
64
-
65
- // emit a roll to the bridge
66
- client.send({ type: 'dnd:roll', payload: { ... } });
67
-
68
- // receive inbound commands from the bridge
69
- const unsub = client.onEvent((event) => {
70
- if (event.type === 'dnd:command') { /* handle damage, conditions, … */ }
53
+ source.onRoll((roll) => {
54
+ // wire to your VTT — notification, chat, peer broadcast, etc.
55
+ console.log(formatRollMessage(roll), rollVariant(roll));
71
56
  });
72
57
 
73
58
  // cleanup
74
- client.dispose();
59
+ source.dispose();
75
60
  ```
76
61
 
77
- ## iframe sandbox requirements
78
-
79
- The bridge must embed the sheet iframe with at least these sandbox tokens:
62
+ ## Documentation
80
63
 
81
- ```
82
- sandbox="allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals"
83
- ```
84
-
85
- `allow-same-origin` is required so the sheet can read its auth cookie and access localStorage. Without it the sheet gets an opaque origin and auth breaks.
64
+ - [SDK guide](docs/sdk-guide.md) — sandbox requirements, receiving events, protocol reference, utilities. Start here regardless of your VTT architecture.
65
+ - [Bridge guide](docs/bridge-guide.md) for VTTs that use an extension/plugin model and need a separate static bridge page. Includes the full annotated OBR bridge and `OwlbearAdapter` reference.
86
66
 
87
67
  ## Reference bridge — Owlbear Rodeo (D&D 5e)
88
68
 
89
- A deployable vanilla-TS bridge for Owlbear Rodeo lives in [`bridges/dnd/`](bridges/dnd/). It is automatically deployed to GitHub Pages on every push to `master`.
69
+ A deployable bridge for Owlbear Rodeo lives in [`bridges/dnd/`](bridges/dnd/). Deployed automatically to GitHub Pages on every push to `master`.
90
70
 
91
71
  **Live manifest:** `https://bridge.longstoryshort.app/dnd/obr/manifest.json`
92
72
 
93
- To install the extension in OBR: Extensions → Add extension → paste the manifest URL above.
73
+ To install in OBR: Extensions → Add extension → paste the manifest URL above.
74
+
75
+ ## Bridge template
94
76
 
95
- To adapt for your own VTT: copy `bridges/dnd/src/main.ts`, swap `OwlbearAdapter` for your own `VTTAdapter` implementation, and deploy as a static page.
77
+ [`bridges/_template/`](bridges/_template/) is a minimal copy-and-modify skeleton iframe embed + `createBridgeSheetSource` + `TODO` comments for your VTT's APIs, ready to build with Vite.
96
78
 
97
79
  ## Protocol events
98
80
 
99
81
  | Type | Status | Direction | Description |
100
82
  |------|--------|-----------|-------------|
101
- | `dnd:roll` | ✅ stable | sheet → host | A roll result |
102
- | `dnd:manifest` | 🧪 reserved | sheet → host | Sheet capabilities at handshake |
103
- | `dnd:health` | 🧪 reserved | sheet → host | HP after an adjust/set |
104
- | `dnd:command` | 🧪 reserved | host → sheet | Narrow inbound ops (adjust HP, toggle condition, …) |
83
+ | `dnd:roll` | ✅ stable | sheet → host | A roll result |
84
+ | `dnd:manifest` | 🧪 reserved | sheet → host | Sheet capabilities at handshake |
85
+ | `dnd:health` | 🧪 reserved | sheet → host | HP after an adjust/set |
86
+ | `dnd:command` | 🧪 reserved | host → sheet | Inbound ops (adjust HP, toggle condition, …) |
105
87
 
106
- Reserved events are typed and functional the sheet implements them — but the bridge-side wiring is considered experimental API and may change. Wire them directly via `BridgeSheetSource.onEvent` rather than through `createRollBridge`.
88
+ Reserved events are typed and wired end-to-end, but the bridge-side API is experimental. Subscribe via `source.onEvent` directly.
107
89
 
108
90
  ## License
109
91
 
@@ -1,16 +1,35 @@
1
- import { V as VTTAdapter, a as VTTUser, S as SheetEvent, N as NotifyVariant } from '../../types-w2_82sqo.js';
1
+ import { S as SheetEvent, N as NotifyVariant } from '../../formatRoll-BhFkInCu.js';
2
+ import * as _owlbear_rodeo_sdk from '@owlbear-rodeo/sdk';
3
+
4
+ interface ObrPlayer {
5
+ id: string;
6
+ name: string;
7
+ role: 'gm' | 'player';
8
+ }
9
+ /** Public contract of {@link OwlbearAdapter} — use this type when you need to reference the adapter without importing the class. */
10
+ interface ObrAdapter {
11
+ readonly isAvailable: boolean;
12
+ ready(): Promise<boolean>;
13
+ getSessionId(): string | undefined;
14
+ getCurrentUser(): ObrPlayer | undefined;
15
+ notify(message: string, variant?: 'info' | 'success' | 'warning' | 'error'): void;
16
+ broadcast(event: SheetEvent): void;
17
+ onEvent(handler: (event: SheetEvent) => void): () => void;
18
+ getRoomMetadata(): Promise<Record<string, unknown>>;
19
+ onRoomMetadataChange(handler: () => void): () => void;
20
+ dispose(): void;
21
+ }
2
22
 
3
23
  /**
4
- * Owlbear Rodeo implementation of {@link VTTAdapter}.
24
+ * Owlbear Rodeo bridge helper.
5
25
  *
6
26
  * Owlbear exposes no public dice API (its 3D roller is first-party and closed),
7
- * so rendering a roll is our job: we broadcast a result for toasts/logs and add
8
- * a transient label item over the roller's token — scene items are shared, so
27
+ * so rendering a roll is the bridge's job: broadcast a result for toasts/logs and
28
+ * add a transient label item over the roller's token — scene items are shared, so
9
29
  * everyone at the table sees the floating number. All scene work is best-effort
10
30
  * and degrades silently; the broadcast/notification path is the guaranteed core.
11
31
  */
12
- declare class OwlbearAdapter implements VTTAdapter {
13
- private sdk;
32
+ declare class OwlbearAdapter implements ObrAdapter {
14
33
  private obr;
15
34
  private user;
16
35
  private sessionId;
@@ -19,13 +38,13 @@ declare class OwlbearAdapter implements VTTAdapter {
19
38
  get isAvailable(): boolean;
20
39
  ready(): Promise<boolean>;
21
40
  private init;
22
- private loadSdk;
23
41
  getSessionId(): string | undefined;
24
- getCurrentUser(): VTTUser | undefined;
42
+ getCurrentUser(): ObrPlayer | undefined;
25
43
  broadcast(event: SheetEvent): void;
26
44
  onEvent(handler: (event: SheetEvent) => void): () => void;
27
45
  notify(message: string, variant?: NotifyVariant): void;
28
- labelOverSelection(text: string, ttlMs?: number): Promise<boolean>;
46
+ getRoomMetadata(): Promise<Record<string, unknown>>;
47
+ onRoomMetadataChange(handler: () => void): () => void;
29
48
  dispose(): void;
30
49
  }
31
50
 
@@ -43,9 +62,39 @@ declare function syncObrref(): void;
43
62
 
44
63
  /** Room-wide pub/sub channel for sheet events. Namespaced to avoid collisions. */
45
64
  declare const BROADCAST_CHANNEL = "rodeo.lss/sheet-events";
46
- /** Marks scene items we create (transient roll labels) so we own/clean them. */
47
- declare const LABEL_METADATA_KEY = "rodeo.lss/label";
48
- /** How long a roll label floats over a token before it is removed. */
49
- declare const DEFAULT_LABEL_TTL_MS = 4000;
65
+ /** OBR room metadata key false when Vortex is connected (logger active), true otherwise. */
66
+ declare const NOTIFY_ROLLS_KEY = "rodeo.lss/notify-rolls";
67
+
68
+ interface ObrLike {
69
+ onReady(cb: () => void): void;
70
+ }
71
+ /** Promisifies `OBR.onReady` — resolves once the OBR_READY handshake completes. */
72
+ declare function whenObrReady(obr: ObrLike): Promise<void>;
73
+
74
+ type OwlbearSdk = typeof _owlbear_rodeo_sdk;
75
+ /**
76
+ * The window shape expected by the SDK preload contract.
77
+ * Set `__lssObrSdk` at the app entry (before any navigation) so
78
+ * `loadObrSdk` can reuse the already-imported module and avoid missing
79
+ * the one-shot OBR_READY handshake.
80
+ */
81
+ interface ObrPreloadWindow {
82
+ __lssObrSdk?: OwlbearSdk;
83
+ }
84
+ /**
85
+ * Call this at the bridge entry module — before any client-side navigation —
86
+ * to stash the already-imported OBR SDK so `loadObrSdk` can find it.
87
+ */
88
+ declare function preloadObrSdk(sdk: OwlbearSdk): void;
89
+ /**
90
+ * Retrieves the OBR SDK for use inside an OBR extension frame.
91
+ *
92
+ * Prefers the copy stashed by `preloadObrSdk` (imported early enough to catch
93
+ * the one-shot OBR_READY handshake). Falls back to a dynamic import if the
94
+ * stash is absent, with a warning — the dynamic path may miss OBR_READY.
95
+ *
96
+ * Returns `null` when not running in a browser or when the import fails.
97
+ */
98
+ declare function loadObrSdk(): Promise<OwlbearSdk | null>;
50
99
 
51
- export { BROADCAST_CHANNEL, DEFAULT_LABEL_TTL_MS, LABEL_METADATA_KEY, OwlbearAdapter, syncObrref };
100
+ export { BROADCAST_CHANNEL, NOTIFY_ROLLS_KEY, type ObrAdapter, type ObrPlayer, type ObrPreloadWindow, OwlbearAdapter, loadObrSdk, preloadObrSdk, syncObrref, whenObrReady };
@@ -1,7 +1,6 @@
1
1
  // src/adapters/owlbear/constants.ts
2
2
  var BROADCAST_CHANNEL = "rodeo.lss/sheet-events";
3
- var LABEL_METADATA_KEY = "rodeo.lss/label";
4
- var DEFAULT_LABEL_TTL_MS = 4e3;
3
+ var NOTIFY_ROLLS_KEY = "rodeo.lss/notify-rolls";
5
4
 
6
5
  // src/adapters/owlbear/obrref.ts
7
6
  var PARAM = "obrref";
@@ -44,8 +43,44 @@ function syncObrref() {
44
43
  }
45
44
  }
46
45
 
47
- // src/adapters/owlbear/OwlbearAdapter.ts
46
+ // src/adapters/owlbear/loadObrSdk.ts
48
47
  var DEV2 = process.env["NODE_ENV"] !== "production";
48
+ function preloadObrSdk(sdk) {
49
+ window.__lssObrSdk = sdk;
50
+ }
51
+ async function loadObrSdk() {
52
+ if (typeof window === "undefined") {
53
+ return null;
54
+ }
55
+ const host = window;
56
+ for (let i = 0; i < 10 && !host.__lssObrSdk; i += 1) {
57
+ await new Promise((resolve) => {
58
+ window.setTimeout(resolve, 50);
59
+ });
60
+ }
61
+ if (host.__lssObrSdk) {
62
+ return host.__lssObrSdk;
63
+ }
64
+ if (DEV2) {
65
+ console.warn("[LSS/OBR] no preloaded SDK \u2014 importing now (may miss OBR_READY)");
66
+ }
67
+ try {
68
+ return await import('@owlbear-rodeo/sdk');
69
+ } catch (error) {
70
+ console.error("[LSS/OBR] SDK import failed:", error);
71
+ return null;
72
+ }
73
+ }
74
+
75
+ // src/adapters/owlbear/whenObrReady.ts
76
+ function whenObrReady(obr) {
77
+ return new Promise((resolve) => {
78
+ obr.onReady(resolve);
79
+ });
80
+ }
81
+
82
+ // src/adapters/owlbear/OwlbearAdapter.ts
83
+ var DEV3 = process.env["NODE_ENV"] !== "production";
49
84
  var NOTIFY_VARIANT = {
50
85
  info: "INFO",
51
86
  success: "SUCCESS",
@@ -54,7 +89,6 @@ var NOTIFY_VARIANT = {
54
89
  };
55
90
  var OwlbearAdapter = class {
56
91
  constructor() {
57
- this.sdk = null;
58
92
  this.obr = null;
59
93
  this.readyPromise = null;
60
94
  this.disposed = false;
@@ -73,13 +107,12 @@ var OwlbearAdapter = class {
73
107
  return false;
74
108
  }
75
109
  syncObrref();
76
- const sdk = await this.loadSdk();
110
+ const sdk = await loadObrSdk();
77
111
  if (!sdk) {
78
112
  return false;
79
113
  }
80
- this.sdk = sdk;
81
114
  this.obr = sdk.default;
82
- if (DEV2) {
115
+ if (DEV3) {
83
116
  console.info(
84
117
  "[LSS/OBR] init \u2014 isAvailable:",
85
118
  this.obr.isAvailable,
@@ -88,18 +121,16 @@ var OwlbearAdapter = class {
88
121
  );
89
122
  }
90
123
  if (!this.obr.isAvailable) {
91
- if (DEV2) {
124
+ if (DEV3) {
92
125
  console.warn("[LSS/OBR] not embedded (origin empty) \u2014 obrref missing at SDK load.");
93
126
  }
94
127
  return false;
95
128
  }
96
- if (DEV2) {
129
+ if (DEV3) {
97
130
  console.info("[LSS/OBR] awaiting onReady (isReady =", this.obr.isReady, ")");
98
131
  }
99
- await new Promise((resolve) => {
100
- this.obr.onReady(resolve);
101
- });
102
- if (DEV2) {
132
+ await whenObrReady(this.obr);
133
+ if (DEV3) {
103
134
  console.info("[LSS/OBR] onReady fired \u2014 fetching player\u2026");
104
135
  }
105
136
  if (this.disposed) {
@@ -112,31 +143,11 @@ var OwlbearAdapter = class {
112
143
  this.obr.player.getRole()
113
144
  ]);
114
145
  this.user = { id, name, role: role === "GM" ? "gm" : "player" };
115
- if (DEV2) {
146
+ if (DEV3) {
116
147
  console.info("[LSS/OBR] ready \u2014 room:", this.sessionId, "user:", this.user);
117
148
  }
118
149
  return true;
119
150
  }
120
- async loadSdk() {
121
- const host = window;
122
- for (let i = 0; i < 10 && !host.__lssObrSdk; i += 1) {
123
- await new Promise((resolve) => {
124
- window.setTimeout(resolve, 50);
125
- });
126
- }
127
- if (host.__lssObrSdk) {
128
- return host.__lssObrSdk;
129
- }
130
- if (DEV2) {
131
- console.warn("[LSS/OBR] no preloaded SDK \u2014 importing now (may miss OBR_READY)");
132
- }
133
- try {
134
- return await import('@owlbear-rodeo/sdk');
135
- } catch (error) {
136
- console.error("[LSS/OBR] SDK import failed:", error);
137
- return null;
138
- }
139
- }
140
151
  getSessionId() {
141
152
  return this.sessionId;
142
153
  }
@@ -160,43 +171,21 @@ var OwlbearAdapter = class {
160
171
  void this.obr?.notification.show(message, NOTIFY_VARIANT[variant]).catch(() => {
161
172
  });
162
173
  }
163
- async labelOverSelection(text, ttlMs = DEFAULT_LABEL_TTL_MS) {
164
- const { obr, sdk } = this;
165
- if (!obr || !sdk) {
166
- return false;
167
- }
168
- try {
169
- const selection = await obr.player.getSelection();
170
- if (!selection || selection.length !== 1) {
171
- if (DEV2) {
172
- console.warn("[OwlbearAdapter] label skipped \u2014 selected tokens:", selection?.length ?? 0);
173
- }
174
- return false;
175
- }
176
- const [token] = await obr.scene.items.getItems(selection);
177
- if (!token) {
178
- if (DEV2) {
179
- console.warn("[OwlbearAdapter] label skipped \u2014 selected item not found in scene");
180
- }
181
- return false;
182
- }
183
- const label = sdk.buildLabel().plainText(text).position(token.position).attachedTo(token.id).pointerHeight(0).disableHit(true).locked(true).layer("TEXT").metadata({ [LABEL_METADATA_KEY]: true }).build();
184
- await obr.scene.items.addItems([label]);
185
- window.setTimeout(() => {
186
- void obr.scene.items.deleteItems([label.id]).catch(() => {
187
- });
188
- }, ttlMs);
189
- return true;
190
- } catch (error) {
191
- if (DEV2) {
192
- console.warn("[OwlbearAdapter] label error:", error);
193
- }
194
- return false;
195
- }
174
+ getRoomMetadata() {
175
+ if (!this.obr) return Promise.resolve({});
176
+ return this.obr.room.getMetadata().then(
177
+ (meta) => meta,
178
+ () => ({})
179
+ );
180
+ }
181
+ onRoomMetadataChange(handler) {
182
+ if (!this.obr) return () => {
183
+ };
184
+ return this.obr.room.onMetadataChange(handler);
196
185
  }
197
186
  dispose() {
198
187
  this.disposed = true;
199
188
  }
200
189
  };
201
190
 
202
- export { BROADCAST_CHANNEL, DEFAULT_LABEL_TTL_MS, LABEL_METADATA_KEY, OwlbearAdapter, syncObrref };
191
+ export { BROADCAST_CHANNEL, NOTIFY_ROLLS_KEY, OwlbearAdapter, loadObrSdk, preloadObrSdk, syncObrref, whenObrReady };
@@ -4,13 +4,6 @@
4
4
  * This file must not import from any project outside this SDK directory —
5
5
  * only from other SDK files and external dependencies.
6
6
  */
7
- type VTTUserRole = 'gm' | 'player';
8
- interface VTTUser {
9
- id: string;
10
- name: string;
11
- role: VTTUserRole;
12
- }
13
- type NotifyVariant = 'info' | 'success' | 'warning' | 'error';
14
7
  /** A single dice roll made on a character sheet, normalized for any VTT. */
15
8
  interface DiceRollPayload {
16
9
  characterId: string;
@@ -140,48 +133,6 @@ interface SheetSource {
140
133
  /** Subscribe to rolls made on the sheet. Returns an unsubscribe fn. */
141
134
  onRoll(handler: (roll: DiceRollPayload) => void): () => void;
142
135
  }
143
- /** Human-facing strings surfaced by the bridge — override to localize. */
144
- interface RollBridgeMessages {
145
- /** Toast shown once the sheet connects to the table. */
146
- connected: string;
147
- /** Toast shown to the roller when a token label could not be placed. */
148
- labelHint: string;
149
- }
150
- interface RollBridgeOptions {
151
- messages?: Partial<RollBridgeMessages>;
152
- }
153
- /**
154
- * The seam every VTT implements. The sheet talks only to this interface; each
155
- * table (Owlbear, Foundry, …) ships a thin adapter that maps its own SDK onto
156
- * these methods.
157
- */
158
- interface VTTAdapter {
159
- /** True only when the page actually runs inside this VTT. */
160
- readonly isAvailable: boolean;
161
- /**
162
- * Loads and handshakes with the VTT SDK. Resolves `true` once ready, or
163
- * `false` if the page is not running inside this VTT. Safe to call multiple
164
- * times — the work happens once.
165
- */
166
- ready(): Promise<boolean>;
167
- getSessionId(): string | undefined;
168
- getCurrentUser(): VTTUser | undefined;
169
- /** Send an event to every other client in the room (sender excluded). */
170
- broadcast(event: SheetEvent): void;
171
- /** Subscribe to events broadcast by other clients. Returns an unsubscribe fn. */
172
- onEvent(handler: (event: SheetEvent) => void): () => void;
173
- /** Local toast on this client. */
174
- notify(message: string, variant?: NotifyVariant): void;
175
- /**
176
- * Float a transient text label over the player's currently selected token.
177
- * Scene items are shared, so the label is visible to everyone at the table.
178
- * Resolves `true` if a label was placed, `false` when there isn't exactly
179
- * one token selected or the scene write was rejected (e.g. no permission).
180
- */
181
- labelOverSelection(text: string, ttlMs?: number): Promise<boolean>;
182
- /** Tear down listeners / SDK handlers. */
183
- dispose(): void;
184
- }
185
136
  /** Minimal "listen for messages" surface — the real `window` satisfies it. */
186
137
  interface MessageHost {
187
138
  addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
@@ -218,4 +169,10 @@ interface SheetClient {
218
169
  dispose(): void;
219
170
  }
220
171
 
221
- export type { CapabilityManifest as C, DiceRollPayload as D, HealthChangedPayload as H, MessageHost as M, NotifyVariant as N, RollBridgeOptions as R, SheetEvent as S, VTTAdapter as V, VTTUser as a, SheetSource as b, SheetClientOptions as c, SheetClient as d, CapabilityDescriptor as e, CapabilityOpAddTag as f, CapabilityOpAdjust as g, CapabilityOpName as h, CapabilityOpRemoveTag as i, CapabilityOpRequestRoll as j, CapabilityOpSet as k, CapabilityOpToggle as l, CapabilityOperation as m, MessageTarget as n, RollBridgeMessages as o, VTTUserRole as p };
172
+ type NotifyVariant = 'info' | 'success' | 'warning' | 'error';
173
+ /** Toast text for a roll, e.g. "🎲 Alice: Longsword Attack — 18 💥". */
174
+ declare function formatRollMessage(payload: DiceRollPayload): string;
175
+ /** Maps a roll's crit state onto a toast variant. */
176
+ declare function rollVariant(payload: DiceRollPayload): NotifyVariant;
177
+
178
+ export { type CapabilityDescriptor as C, type DiceRollPayload as D, type HealthChangedPayload as H, type MessageHost as M, type NotifyVariant as N, type SheetEvent as S, type SheetClientOptions as a, type SheetClient as b, type SheetSource as c, type CapabilityManifest as d, type CapabilityOpAddTag as e, type CapabilityOpAdjust as f, type CapabilityOpName as g, type CapabilityOpRemoveTag as h, type CapabilityOpRequestRoll as i, type CapabilityOpSet as j, type CapabilityOpToggle as k, type CapabilityOperation as l, type MessageTarget as m, formatRollMessage as n, rollVariant as r };
package/dist/index.d.ts CHANGED
@@ -1,22 +1,12 @@
1
- import { b as SheetSource, V as VTTAdapter, R as RollBridgeOptions, c as SheetClientOptions, d as SheetClient, C as CapabilityManifest, S as SheetEvent, M as MessageHost, D as DiceRollPayload, N as NotifyVariant } from './types-w2_82sqo.js';
2
- export { e as CapabilityDescriptor, f as CapabilityOpAddTag, g as CapabilityOpAdjust, h as CapabilityOpName, i as CapabilityOpRemoveTag, j as CapabilityOpRequestRoll, k as CapabilityOpSet, l as CapabilityOpToggle, m as CapabilityOperation, H as HealthChangedPayload, n as MessageTarget, o as RollBridgeMessages, a as VTTUser, p as VTTUserRole } from './types-w2_82sqo.js';
1
+ import { a as SheetClientOptions, b as SheetClient, c as SheetSource, S as SheetEvent, M as MessageHost } from './formatRoll-BhFkInCu.js';
2
+ export { C as CapabilityDescriptor, d as CapabilityManifest, e as CapabilityOpAddTag, f as CapabilityOpAdjust, g as CapabilityOpName, h as CapabilityOpRemoveTag, i as CapabilityOpRequestRoll, j as CapabilityOpSet, k as CapabilityOpToggle, l as CapabilityOperation, D as DiceRollPayload, H as HealthChangedPayload, m as MessageTarget, N as NotifyVariant, n as formatRollMessage, r as rollVariant } from './formatRoll-BhFkInCu.js';
3
3
 
4
4
  /**
5
- * The default roll bridge one opinionated policy, not the full protocol.
6
- *
7
- * Wires {@link SheetSource} {@link VTTAdapter} for the `dnd:roll` event only:
8
- * - a roll on the sheet → local toast for the roller + broadcast to other
9
- * clients + a transient label over the roller's selected token;
10
- * - a roll broadcast by another client → local toast.
11
- *
12
- * Inbound capability wiring (`dnd:command`, `dnd:manifest`, `dnd:health`) is
13
- * deliberately out of scope here — wire those directly via
14
- * {@link BridgeSheetSource.onEvent} when your bridge needs them.
15
- *
16
- * Returns a dispose fn that tears down every subscription it created. The
17
- * adapter is left untouched — it may be shared and longer-lived than the bridge.
5
+ * Minimum sandbox tokens required on the sheet iframe.
6
+ * `allow-same-origin` is essential — without it the sheet gets an opaque origin
7
+ * and auth (cookies / localStorage) breaks entirely.
18
8
  */
19
- declare function createRollBridge(source: SheetSource, adapter: VTTAdapter, options?: RollBridgeOptions): () => void;
9
+ declare const SHEET_IFRAME_SANDBOX = "allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals";
20
10
 
21
11
  /**
22
12
  * Sheet-side half of the postMessage transport. The character sheet (running in
@@ -46,11 +36,9 @@ interface BridgeSheetSourceOptions {
46
36
  /** Origin to post inbound commands to. Default `'*'`. */
47
37
  targetOrigin?: string;
48
38
  }
49
- /** A `SheetSource` (for `createRollBridge`) plus raw access and inbound `send`. */
39
+ /** Bridge-side source: `onRoll` convenience + full `onEvent` access + inbound `send`. */
50
40
  interface BridgeSheetSource extends SheetSource {
51
- /** Subscribe to the sheet's capability manifest (sent once at handshake). Returns an unsubscribe fn. */
52
- onManifest(handler: (manifest: CapabilityManifest) => void): () => void;
53
- /** Subscribe to every event coming from the sheet (not only rolls). */
41
+ /** Subscribe to every event coming from the sheet. Returns an unsubscribe fn. */
54
42
  onEvent(handler: (event: SheetEvent) => void): () => void;
55
43
  /** Post an inbound command to the sheet (e.g. `dnd:command`). */
56
44
  send(event: SheetEvent): void;
@@ -58,18 +46,12 @@ interface BridgeSheetSource extends SheetSource {
58
46
  }
59
47
  /**
60
48
  * Bridge-side half of the postMessage transport. Runs in the bridge frame (the
61
- * VTT extension), listens to the embedded sheet iframe, and exposes a
62
- * `SheetSource` so `createRollBridge` can drive the VTT adapter without the
63
- * bridge ever touching the sheet's internals.
49
+ * VTT extension), listens to the embedded sheet iframe, and delivers typed
50
+ * `SheetEvent`s without the bridge ever touching the sheet's internals.
64
51
  *
65
52
  * `contentWindow` is read live on every message/send, so it survives the sheet
66
53
  * iframe navigating or reloading (e.g. Gatsby dev's full-reload on navigation).
67
54
  */
68
55
  declare function createBridgeSheetSource(options: BridgeSheetSourceOptions): BridgeSheetSource;
69
56
 
70
- /** Toast text for a roll, e.g. "🎲 Alice: Longsword Attack 18 💥". */
71
- declare function formatRollMessage(payload: DiceRollPayload): string;
72
- /** Maps a roll's crit state onto a toast variant. */
73
- declare function rollVariant(payload: DiceRollPayload): NotifyVariant;
74
-
75
- export { type BridgeSheetSource, type BridgeSheetSourceOptions, CapabilityManifest, DiceRollPayload, MessageHost, NotifyVariant, RollBridgeOptions, SheetClient, SheetClientOptions, SheetEvent, type SheetFrameRef, SheetSource, VTTAdapter, createBridgeSheetSource, createRollBridge, createSheetClient, formatRollMessage, rollVariant };
57
+ export { type BridgeSheetSource, type BridgeSheetSourceOptions, MessageHost, SHEET_IFRAME_SANDBOX, SheetClient, SheetClientOptions, SheetEvent, type SheetFrameRef, SheetSource, createBridgeSheetSource, createSheetClient };
package/dist/index.js CHANGED
@@ -1,63 +1,5 @@
1
- // src/formatRoll.ts
2
- function formatRollMessage(payload) {
3
- let crit = "";
4
- if (payload.isCrit) {
5
- if (payload.critKind === "success") {
6
- crit = " \u{1F4A5}";
7
- } else if (payload.critKind === "failure") {
8
- crit = " \u{1F480}";
9
- }
10
- }
11
- return `\u{1F3B2} ${payload.characterName}: ${payload.title} \u2014 ${payload.total}${crit}`;
12
- }
13
- function rollVariant(payload) {
14
- if (payload.isCrit && payload.critKind === "success") {
15
- return "success";
16
- }
17
- if (payload.isCrit && payload.critKind === "failure") {
18
- return "warning";
19
- }
20
- return "info";
21
- }
22
-
23
- // src/createRollBridge.ts
24
- var DEFAULT_MESSAGES = {
25
- connected: "\u{1F3B2} Sheet connected to the table",
26
- labelHint: "No label placed \u2014 select exactly one of your tokens on the map"
27
- };
28
- function createRollBridge(source, adapter, options = {}) {
29
- const messages = { ...DEFAULT_MESSAGES, ...options.messages };
30
- const cleanups = [];
31
- let cancelled = false;
32
- cleanups.push(source.onRoll((roll) => {
33
- if (!adapter.isAvailable) {
34
- return;
35
- }
36
- adapter.notify(formatRollMessage(roll), rollVariant(roll));
37
- adapter.broadcast({ type: "dnd:roll", payload: roll });
38
- void adapter.labelOverSelection(roll.total).then((placed) => {
39
- if (!placed) {
40
- adapter.notify(messages.labelHint, "warning");
41
- }
42
- });
43
- }));
44
- void adapter.ready().then((available) => {
45
- if (cancelled || !available) {
46
- return;
47
- }
48
- adapter.notify(messages.connected, "success");
49
- cleanups.push(adapter.onEvent((event) => {
50
- if (event.type !== "dnd:roll") {
51
- return;
52
- }
53
- adapter.notify(formatRollMessage(event.payload), rollVariant(event.payload));
54
- }));
55
- });
56
- return () => {
57
- cancelled = true;
58
- cleanups.forEach((cleanup) => cleanup());
59
- };
60
- }
1
+ // src/constants.ts
2
+ var SHEET_IFRAME_SANDBOX = "allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals";
61
3
 
62
4
  // src/postMessageProtocol.ts
63
5
  var MARKER = "__lssSheetSdk";
@@ -156,17 +98,6 @@ function createBridgeSheetSource(options) {
156
98
  handlers.delete(wrapped);
157
99
  };
158
100
  },
159
- onManifest(handler) {
160
- const wrapped = (event) => {
161
- if (event.type === "dnd:manifest") {
162
- handler(event.payload);
163
- }
164
- };
165
- handlers.add(wrapped);
166
- return () => {
167
- handlers.delete(wrapped);
168
- };
169
- },
170
101
  onEvent(handler) {
171
102
  handlers.add(handler);
172
103
  return () => {
@@ -185,4 +116,26 @@ function createBridgeSheetSource(options) {
185
116
  };
186
117
  }
187
118
 
188
- export { createBridgeSheetSource, createRollBridge, createSheetClient, formatRollMessage, rollVariant };
119
+ // src/formatRoll.ts
120
+ function formatRollMessage(payload) {
121
+ let crit = "";
122
+ if (payload.isCrit) {
123
+ if (payload.critKind === "success") {
124
+ crit = " \u{1F4A5}";
125
+ } else if (payload.critKind === "failure") {
126
+ crit = " \u{1F480}";
127
+ }
128
+ }
129
+ return `\u{1F3B2} ${payload.characterName}: ${payload.title} \u2014 ${payload.total}${crit}`;
130
+ }
131
+ function rollVariant(payload) {
132
+ if (payload.isCrit && payload.critKind === "success") {
133
+ return "success";
134
+ }
135
+ if (payload.isCrit && payload.critKind === "failure") {
136
+ return "warning";
137
+ }
138
+ return "info";
139
+ }
140
+
141
+ export { SHEET_IFRAME_SANDBOX, createBridgeSheetSource, createSheetClient, formatRollMessage, rollVariant };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longstoryshort/vtt-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Embed a longstoryshort.app character sheet in any virtual tabletop via iframe and postMessage",
5
5
  "license": "MIT",
6
6
  "type": "module",