Repository navigation
feat(core): defineIntegration on the integration runtime (ADR-054, one runtime, two front doors) - #703
Conversation
…untime (ADR-054) The code front door compiles to the same target as an ADR-063 document: an IntegrationBase subclass with a static Definition and an events map. Contract-2 extensions compile to the contract-1 binding next already runs, under a key derived from the module key (crm-webhooks), so devtools, the defined-routes router and the queue worker need no new path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…(a) end to end Example (a) of ADR-054 Appendix A runs unchanged: a signed HubSpot POST reaches the derived extension route, resolves to the installation, hydrates the deal and posts to Slack (both nock'd). Triggers run on Stack B's tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…; design note fromDefinitionDocument, definitionDocumentHost and embedDefinitionDocument replace defineIntegrationClass, defineSharedIntegrationClass and extendIntegrationClass, which stay as aliases until Stack B merges. A test embeds the hubspot-slack document in a defineIntegration class: both kinds of trigger come out of one triggersFor list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
routes[].event and events stay what a hand-bound Definition.extensions (or an older core) runs; defineIntegration ignores them. A module can move an extension to contract 2 without breaking existing integrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
…ng builders The contract-2 binding gets deal-alerts__crmwebhooks under /crm-webhooks/webhooks with no Prisma layer, and triggers get the tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
Records what the defineIntegration build (#703) settled: both front doors compile to Definition + this.events on IntegrationBase; the document door's names; one trigger registry; webhook facts declared once by the module; reserved module keys, read-only ctx.integration, fields as a module export, the contract-2 webhook pipeline, and three new open questions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6a9383b. Configure here.
| } | ||
|
|
||
| /** The compiled plan of a class made by defineIntegration, or null for any other class. */ | ||
| const planOf = (Cls) => (Cls && Object.prototype.hasOwnProperty.call(Cls, PLAN) ? Cls[PLAN] : null); |
There was a problem hiding this comment.
planOf drops embedded subclasses
Medium Severity
planOf only accepts a class that owns the plan symbol, not a subclass. embedDefinitionDocument wraps a defineIntegration class, so reconcileAppSubscriptions sees no extension bindings and skips subscription reconcile for the composed integration.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6a9383b. Configure here.
| if (type === undefined || type === IntegrationClass.Definition.name) out.push(integrationId); | ||
| } | ||
| return out; | ||
| } |
There was a problem hiding this comment.
Lookup treats load failures as matches
Medium Severity
defaultLookup keeps an installation when config.type is missing or loadIntegrationContextById fails. A failed load returns no context, so the type check becomes undefined and the id is still queued for DISPATCH, including other integration types that share the same account.
Reviewed by Cursor Bugbot for commit 6a9383b. Configure here.
… after deploy, on lifecycle and from an admin script reconcileAppSubscriptions removes nothing unless called with prune: true, and never prunes a group whose installations it could not read. A dry run reports create and remove without writing. No recurring trigger runs it: - frigg deploy runs it add-only after a successful deploy (--prune-subscriptions, --skip-subscriptions); a failure never fails the deploy - installation create and config update add what that installation needs when its binding options depend on config (delete removes nothing) - createReconcileSubscriptionsScript is the admin script (prune, dryRun, or args --prune --dry-run); defineApp appends module-exported scripts One provider developer app per stage is the supported setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
|
Stack B review (COMMENT only) — Group 4Verdict (would merge?): changes needed Checklist (ADR-054)
Blocking
Should-fix
|
| for (const integrationId of ids || []) { | ||
| const found = await commands.loadIntegrationContextById(integrationId); | ||
| const type = found?.context?.record?.config?.type; | ||
| if (type === undefined || type === IntegrationClass.Definition.name) out.push(integrationId); |
There was a problem hiding this comment.
[should-fix] defaultLookup keeps an id when config.type is undefined (type === undefined || type === Definition.name).
Failure scenario: A partial/failed loadIntegrationContextById still fans out a DISPATCH for that id; worker may hydrate the wrong installation or fail mid-batch. Treat missing type as non-match (or fail closed) unless you intentionally want legacy rows without type.
Cross-repo: api-module-library reviews (could not post on AML — no write ACL on that repo from this agent)api-module-library #237 — HubSpot contract 2Verdict: changes needed (align with #234 before Stack B cut; checklist otherwise passes)
Blocking vs #234: subscription URLs are Should-fix: bump package version so consumers can pin the contract-2 release. api-module-library #234 — dated API versionsVerdict: changes needed
Please copy these onto AML #237 / #234 as COMMENT reviews if write access is available outside this agent. |





Summary
defineIntegration,defineAppanduse: ADR-054's stable, event-driven one-file path, built on Stack B's runtime. One runtime, two front doors. A JSON integration document (ADR-063) anddefineIntegrationin code both compile to the same target: anIntegrationBasesubclass with a staticDefinitionand aneventsmap. The builders, routers, queue worker, ADR-055 tick and management API read only that target. Neither front door is a serialisation of the other, and they compose:embedDefinitionDocument(DealAlerts, { definition })works.Stacked on
claude/shared-conversation-8hgmbr-stack-9-module-methods(#700). Module side: friggframework/api-module-library#237. Design note:packages/core/integrations/define/DESIGN.md. ADR text: ADR-054 amendment on #669.Example (a) from ADR-054 Appendix A runs unchanged (
integrations/define/examples/deal-alerts.js): a signed HubSpot POST reaches/api/deal-alerts-integration/crm-webhooks/webhooks, resolves portalId to the installation, hydrates the deal and posts to Slack (both nock'd).What is built (
packages/core/integrations/define/)defineIntegration({ name, version, display, usage, modules, config, on, triggers, actions, visible })returns anIntegrationBasesubclass.(payload, ctx)with nothis.ctx = { modules: { <key>: api }, config, log, telemetry, commands, integration }, frozen, andcommandsis created lazily.onevent no extension emits, string handlers, baduse()options, a contract or core-range mismatch.Definition.extensionsbinding thatnextalready runs, under<moduleKey>-<extKey>. Devtools, the defined-routes router and_mergeExtensionsneeded no changes.on['crm.deal.changed'], else the extension default, else ack and count).config: JSON Schema fragments plus module fields (module.fields.x(opts)= schema +options(api)loader) compile togetConfigOptions()/refreshConfigOptions().validateConfig()and NEEDS_CONFIG work unchanged.triggerscompile toDefinition.triggersand run on Stack B's tick (RunTriggerTick, thenonTriggerRun). Extension triggers merge as<moduleKey>.<name>.actionsbecomeUSER_ACTIONevents.visiblebecomesstatic visible(context), honoured byintegration-types.reconcileAppSubscriptions()creates what the union of bindings needs and removes nothing unless called withprune: true. It never prunes a group whose installations it could not read. A dry run reportscreate/removewithout writing. It runs at three points, with no recurring trigger:frigg deploy: add-only, no database.--prune-subscriptionsprunes,--skip-subscriptionsskips it, and a failure never fails the deploy.use()options are functions of config, it adds what that installation needs. On delete it removes nothing (removal is a pruning reconcile). The call sites areonCreate/onUpdate/onDeleteuntil the §5 dispatcher exists.createReconcileSubscriptionsScript()takesprune/dryRunorargs: ['--prune', '--dry-run'].defineAppappends scripts that bound modules export, and HubSpot exportshubspot.reconcile-subscriptions.defineApp()returns{ Definition }and validates againstapp-definition.schema.jsonvia@friggframework/schemaswhen installed. The real schema accepts the ADR sample.Naming decisions
defineIntegrationis the code front door. Stack B's document door is renamed:fromDefinitionDocument(wasdefineIntegrationClass),definitionDocumentHost(wasdefineSharedIntegrationClass) andembedDefinitionDocument(wasextendIntegrationClass). The old names stay as aliases until Stack B merges.config.definitionis the document pin.Definition.triggers. A document'sworkflows[].triggeris a workflow's start condition, and its schedule kind feeds the sametriggersForlist.use()options reference these facts and never restate them.IntegrationBasemember (config,events,send…), because_appendModulessetsthis[key]. ADR-054 said any identifier would work.routes[].event,events) for hand-bound integrations.defineIntegrationignores both, so HubSpot moves to contract 2 without a major version.Changes to Stack B code (additive)
webhookRequestFactsis exported fromintegration-base.js.static visibleis honoured inintegration-types.js. Name aliases were added indefinitions/and the README was updated.nockis a core devDependency.Test plan
integrations/define/tests/*: 62 tests covering composition, compile target, namespacing (same module bound twice), ctx shape, reserved keys, actions, config and fields,visible, the webhook pipeline (verify/401/handshake/resolve fan-out/hydrate/unhandled), triggers on Stack B's tick, extension triggers, subscription reconcile (add-only default,prune, dry run, prune skipped without installations; nock'd HubSpot API), lifecycle invoker (create/update add, delete no call, failure is a warning), admin script,defineApp(fake and real schema), both front doors composing, and example (a) end to end over a real express server with raw-body capturedevtools/.../define-integration-provisioning.test.js: the binding getsdeal-alerts__crmwebhookswithout a Prisma layer, and the triggers get the tickpackages/coreintegrations+handlers: 1152 passed. 3 suites fail the same way on the base branch here (missingaws-sdkv2, DocumentDB/Mongo environment)devtools/frigg-clideploy-subscriptions-step.test.js: add-only by default,--prune-subscriptions,--skip-subscriptions, a failure does not fail the deployChecklist (ADR-054 stable path, items not in this PR)
lifecyclehooks. TODO: move the subscription call sites from theonCreate/onUpdate/onDeleteoverrides into itfrigg deploy), installation lifecycle (create/update), admin script (prune,dryRun,--prune,--dry-run)defineAppappends module-exported admin scripts (module.scripts). An extension-levelscriptskey is still not accepteduse()options depend on installation config: it cannot read installations (no database), so it adds only static needs and never prunes those groups. The lifecycle hooks and the admin script cover themsubscriptions.scope: 'installation'plus a journal-poll trigger (ADR-054 amendment)envscoping in devtools andfrigg validatefields.channel(ADR-054 item 6). The test uses a stand-indeal.propertyChangevsdeal.changed)frigg-extensionsskill🤖 Generated with Claude Code
https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
📦 Published PR as canary version:
2.0.0--canary.703.12b980b.0✨ Test out this PR locally via:
npm install @friggframework/admin-scripts@2.0.0--canary.703.12b980b.0 npm install @friggframework/core@2.0.0--canary.703.12b980b.0 npm install @friggframework/devtools@2.0.0--canary.703.12b980b.0 npm install @friggframework/eslint-config@2.0.0--canary.703.12b980b.0 npm install @friggframework/prettier-config@2.0.0--canary.703.12b980b.0 npm install @friggframework/schemas@2.0.0--canary.703.12b980b.0 npm install @friggframework/serverless-plugin@2.0.0--canary.703.12b980b.0 npm install @friggframework/test@2.0.0--canary.703.12b980b.0 npm install @friggframework/ui@2.0.0--canary.703.12b980b.0 # or yarn add @friggframework/admin-scripts@2.0.0--canary.703.12b980b.0 yarn add @friggframework/core@2.0.0--canary.703.12b980b.0 yarn add @friggframework/devtools@2.0.0--canary.703.12b980b.0 yarn add @friggframework/eslint-config@2.0.0--canary.703.12b980b.0 yarn add @friggframework/prettier-config@2.0.0--canary.703.12b980b.0 yarn add @friggframework/schemas@2.0.0--canary.703.12b980b.0 yarn add @friggframework/serverless-plugin@2.0.0--canary.703.12b980b.0 yarn add @friggframework/test@2.0.0--canary.703.12b980b.0 yarn add @friggframework/ui@2.0.0--canary.703.12b980b.0Note
Medium Risk
Touches integration runtime, webhook verification/dispatch, and provider subscription lifecycle on deploy and installation create/update—important paths but heavily tested and mostly additive exports.
Overview
Adds ADR-054’s code front door for one-file integrations:
defineIntegration,defineApp,use,defineExtension, plus app-scopereconcileAppSubscriptions/createReconcileSubscriptionsScript, all exported from@friggframework/core.defineIntegrationcompiles author specs into the sameIntegrationBase+ staticDefinitiontarget as ADR-063 documents. Contract-2 extensions become the existing extension binding shape (receipt → resolve → dispatch webhooks, namespaced events, config fields, triggers, user actions, frozen(payload, ctx)handlers).integration-typesnow respectsstatic visible, and document-door helpers are renamed (fromDefinitionDocument, etc.) with deprecated aliases.frigg deployruns an add-only webhook subscription reconcile after success (--prune-subscriptions/--skip-subscriptions); failures do not fail deploy. Broad Jest coverage includes the deal-alerts example and provisioning tests;nockadded as a dev dependency.Reviewed by Cursor Bugbot for commit 12b980b. Bugbot is set up for automated code reviews on this repo. Configure here.