clawdex-mobile 5.1.3-internal.0 → 5.1.3-internal.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/setup-and-operations.md +5 -2
- package/docs/troubleshooting.md +10 -2
- package/package.json +1 -1
- package/services/rust-bridge/Cargo.lock +1 -1
- package/services/rust-bridge/Cargo.toml +1 -1
- package/services/rust-bridge/src/main.rs +348 -0
- package/vendor/bridge-binaries/darwin-arm64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/darwin-x64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-arm64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-armv7l/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-x64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/win32-x64/codex-rust-bridge.exe +0 -0
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Notes:
|
|
|
70
70
|
- Browser preview uses a second forwarded port (`8788` by default), so both ports need public visibility.
|
|
71
71
|
- GitHub resets public forwarded ports back to private when a codespace restarts. Restarting the bridge reruns the visibility step.
|
|
72
72
|
- If automatic visibility setup fails, run `gh codespace ports visibility 8787:public 8788:public`.
|
|
73
|
-
- If the mobile app is built with `
|
|
73
|
+
- If the mobile app is built with `EXPO_PUBLIC_GITHUB_APP_CLIENT_ID` and `EXPO_PUBLIC_GITHUB_APP_SLUG`, users can now tap `Use GitHub Codespaces` in onboarding/settings, sign in with GitHub, approve the Claudex GitHub App for only the repositories they want, pick a Codespace, and connect without manually copying the bridge token.
|
|
74
74
|
- The app can also create a new repo-backed Codespace directly. It prefers `<signed-in-user>/<EXPO_PUBLIC_GITHUB_CODESPACES_REPO_NAME>` first. If that repo does not exist, it automatically forks `EXPO_PUBLIC_GITHUB_CODESPACES_SOURCE_OWNER/<EXPO_PUBLIC_GITHUB_CODESPACES_REPO_NAME>` into the signed-in user account, then creates the Codespace there.
|
|
75
75
|
|
|
76
76
|
This repo now also includes a Codespaces bootstrap flow. On Codespace start/resume, `.devcontainer/devcontainer.json` runs:
|
|
@@ -89,7 +89,7 @@ That pre-installs Codex and prebuilds the Rust bridge binary so the later startu
|
|
|
89
89
|
|
|
90
90
|
The published npm package now includes that bootstrap script too, so a minimal Codespaces template repo can install `clawdex-mobile@latest` in `postCreateCommand` and call the packaged bootstrap without vendoring bridge source into the template itself.
|
|
91
91
|
|
|
92
|
-
In Codespaces mode, the bootstrap also enables bridge-side GitHub bearer auth for the current `CODESPACE_NAME`, so the mobile app can authenticate with the same GitHub
|
|
92
|
+
In Codespaces mode, the bootstrap also enables bridge-side GitHub bearer auth for the current `CODESPACE_NAME`, so the mobile app can authenticate with the same GitHub App user token it used to discover and start the Codespace.
|
|
93
93
|
|
|
94
94
|
## OpenCode Setup
|
|
95
95
|
|
|
@@ -63,8 +63,10 @@ Important constraints:
|
|
|
63
63
|
- Browser preview uses the preview port (`8788` by default), so that forwarded port must also be public
|
|
64
64
|
- GitHub resets public forwarded ports back to private whenever the codespace restarts
|
|
65
65
|
- Keep bridge auth enabled and use Codespaces only for repos you trust, because public forwarded ports are internet-reachable
|
|
66
|
-
- If the mobile app build sets `
|
|
66
|
+
- If the mobile app build sets `EXPO_PUBLIC_GITHUB_APP_CLIENT_ID` and `EXPO_PUBLIC_GITHUB_APP_SLUG`, onboarding/settings can now sign in with GitHub, approve the Claudex GitHub App for only the repositories they want, start the Codespace, and connect directly with the same GitHub App user token instead of copying `BRIDGE_AUTH_TOKEN`
|
|
67
|
+
- That same in-app GitHub sign-in also bootstraps GitHub git auth inside the Codespace so `git clone`, `git push`, GitHub HTTPS remotes, and common `git@github.com:...` SSH-style remotes can reuse the app login without extra account setup
|
|
67
68
|
- The same in-app GitHub flow can create a new Codespace. It prefers `<signed-in-user>/<EXPO_PUBLIC_GITHUB_CODESPACES_REPO_NAME>`. If that repo does not exist yet, Clawdex automatically forks `EXPO_PUBLIC_GITHUB_CODESPACES_SOURCE_OWNER/<EXPO_PUBLIC_GITHUB_CODESPACES_REPO_NAME>` into the signed-in user account and creates the Codespace from that fork
|
|
69
|
+
- Older saved GitHub Codespaces sessions may need one fresh sign-in from the app so the stored GitHub App token and refresh token are updated
|
|
68
70
|
|
|
69
71
|
Manual recovery if port visibility does not update automatically:
|
|
70
72
|
|
|
@@ -269,7 +271,8 @@ npm run teardown -- --yes
|
|
|
269
271
|
| Variable | Purpose |
|
|
270
272
|
|---|---|
|
|
271
273
|
| `EXPO_PUBLIC_HOST_BRIDGE_TOKEN` | token used by local mobile dev builds |
|
|
272
|
-
| `
|
|
274
|
+
| `EXPO_PUBLIC_GITHUB_APP_CLIENT_ID` | GitHub App client ID for in-app Codespaces sign-in |
|
|
275
|
+
| `EXPO_PUBLIC_GITHUB_APP_SLUG` | GitHub App slug used to open install/manage-access pages for repository selection |
|
|
273
276
|
| `EXPO_PUBLIC_GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN` | forwarded port domain used to derive Codespaces bridge URLs (`app.github.dev` by default) |
|
|
274
277
|
| `EXPO_PUBLIC_GITHUB_CODESPACES_REPO_NAME` | repository name to sort matching Codespaces first in the in-app picker |
|
|
275
278
|
| `EXPO_PUBLIC_GITHUB_CODESPACES_SOURCE_OWNER` | template/source repository owner used for automatic forking when the signed-in user does not have a same-name repo |
|
package/docs/troubleshooting.md
CHANGED
|
@@ -34,7 +34,7 @@ npm run stop:services
|
|
|
34
34
|
## Bridge auth errors (`401`, invalid token)
|
|
35
35
|
|
|
36
36
|
- For the shipped mobile app, rescan the bridge QR or update the stored token in Settings.
|
|
37
|
-
- For GitHub-auth Codespaces profiles, reopen `GitHub Codespaces` in the app and sign in with GitHub again if the
|
|
37
|
+
- For GitHub-auth Codespaces profiles, reopen `GitHub Codespaces` in the app and sign in with GitHub again if the GitHub App token or refresh token was revoked or expired.
|
|
38
38
|
- For a local dev build, also ensure `BRIDGE_AUTH_TOKEN` in `.env.secure` matches `EXPO_PUBLIC_HOST_BRIDGE_TOKEN` in `apps/mobile/.env`.
|
|
39
39
|
- Restart the bridge after token changes.
|
|
40
40
|
- On secure-launcher installs, `Settings > Bridge Maintenance > Restart bridge safely` can do that from the phone.
|
|
@@ -53,7 +53,8 @@ gh codespace ports visibility 8787:public 8788:public
|
|
|
53
53
|
|
|
54
54
|
- If `gh` is unavailable in the codespace, use the Codespaces `Ports` panel and change both forwarded ports to `Public`.
|
|
55
55
|
- Keep bridge auth enabled. Public forwarded ports without bridge auth are not a safe setup.
|
|
56
|
-
- If GitHub direct sign-in is not showing in the app, confirm the build includes `
|
|
56
|
+
- If GitHub direct sign-in is not showing in the app, confirm the build includes `EXPO_PUBLIC_GITHUB_APP_CLIENT_ID` and `EXPO_PUBLIC_GITHUB_APP_SLUG`.
|
|
57
|
+
- If the app signs in but still cannot create a Codespace or clone/push inside it, reopen the GitHub App access step in the app and make sure the template repo and any target repos are selected for the installation.
|
|
57
58
|
- If in-app Codespace creation forks or targets the wrong repo, check `EXPO_PUBLIC_GITHUB_CODESPACES_REPO_NAME`, `EXPO_PUBLIC_GITHUB_CODESPACES_SOURCE_OWNER`, and `EXPO_PUBLIC_GITHUB_CODESPACES_REPO_REF` in the mobile build env.
|
|
58
59
|
|
|
59
60
|
## GitHub Codespaces bootstrap did not start the bridge
|
|
@@ -89,6 +90,13 @@ ls -la .bridge.pid .bridge.log .env.secure
|
|
|
89
90
|
npm run codespaces:bootstrap -- --no-start
|
|
90
91
|
```
|
|
91
92
|
|
|
93
|
+
## Git push in a Codespace fails with `403` or permission denied
|
|
94
|
+
|
|
95
|
+
- The app now bootstraps GitHub HTTPS git credentials inside the Codespace after GitHub sign-in.
|
|
96
|
+
- If you signed in before this behavior shipped, reopen `GitHub Codespaces` in the app and sign in with GitHub once more so the saved token includes repository access.
|
|
97
|
+
- The bootstrap also rewrites common `git@github.com:...` and `ssh://git@github.com/...` remotes to HTTPS so they can use the same credential.
|
|
98
|
+
- After reconnecting, retry the clone/push from the app or from the Codespace shell.
|
|
99
|
+
|
|
92
100
|
## Voice transcription says no credentials were found
|
|
93
101
|
|
|
94
102
|
- The bridge can transcribe with either `OPENAI_API_KEY`, `BRIDGE_CHATGPT_ACCESS_TOKEN`, or the same ChatGPT auth tokens already used for Codex login.
|
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
#[cfg(unix)]
|
|
2
|
+
use std::os::unix::fs::PermissionsExt;
|
|
1
3
|
use std::{
|
|
2
4
|
collections::{HashMap, HashSet, VecDeque},
|
|
3
5
|
env,
|
|
@@ -86,6 +88,10 @@ const BROWSER_PREVIEW_HTML_REWRITE_LIMIT_BYTES: usize = 4 * 1024 * 1024;
|
|
|
86
88
|
const BROWSER_PREVIEW_DISCOVERY_HTTP_TIMEOUT: Duration = Duration::from_millis(500);
|
|
87
89
|
const GITHUB_CODESPACES_AUTH_CACHE_TTL: Duration = Duration::from_secs(60 * 5);
|
|
88
90
|
const GITHUB_CODESPACES_API_VERSION: &str = "2022-11-28";
|
|
91
|
+
const GITHUB_API_URL: &str = "https://api.github.com";
|
|
92
|
+
const GITHUB_HOST: &str = "github.com";
|
|
93
|
+
const GITHUB_CREDENTIALS_DIR_NAME: &str = ".clawdex";
|
|
94
|
+
const GITHUB_CREDENTIALS_FILE_NAME: &str = "github-credentials";
|
|
89
95
|
|
|
90
96
|
#[derive(Debug, Clone)]
|
|
91
97
|
struct GitHubCodespacesAuthConfig {
|
|
@@ -375,6 +381,222 @@ impl GitHubCodespacesAuthService {
|
|
|
375
381
|
}
|
|
376
382
|
}
|
|
377
383
|
|
|
384
|
+
#[derive(Debug, Clone)]
|
|
385
|
+
struct GitHubViewer {
|
|
386
|
+
login: String,
|
|
387
|
+
scopes: Vec<String>,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async fn install_github_git_auth(
|
|
391
|
+
state: &Arc<AppState>,
|
|
392
|
+
access_token: &str,
|
|
393
|
+
) -> Result<GitHubAuthInstallResponse, BridgeError> {
|
|
394
|
+
let viewer = fetch_github_viewer(state, access_token).await?;
|
|
395
|
+
if !github_token_can_be_used_for_git_auth(&viewer.scopes) {
|
|
396
|
+
return Err(BridgeError::forbidden(
|
|
397
|
+
"github_repo_scope_required",
|
|
398
|
+
"GitHub repository access is required. Sign in again from the app and approve the required repository access.",
|
|
399
|
+
));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let credentials_file = resolve_github_credentials_file_path()?;
|
|
403
|
+
ensure_private_parent_dir(&credentials_file).await?;
|
|
404
|
+
write_github_credentials_file(&credentials_file, access_token).await?;
|
|
405
|
+
configure_git_credential_store(state, &credentials_file).await?;
|
|
406
|
+
|
|
407
|
+
Ok(GitHubAuthInstallResponse {
|
|
408
|
+
installed: true,
|
|
409
|
+
host: GITHUB_HOST.to_string(),
|
|
410
|
+
login: viewer.login,
|
|
411
|
+
scopes: viewer.scopes,
|
|
412
|
+
credential_file: credentials_file.to_string_lossy().to_string(),
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async fn fetch_github_viewer(
|
|
417
|
+
state: &Arc<AppState>,
|
|
418
|
+
access_token: &str,
|
|
419
|
+
) -> Result<GitHubViewer, BridgeError> {
|
|
420
|
+
let trimmed = access_token.trim();
|
|
421
|
+
if trimmed.is_empty() {
|
|
422
|
+
return Err(BridgeError::invalid_params("accessToken must not be empty"));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let api_url = state
|
|
426
|
+
.config
|
|
427
|
+
.github_codespaces_auth
|
|
428
|
+
.as_ref()
|
|
429
|
+
.map(|config| config.api_url.as_str())
|
|
430
|
+
.unwrap_or(GITHUB_API_URL);
|
|
431
|
+
let http = HttpClient::builder()
|
|
432
|
+
.user_agent("clawdex-rust-bridge")
|
|
433
|
+
.build()
|
|
434
|
+
.map_err(|error| {
|
|
435
|
+
BridgeError::server(&format!("failed to build GitHub auth client: {error}"))
|
|
436
|
+
})?;
|
|
437
|
+
let response = http
|
|
438
|
+
.get(format!("{api_url}/user"))
|
|
439
|
+
.header("accept", "application/vnd.github+json")
|
|
440
|
+
.header("x-github-api-version", GITHUB_CODESPACES_API_VERSION)
|
|
441
|
+
.bearer_auth(trimmed)
|
|
442
|
+
.send()
|
|
443
|
+
.await
|
|
444
|
+
.map_err(|error| BridgeError::server(&format!("GitHub auth check failed: {error}")))?;
|
|
445
|
+
|
|
446
|
+
if !response.status().is_success() {
|
|
447
|
+
let status = response.status();
|
|
448
|
+
let body = response.text().await.unwrap_or_default();
|
|
449
|
+
let message = if let Ok(value) = serde_json::from_str::<Value>(&body) {
|
|
450
|
+
read_string(value.get("message"))
|
|
451
|
+
.unwrap_or_else(|| format!("GitHub auth check failed ({status})"))
|
|
452
|
+
} else {
|
|
453
|
+
format!("GitHub auth check failed ({status})")
|
|
454
|
+
};
|
|
455
|
+
return Err(BridgeError::server(&message));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let scopes = parse_github_oauth_scopes(
|
|
459
|
+
response
|
|
460
|
+
.headers()
|
|
461
|
+
.get("x-oauth-scopes")
|
|
462
|
+
.and_then(|value| value.to_str().ok()),
|
|
463
|
+
);
|
|
464
|
+
let payload = response.json::<Value>().await.map_err(|error| {
|
|
465
|
+
BridgeError::server(&format!("failed to parse GitHub user response: {error}"))
|
|
466
|
+
})?;
|
|
467
|
+
let login = read_string(payload.get("login"))
|
|
468
|
+
.ok_or_else(|| BridgeError::server("GitHub auth check returned an invalid user payload"))?;
|
|
469
|
+
|
|
470
|
+
Ok(GitHubViewer { login, scopes })
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
fn parse_github_oauth_scopes(header: Option<&str>) -> Vec<String> {
|
|
474
|
+
header
|
|
475
|
+
.unwrap_or_default()
|
|
476
|
+
.split(',')
|
|
477
|
+
.map(|value| value.trim().to_lowercase())
|
|
478
|
+
.filter(|value| !value.is_empty())
|
|
479
|
+
.collect()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
fn github_scopes_allow_repo_access(scopes: &[String]) -> bool {
|
|
483
|
+
scopes
|
|
484
|
+
.iter()
|
|
485
|
+
.any(|scope| scope == "repo" || scope == "public_repo")
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
fn github_token_can_be_used_for_git_auth(scopes: &[String]) -> bool {
|
|
489
|
+
scopes.is_empty() || github_scopes_allow_repo_access(scopes)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
fn resolve_github_credentials_file_path() -> Result<PathBuf, BridgeError> {
|
|
493
|
+
let home = read_non_empty_env("HOME")
|
|
494
|
+
.ok_or_else(|| BridgeError::server("HOME is not set; cannot install GitHub auth"))?;
|
|
495
|
+
Ok(PathBuf::from(home)
|
|
496
|
+
.join(GITHUB_CREDENTIALS_DIR_NAME)
|
|
497
|
+
.join(GITHUB_CREDENTIALS_FILE_NAME))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async fn ensure_private_parent_dir(path: &Path) -> Result<(), BridgeError> {
|
|
501
|
+
let Some(parent) = path.parent() else {
|
|
502
|
+
return Err(BridgeError::server(
|
|
503
|
+
"failed to resolve GitHub credential directory",
|
|
504
|
+
));
|
|
505
|
+
};
|
|
506
|
+
fs::create_dir_all(parent).await.map_err(|error| {
|
|
507
|
+
BridgeError::server(&format!("failed to create GitHub auth directory: {error}"))
|
|
508
|
+
})?;
|
|
509
|
+
#[cfg(unix)]
|
|
510
|
+
{
|
|
511
|
+
fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
|
|
512
|
+
.await
|
|
513
|
+
.map_err(|error| {
|
|
514
|
+
BridgeError::server(&format!(
|
|
515
|
+
"failed to secure GitHub auth directory permissions: {error}"
|
|
516
|
+
))
|
|
517
|
+
})?;
|
|
518
|
+
}
|
|
519
|
+
Ok(())
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async fn write_github_credentials_file(
|
|
523
|
+
credentials_file: &Path,
|
|
524
|
+
access_token: &str,
|
|
525
|
+
) -> Result<(), BridgeError> {
|
|
526
|
+
let content = format!(
|
|
527
|
+
"https://x-access-token:{}@{}\n",
|
|
528
|
+
access_token.trim(),
|
|
529
|
+
GITHUB_HOST
|
|
530
|
+
);
|
|
531
|
+
fs::write(credentials_file, content)
|
|
532
|
+
.await
|
|
533
|
+
.map_err(|error| {
|
|
534
|
+
BridgeError::server(&format!("failed to write GitHub credentials: {error}"))
|
|
535
|
+
})?;
|
|
536
|
+
#[cfg(unix)]
|
|
537
|
+
{
|
|
538
|
+
fs::set_permissions(credentials_file, std::fs::Permissions::from_mode(0o600))
|
|
539
|
+
.await
|
|
540
|
+
.map_err(|error| {
|
|
541
|
+
BridgeError::server(&format!(
|
|
542
|
+
"failed to secure GitHub credential permissions: {error}"
|
|
543
|
+
))
|
|
544
|
+
})?;
|
|
545
|
+
}
|
|
546
|
+
Ok(())
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async fn configure_git_credential_store(
|
|
550
|
+
state: &Arc<AppState>,
|
|
551
|
+
credentials_file: &Path,
|
|
552
|
+
) -> Result<(), BridgeError> {
|
|
553
|
+
let helper_value = format!("store --file {}", credentials_file.to_string_lossy());
|
|
554
|
+
let commands = vec![
|
|
555
|
+
vec![
|
|
556
|
+
"config".to_string(),
|
|
557
|
+
"--global".to_string(),
|
|
558
|
+
"--replace-all".to_string(),
|
|
559
|
+
"credential.helper".to_string(),
|
|
560
|
+
helper_value,
|
|
561
|
+
],
|
|
562
|
+
vec![
|
|
563
|
+
"config".to_string(),
|
|
564
|
+
"--global".to_string(),
|
|
565
|
+
"--replace-all".to_string(),
|
|
566
|
+
"url.https://github.com/.insteadOf".to_string(),
|
|
567
|
+
"git@github.com:".to_string(),
|
|
568
|
+
],
|
|
569
|
+
vec![
|
|
570
|
+
"config".to_string(),
|
|
571
|
+
"--global".to_string(),
|
|
572
|
+
"--replace-all".to_string(),
|
|
573
|
+
"url.https://github.com/.insteadOf".to_string(),
|
|
574
|
+
"ssh://git@github.com/".to_string(),
|
|
575
|
+
],
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
for args in commands {
|
|
579
|
+
let result = state
|
|
580
|
+
.terminal
|
|
581
|
+
.execute_binary("git", &args, state.config.workdir.clone(), None)
|
|
582
|
+
.await?;
|
|
583
|
+
|
|
584
|
+
if result.code != Some(0) {
|
|
585
|
+
return Err(BridgeError::server(
|
|
586
|
+
&(if !result.stderr.is_empty() {
|
|
587
|
+
result.stderr
|
|
588
|
+
} else if !result.stdout.is_empty() {
|
|
589
|
+
result.stdout
|
|
590
|
+
} else {
|
|
591
|
+
"failed to configure git credentials".to_string()
|
|
592
|
+
}),
|
|
593
|
+
));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
Ok(())
|
|
598
|
+
}
|
|
599
|
+
|
|
378
600
|
#[derive(Clone)]
|
|
379
601
|
struct AppState {
|
|
380
602
|
config: Arc<BridgeConfig>,
|
|
@@ -5200,6 +5422,22 @@ struct GitQueryRequest {
|
|
|
5200
5422
|
cwd: Option<String>,
|
|
5201
5423
|
}
|
|
5202
5424
|
|
|
5425
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
5426
|
+
#[serde(rename_all = "camelCase")]
|
|
5427
|
+
struct GitHubAuthInstallRequest {
|
|
5428
|
+
access_token: String,
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5431
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
5432
|
+
#[serde(rename_all = "camelCase")]
|
|
5433
|
+
struct GitHubAuthInstallResponse {
|
|
5434
|
+
installed: bool,
|
|
5435
|
+
host: String,
|
|
5436
|
+
login: String,
|
|
5437
|
+
scopes: Vec<String>,
|
|
5438
|
+
credential_file: String,
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5203
5441
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
5204
5442
|
#[serde(rename_all = "camelCase")]
|
|
5205
5443
|
struct GitHistoryRequest {
|
|
@@ -6556,6 +6794,13 @@ async fn handle_bridge_method(
|
|
|
6556
6794
|
|
|
6557
6795
|
Ok(result_value)
|
|
6558
6796
|
}
|
|
6797
|
+
"bridge/github/auth/install" => {
|
|
6798
|
+
let request: GitHubAuthInstallRequest =
|
|
6799
|
+
serde_json::from_value(params.unwrap_or_else(|| json!({})))
|
|
6800
|
+
.map_err(|error| BridgeError::invalid_params(&error.to_string()))?;
|
|
6801
|
+
let result = install_github_git_auth(state, &request.access_token).await?;
|
|
6802
|
+
serde_json::to_value(result).map_err(|error| BridgeError::server(&error.to_string()))
|
|
6803
|
+
}
|
|
6559
6804
|
"bridge/attachments/upload" => {
|
|
6560
6805
|
let request: AttachmentUploadRequest =
|
|
6561
6806
|
serde_json::from_value(params.unwrap_or_else(|| json!({})))
|
|
@@ -7085,7 +7330,27 @@ fn bridge_chatgpt_auth_cache() -> &'static StdRwLock<Option<BridgeChatGptAuthBun
|
|
|
7085
7330
|
CACHE.get_or_init(|| StdRwLock::new(None))
|
|
7086
7331
|
}
|
|
7087
7332
|
|
|
7333
|
+
#[cfg(test)]
|
|
7334
|
+
fn bridge_chatgpt_auth_cache_path_override() -> &'static StdRwLock<Option<PathBuf>> {
|
|
7335
|
+
static OVERRIDE: OnceLock<StdRwLock<Option<PathBuf>>> = OnceLock::new();
|
|
7336
|
+
OVERRIDE.get_or_init(|| StdRwLock::new(None))
|
|
7337
|
+
}
|
|
7338
|
+
|
|
7339
|
+
#[cfg(test)]
|
|
7340
|
+
fn set_bridge_chatgpt_auth_cache_path_override(path: Option<PathBuf>) {
|
|
7341
|
+
if let Ok(mut guard) = bridge_chatgpt_auth_cache_path_override().write() {
|
|
7342
|
+
*guard = path;
|
|
7343
|
+
}
|
|
7344
|
+
}
|
|
7345
|
+
|
|
7088
7346
|
fn resolve_bridge_chatgpt_auth_cache_path() -> Option<PathBuf> {
|
|
7347
|
+
#[cfg(test)]
|
|
7348
|
+
if let Ok(guard) = bridge_chatgpt_auth_cache_path_override().read() {
|
|
7349
|
+
if let Some(path) = guard.clone() {
|
|
7350
|
+
return Some(path);
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7089
7354
|
let workdir = read_non_empty_env("BRIDGE_WORKDIR").map(PathBuf::from)?;
|
|
7090
7355
|
Some(workdir.join(BRIDGE_CHATGPT_AUTH_CACHE_FILE_NAME))
|
|
7091
7356
|
}
|
|
@@ -11618,6 +11883,51 @@ fn normalize_path(path: &Path) -> PathBuf {
|
|
|
11618
11883
|
mod tests {
|
|
11619
11884
|
use super::*;
|
|
11620
11885
|
|
|
11886
|
+
fn bridge_chatgpt_auth_test_lock() -> &'static std::sync::Mutex<()> {
|
|
11887
|
+
static LOCK: OnceLock<std::sync::Mutex<()>> = OnceLock::new();
|
|
11888
|
+
LOCK.get_or_init(|| std::sync::Mutex::new(()))
|
|
11889
|
+
}
|
|
11890
|
+
|
|
11891
|
+
struct TestBridgeChatGptAuthCacheScope {
|
|
11892
|
+
_guard: std::sync::MutexGuard<'static, ()>,
|
|
11893
|
+
temp_dir: PathBuf,
|
|
11894
|
+
}
|
|
11895
|
+
|
|
11896
|
+
impl TestBridgeChatGptAuthCacheScope {
|
|
11897
|
+
fn new() -> Self {
|
|
11898
|
+
let guard = bridge_chatgpt_auth_test_lock()
|
|
11899
|
+
.lock()
|
|
11900
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
11901
|
+
clear_cached_bridge_chatgpt_auth();
|
|
11902
|
+
|
|
11903
|
+
let nonce = SystemTime::now()
|
|
11904
|
+
.duration_since(SystemTime::UNIX_EPOCH)
|
|
11905
|
+
.expect("valid time")
|
|
11906
|
+
.as_nanos();
|
|
11907
|
+
let temp_dir = env::temp_dir().join(format!(
|
|
11908
|
+
"clawdex-bridge-chatgpt-auth-test-{}-{nonce}",
|
|
11909
|
+
std::process::id()
|
|
11910
|
+
));
|
|
11911
|
+
std::fs::create_dir_all(&temp_dir).expect("create auth cache test dir");
|
|
11912
|
+
set_bridge_chatgpt_auth_cache_path_override(Some(
|
|
11913
|
+
temp_dir.join(BRIDGE_CHATGPT_AUTH_CACHE_FILE_NAME),
|
|
11914
|
+
));
|
|
11915
|
+
|
|
11916
|
+
Self {
|
|
11917
|
+
_guard: guard,
|
|
11918
|
+
temp_dir,
|
|
11919
|
+
}
|
|
11920
|
+
}
|
|
11921
|
+
}
|
|
11922
|
+
|
|
11923
|
+
impl Drop for TestBridgeChatGptAuthCacheScope {
|
|
11924
|
+
fn drop(&mut self) {
|
|
11925
|
+
clear_cached_bridge_chatgpt_auth();
|
|
11926
|
+
set_bridge_chatgpt_auth_cache_path_override(None);
|
|
11927
|
+
let _ = std::fs::remove_dir_all(&self.temp_dir);
|
|
11928
|
+
}
|
|
11929
|
+
}
|
|
11930
|
+
|
|
11621
11931
|
async fn build_test_bridge(hub: Arc<ClientHub>) -> Arc<AppServerBridge> {
|
|
11622
11932
|
let mut child = Command::new("cat")
|
|
11623
11933
|
.stdin(Stdio::piped())
|
|
@@ -13618,6 +13928,7 @@ mod tests {
|
|
|
13618
13928
|
|
|
13619
13929
|
#[tokio::test]
|
|
13620
13930
|
async fn successful_chatgpt_auth_token_login_populates_bridge_auth_cache() {
|
|
13931
|
+
let _auth_cache_scope = TestBridgeChatGptAuthCacheScope::new();
|
|
13621
13932
|
clear_cached_bridge_chatgpt_auth();
|
|
13622
13933
|
|
|
13623
13934
|
let hub = Arc::new(ClientHub::new());
|
|
@@ -13665,6 +13976,7 @@ mod tests {
|
|
|
13665
13976
|
|
|
13666
13977
|
#[tokio::test]
|
|
13667
13978
|
async fn successful_account_logout_clears_cached_bridge_chatgpt_auth() {
|
|
13979
|
+
let _auth_cache_scope = TestBridgeChatGptAuthCacheScope::new();
|
|
13668
13980
|
clear_cached_bridge_chatgpt_auth();
|
|
13669
13981
|
cache_bridge_chatgpt_auth(BridgeChatGptAuthBundle {
|
|
13670
13982
|
access_token: "cached-before-logout".to_string(),
|
|
@@ -14103,4 +14415,40 @@ mod tests {
|
|
|
14103
14415
|
|
|
14104
14416
|
shutdown_test_backend(&state.backend).await;
|
|
14105
14417
|
}
|
|
14418
|
+
|
|
14419
|
+
#[test]
|
|
14420
|
+
fn github_oauth_scope_header_parsing_is_trimmed_and_lowercased() {
|
|
14421
|
+
let scopes = parse_github_oauth_scopes(Some("codespace, repo, Read:User , public_repo"));
|
|
14422
|
+
assert_eq!(
|
|
14423
|
+
scopes,
|
|
14424
|
+
vec![
|
|
14425
|
+
"codespace".to_string(),
|
|
14426
|
+
"repo".to_string(),
|
|
14427
|
+
"read:user".to_string(),
|
|
14428
|
+
"public_repo".to_string()
|
|
14429
|
+
]
|
|
14430
|
+
);
|
|
14431
|
+
}
|
|
14432
|
+
|
|
14433
|
+
#[test]
|
|
14434
|
+
fn github_repo_scope_check_accepts_repo_and_public_repo() {
|
|
14435
|
+
assert!(github_scopes_allow_repo_access(&["repo".to_string()]));
|
|
14436
|
+
assert!(github_scopes_allow_repo_access(
|
|
14437
|
+
&["public_repo".to_string()]
|
|
14438
|
+
));
|
|
14439
|
+
assert!(!github_scopes_allow_repo_access(&[
|
|
14440
|
+
"codespace".to_string(),
|
|
14441
|
+
"read:user".to_string()
|
|
14442
|
+
]));
|
|
14443
|
+
}
|
|
14444
|
+
|
|
14445
|
+
#[test]
|
|
14446
|
+
fn github_git_auth_accepts_github_app_user_tokens_without_scope_headers() {
|
|
14447
|
+
assert!(github_token_can_be_used_for_git_auth(&[]));
|
|
14448
|
+
assert!(github_token_can_be_used_for_git_auth(&["repo".to_string()]));
|
|
14449
|
+
assert!(!github_token_can_be_used_for_git_auth(&[
|
|
14450
|
+
"codespace".to_string(),
|
|
14451
|
+
"read:user".to_string()
|
|
14452
|
+
]));
|
|
14453
|
+
}
|
|
14106
14454
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|