@monad-inc/embed 0.1.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/CHANGELOG.md +21 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +160 -0
- package/USAGE.md +173 -0
- package/dist/connect/index.cjs +271 -0
- package/dist/connect/index.cjs.map +1 -0
- package/dist/connect/index.d.cts +373 -0
- package/dist/connect/index.d.ts +373 -0
- package/dist/connect/index.js +233 -0
- package/dist/connect/index.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postMessage contract between the host page (@monad-inc/embed/connect) and
|
|
3
|
+
* the Monad-hosted iframe (@monad-inc/embed-frame).
|
|
4
|
+
*
|
|
5
|
+
* Why a protocol module: both sides must agree on message shapes, and a
|
|
6
|
+
* drifting contract is a silent failure (a message just gets ignored).
|
|
7
|
+
* @monad-inc/embed/connect owns it because it's the public-facing SDK; the
|
|
8
|
+
* iframe app imports these types from here.
|
|
9
|
+
*
|
|
10
|
+
* Security model:
|
|
11
|
+
* - Origin is the real boundary. Every listener validates
|
|
12
|
+
* `event.origin` against an expected value before trusting a
|
|
13
|
+
* message. The iframe knows its parent's origin (passed as a URL
|
|
14
|
+
* param); the host knows the frame's origin (it chose it).
|
|
15
|
+
* - `event.source` is also checked host-side so a sibling iframe on
|
|
16
|
+
* the same origin can't impersonate ours.
|
|
17
|
+
* - The `source: 'monad-embed'` tag is NOT security — it's just a
|
|
18
|
+
* cheap filter so unrelated postMessage traffic (analytics SDKs,
|
|
19
|
+
* devtools, wallet extensions) is ignored without parsing.
|
|
20
|
+
* - The session token travels in the `init` message body, never in
|
|
21
|
+
* the iframe URL, so it stays out of browser history and Referer.
|
|
22
|
+
*/
|
|
23
|
+
declare const PROTOCOL_SOURCE: "monad-embed";
|
|
24
|
+
declare const PROTOCOL_VERSION: 1;
|
|
25
|
+
type ComponentKind = 'input' | 'output';
|
|
26
|
+
/**
|
|
27
|
+
* Appearance tokens the host may pass to theme the iframe. The iframe
|
|
28
|
+
* applies each as a `--monad-{token}` CSS variable. The host cannot
|
|
29
|
+
* style across the iframe boundary directly (same-origin policy), so
|
|
30
|
+
* this is the only theming channel — same idea as Stripe's `appearance`.
|
|
31
|
+
*/
|
|
32
|
+
interface Appearance {
|
|
33
|
+
colorPrimary?: string;
|
|
34
|
+
colorText?: string;
|
|
35
|
+
colorBackground?: string;
|
|
36
|
+
colorBorder?: string;
|
|
37
|
+
fontFamily?: string;
|
|
38
|
+
borderRadius?: string;
|
|
39
|
+
}
|
|
40
|
+
interface ReadyMessage {
|
|
41
|
+
source: typeof PROTOCOL_SOURCE;
|
|
42
|
+
type: 'ready';
|
|
43
|
+
version: number;
|
|
44
|
+
}
|
|
45
|
+
interface ResizeMessage {
|
|
46
|
+
source: typeof PROTOCOL_SOURCE;
|
|
47
|
+
type: 'resize';
|
|
48
|
+
/** Content height in CSS pixels — the host sizes the <iframe> to this. */
|
|
49
|
+
height: number;
|
|
50
|
+
}
|
|
51
|
+
interface SavedMessage {
|
|
52
|
+
source: typeof PROTOCOL_SOURCE;
|
|
53
|
+
type: 'saved';
|
|
54
|
+
/** Safe reference to the created/updated connector — no secret material. */
|
|
55
|
+
connector: {
|
|
56
|
+
id: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
interface ErrorMessage {
|
|
61
|
+
source: typeof PROTOCOL_SOURCE;
|
|
62
|
+
type: 'error';
|
|
63
|
+
message: string;
|
|
64
|
+
}
|
|
65
|
+
interface CancelMessage {
|
|
66
|
+
source: typeof PROTOCOL_SOURCE;
|
|
67
|
+
type: 'cancel';
|
|
68
|
+
}
|
|
69
|
+
type FrameMessage = ReadyMessage | ResizeMessage | SavedMessage | ErrorMessage | CancelMessage;
|
|
70
|
+
interface InitMessage {
|
|
71
|
+
source: typeof PROTOCOL_SOURCE;
|
|
72
|
+
type: 'init';
|
|
73
|
+
version: number;
|
|
74
|
+
/** Short-lived embed session token minted by the host's backend. */
|
|
75
|
+
sessionToken: string;
|
|
76
|
+
/** Monad API base, e.g. "https://api.monad.com" or dev "http://localhost/api". */
|
|
77
|
+
apiBase: string;
|
|
78
|
+
/**
|
|
79
|
+
* Team org id the session token is scoped to. The iframe uses this to
|
|
80
|
+
* build `/v1/<org>/*` and `/v2/<org>/*` URLs. Must match the token's
|
|
81
|
+
* `org` claim — server-side `org_access.Middleware` will 403 if not.
|
|
82
|
+
*/
|
|
83
|
+
organizationId: string;
|
|
84
|
+
/** Connector type id, e.g. "runzero-findings". */
|
|
85
|
+
typeId: string;
|
|
86
|
+
kind: ComponentKind;
|
|
87
|
+
/** Display name shown in the frame header. */
|
|
88
|
+
displayName?: string;
|
|
89
|
+
/** Existing connector id — present for edit mode, absent for create. */
|
|
90
|
+
existingId?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Whether the synthetic-data toggle is exposed in the form. Defaults
|
|
93
|
+
* to `false` (toggle hidden). Production embed customers keep this
|
|
94
|
+
* off — synthetic mode is a Monad-internal testing affordance.
|
|
95
|
+
*/
|
|
96
|
+
synthetic?: boolean;
|
|
97
|
+
/** Theming tokens applied as CSS variables inside the iframe. */
|
|
98
|
+
appearance?: Appearance;
|
|
99
|
+
/**
|
|
100
|
+
* Stylesheet URLs the host wants to apply to the iframe page. The
|
|
101
|
+
* iframe loads these as `<link rel="stylesheet">` tags before
|
|
102
|
+
* rendering the form.
|
|
103
|
+
*
|
|
104
|
+
* All-or-nothing: if this array is present and non-empty, the
|
|
105
|
+
* iframe loads ONLY these stylesheets and does NOT load its
|
|
106
|
+
* built-in Monad UI defaults. If it's absent or empty, the iframe
|
|
107
|
+
* loads the built-in defaults and nothing else. Mixing host CSS
|
|
108
|
+
* with the defaults is intentionally not supported — partial
|
|
109
|
+
* styling tends to produce confusing half-themed forms.
|
|
110
|
+
*
|
|
111
|
+
* URLs MUST be reachable from the iframe origin (typically by
|
|
112
|
+
* being served from the host page's own origin, which the browser
|
|
113
|
+
* permits for stylesheet `<link>` loads even cross-origin).
|
|
114
|
+
*/
|
|
115
|
+
stylesheets?: string[];
|
|
116
|
+
}
|
|
117
|
+
type HostMessage = InitMessage;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Connector-lifecycle helpers — the "wiring harness" a host (Okta, in
|
|
121
|
+
* the demo's fiction) drops into its own backend to power Disable /
|
|
122
|
+
* Enable / Delete buttons in its own UI.
|
|
123
|
+
*
|
|
124
|
+
* Unlike createConnectorFrame (browser-only), these are isomorphic:
|
|
125
|
+
* they just sequence Monad's standard `/v2/{org}/*` + `/v1/{org}/*`
|
|
126
|
+
* API calls. They run wherever the host holds its API key — almost
|
|
127
|
+
* always its backend.
|
|
128
|
+
*
|
|
129
|
+
* The host supplies a `MonadRequest` thunk that performs an
|
|
130
|
+
* authenticated request and returns parsed JSON. The harness owns the
|
|
131
|
+
* non-obvious parts: which endpoint + verb each action maps to, the
|
|
132
|
+
* delete ordering (pipeline before the input/output it references),
|
|
133
|
+
* and resolving an input id to its pipeline without the host having to
|
|
134
|
+
* store that mapping.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Authenticated request against the Monad API, returning parsed JSON
|
|
138
|
+
* (or undefined for empty responses). The host implements this with
|
|
139
|
+
* its long-lived API key and base URL. Path is the full API path,
|
|
140
|
+
* e.g. `/v2/{org}/pipelines/{id}`.
|
|
141
|
+
*/
|
|
142
|
+
type MonadRequest = (path: string, init?: RequestInit) => Promise<any>;
|
|
143
|
+
/**
|
|
144
|
+
* Where an integration's resources live and their current state.
|
|
145
|
+
* Returned by findIntegrationPipeline so the host can act on an
|
|
146
|
+
* integration knowing only the input id.
|
|
147
|
+
*/
|
|
148
|
+
interface ResolvedIntegration {
|
|
149
|
+
org: string;
|
|
150
|
+
pipelineId: string;
|
|
151
|
+
/** Whether the pipeline is currently enabled. */
|
|
152
|
+
enabled: boolean;
|
|
153
|
+
inputId: string;
|
|
154
|
+
/** Output id of the pipeline's destination node, if it has one. */
|
|
155
|
+
outputId?: string;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Find the pipeline an input feeds. Lets the host act on an integration
|
|
159
|
+
* (disable/enable/delete) knowing only the input id — no need to
|
|
160
|
+
* persist an input→pipeline map of its own.
|
|
161
|
+
*
|
|
162
|
+
* The pipelines list is slim (no node wiring), so this fetches each
|
|
163
|
+
* pipeline's detail and scans for the input node. Fine for the modest
|
|
164
|
+
* pipeline counts a single tenant team has; a host managing many
|
|
165
|
+
* pipelines per team should instead persist the `pipelineId`
|
|
166
|
+
* buildDevNullPipeline returns and skip the lookup.
|
|
167
|
+
*
|
|
168
|
+
* Returns null when the input has no pipeline.
|
|
169
|
+
*/
|
|
170
|
+
declare function findIntegrationPipeline(opts: {
|
|
171
|
+
request: MonadRequest;
|
|
172
|
+
org: string;
|
|
173
|
+
inputId: string;
|
|
174
|
+
}): Promise<ResolvedIntegration | null>;
|
|
175
|
+
interface SetEnabledOptions {
|
|
176
|
+
request: MonadRequest;
|
|
177
|
+
org: string;
|
|
178
|
+
pipelineId: string;
|
|
179
|
+
/** true → resume processing, false → stop (config is kept either way). */
|
|
180
|
+
enabled: boolean;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Enable or disable an integration's pipeline. Disabling stops data
|
|
184
|
+
* flowing but destroys nothing — the input, output, and pipeline
|
|
185
|
+
* config all survive, so a later enable resumes it as it was.
|
|
186
|
+
*
|
|
187
|
+
* The pipeline PATCH endpoint replaces the whole config rather than
|
|
188
|
+
* accepting a partial, so this reads the current pipeline and sends it
|
|
189
|
+
* back with only the `enabled` flag flipped.
|
|
190
|
+
*
|
|
191
|
+
* Race warning: this is GET-then-PATCH on the same resource, so a
|
|
192
|
+
* concurrent edit between the two requests gets clobbered. Fine for
|
|
193
|
+
* human-driven toggles. If you call this from automation, fan-out, or
|
|
194
|
+
* a retry loop, ask the API team for a server-side `{ enabled }`
|
|
195
|
+
* partial PATCH instead — the round-trip we do here is only a
|
|
196
|
+
* workaround for the current full-config replace semantics.
|
|
197
|
+
*/
|
|
198
|
+
declare function setIntegrationEnabled(opts: SetEnabledOptions): Promise<void>;
|
|
199
|
+
/** Convenience: disable the pipeline (stop without deleting). */
|
|
200
|
+
declare function disableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void>;
|
|
201
|
+
/** Convenience: re-enable a disabled pipeline. */
|
|
202
|
+
declare function enableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* Which resources a delete removes. An embed integration is a pipeline
|
|
205
|
+
* + an input + (in the dev/null flow) an output — but hosts provision
|
|
206
|
+
* and own tenant resources differently. Some keep a shared output,
|
|
207
|
+
* some keep the input so it can be re-wired later, some only ever tear
|
|
208
|
+
* down the pipeline. Choose the policy that matches how you provision;
|
|
209
|
+
* a flag left undefined defaults to `true` (delete it).
|
|
210
|
+
*
|
|
211
|
+
* Three ready-made policies are exported — CLEANUP_FULL,
|
|
212
|
+
* CLEANUP_KEEP_OUTPUT, CLEANUP_PIPELINE_ONLY — or pass your own.
|
|
213
|
+
*/
|
|
214
|
+
interface CleanupPolicy {
|
|
215
|
+
/** Delete the pipeline. Default: true. */
|
|
216
|
+
pipeline?: boolean;
|
|
217
|
+
/** Delete the input connector. Default: true. */
|
|
218
|
+
input?: boolean;
|
|
219
|
+
/** Delete the output connector. Default: true. */
|
|
220
|
+
output?: boolean;
|
|
221
|
+
}
|
|
222
|
+
/** Tear down everything — pipeline, input, and output. */
|
|
223
|
+
declare const CLEANUP_FULL: CleanupPolicy;
|
|
224
|
+
/** Remove the pipeline and input but keep the output (e.g. a shared sink). */
|
|
225
|
+
declare const CLEANUP_KEEP_OUTPUT: CleanupPolicy;
|
|
226
|
+
/** Remove only the pipeline; keep the input so it can be re-wired. */
|
|
227
|
+
declare const CLEANUP_PIPELINE_ONLY: CleanupPolicy;
|
|
228
|
+
interface DeleteIntegrationOptions {
|
|
229
|
+
request: MonadRequest;
|
|
230
|
+
org: string;
|
|
231
|
+
/** Pipeline to remove. Omit when the input has no pipeline. */
|
|
232
|
+
pipelineId?: string;
|
|
233
|
+
/** Input to remove. Omit to leave it. */
|
|
234
|
+
inputId?: string;
|
|
235
|
+
/** Output to remove (the auto-created dev/null sink). Omit to leave it. */
|
|
236
|
+
outputId?: string;
|
|
237
|
+
/**
|
|
238
|
+
* What to actually delete. Defaults to everything supplied
|
|
239
|
+
* (CLEANUP_FULL). Set a flag false to keep that resource even when
|
|
240
|
+
* its id is provided.
|
|
241
|
+
*/
|
|
242
|
+
cleanup?: CleanupPolicy;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Delete an integration per a cleanup policy. Order matters: the
|
|
246
|
+
* pipeline references the input and output, so it goes first. Only
|
|
247
|
+
* resources whose id is supplied AND whose policy flag is true are
|
|
248
|
+
* removed — so nothing the host wants to keep is touched, and nothing
|
|
249
|
+
* it wants gone is left orphaned.
|
|
250
|
+
*/
|
|
251
|
+
declare function deleteIntegration(opts: DeleteIntegrationOptions): Promise<void>;
|
|
252
|
+
interface BuildDevNullPipelineOptions {
|
|
253
|
+
request: MonadRequest;
|
|
254
|
+
org: string;
|
|
255
|
+
inputId: string;
|
|
256
|
+
/** Used to name the auto-created output and pipeline. */
|
|
257
|
+
inputName: string;
|
|
258
|
+
/** Status polls before giving up (default 15, ~2s apart). */
|
|
259
|
+
pollAttempts?: number;
|
|
260
|
+
}
|
|
261
|
+
interface BuiltPipeline {
|
|
262
|
+
pipelineId: string;
|
|
263
|
+
outputId: string;
|
|
264
|
+
status: string;
|
|
265
|
+
/** True once the pipeline reports Running. */
|
|
266
|
+
active: boolean;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Stand up a dev/null pipeline for a freshly-configured input: create a
|
|
270
|
+
* dev/null output, create an enabled pipeline wiring input → output,
|
|
271
|
+
* then poll until it reports Running. The POC's "what happens after an
|
|
272
|
+
* input is configured" path, packaged as a reusable harness call.
|
|
273
|
+
*/
|
|
274
|
+
declare function buildDevNullPipeline(opts: BuildDevNullPipelineOptions): Promise<BuiltPipeline>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @monad-inc/embed/connect — host-side SDK for embedding Monad's
|
|
278
|
+
* connector-config UI as a secure iframe.
|
|
279
|
+
*
|
|
280
|
+
* The customer's page calls `createConnectorFrame()`; this injects an
|
|
281
|
+
* <iframe> pointing at a Monad-hosted page (@monad-inc/embed-frame). The
|
|
282
|
+
* connector form — and every secret the user types into it — runs
|
|
283
|
+
* inside that iframe on Monad's origin. Same-origin policy means the
|
|
284
|
+
* customer's own JavaScript can never read those secrets. The customer
|
|
285
|
+
* gets back only a connector id (a safe reference), the same way a
|
|
286
|
+
* Stripe integration only ever sees a `pm_…` token.
|
|
287
|
+
*
|
|
288
|
+
* The host is still responsible for one thing: minting a short-lived
|
|
289
|
+
* embed session token from its backend (POST /v1/embed/sessions, with
|
|
290
|
+
* the long-lived API key) and handing it to `createConnectorFrame`.
|
|
291
|
+
* That token travels into the iframe over postMessage, never the URL.
|
|
292
|
+
*/
|
|
293
|
+
|
|
294
|
+
interface ConnectorFrameOptions {
|
|
295
|
+
/** DOM node (or selector) the iframe is appended to. */
|
|
296
|
+
container: HTMLElement | string;
|
|
297
|
+
/**
|
|
298
|
+
* URL of the Monad embed-frame page. Defaults to
|
|
299
|
+
* "https://app.monad.com/embed" (production). Override for staging
|
|
300
|
+
* ("https://app.monad.security/embed") or local testing
|
|
301
|
+
* ("http://localhost:5173/embed"). MUST be a different origin than
|
|
302
|
+
* the host page — that cross-origin boundary is what isolates the
|
|
303
|
+
* secrets. A path component is allowed (so the embed page can live
|
|
304
|
+
* at a sub-route of the main app); the postMessage security checks
|
|
305
|
+
* compare against the URL's origin, not its full href.
|
|
306
|
+
*/
|
|
307
|
+
frameOrigin?: string;
|
|
308
|
+
/**
|
|
309
|
+
* Monad API base the iframe should call. Defaults to
|
|
310
|
+
* "https://app.monad.com/api" (production). Override for non-prod
|
|
311
|
+
* environments. Set this together with `frameOrigin` — mixing
|
|
312
|
+
* environments will not work.
|
|
313
|
+
*/
|
|
314
|
+
apiBase?: string;
|
|
315
|
+
/** Connector type id, e.g. "runzero-findings". */
|
|
316
|
+
typeId: string;
|
|
317
|
+
kind: ComponentKind;
|
|
318
|
+
/** Short-lived embed session token (host backend mints it). */
|
|
319
|
+
sessionToken: string;
|
|
320
|
+
/**
|
|
321
|
+
* Team org id the session is scoped to. The host backend already knows
|
|
322
|
+
* this — it's the same value passed to `/v1/embed/sessions` when minting
|
|
323
|
+
* the token. Forwarded into the iframe so it can build `/v1/<org>/*` and
|
|
324
|
+
* `/v2/<org>/*` URLs.
|
|
325
|
+
*/
|
|
326
|
+
organizationId: string;
|
|
327
|
+
/** Display name shown in the frame header. */
|
|
328
|
+
displayName?: string;
|
|
329
|
+
/** Existing connector id — pass for edit mode, omit to create. */
|
|
330
|
+
existingId?: string;
|
|
331
|
+
/**
|
|
332
|
+
* Whether the synthetic-data toggle is exposed in the form. Defaults
|
|
333
|
+
* to `false` — production embed flows keep it hidden.
|
|
334
|
+
*/
|
|
335
|
+
synthetic?: boolean;
|
|
336
|
+
/** Theming tokens applied as CSS variables inside the iframe. */
|
|
337
|
+
appearance?: Appearance;
|
|
338
|
+
/**
|
|
339
|
+
* Stylesheet URLs to apply inside the iframe. All-or-nothing: if
|
|
340
|
+
* any URL is provided, the iframe loads ONLY these and skips its
|
|
341
|
+
* built-in Monad UI defaults; if omitted/empty, the iframe loads
|
|
342
|
+
* the defaults and nothing else. URLs must be reachable from the
|
|
343
|
+
* iframe — typically served by the host page's own origin.
|
|
344
|
+
*/
|
|
345
|
+
stylesheets?: string[];
|
|
346
|
+
/** Fired when the user saves successfully. */
|
|
347
|
+
onSave?: (connector: {
|
|
348
|
+
id: string;
|
|
349
|
+
name?: string;
|
|
350
|
+
}) => void;
|
|
351
|
+
/** Fired when the user cancels. */
|
|
352
|
+
onCancel?: () => void;
|
|
353
|
+
/** Fired on a save/test/load failure inside the iframe. */
|
|
354
|
+
onError?: (message: string) => void;
|
|
355
|
+
}
|
|
356
|
+
interface ConnectorFrame {
|
|
357
|
+
/** Remove the iframe and stop listening for its messages. */
|
|
358
|
+
destroy(): void;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Inject the Monad connector iframe into `container` and wire the
|
|
362
|
+
* postMessage channel. Returns a handle whose `destroy()` tears it all
|
|
363
|
+
* down.
|
|
364
|
+
*
|
|
365
|
+
* Handshake:
|
|
366
|
+
* 1. iframe loads, posts `ready`;
|
|
367
|
+
* 2. host replies with `init` (session token, type, appearance);
|
|
368
|
+
* 3. iframe mounts the form and streams `resize` as content grows;
|
|
369
|
+
* 4. on save, iframe posts `saved {connector}`; on cancel, `cancel`.
|
|
370
|
+
*/
|
|
371
|
+
declare function createConnectorFrame(opts: ConnectorFrameOptions): ConnectorFrame;
|
|
372
|
+
|
|
373
|
+
export { type Appearance, type BuildDevNullPipelineOptions, type BuiltPipeline, CLEANUP_FULL, CLEANUP_KEEP_OUTPUT, CLEANUP_PIPELINE_ONLY, type CleanupPolicy, type ComponentKind, type ConnectorFrame, type ConnectorFrameOptions, type DeleteIntegrationOptions, type FrameMessage, type HostMessage, type InitMessage, type MonadRequest, PROTOCOL_SOURCE, PROTOCOL_VERSION, type ResolvedIntegration, type SetEnabledOptions, buildDevNullPipeline, createConnectorFrame, deleteIntegration, disableIntegration, enableIntegration, findIntegrationPipeline, setIntegrationEnabled };
|