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:
hugocasa
2026-10-20 10:17:36 +02:00
co-authored by Claude Opus 5.5
parent bf40abe638
commit ba994f260f
9 changed files with 129 additions and 133 deletions
@@ -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 })
}
}