Skip to content

feature: Frigg 2.0 features - #394

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

d-klotz wants to merge 1260 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.117

Changelog

🚀 Enhancement

  • @friggframework/core
    • feat(core): add insert-only createMapping and IntegrationBase mapping helpers #674 (@d-klotz)

🐛 Bug Fix

  • @friggframework/admin-scripts, @friggframework/core, @friggframework/devtools, @friggframework/eslint-config, @friggframework/prettier-config, @friggframework/schemas, @friggframework/serverless-plugin, @friggframework/test, @friggframework/ui
    • fix(core): store patchConfig updatedAt as a BSON date in Mongo #673 (@d-klotz)

Authors: 1

Comment thread packages/core/handlers/app-handler-helpers.js Outdated
Comment thread packages/core/handlers/backend-utils.js Outdated
Comment thread packages/core/handlers/backend-utils.js Outdated
Comment thread packages/core/handlers/backend-utils.js Outdated
// 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.

Comment thread packages/core/handlers/app-handler-helpers.js Outdated
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 Outdated
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 Outdated
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.

Comment thread packages/devtools/infrastructure/serverless-template.js Fixed
Comment thread packages/core/handlers/routers/health.js Fixed
Comment thread packages/devtools/management-ui/server/api/cli.js Fixed

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

Comment thread packages/devtools/management-ui/server/services/npm-registry.js Fixed
Comment thread packages/devtools/management-ui/server/services/npm-registry.js Fixed
Comment thread packages/devtools/frigg-cli/build-command/index.js Fixed
Comment thread packages/devtools/frigg-cli/utils/process-manager.js Fixed
@gitguardian

gitguardian Bot commented Nov 20, 2025 •

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 14 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
23374074 Triggered Generic Password 5c1d197 .claude/skills/frigg-canary-test/assets/harness/docker-compose.yml View secret
35818272 Triggered Username Password 769f999 packages/core/modules/requester/oauth-2.test.js View secret
35818272 Triggered Username Password 7a06d54 packages/core/modules/requester/oauth-2.test.js View secret
37580184 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
37580186 Triggered JSON Web Token 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580183 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
37580185 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580187 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37580179 Triggered Generic High Entropy Secret 48b1ba4 packages/core/logs/fixtures/secrets.js View secret
37582161 Triggered Generic High Entropy Secret b23b0ea packages/core/logs/fixtures/vectors.js View secret
37582810 Triggered Bearer Token c1d8958 packages/core/logs/record.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.

Comment thread packages/devtools/management-ui/server/services/template-engine.js Fixed
Comment thread packages/core/handlers/app-handler-helpers.js Fixed
Comment thread packages/devtools/management-ui/server/services/npm-registry.js Fixed
Comment thread packages/devtools/management-ui/server/services/npm-registry.js Fixed
Comment thread packages/devtools/management-ui/src/pages/Environment.jsx Fixed
@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

d-klotz and others added 13 commits July 2, 2026 18:38
Adversarial review found that marking a row ERROR on ON_CREATE failure
interacted badly with reconcileAuthStatus's reuse-time healing (#613):
a retry with valid-but-unrelated-to-the-failure credentials would
silently flip ERROR -> ENABLED without setup ever having run, recreating
the exact 'ENABLED with no webhooks' zombie state born-PROCESSING exists
to eliminate. reconcileAuthStatus's heal only special-cases ERROR, so
leaving the row at its born PROCESSING value instead (recording an
'ON_CREATE Failed' message for visibility) is inert to that healing —
_reuseExisting needs no changes, so #613's reuse/race-backstop tests are
untouched and verified still green. Full retry-safe setup re-run still
needs remote webhook idempotency (P7); this closes the false-heal gap
without it.
The update command's result was discarded, so a rejected write (n:0,
writeErrors) fell through to the read-back, which returned the
unchanged document, and patchConfig reported success with the patch
keys silently missing — the exact silent-config-write-failure class
this feature exists to eliminate, on the one adapter that doesn't do
an atomic single-round-trip write. Also gives a correct 'not found'
error for a nonexistent id instead of the misleading 'consistency
issue' message.
Fully-synchronous read-then-write meant the 'concurrent disjoint
patches' regression test passed vacuously — no interleaving was
possible, so it would pass against any implementation, correct or not
(verified by mutation: reintroducing a stale-snapshot read-modify-write
did not fail it). The double now yields between existence-check and
merge and re-reads the record at write time rather than off a
pre-yield snapshot, mirroring the atomicity guarantee every real
adapter gets from the database (postgres row lock, mongo/documentdb
atomic single-document ops). Re-verified by mutation: reintroducing
the stale-snapshot bug now fails the test as expected.
Second-pass review found the ON_CREATE-failure fix (485962e) only
closed the demotion path inside CreateIntegration's own catch block.
Two other, pre-existing status writers can ALSO demote a still-PROCESSING
row to ERROR while ON_CREATE is running, reopening the exact false-heal
hole: reconcileAuthStatus's own '!authPassed -> ERROR' branch (fires
whenever _reuseExisting's testAuth check fails, unconditionally on
current status), and receiveNotification's CREDENTIAL_INVALIDATED
handler (fired by Module.markCredentialsInvalid on a 401 encountered
mid-setup — the single most common real ON_CREATE failure mode, e.g. a
webhook API call made before an OpenPhone key has finished propagating).
Both now skip the ERROR demotion when status is already PROCESSING,
consistent with the born-PROCESSING invariant: a row that never
completed creation stays honestly incomplete instead of becoming
ERROR and getting silently healed to ENABLED on the next successful
auth check.
…ndler throws

delete-integration-for-user.js deleted the integration row only after
integrationInstance.send('ON_DELETE') resolved. Any vendor-side webhook
deregistration failure inside onDelete (network error, expired token,
already-revoked webhook) aborted the entire uninstall after partial
cleanup, leaving the row alive with vendor webhooks in an unknown state
and no way for the customer to retry (the app still shows "installed").

onDelete failures are now caught, logged, and the deletion proceeds.
Cleanup is inherently best-effort; letting it block row deletion is
worse than leaving orphaned webhooks for out-of-band reconciliation.

DummyIntegration's send() now dispatches ON_DELETE to onDelete(), matching
its existing ON_UPDATE dispatch, so test doubles can exercise this path.

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

The catch block added to swallow onDelete failures read error.message
unconditionally. An onDelete that rejects with null/undefined (or any
non-Error value) made that line itself throw, escaping the catch and
skipping deleteIntegrationById — the exact failure mode the fix was
meant to prevent.

Found by @chatgpt-codex-connector's review on #615.

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

fix(core): don't abort integration deletion when ON_DELETE handler throws
Note that the merge is top-level only and replaces each key's value
wholesale (nested objects are overwritten as a block, not deep-merged),
and point deletions/null writes at UpdateIntegrationConfig.
Reverts the status-lifecycle half of this PR: born-PROCESSING schema
defaults, the ON_CREATE-failure status handling, the default onCreate
persistStatus change, and the PROCESSING guards in reconcileAuthStatus
and receiveNotification (the R746 note and R768 guard block).

Integrations are ENABLED by default again. Post-creation setup that
needs follow-up is expressed by the integration opting into
NEEDS_CONFIG rather than a framework-imposed PROCESSING birth state.

The atomic config-write half of the PR (patchConfig/updateConfig, the
three adapters, use-cases, shared patch validator, and command facade)
is kept unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrations are born IN_CREATION (schema default + documentdb adapter).
The default onCreate validates config via the now-fixed validateConfig —
which reads the react-jsonschema-form jsonSchema.required shape, records
a warning per missing field, and moves the integration to NEEDS_CONFIG —
and enables it only when nothing is missing. A throwing onCreate leaves
the row IN_CREATION: a visibly incomplete create rather than a healthy
looking one. Children can override onCreate.

Deletion marks IN_DELETION before teardown, in the delete use-case, so it
is set before any child onDelete teardown (which typically calls super at
the end). A teardown that throws before the row is deleted leaves a
visibly half-deleted integration. The queue worker discards events for
IN_DELETION integrations so incoming webhooks don't race teardown.

validateConfig previously threw "configOptions is not iterable" against
the real getConfigOptions() shape and was effectively dead code; it now
works and returns whether config is still needed.

Postgres migrations are split (enum ADD VALUE, then SET DEFAULT) because a
new enum value cannot be used in the same transaction that adds it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…status-lifecycle

# Conflicts:
#	packages/core/integrations/use-cases/delete-integration-for-user.js
Three gaps surfaced by adversarial review of ce91780:

- _findDuplicate treated an IN_DELETION row (mid-teardown, or a zombie left
  by a failed deleteIntegrationById) as a live duplicate, so a reinstall
  attempt got handed a row nothing ever moves out of IN_DELETION — a fake
  "success" the queue worker then discards forever. IN_DELETION rows are
  now excluded from duplicate matching; a fresh integration is created
  instead, and the zombie is left for out-of-band reconciliation, matching
  how a failed onDelete is already handled.

- _reuseExisting silently returned a still-IN_CREATION row (ON_CREATE never
  finished) through the same path as a healthy reused integration. Not
  auto-healed — safely retrying setup needs idempotent remote-resource
  creation first, a separate, larger change — but now logged loudly so
  it's diagnosable instead of silently masquerading as success.

- Default onUpdate called validateConfig() but never persisted the
  submitted config, and never had a path back to ENABLED once NEEDS_CONFIG
  — before this PR that call always threw ("not iterable"), so this bug
  was masked by a louder one. Now merges the submitted config in via
  patchConfig and moves NEEDS_CONFIG to ENABLED once nothing required is
  still missing. Other statuses are left alone; a config edit shouldn't
  silently un-pause a DISABLED integration or heal ERROR/IN_CREATION/
  IN_DELETION on its own.

Also: reordered both Prisma enum declarations so IN_CREATION/IN_DELETION
are declared last, matching the physical order Postgres actually produces
(ALTER TYPE ADD VALUE always appends) — avoids a schema/DB drift warning
on the next `prisma migrate dev`. Added rollback and deploy-ordering notes
to the two new migrations (unknown-enum-value read failures on rollback;
transient enum-missing errors if code deploys before migrations run).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
d-klotz and others added 29 commits September 24, 2026 17:18
queryMappings reads through $queryRawUnsafe and $runCommandRaw, which
bypass the Prisma encryption extension. When a custom encryption.schema
once encrypted a nested path such as mapping.apiSecret and the app later
opted it out, older rows still match (their mapping is an object), but
the path came back as ciphertext, while findMappingsByIntegration and the
DocumentDB adapter return it plain.

decryptQueriedMappings runs the returned rows through the extension's
FieldEncryptionService, reading every IntegrationMapping field the schema
lists, opted-out paths included. It is built once per process together
with the write-side check, from the same encryption config, so a stage
without keys still warns once. It hands the rows back untouched while
encryption is off, or when the schema lists nothing besides mapping,
which a queried row always holds as an object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a scenario to the parity suite for each adapter: a row written while
a custom schema encrypted mapping.apiSecret, then a row written after the
app opted that path out. queryMappings must return both plain and equal
to findMappingsByIntegration, match the path by value only on the row
written after the opt-out, and keep an omitted path out of the rows.

Each leg's setUp takes an optional cryptor: the MongoDB and PostgreSQL
legs extend their client with the encryption extension, the DocumentDB
leg injects it into its encryption service. Encryption is switched on
after setUp, because a generated Prisma client may load a .env file
when it is constructed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
queryMappings now decrypts on read like findMappingsByIntegration. A
nested mapping path written encrypted before its opt-out reads plain,
but conditions and ordering on it still see the ciphertext until the row
is written again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
How to use this.logger in integrations and API modules: which ids come
for free per entry point, messages and eventNames, levels, the error
rule, late ids with child(), what not to log, levels per app, queries,
tests with a memory sink, migrating console calls, and a full example.

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

serverless-offline copies only AWS_* shell variables into handlers, so
FRIGG_LOG_LEVEL=warn frigg start had no effect and IS_OFFLINE forced
DEBUG. In local mode the composer now puts the shell (or .env) value
into the function env, where it wins over logging.level, and emits
logging.level INFO too. Deploys never read the shell value.

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

- One table of record-contract keys (logs/contract-keys.js) for record.js
  and the denylist protection; one isError, deepFreeze and isPlainObject.
- The error field is serialized once; bindings are copied once; the
  scope is serialized once per invocation (WeakMap on the ALS store);
  the record is stringified once unless a field is dropped.
- Keys are normalized once per walk, with one suffix regex.
- Call sites pass only the extra request details under invocation, so
  record.js merges with a plain spread.
- Dead code removed: the scrubTokens segment mode, a duplicated sink
  retry block, a try/catch that could not catch.

A scoped info record with a child binding: ~37 µs → ~18 µs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A slow usage DB write could use up the remaining Lambda time and skip
the telemetry and sink flush. The deadline is now fixed at flush start
and the usage rollup runs next to the bounded flush. droppedKeys lists
only real conflicts, not a binding that repeats the scope value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EncryptionLogger maps TRACE to DEBUG and FATAL to ERROR. Every
denylist registration warns once per skipped record-contract key
(logs/denied-keys.js), also on the Prisma registry path. User update
errors carry the user id. The websocket router requires the real
repository factory path, so the Lambda loads.

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

The worker record for that failure now carries error.code, like the
process use cases already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Queue messages may carry integrationId and processId at the top level
or under data. Webhook and Definition.routes dispatches with an
:integrationId route param put that id in the logger scope only (never
the telemetry context, which drives usage writes). A hydrated id in the
store wins over a logger-only id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(core): add queryMappings to IntegrationMappingRepository
feat(core): tell queue handlers their last delivery attempt
# Conflicts:
#	packages/core/CLAUDE.md
#	packages/core/core/Worker.js
A call-site invocation merged into the scope invocation was never a
trim candidate, so many request header or query names could push
frigg.handler.invoked/failed past the cap. Each merged detail key is
now trimmed like any call-site field (listed as invocation.<key>), and
the scope's invocation summary stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(core): structured logging (ADR-048)
A FetchError with no response named only the cause code or name, for
example "GET <url> FetchError". Adopters that detect a Zoho 204 empty
body by "invalid json response body" in error.message stopped matching
after the node-fetch wrap in next.113, so a no-match search threw.

The message now appends the scrubbed cause message.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous change copied the scrubbed node-fetch message into the
FetchError message. For a body that is not JSON, that message quotes
the start of the response body, and scrubString removes only
credentials, so personal data could reach error.message.

The message now adds the fixed phrase "invalid json response body" for
an invalid-json cause and keeps the label alone for other causes.
ADR-048 rule 8 names the phrase.

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

fix(core): FetchError message drops the cause reason, so Zoho no-match searches throw
Conflicts in app-handler-helpers.js, module.js and
process-authorization-callback.js. Took next's ADR-048 structured logging
where both sides changed the same lines (flushDebugLog no longer exists in
those files), and kept the PR's [Frigg][OAuth] step logs.

Dropped the PR's per-request console.log middleware in createApp: it printed
the raw req.path (key-like segments leak) and failed the frigg.http
no-console tests on next.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…thenticate-user

Replace the console.* calls added by this PR (and the neighbouring ones in
process-authorization-callback.js) with getLogger records. Steps are DEBUG;
integration restores and entity repoints are INFO; a failed best-effort
restore is ERROR. Ids and key names go in fields, token and header values
never do. The testAuth-false path no longer logs before it throws, because
the boundary logs the error.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…l-issue

fix(logging): enhance debug logging for auth headers
$runCommandRaw serialises a JS Date as an ISO string, so the Integration
document ended up with a string updatedAt and every later Prisma read failed
with P2023. Use extended JSON ({ $date }) in patchIntegrationConfig and
ProcessRepositoryMongo.applyProcessUpdate, which had the same pattern.

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

fix(core): store patchConfig updatedAt as a BSON date in Mongo
… helpers

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

feat(core): add insert-only createMapping and IntegrationBase mapping helpers
…in queues offline

- schemas: app-definition accepts adminScripts, reports and admin
  (enableScheduling, includeBuiltinReports), which core and devtools already read
- devtools: AdminScriptBuilder registers AdminScriptQueue and ReportQueue under
  custom, so the serverless plugin creates them in LocalStack on frigg start
- serverless-plugin: offline, env vars holding { Ref: <created queue> } (provider
  and function scope) are set to the LocalStack URL, e.g. ADMIN_SCRIPT_QUEUE_URL

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

fix(admin-scripts): allow admin keys in the app schema and create admin queues offline
@sonarqubecloud

sonarqubecloud Bot commented Oct 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5.8% 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