yaver-feedback-react-native 0.8.4 → 0.8.7

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 CHANGED
@@ -16,7 +16,7 @@ Manual fallback:
16
16
  npm install yaver-feedback-react-native
17
17
  ```
18
18
 
19
- > **Mobile only.** This SDK targets React Native (iOS + Android). A `yaver-feedback-web` package exists for browser apps but currently expects a bring-your-own auth token the equivalent in-app sign-in UX (Apple / Google / GitHub / GitLab / Microsoft / email) for the web SDK will land in a future release. Open an issue if you need it sooner.
19
+ > **Mobile only.** This SDK targets React Native (iOS + Android). For browser apps use `yaver-feedback-web`, which now has its own popup OAuth, email auth, login modal, and device picker. The React Native and web SDKs share the same account/device model, but use platform-specific auth UX.
20
20
 
21
21
  ### Peer dependencies
22
22
 
@@ -61,6 +61,31 @@ const AuthOverlay = () => {
61
61
  const [pickerVisible, setPickerVisible] = (0, react_1.useState)(false);
62
62
  const [token, setToken] = (0, react_1.useState)(null);
63
63
  const [pendingInviteCode, setPendingInviteCode] = (0, react_1.useState)(null);
64
+ const activeOverlayRef = (0, react_1.useRef)('none');
65
+ const openLogin = (0, react_1.useCallback)(() => {
66
+ activeOverlayRef.current = 'login';
67
+ setGuestVisible(false);
68
+ setPickerVisible(false);
69
+ setLoginVisible(true);
70
+ }, []);
71
+ const openGuest = (0, react_1.useCallback)(() => {
72
+ activeOverlayRef.current = 'guest';
73
+ setLoginVisible(false);
74
+ setPickerVisible(false);
75
+ setGuestVisible(true);
76
+ }, []);
77
+ const openPicker = (0, react_1.useCallback)(() => {
78
+ activeOverlayRef.current = 'picker';
79
+ setLoginVisible(false);
80
+ setGuestVisible(false);
81
+ setPickerVisible(true);
82
+ }, []);
83
+ const closeAll = (0, react_1.useCallback)(() => {
84
+ activeOverlayRef.current = 'none';
85
+ setLoginVisible(false);
86
+ setGuestVisible(false);
87
+ setPickerVisible(false);
88
+ }, []);
64
89
  (0, react_1.useEffect)(() => {
65
90
  let mounted = true;
66
91
  (async () => {
@@ -68,65 +93,67 @@ const AuthOverlay = () => {
68
93
  if (mounted && cached)
69
94
  setToken(cached);
70
95
  })();
71
- const loginSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startLogin', () => setLoginVisible(true));
96
+ const loginSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startLogin', () => {
97
+ if (activeOverlayRef.current !== 'none')
98
+ return;
99
+ openLogin();
100
+ });
72
101
  const pickerSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startMachinePicker', async () => {
102
+ if (activeOverlayRef.current !== 'none')
103
+ return;
73
104
  const cached = await (0, auth_1.getToken)();
74
105
  if (cached)
75
106
  setToken(cached);
76
107
  if (cached)
77
- setPickerVisible(true);
108
+ openPicker();
78
109
  });
79
110
  return () => {
80
111
  mounted = false;
81
112
  loginSub.remove();
82
113
  pickerSub.remove();
83
114
  };
84
- }, []);
115
+ }, [openLogin, openPicker]);
85
116
  const continueAfterAuth = async (newToken, inviteCode) => {
86
117
  setToken(newToken);
87
118
  await YaverFeedback_1.YaverFeedback.setAuthToken(newToken);
88
119
  const devices = await (0, auth_1.listReachableDevices)(newToken).catch(() => ({ owned: [], shared: [] }));
89
- setLoginVisible(false);
90
120
  const cleanedInviteCode = (inviteCode ?? '').trim().toUpperCase();
91
121
  if (cleanedInviteCode) {
92
122
  setPendingInviteCode(cleanedInviteCode);
93
- setGuestVisible(true);
123
+ openGuest();
94
124
  return;
95
125
  }
96
126
  if (devices.owned.length === 0 && devices.shared.length === 0) {
97
- setGuestVisible(true);
127
+ openGuest();
98
128
  return;
99
129
  }
100
- setPickerVisible(true);
130
+ openPicker();
101
131
  };
102
132
  const handleLoggedIn = async (newToken, opts) => {
103
133
  await continueAfterAuth(newToken, opts?.inviteCode);
104
134
  };
105
135
  const handleDevicePicked = async (device) => {
106
136
  await YaverFeedback_1.YaverFeedback.setPreferredDevice(device.deviceId);
107
- setPickerVisible(false);
108
- setGuestVisible(false);
137
+ closeAll();
109
138
  // Continue straight into the feedback flow the user originally triggered.
110
139
  react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
111
140
  };
112
141
  return (<>
113
- <react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={() => setLoginVisible(false)}>
114
- <LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={() => setLoginVisible(false)} initialInviteCode={pendingInviteCode ?? YaverFeedback_1.YaverFeedback.getConfig()?.guestInviteCode}/>
142
+ <react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
143
+ <LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll} initialInviteCode={pendingInviteCode ?? YaverFeedback_1.YaverFeedback.getConfig()?.guestInviteCode}/>
115
144
  </react_native_1.Modal>
116
145
 
117
- <react_native_1.Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={() => setPickerVisible(false)}>
118
- {token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} currentDeviceId={YaverFeedback_1.YaverFeedback.getConfig()?.preferredDeviceId} onPick={handleDevicePicked} onCancel={() => setPickerVisible(false)}/>)}
146
+ <react_native_1.Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
147
+ {token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} currentDeviceId={YaverFeedback_1.YaverFeedback.getConfig()?.preferredDeviceId} onPick={handleDevicePicked} onCancel={closeAll}/>)}
119
148
  </react_native_1.Modal>
120
149
 
121
- <react_native_1.Modal visible={guestVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={() => setGuestVisible(false)}>
150
+ <react_native_1.Modal visible={guestVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
122
151
  {token && (<GuestOnboardingScreen_1.YaverGuestOnboardingScreen token={token} initialInviteCode={pendingInviteCode ?? YaverFeedback_1.YaverFeedback.getConfig()?.guestInviteCode} onContinue={() => {
123
- setGuestVisible(false);
124
152
  setPendingInviteCode(null);
125
- setPickerVisible(true);
153
+ openPicker();
126
154
  }} onCancel={() => {
127
- setGuestVisible(false);
128
155
  setPendingInviteCode(null);
129
- setPickerVisible(true);
156
+ openPicker();
130
157
  }}/>)}
131
158
  </react_native_1.Modal>
132
159
  </>);
@@ -54,6 +54,7 @@ const FeedbackModal = () => {
54
54
  // is our guaranteed UI for bringing the icon back — we surface a
55
55
  // small "Show quick icon" row when this is true.
56
56
  const [quickIconHidden, setQuickIconHidden] = (0, react_1.useState)(false);
57
+ const [runnerAuthModal, setRunnerAuthModal] = (0, react_1.useState)(null);
57
58
  // Vibing-input mode: same expand-on-tap pattern as email login.
58
59
  // Tap "Vibing" once → the button reveals an input + Send; that lets
59
60
  // the user say WHAT they want to vibe on instead of firing a canned
@@ -666,6 +667,29 @@ const FeedbackModal = () => {
666
667
  {/* Screenshot & Fix */}
667
668
  <ActionRow label={action === 'capturing' ? 'Working…' : 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
668
669
 
670
+ {/* Remote sign-in buttons — trigger codex/claude device-auth
671
+ on the selected agent without leaving the app. Opens a
672
+ small native modal showing the verification URL + 8-char
673
+ code the user enters in any browser. No API keys. */}
674
+ <react_native_1.View style={runnerAuthRowStyles.container}>
675
+ <react_native_1.Pressable onPress={() => setRunnerAuthModal('codex')} disabled={busy} style={({ pressed }) => [
676
+ runnerAuthRowStyles.button,
677
+ pressed && runnerAuthRowStyles.buttonPressed,
678
+ busy && runnerAuthRowStyles.buttonDisabled,
679
+ ]} accessibilityRole="button" accessibilityLabel="Remote sign-in Codex">
680
+ <react_native_1.Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</react_native_1.Text>
681
+ <react_native_1.Text style={runnerAuthRowStyles.buttonName}>Codex</react_native_1.Text>
682
+ </react_native_1.Pressable>
683
+ <react_native_1.Pressable onPress={() => setRunnerAuthModal('claude')} disabled={busy} style={({ pressed }) => [
684
+ runnerAuthRowStyles.button,
685
+ pressed && runnerAuthRowStyles.buttonPressed,
686
+ busy && runnerAuthRowStyles.buttonDisabled,
687
+ ]} accessibilityRole="button" accessibilityLabel="Remote sign-in Claude">
688
+ <react_native_1.Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</react_native_1.Text>
689
+ <react_native_1.Text style={runnerAuthRowStyles.buttonName}>Claude</react_native_1.Text>
690
+ </react_native_1.Pressable>
691
+ </react_native_1.View>
692
+
669
693
  {progress !== null && (<react_native_1.View style={styles.progressTrack}>
670
694
  <react_native_1.View style={[
671
695
  styles.progressFill,
@@ -686,6 +710,7 @@ const FeedbackModal = () => {
686
710
  </react_native_1.KeyboardAvoidingView>
687
711
  </react_native_1.Pressable>
688
712
  </react_native_1.Modal>)}
713
+ {runnerAuthModal ? (<RunnerAuthNativeModal runner={runnerAuthModal} onClose={() => setRunnerAuthModal(null)}/>) : null}
689
714
  </>);
690
715
  };
691
716
  exports.FeedbackModal = FeedbackModal;
@@ -1014,3 +1039,259 @@ const styles = react_native_1.StyleSheet.create({
1014
1039
  opacity: 0.7,
1015
1040
  },
1016
1041
  });
1042
+ /**
1043
+ * Minimal native modal for the codex/claude remote sign-in flow. Opens
1044
+ * the device-auth session on the connected agent, surfaces the
1045
+ * verification URL + one-time code, polls every 1.5 s, and turns green
1046
+ * the moment the CLI writes its auth.json. No API keys, no SSH.
1047
+ */
1048
+ const RunnerAuthNativeModal = ({ runner, onClose }) => {
1049
+ const [session, setSession] = (0, react_1.useState)(null);
1050
+ const [startError, setStartError] = (0, react_1.useState)(null);
1051
+ const [copied, setCopied] = (0, react_1.useState)(false);
1052
+ const startedRef = (0, react_1.useRef)(false);
1053
+ (0, react_1.useEffect)(() => {
1054
+ if (startedRef.current)
1055
+ return;
1056
+ startedRef.current = true;
1057
+ (async () => {
1058
+ try {
1059
+ const s = await YaverFeedback_1.YaverFeedback.startRunnerBrowserAuth(runner);
1060
+ setSession(s);
1061
+ }
1062
+ catch (err) {
1063
+ setStartError(err instanceof Error ? err.message : String(err));
1064
+ }
1065
+ })();
1066
+ }, [runner]);
1067
+ (0, react_1.useEffect)(() => {
1068
+ if (!session)
1069
+ return;
1070
+ if (['completed', 'failed', 'cancelled'].includes(session.status))
1071
+ return;
1072
+ const iv = setInterval(async () => {
1073
+ try {
1074
+ const s = await YaverFeedback_1.YaverFeedback.getRunnerBrowserAuthStatus(session.id);
1075
+ setSession(s);
1076
+ }
1077
+ catch {
1078
+ // keep polling
1079
+ }
1080
+ }, 1500);
1081
+ return () => clearInterval(iv);
1082
+ }, [session?.id, session?.status]);
1083
+ const terminal = session && ['completed', 'failed', 'cancelled'].includes(session.status);
1084
+ const runnerLabel = runner === 'codex' ? 'OpenAI Codex' : runner === 'claude' ? 'Claude Code' : runner;
1085
+ const handleClose = () => {
1086
+ if (session && !terminal) {
1087
+ YaverFeedback_1.YaverFeedback.cancelRunnerBrowserAuth(session.id).catch(() => { });
1088
+ }
1089
+ onClose();
1090
+ };
1091
+ const copyCode = () => {
1092
+ if (!session?.code)
1093
+ return;
1094
+ try {
1095
+ // Avoid a hard Clipboard dep — host app can polyfill.
1096
+ const Clipboard = require('react-native').Clipboard;
1097
+ if (Clipboard?.setString) {
1098
+ Clipboard.setString(session.code);
1099
+ setCopied(true);
1100
+ setTimeout(() => setCopied(false), 1500);
1101
+ }
1102
+ }
1103
+ catch {
1104
+ // best-effort — code is visible on screen regardless
1105
+ }
1106
+ };
1107
+ const openUrl = () => {
1108
+ if (!session?.openUrl)
1109
+ return;
1110
+ try {
1111
+ const { Linking } = require('react-native');
1112
+ Linking.openURL(session.openUrl).catch(() => { });
1113
+ }
1114
+ catch {
1115
+ /* ignore */
1116
+ }
1117
+ };
1118
+ return (<react_native_1.Modal visible={true} transparent animationType="fade" onRequestClose={handleClose}>
1119
+ <react_native_1.View style={runnerAuthModalStyles.overlay}>
1120
+ <react_native_1.View style={runnerAuthModalStyles.card}>
1121
+ <react_native_1.View style={runnerAuthModalStyles.header}>
1122
+ <react_native_1.View style={{ flex: 1 }}>
1123
+ <react_native_1.Text style={runnerAuthModalStyles.title}>Sign in to {runnerLabel}</react_native_1.Text>
1124
+ <react_native_1.Text style={runnerAuthModalStyles.subtitle}>
1125
+ Opens a one-time URL + code. Enter it in any browser.
1126
+ </react_native_1.Text>
1127
+ </react_native_1.View>
1128
+ <react_native_1.Pressable onPress={handleClose} hitSlop={10}>
1129
+ <react_native_1.Text style={runnerAuthModalStyles.close}>×</react_native_1.Text>
1130
+ </react_native_1.Pressable>
1131
+ </react_native_1.View>
1132
+
1133
+ {startError ? (<react_native_1.View style={runnerAuthModalStyles.errorBox}>
1134
+ <react_native_1.Text style={runnerAuthModalStyles.errorTitle}>Couldn't start</react_native_1.Text>
1135
+ <react_native_1.Text style={runnerAuthModalStyles.errorBody}>{startError}</react_native_1.Text>
1136
+ </react_native_1.View>) : !session ? (<react_native_1.Text style={runnerAuthModalStyles.dim}>
1137
+ Starting the sign-in flow on the remote machine…
1138
+ </react_native_1.Text>) : session.status === 'completed' ? (<react_native_1.View style={runnerAuthModalStyles.successBox}>
1139
+ <react_native_1.Text style={runnerAuthModalStyles.successTitle}>✓ Signed in</react_native_1.Text>
1140
+ <react_native_1.Text style={runnerAuthModalStyles.successBody}>
1141
+ {session.detail || 'Auth stored on the remote machine.'}
1142
+ </react_native_1.Text>
1143
+ </react_native_1.View>) : session.status === 'failed' || session.status === 'cancelled' ? (<react_native_1.View style={runnerAuthModalStyles.errorBox}>
1144
+ <react_native_1.Text style={runnerAuthModalStyles.errorTitle}>
1145
+ {session.status === 'cancelled' ? 'Cancelled' : 'Failed'}
1146
+ </react_native_1.Text>
1147
+ <react_native_1.Text style={runnerAuthModalStyles.errorBody}>
1148
+ {session.error || session.detail || 'The CLI exited before sign-in completed.'}
1149
+ </react_native_1.Text>
1150
+ </react_native_1.View>) : (<react_native_1.View>
1151
+ {session.openUrl ? (<react_native_1.Pressable onPress={openUrl} style={runnerAuthModalStyles.urlBox}>
1152
+ <react_native_1.Text style={runnerAuthModalStyles.urlText} numberOfLines={2}>
1153
+ ↗ {session.openUrl}
1154
+ </react_native_1.Text>
1155
+ </react_native_1.Pressable>) : (<react_native_1.Text style={runnerAuthModalStyles.dim}>
1156
+ Waiting for verification URL from the remote CLI…
1157
+ </react_native_1.Text>)}
1158
+ {session.code ? (<react_native_1.View style={{ marginTop: 12 }}>
1159
+ <react_native_1.Text style={runnerAuthModalStyles.codeLabel}>ENTER THIS CODE</react_native_1.Text>
1160
+ <react_native_1.Pressable onPress={copyCode} style={runnerAuthModalStyles.codeBox}>
1161
+ <react_native_1.Text style={runnerAuthModalStyles.codeText}>{session.code}</react_native_1.Text>
1162
+ <react_native_1.Text style={runnerAuthModalStyles.codeHint}>
1163
+ {copied ? 'copied' : 'tap to copy'}
1164
+ </react_native_1.Text>
1165
+ </react_native_1.Pressable>
1166
+ </react_native_1.View>) : null}
1167
+ <react_native_1.Text style={runnerAuthModalStyles.phishingHint}>
1168
+ Device codes are a common phishing target. Never share this code. This dialog
1169
+ turns green automatically once sign-in completes.
1170
+ </react_native_1.Text>
1171
+ </react_native_1.View>)}
1172
+ </react_native_1.View>
1173
+ </react_native_1.View>
1174
+ </react_native_1.Modal>);
1175
+ };
1176
+ const runnerAuthRowStyles = react_native_1.StyleSheet.create({
1177
+ container: {
1178
+ flexDirection: 'row',
1179
+ gap: 8,
1180
+ marginTop: 8,
1181
+ flexWrap: 'wrap',
1182
+ },
1183
+ button: {
1184
+ flexGrow: 1,
1185
+ flexBasis: 0,
1186
+ minWidth: 120,
1187
+ paddingHorizontal: 12,
1188
+ paddingVertical: 10,
1189
+ borderRadius: 10,
1190
+ borderWidth: 1,
1191
+ borderColor: 'rgba(148,163,184,0.22)',
1192
+ backgroundColor: 'rgba(15,23,42,0.6)',
1193
+ },
1194
+ buttonPressed: { opacity: 0.7 },
1195
+ buttonDisabled: { opacity: 0.4 },
1196
+ buttonLabel: {
1197
+ fontSize: 10,
1198
+ color: '#94a3b8',
1199
+ textTransform: 'uppercase',
1200
+ letterSpacing: 0.8,
1201
+ },
1202
+ buttonName: {
1203
+ marginTop: 2,
1204
+ fontSize: 14,
1205
+ fontWeight: '600',
1206
+ color: '#f1f5f9',
1207
+ },
1208
+ });
1209
+ const runnerAuthModalStyles = react_native_1.StyleSheet.create({
1210
+ overlay: {
1211
+ flex: 1,
1212
+ justifyContent: 'center',
1213
+ alignItems: 'center',
1214
+ backgroundColor: 'rgba(2,6,23,0.75)',
1215
+ padding: 16,
1216
+ },
1217
+ card: {
1218
+ width: '100%',
1219
+ maxWidth: 420,
1220
+ backgroundColor: '#0f172a',
1221
+ borderRadius: 14,
1222
+ borderWidth: 1,
1223
+ borderColor: 'rgba(148,163,184,0.18)',
1224
+ padding: 18,
1225
+ },
1226
+ header: {
1227
+ flexDirection: 'row',
1228
+ alignItems: 'flex-start',
1229
+ marginBottom: 12,
1230
+ },
1231
+ title: { color: '#f1f5f9', fontSize: 16, fontWeight: '600' },
1232
+ subtitle: { color: '#94a3b8', fontSize: 11, marginTop: 2 },
1233
+ close: { color: '#94a3b8', fontSize: 22, lineHeight: 22, paddingHorizontal: 4 },
1234
+ dim: {
1235
+ color: '#94a3b8',
1236
+ fontSize: 12,
1237
+ padding: 12,
1238
+ borderRadius: 10,
1239
+ borderWidth: 1,
1240
+ borderColor: 'rgba(148,163,184,0.2)',
1241
+ backgroundColor: 'rgba(15,23,42,0.6)',
1242
+ },
1243
+ errorBox: {
1244
+ padding: 12,
1245
+ borderRadius: 10,
1246
+ borderWidth: 1,
1247
+ borderColor: 'rgba(248,113,113,0.35)',
1248
+ backgroundColor: 'rgba(248,113,113,0.1)',
1249
+ },
1250
+ errorTitle: { color: '#fca5a5', fontWeight: '600', marginBottom: 4, fontSize: 13 },
1251
+ errorBody: { color: '#fca5a5', fontSize: 12 },
1252
+ successBox: {
1253
+ padding: 14,
1254
+ borderRadius: 10,
1255
+ borderWidth: 1,
1256
+ borderColor: 'rgba(34,197,94,0.35)',
1257
+ backgroundColor: 'rgba(34,197,94,0.1)',
1258
+ },
1259
+ successTitle: { color: '#4ade80', fontSize: 14, fontWeight: '600', marginBottom: 4 },
1260
+ successBody: { color: '#86efac', fontSize: 12 },
1261
+ urlBox: {
1262
+ padding: 12,
1263
+ borderRadius: 10,
1264
+ borderWidth: 1,
1265
+ borderColor: 'rgba(99,102,241,0.35)',
1266
+ backgroundColor: 'rgba(99,102,241,0.1)',
1267
+ },
1268
+ urlText: { color: '#c7d2fe', fontSize: 13 },
1269
+ codeLabel: {
1270
+ fontSize: 10,
1271
+ fontWeight: '600',
1272
+ color: '#94a3b8',
1273
+ letterSpacing: 0.8,
1274
+ marginBottom: 4,
1275
+ },
1276
+ codeBox: {
1277
+ padding: 14,
1278
+ borderRadius: 10,
1279
+ borderWidth: 1,
1280
+ borderColor: 'rgba(148,163,184,0.22)',
1281
+ backgroundColor: 'rgba(15,23,42,0.8)',
1282
+ alignItems: 'center',
1283
+ },
1284
+ codeText: {
1285
+ color: '#f1f5f9',
1286
+ fontSize: 22,
1287
+ letterSpacing: 6,
1288
+ fontFamily: 'Menlo',
1289
+ },
1290
+ codeHint: { color: '#64748b', fontSize: 10, marginTop: 4, textTransform: 'uppercase' },
1291
+ phishingHint: {
1292
+ color: '#475569',
1293
+ fontSize: 10,
1294
+ marginTop: 12,
1295
+ lineHeight: 14,
1296
+ },
1297
+ });
@@ -1,4 +1,4 @@
1
- import { FeedbackBundle, TestSession, VoiceCapability } from './types';
1
+ import { CapabilitySnapshot, FeedbackBundle, IncidentEvent, OperationState, RunnerBrowserAuthSession, TestSession, VoiceCapability } from './types';
2
2
  export interface FeedbackEvent {
3
3
  type: string;
4
4
  timestamp: string;
@@ -21,11 +21,49 @@ export interface ReloadAck {
21
21
  export declare class P2PClient {
22
22
  private baseUrl;
23
23
  private authToken;
24
- constructor(baseUrl: string, authToken: string);
24
+ /**
25
+ * Shared relay password. Required when baseUrl points through the
26
+ * Yaver managed relay (e.g. https://public.yaver.io/d/<deviceId>) —
27
+ * the relay rejects unauthenticated requests with 401. Attached as
28
+ * X-Relay-Password on every agent request.
29
+ */
30
+ private relayPassword;
31
+ constructor(baseUrl: string, authToken: string, relayPassword?: string);
25
32
  /** Update the base URL (e.g. after re-discovery). */
26
33
  setBaseUrl(url: string): void;
27
34
  /** Update the auth token. */
28
35
  setAuthToken(token: string): void;
36
+ /** Update the relay password (used for managed-relay baseUrls). */
37
+ setRelayPassword(password: string): void;
38
+ /** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
39
+ private authHeaders;
40
+ /**
41
+ * Start a remote browser-style sign-in for a runner (codex --device-auth
42
+ * / claude auth login --console). Returns a session id; callers poll
43
+ * getRunnerBrowserAuthStatus to surface the verification URL + one-time
44
+ * code. No API keys involved — the CLI writes its own auth.json once
45
+ * the user completes the flow in any browser.
46
+ */
47
+ startRunnerBrowserAuth(runner: string): Promise<RunnerBrowserAuthSession>;
48
+ getRunnerBrowserAuthStatus(sessionId: string): Promise<RunnerBrowserAuthSession>;
49
+ cancelRunnerBrowserAuth(sessionId: string): Promise<void>;
50
+ capabilitySnapshot(): Promise<CapabilitySnapshot | null>;
51
+ incidents(opts?: {
52
+ category?: string;
53
+ severity?: string;
54
+ code?: string;
55
+ deviceId?: string;
56
+ projectPath?: string;
57
+ includeResolved?: boolean;
58
+ limit?: number;
59
+ }): Promise<IncidentEvent[]>;
60
+ operations(opts?: {
61
+ kind?: string;
62
+ status?: string;
63
+ deviceId?: string;
64
+ projectPath?: string;
65
+ limit?: number;
66
+ }): Promise<OperationState[]>;
29
67
  /** Health check — returns true if the agent is reachable. */
30
68
  health(): Promise<boolean>;
31
69
  /** Get agent info (hostname, version, platform). */
package/dist/P2PClient.js CHANGED
@@ -92,9 +92,10 @@ function friendlyReloadError(status, body) {
92
92
  * support for streaming feedback, listing builds, and triggering builds.
93
93
  */
94
94
  class P2PClient {
95
- constructor(baseUrl, authToken) {
95
+ constructor(baseUrl, authToken, relayPassword = '') {
96
96
  this.baseUrl = baseUrl.replace(/\/$/, '');
97
97
  this.authToken = authToken;
98
+ this.relayPassword = relayPassword;
98
99
  }
99
100
  /** Update the base URL (e.g. after re-discovery). */
100
101
  setBaseUrl(url) {
@@ -104,6 +105,118 @@ class P2PClient {
104
105
  setAuthToken(token) {
105
106
  this.authToken = token;
106
107
  }
108
+ /** Update the relay password (used for managed-relay baseUrls). */
109
+ setRelayPassword(password) {
110
+ this.relayPassword = password;
111
+ }
112
+ /** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
113
+ authHeaders(extra = {}) {
114
+ const h = { ...extra };
115
+ if (this.authToken)
116
+ h.Authorization = `Bearer ${this.authToken}`;
117
+ if (this.relayPassword)
118
+ h['X-Relay-Password'] = this.relayPassword;
119
+ return h;
120
+ }
121
+ /**
122
+ * Start a remote browser-style sign-in for a runner (codex --device-auth
123
+ * / claude auth login --console). Returns a session id; callers poll
124
+ * getRunnerBrowserAuthStatus to surface the verification URL + one-time
125
+ * code. No API keys involved — the CLI writes its own auth.json once
126
+ * the user completes the flow in any browser.
127
+ */
128
+ async startRunnerBrowserAuth(runner) {
129
+ const resp = await fetch(`${this.baseUrl}/runner-auth/browser/start`, {
130
+ method: 'POST',
131
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
132
+ body: JSON.stringify({ runner }),
133
+ });
134
+ if (!resp.ok) {
135
+ const text = await resp.text().catch(() => '');
136
+ throw new Error(`startRunnerBrowserAuth(${runner}) HTTP ${resp.status}: ${text}`);
137
+ }
138
+ const data = await resp.json();
139
+ return data.session;
140
+ }
141
+ async getRunnerBrowserAuthStatus(sessionId) {
142
+ const url = `${this.baseUrl}/runner-auth/browser/status?id=${encodeURIComponent(sessionId)}`;
143
+ const resp = await fetch(url, { headers: this.authHeaders() });
144
+ if (!resp.ok) {
145
+ const text = await resp.text().catch(() => '');
146
+ throw new Error(`getRunnerBrowserAuthStatus HTTP ${resp.status}: ${text}`);
147
+ }
148
+ const data = await resp.json();
149
+ return data.session;
150
+ }
151
+ async cancelRunnerBrowserAuth(sessionId) {
152
+ const url = `${this.baseUrl}/runner-auth/browser/cancel?id=${encodeURIComponent(sessionId)}`;
153
+ try {
154
+ await fetch(url, { method: 'POST', headers: this.authHeaders() });
155
+ }
156
+ catch { /* best-effort */ }
157
+ }
158
+ async capabilitySnapshot() {
159
+ try {
160
+ const resp = await fetch(`${this.baseUrl}/capabilities/snapshot`, { headers: this.authHeaders() });
161
+ if (!resp.ok)
162
+ return null;
163
+ const data = await resp.json().catch(() => ({}));
164
+ return (data.snapshot ?? null);
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ async incidents(opts = {}) {
171
+ try {
172
+ const url = new URL(`${this.baseUrl}/incidents`);
173
+ if (opts.category)
174
+ url.searchParams.set('category', opts.category);
175
+ if (opts.severity)
176
+ url.searchParams.set('severity', opts.severity);
177
+ if (opts.code)
178
+ url.searchParams.set('code', opts.code);
179
+ if (opts.deviceId)
180
+ url.searchParams.set('device', opts.deviceId);
181
+ if (opts.projectPath)
182
+ url.searchParams.set('projectPath', opts.projectPath);
183
+ if (opts.includeResolved)
184
+ url.searchParams.set('includeResolved', '1');
185
+ if (typeof opts.limit === 'number')
186
+ url.searchParams.set('limit', String(opts.limit));
187
+ const resp = await fetch(url.toString(), { headers: this.authHeaders() });
188
+ if (!resp.ok)
189
+ return [];
190
+ const data = await resp.json().catch(() => ({}));
191
+ return Array.isArray(data.incidents) ? data.incidents : [];
192
+ }
193
+ catch {
194
+ return [];
195
+ }
196
+ }
197
+ async operations(opts = {}) {
198
+ try {
199
+ const url = new URL(`${this.baseUrl}/operations`);
200
+ if (opts.kind)
201
+ url.searchParams.set('kind', opts.kind);
202
+ if (opts.status)
203
+ url.searchParams.set('status', opts.status);
204
+ if (opts.deviceId)
205
+ url.searchParams.set('device', opts.deviceId);
206
+ if (opts.projectPath)
207
+ url.searchParams.set('projectPath', opts.projectPath);
208
+ if (typeof opts.limit === 'number')
209
+ url.searchParams.set('limit', String(opts.limit));
210
+ const resp = await fetch(url.toString(), { headers: this.authHeaders() });
211
+ if (!resp.ok)
212
+ return [];
213
+ const data = await resp.json().catch(() => ({}));
214
+ return Array.isArray(data.operations) ? data.operations : [];
215
+ }
216
+ catch {
217
+ return [];
218
+ }
219
+ }
107
220
  /** Health check — returns true if the agent is reachable. */
108
221
  async health() {
109
222
  try {
@@ -563,9 +676,7 @@ class P2PClient {
563
676
  async request(method, path) {
564
677
  const response = await fetch(`${this.baseUrl}${path}`, {
565
678
  method,
566
- headers: {
567
- Authorization: `Bearer ${this.authToken}`,
568
- },
679
+ headers: this.authHeaders(),
569
680
  });
570
681
  if (!response.ok) {
571
682
  const text = await response.text().catch(() => '');