Repository navigation
feat: Netlify provider on the v2 stack (port of #545) (F7) - #686
seanspeaks wants to merge 26 commits into
Conversation
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
|
ADR reconciliation needed after Stack A/B — drafted with Claude Code at Sean's request Severity: blocker (security, plus a key collision with ADR-017 on ADR text (please include the amendments for ADRs on
Conflicts
Merge order: #663 → this PR. #644's 028/029 edits land with or before it. Shared scheduler files mean #694 and this PR must be sequenced. |
… nbf The Upstash-Signature check verified the HMAC, issuer, exp and body hash, but compared `sub` only when a destinationUrl option was passed, which the Netlify worker never does, and did not require `nbf`. A valid delivery published to another endpoint of the same QStash account could therefore be replayed against the worker. verifyEvent now requires exp, nbf, sub and body, and compares sub with the configured destinationUrl or, by default, the request's own URL (Netlify's event.rawUrl). A request whose URL cannot be determined is refused. Frigg-signed messages (FRIGG_QUEUE_SIGNING_SECRET, HMAC over timestamp and raw body, constant-time compare, 5-minute window, fail closed) were already enforced on worker-background and /api/queue; tests now also cover the subject and nbf rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
"Deployed" was inferred only from AWS Lambda variables, so a Netlify Function looked like a local run and a deployed STAGE=dev site could accept unsigned queue messages with FRIGG_QUEUE_ALLOW_UNSIGNED=true. core/providers/runtime-detection.js adds isDeployedRuntime(env): not under a local emulator or test runner (IS_OFFLINE, IS_LOCAL, NETLIFY_DEV, JEST_WORKER_ID), and either AWS Lambda's variables or the active provider's detect() (appDefinition.provider or FRIGG_PROVIDER, resolved to its @friggframework/provider-<name> package). isProductionEnvironment, which gates the unsigned-queue opt-in, now treats any deployed function as production whatever STAGE says. provider-netlify: the generated function preamble sets FRIGG_PROVIDER=netlify, the one runtime signal Frigg controls (Netlify documents NETLIFY, SITE_ID and CONTEXT as build-time variables), and detect(env) returns true for it or for NETLIFY. PR #663's fail-closed encryption config is not on this branch; its deployed check can call isDeployedRuntime() instead of the AWS-only one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…in core's route registry
The Netlify provider served three route families outside core's route
registry: GET /api/docs (a "docs" function), /api/admin/* for core extension
routes (an "admin" function) and /api/queue (worker-background). None had an
API Gateway route on AWS, and the admin and queue paths sat under the public
/api prefix although core admin lives under /admin (ADR-053 keeps docs at
/api/meta).
- route-registry: DOCS_ROUTES (GET /api/docs on the health function, a 302
to /api/meta/openapi/v2.json, mounted beside the meta router);
listCoreExtensionRoutes(extensions) (ANY <path> and <path>/{proxy+} on
the auth function, which already mounts extension routes behind the admin
API key); HTTP_QUEUE_ROUTES (POST /admin/queue on the queue worker, only
for hosts that ask with httpQueue: true). getGatewayRoutes takes
extensions and httpQueue; devtools passes the extensions, so AWS gets the
extension and docs routes, and provider-netlify generates every redirect
from it (queueWorker -> worker-background).
- A core extension's routes.path must be /admin/<segment>...; /admin/scripts,
/admin/db-migrate and /admin/queue are reserved (extension loader and the
app-definition JSON schema). db-credentials moves to
/admin/oauth-credentials.
- provider-netlify drops its admin and docs functions (and lib/routers.js):
auth and health serve those routes as they do on AWS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
scheduled-sync sent ONGOING_SYNC once per integration class with data: {},
so the worker never hydrated an installation. Both modes now exist, chosen
per integration in Definition.schedule.scope:
- 'class' (default, unchanged): one message per class, data: {}.
- 'installation': one message per ENABLED integration record of the class,
data: { integrationId }, so the queue worker hydrates it. An optional
schedule.isDue(integration, { now }) skips installations that are not due
(only an explicit true dispatches); without it every installation goes on
every tick.
The dispatch lives in a core use case, DispatchOngoingSyncs (integration
repository + queue provider injected); scheduled-sync builds it, creating
the integration repository only if a class reads records. A failure for one
class or installation is counted and the rest still go out; the response
reports syncsNotDue and syncErrors. resolveScheduleConfig validates the block
at load (ScheduleConfigError, SCHEDULE_CONFIG_INVALID) and the
integration-definition JSON schema describes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…eExtensions
This PR's array of app-level extensions ([{ name, schema, encryption,
routes: { path }, bootstrap }]) used appDefinition.extensions, the key
ADR-017 gives to its keyed core-extensions object on next. The array now
lives at appDefinition.coreExtensions: extension loader (error messages name
coreExtensions[i]), route registry option (getGatewayRoutes({
coreExtensions })), devtools, provider-netlify, the app-definition JSON
schema, the devtools typedef, db-credentials' usage docs, tests and docs.
An array at appDefinition.extensions now stops the app at load with a
message naming the new key, so the old spelling cannot silently drop an
extension; a keyed object there (ADR-017) is left alone.
Integration-level Definition.extensions (Tier 3) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Follow-up to the route registry commit: provider-netlify's admin and docs functions, and the lib/routers.js shim they required, are gone, so the generated project is checked for a lib shim that still exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
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.
…hedule scope, coreExtensions) into named proxy bases Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
|
Reply to the ADR-reconciliation review (comment 6276666982). Everything it raises is fixed in 1. Worker and queue authentication (
2. Provider-aware "deployed" detection (
3. Routes go through core's registry (
4. Scheduler: both modes (
5. Key collision with ADR-017 (
What an app must now set
Not yet verified: whether Netlify's Tests
Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ba9ded3. Configure here.
…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
|




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.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.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./api/admin/oauth-credentialsreturns 500, the same as at b4ca16d. The extension's Prisma model is never composed into the client.routers/adminorrouters/docs./api/docsnow redirects to/api/meta/openapi/v2.json.Risks for the reviewer
worker-backgroundand/api/queuetake requests without authentication, and QStash events aren't signature-checked. Both are as they were in feat: provider plugin architecture + Netlify adapter #545, so frigg-internal is exposed to this today. This needs fixing before it handles per-person credentials.@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.🤖 Generated with Claude Code
https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Generated by Claude Code
Note
High Risk
Large cross-cutting platform port touching auth-adjacent queue workers, extension admin routes, and scheduling; PR notes unauthenticated Netlify workers and unsigned queue paths as existing exposure.
Overview
Ports the Netlify provider plugin stack (#545) onto the current core: new workspace packages
@friggframework/provider-netlify,provider-aws, andextension-db-credentials, with core changes so AWS deployments keep prior behavior.App-level extensions (
appDefinition.coreExtensions): validation/loading, Prisma schema composition, merged encryption fields,/admin/*routes behind the admin API key, and one-shot bootstraps after DB connect viainitializeApp()increateHandler.Module.createresolves asyncdefinition.env(e.g. DB-backed OAuth app credentials).Multi-host runtime:
setAppDefinition()for Netlify’s function layout; injectable queue clients onWorkerwith lazy SQS; queue provider exports and schedulernetlifyadapter withScheduledJobpersistence and poll-and-dispatch;scheduleJobno longer requiresqueueUrlwhen the scheduler supplies its own target. Integration and webhook routers are refactored into reusable builders for single-function hosts; auth router builds lazily and mounts extension routes.Definition.scheduleis validated at load and drivesDispatchOngoingSyncsfor cron ticks. Discovery addsGET /api/docs→ OpenAPI v2.Monorepo workspaces and docs updated; scheduler/EventBridge adapters load lazily so
require('@friggframework/core')does not pull AWS SDK into pruned Netlify bundles.Reviewed by Cursor Bugbot for commit 45cab16. Bugbot is set up for automated code reviews on this repo. Configure here.
📦 Published PR as canary version:
2.0.0--canary.686.45cab16.0✨ Test out this PR locally via:
npm install @friggframework/admin-scripts@2.0.0--canary.686.45cab16.0 npm install @friggframework/core@2.0.0--canary.686.45cab16.0 npm install @friggframework/devtools@2.0.0--canary.686.45cab16.0 npm install @friggframework/eslint-config@2.0.0--canary.686.45cab16.0 npm install @friggframework/extension-db-credentials@2.0.0--canary.686.45cab16.0 npm install @friggframework/prettier-config@2.0.0--canary.686.45cab16.0 npm install @friggframework/provider-aws@2.0.0--canary.686.45cab16.0 npm install @friggframework/provider-netlify@2.0.0--canary.686.45cab16.0 npm install @friggframework/schemas@2.0.0--canary.686.45cab16.0 npm install @friggframework/serverless-plugin@2.0.0--canary.686.45cab16.0 npm install @friggframework/test@2.0.0--canary.686.45cab16.0 npm install @friggframework/ui@2.0.0--canary.686.45cab16.0 # or yarn add @friggframework/admin-scripts@2.0.0--canary.686.45cab16.0 yarn add @friggframework/core@2.0.0--canary.686.45cab16.0 yarn add @friggframework/devtools@2.0.0--canary.686.45cab16.0 yarn add @friggframework/eslint-config@2.0.0--canary.686.45cab16.0 yarn add @friggframework/extension-db-credentials@2.0.0--canary.686.45cab16.0 yarn add @friggframework/prettier-config@2.0.0--canary.686.45cab16.0 yarn add @friggframework/provider-aws@2.0.0--canary.686.45cab16.0 yarn add @friggframework/provider-netlify@2.0.0--canary.686.45cab16.0 yarn add @friggframework/schemas@2.0.0--canary.686.45cab16.0 yarn add @friggframework/serverless-plugin@2.0.0--canary.686.45cab16.0 yarn add @friggframework/test@2.0.0--canary.686.45cab16.0 yarn add @friggframework/ui@2.0.0--canary.686.45cab16.0