Skip to content

feat(core): defineIntegration on the integration runtime (ADR-054, one runtime, two front doors) - #703

Open
seanspeaks wants to merge 6 commits into
claude/shared-conversation-8hgmbr-stack-9-module-methodsfrom
claude/define-integration
Open

seanspeaks wants to merge 6 commits into
claude/shared-conversation-8hgmbr-stack-9-module-methodsfrom
claude/define-integration

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

defineIntegration, defineApp and use: 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) and defineIntegration in code both compile to the same target: an IntegrationBase subclass with a static Definition and an events map. 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 an IntegrationBase subclass.
    • Handlers are (payload, ctx) with no this. ctx = { modules: { <key>: api }, config, log, telemetry, commands, integration }, frozen, and commands is created lazily.
    • Errors are thrown when the class is defined: unknown keys, an on event no extension emits, string handlers, bad use() options, a contract or core-range mismatch.
  • Contract-2 extensions compile to the contract-1 Definition.extensions binding that next already runs, under <moduleKey>-<extKey>. Devtools, the defined-routes router and _mergeExtensions needed no changes.
    • Pipeline: receipt (handshake → verify → receive, no DB) → RESOLVE (worker, dry: entity reverse lookup filtered to this type, fanned out) → DISPATCH (worker, hydrated: normalize, then 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 to getConfigOptions() / refreshConfigOptions(). validateConfig() and NEEDS_CONFIG work unchanged.
  • triggers compile to Definition.triggers and run on Stack B's tick (RunTriggerTick, then onTriggerRun). Extension triggers merge as <moduleKey>.<name>.
  • actions become USER_ACTION events. visible becomes static visible(context), honoured by integration-types.
  • App-scope subscriptions, add-only by default: reconcileAppSubscriptions() creates what the union of bindings needs and removes nothing unless called with prune: true. It never prunes a group whose installations it could not read. A dry run reports create/remove without writing. It runs at three points, with no recurring trigger:
    • (a) after frigg deploy: add-only, no database. --prune-subscriptions prunes, --skip-subscriptions skips it, and a failure never fails the deploy.
    • (b) installation lifecycle: on create and config update, for bindings whose 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 are onCreate/onUpdate/onDelete until the §5 dispatcher exists.
    • (c) admin script: createReconcileSubscriptionsScript() takes prune/dryRun or args: ['--prune', '--dry-run']. defineApp appends scripts that bound modules export, and HubSpot exports hubspot.reconcile-subscriptions.
    • Supported setup: one provider developer app per stage, because subscriptions are shared app-wide.
  • defineApp() returns { Definition } and validates against app-definition.schema.json via @friggframework/schemas when installed. The real schema accepts the ADR sample.

Naming decisions

  • defineIntegration is the code front door. Stack B's document door is renamed: fromDefinitionDocument (was defineIntegrationClass), definitionDocumentHost (was defineSharedIntegrationClass) and embedDefinitionDocument (was extendIntegrationClass). The old names stay as aliases until Stack B merges.
  • "Definition" now means only the class static. The JSON is an integration document, and config.definition is the document pin.
  • There is one trigger registry: Definition.triggers. A document's workflows[].trigger is a workflow's start condition, and its schedule kind feeds the same triggersFor list.
  • Each module declares its webhook facts once:
  • Module keys cannot name an IntegrationBase member (config, events, send…), because _appendModules sets this[key]. ADR-054 said any identifier would work.
  • A contract-2 bundle may keep its contract-1 face (routes[].event, events) for hand-bound integrations. defineIntegration ignores both, so HubSpot moves to contract 2 without a major version.

Changes to Stack B code (additive)

webhookRequestFacts is exported from integration-base.js. static visible is honoured in integration-types.js. Name aliases were added in definitions/ and the README was updated. nock is 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 capture
  • devtools/.../define-integration-provisioning.test.js: the binding gets deal-alerts__crmwebhooks without a Prisma layer, and the triggers get the tick
  • packages/core integrations + handlers: 1152 passed. 3 suites fail the same way on the base branch here (missing aws-sdk v2, DocumentDB/Mongo environment)
  • devtools/frigg-cli deploy-subscriptions-step.test.js: add-only by default, --prune-subscriptions, --skip-subscriptions, a failure does not fail the deploy
  • CI

Checklist (ADR-054 stable path, items not in this PR)

  • Lifecycle dispatcher (§5) and extension lifecycle hooks. TODO: move the subscription call sites from the onCreate/onUpdate/onDelete overrides into it
  • Subscription reconcile invokers: post-deploy step (frigg deploy), installation lifecycle (create/update), admin script (prune, dryRun, --prune, --dry-run)
  • defineApp appends module-exported admin scripts (module.scripts). An extension-level scripts key is still not accepted
  • Post-deploy step for apps whose use() 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 them
  • Evaluate HubSpot's Webhooks Journal API (per-portal subscriptions, pull-only) as subscriptions.scope: 'installation' plus a journal-poll trigger (ADR-054 amendment)
  • ADR-027 env scoping in devtools and frigg validate
  • Slack module promoted to v1-ready with fields.channel (ADR-054 item 6). The test uses a stand-in
  • Capability-manifest webhook blocks generated from contract 2. Converge event names (deal.propertyChange vs deal.changed)
  • Docs: one-file quickstart, EXTENSIONS.md contract 2, the frigg-extensions skill
  • Remove the deprecated Stack B names once Stack B merges

🤖 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.0

Note

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-scope reconcileAppSubscriptions / createReconcileSubscriptionsScript, all exported from @friggframework/core.

defineIntegration compiles author specs into the same IntegrationBase + static Definition target 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-types now respects static visible, and document-door helpers are renamed (fromDefinitionDocument, etc.) with deprecated aliases.

frigg deploy runs 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; nock added as a dev dependency.

Reviewed by Cursor Bugbot for commit 12b980b. Bugbot is set up for automated code reviews on this repo. Configure here.

seanspeaks and others added 5 commits October 22, 2026 16:31
…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
@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 22, 2026
seanspeaks added a commit that referenced this pull request Oct 22, 2026
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
@seanspeaks
seanspeaks marked this pull request as ready for review October 22, 2026 20:50

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ 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.

Comment thread packages/core/integrations/define/define-integration.js
Comment thread packages/core/integrations/define/subscriptions.js Outdated
}

/** 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6a9383b. Configure here.

if (type === undefined || type === IntegrationClass.Definition.name) out.push(integrationId);
}
return out;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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
@sonarqubecloud

Copy link
Copy Markdown

@cursor

cursor Bot commented Oct 24, 2026

Copy link
Copy Markdown

Stack B review (COMMENT only) — Group 4

Verdict (would merge?): changes needed

Checklist (ADR-054)

Item Result
Contract-2 → contract-1 binding next already runs Pass
Module keys can’t shadow IntegrationBase Pass (isReservedModuleKey)
Webhook resolve fans out per account installation Pass
Handler ctx frozen Pass (shallow freeze; tested)
Subscriptions ADD-ONLY unless prune; deploy / lifecycle / admin script Pass
Deprecated aliases kept Pass

Blocking

  • Document schema still integration-definition.v0.1.schema.json — must be integration-document before Stack B tip.

Should-fix

  • defaultLookup treats config.type === undefined as a match (see inline comment).

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@cursor

cursor Bot commented Oct 24, 2026

Copy link
Copy Markdown

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 2

Verdict: changes needed (align with #234 before Stack B cut; checklist otherwise passes)

Checklist Result
Existing routes/events; no major bump Pass (contract-1 kept; version still 1.1.7)
Verification at receipt Pass
hubspot.reconcile-subscriptions (--dry-run, --prune) Pass
README: one developer app per stage Pass

Blocking vs #234: subscription URLs are /webhooks/v3/{appId}/subscriptions[+/{id}] (api.js ~84–86). #234 uses /app-webhooks/2026-09/... and lacks webhookSubscriptionById. Joint merge breaks reconcile create/list/delete depending on order.

Should-fix: bump package version so consumers can pin the contract-2 release.

api-module-library #234 — dated API versions

Verdict: changes needed

  1. Blocking: conflicts with next (mergeable: CONFLICTING).
  2. Blocking: path drift vs Bump @babel/traverse from 7.22.4 to 7.23.6 in /api-module-library/frontify #237 (app-webhooks dated surface vs /webhooks/v3 + missing ById / DELETE helpers).
  3. Should-fix: unused @aws-sdk/client-scheduler dependency; several response shape changes framed as Enhancement — call out breaking or bump major.

Please copy these onto AML #237 / #234 as COMMENT reviews if write access is available outside this agent.

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

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant