feat: 统一 Commilitia Drop 全客户端命名与分发
This commit is contained in:
@@ -68,7 +68,14 @@ func ResolveDeviceName() string {
|
||||
if h, err := os.Hostname(); err == nil && h != "" {
|
||||
return h
|
||||
}
|
||||
return "cdrop-desktop"
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "Commilitia Drop (macOS)"
|
||||
case "windows":
|
||||
return "Commilitia Drop (Windows)"
|
||||
default:
|
||||
return "Commilitia Drop"
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultConfig is what a fresh install gets: clipboard sync on, no autostart.
|
||||
|
||||
@@ -43,7 +43,7 @@ func FetchOAuthConfig(ctx context.Context, apiBase string) (OAuthConfig, error)
|
||||
|
||||
app := c.App
|
||||
if app == "" {
|
||||
app = "cdrop"
|
||||
app = "commilitia-drop"
|
||||
}
|
||||
cfg := OAuthConfig{BrokerURL: c.BrokerURL, App: app}
|
||||
if cfg.BrokerURL == "" {
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestFetchOAuthConfig_Success(t *testing.T) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"auth_mode": "prod",
|
||||
"broker_url": "https://sso.example.net",
|
||||
"broker_app": "cdrop",
|
||||
"broker_app": "commilitia-drop",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -30,13 +30,13 @@ func TestFetchOAuthConfig_Success(t *testing.T) {
|
||||
if cfg.BrokerURL != "https://sso.example.net" {
|
||||
t.Errorf("broker_url = %q", cfg.BrokerURL)
|
||||
}
|
||||
if cfg.App != "cdrop" {
|
||||
t.Errorf("app = %q, want cdrop", cfg.App)
|
||||
if cfg.App != "commilitia-drop" {
|
||||
t.Errorf("app = %q, want commilitia-drop", cfg.App)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchOAuthConfig_DefaultsApp(t *testing.T) {
|
||||
// broker_app omitted → defaults to "cdrop".
|
||||
// broker_app omitted → defaults to "commilitia-drop".
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"broker_url": "https://sso.example.net"})
|
||||
@@ -46,8 +46,8 @@ func TestFetchOAuthConfig_DefaultsApp(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("FetchOAuthConfig: %v", err)
|
||||
}
|
||||
if cfg.App != "cdrop" {
|
||||
t.Errorf("app = %q, want default cdrop", cfg.App)
|
||||
if cfg.App != "commilitia-drop" {
|
||||
t.Errorf("app = %q, want default commilitia-drop", cfg.App)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// 代铸 (proxy-mint) on the desktop. The device-authorization flow yields a bootstrap
|
||||
// machine token (scope app:cdrop, no meta) that proves the user's identity but is not a
|
||||
// machine token (scope app:commilitia-drop, no meta) that proves the user's identity but is not a
|
||||
// cdrop-managed device. This call exchanges it for a managed device session bound to this
|
||||
// device's stable device_id, so the desktop joins cdrop's unified device list and is
|
||||
// managed exactly like a browser — the same model, not a separate native-only track.
|
||||
|
||||
@@ -46,7 +46,7 @@ func writeInto(dir, name string, data []byte) (string, error) {
|
||||
}
|
||||
safe := sanitizeFileName(name)
|
||||
if safe == "" {
|
||||
safe = "cdrop-download"
|
||||
safe = "Commilitia Drop Download"
|
||||
}
|
||||
target := uniquePath(dir, safe)
|
||||
if err := os.WriteFile(target, data, 0o644); err != nil {
|
||||
@@ -248,7 +248,7 @@ func FinalizeStreamingDownload(sessionId, name string) (string, error) {
|
||||
}
|
||||
safe := sanitizeFileName(name)
|
||||
if safe == "" {
|
||||
safe = "cdrop-download"
|
||||
safe = "Commilitia Drop Download"
|
||||
}
|
||||
target := uniquePath(d.dir, safe)
|
||||
if err := os.Rename(d.tmp, target); err != nil {
|
||||
|
||||
@@ -69,7 +69,7 @@ func HealIdentity(s *LoginResult, apiBase string) *LoginResult {
|
||||
}
|
||||
if changed {
|
||||
if err := SaveSession(*s); err != nil {
|
||||
slog.Warn("cdrop: heal identity save failed", "err", err)
|
||||
slog.Warn("Commilitia Drop: heal identity save failed", "err", err)
|
||||
}
|
||||
}
|
||||
return s
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// launchAgentLabel is the reverse-DNS label + plist filename for the per-user
|
||||
// LaunchAgent that starts cdrop at login.
|
||||
// launchAgentLabel is a stable internal identifier. The product name shown to
|
||||
// users comes from the application metadata, not this LaunchAgent label.
|
||||
const launchAgentLabel = "net.commilitia.cdrop"
|
||||
|
||||
func launchAgentPath() (string, error) {
|
||||
@@ -71,7 +71,40 @@ func SetLaunchAtLogin(enabled bool) error {
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, []byte(buildLaunchAgentPlist(launchAgentLabel, args)), 0o644)
|
||||
if err := writeLaunchAgentAtomically(
|
||||
p,
|
||||
[]byte(buildLaunchAgentPlist(launchAgentLabel, args)),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeLaunchAgentAtomically(path string, contents []byte) error {
|
||||
dir := filepath.Dir(path)
|
||||
f, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempPath := f.Name()
|
||||
defer os.Remove(tempPath)
|
||||
|
||||
if err := f.Chmod(0o644); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(contents); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempPath, path)
|
||||
}
|
||||
|
||||
// IsLaunchAtLoginEnabled reports whether the LaunchAgent plist is present.
|
||||
|
||||
@@ -48,8 +48,8 @@ func TestSetLaunchAtLoginInstallsAndRemoves(t *testing.T) {
|
||||
|
||||
func TestAppBundlePath(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"/Apps/cdrop.app/Contents/MacOS/desktop": "/Apps/cdrop.app",
|
||||
"/usr/local/bin/desktop": "",
|
||||
"/Apps/Commilitia Drop.app/Contents/MacOS/Commilitia Drop": "/Apps/Commilitia Drop.app",
|
||||
"/usr/local/bin/Commilitia Drop": "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := appBundlePath(in); got != want {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#import <Network/Network.h>
|
||||
|
||||
// cdropTriggerLocalNetwork:起一个对 _cdrop._tcp 的 Bonjour 浏览,触发 macOS 本地网络权限弹窗。
|
||||
// cdropTriggerLocalNetwork:起一个对 _commilitia-drop._tcp 的 Bonjour 浏览,触发 macOS 本地网络权限弹窗。
|
||||
// WKWebView 自身不会触发该权限请求,须由宿主 App 主动发起一次本地网络访问;授权后本进程内的
|
||||
// WebRTC 才能收集 host / mDNS 候选实现同内网直连(见 localnetwork_darwin.go 注释)。浏览结果本身
|
||||
// 不关心——「发起访问」这一动作即触发授权。保活单个 browser(静态全局,ARC 下持有),幂等。
|
||||
@@ -11,7 +11,7 @@ void cdropTriggerLocalNetwork(void) {
|
||||
return;
|
||||
}
|
||||
nw_browse_descriptor_t descriptor =
|
||||
nw_browse_descriptor_create_bonjour_service("_cdrop._tcp", NULL);
|
||||
nw_browse_descriptor_create_bonjour_service("_commilitia-drop._tcp", NULL);
|
||||
nw_parameters_t parameters = nw_parameters_create();
|
||||
nw_parameters_set_include_peer_to_peer(parameters, true);
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// InitializeNotifications is a no-op on macOS; the notification center is
|
||||
// initialized lazily by the native framework.
|
||||
func InitializeNotifications() {}
|
||||
|
||||
// Notify shows a native system notification.
|
||||
//
|
||||
// macOS uses UNUserNotificationCenter, which REQUIRES the app bundle to be
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
package platform
|
||||
|
||||
// InitializeNotifications is a no-op on unsupported desktop platforms.
|
||||
func InitializeNotifications() {}
|
||||
|
||||
// Notify is a no-op on platforms without a native notification backend wired up.
|
||||
// The desktop client targets macOS and Windows; this keeps the package building
|
||||
// on other GOOS (e.g. a Linux `go vet` / CI pass).
|
||||
|
||||
@@ -4,14 +4,18 @@ package platform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
toast "git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
// toastAppID 是 Windows Action Center 里显示的应用标识,与 macOS bundle id 对齐。
|
||||
toastAppID = "net.commilitia.cdrop"
|
||||
// toastAppID 是稳定的内部 AppUserModelID;Windows 通知设置中的用户可见名称由
|
||||
// toastDisplayName 单独写入 DisplayName,避免为了改显示名破坏系统身份。
|
||||
toastAppID = "net.commilitia.cdrop"
|
||||
toastDisplayName = "Commilitia Drop"
|
||||
// toastGUID 固定不变——它把通知归属到注册表里的本应用条目;更换会让既有通知
|
||||
// 失去归属。
|
||||
toastGUID = "{c4d8e2a1-6b3f-4e7a-9c2d-1f5b8a0e3d6c}"
|
||||
@@ -19,20 +23,41 @@ const (
|
||||
|
||||
var toastInit sync.Once
|
||||
|
||||
// Notify shows a native Windows toast. go-toast renders via the WinRT/COM path
|
||||
// (PowerShell fallback when the AppID isn't registered), so it works without code
|
||||
// signing — SmartScreen only gates the installer, not notifications. Best-effort:
|
||||
// errors are swallowed. SetAppData registers the app identity once so the toast
|
||||
// shows "cdrop" rather than the PowerShell host.
|
||||
func Notify(title, body string) {
|
||||
// InitializeNotifications registers the stable Windows notification identity
|
||||
// and corrects its user-visible name. It is safe to call repeatedly.
|
||||
func InitializeNotifications() {
|
||||
toastInit.Do(func() {
|
||||
data := toast.AppData{AppID: toastAppID, GUID: toastGUID}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
data.ActivationExe = exe
|
||||
}
|
||||
_ = toast.SetAppData(data)
|
||||
})
|
||||
|
||||
// go-toast v2.0.3 没有独立 DisplayName 字段,会把 AppID 写进
|
||||
// DisplayName,且已有值时不更新;在同一稳定键上显式覆盖显示名。
|
||||
appKey := filepath.Join(
|
||||
"SOFTWARE",
|
||||
"Classes",
|
||||
"AppUserModelId",
|
||||
toastAppID,
|
||||
)
|
||||
if key, err := registry.OpenKey(
|
||||
registry.CURRENT_USER,
|
||||
appKey,
|
||||
registry.SET_VALUE,
|
||||
); err == nil {
|
||||
_ = key.SetStringValue("DisplayName", toastDisplayName)
|
||||
_ = key.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Notify shows a native Windows toast. go-toast renders via the WinRT/COM path
|
||||
// (PowerShell fallback when the AppID isn't registered), so it works without code
|
||||
// signing — SmartScreen only gates the installer, not notifications. Best-effort:
|
||||
// errors are swallowed.
|
||||
func Notify(title, body string) {
|
||||
InitializeNotifications()
|
||||
n := toast.Notification{
|
||||
AppID: toastAppID,
|
||||
Title: title,
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
// OAuthConfig carries the Auth Broker coordinates. BrokerURL is the broker's PUBLIC
|
||||
// origin (e.g. https://sso.commilitia.net); App is this app's key in the broker apps
|
||||
// registry ("cdrop"). The app's loopback redirect must be registered in the broker's
|
||||
// registry ("commilitia-drop"). The app's loopback redirect must be registered in the broker's
|
||||
// apps.json (redirect_uris) for the device flow to accept it.
|
||||
type OAuthConfig struct {
|
||||
BrokerURL string
|
||||
@@ -138,7 +138,7 @@ func (f *Flow) Login(ctx context.Context) (*TokenResult, error) {
|
||||
"state": {state},
|
||||
"code_challenge": {challenge},
|
||||
"code_challenge_method": {"S256"},
|
||||
"description": {"cdrop 桌面客户端"},
|
||||
"description": {"Commilitia Drop"},
|
||||
}.Encode()
|
||||
f.openURL(authURL)
|
||||
|
||||
@@ -255,11 +255,11 @@ func randString(n int) (string, error) {
|
||||
// the real affordance; the close attempt is best-effort.
|
||||
func writeClosePage(w http.ResponseWriter, ok bool) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
msg := "已登录,可关闭本页返回 cdrop。"
|
||||
msg := "已登录,可关闭本页返回 Commilitia Drop。"
|
||||
if !ok {
|
||||
msg = "登录未完成,可关闭本页返回 cdrop 重试。"
|
||||
msg = "登录未完成,可关闭本页返回 Commilitia Drop 重试。"
|
||||
}
|
||||
fmt.Fprintf(w, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>cdrop</title></head>`+
|
||||
fmt.Fprintf(w, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>Commilitia Drop</title></head>`+
|
||||
`<body style="font-family:system-ui,sans-serif;text-align:center;margin-top:20vh">`+
|
||||
`<p>%s</p><script>setTimeout(function(){window.close();},800);</script></body></html>`, msg)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestLogin_Success(t *testing.T) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "sid-1",
|
||||
"app": "cdrop",
|
||||
"app": "commilitia-drop",
|
||||
"access": "at-123",
|
||||
"refresh": "rtk-456",
|
||||
"access_expires": time.Now().Add(15 * time.Minute).Unix(),
|
||||
@@ -63,7 +63,7 @@ func TestLogin_Success(t *testing.T) {
|
||||
}))
|
||||
defer brokerSrv.Close()
|
||||
|
||||
cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "cdrop"}
|
||||
cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "commilitia-drop"}
|
||||
|
||||
// Fake browser: parse the /device/authorize URL, assert it carries app + PKCE,
|
||||
// then GET the loopback redirect with a code + the same state (approved).
|
||||
@@ -80,7 +80,7 @@ func TestLogin_Success(t *testing.T) {
|
||||
if q.Get("code_challenge") == "" || q.Get("code_challenge_method") != "S256" {
|
||||
t.Errorf("authorize request missing PKCE challenge: %v", q)
|
||||
}
|
||||
if q.Get("app") != "cdrop" {
|
||||
if q.Get("app") != "commilitia-drop" {
|
||||
t.Errorf("authorize app = %q", q.Get("app"))
|
||||
}
|
||||
cb := q.Get("redirect_uri") + "?code=auth-code-xyz&state=" + url.QueryEscape(q.Get("state"))
|
||||
@@ -119,7 +119,7 @@ func TestLogin_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLogin_StateMismatch(t *testing.T) {
|
||||
cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "cdrop"}
|
||||
cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "commilitia-drop"}
|
||||
openURL := func(authURL string) {
|
||||
u, _ := url.Parse(authURL)
|
||||
redirect := u.Query().Get("redirect_uri")
|
||||
@@ -157,7 +157,7 @@ func TestRefresh_Success(t *testing.T) {
|
||||
}))
|
||||
defer brokerSrv.Close()
|
||||
|
||||
cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "cdrop"}
|
||||
cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "commilitia-drop"}
|
||||
tok, err := NewFlow(cfg, func(string) {}).Refresh(context.Background(), "old-rtk")
|
||||
if err != nil {
|
||||
t.Fatalf("Refresh: %v", err)
|
||||
@@ -174,7 +174,7 @@ func TestRefresh_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRefresh_EmptyToken(t *testing.T) {
|
||||
cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "cdrop"}
|
||||
cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "commilitia-drop"}
|
||||
if _, err := NewFlow(cfg, func(string) {}).Refresh(context.Background(), ""); err == nil {
|
||||
t.Fatal("want error for empty refresh token")
|
||||
}
|
||||
|
||||
@@ -177,7 +177,11 @@ func SaveSession(res LoginResult) error {
|
||||
if enc, err := encryptToken(res.RefreshToken); err == nil {
|
||||
rec.RefreshTokenEnc = enc
|
||||
} else {
|
||||
slog.Warn("cdrop: refresh_token kept in session file; OS secret store unavailable", "err", err)
|
||||
slog.Warn(
|
||||
"Commilitia Drop: refresh_token kept in session file; OS secret store unavailable",
|
||||
"err",
|
||||
err,
|
||||
)
|
||||
rec.RefreshToken = res.RefreshToken
|
||||
}
|
||||
}
|
||||
@@ -226,7 +230,7 @@ func LoadSession() (*LoginResult, error) {
|
||||
// still unavailable, leaving the file as-is).
|
||||
res.RefreshToken = rec.RefreshToken
|
||||
if err := SaveSession(*res); err != nil {
|
||||
slog.Warn("cdrop: refresh_token migration to secret store failed", "err", err)
|
||||
slog.Warn("Commilitia Drop: refresh_token migration to secret store failed", "err", err)
|
||||
}
|
||||
case rec.RefreshTokenEnc != "":
|
||||
if rt, err := decryptToken(rec.RefreshTokenEnc); err == nil {
|
||||
@@ -234,7 +238,7 @@ func LoadSession() (*LoginResult, error) {
|
||||
} else {
|
||||
// Key gone / ciphertext corrupt: drop to an access-token-only session;
|
||||
// the app will require a fresh login once the access_token lapses.
|
||||
slog.Warn("cdrop: decrypt refresh_token failed; re-login will be required", "err", err)
|
||||
slog.Warn("Commilitia Drop: decrypt refresh_token failed; re-login will be required", "err", err)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
@@ -252,7 +256,7 @@ func ClearSession() error {
|
||||
}
|
||||
if err := keyring.Delete(keyringService, keyringKeyAccount); err != nil &&
|
||||
!errors.Is(err, keyring.ErrNotFound) {
|
||||
slog.Warn("cdrop: clear session key from secret store failed", "err", err)
|
||||
slog.Warn("Commilitia Drop: clear session key from secret store failed", "err", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package platform
|
||||
|
||||
/*
|
||||
#cgo darwin CFLAGS: -x objective-c -fobjc-arc
|
||||
#cgo darwin LDFLAGS: -framework Cocoa
|
||||
#cgo darwin LDFLAGS: -framework Cocoa -framework UniformTypeIdentifiers
|
||||
#include <stdlib.h>
|
||||
|
||||
void cdropStatusBarInstall(const void *iconPNG, int iconLen, const char *title,
|
||||
|
||||
Reference in New Issue
Block a user