@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.
@@ -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 };