yaver-feedback-react-native 0.8.9 → 0.8.11
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/dist/Discovery.js +9 -3
- package/dist/FeedbackModal.js +69 -2
- package/dist/P2PClient.d.ts +6 -0
- package/dist/P2PClient.js +39 -42
- package/dist/YaverFeedback.d.ts +4 -0
- package/dist/YaverFeedback.js +62 -3
- package/package.json +1 -1
- package/src/Discovery.ts +9 -3
- package/src/FeedbackModal.tsx +93 -2
- package/src/P2PClient.ts +43 -42
- package/src/YaverFeedback.ts +66 -3
package/dist/Discovery.js
CHANGED
|
@@ -257,15 +257,21 @@ class YaverDiscovery {
|
|
|
257
257
|
*/
|
|
258
258
|
static async discoverViaRelay(convexUrl, authToken, deviceId) {
|
|
259
259
|
try {
|
|
260
|
-
|
|
260
|
+
// /settings returns {ok, settings: {relayUrl, relayPassword, ...}}
|
|
261
|
+
// — `/auth/validate` only returns the user record. Using the
|
|
262
|
+
// wrong endpoint historically meant relayPassword stayed
|
|
263
|
+
// undefined and every relay-routed probe rejected with 401
|
|
264
|
+
// "invalid relay password" (relay/server.go:957).
|
|
265
|
+
const settingsRes = await fetch(`${convexUrl}/settings`, {
|
|
261
266
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
262
267
|
});
|
|
263
268
|
let relayUrl;
|
|
264
269
|
let relayPassword;
|
|
265
270
|
if (settingsRes.ok) {
|
|
266
271
|
const settingsData = await settingsRes.json();
|
|
267
|
-
|
|
268
|
-
|
|
272
|
+
const inner = settingsData?.settings;
|
|
273
|
+
relayUrl = inner?.relayUrl ?? settingsData?.relayUrl;
|
|
274
|
+
relayPassword = inner?.relayPassword ?? settingsData?.relayPassword;
|
|
269
275
|
}
|
|
270
276
|
if (!relayUrl) {
|
|
271
277
|
const configRes = await fetch(`${convexUrl}/platform-config?key=relay_servers`);
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -1049,7 +1049,15 @@ const RunnerAuthNativeModal = ({ runner, onClose }) => {
|
|
|
1049
1049
|
const [session, setSession] = (0, react_1.useState)(null);
|
|
1050
1050
|
const [startError, setStartError] = (0, react_1.useState)(null);
|
|
1051
1051
|
const [copied, setCopied] = (0, react_1.useState)(false);
|
|
1052
|
+
const [pasteCode, setPasteCode] = (0, react_1.useState)('');
|
|
1053
|
+
const [submitting, setSubmitting] = (0, react_1.useState)(false);
|
|
1054
|
+
const [submitError, setSubmitError] = (0, react_1.useState)(null);
|
|
1052
1055
|
const startedRef = (0, react_1.useRef)(false);
|
|
1056
|
+
// Claude is the only runner that needs the user to paste a verifier
|
|
1057
|
+
// code back from platform.claude.com's callback page; Codex device-
|
|
1058
|
+
// auth and OpenCode (no OAuth at all) bypass this. Mirrors the
|
|
1059
|
+
// requiresPasteBack check in iOS YaverRunnerAuthFlowPane.swift.
|
|
1060
|
+
const needsPasteBack = runner === 'claude' || runner === 'claude-code';
|
|
1053
1061
|
(0, react_1.useEffect)(() => {
|
|
1054
1062
|
if (startedRef.current)
|
|
1055
1063
|
return;
|
|
@@ -1164,9 +1172,68 @@ const RunnerAuthNativeModal = ({ runner, onClose }) => {
|
|
|
1164
1172
|
</react_native_1.Text>
|
|
1165
1173
|
</react_native_1.Pressable>
|
|
1166
1174
|
</react_native_1.View>) : null}
|
|
1175
|
+
{needsPasteBack ? (<react_native_1.View style={{ marginTop: 14 }}>
|
|
1176
|
+
<react_native_1.Text style={runnerAuthModalStyles.codeLabel}>
|
|
1177
|
+
PASTE CODE FROM CLAUDE.COM
|
|
1178
|
+
</react_native_1.Text>
|
|
1179
|
+
<react_native_1.View style={{ flexDirection: 'row', gap: 8, marginTop: 6 }}>
|
|
1180
|
+
<react_native_1.View style={{
|
|
1181
|
+
flex: 1,
|
|
1182
|
+
backgroundColor: 'rgba(148,163,184,0.10)',
|
|
1183
|
+
borderRadius: 10,
|
|
1184
|
+
paddingHorizontal: 10,
|
|
1185
|
+
}}>
|
|
1186
|
+
{/* Lazy-import TextInput so the SDK doesn't pull
|
|
1187
|
+
extra surface from react-native at module load. */}
|
|
1188
|
+
{(() => {
|
|
1189
|
+
const { TextInput } = require('react-native');
|
|
1190
|
+
return (<TextInput value={pasteCode} onChangeText={(t) => {
|
|
1191
|
+
setPasteCode(t);
|
|
1192
|
+
setSubmitError(null);
|
|
1193
|
+
}} placeholder="paste code here" placeholderTextColor="#64748b" autoCapitalize="none" autoCorrect={false} spellCheck={false} style={{ color: '#f1f5f9', fontSize: 14, paddingVertical: 10 }}/>);
|
|
1194
|
+
})()}
|
|
1195
|
+
</react_native_1.View>
|
|
1196
|
+
<react_native_1.Pressable disabled={!pasteCode.trim() || submitting} onPress={async () => {
|
|
1197
|
+
if (!session || !pasteCode.trim())
|
|
1198
|
+
return;
|
|
1199
|
+
setSubmitting(true);
|
|
1200
|
+
setSubmitError(null);
|
|
1201
|
+
try {
|
|
1202
|
+
const next = await YaverFeedback_1.YaverFeedback.submitRunnerBrowserAuthCode(session.id, pasteCode.trim());
|
|
1203
|
+
setSession(next);
|
|
1204
|
+
setPasteCode('');
|
|
1205
|
+
}
|
|
1206
|
+
catch (err) {
|
|
1207
|
+
setSubmitError(err instanceof Error ? err.message : String(err));
|
|
1208
|
+
}
|
|
1209
|
+
finally {
|
|
1210
|
+
setSubmitting(false);
|
|
1211
|
+
}
|
|
1212
|
+
}} style={{
|
|
1213
|
+
paddingHorizontal: 14,
|
|
1214
|
+
justifyContent: 'center',
|
|
1215
|
+
backgroundColor: !pasteCode.trim() || submitting
|
|
1216
|
+
? 'rgba(124,58,237,0.4)'
|
|
1217
|
+
: '#7c3aed',
|
|
1218
|
+
borderRadius: 10,
|
|
1219
|
+
}}>
|
|
1220
|
+
<react_native_1.Text style={{ color: 'white', fontWeight: '600' }}>
|
|
1221
|
+
{submitting ? '…' : 'Submit'}
|
|
1222
|
+
</react_native_1.Text>
|
|
1223
|
+
</react_native_1.Pressable>
|
|
1224
|
+
</react_native_1.View>
|
|
1225
|
+
{submitError ? (<react_native_1.Text style={{
|
|
1226
|
+
marginTop: 6,
|
|
1227
|
+
color: '#fca5a5',
|
|
1228
|
+
fontSize: 12,
|
|
1229
|
+
}}>
|
|
1230
|
+
{submitError}
|
|
1231
|
+
</react_native_1.Text>) : null}
|
|
1232
|
+
</react_native_1.View>) : null}
|
|
1167
1233
|
<react_native_1.Text style={runnerAuthModalStyles.phishingHint}>
|
|
1168
|
-
|
|
1169
|
-
|
|
1234
|
+
{needsPasteBack
|
|
1235
|
+
? 'After authorising on platform.claude.com, copy the code from the callback page and paste it above. Never share this code.'
|
|
1236
|
+
: 'Device codes are a common phishing target. Never share this code. This dialog turns green automatically once sign-in completes.'}
|
|
1170
1237
|
</react_native_1.Text>
|
|
1171
1238
|
</react_native_1.View>)}
|
|
1172
1239
|
</react_native_1.View>
|
package/dist/P2PClient.d.ts
CHANGED
|
@@ -47,6 +47,12 @@ export declare class P2PClient {
|
|
|
47
47
|
startRunnerBrowserAuth(runner: string): Promise<RunnerBrowserAuthSession>;
|
|
48
48
|
getRunnerBrowserAuthStatus(sessionId: string): Promise<RunnerBrowserAuthSession>;
|
|
49
49
|
cancelRunnerBrowserAuth(sessionId: string): Promise<void>;
|
|
50
|
+
/** Submit the verifier code Anthropic shows on the callback page so
|
|
51
|
+
* the agent can finalise claude CLI's OAuth handshake. Codex doesn't
|
|
52
|
+
* use this — its device-auth flow auto-resolves via polling — but
|
|
53
|
+
* the SDK still exposes it for symmetry with mobile/src/components/
|
|
54
|
+
* RunnerAuthModal.tsx and the Swift YaverRunnerAuthFlowPane. */
|
|
55
|
+
submitRunnerBrowserAuthCode(sessionId: string, code: string): Promise<RunnerBrowserAuthSession>;
|
|
50
56
|
capabilitySnapshot(): Promise<CapabilitySnapshot | null>;
|
|
51
57
|
incidents(opts?: {
|
|
52
58
|
category?: string;
|
package/dist/P2PClient.js
CHANGED
|
@@ -155,6 +155,25 @@ class P2PClient {
|
|
|
155
155
|
}
|
|
156
156
|
catch { /* best-effort */ }
|
|
157
157
|
}
|
|
158
|
+
/** Submit the verifier code Anthropic shows on the callback page so
|
|
159
|
+
* the agent can finalise claude CLI's OAuth handshake. Codex doesn't
|
|
160
|
+
* use this — its device-auth flow auto-resolves via polling — but
|
|
161
|
+
* the SDK still exposes it for symmetry with mobile/src/components/
|
|
162
|
+
* RunnerAuthModal.tsx and the Swift YaverRunnerAuthFlowPane. */
|
|
163
|
+
async submitRunnerBrowserAuthCode(sessionId, code) {
|
|
164
|
+
const url = `${this.baseUrl}/runner-auth/browser/submit-code`;
|
|
165
|
+
const resp = await fetch(url, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: { ...this.authHeaders(), 'Content-Type': 'application/json' },
|
|
168
|
+
body: JSON.stringify({ id: sessionId, code }),
|
|
169
|
+
});
|
|
170
|
+
if (!resp.ok) {
|
|
171
|
+
const text = await resp.text().catch(() => '');
|
|
172
|
+
throw new Error(`submitRunnerBrowserAuthCode HTTP ${resp.status}: ${text}`);
|
|
173
|
+
}
|
|
174
|
+
const data = await resp.json();
|
|
175
|
+
return data.session;
|
|
176
|
+
}
|
|
158
177
|
async capabilitySnapshot() {
|
|
159
178
|
try {
|
|
160
179
|
const resp = await fetch(`${this.baseUrl}/capabilities/snapshot`, { headers: this.authHeaders() });
|
|
@@ -272,11 +291,12 @@ class P2PClient {
|
|
|
272
291
|
name: 'voice_note.m4a',
|
|
273
292
|
});
|
|
274
293
|
}
|
|
294
|
+
// Use authHeaders() so a relay-routed baseUrl carries
|
|
295
|
+
// X-Relay-Password — without it the relay rejects with 401
|
|
296
|
+
// "invalid relay password" before the agent ever sees the form.
|
|
275
297
|
const response = await fetch(`${this.baseUrl}/feedback`, {
|
|
276
298
|
method: 'POST',
|
|
277
|
-
headers:
|
|
278
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
279
|
-
},
|
|
299
|
+
headers: this.authHeaders(),
|
|
280
300
|
body: formData,
|
|
281
301
|
});
|
|
282
302
|
if (!response.ok) {
|
|
@@ -294,10 +314,7 @@ class P2PClient {
|
|
|
294
314
|
for await (const event of events) {
|
|
295
315
|
const response = await fetch(`${this.baseUrl}/feedback/stream`, {
|
|
296
316
|
method: 'POST',
|
|
297
|
-
headers: {
|
|
298
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
299
|
-
'Content-Type': 'application/json',
|
|
300
|
-
},
|
|
317
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
301
318
|
body: JSON.stringify(event),
|
|
302
319
|
});
|
|
303
320
|
if (!response.ok) {
|
|
@@ -316,10 +333,7 @@ class P2PClient {
|
|
|
316
333
|
async startBuild(platform) {
|
|
317
334
|
const response = await fetch(`${this.baseUrl}/builds`, {
|
|
318
335
|
method: 'POST',
|
|
319
|
-
headers: {
|
|
320
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
321
|
-
'Content-Type': 'application/json',
|
|
322
|
-
},
|
|
336
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
323
337
|
body: JSON.stringify({ platform }),
|
|
324
338
|
});
|
|
325
339
|
if (!response.ok) {
|
|
@@ -359,9 +373,7 @@ class P2PClient {
|
|
|
359
373
|
});
|
|
360
374
|
const response = await fetch(`${this.baseUrl}/voice/transcribe`, {
|
|
361
375
|
method: 'POST',
|
|
362
|
-
headers:
|
|
363
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
364
|
-
},
|
|
376
|
+
headers: this.authHeaders(),
|
|
365
377
|
body: formData,
|
|
366
378
|
});
|
|
367
379
|
if (!response.ok) {
|
|
@@ -401,7 +413,7 @@ class P2PClient {
|
|
|
401
413
|
if (mode === 'dev') {
|
|
402
414
|
const primary = await fetch(`${this.baseUrl}/dev/reload`, {
|
|
403
415
|
method: 'POST',
|
|
404
|
-
headers:
|
|
416
|
+
headers: this.authHeaders(),
|
|
405
417
|
});
|
|
406
418
|
if (primary.ok) {
|
|
407
419
|
const payload = await primary.json().catch(() => ({}));
|
|
@@ -432,10 +444,7 @@ class P2PClient {
|
|
|
432
444
|
const identity = resolveAppIdentity(opts);
|
|
433
445
|
const res = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
434
446
|
method: 'POST',
|
|
435
|
-
headers: {
|
|
436
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
437
|
-
'Content-Type': 'application/json',
|
|
438
|
-
},
|
|
447
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
439
448
|
body: JSON.stringify({
|
|
440
449
|
mode: 'bundle',
|
|
441
450
|
...identity,
|
|
@@ -479,10 +488,7 @@ class P2PClient {
|
|
|
479
488
|
const identity = resolveAppIdentity(opts);
|
|
480
489
|
const response = await fetch(`${this.baseUrl}/vibing/execute`, {
|
|
481
490
|
method: 'POST',
|
|
482
|
-
headers: {
|
|
483
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
484
|
-
'Content-Type': 'application/json',
|
|
485
|
-
},
|
|
491
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
486
492
|
body: JSON.stringify({
|
|
487
493
|
prompt,
|
|
488
494
|
projectPath: identity.projectPath ?? opts?.projectPath ?? '',
|
|
@@ -510,7 +516,7 @@ class P2PClient {
|
|
|
510
516
|
}
|
|
511
517
|
const response = await fetch(`${this.baseUrl}/vibing/eligibility?${params.toString()}`, {
|
|
512
518
|
method: 'GET',
|
|
513
|
-
headers:
|
|
519
|
+
headers: this.authHeaders(),
|
|
514
520
|
});
|
|
515
521
|
if (!response.ok) {
|
|
516
522
|
const text = await response.text().catch(() => '');
|
|
@@ -527,7 +533,7 @@ class P2PClient {
|
|
|
527
533
|
async triggerFix(feedbackId) {
|
|
528
534
|
const response = await fetch(`${this.baseUrl}/feedback/${encodeURIComponent(feedbackId)}/fix`, {
|
|
529
535
|
method: 'POST',
|
|
530
|
-
headers:
|
|
536
|
+
headers: this.authHeaders(),
|
|
531
537
|
});
|
|
532
538
|
if (!response.ok) {
|
|
533
539
|
const text = await response.text().catch(() => '');
|
|
@@ -548,10 +554,7 @@ class P2PClient {
|
|
|
548
554
|
async startTestSession() {
|
|
549
555
|
const response = await fetch(`${this.baseUrl}/test-app/start`, {
|
|
550
556
|
method: 'POST',
|
|
551
|
-
headers: {
|
|
552
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
553
|
-
'Content-Type': 'application/json',
|
|
554
|
-
},
|
|
557
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
555
558
|
body: JSON.stringify({ source: 'feedback-sdk' }),
|
|
556
559
|
});
|
|
557
560
|
if (!response.ok) {
|
|
@@ -564,7 +567,7 @@ class P2PClient {
|
|
|
564
567
|
async stopTestSession() {
|
|
565
568
|
await fetch(`${this.baseUrl}/test-app/stop`, {
|
|
566
569
|
method: 'POST',
|
|
567
|
-
headers:
|
|
570
|
+
headers: this.authHeaders(),
|
|
568
571
|
});
|
|
569
572
|
}
|
|
570
573
|
/** Get the current test session status and list of fixes. */
|
|
@@ -580,10 +583,7 @@ class P2PClient {
|
|
|
580
583
|
async rotateToken() {
|
|
581
584
|
const response = await fetch(`${this.baseUrl}/sdk/token/rotate`, {
|
|
582
585
|
method: 'POST',
|
|
583
|
-
headers: {
|
|
584
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
585
|
-
'Content-Type': 'application/json',
|
|
586
|
-
},
|
|
586
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
587
587
|
});
|
|
588
588
|
if (!response.ok) {
|
|
589
589
|
const text = await response.text().catch(() => '');
|
|
@@ -602,7 +602,7 @@ class P2PClient {
|
|
|
602
602
|
* for 30s in getFlagsCached().
|
|
603
603
|
*/
|
|
604
604
|
async flagsEvaluate(userId = 'anonymous') {
|
|
605
|
-
const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}`, { headers:
|
|
605
|
+
const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}`, { headers: this.authHeaders() });
|
|
606
606
|
if (!res.ok)
|
|
607
607
|
return {};
|
|
608
608
|
const data = await res.json();
|
|
@@ -610,7 +610,7 @@ class P2PClient {
|
|
|
610
610
|
}
|
|
611
611
|
/** Evaluate a single flag by key — shortcut when you only need one. */
|
|
612
612
|
async flagsEvaluateOne(key, userId = 'anonymous') {
|
|
613
|
-
const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}&flag=${encodeURIComponent(key)}`, { headers:
|
|
613
|
+
const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}&flag=${encodeURIComponent(key)}`, { headers: this.authHeaders() });
|
|
614
614
|
if (!res.ok)
|
|
615
615
|
return undefined;
|
|
616
616
|
const data = await res.json();
|
|
@@ -628,7 +628,7 @@ class P2PClient {
|
|
|
628
628
|
if (deviceId)
|
|
629
629
|
params.set('device', deviceId);
|
|
630
630
|
const res = await fetch(`${this.baseUrl}/releases/latest?${params.toString()}`, {
|
|
631
|
-
headers:
|
|
631
|
+
headers: this.authHeaders(),
|
|
632
632
|
});
|
|
633
633
|
if (!res.ok)
|
|
634
634
|
return null;
|
|
@@ -638,7 +638,7 @@ class P2PClient {
|
|
|
638
638
|
async releasesDownload(channel, semver) {
|
|
639
639
|
const params = new URLSearchParams({ channel, semver });
|
|
640
640
|
const res = await fetch(`${this.baseUrl}/releases/bundle?${params.toString()}`, {
|
|
641
|
-
headers:
|
|
641
|
+
headers: this.authHeaders(),
|
|
642
642
|
});
|
|
643
643
|
if (!res.ok)
|
|
644
644
|
return null;
|
|
@@ -654,10 +654,7 @@ class P2PClient {
|
|
|
654
654
|
try {
|
|
655
655
|
const res = await fetch(`${this.baseUrl}/analytics/ingest`, {
|
|
656
656
|
method: 'POST',
|
|
657
|
-
headers: {
|
|
658
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
659
|
-
'Content-Type': 'application/json',
|
|
660
|
-
},
|
|
657
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
661
658
|
body: JSON.stringify({
|
|
662
659
|
name,
|
|
663
660
|
props,
|
package/dist/YaverFeedback.d.ts
CHANGED
|
@@ -78,6 +78,10 @@ export declare class YaverFeedback {
|
|
|
78
78
|
static startRunnerBrowserAuth(runner: string): Promise<import('./types').RunnerBrowserAuthSession>;
|
|
79
79
|
static getRunnerBrowserAuthStatus(sessionId: string): Promise<import('./types').RunnerBrowserAuthSession>;
|
|
80
80
|
static cancelRunnerBrowserAuth(sessionId: string): Promise<void>;
|
|
81
|
+
/** Submit the Claude paste-back verifier so the agent can finalise the
|
|
82
|
+
* OAuth handshake. RunnerAuthModal calls this after the user copies
|
|
83
|
+
* the code from platform.claude.com's callback page. */
|
|
84
|
+
static submitRunnerBrowserAuthCode(sessionId: string, code: string): Promise<import('./types').RunnerBrowserAuthSession>;
|
|
81
85
|
/**
|
|
82
86
|
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
83
87
|
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
package/dist/YaverFeedback.js
CHANGED
|
@@ -135,7 +135,50 @@ let enabled = false;
|
|
|
135
135
|
let p2pClient = null;
|
|
136
136
|
let shakeDetector = null;
|
|
137
137
|
let p2pAuthToken = null;
|
|
138
|
+
let p2pRelayPassword = '';
|
|
138
139
|
let reportLaunchInFlight = false;
|
|
140
|
+
/** Resolve the user's relay password by validating their auth token
|
|
141
|
+
* against Convex. Used whenever we (re)build the P2PClient so a
|
|
142
|
+
* relay-routed agentUrl carries a valid X-Relay-Password — without
|
|
143
|
+
* this, every relay-tunneled request rejects with HTTP 401
|
|
144
|
+
* "invalid relay password" (relay/server.go:957).
|
|
145
|
+
*
|
|
146
|
+
* Cached on `p2pRelayPassword` so we only round-trip Convex when the
|
|
147
|
+
* user's auth token actually changes. Returns "" on any failure so
|
|
148
|
+
* direct LAN agentUrls (which need no password) keep working.
|
|
149
|
+
*/
|
|
150
|
+
async function resolveRelayPassword(authToken, convexUrl) {
|
|
151
|
+
const trimmed = (authToken || '').trim();
|
|
152
|
+
if (!trimmed) {
|
|
153
|
+
p2pRelayPassword = '';
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
const url = (convexUrl || config?.convexUrl || auth_1.DEFAULT_CONVEX_SITE_URL).replace(/\/+$/, '');
|
|
157
|
+
try {
|
|
158
|
+
// /settings returns {ok, settings: {relayPassword, relayUrl, ...}}
|
|
159
|
+
// Older accounts may flatten relayPassword to the top — match the
|
|
160
|
+
// tolerance the web shell already uses (route.ts:77).
|
|
161
|
+
const res = await fetch(`${url}/settings`, {
|
|
162
|
+
headers: { Authorization: `Bearer ${trimmed}` },
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok)
|
|
165
|
+
return p2pRelayPassword;
|
|
166
|
+
const data = await res.json().catch(() => ({}));
|
|
167
|
+
const settings = data?.settings;
|
|
168
|
+
const pw = (typeof settings?.relayPassword === 'string' && settings.relayPassword) ||
|
|
169
|
+
(typeof data?.relayPassword === 'string'
|
|
170
|
+
? data.relayPassword
|
|
171
|
+
: '') ||
|
|
172
|
+
'';
|
|
173
|
+
p2pRelayPassword = pw;
|
|
174
|
+
return pw;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Network failure on a passive Convex round-trip shouldn't break
|
|
178
|
+
// direct-LAN flows. Fall through with whatever we already cached.
|
|
179
|
+
return p2pRelayPassword;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
139
182
|
/** Ring buffer of captured errors. */
|
|
140
183
|
let errorBuffer = [];
|
|
141
184
|
let maxErrors = 5;
|
|
@@ -192,7 +235,8 @@ class YaverFeedback {
|
|
|
192
235
|
return;
|
|
193
236
|
}
|
|
194
237
|
p2pAuthToken = token;
|
|
195
|
-
|
|
238
|
+
const rp = await resolveRelayPassword(token);
|
|
239
|
+
p2pClient = new P2PClient_1.P2PClient(effectiveUrl, token, rp);
|
|
196
240
|
}
|
|
197
241
|
/**
|
|
198
242
|
* Initialize the feedback SDK with the given configuration.
|
|
@@ -271,7 +315,12 @@ class YaverFeedback {
|
|
|
271
315
|
// Create P2P client if we have a URL
|
|
272
316
|
if (config.agentUrl) {
|
|
273
317
|
p2pAuthToken = config.authToken ?? null;
|
|
274
|
-
|
|
318
|
+
// Initial construction uses the cached p2pRelayPassword (empty on
|
|
319
|
+
// first init). rebuildP2PClient below resolves the real password
|
|
320
|
+
// from Convex and replaces this client — but only when authToken
|
|
321
|
+
// is set, so set a placeholder header here that won't 401 a
|
|
322
|
+
// direct-LAN url and will be overwritten before any relay hop.
|
|
323
|
+
p2pClient = new P2PClient_1.P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
|
|
275
324
|
if (config.authToken) {
|
|
276
325
|
void YaverFeedback.rebuildP2PClient(config.agentUrl);
|
|
277
326
|
}
|
|
@@ -571,6 +620,15 @@ class YaverFeedback {
|
|
|
571
620
|
return;
|
|
572
621
|
await p2pClient.cancelRunnerBrowserAuth(sessionId);
|
|
573
622
|
}
|
|
623
|
+
/** Submit the Claude paste-back verifier so the agent can finalise the
|
|
624
|
+
* OAuth handshake. RunnerAuthModal calls this after the user copies
|
|
625
|
+
* the code from platform.claude.com's callback page. */
|
|
626
|
+
static async submitRunnerBrowserAuthCode(sessionId, code) {
|
|
627
|
+
if (!p2pClient) {
|
|
628
|
+
throw new Error('Not connected to any agent.');
|
|
629
|
+
}
|
|
630
|
+
return p2pClient.submitRunnerBrowserAuthCode(sessionId, code);
|
|
631
|
+
}
|
|
574
632
|
/**
|
|
575
633
|
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
576
634
|
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
|
@@ -891,7 +949,8 @@ class YaverFeedback {
|
|
|
891
949
|
});
|
|
892
950
|
if (result) {
|
|
893
951
|
config.agentUrl = result.url;
|
|
894
|
-
|
|
952
|
+
const rp = await resolveRelayPassword(config.authToken ?? '');
|
|
953
|
+
p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '', rp);
|
|
895
954
|
}
|
|
896
955
|
}
|
|
897
956
|
catch { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.11",
|
|
4
4
|
"description": "Visual feedback SDK for Yaver \u2014 bug reports, screen recording, voice annotations, and local-first developer workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/Discovery.ts
CHANGED
|
@@ -283,7 +283,12 @@ export class YaverDiscovery {
|
|
|
283
283
|
deviceId: string,
|
|
284
284
|
): Promise<DiscoveryResult | null> {
|
|
285
285
|
try {
|
|
286
|
-
|
|
286
|
+
// /settings returns {ok, settings: {relayUrl, relayPassword, ...}}
|
|
287
|
+
// — `/auth/validate` only returns the user record. Using the
|
|
288
|
+
// wrong endpoint historically meant relayPassword stayed
|
|
289
|
+
// undefined and every relay-routed probe rejected with 401
|
|
290
|
+
// "invalid relay password" (relay/server.go:957).
|
|
291
|
+
const settingsRes = await fetch(`${convexUrl}/settings`, {
|
|
287
292
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
288
293
|
});
|
|
289
294
|
let relayUrl: string | undefined;
|
|
@@ -291,8 +296,9 @@ export class YaverDiscovery {
|
|
|
291
296
|
|
|
292
297
|
if (settingsRes.ok) {
|
|
293
298
|
const settingsData = await settingsRes.json();
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
const inner = settingsData?.settings;
|
|
300
|
+
relayUrl = inner?.relayUrl ?? settingsData?.relayUrl;
|
|
301
|
+
relayPassword = inner?.relayPassword ?? settingsData?.relayPassword;
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
if (!relayUrl) {
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -1251,7 +1251,15 @@ const RunnerAuthNativeModal: React.FC<{
|
|
|
1251
1251
|
const [session, setSession] = useState<import('./types').RunnerBrowserAuthSession | null>(null);
|
|
1252
1252
|
const [startError, setStartError] = useState<string | null>(null);
|
|
1253
1253
|
const [copied, setCopied] = useState(false);
|
|
1254
|
+
const [pasteCode, setPasteCode] = useState('');
|
|
1255
|
+
const [submitting, setSubmitting] = useState(false);
|
|
1256
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
1254
1257
|
const startedRef = useRef(false);
|
|
1258
|
+
// Claude is the only runner that needs the user to paste a verifier
|
|
1259
|
+
// code back from platform.claude.com's callback page; Codex device-
|
|
1260
|
+
// auth and OpenCode (no OAuth at all) bypass this. Mirrors the
|
|
1261
|
+
// requiresPasteBack check in iOS YaverRunnerAuthFlowPane.swift.
|
|
1262
|
+
const needsPasteBack = runner === 'claude' || runner === 'claude-code';
|
|
1255
1263
|
|
|
1256
1264
|
useEffect(() => {
|
|
1257
1265
|
if (startedRef.current) return;
|
|
@@ -1380,9 +1388,92 @@ const RunnerAuthNativeModal: React.FC<{
|
|
|
1380
1388
|
</Pressable>
|
|
1381
1389
|
</View>
|
|
1382
1390
|
) : null}
|
|
1391
|
+
{needsPasteBack ? (
|
|
1392
|
+
<View style={{ marginTop: 14 }}>
|
|
1393
|
+
<Text style={runnerAuthModalStyles.codeLabel}>
|
|
1394
|
+
PASTE CODE FROM CLAUDE.COM
|
|
1395
|
+
</Text>
|
|
1396
|
+
<View style={{ flexDirection: 'row', gap: 8, marginTop: 6 }}>
|
|
1397
|
+
<View
|
|
1398
|
+
style={{
|
|
1399
|
+
flex: 1,
|
|
1400
|
+
backgroundColor: 'rgba(148,163,184,0.10)',
|
|
1401
|
+
borderRadius: 10,
|
|
1402
|
+
paddingHorizontal: 10,
|
|
1403
|
+
}}
|
|
1404
|
+
>
|
|
1405
|
+
{/* Lazy-import TextInput so the SDK doesn't pull
|
|
1406
|
+
extra surface from react-native at module load. */}
|
|
1407
|
+
{(() => {
|
|
1408
|
+
const { TextInput } = require('react-native');
|
|
1409
|
+
return (
|
|
1410
|
+
<TextInput
|
|
1411
|
+
value={pasteCode}
|
|
1412
|
+
onChangeText={(t: string) => {
|
|
1413
|
+
setPasteCode(t);
|
|
1414
|
+
setSubmitError(null);
|
|
1415
|
+
}}
|
|
1416
|
+
placeholder="paste code here"
|
|
1417
|
+
placeholderTextColor="#64748b"
|
|
1418
|
+
autoCapitalize="none"
|
|
1419
|
+
autoCorrect={false}
|
|
1420
|
+
spellCheck={false}
|
|
1421
|
+
style={{ color: '#f1f5f9', fontSize: 14, paddingVertical: 10 }}
|
|
1422
|
+
/>
|
|
1423
|
+
);
|
|
1424
|
+
})()}
|
|
1425
|
+
</View>
|
|
1426
|
+
<Pressable
|
|
1427
|
+
disabled={!pasteCode.trim() || submitting}
|
|
1428
|
+
onPress={async () => {
|
|
1429
|
+
if (!session || !pasteCode.trim()) return;
|
|
1430
|
+
setSubmitting(true);
|
|
1431
|
+
setSubmitError(null);
|
|
1432
|
+
try {
|
|
1433
|
+
const next = await YaverFeedback.submitRunnerBrowserAuthCode(
|
|
1434
|
+
session.id,
|
|
1435
|
+
pasteCode.trim(),
|
|
1436
|
+
);
|
|
1437
|
+
setSession(next);
|
|
1438
|
+
setPasteCode('');
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
setSubmitError(err instanceof Error ? err.message : String(err));
|
|
1441
|
+
} finally {
|
|
1442
|
+
setSubmitting(false);
|
|
1443
|
+
}
|
|
1444
|
+
}}
|
|
1445
|
+
style={{
|
|
1446
|
+
paddingHorizontal: 14,
|
|
1447
|
+
justifyContent: 'center',
|
|
1448
|
+
backgroundColor:
|
|
1449
|
+
!pasteCode.trim() || submitting
|
|
1450
|
+
? 'rgba(124,58,237,0.4)'
|
|
1451
|
+
: '#7c3aed',
|
|
1452
|
+
borderRadius: 10,
|
|
1453
|
+
}}
|
|
1454
|
+
>
|
|
1455
|
+
<Text style={{ color: 'white', fontWeight: '600' }}>
|
|
1456
|
+
{submitting ? '…' : 'Submit'}
|
|
1457
|
+
</Text>
|
|
1458
|
+
</Pressable>
|
|
1459
|
+
</View>
|
|
1460
|
+
{submitError ? (
|
|
1461
|
+
<Text
|
|
1462
|
+
style={{
|
|
1463
|
+
marginTop: 6,
|
|
1464
|
+
color: '#fca5a5',
|
|
1465
|
+
fontSize: 12,
|
|
1466
|
+
}}
|
|
1467
|
+
>
|
|
1468
|
+
{submitError}
|
|
1469
|
+
</Text>
|
|
1470
|
+
) : null}
|
|
1471
|
+
</View>
|
|
1472
|
+
) : null}
|
|
1383
1473
|
<Text style={runnerAuthModalStyles.phishingHint}>
|
|
1384
|
-
|
|
1385
|
-
|
|
1474
|
+
{needsPasteBack
|
|
1475
|
+
? 'After authorising on platform.claude.com, copy the code from the callback page and paste it above. Never share this code.'
|
|
1476
|
+
: 'Device codes are a common phishing target. Never share this code. This dialog turns green automatically once sign-in completes.'}
|
|
1386
1477
|
</Text>
|
|
1387
1478
|
</View>
|
|
1388
1479
|
)}
|
package/src/P2PClient.ts
CHANGED
|
@@ -199,6 +199,29 @@ export class P2PClient {
|
|
|
199
199
|
try { await fetch(url, { method: 'POST', headers: this.authHeaders() }); } catch { /* best-effort */ }
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** Submit the verifier code Anthropic shows on the callback page so
|
|
203
|
+
* the agent can finalise claude CLI's OAuth handshake. Codex doesn't
|
|
204
|
+
* use this — its device-auth flow auto-resolves via polling — but
|
|
205
|
+
* the SDK still exposes it for symmetry with mobile/src/components/
|
|
206
|
+
* RunnerAuthModal.tsx and the Swift YaverRunnerAuthFlowPane. */
|
|
207
|
+
async submitRunnerBrowserAuthCode(
|
|
208
|
+
sessionId: string,
|
|
209
|
+
code: string,
|
|
210
|
+
): Promise<RunnerBrowserAuthSession> {
|
|
211
|
+
const url = `${this.baseUrl}/runner-auth/browser/submit-code`;
|
|
212
|
+
const resp = await fetch(url, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { ...this.authHeaders(), 'Content-Type': 'application/json' },
|
|
215
|
+
body: JSON.stringify({ id: sessionId, code }),
|
|
216
|
+
});
|
|
217
|
+
if (!resp.ok) {
|
|
218
|
+
const text = await resp.text().catch(() => '');
|
|
219
|
+
throw new Error(`submitRunnerBrowserAuthCode HTTP ${resp.status}: ${text}`);
|
|
220
|
+
}
|
|
221
|
+
const data = await resp.json();
|
|
222
|
+
return data.session as RunnerBrowserAuthSession;
|
|
223
|
+
}
|
|
224
|
+
|
|
202
225
|
async capabilitySnapshot(): Promise<CapabilitySnapshot | null> {
|
|
203
226
|
try {
|
|
204
227
|
const resp = await fetch(`${this.baseUrl}/capabilities/snapshot`, { headers: this.authHeaders() });
|
|
@@ -323,11 +346,12 @@ export class P2PClient {
|
|
|
323
346
|
} as any);
|
|
324
347
|
}
|
|
325
348
|
|
|
349
|
+
// Use authHeaders() so a relay-routed baseUrl carries
|
|
350
|
+
// X-Relay-Password — without it the relay rejects with 401
|
|
351
|
+
// "invalid relay password" before the agent ever sees the form.
|
|
326
352
|
const response = await fetch(`${this.baseUrl}/feedback`, {
|
|
327
353
|
method: 'POST',
|
|
328
|
-
headers:
|
|
329
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
330
|
-
},
|
|
354
|
+
headers: this.authHeaders(),
|
|
331
355
|
body: formData,
|
|
332
356
|
});
|
|
333
357
|
|
|
@@ -348,10 +372,7 @@ export class P2PClient {
|
|
|
348
372
|
for await (const event of events) {
|
|
349
373
|
const response = await fetch(`${this.baseUrl}/feedback/stream`, {
|
|
350
374
|
method: 'POST',
|
|
351
|
-
headers: {
|
|
352
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
353
|
-
'Content-Type': 'application/json',
|
|
354
|
-
},
|
|
375
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
355
376
|
body: JSON.stringify(event),
|
|
356
377
|
});
|
|
357
378
|
|
|
@@ -375,10 +396,7 @@ export class P2PClient {
|
|
|
375
396
|
async startBuild(platform: string): Promise<any> {
|
|
376
397
|
const response = await fetch(`${this.baseUrl}/builds`, {
|
|
377
398
|
method: 'POST',
|
|
378
|
-
headers: {
|
|
379
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
380
|
-
'Content-Type': 'application/json',
|
|
381
|
-
},
|
|
399
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
382
400
|
body: JSON.stringify({ platform }),
|
|
383
401
|
});
|
|
384
402
|
|
|
@@ -423,9 +441,7 @@ export class P2PClient {
|
|
|
423
441
|
|
|
424
442
|
const response = await fetch(`${this.baseUrl}/voice/transcribe`, {
|
|
425
443
|
method: 'POST',
|
|
426
|
-
headers:
|
|
427
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
428
|
-
},
|
|
444
|
+
headers: this.authHeaders(),
|
|
429
445
|
body: formData,
|
|
430
446
|
});
|
|
431
447
|
|
|
@@ -471,7 +487,7 @@ export class P2PClient {
|
|
|
471
487
|
if (mode === 'dev') {
|
|
472
488
|
const primary = await fetch(`${this.baseUrl}/dev/reload`, {
|
|
473
489
|
method: 'POST',
|
|
474
|
-
headers:
|
|
490
|
+
headers: this.authHeaders(),
|
|
475
491
|
});
|
|
476
492
|
if (primary.ok) {
|
|
477
493
|
const payload = await primary.json().catch(() => ({} as Record<string, unknown>));
|
|
@@ -505,10 +521,7 @@ export class P2PClient {
|
|
|
505
521
|
|
|
506
522
|
const res = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
507
523
|
method: 'POST',
|
|
508
|
-
headers: {
|
|
509
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
510
|
-
'Content-Type': 'application/json',
|
|
511
|
-
},
|
|
524
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
512
525
|
body: JSON.stringify({
|
|
513
526
|
mode: 'bundle',
|
|
514
527
|
...identity,
|
|
@@ -558,10 +571,7 @@ export class P2PClient {
|
|
|
558
571
|
const identity = resolveAppIdentity(opts);
|
|
559
572
|
const response = await fetch(`${this.baseUrl}/vibing/execute`, {
|
|
560
573
|
method: 'POST',
|
|
561
|
-
headers: {
|
|
562
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
563
|
-
'Content-Type': 'application/json',
|
|
564
|
-
},
|
|
574
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
565
575
|
body: JSON.stringify({
|
|
566
576
|
prompt,
|
|
567
577
|
projectPath: identity.projectPath ?? opts?.projectPath ?? '',
|
|
@@ -600,7 +610,7 @@ export class P2PClient {
|
|
|
600
610
|
}
|
|
601
611
|
const response = await fetch(`${this.baseUrl}/vibing/eligibility?${params.toString()}`, {
|
|
602
612
|
method: 'GET',
|
|
603
|
-
headers:
|
|
613
|
+
headers: this.authHeaders(),
|
|
604
614
|
});
|
|
605
615
|
if (!response.ok) {
|
|
606
616
|
const text = await response.text().catch(() => '');
|
|
@@ -618,7 +628,7 @@ export class P2PClient {
|
|
|
618
628
|
async triggerFix(feedbackId: string): Promise<{ taskId: string; prompt: string }> {
|
|
619
629
|
const response = await fetch(`${this.baseUrl}/feedback/${encodeURIComponent(feedbackId)}/fix`, {
|
|
620
630
|
method: 'POST',
|
|
621
|
-
headers:
|
|
631
|
+
headers: this.authHeaders(),
|
|
622
632
|
});
|
|
623
633
|
if (!response.ok) {
|
|
624
634
|
const text = await response.text().catch(() => '');
|
|
@@ -641,10 +651,7 @@ export class P2PClient {
|
|
|
641
651
|
async startTestSession(): Promise<{ sessionId: string }> {
|
|
642
652
|
const response = await fetch(`${this.baseUrl}/test-app/start`, {
|
|
643
653
|
method: 'POST',
|
|
644
|
-
headers: {
|
|
645
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
646
|
-
'Content-Type': 'application/json',
|
|
647
|
-
},
|
|
654
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
648
655
|
body: JSON.stringify({ source: 'feedback-sdk' }),
|
|
649
656
|
});
|
|
650
657
|
|
|
@@ -660,7 +667,7 @@ export class P2PClient {
|
|
|
660
667
|
async stopTestSession(): Promise<void> {
|
|
661
668
|
await fetch(`${this.baseUrl}/test-app/stop`, {
|
|
662
669
|
method: 'POST',
|
|
663
|
-
headers:
|
|
670
|
+
headers: this.authHeaders(),
|
|
664
671
|
});
|
|
665
672
|
}
|
|
666
673
|
|
|
@@ -678,10 +685,7 @@ export class P2PClient {
|
|
|
678
685
|
async rotateToken(): Promise<{ token: string; expiresAt: number }> {
|
|
679
686
|
const response = await fetch(`${this.baseUrl}/sdk/token/rotate`, {
|
|
680
687
|
method: 'POST',
|
|
681
|
-
headers: {
|
|
682
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
683
|
-
'Content-Type': 'application/json',
|
|
684
|
-
},
|
|
688
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
685
689
|
});
|
|
686
690
|
|
|
687
691
|
if (!response.ok) {
|
|
@@ -706,7 +710,7 @@ export class P2PClient {
|
|
|
706
710
|
async flagsEvaluate(userId: string = 'anonymous'): Promise<Record<string, unknown>> {
|
|
707
711
|
const res = await fetch(
|
|
708
712
|
`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}`,
|
|
709
|
-
{ headers:
|
|
713
|
+
{ headers: this.authHeaders() },
|
|
710
714
|
);
|
|
711
715
|
if (!res.ok) return {};
|
|
712
716
|
const data = await res.json();
|
|
@@ -720,7 +724,7 @@ export class P2PClient {
|
|
|
720
724
|
): Promise<T | undefined> {
|
|
721
725
|
const res = await fetch(
|
|
722
726
|
`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}&flag=${encodeURIComponent(key)}`,
|
|
723
|
-
{ headers:
|
|
727
|
+
{ headers: this.authHeaders() },
|
|
724
728
|
);
|
|
725
729
|
if (!res.ok) return undefined;
|
|
726
730
|
const data = await res.json();
|
|
@@ -753,7 +757,7 @@ export class P2PClient {
|
|
|
753
757
|
const params = new URLSearchParams({ channel });
|
|
754
758
|
if (deviceId) params.set('device', deviceId);
|
|
755
759
|
const res = await fetch(`${this.baseUrl}/releases/latest?${params.toString()}`, {
|
|
756
|
-
headers:
|
|
760
|
+
headers: this.authHeaders(),
|
|
757
761
|
});
|
|
758
762
|
if (!res.ok) return null;
|
|
759
763
|
return res.json();
|
|
@@ -766,7 +770,7 @@ export class P2PClient {
|
|
|
766
770
|
): Promise<ArrayBuffer | null> {
|
|
767
771
|
const params = new URLSearchParams({ channel, semver });
|
|
768
772
|
const res = await fetch(`${this.baseUrl}/releases/bundle?${params.toString()}`, {
|
|
769
|
-
headers:
|
|
773
|
+
headers: this.authHeaders(),
|
|
770
774
|
});
|
|
771
775
|
if (!res.ok) return null;
|
|
772
776
|
return res.arrayBuffer();
|
|
@@ -787,10 +791,7 @@ export class P2PClient {
|
|
|
787
791
|
try {
|
|
788
792
|
const res = await fetch(`${this.baseUrl}/analytics/ingest`, {
|
|
789
793
|
method: 'POST',
|
|
790
|
-
headers: {
|
|
791
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
792
|
-
'Content-Type': 'application/json',
|
|
793
|
-
},
|
|
794
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
794
795
|
body: JSON.stringify({
|
|
795
796
|
name,
|
|
796
797
|
props,
|
package/src/YaverFeedback.ts
CHANGED
|
@@ -148,8 +148,51 @@ let enabled = false;
|
|
|
148
148
|
let p2pClient: P2PClient | null = null;
|
|
149
149
|
let shakeDetector: ShakeDetector | null = null;
|
|
150
150
|
let p2pAuthToken: string | null = null;
|
|
151
|
+
let p2pRelayPassword: string = '';
|
|
151
152
|
let reportLaunchInFlight = false;
|
|
152
153
|
|
|
154
|
+
/** Resolve the user's relay password by validating their auth token
|
|
155
|
+
* against Convex. Used whenever we (re)build the P2PClient so a
|
|
156
|
+
* relay-routed agentUrl carries a valid X-Relay-Password — without
|
|
157
|
+
* this, every relay-tunneled request rejects with HTTP 401
|
|
158
|
+
* "invalid relay password" (relay/server.go:957).
|
|
159
|
+
*
|
|
160
|
+
* Cached on `p2pRelayPassword` so we only round-trip Convex when the
|
|
161
|
+
* user's auth token actually changes. Returns "" on any failure so
|
|
162
|
+
* direct LAN agentUrls (which need no password) keep working.
|
|
163
|
+
*/
|
|
164
|
+
async function resolveRelayPassword(authToken: string, convexUrl?: string): Promise<string> {
|
|
165
|
+
const trimmed = (authToken || '').trim();
|
|
166
|
+
if (!trimmed) {
|
|
167
|
+
p2pRelayPassword = '';
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
const url = (convexUrl || config?.convexUrl || DEFAULT_CONVEX_SITE_URL).replace(/\/+$/, '');
|
|
171
|
+
try {
|
|
172
|
+
// /settings returns {ok, settings: {relayPassword, relayUrl, ...}}
|
|
173
|
+
// Older accounts may flatten relayPassword to the top — match the
|
|
174
|
+
// tolerance the web shell already uses (route.ts:77).
|
|
175
|
+
const res = await fetch(`${url}/settings`, {
|
|
176
|
+
headers: { Authorization: `Bearer ${trimmed}` },
|
|
177
|
+
});
|
|
178
|
+
if (!res.ok) return p2pRelayPassword;
|
|
179
|
+
const data = await res.json().catch(() => ({} as Record<string, unknown>));
|
|
180
|
+
const settings = (data as { settings?: { relayPassword?: string } })?.settings;
|
|
181
|
+
const pw =
|
|
182
|
+
(typeof settings?.relayPassword === 'string' && settings.relayPassword) ||
|
|
183
|
+
(typeof (data as { relayPassword?: string })?.relayPassword === 'string'
|
|
184
|
+
? (data as { relayPassword?: string }).relayPassword
|
|
185
|
+
: '') ||
|
|
186
|
+
'';
|
|
187
|
+
p2pRelayPassword = pw;
|
|
188
|
+
return pw;
|
|
189
|
+
} catch {
|
|
190
|
+
// Network failure on a passive Convex round-trip shouldn't break
|
|
191
|
+
// direct-LAN flows. Fall through with whatever we already cached.
|
|
192
|
+
return p2pRelayPassword;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
153
196
|
/** Ring buffer of captured errors. */
|
|
154
197
|
let errorBuffer: CapturedError[] = [];
|
|
155
198
|
let maxErrors = 5;
|
|
@@ -212,7 +255,8 @@ export class YaverFeedback {
|
|
|
212
255
|
return;
|
|
213
256
|
}
|
|
214
257
|
p2pAuthToken = token;
|
|
215
|
-
|
|
258
|
+
const rp = await resolveRelayPassword(token);
|
|
259
|
+
p2pClient = new P2PClient(effectiveUrl, token, rp);
|
|
216
260
|
}
|
|
217
261
|
|
|
218
262
|
/**
|
|
@@ -295,7 +339,12 @@ export class YaverFeedback {
|
|
|
295
339
|
// Create P2P client if we have a URL
|
|
296
340
|
if (config.agentUrl) {
|
|
297
341
|
p2pAuthToken = config.authToken ?? null;
|
|
298
|
-
|
|
342
|
+
// Initial construction uses the cached p2pRelayPassword (empty on
|
|
343
|
+
// first init). rebuildP2PClient below resolves the real password
|
|
344
|
+
// from Convex and replaces this client — but only when authToken
|
|
345
|
+
// is set, so set a placeholder header here that won't 401 a
|
|
346
|
+
// direct-LAN url and will be overwritten before any relay hop.
|
|
347
|
+
p2pClient = new P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
|
|
299
348
|
if (config.authToken) {
|
|
300
349
|
void YaverFeedback.rebuildP2PClient(config.agentUrl);
|
|
301
350
|
}
|
|
@@ -596,6 +645,19 @@ export class YaverFeedback {
|
|
|
596
645
|
await p2pClient.cancelRunnerBrowserAuth(sessionId);
|
|
597
646
|
}
|
|
598
647
|
|
|
648
|
+
/** Submit the Claude paste-back verifier so the agent can finalise the
|
|
649
|
+
* OAuth handshake. RunnerAuthModal calls this after the user copies
|
|
650
|
+
* the code from platform.claude.com's callback page. */
|
|
651
|
+
static async submitRunnerBrowserAuthCode(
|
|
652
|
+
sessionId: string,
|
|
653
|
+
code: string,
|
|
654
|
+
): Promise<import('./types').RunnerBrowserAuthSession> {
|
|
655
|
+
if (!p2pClient) {
|
|
656
|
+
throw new Error('Not connected to any agent.');
|
|
657
|
+
}
|
|
658
|
+
return p2pClient.submitRunnerBrowserAuthCode(sessionId, code);
|
|
659
|
+
}
|
|
660
|
+
|
|
599
661
|
/**
|
|
600
662
|
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
601
663
|
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
|
@@ -936,7 +998,8 @@ export class YaverFeedback {
|
|
|
936
998
|
});
|
|
937
999
|
if (result) {
|
|
938
1000
|
config.agentUrl = result.url;
|
|
939
|
-
|
|
1001
|
+
const rp = await resolveRelayPassword(config.authToken ?? '');
|
|
1002
|
+
p2pClient = new P2PClient(result.url, config.authToken ?? '', rp);
|
|
940
1003
|
}
|
|
941
1004
|
} catch {}
|
|
942
1005
|
}
|