Skip to content

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

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

seanspeaks wants to merge 44 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, now on appDefinition.coreExtensions (ADR-017 Amendment 1).

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, now an atomic lease-based claim.
  • 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. /admin/oauth-credentials returns 503 EXTENSION_SCHEMA_NOT_COMPOSED on this PR. The extension's Prisma model is composed into the client by feat(core,devtools): compose core extension Prisma models and migrations at build #707, which is stacked on this one.
  4. This core has no routers/admin or routers/docs. GET /api/docs is registered and redirects to /api/meta/openapi/v2.json.

Risks for the reviewer

These were open when the PR was first posted and are now fixed on this branch (see the 19 and 23 Oct comments):

  • Queue authentication. worker-background and POST /admin/queue (was /api/queue) require Frigg's HMAC signature (FRIGG_QUEUE_SIGNING_SECRET) or a verified QStash signature, checked before parsing or connecting to the database. They fail closed with no secret.
  • Queue targets. A Netlify queue id must be /.netlify/functions/<name>-background on an absolute http(s) site URL, so a stored job cannot reach another host or a non-queue function (20eda6e4).
  • Extension routes on AWS. They are registered in the route registry under /admin/<name>, which generates both the API Gateway routes and the Netlify redirects.
  • Deployed detection. Generated Netlify functions set FRIGG_PROVIDER=netlify, so they count as deployed for fail-closed encryption and queue auth. A hand-written function must set it too; the provider README says how (df38e8e8).

Still open:

  • Netlify bundles trace @aws-sdk (about 95 MB). Check function size against the 250 MB limit.
  • /api/v2/reports on Netlify. It is a 404, because admin-scripts don't run there.
  • Setup an app must now do: set FRIGG_QUEUE_SIGNING_SECRET in every deploy context, plus QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY with QStash.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

📦 Published PR as canary version: 2.0.0--canary.686.df38e8e.0

✨ Test out this PR locally via:

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

seanspeaks and others added 16 commits October 2, 2026 16:01
Field-level encryption used to be skipped whenever STAGE was dev, test
or local, and silently turned off (with a warning) when no key was
configured. `frigg deploy` defaults to --stage dev, so a plain deploy
stored credentials in plaintext.

The rule now keys off where the code runs, not the stage name, and
lives in one place (database/encryption/encryption-config.js) shared by
the Prisma client, DocumentDBEncryptionService, the integration-mapping
helpers and the encryption health check:

- Deployed (in Lambda, not serverless-offline / invoke local / Jest):
  KMS_KEY_ARN or AES_KEY_ID+AES_KEY encrypts on every stage, dev
  included. No key throws EncryptionConfigurationError at Prisma client
  creation, naming the fix, unless FRIGG_ENCRYPTION_DISABLED=true opts
  out explicitly (warned once per cold start). A configured key wins
  over the opt-out.
- Local (frigg start, tests, scripts): unchanged. dev/test/local skip
  encryption; other stages encrypt when a key is set.

The Prisma client also stops continuing without encryption when the
extension fails to initialise. /health/detailed reports a deployed stage
without a key as unhealthy and an opt-out as disabled; it no longer
reads BYPASS_ENCRYPTION_STAGE, which never switched encryption off.

BREAKING CHANGE: a deployed stage with no KMS_KEY_ARN and no
AES_KEY_ID/AES_KEY now refuses to start instead of writing plaintext,
including stages named dev, test or local. Set
encryption.fieldLevelEncryptionMethod: 'kms' (or provide AES keys), or
opt out with FRIGG_ENCRYPTION_DISABLED=true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`encryption: { fieldLevelEncryptionMethod: 'none' }` is the declarative
way to run a deployed stage without field-level encryption now that core
refuses to start without a key. The environment builder turns it into
FRIGG_ENCRYPTION_DISABLED=true on every function, and the SSM offload
blocklist keeps that variable in the Lambda environment, since Prisma
reads it at INIT before SSM values load.

The KMS wiring already ignores the stage name, so an app with
fieldLevelEncryptionMethod: 'kms' deployed with --stage dev gets
KMS_KEY_ARN and encrypts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the "dev, test and local stages bypass encryption" guidance in
CLAUDE.md, the core encryption README, the DocumentDB encryption design
note, the frigg and canary skills, the infrastructure README and the
website assistant prompt with the deployed/local rule, the explicit
opt-out, and the startup error and its fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loadModuleEncryptionSchemas required a path that does not exist, so module
credentialFields were never registered for encryption (the error was
swallowed). Fix the path, consume the entity registry so registry-only
modules count, filter fields core already encrypts, and add module fields
to the app's own custom schema instead of replacing it (other models
included).

Because that makes more fields encrypted, make reads tolerate plaintext:
_isEncrypted now requires the exact Cryptor shape (keyId:iv:ciphertext:
encryptedKey, base64/32-hex/hex/base64) instead of any string with three
colons, and a field that fails to decrypt is returned as stored with a
warning instead of failing the whole read, findMany included.

Existing rows stay plaintext until rewritten. See CHANGELOG (Unreleased).

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

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 Resolved
…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
…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
@seanspeaks

Copy link
Copy Markdown
Contributor Author

Decision (Sean, 22 Oct): Netlify is a 2.0.1 launch dependency. Left Hook's internal Frigg app runs on Netlify, and 2.0.1 ships only when both Left Hook products run on a published next prerelease (the dogfood gate). The Integrator's hosted Frigg runs on AWS. So both providers have to work at GA.

These move from "known risk" to must fix before merge:

  1. Authenticate worker-background and /api/queue.
  2. Verify QStash signatures on every delivery.
  3. Provider-aware "deployed" detection for fail-closed encryption: see the comment on feat(core)!: fail closed on field-level encryption in deployed stages #663. Deployed dev stages encrypt, and decryption fails closed (fix(core): encrypt module credential fields, and read plaintext safely #680).
  4. Route the extension, admin and docs routes through feat: Management API v2, /api/meta, v1 deprecation and the locked-down entity proxy (ADR-052, ADR-053) #671's registry (ADR-053). That gives AWS apps API Gateway routes to them and keeps new routes under /api/v2 or /admin.
  5. Resolve the extensions key collision with ADR-017.

frigg-internal should move off this PR's canary to a next prerelease once the stack merges.

🤖 Generated with Claude Code

…vel encryption

isDeployedRuntime read only AWS Lambda's variables, so a Netlify function
with STAGE=dev counted as a local run and skipped encryption. A non-aws
FRIGG_PROVIDER (set by provider-netlify's generated entry points) now marks a
deployed function, and NETLIFY_DEV marks `netlify dev` as local. Netlify's
SITE_ID, CONTEXT and NETLIFY are build-time variables and are not used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…pted in a deployed runtime

A value with Cryptor's keyId:iv:ciphertext:encryptedKey shape that fails to
decrypt (wrong, rotated or missing key) now throws FieldDecryptionError, with
the cause, when the process runs deployed (encryption-config's
isDeployedRuntime: AWS Lambda or a non-aws FRIGG_PROVIDER such as Netlify).
Returning it as stored would hand ciphertext to an API module as a token.
Legacy plaintext never takes this path, and local runs keep the warning.

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

encryption-config's isDeployedRuntime now delegates to
providers/runtime-detection, which asks the active provider's detect() and
fails closed when a named non-AWS provider cannot be loaded. The matrix from
#663 (with its Netlify rows) still passes unchanged against 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.

Copy link
Copy Markdown
Contributor Author

Status on the 22 Oct must-fix list. All five are done; four were already on this branch from 19 Oct (see the reply above).

  1. Worker and queue authentication: done (dbe6b863). Frigg-signed HMAC or a verified QStash signature, checked before parsing or connecting the database. The endpoint is now POST /admin/queue, not /api/queue.
  2. QStash signatures on every delivery: done (dbe6b863). exp, nbf, sub and the body hash are required; with no known destination the delivery is refused.
  3. Deployed detection and fail-closed encryption: done (328cbd08, e94de4d5).
  4. Routes through the registry: done (1a6351ee). /api/docs, /admin/<extension> and /admin/queue are registered.
  5. The extensions collision: done (0dfbaf55). The array is now coreExtensions, and an array left at extensions stops the app at load.

Tests:

  • Core: the same 61 tests fail before and after, matched by name; passing went from 3795 to 3890.
  • provider-netlify 138/138, schemas 237/237, and the devtools environment-builder and parameters suites 106/106.

Merge order is unchanged: #663, then #680, then this PR. #663 should take 8de669cb (merge claude/internal-mcp-09-deployed-runtime); the same commit is already in #680 and here, so the later merges see no conflict.


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 1 potential issue.

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 e94de4d. Configure here.

GitHub's ubuntu-latest is now Ubuntu 24.04, which has no libcrypto.so.1.1, so
mongodb-memory-server's default MongoDB 5.0.19 could not start and core's test
step failed before any test ran. Pin MongoDB 7.0.14 (published for Ubuntu
24.04; MONGOMS_VERSION still overrides it) and the wiredTiger storage engine,
since the library's default ephemeralForTest was removed in MongoDB 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
On AWS the environment builder turns the app definition's plaintext opt-out
into FRIGG_ENCRYPTION_DISABLED=true. Netlify functions had no such step, so
once a Netlify function counts as deployed, an app that opted out with no key
refused to start. The generated preamble now sets the variable from the app
definition (an explicit site value wins). Reported by Cursor Bugbot on #686.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
7.0.14 has no ubuntu2404 build (fastdl answers 403), so the in-memory server
still could not start on GitHub's runners. 8.0.4's ubuntu2404 build downloads
and starts with the wiredTiger engine; checked with an empty binary cache.

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

Copy link
Copy Markdown
Contributor Author

build (22.x) on 3e27a92b: core's tests now run in CI again. None of the remaining red is from this PR's changes.

Why core never ran: GitHub's ubuntu-latest moved to Ubuntu 24.04, which has no libcrypto.so.1.1. mongodb-memory-server's default MongoDB 5.0.19 therefore could not start, and core's test step died in global setup on every branch.

  • 588657b8 and 3e27a92b pin MongoDB 8.0.4 in packages/test/mongodb.js. 8.0.4 has an Ubuntu 24.04 build; 7.0.14 does not (403).
  • They also use the wiredTiger engine, because the library's default ephemeralForTest was removed in MongoDB 7.
  • MONGOMS_VERSION still overrides the version.

What is still red:

  • Core: 59 tests in 13 suites. They are the same suites that fail locally on this branch before the feat(core)!: fail closed on field-level encryption in deployed stages #663 merge (45cab161, 61 failures, matched by name): Cryptor and the encryption-schema registry, the DocumentDB/Mongo/Postgres decryption verifications, queuer-util, webhook-flow, check-database-state, integration-commands and find-integration-context. None of them touches what this PR adds. There is no fix for them yet, and they belong in their own PR, not here.
  • Devtools: 33 suites. The same pre-existing failures already noted above.
  • This PR's packages are green: provider-netlify 142/142, provider-aws 3/3, db-credentials 17/17, schemas 237/237, serverless-plugin 43/43, admin-scripts 278/278.

Known intermittent: prisma.encryption.test.js (from #663, merged in here) failed 2 of 4 full core runs locally on "installs the encryption extension when deployed with a KMS key". It passed in this CI run. Instrumented, the encryption config read deployed + KMS correctly; the cause is not found yet.

A re-run would not change this result, so I haven't requested one.


Generated by Claude Code

GitHub's ubuntu-latest is now Ubuntu 24.04, which has no libcrypto.so.1.1, so
mongodb-memory-server's default MongoDB 5.0.19 could not start and core's test
step failed before any test ran. Pin MongoDB 7.0.14 (published for Ubuntu
24.04; MONGOMS_VERSION still overrides it) and the wiredTiger storage engine,
since the library's default ephemeralForTest was removed in MongoDB 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
7.0.14 has no ubuntu2404 build (fastdl answers 403), so the in-memory server
still could not start on GitHub's runners. 8.0.4's ubuntu2404 build downloads
and starts with the wiredTiger engine; checked with an empty binary cache.

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

cursor Bot commented Oct 24, 2026

Copy link
Copy Markdown

Frigg 2.0.1 review (Stack A) — COMMENT only

Verdict (would): would approve

Launch checklist items look fixed on this branch: queue endpoint is /admin/queue (not /api/queue); HMAC/withQueueEventAuth before parse/DB; QStash JWT + sub + body hash verified (unsigned refused in prod); provider-aware encryption via isDeployedRuntime + FRIGG_PROVIDER=netlify; gateway routes from registry (getGatewayRoutes({ httpQueue: true, … })). No merge-blocking auth bypass found on queue/worker.

Blocking

None for queue auth / QStash / encryption / registry routing.

Should-fix

  1. Queue path SSRF — NetlifyBackgroundProvider.send does `${baseUrl}${queueId}`; toQueueResourceId accepts any string starting with /, including //evil.example/....
    Failure: Empty URL/baseUrl + queueResourceId='//evil.example/...' → protocol-relative fetch when a due job is dispatched. Allow-list DEFAULT_WORKER_PATH (reject //, schemes, ..).
  2. ADR-017 vs coreExtensions — ADR still documents appDefinition.extensions; loader implements coreExtensions[] and errors on an extensions array. Amend ADR-017 (keyed extensions reserved) so adopters don’t wire the wrong key.
  3. Deployed detection — Hand-rolled Netlify entrypoints without FRIGG_PROVIDER look “local” → unsigned queue / encryption bypass possible. Document preamble as required, or detect Netlify runtime signals without only the preamble.

Nits

  • PR body “Risks” still claim unauthenticated worker / unchecked QStash — outdated vs the branch; update so reviewers don’t miss remaining queueId hardening.
  • loadSecrets() env-only is fine if AES / FRIGG_QUEUE_SIGNING_SECRET are required in deploy docs/doctor.

No formal approve / request-changes on this review.

NetlifyBackgroundProvider.send() appended the queue id to the site URL as
given, and the Netlify scheduler kept any queue URL starting with '/'. With
an empty site URL a stored '//evil.example/x' became a protocol-relative
request, and any '/.netlify/functions/*' was reachable as a queue.

send() now accepts only '/.netlify/functions/<name>-background' on an
absolute http(s) site origin, and the scheduler maps anything else to the
worker function.

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

From the Stack A review on #686:

- ADR-017 still placed core extensions on appDefinition.extensions, while the
  loader reads the coreExtensions array and refuses an array at extensions.
  Amendment 1 records the split: coreExtensions holds the runtime contract
  (name, schema, encryption, admin routes, bootstrap); extensions stays
  reserved for ADR-017's keyed object.
- provider-netlify's README now says a hand-written function entry point must
  set FRIGG_PROVIDER=netlify (or the site sets it), since only the generated
  preamble does and without it the function counts as a local run.

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

Copy link
Copy Markdown
Contributor Author

On the Stack A review (comment 6379022522):

  1. Queue path SSRF. Fixed in 20eda6e4; details are on the two inline threads.
  2. ADR-017 vs coreExtensions. df38e8e8 adds docs/architecture-decisions/017-amendment-1-core-extensions-key.md, and ADR-017's header links to it.
    • coreExtensions holds the runtime contract: name, schema, encryption, /admin routes, bootstrap.
    • extensions stays reserved for ADR-017's keyed object, and an array there stops the app at load.
  3. Deployed detection for hand-written entry points. Documented in df38e8e8. The provider-netlify README now says a function written by hand must set FRIGG_PROVIDER=netlify, either in code before requiring Frigg or in the site's environment. Otherwise it counts as a local run.
    • I didn't add detection from other Netlify runtime signals. Netlify documents NETLIFY, SITE_ID and CONTEXT as build-time variables, so they are not reliable at runtime. NETLIFY is still honoured when present.
  4. Nit: stale "Risks". The PR body is updated. What is fixed is listed as fixed, and what remains (bundle size, /api/v2/reports on Netlify, the required queue secrets) is listed as open.

build (22.x) on 20eda6e4 fails only in devtools, with the same 33 pre-existing suites noted above. provider-netlify passes 142/142, including the scheduler round trip.


Generated by Claude Code

@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