feat(api,cli): strict UUID validation for app-id and workspace-id (#37212)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Xiyuan Chen 2026-06-09 00:35:18 -07:00 committed by GitHub
parent 686e643632
commit 1502a57381
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 99 additions and 92 deletions

View File

@ -6,7 +6,7 @@ from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
from libs.helper import EmailStr, UUIDStrOrEmpty, uuid_value
from libs.helper import EmailStr, UUIDStr, UUIDStrOrEmpty, uuid_value
from models.model import AppMode
# Server-side cap on `limit` query param for /openapi/v1/* list endpoints.
@ -262,22 +262,6 @@ class AppDescribeQuery(BaseModel):
model_config = ConfigDict(extra="forbid")
fields: set[str] | None = Field(default=None, json_schema_extra=_csv_string_query_schema)
workspace_id: str | None = None
@field_validator("workspace_id", mode="before")
@classmethod
def _validate_workspace_id(cls, v: object) -> str | None:
if v is None or v == "":
return None
if not isinstance(v, str):
raise ValueError("workspace_id must be a string")
try:
import uuid as _uuid
_uuid.UUID(v)
except ValueError:
raise ValueError("workspace_id must be a valid UUID")
return v
@field_validator("fields", mode="before")
@classmethod
@ -297,7 +281,7 @@ class AppDescribeQuery(BaseModel):
class AppListQuery(BaseModel):
"""mode is a closed enum."""
workspace_id: str
workspace_id: UUIDStr
page: int = Field(1, ge=1)
limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT)
mode: AppMode | None = None

View File

@ -97,7 +97,7 @@ class AppDescribeApi(AppReadResource):
except ValidationError as exc:
raise UnprocessableEntity(exc.json())
app = self._load(app_id, workspace_id=query.workspace_id)
app = self._load(app_id)
requested = query.fields
want_info = requested is None or "info" in requested

View File

@ -19,6 +19,10 @@ def load_app(data: AuthData) -> None:
if data.app is not None:
return
app_id = data.path_params["app_id"]
try:
uuid.UUID(app_id)
except ValueError:
raise NotFound("app not found")
app = AppService.get_app_by_id(db.session, app_id)
if not app or app.status != "normal":
raise NotFound("app not found")

View File

@ -273,6 +273,18 @@ def parse_uuid_str_or_none(value: str | None) -> str | None:
UUIDStrOrEmpty = Annotated[str, AfterValidator(normalize_uuid)]
def _strict_uuid(value: str | UUID) -> str:
if not value:
raise ValueError("must be a non-empty valid UUID")
try:
return uuid_value(value)
except ValueError as exc:
raise ValueError("must be a valid UUID") from exc
UUIDStr = Annotated[str, AfterValidator(_strict_uuid)]
def alphanumeric(value: str):
# check if the value is alphanumeric and underlined
if re.match(r"^[a-zA-Z0-9_]+$", value):

View File

@ -112,7 +112,6 @@ User-scoped operations
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | | Yes | string |
| fields | query | | No | string |
| workspace_id | query | | No | string |
##### Responses
@ -454,7 +453,6 @@ Empty / omitted → all blocks. Unknown member → ValidationError → 422.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| fields | string | | No |
| workspace_id | string | | No |
#### AppDescribeResponse

View File

@ -32,18 +32,27 @@ def _make_auth_data(**kwargs) -> AuthData:
return data
_VALID_APP_UUID = "00000000-0000-0000-0000-000000000001"
def test_load_app_writes_app_to_data():
app = MagicMock()
app.status = "normal"
app.enable_api = True
data = _make_auth_data(path_params={"app_id": "abc"})
data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID})
with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app):
load_app(data)
assert data.app is app
def test_load_app_raises_not_found_for_non_uuid_app_id():
data = _make_auth_data(path_params={"app_id": "not-a-uuid"})
with pytest.raises(NotFound):
load_app(data)
def test_load_app_raises_not_found_when_missing():
data = _make_auth_data(path_params={"app_id": "missing"})
data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID})
with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=None):
with pytest.raises(NotFound):
load_app(data)
@ -52,7 +61,7 @@ def test_load_app_raises_not_found_when_missing():
def test_load_app_raises_not_found_when_not_normal():
app = MagicMock()
app.status = "archived"
data = _make_auth_data(path_params={"app_id": "abc"})
data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID})
with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app):
with pytest.raises(NotFound):
load_app(data)
@ -62,7 +71,7 @@ def test_load_app_stashes_app_even_when_api_disabled():
app = MagicMock()
app.status = "normal"
app.enable_api = False
data = _make_auth_data(path_params={"app_id": "abc"})
data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID})
with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app):
load_app(data)
assert data.app is app

View File

@ -18,8 +18,8 @@ from controllers.openapi.apps import AppListQuery
def test_defaults():
q = AppListQuery.model_validate({"workspace_id": "ws-1"})
assert q.workspace_id == "ws-1"
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001"})
assert q.workspace_id == "00000000-0000-0000-0000-000000000001"
assert q.page == 1
assert q.limit == 20
assert q.mode is None
@ -34,61 +34,63 @@ def test_workspace_id_required():
def test_page_must_be_positive():
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "page": 0})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": 0})
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "page": -1})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": -1})
def test_page_rejects_non_integer_string():
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "page": "abc"})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": "abc"})
def test_limit_must_be_positive():
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "limit": 0})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": 0})
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "limit": -1})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": -1})
def test_limit_caps_at_max_page_limit():
# Boundary accepts.
q = AppListQuery.model_validate({"workspace_id": "ws-1", "limit": MAX_PAGE_LIMIT})
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": MAX_PAGE_LIMIT})
assert q.limit == MAX_PAGE_LIMIT
# Just over rejects.
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "limit": MAX_PAGE_LIMIT + 1})
AppListQuery.model_validate(
{"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": MAX_PAGE_LIMIT + 1}
)
def test_mode_whitelisted_against_app_mode():
# Valid mode passes.
q = AppListQuery.model_validate({"workspace_id": "ws-1", "mode": "chat"})
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "mode": "chat"})
assert q.mode is not None
assert q.mode.value == "chat"
# Invalid mode rejects.
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "mode": "not-a-mode"})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "mode": "not-a-mode"})
def test_name_length_capped():
AppListQuery.model_validate({"workspace_id": "ws-1", "name": "x" * 200})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 200})
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "name": "x" * 201})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 201})
def test_tag_length_capped():
AppListQuery.model_validate({"workspace_id": "ws-1", "tag": "x" * 100})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 100})
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "ws-1", "tag": "x" * 101})
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 101})
def test_all_fields_accept_valid_values():
"""Pin the happy-path acceptance for every field in one place."""
q = AppListQuery.model_validate(
{
"workspace_id": "ws-1",
"workspace_id": "00000000-0000-0000-0000-000000000001",
"page": 5,
"limit": 50,
"mode": "workflow",
@ -96,7 +98,7 @@ def test_all_fields_accept_valid_values():
"tag": "prod",
}
)
assert q.workspace_id == "ws-1"
assert q.workspace_id == "00000000-0000-0000-0000-000000000001"
assert q.page == 5
assert q.limit == 50
assert q.mode is not None

View File

@ -37,11 +37,11 @@ describe('AppMetaClient', () => {
const spy = vi.spyOn(apps, 'describe')
const client = new AppMetaClient({ apps, host: mock.url, cache })
const m1 = await client.get('app-1', 'ws-1', [FieldInfo])
const m1 = await client.get('app-1', [FieldInfo])
expect(m1.info?.id).toBe('app-1')
expect(spy).toHaveBeenCalledTimes(1)
const m2 = await client.get('app-1', 'ws-1', [FieldInfo])
const m2 = await client.get('app-1', [FieldInfo])
expect(m2.info?.id).toBe('app-1')
expect(spy).toHaveBeenCalledTimes(1)
})
@ -52,10 +52,10 @@ describe('AppMetaClient', () => {
const spy = vi.spyOn(apps, 'describe')
const client = new AppMetaClient({ apps, host: mock.url, cache })
await client.get('app-1', 'ws-1', [FieldInfo])
await client.get('app-1', [FieldInfo])
expect(spy).toHaveBeenCalledTimes(1)
const full = await client.get('app-1', 'ws-1', [FieldInfo, FieldParameters])
const full = await client.get('app-1', [FieldInfo, FieldParameters])
expect(spy).toHaveBeenCalledTimes(2)
expect(full.coveredFields.has(FieldParameters)).toBe(true)
})
@ -66,11 +66,11 @@ describe('AppMetaClient', () => {
const spy = vi.spyOn(apps, 'describe')
const client = new AppMetaClient({ apps, host: mock.url, cache, now: () => new Date('2026-05-09T00:00:00Z') })
await client.get('app-1', 'ws-1', [FieldInfo])
await client.get('app-1', [FieldInfo])
expect(spy).toHaveBeenCalledTimes(1)
const client2 = new AppMetaClient({ apps, host: mock.url, cache, now: () => new Date('2026-05-09T00:00:01Z') })
await client2.get('app-1', 'ws-1', [FieldInfo])
await client2.get('app-1', [FieldInfo])
expect(spy).toHaveBeenCalledTimes(2)
})
@ -80,11 +80,11 @@ describe('AppMetaClient', () => {
const spy = vi.spyOn(apps, 'describe')
const client = new AppMetaClient({ apps, host: mock.url, cache })
await client.get('app-1', 'ws-1', [FieldInfo])
await client.get('app-1', [FieldInfo])
expect(spy).toHaveBeenCalledTimes(1)
await client.invalidate('app-1')
await client.get('app-1', 'ws-1', [FieldInfo])
await client.get('app-1', [FieldInfo])
expect(spy).toHaveBeenCalledTimes(2)
})
@ -93,8 +93,8 @@ describe('AppMetaClient', () => {
const spy = vi.spyOn(apps, 'describe')
const client = new AppMetaClient({ apps, host: mock.url })
await client.get('app-1', 'ws-1', [FieldInfo])
await client.get('app-1', 'ws-1', [FieldInfo])
await client.get('app-1', [FieldInfo])
await client.get('app-1', [FieldInfo])
expect(spy).toHaveBeenCalledTimes(2)
})
})

View File

@ -23,12 +23,12 @@ export class AppMetaClient {
this.now = opts.now ?? (() => new Date())
}
async get(appId: string, workspaceId: string, fields: readonly AppMetaFieldKey[] = []): Promise<AppMeta> {
async get(appId: string, fields: readonly AppMetaFieldKey[] = []): Promise<AppMeta> {
const cached = this.cache?.get(this.host, appId)
if (cached !== undefined && this.cache?.isFresh(cached, this.now()) === true && covers(cached.meta, fields))
return cached.meta
const resp = await this.apps.describe(appId, workspaceId, fields.length === 0 ? undefined : fields)
const resp = await this.apps.describe(appId, fields.length === 0 ? undefined : fields)
const fresh = fromDescribe(resp, fields)
const merged = cached !== undefined && this.cache?.isFresh(cached, this.now()) === true
? mergeMeta(cached.meta, fresh)

View File

@ -86,14 +86,14 @@ describe('AppsClient.describe', () => {
await stub?.stop()
})
it('hits /apps/<id>/describe, sends workspace_id, omits fields when none given', async () => {
it('hits /apps/<id>/describe, omits workspace_id and fields when not given', async () => {
stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap))
const res = await makeClient(stub.url).describe('app-1', 'ws-1')
const res = await makeClient(stub.url).describe('app-1')
expect(stub.captured.url?.split('?')[0]).toBe('/openapi/v1/apps/app-1/describe')
const q = queryOf(stub.captured.url)
expect(q.get('workspace_id')).toBe('ws-1')
expect(q.has('workspace_id')).toBe(false)
expect(q.has('fields')).toBe(false)
expect(res.info?.id).toBe('app-1')
})
@ -101,7 +101,7 @@ describe('AppsClient.describe', () => {
it('joins fields with commas', async () => {
stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap))
await makeClient(stub.url).describe('app-1', 'ws-1', ['parameters', 'input_schema'])
await makeClient(stub.url).describe('app-1', ['parameters', 'input_schema'])
expect(queryOf(stub.captured.url).get('fields')).toBe('parameters,input_schema')
})
@ -109,7 +109,7 @@ describe('AppsClient.describe', () => {
it('URL-encodes the app id', async () => {
stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap))
await makeClient(stub.url).describe('app/with space', 'ws-1')
await makeClient(stub.url).describe('app/with space')
expect(stub.captured.url?.split('?')[0]).toBe('/openapi/v1/apps/app%2Fwith%20space/describe')
})

View File

@ -32,13 +32,10 @@ export class AppsClient {
})
}
async describe(appId: string, workspaceId: string, fields?: readonly string[]): Promise<AppDescribeResponse> {
async describe(appId: string, fields?: readonly string[]): Promise<AppDescribeResponse> {
return this.orpc.apps.byAppId.describe.get({
params: { app_id: appId },
query: {
workspace_id: workspaceId,
// The backend parses a comma-separated string (validator splits on ','); the contract
// types `fields` as a string accordingly, so join here rather than send an array.
fields: fields !== undefined && fields.length > 0 ? fields.join(',') : undefined,
},
})

View File

@ -1,7 +1,10 @@
import { DifyCommand } from '@/commands/_shared/dify-command'
import { httpRetryFlag } from '@/commands/_shared/global-flags'
import { BaseError } from '@/errors/base'
import { ErrorCode } from '@/errors/codes'
import { Args, Flags } from '@/framework/flags'
import { formatted, OutputFormat } from '@/framework/output'
import { isValidUuid } from '@/workspace/resolver'
import { agentGuide } from './guide'
import { runDescribeApp } from './run'
@ -9,13 +12,13 @@ export default class DescribeApp extends DifyCommand {
static override description = 'Describe a single app (kubectl-describe-style)'
static override examples = [
'<%= config.bin %> describe app app-1',
'<%= config.bin %> describe app app-1 -o json',
'<%= config.bin %> describe app app-1 --refresh',
'<%= config.bin %> describe app <uuid>',
'<%= config.bin %> describe app <uuid> -o json',
'<%= config.bin %> describe app <uuid> --refresh',
]
static override args = {
id: Args.string({ description: 'app id', required: true }),
id: Args.string({ description: 'app UUID', required: true }),
}
static override flags = {
@ -27,6 +30,8 @@ export default class DescribeApp extends DifyCommand {
async run(argv: string[]) {
const { args, flags } = this.parse(DescribeApp, argv)
if (!isValidUuid(args.id))
throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `${JSON.stringify(args.id)} is not a valid app UUID` })
const format = flags.output
const ctx = await this.authedCtx({ retryFlag: flags['http-retry'], withCache: true, format })
return formatted({

View File

@ -4,11 +4,9 @@ import type { HttpClient } from '@/http/types'
import type { IOStreams } from '@/sys/io/streams'
import { AppMetaClient } from '@/api/app-meta'
import { AppsClient } from '@/api/apps'
import { getEnv } from '@/sys/index'
import { runWithSpinner } from '@/sys/io/spinner'
import { nullStreams } from '@/sys/io/streams'
import { FieldInfo, FieldInputSchema, FieldParameters } from '@/types/app-meta'
import { resolveWorkspaceId } from '@/workspace/resolver'
import { AppDescribeOutput } from './handlers.js'
export type DescribeAppOptions = {
@ -28,8 +26,6 @@ export type DescribeAppDeps = {
}
export async function runDescribeApp(opts: DescribeAppOptions, deps: DescribeAppDeps): Promise<AppDescribeOutput> {
const env = deps.envLookup ?? getEnv
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
const apps = new AppsClient(deps.http)
const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache })
const io = deps.io ?? nullStreams()
@ -38,7 +34,7 @@ export async function runDescribeApp(opts: DescribeAppOptions, deps: DescribeApp
async () => {
if (opts.refresh === true)
await meta.invalidate(opts.appId)
return meta.get(opts.appId, wsId, [FieldInfo, FieldParameters, FieldInputSchema])
return meta.get(opts.appId, [FieldInfo, FieldParameters, FieldInputSchema])
},
)
return new AppDescribeOutput(result)

View File

@ -59,7 +59,7 @@ export async function runGetApp(opts: GetAppOptions, deps: GetAppDeps): Promise<
if (opts.appId !== undefined && opts.appId !== '') {
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
const wsName = workspaceNameForId(deps.active, wsId)
const desc = await apps.describe(opts.appId, wsId, ['info'])
const desc = await apps.describe(opts.appId, ['info'])
return describeToEnvelope(desc, wsId, wsName)
}
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })

View File

@ -8,10 +8,9 @@ import { AppRunClient } from '@/api/app-run'
import { AppsClient } from '@/api/apps'
import { pickStrategy } from '@/commands/run/app/_strategies/index'
import { RUN_MODES } from '@/commands/run/app/handlers'
import { getEnv, processExit } from '@/sys/index'
import { processExit } from '@/sys/index'
import { colorEnabled, colorScheme } from '@/sys/io/color'
import { FieldInfo } from '@/types/app-meta'
import { resolveWorkspaceId } from '@/workspace/resolver'
export type ResumeAppOptions = {
readonly appId: string
@ -76,12 +75,9 @@ async function resolveInputs(
}
export async function resumeApp(opts: ResumeAppOptions, deps: ResumeAppDeps): Promise<void> {
const env = deps.envLookup ?? getEnv
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
const apps = new AppsClient(deps.http)
const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache })
const m = await meta.get(opts.appId, wsId, [FieldInfo])
const m = await meta.get(opts.appId, [FieldInfo])
const mode = m.info?.mode ?? RUN_MODES.Workflow
const runClient = new AppRunClient(deps.http)

View File

@ -9,9 +9,8 @@ import { FileUploadClient } from '@/api/file-upload'
import { pickStrategy } from '@/commands/run/app/_strategies/index'
import { BaseError, HttpClientError } from '@/errors/base'
import { ErrorCode } from '@/errors/codes'
import { getEnv, processExit } from '@/sys/index'
import { processExit } from '@/sys/index'
import { FieldInfo } from '@/types/app-meta'
import { resolveWorkspaceId } from '@/workspace/resolver'
import { resolveFileInputs } from './file-flags.js'
import { RUN_MODES } from './handlers.js'
@ -78,13 +77,11 @@ async function resolveInputs(
}
export async function runApp(opts: RunAppOptions, deps: RunAppDeps): Promise<void> {
const env = deps.envLookup ?? getEnv
const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active })
const apps = new AppsClient(deps.http)
const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache })
try {
await executeRun(opts, deps, meta, wsId)
await executeRun(opts, deps, meta)
}
catch (err) {
if (err instanceof HttpClientError && err.httpStatus === 422) {
@ -99,9 +96,8 @@ async function executeRun(
opts: RunAppOptions,
deps: RunAppDeps,
meta: AppMetaClient,
wsId: string,
): Promise<void> {
const m = await meta.get(opts.appId, wsId, [FieldInfo])
const m = await meta.get(opts.appId, [FieldInfo])
const mode = m.info?.mode ?? ''
if (mode === '')
throw new Error(`app ${opts.appId}: mode missing from /describe`)

View File

@ -8,11 +8,23 @@ export type WorkspaceResolveInputs = {
readonly active?: ActiveContext
}
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
export function isValidUuid(v: string): boolean {
return UUID_RE.test(v)
}
export function resolveWorkspaceId(inputs: WorkspaceResolveInputs): string {
if (truthy(inputs.flag))
if (truthy(inputs.flag)) {
if (!isValidUuid(inputs.flag))
throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `--workspace value ${JSON.stringify(inputs.flag)} is not a valid UUID` })
return inputs.flag
if (truthy(inputs.env))
}
if (truthy(inputs.env)) {
if (!isValidUuid(inputs.env))
throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `DIFY_WORKSPACE_ID value ${JSON.stringify(inputs.env)} is not a valid UUID` })
return inputs.env
}
const ctx = inputs.active?.ctx
if (ctx !== undefined) {
if (truthy(ctx.workspace?.id))

View File

@ -33,7 +33,6 @@ export type AppDescribeInfo = {
export type AppDescribeQuery = {
fields?: string
workspace_id?: string | null
}
export type AppDescribeResponse = {
@ -445,7 +444,6 @@ export type GetAppsByAppIdDescribeData = {
}
query?: {
fields?: string
workspace_id?: string
}
url: '/apps/{app_id}/describe'
}

View File

@ -20,7 +20,6 @@ export const zAccountPayload = z.object({
*/
export const zAppDescribeQuery = z.object({
fields: z.string().optional(),
workspace_id: z.string().nullish(),
})
/**
@ -533,7 +532,6 @@ export const zGetAppsByAppIdDescribePath = z.object({
export const zGetAppsByAppIdDescribeQuery = z.object({
fields: z.string().optional(),
workspace_id: z.string().optional(),
})
/**