Skip to content

feat: Netlify provider on the v2 stack (port of #545) (F7) - #686

Open
seanspeaks wants to merge 26 commits into
claude/internal-mcp-06-proxy-scopesfrom
claude/internal-mcp-07-netlify-provider
Open

seanspeaks wants to merge 26 commits into
claude/internal-mcp-06-proxy-scopesfrom
claude/internal-mcp-07-netlify-provider

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This ports #545 (provider plugin architecture + Netlify adapter) onto the internal-MCP stack, so one canary carries both. It is stacked on #685 (F6).

Why: Left Hook's frigg-internal runs on Netlify from canary 2.0.0--canary.545.b4ca16d.0. #545 is based on an older branch with about 2,000 commits that aren't in next. Merging it as-is would drag all of that in, so this PR ports only #545's own diff (893db5dc..b4ca16d). Where #545 and this stack disagree, the stack's behaviour wins.

Ported (a)

@friggframework/provider-netlify. Its handlers now run on core's own createHandler and createApp, not #545's copies, which answered /api/v2 errors in the v1 shape.

  • auth serves core's routers/auth. That brings the full v2 API with it: adopter-JWT, x-frigg and shared-secret auth, the registry, memberships, roles, credentials, the entity proxy with its scopes, v1 or its 410, and extension routes.
  • integration-routes and webhooks use core's router builders. feat: provider plugin architecture + Netlify adapter #545's inline copies skipped extension bindings and conflict checks, and dispatched webhooks to every integration.
  • worker-background passes the app's module definitions to its workers, as the AWS workers do.
  • netlify.toml redirects are generated from core's route registry, which adds the v1 /api/entity* and /api/meta paths feat: provider plugin architecture + Netlify adapter #545 missed.
  • The lib shims require provider-netlify through backend/node_modules. A bare require loads a second core in pnpm workspaces.

@friggframework/provider-aws. A facade over core's own AWS adapters. #545 moved those adapters into the package; here they stay in core.

@friggframework/extension-db-credentials. Ported along with the app-level extension system.

Ported into core (b), adapted so AWS apps don't break

  • setAppDefinition(). It resolves the definition the same way AWS does: adopter-JWT check, entity registry, ownership, managementApi.
  • Extension system. Includes async definition.env.
  • Queue providers. Netlify Background, QStash and SQS. The Worker defaults to SQS when no queueClient is injected, where feat: provider plugin architecture + Netlify adapter #545 threw.
  • NetlifySchedulerAdapter. Ported with its PROCESSING guard.
  • ScheduledJob model. Migration 20261017000000_add_scheduled_job uses only IF NOT EXISTS.
  • Lazy scheduler barrel. require('@friggframework/core') used to load @aws-sdk/client-scheduler, which isn't a core dependency.
  • devtools provider dispatch. Covers package, build, deploy and start. doctor, repair and generate-iam refuse non-AWS apps.

Left out (c)

Test plan

Known gaps (for frigg-internal; not Netlify-specific unless noted)

  1. Possible-integration listing returns 500. Core's Options requires display.detailsUrl, and frigg-internal's integrations don't set it.
  2. /user/create and /user/login return 500 when user.primary: 'organization'. It is the same core router on AWS too.
  3. /api/admin/oauth-credentials returns 500, the same as at b4ca16d. The extension's Prisma model is never composed into the client.
  4. This core has no routers/admin or routers/docs. /api/docs now redirects to /api/meta/openapi/v2.json.

Risks for the reviewer

  • worker-background and /api/queue take requests without authentication, and QStash events aren't signature-checked. Both are as they were in feat: provider plugin architecture + Netlify adapter #545, so frigg-internal is exposed to this today. This needs fixing before it handles per-person credentials.
  • Netlify bundles now trace @aws-sdk (about 95 MB). Check function size against the 250 MB limit.
  • Extension routes on AWS. They are mounted on the auth router, but no API Gateway route reaches them on AWS.
  • /api/v2/reports on Netlify. It is a 404, because admin-scripts don't run there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6


Generated by Claude Code


Note

High Risk
Large cross-cutting platform port touching auth-adjacent queue workers, extension admin routes, and scheduling; PR notes unauthenticated Netlify workers and unsigned queue paths as existing exposure.

Overview
Ports the Netlify provider plugin stack (#545) onto the current core: new workspace packages @friggframework/provider-netlify, provider-aws, and extension-db-credentials, with core changes so AWS deployments keep prior behavior.

App-level extensions (appDefinition.coreExtensions): validation/loading, Prisma schema composition, merged encryption fields, /admin/* routes behind the admin API key, and one-shot bootstraps after DB connect via initializeApp() in createHandler. Module.create resolves async definition.env (e.g. DB-backed OAuth app credentials).

Multi-host runtime: setAppDefinition() for Netlify’s function layout; injectable queue clients on Worker with lazy SQS; queue provider exports and scheduler netlify adapter with ScheduledJob persistence and poll-and-dispatch; scheduleJob no longer requires queueUrl when the scheduler supplies its own target. Integration and webhook routers are refactored into reusable builders for single-function hosts; auth router builds lazily and mounts extension routes. Definition.schedule is validated at load and drives DispatchOngoingSyncs for cron ticks. Discovery adds GET /api/docs → OpenAPI v2.

Monorepo workspaces and docs updated; scheduler/EventBridge adapters load lazily so require('@friggframework/core') does not pull AWS SDK into pruned Netlify bundles.

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

📦 Published PR as canary version: 2.0.0--canary.686.45cab16.0

✨ Test out this PR locally via:

npm install @friggframework/admin-scripts@2.0.0--canary.686.45cab16.0
npm install @friggframework/core@2.0.0--canary.686.45cab16.0
npm install @friggframework/devtools@2.0.0--canary.686.45cab16.0
npm install @friggframework/eslint-config@2.0.0--canary.686.45cab16.0
npm install @friggframework/extension-db-credentials@2.0.0--canary.686.45cab16.0
npm install @friggframework/prettier-config@2.0.0--canary.686.45cab16.0
npm install @friggframework/provider-aws@2.0.0--canary.686.45cab16.0
npm install @friggframework/provider-netlify@2.0.0--canary.686.45cab16.0
npm install @friggframework/schemas@2.0.0--canary.686.45cab16.0
npm install @friggframework/serverless-plugin@2.0.0--canary.686.45cab16.0
npm install @friggframework/test@2.0.0--canary.686.45cab16.0
npm install @friggframework/ui@2.0.0--canary.686.45cab16.0
# or 
yarn add @friggframework/admin-scripts@2.0.0--canary.686.45cab16.0
yarn add @friggframework/core@2.0.0--canary.686.45cab16.0
yarn add @friggframework/devtools@2.0.0--canary.686.45cab16.0
yarn add @friggframework/eslint-config@2.0.0--canary.686.45cab16.0
yarn add @friggframework/extension-db-credentials@2.0.0--canary.686.45cab16.0
yarn add @friggframework/prettier-config@2.0.0--canary.686.45cab16.0
yarn add @friggframework/provider-aws@2.0.0--canary.686.45cab16.0
yarn add @friggframework/provider-netlify@2.0.0--canary.686.45cab16.0
yarn add @friggframework/schemas@2.0.0--canary.686.45cab16.0
yarn add @friggframework/serverless-plugin@2.0.0--canary.686.45cab16.0
yarn add @friggframework/test@2.0.0--canary.686.45cab16.0
yarn add @friggframework/ui@2.0.0--canary.686.45cab16.0

claude added 11 commits October 16, 2026 03:27
Port from #545 (e7becd9, d336d2a, 60108f0),
category (b): core changes the Netlify provider needs. On Netlify the
function bundler only traces static require() calls and process.cwd() is
the function root at runtime, so the generated function entry points
require backend/index.js statically and hand its Definition to core.

Adapted to this stack rather than copied:
- #545's setAppDefinition() made loadAppDefinition() return the raw
  definition with only { integrations, userConfig }. Here the preset
  definition goes through the same resolution as a discovered one
  (adopter-JWT assertion, entity registry, ownership, managementApi
  config, credential log keys, telemetry), so a Netlify function loads
  the app exactly as an AWS Lambda does. loadAppDefinition() also returns
  the raw Definition as `appDefinition` on both paths, which #545's
  callers read.
- database/config.js reads the preset definition first and keeps its own
  discovery (and its detailed require-error message) as the fallback,
  instead of routing through the full loadAppDefinition(), which would
  now also run validation while merely picking the Prisma client.
  getPresetAppDefinition() is the raw accessor for such low-level readers.
- prisma.js: the __dirname-based client path from 60108f0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…edentials

Port from #545 (69acce5, 4f9d12a, ccb5010,
6f62ce6, 5a3df8f, b4ca16d). Classified (a)/(b): frigg-internal's
backend/index.js requires @friggframework/extension-db-credentials and
declares appDefinition.extensions, so the canary must ship both.

Ported faithfully:
- packages/core/extensions/* (loader, schema composer, route mounter,
  bootstrap runner, initializeApp) and their tests.
- packages/extensions/db-credentials (OAuthAppCredential schema, admin
  CRUD routes, bootstrap) and its tests; workspaces + lerna gain
  packages/extensions/* (and packages/providers/* for the next commits).
- Module.resolveEnv()/Module.create() so definition.env may be an async
  function; ModuleFactory awaits Module.create().
- app-definition schema: `extensions` and provider 'netlify'.

Adapted to this stack:
- Routes: #545 mounted extension routes at the end of the v1
  createIntegrationRouter(). Here the v1 router is frozen behind the
  Management API, so createExtensionRoutesRouter() builds one lazy,
  admin-key-protected router that routers/auth.js mounts after the
  Management API router, and that provider-netlify's admin function will
  serve (Netlify sends /api/admin/* there).
- routers/auth.js builds its router on first access instead of at
  require time (c40eca4), so a preset definition may be set either side
  of the require.
- Encryption: extension fields are merged into the registered custom
  schema (registerCustomSchema replaces; #545 would have dropped the
  app's own encryption.schema), and are registered before the module
  credential schemas, whose loader still throws on purpose in this stack
  (see the NOTE in loadModuleEncryptionSchemas).
- The extension schema/encryption config is read from the preset
  definition (setAppDefinition) first, then by discovery.

Left out: the frigg-cli validate command's extension sanitizer
(validate-command does not exist in this stack).

Not wired, as in #545: nothing calls initializeApp(), so extension
bootstraps (db-credentials' env patching) do not run on any provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…rker client injection

Port from #545, category (b): the core pieces the
Netlify provider runs on.

Ported faithfully:
- QueueProvider port, QueueClientInterface port, NetlifyBackgroundProvider
  and QStashQueueProvider (188110e).
- NetlifySchedulerAdapter with the PENDING -> PROCESSING -> deleted|FAILED
  guard and its 23 tests (9513a42, a2a8f01).
- providers/resolve-provider (appDefinition.provider -> the
  @friggframework/provider-<name> package) and its tests (88cf983).
- Lazy scheduler barrel and factory (1176a00): requiring core no longer
  loads @aws-sdk/client-scheduler, which is not a core dependency. Measured
  before this change: require('@friggframework/core') loaded it, so a
  pruned Netlify bundle without it would crash on the first require.
- Worker accepts an injected { queueClient } (023b098/59497126).

Adapted to this stack (the AWS SDK stays a core dependency here):
- #545 made provider-aws mandatory: Worker threw without a queueClient,
  and the queue/scheduler factories resolved every provider, including the
  default 'aws', through resolveProvider(). Here Worker falls back to an
  SqsQueueClient configured exactly as before (AWS_REGION), now created on
  first use instead of at module load; the factories build the built-in
  adapters themselves (sqs/aws, netlify-background/netlify, qstash;
  eventbridge, mock, netlify) and only an unknown name goes through
  resolveProvider(). An AWS app needs no new package.
- SqsQueueProvider/SqsQueueClient live in core (lazy SDK) and are
  re-exported by provider-aws in a later commit.
- The barrel keeps exporting EventBridgeSchedulerAdapter (lazily) and
  determineProvider(); #545 removed both.

Left out (c), and why:
- Cryptor keyProvider requirement, QueuerUtil.setQueueClient, WebSocket
  messageSender injection, health-router KMS extraction, removing
  @aws-sdk/* from core and moving the EventBridge/Lambda/S3 adapters to
  provider-aws (023b098, 5949712, 9237e31, a8d08b4). Not needed by the
  providers, and breaking for AWS: at b4ca16d prisma.js still builds
  `new Cryptor({ shouldUseAws: true })` without a keyProvider, which
  throws, and the catch around it logs "Continuing without encryption" —
  KMS field-level encryption silently turned off.
- Lazy Mongo/DocumentDB requires in the repository factories (5ec2975,
  8db2e0d): the crash they fixed was `Cannot find module 'mongodb'`; in
  this stack those repositories need only `bson`, a core dependency, and
  require('@friggframework/core') loads no mongodb/mongoose (measured).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Groundwork for provider-netlify (#545, c40eca4 /
3dadab6), adapted. #545's Netlify integration-routes and webhooks
functions re-implemented the routing inline: they mounted webhook routes
for every integration (ignoring Definition.webhooks), skipped Tier 3
extension bindings and their route-conflict checks, and dropped the
webhook logging scope. Here the AWS modules expose the pure builders they
already used:

- integration-defined-routers: buildIntegrationRouters(classes) and
  createIntegrationDefinedRouter(classes?) — Definition.routes plus
  extension bindings, with the conflict checks.
- integration-webhook-routers: buildWebhookRouters(classes) and
  createWebhookRouter(classes?) — only integrations with webhooks
  enabled, same dispatch and logging scope.

The AWS `handlers` are built from the same entries at require time, as
before, so a route conflict still fails the cold start (#545 made them
lazy; this stack's tests pin the boot-time failure). Prettier reflowed
the two files; the logic is unchanged apart from the extraction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…h scheduler

Port from #545 (9513a42), category (b): the table
NetlifySchedulerAdapter persists one-time jobs in, read and written by
provider-netlify's ScheduledJobRepository and its scheduled-sync function.
Same model in both schemas as #545.

Adapted: #545 changed only the Prisma schemas, with no migration, so
`prisma migrate deploy` would never have created the table. This adds
20261017000000_add_scheduled_job with the SQL `prisma migrate diff`
generates, each statement IF NOT EXISTS so it is a no-op on a database
that already has the table from `prisma db push` under the #545 canary.

Left out: #545's engineType/binaryTargets edits (ae2019f, 676b219).
This stack already uses engineType "library" (frigg#580) with
["native", "rhel-openssl-3.0.x"], which is where #545 ended up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…ement API

Port of @friggframework/provider-netlify from #545
(188110e ... 3dadab6, 077502a, e7becd9, ae2019f, 60108f0,
2eed07a, 302ab9b), category (a). Ported faithfully: the plugin
interface (index.js), netlify.toml + env-template generator, the
function entry-point generator with the setAppDefinition preamble and
core requires rewritten through backend/node_modules, deploy/preflight/
teardown, validate, detect, netlify-db, ScheduledJobRepository,
invoke-function-adapter, scheduled-sync, and #545's 7 test suites.

Adapted so a Netlify function serves exactly what the AWS handlers do:
- Handlers run on core's createHandler and createApp (lib/
  create-netlify-handler, create-netlify-app-handler) instead of #545's
  copies. That keeps the invocation scope, structured logging, telemetry,
  the DB connect, and the Management API v2 error envelope with its
  version header; #545's copied createApp answered /api/v2 errors in the
  v1 shape. createHandler's AWS steps are env-gated no-ops on Netlify.
- auth.js serves core's routers/auth, which is the Management API router
  (v2 under /api/v2 with adopter-JWT / x-frigg / shared-secret auth, the
  entity registry, memberships, ownership, roles, credentials and the
  entity proxy with its scopes; v1 or its 410) plus app-level extension
  routes. The app definition comes from the preamble's
  setAppDefinition(), resolved by core's loadAppDefinition as on AWS.
- integration-routes.js and webhooks.js mount core's router builders
  (createIntegrationDefinedRouter / createWebhookRouter) instead of #545's
  inline copies: Tier 3 extension bindings and conflict checks included,
  webhooks only where Definition.webhooks is enabled.
- worker-background.js builds queue workers with the app's offered module
  definitions (entity registry), as integration-defined-workers does, and
  honours QUEUE_PROVIDER / appDefinition.queue.provider.
- admin.js: #545 served core's handlers/routers/admin (user and global-
  entity admin from the integration-router-v2 branch), which this core does
  not have. It now serves the app-level extension routes (e.g.
  db-credentials' /api/admin/oauth-credentials) behind the admin key and
  404s the rest. docs.js: #545's routers/docs does not exist either; it
  serves the Management API discovery router and redirects /api/docs to
  /api/meta/openapi/v2.json. Both keep their file names so an app with
  the old generated files committed gets working replacements.
- netlify.toml: the Management API redirects are generated from core's
  route registry (the source of the AWS API Gateway routes), so they
  include the v1 /api/entity paths #545 missed, drop v1 with
  managementApi.v1: false, add /api/meta -> health, keep a /api/v2/*
  fallback; plus a bare /webhooks redirect per integration. The
  /config/integration-settings and /api/credentials redirects are gone:
  neither route exists in this core.
- lib shims require provider-netlify through backend/node_modules too.
  With a bare specifier a pnpm workspace resolves another provider copy,
  whose own require('@friggframework/core') loads a second core that
  never saw setAppDefinition(); harmless in #545 (its handlers never
  touched core state), fatal once the handler connects the database.
- Generated functions require nothing but core and ../lib; the express
  and Boom routers moved into lib/routers.js.

New tests: generated-functions.runtime (writes a project, generates the
functions and lib shims, requires them by path and invokes them: v2 401
in the v2 envelope, v1 410 from the preset definition, admin key on
extension routes, /api/meta, /api/docs) and management-api-redirects.
Mutation-checked: dropping setAppDefinition from the preamble fails 5 of
6 runtime tests; replacing core's createApp with a plain express app
fails the v2-envelope test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Port of @friggframework/provider-aws from #545
(9237e31, 023b098, a8d08b4), category (a), adapted. In #545 this
package received the AWS adapters moved out of core (SQS, EventBridge,
Lambda invoker, S3 migration status, KMS key provider, API Gateway
sender, KMS health checks), and core required it for every AWS app.

This stack keeps those adapters, and the AWS SDK, in core (see the queue
commit for why), so provider-aws carries no copies: it exposes core's
SqsQueueProvider, SqsQueueClient, EventBridgeSchedulerAdapter,
LambdaInvoker and MigrationStatusRepositoryS3 under #545's plugin
interface (QueueProvider, SchedulerAdapter, invokeFunctionAdapter,
loadSecrets, name 'aws'), all as lazy getters. CryptorAdapter and
WebSocketAdapter are null because core has no such port here. It
depends on @aws-sdk/client-scheduler, which core's EventBridge adapter
needs and core does not declare. resolveProvider({ provider: 'aws' })
resolves it; nothing requires it for an AWS app to work.

Left out: the moved implementation files and their tests (they are the
core files, still tested in core), kms-encryption-key-provider,
api-gateway-message-sender and kms-health-check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…r plugin

Port from #545 (88cf983, c870571, 7b93757,
302ab9b), category (a)/(b): what lets a Netlify app build at all.
frigg-internal's backend build is `node infrastructure.js package`, so
createFriggInfrastructure() must route a non-AWS provider to its plugin.

Ported:
- createFriggInfrastructure(): provider !== 'aws' -> validate,
  generateConfig, function entry points, lib shims; returns an empty
  serverless definition.
- frigg build / deploy / start dispatch to the plugin; doctor, repair and
  generate-iam refuse a non-AWS app (AWS/CloudFormation only).
- frigg-cli/utils/provider-helper (loadProviderForCli,
  loadCliAppDefinition).

Adapted:
- One shared infrastructure/provider-build.js instead of #545's two
  copies. #545's CLI copy wrote netlify.toml and netlify/functions into
  the backend directory, where the generated '../../backend/...' requires
  do not resolve; both paths now write beside backend/, as the
  infrastructure path did. Validation errors write nothing.
- The provider check runs before the 60-second serverless-definition
  cache, which only ever holds AWS definitions.
- provider-helper also returns backendDir and isNonAwsProject(); a
  missing plugin package for a non-AWS app is an error in `frigg build`
  rather than a silent AWS build.
- `frigg start` for Netlify runs `netlify dev` from the project root
  (where netlify.toml is), not from the backend directory.

Left out: #545's provider-dispatch test (heavily mocked, it pinned the
backend-directory output); replaced by provider-dispatch tests on real
temp projects with the real provider-netlify, plus a
createFriggInfrastructure test. The validate-command extension sanitizer
(no validate-command in this stack). Emoji removals in build-command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
`npm install --package-lock-only`: links for provider-aws,
provider-netlify and extension-db-credentials (packages/providers/*,
packages/extensions/*), and provider-netlify's own serverless-http@3.
No other entry changed. (#545's lockfile commits regenerated the whole
file against its own base; not taken.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…tlify provider port

Brings in the fixed 05c: the deprecated module-definitions wrapper, the
DocumentDB ownership drains, the v1 POST /api/entity owner fix and admin
gate, and the membership-role fallback. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Follow-up to the extension commit, found by F6's entity-scopes suite after
the merge: ModuleFactory awaited Module.create() for every entity, so any
code or test double that provides Module without the new static factory
broke (3 entity-scopes tests answered 500). Module.create() is only
needed when definition.env is a function (the db-credentials bootstrap's
async loader); a static env now builds with `new Module(...)` exactly as
before. Test added for the async path; mutation-checked (forcing the
constructor path fails it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

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

@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 16, 2026 — with Claude

Copy link
Copy Markdown
Contributor Author

build (22.x) is red, but the failures are the existing @friggframework/devtools ones from the base branches, not this PR's.

No fix for the devtools failures exists yet, and they predate this stack. I'll port one into this PR once it lands.


Generated by Claude Code

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

Stale Bugbot comment from a previous run.

Comment thread packages/core/extensions/initialize-app.js
Comment thread packages/core/infrastructure/scheduler/scheduler-service-factory.js
Comment thread packages/core/infrastructure/scheduler/netlify-scheduler-adapter.js Outdated
worker-background and the /api/queue redirect ran any POSTed message:
Netlify does not authenticate a Background Function's caller and nothing
verified a signature, QStash's included.

- NetlifyBackgroundProvider signs each message (HMAC-SHA256 over
  `${timestamp}.${rawBody}` with FRIGG_QUEUE_SIGNING_SECRET, sent as
  x-frigg-queue-timestamp / x-frigg-queue-signature) and verifyEvent()
  checks it: constant-time compare, 5-minute window both ways.
- QStashQueueProvider.verifyEvent() checks the Upstash-Signature JWT with
  the current, then next signing key (HS256, iss Upstash, exp required,
  body claim = base64url SHA-256 of the raw body), per @upstash/qstash's
  Receiver; uses core's existing jose dependency.
- QueueProvider.verifyEvent() is new on the port and refuses by default;
  SQS overrides it (its events come from the Lambda event source).
- The worker verifies before createHandler runs, so a refused event is
  not parsed, connects no database and dispatches nothing: 401 internally
  and one WARN, frigg.netlify.queue.rejected. With the qstash provider it
  also accepts Frigg-signed messages, which scheduled-sync sends.
- Fail closed: with no secret every event is refused and send() throws;
  FRIGG_QUEUE_ALLOW_UNSIGNED=true opens it only outside production (an
  unset STAGE and NODE_ENV count as production).
- README for provider-netlify, env docs, netlify.toml note and env
  template mark FRIGG_QUEUE_SIGNING_SECRET as required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Bugbot (HIGH) was right: initializeApp / runExtensionBootstraps were
called only from tests, so the db-credentials bootstrap never swapped a
module's env for its DB loader and stored OAuth app credentials were
ignored.

- createHandler runs initializeApp() after connectPrisma, once per
  process (initializeApp caches its promise), for every handler that uses
  the database. AWS Lambdas and Netlify functions both go through it.
- Enabling the bootstrap alone would have broken OAuth: every use case
  built modules with `new Module()`, which spreads a function env as
  nothing, so the API client would get no client_id. All module use cases
  now use the async Module.create(), and a structural test fails on any
  `new Module(` under modules/use-cases.
- The db-credentials bootstrap read `moduleName` off the Definition.modules
  entry, which is `{ definition }`; it skipped every real module. It now
  reads entry.definition (bare definitions still accepted) and wraps a
  shared or re-bootstrapped definition only once.
- The extension's Prisma model is still not composed into the generated
  client (nothing calls composeSchemas, and the fragment is MongoDB-only).
  The bootstrap already falls back to the static env when the model is
  missing; /api/admin/oauth-credentials now answers 503
  EXTENSION_SCHEMA_NOT_COMPOSED instead of a TypeError 500. Composing at
  build time is left for a follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…ository

Bugbot (HIGH) was right: on Netlify determineProvider() picked EventBridge
whenever STAGE was not local and SCHEDULER_PROVIDER unset, and nothing
set it; createSchedulerCommands() built the service with no repository,
so the Netlify adapter fell back to memory. scheduleJob never reached the
ScheduledJob rows scheduled-sync polls.

- determineProvider() returns 'netlify' when NETLIFY=true (before the
  stage check); provider-netlify's generated functions also default
  SCHEDULER_PROVIDER to netlify, and the env template lists it.
- The factory gives the Netlify adapter a Prisma ScheduledJobRepository
  by default. The repository moved to core beside the adapter and the
  model (provider-netlify's lib file re-exports it).
- scheduleJob derived an SQS ARN from queueUrl, which fails on Netlify
  (and the {INTEGRATION}_QUEUE_URL env is AWS-only). An adapter may now
  resolve its own target: the Netlify adapter sends to
  /.netlify/functions/worker-background unless given a function path.
  SQS-backed schedulers still require queueUrl.
- Round-trip test: scheduleJob with no provider or repository stores a
  row, scheduled-sync dispatches it signed to the worker and deletes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Bugbot (MEDIUM) was right: processDueSchedules marked a job PROCESSING
with a plain upsert after findDue, so two overlapping cron ticks could
both dispatch it, and a function that timed out after the PROCESSING
write left the job stuck for good (findDue only returned PENDING).

- ScheduledJobRepository.claim(name, now) is a conditional updateMany
  (state PENDING, or PROCESSING with an expired lease) that stamps
  updatedAt; the caller acts only when count === 1.
- findDue also returns PROCESSING rows whose updatedAt is older than the
  lease (default 5 minutes, past the 60 s scheduled-function limit;
  NetlifySchedulerAdapter takes leaseMs). No schema change: updatedAt is
  the lease clock.
- processDueSchedules skips a job it did not win and reports `skipped`.
  A custom repository without claim() keeps the old behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
- core/modules/module.js:61 (BUG, "always true"): the null default on
  resolvedEnv means `resolvedEnv !== null ? resolvedEnv : definition.env`
  already behaved as `??`; it is now written that way. The hazard behind
  it is made explicit: a function env reaching `new Module()` (which would
  spread to no client_id) throws, pointing at Module.create.
- provider-netlify/lib/deploy.js:245 (VULNERABILITY, PATH): CLI commands
  are resolved to an absolute path by resolve-executable.js, which
  searches only absolute PATH directories and requires the executable bit.
  teardown runs execFileSync(<abs>, ['sites:delete', '--force']) with no
  shell, deploy spawns the absolute netlify (or npx) path, and the
  `which`/`where` shell-out is gone.
- Assertions that could not fail: provider-plugin-interface now loads
  index.js and checks the plugin interface; route-mounter drives HTTP
  requests and asserts the default and custom auth middleware run; the
  db-credentials bootstrap no-op test asserts it resolves and never
  queries.
- Queue providers: batchSend(entries = [], queueId) had a default before
  a required parameter; the default moved into the body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

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

Stale Bugbot comment from a previous run.

Comment thread packages/core/modules/module.js
Comment thread packages/core/queues/providers/sqs-queue-provider.js
…ules

CI at 219e1e7 lost jest from the root node_modules while devtools ran,
and every later workspace failed with `jest: not found`. The mechanism
is in devtools, not in the provider or core changes, and is present at
87e9875 too (reproduced in a fresh `npm ci` clone of each commit):

- infrastructure-composer.test.js and template-generation.test.js call
  composeServerlessDefinition() unmocked, which runs
  ensurePrismaLayerExists() with cwd packages/devtools. Once the tracked
  layers/prisma/.build-complete marker is gone (those same builds delete
  it on failure), every compose rebuilds the real Prisma layer: 61 builds
  in one baseline run, from two jest workers at once.
- One build cleans the layer directory while another's
  `npm install --omit=dev` is starting. npm finds no package.json in its
  cwd, walks up into the workspace and runs install --omit=dev there,
  pruning devDependencies (jest, jest-util) out of the root node_modules.
  Reproduced in a toy workspace: the devDependency disappears.
  At 87e9875 the race only happened to miss.

Fix:
- build-prisma-layer: runLayerInstall() refuses when the layer
  package.json is missing and pins npm with
  `--prefix <layer>/nodejs --workspaces=false` (verified in the toy
  workspace: the root is untouched, the layer installs locally).
- The two composer tests mock the layer manager; no test builds a real
  layer now (0 builds in the fixed run, tracked layer files intact).
- devtools jest.config.js adds a setupFiles guard
  (test/npm-install-guard.js) that makes any npm/yarn/pnpm install,
  ci or prune throw unless it targets a scratch dir (under os.tmpdir() and
  outside this repository). Its tests use --dry-run, so a broken guard
  still cannot touch node_modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Bugbot (MEDIUM) was right: SqsQueueProvider.verifyEvent() answers ok to
anything (its trust is the Lambda event source), and the worker's
verifier fell back to the configured provider after the Frigg HMAC check
failed. With QUEUE_PROVIDER or queue.provider set to sqs, an unsigned
POST of { Records: [...] } to worker-background was dispatched.

- createQueueEventVerifier() always checks the Frigg HMAC and consults the
  configured provider only when it declares verifiesHttpDelivery (the
  Netlify background and QStash providers do; SQS does not).
- The worker parses every delivery as the single JSON message an HTTP
  POST carries, not with the configured provider's parser.
- Tests: QUEUE_PROVIDER=sqs with an unsigned Records body is rejected and
  nothing dispatched; a Frigg-signed message still runs; a transport-
  trust provider is never consulted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…defined

Bugbot (MEDIUM) was right: Module.create passed resolvedEnv, the
constructor defaulted it to null and fell back with `??`, so an env
function that resolved to undefined (the db-credentials bootstrap on a
module with no static env and no DB row) re-read definition.env, found
the function and threw the TypeError added in 219e1e7. Auth, entity and
worker flows for such a module would fail.

"Resolved" is now an explicit envResolved flag that Module.create sets,
so undefined and null are handed over as the resolved value. new
Module() without the flag still uses the static env, and still refuses a
function env.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
@seanspeaks

Copy link
Copy Markdown
Contributor Author

ADR reconciliation needed after Stack A/B — drafted with Claude Code at Sean's request

Severity: blocker (security, plus a key collision with ADR-017 on next). This PR is the first code for ADR-028/029 (#644), so several ADRs need edits.

ADR text (please include the amendments for ADRs on next in this PR)

  • ADR-016 (next): specifies plugins: { provider: { kind } }. This PR implements ADR-028's appDefinition.provider: 'aws'|'netlify' with FRIGG_PROVIDER (schema enum updated). Amend 016 to point at 028's resolution chain.
  • ADR-017 (next): appDefinition.extensions is a keyed object { binding: { extension, config } } with contract { name, type, hooks, routes, capabilities }. This PR uses the same key for an array of { name, schema, encryption, routes: { path }, bootstrap }. Either amend 017 to this contract (Prisma fragments, encrypted fields and bootstrap become core-extension capabilities), or rename the key. Two shapes on one key cannot both validate (ADR-051).
  • ADR-028/029 (docs(adr): reconcile the register — 13 unmerged ADRs into one contiguous sequence #644): asked there. 028 now has an implementation. 029 should drop "breaking, explicit injection required": this PR keeps Cryptor/QueuerUtil/WebSocket coupled on purpose, and notes that feat: provider plugin architecture + Netlify adapter #545's Cryptor change silently disabled KMS.

Conflicts

Merge order: #663 → this PR. #644's 028/029 edits land with or before it. Shared scheduler files mean #694 and this PR must be sequenced.

… nbf

The Upstash-Signature check verified the HMAC, issuer, exp and body hash,
but compared `sub` only when a destinationUrl option was passed, which the
Netlify worker never does, and did not require `nbf`. A valid delivery
published to another endpoint of the same QStash account could therefore be
replayed against the worker.

verifyEvent now requires exp, nbf, sub and body, and compares sub with the
configured destinationUrl or, by default, the request's own URL (Netlify's
event.rawUrl). A request whose URL cannot be determined is refused.

Frigg-signed messages (FRIGG_QUEUE_SIGNING_SECRET, HMAC over timestamp and
raw body, constant-time compare, 5-minute window, fail closed) were already
enforced on worker-background and /api/queue; tests now also cover the
subject and nbf rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
"Deployed" was inferred only from AWS Lambda variables, so a Netlify
Function looked like a local run and a deployed STAGE=dev site could accept
unsigned queue messages with FRIGG_QUEUE_ALLOW_UNSIGNED=true.

core/providers/runtime-detection.js adds isDeployedRuntime(env): not under a
local emulator or test runner (IS_OFFLINE, IS_LOCAL, NETLIFY_DEV,
JEST_WORKER_ID), and either AWS Lambda's variables or the active provider's
detect() (appDefinition.provider or FRIGG_PROVIDER, resolved to its
@friggframework/provider-<name> package). isProductionEnvironment, which
gates the unsigned-queue opt-in, now treats any deployed function as
production whatever STAGE says.

provider-netlify: the generated function preamble sets
FRIGG_PROVIDER=netlify, the one runtime signal Frigg controls (Netlify
documents NETLIFY, SITE_ID and CONTEXT as build-time variables), and
detect(env) returns true for it or for NETLIFY.

PR #663's fail-closed encryption config is not on this branch; its
deployed check can call isDeployedRuntime() instead of the AWS-only one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…in core's route registry

The Netlify provider served three route families outside core's route
registry: GET /api/docs (a "docs" function), /api/admin/* for core extension
routes (an "admin" function) and /api/queue (worker-background). None had an
API Gateway route on AWS, and the admin and queue paths sat under the public
/api prefix although core admin lives under /admin (ADR-053 keeps docs at
/api/meta).

- route-registry: DOCS_ROUTES (GET /api/docs on the health function, a 302
  to /api/meta/openapi/v2.json, mounted beside the meta router);
  listCoreExtensionRoutes(extensions) (ANY <path> and <path>/{proxy+} on
  the auth function, which already mounts extension routes behind the admin
  API key); HTTP_QUEUE_ROUTES (POST /admin/queue on the queue worker, only
  for hosts that ask with httpQueue: true). getGatewayRoutes takes
  extensions and httpQueue; devtools passes the extensions, so AWS gets the
  extension and docs routes, and provider-netlify generates every redirect
  from it (queueWorker -> worker-background).
- A core extension's routes.path must be /admin/<segment>...; /admin/scripts,
  /admin/db-migrate and /admin/queue are reserved (extension loader and the
  app-definition JSON schema). db-credentials moves to
  /admin/oauth-credentials.
- provider-netlify drops its admin and docs functions (and lib/routers.js):
  auth and health serve those routes as they do on AWS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
scheduled-sync sent ONGOING_SYNC once per integration class with data: {},
so the worker never hydrated an installation. Both modes now exist, chosen
per integration in Definition.schedule.scope:

- 'class' (default, unchanged): one message per class, data: {}.
- 'installation': one message per ENABLED integration record of the class,
  data: { integrationId }, so the queue worker hydrates it. An optional
  schedule.isDue(integration, { now }) skips installations that are not due
  (only an explicit true dispatches); without it every installation goes on
  every tick.

The dispatch lives in a core use case, DispatchOngoingSyncs (integration
repository + queue provider injected); scheduled-sync builds it, creating
the integration repository only if a class reads records. A failure for one
class or installation is counted and the rest still go out; the response
reports syncsNotDue and syncErrors. resolveScheduleConfig validates the block
at load (ScheduleConfigError, SCHEDULE_CONFIG_INVALID) and the
integration-definition JSON schema describes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…eExtensions

This PR's array of app-level extensions ([{ name, schema, encryption,
routes: { path }, bootstrap }]) used appDefinition.extensions, the key
ADR-017 gives to its keyed core-extensions object on next. The array now
lives at appDefinition.coreExtensions: extension loader (error messages name
coreExtensions[i]), route registry option (getGatewayRoutes({
coreExtensions })), devtools, provider-netlify, the app-definition JSON
schema, the devtools typedef, db-credentials' usage docs, tests and docs.

An array at appDefinition.extensions now stops the app at load with a
message naming the new key, so the old spelling cannot silently drop an
extension; a keyed object there (ADR-017) is left alone.

Integration-level Definition.extensions (Tier 3) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Follow-up to the route registry commit: provider-netlify's admin and docs
functions, and the lib/routers.js shim they required, are gone, so the
generated project is checked for a lib shim that still exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

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

seanspeaks pushed a commit that referenced this pull request Oct 19, 2026
…hedule scope, coreExtensions) into named proxy bases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

Copy link
Copy Markdown
Contributor Author

Reply to the ADR-reconciliation review (comment 6276666982). Everything it raises is fixed in 00d79d89..ba9ded3f.

1. Worker and queue authentication (dbe6b863).

  • Frigg's own messages. worker-background and the queue endpoint already required Frigg's own HMAC signature:
    • FRIGG_QUEUE_SIGNING_SECRET, HMAC-SHA256 over ${ts}.${rawBody};
    • compared with timingSafeEqual, with a 5-minute replay window;
    • checked before parsing, before the database connects and before dispatch.
    • It fails closed: with no secret, send() throws and receive refuses. FRIGG_QUEUE_ALLOW_UNSIGNED is ignored in any deployed function.
  • QStash gaps, now closed. The QStash check only compared sub when a destinationUrl option was passed (the worker never passed one), and nbf was optional. Now:
    • exp, nbf, sub and the body hash are all required;
    • sub must equal the configured destination, or else the request URL; with neither, the delivery is refused;
    • issuer Upstash, HS256 with the current key and then the next key.
  • Tests. Each guard is covered by a test, and breaking any one of them fails a test (14 checked).

2. Provider-aware "deployed" detection (37fd0964).

3. Routes go through core's registry (1a6351ee). AWS gateway routes and the Netlify redirects are both generated from the registry now.

  • Docs: GET /api/docs is registered on the health function and redirects 302 to /api/meta/openapi/v2.json.
  • Core extension routes: moved to /admin/<name>, mounted behind the admin key (db-credentials is now /admin/oauth-credentials).
    • routes.path must match /admin/<segment>….
    • /admin/scripts, /admin/db-migrate and /admin/queue are reserved.
    • Both rules are enforced by the extension loader and the schema.
  • Queue endpoint: now POST /admin/queue (was /api/queue), generated only for hosts that ask for an HTTP queue (Netlify yes, AWS no).

4. Scheduler: both modes (4340a146). New Definition.schedule, validated at load (ScheduleConfigError) and in the integration-definition schema. The logic is in a core use case, DispatchOngoingSyncs.

5. Key collision with ADR-017 (0dfbaf55). This PR's array is renamed to appDefinition.coreExtensions, everywhere it appears.

  • An array left at appDefinition.extensions stops the app at load, with a message naming the new key.
  • ADR-017's keyed object there is left alone.
  • Integration Definition.extensions is unchanged.

What an app must now set

  • FRIGG_QUEUE_SIGNING_SECRET in every deploy context.
  • With QStash: QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY.
  • The QStash destination must be exactly https://<site>/admin/queue.

Not yet verified: whether Netlify's rawUrl shows the public URL or the function path for a rewritten request. If it shows the function path, set destinationUrl explicitly. Check on the first deploy.

Tests

  • Core: the same 14 suites / 61 tests fail before and after, the same tests by name. Passing went from 3734 to 3793.
  • Green: schemas, provider-netlify (138), db-credentials (17), provider-aws.
  • 55 mutations killed in total across the five changes.

Generated by Claude Code

@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 2 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 ba9ded3. Configure here.

Comment thread packages/core/providers/runtime-detection.js
Comment thread packages/schemas/schemas/app-definition.schema.json
…017's keyed extensions

isDeployedRuntime treated a named non-AWS provider whose package could not
be loaded (or had no detect()) as a local run, so a deployed function with
STAGE=dev and FRIGG_QUEUE_ALLOW_UNSIGNED=true would accept unsigned queue
messages. A named provider that cannot answer now counts as deployed; a
local run (NETLIFY_DEV, IS_OFFLINE, ...) still wins.

The app-definition schema had no extensions property and rejects unknown
keys, so ADR-017's keyed appDefinition.extensions object, which the loader
deliberately ignores, failed validation. It is now an object of
{ extension, config } bindings; an array there is still rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
seanspeaks pushed a commit that referenced this pull request Oct 19, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
@sonarqubecloud

Copy link
Copy Markdown

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.

2 participants