Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ tests/mail/reports/
internal/registry/meta_data.json
cmd/api/download.bin
app.log
/sidecar-server-demo
/server-demo
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

All notable changes to this project will be documented in this file.

## [v1.0.15] - 2026-04-20

### Features

- **sheets**: Add float image shortcuts (#494)
- **approval**: Document `remind` and `initiated` methods in skill (#554)

### Bug Fixes

- **base**: Preserve attachment metadata on base uploads (#563)
- **base**: Fix role view and record default permission on edit (#530)
- **sheets**: Normalize single-cell range in `+set-style` and `+batch-set-style` (#548)
- **im**: Cap `basic_batch` user_ids at 10 per API limit (#551)
- **install**: Refine install wizard messages (#529)
- **whiteboard**: Deprecate old `lark-whiteboard-cli` skill (#547)

## [v1.0.14] - 2026-04-17

### Features
Expand Down Expand Up @@ -404,6 +420,7 @@ Bundled AI agent skills for intelligent assistance:
- Bilingual documentation (English & Chinese).
- CI/CD pipelines: linting, testing, coverage reporting, and automated releases.

[v1.0.15]: https://github.com/larksuite/cli/releases/tag/v1.0.15
[v1.0.14]: https://github.com/larksuite/cli/releases/tag/v1.0.14
[v1.0.13]: https://github.com/larksuite/cli/releases/tag/v1.0.13
[v1.0.12]: https://github.com/larksuite/cli/releases/tag/v1.0.12
Expand Down
23 changes: 21 additions & 2 deletions extension/credential/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,39 @@

package credential

import "sync"
import (
"sort"
"sync"
)

var (
mu sync.Mutex
providers []Provider
)

// Register registers a credential Provider.
// Providers are consulted in registration order.
// Providers are consulted in priority order (lowest value first).
// Providers that implement Priority() int are sorted accordingly;
// those that do not default to priority 10.
// Typically called from init() via blank import.
func Register(p Provider) {
mu.Lock()
defer mu.Unlock()
providers = append(providers, p)
sort.SliceStable(providers, func(i, j int) bool {
return providerPriority(providers[i]) < providerPriority(providers[j])
})
}

// providerPriority returns the priority of a provider.
// If the provider implements interface{ Priority() int }, that value is used;
// otherwise 10 is returned as the default priority.
// Lower values are consulted first.
func providerPriority(p Provider) int {
if pp, ok := p.(interface{ Priority() int }); ok {
return pp.Priority()
}
return 10
}

// Providers returns all registered providers (snapshot).
Expand Down
26 changes: 26 additions & 0 deletions extension/credential/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,32 @@ func TestRegisterAndProviders(t *testing.T) {
}
}

type priorityProvider struct {
stubProvider
priority int
}

func (p *priorityProvider) Priority() int { return p.priority }

func TestRegister_PriorityOrder(t *testing.T) {
mu.Lock()
old := providers
providers = nil
mu.Unlock()
defer func() { mu.Lock(); providers = old; mu.Unlock() }()

Register(&stubProvider{name: "env"}) // priority 10 (default)
Register(&priorityProvider{stubProvider: stubProvider{name: "sidecar"}, priority: 0}) // priority 0 (first)

got := Providers()
if len(got) != 2 {
t.Fatalf("expected 2, got %d", len(got))
}
if got[0].Name() != "sidecar" || got[1].Name() != "env" {
t.Errorf("expected sidecar before env, got %s, %s", got[0].Name(), got[1].Name())
}
}

func TestProviders_ReturnsSnapshot(t *testing.T) {
mu.Lock()
old := providers
Expand Down
131 changes: 131 additions & 0 deletions extension/credential/sidecar/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

//go:build authsidecar

// Package sidecar provides a noop credential provider for the auth sidecar
// proxy mode. When LARKSUITE_CLI_AUTH_PROXY is set, this provider supplies
// placeholder credentials so the CLI's auth pipeline can proceed normally.
// Real tokens are never present in the sandbox; the sidecar transport
// interceptor routes requests to the trusted sidecar process instead.
package sidecar

import (
"context"
"fmt"
"os"

"github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/envvars"
"github.com/larksuite/cli/sidecar"
)

// Provider is the noop credential provider for sidecar mode.
type Provider struct{}

func (p *Provider) Name() string { return "sidecar" }
func (p *Provider) Priority() int { return 0 }

// ResolveAccount returns a minimal Account when sidecar mode is active.
// The account contains AppID and Brand from environment variables, a
// placeholder secret, and SupportedIdentities derived from STRICT_MODE.
// Returns nil, nil when sidecar mode is not active (AUTH_PROXY not set).
func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, error) {
proxyAddr := os.Getenv(envvars.CliAuthProxy)
if proxyAddr == "" {
return nil, nil // not in sidecar mode, skip
}

if err := sidecar.ValidateProxyAddr(proxyAddr); err != nil {
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: fmt.Sprintf("invalid %s %q: %v", envvars.CliAuthProxy, proxyAddr, err),
}
}

appID := os.Getenv(envvars.CliAppID)
if appID == "" {
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: envvars.CliAuthProxy + " is set but " + envvars.CliAppID + " is missing",
}
}

if os.Getenv(envvars.CliProxyKey) == "" {
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: envvars.CliAuthProxy + " is set but " + envvars.CliProxyKey + " is missing",
}
}

brand := credential.Brand(os.Getenv(envvars.CliBrand))
if brand == "" {
brand = credential.BrandFeishu
}

acct := &credential.Account{
AppID: appID,
AppSecret: credential.NoAppSecret,
Brand: brand,
}

// Parse DefaultAs
switch id := credential.Identity(os.Getenv(envvars.CliDefaultAs)); id {
case "", credential.IdentityAuto:
acct.DefaultAs = id
case credential.IdentityUser, credential.IdentityBot:
acct.DefaultAs = id
default:
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: fmt.Sprintf("invalid %s %q (want user, bot, or auto)", envvars.CliDefaultAs, id),
}
}

// Parse SupportedIdentities from STRICT_MODE, default to SupportsAll.
switch strictMode := os.Getenv(envvars.CliStrictMode); strictMode {
case "bot":
acct.SupportedIdentities = credential.SupportsBot
case "user":
acct.SupportedIdentities = credential.SupportsUser
case "off", "":
acct.SupportedIdentities = credential.SupportsAll
default:
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: fmt.Sprintf("invalid %s %q (want bot, user, or off)", envvars.CliStrictMode, strictMode),
}
}

return acct, nil
}

// ResolveToken returns a sentinel token whose value encodes the token type.
// The transport interceptor reads this sentinel to determine the identity
// (user vs bot), strips it, and the sidecar injects the real token.
// Returns nil, nil when sidecar mode is not active.
func (p *Provider) ResolveToken(ctx context.Context, req credential.TokenSpec) (*credential.Token, error) {
if os.Getenv(envvars.CliAuthProxy) == "" {
return nil, nil
}

var sentinel string
switch req.Type {
case credential.TokenTypeUAT:
sentinel = sidecar.SentinelUAT
case credential.TokenTypeTAT:
sentinel = sidecar.SentinelTAT
default:
return nil, nil
}

return &credential.Token{
Value: sentinel,
Scopes: "", // empty → scope pre-check is skipped
Source: "sidecar",
}, nil
}

func init() {
credential.Register(&Provider{})
}
Loading
Loading