feat(BEX-290): add UI app support for action links - #41
Open
piyushsarin-sib wants to merge 5 commits into
Open
Conversation
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>
… 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>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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_appblock toapp-config.jsondescribing 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, notmain— this extendsapplyConditionals, 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_appblock is the app-store backend'sapp_versions.snapshotpayload 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
appSnapshotin app-store-backendfeature/BEX-308-extensibility-app-configsandActionLinkConfig/getExtensionActionsin integrations-common-frontendbex-350-app-configs-link-target. The second commit replaced an earlier draft that followed the product spec'sproperties/triggervocabulary — which nothing on either side of the platform reads.Two spec'd fields are deliberately absent, because the platform has no counterpart:
getExtensionActionsusesapp.appName);contextProperties— the record context an action receives is an allow-list on theextension_pointsregistry row (AllowedContextField), chosen per slot by the platform, not declared by the partner.Command surface
app create --type <oauth|ui>oauth; non-interactive runs without the flag are unchanged.--surface <contact|company|deal>,--heading,--subheading,--redirect-link,--link-target <_blank|_self>. Every prompt has a scriptable equivalent.app deploy <account-id>app upload.app remove <account-id>0.Notable behaviour
<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.widgetslot is rejected.modalIframeUrlis rejected on an action link — the UI kit keeps it only foriframe_extension, so authoring one here would be silently discarded.resolveRedirectUrlsfalls back tohttp://localhost:3009/auth/callbackin non-TTY runs, so without an explicit skip a UI app would acquire a callback it has no flow for.redirect_urisis omitted from the create call entirely.app uploadis now OAuth-only.hasNoChangescompared every field except this one, so editing onlyui_appwould report "Already up to date" and never push. Comparison is key-order insensitive.app scaffoldno longer destroys a hand-editedui_appblock. On detected drift it rewritesapp-config.jsonwholesale from server values, which don't include it.fetchAppContexttakes the block from its caller only — honouring a server-returned snapshot could reclassify a project explicitly created as OAuth. Regression test included.applyConditionalsgeneralised 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.tspreviously assertedit('never sends a ui_app field'), andQA-TESTCASES.mdTC-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:
app_versions.snapshot; app-store-bo-be'sPOST /apps/{appId}/buildwrites the separateconfigcolumn via a multipartconfigfield. The CLI currently sends the block under asnapshotkey onPOST /v3/app-store/apps/{id}/upload— needs confirming or correcting once the write endpoint lands.src/types.ts(UploadAppPayload) andupload.tsare the only places to change.POST /v3/app-store/apps/{id}/deploy|removewithaccount_idin the body, and HTTP 422 for "not uploaded" / "not deployed".extension_pointsregistry, and backend together. A CLI release ahead of the reseed authors names that resolve to nothing, silently. The CLI's registry copy lives insrc/lib/constants.ts(EXTENSION_POINTS) and must move in lockstep.Tracked in
RELEASE-CHECKLIST.md→ Before UI-apps GA.Testing
yarn test— 847 passing (46 suites). New:deploy.test.ts,remove.test.ts, plus UI-app blocks increate/upload/scaffold/validators/conditionals, including a case per silent-failure slot-name mode.yarn lint,tsc --noEmit,prettier --checkclean.snapshot) → deploy → remove; each validation rejection; the deploy gate; the snapshot-only diff; and an OAuth regression sweep confirming an unchanged config and payload.RELEASE-CHECKLIST.md→ Per-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.mdandagent-context/AGENTS.mdboth updated and kept in sync perCLAUDE.md— new commands, flags, the snapshot shape, the slot grammar and its silent-failure modes, and the fields that don't exist. Both carry aREADME.md(including a stalebrevo app updaterow that predates this change),CLAUDE.md, rootAGENTS.md,QA-TESTCASES.md(section 12, 14 cases),RELEASE-CHECKLIST.md,TODO.md, and a changeset (minor).🤖 Generated with Claude Code