Repository navigation
feat: Netlify provider on the v2 stack (port of #545) (F7) - #686
seanspeaks wants to merge 44 commits into
Conversation
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
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
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 |
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
…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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
|
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 These move from "known risk" to must fix before merge:
frigg-internal should move off this PR's canary to a 🤖 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
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
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).
Tests:
Merge order is unchanged: #663, then #680, then this PR. #663 should take Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ 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
|
Why core never ran: GitHub's
What is still red:
Known intermittent: 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
…o the main line
Frigg 2.0.1 review (Stack A) — COMMENT onlyVerdict (would): would approve Launch checklist items look fixed on this branch: queue endpoint is BlockingNone for queue auth / QStash / encryption / registry routing. Should-fix
Nits
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
|
On the Stack A review (comment 6379022522):
Generated by Claude Code |
|




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 innext. 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 owncreateHandlerandcreateApp, not #545's copies, which answered/api/v2errors in the v1 shape.authserves core'srouters/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-routesandwebhooksuse 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-backgroundpasses the app's module definitions to its workers, as the AWS workers do.netlify.tomlredirects are generated from core's route registry, which adds the v1/api/entity*and/api/metapaths feat: provider plugin architecture + Netlify adapter #545 missed.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 onappDefinition.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.definition.env.queueClientis injected, where feat: provider plugin architecture + Netlify adapter #545 threw.NetlifySchedulerAdapter. Ported with its PROCESSING guard, now an atomic lease-based claim.ScheduledJobmodel. Migration20261017000000_add_scheduled_jobuses onlyIF NOT EXISTS.require('@friggframework/core')used to load@aws-sdk/client-scheduler, which isn't a core dependency.package,build,deployandstart.doctor,repairandgenerate-iamrefuse non-AWS apps.Left out (c)
new Cryptor({shouldUseAws:true})throws without a keyProvider, and the error is caught as "Continuing without encryption".Test plan
createApp.prisma migrate diffshows nothing left over, and a re-run is a no-op./api/v2/*works.Known gaps (for frigg-internal; not Netlify-specific unless noted)
Optionsrequiresdisplay.detailsUrl, and frigg-internal's integrations don't set it./user/createand/user/loginreturn 500 whenuser.primary: 'organization'. It is the same core router on AWS too./admin/oauth-credentialsreturns 503EXTENSION_SCHEMA_NOT_COMPOSEDon 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.routers/adminorrouters/docs.GET /api/docsis 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):
worker-backgroundandPOST /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./.netlify/functions/<name>-backgroundon an absolute http(s) site URL, so a stored job cannot reach another host or a non-queue function (20eda6e4)./admin/<name>, which generates both the API Gateway routes and the Netlify redirects.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:
@aws-sdk(about 95 MB). Check function size against the 250 MB limit./api/v2/reportson Netlify. It is a 404, because admin-scripts don't run there.FRIGG_QUEUE_SIGNING_SECRETin every deploy context, plusQSTASH_CURRENT_SIGNING_KEYandQSTASH_NEXT_SIGNING_KEYwith 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