androdex 1.1.3 → 1.1.6
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 +90 -37
- package/bin/androdex.js +3 -1
- package/bin/cli.js +159 -56
- package/package.json +8 -4
- package/src/account-status.js +127 -0
- package/src/bridge.js +805 -169
- package/src/codex-desktop-refresher.js +144 -161
- package/src/codex-rpc-client.js +102 -0
- package/src/codex-transport.js +42 -46
- package/src/daemon-state.js +147 -0
- package/src/git-handler.js +608 -32
- package/src/index.js +24 -27
- package/src/macos-launch-agent.js +557 -0
- package/src/notifications-handler.js +104 -0
- package/src/push-notification-completion-dedupe.js +146 -0
- package/src/push-notification-service-client.js +147 -0
- package/src/push-notification-tracker.js +502 -0
- package/src/qr.js +1 -1
- package/src/rollout-live-mirror.js +705 -0
- package/src/rollout-watch.js +519 -3
- package/src/runtime-compat.js +382 -0
- package/src/secure-device-state.js +154 -67
- package/src/secure-transport.js +58 -20
- package/src/session-state.js +4 -7
- package/src/thread-context-handler.js +79 -0
- package/src/workspace-browser.js +216 -0
- package/src/workspace-handler.js +78 -8
- package/src/workspace-runtime.js +221 -0
- package/src/codex-desktop-launcher.js +0 -93
- package/src/daemon-control.js +0 -191
- package/src/daemon-runtime.js +0 -135
- package/src/daemon-store.js +0 -92
- package/src/host-runtime.js +0 -554
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# androdex
|
|
2
2
|
|
|
3
|
-
`androdex` is the host
|
|
3
|
+
`androdex` is the macOS host bridge for the Androdex Android client.
|
|
4
4
|
|
|
5
|
-
It
|
|
5
|
+
It keeps Codex running locally on the Mac, exposes a relay-backed encrypted session for Android, and uses a launchd-managed background service so pairing and reconnect survive terminal closes.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -13,59 +13,84 @@ npm install -g androdex
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
androdex pair
|
|
17
16
|
androdex up
|
|
18
|
-
androdex
|
|
17
|
+
androdex start
|
|
18
|
+
androdex restart
|
|
19
|
+
androdex stop
|
|
20
|
+
androdex status
|
|
19
21
|
androdex reset-pairing
|
|
20
22
|
androdex resume
|
|
21
|
-
androdex watch
|
|
23
|
+
androdex watch [threadId]
|
|
22
24
|
```
|
|
23
25
|
|
|
26
|
+
## Command Model
|
|
27
|
+
|
|
28
|
+
- `androdex up`
|
|
29
|
+
Starts or refreshes the macOS bridge service, waits for a fresh pairing QR, prints it, and binds the current working directory as the active workspace.
|
|
30
|
+
- `androdex start`
|
|
31
|
+
Starts the launchd-managed macOS bridge service without changing the active workspace.
|
|
32
|
+
- `androdex restart`
|
|
33
|
+
Restarts the launchd-managed macOS bridge service without changing the active workspace.
|
|
34
|
+
- `androdex stop`
|
|
35
|
+
Stops the macOS bridge service and clears stale in-memory runtime state.
|
|
36
|
+
- `androdex status`
|
|
37
|
+
Prints launchd status, persisted bridge status, and the stdout/stderr log paths under `~/.androdex`.
|
|
38
|
+
- `androdex run`
|
|
39
|
+
Runs the bridge in the foreground.
|
|
40
|
+
- `androdex run-service`
|
|
41
|
+
Internal launchd entrypoint used by the installed plist.
|
|
42
|
+
- `androdex reset-pairing`
|
|
43
|
+
Stops the service and clears the saved trusted-device state so the next `androdex up` starts fresh.
|
|
44
|
+
- `androdex resume`
|
|
45
|
+
Reopens the last active thread in the local Codex desktop app if available.
|
|
46
|
+
- `androdex watch [threadId]`
|
|
47
|
+
Tails the rollout log for the selected thread in real time.
|
|
48
|
+
|
|
24
49
|
## What it does
|
|
25
50
|
|
|
26
|
-
-
|
|
27
|
-
- prints a pairing QR
|
|
28
|
-
-
|
|
51
|
+
- runs a launchd-managed macOS bridge service with label `io.androdex.bridge`
|
|
52
|
+
- prints a pairing QR for the current relay session
|
|
53
|
+
- restores the last active workspace on service start when possible
|
|
54
|
+
- lets the Android client browse host folders and switch workspaces remotely
|
|
29
55
|
- forwards JSON-RPC traffic between the host and the Android client
|
|
30
|
-
- handles git and workspace actions on the host machine
|
|
31
|
-
|
|
32
|
-
## Commands
|
|
33
|
-
|
|
34
|
-
### `androdex up`
|
|
35
|
-
|
|
36
|
-
Activates the current workspace in the daemon and launches `codex app-server` locally if needed.
|
|
37
|
-
|
|
38
|
-
### `androdex pair`
|
|
56
|
+
- handles git and workspace actions on the host machine, including branch/worktree management and reverse-patch safety checks
|
|
57
|
+
- keeps the local Codex desktop app aligned with phone-authored thread activity when desktop refresh is enabled
|
|
39
58
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
### `androdex daemon [start|stop|status]`
|
|
43
|
-
|
|
44
|
-
Manages the background daemon that owns the stable host identity and relay presence.
|
|
59
|
+
## Environment variables
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
`androdex` accepts `ANDRODEX_*` variables.
|
|
47
62
|
|
|
48
|
-
|
|
63
|
+
Useful variables:
|
|
49
64
|
|
|
50
|
-
|
|
65
|
+
- `ANDRODEX_RELAY`: set the relay URL explicitly and override any packaged default
|
|
66
|
+
- `ANDRODEX_DEFAULT_RELAY_URL`: override the built-in default relay URL when no explicit override is present
|
|
67
|
+
- `ANDRODEX_CODEX_ENDPOINT`: connect to an existing Codex WebSocket instead of spawning a local runtime
|
|
68
|
+
- `ANDRODEX_REFRESH_ENABLED`: enable or disable desktop refresh explicitly
|
|
69
|
+
- `ANDRODEX_REFRESH_DEBOUNCE_MS`: adjust refresh debounce timing
|
|
70
|
+
- `ANDRODEX_REFRESH_COMMAND`: override desktop refresh with a custom command
|
|
71
|
+
- `ANDRODEX_CODEX_BUNDLE_ID`: override the Codex desktop bundle ID on macOS
|
|
72
|
+
- `ANDRODEX_PUSH_SERVICE_URL`: optional Android push service endpoint for device registration and completion notifications
|
|
51
73
|
|
|
52
|
-
|
|
74
|
+
The bridge resolves relay configuration in this order:
|
|
53
75
|
|
|
54
|
-
|
|
76
|
+
1. `ANDRODEX_RELAY`
|
|
77
|
+
2. `ANDRODEX_DEFAULT_RELAY_URL`
|
|
78
|
+
3. `wss://relay.androdex.xyz/relay`
|
|
55
79
|
|
|
56
|
-
|
|
80
|
+
Common relay patterns:
|
|
57
81
|
|
|
58
|
-
|
|
82
|
+
```sh
|
|
83
|
+
# Built-in public relay
|
|
84
|
+
androdex up
|
|
59
85
|
|
|
60
|
-
|
|
86
|
+
# Local relay on your own network
|
|
87
|
+
ANDRODEX_RELAY=ws://192.168.x.x:8787/relay androdex up
|
|
61
88
|
|
|
62
|
-
|
|
89
|
+
# Public relay you control
|
|
90
|
+
ANDRODEX_RELAY=wss://relay.example.com/relay androdex up
|
|
91
|
+
```
|
|
63
92
|
|
|
64
|
-
- `
|
|
65
|
-
- `ANDRODEX_CODEX_ENDPOINT`: connect to an existing Codex WebSocket instead of spawning a local runtime
|
|
66
|
-
- `ANDRODEX_REFRESH_ENABLED`: enable the macOS desktop refresh workaround explicitly
|
|
67
|
-
- `ANDRODEX_REFRESH_DEBOUNCE_MS`: adjust refresh debounce timing
|
|
68
|
-
- `ANDRODEX_REFRESH_COMMAND`: override desktop refresh with a custom command
|
|
93
|
+
If you change relay URLs after pairing, run `androdex reset-pairing` and pair again with `androdex up` so Android gets a fresh payload for the new relay session.
|
|
69
94
|
|
|
70
95
|
## Source builds
|
|
71
96
|
|
|
@@ -77,8 +102,36 @@ npm install
|
|
|
77
102
|
npm start
|
|
78
103
|
```
|
|
79
104
|
|
|
105
|
+
## Release
|
|
106
|
+
|
|
107
|
+
Publish the npm package from this directory, not from the repository root:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
cd androdex-bridge
|
|
111
|
+
npm test
|
|
112
|
+
npm pack --dry-run
|
|
113
|
+
npm version patch --no-git-tag-version
|
|
114
|
+
npm publish --access public
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Verify the published version after the release:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
npm view androdex version
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If your npm account requires write-time 2FA, rerun the publish command with `--otp=<code>`.
|
|
124
|
+
|
|
125
|
+
## Manual Smoke Checklist
|
|
126
|
+
|
|
127
|
+
1. Run `androdex up` and confirm the Android app can pair successfully.
|
|
128
|
+
2. Run `androdex up` inside a workspace and confirm the host keeps Codex bound to that local project.
|
|
129
|
+
3. From Android, open an existing thread and create a new one to confirm the remote client flow still works end to end.
|
|
130
|
+
4. If desktop refresh is enabled, verify phone-authored thread activity updates the host Codex desktop via the Settings-bounce remount workaround.
|
|
131
|
+
5. Restart the launchd service or reconnect the phone and confirm the saved pairing and active workspace recover without losing host-local state.
|
|
132
|
+
|
|
80
133
|
## Project status
|
|
81
134
|
|
|
82
|
-
This package is part of Androdex, a local
|
|
135
|
+
This package is part of Androdex, a macOS host-local Codex plus Android remote-access workflow.
|
|
83
136
|
|
|
84
137
|
Credit for the upstream fork chain remains with [relaydex](https://github.com/Ranats/relaydex) and [Remodex](https://github.com/Emanuele-web04/remodex).
|
package/bin/androdex.js
CHANGED
package/bin/cli.js
CHANGED
|
@@ -1,105 +1,208 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// FILE: cli.js
|
|
3
|
-
// Purpose: CLI surface for
|
|
3
|
+
// Purpose: CLI surface for foreground bridge runs, pairing reset, thread resume, and macOS service control.
|
|
4
4
|
// Layer: CLI binary
|
|
5
5
|
// Exports: none
|
|
6
6
|
// Depends on: ../src
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
printMacOSBridgePairingQr,
|
|
10
|
+
printMacOSBridgeServiceStatus,
|
|
11
|
+
readBridgeConfig,
|
|
12
|
+
resetMacOSBridgePairing,
|
|
13
|
+
runMacOSBridgeService,
|
|
12
14
|
startBridge,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
startMacOSBridgeService,
|
|
16
|
+
stopMacOSBridgeService,
|
|
15
17
|
resetBridgePairing,
|
|
16
18
|
openLastActiveThread,
|
|
17
19
|
watchThreadRollout,
|
|
18
20
|
} = require("../src");
|
|
19
|
-
const {
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const { version } = require("../package.json");
|
|
22
|
+
|
|
23
|
+
const defaultDeps = {
|
|
24
|
+
printMacOSBridgePairingQr,
|
|
25
|
+
printMacOSBridgeServiceStatus,
|
|
26
|
+
readBridgeConfig,
|
|
27
|
+
resetMacOSBridgePairing,
|
|
28
|
+
runMacOSBridgeService,
|
|
29
|
+
startBridge,
|
|
30
|
+
startMacOSBridgeService,
|
|
31
|
+
stopMacOSBridgeService,
|
|
32
|
+
resetBridgePairing,
|
|
33
|
+
openLastActiveThread,
|
|
34
|
+
watchThreadRollout,
|
|
35
|
+
};
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
37
|
+
if (require.main === module) {
|
|
38
|
+
void main();
|
|
39
|
+
}
|
|
29
40
|
|
|
30
|
-
async function main(
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
async function main({
|
|
42
|
+
argv = process.argv,
|
|
43
|
+
platform = process.platform,
|
|
44
|
+
consoleImpl = console,
|
|
45
|
+
exitImpl = process.exit,
|
|
46
|
+
deps = defaultDeps,
|
|
47
|
+
} = {}) {
|
|
48
|
+
const command = argv[2] || "up";
|
|
49
|
+
|
|
50
|
+
if (isVersionCommand(command)) {
|
|
51
|
+
consoleImpl.log(version);
|
|
33
52
|
return;
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
if (command === "up") {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
assertMacOSCommand(command, {
|
|
57
|
+
platform,
|
|
58
|
+
consoleImpl,
|
|
59
|
+
exitImpl,
|
|
60
|
+
});
|
|
61
|
+
deps.readBridgeConfig();
|
|
62
|
+
const result = await deps.startMacOSBridgeService({
|
|
63
|
+
waitForPairing: true,
|
|
64
|
+
activeCwd: process.cwd(),
|
|
65
|
+
});
|
|
66
|
+
deps.printMacOSBridgePairingQr({
|
|
67
|
+
pairingSession: result.pairingSession,
|
|
68
|
+
});
|
|
40
69
|
return;
|
|
41
70
|
}
|
|
42
71
|
|
|
43
|
-
if (command === "
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
if (command === "run") {
|
|
73
|
+
assertMacOSCommand(command, {
|
|
74
|
+
platform,
|
|
75
|
+
consoleImpl,
|
|
76
|
+
exitImpl,
|
|
77
|
+
});
|
|
78
|
+
deps.startBridge();
|
|
46
79
|
return;
|
|
47
80
|
}
|
|
48
81
|
|
|
49
|
-
if (command === "
|
|
50
|
-
|
|
82
|
+
if (command === "run-service") {
|
|
83
|
+
assertMacOSCommand(command, {
|
|
84
|
+
platform,
|
|
85
|
+
consoleImpl,
|
|
86
|
+
exitImpl,
|
|
87
|
+
});
|
|
88
|
+
deps.runMacOSBridgeService();
|
|
51
89
|
return;
|
|
52
90
|
}
|
|
53
91
|
|
|
54
|
-
if (command === "
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
if (command === "start") {
|
|
93
|
+
assertMacOSCommand(command, {
|
|
94
|
+
platform,
|
|
95
|
+
consoleImpl,
|
|
96
|
+
exitImpl,
|
|
97
|
+
});
|
|
98
|
+
deps.readBridgeConfig();
|
|
99
|
+
await deps.startMacOSBridgeService({
|
|
100
|
+
waitForPairing: false,
|
|
101
|
+
});
|
|
102
|
+
consoleImpl.log("[androdex] macOS bridge service is running.");
|
|
57
103
|
return;
|
|
58
104
|
}
|
|
59
105
|
|
|
60
|
-
if (command === "
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
106
|
+
if (command === "restart") {
|
|
107
|
+
assertMacOSCommand(command, {
|
|
108
|
+
platform,
|
|
109
|
+
consoleImpl,
|
|
110
|
+
exitImpl,
|
|
111
|
+
});
|
|
112
|
+
deps.readBridgeConfig();
|
|
113
|
+
await deps.startMacOSBridgeService({
|
|
114
|
+
waitForPairing: false,
|
|
115
|
+
});
|
|
116
|
+
consoleImpl.log("[androdex] macOS bridge service restarted.");
|
|
65
117
|
return;
|
|
66
118
|
}
|
|
67
119
|
|
|
68
|
-
if (command === "
|
|
69
|
-
|
|
120
|
+
if (command === "stop") {
|
|
121
|
+
assertMacOSCommand(command, {
|
|
122
|
+
platform,
|
|
123
|
+
consoleImpl,
|
|
124
|
+
exitImpl,
|
|
125
|
+
});
|
|
126
|
+
deps.stopMacOSBridgeService();
|
|
127
|
+
consoleImpl.log("[androdex] macOS bridge service stopped.");
|
|
70
128
|
return;
|
|
71
129
|
}
|
|
72
130
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
131
|
+
if (command === "status") {
|
|
132
|
+
assertMacOSCommand(command, {
|
|
133
|
+
platform,
|
|
134
|
+
consoleImpl,
|
|
135
|
+
exitImpl,
|
|
136
|
+
});
|
|
137
|
+
deps.printMacOSBridgeServiceStatus();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (command === "reset-pairing") {
|
|
142
|
+
try {
|
|
143
|
+
if (platform === "darwin") {
|
|
144
|
+
deps.resetMacOSBridgePairing();
|
|
145
|
+
consoleImpl.log("[androdex] Stopped the macOS bridge service and cleared the saved pairing state. Run `androdex up` to pair again.");
|
|
146
|
+
} else {
|
|
147
|
+
deps.resetBridgePairing();
|
|
148
|
+
consoleImpl.log("[androdex] Cleared the saved pairing state. Run `androdex up` to pair again.");
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
consoleImpl.error(`[androdex] ${(error && error.message) || "Failed to clear the saved pairing state."}`);
|
|
152
|
+
exitImpl(1);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
77
156
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
157
|
+
if (command === "resume") {
|
|
158
|
+
try {
|
|
159
|
+
const state = deps.openLastActiveThread();
|
|
160
|
+
consoleImpl.log(
|
|
161
|
+
`[androdex] Opened last active thread: ${state.threadId} (${state.source || "unknown"})`
|
|
162
|
+
);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
consoleImpl.error(`[androdex] ${(error && error.message) || "Failed to reopen the last thread."}`);
|
|
165
|
+
exitImpl(1);
|
|
166
|
+
}
|
|
82
167
|
return;
|
|
83
168
|
}
|
|
84
169
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
170
|
+
if (command === "watch") {
|
|
171
|
+
try {
|
|
172
|
+
deps.watchThreadRollout(argv[3] || "");
|
|
173
|
+
} catch (error) {
|
|
174
|
+
consoleImpl.error(`[androdex] ${(error && error.message) || "Failed to watch the thread rollout."}`);
|
|
175
|
+
exitImpl(1);
|
|
176
|
+
}
|
|
88
177
|
return;
|
|
89
178
|
}
|
|
90
179
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
180
|
+
consoleImpl.error(`Unknown command: ${command}`);
|
|
181
|
+
consoleImpl.error(
|
|
182
|
+
"Usage: androdex up | androdex run | androdex start | androdex restart | androdex stop | androdex status | "
|
|
183
|
+
+ "androdex reset-pairing | androdex resume | androdex watch [threadId] | androdex --version"
|
|
184
|
+
);
|
|
185
|
+
exitImpl(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assertMacOSCommand(name, {
|
|
189
|
+
platform = process.platform,
|
|
190
|
+
consoleImpl = console,
|
|
191
|
+
exitImpl = process.exit,
|
|
192
|
+
} = {}) {
|
|
193
|
+
if (platform === "darwin") {
|
|
94
194
|
return;
|
|
95
195
|
}
|
|
96
196
|
|
|
97
|
-
|
|
197
|
+
consoleImpl.error(`[androdex] \`${name}\` is only available on macOS right now.`);
|
|
198
|
+
exitImpl(1);
|
|
98
199
|
}
|
|
99
200
|
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
console.log(`${CLI_PREFIX} Host ID: ${status.hostId || "unavailable"}`);
|
|
103
|
-
console.log(`${CLI_PREFIX} Workspace: ${status.currentCwd || "none"}`);
|
|
104
|
-
console.log(`${CLI_PREFIX} Workspace active: ${status.workspaceActive ? "yes" : "no"}`);
|
|
201
|
+
function isVersionCommand(value) {
|
|
202
|
+
return value === "-v" || value === "--v" || value === "-V" || value === "--version" || value === "version";
|
|
105
203
|
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
isVersionCommand,
|
|
207
|
+
main,
|
|
208
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "androdex",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.6",
|
|
4
|
+
"description": "macOS host bridge between Codex and the Androdex Android app. Run `androdex up` to start.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"androdex": "bin/androdex.js"
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"start": "node ./bin/androdex.js up",
|
|
17
|
-
"test": "node --test
|
|
17
|
+
"test": "node --test --test-concurrency=1"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"androdex",
|
|
21
21
|
"codex",
|
|
22
22
|
"bridge",
|
|
23
23
|
"cli",
|
|
24
|
-
"android"
|
|
24
|
+
"android",
|
|
25
|
+
"macos"
|
|
25
26
|
],
|
|
26
27
|
"author": "Robert Gordon",
|
|
27
28
|
"license": "ISC",
|
|
@@ -29,6 +30,9 @@
|
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=18"
|
|
31
32
|
},
|
|
33
|
+
"os": [
|
|
34
|
+
"darwin"
|
|
35
|
+
],
|
|
32
36
|
"publishConfig": {
|
|
33
37
|
"access": "public"
|
|
34
38
|
},
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// FILE: account-status.js
|
|
2
|
+
// Purpose: Converts raw Codex account/auth responses into a sanitized host-account snapshot for Android.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: composeAccountStatus, composeSanitizedAuthStatusFromSettledResults, redactAuthStatus
|
|
5
|
+
// Depends on: ../package.json
|
|
6
|
+
|
|
7
|
+
const { version: bridgePackageVersion = "" } = require("../package.json");
|
|
8
|
+
|
|
9
|
+
function composeAccountStatus({
|
|
10
|
+
accountRead = null,
|
|
11
|
+
authStatus = null,
|
|
12
|
+
loginInFlight = false,
|
|
13
|
+
bridgeVersionInfo = null,
|
|
14
|
+
} = {}) {
|
|
15
|
+
const account = accountRead?.account || null;
|
|
16
|
+
const authToken = normalizeString(authStatus?.authToken);
|
|
17
|
+
const hasAccountLogin = hasExplicitAccountLogin(account);
|
|
18
|
+
const authMethod = firstNonEmpty([
|
|
19
|
+
normalizeString(authStatus?.authMethod),
|
|
20
|
+
normalizeString(account?.type),
|
|
21
|
+
]) || null;
|
|
22
|
+
const tokenReady = Boolean(authToken);
|
|
23
|
+
const requiresOpenaiAuth = Boolean(accountRead?.requiresOpenaiAuth || authStatus?.requiresOpenaiAuth);
|
|
24
|
+
const hasPriorLoginContext = hasAccountLogin || Boolean(authMethod);
|
|
25
|
+
const needsReauth = !loginInFlight && requiresOpenaiAuth && hasPriorLoginContext;
|
|
26
|
+
const isAuthenticated = !needsReauth && (tokenReady || hasAccountLogin);
|
|
27
|
+
const status = isAuthenticated
|
|
28
|
+
? "authenticated"
|
|
29
|
+
: (loginInFlight ? "pending_login" : (needsReauth ? "expired" : "not_logged_in"));
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
status,
|
|
33
|
+
authMethod,
|
|
34
|
+
email: normalizeString(account?.email) || null,
|
|
35
|
+
planType: normalizeString(account?.planType) || null,
|
|
36
|
+
loginInFlight: Boolean(loginInFlight),
|
|
37
|
+
needsReauth,
|
|
38
|
+
tokenReady,
|
|
39
|
+
expiresAt: null,
|
|
40
|
+
requiresOpenaiAuth,
|
|
41
|
+
bridgeVersion: firstNonEmpty([
|
|
42
|
+
normalizeString(bridgeVersionInfo?.bridgeVersion),
|
|
43
|
+
normalizeString(bridgePackageVersion),
|
|
44
|
+
]) || null,
|
|
45
|
+
bridgeLatestVersion: normalizeString(bridgeVersionInfo?.bridgeLatestVersion) || null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function redactAuthStatus(authStatus = null, extras = {}) {
|
|
50
|
+
const composed = composeAccountStatus({
|
|
51
|
+
accountRead: extras.accountRead || null,
|
|
52
|
+
authStatus,
|
|
53
|
+
loginInFlight: Boolean(extras.loginInFlight),
|
|
54
|
+
bridgeVersionInfo: extras.bridgeVersionInfo || null,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
authMethod: composed.authMethod,
|
|
59
|
+
status: composed.status,
|
|
60
|
+
email: composed.email,
|
|
61
|
+
planType: composed.planType,
|
|
62
|
+
loginInFlight: composed.loginInFlight,
|
|
63
|
+
needsReauth: composed.needsReauth,
|
|
64
|
+
tokenReady: composed.tokenReady,
|
|
65
|
+
expiresAt: composed.expiresAt,
|
|
66
|
+
bridgeVersion: composed.bridgeVersion,
|
|
67
|
+
bridgeLatestVersion: composed.bridgeLatestVersion,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function composeSanitizedAuthStatusFromSettledResults({
|
|
72
|
+
accountReadResult = null,
|
|
73
|
+
authStatusResult = null,
|
|
74
|
+
loginInFlight = false,
|
|
75
|
+
bridgeVersionInfo = null,
|
|
76
|
+
} = {}) {
|
|
77
|
+
const accountRead = accountReadResult?.status === "fulfilled" ? accountReadResult.value : null;
|
|
78
|
+
const authStatus = authStatusResult?.status === "fulfilled" ? authStatusResult.value : null;
|
|
79
|
+
|
|
80
|
+
if (!accountRead && !authStatus) {
|
|
81
|
+
const error = new Error("Unable to read host account status from the bridge.");
|
|
82
|
+
error.errorCode = "auth_status_unavailable";
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return redactAuthStatus(authStatus, {
|
|
87
|
+
accountRead,
|
|
88
|
+
loginInFlight: Boolean(loginInFlight),
|
|
89
|
+
bridgeVersionInfo,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasExplicitAccountLogin(account) {
|
|
94
|
+
if (!account || typeof account !== "object") {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (parseBoolean(account.loggedIn) || parseBoolean(account.logged_in) || parseBoolean(account.isLoggedIn)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Boolean(normalizeString(account.email));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function firstNonEmpty(values) {
|
|
106
|
+
for (const value of values) {
|
|
107
|
+
const normalized = normalizeString(value);
|
|
108
|
+
if (normalized) {
|
|
109
|
+
return normalized;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeString(value) {
|
|
116
|
+
return typeof value === "string" ? value.trim() : "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseBoolean(value) {
|
|
120
|
+
return value === true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
composeAccountStatus,
|
|
125
|
+
composeSanitizedAuthStatusFromSettledResults,
|
|
126
|
+
redactAuthStatus,
|
|
127
|
+
};
|