Repository navigation
feat: Management API v2, /api/meta, v1 deprecation and the locked-down entity proxy (ADR-052, ADR-053) - #671
Draft
seanspeaks wants to merge 13 commits into
Draft
seanspeaks wants to merge 13 commits into
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
|
This was referenced Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




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