Skip to content

feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-459] - #87

Open
piyushsarin-sib wants to merge 7 commits into
mainfrom
feat/iframe-extension-authoring
Open

feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-459]#87
piyushsarin-sib wants to merge 7 commits into
mainfrom
feat/iframe-extension-authoring

Conversation

@piyushsarin-sib

@piyushsarin-sib piyushsarin-sib commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Jira: BEX-459 · Epic: BEX-211

What

Phase 1 (authoring release) of the iframe-extension launch, CLI side. The validator, the upload diff (modal URL: row), the wire-only strip and the write-back already spoke iframeExtension — only the prompt withheld it (removed in 63bc9ee while authoring wasn't ready).

  • Prompt: the UI-app integration-type prompt offers Iframe (Embeds your page in a modal) next to Link — restored enabled, not as the old disabled "coming soon" entry. Private apps only: on a public app the choice is hidden (not disabled), because iframe extensions are private-only in v1 and a disabled entry would advertise a combination every layer refuses.
  • Iframe branch: the URL question becomes Iframe URL — the page Brevo embeds in the modal (record context arrives as query params) and the answer lands in each entry's modal_iframe_url instead of redirect_link. No size prompt (decided): the modal's dimensions are hand-authored per entry in app-config.json.
  • Private-only, locally too: uiAppType.validateConfig refuses a hand-authored iframeExtension block on a public app before any round trip, with the same wording as bo-be's 400. (readProjectConfig defaults an absent distribution_type to private, so only an explicit public reaches it.)
  • Smoke: yarn smoke --suite=ui grows an iframe leg — create (pty) → upload no-op → install → uninstall → delete — that skips rather than fails on a build whose prompt has no Iframe choice or an environment whose registry has no slot enabled for iframeExtension yet (pre-flip). New prompt patterns are pinned against messages in en.test.ts like the existing ones (the PR fix: harmonize app-create prompt copy (distribution & UI-app fields) #73 pty-timeout precedent).
  • Docs in the same PR (repo rule): agent-context/SKILL.md, agent-context/AGENTS.md, CLAUDE.md. One changeset, minor.

Tests

  • create: Link+Iframe both enabled on private; Iframe hidden on public; iframe branch authors modal_iframe_url (never redirect_link/link_target) and narrows both registry reads by iframeExtension; iframe-specific URL prompt copy.
  • upload: private-only refusal before any round trip; existing iframe upload/diff tests unchanged.
  • 1415 tests green, lint + prettier clean, smoke scripts typecheck under scripts/tsconfig.json.

Sequencing

The staging registry flip lands before this releases (bo-be PR DTSL/app-store-bo-be#404 carries the SQL): on an un-flipped environment the restored choice dead-ends in APP_CREATE_UI_POINTS_NONE_FOR_TYPE, which reads as a CLI bug and isn't. The prod flip follows this CLI release.

Part of the iframe-extension Phase 1 (epic BEX-281). Counterparts: bo-be DTSL/app-store-bo-be#404 (registry flip + server-side private-only rule); kit PR (sandbox + modal size) to follow.

🤖 Generated with Claude Code


Update 2026-09-03 — rebased onto main (#92 + #97)

Main gained the registry default_size seeding (#92, the cherry-picked successor of #91, released in 2.2.1) and the non-interactive actionLink UI creation (#97, released in 2.2.2). Reconciliation on this branch:

1434 tests green, lint + prettier clean, smoke scripts typecheck.


Update 2026-09-03 (2) — inline iframe cards

An iframeExtension entry on a widget slot can now author layout: "inline" (page embedded directly in the card body) vs "modal" (the default — never written, so layout-less configs are byte-identical). app create asks the question only for iframe widget placements; validateUiApp refuses the field on actionLink entries; the diff/box/install summary gain a layout: row via VALUE_ROWS. Counterparts: bo-be #404 (validation + storage, widget-only rule from the registry), app-store-backend #757 (serves appConfigs.layout, cacheSchemaVersion v9→v10), kit #317 (inline routing). Deploy order: bo-be → backend → kit → CLI.


Update 2026-09-04 — layout is widget-only, and what still lands in this PR

Rule. layout: "inline" is only meaningful on the 6 widget slots (contactDetails|dealDetails|companyDetails × overviewMain.widget|overviewSidebar.widget), the registry rows whose component_type is widget and therefore render a card. The 3 header-menu slots (*.headerMenu.action, component_type: "action") yield a menu entry that must open something, so there is nothing to be inline in. The slot kind is judged from the registry row, never from a local list (CLAUDE.md: no registry mirror in the CLI).

What this PR does today

  • Prompt gatepromptIframeLayout (src/app-types/ui/authoring.ts) asks How should it appear on the page? only when the picked placement's row has component_type === 'widget'. A header-menu placement never sees the question and its entry is written without layout. buildSurfacePointList can only ever write inline; modal is the default and is never stamped.
  • ValidationvalidateUiApp pins the vocabulary ("<value>" is not supported — use "modal" or "inline") and refuses layout on any actionLink entry. It deliberately has no local slot-kind check; the platform's validateLayoutPerEntry answers that on create and upload with:
    400 ui_app.surface_point_list authors layout "inline" on slot(s) that render no card: <slug> (component_type "action")
  • Non-interactive--ui-app / --ui-config stay actionLink-only; layout is not part of their input.

Landing before merge (same PR)

  1. Make the create-time gate a positive assertion: if any path tries to write layout for a row whose component_type is not widget, throw locally naming the entry (ui_app.surface_point_list["<slug>"].layout: "inline" needs a widget slot; <slug> renders a menu entry). Same outcome as the skip today, but a future multi-select or a new component_type value can no longer author inline silently.
  2. Map bo-be's layout 400 to an en.ts message so brevo app upload of a hand-edited inline on a header-menu slot reads like every other per-entry refusal.
  3. Tests: validator cases for the vocabulary pin and the actionLink refusal (none exist yet), en.test.ts pins for the three layout prompt strings, and a create test that the question is absent for a header-menu placement (exists) plus one that a widget placement gets it.
  4. --ui-config refuses a layout key explicitly instead of dropping it.

Planned in this PR: modal size presets. For an iframe entry that opens a modal (widget slot with modal layout, or any header-menu slot) app create asks Modal sizeSmall / Medium / Large, default Large. Large is the platform's current behaviour (the kit's ModalDialog is hard-coded size="lg"), so an unanswered or Large answer writes nothing and layout-less configs stay byte-identical. Wire: per-entry modal_size ("small" | "medium" | "large", iframeExtension only, refused on actionLink), validated by bo-be, served as appConfigs.modalSize inside the same v9→v10 cache bump as layout, mapped by the kit onto the dialog size. This is a preset for the dialog, distinct from the widget card's size, which is still seeded from the slot's registry default (BEX-461) and hand-edited in px/%; the CLI still asks no px/% question. Counterparts to follow in bo-be #404, backend #757, kit #317. Tracked under BEX-211 / BEX-467.

Sandbox stored in the snapshot, never authorable (2026-09-04). bo-be now stamps sandbox into the stored ui_app snapshot at the root with the default allow-scripts allow-same-origin allow-forms allow-popups, server-side only. The CLI API neither accepts it (an authored sandbox in app-config.json is a 400 unknown key) nor echoes it. This PR adds sandbox to uiAppType.wireOnlyKeys so a value can never reach app-config.json even if it were echoed, exactly like link_target and extension_point_name. In M1 the value is store-only: the manifest does not serve it and the kit keeps its constant.


Update 2026-09-04 (3) — plan sync

  • Phase 2c exists now. ui.showConfirm is no longer dropped from v1; it moved to Phase 2c (host confirm dialogs for inline cards, M2c 2026-11-13). No change to this PR.
  • Explicit app type in app-config.json (Phase 1, sibling change). create will write an app-type field (OAuth vs UI) so the file states what it is instead of relying on the presence of ui_app; upload checks it agrees with the ui_app / auth block and refuses a mismatch with a migration hint; isUiAppConfig() stays the single reader; files without the field keep working. Open: wire (bo-be reflected key sets + openapi) vs file-only key stripped before upload. Lands in its own PR on top of this branch; CLAUDE.md's "there is no appType key" note changes with it.
  • Test iframe app. The smoke iframe leg and the POM cases will target a Brevo-owned test iframe app on staging (echoes context params, renders modal S/M/L and inline, one entry per slot kind) instead of an ad-hoc URL. Built in brevo-cli-apps in W1–W2.
  • No review gate before 2a. Contract details for the trust layer are decided inside the backend mint PR and mirrored in the kit PR; no security-review kickoff or PII review is planned, and the context vocabulary stays identifiers-only.
    Plan of record: https://app.notion.com/p/sendinblue/Iframe-Embed-timelines-3d1449002dcb80fc8c3ff1a6a0c7c0b0

@piyushsarin-sib piyushsarin-sib changed the title feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-281] feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-459] Sep 1, 2026
piyushsarin-sib and others added 2 commits September 3, 2026 13:05
…nsions, private-only [BEX-281]

The iframe-extension launch, CLI side:

- The UI-app integration-type prompt offers Iframe (Embeds your page in
  a modal) next to Link, on a private app only — iframe extensions are
  private-only in v1. The choice is hidden (not disabled) on a public
  app: every layer refuses the combination.
- The iframe branch asks for the embed URL and writes it to each
  entry's modal_iframe_url instead of redirect_link; the example
  context-URL lines in the created-app box read either destination.
- Private-only enforced locally too: uiAppType.validateConfig refuses
  an iframeExtension block on a public app before any round trip
  (APP_UI_IFRAME_PRIVATE_ONLY), mirroring bo-be's 400.
- Smoke: yarn smoke --suite=ui grows an iframe leg (create → upload
  no-op → install → uninstall → delete) that skips on a build without
  the choice or an environment without the registry flip; its new
  prompt pins are covered by en.test.ts like the existing ones.
- Docs updated in the same PR (repo rule): agent-context/SKILL.md,
  agent-context/AGENTS.md, CLAUDE.md. One changeset (minor).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and size seeding

Rebase fallout from #92 (registry default_size seeding) and #97
(non-interactive actionLink UI creation) landing on main first:

- resolveUiAppOrRedirectUris threads the distribution answer through to
  resolveUiApp, which gates the Iframe choice (private-only). The
  non-interactive path deliberately doesn't take it — that route is
  actionLink-only by design, so its buildSurfacePointList call pins
  urlField: 'redirect_link'.
- buildSurfacePointList carries both #92's sizeFor and this branch's
  urlField/url; its unit tests updated to the merged signature.
- Agent docs merged: the non-interactive flags (#97) and the Iframe
  choice documented side by side, both noting the non-interactive routes
  stay actionLink-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@piyushsarin-sib
piyushsarin-sib force-pushed the feat/iframe-extension-authoring branch from 5b957d8 to 7c31953 Compare September 3, 2026 07:38
piyushsarin-sib and others added 5 commits September 3, 2026 17:42
An iframeExtension entry on a widget slot can author layout: "inline"
(the page embedded directly in the card body) vs "modal" (the default —
never written, so layout-less configs stay byte-identical).

- create asks the question only for an iframe WIDGET placement (an
  action slot's menu entry is a modal by definition) and writes the key
  only for inline.
- validateUiApp refuses layout on actionLink entries and pins the
  vocabulary; the widget-only rule stays server-side (registry fact).
- fields.ts gains the layout: row, so the upload diff, created-app box
  and install summary all show it.
- Docs (SKILL/AGENTS) + changeset extended; 1437 tests green.

Requires bo-be with per-entry layout support (same-branch PR #404) —
an older server 400s the key as unknown, the standard sequencing rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…[BEX-467]

bo-be stamps a root `sandbox` onto the stored snapshot — the iframe policy
the platform decides, not the partner. Left in the wire-only list's blind
spot, the first successful upload writes it into app-config.json and every
upload after that reports drift on a field nobody can edit.

`stripUiAppWireOnlyKeys` is the single reader and strips at every depth, so
naming the key in `uiAppType.wireOnlyKeys` is the whole change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…[BEX-459]

Four rules that finish per-entry iframe authoring, all of them about a value
the CLI would otherwise write and something downstream would then ignore or
reject.

`modal_size` — `small` / `medium` / `large`, asked for every iframe entry that
actually OPENS a modal. That is deliberately a different set of placements from
the layout question's: layout is widget-only (an action slot's menu entry has
one presentation), while the menu entry opens a modal too, and a widget card
answered `inline` opens none. Large is the default, is pre-selected rather than
listed first, and writes nothing — so a default answer leaves app-config.json
byte-identical to one authored before the question existed, the same contract
`layout: "modal"` has. `validateUiApp` pins the vocabulary and refuses the field
on an `actionLink`; the `modal size:` row is one more `VALUE_ROWS` entry, so the
upload diff, the created-app box and `app install`'s summary all gain it at once.

`buildSurfacePointList` now refuses a `layout` on a row that renders no card,
naming the entry the way the validator does. The prompt never asks for one
there, so this cannot fire from the interactive flow — but the builder is what
stamps the field and it stamps whatever row it is handed, so an unchecked caller
would author a block the upload endpoint rejects and the partner would meet the
rule one round trip later, phrased by the server.

`--ui-config` refuses `layout` and `modal_size` by name instead of dropping
them. The file's key set is fixed and everything else is discarded, so a
silently ignored presentation field means the created app renders differently
from the file that asked for it, with nothing said anywhere.

`app upload` translates the platform's own layout refusal — narrowly, exactly
like `isPublicDistributionRefusal`: only a 400 that mentions `layout` is
relabelled, so an unrelated 400 keeps the server's text. No local slot list:
whether a slot renders a card is a registry fact and the CLI holds no copy of
the registry, so the server stays the authority and this only says what to edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ui suite's iframe leg went URL -> output directory. Two questions can
render in between and neither is guaranteed: the layout list appears only when
the first iframe-enabled registry row on the picked page is a widget, and the
modal-size list only when a modal actually opens. Which row the registry serves
first is not this suite's business, so the leg cannot know in advance, and
either question is also missing from an older build under --against=published.

The pty runner gains `optional` on an exchange: a prompt that hasn't appeared is
skipped once a LATER one has — proof the flow moved past it, rather than that it
is merely slow — and trailing optionals left unmatched at exit are not a broken
flow. Distinct from the existing `send: () => null` abort, which says the prompt
rendered without the choice this leg needs and skips the whole leg.

Both new questions are answered with their default (Enter), and the leg now
asserts neither wrote a key — the only part of the omit-the-default contract a
real run can prove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y [BEX-459]

Both agent docs gain `modal_size` next to `layout`, and say plainly that the
two are gated differently — an agent that assumes they cover the same
placements will author a size on an inline card, or omit one on a menu entry.
Also: `--ui-config` refuses either field by name, and `sandbox` joins the
server-stamped keys that must never be written into app-config.json.

Appended to the branch's existing changeset rather than adding a second file;
the bump level stays minor, since this is all new surface on an unreleased
feature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant