Repository navigation
feat: Management API v2, /api/meta, v1 deprecation and the locked-down entity proxy (ADR-052, ADR-053) - #671
seanspeaks wants to merge 13 commits into
Conversation
… router
The auth Lambda serves core's integration router, which registers
POST /api/entity, GET /api/entity/options/:credentialId and the
/api/entities/:entityId family (get, test-auth, options, options/refresh).
The generated HTTP API only declared /api/integrations,
/api/integrations/{proxy+} and /api/authorize, and HTTP APIs have no
implicit catch-all, so every entity route 404'd at the gateway in a
deployed app (and under serverless-offline) before reaching Express.
Add /api/entity, /api/entity/{proxy+}, /api/entities and
/api/entities/{proxy+} (ANY) to the auth function, and add a test that
reads the routes off a live createIntegrationRouter(), composes a full
app definition, and asserts each one selects an API Gateway route on the
auth function, so a route added to core later fails until the gateway
is updated too. It also pins the neighbouring report, admin-script,
webhook, extension and integration-defined routes to their own
functions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds packages/core/management-api with the single route registry that the
Express routers, the OpenAPI documents and the devtools API Gateway routes
will all read:
- V1_ROUTES mirrors the frozen unprefixed routes of createIntegrationRouter.
- V2_ROUTES declares every /api/v2 route with an operationId, tag,
capability and stability (the entity proxy is beta and only listed when
the app enables it, ADR-052).
- META_ROUTES are version-neutral and served by the DB-free health Lambda.
- The reports segment under /api/v2 is reserved for ADR-010's report
router and never assigned.
- getGatewayRoutes() derives per-resource HTTP API routes and never emits
ANY /api/v2/{proxy+}.
GET /api/meta answers without auth or database access: the supported
majors with status and OpenAPI links, the capability flags derived from
the mounted routes, and the core version only for callers with the admin
API key. It is mounted on the health router ahead of the health key check.
resolveManagementApiConfig() reads the new managementApi app-definition
key (v1 switch, proxy enable, ADR-052 limits and per-module overrides).
loadAppDefinition() returns it and app-definition.schema.json accepts it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntApi.v1 switch
The auth Lambda now serves createManagementApiRouter(): the v2 router under
/api/v2 and the frozen v1 router at the unprefixed paths (ADR-053 §6).
- Every v1 response carries `Deprecation: @<epoch>` (RFC 9745) and
`Link: <migration guide>; rel="deprecation"`. Bodies and statuses are
unchanged.
- Each v1 call writes one frigg.api.deprecated_route record (route template,
method, Frigg-Client header; never the concrete path) and increments the
frigg.api.v1.requests counter.
- managementApi.v1: false never builds the v1 handlers and answers the v1
paths with 410 API_VERSION_DISABLED plus the migration link. The OAuth
redirect route is unversioned and keeps working either way.
- Unknown /api/v2 paths answer a v2 404, and every /api/v2 response has
Frigg-API-Version: 2.
The HTTP error boundary no longer turns an exposed client error into a 500:
a body-parser 400/413 stays a 400/413. Errors that carry a status without
marking it safe (an upstream FetchError's statusCode) are still 500s. On
/api/v2 and /api/meta the boundary answers { error: { code, message,
details? } }; a 5xx hides its message unless the error marks it safe. v1
keeps its { error: message } body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
createManagementApiV2Router() mounts the registry's v2 routes in registry
order and runs the handler for each operationId after AuthenticateUser
(bearer token, x-frigg headers or adopter JWT). Every v2 route
authenticates, integration options included. Handlers only call use cases;
dependencies are built in v2/dependencies.js or injected for tests.
Integrations (/api/v2/integrations): list returns { integrations } only,
options, create, get, update, delete (204), config options and refresh,
user actions, action options and refresh, run action, test-auth. New use
cases in integrations/use-cases/integration-operations-for-user.js put an
ownership check (missing or foreign = 404 INTEGRATION_NOT_FOUND) in front of
the existing ones and pass the record owner on. Creating an integration now
checks that the caller owns every entity and that config.type is a known
integration. Running an action only accepts action ids the integration
lists as user actions, so callers cannot fire internal events.
Entities (/api/v2/entities): list, get, delete (409 ENTITY_IN_USE while
integrations use it), test-auth, options and options refresh, and entity
types: /types, /types/:entityType, /types/:entityType/requirements. The
entity DTO carries the credential id and auth state, never credential
fields. GetEntityModuleForUser is the one ownership check (User.ownsUserId,
so organisation users work) and builds the module from the persisted
entity. The v1 entity use cases call Module.getEntityModelFromDefinition,
which does not exist, so v2 does not reuse them.
Entity types describe a module from its definition only (display name,
auth type, multi-step step count). GetAuthorizationRequirements returns
{ type, data, step, totalSteps, isMultiStep } for single-step and
multi-step modules (getAuthStepCount / getAuthRequirementsForStep).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/v2/credentials, GET /api/v2/credentials/:credentialId and DELETE /api/v2/credentials/:credentialId. - Repositories gain findCredentialsByUserId() for PostgreSQL, MongoDB and DocumentDB (decrypting each document), newest first. Stored data fields cannot override the id, userId or auth state of the record. - A credential's type and the entities using it come from the caller's entities. Missing or foreign credentials are 404 CREDENTIAL_NOT_FOUND. - Responses mask every provider field: strings keep at most their last four characters (only when 16+ long), objects are hidden whole, numbers and booleans (expiry times, flags) pass through. Field names stay visible. - Deleting a credential first unsets it on the entities that used it, so they show as needing re-authorization instead of vanishing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…agement API v2)
GET/POST /api/v2/authorize and GET/POST
/api/v2/credentials/:credentialId/reauthorize.
- AuthorizationSession domain object: unguessable id, bound to the user,
the entity type and (for re-authorization) the credential; expires after
AUTH_SESSION_EXPIRY_MINUTES (default 15). Steps advance one at a time;
step 1 may restart a flow.
- Repositories: one Prisma adapter for PostgreSQL and MongoDB, a raw-command
adapter for DocumentDB, and a factory on DB_TYPE. Lookups never return an
expired session; expired rows are removed opportunistically when a new
session starts.
- Prisma model AuthorizationSession in both schemas and the PostgreSQL
migration 20261003000000_add_authorization_session.
- stepData is encrypted at rest: AuthorizationSession.stepData joins the
core encryption schema (Prisma extension; DocumentDBEncryptionService for
DocumentDB).
- Single-step modules complete at once through ProcessAuthorizationCallback
(the v1 authorize logic). Multi-step modules use the documented
getAuthStepCount / getAuthRequirementsForStep / processAuthorizationStep
contract; the final authData goes through the same callback.
- POST answers { status: 'complete', entity, credential } (credential
masked) or { status: 'pending', step, totalSteps, sessionId, requirements,
message? }. A session belonging to someone else, another entity type or
another credential is 404 AUTHORIZATION_SESSION_NOT_FOUND; a skipped step
is 409 STEP_OUT_OF_ORDER; step 2+ without a session is 400
SESSION_REQUIRED. Module step errors are 400 AUTHORIZATION_FAILED with the
module's message; provider HTTP failures are 502 UPSTREAM_ERROR.
- Re-authorizing a credential runs the flow for its entity type and reports
previousCredentialId when the provider account changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/proxy (ADR-052) Off by default: the route is mounted, and the entityProxy capability advertised, only with managementApi.proxy.enable. It is marked beta. Requester._proxyRequest is the one generic, path-checked entry point (requester/proxy-request.js). It runs through the requester's own _request, so auth headers, single-flight token refresh, credential invalidation, the per-attempt timeout, telemetry and log redaction apply unchanged, for OAuth2, API-key and basic-auth modules alike. It adds: - the URL is always the module base URL (declared proxy.baseUrl, else the Api's baseUrl/baseURL built from the persisted credential) plus a path; - path rules: single leading /, no //, \, ?, #, dot segments, encoded separators, double encoding, control or whitespace characters; after the join the URL must be https, same origin, and under the base path; - no backoff retries; upstream errors come back instead of throwing; - manual redirects: same origin and under the base path, at most three hops, each re-checked against the allow-list; anything else is returned with its Location removed; - one deadline for the whole call and a response size cap. _rawRequest gains three opt-in options for this (timeoutMs, noRetry, returnErrorResponses); existing callers are unaffected. ExecuteEntityProxyRequest enforces the rest: the caller must own the entity (404 otherwise); the module's proxy.allow list is deny-by-default (403 PROXY_NOT_ALLOWED), a module cannot grant itself a wildcard, and the app definition may replace a module's list (managementApi.proxy.modules); the entity needs a usable credential (401 INVALID_CREDENTIALS); request headers are an allow-list and credential, Host and forwarding headers are refused; response credential and hop-by-hop headers are stripped; request and response size limits (413, 502 RESPONSE_TOO_LARGE); binary bodies only when the module allows them (415 otherwise). Upstream 4xx are relayed with their status, 429 keeps Retry-After, 5xx become 502 UPSTREAM_ERROR, timeouts 504. Every call writes one frigg.api.proxy.request audit record (no path, query, headers or bodies) and counts frigg.api.proxy.requests by module, method and outcome. api-proxy.schema.json gains the ADR-052 error codes, the path and header rules and relayed 4xx; api-module-definition.schema.json gains proxy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t test GET /api/meta/openapi/v1.json and /api/meta/openapi/v2.json serve OpenAPI 3.0.3 documents built from the route registry (ADR-053 §5): paths and methods come from the registry, per-operation docs are keyed by operationId, path parameters are derived from the Express path, and x-frigg-stability marks the beta entity proxy (documented only when the app enables it). v1 lists every frozen route as deprecated with its v2 replacement. info.version is the core version; x-frigg-api-version is the major. Other /api/meta paths answer a v2-shaped 404. Components are reconciled with packages/schemas; a parity test checks that required fields agree both ways. The schemas move their descriptions to the /api/v2 paths (ADR-053's ADR-006 amendment), mark the routes v2 does not serve, add authorizeComplete / authorizePending / authorizeV2Response, and accept module-defined step types (email, otp) in requirements. contract.test.js asserts registry == router == OpenAPI: every v2 operationId has a handler, the v2 router mounts exactly the registry routes in order (with and without the proxy), the frozen v1 router serves exactly the registered v1 routes, the meta router serves the meta routes, operationIds are unique and every $ref resolves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ute registry
The auth function's HTTP API events now come from core's
getGatewayRoutes() (ADR-053 §5), the same registry that mounts the Express
routers and builds the OpenAPI documents:
- v1 routes as before (minus the exact ANY /api/entities route, which no
router serves), dropped when managementApi.v1 is false; the OAuth
redirect then gets its own exact route;
- v2 per resource: /api/v2/{integrations,entities,credentials} and their
{proxy+}, and /api/v2/authorize. Never ANY /api/v2/{proxy+}, which would
catch methods /api/v2/reports does not declare and send them to the
wrong Lambda;
- GET /api/meta and /api/meta/{proxy+} on the DB-free health function.
The gateway route test now reads the live auth and health routers
(nested routers included, entity proxy enabled) and checks that every v1,
v2 and meta route reaches its function, that /api/v2/reports still reaches
the report router, and that no /api/v2 catch-all exists.
Builds on #667 (fix/v1-entity-gateway-routes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…client
new API(baseUrl, jwt, { apiVersion: 'v1' | 'v2' }); the default stays 'v1'
in 2.0.1.
- In v2 mode the client reads GET /api/meta once before its first /api/v2
call and throws FriggApiVersionError ("needs @friggframework/core >=
2.0.1; this server serves: v1") when the backend does not serve v2.
- The methods the bundled components call keep their v1 return shapes in
v2 mode: listIntegrations assembles { entities: { options, authorized },
integrations } from three v2 calls, getAuthorizeRequirements copies the
OAuth url to the top level, authorize adds entity_id / credential_id /
type to a completed flow, and getUserActions uses GET with ?actionType.
- v2-only methods for the routes core serves: entities, entity types and
requirements, multi-step authorize (getAuthorizationStep,
submitAuthorizationStep), credentials and re-authorization, integration
get and test-auth, the entity proxy, getMeta and hasCapability.
- Every request sends Frigg-Client: @friggframework/ui for the backend's
v1 usage log. API and FriggApiVersionError are exported from the package.
- npm test runs the client tests with node --test (no new dependencies).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kills - docs/reference/api-reference.md: versions, GET /api/meta, v2 conventions and endpoint list, multi-step authorization, the entity proxy and its app/module configuration, the v1 → v2 migration table, deprecation signals, managementApi.v1: false, and the ui apiVersion option. The v1 swagger embeds move under "v1 reference (deprecated)". - frigg-management-api skill: start with /api/meta, document v2 first, v1 as a legacy table. - frigg-user-actions skill: the provisioning runbook on v2 paths, with the v1 differences listed. - CLAUDE.md: where the Management API lives and how to add a route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the end-to-end run: an API-key module that declares requiredAuthMethods.getAuthorizationRequirements(api) (the form convention frigg auth uses) made GET /api/v2/authorize fail, because Module copies requiredAuthMethods onto itself and the Api had no such method. GetAuthorizationRequirements now asks, in order, the module's requiredAuthMethods.getAuthorizationRequirements(api), then the Api's own getAuthorizationRequirements() (OAuth2Requester's authorize URL). A module with neither answers 501 REQUIREMENTS_NOT_DECLARED with a message saying what to add, instead of a 500. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
✅ Deploy Preview for friggframework-org canceled.
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7
|
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 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 e50fe9c. Configure here.
| entityType, | ||
| params: result.authData || {}, | ||
| credentialId, | ||
| }); |
There was a problem hiding this comment.
Session marked complete before finalize
Medium Severity
The multi-step flow persists completed on the AuthorizationSession before complete() finishes token exchange and the auth test. A later failure (provider 502, failed auth test) leaves the session completed, so the same sessionId cannot be retried or restarted. loadSession rejects completed sessions with AUTHORIZATION_SESSION_COMPLETED before the step-1 restart() path runs, so the caller must start over and re-collect earlier steps.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e50fe9c. Configure here.
| } | ||
| return readResponse(response, maxResponseBytes, { | ||
| redirectBlocked: !next, | ||
| }); |
There was a problem hiding this comment.
Unfollowed redirect Location not stripped
High Severity
redirectBlocked is set only when the redirect target leaves the module API. Same-origin redirects that are not followed because they fail the allow-list or exceed three hops still return the upstream Location to the caller. That header is often a full URL and can carry tokens or other secrets from the provider.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e50fe9c. Configure here.
| return { | ||
| url: `${target.origin}${target.basePath}${checkedPath}${next.search ? decodeURI(next.search) : ''}`, | ||
| path: checkedPath, | ||
| }; |
There was a problem hiding this comment.
Malformed redirect query throws 500
Low Severity
sameApiRedirect decodes the redirect path inside try/catch, then calls decodeURI on next.search with no guard. A Location whose query contains a malformed percent-escape throws URIError and becomes a 500 instead of a blocked redirect.
Reviewed by Cursor Bugbot for commit e50fe9c. Configure here.
Frigg 2.0.1 review (Stack A) — COMMENT onlyVerdict (would): changes needed Reviewed against ADR-052 / ADR-053 and Blocking
Should-fix
Nits
No formal approve / request-changes on this review. |
| continue; | ||
| } | ||
| return readResponse(response, maxResponseBytes, { | ||
| redirectBlocked: !next, |
There was a problem hiding this comment.
blocking — Location leaked on unfollowed same-origin redirects
redirectBlocked is only set when sameApiRedirect returns null. If the target is same-origin but not followed (allow-list miss or MAX_REDIRECTS), Location is still returned via filterResponseHeaders.
Scenario: GET an allow-listed path that redirects to /oauth/callback?code=… (or any non-allowed path with secrets in the query) → 302 response includes location with those secrets even though the hop was not followed. Cross-origin correctly strips Location.
Fix: Set redirectBlocked: true whenever the redirect is not followed (!(next && hop < MAX && allowed)), not only when !next. ADR-052 §4/§6 intent is to remove Location when the proxy does not follow.
There was a problem hiding this comment.
Verdict: confirmed. Checked on this branch's head (e50fe9c3) with a throwaway test (not committed) that runs Requester._proxyRequest and then filterResponseHeaders.
modules/requester/proxy-request.js:91-107: thereturn readResponse(...)at line 105 runs only when the hop is not followed, yet it passesredirectBlocked: !next. For a same-API target that failsisAllowedRedirect, or for the 4th hop (hop >= MAX_REDIRECTS),nextis non-null, soredirectBlockedisfalse.modules/use-cases/execute-entity-proxy-request.js:178-179→modules/proxy/proxy-policy.js:200then keepslocation.- Repro: upstream answers
302 Location: https://api.example.com/v2/oauth/callback?code=SECRET, and the allow-list refuses/oauth/callback. The proxy returns{ redirectBlocked: false }, and the filtered headers are{ location: 'https://api.example.com/v2/oauth/callback?code=SECRET' }. The hop-limit case behaves the same way (the 4thLocationcomes back). - The existing test
re-checks each redirect hop against the allow-list and stops after three hopsasserts status and fetch count only, so it doesn't catch this.
Minimal fix: every exit through line 105 is an unfollowed redirect, so the flag should be unconditional:
return readResponse(response, maxResponseBytes, { redirectBlocked: true });Then add expect(result.redirectBlocked).toBe(true) and expect(stopped.redirectBlocked).toBe(true) to that test. Also add one use-case-level assertion that location is absent from the proxied response for an allow-list miss.
Related, same PR: v2 toIntegrationDto (management-api/v2/dto.js:37-40) passes integration.entities through as-is. GetIntegrationsForUser.toDto fills those from findEntitiesByIds, which populates each entity's decrypted credential (data.access_token etc.). So GET /api/v2/integrations returns credential material even though v2 entity responses are masked. Details are in my reply on #682. The fix is to map embedded entities through the same toEntityDto.
🤖 Generated with Claude Code
There was a problem hiding this comment.
Confirmed, and fixed in 549c3015 (pushing now). Every redirect the proxy does not follow now returns redirectBlocked: true, so Location is dropped. That covers an allow-list miss and the hop limit as well as an off-API target.
Tests:
proxy-request.test.jsassertsredirectBlockedon the existing tests:trueon the allow-list denial and on the hop-limit stop, andfalseon a followed same-API redirect.- New use-case test: a 302 whose
Locationcarries?code=comes back with nolocationheader, and the code appears nowhere in the response. - Restoring
redirectBlocked: !nextfails a test.
The v2 integration DTO on this branch also passes integration.entities through as it is. The credential leak fix (toPublicEntity) is on #682, where those responses carry entity records. It applies to this code once the stack merges.
Generated by Claude Code
There was a problem hiding this comment.
Correction: 549c3015 is not pushed yet. This session may only push to claude/internal-mcp-* branches, and feat/management-api-v2 isn't one. The commit is ready and is waiting on Sean to push it, or to allow this session to push to this branch.
Generated by Claude Code






Summary
Management API v2 for 2.0.1, built to the accepted versioning model (ADR-053) and the locked-down entity proxy (ADR-052), both in #668.
packages/core/management-api/route-registry.js) drives the Express routers, the OpenAPI documents and the API Gateway routes devtools generates. Gateway routes are per resource (/api/v2/{integrations,entities,credentials,authorize}); there is noANY /api/v2/{proxy+}, and thereportssegment is reserved so/api/v2/reports(ADR-010) is untouched.GET /api/meta(no auth, no database, on the health Lambda): supported majors with status and OpenAPI links, capability flags (credentials,multiStepAuthorize,entityProxyonly when enabled), core version for admin callers only. OpenAPI per major at/api/meta/openapi/v{1,2}.json.Deprecation(RFC 9745) andLink: rel="deprecation"headers, afrigg.api.deprecated_routelog record and afrigg.api.v1.requestscounter per call.managementApi.v1: falsedrops v1 from the router (410) and from the gateway routes; the OAuth redirect keeps working.{ integrations }only), entities, entity types and requirements, credentials (always masked), multi-step authorize with anAuthorizationSessionmodel (Postgres, MongoDB, DocumentDB; migration20261003000000_add_authorization_session;stepDataencrypted at rest), credential re-authorization, and the entity proxy. Every v2 route authenticates. Missing and foreign resources are the same 404. Errors are{ error: { code, message, details? } }; responses carryFrigg-API-Version: 2.Requester._proxyRequestis the one path-checked entry point, so refresh, credential invalidation, timeouts, telemetry and redaction apply for OAuth2, API-key and basic modules. Path-only input, same-origin/under-base checks, manual redirects (same API only, 3 hops, re-checked), deny-by-default module allow-lists with an app override, header allow-list, size and time limits, status mapping per ADR-052 §10, an audit record and a usage counter per call.FetchError.statusCodeis still a 500).@friggframework/ui:new API(baseUrl, jwt, { apiVersion: 'v1' | 'v2' }), default'v1'. v2 mode checks/api/metafirst and throwsFriggApiVersionError("needs @friggframework/core >= 2.0.1"); component-facing methods keep their v1 return shapes; v2-only methods for the routes that exist.docs/reference/api-reference.md(versions, v2 endpoints, multi-step authorize, proxy, v1 → v2 migration table), thefrigg-management-apiandfrigg-user-actionsskills (v2 first), CLAUDE.md.The old work on
claude/split-522-03-corewas used as a reference only (it is ~458 commits behind and several pieces did not run:api.request()does not exist, the router called its own use cases with the wrong signatures, re-authorize called a non-existent repository method). Use cases and tests here are new.Merge dependencies
fix/v1-entity-gateway-routes): this branch is built on it; the devtools gateway test extends it.frigg validate): adds a schema that rejects unknown top-level keys. This branch addsmanagementApitoapp-definition.schema.json; whichever lands second needs the key in feat(devtools): frigg validate, and make the app-definition schema match the runtime (ADR-051) #666's validator too.Checklist
GET /api/metamanagementApi.v1: falsefindCredentialsByUserIdfor all three databasesapiVersionapiVersionprop in the ui React components (they constructAPIwith the default'v1'); ui 2.x switching to v2 by default/api/meta/docs, CIoasdiff breakinggate (ADR-053 §5, deferrable per the ADR)Sunsetheader (no removal release scheduled yet; supported oncesunsetis set in the registry)POST /api/v2/entitiesandGET /api/v2/entities/options/:credentialId(in the ADR-053 sketch table; left out because v1's versions of both are broken and authorize already creates entities)Test plan
packages/core: new suites for registry, meta, config, deprecation, error format, v2 router (fake dependencies), every new use case, repositories (stubbed Prisma / DocumentDB commands), proxy path/policy/requester/use case, OpenAPI contract and schema parity. Full core suite: same 16 failing suites / 64 failing tests as the base branch (pre-existing: AWS credentials, embedded Mongo, encryption fixtures), +326 passing tests.packages/schemas: 141 passing.packages/devtools: base-definition-factory and gateway route tests pass. The full suite fails the same suites as the base branch; the failing-test count is load-dependent on both (base 133 and 171 in two runs, this branch 171 and 104).packages/ui:npm test(node --test), 12 passing.prisma migrate deploy, and a MongoDB 7 replica set afterprisma db push), with AES field encryption on and an HTTPS fake upstream: meta and OpenAPI, auth, entity types, single-step authorize, multi-step authorize (wrong code, foreign user, skipped step, reuse after completion,stepDatanot readable in the raw row), masked credentials, ownership 404s, integration create/action/delete, v1 deprecation header, proxy allow/deny/SSRF/header refusal/ownership/same-origin redirect followed/cross-host redirect not followed/429 with Retry-After/5xx → 502, delete ordering (409 while in use). All steps pass on both databases.prisma migrate diffshows no drift between the migration and the schema.Open questions
deprecatedAtis2026-11-01, taken from the ADR example. Confirm the date 2.0.1 ships./user/*is listed as v1 in ADR-053 but has no v2 replacement, so it gets no deprecation headers here andmanagementApi.v1: falseleaves it alone. Should it?GET /api/entities/:entityId(and test-auth, options, options/refresh) callModule.getEntityModelFromDefinition, which does not exist, so they 500 onnext. v1 is frozen, so this PR does not fix them; v2 does not reuse those use cases. Fix in v1 or leave?🤖 Generated with Claude Code
📦 Published PR as canary version:
2.0.0--canary.671.e50fe9c.0✨ Test out this PR locally via:
npm install @friggframework/admin-scripts@2.0.0--canary.671.e50fe9c.0 npm install @friggframework/core@2.0.0--canary.671.e50fe9c.0 npm install @friggframework/devtools@2.0.0--canary.671.e50fe9c.0 npm install @friggframework/eslint-config@2.0.0--canary.671.e50fe9c.0 npm install @friggframework/prettier-config@2.0.0--canary.671.e50fe9c.0 npm install @friggframework/schemas@2.0.0--canary.671.e50fe9c.0 npm install @friggframework/serverless-plugin@2.0.0--canary.671.e50fe9c.0 npm install @friggframework/test@2.0.0--canary.671.e50fe9c.0 npm install @friggframework/ui@2.0.0--canary.671.e50fe9c.0 # or yarn add @friggframework/admin-scripts@2.0.0--canary.671.e50fe9c.0 yarn add @friggframework/core@2.0.0--canary.671.e50fe9c.0 yarn add @friggframework/devtools@2.0.0--canary.671.e50fe9c.0 yarn add @friggframework/eslint-config@2.0.0--canary.671.e50fe9c.0 yarn add @friggframework/prettier-config@2.0.0--canary.671.e50fe9c.0 yarn add @friggframework/schemas@2.0.0--canary.671.e50fe9c.0 yarn add @friggframework/serverless-plugin@2.0.0--canary.671.e50fe9c.0 yarn add @friggframework/test@2.0.0--canary.671.e50fe9c.0 yarn add @friggframework/ui@2.0.0--canary.671.e50fe9c.0Note
High Risk
Large surface-area change to authentication, credential handling, authorization sessions, and a new outbound entity proxy; clients must migrate paths and error shapes while v1 remains callable by default.
Overview
Introduces Management API v2 (
/api/v2/*) per ADR-053, with a single route registry wiring Express handlers, OpenAPI, and API Gateway routes. The auth Lambda now mountscreateManagementApiRouter()(v2 + frozen v1); discovery lives on the health Lambda as unauthenticatedGET /api/metaand per-major OpenAPI JSON.v2 behavior adds ownership-aware integrations/entities/credentials (masked secrets, list/delete/reauthorize), multi-step authorize with encrypted
AuthorizationSession.stepData, stricter integration user-action allowlisting, and a beta entity proxy gated bymanagementApi.proxy.enable(ADR-052). Responses use{ error: { code, message, details? } }andFrigg-API-Version: 2; the global error boundary no longer turns body-parser 4xx into 500 on versioned paths.v1 keeps existing handlers but gets RFC 9745 deprecation headers, logging/metrics, and optional
managementApi.v1: false→ 410; OAuth redirect stays unversioned. App definitions gainmanagementApivialoadAppDefinition. Docs, skills, andapi-reference.mdare rewritten for v2-first clients with a v1 migration table.Reviewed by Cursor Bugbot for commit e50fe9c. Bugbot is set up for automated code reviews on this repo. Configure here.