Skip to content

Commit 81e53e7

Browse files
committed
feat(remote): support github token login bootstrap
1 parent dcf6146 commit 81e53e7

7 files changed

Lines changed: 81 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
- Update `crawlkit` to v0.7.0.
88
- Add read-only Cloudflare remote archive scaffolding with `[remote]` config,
9-
`subscribe-cloud`, GitHub-backed `remote login`, `remote status`,
10-
`remote archives`, and cloud-mode `status --json` output that does not open
11-
or create a local SQLite database.
9+
`subscribe-cloud`, GitHub-backed `remote login` with OAuth or token-env
10+
bootstrap, `remote status`, `remote archives`, and cloud-mode `status --json`
11+
output that does not open or create a local SQLite database.
1212
- Route cloud-mode `search` and filtered `messages` reads to Worker named
1313
queries so subscribers can inspect live D1 data without local SQLite.
1414
- Add `discrawl cloud publish` to export non-DM local SQLite rows into the

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,9 @@ discrawl whoami
550550
SQLite database.
551551
`remote login` starts the Worker GitHub OAuth flow, verifies org/team
552552
membership server-side, and stores the signed bearer token in the OS keyring.
553+
Use `remote login --github-token-env GITHUB_TOKEN` for non-browser bootstrap;
554+
the Worker verifies that GitHub token against the same org/team policy and
555+
stores only the returned remote session token locally.
553556

554557
Publishers can send the current non-DM SQLite archive into the Worker-backed
555558
D1 archive:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ require (
4444
github.com/muesli/cancelreader v0.2.2 // indirect
4545
github.com/muesli/termenv v0.16.0 // indirect
4646
github.com/ncruces/go-strftime v1.0.0 // indirect
47-
github.com/openclaw/crawlkit v0.7.1-0.20260527173115-df4eca58a4c9
47+
github.com/openclaw/crawlkit v0.7.1-0.20260527174716-74916a98ee45
4848
github.com/pmezard/go-difflib v1.0.0 // indirect
4949
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
5050
github.com/rivo/uniseg v0.4.7 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
7171
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
7272
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
7373
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
74-
github.com/openclaw/crawlkit v0.7.1-0.20260527173115-df4eca58a4c9 h1:FbutsmKLVob/J5+4fmHOgvGv4btcxq5E/n5FbWyhgDw=
75-
github.com/openclaw/crawlkit v0.7.1-0.20260527173115-df4eca58a4c9/go.mod h1:2XkSx3N8yzyjc+Jyf1Zl9VEQVlElh/dzBMW1cRMQQGw=
74+
github.com/openclaw/crawlkit v0.7.1-0.20260527174716-74916a98ee45 h1:dx8+XyE2JddHEzhhhM7BupViAhzI5Ws4qKvNDegK04g=
75+
github.com/openclaw/crawlkit v0.7.1-0.20260527174716-74916a98ee45/go.mod h1:2XkSx3N8yzyjc+Jyf1Zl9VEQVlElh/dzBMW1cRMQQGw=
7676
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
7777
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
7878
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=

internal/cli/cli_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,53 @@ func TestRemoteLoginStoresKeyringToken(t *testing.T) {
12271227
require.Contains(t, whoami.String(), `"login": "alice"`)
12281228
}
12291229

1230+
func TestRemoteLoginWithGitHubTokenEnvStoresKeyringToken(t *testing.T) {
1231+
ctx := context.Background()
1232+
dir := t.TempDir()
1233+
keyring.MockInit()
1234+
t.Setenv("DISCRAWL_TEST_GITHUB_TOKEN", "github-token")
1235+
1236+
var sawToken string
1237+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
1238+
w.Header().Set("content-type", "application/json")
1239+
switch req.URL.Path {
1240+
case "/v1/auth/github/token":
1241+
var body crawlremote.GitHubTokenLoginRequest
1242+
require.NoError(t, json.NewDecoder(req.Body).Decode(&body))
1243+
sawToken = body.Token
1244+
_ = json.NewEncoder(w).Encode(crawlremote.LoginPollResult{Status: "complete", Token: "session-token", Org: "openclaw", Login: "alice"})
1245+
case "/v1/whoami":
1246+
require.Equal(t, "Bearer session-token", req.Header.Get("Authorization"))
1247+
_ = json.NewEncoder(w).Encode(crawlremote.Identity{Owner: "openclaw", Org: "openclaw", Login: "alice", Auth: "github"})
1248+
default:
1249+
http.NotFound(w, req)
1250+
}
1251+
}))
1252+
defer server.Close()
1253+
1254+
cfgPath := filepath.Join(dir, "config.toml")
1255+
var out bytes.Buffer
1256+
require.NoError(t, Run(ctx, []string{
1257+
"--config", cfgPath,
1258+
"--json",
1259+
"remote", "login",
1260+
"--endpoint", server.URL,
1261+
"--github-token-env", "DISCRAWL_TEST_GITHUB_TOKEN",
1262+
}, &out, &bytes.Buffer{}))
1263+
require.Equal(t, "github-token", sawToken)
1264+
require.Contains(t, out.String(), `"login_method": "github-token"`)
1265+
1266+
cfg, err := config.Load(cfgPath)
1267+
require.NoError(t, err)
1268+
stored, err := keyring.Get(cfg.Remote.Auth.KeyringService, cfg.Remote.Auth.KeyringAccount)
1269+
require.NoError(t, err)
1270+
require.Equal(t, "session-token", stored)
1271+
1272+
var whoami bytes.Buffer
1273+
require.NoError(t, Run(ctx, []string{"--config", cfgPath, "--json", "whoami"}, &whoami, &bytes.Buffer{}))
1274+
require.Contains(t, whoami.String(), `"login": "alice"`)
1275+
}
1276+
12301277
func TestCloudPublishSendsNonDMRows(t *testing.T) {
12311278
ctx := context.Background()
12321279
dir := t.TempDir()

internal/cli/output.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ Read-only SQL is allowed by default. Use "-" or no query to read SQL from stdin.
221221
discrawl remote status
222222
discrawl remote archives
223223
discrawl remote login --endpoint URL
224+
discrawl remote login --endpoint URL --github-token-env GITHUB_TOKEN
224225
discrawl remote whoami
225226
226227
Reads the configured Cloudflare-backed remote archive without opening the local SQLite database.

internal/cli/remote_commands.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"flag"
77
"fmt"
88
"io"
9+
"os"
910
"os/exec"
1011
goruntime "runtime"
1112
"strings"
@@ -109,6 +110,7 @@ func (r *runtime) runRemoteLogin(args []string) error {
109110
fs := flag.NewFlagSet("remote login", flag.ContinueOnError)
110111
fs.SetOutput(io.Discard)
111112
endpoint := fs.String("endpoint", "", "")
113+
githubTokenEnv := fs.String("github-token-env", "", "")
112114
noBrowser := fs.Bool("no-browser", false, "")
113115
timeoutRaw := fs.String("timeout", "5m", "")
114116
pollRaw := fs.String("poll-interval", "2s", "")
@@ -151,6 +153,17 @@ func (r *runtime) runRemoteLogin(args []string) error {
151153
if err != nil {
152154
return configErr(err)
153155
}
156+
if tokenEnv := strings.TrimSpace(*githubTokenEnv); tokenEnv != "" {
157+
githubToken := strings.TrimSpace(os.Getenv(tokenEnv))
158+
if githubToken == "" {
159+
return fmt.Errorf("%s is empty", tokenEnv)
160+
}
161+
result, err := client.LoginWithGitHubToken(r.ctx, githubToken)
162+
if err != nil {
163+
return err
164+
}
165+
return r.finishRemoteLogin(cfg, "github-token", result)
166+
}
154167
pollSecret, err := crawlremote.NewLoginPollSecret()
155168
if err != nil {
156169
return err
@@ -170,6 +183,16 @@ func (r *runtime) runRemoteLogin(args []string) error {
170183
if err != nil {
171184
return err
172185
}
186+
return r.finishRemoteLogin(cfg, "github-oauth", result)
187+
}
188+
189+
func (r *runtime) finishRemoteLogin(cfg config.Config, method string, result crawlremote.LoginPollResult) error {
190+
if strings.ToLower(strings.TrimSpace(result.Status)) != "complete" {
191+
return fmt.Errorf("remote login returned status %q", result.Status)
192+
}
193+
if strings.TrimSpace(result.Token) == "" {
194+
return errors.New("remote login completed without token")
195+
}
173196
auth, err := config.StoreRemoteToken(cfg, result.Token)
174197
if err != nil {
175198
return configErr(fmt.Errorf("store remote token: %w", err))
@@ -188,6 +211,7 @@ func (r *runtime) runRemoteLogin(args []string) error {
188211
"login": result.Login,
189212
"org": result.Org,
190213
"owner": result.Owner,
214+
"login_method": method,
191215
"auth_source": cfg.Remote.Auth.TokenSource,
192216
"keyring_service": cfg.Remote.Auth.KeyringService,
193217
"keyring_account": cfg.Remote.Auth.KeyringAccount,

0 commit comments

Comments
 (0)