diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 945f83f19..57112f4f7 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -592,7 +592,7 @@ paths: description: | Avatar image file. - Maximum size is 5 MiB (5242880 bytes). + Maximum size is 5 MiB. type: string format: binary encoding: @@ -871,7 +871,7 @@ paths: description: | Avatar image file. - Maximum size is 5 MiB (5242880 bytes). + Maximum size is 5 MiB. type: string format: binary encoding: @@ -3985,7 +3985,7 @@ paths: Image requirements: - Format: PNG only (`image/png`) - - Maximum image size: 5 MiB (5242880 bytes) after decoding the base64 content + - Maximum image size: 5 MiB after decoding the base64 content - Images must be provided as base64-encoded data URLs - Other formats must be converted to PNG before sending @@ -4432,7 +4432,7 @@ paths: description: | Scratch project file, such as .sb2, .sb3, or project zip. Form field name must be `file`. - Maximum size is 64 MiB (67108864 bytes). + Maximum size is 64 MiB. type: string format: binary responses: @@ -4641,7 +4641,7 @@ paths: description: | Avatar image file. - Maximum size is 5 MiB (5242880 bytes). + Maximum size is 5 MiB. type: string format: binary encoding: @@ -7876,7 +7876,7 @@ components: All images must be provided as PNG format. If you have images in other formats (SVG, JPEG, WebP), convert them to PNG before sending. - Maximum image size: 5 MiB (5242880 bytes) after decoding the base64 content. + Maximum image size: 5 MiB after decoding the base64 content. Format: `data:image/png;base64,` type: string diff --git a/spx-gui/AGENTS.md b/spx-gui/AGENTS.md index e81023631..cc80620c6 100644 --- a/spx-gui/AGENTS.md +++ b/spx-gui/AGENTS.md @@ -26,6 +26,15 @@ Keep import statements in order: 2. Internal libraries: from base to specific, e.g., from `utils` to `models` to `components` 3. Local files: relative paths starting with `./` or `../` +### String Length Validation + +* Use native `string.length`, browser input constraints, and `z.string().max()` for ordinary frontend input validation, + following the app's existing validation and feedback patterns. +* Use `getStringLengthInCodePoints` only when frontend logic must exactly match backend/OpenAPI Unicode code point + semantics, or when slicing, truncating, or constructing hard-budget strings such as LLM/API payloads. +* Count after submission-time normalization such as `trim()`. Use byte or source-file-size checks for explicit byte or + payload-size limits. + ### Asset URLs * For widget-safe full asset URLs, use `new URL('...', import.meta.url).href` instead of `import x from './file.ext?url'`. diff --git a/spx-gui/src/apis/admin/account.ts b/spx-gui/src/apis/admin/account.ts index 5350e95b1..fdb7bb181 100644 --- a/spx-gui/src/apis/admin/account.ts +++ b/spx-gui/src/apis/admin/account.ts @@ -27,7 +27,13 @@ export type { CreatedAccountAppToken } from '@/apis/account/common' -/** Maximum allowed length for an app token name. */ +export const accountUsersKeywordMaxLength = 100 +export const accountUserUsernameMaxLength = 100 +export const accountUserDisplayNameMaxLength = 100 +export const accountUserPasswordMinLength = 8 +export const accountUserPasswordMaxLength = 128 +export const accountAppDisplayNameMaxLength = 100 +export const accountAppSecretNameMaxLength = 100 export const accountAppTokenNameMaxLength = 100 export type ListAccountUsersParams = PaginationParams & { @@ -149,7 +155,7 @@ export function listAccountAppGrantTokens(appGrantID: string, params?: ListAccou export type CreateAccountAppGrantTokenParams = { /** OAuth token type to create */ tokenType: 'accessToken' - /** Human-readable token name. Maximum length: accountAppTokenNameMaxLength. */ + /** Human-readable token name */ name: string /** Expiration timestamp */ expiresAt: string diff --git a/spx-gui/src/apis/asset.ts b/spx-gui/src/apis/asset.ts index 656ff4c56..5c9aab563 100644 --- a/spx-gui/src/apis/asset.ts +++ b/spx-gui/src/apis/asset.ts @@ -13,6 +13,8 @@ import { client, Visibility } from './common' export { Visibility } +export const assetDisplayNameMaxLength = 100 + export enum AssetType { Sprite = 'sprite', Backdrop = 'backdrop', diff --git a/spx-gui/src/apis/copilot.ts b/spx-gui/src/apis/copilot.ts index 70775c444..242e607ef 100644 --- a/spx-gui/src/apis/copilot.ts +++ b/spx-gui/src/apis/copilot.ts @@ -5,6 +5,8 @@ import type { JsonSchema7Type } from 'zod-to-json-schema' import { client } from './common' +export const copilotMessageContentMaxLength = 40000 + export enum ToolType { Function = 'function' } diff --git a/spx-gui/src/apis/project.ts b/spx-gui/src/apis/project.ts index 86b7631af..8e83e08af 100644 --- a/spx-gui/src/apis/project.ts +++ b/spx-gui/src/apis/project.ts @@ -7,6 +7,7 @@ import type { Prettify } from '@/utils/types' export { Visibility } +export const projectNameMaxLength = 100 export const projectDisplayNameMaxLength = 100 export const projectDescriptionMaxLength = 400 export const projectInstructionsMaxLength = 400 diff --git a/spx-gui/src/apis/user.ts b/spx-gui/src/apis/user.ts index aaa920db6..b5fd4a17e 100644 --- a/spx-gui/src/apis/user.ts +++ b/spx-gui/src/apis/user.ts @@ -1,6 +1,7 @@ import { client, type ByPage, type PaginationParams } from './common' import { ApiException, ApiExceptionCode } from './common/exception' +export const usernameMaxLength = 100 export const userDisplayNameMaxLength = 100 export const userDescriptionMaxLength = 200 diff --git a/spx-gui/src/apps/xbuilder/pages/admin/app.vue b/spx-gui/src/apps/xbuilder/pages/admin/app.vue index 763d68a7f..be069ef08 100644 --- a/spx-gui/src/apps/xbuilder/pages/admin/app.vue +++ b/spx-gui/src/apps/xbuilder/pages/admin/app.vue @@ -59,6 +59,7 @@ const savedRedirectURIs = ref([]) const savedAllowedOrigins = ref([]) const appUpdatedAt = ref('') const appFallbackText = computed(() => displayName.value.trim().charAt(0).toUpperCase() || '?') +const trimmedDisplayName = computed(() => displayName.value.trim()) watch( app, @@ -81,7 +82,7 @@ const parsedRedirectURIs = computed(() => parseLines(redirectURIs.value)) const parsedAllowedOrigins = computed(() => parseLines(allowedOrigins.value)) const isActive = computed(() => status.value === 'active') const isIdentityChanged = computed( - () => displayName.value.trim() !== '' && displayName.value.trim() !== savedDisplayName.value + () => trimmedDisplayName.value !== '' && trimmedDisplayName.value !== savedDisplayName.value ) const isStatusChanged = computed(() => status.value !== savedStatus.value) const areEndpointsChanged = computed( @@ -114,7 +115,7 @@ function refetchAll() { const handleUpdateIdentity = useMessageHandle( async () => { - const updated = await accountAdminApis.updateAccountApp(props.appID, { displayName: displayName.value.trim() }) + const updated = await accountAdminApis.updateAccountApp(props.appID, { displayName: trimmedDisplayName.value }) savedDisplayName.value = updated.displayName appUpdatedAt.value = updated.updatedAt }, @@ -148,10 +149,12 @@ const handleUpdateStatus = useMessageHandle( const secretName = ref('') const createdSecret = ref(null) +const trimmedSecretName = computed(() => secretName.value.trim()) +const isSecretNameValid = computed(() => trimmedSecretName.value !== '') const handleCreateSecret = useMessageHandle( async () => { - const secret = await accountAdminApis.createAccountAppSecret(props.appID, { name: secretName.value.trim() }) + const secret = await accountAdminApis.createAccountAppSecret(props.appID, { name: trimmedSecretName.value }) createdSecret.value = secret secretName.value = '' secretsQuery.refetch() @@ -247,7 +250,6 @@ function deleteSecret(secretID: string) { icon="reload" shape="square" type="white" - :aria-label="$t({ en: 'Refresh app details', zh: '刷新应用详情' })" @click="refetchAll" /> @@ -277,7 +279,10 @@ function deleteSecret(secretID: string) {
{{ $t({ en: 'Client ID', zh: '客户端 ID' }) }}
@@ -399,7 +404,6 @@ function deleteSecret(secretID: string) { desc: 'Enable or disable OAuth flows for this app' }" :value="isActive" - :aria-label="$t({ en: 'App availability', zh: '应用可用状态' })" @update:value="handleActiveChange" />
@@ -464,7 +468,7 @@ function deleteSecret(secretID: string) {