mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 22:24:31 +08:00
Every failure on the authentication, authorization, and configuration
path now surfaces as a typed structured error instead of an ad-hoc
envelope. Users and scripts that consume CLI output get:
- a fixed nine-category taxonomy on the wire, each mapped to a
stable shell exit code (authentication/authorization/config = 3,
network = 4, internal = 5, policy = 6, confirmation = 10)
- identity-aware detail fields (missing_scopes, requested_scopes,
granted_scopes, console_url, log_id, retryable, hint) carried
uniformly on the envelope
- a single canonical policy envelope at exit 6; the legacy
auth_error carve-out is retired
- per-subtype canonical message + hint that preserves Lark's
diagnostic phrasing and routes recovery to the right actor:
app developer (app_scope_not_applied), user (missing_scope,
token_scope_insufficient, user_unauthorized), or tenant admin
(app_unavailable, app_disabled)
- wrong app credentials classify as config/invalid_client whether
surfaced by the Open API endpoint (99991543) or the tenant
access-token mint endpoint (10003 / 10014), instead of
collapsing to a transport error or api/unknown
- local shortcut scope preflight emits the same
authorization/missing_scope envelope (identity + deterministic
missing-scope set) used by the post-call permission path, so AI
consumers read the same structured shape from precheck and from
server-returned permission denial
- streaming download/upload failures keep the same network subtype
split (timeout / TLS / DNS / transport) as the non-stream path
instead of collapsing every cause to a generic transport failure
- console_url is carried only on the bot-perspective
app_scope_not_applied envelope (where the recovery action is
"developer applies the scope at the developer console"); the
user-perspective missing_scope envelope drops the field, since
the only actionable user recovery is `lark-cli auth login --scope`
and pointing an end user at a console they cannot modify is
misleading
- bind workflows (Hermes / OpenClaw / lark-channel) flatten dynamic
Type tags to wire 'config' with the original module name kept
as a metric label
All 10 typed errors are cause-bearing, nil-safe on .Error() and
.Unwrap(), and defensively clone slice setter inputs. Four lint
rules (CheckNilSafeError / CheckBuilderImmutable / CheckUnwrapSymmetry
/ CheckBuildAPIErrorArms) lock these invariants on migrated paths.
90 lines
5.1 KiB
Go
90 lines
5.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package errs
|
|
|
|
// Subtype is the second-level taxonomy axis. Wire JSON: "subtype".
|
|
type Subtype string
|
|
|
|
const (
|
|
SubtypeUnknown Subtype = "unknown" // catch-all fallback; producers must prefer a specific subtype
|
|
)
|
|
|
|
// CategoryValidation subtypes
|
|
const (
|
|
SubtypeInvalidArgument Subtype = "invalid_argument" // user-supplied flag / arg failed validation (gRPC INVALID_ARGUMENT alignment)
|
|
)
|
|
|
|
// CategoryAuthentication subtypes
|
|
const (
|
|
SubtypeTokenMissing Subtype = "token_missing" // no token in request (Authorization header absent / no local token cache)
|
|
SubtypeTokenInvalid Subtype = "token_invalid" // token present but content/format wrong
|
|
SubtypeTokenExpired Subtype = "token_expired" // token explicitly expired
|
|
SubtypeRefreshTokenInvalid Subtype = "refresh_token_invalid" // refresh_token is v1 legacy format, unusable
|
|
SubtypeRefreshTokenExpired Subtype = "refresh_token_expired" // refresh_token expired
|
|
SubtypeRefreshTokenRevoked Subtype = "refresh_token_revoked" // refresh_token revoked (user logout / admin action)
|
|
SubtypeRefreshTokenReused Subtype = "refresh_token_reused" // refresh_token already used (single-use rotation triggered)
|
|
SubtypeRefreshServerError Subtype = "refresh_server_error" // refresh endpoint transient error (retryable)
|
|
)
|
|
|
|
// CategoryAuthorization subtypes
|
|
const (
|
|
SubtypeMissingScope Subtype = "missing_scope" // user authorized app but did not grant this scope
|
|
SubtypeUserUnauthorized Subtype = "user_unauthorized" // user never authorized the app
|
|
SubtypeAppScopeNotApplied Subtype = "app_scope_not_applied" // app did not apply for this scope on the open platform
|
|
SubtypeTokenScopeInsufficient Subtype = "token_scope_insufficient" // token was issued without this scope (RFC 6750 alignment)
|
|
SubtypeAppUnavailable Subtype = "app_unavailable" // app status unavailable
|
|
SubtypeAppDisabled Subtype = "app_disabled" // app currently disabled in this tenant (was installed/enabled before)
|
|
SubtypePermissionDenied Subtype = "permission_denied" // resource-level permission denial (authenticated but lacks rights for this resource, HTTP 403 / gRPC PERMISSION_DENIED alignment)
|
|
)
|
|
|
|
// CategoryConfig subtypes
|
|
const (
|
|
SubtypeInvalidClient Subtype = "invalid_client" // app_id / app_secret incorrect (RFC 6749 §5.2 alignment)
|
|
SubtypeNotConfigured Subtype = "not_configured" // local config file absent (user has not run `config init`)
|
|
SubtypeInvalidConfig Subtype = "invalid_config" // local config file present but malformed
|
|
)
|
|
|
|
// CategoryNetwork subtypes
|
|
const (
|
|
SubtypeNetworkTransport Subtype = "transport" // fallback when no more-specific network subtype matches
|
|
SubtypeNetworkTimeout Subtype = "timeout" // dial / read timeout
|
|
SubtypeNetworkTLS Subtype = "tls" // TLS handshake / cert failure
|
|
SubtypeNetworkDNS Subtype = "dns" // DNS resolution failure
|
|
SubtypeNetworkServer Subtype = "server_error" // upstream HTTP 5xx
|
|
)
|
|
|
|
// CategoryAPI subtypes
|
|
const (
|
|
SubtypeRateLimit Subtype = "rate_limit" // request rate limit exceeded
|
|
SubtypeConflict Subtype = "conflict" // resource state conflict (e.g. concurrent modification)
|
|
SubtypeCrossTenant Subtype = "cross_tenant" // operation crosses tenant boundary (not supported)
|
|
SubtypeCrossBrand Subtype = "cross_brand" // operation crosses brand boundary (feishu vs lark, not supported)
|
|
SubtypeInvalidParameters Subtype = "invalid_parameters" // API-side parameter validation rejected the request
|
|
SubtypeOwnershipMismatch Subtype = "ownership_mismatch" // caller is not the resource owner
|
|
SubtypeNotFound Subtype = "not_found" // referenced resource does not exist (HTTP 404 alignment)
|
|
SubtypeServerError Subtype = "server_error" // upstream server-side transient error (HTTP 5xx alignment, retryable)
|
|
SubtypeQuotaExceeded Subtype = "quota_exceeded" // resource quota / collection size limit reached (assignees, followers, members, etc.)
|
|
SubtypeAlreadyExists Subtype = "already_exists" // idempotency violation: resource already exists in target state
|
|
)
|
|
|
|
// CategoryPolicy subtypes (security-policy envelope shape)
|
|
const (
|
|
SubtypeChallengeRequired Subtype = "challenge_required" // user must complete browser challenge / MFA
|
|
SubtypeAccessDenied Subtype = "access_denied" // policy denies access outright
|
|
)
|
|
|
|
// CategoryInternal subtypes
|
|
const (
|
|
SubtypeSDKError Subtype = "sdk_error" // lark SDK Do() returned an unexpected error
|
|
SubtypeInvalidResponse Subtype = "invalid_response" // SDK response body not parsable as JSON
|
|
SubtypeFileIO Subtype = "file_io" // local file I/O failure (mkdir / write / read)
|
|
SubtypeStorage Subtype = "storage" // local persistence failure (e.g. config file save)
|
|
// Generic untyped error lifted to InternalError uses SubtypeUnknown.
|
|
)
|
|
|
|
// CategoryConfirmation subtypes
|
|
const (
|
|
SubtypeConfirmationRequired Subtype = "confirmation_required" // high-risk operation needs explicit --yes
|
|
)
|