* Add @executor-js/http-auth: shared placements-based auth-method model One vocabulary for the HTTP protocol plugins (openapi/graphql/mcp): an apikey method carries N header/query placements, each bound to its own credential variable (or sharing one), plus literal placements for static values. Includes the renderer, required-variable extraction, slug normalization, and catalog projection helpers. Core gains the carrier-agnostic getValues seam on ResolveToolsInput (multi-input analog of getValue) and the catalog wire schema now carries placement variable/literal so multi-input methods survive the HTTP boundary. Removes the dead http-source and graphql-greenfield packages (zero consumers; they forked this exact logic). * MCP: adopt the shared placements auth model McpAuthMethod becomes none | apikey(placements) | oauth2 — one method can mix header and query placements, each rendered from its own credential input (or sharing one). buildConnectorInput renders via the shared renderer over credential.values; invokeTool fails explicitly with connection_value_missing instead of dialing unauthenticated; discovery keeps its tolerant skip. The addServer 'auth' shorthand stays as input sugar. Pre-canonical stored shapes (singular auth, retired header/query kinds, openapi-shaped v1-migration templates) are rewritten by the new one-off migrate-config module — runtime decodes only the canonical shape. Slugs and variable names are preserved verbatim (connection.template / item_ids contracts). The react codec collapses onto the new shared-auth-method-codec in packages/react, deleting the first-named-placement truncation; the edit screen diffs placements deeply. Wire tests pin the extreme case: one method = bearer header + team-id query param, asserted on what the server received. * OpenAPI: adopt the shared placements auth model The apiKey arm of Authentication becomes the shared ApiKeyAuthMethod (placements bound to per-input variables) — the variable()-templated header/query records are retired. The oauth arm keeps its core shape (type: "oauth", stored endpoints + scopes). renderAuthTemplate and requiredTemplateVariables now sit on the shared renderer; the catalog projection and react codec collapse onto the shared helpers. Stored legacy templates are rewritten by the one-off migrate-config module; equivalence tests pin that migrated templates render byte-identically to the legacy renderer (multi-variable Datadog-style, literal-only values, prefix extraction). Slugs and variable names are preserved verbatim. * Wire the one-off auth-config migration + unified-auth e2e scenarios The migration planner (and a shared SQLite runner) live in http-auth; each app routes by plugin_id to the plugins' migrate functions: local runs it at boot after the v1->v2 migration (desktop inherits), selfhost at boot beside its defensive column adds, cloud as an out-of-band script (db:migrate-auth:prod) run before deploying canonical-only code. e2e: scenarios move to canonical wire shapes and gain the extreme case — one MCP source declaring [oauth][bearer header + team-id query (two inputs)][bearer header][query token], one connection per method, every invocation made through the real MCP execute surface and asserted against what a recording MCP server received. The selfhost browser suite gains the 2-input connect modal scenario (one field per input, both rendered together on the wire). * GraphQL: adopt the shared placements auth model GraphqlAuthMethod becomes none | apikey(placements) | oauth2(header?, prefix?) — one method can mix header and query placements, each rendered from its own credential input. The local renderer is deleted: apikey renders through the shared renderer, oauth2 through oauthBearerPlacement (custom header/prefix overrides preserved). Introspection, discovery, and invoke run on credential.values; a missing input fails explicitly, naming the variable. Pre-canonical stored shapes (retired {kind:'apiKey', in, name} entries, openapi-shaped v1-migration templates, the singular auth field — incl. the v1 output that wrote BOTH auth:{oauth2} and an apiKey array) are rewritten by the one-off migrate-config module. The react codec collapses onto shared-auth-method-codec, deleting the one-template-per-placement workaround. Wire tests pin the extreme case (bearer header + team-id query on one method) against a recording GraphQL server, on both introspection and invocation. * Sweep remaining consumers onto the canonical auth shapes Examples (docs quickstart + regenerated snippet, promise-sdk, all-plugins), the kernel dynamic-worker integration test, and the local/cloud auth-tool-failure suites move off the retired variable()-templated apiKey shape. Boundary lint suppressions for the migration runner/script; changelog stub for the new http-auth package. * e2e: make the sandbox tool-result assertion shape-agnostic * Changeset + repo notes for the unified auth model * Fold http-auth into @executor-js/sdk as the /http-auth subpath Carrier-agnosticism is a composition property, not a location one: core never imports the module, so it doesn't need its own package. The vocabulary ships as @executor-js/sdk/http-auth (browser-safe subpath, own tsup entry); the standalone package, its workspace entry, deps, and fixed-group/changeset references are gone. * Accept request-shaped auth authoring on every plugin Auth inputs take two dialects, normalized to the one stored model: the canonical placements shape, and the request-shaped template that reads like the request it produces — { type: "apiKey", headers: { Authorization: ["Bearer ", variable("token")] }, queryParams: { team_id: [variable("team_id")] } } variable() is exported from each plugin. Authoring is strict where the migration is tolerant: a template value renders at most ONE variable, as the FINAL part — exotic interleavings are rejected at decode with a pointed message instead of silently flattened (only the one-off migration flattens, for data that already exists). The dialect is accepted by addServer/addIntegration/addSpec/configure on all three plugins (typed clients included — the docs quickstart and the auth-tool-failure suites author this way again), expanded before merge so stored configs stay canonical. Wire tests pin that an authored template renders identically to its canonical twin. * One apikey authoring dialect: request-shaped in, placements out Auth INPUTS accept exactly one apikey shape — the request-shaped template (headers/queryParams records with variable() slots) — expanded to canonical placements at the boundary. Stored configs and the catalog keep reading as placements; apiKeyAuthTemplateFromMethod serializes them back, so every read-modify-write surface (edit screen replace, custom- method remove, add flows) round-trips through one serializer instead of the wire accepting two dialects. Published schemas now teach a single authoring shape; canonical placement inputs are rejected with a schema error pointing at the dialect. * One discriminator: OpenAPI oauth templates re-key to kind:"oauth2" Every auth method is now kind-keyed — the core OAuthAuthentication shape drops its legacy type:"oauth" spelling, so the Authentication union narrows on .kind alone and the '"kind" in template' guards are gone. The one-off config migration rewrites stored type:"oauth" entries (slug and endpoints verbatim); oauth INPUTS are wire-typed (plain slug) and branded in the normalize pass, letting the HTTP handlers pass payloads through untouched. * Migration script: TLS for direct connections + post-migration decode audit The script connects to PlanetScale directly (no Hyperdrive), which requires TLS. The plan now also audits the SIMULATED final state of every protocol-plugin row — rewritten or untouched — against the new runtime's canonical decoders and refuses to write if any row would read as 'no usable config' after deploy. Verified against prod: 343 rows, 302 rewrite, audit clean.
Showing the changes to packages/plugins/openapi/src/api (2 of 122 files). Show all
Unified · Split
| @@ -1,5 +1,6 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"; | 1 | import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"; |
| 2 | import { Schema } from "effect"; | 2 | import { Schema } from "effect"; |
| 3 | import { ApiKeyAuthMethod, ApiKeyAuthTemplate } from "@executor-js/sdk/http-auth"; | ||
| 3 | import { | 4 | import { |
| 4 | InternalError, | 5 | InternalError, |
| 5 | IntegrationAlreadyExistsError, | 6 | IntegrationAlreadyExistsError, |
| @@ -46,29 +47,19 @@ const OpenApiSpecInputPayload = Schema.Union([ · 40 unmodified lines | |||
| 46 | }), | 47 | }), |
| 47 | ]); | 48 | ]); |
| 48 | 49 | ||
| 49 | const AuthenticationVariablePayload = Schema.Struct({ | 50 | const OAuthTemplatePayload = Schema.Struct({ |
| 50 | type: Schema.Literal("variable"), | 51 | slug: Schema.String, |
| 51 | name: Schema.String, | 52 | kind: Schema.Literal("oauth2"), |
| 53 | authorizationUrl: Schema.String, | ||
| 54 | tokenUrl: Schema.String, | ||
| 55 | scopes: Schema.Array(Schema.String), | ||
| 52 | }); | 56 | }); |
| 53 | const AuthenticationTemplateValuePayload = Schema.Union([ | 57 | |
| 54 | Schema.String, | 58 | /** Auth INPUTS: oauth templates + the request-shaped apikey dialect. */ |
| 55 | Schema.Array(Schema.Union([Schema.String, AuthenticationVariablePayload])), | 59 | const AuthenticationPayload = Schema.Union([OAuthTemplatePayload, ApiKeyAuthTemplate]); |
| 56 | ]); | 60 | |
| 57 | const AuthenticationPayload = Schema.Union([ | 61 | /** Auth in RESPONSES: the canonical stored shapes (placements). */ |
| 58 | Schema.Struct({ | 62 | const AuthenticationResponse = Schema.Union([OAuthTemplatePayload, ApiKeyAuthMethod]); |
| 59 | slug: Schema.String, | ||
| 60 | type: Schema.Literal("apiKey"), | ||
| 61 | headers: Schema.optional(Schema.Record(Schema.String, AuthenticationTemplateValuePayload)), | ||
| 62 | queryParams: Schema.optional(Schema.Record(Schema.String, AuthenticationTemplateValuePayload)), | ||
| 63 | }), | ||
| 64 | Schema.Struct({ | ||
| 65 | slug: Schema.String, | ||
| 66 | type: Schema.Literal("oauth"), | ||
| 67 | authorizationUrl: Schema.String, | ||
| 68 | tokenUrl: Schema.String, | ||
| 69 | scopes: Schema.Array(Schema.String), | ||
| 70 | }), | ||
| 71 | ]); | ||
| 72 | 63 | ||
| 73 | const AddSpecPayload = Schema.Struct({ | 64 | const AddSpecPayload = Schema.Struct({ |
| 74 | spec: OpenApiSpecInputPayload, | 65 | spec: OpenApiSpecInputPayload, |
| @@ -119,13 +110,13 @@ const OpenApiConfigView = Schema.Struct({ · 44 unmodified lines | |||
| 119 | baseUrl: Schema.optional(Schema.String), | 110 | baseUrl: Schema.optional(Schema.String), |
| 120 | headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), | 111 | headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), |
| 121 | queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)), | 112 | queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)), |
| 122 | authenticationTemplate: Schema.optional(Schema.Array(AuthenticationPayload)), | 113 | authenticationTemplate: Schema.optional(Schema.Array(AuthenticationResponse)), |
| 123 | }); | 114 | }); |
| 124 | 115 | ||
| 125 | // The configure result — the merged `authenticationTemplate` after the new | 116 | // The configure result — the merged `authenticationTemplate` after the new |
| 126 | // custom methods were appended/replaced. | 117 | // custom methods were appended/replaced. |
| 127 | const ConfigureResponse = Schema.Struct({ | 118 | const ConfigureResponse = Schema.Struct({ |
| 128 | authenticationTemplate: Schema.Array(AuthenticationPayload), | 119 | authenticationTemplate: Schema.Array(AuthenticationResponse), |
| 129 | }); | 120 | }); |
| 130 | 121 | ||
| 131 | // --------------------------------------------------------------------------- | 122 | // --------------------------------------------------------------------------- |
| @@ -2,8 +2,6 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; · 1 unmodified line | |||
| 2 | import { Context, Effect } from "effect"; | 2 | import { Context, Effect } from "effect"; |
| 3 | 3 | ||
| 4 | import { addGroup, capture } from "@executor-js/api"; | 4 | import { addGroup, capture } from "@executor-js/api"; |
| 5 | import { AuthTemplateSlug } from "@executor-js/sdk/shared"; | ||
| 6 | import type { Authentication } from "../sdk"; | ||
| 7 | import type { OpenApiPluginExtension } from "../sdk/plugin"; | 5 | import type { OpenApiPluginExtension } from "../sdk/plugin"; |
| 8 | import { OpenApiGroup } from "./group"; | 6 | import { OpenApiGroup } from "./group"; |
| 9 | 7 | ||
| @@ -60,12 +58,7 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 50 unmodified lines | |||
| 60 | baseUrl: payload.baseUrl, | 58 | baseUrl: payload.baseUrl, |
| 61 | headers: payload.headers ? { ...payload.headers } : undefined, | 59 | headers: payload.headers ? { ...payload.headers } : undefined, |
| 62 | queryParams: payload.queryParams ? { ...payload.queryParams } : undefined, | 60 | queryParams: payload.queryParams ? { ...payload.queryParams } : undefined, |
| 63 | authenticationTemplate: payload.authenticationTemplate?.map( | 61 | authenticationTemplate: payload.authenticationTemplate, |
| 64 | (entry): Authentication => ({ | ||
| 65 | ...entry, | ||
| 66 | slug: AuthTemplateSlug.make(entry.slug), | ||
| 67 | }), | ||
| 68 | ), | ||
| 69 | }); | 62 | }); |
| 70 | }), | 63 | }), |
| 71 | ), | 64 | ), |
| @@ -115,12 +108,7 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 43 unmodified lines | |||
| 115 | Effect.gen(function* () { | 108 | Effect.gen(function* () { |
| 116 | const ext = yield* OpenApiExtensionService; | 109 | const ext = yield* OpenApiExtensionService; |
| 117 | const authenticationTemplate = yield* ext.configure(params.slug, { | 110 | const authenticationTemplate = yield* ext.configure(params.slug, { |
| 118 | authenticationTemplate: payload.authenticationTemplate.map( | 111 | authenticationTemplate: payload.authenticationTemplate, |
| 119 | (entry): Authentication => ({ | ||
| 120 | ...entry, | ||
| 121 | slug: AuthTemplateSlug.make(entry.slug), | ||
| 122 | }), | ||
| 123 | ), | ||
| 124 | mode: payload.mode ?? "merge", | 112 | mode: payload.mode ?? "merge", |
| 125 | }); | 113 | }); |
| 126 | return { authenticationTemplate: [...authenticationTemplate] }; | 114 | return { authenticationTemplate: [...authenticationTemplate] }; |