mirror of
https://github.com/larksuite/cli.git
synced 2026-07-05 15:47:54 +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.
95 lines
5.4 KiB
Go
95 lines
5.4 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package errclass
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
)
|
|
|
|
// CodeMeta is the classification metadata attached to a Lark numeric code.
|
|
// It does NOT carry Message or Hint — those are derived at the dispatcher
|
|
// (see BuildAPIError).
|
|
//
|
|
// Risk + Action are populated only for codes that route to CategoryConfirmation;
|
|
// the dispatcher falls back to RiskUnknown + ctx.LarkCmd when either is empty
|
|
// so the envelope is never wire-invalid.
|
|
type CodeMeta struct {
|
|
Category errs.Category
|
|
Subtype errs.Subtype
|
|
Retryable bool
|
|
Risk string // CategoryConfirmation arm only; empty otherwise
|
|
Action string // CategoryConfirmation arm only; empty otherwise
|
|
}
|
|
|
|
// codeMeta is the central registry. Top-level entries (auth/authorization/api/
|
|
// policy/config codes shared across services) live here; service-specific
|
|
// sub-tables (e.g. task) live in dedicated files like codemeta_task.go and
|
|
// merge into this map via init().
|
|
//
|
|
// Go language guarantees package-level vars initialize before init() functions,
|
|
// so sub-tables registering via init() can always assume codeMeta is non-nil.
|
|
var codeMeta = map[int]CodeMeta{
|
|
// CategoryAuthentication
|
|
99991661: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeTokenMissing}, // Authorization header missing
|
|
99991671: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeTokenInvalid}, // token format error (must start with t- / u-)
|
|
99991668: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeTokenInvalid}, // UAT invalid/expired (server does not distinguish)
|
|
99991663: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeTokenInvalid}, // access_token invalid
|
|
99991677: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeTokenExpired}, // UAT expired
|
|
20026: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeRefreshTokenInvalid}, // refresh_token v1 legacy format
|
|
20037: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeRefreshTokenExpired}, // refresh_token expired
|
|
20064: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeRefreshTokenRevoked}, // refresh_token revoked
|
|
20073: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeRefreshTokenReused}, // refresh_token already used
|
|
20050: {Category: errs.CategoryAuthentication, Subtype: errs.SubtypeRefreshServerError, Retryable: true}, // refresh endpoint transient error
|
|
|
|
// CategoryAuthorization
|
|
99991672: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypeAppScopeNotApplied},
|
|
99991676: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypeTokenScopeInsufficient},
|
|
99991679: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypeMissingScope}, // user authorized app but did not grant this scope
|
|
230027: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypeUserUnauthorized}, // user never authorized the app
|
|
99991673: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypeAppUnavailable}, // app status unavailable
|
|
99991662: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypeAppDisabled}, // app currently disabled in tenant
|
|
|
|
// CategoryAPI
|
|
99991400: {Category: errs.CategoryAPI, Subtype: errs.SubtypeRateLimit, Retryable: true},
|
|
1061045: {Category: errs.CategoryAPI, Subtype: errs.SubtypeConflict, Retryable: true},
|
|
131009: {Category: errs.CategoryAPI, Subtype: errs.SubtypeConflict, Retryable: true}, // wiki write-path lock contention; retryable with backoff
|
|
1064510: {Category: errs.CategoryAPI, Subtype: errs.SubtypeCrossTenant},
|
|
1064511: {Category: errs.CategoryAPI, Subtype: errs.SubtypeCrossBrand},
|
|
1310246: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters},
|
|
1063006: {Category: errs.CategoryAPI, Subtype: errs.SubtypeRateLimit}, // drive perm-apply quota; 5/day, not short-term retryable
|
|
1063007: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters},
|
|
231205: {Category: errs.CategoryAPI, Subtype: errs.SubtypeOwnershipMismatch},
|
|
|
|
// CategoryConfig
|
|
99991543: {Category: errs.CategoryConfig, Subtype: errs.SubtypeInvalidClient}, // RFC 6749 §5.2 — app_id / app_secret incorrect (Open API)
|
|
10014: {Category: errs.CategoryConfig, Subtype: errs.SubtypeInvalidClient}, // TAT endpoint — "app secret invalid" (TAT-mint variant of 99991543)
|
|
|
|
// CategoryPolicy
|
|
21000: {Category: errs.CategoryPolicy, Subtype: errs.SubtypeChallengeRequired},
|
|
21001: {Category: errs.CategoryPolicy, Subtype: errs.SubtypeAccessDenied},
|
|
}
|
|
|
|
// LookupCodeMeta is the single lookup entry. Returns ok=false for unknown codes —
|
|
// the caller (BuildAPIError) is responsible for falling back to
|
|
// CategoryAPI/SubtypeUnknown.
|
|
func LookupCodeMeta(code int) (CodeMeta, bool) {
|
|
m, ok := codeMeta[code]
|
|
return m, ok
|
|
}
|
|
|
|
// mergeCodeMeta is invoked by sub-table init() functions to merge service-specific
|
|
// codes into the central registry. Panics on duplicate code so a misregistration
|
|
// fails fast at startup rather than producing silently-inconsistent classification.
|
|
func mergeCodeMeta(src map[int]CodeMeta, owner string) {
|
|
for code, meta := range src {
|
|
if existing, dup := codeMeta[code]; dup {
|
|
panic(fmt.Sprintf("codeMeta dup: code %d already mapped %+v; %s wants %+v",
|
|
code, existing, owner, meta))
|
|
}
|
|
codeMeta[code] = meta
|
|
}
|
|
}
|