Skip to content

feat(BEX-290): add UI app support for action links - #41

Open
piyushsarin-sib wants to merge 5 commits into
features_set_public_clifrom
BEX-290_ui-components
Open

feat(BEX-290): add UI app support for action links#41
piyushsarin-sib wants to merge 5 commits into
features_set_public_clifrom
BEX-290_ui-components

Conversation

@piyushsarin-sib

@piyushsarin-sib piyushsarin-sib commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a second app type alongside OAuth integrations. A UI app shares the app record, credentials, and version lifecycle with an OAuth app, and adds a ui_app block to app-config.json describing where and how it renders inside Brevo. The first variant is the action link: an entry in a CRM record's header More menu that opens an external URL with record context.

Based on features_set_public_cli, not main — this extends applyConditionals, which the PKCE work introduced on that branch.

Two commits: the initial implementation, then a refactor! aligning the contract with the platform (see Contract below).

Contract — verified against the platform

The ui_app block is the app-store backend's app_versions.snapshot payload field for field, so an authored config needs no translation to be stored, served and rendered:

{
  "ui_app": {
    "extensionType": "action_link",
    "surfacePointList": ["contactDetails.headerMenu.action"],
    "heading": "Invoice Manager",
    "subheading": "Review invoice history for this contact",
    "redirectLink": "https://example.com/brevo",
    "linkTarget": "_blank"
  }
}

Verified against appSnapshot in app-store-backend feature/BEX-308-extensibility-app-configs and ActionLinkConfig / getExtensionActions in integrations-common-frontend bex-350-app-configs-link-target. The second commit replaced an earlier draft that followed the product spec's properties/trigger vocabulary — which nothing on either side of the platform reads.

Two spec'd fields are deliberately absent, because the platform has no counterpart:

  • a per-action label — the menu entry is labelled with the app name (getExtensionActions uses app.appName);
  • contextProperties — the record context an action receives is an allow-list on the extension_points registry row (AllowedContextField), chosen per slot by the platform, not declared by the partner.

Command surface

Change Detail
app create --type <oauth|ui> New flag plus a leading app-type prompt. Defaults to oauth; non-interactive runs without the flag are unchanged.
UI-app flags Repeatable --surface <contact|company|deal>, --heading, --subheading, --redirect-link, --link-target <_blank|_self>. Every prompt has a scriptable equivalent.
app deploy <account-id> Make an app available in one account. Refuses until the config has been validated by app upload.
app remove <account-id> Remove it from one account. No upload gate; "not deployed" exits 0.

Notable behaviour

  • Extension-point names are validated locally against the twelve-point registry. This is the highest-value check in the flow. Slot names follow the BEX-350 grammar <location>.<place>.<kind>; the UI kit matches them by exact string equality and the backend drops any name without a registry row. A typo, a stale .region-era name, or a casing slip therefore produces an empty slot, an HTTP 200, and no error anywhere in the stack — the CLI is the only layer that can report it. An action link may only target <location>.headerMenu.action; a .widget slot is rejected.
  • modalIframeUrl is rejected on an action link — the UI kit keeps it only for iframe_extension, so authoring one here would be silently discarded.
  • The UI path never collects or defaults a redirect URL. resolveRedirectUrls falls back to http://localhost:3009/auth/callback in non-TTY runs, so without an explicit skip a UI app would acquire a callback it has no flow for. redirect_uris is omitted from the create call entirely.
  • The redirect-URL requirement on app upload is now OAuth-only.
  • The upload diff covers the snapshot. Previously hasNoChanges compared every field except this one, so editing only ui_app would report "Already up to date" and never push. Comparison is key-order insensitive.
  • app scaffold no longer destroys a hand-edited ui_app block. On detected drift it rewrites app-config.json wholesale from server values, which don't include it.
  • The app type is decided locally, never from server data. fetchAppContext takes the block from its caller only — honouring a server-returned snapshot could reclassify a project explicitly created as OAuth. Regression test included.
  • applyConditionals generalised from a single distribution value to a flag set, preserving its byte-for-byte output invariant for existing scaffolds.

Inverted contract — please note in review

upload.test.ts previously asserted it('never sends a ui_app field'), and QA-TESTCASES.md TC-5.7 said the same. That guarantee now applies to OAuth apps only; both were updated rather than deleted. The OAuth upload payload is otherwise unchanged and still never carries the key.

What still needs confirming

The field names are settled. What isn't:

  1. The snapshot write path doesn't exist yet. On the BEX-308 branch only the manifest read path parses app_versions.snapshot; app-store-bo-be's POST /apps/{appId}/build writes the separate config column via a multipart config field. The CLI currently sends the block under a snapshot key on POST /v3/app-store/apps/{id}/upload — needs confirming or correcting once the write endpoint lands. src/types.ts (UploadAppPayload) and upload.ts are the only places to change.
  2. Deploy/remove endpoints assume POST /v3/app-store/apps/{id}/deploy|remove with account_id in the body, and HTTP 422 for "not uploaded" / "not deployed".
  3. BEX-350 needs a coordinated release — kit, reseeded extension_points registry, and backend together. A CLI release ahead of the reseed authors names that resolve to nothing, silently. The CLI's registry copy lives in src/lib/constants.ts (EXTENSION_POINTS) and must move in lockstep.

Tracked in RELEASE-CHECKLIST.mdBefore UI-apps GA.

Testing

  • yarn test — 847 passing (46 suites). New: deploy.test.ts, remove.test.ts, plus UI-app blocks in create/upload/scaffold/validators/conditionals, including a case per silent-failure slot-name mode.
  • yarn lint, tsc --noEmit, prettier --check clean.
  • Manually driven end to end against a mock API: UI-app create → config inspection → upload (payload key confirmed as snapshot) → deploy → remove; each validation rejection; the deploy gate; the snapshot-only diff; and an OAuth regression sweep confirming an unchanged config and payload.
  • Manual verification still outstanding against a real test account — see RELEASE-CHECKLIST.mdPer-branch verification. In particular whether the backend accepts the snapshot, and whether the action link renders once the registry is reseeded.

Docs

Design notes are committed at BEX-290-ui-app-action-links.md — the contract, which changes were forced by existing code, and what's still assumed. A third commit sanitised internal references (private repo/branch names, source paths, internal symbols, DB schema, a partner name) out of both those notes and the code comments added alongside them, replacing them with the platform roles the CLI depends on plus the Jira keys. Nothing load-bearing was lost — the wire contract, slot grammar, registry and validation rules are all partner-facing and already in the shipped agent-context files.

agent-context/SKILL.md and agent-context/AGENTS.md both updated and kept in sync per CLAUDE.md — new commands, flags, the snapshot shape, the slot grammar and its silent-failure modes, and the fields that don't exist. Both carry a ⚠️ UI apps are not available yet notice mirroring the public-apps one, reusing the same internal-Brevo-account exception so dogfooding still works — flagging this as a judgement call, easy to drop if unwanted. Also updated: README.md (including a stale brevo app update row that predates this change), CLAUDE.md, root AGENTS.md, QA-TESTCASES.md (section 12, 14 cases), RELEASE-CHECKLIST.md, TODO.md, and a changeset (minor).

🤖 Generated with Claude Code

piyushsarin-sib and others added 3 commits July 28, 2026 19:30
Introduce a second app type alongside OAuth integrations. A UI app shares the
app record, credentials, and version lifecycle with an OAuth app and adds a
`ui_app` block to app-config.json describing where and how it renders inside
Brevo. The first variant is the action link: an entry point in a CRM record's
action menu that opens an external URL with record context.

- `app create` gains `--type <oauth|ui>` plus a leading app-type prompt. The UI
  path collects placement and destination instead of OAuth callbacks and never
  defaults a redirect URL, so a UI app can't acquire a callback it has no flow
  for. Defaults to `oauth`, and non-interactive runs without the flag are
  unchanged.
- `app upload` sends and validates `ui_app` for UI apps; the redirect-URL
  requirement becomes OAuth-only. The upload diff covers `ui_app` (key-order
  insensitive) so a ui_app-only edit is no longer reported as up to date. The
  OAuth payload is unchanged and still never carries the key.
- New `app deploy <account-id>` / `app remove <account-id>` manage per-account
  availability. `deploy` refuses until the config has been validated by an
  upload; `remove` treats "not deployed" as informational and exits 0.
- `app scaffold` in a UI-app project refreshes the base config, reports that
  there are no features to scaffold, and preserves a hand-edited `ui_app` block
  through the refresh that previously rewrote app-config.json wholesale.
- `applyConditionals` generalised from a single distribution value to a flag
  set, preserving its byte-for-byte output for existing scaffolds.

The `ui_app` field names and the deploy/remove endpoint contract are
implemented against stated assumptions, marked in code comments and tracked in
RELEASE-CHECKLIST.md. UI apps are not live on the platform yet, so the agent
docs carry a not-available notice mirroring the public-apps one, including the
internal-account exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite the `ui_app` block to the shape the platform actually consumes, verified
against app-store-backend `feature/BEX-308-extensibility-app-configs`
(`appSnapshot`) and integrations-common-frontend
`bex-350-app-configs-link-target` (`ActionLinkConfig`, `getExtensionActions`).

The previous shape followed the UIApp Support Spec's `properties`/`trigger`
vocabulary, which nothing on either side of the platform reads. The block is now
`app_versions.snapshot` field for field, so an authored config needs no
translation to be stored, served and rendered:

  type: "link"                  -> extensionType: "action_link"
  properties.surface/placement   -> surfacePointList: ["<location>.headerMenu.action"]
  properties.title/description   -> heading / subheading
  trigger.externalUrl            -> redirectLink
  (new)                          -> linkTarget: "_blank" | "_self"

Two spec'd fields are dropped because the platform has no counterpart: a
per-action label (the record action menu labels the entry with the app name) and
`contextProperties` (record context is an allow-list on the extension_points
registry row, chosen per slot by the platform). `--cta-label` and
`--context-property` go with them; `--surface` becomes repeatable so one action
link can target several record pages, and `--link-target` is added.

Extension-point names get local registry validation, which is the highest-value
check in the flow: names follow the BEX-350 grammar `<location>.<place>.<kind>`,
the UI kit matches them by exact string equality, and the backend drops any name
without a registry row. A typo, a stale `.region`-era name or a casing slip
therefore yields an empty slot, an HTTP 200 and no error anywhere — the CLI is
the only layer that can report it. `modalIframeUrl` is likewise rejected on an
action link, since the kit keeps it only for `iframe_extension`.

The upload payload key becomes `snapshot` (the destination column) rather than
`ui_app`. That transport remains an assumption: nothing writes
app_versions.snapshot yet — only the manifest read path exists, and
app-store-bo-be's build endpoint writes the separate `config` column. The field
names, however, are now confirmed.

BREAKING CHANGE: a `ui_app` block written by the previous commit is rejected on
upload. No published release carried the old shape, so this affects only configs
created from this branch — recreate with `brevo app create --type ui`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This repo is public, so the BEX-290 notes and the code comments added alongside
them named private repositories, feature branches, source paths, internal
symbols, and database schema. None of that is needed to explain the change, and
all of it leaks internal structure to anyone reading the repo or the published
npm tarball.

Replaced throughout with the platform *roles* the CLI actually depends on — "the
app snapshot the platform stores", "the manifest read path", "the extensibility
UI kit", "the extension-point registry" — plus the Jira keys, which CLAUDE.md
already allows and which are enough for a maintainer to find the source. Also
dropped a partner name that only appeared as an aside about a legacy rendering
path, and an internal product-surface name in the deploy/remove rationale.

Nothing load-bearing is lost: the wire contract, the slot grammar, the twelve
registered extension points, the validation rules, and the silent-failure modes
they guard against are all partner-facing and already documented in the shipped
agent-context files.

The design notes themselves are now committed. They were held back for exactly
this reason; with the internal detail out they are a useful record of why the
contract looks the way it does, which of the changes were forced by existing
code, and what is still assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@piyushsarin-sib piyushsarin-sib added the documentation Improvements or additions to documentation label Jul 28, 2026
piyushsarin-sib and others added 2 commits July 29, 2026 01:22
… extensionType

Two corrections to the UI-app surface before review.

Remove `--type` and the five UI-app field flags (`--surface`, `--heading`,
`--subheading`, `--redirect-link`, `--link-target`) from `brevo app create`. A UI
app is now authored entirely through the interactive prompts. UI apps are not
live on the platform, and a scriptable create surface would invite pipelines to
pin to a shape that can still change.

`resolveAppType` is gated on `interactive` rather than `isTTY`, so `--json` on a
TTY yields an OAuth app instead of prompting into a JSON stream — consistent with
how the logo prompt already treats `--json`. Every non-interactive run therefore
creates an OAuth app, exactly as before BEX-290, leaving existing scripted
invocations unaffected. `resolveUiApp` drops its flag-merge logic and fallback
helper; the prompts' own `validate` callbacks plus `validateUiApp` on the
assembled block are the input checks.

Adopt the camelCase `extensionType` vocabulary from BEX-350: `actionLink`,
`iframeExtension`, `legacyComponent`. These match `FEATURE_TYPES` in the
extensibility UI kit, which is what routes on the value. The pre-BEX-350
snake_case spellings are not accepted — the kit still maps them for consumers,
but that map is slated for removal once every producer emits camelCase, and the
CLI is a producer. The feature is still in development, so there is no authored
config to migrate and no alias map is carried.

One consequence worth recording: the platform's own server-side `linkTarget`
default is gated on the literal snake_case value, so it no longer fires for
CLI-authored apps. Harmless — the CLI always writes `linkTarget` explicitly and
the UI kit defaults an absent value client-side.

Also drop the unused `EXTENSION_TYPES` export.

Tests: the UI-app suite is rewritten to drive inquirer, keyed by question name
rather than call order so a reordered prompt cannot shift an answer onto the
wrong field. Flag-validation cases that are now unreachable are removed, the
non-TTY case is inverted to expect an OAuth app, and a `--json`-on-a-TTY case is
added. Docs, agent context, QA cases and the changeset are updated to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Name the type for the field it types. `surfacePointList` entries were typed as
`ExtensionPointName`, which read as a third term for a value already carrying
two: the CLI authors it as a surface point and the platform serves the same
string back as `extensionPoint`. Dropping the `Name` suffix also stops implying
the type is a label for a slot rather than the slot itself.

`validateExtensionPointName` follows to `validateSurfacePoint` — leaving it
would have contradicted the rename at the one call site that uses both.

Registry constants (`EXTENSION_POINTS`, `EXTENSION_ACTION_POINTS`,
`EXTENSION_LOCATIONS`) keep their names deliberately: they mirror the platform's
extension-point registry, which is a different thing from the field a partner
authors. User-facing copy is unchanged for the same reason — "extension point"
is what a partner sees in the registry, so the validation errors still say it.

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

Copy link
Copy Markdown

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

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant