Compare commits

..

7 Commits

Author SHA1 Message Date
GitHub Action
50cbf31376 [automated] Apply ESLint and Oxfmt fixes 2026-05-29 01:16:19 +00:00
mattmillerai
b008988055 [chore] Update Ingest API types from cloud@613dcfb 2026-05-29 01:12:45 +00:00
AustinMroz
767bd17077 Fix "open tutorial button" not working in templates (#12511)
The "open tutorial" button only existed in the DOM when the template
card as actively hovered. For reasons I can not comprehend (probably
overzealous pointer handlers somewhere), the act of clicking on the
button would fire a mouseleave event. This caused the button to
disappear for the exact moment it was clicked alike to a mischievous
dondurma vendor.

This is resolved by keeping the button always in DOM, but making it
invisible when the card isn't hovered.

The PR also removes a deeply nested `v-bind='$attrs'`. I'm assuming it
must be a mistake that attributes applied to the entire template
selector dialogue would be bound to every deeply nested tutorial button
on individual workflow cards.
2026-05-28 23:38:15 +00:00
imick-io
0d0231453a fix(website): stack role title above team and location on careers list (#12510)
## Summary
- Long role titles wrapped awkwardly next to the inline department label
on the careers list, especially on narrow viewports.
- Restructured the role link so the title sits on its own row with the
arrow icon on the right, and the department + location wrap together on
a metadata row beneath (16px gap between them).

## Test plan
- [ ] Open `/careers` on mobile width and confirm long titles (e.g.
"Senior Software Engineer, Frontend") no longer collide with the
department label.
- [ ] Confirm desktop layout still reads cleanly.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:49:53 +00:00
Luke Mino-Altherr
cc29a3d72d Add unreviewed merge detector for SOC 2 compliance (#12497)
## Summary

- Adds a GitHub Actions workflow that detects PRs merged to `main`
without an approving review
- Creates tracking issues in
[`Comfy-Org/unreviewed-merges`](https://github.com/Comfy-Org/unreviewed-merges)
(private) for SOC 2 audit purposes
- Supports inline justification via `Justification: <reason>` in PR body
or comments

## How it works

Triggers on `push` to `main`. Uses the GitHub API to find the associated
PR and check for approving reviews. If none found, creates a tracking
issue with the `unreviewed-merge` label. No code checkout required — API
calls only.

## Test plan

- [ ] Verify workflow YAML is valid
- [ ] Merge a test PR without approval and confirm issue creation in
`unreviewed-merges` repo

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-05-28 19:22:46 +00:00
AustinMroz
62430d6311 Remove unneeded overrides, add new ones (#12501)
Adds additional version overrides to handle 16 of the remaining 18
dependabot alerts.

Removes overrides which are no longer needed.
2026-05-28 19:18:54 +00:00
jaeone94
dc8471c6d3 fix: show workflow refresh loading state (#12509)
## Summary

Adds visible loading feedback to the Workflows sidebar refresh button so
users can tell when a workflow sync request is in flight.

## Changes

- **What**: Exposes `isSyncLoading` from the workflow store and binds
the Workflows sidebar refresh button to disabled, `aria-busy`, and
spinning icon states while sync is pending.
- **What**: Adds stable E2E selectors for the workflows refresh button
and covers the loading state with unit and browser tests.
- **Dependencies**: None.

## Review Focus

Please verify the refresh control behavior while
`/api/userdata?dir=workflows` is pending, especially that the button is
disabled, exposes busy state, and returns to idle after sync completes.

## Validation

- `pnpm format`
- `pnpm test:unit
src/components/sidebar/tabs/BaseWorkflowsSidebarTab.test.ts`
- `pnpm test:browser:local browser_tests/tests/sidebar/workflows.spec.ts
-g "Shows loading state while refreshing workflows"`
- `pnpm lint`
- Commit hooks: `oxfmt`, `oxlint`, `eslint`, `typecheck`,
`typecheck:browser`

## Screenshots (if applicable)


https://github.com/user-attachments/assets/e8b893ae-a91d-45c9-81ea-adaf164de227
2026-05-28 17:31:42 +00:00
16 changed files with 1291 additions and 383 deletions

View File

@@ -0,0 +1,24 @@
name: Detect Unreviewed Merge
# SOC 2 compliance — reusable workflow lives in Comfy-Org/github-workflows,
# tracking issues are filed in Comfy-Org/unreviewed-merges.
on:
push:
branches: [main, master]
concurrency:
group: detect-unreviewed-merge-${{ github.sha }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: read
jobs:
detect:
uses: Comfy-Org/github-workflows/.github/workflows/detect-unreviewed-merge.yml@4d9cb6b87f953bb7cd69954280e1465fb9bd2040 # v1
with:
approval-mode: latest-per-reviewer
secrets:
UNREVIEWED_MERGES_TOKEN: ${{ secrets.UNREVIEWED_MERGES_TOKEN }}

View File

@@ -87,8 +87,8 @@ function scrollToDepartment(deptKey: string) {
<template>
<section class="px-6 py-20 md:px-20 md:py-32" data-testid="careers-roles">
<div class="mx-auto max-w-6xl">
<div class="flex flex-col gap-12 md:flex-row md:gap-20">
<div class="shrink-0 md:w-48">
<div class="flex flex-col gap-12 lg:flex-row lg:gap-20">
<div class="shrink-0 lg:min-w-48">
<div
class="bg-primary-comfy-ink sticky top-20 z-10 py-4 md:top-28 md:py-0"
>
@@ -133,30 +133,41 @@ function scrollToDepartment(deptKey: string) {
:href="role.jobUrl"
target="_blank"
rel="noopener noreferrer"
class="border-primary-warm-gray/20 group flex items-center justify-between border-b py-5"
class="border-primary-warm-gray/20 hover:border-primary-comfy-canvas group flex items-center gap-4 border-b py-5 transition-colors duration-200"
data-testid="careers-role-link"
>
<div class="min-w-0">
<div
class="flex min-w-0 flex-1 flex-col md:flex-row md:items-baseline md:gap-x-4"
>
<span
class="text-primary-comfy-canvas text-base font-medium md:text-lg"
>
{{ role.title }}
</span>
<span class="text-primary-warm-gray ml-3 text-sm">
{{ role.department }}
</span>
<div
class="text-primary-warm-gray mt-1 flex flex-wrap gap-x-4 gap-y-1 text-sm md:mt-0 md:contents"
>
<span>{{ role.department }}</span>
<span class="md:hidden">{{ role.location }}</span>
</div>
</div>
<div class="ml-4 flex shrink-0 items-center gap-3">
<span class="text-primary-warm-gray text-sm">
{{ role.location }}
</span>
<img
src="/icons/arrow-up-right.svg"
alt=""
class="size-5"
<span
class="text-primary-warm-gray hidden shrink-0 text-sm md:inline"
>
{{ role.location }}
</span>
<span
class="bg-primary-comfy-yellow/0 group-hover:bg-primary-comfy-yellow relative grid size-7 shrink-0 place-items-center rounded-sm transition-colors duration-300 ease-out"
>
<span
class="bg-primary-comfy-yellow group-hover:bg-primary-comfy-ink size-5 transition-colors duration-300 ease-out"
style="
mask: url('/icons/arrow-up-right.svg') center / contain
no-repeat;
"
aria-hidden="true"
/>
</div>
</span>
</a>
</div>
</div>

View File

@@ -18,7 +18,7 @@ const emit = defineEmits<{
<template>
<nav
class="scrollbar-none flex items-center gap-3 overflow-x-auto lg:flex-col lg:overflow-x-hidden"
class="flex w-full scrollbar-none items-center gap-3 overflow-x-auto lg:flex-col lg:overflow-x-hidden"
aria-label="Category filter"
>
<button

View File

@@ -139,6 +139,7 @@ export class WorkflowsSidebarTab extends SidebarTab {
public readonly root: Locator
public readonly activeWorkflowLabel: Locator
public readonly searchInput: Locator
public readonly refreshButton: Locator
constructor(public override readonly page: Page) {
super(page, 'workflows')
@@ -147,6 +148,9 @@ export class WorkflowsSidebarTab extends SidebarTab {
'.comfyui-workflows-open .p-tree-node-selected .node-label'
)
this.searchInput = this.root.getByRole('combobox').first()
this.refreshButton = this.root.getByTestId(
TestIds.sidebar.workflowsRefreshButton
)
}
async getOpenedWorkflowNames() {

View File

@@ -10,6 +10,7 @@ export const TestIds = {
nodeLibrarySearch: 'node-library-search',
nodePreviewCard: 'node-preview-card',
workflows: 'workflows-sidebar',
workflowsRefreshButton: 'workflows-refresh-button',
modeToggle: 'mode-toggle'
},
tree: {

View File

@@ -1,8 +1,10 @@
import { expect } from '@playwright/test'
import type { Route } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { openErrorsTab } from '@e2e/fixtures/helpers/ErrorsTabHelper'
import type { UserDataFullInfo } from '@/schemas/apiSchema'
test.describe('Workflows sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -45,6 +47,56 @@ test.describe('Workflows sidebar', () => {
.toEqual(expect.arrayContaining(['workflow1', 'workflow2']))
})
test(
'Shows loading state while refreshing workflows',
{ tag: '@smoke' },
async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
const workflowsSyncRoute = /\/api\/userdata\?[^#]*\bdir=workflows\b/
const emptyWorkflowList: UserDataFullInfo[] = []
let releaseSync!: () => void
const syncBlocked = new Promise<void>((resolve) => {
releaseSync = resolve
})
const syncFulfillments: Promise<void>[] = []
const holdSyncResponse = async (route: Route) => {
if (route.request().method() !== 'GET') {
await route.fallback()
return
}
const syncFulfilled = syncBlocked.then(() =>
route.fulfill({ json: emptyWorkflowList })
)
syncFulfillments.push(syncFulfilled)
await syncFulfilled
}
await comfyPage.page.route(workflowsSyncRoute, holdSyncResponse)
try {
const syncRequest = comfyPage.page.waitForRequest((request) =>
workflowsSyncRoute.test(request.url())
)
await tab.refreshButton.click()
await syncRequest
await expect(tab.refreshButton).toBeDisabled()
await expect(tab.refreshButton).toHaveAttribute('aria-busy', 'true')
} finally {
releaseSync()
await Promise.all(syncFulfillments)
await comfyPage.page.unroute(workflowsSyncRoute, holdSyncResponse)
}
await expect(tab.refreshButton).toBeEnabled()
await expect(tab.refreshButton).toHaveAttribute('aria-busy', 'false')
}
)
test('Can duplicate workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1')

View File

@@ -2,6 +2,7 @@ import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import type { WorkflowTemplates } from '@/platform/workflow/templates/types/template'
import { getWav } from '@e2e/fixtures/components/AudioPreview'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
@@ -450,4 +451,57 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
)
}
)
test('Can open associated tutorial', async ({ comfyPage }) => {
const tutorialUrl = 'https://comfyanonymous.github.io/ComfyUI_examples/'
await comfyPage.page.route('**/templates/index.json', async (route) => {
const response = [
{
moduleName: 'default',
title: 'Test Templates',
type: 'image',
templates: [
{
name: 'template-with-tutorial',
title: 'Template with a tutorial',
mediaType: 'audio',
mediaSubtype: 'wav',
description: 'This template has a tutorial',
tutorialUrl
}
]
}
]
await route.fulfill({
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
})
await comfyPage.page.route('**/templates/**.wav', async (route) => {
await route.fulfill({
status: 200,
body: getWav(),
headers: {
'Content-Type': 'image/x-wav',
'Cache-Control': 'no-store'
}
})
})
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
const card = comfyPage.page.getByTestId(
'template-workflow-template-with-tutorial'
)
await card.hover()
const tutorialButton = card.getByRole('button', { name: 'See a tutorial' })
await expect(tutorialButton).toBeVisible()
const popupPromise = comfyPage.page.waitForEvent('popup', { timeout: 0 })
await tutorialButton.click()
const popup = await popupPromise
expect(popup.url()).toEqual(tutorialUrl)
})
})

View File

@@ -356,10 +356,7 @@ export type {
GetModelFoldersResponse,
GetModelFoldersResponses,
GetModelPreviewData,
GetModelPreviewError,
GetModelPreviewErrors,
GetModelPreviewResponse,
GetModelPreviewResponses,
GetModelsInFolderData,
GetModelsInFolderError,
GetModelsInFolderErrors,
@@ -389,8 +386,21 @@ export type {
GetNodeReplacementsErrors,
GetNodeReplacementsResponse,
GetNodeReplacementsResponses,
GetOpenapiSpecData,
GetOpenapiSpecResponses,
GetOAuthAuthorizationServerData,
GetOAuthAuthorizationServerError,
GetOAuthAuthorizationServerErrors,
GetOAuthAuthorizationServerResponse,
GetOAuthAuthorizationServerResponses,
GetOAuthAuthorizeData,
GetOAuthAuthorizeError,
GetOAuthAuthorizeErrors,
GetOAuthAuthorizeResponse,
GetOAuthAuthorizeResponses,
GetOAuthProtectedResourceData,
GetOAuthProtectedResourceError,
GetOAuthProtectedResourceErrors,
GetOAuthProtectedResourceResponse,
GetOAuthProtectedResourceResponses,
GetPaymentPortalData,
GetPaymentPortalError,
GetPaymentPortalErrors,
@@ -427,11 +437,11 @@ export type {
GetSecretErrors,
GetSecretResponse,
GetSecretResponses,
GetSettingByKeyData,
GetSettingByKeyError,
GetSettingByKeyErrors,
GetSettingByKeyResponse,
GetSettingByKeyResponses,
GetSettingByIdData,
GetSettingByIdError,
GetSettingByIdErrors,
GetSettingByIdResponse,
GetSettingByIdResponses,
GetStaticExtensionsData,
GetStaticExtensionsErrors,
GetStaticExtensionsResponses,
@@ -447,6 +457,7 @@ export type {
GetTaskResponses,
GetTemplateProxyData,
GetTemplateProxyErrors,
GetTemplateProxyResponses,
GetUserData,
GetUserdataData,
GetUserdataError,
@@ -534,6 +545,11 @@ export type {
ImportPublishedAssetsResponse,
ImportPublishedAssetsResponse2,
ImportPublishedAssetsResponses,
InsertDynamicConfigData,
InsertDynamicConfigError,
InsertDynamicConfigErrors,
InsertDynamicConfigResponse,
InsertDynamicConfigResponses,
InterruptJobData,
InterruptJobError,
InterruptJobErrors,
@@ -642,6 +658,17 @@ export type {
MoveUserdataFileResponse,
MoveUserdataFileResponses,
NodeInfo,
OAuthAuthorizationServerMetadata,
OAuthAuthorizeRedirectResponse,
OAuthConsentChallenge,
OAuthConsentChallengeWorkspace,
OAuthProtectedResourceMetadata,
OAuthRegisterBadRequestResponse,
OAuthRegisterError,
OAuthRegisterRequest,
OAuthRegisterResponse,
OAuthTokenError,
OAuthTokenResponse,
PaginationInfo,
PartnerUsageRequest,
PartnerUsageResponse,
@@ -663,6 +690,21 @@ export type {
PostMonitoringTasksSubpathData,
PostMonitoringTasksSubpathErrors,
PostMonitoringTasksSubpathResponses,
PostOAuthAuthorizeData,
PostOAuthAuthorizeError,
PostOAuthAuthorizeErrors,
PostOAuthAuthorizeResponse,
PostOAuthAuthorizeResponses,
PostOAuthRegisterData,
PostOAuthRegisterError,
PostOAuthRegisterErrors,
PostOAuthRegisterResponse,
PostOAuthRegisterResponses,
PostOAuthTokenData,
PostOAuthTokenError,
PostOAuthTokenErrors,
PostOAuthTokenResponse,
PostOAuthTokenResponses,
PostPprofSymbolData,
PostPprofSymbolResponses,
PostUserdataFileData,
@@ -799,11 +841,11 @@ export type {
UpdateSecretRequest,
UpdateSecretResponse,
UpdateSecretResponses,
UpdateSettingByKeyData,
UpdateSettingByKeyError,
UpdateSettingByKeyErrors,
UpdateSettingByKeyResponse,
UpdateSettingByKeyResponses,
UpdateSettingByIdData,
UpdateSettingByIdError,
UpdateSettingByIdErrors,
UpdateSettingByIdResponse,
UpdateSettingByIdResponses,
UpdateSubscriptionCacheData,
UpdateSubscriptionCacheError,
UpdateSubscriptionCacheErrors,

View File

@@ -1382,6 +1382,250 @@ export type JwkKey = {
y: string
}
/**
* RFC 6749 §5.2 error response.
*/
export type OAuthTokenError = {
/**
* RFC 6749 §5.2 error code: invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope.
*/
error: string
/**
* Human-readable, no leak of internal storage state.
*/
error_description?: string
}
/**
* RFC 6749 §5.1 successful token response.
*/
export type OAuthTokenResponse = {
/**
* Resource-bound Cloud JWT (audience matches the protected resource).
*/
access_token: string
token_type: 'Bearer'
/**
* Access token lifetime in seconds.
*/
expires_in: number
/**
* Opaque refresh token. Rotates on every successful refresh; presenting an already-rotated token revokes the entire family.
*/
refresh_token: string
/**
* Space-delimited scopes granted with this token.
*/
scope: string
}
/**
* One workspace option presented in the OAuth consent challenge. Promoted to a named schema so the generated Go type is referenceable in handlers and tests rather than re-declared as an anonymous struct at every callsite.
*
*/
export type OAuthConsentChallengeWorkspace = {
id: string
name: string
type: 'personal' | 'team'
role: 'owner' | 'member'
}
/**
* Redirect target produced after a JSON consent submission. The frontend must navigate the browser to this URL so custom-scheme client callbacks work without relying on fetch-visible 302 headers.
*/
export type OAuthAuthorizeRedirectResponse = {
/**
* OAuth client redirect URI with either code+state for allow, or error+state for deny.
*/
redirect_url: string
}
/**
* Server-side state describing the OAuth consent decision the user is being asked to make. Returned by GET /oauth/authorize when a valid Cloud session exists; the frontend renders the consent UI from this payload and POSTs the decision back. Browser never sees the original OAuth params on resume.
*
*/
export type OAuthConsentChallenge = {
/**
* Opaque server-side identifier for the authorization-request row. Carried back unchanged in the consent submission.
*/
oauth_request_id: string
/**
* Per-row CSRF token bound to this authorization request (not to the session). Must be echoed back on POST.
*/
csrf_token: string
/**
* Human-readable name of the OAuth client requesting authorization, from oauth_clients.display_name.
*/
client_display_name: string
/**
* Human-readable name of the protected resource, from oauth_resources.display_name.
*/
resource_display_name: string
/**
* Scopes the client is requesting for this resource. The frontend should present these for the user to approve.
*/
scopes: Array<string>
/**
* Workspaces the user can select from. Membership is re-checked on POST.
*/
workspaces: Array<OAuthConsentChallengeWorkspace>
}
/**
* OAuth 2.1 protected-resource metadata (RFC 9728).
*/
export type OAuthProtectedResourceMetadata = {
resource: string
authorization_servers: Array<string>
scopes_supported: Array<string>
bearer_methods_supported?: Array<string>
}
/**
* RFC 7591 §3.2.2 error response.
*/
export type OAuthRegisterError = {
error: 'invalid_redirect_uri' | 'invalid_client_metadata'
error_description?: string | null
}
/**
* Union of the two 400 shapes /oauth/register can emit. `OAuthRegisterError` is the handler-shaped RFC 7591 §3.2.2 error; `BindingErrorResponse` is the strict-server binding-layer error fired when the request body fails OpenAPI-schema validation before the handler runs.
*
*/
export type OAuthRegisterBadRequestResponse =
| OAuthRegisterError
| BindingErrorResponse
/**
* Error shape returned when request binding or validation fails before the handler runs.
*/
export type BindingErrorResponse = {
message: string
}
/**
* RFC 7591 §3.2.1 successful registration response.
*/
export type OAuthRegisterResponse = {
/**
* Server-generated client_id. Always carries the `comfy-dyn-` prefix.
*/
client_id: string
/**
* Unix timestamp (seconds) when the client was registered.
*/
client_id_issued_at: number
client_name?: string
redirect_uris: Array<string>
grant_types: Array<string>
response_types: Array<string>
token_endpoint_auth_method: 'none'
application_type: 'native' | 'web'
}
/**
* RFC 7591 §2 client metadata document. Only the fields the server honors are listed; presence of `scope` or `resource_grants` in the request is rejected (`invalid_client_metadata`) because those are server-owned for dynamic clients. `additionalProperties: false` mirrors the runtime middleware that rejects any unknown metadata key.
*
*/
export type OAuthRegisterRequest = {
/**
* 15 redirect URIs. Validated against `application_type` policy.
*/
redirect_uris: Array<string>
/**
* Human-readable name shown in the consent UI. Reserved-name list rejects impersonation of major MCP clients.
*/
client_name?: string
/**
* RFC 7591 §2 application_type. **OPTIONAL** — omit the field to default to `native` (the loopback-friendly policy), rather than the RFC's nominal `web` default; the MCP SDK's DCR client omits it. `native` for desktop / CLI / MCP-spec-strict clients (loopback redirects); `web` for hosted clients (HTTPS only, host must be allowlisted). The realistic MCP-client population is overwhelmingly native/loopback, so defaulting to `web` would silently bounce those clients off the wrong redirect policy. A *present* value must be one of the enum members; any other value rejects with `invalid_client_metadata`. (The server has no runtime enum validation and defensively coerces a null/empty value to `native`, but spec-validating clients should omit the field rather than send `""` — the enum does not permit it.)
*
*/
application_type?: 'native' | 'web'
/**
* Public clients only this phase — must be `none` if present. The server forces `none` regardless.
*/
token_endpoint_auth_method?: 'none'
/**
* Optional. Defaults to `["authorization_code","refresh_token"]`.
*/
grant_types?: Array<'authorization_code' | 'refresh_token'>
/**
* Optional. Defaults to `["code"]`.
*/
response_types?: Array<'code'>
/**
* **REJECTED IF PRESENT.** Dynamic clients do not pick scopes — the server assigns scopes from the active MCP resource's published list. Sending `scope` in the registration body is treated as a privilege-escalation attempt and returns `invalid_client_metadata`. The field is documented here so clients see a well-defined error rather than silent drop.
*
*/
scope?: string | null
/**
* **REJECTED IF PRESENT.** Same reason as `scope`. The set of resources and scopes a dynamic client may request is server-policy, not request-driven.
*
*/
resource_grants?: {
[key: string]: Array<string>
} | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
client_uri?: string | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
logo_uri?: string | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
tos_uri?: string | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
policy_uri?: string | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
software_id?: string | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
software_version?: string | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
contacts?: Array<string> | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
jwks?: {
[key: string]: unknown
} | null
/**
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
*/
jwks_uri?: string | null
}
/**
* OAuth 2.1 authorization-server metadata (RFC 8414).
*/
export type OAuthAuthorizationServerMetadata = {
issuer: string
authorization_endpoint: string
token_endpoint: string
jwks_uri: string
/**
* RFC 7591 §3.1 Dynamic Client Registration endpoint. Advertised so MCP-spec-compliant clients can auto-discover and self-register without operator involvement. Present only when DCR is enabled.
*
*/
registration_endpoint?: string
response_types_supported: Array<string>
grant_types_supported: Array<string>
code_challenge_methods_supported: Array<string>
token_endpoint_auth_methods_supported: Array<string>
scopes_supported?: Array<string>
}
/**
* JSON Web Key Set containing the public keys used to verify Cloud JWTs.
*/
@@ -1531,6 +1775,10 @@ export type WorkspaceApiKeyInfo = {
* User-provided label
*/
name: string
/**
* User-provided description of the key's purpose. Limit is byte-based (UTF-8 encoding); 5000 bytes equals 5000 ASCII characters or fewer multi-byte characters.
*/
description: string
/**
* First 8 chars after prefix for display
*/
@@ -1565,6 +1813,10 @@ export type CreateWorkspaceApiKeyResponse = {
* User-provided label
*/
name: string
/**
* User-provided description of the key's purpose. Limit is byte-based (UTF-8 encoding); 5000 bytes equals 5000 ASCII characters or fewer multi-byte characters.
*/
description: string
/**
* The full plaintext API key (only shown once)
*/
@@ -1591,6 +1843,10 @@ export type CreateWorkspaceApiKeyRequest = {
* User-provided label for the key
*/
name: string
/**
* User-provided description of the key's purpose. Limit is byte-based (UTF-8 encoding); 5000 bytes equals 5000 ASCII characters or fewer multi-byte characters.
*/
description?: string
/**
* Optional expiration timestamp
*/
@@ -2270,6 +2526,12 @@ export type ListAssetsResponse = {
* Whether more assets are available beyond this page
*/
has_more: boolean
/**
* Opaque cursor to pass as the `after` query parameter to fetch the
* next page. Omitted from the response when there are no more results.
*
*/
next_cursor?: string
}
/**
@@ -2284,6 +2546,10 @@ export type Asset = {
* Name of the asset file
*/
name: string
/**
* Display name of the asset. Mirrors name for backwards compatibility.
*/
display_name?: string | null
/**
* Blake3 hash of the asset content
*/
@@ -2360,6 +2626,10 @@ export type AssetUpdated = {
* Updated name of the asset
*/
name?: string
/**
* Display name of the asset. Mirrors name for backwards compatibility.
*/
display_name?: string | null
/**
* Blake3 hash of the asset content
*/
@@ -3035,13 +3305,6 @@ export type ExportDownloadUrlResponse = {
expires_at?: string
}
/**
* Error shape returned when request binding or validation fails before the handler runs.
*/
export type BindingErrorResponse = {
message: string
}
/**
* Standard error response with a machine-readable code and human-readable message.
*/
@@ -3124,6 +3387,12 @@ export type ListAssetsResponseWritable = {
* Whether more assets are available beyond this page
*/
has_more: boolean
/**
* Opaque cursor to pass as the `after` query parameter to fetch the
* next page. Omitted from the response when there are no more results.
*
*/
next_cursor?: string
}
/**
@@ -3138,6 +3407,10 @@ export type AssetWritable = {
* Name of the asset file
*/
name: string
/**
* Display name of the asset. Mirrors name for backwards compatibility.
*/
display_name?: string | null
/**
* Blake3 hash of the asset content
*/
@@ -3507,50 +3780,6 @@ export type GetModelsInFolderResponses = {
export type GetModelsInFolderResponse =
GetModelsInFolderResponses[keyof GetModelsInFolderResponses]
export type GetModelPreviewData = {
body?: never
path: {
/**
* The folder name containing the model
*/
folder: string
/**
* The path index (usually 0 for cloud service)
*/
path_index: number
/**
* The model filename (with or without .webp extension)
*/
filename: string
}
query?: never
url: '/api/experiment/models/preview/{folder}/{path_index}/{filename}'
}
export type GetModelPreviewErrors = {
/**
* Model not found or preview not available
*/
404: ErrorResponse
/**
* Internal server error
*/
500: ErrorResponse
}
export type GetModelPreviewError =
GetModelPreviewErrors[keyof GetModelPreviewErrors]
export type GetModelPreviewResponses = {
/**
* Success - Model preview image
*/
200: Blob | File
}
export type GetModelPreviewResponse =
GetModelPreviewResponses[keyof GetModelPreviewResponses]
export type GetLegacyHistoryData = {
body?: never
path?: never
@@ -4012,10 +4241,6 @@ export type ListAssetsData = {
* Sort order
*/
order?: 'asc' | 'desc'
/**
* Filter assets by job IDs (prompt IDs)
*/
job_ids?: Array<string>
/**
* Whether to include public/shared assets in results
*/
@@ -4024,6 +4249,17 @@ export type ListAssetsData = {
* Filter assets by exact content hash
*/
asset_hash?: string
/**
* Opaque cursor for keyset pagination. Pass the `next_cursor` value
* from the previous response to fetch the next page. When provided,
* `offset` is ignored. Cursor pagination is only supported with
* `sort` values `created_at`, `updated_at`, `name`, or `size`;
* requests combining `after` with other sort fields return 400.
* The cursor must have been minted under the same `sort` value used
* in the follow-up request.
*
*/
after?: string
}
url: '/api/assets'
}
@@ -4122,10 +4358,6 @@ export type UploadAssetErrors = {
export type UploadAssetError = UploadAssetErrors[keyof UploadAssetErrors]
export type UploadAssetResponses = {
/**
* Asset already exists (returned existing asset)
*/
200: AssetCreated
/**
* Asset created successfully
*/
@@ -4188,10 +4420,6 @@ export type CreateAssetFromHashError =
CreateAssetFromHashErrors[keyof CreateAssetFromHashErrors]
export type CreateAssetFromHashResponses = {
/**
* Asset reference already exists (returned existing)
*/
200: AssetCreated
/**
* Asset reference created successfully
*/
@@ -5345,19 +5573,19 @@ export type UpdateMultipleSettingsResponses = {
export type UpdateMultipleSettingsResponse =
UpdateMultipleSettingsResponses[keyof UpdateMultipleSettingsResponses]
export type GetSettingByKeyData = {
export type GetSettingByIdData = {
body?: never
path: {
/**
* Setting key to retrieve
* Setting id to retrieve
*/
key: string
id: string
}
query?: never
url: '/api/settings/{key}'
url: '/api/settings/{id}'
}
export type GetSettingByKeyErrors = {
export type GetSettingByIdErrors = {
/**
* Unauthorized
*/
@@ -5368,10 +5596,10 @@ export type GetSettingByKeyErrors = {
404: ErrorResponse
}
export type GetSettingByKeyError =
GetSettingByKeyErrors[keyof GetSettingByKeyErrors]
export type GetSettingByIdError =
GetSettingByIdErrors[keyof GetSettingByIdErrors]
export type GetSettingByKeyResponses = {
export type GetSettingByIdResponses = {
/**
* Setting value response
*/
@@ -5383,25 +5611,25 @@ export type GetSettingByKeyResponses = {
}
}
export type GetSettingByKeyResponse =
GetSettingByKeyResponses[keyof GetSettingByKeyResponses]
export type GetSettingByIdResponse =
GetSettingByIdResponses[keyof GetSettingByIdResponses]
export type UpdateSettingByKeyData = {
export type UpdateSettingByIdData = {
/**
* New value for the setting
*/
body: unknown
path: {
/**
* Setting key to update
* Setting id to update
*/
key: string
id: string
}
query?: never
url: '/api/settings/{key}'
url: '/api/settings/{id}'
}
export type UpdateSettingByKeyErrors = {
export type UpdateSettingByIdErrors = {
/**
* Invalid request
*/
@@ -5412,10 +5640,10 @@ export type UpdateSettingByKeyErrors = {
401: ErrorResponse
}
export type UpdateSettingByKeyError =
UpdateSettingByKeyErrors[keyof UpdateSettingByKeyErrors]
export type UpdateSettingByIdError =
UpdateSettingByIdErrors[keyof UpdateSettingByIdErrors]
export type UpdateSettingByKeyResponses = {
export type UpdateSettingByIdResponses = {
/**
* Updated setting value response
*/
@@ -5427,8 +5655,8 @@ export type UpdateSettingByKeyResponses = {
}
}
export type UpdateSettingByKeyResponse =
UpdateSettingByKeyResponses[keyof UpdateSettingByKeyResponses]
export type UpdateSettingByIdResponse =
UpdateSettingByIdResponses[keyof UpdateSettingByIdResponses]
export type SubmitFeedbackData = {
body: FeedbackRequest
@@ -5916,40 +6144,6 @@ export type UploadMaskResponses = {
* Type of upload (e.g., "output")
*/
type?: string
/**
* Additional metadata for mask detection and re-editing
*/
metadata?: {
/**
* Whether this file is a mask
*/
is_mask?: boolean
/**
* Hash of the original unmasked image
*/
original_hash?: string
/**
* Type of mask (e.g., "painted_masked")
*/
mask_type?: string
/**
* Related mask layer files (if available)
*/
related_files?: {
/**
* Hash of the mask layer
*/
mask?: string
/**
* Hash of the paint layer
*/
paint?: string
/**
* Hash of the painted image
*/
painted?: string
}
}
}
}
@@ -6117,6 +6311,229 @@ export type GetJwksResponses = {
export type GetJwksResponse = GetJwksResponses[keyof GetJwksResponses]
export type GetOAuthAuthorizationServerData = {
body?: never
path?: never
query?: never
url: '/.well-known/oauth-authorization-server'
}
export type GetOAuthAuthorizationServerErrors = {
/**
* OAuth disabled
*/
404: ErrorResponse
}
export type GetOAuthAuthorizationServerError =
GetOAuthAuthorizationServerErrors[keyof GetOAuthAuthorizationServerErrors]
export type GetOAuthAuthorizationServerResponses = {
/**
* Authorization-server metadata
*/
200: OAuthAuthorizationServerMetadata
}
export type GetOAuthAuthorizationServerResponse =
GetOAuthAuthorizationServerResponses[keyof GetOAuthAuthorizationServerResponses]
export type GetOAuthProtectedResourceData = {
body?: never
path?: never
query?: never
url: '/.well-known/oauth-protected-resource'
}
export type GetOAuthProtectedResourceErrors = {
/**
* OAuth disabled or no active resource configured
*/
404: ErrorResponse
}
export type GetOAuthProtectedResourceError =
GetOAuthProtectedResourceErrors[keyof GetOAuthProtectedResourceErrors]
export type GetOAuthProtectedResourceResponses = {
/**
* Protected-resource metadata
*/
200: OAuthProtectedResourceMetadata
}
export type GetOAuthProtectedResourceResponse =
GetOAuthProtectedResourceResponses[keyof GetOAuthProtectedResourceResponses]
export type GetOAuthAuthorizeData = {
body?: never
path?: never
query?: {
response_type?: string
client_id?: string
redirect_uri?: string
scope?: string
/**
* RFC 6749 §10.12 marks `state` as RECOMMENDED. Our hardening makes
* it REQUIRED on the initial-entry path (omitted only on the resume
* path where `oauth_request_id` is supplied instead). This parameter
* is `required: false` at the spec level only because the operation
* is dual-mode (initial entry vs. resume); the runtime parser
* (services/ingest/server/implementation/oauth/protocol/request.go)
* rejects empty `state` on the initial-entry path with a stable
* `invalid_request` 400.
*
*/
state?: string
code_challenge?: string
code_challenge_method?: string
resource?: string
oauth_request_id?: string
}
url: '/oauth/authorize'
}
export type GetOAuthAuthorizeErrors = {
/**
* Invalid authorize request (pre-redirect failure — unknown client, redirect mismatch, malformed params)
*/
400: ErrorResponse
/**
* OAuth disabled
*/
404: ErrorResponse
}
export type GetOAuthAuthorizeError =
GetOAuthAuthorizeErrors[keyof GetOAuthAuthorizeErrors]
export type GetOAuthAuthorizeResponses = {
/**
* Consent challenge payload (cookie present, email verified). Frontend renders the consent UI from this payload and POSTs back to /oauth/authorize.
*
*/
200: OAuthConsentChallenge
}
export type GetOAuthAuthorizeResponse =
GetOAuthAuthorizeResponses[keyof GetOAuthAuthorizeResponses]
export type PostOAuthAuthorizeData = {
body: {
oauth_request_id: string
csrf_token: string
decision: 'allow' | 'deny'
workspace_id: string
}
path?: never
query?: never
url: '/oauth/authorize'
}
export type PostOAuthAuthorizeErrors = {
/**
* Bad request (CSRF mismatch, expired/consumed request, inaccessible workspace)
*/
400: ErrorResponse
/**
* Scope broadening on consent re-grant — fresh consent flow required
*/
403: ErrorResponse
/**
* OAuth disabled
*/
404: ErrorResponse
}
export type PostOAuthAuthorizeError =
PostOAuthAuthorizeErrors[keyof PostOAuthAuthorizeErrors]
export type PostOAuthAuthorizeResponses = {
/**
* Redirect URL for the frontend to navigate to (allow → with code+state; deny → with error+state)
*/
200: OAuthAuthorizeRedirectResponse
}
export type PostOAuthAuthorizeResponse =
PostOAuthAuthorizeResponses[keyof PostOAuthAuthorizeResponses]
export type PostOAuthTokenData = {
body: {
grant_type: 'authorization_code' | 'refresh_token'
client_id: string
code?: string
redirect_uri?: string
code_verifier?: string
refresh_token?: string
scope?: string
client_secret?: string
}
path?: never
query?: never
url: '/oauth/token'
}
export type PostOAuthTokenErrors = {
/**
* RFC 6749 §5.2 error
*/
400: OAuthTokenError
/**
* OAuth disabled
*/
404: ErrorResponse
}
export type PostOAuthTokenError =
PostOAuthTokenErrors[keyof PostOAuthTokenErrors]
export type PostOAuthTokenResponses = {
/**
* New token pair
*/
200: OAuthTokenResponse
}
export type PostOAuthTokenResponse =
PostOAuthTokenResponses[keyof PostOAuthTokenResponses]
export type PostOAuthRegisterData = {
body: OAuthRegisterRequest
path?: never
query?: never
url: '/oauth/register'
}
export type PostOAuthRegisterErrors = {
/**
* Bad request. Two shapes possible: `OAuthRegisterError` (RFC 7591 §3.2.2, emitted by the handler for invalid client metadata, missing application_type, reserved client_name, etc.) OR `BindingErrorResponse` (emitted by the strict-server binding layer when the request body fails OpenAPI-schema validation — malformed JSON, missing required fields, `additionalProperties: false` violations).
*
*/
400: OAuthRegisterBadRequestResponse
/**
* OAuth disabled
*/
404: ErrorResponse
/**
* No active MCP resource is configured — DCR cannot mint a usable client until ops seeds an active oauth_resources row.
*/
503: ErrorResponse
}
export type PostOAuthRegisterError =
PostOAuthRegisterErrors[keyof PostOAuthRegisterErrors]
export type PostOAuthRegisterResponses = {
/**
* Registered. Body echoes the metadata RFC 7591 §3.2.1 requires.
*/
201: OAuthRegisterResponse
}
export type PostOAuthRegisterResponse =
PostOAuthRegisterResponses[keyof PostOAuthRegisterResponses]
export type ListWorkspacesData = {
body?: never
path?: never
@@ -6679,7 +7096,7 @@ export type CreateWorkspaceApiKeyErrors = {
*/
401: ErrorResponse
/**
* Not a workspace member or personal workspace
* Not a workspace member
*/
403: ErrorResponse
/**
@@ -7140,6 +7557,51 @@ export type UpdateSubscriptionCacheResponses = {
export type UpdateSubscriptionCacheResponse =
UpdateSubscriptionCacheResponses[keyof UpdateSubscriptionCacheResponses]
export type InsertDynamicConfigData = {
/**
* A valid dynamicconfig.Config JSON object.
*/
body: {
[key: string]: unknown
}
path?: never
query?: never
url: '/admin/api/dynamic-config'
}
export type InsertDynamicConfigErrors = {
/**
* Invalid or missing request body
*/
400: ErrorResponse
/**
* Database insert failed
*/
500: ErrorResponse
}
export type InsertDynamicConfigError =
InsertDynamicConfigErrors[keyof InsertDynamicConfigErrors]
export type InsertDynamicConfigResponses = {
/**
* Config inserted successfully
*/
201: {
/**
* The database ID of the newly inserted config row.
*/
id?: number
/**
* Human-readable success message.
*/
message?: string
}
}
export type InsertDynamicConfigResponse =
InsertDynamicConfigResponses[keyof InsertDynamicConfigResponses]
export type SyncApiKeyData = {
body: SyncApiKeyRequest
path?: never
@@ -8888,9 +9350,20 @@ export type GetTemplateProxyData = {
export type GetTemplateProxyErrors = {
/**
* Template not found
* Template not found.
*/
404: unknown
/**
* Workflow templates version not available.
*/
503: unknown
}
export type GetTemplateProxyResponses = {
/**
* Template file content streamed from GCS.
*/
200: unknown
}
export type GetHealthData = {
@@ -8918,20 +9391,6 @@ export type GetHealthResponses = {
export type GetHealthResponse = GetHealthResponses[keyof GetHealthResponses]
export type GetOpenapiSpecData = {
body?: never
path?: never
query?: never
url: '/openapi'
}
export type GetOpenapiSpecResponses = {
/**
* OpenAPI specification document
*/
200: unknown
}
export type GetMonitoringTasksData = {
body?: never
path?: never
@@ -9194,6 +9653,33 @@ export type PostCustomNodeProxyResponses = {
200: unknown
}
export type GetModelPreviewData = {
body?: never
path: {
/**
* The folder name containing the model.
*/
folder: string
/**
* The path index (usually 0 for cloud service).
*/
path_index: number
/**
* The model filename (with or without .webp extension).
*/
filename: string
}
query?: never
url: '/api/experiment/models/preview/{folder}/{path_index}/{filename}'
}
export type GetModelPreviewErrors = {
/**
* Preview not available on Cloud
*/
404: unknown
}
export type GetLegacyPromptByIdData = {
body?: never
path: {

View File

@@ -879,6 +879,153 @@ export const zJwkKey = z.object({
y: z.string()
})
/**
* RFC 6749 §5.2 error response.
*/
export const zOAuthTokenError = z.object({
error: z.string(),
error_description: z.string().optional()
})
/**
* RFC 6749 §5.1 successful token response.
*/
export const zOAuthTokenResponse = z.object({
access_token: z.string(),
token_type: z.enum(['Bearer']),
expires_in: z.number().int(),
refresh_token: z.string(),
scope: z.string()
})
/**
* One workspace option presented in the OAuth consent challenge. Promoted to a named schema so the generated Go type is referenceable in handlers and tests rather than re-declared as an anonymous struct at every callsite.
*
*/
export const zOAuthConsentChallengeWorkspace = z.object({
id: z.string(),
name: z.string(),
type: z.enum(['personal', 'team']),
role: z.enum(['owner', 'member'])
})
/**
* Redirect target produced after a JSON consent submission. The frontend must navigate the browser to this URL so custom-scheme client callbacks work without relying on fetch-visible 302 headers.
*/
export const zOAuthAuthorizeRedirectResponse = z.object({
redirect_url: z.string().url()
})
/**
* Server-side state describing the OAuth consent decision the user is being asked to make. Returned by GET /oauth/authorize when a valid Cloud session exists; the frontend renders the consent UI from this payload and POSTs the decision back. Browser never sees the original OAuth params on resume.
*
*/
export const zOAuthConsentChallenge = z.object({
oauth_request_id: z.string().uuid(),
csrf_token: z.string(),
client_display_name: z.string(),
resource_display_name: z.string(),
scopes: z.array(z.string()),
workspaces: z.array(zOAuthConsentChallengeWorkspace)
})
/**
* OAuth 2.1 protected-resource metadata (RFC 9728).
*/
export const zOAuthProtectedResourceMetadata = z.object({
resource: z.string().url(),
authorization_servers: z.array(z.string().url()),
scopes_supported: z.array(z.string()),
bearer_methods_supported: z.array(z.string()).optional()
})
/**
* RFC 7591 §3.2.2 error response.
*/
export const zOAuthRegisterError = z.object({
error: z.enum(['invalid_redirect_uri', 'invalid_client_metadata']),
error_description: z.string().nullish()
})
/**
* Error shape returned when request binding or validation fails before the handler runs.
*/
export const zBindingErrorResponse = z.object({
message: z.string()
})
/**
* Union of the two 400 shapes /oauth/register can emit. `OAuthRegisterError` is the handler-shaped RFC 7591 §3.2.2 error; `BindingErrorResponse` is the strict-server binding-layer error fired when the request body fails OpenAPI-schema validation before the handler runs.
*
*/
export const zOAuthRegisterBadRequestResponse = z.union([
zOAuthRegisterError,
zBindingErrorResponse
])
/**
* RFC 7591 §3.2.1 successful registration response.
*/
export const zOAuthRegisterResponse = z.object({
client_id: z.string(),
client_id_issued_at: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
})
.max(BigInt('9223372036854775807'), {
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
}),
client_name: z.string().optional(),
redirect_uris: z.array(z.string()),
grant_types: z.array(z.string()),
response_types: z.array(z.string()),
token_endpoint_auth_method: z.enum(['none']),
application_type: z.enum(['native', 'web'])
})
/**
* RFC 7591 §2 client metadata document. Only the fields the server honors are listed; presence of `scope` or `resource_grants` in the request is rejected (`invalid_client_metadata`) because those are server-owned for dynamic clients. `additionalProperties: false` mirrors the runtime middleware that rejects any unknown metadata key.
*
*/
export const zOAuthRegisterRequest = z.object({
redirect_uris: z.array(z.string()).min(1).max(5),
client_name: z.string().max(100).optional(),
application_type: z.enum(['native', 'web']).optional(),
token_endpoint_auth_method: z.enum(['none']).optional(),
grant_types: z
.array(z.enum(['authorization_code', 'refresh_token']))
.optional(),
response_types: z.array(z.enum(['code'])).optional(),
scope: z.string().nullish(),
resource_grants: z.record(z.array(z.string())).nullish(),
client_uri: z.string().nullish(),
logo_uri: z.string().nullish(),
tos_uri: z.string().nullish(),
policy_uri: z.string().nullish(),
software_id: z.string().nullish(),
software_version: z.string().nullish(),
contacts: z.array(z.string()).nullish(),
jwks: z.record(z.unknown()).nullish(),
jwks_uri: z.string().nullish()
})
/**
* OAuth 2.1 authorization-server metadata (RFC 8414).
*/
export const zOAuthAuthorizationServerMetadata = z.object({
issuer: z.string().url(),
authorization_endpoint: z.string().url(),
token_endpoint: z.string().url(),
jwks_uri: z.string().url(),
registration_endpoint: z.string().url().optional(),
response_types_supported: z.array(z.string()),
grant_types_supported: z.array(z.string()),
code_challenge_methods_supported: z.array(z.string()),
token_endpoint_auth_methods_supported: z.array(z.string()),
scopes_supported: z.array(z.string()).optional()
})
/**
* JSON Web Key Set containing the public keys used to verify Cloud JWTs.
*/
@@ -940,6 +1087,7 @@ export const zWorkspaceApiKeyInfo = z.object({
workspace_id: z.string(),
user_id: z.string(),
name: z.string(),
description: z.string().max(5000),
key_prefix: z.string(),
expires_at: z.string().datetime().optional(),
last_used_at: z.string().datetime().optional(),
@@ -960,6 +1108,7 @@ export const zListWorkspaceApiKeysResponse = z.object({
export const zCreateWorkspaceApiKeyResponse = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().max(5000),
key: z.string(),
key_prefix: z.string(),
expires_at: z.string().datetime().optional(),
@@ -971,6 +1120,7 @@ export const zCreateWorkspaceApiKeyResponse = z.object({
*/
export const zCreateWorkspaceApiKeyRequest = z.object({
name: z.string(),
description: z.string().max(5000).optional(),
expires_at: z.string().datetime().optional()
})
@@ -1353,6 +1503,7 @@ export const zListTagsResponse = z.object({
export const zAsset = z.object({
id: z.string().uuid(),
name: z.string(),
display_name: z.string().nullish(),
asset_hash: z
.string()
.regex(/^blake3:[a-f0-9]{64}$/)
@@ -1385,7 +1536,8 @@ export const zAsset = z.object({
export const zListAssetsResponse = z.object({
assets: z.array(zAsset),
total: z.number().int(),
has_more: z.boolean()
has_more: z.boolean(),
next_cursor: z.string().optional()
})
/**
@@ -1394,6 +1546,7 @@ export const zListAssetsResponse = z.object({
export const zAssetUpdated = z.object({
id: z.string().uuid(),
name: z.string().optional(),
display_name: z.string().nullish(),
asset_hash: z
.string()
.regex(/^blake3:[a-f0-9]{64}$/)
@@ -1753,13 +1906,6 @@ export const zExportDownloadUrlResponse = z.object({
expires_at: z.string().datetime().optional()
})
/**
* Error shape returned when request binding or validation fails before the handler runs.
*/
export const zBindingErrorResponse = z.object({
message: z.string()
})
/**
* Standard error response with a machine-readable code and human-readable message.
*/
@@ -1796,6 +1942,7 @@ export const zPromptRequest = z.object({
export const zAssetWritable = z.object({
id: z.string().uuid(),
name: z.string(),
display_name: z.string().nullish(),
asset_hash: z
.string()
.regex(/^blake3:[a-f0-9]{64}$/)
@@ -1827,7 +1974,8 @@ export const zAssetWritable = z.object({
export const zListAssetsResponseWritable = z.object({
assets: z.array(zAssetWritable),
total: z.number().int(),
has_more: z.boolean()
has_more: z.boolean(),
next_cursor: z.string().optional()
})
/**
@@ -1961,21 +2109,6 @@ export const zGetModelsInFolderData = z.object({
*/
export const zGetModelsInFolderResponse = z.array(zModelFile)
export const zGetModelPreviewData = z.object({
body: z.never().optional(),
path: z.object({
folder: z.string(),
path_index: z.number().int(),
filename: z.string()
}),
query: z.never().optional()
})
/**
* Success - Model preview image
*/
export const zGetModelPreviewResponse = z.string()
export const zGetLegacyHistoryData = z.object({
body: z.never().optional(),
path: z.never().optional(),
@@ -2132,9 +2265,9 @@ export const zListAssetsData = z.object({
.enum(['name', 'created_at', 'updated_at', 'size', 'last_access_time'])
.optional(),
order: z.enum(['asc', 'desc']).optional(),
job_ids: z.array(z.string().uuid()).optional(),
include_public: z.boolean().optional().default(true),
asset_hash: z.string().optional()
asset_hash: z.string().optional(),
after: z.string().optional()
})
.optional()
})
@@ -2157,7 +2290,7 @@ export const zUploadAssetData = z.object({
})
/**
* Asset already exists (returned existing asset)
* Asset created successfully
*/
export const zUploadAssetResponse = zAssetCreated
@@ -2174,7 +2307,7 @@ export const zCreateAssetFromHashData = z.object({
})
/**
* Asset reference already exists (returned existing)
* Asset reference created successfully
*/
export const zCreateAssetFromHashResponse = zAssetCreated
@@ -2509,10 +2642,10 @@ export const zUpdateMultipleSettingsData = z.object({
*/
export const zUpdateMultipleSettingsResponse = z.record(z.unknown())
export const zGetSettingByKeyData = z.object({
export const zGetSettingByIdData = z.object({
body: z.never().optional(),
path: z.object({
key: z.string()
id: z.string()
}),
query: z.never().optional()
})
@@ -2520,14 +2653,14 @@ export const zGetSettingByKeyData = z.object({
/**
* Setting value response
*/
export const zGetSettingByKeyResponse = z.object({
export const zGetSettingByIdResponse = z.object({
value: z.unknown().optional()
})
export const zUpdateSettingByKeyData = z.object({
export const zUpdateSettingByIdData = z.object({
body: z.unknown(),
path: z.object({
key: z.string()
id: z.string()
}),
query: z.never().optional()
})
@@ -2535,7 +2668,7 @@ export const zUpdateSettingByKeyData = z.object({
/**
* Updated setting value response
*/
export const zUpdateSettingByKeyResponse = z.object({
export const zUpdateSettingByIdResponse = z.object({
value: z.unknown().optional()
})
@@ -2691,21 +2824,7 @@ export const zUploadMaskData = z.object({
export const zUploadMaskResponse = z.object({
name: z.string().optional(),
subfolder: z.string().optional(),
type: z.string().optional(),
metadata: z
.object({
is_mask: z.boolean().optional(),
original_hash: z.string().optional(),
mask_type: z.string().optional(),
related_files: z
.object({
mask: z.string().optional(),
paint: z.string().optional(),
painted: z.string().optional()
})
.optional()
})
.optional()
type: z.string().optional()
})
export const zGetLogsData = z.object({
@@ -2774,6 +2893,101 @@ export const zGetJwksData = z.object({
*/
export const zGetJwksResponse = zJwksResponse
export const zGetOAuthAuthorizationServerData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
/**
* Authorization-server metadata
*/
export const zGetOAuthAuthorizationServerResponse =
zOAuthAuthorizationServerMetadata
export const zGetOAuthProtectedResourceData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
/**
* Protected-resource metadata
*/
export const zGetOAuthProtectedResourceResponse =
zOAuthProtectedResourceMetadata
export const zGetOAuthAuthorizeData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z
.object({
response_type: z.string().optional(),
client_id: z.string().optional(),
redirect_uri: z.string().optional(),
scope: z.string().optional(),
state: z.string().optional(),
code_challenge: z.string().optional(),
code_challenge_method: z.string().optional(),
resource: z.string().optional(),
oauth_request_id: z.string().optional()
})
.optional()
})
/**
* Consent challenge payload (cookie present, email verified). Frontend renders the consent UI from this payload and POSTs back to /oauth/authorize.
*
*/
export const zGetOAuthAuthorizeResponse = zOAuthConsentChallenge
export const zPostOAuthAuthorizeData = z.object({
body: z.object({
oauth_request_id: z.string().uuid(),
csrf_token: z.string(),
decision: z.enum(['allow', 'deny']),
workspace_id: z.string()
}),
path: z.never().optional(),
query: z.never().optional()
})
/**
* Redirect URL for the frontend to navigate to (allow → with code+state; deny → with error+state)
*/
export const zPostOAuthAuthorizeResponse = zOAuthAuthorizeRedirectResponse
export const zPostOAuthTokenData = z.object({
body: z.object({
grant_type: z.enum(['authorization_code', 'refresh_token']),
client_id: z.string(),
code: z.string().optional(),
redirect_uri: z.string().optional(),
code_verifier: z.string().optional(),
refresh_token: z.string().optional(),
scope: z.string().optional(),
client_secret: z.string().optional()
}),
path: z.never().optional(),
query: z.never().optional()
})
/**
* New token pair
*/
export const zPostOAuthTokenResponse = zOAuthTokenResponse
export const zPostOAuthRegisterData = z.object({
body: zOAuthRegisterRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* Registered. Body echoes the metadata RFC 7591 §3.2.1 requires.
*/
export const zPostOAuthRegisterResponse = zOAuthRegisterResponse
export const zListWorkspacesData = z.object({
body: z.never().optional(),
path: z.never().optional(),
@@ -3078,6 +3292,28 @@ export const zUpdateSubscriptionCacheResponse = z.object({
status: z.string().optional()
})
export const zInsertDynamicConfigData = z.object({
body: z.record(z.unknown()),
path: z.never().optional(),
query: z.never().optional()
})
/**
* Config inserted successfully
*/
export const zInsertDynamicConfigResponse = z.object({
id: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
})
.max(BigInt('9223372036854775807'), {
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
})
.optional(),
message: z.string().optional()
})
export const zSyncApiKeyData = z.object({
body: zSyncApiKeyRequest,
path: z.never().optional(),
@@ -3671,12 +3907,6 @@ export const zGetHealthData = z.object({
*/
export const zGetHealthResponse = z.string()
export const zGetOpenapiSpecData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
export const zGetMonitoringTasksData = z.object({
body: z.never().optional(),
path: z.never().optional(),
@@ -3757,6 +3987,16 @@ export const zPostCustomNodeProxyData = z.object({
query: z.never().optional()
})
export const zGetModelPreviewData = z.object({
body: z.never().optional(),
path: z.object({
folder: z.string(),
path_index: z.number().int(),
filename: z.string()
}),
query: z.never().optional()
})
export const zGetLegacyPromptByIdData = z.object({
body: z.never().optional(),
path: z.object({

243
pnpm-lock.yaml generated
View File

@@ -407,9 +407,14 @@ overrides:
vite: ^8.0.13
'@tiptap/pm': 2.27.2
'@types/eslint': '-'
protobufjs: ~7.6.0
flatted: ~3.4.2
defu: ~6.1.7
lodash: ^4.18.0
yaml: ^2.8.3
minimatch@^9.0.0: ^9.0.7
minimatch@^10.0.0: ^10.2.3
brace-expansion@^1.0.0: ^1.1.13
brace-expansion@^2.0.0: ^2.0.3
ajv@^8.0.0: ^8.18.0
ws@^8.0.0: ^8.20.1
importers:
@@ -634,7 +639,7 @@ importers:
version: 4.1.1(eslint@9.39.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.4.0(eslint@9.39.1(jiti@2.6.1)))(yaml-eslint-parser@1.3.0)
'@lobehub/i18n-cli':
specifier: 'catalog:'
version: 1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.19.0)(zod@3.25.76)
version: 1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.21.0)(zod@3.25.76)
'@pinia/testing':
specifier: 'catalog:'
version: 1.0.3(pinia@3.0.4(typescript@5.9.3)(vue@3.5.34(typescript@5.9.3)))
@@ -4466,7 +4471,7 @@ packages:
ajv-draft-04@1.0.0:
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
peerDependencies:
ajv: ^8.5.0
ajv: ^8.18.0
peerDependenciesMeta:
ajv:
optional: true
@@ -4474,7 +4479,7 @@ packages:
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
ajv: ^8.0.0
ajv: ^8.18.0
peerDependenciesMeta:
ajv:
optional: true
@@ -4482,12 +4487,6 @@ packages:
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ajv@8.12.0:
resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
ajv@8.13.0:
resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==}
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
@@ -4683,15 +4682,15 @@ packages:
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
engines: {node: '>=18'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@1.1.15:
resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
brace-expansion@2.1.1:
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
brace-expansion@5.0.2:
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
engines: {node: 20 || >=22}
brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -5128,8 +5127,8 @@ packages:
resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==}
engines: {node: '>=18'}
devalue@5.6.4:
resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==}
devalue@5.8.1:
resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
@@ -5576,8 +5575,8 @@ packages:
resolution: {integrity: sha512-dBR+30yHAqBGvOuxxQdnn2lTLHCO6r/9B+M4yF8mNrzr3u1yiF+YVJ6u3GTyPN/VRWqaE1FcscZDdBgVKmrmQQ==}
engines: {node: '>=18.2.0'}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
fastest-levenshtein@1.0.16:
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
@@ -6262,9 +6261,9 @@ packages:
engines: {node: '>=14'}
hasBin: true
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-cookie@3.0.7:
resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==}
engines: {node: '>=20'}
js-stringify@1.0.2:
resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==}
@@ -6523,8 +6522,8 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.23:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
@@ -6535,8 +6534,8 @@ packages:
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
log-update@6.1.0:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
@@ -6808,10 +6807,6 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
minimatch@10.2.1:
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
engines: {node: 20 || >=22}
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22}
@@ -6819,20 +6814,16 @@ packages:
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
minimatch@5.1.9:
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
engines: {node: '>=10'}
minimatch@8.0.4:
resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==}
minimatch@8.0.7:
resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==}
engines: {node: '>=16 || 14 >=14.17'}
minimatch@9.0.1:
resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
engines: {node: '>=16 || 14 >=14.17'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
@@ -7014,7 +7005,7 @@ packages:
resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==}
hasBin: true
peerDependencies:
ws: ^8.18.0
ws: ^8.20.1
zod: ^3.23.8
peerDependenciesMeta:
ws:
@@ -7182,8 +7173,8 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
picomatch@2.3.2:
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
engines: {node: '>=8.6'}
picomatch@4.0.4:
@@ -8493,7 +8484,7 @@ packages:
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
yaml: ^2.8.3
peerDependenciesMeta:
'@types/node':
optional: true
@@ -8876,8 +8867,8 @@ packages:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -8945,16 +8936,6 @@ packages:
resolution: {integrity: sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==}
hasBin: true
yaml@2.7.1:
resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==}
engines: {node: '>= 14'}
hasBin: true
yaml@2.8.2:
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
engines: {node: '>= 14.6'}
hasBin: true
yaml@2.9.0:
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
engines: {node: '>= 14.6'}
@@ -9269,7 +9250,7 @@ snapshots:
'@astrojs/yaml2ts@0.2.3':
dependencies:
yaml: 2.8.2
yaml: 2.9.0
'@atlaskit/pragmatic-drag-and-drop@1.3.1':
dependencies:
@@ -10359,7 +10340,7 @@ snapshots:
js-yaml: 4.1.1
json5: 2.2.3
jsonc-eslint-parser: 2.4.0
lodash: 4.17.23
lodash: 4.18.1
parse5: 7.3.0
semver: 7.7.4
synckit: 0.10.4
@@ -10444,7 +10425,7 @@ snapshots:
- react-devtools-core
- utf-8-validate
'@lobehub/i18n-cli@1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.19.0)(zod@3.25.76)':
'@lobehub/i18n-cli@1.26.1(@types/react@19.1.9)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.21.0)(zod@3.25.76)':
dependencies:
'@lobehub/cli-ui': 1.13.0(@types/react@19.1.9)
'@yutengjing/eld': 0.0.2
@@ -10462,8 +10443,8 @@ snapshots:
ink: 6.8.0(@types/react@19.1.9)(react@19.2.4)
json-stable-stringify: 1.3.0
just-diff: 6.0.2
lodash-es: 4.17.23
openai: 4.104.0(ws@8.19.0)(zod@3.25.76)
lodash-es: 4.18.1
openai: 4.104.0(ws@8.21.0)(zod@3.25.76)
p-map: 7.0.4
pangu: 4.0.7
react: 19.2.4
@@ -10513,8 +10494,8 @@ snapshots:
'@rushstack/terminal': 0.22.1(@types/node@24.10.4)
'@rushstack/ts-command-line': 5.3.1(@types/node@24.10.4)
diff: 8.0.3
lodash: 4.17.23
minimatch: 10.2.1
lodash: 4.18.1
minimatch: 10.2.4
resolve: 1.22.11
semver: 7.5.4
source-map: 0.6.1
@@ -10525,7 +10506,7 @@ snapshots:
'@microsoft/tsdoc-config@0.18.0':
dependencies:
'@microsoft/tsdoc': 0.16.0
ajv: 8.12.0
ajv: 8.18.0
jju: 1.4.0
resolve: 1.22.11
@@ -11071,7 +11052,7 @@ snapshots:
'@rollup/pluginutils@4.2.1':
dependencies:
estree-walker: 2.0.2
picomatch: 2.3.1
picomatch: 2.3.2
'@rollup/pluginutils@5.3.0':
dependencies:
@@ -11081,9 +11062,9 @@ snapshots:
'@rushstack/node-core-library@5.20.1(@types/node@24.10.4)':
dependencies:
ajv: 8.13.0
ajv-draft-04: 1.0.0(ajv@8.13.0)
ajv-formats: 3.0.1(ajv@8.13.0)
ajv: 8.18.0
ajv-draft-04: 1.0.0(ajv@8.18.0)
ajv-formats: 3.0.1(ajv@8.18.0)
fs-extra: 11.3.2
import-lazy: 4.0.0
jju: 1.4.0
@@ -11884,7 +11865,7 @@ snapshots:
'@typescript-eslint/types': 8.49.0
'@typescript-eslint/visitor-keys': 8.49.0
debug: 4.4.3
minimatch: 9.0.5
minimatch: 9.0.9
semver: 7.7.4
tinyglobby: 0.2.16
ts-api-utils: 2.4.0(typescript@5.9.3)
@@ -11899,7 +11880,7 @@ snapshots:
'@typescript-eslint/types': 8.56.0
'@typescript-eslint/visitor-keys': 8.56.0
debug: 4.4.3
minimatch: 9.0.5
minimatch: 9.0.9
semver: 7.7.4
tinyglobby: 0.2.16
ts-api-utils: 2.4.0(typescript@5.9.3)
@@ -12346,7 +12327,7 @@ snapshots:
'@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.34
alien-signals: 0.4.14
minimatch: 9.0.5
minimatch: 9.0.9
muggle-string: 0.4.1
path-browserify: 1.0.1
optionalDependencies:
@@ -12359,7 +12340,7 @@ snapshots:
'@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.34
alien-signals: 1.0.13
minimatch: 9.0.5
minimatch: 9.0.9
muggle-string: 0.4.1
path-browserify: 1.0.1
optionalDependencies:
@@ -12503,18 +12484,10 @@ snapshots:
dependencies:
humanize-ms: 1.2.1
ajv-draft-04@1.0.0(ajv@8.13.0):
optionalDependencies:
ajv: 8.13.0
ajv-draft-04@1.0.0(ajv@8.18.0):
optionalDependencies:
ajv: 8.18.0
ajv-formats@3.0.1(ajv@8.13.0):
optionalDependencies:
ajv: 8.13.0
ajv-formats@3.0.1(ajv@8.18.0):
optionalDependencies:
ajv: 8.18.0
@@ -12526,24 +12499,10 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.12.0:
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js: 4.4.1
ajv@8.13.0:
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js: 4.4.1
ajv@8.18.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
fast-uri: 3.1.2
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@@ -12596,7 +12555,7 @@ snapshots:
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
picomatch: 2.3.2
arg@5.0.2: {}
@@ -12669,7 +12628,7 @@ snapshots:
cssesc: 3.0.0
debug: 4.4.3
deterministic-object-hash: 2.0.2
devalue: 5.6.4
devalue: 5.8.1
diff: 8.0.3
dlv: 1.1.3
dset: 3.1.4
@@ -12824,16 +12783,16 @@ snapshots:
widest-line: 5.0.0
wrap-ansi: 9.0.2
brace-expansion@1.1.12:
brace-expansion@1.1.15:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.0.2:
brace-expansion@2.1.1:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.2:
brace-expansion@5.0.6:
dependencies:
balanced-match: 4.0.3
@@ -13271,7 +13230,7 @@ snapshots:
dependencies:
base-64: 1.0.0
devalue@5.6.4: {}
devalue@5.8.1: {}
devlop@1.1.0:
dependencies:
@@ -13364,7 +13323,7 @@ snapshots:
dependencies:
'@one-ini/wasm': 0.1.1
commander: 10.0.1
minimatch: 9.0.1
minimatch: 9.0.9
semver: 7.7.4
eight-colors@1.3.3: {}
@@ -13567,7 +13526,7 @@ snapshots:
eslint: 9.39.1(jiti@2.6.1)
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
is-glob: 4.0.3
minimatch: 10.2.4
minimatch: 9.0.9
semver: 7.7.4
stable-hash-x: 0.2.0
unrs-resolver: 1.11.1
@@ -13806,7 +13765,7 @@ snapshots:
'@babel/runtime': 7.29.2
tslib: 2.8.1
fast-uri@3.1.0: {}
fast-uri@3.1.2: {}
fastest-levenshtein@1.0.16: {}
@@ -13848,7 +13807,7 @@ snapshots:
filelist@1.0.4:
dependencies:
minimatch: 5.1.6
minimatch: 5.1.9
fill-range@7.1.1:
dependencies:
@@ -14029,7 +13988,7 @@ snapshots:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
minimatch: 9.0.9
minipass: 7.1.3
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
@@ -14043,7 +14002,7 @@ snapshots:
glob@9.3.5:
dependencies:
fs.realpath: 1.0.0
minimatch: 8.0.4
minimatch: 8.0.7
minipass: 4.2.8
path-scurry: 1.11.1
@@ -14114,7 +14073,7 @@ snapshots:
'@types/ws': 8.18.1
entities: 7.0.1
whatwg-mimetype: 3.0.0
ws: 8.19.0
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@@ -14355,7 +14314,7 @@ snapshots:
type-fest: 5.4.4
widest-line: 6.0.0
wrap-ansi: 9.0.2
ws: 8.19.0
ws: 8.21.0
yoga-layout: 3.2.1
optionalDependencies:
'@types/react': 19.1.9
@@ -14573,10 +14532,10 @@ snapshots:
config-chain: 1.1.13
editorconfig: 1.0.4
glob: 10.5.0
js-cookie: 3.0.5
js-cookie: 3.0.7
nopt: 7.2.1
js-cookie@3.0.5: {}
js-cookie@3.0.7: {}
js-stringify@1.0.2: {}
@@ -14613,7 +14572,7 @@ snapshots:
webidl-conversions: 8.0.0
whatwg-mimetype: 4.0.0
whatwg-url: 15.1.0
ws: 8.19.0
ws: 8.21.0
xml-name-validator: 5.0.0
transitivePeerDependencies:
- '@exodus/crypto'
@@ -14810,7 +14769,7 @@ snapshots:
picomatch: 4.0.4
string-argv: 0.3.2
tinyexec: 1.0.4
yaml: 2.8.2
yaml: 2.9.0
listr2@9.0.5:
dependencies:
@@ -14831,7 +14790,7 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.23: {}
lodash-es@4.18.1: {}
lodash.camelcase@4.3.0: {}
@@ -14839,7 +14798,7 @@ snapshots:
lodash.truncate@4.4.2: {}
lodash@4.17.23: {}
lodash@4.18.1: {}
log-update@6.1.0:
dependencies:
@@ -15288,7 +15247,7 @@ snapshots:
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
picomatch: 2.3.2
mime-db@1.52.0: {}
@@ -15302,33 +15261,25 @@ snapshots:
min-indent@1.0.1: {}
minimatch@10.2.1:
dependencies:
brace-expansion: 5.0.2
minimatch@10.2.4:
dependencies:
brace-expansion: 5.0.2
brace-expansion: 5.0.6
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.12
brace-expansion: 1.1.15
minimatch@5.1.6:
minimatch@5.1.9:
dependencies:
brace-expansion: 2.0.2
brace-expansion: 2.1.1
minimatch@8.0.4:
minimatch@8.0.7:
dependencies:
brace-expansion: 2.0.2
brace-expansion: 2.1.1
minimatch@9.0.1:
minimatch@9.0.9:
dependencies:
brace-expansion: 2.0.2
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
brace-expansion: 2.1.1
minimist@1.2.8: {}
@@ -15504,7 +15455,7 @@ snapshots:
is-docker: 2.2.1
is-wsl: 2.2.0
openai@4.104.0(ws@8.19.0)(zod@3.25.76):
openai@4.104.0(ws@8.21.0)(zod@3.25.76):
dependencies:
'@types/node': 18.19.130
'@types/node-fetch': 2.6.13
@@ -15514,7 +15465,7 @@ snapshots:
formdata-node: 4.4.1
node-fetch: 2.7.0
optionalDependencies:
ws: 8.19.0
ws: 8.21.0
zod: 3.25.76
transitivePeerDependencies:
- encoding
@@ -15753,7 +15704,7 @@ snapshots:
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@2.3.2: {}
picomatch@4.0.4: {}
@@ -16133,7 +16084,7 @@ snapshots:
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
picomatch: 2.3.2
readdirp@4.1.2: {}
@@ -16610,7 +16561,7 @@ snapshots:
recast: 0.23.11
semver: 7.7.4
use-sync-external-store: 1.6.0(react@19.2.4)
ws: 8.19.0
ws: 8.21.0
optionalDependencies:
prettier: 3.7.4
transitivePeerDependencies:
@@ -17839,7 +17790,7 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 4.1.0
ws@8.19.0: {}
ws@8.21.0: {}
wsl-utils@0.1.0:
dependencies:
@@ -17882,7 +17833,7 @@ snapshots:
yaml-eslint-parser@1.3.0:
dependencies:
eslint-visitor-keys: 3.4.3
yaml: 2.8.2
yaml: 2.9.0
yaml-language-server@1.20.0:
dependencies:
@@ -17896,11 +17847,7 @@ snapshots:
vscode-languageserver-textdocument: 1.0.12
vscode-languageserver-types: 3.17.5
vscode-uri: 3.1.0
yaml: 2.7.1
yaml@2.7.1: {}
yaml@2.8.2: {}
yaml: 2.9.0
yaml@2.9.0: {}

View File

@@ -161,13 +161,12 @@ overrides:
vite: 'catalog:'
'@tiptap/pm': 2.27.2
'@types/eslint': '-'
protobufjs: ~7.6.0
flatted: ~3.4.2
defu: ~6.1.7
# Security overrides (see pnpm.overrides in package.json for the actual pins):
# protobufjs ~7.6.0 — CVE-2026-41242 (CVSS 9.8): arbitrary code execution.
# Transitive via firebase, posthog-js. Remove after firebase upgrades protobufjs.
# flatted ~3.4.2 — GHSA-x7hr-w5r2-h6qg: prototype pollution.
# Transitive via eslint flat-cache@4.0.1. Dev-only. Remove after eslint upgrades flat-cache.
# defu ~6.1.7 — GHSA-47f6-5gq3-vx9c: prototype pollution.
# Transitive via reka-ui, c12, unplugin-typegpu. Remove after reka-ui upgrades defu.
#Security overrides
lodash: ^4.18.0
yaml: ^2.8.3
minimatch@^9.0.0: ^9.0.7
minimatch@^10.0.0: ^10.2.3
brace-expansion@^1.0.0: ^1.1.13
brace-expansion@^2.0.0: ^2.0.3
ajv@^8.0.0: ^8.18.0
ws@^8.0.0: ^8.20.1

View File

@@ -190,7 +190,7 @@
variant="ghost"
rounded="lg"
:data-testid="`template-workflow-${template.name}`"
class="hover:bg-base-background"
class="group/card hover:bg-base-background"
@mouseenter="hoveredTemplate = template.name"
@mouseleave="hoveredTemplate = null"
@click="onLoadWorkflow(template)"
@@ -316,11 +316,11 @@
class="flex flex-col-reverse justify-center"
>
<Button
v-if="hoveredTemplate === template.name"
v-tooltip.bottom="$t('g.seeTutorial')"
v-bind="$attrs"
:aria-label="$t('g.seeTutorial')"
variant="inverted"
size="icon"
class="not-group-hover/card:opacity-0"
@click.stop="openTutorial(template)"
>
<i class="icon-[lucide--info] size-4" />

View File

@@ -1,6 +1,7 @@
import { createTestingPinia } from '@pinia/testing'
import { fromPartial } from '@total-typescript/shoehorn'
import { render } from '@testing-library/vue'
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import { nextTick, reactive, ref, watchEffect } from 'vue'
@@ -34,6 +35,7 @@ const {
bookmarkedWorkflows: [] as ComfyWorkflow[],
openWorkflows: [] as ComfyWorkflow[],
activeWorkflow: null as ComfyWorkflow | null,
isSyncLoading: false,
syncWorkflows: vi.fn().mockResolvedValue(undefined)
}
@@ -232,6 +234,7 @@ describe('BaseWorkflowsSidebarTab', () => {
mockWorkflowStore.bookmarkedWorkflows = []
mockWorkflowStore.openWorkflows = []
mockWorkflowStore.activeWorkflow = null
mockWorkflowStore.isSyncLoading = false
})
const renderComponent = () =>
@@ -279,6 +282,36 @@ describe('BaseWorkflowsSidebarTab', () => {
expect(getLeafPaths(getSearchRoot())).toEqual(['workflows/test-alpha.json'])
})
it('refreshes when idle and exposes busy state while workflows are syncing', async () => {
const user = userEvent.setup()
renderComponent()
const refreshButton = screen.getByRole('button', { name: 'g.refresh' })
expect(refreshButton).toBeEnabled()
expect(refreshButton).toHaveAttribute('aria-busy', 'false')
await user.click(refreshButton)
expect(mockWorkflowStore.syncWorkflows).toHaveBeenCalledTimes(1)
mockWorkflowStore.isSyncLoading = true
await nextTick()
expect(refreshButton).toBeDisabled()
expect(refreshButton).toHaveAttribute('aria-busy', 'true')
mockWorkflowStore.isSyncLoading = false
await nextTick()
expect(refreshButton).toBeEnabled()
expect(refreshButton).toHaveAttribute('aria-busy', 'false')
await user.click(refreshButton)
expect(mockWorkflowStore.syncWorkflows).toHaveBeenCalledTimes(2)
})
it('reactively updates filtered workflows when a workflow is removed', async () => {
mockWorkflowStore.workflows = [
createMockWorkflow('workflows/test-alpha.json'),

View File

@@ -11,12 +11,24 @@
<template #tool-buttons>
<Button
v-tooltip.bottom="$t('g.refresh')"
data-testid="workflows-refresh-button"
variant="muted-textonly"
size="icon"
:aria-label="$t('g.refresh')"
:aria-busy="workflowStore.isSyncLoading"
:disabled="workflowStore.isSyncLoading"
@click="workflowStore.syncWorkflows()"
>
<i class="icon-[lucide--refresh-cw] size-4" />
<i
aria-hidden="true"
data-testid="workflows-refresh-icon"
:class="
cn(
'icon-[lucide--refresh-cw] size-4',
workflowStore.isSyncLoading && 'animate-spin'
)
"
/>
</Button>
</template>
<template #header>
@@ -170,6 +182,7 @@ import {
getWorkflowSuffix
} from '@/utils/formatUtil'
import { buildTree, sortedTree } from '@/utils/treeUtil'
import { cn } from '@comfyorg/tailwind-utils'
const { title, filter, searchSubject, dataTestid, hideLeafIcon } = defineProps<{
title: string

View File

@@ -72,6 +72,7 @@ interface WorkflowStore {
modifiedWorkflows: ComfyWorkflow[]
getWorkflowByPath: (path: string) => ComfyWorkflow | null
syncWorkflows: (dir?: string) => Promise<void>
isSyncLoading: boolean
reorderWorkflows: (from: number, to: number) => void
/** `true` if any subgraph is currently being viewed. */
@@ -785,6 +786,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
modifiedWorkflows,
getWorkflowByPath,
syncWorkflows,
isSyncLoading,
loadWorkflows,
isSubgraphActive,