yaver-feedback-react-native 0.8.10 → 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 CHANGED
@@ -257,15 +257,21 @@ class YaverDiscovery {
257
257
  */
258
258
  static async discoverViaRelay(convexUrl, authToken, deviceId) {
259
259
  try {
260
- const settingsRes = await fetch(`${convexUrl}/auth/validate`, {
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
- relayUrl = settingsData.relayUrl;
268
- relayPassword = settingsData.relayPassword;
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/P2PClient.js CHANGED
@@ -291,11 +291,12 @@ class P2PClient {
291
291
  name: 'voice_note.m4a',
292
292
  });
293
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.
294
297
  const response = await fetch(`${this.baseUrl}/feedback`, {
295
298
  method: 'POST',
296
- headers: {
297
- Authorization: `Bearer ${this.authToken}`,
298
- },
299
+ headers: this.authHeaders(),
299
300
  body: formData,
300
301
  });
301
302
  if (!response.ok) {
@@ -313,10 +314,7 @@ class P2PClient {
313
314
  for await (const event of events) {
314
315
  const response = await fetch(`${this.baseUrl}/feedback/stream`, {
315
316
  method: 'POST',
316
- headers: {
317
- Authorization: `Bearer ${this.authToken}`,
318
- 'Content-Type': 'application/json',
319
- },
317
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
320
318
  body: JSON.stringify(event),
321
319
  });
322
320
  if (!response.ok) {
@@ -335,10 +333,7 @@ class P2PClient {
335
333
  async startBuild(platform) {
336
334
  const response = await fetch(`${this.baseUrl}/builds`, {
337
335
  method: 'POST',
338
- headers: {
339
- Authorization: `Bearer ${this.authToken}`,
340
- 'Content-Type': 'application/json',
341
- },
336
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
342
337
  body: JSON.stringify({ platform }),
343
338
  });
344
339
  if (!response.ok) {
@@ -378,9 +373,7 @@ class P2PClient {
378
373
  });
379
374
  const response = await fetch(`${this.baseUrl}/voice/transcribe`, {
380
375
  method: 'POST',
381
- headers: {
382
- Authorization: `Bearer ${this.authToken}`,
383
- },
376
+ headers: this.authHeaders(),
384
377
  body: formData,
385
378
  });
386
379
  if (!response.ok) {
@@ -420,7 +413,7 @@ class P2PClient {
420
413
  if (mode === 'dev') {
421
414
  const primary = await fetch(`${this.baseUrl}/dev/reload`, {
422
415
  method: 'POST',
423
- headers: { Authorization: `Bearer ${this.authToken}` },
416
+ headers: this.authHeaders(),
424
417
  });
425
418
  if (primary.ok) {
426
419
  const payload = await primary.json().catch(() => ({}));
@@ -451,10 +444,7 @@ class P2PClient {
451
444
  const identity = resolveAppIdentity(opts);
452
445
  const res = await fetch(`${this.baseUrl}/dev/reload-app`, {
453
446
  method: 'POST',
454
- headers: {
455
- Authorization: `Bearer ${this.authToken}`,
456
- 'Content-Type': 'application/json',
457
- },
447
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
458
448
  body: JSON.stringify({
459
449
  mode: 'bundle',
460
450
  ...identity,
@@ -498,10 +488,7 @@ class P2PClient {
498
488
  const identity = resolveAppIdentity(opts);
499
489
  const response = await fetch(`${this.baseUrl}/vibing/execute`, {
500
490
  method: 'POST',
501
- headers: {
502
- Authorization: `Bearer ${this.authToken}`,
503
- 'Content-Type': 'application/json',
504
- },
491
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
505
492
  body: JSON.stringify({
506
493
  prompt,
507
494
  projectPath: identity.projectPath ?? opts?.projectPath ?? '',
@@ -529,7 +516,7 @@ class P2PClient {
529
516
  }
530
517
  const response = await fetch(`${this.baseUrl}/vibing/eligibility?${params.toString()}`, {
531
518
  method: 'GET',
532
- headers: { Authorization: `Bearer ${this.authToken}` },
519
+ headers: this.authHeaders(),
533
520
  });
534
521
  if (!response.ok) {
535
522
  const text = await response.text().catch(() => '');
@@ -546,7 +533,7 @@ class P2PClient {
546
533
  async triggerFix(feedbackId) {
547
534
  const response = await fetch(`${this.baseUrl}/feedback/${encodeURIComponent(feedbackId)}/fix`, {
548
535
  method: 'POST',
549
- headers: { Authorization: `Bearer ${this.authToken}` },
536
+ headers: this.authHeaders(),
550
537
  });
551
538
  if (!response.ok) {
552
539
  const text = await response.text().catch(() => '');
@@ -567,10 +554,7 @@ class P2PClient {
567
554
  async startTestSession() {
568
555
  const response = await fetch(`${this.baseUrl}/test-app/start`, {
569
556
  method: 'POST',
570
- headers: {
571
- Authorization: `Bearer ${this.authToken}`,
572
- 'Content-Type': 'application/json',
573
- },
557
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
574
558
  body: JSON.stringify({ source: 'feedback-sdk' }),
575
559
  });
576
560
  if (!response.ok) {
@@ -583,7 +567,7 @@ class P2PClient {
583
567
  async stopTestSession() {
584
568
  await fetch(`${this.baseUrl}/test-app/stop`, {
585
569
  method: 'POST',
586
- headers: { Authorization: `Bearer ${this.authToken}` },
570
+ headers: this.authHeaders(),
587
571
  });
588
572
  }
589
573
  /** Get the current test session status and list of fixes. */
@@ -599,10 +583,7 @@ class P2PClient {
599
583
  async rotateToken() {
600
584
  const response = await fetch(`${this.baseUrl}/sdk/token/rotate`, {
601
585
  method: 'POST',
602
- headers: {
603
- Authorization: `Bearer ${this.authToken}`,
604
- 'Content-Type': 'application/json',
605
- },
586
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
606
587
  });
607
588
  if (!response.ok) {
608
589
  const text = await response.text().catch(() => '');
@@ -621,7 +602,7 @@ class P2PClient {
621
602
  * for 30s in getFlagsCached().
622
603
  */
623
604
  async flagsEvaluate(userId = 'anonymous') {
624
- const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}`, { headers: { Authorization: `Bearer ${this.authToken}` } });
605
+ const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}`, { headers: this.authHeaders() });
625
606
  if (!res.ok)
626
607
  return {};
627
608
  const data = await res.json();
@@ -629,7 +610,7 @@ class P2PClient {
629
610
  }
630
611
  /** Evaluate a single flag by key — shortcut when you only need one. */
631
612
  async flagsEvaluateOne(key, userId = 'anonymous') {
632
- const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}&flag=${encodeURIComponent(key)}`, { headers: { Authorization: `Bearer ${this.authToken}` } });
613
+ const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}&flag=${encodeURIComponent(key)}`, { headers: this.authHeaders() });
633
614
  if (!res.ok)
634
615
  return undefined;
635
616
  const data = await res.json();
@@ -647,7 +628,7 @@ class P2PClient {
647
628
  if (deviceId)
648
629
  params.set('device', deviceId);
649
630
  const res = await fetch(`${this.baseUrl}/releases/latest?${params.toString()}`, {
650
- headers: { Authorization: `Bearer ${this.authToken}` },
631
+ headers: this.authHeaders(),
651
632
  });
652
633
  if (!res.ok)
653
634
  return null;
@@ -657,7 +638,7 @@ class P2PClient {
657
638
  async releasesDownload(channel, semver) {
658
639
  const params = new URLSearchParams({ channel, semver });
659
640
  const res = await fetch(`${this.baseUrl}/releases/bundle?${params.toString()}`, {
660
- headers: { Authorization: `Bearer ${this.authToken}` },
641
+ headers: this.authHeaders(),
661
642
  });
662
643
  if (!res.ok)
663
644
  return null;
@@ -673,10 +654,7 @@ class P2PClient {
673
654
  try {
674
655
  const res = await fetch(`${this.baseUrl}/analytics/ingest`, {
675
656
  method: 'POST',
676
- headers: {
677
- Authorization: `Bearer ${this.authToken}`,
678
- 'Content-Type': 'application/json',
679
- },
657
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
680
658
  body: JSON.stringify({
681
659
  name,
682
660
  props,
@@ -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
- p2pClient = new P2PClient_1.P2PClient(effectiveUrl, token);
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
- p2pClient = new P2PClient_1.P2PClient(config.agentUrl, config.authToken ?? '');
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
  }
@@ -900,7 +949,8 @@ class YaverFeedback {
900
949
  });
901
950
  if (result) {
902
951
  config.agentUrl = result.url;
903
- p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '');
952
+ const rp = await resolveRelayPassword(config.authToken ?? '');
953
+ p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '', rp);
904
954
  }
905
955
  }
906
956
  catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.8.10",
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
- const settingsRes = await fetch(`${convexUrl}/auth/validate`, {
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
- relayUrl = settingsData.relayUrl;
295
- relayPassword = settingsData.relayPassword;
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/P2PClient.ts CHANGED
@@ -346,11 +346,12 @@ export class P2PClient {
346
346
  } as any);
347
347
  }
348
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.
349
352
  const response = await fetch(`${this.baseUrl}/feedback`, {
350
353
  method: 'POST',
351
- headers: {
352
- Authorization: `Bearer ${this.authToken}`,
353
- },
354
+ headers: this.authHeaders(),
354
355
  body: formData,
355
356
  });
356
357
 
@@ -371,10 +372,7 @@ export class P2PClient {
371
372
  for await (const event of events) {
372
373
  const response = await fetch(`${this.baseUrl}/feedback/stream`, {
373
374
  method: 'POST',
374
- headers: {
375
- Authorization: `Bearer ${this.authToken}`,
376
- 'Content-Type': 'application/json',
377
- },
375
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
378
376
  body: JSON.stringify(event),
379
377
  });
380
378
 
@@ -398,10 +396,7 @@ export class P2PClient {
398
396
  async startBuild(platform: string): Promise<any> {
399
397
  const response = await fetch(`${this.baseUrl}/builds`, {
400
398
  method: 'POST',
401
- headers: {
402
- Authorization: `Bearer ${this.authToken}`,
403
- 'Content-Type': 'application/json',
404
- },
399
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
405
400
  body: JSON.stringify({ platform }),
406
401
  });
407
402
 
@@ -446,9 +441,7 @@ export class P2PClient {
446
441
 
447
442
  const response = await fetch(`${this.baseUrl}/voice/transcribe`, {
448
443
  method: 'POST',
449
- headers: {
450
- Authorization: `Bearer ${this.authToken}`,
451
- },
444
+ headers: this.authHeaders(),
452
445
  body: formData,
453
446
  });
454
447
 
@@ -494,7 +487,7 @@ export class P2PClient {
494
487
  if (mode === 'dev') {
495
488
  const primary = await fetch(`${this.baseUrl}/dev/reload`, {
496
489
  method: 'POST',
497
- headers: { Authorization: `Bearer ${this.authToken}` },
490
+ headers: this.authHeaders(),
498
491
  });
499
492
  if (primary.ok) {
500
493
  const payload = await primary.json().catch(() => ({} as Record<string, unknown>));
@@ -528,10 +521,7 @@ export class P2PClient {
528
521
 
529
522
  const res = await fetch(`${this.baseUrl}/dev/reload-app`, {
530
523
  method: 'POST',
531
- headers: {
532
- Authorization: `Bearer ${this.authToken}`,
533
- 'Content-Type': 'application/json',
534
- },
524
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
535
525
  body: JSON.stringify({
536
526
  mode: 'bundle',
537
527
  ...identity,
@@ -581,10 +571,7 @@ export class P2PClient {
581
571
  const identity = resolveAppIdentity(opts);
582
572
  const response = await fetch(`${this.baseUrl}/vibing/execute`, {
583
573
  method: 'POST',
584
- headers: {
585
- Authorization: `Bearer ${this.authToken}`,
586
- 'Content-Type': 'application/json',
587
- },
574
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
588
575
  body: JSON.stringify({
589
576
  prompt,
590
577
  projectPath: identity.projectPath ?? opts?.projectPath ?? '',
@@ -623,7 +610,7 @@ export class P2PClient {
623
610
  }
624
611
  const response = await fetch(`${this.baseUrl}/vibing/eligibility?${params.toString()}`, {
625
612
  method: 'GET',
626
- headers: { Authorization: `Bearer ${this.authToken}` },
613
+ headers: this.authHeaders(),
627
614
  });
628
615
  if (!response.ok) {
629
616
  const text = await response.text().catch(() => '');
@@ -641,7 +628,7 @@ export class P2PClient {
641
628
  async triggerFix(feedbackId: string): Promise<{ taskId: string; prompt: string }> {
642
629
  const response = await fetch(`${this.baseUrl}/feedback/${encodeURIComponent(feedbackId)}/fix`, {
643
630
  method: 'POST',
644
- headers: { Authorization: `Bearer ${this.authToken}` },
631
+ headers: this.authHeaders(),
645
632
  });
646
633
  if (!response.ok) {
647
634
  const text = await response.text().catch(() => '');
@@ -664,10 +651,7 @@ export class P2PClient {
664
651
  async startTestSession(): Promise<{ sessionId: string }> {
665
652
  const response = await fetch(`${this.baseUrl}/test-app/start`, {
666
653
  method: 'POST',
667
- headers: {
668
- Authorization: `Bearer ${this.authToken}`,
669
- 'Content-Type': 'application/json',
670
- },
654
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
671
655
  body: JSON.stringify({ source: 'feedback-sdk' }),
672
656
  });
673
657
 
@@ -683,7 +667,7 @@ export class P2PClient {
683
667
  async stopTestSession(): Promise<void> {
684
668
  await fetch(`${this.baseUrl}/test-app/stop`, {
685
669
  method: 'POST',
686
- headers: { Authorization: `Bearer ${this.authToken}` },
670
+ headers: this.authHeaders(),
687
671
  });
688
672
  }
689
673
 
@@ -701,10 +685,7 @@ export class P2PClient {
701
685
  async rotateToken(): Promise<{ token: string; expiresAt: number }> {
702
686
  const response = await fetch(`${this.baseUrl}/sdk/token/rotate`, {
703
687
  method: 'POST',
704
- headers: {
705
- Authorization: `Bearer ${this.authToken}`,
706
- 'Content-Type': 'application/json',
707
- },
688
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
708
689
  });
709
690
 
710
691
  if (!response.ok) {
@@ -729,7 +710,7 @@ export class P2PClient {
729
710
  async flagsEvaluate(userId: string = 'anonymous'): Promise<Record<string, unknown>> {
730
711
  const res = await fetch(
731
712
  `${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}`,
732
- { headers: { Authorization: `Bearer ${this.authToken}` } },
713
+ { headers: this.authHeaders() },
733
714
  );
734
715
  if (!res.ok) return {};
735
716
  const data = await res.json();
@@ -743,7 +724,7 @@ export class P2PClient {
743
724
  ): Promise<T | undefined> {
744
725
  const res = await fetch(
745
726
  `${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}&flag=${encodeURIComponent(key)}`,
746
- { headers: { Authorization: `Bearer ${this.authToken}` } },
727
+ { headers: this.authHeaders() },
747
728
  );
748
729
  if (!res.ok) return undefined;
749
730
  const data = await res.json();
@@ -776,7 +757,7 @@ export class P2PClient {
776
757
  const params = new URLSearchParams({ channel });
777
758
  if (deviceId) params.set('device', deviceId);
778
759
  const res = await fetch(`${this.baseUrl}/releases/latest?${params.toString()}`, {
779
- headers: { Authorization: `Bearer ${this.authToken}` },
760
+ headers: this.authHeaders(),
780
761
  });
781
762
  if (!res.ok) return null;
782
763
  return res.json();
@@ -789,7 +770,7 @@ export class P2PClient {
789
770
  ): Promise<ArrayBuffer | null> {
790
771
  const params = new URLSearchParams({ channel, semver });
791
772
  const res = await fetch(`${this.baseUrl}/releases/bundle?${params.toString()}`, {
792
- headers: { Authorization: `Bearer ${this.authToken}` },
773
+ headers: this.authHeaders(),
793
774
  });
794
775
  if (!res.ok) return null;
795
776
  return res.arrayBuffer();
@@ -810,10 +791,7 @@ export class P2PClient {
810
791
  try {
811
792
  const res = await fetch(`${this.baseUrl}/analytics/ingest`, {
812
793
  method: 'POST',
813
- headers: {
814
- Authorization: `Bearer ${this.authToken}`,
815
- 'Content-Type': 'application/json',
816
- },
794
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
817
795
  body: JSON.stringify({
818
796
  name,
819
797
  props,
@@ -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
- p2pClient = new P2PClient(effectiveUrl, token);
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
- p2pClient = new P2PClient(config.agentUrl, config.authToken ?? '');
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
  }
@@ -949,7 +998,8 @@ export class YaverFeedback {
949
998
  });
950
999
  if (result) {
951
1000
  config.agentUrl = result.url;
952
- p2pClient = new P2PClient(result.url, config.authToken ?? '');
1001
+ const rp = await resolveRelayPassword(config.authToken ?? '');
1002
+ p2pClient = new P2PClient(result.url, config.authToken ?? '', rp);
953
1003
  }
954
1004
  } catch {}
955
1005
  }