Skip to content

feature: Frigg 2.0 features - #394

Draft
d-klotz wants to merge 1395 commits into
mainfrom
next
Draft

d-klotz wants to merge 1395 commits into
mainfrom
next

Conversation

@d-klotz

@d-klotz d-klotz commented Jun 4, 2025 •

Copy link
Copy Markdown
Contributor

Version

Published prerelease version: v2.0.0-next.127

Changelog

🐛 Bug Fix

  • @friggframework/admin-scripts, @friggframework/core, @friggframework/devtools
    • fix(deps): bump stale security overrides and express/body-parser ranges #728 (@d-klotz)
  • @friggframework/admin-scripts, @friggframework/core, @friggframework/devtools, @friggframework/eslint-config, @friggframework/prettier-config, @friggframework/schemas, @friggframework/serverless-plugin, @friggframework/test, @friggframework/ui
    • fix(core): keep the HTTP status of client-safe errors (expired token → 401, not 500) #727 (@d-klotz)

Authors: 1

Comment thread packages/core/handlers/backend-utils.js Resolved
Comment thread packages/core/handlers/backend-utils.js Resolved
Comment thread packages/core/handlers/backend-utils.js Resolved
// Removes "Bearer " and trims
const token = authorizationHeader.split(' ')[1].trim();
// Load user for later middleware/routes to use
req.user = await User.newUser({ token });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

User.newUser is misleading, it does not create a new user, instead it retrieves a user based on the JWT token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

User.fromToken({ token }) or User.getByToken({ token }) would be a better fit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yup agreed. I was using existing methods though.

const appDefinition = backendJsFile.Definition;

const backend = createFriggBackend(appDefinition);
const loadRouterFromObject = (IntegrationClass, routerObject) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"Object" is too vague, I have no clue what can be in there or what that means

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we could call it loadIntegrationRoutes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also the routerObject is already inside of IntegrationClass isn't it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The overall method of loadIntegrationRoutes makes sense, or integration defined routes.

The load from object method was intended to load from a... route object? Dunno what to call it. The "method", "path", "event" object. I'm not even sure where I got that concept from except it's a common way to represent an http endpoint. Minus the event thing. That I want to be an easy way for a dev to reference when creating event handlers.


for (const routeDef of IntegrationClass.Definition.routes) {
if (typeof routeDef === 'function') {
router.use(basePath, routeDef(IntegrationClass));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't understand this.

You're looping through IntegrationClass.Definition.routes and from every routeDef you're passing the same integration as parameter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you have an example of a place where we declare routes as a function and need it's own integration class as parameter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why not stick to one type of route definition? Either function or object

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the rationale was that in some cases, you need to have some logic that runs outside of express route generation. https://github.com/lefthookhq/frigg-2.0-prototyping/blob/460ad99b85b5a53bac5a859de8b8d3780b85937d/backend/src/testRouter.js#L8

In other cases, relying on the normal instantiation is fine and for those you can reach for either the straight express route or the static object.

I wanted to provide a "quick and easy", "moderate complexity", "high complexity" set of options.

That said I'm not sure what I did was the way to do it.

Comment thread packages/core/handlers/backend-utils.js Outdated
router[method.toLowerCase()](path, async (req, res, next) => {
try {
const integration = new IntegrationClass({});
await integration.loadModules();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We already loadModules in the IntegrationBase constructor, we could registerEventHandlers in the constructor as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think I had an issue of not wanting to load event handlers yet because of some dynamic issues? Or something else. But, happy to not duplicate invoking!

await integration.loadModules();
await integration.registerEventHandlers();
const result = await integration.send(event, {req, res, next});
res.json(result);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do we really have to return the result here?
Developers will by instict call the http response when it's available to them. I was not aware that this existed until now and probably other developers won't either.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, the intent was to remove the need to think about express inside the handler function. Just, do what you need, then if you need to return something, return it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

But if someones sees a "res" object, one would not simply return and ignore "res".
BTW, one of the reasons I don't like ruby on rails is because it does too much magic and I don't know why and how 🫠

for (const [entityId, key] of Object.entries(
integrationRecord.entityReference
)) {
const moduleInstance =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Are the modules not already instantiated in IntegrationBase -> loadModules() when we do:

const instance = new integrationClass({
userId,
integrationId: params.integrationId,
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, looks like loadModules doesn't actualy loads modules but it simply instantiates the module api.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's a difference between "load module definitions" and "load module entities into the module instance"

integrationRecord.config.type
);

const instance = new integrationClass({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

rename to integrationInstance

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm for it

// for each entity, get the moduleinstance and load them according to their keys
// If it's the first entity, load the moduleinstance into primary as well
// If it's the second entity, load the moduleinstance into target as well
const moduleTypesAndKeys =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this comment also does not make sense anymore, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The first line does make sense. The second and third lines are for backwards compatibility, where we enforced a "primary" and "target" concept via naming conventions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think we need to maintain that though. Let things error and people correct the errors.

moduleClass &&
typeof moduleClass.definition.getName === 'function'
) {
const moduleType = moduleClass.definition.getName();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

moduleType also comes from the name?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, though we can debate that

const integrationClassIndex = this.integrationTypes.indexOf(type);
return this.integrationClasses[integrationClassIndex];
}
getModuleTypesAndKeys(integrationClass) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I dont get what this does

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this implementation is fully what I intended. But anywho, the goal is that we grab an integration definition, and from it we can determine which api modules are part of it, which allows us to get the required module entity instances in order to create a complete integration record (TODO allow for a module to be optional and not required on creation of an integration record, potentially make them required on a per event basis, ie we throw an error/force a user to assign a connection or create a new connection if they go to use a feature that is only available if they have a specific module instance added).

The direct use case is during the management inside the frontend experience. The ui should see "user wants to create a HubSpot integration. The HubSpot integration requires two modules. The user has one module connection (entity) available to use, but needs the other one. I'll run the auth flow for the other one and then ask them to confirm the use of that new connection."

The reason I say this implementation may not be what I intended is that we should allow multiple modules of the same type to be assigned different module names so you have something like "slackUser" and "slackApp" both pointing to the slack api module.

@seanspeaks seanspeaks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did my comments to your comments and a few added comments in there

Comment thread lerna.json
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "1.2.2",
"version": "2.0.0-next.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have no idea if this should be committed 😅

Comment thread packages/core/handlers/backend-utils.js Resolved
const appDefinition = backendJsFile.Definition;

const backend = createFriggBackend(appDefinition);
const loadRouterFromObject = (IntegrationClass, routerObject) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The overall method of loadIntegrationRoutes makes sense, or integration defined routes.

The load from object method was intended to load from a... route object? Dunno what to call it. The "method", "path", "event" object. I'm not even sure where I got that concept from except it's a common way to represent an http endpoint. Minus the event thing. That I want to be an easy way for a dev to reference when creating event handlers.

Comment thread packages/core/handlers/backend-utils.js Resolved
Comment thread packages/core/handlers/backend-utils.js Outdated
router[method.toLowerCase()](path, async (req, res, next) => {
try {
const integration = new IntegrationClass({});
await integration.loadModules();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think I had an issue of not wanting to load event handlers yet because of some dynamic issues? Or something else. But, happy to not duplicate invoking!


getIntegrationById: async function(id) {
return IntegrationModel.findById(id);
getIntegrationById: async function (id) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I really need to lock this concept into my brain. Git repo takes the entire definition of "repository" in my head.

const integration =
await integrationFactory.getInstanceFromIntegrationId({
integrationId: integrationRecord.id,
userId: getUserId(req),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we discussed this on our call. Likely just was throwing things that stuck, for context on why this is here.

@@ -349,9 +375,7 @@ function setEntityRoutes(router, factory, getUserId) {
throw Boom.forbidden('Credential does not belong to user');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think there were moments where this failed? Dunno what those moments were/are though.

Comment thread packages/core/module-plugin/auther.js Outdated
const {ModuleConstants} = require("./ModuleConstants");
const { ModuleConstants } = require('./ModuleConstants');

class Auther extends Delegate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All for it. At one point @MichaelRyanWebber and I were debating both naming and intent of the class (hi Michael! Not to rope you in but, you likely can either find the note somewhere or comment on the future improvements you/we had in mind).

Comment thread packages/core/module-plugin/auther.js Outdated
this.EntityModel = definition.Entity || this.getEntityModel();
}

static async getInstance(params) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Async construction.

It's a debate in nodejs world. Might be a resolved debate in node 20? But basically "if you need to await/promise something in order to instantiate a class, do you make it an async constructor, or do you create a static async instantiation method?" Aka the get

That's the root of this decision at any rate.

@github-advanced-security github-advanced-security AI 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.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@gitguardian

gitguardian Bot commented Nov 20, 2025 •

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 12 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
37582161 Triggered Generic High Entropy Secret b23b0ea packages/core/logs/fixtures/vectors.js View secret
37580179 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580180 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580181 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580182 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580183 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37582810 Triggered Bearer Token c1d8958 packages/core/logs/record.test.js View secret
37580184 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580185 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580186 Triggered JSON Web Token 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580187 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
35818272 Triggered Username Password 769f999 packages/core/modules/requester/oauth-2.test.js View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@sonarqubecloud

sonarqubecloud Bot commented May 5, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
281 Security Hotspots
6.6% Duplication on New Code (required ≤ 3%)
E Security Rating on New Code (required ≥ A)
E Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@github-advanced-security github-advanced-security AI 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.

SonarCloud found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@seanspeaks

Copy link
Copy Markdown
Contributor

@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
63 Security Hotspots
6.6% Duplication on New Code (required ≤ 3%)
E Reliability Rating on New Code (required ≥ A)
E Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@netlify

netlify Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for friggframework-org ready!

Name Link
🔨 Latest commit 6f37508
🔍 Latest deploy log https://app.netlify.com/projects/friggframework-org/deploys/6aedf0c29f66d31663c24db0
😎 Deploy Preview https://deploy-preview-394--friggframework-org.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

d-klotz and others added 12 commits August 12, 2026 10:58
…ter updates

UpdateProcessMetrics wrote twice per call: phase 1 applied counter
increments atomically via applyProcessUpdate (jsonb_set), then phase 2
persisted derived fields (duration, recordsPerSecond,
estimatedCompletion) by writing the WHOLE context/results blobs back
through the legacy update(). Under concurrent callers a stale phase-2
write replayed its phase-1 snapshot over the row and permanently
discarded increments other callers had landed in between.

Observed in production (HubSpot<->Clockwork initial sync, 88 concurrent
batch handlers): processedRecords ended exactly one 25-record chunk
short of totalFetched, so the completion gate (processed >= totalFetched)
never fired and the sync sat in PROCESSING_BATCHES forever. A simulation
against real Postgres lost 1025 of 2200 increments under an 88-way burst
with the old code, and 0 with this fix.

Phase 2 now writes only the three derived paths it owns through the same
atomic primitive. The derived values stay best-effort under concurrency,
same as before; they just can no longer destroy counters. All three
repository backends (postgres, mongo, documentdb) already support the
set op.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s-lost-update

fix(core): write derived process metrics atomically to stop lost counter updates
Responds to the review on PR #637 (changes requested):

- Cut from 1027 to 337 lines (~67%).
- Restructured around the review's four options. The decision is
  options 4 and 2 together: a reactive database check before refresh
  and on 401 as the mechanism, and proactive scheduled refresh via the
  admin scripts as the companion.
- Folded in both inline review findings: the adoption test keys on the
  refresh token (not the access token), and only definitive
  authorization rejections may invalidate a credential — transport
  failures (timeout, 429, 5xx) stay retryable.
- Addressed the database-propagation caveat with the measured 716ms
  window, bounded re-read backoff, and the readPreference=primary
  requirement.
- Compare-and-swap moved from load-bearing to deferred hardening. The
  serialization family stays deferred behind the named metric.
- The full prior analysis remains available at commit 42d1e6f.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scheduled-refresh companion no longer requires core changes. The
script hydrates each module and calls refreshAuth() serially; the
existing setTokens -> DLGT_TOKEN_UPDATE -> onTokenUpdate ->
upsertCredential chain persists the result with no new code. Expiry
tracking (the persist/hydrate work and the null-expires_in computation
fix) moves from prerequisite to optional later optimization, taken only
if the bounded waste of blind refreshes ever matters.

Option 4 is unchanged: the database lookup before any refresh, and the
re-read-and-adopt on 401/invalid_grant, remain the mechanism and ship
first. The sequence shrinks from three PRs to two.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng it

Implements ADR-031 option 4 (frigg PR #637), the cross-invocation half
of the refresh race. Separate Lambda invocations hydrate the same
credential, refresh from stale memory, and the loser gets invalid_grant
— then flags the shared credential invalid, which silently killed 95
records in a production app over four months.

refreshAuth() now asks its delegate for the stored credential (new
DLGT_CREDENTIAL_RELOAD, handled read-only by Module.reloadCredential)
and adopts it when the stored refresh_token differs from the in-memory
one: another invocation already refreshed, so adopting always beats
spending a rotation. The comparison is keyed on the refresh token,
never the access token — a provider can rotate one and return the
other unchanged (review finding on the ADR).

Escalation is now restricted to proof of death: a definitive
authorization rejection (invalid_grant / invalid_client) AND no newer
token appearing across a bounded re-read backoff (default 500/1000/
1500ms; the observed winner-write-to-loser-failure gap in production
was 716ms). Transport failures — timeouts, 429s, 5xx, unmarked 400s —
now rethrow as retryable instead of invalidating a healthy credential
(second review finding). Two counters expose the outcome:
frigg.auth.refresh_race_recovered and frigg.auth.refresh_race_lost.

Four existing oauth-2 tests encoded the old any-error-invalidates
contract and were updated to the new one; their original intents (no
secret leak to delegates, no retry after a failed refresh) are
preserved and still asserted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four defects found by the two-axis review, all verified against source
before fixing:

1. The credential reload was dead code. reloadCredential required a
   nested `data` key, but every findCredentialById adapter returns the
   decrypted token fields spread at the top level. The reload always
   returned null, so adoption never ran. The tests hid it by mocking
   the nested shape. Fixed to accept both shapes; tests now mock the
   real adapter shape and keep one nested-shape case.

2. Rejection detection failed in production. FetchError sanitizes the
   response body outside dev, so a real invalid_grant carried no marker
   and was classified as transport — a dead credential would never
   invalidate. The status code now decides: 400/401 from the token
   endpoint is definitive (RFC 6749 §5.2), 429/5xx is always transport
   even when the body text mentions invalid_grant, and the word-bounded
   marker fallback remains only for statusless SDK errors.

3. The transport rethrow could leak the refresh request body (which
   carries client_secret in dev-stage FetchError messages). Transport
   failures now rethrow a fresh error carrying only the status code.

4. The silent-ack companion was missing from this PR even though the
   ADR scopes it into PR A. New checkIntegrationRunnable in
   backend-utils: ERROR now rejects the message so SQS retries and
   DLQs; DISABLED and IN_DELETION keep the intentional silent ack;
   FRIGG_LEGACY_ERROR_ACK=true is the kill switch. One worker test
   encoded the old ERROR-silent-ack behavior — the incident amplifier —
   and now asserts the new contract, plus a kill-switch case.

Also: the recovered-race counter now fires only on a real recovery
(adoption after a rejection), not on pre-refresh adoption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ential-refresh

ADR-031: Concurrent OAuth Credential Refresh Across Lambda Invocations
…straction

Comments now carry only the non-obvious why; the narration that restated
the PR body is gone. The dense promise chain in _refreshAuthOnce() splits
into _runMarkedRefresh(), the ALS check gets a name (_isInsideRefreshFlow),
the 401 branch names its condition (tokenReplacedWhileInFlight), and the
refreshAuth() catch block delegates to _transportFailureError() and
_adoptNewerCredentialWithBackoff(). No behavior change; all 111 touched
tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The queue-worker change (ERROR throws instead of silently acking, with
the FRIGG_LEGACY_ERROR_ACK kill switch) is independent of the requester
refresh work and is the one default-on behavior change affecting every
app — the ADR-027 exception a maintainer must ratify on its own terms.
It will return as its own small PR; the implementation stays recoverable
in this branch's history (0b28b4a and earlier).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heck

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fresh.data fallback guarded a shape no adapter produces — all three
findCredentialById implementations spread the decrypted token fields at
the top level — so it is gone, along with its speculative test. The
result variable now says where the row comes from, and the doc states
what a null return means to the requester.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the Graphite findings: _adoptNewerCredential no longer bumps
_authGeneration (the slot in _refreshAuthOnce owns the bump, so one
adoption bumps exactly once — the reload test now asserts through the
slot and would fail on +2), and the access-token copy is a plain guard
instead of a ??-fallback. The keep-old-token semantics stay: rejecting
an adoption over a missing access_token would leave the recovery inert
for modules that do not persist one, and access and refresh tokens are
presented independently — a dead old access token costs one extra 401,
which then refreshes with the adopted refresh token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d-klotz and others added 29 commits October 30, 2026 12:21
…lnerability-e5a747

fix(deps): bump stale security overrides and express/body-parser ranges
…ture app-definition key

One registry maps each AWS architecture name (x86_64, arm64) to the
Prisma engine it runs. resolveLambdaArchitecture() returns the name,
the Prisma binary target and whether the app set the key, so an unset
key keeps today's template. An unknown value throws.

The schema key is an enum with no default: applyAppDefinitionDefaults
writes schema defaults, and a default here would add fields to every
app's template (contract rule 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ents

Both schemas add linux-arm64-openssl-3.0.x to binaryTargets, so the
clients published with core carry the engine an arm64 (Graviton)
Lambda loads next to the rhel one x86_64 uses.

A *.db.test.js suite checks each generated client for both engines, so
the db job (the one CI job that runs prisma:generate) fails before a
publish could ship a client without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hitectures from the app definition

Only an explicit `architecture` adds the two fields; an unset key keeps
the template as it was, apart from the layer description, which no
longer names the rhel engine because the engine now follows the
architecture. osls applies provider.architecture to every function,
so no function gets a field of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… architecture

buildPrismaLayer and ensurePrismaLayerExists take the Prisma binary
target, resolved from the app definition's architecture. The build
keeps only that target's engines: the darwin, debian, musl and windows
patterns stay, and every libquery_engine-*, query-engine-* and
schema-engine-* file whose name lacks the target goes.

Verification is per client: each generated client must hold the target
query engine and no other Linux engine. A missing engine names the
core version and the target, since it means the installed core was
generated before that target was added to its schemas.

The PRISMA_CLI_BINARY_TARGETS on the layer's npm install is gone. The
layer installs only @prisma/client, which has no engine postinstall,
so the variable never did anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and versions

The .build-complete marker is now JSON recording the Prisma binary
target, the generated clients, and the core and @prisma/client
versions the layer was built from. Every early return (marker present,
marker appearing during a concurrent build, final check after the
wait) reuses the layer only when those match the requested inputs;
anything else rebuilds. A legacy plain-text marker recorded none of
them, so it rebuilds once.

A concurrent build that finishes for other inputs no longer makes the
waiter sit out the 60 s timeout: it rebuilds as soon as the marker
appears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the deployed architecture

`prisma migrate` needs two engines in node_modules/@prisma/engines/:
schema-engine-<target> and libquery_engine-<target>.so.node. The
@prisma/engines postinstall puts only the deploy host's own pair there,
so the composer now runs that postinstall with
PRISMA_CLI_BINARY_TARGETS=<target> before packaging, whenever the
migration worker is built, and fails packaging if either engine is
still missing. The postinstall adds only missing targets, so the host's
engines stay.

The worker and router packages exclude every other engine: the host
platforms (darwin, debian, musl, windows) and the other Lambda
architecture's target, both read from the registry. The worker pins
PRISMA_SCHEMA_ENGINE_BINARY and PRISMA_QUERY_ENGINE_LIBRARY to the
shipped files, so the CLI neither downloads into the read-only
/var/task at cold start nor picks another file. prisma-runner spreads
process.env into the CLI, so core needs no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the deployed architecture

With usePrismaLambdaLayer=false every function ships the generated
clients, which carry an engine per binaryTarget. The base, integration
and admin-script package configs now exclude the other architectures'
engines and the host platforms', read from the registry, so a bundled
function holds one engine. This also drops the debian copy x86_64
bundles shipped until now. With the layer on, Prisma is excluded as a
whole, so those configs do not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Explains what an explicit architecture changes, what an unset key keeps,
and what to check before flipping an app to arm64: the core release,
CDN access on the deploy host, native addons, the mixed-architecture
window during the stack update, and the layer rebuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The foreign-engine package excludes were bare platform globs such as
**/*windows* and **/*darwin*, so they also dropped runtime dependencies
like is-windows. Each pattern now starts with a Prisma engine file prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iles fetched

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nces

Cryptor instances now share one module-level KMS data-key source that owns a
single KMSClient and a TTL/LRU cache keyed by the encrypted data key. The
cache holds in-flight promises, so concurrent decrypts of one key make a
single Decrypt call, and failed calls are evicted. Encrypt seeds the cache
with the key it generates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FieldEncryptionService.encryptUpsert reuses create's ciphertext for update
where the same field path holds the same value, so the upsert hook makes one
call and encryptFields keeps its original signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r test cases

The three function builders each resolved the architecture and picked the
engine excludes for a layer-off bundle, and each copied the same test cases.
bundledPrismaEngineExcludes() now holds that rule in the registry, and the
builder suites share one set of cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… parser

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…viton)

Records why the architecture key has no default, why one registry owns the
Prisma binary targets, and why the layer, the migration worker and layer-off
bundles each keep only the deployed target's engines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…viton)

Records why the architecture key has no default, why one registry owns the
Prisma binary targets, and why the layer, the migration worker and layer-off
bundles each keep only the deployed target's engines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADRs ship in their own PR against next; ADR-068 is proposed in #732.

This reverts commit 3f2b915.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gine fetch

With PRISMA_SCHEMA_ENGINE_BINARY or PRISMA_QUERY_ENGINE_LIBRARY set on the
deploy host, the @prisma/engines postinstall skips those engines instead of
downloading the target's, and packaging fails blaming the CDN. The README now
states the real version rule: the worker's CLI and its engines both come from
the app's own node_modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Correct the layer cache match set, the migration engine version rule, the
pre-change x86_64 worker condition and the Prisma 6 driver-adapter status;
list every template change; cite ADR-051's contract test; add the
fetch-at-layer-build alternative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gage

When Definition.telemetry.exporter is unset, honor OTEL_EXPORTER_OTLP_ENDPOINT
by resolving to { type: 'otlp' } (explicit exporter still wins). Stamp
frigg.integration_id / frigg.user_id / integration_type / frigg.version from
active baggage/ALS onto exported spans only — never metric labels (ADR-011).

Co-authored-by: Sean Matthews <sean.matthews@lefthook.co>
When Definition.telemetry.exporter is omitted, register OTEL_EXPORTER_OTLP_*
Lambda passthroughs if the deploy shell has a non-empty endpoint or the key
is SSM-offloaded. Explicit exporter.type still wins. Isolate OTEL endpoint
env in stage-default telemetry-config and environment-builder tests.

Co-authored-by: Sean Matthews <sean.matthews@lefthook.co>
…n-attrs-7af3

fix(telemetry): zero-code OTLP env activation + span attribution from baggage (D-SCHEM-3)
skipEsbuild function packages excluded node_modules/**/node_modules/**.
npm nests a package only when the root copy cannot satisfy its dependant,
so the exclusion made nested packages resolve the hoisted version, and
dropped a package entirely when its root copy is a dev dependency. Two
apps have crashed at Lambda init this way (undici under jsforce; qs's
side-channel), each time with the lambda.keepNestedNodeModules opt-in
available but unset.

Keep nested node_modules by default, still excluding nested Frigg, AWS
SDK and, with the layer, Prisma copies. lambda.keepNestedNodeModules:
false restores the old exclusion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
perf(core): cache KMS data keys in Cryptor; ship nested node_modules by default
…re-arm64

docs(adr): ADR-068 Lambda architecture selection (arm64 / Graviton)
feat(devtools): deploy Lambdas on arm64 (Graviton) via an architecture app-definition key
@sonarqubecloud

sonarqubecloud Bot commented Nov 6, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5.5% Duplication on New Code (required ≤ 3%)
E Reliability Rating on New Code (required ≥ A)
E Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants