mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-26 08:02:31 +00:00
refactor: create an OAuth token variable and its account in one request
createAccount takes the variable and the store creates both in one transaction, so a connect is either fully saved or not at all. The connect flows make that single call instead of creating the variable and linking it afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bf40abe638
commit
ba994f260f
@@ -988,41 +988,27 @@
|
||||
// OAuth flow: single secret variable for the token
|
||||
if (typeof value == 'string' && value != '' && !value.startsWith('$var:')) {
|
||||
savedVariableCount++
|
||||
// Read once: the path field stays editable while this awaits, and the account
|
||||
// must link the variable this call created.
|
||||
const tokenPath = path
|
||||
await VariableService.createVariable({
|
||||
workspace: effectiveWorkspace,
|
||||
requestBody: {
|
||||
path: tokenPath,
|
||||
value: value,
|
||||
is_secret: true,
|
||||
description: emptyString(description)
|
||||
? `OAuth token for ${resourceType}`
|
||||
: description,
|
||||
is_oauth: true,
|
||||
ws_specific: wsSpecific
|
||||
}
|
||||
})
|
||||
if (accountData) {
|
||||
try {
|
||||
await OauthService.createAccount({
|
||||
workspace: effectiveWorkspace,
|
||||
requestBody: { ...accountData, variable_path: tokenPath }
|
||||
})
|
||||
} catch (e) {
|
||||
// An unlinked token never refreshes, and its path would block a retry. Not when
|
||||
// filling: deleting a variable also deletes the resource at its path.
|
||||
if (!filling) {
|
||||
await VariableService.deleteVariable({
|
||||
workspace: effectiveWorkspace,
|
||||
path: tokenPath
|
||||
}).catch(() => {})
|
||||
}
|
||||
throw e
|
||||
}
|
||||
const tokenVariable = {
|
||||
path,
|
||||
value: value,
|
||||
is_secret: true,
|
||||
description: emptyString(description) ? `OAuth token for ${resourceType}` : description,
|
||||
is_oauth: true,
|
||||
ws_specific: wsSpecific
|
||||
}
|
||||
resourceValue['token'] = `$var:${tokenPath}`
|
||||
if (accountData) {
|
||||
// Creates the variable with the account it refreshes through, in one transaction.
|
||||
await OauthService.createAccount({
|
||||
workspace: effectiveWorkspace,
|
||||
requestBody: { ...accountData, variable: tokenVariable }
|
||||
})
|
||||
} else {
|
||||
await VariableService.createVariable({
|
||||
workspace: effectiveWorkspace,
|
||||
requestBody: tokenVariable
|
||||
})
|
||||
}
|
||||
resourceValue['token'] = `$var:${tokenVariable.path}`
|
||||
}
|
||||
} else if (linkedSecrets.length === 1) {
|
||||
// Single secret: use the resource path as variable name (original behavior)
|
||||
|
||||
@@ -488,7 +488,7 @@ export const variableRequestSchema = z.object({
|
||||
"value": z.string().describe("The value of the variable"),
|
||||
"is_secret": z.boolean().describe("Whether the variable is a secret"),
|
||||
"description": z.string().describe("The description of the variable"),
|
||||
"account": z.number().int().describe("Ignored. A variable is linked to an OAuth account only by `createAccount`'s `variable_path`.").optional(),
|
||||
"account": z.number().int().describe("Ignored. A variable is linked to an OAuth account only when `createAccount` creates both.").optional(),
|
||||
"is_oauth": z.boolean().describe("Whether the variable is an OAuth variable").optional(),
|
||||
"expires_at": z.string().datetime({ offset: true }).describe("The expiration date of the variable").optional(),
|
||||
"labels": z.array(z.string()).optional(),
|
||||
|
||||
@@ -283,20 +283,17 @@
|
||||
path,
|
||||
value: res.access_token,
|
||||
resourcePath: path,
|
||||
isOauth: true
|
||||
isOauth: true,
|
||||
account:
|
||||
res?.expires_in != undefined
|
||||
? {
|
||||
refresh_token: res.refresh_token ?? '',
|
||||
expires_in: res.expires_in,
|
||||
client: t.client,
|
||||
scopes: t.scopes
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
if (res?.expires_in != undefined) {
|
||||
await OauthService.createAccount({
|
||||
workspace,
|
||||
requestBody: {
|
||||
refresh_token: res.refresh_token ?? '',
|
||||
expires_in: res.expires_in,
|
||||
client: t.client,
|
||||
scopes: t.scopes,
|
||||
variable_path: path
|
||||
}
|
||||
})
|
||||
}
|
||||
await createMcpResource(t, `$var:${path}`)
|
||||
sendUserToast(`Connected ${t.label}`)
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
McpOauthService,
|
||||
OauthService,
|
||||
ResourceService,
|
||||
type DiscoverMcpOauthResponse
|
||||
} from '$lib/gen'
|
||||
import { McpOauthService, ResourceService, type DiscoverMcpOauthResponse } from '$lib/gen'
|
||||
import { Button } from '$lib/components/common'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -141,20 +136,17 @@
|
||||
path,
|
||||
value: data.access_token,
|
||||
resourcePath: path,
|
||||
isOauth: true
|
||||
isOauth: true,
|
||||
account:
|
||||
data.expires_in && data.refresh_token
|
||||
? {
|
||||
refresh_token: data.refresh_token,
|
||||
expires_in: data.expires_in,
|
||||
client: 'mcp',
|
||||
mcp_server_url: data.mcp_server_url
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
if (data.expires_in && data.refresh_token) {
|
||||
await OauthService.createAccount({
|
||||
workspace,
|
||||
requestBody: {
|
||||
refresh_token: data.refresh_token,
|
||||
expires_in: data.expires_in,
|
||||
client: 'mcp',
|
||||
mcp_server_url: data.mcp_server_url,
|
||||
variable_path: path
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await ResourceService.createResource({
|
||||
workspace,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResourceService, VariableService } from '$lib/gen'
|
||||
import { OauthService, ResourceService, VariableService } from '$lib/gen'
|
||||
|
||||
/**
|
||||
* What this connection writes in its token variable's description, and the only
|
||||
@@ -33,8 +33,16 @@ export async function upsertSecretVariable(args: {
|
||||
/** The MCP resource this token belongs to; stamped into the description. */
|
||||
resourcePath: string
|
||||
isOauth?: boolean
|
||||
/** The account an expiring token refreshes through, created with the variable and linked to it. */
|
||||
account?: {
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
client: string
|
||||
scopes?: string[]
|
||||
mcp_server_url?: string
|
||||
}
|
||||
}): Promise<void> {
|
||||
const { workspace, path, value, resourcePath, isOauth } = args
|
||||
const { workspace, path, value, resourcePath, isOauth, account } = args
|
||||
const description = mcpTokenDescription(resourcePath)
|
||||
|
||||
// The path picker rejects an occupied path, but it validates on a debounce and
|
||||
@@ -63,8 +71,10 @@ export async function upsertSecretVariable(args: {
|
||||
await VariableService.deleteVariable({ workspace, path })
|
||||
}
|
||||
|
||||
await VariableService.createVariable({
|
||||
workspace,
|
||||
requestBody: { path, value, is_secret: true, is_oauth: isOauth, description }
|
||||
})
|
||||
const variable = { path, value, is_secret: true, is_oauth: isOauth, description }
|
||||
if (account) {
|
||||
await OauthService.createAccount({ workspace, requestBody: { ...account, variable } })
|
||||
} else {
|
||||
await VariableService.createVariable({ workspace, requestBody: variable })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user