Unified · Split
| @@ -126,6 +126,7 @@ export type IntegrationPresetAuthentication = · 125 unmodified lines | |||
| 126 | | { | 126 | | { |
| 127 | readonly slug: string; | 127 | readonly slug: string; |
| 128 | readonly kind: "oauth2"; | 128 | readonly kind: "oauth2"; |
| 129 | readonly label?: string; | ||
| 129 | readonly authorizationUrl: string; | 130 | readonly authorizationUrl: string; |
| 130 | readonly tokenUrl: string; | 131 | readonly tokenUrl: string; |
| 131 | readonly resource?: string | null; | 132 | readonly resource?: string | null; |
| @@ -31,6 +31,10 @@ export type OAuthGrant = "authorization_code" | "client_credentials"; · 30 unmodified lines | |||
| 31 | export interface OAuthAuthentication { | 31 | export interface OAuthAuthentication { |
| 32 | readonly slug: AuthTemplateSlug; | 32 | readonly slug: AuthTemplateSlug; |
| 33 | readonly kind: "oauth2"; | 33 | readonly kind: "oauth2"; |
| 34 | /** Display label distinguishing this method when an integration declares | ||
| 35 | * several oauth2 templates (e.g. delegated vs app-only). UIs fall back to | ||
| 36 | * "OAuth2" when absent. */ | ||
| 37 | readonly label?: string; | ||
| 34 | readonly authorizationUrl: string; | 38 | readonly authorizationUrl: string; |
| 35 | readonly tokenUrl: string; | 39 | readonly tokenUrl: string; |
| 36 | /** RFC 8707 Resource Indicator to bind the OAuth flow to this protected | 40 | /** RFC 8707 Resource Indicator to bind the OAuth flow to this protected |
| @@ -582,6 +582,7 @@ export type IntegrationPresetAuthentication = · 581 unmodified lines | |||
| 582 | | { | 582 | | { |
| 583 | readonly slug: string; | 583 | readonly slug: string; |
| 584 | readonly kind: "oauth2"; | 584 | readonly kind: "oauth2"; |
| 585 | readonly label?: string; | ||
| 585 | readonly authorizationUrl: string; | 586 | readonly authorizationUrl: string; |
| 586 | readonly tokenUrl: string; | 587 | readonly tokenUrl: string; |
| 587 | readonly resource?: string | null; | 588 | readonly resource?: string | null; |
| @@ -62,6 +62,7 @@ const OpenApiSpecInputPayload = Schema.Union([ · 61 unmodified lines | |||
| 62 | const OAuthTemplatePayload = Schema.Struct({ | 62 | const OAuthTemplatePayload = Schema.Struct({ |
| 63 | slug: Schema.String, | 63 | slug: Schema.String, |
| 64 | kind: Schema.Literal("oauth2"), | 64 | kind: Schema.Literal("oauth2"), |
| 65 | label: Schema.optional(Schema.String), | ||
| 65 | authorizationUrl: Schema.String, | 66 | authorizationUrl: Schema.String, |
| 66 | tokenUrl: Schema.String, | 67 | tokenUrl: Schema.String, |
| 67 | resource: Schema.optional(Schema.NullOr(Schema.String)), | 68 | resource: Schema.optional(Schema.NullOr(Schema.String)), |
| @@ -23,7 +23,9 @@ import { · 22 unmodified lines | |||
| 23 | import { | 23 | import { |
| 24 | MICROSOFT_AUTHORIZATION_URL, | 24 | MICROSOFT_AUTHORIZATION_URL, |
| 25 | MICROSOFT_AUTH_TEMPLATE_SLUG, | 25 | MICROSOFT_AUTH_TEMPLATE_SLUG, |
| 26 | MICROSOFT_CLIENT_CREDENTIALS_AUTH_LABEL, | ||
| 26 | MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG, | 27 | MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG, |
| 28 | MICROSOFT_DELEGATED_AUTH_LABEL, | ||
| 27 | MICROSOFT_GRAPH_BASE_SCOPES, | 29 | MICROSOFT_GRAPH_BASE_SCOPES, |
| 28 | MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES, | 30 | MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES, |
| 29 | MICROSOFT_GRAPH_DELEGATED_DEFAULT_SCOPES, | 31 | MICROSOFT_GRAPH_DELEGATED_DEFAULT_SCOPES, |
| @@ -173,6 +175,7 @@ const microsoftOAuthTemplate = ( · 143 unmodified lines | |||
| 173 | { | 175 | { |
| 174 | slug: AuthTemplateSlug.make(MICROSOFT_AUTH_TEMPLATE_SLUG), | 176 | slug: AuthTemplateSlug.make(MICROSOFT_AUTH_TEMPLATE_SLUG), |
| 175 | kind: "oauth2", | 177 | kind: "oauth2", |
| 178 | label: MICROSOFT_DELEGATED_AUTH_LABEL, | ||
| 176 | authorizationUrl: endpoints.authorizationUrl, | 179 | authorizationUrl: endpoints.authorizationUrl, |
| 177 | tokenUrl: endpoints.tokenUrl, | 180 | tokenUrl: endpoints.tokenUrl, |
| 178 | scopes, | 181 | scopes, |
| @@ -180,6 +183,7 @@ const microsoftOAuthTemplate = ( · 1 unmodified line | |||
| 180 | { | 183 | { |
| 181 | slug: AuthTemplateSlug.make(MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG), | 184 | slug: AuthTemplateSlug.make(MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG), |
| 182 | kind: "oauth2", | 185 | kind: "oauth2", |
| 186 | label: MICROSOFT_CLIENT_CREDENTIALS_AUTH_LABEL, | ||
| 183 | authorizationUrl: endpoints.authorizationUrl, | 187 | authorizationUrl: endpoints.authorizationUrl, |
| 184 | tokenUrl: endpoints.clientCredentialsTokenUrl, | 188 | tokenUrl: endpoints.clientCredentialsTokenUrl, |
| 185 | scopes: [...MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES], | 189 | scopes: [...MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES], |
| @@ -288,6 +288,13 @@ describe("Microsoft Graph scope presets", () => { · 287 unmodified lines | |||
| 288 | expect(preset.defaultSlug).toBeTruthy(); | 288 | expect(preset.defaultSlug).toBeTruthy(); |
| 289 | expect(preset.authTemplate).toHaveLength(2); | 289 | expect(preset.authTemplate).toHaveLength(2); |
| 290 | expect(preset.authTemplate?.map((template) => template.kind)).toEqual(["oauth2", "oauth2"]); | 290 | expect(preset.authTemplate?.map((template) => template.kind)).toEqual(["oauth2", "oauth2"]); |
| 291 | // Both methods are oauth2, so distinct labels are what keeps the | ||
| 292 | // delegated and app-only tabs tellable apart in the connection dialog. | ||
| 293 | expect( | ||
| 294 | preset.authTemplate?.map((template) => | ||
| 295 | template.kind === "oauth2" ? template.label : null, | ||
| 296 | ), | ||
| 297 | ).toEqual(["OAuth2 (user)", "OAuth2 (app-only)"]); | ||
| 291 | } | 298 | } |
| 292 | }); | 299 | }); |
| 293 | 300 | ||
| @@ -44,6 +44,11 @@ export const MICROSOFT_AUTHORIZATION_URL = · 43 unmodified lines | |||
| 44 | export const MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; | 44 | export const MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; |
| 45 | export const MICROSOFT_AUTH_TEMPLATE_SLUG = "azureAdDelegated"; | 45 | export const MICROSOFT_AUTH_TEMPLATE_SLUG = "azureAdDelegated"; |
| 46 | export const MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG = "azureAdClientCredentials"; | 46 | export const MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG = "azureAdClientCredentials"; |
| 47 | // Both templates are plain oauth2, so without labels they'd render as two | ||
| 48 | // identical "OAuth2" methods. Microsoft's terms: delegated (signed-in user) | ||
| 49 | // vs app-only (client credentials, no user). | ||
| 50 | export const MICROSOFT_DELEGATED_AUTH_LABEL = "OAuth2 (user)"; | ||
| 51 | export const MICROSOFT_CLIENT_CREDENTIALS_AUTH_LABEL = "OAuth2 (app-only)"; | ||
| 47 | export const MICROSOFT_GRAPH_BASE_SCOPES: readonly string[] = ["offline_access"]; | 52 | export const MICROSOFT_GRAPH_BASE_SCOPES: readonly string[] = ["offline_access"]; |
| 48 | export const MICROSOFT_GRAPH_IDENTITY_SCOPE = "User.Read"; | 53 | export const MICROSOFT_GRAPH_IDENTITY_SCOPE = "User.Read"; |
| 49 | export const MICROSOFT_GRAPH_DEFAULT_SCOPE = "https://graph.microsoft.com/.default"; | 54 | export const MICROSOFT_GRAPH_DEFAULT_SCOPE = "https://graph.microsoft.com/.default"; |
| @@ -539,6 +544,7 @@ const microsoftGraphCatalogAuthTemplate = (preset: MicrosoftGraphScopePreset) => · 489 unmodified lines | |||
| 539 | { | 544 | { |
| 540 | slug: MICROSOFT_AUTH_TEMPLATE_SLUG, | 545 | slug: MICROSOFT_AUTH_TEMPLATE_SLUG, |
| 541 | kind: "oauth2" as const, | 546 | kind: "oauth2" as const, |
| 547 | label: MICROSOFT_DELEGATED_AUTH_LABEL, | ||
| 542 | authorizationUrl: MICROSOFT_AUTHORIZATION_URL, | 548 | authorizationUrl: MICROSOFT_AUTHORIZATION_URL, |
| 543 | tokenUrl: MICROSOFT_TOKEN_URL, | 549 | tokenUrl: MICROSOFT_TOKEN_URL, |
| 544 | scopes: microsoftGraphScopesForPresetIds([preset.id]), | 550 | scopes: microsoftGraphScopesForPresetIds([preset.id]), |
| @@ -546,6 +552,7 @@ const microsoftGraphCatalogAuthTemplate = (preset: MicrosoftGraphScopePreset) => · 1 unmodified line | |||
| 546 | { | 552 | { |
| 547 | slug: MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG, | 553 | slug: MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG, |
| 548 | kind: "oauth2" as const, | 554 | kind: "oauth2" as const, |
| 555 | label: MICROSOFT_CLIENT_CREDENTIALS_AUTH_LABEL, | ||
| 549 | authorizationUrl: MICROSOFT_AUTHORIZATION_URL, | 556 | authorizationUrl: MICROSOFT_AUTHORIZATION_URL, |
| 550 | tokenUrl: MICROSOFT_TOKEN_URL, | 557 | tokenUrl: MICROSOFT_TOKEN_URL, |
| 551 | scopes: [...MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES], | 558 | scopes: [...MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES], |
| @@ -91,6 +91,22 @@ describe("editor round-trip", () => { · 90 unmodified lines | |||
| 91 | }); | 91 | }); |
| 92 | }); | 92 | }); |
| 93 | 93 | ||
| 94 | it("oauth stored → editor → stored preserves the display label", () => { | ||
| 95 | const stored: Authentication = { | ||
| 96 | slug: AuthTemplateSlug.make("azureAdDelegated"), | ||
| 97 | kind: "oauth2", | ||
| 98 | label: "OAuth2 (user)", | ||
| 99 | authorizationUrl: "https://x.example/auth", | ||
| 100 | tokenUrl: "https://x.example/token", | ||
| 101 | resource: null, | ||
| 102 | scopes: ["a"], | ||
| 103 | }; | ||
| 104 | const editor = editorValueFromAuthentication(stored); | ||
| 105 | const back = authenticationFromEditorValue(editor, "azureAdDelegated"); | ||
| 106 | expect(back).toEqual(stored); | ||
| 107 | expect(authMethodsFromConfig([stored])[0]?.label).toBe("OAuth2 (user)"); | ||
| 108 | }); | ||
| 109 | |||
| 94 | it("none editor value yields no method", () => { | 110 | it("none editor value yields no method", () => { |
| 95 | expect(authenticationFromEditorValue({ kind: "none" })).toBeNull(); | 111 | expect(authenticationFromEditorValue({ kind: "none" })).toBeNull(); |
| 96 | }); | 112 | }); |
| @@ -31,7 +31,7 @@ const oauthAuthMethod = (template: Extract<Authentication, { kind: "oauth2" }>): · 30 unmodified lines | |||
| 31 | const slug = String(template.slug); | 31 | const slug = String(template.slug); |
| 32 | return { | 32 | return { |
| 33 | id: slug, | 33 | id: slug, |
| 34 | label: "OAuth2", | 34 | label: template.label ?? "OAuth2", |
| 35 | kind: "oauth", | 35 | kind: "oauth", |
| 36 | source: slug.startsWith("custom_") ? "custom" : "spec", | 36 | source: slug.startsWith("custom_") ? "custom" : "spec", |
| 37 | template: AuthTemplateSlug.make(slug), | 37 | template: AuthTemplateSlug.make(slug), |
| @@ -78,6 +78,7 @@ export function editorValueFromAuthentication(template: Authentication): AuthTem · 40 unmodified lines | |||
| 78 | if (template.kind === "oauth2") { | 78 | if (template.kind === "oauth2") { |
| 79 | return { | 79 | return { |
| 80 | kind: "oauth", | 80 | kind: "oauth", |
| 81 | ...(template.label !== undefined ? { label: template.label } : {}), | ||
| 81 | authorizationUrl: template.authorizationUrl ?? "", | 82 | authorizationUrl: template.authorizationUrl ?? "", |
| 82 | tokenUrl: template.tokenUrl ?? "", | 83 | tokenUrl: template.tokenUrl ?? "", |
| 83 | resource: template.resource ?? null, | 84 | resource: template.resource ?? null, |
| @@ -95,6 +96,7 @@ const oauthTemplateFromEditorValue = ( · 11 unmodified lines | |||
| 95 | ): Authentication => ({ | 96 | ): Authentication => ({ |
| 96 | slug: AuthTemplateSlug.make(slug ?? ""), | 97 | slug: AuthTemplateSlug.make(slug ?? ""), |
| 97 | kind: "oauth2", | 98 | kind: "oauth2", |
| 99 | ...(value.label !== undefined ? { label: value.label } : {}), | ||
| 98 | authorizationUrl: value.authorizationUrl, | 100 | authorizationUrl: value.authorizationUrl, |
| 99 | tokenUrl: value.tokenUrl, | 101 | tokenUrl: value.tokenUrl, |
| 100 | resource: value.resource ?? null, | 102 | resource: value.resource ?? null, |
| @@ -32,6 +32,7 @@ import { SpecOverridesSchema, type SpecOverrides } from "./spec-overrides"; · 31 unmodified lines | |||
| 32 | const OAuthAuthenticationSchema = Schema.Struct({ | 32 | const OAuthAuthenticationSchema = Schema.Struct({ |
| 33 | slug: Schema.String, | 33 | slug: Schema.String, |
| 34 | kind: Schema.Literal("oauth2"), | 34 | kind: Schema.Literal("oauth2"), |
| 35 | label: Schema.optional(Schema.String), | ||
| 35 | authorizationUrl: Schema.String, | 36 | authorizationUrl: Schema.String, |
| 36 | tokenUrl: Schema.String, | 37 | tokenUrl: Schema.String, |
| 37 | resource: Schema.optional(Schema.NullOr(Schema.String)), | 38 | resource: Schema.optional(Schema.NullOr(Schema.String)), |
| @@ -29,7 +29,7 @@ import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testin · 28 unmodified lines | |||
| 29 | 29 | ||
| 30 | import { openApiPlugin } from "./plugin"; | 30 | import { openApiPlugin } from "./plugin"; |
| 31 | import type { APIKeyAuthentication, Authentication, AuthenticationInput } from "./types"; | 31 | import type { APIKeyAuthentication, Authentication, AuthenticationInput } from "./types"; |
| 32 | import { variable } from "@executor-js/sdk/http-auth"; | 32 | import { variable, type ApiKeyAuthTemplate } from "@executor-js/sdk/http-auth"; |
| 33 | import { | 33 | import { |
| 34 | makeOpenApiHttpApiTestIntegrationConfig, | 34 | makeOpenApiHttpApiTestIntegrationConfig, |
| 35 | serveOpenApiHttpApiTestServer, | 35 | serveOpenApiHttpApiTestServer, |
| @@ -83,7 +83,7 @@ const customApiKey: AuthenticationInput = { · 47 unmodified lines | |||
| 83 | // omits it, which `configure` should backfill with a generated `custom_<id>`. | 83 | // omits it, which `configure` should backfill with a generated `custom_<id>`. |
| 84 | // The cast is confined to this one boundary helper. | 84 | // The cast is confined to this one boundary helper. |
| 85 | const sluglessApiKey = ( | 85 | const sluglessApiKey = ( |
| 86 | template: Omit<AuthenticationInput & { type: "apiKey" }, "slug" | "type">, | 86 | template: Omit<ApiKeyAuthTemplate, "slug" | "type">, |
| 87 | ): AuthenticationInput => ({ type: "apiKey", ...template }); | 87 | ): AuthenticationInput => ({ type: "apiKey", ...template }); |
| 88 | 88 | ||
| 89 | describe("OpenAPI Plugin — configure (custom auth method)", () => { | 89 | describe("OpenAPI Plugin — configure (custom auth method)", () => { |
| @@ -1,6 +1,8 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { Option } from "effect"; | ||
| 2 | 3 | ||
| 3 | import { resolvedOAuthScopes } from "./derive-auth"; | 4 | import { detectedAuthenticationTemplates, resolvedOAuthScopes } from "./derive-auth"; |
| 5 | import { OAuth2Preset } from "./preview"; | ||
| 4 | 6 | ||
| 5 | describe("resolvedOAuthScopes", () => { | 7 | describe("resolvedOAuthScopes", () => { |
| 6 | it("does not synthesize OIDC scopes for a plain OAuth provider", () => { | 8 | it("does not synthesize OIDC scopes for a plain OAuth provider", () => { |
| @@ -26,3 +28,46 @@ describe("resolvedOAuthScopes", () => { · 19 unmodified lines | |||
| 26 | ]); | 28 | ]); |
| 27 | }); | 29 | }); |
| 28 | }); | 30 | }); |
| 31 | |||
| 32 | const oauth2Preset = ( | ||
| 33 | securitySchemeName: string, | ||
| 34 | flow: "authorizationCode" | "clientCredentials", | ||
| 35 | ) => | ||
| 36 | OAuth2Preset.make({ | ||
| 37 | label: `OAuth2 ${flow === "authorizationCode" ? "Authorization Code" : "Client Credentials"} · ${securitySchemeName}`, | ||
| 38 | securitySchemeName, | ||
| 39 | flow, | ||
| 40 | authorizationUrl: | ||
| 41 | flow === "authorizationCode" ? Option.some("https://example.com/auth") : Option.none(), | ||
| 42 | tokenUrl: "https://example.com/token", | ||
| 43 | resource: Option.none(), | ||
| 44 | refreshUrl: Option.none(), | ||
| 45 | scopes: { read: "Read access" }, | ||
| 46 | identityScopes: "auto", | ||
| 47 | }); | ||
| 48 | |||
| 49 | describe("detectedAuthenticationTemplates", () => { | ||
| 50 | it("stores no label for a lone oauth2 method", () => { | ||
| 51 | const templates = detectedAuthenticationTemplates( | ||
| 52 | [], | ||
| 53 | [oauth2Preset("oauth_app", "authorizationCode")], | ||
| 54 | "https://example.com", | ||
| 55 | ); | ||
| 56 | expect(templates).toHaveLength(1); | ||
| 57 | expect(templates[0]).not.toHaveProperty("label"); | ||
| 58 | }); | ||
| 59 | |||
| 60 | it("labels each oauth2 method from its preset when several are detected", () => { | ||
| 61 | const templates = detectedAuthenticationTemplates( | ||
| 62 | [], | ||
| 63 | [ | ||
| 64 | oauth2Preset("oauth_app", "authorizationCode"), | ||
| 65 | oauth2Preset("oauth_app", "clientCredentials"), | ||
| 66 | ], | ||
| 67 | "https://example.com", | ||
| 68 | ); | ||
| 69 | expect( | ||
| 70 | templates.map((template) => (template.kind === "oauth2" ? template.label : null)), | ||
| 71 | ).toEqual(["OAuth2 Authorization Code · oauth_app", "OAuth2 Client Credentials · oauth_app"]); | ||
| 72 | }); | ||
| 73 | }); | ||
| @@ -126,9 +126,11 @@ const oauthTemplateFromPreset = ( · 125 unmodified lines | |||
| 126 | baseUrl: string, | 126 | baseUrl: string, |
| 127 | slug: AuthTemplateSlug, | 127 | slug: AuthTemplateSlug, |
| 128 | scopes: readonly string[], | 128 | scopes: readonly string[], |
| 129 | label?: string, | ||
| 129 | ): OAuthAuthentication => ({ | 130 | ): OAuthAuthentication => ({ |
| 130 | slug, | 131 | slug, |
| 131 | kind: "oauth2", | 132 | kind: "oauth2", |
| 133 | ...(label !== undefined ? { label } : {}), | ||
| 132 | authorizationUrl: resolveOAuthUrl( | 134 | authorizationUrl: resolveOAuthUrl( |
| 133 | Option.getOrElse(preset.authorizationUrl, () => ""), | 135 | Option.getOrElse(preset.authorizationUrl, () => ""), |
| 134 | baseUrl, | 136 | baseUrl, |
| @@ -161,6 +163,10 @@ export const detectedAuthenticationTemplates = ( · 26 unmodified lines | |||
| 161 | apiKeyTemplateFromHeaderPreset(preset, AuthTemplateSlug.make(`apikey-${index}`)), | 163 | apiKeyTemplateFromHeaderPreset(preset, AuthTemplateSlug.make(`apikey-${index}`)), |
| 162 | ); | 164 | ); |
| 163 | }); | 165 | }); |
| 166 | // A lone oauth2 method needs no disambiguating label (it renders as plain | ||
| 167 | // "OAuth2"); with several, each carries its preset label so the stored | ||
| 168 | // methods stay tellable apart in the UI. | ||
| 169 | const labelled = oauth2Presets.length > 1; | ||
| 164 | for (const preset of oauth2Presets) { | 170 | for (const preset of oauth2Presets) { |
| 165 | const scopes = resolvedOAuthScopes(Object.keys(preset.scopes), preset.identityScopes); | 171 | const scopes = resolvedOAuthScopes(Object.keys(preset.scopes), preset.identityScopes); |
| 166 | templates.push( | 172 | templates.push( |
| @@ -169,6 +175,7 @@ export const detectedAuthenticationTemplates = ( · 2 unmodified lines | |||
| 169 | baseUrl, | 175 | baseUrl, |
| 170 | AuthTemplateSlug.make(`oauth-${preset.securitySchemeName}`), | 176 | AuthTemplateSlug.make(`oauth-${preset.securitySchemeName}`), |
| 171 | scopes, | 177 | scopes, |
| 178 | labelled ? preset.label : undefined, | ||
| 172 | ), | 179 | ), |
| 173 | ); | 180 | ); |
| 174 | } | 181 | } |
| @@ -84,6 +84,23 @@ describe("describeOpenApiAuthMethods", () => { · 83 unmodified lines | |||
| 84 | ]); | 84 | ]); |
| 85 | }); | 85 | }); |
| 86 | 86 | ||
| 87 | it("prefers a stored oauth label over the generic OAuth2 fallback", () => { | ||
| 88 | const methods = describeOpenApiAuthMethods( | ||
| 89 | recordWith([ | ||
| 90 | { | ||
| 91 | slug: AuthTemplateSlug.make("azureAdDelegated"), | ||
| 92 | kind: "oauth2", | ||
| 93 | label: "OAuth2 (user)", | ||
| 94 | authorizationUrl: "https://auth.example/authorize", | ||
| 95 | tokenUrl: "https://auth.example/token", | ||
| 96 | scopes: ["read"], | ||
| 97 | }, | ||
| 98 | ]), | ||
| 99 | ); | ||
| 100 | |||
| 101 | expect(methods.map((method) => method.label)).toEqual(["OAuth2 (user)"]); | ||
| 102 | }); | ||
| 103 | |||
| 87 | it("returns [] when no auth template is declared and for a foreign config", () => { | 104 | it("returns [] when no auth template is declared and for a foreign config", () => { |
| 88 | expect(describeOpenApiAuthMethods(recordWith([]))).toEqual([]); | 105 | expect(describeOpenApiAuthMethods(recordWith([]))).toEqual([]); |
| 89 | expect( | 106 | expect( |
| @@ -288,6 +288,7 @@ const AuthenticationSchema = Schema.Union([ · 287 unmodified lines | |||
| 288 | Schema.Struct({ | 288 | Schema.Struct({ |
| 289 | slug: Schema.String, | 289 | slug: Schema.String, |
| 290 | kind: Schema.Literal("oauth2"), | 290 | kind: Schema.Literal("oauth2"), |
| 291 | label: Schema.optional(Schema.String), | ||
| 291 | authorizationUrl: Schema.String, | 292 | authorizationUrl: Schema.String, |
| 292 | tokenUrl: Schema.String, | 293 | tokenUrl: Schema.String, |
| 293 | resource: Schema.optional(Schema.NullOr(Schema.String)), | 294 | resource: Schema.optional(Schema.NullOr(Schema.String)), |
| @@ -580,7 +581,7 @@ export const describeOpenApiAuthMethods = ( · 286 unmodified lines | |||
| 580 | if (template.kind === "oauth2") { | 581 | if (template.kind === "oauth2") { |
| 581 | return { | 582 | return { |
| 582 | id: String(template.slug), | 583 | id: String(template.slug), |
| 583 | label: "OAuth2", | 584 | label: template.label ?? "OAuth2", |
| 584 | kind: "oauth", | 585 | kind: "oauth", |
| 585 | template: String(template.slug), | 586 | template: String(template.slug), |
| 586 | oauth: { | 587 | oauth: { |
| @@ -31,6 +31,9 @@ export type AuthTemplateEditorValue = · 30 unmodified lines | |||
| 31 | | { readonly kind: "apikey"; readonly placements: readonly Placement[] } | 31 | | { readonly kind: "apikey"; readonly placements: readonly Placement[] } |
| 32 | | { | 32 | | { |
| 33 | readonly kind: "oauth"; | 33 | readonly kind: "oauth"; |
| 34 | /** Display label of the stored method, carried through untouched so an | ||
| 35 | * editor round-trip doesn't strip it. Not edited here. */ | ||
| 36 | readonly label?: string; | ||
| 34 | readonly authorizationUrl: string; | 37 | readonly authorizationUrl: string; |
| 35 | readonly tokenUrl: string; | 38 | readonly tokenUrl: string; |
| 36 | readonly resource?: string | null; | 39 | readonly resource?: string | null; |