@monad-inc/embed 0.3.1 → 0.4.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e801d03: Add a `default` key to `configMetaOverrides` fields so hosts can prefill a value on new connectors.
8
+
3
9
  ## 0.3.1
4
10
 
5
11
  ### Patch Changes
package/USAGE.md CHANGED
@@ -120,7 +120,7 @@ controls the surrounding chrome.
120
120
  | `isDescriptionEditable` | `boolean` | ⛔ | `false` | Show an editable description input in the iframe (prefilled with `description` if provided). |
121
121
  | `appearance` | `Appearance` | ⛔ | Monad defaults | Theming tokens (see below). |
122
122
  | `stylesheets` | `string[]` | ⛔ | Monad's built-in stylesheet | Your own stylesheet URLs. All-or-nothing: if set, Monad's defaults are not loaded (see below). |
123
- | `configMetaOverrides` | `ConfigMetaOverrides` | ⛔ | the connector's own copy | Per-field label / description / placeholder overrides (see below). |
123
+ | `configMetaOverrides` | `ConfigMetaOverrides` | ⛔ | the connector's own copy | Per-field label / description / placeholder / default overrides (see below). |
124
124
  | `synthetic` | `boolean` | ⛔ | `false` | Exposes Monad's internal synthetic-data toggle. Keep off in production. |
125
125
  | `frameOrigin` | `string` | ⛔ | `https://app.monad.com/embed` | Override only for non-prod testing. |
126
126
  | `apiBase` | `string` | ⛔ | `https://app.monad.com/api` | Override only for non-prod testing. |
@@ -190,13 +190,25 @@ original copy.
190
190
 
191
191
  **`placeholder` is a hint, not a default.** It shows as grey text and
192
192
  disappears as soon as the user types. A field the user never touches
193
- still submits an empty value. There is no option to pre-fill a real
194
- value, because that would change the data your connector is configured
195
- with.
193
+ still submits an empty value. To pre-fill a real value, use `default`.
196
194
 
197
195
  ### What you can override
198
196
 
199
- Per field: `name`, `description`, `placeholder`.
197
+ Per field: `name`, `description`, `placeholder`, `default`.
198
+
199
+ `default` pre-fills the field when a user creates a connector. The value
200
+ is editable and is submitted like any other. It is ignored when editing an
201
+ existing connector, so the saved value always wins. It works on string
202
+ (including dropdowns — the value must be one of the options), number, and
203
+ boolean fields; anything else, or a value of the wrong type, is ignored.
204
+
205
+ ```ts
206
+ configMetaOverrides: {
207
+ settings: {
208
+ summary_config: { children: { alert_source: { default: 'northstar' } } },
209
+ },
210
+ }
211
+ ```
200
212
 
201
213
  Nested fields are reached with `children`, and the variants of a
202
214
  multiple-choice field with `discriminator.one_of`:
@@ -217,8 +229,8 @@ configMetaOverrides: {
217
229
  }
218
230
  ```
219
231
 
220
- A field's type, whether it is required, its allowed values, and its
221
- default are fixed by the connector and cannot be overridden. Some
232
+ A field's type, whether it is required, and its allowed values are fixed
233
+ by the connector and cannot be overridden. Some
222
234
  controls have nowhere to show a placeholder — dropdowns, checkboxes, and
223
235
  code editors ignore it. If you need a field to _behave_ differently,
224
236
  that's a change to the connector itself; file an issue.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/connect/index.ts","../../src/connect/protocol.ts","../../src/connect/lifecycle.ts"],"sourcesContent":["/**\n * @monad-inc/embed/connect — host-side SDK for embedding Monad's\n * connector-config UI as a secure iframe.\n *\n * The customer's page calls `createConnectorFrame()`; this injects an\n * <iframe> pointing at a Monad-hosted page (@monad-inc/embed-frame). The\n * connector form — and every secret the user types into it — runs\n * inside that iframe on Monad's origin. Same-origin policy means the\n * customer's own JavaScript can never read those secrets. The customer\n * gets back only a connector id (a safe reference), the same way a\n * Stripe integration only ever sees a `pm_…` token.\n *\n * The host is still responsible for one thing: minting a short-lived\n * embed session token from its backend (POST /v1/embed/sessions, with\n * the long-lived API key) and handing it to `createConnectorFrame`.\n * That token travels into the iframe over postMessage, never the URL.\n */\nimport {\n\tPROTOCOL_SOURCE,\n\tPROTOCOL_VERSION,\n\ttype Appearance,\n\ttype ComponentKind,\n\ttype ConfigMetaOverrides,\n\ttype FrameMessage,\n\ttype HostMessage\n} from './protocol';\n\nexport {\n\tPROTOCOL_SOURCE,\n\tPROTOCOL_VERSION,\n\ttype Appearance,\n\ttype ComponentKind,\n\ttype ConfigMetaOverrides,\n\ttype DiscriminatorMetaOverride,\n\ttype FieldMetaOverride,\n\ttype FrameMessage,\n\ttype HostMessage,\n\ttype InitMessage\n} from './protocol';\n\nexport {\n\tfindIntegrationPipeline,\n\tsetIntegrationEnabled,\n\tdisableIntegration,\n\tenableIntegration,\n\tdeleteIntegration,\n\tbuildDevNullPipeline,\n\tCLEANUP_FULL,\n\tCLEANUP_KEEP_OUTPUT,\n\tCLEANUP_PIPELINE_ONLY,\n\ttype MonadRequest,\n\ttype ResolvedIntegration,\n\ttype SetEnabledOptions,\n\ttype DeleteIntegrationOptions,\n\ttype CleanupPolicy,\n\ttype BuildDevNullPipelineOptions,\n\ttype BuiltPipeline\n} from './lifecycle';\n\nexport interface ConnectorFrameOptions {\n\t/** DOM node (or selector) the iframe is appended to. */\n\tcontainer: HTMLElement | string;\n\t/**\n\t * URL of the Monad embed-frame page. Defaults to\n\t * \"https://app.monad.com/embed\" (production). Override for staging\n\t * (\"https://app.monad.security/embed\") or local testing\n\t * (\"http://localhost:5173/embed\"). MUST be a different origin than\n\t * the host page — that cross-origin boundary is what isolates the\n\t * secrets. A path component is allowed (so the embed page can live\n\t * at a sub-route of the main app); the postMessage security checks\n\t * compare against the URL's origin, not its full href.\n\t */\n\tframeOrigin?: string;\n\t/**\n\t * Monad API base the iframe should call. Defaults to\n\t * \"https://app.monad.com/api\" (production). Override for non-prod\n\t * environments. Set this together with `frameOrigin` — mixing\n\t * environments will not work.\n\t */\n\tapiBase?: string;\n\t/** Connector type id, e.g. \"runzero-findings\". */\n\ttypeId: string;\n\tkind: ComponentKind;\n\t/** Short-lived embed session token (host backend mints it). */\n\tsessionToken: string;\n\t/**\n\t * Sub-org id the session is scoped to. The host backend already knows\n\t * this — it's the same value passed to `/v1/embed/sessions` when minting\n\t * the token. Forwarded into the iframe so it can build `/v1/<org>/*` and\n\t * `/v2/<org>/*` URLs.\n\t */\n\torganizationId: string;\n\t/** Display name shown in the frame header. */\n\tdisplayName?: string;\n\t/**\n\t * Name for the created connector. On create, defaults to the connector\n\t * type's name plus a short unique suffix. On edit, omit to keep the\n\t * existing name.\n\t */\n\tname?: string;\n\t/**\n\t * Description for the created connector. On create, defaults to the\n\t * connector type's description. On edit, omit to keep the existing\n\t * description.\n\t */\n\tdescription?: string;\n\t/**\n\t * Show an editable name input inside the iframe (prefilled with the\n\t * resolved name) so the end user can name the connector themselves.\n\t * A non-empty name is required when shown. Defaults to false.\n\t */\n\tisNameEditable?: boolean;\n\t/**\n\t * Show an editable description input inside the iframe (prefilled with\n\t * the resolved description). Defaults to false.\n\t */\n\tisDescriptionEditable?: boolean;\n\t/** Existing connector id — pass for edit mode, omit to create. */\n\texistingId?: string;\n\t/**\n\t * Whether the synthetic-data toggle is exposed in the form. Defaults\n\t * to `false` — production embed flows keep it hidden.\n\t */\n\tsynthetic?: boolean;\n\t/** Theming tokens applied as CSS variables inside the iframe. */\n\tappearance?: Appearance;\n\t/**\n\t * Stylesheet URLs to apply inside the iframe. All-or-nothing: if\n\t * any URL is provided, the iframe loads ONLY these and skips its\n\t * built-in Monad UI defaults; if omitted/empty, the iframe loads\n\t * the defaults and nothing else. URLs must be reachable from the\n\t * iframe — typically served by the host page's own origin.\n\t */\n\tstylesheets?: string[];\n\t/**\n\t * Per-field copy overrides for the connector's config form: label,\n\t * description, placeholder. Copy only — nothing that changes a\n\t * field's type, requiredness, or submitted value.\n\t *\n\t * configMetaOverrides: {\n\t * settings: {\n\t * ddsource: { placeholder: 'northstar' },\n\t * service: { placeholder: 'northstar-web' },\n\t * },\n\t * }\n\t *\n\t * Keys are the connector's own wire keys, so check the connector-type\n\t * response rather than guessing — the Datadog output's source field is\n\t * `ddsource`, not `source`. Unmatched keys are ignored.\n\t *\n\t * `placeholder` is a hint, not a default: a field the user never\n\t * touches still submits `\"\"`. Nested fields are reached with\n\t * `children`, union variants with `discriminator.one_of.<variant>`.\n\t */\n\tconfigMetaOverrides?: ConfigMetaOverrides;\n\t/** Fired when the user saves successfully. */\n\tonSave?: (connector: { id: string; name?: string }) => void;\n\t/** Fired when the user cancels. */\n\tonCancel?: () => void;\n\t/** Fired on a save/test/load failure inside the iframe. */\n\tonError?: (message: string) => void;\n}\n\nexport interface ConnectorFrame {\n\t/** Remove the iframe and stop listening for its messages. */\n\tdestroy(): void;\n}\n\n/**\n * Production defaults. Customers integrating against prod don't pass\n * `frameOrigin`/`apiBase` at all; non-prod environments override both\n * together. Treat these as a public API contract — changing a hostname\n * is a breaking change for anyone on the default.\n */\nconst DEFAULT_FRAME_ORIGIN = 'https://app.monad.com/embed';\nconst DEFAULT_API_BASE = 'https://app.monad.com/api';\n\n/**\n * Inject the Monad connector iframe into `container` and wire the\n * postMessage channel. Returns a handle whose `destroy()` tears it all\n * down.\n *\n * Handshake:\n * 1. iframe loads, posts `ready`;\n * 2. host replies with `init` (session token, type, appearance);\n * 3. iframe mounts the form and streams `resize` as content grows;\n * 4. on save, iframe posts `saved {connector}`; on cancel, `cancel`.\n */\nexport function createConnectorFrame(opts: ConnectorFrameOptions): ConnectorFrame {\n\tconst container =\n\t\ttypeof opts.container === 'string'\n\t\t\t? document.querySelector<HTMLElement>(opts.container)\n\t\t\t: opts.container;\n\tif (!container) {\n\t\tthrow new Error('[monad-inc/connect] container element not found');\n\t}\n\n\tconst frameInput = opts.frameOrigin ?? DEFAULT_FRAME_ORIGIN;\n\tconst apiBase = opts.apiBase ?? DEFAULT_API_BASE;\n\n\t// Split the input URL into two pieces:\n\t// * `frameUrl` keeps the path (e.g. /embed) — used as iframe.src.\n\t// * `frameOrigin` is the bare origin — used for the postMessage\n\t// security checks below, which must compare against the value\n\t// the browser surfaces on MessageEvent.origin (always a bare\n\t// origin, never a path).\n\t// Previously these were collapsed into one `new URL(...).href`,\n\t// which silently broke any frameOrigin with a path — the equality\n\t// check `event.origin !== frameOrigin` would always fail and `init`\n\t// never made it back to the iframe.\n\tconst frameUrl = new URL(frameInput);\n\tconst frameOrigin = frameUrl.origin;\n\tconst framePath = frameUrl.pathname.replace(/\\/$/, '');\n\n\tconst iframe = document.createElement('iframe');\n\tiframe.className = 'monad-connect-frame';\n\tiframe.title = 'Monad connector configuration';\n\tiframe.style.width = '100%';\n\tiframe.style.border = '0';\n\tiframe.style.display = 'block';\n\t// The iframe needs its own scripts and same-origin access to the\n\t// Monad API; it must NOT get same-origin access to the host. A\n\t// cross-origin iframe already can't reach the host — no sandbox\n\t// attribute is needed for isolation, and adding one would block the\n\t// form's own scripts. Forms/popups stay enabled for OAuth-style\n\t// connectors that may open a window.\n\tiframe.allow = 'clipboard-write';\n\t// parentOrigin lets the iframe scope its postMessage target + sender\n\t// check. It is NOT a secret and NOT a capability — lying about it\n\t// only breaks the liar's own embed (see protocol.ts).\n\tiframe.src = `${frameOrigin}${framePath}/?parentOrigin=${encodeURIComponent(window.location.origin)}`;\n\n\tlet destroyed = false;\n\n\tfunction postToFrame(msg: HostMessage): void {\n\t\tiframe.contentWindow?.postMessage(msg, frameOrigin);\n\t}\n\n\tfunction onMessage(event: MessageEvent): void {\n\t\tif (destroyed) return;\n\t\t// --- security boundary: validate before trusting anything ---\n\t\tif (event.origin !== frameOrigin) return;\n\t\tif (event.source !== iframe.contentWindow) return;\n\t\tconst msg = event.data as FrameMessage | undefined;\n\t\tif (!msg || msg.source !== PROTOCOL_SOURCE) return;\n\n\t\tswitch (msg.type) {\n\t\t\tcase 'ready':\n\t\t\t\tpostToFrame({\n\t\t\t\t\tsource: PROTOCOL_SOURCE,\n\t\t\t\t\ttype: 'init',\n\t\t\t\t\tversion: PROTOCOL_VERSION,\n\t\t\t\t\tsessionToken: opts.sessionToken,\n\t\t\t\t\tapiBase,\n\t\t\t\t\torganizationId: opts.organizationId,\n\t\t\t\t\ttypeId: opts.typeId,\n\t\t\t\t\tkind: opts.kind,\n\t\t\t\t\tdisplayName: opts.displayName,\n\t\t\t\t\tname: opts.name,\n\t\t\t\t\tdescription: opts.description,\n\t\t\t\t\tisNameEditable: opts.isNameEditable,\n\t\t\t\t\tisDescriptionEditable: opts.isDescriptionEditable,\n\t\t\t\t\texistingId: opts.existingId,\n\t\t\t\t\tsynthetic: opts.synthetic,\n\t\t\t\t\tappearance: opts.appearance,\n\t\t\t\t\tstylesheets: opts.stylesheets,\n\t\t\t\t\tconfigMetaOverrides: opts.configMetaOverrides\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase 'resize': {\n\t\t\t\t// Clamp to a sane range so a renderer bug in the iframe\n\t\t\t\t// can't blow up the host layout with a multi-million-px\n\t\t\t\t// height. The origin check above already gates who can\n\t\t\t\t// send this — clamping is defense in depth. Number.isFinite\n\t\t\t\t// guards against non-numeric heights that would otherwise\n\t\t\t\t// produce \"NaNpx\" and leave the iframe collapsed.\n\t\t\t\tconst h = Number.isFinite(msg.height) ? msg.height : 0;\n\t\t\t\tiframe.style.height = `${Math.min(Math.max(h, 0), 20000)}px`;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'saved':\n\t\t\t\topts.onSave?.(msg.connector);\n\t\t\t\tbreak;\n\t\t\tcase 'error':\n\t\t\t\topts.onError?.(msg.message);\n\t\t\t\tbreak;\n\t\t\tcase 'cancel':\n\t\t\t\topts.onCancel?.();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\twindow.addEventListener('message', onMessage);\n\tcontainer.appendChild(iframe);\n\n\treturn {\n\t\tdestroy(): void {\n\t\t\tdestroyed = true;\n\t\t\twindow.removeEventListener('message', onMessage);\n\t\t\tiframe.remove();\n\t\t}\n\t};\n}\n","/**\n * postMessage contract between the host page (@monad-inc/embed/connect) and\n * the Monad-hosted iframe (@monad-inc/embed-frame).\n *\n * Why a protocol module: both sides must agree on message shapes, and a\n * drifting contract is a silent failure (a message just gets ignored).\n * @monad-inc/embed/connect owns it because it's the public-facing SDK; the\n * iframe app imports these types from here.\n *\n * Security model:\n * - Origin is the real boundary. Every listener validates\n * `event.origin` against an expected value before trusting a\n * message. The iframe knows its parent's origin (passed as a URL\n * param); the host knows the frame's origin (it chose it).\n * - `event.source` is also checked host-side so a sibling iframe on\n * the same origin can't impersonate ours.\n * - The `source: 'monad-embed'` tag is NOT security — it's just a\n * cheap filter so unrelated postMessage traffic (analytics SDKs,\n * devtools, wallet extensions) is ignored without parsing.\n * - The session token travels in the `init` message body, never in\n * the iframe URL, so it stays out of browser history and Referer.\n */\n\nexport const PROTOCOL_SOURCE = 'monad-embed' as const;\nexport const PROTOCOL_VERSION = 1 as const;\n\nexport type ComponentKind = 'input' | 'output';\n\n/**\n * Appearance tokens the host may pass to theme the iframe. The iframe\n * applies each as a `--monad-{token}` CSS variable. The host cannot\n * style across the iframe boundary directly (same-origin policy), so\n * this is the only theming channel — same idea as Stripe's `appearance`.\n */\nexport interface Appearance {\n\tcolorPrimary?: string;\n\tcolorText?: string;\n\tcolorBackground?: string;\n\tcolorBorder?: string;\n\tfontFamily?: string;\n\tborderRadius?: string;\n}\n\n/**\n * Copy overrides for a single field of a connector's config form.\n *\n * Only copy can be overridden. A field's type, requiredness, allowed\n * values, and default are fixed by the connector and cannot be changed\n * here.\n *\n * `placeholder` is a hint, not a default: a field the user never touches\n * still submits an empty value.\n *\n * Keys mirror the config shape returned for the connector type, so they\n * are snake_case — unlike the camelCase options on\n * `ConnectorFrameOptions`. Field keys are the connector's own keys; the\n * Datadog output's source field is `ddsource`, not `source`. Keys that\n * don't match the connector are ignored.\n */\nexport interface FieldMetaOverride {\n\t/** Field label. An empty string is ignored — unlabelled fields are a bug. */\n\tname?: string;\n\t/** Helper text shown with the control. Empty string blanks it. */\n\tdescription?: string;\n\t/** Ephemeral hint text inside the control. Empty string blanks it. */\n\tplaceholder?: string;\n\t/** Overrides for a nested object's child fields, keyed by child wire key. */\n\tchildren?: Record<string, FieldMetaOverride>;\n\t/** Overrides for a discriminated-union (one_of) field. */\n\tdiscriminator?: DiscriminatorMetaOverride;\n}\n\n/**\n * Copy overrides for a one_of field's variant selector. The set of\n * variants, their order, and which one is selected by default are fixed\n * by the connector.\n */\nexport interface DiscriminatorMetaOverride {\n\t/** Label on the variant selector (defaults to \"Type\"). */\n\tname?: string;\n\t/** Helper text for the variant selector. */\n\tdescription?: string;\n\t/** Per-variant overrides, keyed by discriminator value. */\n\tone_of?: Record<string, FieldMetaOverride>;\n}\n\n/**\n * Config-form copy overrides for one connector, mirroring the two\n * sections returned for the connector type. Overrides never add fields —\n * keys that don't match the connector are ignored.\n */\nexport interface ConfigMetaOverrides {\n\tsettings?: Record<string, FieldMetaOverride>;\n\tsecrets?: Record<string, FieldMetaOverride>;\n}\n\n/* ===== iframe -> host ===== */\n\nexport interface ReadyMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'ready';\n\tversion: number;\n}\n\nexport interface ResizeMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'resize';\n\t/** Content height in CSS pixels — the host sizes the <iframe> to this. */\n\theight: number;\n}\n\nexport interface SavedMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'saved';\n\t/** Safe reference to the created/updated connector — no secret material. */\n\tconnector: { id: string; name?: string };\n}\n\nexport interface ErrorMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'error';\n\tmessage: string;\n}\n\nexport interface CancelMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'cancel';\n}\n\nexport type FrameMessage =\n\t| ReadyMessage\n\t| ResizeMessage\n\t| SavedMessage\n\t| ErrorMessage\n\t| CancelMessage;\n\n/* ===== host -> iframe ===== */\n\nexport interface InitMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'init';\n\tversion: number;\n\t/** Short-lived embed session token minted by the host's backend. */\n\tsessionToken: string;\n\t/** Monad API base, e.g. \"https://api.monad.com\" or dev \"http://localhost/api\". */\n\tapiBase: string;\n\t/**\n\t * Sub-org id the session token is scoped to. The iframe uses this to\n\t * build `/v1/<org>/*` and `/v2/<org>/*` URLs. Must match the token's\n\t * `org` claim — server-side `org_access.Middleware` will 403 if not.\n\t */\n\torganizationId: string;\n\t/** Connector type id, e.g. \"runzero-findings\". */\n\ttypeId: string;\n\tkind: ComponentKind;\n\t/** Display name shown in the frame header. */\n\tdisplayName?: string;\n\t/**\n\t * Name for the created connector. On create, defaults to the connector\n\t * type's name plus a short unique suffix. On edit, omit to keep the\n\t * existing name.\n\t */\n\tname?: string;\n\t/**\n\t * Description for the created connector. On create, defaults to the\n\t * connector type's description. On edit, omit to keep the existing\n\t * description.\n\t */\n\tdescription?: string;\n\t/**\n\t * Show an editable name input inside the iframe, prefilled with the\n\t * resolved name, so the end user can name the connector themselves.\n\t * A non-empty name is required when shown. Defaults to false.\n\t */\n\tisNameEditable?: boolean;\n\t/**\n\t * Show an editable description input inside the iframe, prefilled with\n\t * the resolved description. Defaults to false.\n\t */\n\tisDescriptionEditable?: boolean;\n\t/** Existing connector id — present for edit mode, absent for create. */\n\texistingId?: string;\n\t/**\n\t * Whether the synthetic-data toggle is exposed in the form. Defaults\n\t * to `false` (toggle hidden). Production embed customers keep this\n\t * off — synthetic mode is a Monad-internal testing affordance.\n\t */\n\tsynthetic?: boolean;\n\t/** Theming tokens applied as CSS variables inside the iframe. */\n\tappearance?: Appearance;\n\t/**\n\t * Stylesheet URLs the host wants to apply to the iframe page. The\n\t * iframe loads these as `<link rel=\"stylesheet\">` tags before\n\t * rendering the form.\n\t *\n\t * All-or-nothing: if this array is present and non-empty, the\n\t * iframe loads ONLY these stylesheets and does NOT load its\n\t * built-in Monad UI defaults. If it's absent or empty, the iframe\n\t * loads the built-in defaults and nothing else. Mixing host CSS\n\t * with the defaults is intentionally not supported — partial\n\t * styling tends to produce confusing half-themed forms.\n\t *\n\t * URLs MUST be reachable from the iframe origin (typically by\n\t * being served from the host page's own origin, which the browser\n\t * permits for stylesheet `<link>` loads even cross-origin).\n\t */\n\tstylesheets?: string[];\n\t/**\n\t * Per-field copy overrides merged onto the connector's config form.\n\t * Copy only — see FieldMetaOverride.\n\t */\n\tconfigMetaOverrides?: ConfigMetaOverrides;\n}\n\nexport type HostMessage = InitMessage;\n","/**\n * Connector-lifecycle helpers — the \"wiring harness\" a host (Okta, in\n * the demo's fiction) drops into its own backend to power Disable /\n * Enable / Delete buttons in its own UI.\n *\n * Unlike createConnectorFrame (browser-only), these are isomorphic:\n * they just sequence Monad's standard `/v2/{org}/*` + `/v1/{org}/*`\n * API calls. They run wherever the host holds its API key — almost\n * always its backend.\n *\n * The host supplies a `MonadRequest` thunk that performs an\n * authenticated request and returns parsed JSON. The harness owns the\n * non-obvious parts: which endpoint + verb each action maps to, the\n * delete ordering (pipeline before the input/output it references),\n * and resolving an input id to its pipeline without the host having to\n * store that mapping.\n */\n\n/**\n * Authenticated request against the Monad API, returning parsed JSON\n * (or undefined for empty responses). The host implements this with\n * its long-lived API key and base URL. Path is the full API path,\n * e.g. `/v2/{org}/pipelines/{id}`.\n */\nexport type MonadRequest = (path: string, init?: RequestInit) => Promise<any>;\n\n/* ===== resolve ===== */\n\n/**\n * Where an integration's resources live and their current state.\n * Returned by findIntegrationPipeline so the host can act on an\n * integration knowing only the input id.\n */\nexport interface ResolvedIntegration {\n\torg: string;\n\tpipelineId: string;\n\t/** Whether the pipeline is currently enabled. */\n\tenabled: boolean;\n\tinputId: string;\n\t/** Output id of the pipeline's destination node, if it has one. */\n\toutputId?: string;\n}\n\ninterface PipelineNode {\n\tcomponent_id?: string;\n\tcomponent_type?: string;\n}\n\n/**\n * Find the pipeline an input feeds. Lets the host act on an integration\n * (disable/enable/delete) knowing only the input id — no need to\n * persist an input→pipeline map of its own.\n *\n * The pipelines list is slim (no node wiring), so this fetches each\n * pipeline's detail and scans for the input node. Fine for the modest\n * pipeline counts a single tenant sub-org has; a host managing many\n * pipelines per sub-org should instead persist the `pipelineId`\n * buildDevNullPipeline returns and skip the lookup.\n *\n * Returns null when the input has no pipeline.\n */\nexport async function findIntegrationPipeline(opts: {\n\trequest: MonadRequest;\n\torg: string;\n\tinputId: string;\n}): Promise<ResolvedIntegration | null> {\n\tconst { request, org, inputId } = opts;\n\tconst listed = await request(`/v2/${org}/pipelines/`);\n\tconst pipelines: any[] = Array.isArray(listed)\n\t\t? listed\n\t\t: (listed?.pipelines ?? listed?.data ?? []);\n\n\tfor (const summary of pipelines) {\n\t\tif (!summary?.id) continue;\n\t\tlet detail: any;\n\t\ttry {\n\t\t\tdetail = await request(`/v2/${org}/pipelines/${summary.id}`);\n\t\t} catch {\n\t\t\tcontinue; // skip a pipeline we can't read rather than failing\n\t\t}\n\t\tconst pipeline = detail?.config ?? detail;\n\t\tconst nodes: PipelineNode[] = pipeline?.nodes ?? [];\n\t\tconst inputNode = nodes.find((n) => n.component_type === 'input' && n.component_id === inputId);\n\t\tif (!inputNode) continue;\n\t\tconst outputNode = nodes.find((n) => n.component_type === 'output');\n\t\treturn {\n\t\t\torg,\n\t\t\tpipelineId: summary.id,\n\t\t\tenabled: Boolean(pipeline.enabled),\n\t\t\tinputId,\n\t\t\toutputId: outputNode?.component_id\n\t\t};\n\t}\n\treturn null;\n}\n\n/* ===== enable / disable ===== */\n\nexport interface SetEnabledOptions {\n\trequest: MonadRequest;\n\torg: string;\n\tpipelineId: string;\n\t/** true → resume processing, false → stop (config is kept either way). */\n\tenabled: boolean;\n}\n\n/**\n * Enable or disable an integration's pipeline. Disabling stops data\n * flowing but destroys nothing — the input, output, and pipeline\n * config all survive, so a later enable resumes it as it was.\n *\n * The pipeline PATCH endpoint replaces the whole config rather than\n * accepting a partial, so this reads the current pipeline and sends it\n * back with only the `enabled` flag flipped.\n *\n * Race warning: this is GET-then-PATCH on the same resource, so a\n * concurrent edit between the two requests gets clobbered. Fine for\n * human-driven toggles. If you call this from automation, fan-out, or\n * a retry loop, ask the API team for a server-side `{ enabled }`\n * partial PATCH instead — the round-trip we do here is only a\n * workaround for the current full-config replace semantics.\n */\nexport async function setIntegrationEnabled(opts: SetEnabledOptions): Promise<void> {\n\tconst { request, org, pipelineId, enabled } = opts;\n\tconst detail = await request(`/v2/${org}/pipelines/${pipelineId}`);\n\tconst pipeline = detail?.config ?? detail;\n\tawait request(`/v2/${org}/pipelines/${pipelineId}`, {\n\t\tmethod: 'PATCH',\n\t\tbody: JSON.stringify({\n\t\t\tname: pipeline.name,\n\t\t\tdescription: pipeline.description ?? '',\n\t\t\tenabled,\n\t\t\tnodes: (pipeline.nodes ?? []).map((n: any) => ({\n\t\t\t\tid: n.id,\n\t\t\t\tslug: n.slug,\n\t\t\t\tcomponent_id: n.component_id,\n\t\t\t\tcomponent_type: n.component_type,\n\t\t\t\tenabled: n.enabled ?? true\n\t\t\t})),\n\t\t\tedges: (pipeline.edges ?? []).map((e: any) => ({\n\t\t\t\tname: e.name,\n\t\t\t\tdescription: e.description ?? '',\n\t\t\t\tfrom_node_instance_id: e.from_node_instance_id,\n\t\t\t\tto_node_instance_id: e.to_node_instance_id,\n\t\t\t\tdisabled: e.disabled ?? false,\n\t\t\t\tconditions: e.conditions\n\t\t\t}))\n\t\t})\n\t});\n}\n\n/** Convenience: disable the pipeline (stop without deleting). */\nexport function disableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void> {\n\treturn setIntegrationEnabled({ ...opts, enabled: false });\n}\n\n/** Convenience: re-enable a disabled pipeline. */\nexport function enableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void> {\n\treturn setIntegrationEnabled({ ...opts, enabled: true });\n}\n\n/* ===== delete ===== */\n\n/**\n * Which resources a delete removes. An embed integration is a pipeline\n * + an input + (in the dev/null flow) an output — but hosts provision\n * and own tenant resources differently. Some keep a shared output,\n * some keep the input so it can be re-wired later, some only ever tear\n * down the pipeline. Choose the policy that matches how you provision;\n * a flag left undefined defaults to `true` (delete it).\n *\n * Three ready-made policies are exported — CLEANUP_FULL,\n * CLEANUP_KEEP_OUTPUT, CLEANUP_PIPELINE_ONLY — or pass your own.\n */\nexport interface CleanupPolicy {\n\t/** Delete the pipeline. Default: true. */\n\tpipeline?: boolean;\n\t/** Delete the input connector. Default: true. */\n\tinput?: boolean;\n\t/** Delete the output connector. Default: true. */\n\toutput?: boolean;\n}\n\n/** Tear down everything — pipeline, input, and output. */\nexport const CLEANUP_FULL: CleanupPolicy = { pipeline: true, input: true, output: true };\n\n/** Remove the pipeline and input but keep the output (e.g. a shared sink). */\nexport const CLEANUP_KEEP_OUTPUT: CleanupPolicy = {\n\tpipeline: true,\n\tinput: true,\n\toutput: false\n};\n\n/** Remove only the pipeline; keep the input so it can be re-wired. */\nexport const CLEANUP_PIPELINE_ONLY: CleanupPolicy = {\n\tpipeline: true,\n\tinput: false,\n\toutput: false\n};\n\nexport interface DeleteIntegrationOptions {\n\trequest: MonadRequest;\n\torg: string;\n\t/** Pipeline to remove. Omit when the input has no pipeline. */\n\tpipelineId?: string;\n\t/** Input to remove. Omit to leave it. */\n\tinputId?: string;\n\t/** Output to remove (the auto-created dev/null sink). Omit to leave it. */\n\toutputId?: string;\n\t/**\n\t * What to actually delete. Defaults to everything supplied\n\t * (CLEANUP_FULL). Set a flag false to keep that resource even when\n\t * its id is provided.\n\t */\n\tcleanup?: CleanupPolicy;\n}\n\n/**\n * Delete an integration per a cleanup policy. Order matters: the\n * pipeline references the input and output, so it goes first. Only\n * resources whose id is supplied AND whose policy flag is true are\n * removed — so nothing the host wants to keep is touched, and nothing\n * it wants gone is left orphaned.\n */\nexport async function deleteIntegration(opts: DeleteIntegrationOptions): Promise<void> {\n\tconst { request, org, pipelineId, inputId, outputId } = opts;\n\tconst policy: Required<CleanupPolicy> = {\n\t\tpipeline: true,\n\t\tinput: true,\n\t\toutput: true,\n\t\t...opts.cleanup\n\t};\n\tif (pipelineId && policy.pipeline) {\n\t\tawait request(`/v2/${org}/pipelines/${pipelineId}`, { method: 'DELETE' });\n\t}\n\tif (inputId && policy.input) {\n\t\tawait request(`/v1/${org}/inputs/${inputId}`, { method: 'DELETE' });\n\t}\n\tif (outputId && policy.output) {\n\t\tawait request(`/v1/${org}/outputs/${outputId}`, { method: 'DELETE' });\n\t}\n}\n\n/* ===== build ===== */\n\nexport interface BuildDevNullPipelineOptions {\n\trequest: MonadRequest;\n\torg: string;\n\tinputId: string;\n\t/** Used to name the auto-created output and pipeline. */\n\tinputName: string;\n\t/** Status polls before giving up (default 15, ~2s apart). */\n\tpollAttempts?: number;\n}\n\nexport interface BuiltPipeline {\n\tpipelineId: string;\n\toutputId: string;\n\tstatus: string;\n\t/** True once the pipeline reports Running. */\n\tactive: boolean;\n}\n\n/**\n * Stand up a dev/null pipeline for a freshly-configured input: create a\n * dev/null output, create an enabled pipeline wiring input → output,\n * then poll until it reports Running. The POC's \"what happens after an\n * input is configured\" path, packaged as a reusable harness call.\n */\nexport async function buildDevNullPipeline(\n\topts: BuildDevNullPipelineOptions\n): Promise<BuiltPipeline> {\n\tconst { request, org, inputId, inputName, pollAttempts = 15 } = opts;\n\tconst label = inputName || 'Input';\n\n\tconst output = await request(`/v2/${org}/outputs`, {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({\n\t\t\toutput_type: 'dev-null',\n\t\t\tname: `${label} → /dev/null`,\n\t\t\tdescription: 'Auto-created sink for embed pipeline',\n\t\t\tpromise_id: '',\n\t\t\tconfig: { settings: {}, secrets: {} }\n\t\t})\n\t});\n\n\tconst pipeline = await request(`/v2/${org}/pipelines/`, {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({\n\t\t\tname: `${label} pipeline`,\n\t\t\tdescription: 'Auto-created when the input was configured via embed',\n\t\t\tenabled: true,\n\t\t\tnodes: [\n\t\t\t\t{ slug: 'in', component_id: inputId, component_type: 'input', enabled: true },\n\t\t\t\t{ slug: 'out', component_id: output.id, component_type: 'output', enabled: true }\n\t\t\t],\n\t\t\tedges: [\n\t\t\t\t{\n\t\t\t\t\tfrom_node_instance_id: 'in',\n\t\t\t\t\tto_node_instance_id: 'out',\n\t\t\t\t\tdescription: 'all records',\n\t\t\t\t\tconditions: { operator: 'always' }\n\t\t\t\t}\n\t\t\t]\n\t\t})\n\t});\n\n\tlet status = 'Pending';\n\tfor (let attempt = 0; attempt < pollAttempts; attempt++) {\n\t\tconst s = await request(`/v2/${org}/pipelines/${pipeline.id}/status`);\n\t\tstatus = s?.status ?? 'Unknown';\n\t\tif (status === 'Running' || status === 'Erroring') break;\n\t\tawait new Promise((r) => setTimeout(r, 2000));\n\t}\n\n\treturn {\n\t\tpipelineId: pipeline.id,\n\t\toutputId: output.id,\n\t\tstatus,\n\t\tactive: status === 'Running'\n\t};\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/connect/index.ts","../../src/connect/protocol.ts","../../src/connect/lifecycle.ts"],"sourcesContent":["/**\n * @monad-inc/embed/connect — host-side SDK for embedding Monad's\n * connector-config UI as a secure iframe.\n *\n * The customer's page calls `createConnectorFrame()`; this injects an\n * <iframe> pointing at a Monad-hosted page (@monad-inc/embed-frame). The\n * connector form — and every secret the user types into it — runs\n * inside that iframe on Monad's origin. Same-origin policy means the\n * customer's own JavaScript can never read those secrets. The customer\n * gets back only a connector id (a safe reference), the same way a\n * Stripe integration only ever sees a `pm_…` token.\n *\n * The host is still responsible for one thing: minting a short-lived\n * embed session token from its backend (POST /v1/embed/sessions, with\n * the long-lived API key) and handing it to `createConnectorFrame`.\n * That token travels into the iframe over postMessage, never the URL.\n */\nimport {\n\tPROTOCOL_SOURCE,\n\tPROTOCOL_VERSION,\n\ttype Appearance,\n\ttype ComponentKind,\n\ttype ConfigMetaOverrides,\n\ttype FrameMessage,\n\ttype HostMessage\n} from './protocol';\n\nexport {\n\tPROTOCOL_SOURCE,\n\tPROTOCOL_VERSION,\n\ttype Appearance,\n\ttype ComponentKind,\n\ttype ConfigMetaOverrides,\n\ttype DiscriminatorMetaOverride,\n\ttype FieldMetaOverride,\n\ttype FrameMessage,\n\ttype HostMessage,\n\ttype InitMessage\n} from './protocol';\n\nexport {\n\tfindIntegrationPipeline,\n\tsetIntegrationEnabled,\n\tdisableIntegration,\n\tenableIntegration,\n\tdeleteIntegration,\n\tbuildDevNullPipeline,\n\tCLEANUP_FULL,\n\tCLEANUP_KEEP_OUTPUT,\n\tCLEANUP_PIPELINE_ONLY,\n\ttype MonadRequest,\n\ttype ResolvedIntegration,\n\ttype SetEnabledOptions,\n\ttype DeleteIntegrationOptions,\n\ttype CleanupPolicy,\n\ttype BuildDevNullPipelineOptions,\n\ttype BuiltPipeline\n} from './lifecycle';\n\nexport interface ConnectorFrameOptions {\n\t/** DOM node (or selector) the iframe is appended to. */\n\tcontainer: HTMLElement | string;\n\t/**\n\t * URL of the Monad embed-frame page. Defaults to\n\t * \"https://app.monad.com/embed\" (production). Override for staging\n\t * (\"https://app.monad.security/embed\") or local testing\n\t * (\"http://localhost:5173/embed\"). MUST be a different origin than\n\t * the host page — that cross-origin boundary is what isolates the\n\t * secrets. A path component is allowed (so the embed page can live\n\t * at a sub-route of the main app); the postMessage security checks\n\t * compare against the URL's origin, not its full href.\n\t */\n\tframeOrigin?: string;\n\t/**\n\t * Monad API base the iframe should call. Defaults to\n\t * \"https://app.monad.com/api\" (production). Override for non-prod\n\t * environments. Set this together with `frameOrigin` — mixing\n\t * environments will not work.\n\t */\n\tapiBase?: string;\n\t/** Connector type id, e.g. \"runzero-findings\". */\n\ttypeId: string;\n\tkind: ComponentKind;\n\t/** Short-lived embed session token (host backend mints it). */\n\tsessionToken: string;\n\t/**\n\t * Sub-org id the session is scoped to. The host backend already knows\n\t * this — it's the same value passed to `/v1/embed/sessions` when minting\n\t * the token. Forwarded into the iframe so it can build `/v1/<org>/*` and\n\t * `/v2/<org>/*` URLs.\n\t */\n\torganizationId: string;\n\t/** Display name shown in the frame header. */\n\tdisplayName?: string;\n\t/**\n\t * Name for the created connector. On create, defaults to the connector\n\t * type's name plus a short unique suffix. On edit, omit to keep the\n\t * existing name.\n\t */\n\tname?: string;\n\t/**\n\t * Description for the created connector. On create, defaults to the\n\t * connector type's description. On edit, omit to keep the existing\n\t * description.\n\t */\n\tdescription?: string;\n\t/**\n\t * Show an editable name input inside the iframe (prefilled with the\n\t * resolved name) so the end user can name the connector themselves.\n\t * A non-empty name is required when shown. Defaults to false.\n\t */\n\tisNameEditable?: boolean;\n\t/**\n\t * Show an editable description input inside the iframe (prefilled with\n\t * the resolved description). Defaults to false.\n\t */\n\tisDescriptionEditable?: boolean;\n\t/** Existing connector id — pass for edit mode, omit to create. */\n\texistingId?: string;\n\t/**\n\t * Whether the synthetic-data toggle is exposed in the form. Defaults\n\t * to `false` — production embed flows keep it hidden.\n\t */\n\tsynthetic?: boolean;\n\t/** Theming tokens applied as CSS variables inside the iframe. */\n\tappearance?: Appearance;\n\t/**\n\t * Stylesheet URLs to apply inside the iframe. All-or-nothing: if\n\t * any URL is provided, the iframe loads ONLY these and skips its\n\t * built-in Monad UI defaults; if omitted/empty, the iframe loads\n\t * the defaults and nothing else. URLs must be reachable from the\n\t * iframe — typically served by the host page's own origin.\n\t */\n\tstylesheets?: string[];\n\t/**\n\t * Per-field copy overrides for the connector's config form: label,\n\t * description, placeholder. Copy only — nothing that changes a\n\t * field's type, requiredness, or submitted value.\n\t *\n\t * configMetaOverrides: {\n\t * settings: {\n\t * ddsource: { placeholder: 'northstar' },\n\t * service: { placeholder: 'northstar-web' },\n\t * },\n\t * }\n\t *\n\t * Keys are the connector's own wire keys, so check the connector-type\n\t * response rather than guessing — the Datadog output's source field is\n\t * `ddsource`, not `source`. Unmatched keys are ignored.\n\t *\n\t * `placeholder` is a hint, not a default: a field the user never\n\t * touches still submits `\"\"`. Nested fields are reached with\n\t * `children`, union variants with `discriminator.one_of.<variant>`.\n\t */\n\tconfigMetaOverrides?: ConfigMetaOverrides;\n\t/** Fired when the user saves successfully. */\n\tonSave?: (connector: { id: string; name?: string }) => void;\n\t/** Fired when the user cancels. */\n\tonCancel?: () => void;\n\t/** Fired on a save/test/load failure inside the iframe. */\n\tonError?: (message: string) => void;\n}\n\nexport interface ConnectorFrame {\n\t/** Remove the iframe and stop listening for its messages. */\n\tdestroy(): void;\n}\n\n/**\n * Production defaults. Customers integrating against prod don't pass\n * `frameOrigin`/`apiBase` at all; non-prod environments override both\n * together. Treat these as a public API contract — changing a hostname\n * is a breaking change for anyone on the default.\n */\nconst DEFAULT_FRAME_ORIGIN = 'https://app.monad.com/embed';\nconst DEFAULT_API_BASE = 'https://app.monad.com/api';\n\n/**\n * Inject the Monad connector iframe into `container` and wire the\n * postMessage channel. Returns a handle whose `destroy()` tears it all\n * down.\n *\n * Handshake:\n * 1. iframe loads, posts `ready`;\n * 2. host replies with `init` (session token, type, appearance);\n * 3. iframe mounts the form and streams `resize` as content grows;\n * 4. on save, iframe posts `saved {connector}`; on cancel, `cancel`.\n */\nexport function createConnectorFrame(opts: ConnectorFrameOptions): ConnectorFrame {\n\tconst container =\n\t\ttypeof opts.container === 'string'\n\t\t\t? document.querySelector<HTMLElement>(opts.container)\n\t\t\t: opts.container;\n\tif (!container) {\n\t\tthrow new Error('[monad-inc/connect] container element not found');\n\t}\n\n\tconst frameInput = opts.frameOrigin ?? DEFAULT_FRAME_ORIGIN;\n\tconst apiBase = opts.apiBase ?? DEFAULT_API_BASE;\n\n\t// Split the input URL into two pieces:\n\t// * `frameUrl` keeps the path (e.g. /embed) — used as iframe.src.\n\t// * `frameOrigin` is the bare origin — used for the postMessage\n\t// security checks below, which must compare against the value\n\t// the browser surfaces on MessageEvent.origin (always a bare\n\t// origin, never a path).\n\t// Previously these were collapsed into one `new URL(...).href`,\n\t// which silently broke any frameOrigin with a path — the equality\n\t// check `event.origin !== frameOrigin` would always fail and `init`\n\t// never made it back to the iframe.\n\tconst frameUrl = new URL(frameInput);\n\tconst frameOrigin = frameUrl.origin;\n\tconst framePath = frameUrl.pathname.replace(/\\/$/, '');\n\n\tconst iframe = document.createElement('iframe');\n\tiframe.className = 'monad-connect-frame';\n\tiframe.title = 'Monad connector configuration';\n\tiframe.style.width = '100%';\n\tiframe.style.border = '0';\n\tiframe.style.display = 'block';\n\t// The iframe needs its own scripts and same-origin access to the\n\t// Monad API; it must NOT get same-origin access to the host. A\n\t// cross-origin iframe already can't reach the host — no sandbox\n\t// attribute is needed for isolation, and adding one would block the\n\t// form's own scripts. Forms/popups stay enabled for OAuth-style\n\t// connectors that may open a window.\n\tiframe.allow = 'clipboard-write';\n\t// parentOrigin lets the iframe scope its postMessage target + sender\n\t// check. It is NOT a secret and NOT a capability — lying about it\n\t// only breaks the liar's own embed (see protocol.ts).\n\tiframe.src = `${frameOrigin}${framePath}/?parentOrigin=${encodeURIComponent(window.location.origin)}`;\n\n\tlet destroyed = false;\n\n\tfunction postToFrame(msg: HostMessage): void {\n\t\tiframe.contentWindow?.postMessage(msg, frameOrigin);\n\t}\n\n\tfunction onMessage(event: MessageEvent): void {\n\t\tif (destroyed) return;\n\t\t// --- security boundary: validate before trusting anything ---\n\t\tif (event.origin !== frameOrigin) return;\n\t\tif (event.source !== iframe.contentWindow) return;\n\t\tconst msg = event.data as FrameMessage | undefined;\n\t\tif (!msg || msg.source !== PROTOCOL_SOURCE) return;\n\n\t\tswitch (msg.type) {\n\t\t\tcase 'ready':\n\t\t\t\tpostToFrame({\n\t\t\t\t\tsource: PROTOCOL_SOURCE,\n\t\t\t\t\ttype: 'init',\n\t\t\t\t\tversion: PROTOCOL_VERSION,\n\t\t\t\t\tsessionToken: opts.sessionToken,\n\t\t\t\t\tapiBase,\n\t\t\t\t\torganizationId: opts.organizationId,\n\t\t\t\t\ttypeId: opts.typeId,\n\t\t\t\t\tkind: opts.kind,\n\t\t\t\t\tdisplayName: opts.displayName,\n\t\t\t\t\tname: opts.name,\n\t\t\t\t\tdescription: opts.description,\n\t\t\t\t\tisNameEditable: opts.isNameEditable,\n\t\t\t\t\tisDescriptionEditable: opts.isDescriptionEditable,\n\t\t\t\t\texistingId: opts.existingId,\n\t\t\t\t\tsynthetic: opts.synthetic,\n\t\t\t\t\tappearance: opts.appearance,\n\t\t\t\t\tstylesheets: opts.stylesheets,\n\t\t\t\t\tconfigMetaOverrides: opts.configMetaOverrides\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase 'resize': {\n\t\t\t\t// Clamp to a sane range so a renderer bug in the iframe\n\t\t\t\t// can't blow up the host layout with a multi-million-px\n\t\t\t\t// height. The origin check above already gates who can\n\t\t\t\t// send this — clamping is defense in depth. Number.isFinite\n\t\t\t\t// guards against non-numeric heights that would otherwise\n\t\t\t\t// produce \"NaNpx\" and leave the iframe collapsed.\n\t\t\t\tconst h = Number.isFinite(msg.height) ? msg.height : 0;\n\t\t\t\tiframe.style.height = `${Math.min(Math.max(h, 0), 20000)}px`;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'saved':\n\t\t\t\topts.onSave?.(msg.connector);\n\t\t\t\tbreak;\n\t\t\tcase 'error':\n\t\t\t\topts.onError?.(msg.message);\n\t\t\t\tbreak;\n\t\t\tcase 'cancel':\n\t\t\t\topts.onCancel?.();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\twindow.addEventListener('message', onMessage);\n\tcontainer.appendChild(iframe);\n\n\treturn {\n\t\tdestroy(): void {\n\t\t\tdestroyed = true;\n\t\t\twindow.removeEventListener('message', onMessage);\n\t\t\tiframe.remove();\n\t\t}\n\t};\n}\n","/**\n * postMessage contract between the host page (@monad-inc/embed/connect) and\n * the Monad-hosted iframe (@monad-inc/embed-frame).\n *\n * Why a protocol module: both sides must agree on message shapes, and a\n * drifting contract is a silent failure (a message just gets ignored).\n * @monad-inc/embed/connect owns it because it's the public-facing SDK; the\n * iframe app imports these types from here.\n *\n * Security model:\n * - Origin is the real boundary. Every listener validates\n * `event.origin` against an expected value before trusting a\n * message. The iframe knows its parent's origin (passed as a URL\n * param); the host knows the frame's origin (it chose it).\n * - `event.source` is also checked host-side so a sibling iframe on\n * the same origin can't impersonate ours.\n * - The `source: 'monad-embed'` tag is NOT security — it's just a\n * cheap filter so unrelated postMessage traffic (analytics SDKs,\n * devtools, wallet extensions) is ignored without parsing.\n * - The session token travels in the `init` message body, never in\n * the iframe URL, so it stays out of browser history and Referer.\n */\n\nexport const PROTOCOL_SOURCE = 'monad-embed' as const;\nexport const PROTOCOL_VERSION = 1 as const;\n\nexport type ComponentKind = 'input' | 'output';\n\n/**\n * Appearance tokens the host may pass to theme the iframe. The iframe\n * applies each as a `--monad-{token}` CSS variable. The host cannot\n * style across the iframe boundary directly (same-origin policy), so\n * this is the only theming channel — same idea as Stripe's `appearance`.\n */\nexport interface Appearance {\n\tcolorPrimary?: string;\n\tcolorText?: string;\n\tcolorBackground?: string;\n\tcolorBorder?: string;\n\tfontFamily?: string;\n\tborderRadius?: string;\n}\n\n/**\n * Copy overrides for a single field of a connector's config form.\n *\n * Only copy and a scalar `default` can be overridden. A field's type,\n * requiredness, and allowed values are fixed by the connector.\n *\n * `placeholder` is a hint: a field the user never touches still submits\n * an empty value. Use `default` to prefill a real value.\n *\n * Keys mirror the config shape returned for the connector type, so they\n * are snake_case — unlike the camelCase options on\n * `ConnectorFrameOptions`. Field keys are the connector's own keys; the\n * Datadog output's source field is `ddsource`, not `source`. Keys that\n * don't match the connector are ignored.\n */\nexport interface FieldMetaOverride {\n\t/** Field label. An empty string is ignored — unlabelled fields are a bug. */\n\tname?: string;\n\t/** Helper text shown with the control. Empty string blanks it. */\n\tdescription?: string;\n\t/** Ephemeral hint text inside the control. Empty string blanks it. */\n\tplaceholder?: string;\n\t/**\n\t * Prefilled value for a new connector. Stays editable and is submitted.\n\t * Ignored when editing, so the saved value wins. Applies to string\n\t * (within its allowed values), number and boolean fields only.\n\t */\n\tdefault?: string | number | boolean;\n\t/** Overrides for a nested object's child fields, keyed by child wire key. */\n\tchildren?: Record<string, FieldMetaOverride>;\n\t/** Overrides for a discriminated-union (one_of) field. */\n\tdiscriminator?: DiscriminatorMetaOverride;\n}\n\n/**\n * Copy overrides for a one_of field's variant selector. The set of\n * variants, their order, and which one is selected by default are fixed\n * by the connector.\n */\nexport interface DiscriminatorMetaOverride {\n\t/** Label on the variant selector (defaults to \"Type\"). */\n\tname?: string;\n\t/** Helper text for the variant selector. */\n\tdescription?: string;\n\t/** Per-variant overrides, keyed by discriminator value. */\n\tone_of?: Record<string, FieldMetaOverride>;\n}\n\n/**\n * Config-form copy overrides for one connector, mirroring the two\n * sections returned for the connector type. Overrides never add fields —\n * keys that don't match the connector are ignored.\n */\nexport interface ConfigMetaOverrides {\n\tsettings?: Record<string, FieldMetaOverride>;\n\tsecrets?: Record<string, FieldMetaOverride>;\n}\n\n/* ===== iframe -> host ===== */\n\nexport interface ReadyMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'ready';\n\tversion: number;\n}\n\nexport interface ResizeMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'resize';\n\t/** Content height in CSS pixels — the host sizes the <iframe> to this. */\n\theight: number;\n}\n\nexport interface SavedMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'saved';\n\t/** Safe reference to the created/updated connector — no secret material. */\n\tconnector: { id: string; name?: string };\n}\n\nexport interface ErrorMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'error';\n\tmessage: string;\n}\n\nexport interface CancelMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'cancel';\n}\n\nexport type FrameMessage =\n\t| ReadyMessage\n\t| ResizeMessage\n\t| SavedMessage\n\t| ErrorMessage\n\t| CancelMessage;\n\n/* ===== host -> iframe ===== */\n\nexport interface InitMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'init';\n\tversion: number;\n\t/** Short-lived embed session token minted by the host's backend. */\n\tsessionToken: string;\n\t/** Monad API base, e.g. \"https://api.monad.com\" or dev \"http://localhost/api\". */\n\tapiBase: string;\n\t/**\n\t * Sub-org id the session token is scoped to. The iframe uses this to\n\t * build `/v1/<org>/*` and `/v2/<org>/*` URLs. Must match the token's\n\t * `org` claim — server-side `org_access.Middleware` will 403 if not.\n\t */\n\torganizationId: string;\n\t/** Connector type id, e.g. \"runzero-findings\". */\n\ttypeId: string;\n\tkind: ComponentKind;\n\t/** Display name shown in the frame header. */\n\tdisplayName?: string;\n\t/**\n\t * Name for the created connector. On create, defaults to the connector\n\t * type's name plus a short unique suffix. On edit, omit to keep the\n\t * existing name.\n\t */\n\tname?: string;\n\t/**\n\t * Description for the created connector. On create, defaults to the\n\t * connector type's description. On edit, omit to keep the existing\n\t * description.\n\t */\n\tdescription?: string;\n\t/**\n\t * Show an editable name input inside the iframe, prefilled with the\n\t * resolved name, so the end user can name the connector themselves.\n\t * A non-empty name is required when shown. Defaults to false.\n\t */\n\tisNameEditable?: boolean;\n\t/**\n\t * Show an editable description input inside the iframe, prefilled with\n\t * the resolved description. Defaults to false.\n\t */\n\tisDescriptionEditable?: boolean;\n\t/** Existing connector id — present for edit mode, absent for create. */\n\texistingId?: string;\n\t/**\n\t * Whether the synthetic-data toggle is exposed in the form. Defaults\n\t * to `false` (toggle hidden). Production embed customers keep this\n\t * off — synthetic mode is a Monad-internal testing affordance.\n\t */\n\tsynthetic?: boolean;\n\t/** Theming tokens applied as CSS variables inside the iframe. */\n\tappearance?: Appearance;\n\t/**\n\t * Stylesheet URLs the host wants to apply to the iframe page. The\n\t * iframe loads these as `<link rel=\"stylesheet\">` tags before\n\t * rendering the form.\n\t *\n\t * All-or-nothing: if this array is present and non-empty, the\n\t * iframe loads ONLY these stylesheets and does NOT load its\n\t * built-in Monad UI defaults. If it's absent or empty, the iframe\n\t * loads the built-in defaults and nothing else. Mixing host CSS\n\t * with the defaults is intentionally not supported — partial\n\t * styling tends to produce confusing half-themed forms.\n\t *\n\t * URLs MUST be reachable from the iframe origin (typically by\n\t * being served from the host page's own origin, which the browser\n\t * permits for stylesheet `<link>` loads even cross-origin).\n\t */\n\tstylesheets?: string[];\n\t/**\n\t * Per-field copy overrides merged onto the connector's config form.\n\t * Copy only — see FieldMetaOverride.\n\t */\n\tconfigMetaOverrides?: ConfigMetaOverrides;\n}\n\nexport type HostMessage = InitMessage;\n","/**\n * Connector-lifecycle helpers — the \"wiring harness\" a host (Okta, in\n * the demo's fiction) drops into its own backend to power Disable /\n * Enable / Delete buttons in its own UI.\n *\n * Unlike createConnectorFrame (browser-only), these are isomorphic:\n * they just sequence Monad's standard `/v2/{org}/*` + `/v1/{org}/*`\n * API calls. They run wherever the host holds its API key — almost\n * always its backend.\n *\n * The host supplies a `MonadRequest` thunk that performs an\n * authenticated request and returns parsed JSON. The harness owns the\n * non-obvious parts: which endpoint + verb each action maps to, the\n * delete ordering (pipeline before the input/output it references),\n * and resolving an input id to its pipeline without the host having to\n * store that mapping.\n */\n\n/**\n * Authenticated request against the Monad API, returning parsed JSON\n * (or undefined for empty responses). The host implements this with\n * its long-lived API key and base URL. Path is the full API path,\n * e.g. `/v2/{org}/pipelines/{id}`.\n */\nexport type MonadRequest = (path: string, init?: RequestInit) => Promise<any>;\n\n/* ===== resolve ===== */\n\n/**\n * Where an integration's resources live and their current state.\n * Returned by findIntegrationPipeline so the host can act on an\n * integration knowing only the input id.\n */\nexport interface ResolvedIntegration {\n\torg: string;\n\tpipelineId: string;\n\t/** Whether the pipeline is currently enabled. */\n\tenabled: boolean;\n\tinputId: string;\n\t/** Output id of the pipeline's destination node, if it has one. */\n\toutputId?: string;\n}\n\ninterface PipelineNode {\n\tcomponent_id?: string;\n\tcomponent_type?: string;\n}\n\n/**\n * Find the pipeline an input feeds. Lets the host act on an integration\n * (disable/enable/delete) knowing only the input id — no need to\n * persist an input→pipeline map of its own.\n *\n * The pipelines list is slim (no node wiring), so this fetches each\n * pipeline's detail and scans for the input node. Fine for the modest\n * pipeline counts a single tenant sub-org has; a host managing many\n * pipelines per sub-org should instead persist the `pipelineId`\n * buildDevNullPipeline returns and skip the lookup.\n *\n * Returns null when the input has no pipeline.\n */\nexport async function findIntegrationPipeline(opts: {\n\trequest: MonadRequest;\n\torg: string;\n\tinputId: string;\n}): Promise<ResolvedIntegration | null> {\n\tconst { request, org, inputId } = opts;\n\tconst listed = await request(`/v2/${org}/pipelines/`);\n\tconst pipelines: any[] = Array.isArray(listed)\n\t\t? listed\n\t\t: (listed?.pipelines ?? listed?.data ?? []);\n\n\tfor (const summary of pipelines) {\n\t\tif (!summary?.id) continue;\n\t\tlet detail: any;\n\t\ttry {\n\t\t\tdetail = await request(`/v2/${org}/pipelines/${summary.id}`);\n\t\t} catch {\n\t\t\tcontinue; // skip a pipeline we can't read rather than failing\n\t\t}\n\t\tconst pipeline = detail?.config ?? detail;\n\t\tconst nodes: PipelineNode[] = pipeline?.nodes ?? [];\n\t\tconst inputNode = nodes.find((n) => n.component_type === 'input' && n.component_id === inputId);\n\t\tif (!inputNode) continue;\n\t\tconst outputNode = nodes.find((n) => n.component_type === 'output');\n\t\treturn {\n\t\t\torg,\n\t\t\tpipelineId: summary.id,\n\t\t\tenabled: Boolean(pipeline.enabled),\n\t\t\tinputId,\n\t\t\toutputId: outputNode?.component_id\n\t\t};\n\t}\n\treturn null;\n}\n\n/* ===== enable / disable ===== */\n\nexport interface SetEnabledOptions {\n\trequest: MonadRequest;\n\torg: string;\n\tpipelineId: string;\n\t/** true → resume processing, false → stop (config is kept either way). */\n\tenabled: boolean;\n}\n\n/**\n * Enable or disable an integration's pipeline. Disabling stops data\n * flowing but destroys nothing — the input, output, and pipeline\n * config all survive, so a later enable resumes it as it was.\n *\n * The pipeline PATCH endpoint replaces the whole config rather than\n * accepting a partial, so this reads the current pipeline and sends it\n * back with only the `enabled` flag flipped.\n *\n * Race warning: this is GET-then-PATCH on the same resource, so a\n * concurrent edit between the two requests gets clobbered. Fine for\n * human-driven toggles. If you call this from automation, fan-out, or\n * a retry loop, ask the API team for a server-side `{ enabled }`\n * partial PATCH instead — the round-trip we do here is only a\n * workaround for the current full-config replace semantics.\n */\nexport async function setIntegrationEnabled(opts: SetEnabledOptions): Promise<void> {\n\tconst { request, org, pipelineId, enabled } = opts;\n\tconst detail = await request(`/v2/${org}/pipelines/${pipelineId}`);\n\tconst pipeline = detail?.config ?? detail;\n\tawait request(`/v2/${org}/pipelines/${pipelineId}`, {\n\t\tmethod: 'PATCH',\n\t\tbody: JSON.stringify({\n\t\t\tname: pipeline.name,\n\t\t\tdescription: pipeline.description ?? '',\n\t\t\tenabled,\n\t\t\tnodes: (pipeline.nodes ?? []).map((n: any) => ({\n\t\t\t\tid: n.id,\n\t\t\t\tslug: n.slug,\n\t\t\t\tcomponent_id: n.component_id,\n\t\t\t\tcomponent_type: n.component_type,\n\t\t\t\tenabled: n.enabled ?? true\n\t\t\t})),\n\t\t\tedges: (pipeline.edges ?? []).map((e: any) => ({\n\t\t\t\tname: e.name,\n\t\t\t\tdescription: e.description ?? '',\n\t\t\t\tfrom_node_instance_id: e.from_node_instance_id,\n\t\t\t\tto_node_instance_id: e.to_node_instance_id,\n\t\t\t\tdisabled: e.disabled ?? false,\n\t\t\t\tconditions: e.conditions\n\t\t\t}))\n\t\t})\n\t});\n}\n\n/** Convenience: disable the pipeline (stop without deleting). */\nexport function disableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void> {\n\treturn setIntegrationEnabled({ ...opts, enabled: false });\n}\n\n/** Convenience: re-enable a disabled pipeline. */\nexport function enableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void> {\n\treturn setIntegrationEnabled({ ...opts, enabled: true });\n}\n\n/* ===== delete ===== */\n\n/**\n * Which resources a delete removes. An embed integration is a pipeline\n * + an input + (in the dev/null flow) an output — but hosts provision\n * and own tenant resources differently. Some keep a shared output,\n * some keep the input so it can be re-wired later, some only ever tear\n * down the pipeline. Choose the policy that matches how you provision;\n * a flag left undefined defaults to `true` (delete it).\n *\n * Three ready-made policies are exported — CLEANUP_FULL,\n * CLEANUP_KEEP_OUTPUT, CLEANUP_PIPELINE_ONLY — or pass your own.\n */\nexport interface CleanupPolicy {\n\t/** Delete the pipeline. Default: true. */\n\tpipeline?: boolean;\n\t/** Delete the input connector. Default: true. */\n\tinput?: boolean;\n\t/** Delete the output connector. Default: true. */\n\toutput?: boolean;\n}\n\n/** Tear down everything — pipeline, input, and output. */\nexport const CLEANUP_FULL: CleanupPolicy = { pipeline: true, input: true, output: true };\n\n/** Remove the pipeline and input but keep the output (e.g. a shared sink). */\nexport const CLEANUP_KEEP_OUTPUT: CleanupPolicy = {\n\tpipeline: true,\n\tinput: true,\n\toutput: false\n};\n\n/** Remove only the pipeline; keep the input so it can be re-wired. */\nexport const CLEANUP_PIPELINE_ONLY: CleanupPolicy = {\n\tpipeline: true,\n\tinput: false,\n\toutput: false\n};\n\nexport interface DeleteIntegrationOptions {\n\trequest: MonadRequest;\n\torg: string;\n\t/** Pipeline to remove. Omit when the input has no pipeline. */\n\tpipelineId?: string;\n\t/** Input to remove. Omit to leave it. */\n\tinputId?: string;\n\t/** Output to remove (the auto-created dev/null sink). Omit to leave it. */\n\toutputId?: string;\n\t/**\n\t * What to actually delete. Defaults to everything supplied\n\t * (CLEANUP_FULL). Set a flag false to keep that resource even when\n\t * its id is provided.\n\t */\n\tcleanup?: CleanupPolicy;\n}\n\n/**\n * Delete an integration per a cleanup policy. Order matters: the\n * pipeline references the input and output, so it goes first. Only\n * resources whose id is supplied AND whose policy flag is true are\n * removed — so nothing the host wants to keep is touched, and nothing\n * it wants gone is left orphaned.\n */\nexport async function deleteIntegration(opts: DeleteIntegrationOptions): Promise<void> {\n\tconst { request, org, pipelineId, inputId, outputId } = opts;\n\tconst policy: Required<CleanupPolicy> = {\n\t\tpipeline: true,\n\t\tinput: true,\n\t\toutput: true,\n\t\t...opts.cleanup\n\t};\n\tif (pipelineId && policy.pipeline) {\n\t\tawait request(`/v2/${org}/pipelines/${pipelineId}`, { method: 'DELETE' });\n\t}\n\tif (inputId && policy.input) {\n\t\tawait request(`/v1/${org}/inputs/${inputId}`, { method: 'DELETE' });\n\t}\n\tif (outputId && policy.output) {\n\t\tawait request(`/v1/${org}/outputs/${outputId}`, { method: 'DELETE' });\n\t}\n}\n\n/* ===== build ===== */\n\nexport interface BuildDevNullPipelineOptions {\n\trequest: MonadRequest;\n\torg: string;\n\tinputId: string;\n\t/** Used to name the auto-created output and pipeline. */\n\tinputName: string;\n\t/** Status polls before giving up (default 15, ~2s apart). */\n\tpollAttempts?: number;\n}\n\nexport interface BuiltPipeline {\n\tpipelineId: string;\n\toutputId: string;\n\tstatus: string;\n\t/** True once the pipeline reports Running. */\n\tactive: boolean;\n}\n\n/**\n * Stand up a dev/null pipeline for a freshly-configured input: create a\n * dev/null output, create an enabled pipeline wiring input → output,\n * then poll until it reports Running. The POC's \"what happens after an\n * input is configured\" path, packaged as a reusable harness call.\n */\nexport async function buildDevNullPipeline(\n\topts: BuildDevNullPipelineOptions\n): Promise<BuiltPipeline> {\n\tconst { request, org, inputId, inputName, pollAttempts = 15 } = opts;\n\tconst label = inputName || 'Input';\n\n\tconst output = await request(`/v2/${org}/outputs`, {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({\n\t\t\toutput_type: 'dev-null',\n\t\t\tname: `${label} → /dev/null`,\n\t\t\tdescription: 'Auto-created sink for embed pipeline',\n\t\t\tpromise_id: '',\n\t\t\tconfig: { settings: {}, secrets: {} }\n\t\t})\n\t});\n\n\tconst pipeline = await request(`/v2/${org}/pipelines/`, {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({\n\t\t\tname: `${label} pipeline`,\n\t\t\tdescription: 'Auto-created when the input was configured via embed',\n\t\t\tenabled: true,\n\t\t\tnodes: [\n\t\t\t\t{ slug: 'in', component_id: inputId, component_type: 'input', enabled: true },\n\t\t\t\t{ slug: 'out', component_id: output.id, component_type: 'output', enabled: true }\n\t\t\t],\n\t\t\tedges: [\n\t\t\t\t{\n\t\t\t\t\tfrom_node_instance_id: 'in',\n\t\t\t\t\tto_node_instance_id: 'out',\n\t\t\t\t\tdescription: 'all records',\n\t\t\t\t\tconditions: { operator: 'always' }\n\t\t\t\t}\n\t\t\t]\n\t\t})\n\t});\n\n\tlet status = 'Pending';\n\tfor (let attempt = 0; attempt < pollAttempts; attempt++) {\n\t\tconst s = await request(`/v2/${org}/pipelines/${pipeline.id}/status`);\n\t\tstatus = s?.status ?? 'Unknown';\n\t\tif (status === 'Running' || status === 'Erroring') break;\n\t\tawait new Promise((r) => setTimeout(r, 2000));\n\t}\n\n\treturn {\n\t\tpipelineId: pipeline.id,\n\t\toutputId: output.id,\n\t\tstatus,\n\t\tactive: status === 'Running'\n\t};\n}\n"],"mappings":"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","names":[]}
@@ -40,12 +40,11 @@ interface Appearance {
40
40
  /**
41
41
  * Copy overrides for a single field of a connector's config form.
42
42
  *
43
- * Only copy can be overridden. A field's type, requiredness, allowed
44
- * values, and default are fixed by the connector and cannot be changed
45
- * here.
43
+ * Only copy and a scalar `default` can be overridden. A field's type,
44
+ * requiredness, and allowed values are fixed by the connector.
46
45
  *
47
- * `placeholder` is a hint, not a default: a field the user never touches
48
- * still submits an empty value.
46
+ * `placeholder` is a hint: a field the user never touches still submits
47
+ * an empty value. Use `default` to prefill a real value.
49
48
  *
50
49
  * Keys mirror the config shape returned for the connector type, so they
51
50
  * are snake_case — unlike the camelCase options on
@@ -60,6 +59,12 @@ interface FieldMetaOverride {
60
59
  description?: string;
61
60
  /** Ephemeral hint text inside the control. Empty string blanks it. */
62
61
  placeholder?: string;
62
+ /**
63
+ * Prefilled value for a new connector. Stays editable and is submitted.
64
+ * Ignored when editing, so the saved value wins. Applies to string
65
+ * (within its allowed values), number and boolean fields only.
66
+ */
67
+ default?: string | number | boolean;
63
68
  /** Overrides for a nested object's child fields, keyed by child wire key. */
64
69
  children?: Record<string, FieldMetaOverride>;
65
70
  /** Overrides for a discriminated-union (one_of) field. */
@@ -40,12 +40,11 @@ interface Appearance {
40
40
  /**
41
41
  * Copy overrides for a single field of a connector's config form.
42
42
  *
43
- * Only copy can be overridden. A field's type, requiredness, allowed
44
- * values, and default are fixed by the connector and cannot be changed
45
- * here.
43
+ * Only copy and a scalar `default` can be overridden. A field's type,
44
+ * requiredness, and allowed values are fixed by the connector.
46
45
  *
47
- * `placeholder` is a hint, not a default: a field the user never touches
48
- * still submits an empty value.
46
+ * `placeholder` is a hint: a field the user never touches still submits
47
+ * an empty value. Use `default` to prefill a real value.
49
48
  *
50
49
  * Keys mirror the config shape returned for the connector type, so they
51
50
  * are snake_case — unlike the camelCase options on
@@ -60,6 +59,12 @@ interface FieldMetaOverride {
60
59
  description?: string;
61
60
  /** Ephemeral hint text inside the control. Empty string blanks it. */
62
61
  placeholder?: string;
62
+ /**
63
+ * Prefilled value for a new connector. Stays editable and is submitted.
64
+ * Ignored when editing, so the saved value wins. Applies to string
65
+ * (within its allowed values), number and boolean fields only.
66
+ */
67
+ default?: string | number | boolean;
63
68
  /** Overrides for a nested object's child fields, keyed by child wire key. */
64
69
  children?: Record<string, FieldMetaOverride>;
65
70
  /** Overrides for a discriminated-union (one_of) field. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/connect/protocol.ts","../../src/connect/lifecycle.ts","../../src/connect/index.ts"],"sourcesContent":["/**\n * postMessage contract between the host page (@monad-inc/embed/connect) and\n * the Monad-hosted iframe (@monad-inc/embed-frame).\n *\n * Why a protocol module: both sides must agree on message shapes, and a\n * drifting contract is a silent failure (a message just gets ignored).\n * @monad-inc/embed/connect owns it because it's the public-facing SDK; the\n * iframe app imports these types from here.\n *\n * Security model:\n * - Origin is the real boundary. Every listener validates\n * `event.origin` against an expected value before trusting a\n * message. The iframe knows its parent's origin (passed as a URL\n * param); the host knows the frame's origin (it chose it).\n * - `event.source` is also checked host-side so a sibling iframe on\n * the same origin can't impersonate ours.\n * - The `source: 'monad-embed'` tag is NOT security — it's just a\n * cheap filter so unrelated postMessage traffic (analytics SDKs,\n * devtools, wallet extensions) is ignored without parsing.\n * - The session token travels in the `init` message body, never in\n * the iframe URL, so it stays out of browser history and Referer.\n */\n\nexport const PROTOCOL_SOURCE = 'monad-embed' as const;\nexport const PROTOCOL_VERSION = 1 as const;\n\nexport type ComponentKind = 'input' | 'output';\n\n/**\n * Appearance tokens the host may pass to theme the iframe. The iframe\n * applies each as a `--monad-{token}` CSS variable. The host cannot\n * style across the iframe boundary directly (same-origin policy), so\n * this is the only theming channel — same idea as Stripe's `appearance`.\n */\nexport interface Appearance {\n\tcolorPrimary?: string;\n\tcolorText?: string;\n\tcolorBackground?: string;\n\tcolorBorder?: string;\n\tfontFamily?: string;\n\tborderRadius?: string;\n}\n\n/**\n * Copy overrides for a single field of a connector's config form.\n *\n * Only copy can be overridden. A field's type, requiredness, allowed\n * values, and default are fixed by the connector and cannot be changed\n * here.\n *\n * `placeholder` is a hint, not a default: a field the user never touches\n * still submits an empty value.\n *\n * Keys mirror the config shape returned for the connector type, so they\n * are snake_case — unlike the camelCase options on\n * `ConnectorFrameOptions`. Field keys are the connector's own keys; the\n * Datadog output's source field is `ddsource`, not `source`. Keys that\n * don't match the connector are ignored.\n */\nexport interface FieldMetaOverride {\n\t/** Field label. An empty string is ignored — unlabelled fields are a bug. */\n\tname?: string;\n\t/** Helper text shown with the control. Empty string blanks it. */\n\tdescription?: string;\n\t/** Ephemeral hint text inside the control. Empty string blanks it. */\n\tplaceholder?: string;\n\t/** Overrides for a nested object's child fields, keyed by child wire key. */\n\tchildren?: Record<string, FieldMetaOverride>;\n\t/** Overrides for a discriminated-union (one_of) field. */\n\tdiscriminator?: DiscriminatorMetaOverride;\n}\n\n/**\n * Copy overrides for a one_of field's variant selector. The set of\n * variants, their order, and which one is selected by default are fixed\n * by the connector.\n */\nexport interface DiscriminatorMetaOverride {\n\t/** Label on the variant selector (defaults to \"Type\"). */\n\tname?: string;\n\t/** Helper text for the variant selector. */\n\tdescription?: string;\n\t/** Per-variant overrides, keyed by discriminator value. */\n\tone_of?: Record<string, FieldMetaOverride>;\n}\n\n/**\n * Config-form copy overrides for one connector, mirroring the two\n * sections returned for the connector type. Overrides never add fields —\n * keys that don't match the connector are ignored.\n */\nexport interface ConfigMetaOverrides {\n\tsettings?: Record<string, FieldMetaOverride>;\n\tsecrets?: Record<string, FieldMetaOverride>;\n}\n\n/* ===== iframe -> host ===== */\n\nexport interface ReadyMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'ready';\n\tversion: number;\n}\n\nexport interface ResizeMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'resize';\n\t/** Content height in CSS pixels — the host sizes the <iframe> to this. */\n\theight: number;\n}\n\nexport interface SavedMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'saved';\n\t/** Safe reference to the created/updated connector — no secret material. */\n\tconnector: { id: string; name?: string };\n}\n\nexport interface ErrorMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'error';\n\tmessage: string;\n}\n\nexport interface CancelMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'cancel';\n}\n\nexport type FrameMessage =\n\t| ReadyMessage\n\t| ResizeMessage\n\t| SavedMessage\n\t| ErrorMessage\n\t| CancelMessage;\n\n/* ===== host -> iframe ===== */\n\nexport interface InitMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'init';\n\tversion: number;\n\t/** Short-lived embed session token minted by the host's backend. */\n\tsessionToken: string;\n\t/** Monad API base, e.g. \"https://api.monad.com\" or dev \"http://localhost/api\". */\n\tapiBase: string;\n\t/**\n\t * Sub-org id the session token is scoped to. The iframe uses this to\n\t * build `/v1/<org>/*` and `/v2/<org>/*` URLs. Must match the token's\n\t * `org` claim — server-side `org_access.Middleware` will 403 if not.\n\t */\n\torganizationId: string;\n\t/** Connector type id, e.g. \"runzero-findings\". */\n\ttypeId: string;\n\tkind: ComponentKind;\n\t/** Display name shown in the frame header. */\n\tdisplayName?: string;\n\t/**\n\t * Name for the created connector. On create, defaults to the connector\n\t * type's name plus a short unique suffix. On edit, omit to keep the\n\t * existing name.\n\t */\n\tname?: string;\n\t/**\n\t * Description for the created connector. On create, defaults to the\n\t * connector type's description. On edit, omit to keep the existing\n\t * description.\n\t */\n\tdescription?: string;\n\t/**\n\t * Show an editable name input inside the iframe, prefilled with the\n\t * resolved name, so the end user can name the connector themselves.\n\t * A non-empty name is required when shown. Defaults to false.\n\t */\n\tisNameEditable?: boolean;\n\t/**\n\t * Show an editable description input inside the iframe, prefilled with\n\t * the resolved description. Defaults to false.\n\t */\n\tisDescriptionEditable?: boolean;\n\t/** Existing connector id — present for edit mode, absent for create. */\n\texistingId?: string;\n\t/**\n\t * Whether the synthetic-data toggle is exposed in the form. Defaults\n\t * to `false` (toggle hidden). Production embed customers keep this\n\t * off — synthetic mode is a Monad-internal testing affordance.\n\t */\n\tsynthetic?: boolean;\n\t/** Theming tokens applied as CSS variables inside the iframe. */\n\tappearance?: Appearance;\n\t/**\n\t * Stylesheet URLs the host wants to apply to the iframe page. The\n\t * iframe loads these as `<link rel=\"stylesheet\">` tags before\n\t * rendering the form.\n\t *\n\t * All-or-nothing: if this array is present and non-empty, the\n\t * iframe loads ONLY these stylesheets and does NOT load its\n\t * built-in Monad UI defaults. If it's absent or empty, the iframe\n\t * loads the built-in defaults and nothing else. Mixing host CSS\n\t * with the defaults is intentionally not supported — partial\n\t * styling tends to produce confusing half-themed forms.\n\t *\n\t * URLs MUST be reachable from the iframe origin (typically by\n\t * being served from the host page's own origin, which the browser\n\t * permits for stylesheet `<link>` loads even cross-origin).\n\t */\n\tstylesheets?: string[];\n\t/**\n\t * Per-field copy overrides merged onto the connector's config form.\n\t * Copy only — see FieldMetaOverride.\n\t */\n\tconfigMetaOverrides?: ConfigMetaOverrides;\n}\n\nexport type HostMessage = InitMessage;\n","/**\n * Connector-lifecycle helpers — the \"wiring harness\" a host (Okta, in\n * the demo's fiction) drops into its own backend to power Disable /\n * Enable / Delete buttons in its own UI.\n *\n * Unlike createConnectorFrame (browser-only), these are isomorphic:\n * they just sequence Monad's standard `/v2/{org}/*` + `/v1/{org}/*`\n * API calls. They run wherever the host holds its API key — almost\n * always its backend.\n *\n * The host supplies a `MonadRequest` thunk that performs an\n * authenticated request and returns parsed JSON. The harness owns the\n * non-obvious parts: which endpoint + verb each action maps to, the\n * delete ordering (pipeline before the input/output it references),\n * and resolving an input id to its pipeline without the host having to\n * store that mapping.\n */\n\n/**\n * Authenticated request against the Monad API, returning parsed JSON\n * (or undefined for empty responses). The host implements this with\n * its long-lived API key and base URL. Path is the full API path,\n * e.g. `/v2/{org}/pipelines/{id}`.\n */\nexport type MonadRequest = (path: string, init?: RequestInit) => Promise<any>;\n\n/* ===== resolve ===== */\n\n/**\n * Where an integration's resources live and their current state.\n * Returned by findIntegrationPipeline so the host can act on an\n * integration knowing only the input id.\n */\nexport interface ResolvedIntegration {\n\torg: string;\n\tpipelineId: string;\n\t/** Whether the pipeline is currently enabled. */\n\tenabled: boolean;\n\tinputId: string;\n\t/** Output id of the pipeline's destination node, if it has one. */\n\toutputId?: string;\n}\n\ninterface PipelineNode {\n\tcomponent_id?: string;\n\tcomponent_type?: string;\n}\n\n/**\n * Find the pipeline an input feeds. Lets the host act on an integration\n * (disable/enable/delete) knowing only the input id — no need to\n * persist an input→pipeline map of its own.\n *\n * The pipelines list is slim (no node wiring), so this fetches each\n * pipeline's detail and scans for the input node. Fine for the modest\n * pipeline counts a single tenant sub-org has; a host managing many\n * pipelines per sub-org should instead persist the `pipelineId`\n * buildDevNullPipeline returns and skip the lookup.\n *\n * Returns null when the input has no pipeline.\n */\nexport async function findIntegrationPipeline(opts: {\n\trequest: MonadRequest;\n\torg: string;\n\tinputId: string;\n}): Promise<ResolvedIntegration | null> {\n\tconst { request, org, inputId } = opts;\n\tconst listed = await request(`/v2/${org}/pipelines/`);\n\tconst pipelines: any[] = Array.isArray(listed)\n\t\t? listed\n\t\t: (listed?.pipelines ?? listed?.data ?? []);\n\n\tfor (const summary of pipelines) {\n\t\tif (!summary?.id) continue;\n\t\tlet detail: any;\n\t\ttry {\n\t\t\tdetail = await request(`/v2/${org}/pipelines/${summary.id}`);\n\t\t} catch {\n\t\t\tcontinue; // skip a pipeline we can't read rather than failing\n\t\t}\n\t\tconst pipeline = detail?.config ?? detail;\n\t\tconst nodes: PipelineNode[] = pipeline?.nodes ?? [];\n\t\tconst inputNode = nodes.find((n) => n.component_type === 'input' && n.component_id === inputId);\n\t\tif (!inputNode) continue;\n\t\tconst outputNode = nodes.find((n) => n.component_type === 'output');\n\t\treturn {\n\t\t\torg,\n\t\t\tpipelineId: summary.id,\n\t\t\tenabled: Boolean(pipeline.enabled),\n\t\t\tinputId,\n\t\t\toutputId: outputNode?.component_id\n\t\t};\n\t}\n\treturn null;\n}\n\n/* ===== enable / disable ===== */\n\nexport interface SetEnabledOptions {\n\trequest: MonadRequest;\n\torg: string;\n\tpipelineId: string;\n\t/** true → resume processing, false → stop (config is kept either way). */\n\tenabled: boolean;\n}\n\n/**\n * Enable or disable an integration's pipeline. Disabling stops data\n * flowing but destroys nothing — the input, output, and pipeline\n * config all survive, so a later enable resumes it as it was.\n *\n * The pipeline PATCH endpoint replaces the whole config rather than\n * accepting a partial, so this reads the current pipeline and sends it\n * back with only the `enabled` flag flipped.\n *\n * Race warning: this is GET-then-PATCH on the same resource, so a\n * concurrent edit between the two requests gets clobbered. Fine for\n * human-driven toggles. If you call this from automation, fan-out, or\n * a retry loop, ask the API team for a server-side `{ enabled }`\n * partial PATCH instead — the round-trip we do here is only a\n * workaround for the current full-config replace semantics.\n */\nexport async function setIntegrationEnabled(opts: SetEnabledOptions): Promise<void> {\n\tconst { request, org, pipelineId, enabled } = opts;\n\tconst detail = await request(`/v2/${org}/pipelines/${pipelineId}`);\n\tconst pipeline = detail?.config ?? detail;\n\tawait request(`/v2/${org}/pipelines/${pipelineId}`, {\n\t\tmethod: 'PATCH',\n\t\tbody: JSON.stringify({\n\t\t\tname: pipeline.name,\n\t\t\tdescription: pipeline.description ?? '',\n\t\t\tenabled,\n\t\t\tnodes: (pipeline.nodes ?? []).map((n: any) => ({\n\t\t\t\tid: n.id,\n\t\t\t\tslug: n.slug,\n\t\t\t\tcomponent_id: n.component_id,\n\t\t\t\tcomponent_type: n.component_type,\n\t\t\t\tenabled: n.enabled ?? true\n\t\t\t})),\n\t\t\tedges: (pipeline.edges ?? []).map((e: any) => ({\n\t\t\t\tname: e.name,\n\t\t\t\tdescription: e.description ?? '',\n\t\t\t\tfrom_node_instance_id: e.from_node_instance_id,\n\t\t\t\tto_node_instance_id: e.to_node_instance_id,\n\t\t\t\tdisabled: e.disabled ?? false,\n\t\t\t\tconditions: e.conditions\n\t\t\t}))\n\t\t})\n\t});\n}\n\n/** Convenience: disable the pipeline (stop without deleting). */\nexport function disableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void> {\n\treturn setIntegrationEnabled({ ...opts, enabled: false });\n}\n\n/** Convenience: re-enable a disabled pipeline. */\nexport function enableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void> {\n\treturn setIntegrationEnabled({ ...opts, enabled: true });\n}\n\n/* ===== delete ===== */\n\n/**\n * Which resources a delete removes. An embed integration is a pipeline\n * + an input + (in the dev/null flow) an output — but hosts provision\n * and own tenant resources differently. Some keep a shared output,\n * some keep the input so it can be re-wired later, some only ever tear\n * down the pipeline. Choose the policy that matches how you provision;\n * a flag left undefined defaults to `true` (delete it).\n *\n * Three ready-made policies are exported — CLEANUP_FULL,\n * CLEANUP_KEEP_OUTPUT, CLEANUP_PIPELINE_ONLY — or pass your own.\n */\nexport interface CleanupPolicy {\n\t/** Delete the pipeline. Default: true. */\n\tpipeline?: boolean;\n\t/** Delete the input connector. Default: true. */\n\tinput?: boolean;\n\t/** Delete the output connector. Default: true. */\n\toutput?: boolean;\n}\n\n/** Tear down everything — pipeline, input, and output. */\nexport const CLEANUP_FULL: CleanupPolicy = { pipeline: true, input: true, output: true };\n\n/** Remove the pipeline and input but keep the output (e.g. a shared sink). */\nexport const CLEANUP_KEEP_OUTPUT: CleanupPolicy = {\n\tpipeline: true,\n\tinput: true,\n\toutput: false\n};\n\n/** Remove only the pipeline; keep the input so it can be re-wired. */\nexport const CLEANUP_PIPELINE_ONLY: CleanupPolicy = {\n\tpipeline: true,\n\tinput: false,\n\toutput: false\n};\n\nexport interface DeleteIntegrationOptions {\n\trequest: MonadRequest;\n\torg: string;\n\t/** Pipeline to remove. Omit when the input has no pipeline. */\n\tpipelineId?: string;\n\t/** Input to remove. Omit to leave it. */\n\tinputId?: string;\n\t/** Output to remove (the auto-created dev/null sink). Omit to leave it. */\n\toutputId?: string;\n\t/**\n\t * What to actually delete. Defaults to everything supplied\n\t * (CLEANUP_FULL). Set a flag false to keep that resource even when\n\t * its id is provided.\n\t */\n\tcleanup?: CleanupPolicy;\n}\n\n/**\n * Delete an integration per a cleanup policy. Order matters: the\n * pipeline references the input and output, so it goes first. Only\n * resources whose id is supplied AND whose policy flag is true are\n * removed — so nothing the host wants to keep is touched, and nothing\n * it wants gone is left orphaned.\n */\nexport async function deleteIntegration(opts: DeleteIntegrationOptions): Promise<void> {\n\tconst { request, org, pipelineId, inputId, outputId } = opts;\n\tconst policy: Required<CleanupPolicy> = {\n\t\tpipeline: true,\n\t\tinput: true,\n\t\toutput: true,\n\t\t...opts.cleanup\n\t};\n\tif (pipelineId && policy.pipeline) {\n\t\tawait request(`/v2/${org}/pipelines/${pipelineId}`, { method: 'DELETE' });\n\t}\n\tif (inputId && policy.input) {\n\t\tawait request(`/v1/${org}/inputs/${inputId}`, { method: 'DELETE' });\n\t}\n\tif (outputId && policy.output) {\n\t\tawait request(`/v1/${org}/outputs/${outputId}`, { method: 'DELETE' });\n\t}\n}\n\n/* ===== build ===== */\n\nexport interface BuildDevNullPipelineOptions {\n\trequest: MonadRequest;\n\torg: string;\n\tinputId: string;\n\t/** Used to name the auto-created output and pipeline. */\n\tinputName: string;\n\t/** Status polls before giving up (default 15, ~2s apart). */\n\tpollAttempts?: number;\n}\n\nexport interface BuiltPipeline {\n\tpipelineId: string;\n\toutputId: string;\n\tstatus: string;\n\t/** True once the pipeline reports Running. */\n\tactive: boolean;\n}\n\n/**\n * Stand up a dev/null pipeline for a freshly-configured input: create a\n * dev/null output, create an enabled pipeline wiring input → output,\n * then poll until it reports Running. The POC's \"what happens after an\n * input is configured\" path, packaged as a reusable harness call.\n */\nexport async function buildDevNullPipeline(\n\topts: BuildDevNullPipelineOptions\n): Promise<BuiltPipeline> {\n\tconst { request, org, inputId, inputName, pollAttempts = 15 } = opts;\n\tconst label = inputName || 'Input';\n\n\tconst output = await request(`/v2/${org}/outputs`, {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({\n\t\t\toutput_type: 'dev-null',\n\t\t\tname: `${label} → /dev/null`,\n\t\t\tdescription: 'Auto-created sink for embed pipeline',\n\t\t\tpromise_id: '',\n\t\t\tconfig: { settings: {}, secrets: {} }\n\t\t})\n\t});\n\n\tconst pipeline = await request(`/v2/${org}/pipelines/`, {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({\n\t\t\tname: `${label} pipeline`,\n\t\t\tdescription: 'Auto-created when the input was configured via embed',\n\t\t\tenabled: true,\n\t\t\tnodes: [\n\t\t\t\t{ slug: 'in', component_id: inputId, component_type: 'input', enabled: true },\n\t\t\t\t{ slug: 'out', component_id: output.id, component_type: 'output', enabled: true }\n\t\t\t],\n\t\t\tedges: [\n\t\t\t\t{\n\t\t\t\t\tfrom_node_instance_id: 'in',\n\t\t\t\t\tto_node_instance_id: 'out',\n\t\t\t\t\tdescription: 'all records',\n\t\t\t\t\tconditions: { operator: 'always' }\n\t\t\t\t}\n\t\t\t]\n\t\t})\n\t});\n\n\tlet status = 'Pending';\n\tfor (let attempt = 0; attempt < pollAttempts; attempt++) {\n\t\tconst s = await request(`/v2/${org}/pipelines/${pipeline.id}/status`);\n\t\tstatus = s?.status ?? 'Unknown';\n\t\tif (status === 'Running' || status === 'Erroring') break;\n\t\tawait new Promise((r) => setTimeout(r, 2000));\n\t}\n\n\treturn {\n\t\tpipelineId: pipeline.id,\n\t\toutputId: output.id,\n\t\tstatus,\n\t\tactive: status === 'Running'\n\t};\n}\n","/**\n * @monad-inc/embed/connect — host-side SDK for embedding Monad's\n * connector-config UI as a secure iframe.\n *\n * The customer's page calls `createConnectorFrame()`; this injects an\n * <iframe> pointing at a Monad-hosted page (@monad-inc/embed-frame). The\n * connector form — and every secret the user types into it — runs\n * inside that iframe on Monad's origin. Same-origin policy means the\n * customer's own JavaScript can never read those secrets. The customer\n * gets back only a connector id (a safe reference), the same way a\n * Stripe integration only ever sees a `pm_…` token.\n *\n * The host is still responsible for one thing: minting a short-lived\n * embed session token from its backend (POST /v1/embed/sessions, with\n * the long-lived API key) and handing it to `createConnectorFrame`.\n * That token travels into the iframe over postMessage, never the URL.\n */\nimport {\n\tPROTOCOL_SOURCE,\n\tPROTOCOL_VERSION,\n\ttype Appearance,\n\ttype ComponentKind,\n\ttype ConfigMetaOverrides,\n\ttype FrameMessage,\n\ttype HostMessage\n} from './protocol';\n\nexport {\n\tPROTOCOL_SOURCE,\n\tPROTOCOL_VERSION,\n\ttype Appearance,\n\ttype ComponentKind,\n\ttype ConfigMetaOverrides,\n\ttype DiscriminatorMetaOverride,\n\ttype FieldMetaOverride,\n\ttype FrameMessage,\n\ttype HostMessage,\n\ttype InitMessage\n} from './protocol';\n\nexport {\n\tfindIntegrationPipeline,\n\tsetIntegrationEnabled,\n\tdisableIntegration,\n\tenableIntegration,\n\tdeleteIntegration,\n\tbuildDevNullPipeline,\n\tCLEANUP_FULL,\n\tCLEANUP_KEEP_OUTPUT,\n\tCLEANUP_PIPELINE_ONLY,\n\ttype MonadRequest,\n\ttype ResolvedIntegration,\n\ttype SetEnabledOptions,\n\ttype DeleteIntegrationOptions,\n\ttype CleanupPolicy,\n\ttype BuildDevNullPipelineOptions,\n\ttype BuiltPipeline\n} from './lifecycle';\n\nexport interface ConnectorFrameOptions {\n\t/** DOM node (or selector) the iframe is appended to. */\n\tcontainer: HTMLElement | string;\n\t/**\n\t * URL of the Monad embed-frame page. Defaults to\n\t * \"https://app.monad.com/embed\" (production). Override for staging\n\t * (\"https://app.monad.security/embed\") or local testing\n\t * (\"http://localhost:5173/embed\"). MUST be a different origin than\n\t * the host page — that cross-origin boundary is what isolates the\n\t * secrets. A path component is allowed (so the embed page can live\n\t * at a sub-route of the main app); the postMessage security checks\n\t * compare against the URL's origin, not its full href.\n\t */\n\tframeOrigin?: string;\n\t/**\n\t * Monad API base the iframe should call. Defaults to\n\t * \"https://app.monad.com/api\" (production). Override for non-prod\n\t * environments. Set this together with `frameOrigin` — mixing\n\t * environments will not work.\n\t */\n\tapiBase?: string;\n\t/** Connector type id, e.g. \"runzero-findings\". */\n\ttypeId: string;\n\tkind: ComponentKind;\n\t/** Short-lived embed session token (host backend mints it). */\n\tsessionToken: string;\n\t/**\n\t * Sub-org id the session is scoped to. The host backend already knows\n\t * this — it's the same value passed to `/v1/embed/sessions` when minting\n\t * the token. Forwarded into the iframe so it can build `/v1/<org>/*` and\n\t * `/v2/<org>/*` URLs.\n\t */\n\torganizationId: string;\n\t/** Display name shown in the frame header. */\n\tdisplayName?: string;\n\t/**\n\t * Name for the created connector. On create, defaults to the connector\n\t * type's name plus a short unique suffix. On edit, omit to keep the\n\t * existing name.\n\t */\n\tname?: string;\n\t/**\n\t * Description for the created connector. On create, defaults to the\n\t * connector type's description. On edit, omit to keep the existing\n\t * description.\n\t */\n\tdescription?: string;\n\t/**\n\t * Show an editable name input inside the iframe (prefilled with the\n\t * resolved name) so the end user can name the connector themselves.\n\t * A non-empty name is required when shown. Defaults to false.\n\t */\n\tisNameEditable?: boolean;\n\t/**\n\t * Show an editable description input inside the iframe (prefilled with\n\t * the resolved description). Defaults to false.\n\t */\n\tisDescriptionEditable?: boolean;\n\t/** Existing connector id — pass for edit mode, omit to create. */\n\texistingId?: string;\n\t/**\n\t * Whether the synthetic-data toggle is exposed in the form. Defaults\n\t * to `false` — production embed flows keep it hidden.\n\t */\n\tsynthetic?: boolean;\n\t/** Theming tokens applied as CSS variables inside the iframe. */\n\tappearance?: Appearance;\n\t/**\n\t * Stylesheet URLs to apply inside the iframe. All-or-nothing: if\n\t * any URL is provided, the iframe loads ONLY these and skips its\n\t * built-in Monad UI defaults; if omitted/empty, the iframe loads\n\t * the defaults and nothing else. URLs must be reachable from the\n\t * iframe — typically served by the host page's own origin.\n\t */\n\tstylesheets?: string[];\n\t/**\n\t * Per-field copy overrides for the connector's config form: label,\n\t * description, placeholder. Copy only — nothing that changes a\n\t * field's type, requiredness, or submitted value.\n\t *\n\t * configMetaOverrides: {\n\t * settings: {\n\t * ddsource: { placeholder: 'northstar' },\n\t * service: { placeholder: 'northstar-web' },\n\t * },\n\t * }\n\t *\n\t * Keys are the connector's own wire keys, so check the connector-type\n\t * response rather than guessing — the Datadog output's source field is\n\t * `ddsource`, not `source`. Unmatched keys are ignored.\n\t *\n\t * `placeholder` is a hint, not a default: a field the user never\n\t * touches still submits `\"\"`. Nested fields are reached with\n\t * `children`, union variants with `discriminator.one_of.<variant>`.\n\t */\n\tconfigMetaOverrides?: ConfigMetaOverrides;\n\t/** Fired when the user saves successfully. */\n\tonSave?: (connector: { id: string; name?: string }) => void;\n\t/** Fired when the user cancels. */\n\tonCancel?: () => void;\n\t/** Fired on a save/test/load failure inside the iframe. */\n\tonError?: (message: string) => void;\n}\n\nexport interface ConnectorFrame {\n\t/** Remove the iframe and stop listening for its messages. */\n\tdestroy(): void;\n}\n\n/**\n * Production defaults. Customers integrating against prod don't pass\n * `frameOrigin`/`apiBase` at all; non-prod environments override both\n * together. Treat these as a public API contract — changing a hostname\n * is a breaking change for anyone on the default.\n */\nconst DEFAULT_FRAME_ORIGIN = 'https://app.monad.com/embed';\nconst DEFAULT_API_BASE = 'https://app.monad.com/api';\n\n/**\n * Inject the Monad connector iframe into `container` and wire the\n * postMessage channel. Returns a handle whose `destroy()` tears it all\n * down.\n *\n * Handshake:\n * 1. iframe loads, posts `ready`;\n * 2. host replies with `init` (session token, type, appearance);\n * 3. iframe mounts the form and streams `resize` as content grows;\n * 4. on save, iframe posts `saved {connector}`; on cancel, `cancel`.\n */\nexport function createConnectorFrame(opts: ConnectorFrameOptions): ConnectorFrame {\n\tconst container =\n\t\ttypeof opts.container === 'string'\n\t\t\t? document.querySelector<HTMLElement>(opts.container)\n\t\t\t: opts.container;\n\tif (!container) {\n\t\tthrow new Error('[monad-inc/connect] container element not found');\n\t}\n\n\tconst frameInput = opts.frameOrigin ?? DEFAULT_FRAME_ORIGIN;\n\tconst apiBase = opts.apiBase ?? DEFAULT_API_BASE;\n\n\t// Split the input URL into two pieces:\n\t// * `frameUrl` keeps the path (e.g. /embed) — used as iframe.src.\n\t// * `frameOrigin` is the bare origin — used for the postMessage\n\t// security checks below, which must compare against the value\n\t// the browser surfaces on MessageEvent.origin (always a bare\n\t// origin, never a path).\n\t// Previously these were collapsed into one `new URL(...).href`,\n\t// which silently broke any frameOrigin with a path — the equality\n\t// check `event.origin !== frameOrigin` would always fail and `init`\n\t// never made it back to the iframe.\n\tconst frameUrl = new URL(frameInput);\n\tconst frameOrigin = frameUrl.origin;\n\tconst framePath = frameUrl.pathname.replace(/\\/$/, '');\n\n\tconst iframe = document.createElement('iframe');\n\tiframe.className = 'monad-connect-frame';\n\tiframe.title = 'Monad connector configuration';\n\tiframe.style.width = '100%';\n\tiframe.style.border = '0';\n\tiframe.style.display = 'block';\n\t// The iframe needs its own scripts and same-origin access to the\n\t// Monad API; it must NOT get same-origin access to the host. A\n\t// cross-origin iframe already can't reach the host — no sandbox\n\t// attribute is needed for isolation, and adding one would block the\n\t// form's own scripts. Forms/popups stay enabled for OAuth-style\n\t// connectors that may open a window.\n\tiframe.allow = 'clipboard-write';\n\t// parentOrigin lets the iframe scope its postMessage target + sender\n\t// check. It is NOT a secret and NOT a capability — lying about it\n\t// only breaks the liar's own embed (see protocol.ts).\n\tiframe.src = `${frameOrigin}${framePath}/?parentOrigin=${encodeURIComponent(window.location.origin)}`;\n\n\tlet destroyed = false;\n\n\tfunction postToFrame(msg: HostMessage): void {\n\t\tiframe.contentWindow?.postMessage(msg, frameOrigin);\n\t}\n\n\tfunction onMessage(event: MessageEvent): void {\n\t\tif (destroyed) return;\n\t\t// --- security boundary: validate before trusting anything ---\n\t\tif (event.origin !== frameOrigin) return;\n\t\tif (event.source !== iframe.contentWindow) return;\n\t\tconst msg = event.data as FrameMessage | undefined;\n\t\tif (!msg || msg.source !== PROTOCOL_SOURCE) return;\n\n\t\tswitch (msg.type) {\n\t\t\tcase 'ready':\n\t\t\t\tpostToFrame({\n\t\t\t\t\tsource: PROTOCOL_SOURCE,\n\t\t\t\t\ttype: 'init',\n\t\t\t\t\tversion: PROTOCOL_VERSION,\n\t\t\t\t\tsessionToken: opts.sessionToken,\n\t\t\t\t\tapiBase,\n\t\t\t\t\torganizationId: opts.organizationId,\n\t\t\t\t\ttypeId: opts.typeId,\n\t\t\t\t\tkind: opts.kind,\n\t\t\t\t\tdisplayName: opts.displayName,\n\t\t\t\t\tname: opts.name,\n\t\t\t\t\tdescription: opts.description,\n\t\t\t\t\tisNameEditable: opts.isNameEditable,\n\t\t\t\t\tisDescriptionEditable: opts.isDescriptionEditable,\n\t\t\t\t\texistingId: opts.existingId,\n\t\t\t\t\tsynthetic: opts.synthetic,\n\t\t\t\t\tappearance: opts.appearance,\n\t\t\t\t\tstylesheets: opts.stylesheets,\n\t\t\t\t\tconfigMetaOverrides: opts.configMetaOverrides\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase 'resize': {\n\t\t\t\t// Clamp to a sane range so a renderer bug in the iframe\n\t\t\t\t// can't blow up the host layout with a multi-million-px\n\t\t\t\t// height. The origin check above already gates who can\n\t\t\t\t// send this — clamping is defense in depth. Number.isFinite\n\t\t\t\t// guards against non-numeric heights that would otherwise\n\t\t\t\t// produce \"NaNpx\" and leave the iframe collapsed.\n\t\t\t\tconst h = Number.isFinite(msg.height) ? msg.height : 0;\n\t\t\t\tiframe.style.height = `${Math.min(Math.max(h, 0), 20000)}px`;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'saved':\n\t\t\t\topts.onSave?.(msg.connector);\n\t\t\t\tbreak;\n\t\t\tcase 'error':\n\t\t\t\topts.onError?.(msg.message);\n\t\t\t\tbreak;\n\t\t\tcase 'cancel':\n\t\t\t\topts.onCancel?.();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\twindow.addEventListener('message', onMessage);\n\tcontainer.appendChild(iframe);\n\n\treturn {\n\t\tdestroy(): void {\n\t\t\tdestroyed = true;\n\t\t\twindow.removeEventListener('message', onMessage);\n\t\t\tiframe.remove();\n\t\t}\n\t};\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/connect/protocol.ts","../../src/connect/lifecycle.ts","../../src/connect/index.ts"],"sourcesContent":["/**\n * postMessage contract between the host page (@monad-inc/embed/connect) and\n * the Monad-hosted iframe (@monad-inc/embed-frame).\n *\n * Why a protocol module: both sides must agree on message shapes, and a\n * drifting contract is a silent failure (a message just gets ignored).\n * @monad-inc/embed/connect owns it because it's the public-facing SDK; the\n * iframe app imports these types from here.\n *\n * Security model:\n * - Origin is the real boundary. Every listener validates\n * `event.origin` against an expected value before trusting a\n * message. The iframe knows its parent's origin (passed as a URL\n * param); the host knows the frame's origin (it chose it).\n * - `event.source` is also checked host-side so a sibling iframe on\n * the same origin can't impersonate ours.\n * - The `source: 'monad-embed'` tag is NOT security — it's just a\n * cheap filter so unrelated postMessage traffic (analytics SDKs,\n * devtools, wallet extensions) is ignored without parsing.\n * - The session token travels in the `init` message body, never in\n * the iframe URL, so it stays out of browser history and Referer.\n */\n\nexport const PROTOCOL_SOURCE = 'monad-embed' as const;\nexport const PROTOCOL_VERSION = 1 as const;\n\nexport type ComponentKind = 'input' | 'output';\n\n/**\n * Appearance tokens the host may pass to theme the iframe. The iframe\n * applies each as a `--monad-{token}` CSS variable. The host cannot\n * style across the iframe boundary directly (same-origin policy), so\n * this is the only theming channel — same idea as Stripe's `appearance`.\n */\nexport interface Appearance {\n\tcolorPrimary?: string;\n\tcolorText?: string;\n\tcolorBackground?: string;\n\tcolorBorder?: string;\n\tfontFamily?: string;\n\tborderRadius?: string;\n}\n\n/**\n * Copy overrides for a single field of a connector's config form.\n *\n * Only copy and a scalar `default` can be overridden. A field's type,\n * requiredness, and allowed values are fixed by the connector.\n *\n * `placeholder` is a hint: a field the user never touches still submits\n * an empty value. Use `default` to prefill a real value.\n *\n * Keys mirror the config shape returned for the connector type, so they\n * are snake_case — unlike the camelCase options on\n * `ConnectorFrameOptions`. Field keys are the connector's own keys; the\n * Datadog output's source field is `ddsource`, not `source`. Keys that\n * don't match the connector are ignored.\n */\nexport interface FieldMetaOverride {\n\t/** Field label. An empty string is ignored — unlabelled fields are a bug. */\n\tname?: string;\n\t/** Helper text shown with the control. Empty string blanks it. */\n\tdescription?: string;\n\t/** Ephemeral hint text inside the control. Empty string blanks it. */\n\tplaceholder?: string;\n\t/**\n\t * Prefilled value for a new connector. Stays editable and is submitted.\n\t * Ignored when editing, so the saved value wins. Applies to string\n\t * (within its allowed values), number and boolean fields only.\n\t */\n\tdefault?: string | number | boolean;\n\t/** Overrides for a nested object's child fields, keyed by child wire key. */\n\tchildren?: Record<string, FieldMetaOverride>;\n\t/** Overrides for a discriminated-union (one_of) field. */\n\tdiscriminator?: DiscriminatorMetaOverride;\n}\n\n/**\n * Copy overrides for a one_of field's variant selector. The set of\n * variants, their order, and which one is selected by default are fixed\n * by the connector.\n */\nexport interface DiscriminatorMetaOverride {\n\t/** Label on the variant selector (defaults to \"Type\"). */\n\tname?: string;\n\t/** Helper text for the variant selector. */\n\tdescription?: string;\n\t/** Per-variant overrides, keyed by discriminator value. */\n\tone_of?: Record<string, FieldMetaOverride>;\n}\n\n/**\n * Config-form copy overrides for one connector, mirroring the two\n * sections returned for the connector type. Overrides never add fields —\n * keys that don't match the connector are ignored.\n */\nexport interface ConfigMetaOverrides {\n\tsettings?: Record<string, FieldMetaOverride>;\n\tsecrets?: Record<string, FieldMetaOverride>;\n}\n\n/* ===== iframe -> host ===== */\n\nexport interface ReadyMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'ready';\n\tversion: number;\n}\n\nexport interface ResizeMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'resize';\n\t/** Content height in CSS pixels — the host sizes the <iframe> to this. */\n\theight: number;\n}\n\nexport interface SavedMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'saved';\n\t/** Safe reference to the created/updated connector — no secret material. */\n\tconnector: { id: string; name?: string };\n}\n\nexport interface ErrorMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'error';\n\tmessage: string;\n}\n\nexport interface CancelMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'cancel';\n}\n\nexport type FrameMessage =\n\t| ReadyMessage\n\t| ResizeMessage\n\t| SavedMessage\n\t| ErrorMessage\n\t| CancelMessage;\n\n/* ===== host -> iframe ===== */\n\nexport interface InitMessage {\n\tsource: typeof PROTOCOL_SOURCE;\n\ttype: 'init';\n\tversion: number;\n\t/** Short-lived embed session token minted by the host's backend. */\n\tsessionToken: string;\n\t/** Monad API base, e.g. \"https://api.monad.com\" or dev \"http://localhost/api\". */\n\tapiBase: string;\n\t/**\n\t * Sub-org id the session token is scoped to. The iframe uses this to\n\t * build `/v1/<org>/*` and `/v2/<org>/*` URLs. Must match the token's\n\t * `org` claim — server-side `org_access.Middleware` will 403 if not.\n\t */\n\torganizationId: string;\n\t/** Connector type id, e.g. \"runzero-findings\". */\n\ttypeId: string;\n\tkind: ComponentKind;\n\t/** Display name shown in the frame header. */\n\tdisplayName?: string;\n\t/**\n\t * Name for the created connector. On create, defaults to the connector\n\t * type's name plus a short unique suffix. On edit, omit to keep the\n\t * existing name.\n\t */\n\tname?: string;\n\t/**\n\t * Description for the created connector. On create, defaults to the\n\t * connector type's description. On edit, omit to keep the existing\n\t * description.\n\t */\n\tdescription?: string;\n\t/**\n\t * Show an editable name input inside the iframe, prefilled with the\n\t * resolved name, so the end user can name the connector themselves.\n\t * A non-empty name is required when shown. Defaults to false.\n\t */\n\tisNameEditable?: boolean;\n\t/**\n\t * Show an editable description input inside the iframe, prefilled with\n\t * the resolved description. Defaults to false.\n\t */\n\tisDescriptionEditable?: boolean;\n\t/** Existing connector id — present for edit mode, absent for create. */\n\texistingId?: string;\n\t/**\n\t * Whether the synthetic-data toggle is exposed in the form. Defaults\n\t * to `false` (toggle hidden). Production embed customers keep this\n\t * off — synthetic mode is a Monad-internal testing affordance.\n\t */\n\tsynthetic?: boolean;\n\t/** Theming tokens applied as CSS variables inside the iframe. */\n\tappearance?: Appearance;\n\t/**\n\t * Stylesheet URLs the host wants to apply to the iframe page. The\n\t * iframe loads these as `<link rel=\"stylesheet\">` tags before\n\t * rendering the form.\n\t *\n\t * All-or-nothing: if this array is present and non-empty, the\n\t * iframe loads ONLY these stylesheets and does NOT load its\n\t * built-in Monad UI defaults. If it's absent or empty, the iframe\n\t * loads the built-in defaults and nothing else. Mixing host CSS\n\t * with the defaults is intentionally not supported — partial\n\t * styling tends to produce confusing half-themed forms.\n\t *\n\t * URLs MUST be reachable from the iframe origin (typically by\n\t * being served from the host page's own origin, which the browser\n\t * permits for stylesheet `<link>` loads even cross-origin).\n\t */\n\tstylesheets?: string[];\n\t/**\n\t * Per-field copy overrides merged onto the connector's config form.\n\t * Copy only — see FieldMetaOverride.\n\t */\n\tconfigMetaOverrides?: ConfigMetaOverrides;\n}\n\nexport type HostMessage = InitMessage;\n","/**\n * Connector-lifecycle helpers — the \"wiring harness\" a host (Okta, in\n * the demo's fiction) drops into its own backend to power Disable /\n * Enable / Delete buttons in its own UI.\n *\n * Unlike createConnectorFrame (browser-only), these are isomorphic:\n * they just sequence Monad's standard `/v2/{org}/*` + `/v1/{org}/*`\n * API calls. They run wherever the host holds its API key — almost\n * always its backend.\n *\n * The host supplies a `MonadRequest` thunk that performs an\n * authenticated request and returns parsed JSON. The harness owns the\n * non-obvious parts: which endpoint + verb each action maps to, the\n * delete ordering (pipeline before the input/output it references),\n * and resolving an input id to its pipeline without the host having to\n * store that mapping.\n */\n\n/**\n * Authenticated request against the Monad API, returning parsed JSON\n * (or undefined for empty responses). The host implements this with\n * its long-lived API key and base URL. Path is the full API path,\n * e.g. `/v2/{org}/pipelines/{id}`.\n */\nexport type MonadRequest = (path: string, init?: RequestInit) => Promise<any>;\n\n/* ===== resolve ===== */\n\n/**\n * Where an integration's resources live and their current state.\n * Returned by findIntegrationPipeline so the host can act on an\n * integration knowing only the input id.\n */\nexport interface ResolvedIntegration {\n\torg: string;\n\tpipelineId: string;\n\t/** Whether the pipeline is currently enabled. */\n\tenabled: boolean;\n\tinputId: string;\n\t/** Output id of the pipeline's destination node, if it has one. */\n\toutputId?: string;\n}\n\ninterface PipelineNode {\n\tcomponent_id?: string;\n\tcomponent_type?: string;\n}\n\n/**\n * Find the pipeline an input feeds. Lets the host act on an integration\n * (disable/enable/delete) knowing only the input id — no need to\n * persist an input→pipeline map of its own.\n *\n * The pipelines list is slim (no node wiring), so this fetches each\n * pipeline's detail and scans for the input node. Fine for the modest\n * pipeline counts a single tenant sub-org has; a host managing many\n * pipelines per sub-org should instead persist the `pipelineId`\n * buildDevNullPipeline returns and skip the lookup.\n *\n * Returns null when the input has no pipeline.\n */\nexport async function findIntegrationPipeline(opts: {\n\trequest: MonadRequest;\n\torg: string;\n\tinputId: string;\n}): Promise<ResolvedIntegration | null> {\n\tconst { request, org, inputId } = opts;\n\tconst listed = await request(`/v2/${org}/pipelines/`);\n\tconst pipelines: any[] = Array.isArray(listed)\n\t\t? listed\n\t\t: (listed?.pipelines ?? listed?.data ?? []);\n\n\tfor (const summary of pipelines) {\n\t\tif (!summary?.id) continue;\n\t\tlet detail: any;\n\t\ttry {\n\t\t\tdetail = await request(`/v2/${org}/pipelines/${summary.id}`);\n\t\t} catch {\n\t\t\tcontinue; // skip a pipeline we can't read rather than failing\n\t\t}\n\t\tconst pipeline = detail?.config ?? detail;\n\t\tconst nodes: PipelineNode[] = pipeline?.nodes ?? [];\n\t\tconst inputNode = nodes.find((n) => n.component_type === 'input' && n.component_id === inputId);\n\t\tif (!inputNode) continue;\n\t\tconst outputNode = nodes.find((n) => n.component_type === 'output');\n\t\treturn {\n\t\t\torg,\n\t\t\tpipelineId: summary.id,\n\t\t\tenabled: Boolean(pipeline.enabled),\n\t\t\tinputId,\n\t\t\toutputId: outputNode?.component_id\n\t\t};\n\t}\n\treturn null;\n}\n\n/* ===== enable / disable ===== */\n\nexport interface SetEnabledOptions {\n\trequest: MonadRequest;\n\torg: string;\n\tpipelineId: string;\n\t/** true → resume processing, false → stop (config is kept either way). */\n\tenabled: boolean;\n}\n\n/**\n * Enable or disable an integration's pipeline. Disabling stops data\n * flowing but destroys nothing — the input, output, and pipeline\n * config all survive, so a later enable resumes it as it was.\n *\n * The pipeline PATCH endpoint replaces the whole config rather than\n * accepting a partial, so this reads the current pipeline and sends it\n * back with only the `enabled` flag flipped.\n *\n * Race warning: this is GET-then-PATCH on the same resource, so a\n * concurrent edit between the two requests gets clobbered. Fine for\n * human-driven toggles. If you call this from automation, fan-out, or\n * a retry loop, ask the API team for a server-side `{ enabled }`\n * partial PATCH instead — the round-trip we do here is only a\n * workaround for the current full-config replace semantics.\n */\nexport async function setIntegrationEnabled(opts: SetEnabledOptions): Promise<void> {\n\tconst { request, org, pipelineId, enabled } = opts;\n\tconst detail = await request(`/v2/${org}/pipelines/${pipelineId}`);\n\tconst pipeline = detail?.config ?? detail;\n\tawait request(`/v2/${org}/pipelines/${pipelineId}`, {\n\t\tmethod: 'PATCH',\n\t\tbody: JSON.stringify({\n\t\t\tname: pipeline.name,\n\t\t\tdescription: pipeline.description ?? '',\n\t\t\tenabled,\n\t\t\tnodes: (pipeline.nodes ?? []).map((n: any) => ({\n\t\t\t\tid: n.id,\n\t\t\t\tslug: n.slug,\n\t\t\t\tcomponent_id: n.component_id,\n\t\t\t\tcomponent_type: n.component_type,\n\t\t\t\tenabled: n.enabled ?? true\n\t\t\t})),\n\t\t\tedges: (pipeline.edges ?? []).map((e: any) => ({\n\t\t\t\tname: e.name,\n\t\t\t\tdescription: e.description ?? '',\n\t\t\t\tfrom_node_instance_id: e.from_node_instance_id,\n\t\t\t\tto_node_instance_id: e.to_node_instance_id,\n\t\t\t\tdisabled: e.disabled ?? false,\n\t\t\t\tconditions: e.conditions\n\t\t\t}))\n\t\t})\n\t});\n}\n\n/** Convenience: disable the pipeline (stop without deleting). */\nexport function disableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void> {\n\treturn setIntegrationEnabled({ ...opts, enabled: false });\n}\n\n/** Convenience: re-enable a disabled pipeline. */\nexport function enableIntegration(opts: Omit<SetEnabledOptions, 'enabled'>): Promise<void> {\n\treturn setIntegrationEnabled({ ...opts, enabled: true });\n}\n\n/* ===== delete ===== */\n\n/**\n * Which resources a delete removes. An embed integration is a pipeline\n * + an input + (in the dev/null flow) an output — but hosts provision\n * and own tenant resources differently. Some keep a shared output,\n * some keep the input so it can be re-wired later, some only ever tear\n * down the pipeline. Choose the policy that matches how you provision;\n * a flag left undefined defaults to `true` (delete it).\n *\n * Three ready-made policies are exported — CLEANUP_FULL,\n * CLEANUP_KEEP_OUTPUT, CLEANUP_PIPELINE_ONLY — or pass your own.\n */\nexport interface CleanupPolicy {\n\t/** Delete the pipeline. Default: true. */\n\tpipeline?: boolean;\n\t/** Delete the input connector. Default: true. */\n\tinput?: boolean;\n\t/** Delete the output connector. Default: true. */\n\toutput?: boolean;\n}\n\n/** Tear down everything — pipeline, input, and output. */\nexport const CLEANUP_FULL: CleanupPolicy = { pipeline: true, input: true, output: true };\n\n/** Remove the pipeline and input but keep the output (e.g. a shared sink). */\nexport const CLEANUP_KEEP_OUTPUT: CleanupPolicy = {\n\tpipeline: true,\n\tinput: true,\n\toutput: false\n};\n\n/** Remove only the pipeline; keep the input so it can be re-wired. */\nexport const CLEANUP_PIPELINE_ONLY: CleanupPolicy = {\n\tpipeline: true,\n\tinput: false,\n\toutput: false\n};\n\nexport interface DeleteIntegrationOptions {\n\trequest: MonadRequest;\n\torg: string;\n\t/** Pipeline to remove. Omit when the input has no pipeline. */\n\tpipelineId?: string;\n\t/** Input to remove. Omit to leave it. */\n\tinputId?: string;\n\t/** Output to remove (the auto-created dev/null sink). Omit to leave it. */\n\toutputId?: string;\n\t/**\n\t * What to actually delete. Defaults to everything supplied\n\t * (CLEANUP_FULL). Set a flag false to keep that resource even when\n\t * its id is provided.\n\t */\n\tcleanup?: CleanupPolicy;\n}\n\n/**\n * Delete an integration per a cleanup policy. Order matters: the\n * pipeline references the input and output, so it goes first. Only\n * resources whose id is supplied AND whose policy flag is true are\n * removed — so nothing the host wants to keep is touched, and nothing\n * it wants gone is left orphaned.\n */\nexport async function deleteIntegration(opts: DeleteIntegrationOptions): Promise<void> {\n\tconst { request, org, pipelineId, inputId, outputId } = opts;\n\tconst policy: Required<CleanupPolicy> = {\n\t\tpipeline: true,\n\t\tinput: true,\n\t\toutput: true,\n\t\t...opts.cleanup\n\t};\n\tif (pipelineId && policy.pipeline) {\n\t\tawait request(`/v2/${org}/pipelines/${pipelineId}`, { method: 'DELETE' });\n\t}\n\tif (inputId && policy.input) {\n\t\tawait request(`/v1/${org}/inputs/${inputId}`, { method: 'DELETE' });\n\t}\n\tif (outputId && policy.output) {\n\t\tawait request(`/v1/${org}/outputs/${outputId}`, { method: 'DELETE' });\n\t}\n}\n\n/* ===== build ===== */\n\nexport interface BuildDevNullPipelineOptions {\n\trequest: MonadRequest;\n\torg: string;\n\tinputId: string;\n\t/** Used to name the auto-created output and pipeline. */\n\tinputName: string;\n\t/** Status polls before giving up (default 15, ~2s apart). */\n\tpollAttempts?: number;\n}\n\nexport interface BuiltPipeline {\n\tpipelineId: string;\n\toutputId: string;\n\tstatus: string;\n\t/** True once the pipeline reports Running. */\n\tactive: boolean;\n}\n\n/**\n * Stand up a dev/null pipeline for a freshly-configured input: create a\n * dev/null output, create an enabled pipeline wiring input → output,\n * then poll until it reports Running. The POC's \"what happens after an\n * input is configured\" path, packaged as a reusable harness call.\n */\nexport async function buildDevNullPipeline(\n\topts: BuildDevNullPipelineOptions\n): Promise<BuiltPipeline> {\n\tconst { request, org, inputId, inputName, pollAttempts = 15 } = opts;\n\tconst label = inputName || 'Input';\n\n\tconst output = await request(`/v2/${org}/outputs`, {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({\n\t\t\toutput_type: 'dev-null',\n\t\t\tname: `${label} → /dev/null`,\n\t\t\tdescription: 'Auto-created sink for embed pipeline',\n\t\t\tpromise_id: '',\n\t\t\tconfig: { settings: {}, secrets: {} }\n\t\t})\n\t});\n\n\tconst pipeline = await request(`/v2/${org}/pipelines/`, {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({\n\t\t\tname: `${label} pipeline`,\n\t\t\tdescription: 'Auto-created when the input was configured via embed',\n\t\t\tenabled: true,\n\t\t\tnodes: [\n\t\t\t\t{ slug: 'in', component_id: inputId, component_type: 'input', enabled: true },\n\t\t\t\t{ slug: 'out', component_id: output.id, component_type: 'output', enabled: true }\n\t\t\t],\n\t\t\tedges: [\n\t\t\t\t{\n\t\t\t\t\tfrom_node_instance_id: 'in',\n\t\t\t\t\tto_node_instance_id: 'out',\n\t\t\t\t\tdescription: 'all records',\n\t\t\t\t\tconditions: { operator: 'always' }\n\t\t\t\t}\n\t\t\t]\n\t\t})\n\t});\n\n\tlet status = 'Pending';\n\tfor (let attempt = 0; attempt < pollAttempts; attempt++) {\n\t\tconst s = await request(`/v2/${org}/pipelines/${pipeline.id}/status`);\n\t\tstatus = s?.status ?? 'Unknown';\n\t\tif (status === 'Running' || status === 'Erroring') break;\n\t\tawait new Promise((r) => setTimeout(r, 2000));\n\t}\n\n\treturn {\n\t\tpipelineId: pipeline.id,\n\t\toutputId: output.id,\n\t\tstatus,\n\t\tactive: status === 'Running'\n\t};\n}\n","/**\n * @monad-inc/embed/connect — host-side SDK for embedding Monad's\n * connector-config UI as a secure iframe.\n *\n * The customer's page calls `createConnectorFrame()`; this injects an\n * <iframe> pointing at a Monad-hosted page (@monad-inc/embed-frame). The\n * connector form — and every secret the user types into it — runs\n * inside that iframe on Monad's origin. Same-origin policy means the\n * customer's own JavaScript can never read those secrets. The customer\n * gets back only a connector id (a safe reference), the same way a\n * Stripe integration only ever sees a `pm_…` token.\n *\n * The host is still responsible for one thing: minting a short-lived\n * embed session token from its backend (POST /v1/embed/sessions, with\n * the long-lived API key) and handing it to `createConnectorFrame`.\n * That token travels into the iframe over postMessage, never the URL.\n */\nimport {\n\tPROTOCOL_SOURCE,\n\tPROTOCOL_VERSION,\n\ttype Appearance,\n\ttype ComponentKind,\n\ttype ConfigMetaOverrides,\n\ttype FrameMessage,\n\ttype HostMessage\n} from './protocol';\n\nexport {\n\tPROTOCOL_SOURCE,\n\tPROTOCOL_VERSION,\n\ttype Appearance,\n\ttype ComponentKind,\n\ttype ConfigMetaOverrides,\n\ttype DiscriminatorMetaOverride,\n\ttype FieldMetaOverride,\n\ttype FrameMessage,\n\ttype HostMessage,\n\ttype InitMessage\n} from './protocol';\n\nexport {\n\tfindIntegrationPipeline,\n\tsetIntegrationEnabled,\n\tdisableIntegration,\n\tenableIntegration,\n\tdeleteIntegration,\n\tbuildDevNullPipeline,\n\tCLEANUP_FULL,\n\tCLEANUP_KEEP_OUTPUT,\n\tCLEANUP_PIPELINE_ONLY,\n\ttype MonadRequest,\n\ttype ResolvedIntegration,\n\ttype SetEnabledOptions,\n\ttype DeleteIntegrationOptions,\n\ttype CleanupPolicy,\n\ttype BuildDevNullPipelineOptions,\n\ttype BuiltPipeline\n} from './lifecycle';\n\nexport interface ConnectorFrameOptions {\n\t/** DOM node (or selector) the iframe is appended to. */\n\tcontainer: HTMLElement | string;\n\t/**\n\t * URL of the Monad embed-frame page. Defaults to\n\t * \"https://app.monad.com/embed\" (production). Override for staging\n\t * (\"https://app.monad.security/embed\") or local testing\n\t * (\"http://localhost:5173/embed\"). MUST be a different origin than\n\t * the host page — that cross-origin boundary is what isolates the\n\t * secrets. A path component is allowed (so the embed page can live\n\t * at a sub-route of the main app); the postMessage security checks\n\t * compare against the URL's origin, not its full href.\n\t */\n\tframeOrigin?: string;\n\t/**\n\t * Monad API base the iframe should call. Defaults to\n\t * \"https://app.monad.com/api\" (production). Override for non-prod\n\t * environments. Set this together with `frameOrigin` — mixing\n\t * environments will not work.\n\t */\n\tapiBase?: string;\n\t/** Connector type id, e.g. \"runzero-findings\". */\n\ttypeId: string;\n\tkind: ComponentKind;\n\t/** Short-lived embed session token (host backend mints it). */\n\tsessionToken: string;\n\t/**\n\t * Sub-org id the session is scoped to. The host backend already knows\n\t * this — it's the same value passed to `/v1/embed/sessions` when minting\n\t * the token. Forwarded into the iframe so it can build `/v1/<org>/*` and\n\t * `/v2/<org>/*` URLs.\n\t */\n\torganizationId: string;\n\t/** Display name shown in the frame header. */\n\tdisplayName?: string;\n\t/**\n\t * Name for the created connector. On create, defaults to the connector\n\t * type's name plus a short unique suffix. On edit, omit to keep the\n\t * existing name.\n\t */\n\tname?: string;\n\t/**\n\t * Description for the created connector. On create, defaults to the\n\t * connector type's description. On edit, omit to keep the existing\n\t * description.\n\t */\n\tdescription?: string;\n\t/**\n\t * Show an editable name input inside the iframe (prefilled with the\n\t * resolved name) so the end user can name the connector themselves.\n\t * A non-empty name is required when shown. Defaults to false.\n\t */\n\tisNameEditable?: boolean;\n\t/**\n\t * Show an editable description input inside the iframe (prefilled with\n\t * the resolved description). Defaults to false.\n\t */\n\tisDescriptionEditable?: boolean;\n\t/** Existing connector id — pass for edit mode, omit to create. */\n\texistingId?: string;\n\t/**\n\t * Whether the synthetic-data toggle is exposed in the form. Defaults\n\t * to `false` — production embed flows keep it hidden.\n\t */\n\tsynthetic?: boolean;\n\t/** Theming tokens applied as CSS variables inside the iframe. */\n\tappearance?: Appearance;\n\t/**\n\t * Stylesheet URLs to apply inside the iframe. All-or-nothing: if\n\t * any URL is provided, the iframe loads ONLY these and skips its\n\t * built-in Monad UI defaults; if omitted/empty, the iframe loads\n\t * the defaults and nothing else. URLs must be reachable from the\n\t * iframe — typically served by the host page's own origin.\n\t */\n\tstylesheets?: string[];\n\t/**\n\t * Per-field copy overrides for the connector's config form: label,\n\t * description, placeholder. Copy only — nothing that changes a\n\t * field's type, requiredness, or submitted value.\n\t *\n\t * configMetaOverrides: {\n\t * settings: {\n\t * ddsource: { placeholder: 'northstar' },\n\t * service: { placeholder: 'northstar-web' },\n\t * },\n\t * }\n\t *\n\t * Keys are the connector's own wire keys, so check the connector-type\n\t * response rather than guessing — the Datadog output's source field is\n\t * `ddsource`, not `source`. Unmatched keys are ignored.\n\t *\n\t * `placeholder` is a hint, not a default: a field the user never\n\t * touches still submits `\"\"`. Nested fields are reached with\n\t * `children`, union variants with `discriminator.one_of.<variant>`.\n\t */\n\tconfigMetaOverrides?: ConfigMetaOverrides;\n\t/** Fired when the user saves successfully. */\n\tonSave?: (connector: { id: string; name?: string }) => void;\n\t/** Fired when the user cancels. */\n\tonCancel?: () => void;\n\t/** Fired on a save/test/load failure inside the iframe. */\n\tonError?: (message: string) => void;\n}\n\nexport interface ConnectorFrame {\n\t/** Remove the iframe and stop listening for its messages. */\n\tdestroy(): void;\n}\n\n/**\n * Production defaults. Customers integrating against prod don't pass\n * `frameOrigin`/`apiBase` at all; non-prod environments override both\n * together. Treat these as a public API contract — changing a hostname\n * is a breaking change for anyone on the default.\n */\nconst DEFAULT_FRAME_ORIGIN = 'https://app.monad.com/embed';\nconst DEFAULT_API_BASE = 'https://app.monad.com/api';\n\n/**\n * Inject the Monad connector iframe into `container` and wire the\n * postMessage channel. Returns a handle whose `destroy()` tears it all\n * down.\n *\n * Handshake:\n * 1. iframe loads, posts `ready`;\n * 2. host replies with `init` (session token, type, appearance);\n * 3. iframe mounts the form and streams `resize` as content grows;\n * 4. on save, iframe posts `saved {connector}`; on cancel, `cancel`.\n */\nexport function createConnectorFrame(opts: ConnectorFrameOptions): ConnectorFrame {\n\tconst container =\n\t\ttypeof opts.container === 'string'\n\t\t\t? document.querySelector<HTMLElement>(opts.container)\n\t\t\t: opts.container;\n\tif (!container) {\n\t\tthrow new Error('[monad-inc/connect] container element not found');\n\t}\n\n\tconst frameInput = opts.frameOrigin ?? DEFAULT_FRAME_ORIGIN;\n\tconst apiBase = opts.apiBase ?? DEFAULT_API_BASE;\n\n\t// Split the input URL into two pieces:\n\t// * `frameUrl` keeps the path (e.g. /embed) — used as iframe.src.\n\t// * `frameOrigin` is the bare origin — used for the postMessage\n\t// security checks below, which must compare against the value\n\t// the browser surfaces on MessageEvent.origin (always a bare\n\t// origin, never a path).\n\t// Previously these were collapsed into one `new URL(...).href`,\n\t// which silently broke any frameOrigin with a path — the equality\n\t// check `event.origin !== frameOrigin` would always fail and `init`\n\t// never made it back to the iframe.\n\tconst frameUrl = new URL(frameInput);\n\tconst frameOrigin = frameUrl.origin;\n\tconst framePath = frameUrl.pathname.replace(/\\/$/, '');\n\n\tconst iframe = document.createElement('iframe');\n\tiframe.className = 'monad-connect-frame';\n\tiframe.title = 'Monad connector configuration';\n\tiframe.style.width = '100%';\n\tiframe.style.border = '0';\n\tiframe.style.display = 'block';\n\t// The iframe needs its own scripts and same-origin access to the\n\t// Monad API; it must NOT get same-origin access to the host. A\n\t// cross-origin iframe already can't reach the host — no sandbox\n\t// attribute is needed for isolation, and adding one would block the\n\t// form's own scripts. Forms/popups stay enabled for OAuth-style\n\t// connectors that may open a window.\n\tiframe.allow = 'clipboard-write';\n\t// parentOrigin lets the iframe scope its postMessage target + sender\n\t// check. It is NOT a secret and NOT a capability — lying about it\n\t// only breaks the liar's own embed (see protocol.ts).\n\tiframe.src = `${frameOrigin}${framePath}/?parentOrigin=${encodeURIComponent(window.location.origin)}`;\n\n\tlet destroyed = false;\n\n\tfunction postToFrame(msg: HostMessage): void {\n\t\tiframe.contentWindow?.postMessage(msg, frameOrigin);\n\t}\n\n\tfunction onMessage(event: MessageEvent): void {\n\t\tif (destroyed) return;\n\t\t// --- security boundary: validate before trusting anything ---\n\t\tif (event.origin !== frameOrigin) return;\n\t\tif (event.source !== iframe.contentWindow) return;\n\t\tconst msg = event.data as FrameMessage | undefined;\n\t\tif (!msg || msg.source !== PROTOCOL_SOURCE) return;\n\n\t\tswitch (msg.type) {\n\t\t\tcase 'ready':\n\t\t\t\tpostToFrame({\n\t\t\t\t\tsource: PROTOCOL_SOURCE,\n\t\t\t\t\ttype: 'init',\n\t\t\t\t\tversion: PROTOCOL_VERSION,\n\t\t\t\t\tsessionToken: opts.sessionToken,\n\t\t\t\t\tapiBase,\n\t\t\t\t\torganizationId: opts.organizationId,\n\t\t\t\t\ttypeId: opts.typeId,\n\t\t\t\t\tkind: opts.kind,\n\t\t\t\t\tdisplayName: opts.displayName,\n\t\t\t\t\tname: opts.name,\n\t\t\t\t\tdescription: opts.description,\n\t\t\t\t\tisNameEditable: opts.isNameEditable,\n\t\t\t\t\tisDescriptionEditable: opts.isDescriptionEditable,\n\t\t\t\t\texistingId: opts.existingId,\n\t\t\t\t\tsynthetic: opts.synthetic,\n\t\t\t\t\tappearance: opts.appearance,\n\t\t\t\t\tstylesheets: opts.stylesheets,\n\t\t\t\t\tconfigMetaOverrides: opts.configMetaOverrides\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase 'resize': {\n\t\t\t\t// Clamp to a sane range so a renderer bug in the iframe\n\t\t\t\t// can't blow up the host layout with a multi-million-px\n\t\t\t\t// height. The origin check above already gates who can\n\t\t\t\t// send this — clamping is defense in depth. Number.isFinite\n\t\t\t\t// guards against non-numeric heights that would otherwise\n\t\t\t\t// produce \"NaNpx\" and leave the iframe collapsed.\n\t\t\t\tconst h = Number.isFinite(msg.height) ? msg.height : 0;\n\t\t\t\tiframe.style.height = `${Math.min(Math.max(h, 0), 20000)}px`;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'saved':\n\t\t\t\topts.onSave?.(msg.connector);\n\t\t\t\tbreak;\n\t\t\tcase 'error':\n\t\t\t\topts.onError?.(msg.message);\n\t\t\t\tbreak;\n\t\t\tcase 'cancel':\n\t\t\t\topts.onCancel?.();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\twindow.addEventListener('message', onMessage);\n\tcontainer.appendChild(iframe);\n\n\treturn {\n\t\tdestroy(): void {\n\t\t\tdestroyed = true;\n\t\t\twindow.removeEventListener('message', onMessage);\n\t\t\tiframe.remove();\n\t\t}\n\t};\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monad-inc/embed",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "SDK for embedding Monad's UI in other apps via a secure cross-origin iframe. The connect module (createConnectorFrame()) renders the connector-config form inside a Monad-hosted iframe, so the host's JavaScript never sees credential material — only a connector id reference comes back. Zero runtime dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Monad Inc.",