@contrail/extensions-sdk 1.0.18-3 → 1.0.18-4

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,35 +1,3 @@
1
1
  # Contrail Extensions SDK
2
2
 
3
- Client library for interfacing with VibeIQ's services and apps from an extension. Extensions run in an iframe inside the host (core) app and communicate via postMessage (Penpal).
4
-
5
- ## Overview
6
-
7
- - **Host (core app)**: Embeds the extension in an iframe, passes initial context, and can push events (e.g. showcase frame changed) to the extension.
8
- - **Extension**: Connects to the host, receives context and events, and can send messages back (e.g. close, or custom commands).
9
-
10
- Detailed guides:
11
-
12
- - **[Opening the extension](docs/OPENING-THE-EXTENSION.md)** — How the host app loads the extension (iframe + register), and how the extension connects. Includes connection order and when to call each API.
13
- - **[Messages and events](docs/MESSAGES-AND-EVENTS.md)** — How the host sends events to the extension and how the extension subscribes to them; how the extension sends messages to the host and **how the core app reads those messages** (via `AppExtensionMessageHandler.handleMessage`).
14
-
15
- ## Quick reference
16
-
17
- ### Host app (core): open the extension
18
-
19
- 1. Create an iframe and set its `src` to your extension's URL.
20
- 2. When the iframe has loaded, call `AppExtensionHost.registerHostWithAppExtension(iframe, context, messageHandler)`.
21
- 3. The host can then push updates with `AppExtensionHost.emitEvent({ type, data })`.
22
-
23
- ### Extension: connect and read messages
24
-
25
- 1. On your extension's page load, call `await AppExtension.registerAppExtension()` (before any code that needs context).
26
- 2. Use `AppExtension.subscribe(eventType, (data) => { ... })` to react to events from the host.
27
- 3. Use `getAppContext()` for the current app context (and use `getExtensionActions().sendMessageToHost(message)` for custom messages to the host).
28
-
29
- ## Installation
30
-
31
- ```bash
32
- npm install @contrail/extensions-sdk
33
- ```
34
-
35
- Requires peer dependency `@contrail/sdk`.
3
+ Client library for interfacing with VibeIQ's services and apps from an extension.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/extensions-sdk",
3
- "version": "1.0.18-3",
3
+ "version": "1.0.18-4",
4
4
  "description": "Client library for interfacing with VibeIQ's services and apps from an extension.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -1,176 +0,0 @@
1
- # Messages and Events
2
-
3
- This guide explains how the **core app (host)** sends events to the extension and how the **extension** reads those events and sends messages back to the host.
4
-
5
- ## Events: host → extension
6
-
7
- The host pushes updates to the extension by emitting **events**. The extension subscribes to event types and receives the payload (and optionally the current value immediately).
8
-
9
- ### Event types
10
-
11
- Events are defined by `AppEventType` and carry a `data` payload:
12
-
13
- | Event type | Description | `data` shape |
14
- |------------|-------------|---------------|
15
- | `SelectedElementsChanged` | Selected elements in the app changed | Selected elements array |
16
- | `ShowcaseFramesChanged` | List of showcase frames changed | `Frame[]` |
17
- | `ShowcaseCurrentFrameChanged` | Current showcase frame changed | `Frame` |
18
-
19
- (Exact types are in `AppExtensionEvent` and the app-context types from the SDK.)
20
-
21
- ### Host: emitting events
22
-
23
- After the host has called `AppExtensionHost.registerHostWithAppExtension(...)` and the promise has resolved, use:
24
-
25
- ```ts
26
- import { AppExtensionHost } from '@contrail/extensions-sdk';
27
- import { AppEventType } from '@contrail/extensions-sdk';
28
-
29
- // Notify the extension that the current frame changed
30
- AppExtensionHost.emitEvent({
31
- type: AppEventType.ShowcaseCurrentFrameChanged,
32
- data: newCurrentFrame,
33
- });
34
-
35
- // Notify that the frames list changed
36
- AppExtensionHost.emitEvent({
37
- type: AppEventType.ShowcaseFramesChanged,
38
- data: framesArray,
39
- });
40
- ```
41
-
42
- - **When to emit**: Whenever the corresponding state changes in the core app (e.g. user switches frame, selection changes). The extension’s subscribed listeners will be called with the new `data`.
43
- - **No extension connected**: If the extension is not connected, `emitEvent` is a no-op (the host may log a warning).
44
-
45
- ### Extension: reading events (subscribe)
46
-
47
- In the extension, after `AppExtension.registerAppExtension()` has resolved, subscribe to one or more event types:
48
-
49
- ```ts
50
- import { AppExtension, AppEventType } from '@contrail/extensions-sdk';
51
-
52
- // Subscribe to current frame changes
53
- const unsubscribe = AppExtension.subscribe(
54
- AppEventType.ShowcaseCurrentFrameChanged,
55
- (frame) => {
56
- console.log('Current frame:', frame);
57
- // Update your UI, load frame-specific data, etc.
58
- }
59
- );
60
-
61
- // Subscribe to frames list changes
62
- AppExtension.subscribe(AppEventType.ShowcaseFramesChanged, (frames) => {
63
- console.log('Frames list:', frames);
64
- });
65
-
66
- // Later: remove the first listener
67
- unsubscribe();
68
- ```
69
-
70
- **Behavior:**
71
-
72
- - **Immediate callback**: The listener is called once immediately with the **current** value for that event (if the host has already set context or emitted that event). After that, it is called again whenever the host emits that event type.
73
- - **Return value**: `subscribe` returns an **unsubscribe** function. Call it to stop receiving that listener (e.g. on component destroy).
74
-
75
- You can subscribe at any time after `registerAppExtension()` has resolved. If you subscribe before the host has emitted an event, the first call to the listener may still happen with the current value from `getAppContext()` (e.g. from `init(context)`).
76
-
77
- ### Extension: reading current context without subscribing
78
-
79
- For the current app context (user, showcase, plan, board, etc.) without listening for updates:
80
-
81
- ```ts
82
- import { getAppContext } from '@contrail/extensions-sdk';
83
-
84
- const ctx = getAppContext();
85
- const showcase = ctx.appContext?.showcase;
86
- const currentFrame = showcase?.currentFrame;
87
- ```
88
-
89
- This is the same context that is updated when the host calls `init(context)` and when the host emits events (the SDK merges event data into the stored context). So after an event, `getAppContext()` reflects the latest state.
90
-
91
- ---
92
-
93
- ## Messages: extension → host
94
-
95
- The extension can send **messages** to the host. The host receives them in the `AppExtensionMessageHandler` it passed to `registerHostWithAppExtension`.
96
-
97
- ### Message format
98
-
99
- Messages are plain objects:
100
-
101
- ```ts
102
- interface AppExtensionMessage {
103
- command: string; // e.g. 'close', 'navigate', or a custom command
104
- data?: any; // optional payload
105
- }
106
- ```
107
-
108
- ### Extension: sending messages to the host
109
-
110
- Use the actions object that the SDK stores after connection. The main entry point for custom host messages is `sendMessageToHost`:
111
-
112
- ```ts
113
- import { getExtensionActions } from '@contrail/extensions-sdk';
114
-
115
- // Send a message to the host
116
- getExtensionActions().sendMessageToHost({
117
- command: 'close',
118
- });
119
-
120
- // Custom command with data
121
- getExtensionActions().sendMessageToHost({
122
- command: 'navigate',
123
- data: { route: '/some-path' },
124
- });
125
- ```
126
-
127
- - **When to use**: For any action that the host must perform (close panel, navigate, open a dialog, etc.). The host implements the behavior in `handleMessage`.
128
- - **If not connected**: `getExtensionActions()` may be undefined or the methods may no-op if the host has not yet registered. Ensure `registerAppExtension()` has resolved before calling.
129
-
130
- ### Host: handling messages from the extension
131
-
132
- When you call `AppExtensionHost.registerHostWithAppExtension(iframe, context, customMessageHandler)`, the `customMessageHandler` is invoked whenever the extension calls `sendMessageToHost(message)`. Implement `handleMessage` to handle each `command`:
133
-
134
- ```ts
135
- const messageHandler: AppExtensionMessageHandler = {
136
- async handleMessage(message) {
137
- switch (message.command) {
138
- case 'close':
139
- // Close the extension panel, remove iframe, etc.
140
- closeExtensionPanel();
141
- break;
142
- case 'navigate':
143
- router.navigate(message.data?.route ?? '/');
144
- break;
145
- default:
146
- console.warn('Unknown extension command:', message.command);
147
- }
148
- },
149
- };
150
- ```
151
-
152
- You can return a value from `handleMessage`; the extension’s `sendMessageToHost` returns a Promise that resolves to that value if the host returns it.
153
-
154
- ---
155
-
156
- ## Other extension → host actions
157
-
158
- The SDK also exposes:
159
-
160
- - **`close()`** — Convenience: the host can implement this as “close the extension” in its message handler (e.g. when handling `command: 'close'`). The extension can call `AppExtension.close()`, which invokes the host’s `close` action.
161
- - **`sendMessageToEntitiesClient(message)`** / **`sendMessageToTypesClient(message)`** — For entity/types client actions from the extension. Document these in your own API docs if you use them.
162
-
163
- The host provides the implementation for these when it registers (via `ExtensionActions`). The extension gets them from `getExtensionActions()` after `registerAppExtension()` has resolved.
164
-
165
- ---
166
-
167
- ## Summary
168
-
169
- | Direction | Mechanism | Host | Extension |
170
- |-----------|------------|------|-----------|
171
- | Host → extension | **Events** | `AppExtensionHost.emitEvent({ type, data })` | `AppExtension.subscribe(eventType, (data) => { ... })` |
172
- | Host → extension | **Initial context** | Pass `context` in `registerHostWithAppExtension(iframe, context, ...)` | `getAppContext()` after `registerAppExtension()` |
173
- | Extension → host | **Messages** | Implement `AppExtensionMessageHandler.handleMessage(message)` | `getExtensionActions().sendMessageToHost({ command, data })` |
174
- | Extension → host | **Close** | Handled in message handler (e.g. `command: 'close'`) or via host’s `close` action | `AppExtension.close()` or `sendMessageToHost({ command: 'close' })` |
175
-
176
- For a working setup, the host must [open the extension](OPENING-THE-EXTENSION.md) first; then events and messages can flow as described above.
@@ -1,154 +0,0 @@
1
- # Opening the Extension
2
-
3
- This guide describes how to **open** (load and connect) an extension from the core app, and what the extension must do to connect back. Both sides must complete their steps for the channel to work.
4
-
5
- ## 1. Core app (host): load and register the extension
6
-
7
- The host app is responsible for embedding the extension and establishing the connection.
8
-
9
- ### Step 1: Create the iframe
10
-
11
- Create an `HTMLIFrameElement` and set its `src` to the URL where your extension app is served (e.g. the extension’s route or deployment URL).
12
-
13
- ```ts
14
- const iframe = document.createElement('iframe');
15
- iframe.src = 'https://your-extension-origin.com/extension'; // Your extension's URL
16
- iframe.setAttribute('data-penpal-iframe', 'true'); // optional, for Penpal
17
- document.getElementById('extension-container').appendChild(iframe);
18
- ```
19
-
20
- The extension app will load inside this iframe. It must call `AppExtension.registerAppExtension()` when it boots (see “Extension side” below).
21
-
22
- ### Step 2: Wait for the iframe to load
23
-
24
- Before calling the SDK, the iframe content must have loaded so the extension script has run and is waiting for the host. Use the iframe’s `load` event:
25
-
26
- ```ts
27
- iframe.addEventListener('load', async () => {
28
- // Extension has loaded; now register the host (see Step 3).
29
- });
30
- ```
31
-
32
- If you call `registerHostWithAppExtension` before the iframe has loaded and the extension has called `registerAppExtension()`, the connection will not be established.
33
-
34
- ### Step 3: Register the host with the extension
35
-
36
- Once the iframe has loaded, call `AppExtensionHost.registerHostWithAppExtension` with:
37
-
38
- 1. **`iframe`** — The same iframe element whose `src` points to the extension.
39
- 2. **`context`** — The initial `AppContext` to inject into the extension (user, app type, plan/board/showcase context, etc.).
40
- 3. **`customMessageHandler`** — An object that implements `AppExtensionMessageHandler` (a `handleMessage(message)` method). This is where the host receives messages **from** the extension (e.g. “close” or custom commands).
41
-
42
- ```ts
43
- import { AppExtensionHost } from '@contrail/extensions-sdk';
44
- import type { AppContext } from '@contrail/extensions-sdk';
45
- import type { AppExtensionMessageHandler } from '@contrail/extensions-sdk';
46
-
47
- const context: AppContext = {
48
- appContext: {
49
- vibeIQApp: VibeIQAppType.SHOWCASE,
50
- showcase: { id: '...', name: '...', orgId: '...', currentFrame: {...}, frames: [...] },
51
- // ...
52
- },
53
- user: currentUser,
54
- };
55
-
56
- const messageHandler: AppExtensionMessageHandler = {
57
- async handleMessage(message) {
58
- if (message.command === 'close') {
59
- // Close the extension panel / iframe.
60
- }
61
- // Handle other commands...
62
- },
63
- };
64
-
65
- await AppExtensionHost.registerHostWithAppExtension(iframe, context, messageHandler);
66
- // Optional: pass a timeout to fail fast if the extension never connects
67
- await AppExtensionHost.registerHostWithAppExtension(iframe, context, messageHandler, { timeoutMs: 15000 });
68
- ```
69
-
70
- After this promise resolves, the host and extension are connected **and** the extension has received and applied the initial context. Only then should you treat the extension as open (e.g. emit events or show UI). The host can then:
71
-
72
- - Call `AppExtensionHost.emitEvent({ type, data })` to push events to the extension.
73
- - Receive messages from the extension via `messageHandler.handleMessage(message)`.
74
-
75
- ### Summary: host flow
76
-
77
- 1. Create iframe, set `iframe.src` to extension URL.
78
- 2. Append iframe to DOM.
79
- 3. On iframe `load`, call `AppExtensionHost.registerHostWithAppExtension(iframe, context, messageHandler)`.
80
- 4. Use `AppExtensionHost.emitEvent(...)` to send events; handle incoming messages in `messageHandler.handleMessage`.
81
-
82
- ---
83
-
84
- ## 2. Extension: connect and receive context
85
-
86
- The extension runs inside the iframe. It must register with the host so that the SDK can receive context and events.
87
-
88
- ### Step 1: Call `registerAppExtension()` on page load
89
-
90
- As early as possible in your extension’s application bootstrap (e.g. in `main.ts`, or the top-level component that mounts the extension UI), call:
91
-
92
- ```ts
93
- import { AppExtension } from '@contrail/extensions-sdk';
94
-
95
- async function bootstrap() {
96
- await AppExtension.registerAppExtension();
97
- // Optional: await AppExtension.registerAppExtension({ timeoutMs: 15000 });
98
- // Now getAppContext() and subscribe() will work.
99
- startYourApp();
100
- }
101
- bootstrap();
102
- ```
103
-
104
- - This method **must** be called from the extension’s iframe (the same document that the host loads in the iframe).
105
- - It establishes the postMessage channel with the parent window (the host) and waits for the host to call `registerHostWithAppExtension`. Once the host registers, the extension receives the initial `AppContext` and the promise resolves.
106
- - Do **not** rely on `getAppContext()` or `AppExtension.subscribe()` before this promise has resolved; the context will be empty and events will not be received until the host has sent `init(context)`.
107
-
108
- ### Step 2: Use context and subscribe to events
109
-
110
- After `registerAppExtension()` has resolved:
111
-
112
- - **Context**: Use `getAppContext()` from `@contrail/extensions-sdk` to read the current app context (user, showcase, plan, board, etc.).
113
- - **Events**: Use `AppExtension.subscribe(eventType, (data) => { ... })` to react to events pushed by the host (e.g. `ShowcaseCurrentFrameChanged`). See [Messages and events](MESSAGES-AND-EVENTS.md).
114
-
115
- ### Summary: extension flow
116
-
117
- 1. Extension page loads inside the host’s iframe.
118
- 2. On load, call `await AppExtension.registerAppExtension()` and wait for it to resolve.
119
- 3. After that, use `getAppContext()` and `AppExtension.subscribe(...)` to read context and react to host events.
120
-
121
- ---
122
-
123
- ## Connection order
124
-
125
- For the connection to succeed:
126
-
127
- 1. Host creates iframe and sets `src` → extension page loads.
128
- 2. Extension script runs and calls `registerAppExtension()` → extension waits for the host.
129
- 3. Host’s iframe fires `load` → host calls `registerHostWithAppExtension(iframe, context, messageHandler)`.
130
- 4. SDK (Penpal) links the two → host’s `init(context)` runs in the extension, then both sides can use `emitEvent` / `subscribe` and the message handler.
131
-
132
- If the host registers too early (before the iframe has loaded and the extension has called `registerAppExtension()`), the connection will fail. Always register the host in the iframe’s `load` callback (or an equivalent “extension ready” signal).
133
-
134
- ---
135
-
136
- ## Troubleshooting: extension won’t open
137
-
138
- If the extension never opens or the connection never completes:
139
-
140
- 1. **Check the iframe URL** — Ensure `iframe.src` points to the correct extension origin and path. Open the URL in a tab and confirm the extension app loads without script errors.
141
-
142
- 2. **Extension must call `registerAppExtension()` on load** — The extension’s entry point (e.g. `main.ts`) must call `await AppExtension.registerAppExtension()` before any code that uses `getAppContext()` or `subscribe()`. If your app bootstraps Angular or other services first, they may run before context is available; consider awaiting `registerAppExtension()` before bootstrapping.
143
-
144
- 3. **Host must register after the iframe has loaded** — Call `registerHostWithAppExtension(...)` inside the iframe’s `load` event (or after you know the extension script has run). If you call it before the extension has called `registerAppExtension()`, the host will wait indefinitely unless you use `timeoutMs`.
145
-
146
- 4. **Use optional timeout** — Pass `{ timeoutMs: 15000 }` (or similar) as the fourth argument to `registerHostWithAppExtension`, or `{ timeoutMs: 15000 }` to `registerAppExtension()`. If the other side doesn’t connect in time, you’ll get `HostConnectionTimeoutError` or `ExtensionConnectionTimeoutError` instead of hanging.
147
-
148
- 5. **Check the console** — Look for Penpal or SDK errors in both the host and the extension (inspect the iframe’s context in devtools). Common issues: wrong origin, script errors in the extension before `registerAppExtension()` runs, or the host never calling `registerHostWithAppExtension`.
149
-
150
- 6. **Await `registerHostWithAppExtension`** — The host should `await` the call and only then consider the extension “open” and emit events. The promise resolves after the extension has received and applied the initial context.
151
-
152
- ---
153
-
154
- **Next:** To send events to the extension and **read messages from the extension** in the core app, see [Messages and events](MESSAGES-AND-EVENTS.md).