yaver-feedback-react-native 0.7.7 → 0.7.9

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
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.YaverDiscovery = void 0;
4
37
  function getAsyncStorage() {
@@ -201,38 +234,20 @@ class YaverDiscovery {
201
234
  * does on same-LAN.
202
235
  */
203
236
  static async raceProbe(urls) {
204
- if (!urls || urls.length === 0)
205
- return null;
206
- const attempts = urls.map((url) => YaverDiscovery.probe(url).then((r) => {
207
- if (!r)
208
- throw new Error('no-200');
209
- return r;
210
- }));
211
- try {
212
- // `Promise.any` isn't on every RN runtime yet (older Hermes).
213
- // Hand-roll the same behaviour so we don't need a polyfill.
214
- return await new Promise((resolve) => {
215
- let remaining = attempts.length;
216
- let settled = false;
217
- attempts.forEach((p) => {
218
- p.then((r) => {
219
- if (settled)
220
- return;
221
- settled = true;
222
- resolve(r);
223
- }).catch(() => {
224
- remaining -= 1;
225
- if (remaining <= 0 && !settled) {
226
- settled = true;
227
- resolve(null);
228
- }
229
- });
230
- });
231
- });
232
- }
233
- catch {
237
+ // Delegate to the canonical client-core implementation so mobile +
238
+ // SDK run the same race logic. Returned shape is `ProbeResult` —
239
+ // structurally compatible with DiscoveryResult minus the required
240
+ // hostname/version fields, which we backfill with sane defaults.
241
+ const { raceHealthProbes } = await Promise.resolve().then(() => __importStar(require('./_core/device')));
242
+ const res = await raceHealthProbes(urls, { timeoutMs: PROBE_TIMEOUT_MS });
243
+ if (!res)
234
244
  return null;
235
- }
245
+ return {
246
+ url: res.url,
247
+ hostname: res.hostname ?? 'Unknown',
248
+ version: res.version ?? 'unknown',
249
+ latency: res.latency ?? 0,
250
+ };
236
251
  }
237
252
  /**
238
253
  * Discover agent via relay HTTP proxy. Uses the user's configured
@@ -37,6 +37,7 @@ exports.YaverMachinePickerScreen = void 0;
37
37
  const react_1 = __importStar(require("react"));
38
38
  const react_native_1 = require("react-native");
39
39
  const auth_1 = require("./auth");
40
+ const PairDeviceModal_1 = require("./PairDeviceModal");
40
41
  /**
41
42
  * List of remote dev machines the signed-in user can reach. Split into
42
43
  * - Owned machines (user is the host)
@@ -51,6 +52,7 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
51
52
  const [refreshing, setRefreshing] = (0, react_1.useState)(false);
52
53
  const [error, setError] = (0, react_1.useState)(null);
53
54
  const [list, setList] = (0, react_1.useState)({ owned: [], shared: [] });
55
+ const [pairingDevice, setPairingDevice] = (0, react_1.useState)(null);
54
56
  const load = (0, react_1.useCallback)(async (silent = false) => {
55
57
  if (!silent)
56
58
  setLoading(true);
@@ -74,6 +76,15 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
74
76
  void load();
75
77
  }, [load]);
76
78
  const handlePick = async (device) => {
79
+ // Needs-auth device — show the in-SDK pair modal instead of
80
+ // treating the tap as a "pick". The user enters the 6-char code
81
+ // from their Mac terminal; the SDK POSTs it to /auth/pair/submit
82
+ // on the agent directly. Once the device flips out of bootstrap
83
+ // mode, the next load() picks up the fresh state.
84
+ if (device.isOnline && device.needsAuth) {
85
+ setPairingDevice(device);
86
+ return;
87
+ }
77
88
  await (0, auth_1.saveSelectedDeviceId)(device.deviceId);
78
89
  onPick(device);
79
90
  };
@@ -149,6 +160,13 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
149
160
  {error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
150
161
  </>)}
151
162
  </react_native_1.ScrollView>
163
+
164
+ <PairDeviceModal_1.PairDeviceModal device={pairingDevice} onClose={() => setPairingDevice(null)} onPaired={() => {
165
+ // Give the agent a moment to flip bootstrap → owner mode,
166
+ // then reload the list so the now-authenticated device shows
167
+ // up with a green dot and can be selected normally.
168
+ setTimeout(() => void load(true), 1500);
169
+ }}/>
152
170
  </react_native_1.SafeAreaView>);
153
171
  };
154
172
  exports.YaverMachinePickerScreen = YaverMachinePickerScreen;
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import type { RemoteDevice } from './auth';
3
+ /**
4
+ * In-SDK remote-pair modal. Shown when the user taps a device in the
5
+ * machine picker that's in `needsAuth` state.
6
+ *
7
+ * Flow:
8
+ * 1. User types the 6-char bootstrap passkey printed in the
9
+ * `yaver serve` terminal on the Mac (also shown in Yaver mobile
10
+ * app when pairing interactively).
11
+ * 2. SDK POSTs to `http://<device.quicHost>:<device.httpPort>/auth/pair/submit?code=XXXXXX`
12
+ * with the user's Convex session token (same one the SDK already
13
+ * has after Apple / Google / email sign-in).
14
+ * 3. Agent validates the token against Convex, persists it, flips
15
+ * out of bootstrap mode — within a couple of seconds it'll
16
+ * report `needsAuth=false` in /devices/list.
17
+ *
18
+ * This avoids making the user bounce to the Yaver mobile app just to
19
+ * adopt a machine. Works for owners and shared-scope guests since the
20
+ * pair endpoint accepts any valid Convex session that matches the
21
+ * expected account type.
22
+ */
23
+ export interface PairDeviceModalProps {
24
+ device: RemoteDevice | null;
25
+ onClose: () => void;
26
+ onPaired?: (device: RemoteDevice) => void;
27
+ }
28
+ export declare const PairDeviceModal: React.FC<PairDeviceModalProps>;
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PairDeviceModal = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const auth_1 = require("./auth");
40
+ const PairDeviceModal = ({ device, onClose, onPaired, }) => {
41
+ const [code, setCode] = (0, react_1.useState)('');
42
+ const [busy, setBusy] = (0, react_1.useState)(false);
43
+ const [error, setError] = (0, react_1.useState)(null);
44
+ const [success, setSuccess] = (0, react_1.useState)(false);
45
+ (0, react_1.useEffect)(() => {
46
+ if (device) {
47
+ setCode('');
48
+ setError(null);
49
+ setSuccess(false);
50
+ }
51
+ }, [device?.deviceId]);
52
+ const handleSubmit = async () => {
53
+ if (!device)
54
+ return;
55
+ const trimmed = code.trim().toUpperCase();
56
+ if (trimmed.length !== 6) {
57
+ setError('Code must be 6 characters.');
58
+ return;
59
+ }
60
+ const token = await (0, auth_1.getToken)();
61
+ if (!token) {
62
+ setError('Not signed in.');
63
+ return;
64
+ }
65
+ const host = (device.quicHost || '').trim();
66
+ const port = device.httpPort || device.quicPort || 18080;
67
+ if (!host) {
68
+ setError('No reachable address for this machine.');
69
+ return;
70
+ }
71
+ setBusy(true);
72
+ setError(null);
73
+ try {
74
+ const url = `http://${host}:${port}/auth/pair/submit?code=${encodeURIComponent(trimmed)}`;
75
+ const res = await fetch(url, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({
79
+ token,
80
+ convexSiteUrl: (0, auth_1.getConvexSiteUrl)(),
81
+ // Backend reads userId from the session, but older agents
82
+ // expect it in the body. Pass empty string if unknown.
83
+ userId: '',
84
+ }),
85
+ });
86
+ if (!res.ok) {
87
+ let msg = `Agent rejected pair (HTTP ${res.status}).`;
88
+ try {
89
+ const body = await res.json();
90
+ if (body?.error)
91
+ msg = String(body.error);
92
+ }
93
+ catch {
94
+ // body not JSON
95
+ }
96
+ throw new Error(msg);
97
+ }
98
+ setSuccess(true);
99
+ onPaired?.(device);
100
+ setTimeout(() => {
101
+ onClose();
102
+ }, 1200);
103
+ }
104
+ catch (err) {
105
+ setError(err instanceof Error ? err.message : String(err));
106
+ }
107
+ finally {
108
+ setBusy(false);
109
+ }
110
+ };
111
+ return (<react_native_1.Modal visible={!!device} animationType="slide" transparent onRequestClose={onClose}>
112
+ <react_native_1.Pressable style={styles.overlay} onPress={onClose}>
113
+ <react_native_1.Pressable style={styles.card} onPress={(e) => e.stopPropagation()}>
114
+ <react_native_1.View style={styles.header}>
115
+ <react_native_1.Text style={styles.title}>Pair this Mac</react_native_1.Text>
116
+ <react_native_1.Pressable onPress={onClose} hitSlop={12} style={styles.closeBtn}>
117
+ <react_native_1.Text style={styles.closeIcon}>×</react_native_1.Text>
118
+ </react_native_1.Pressable>
119
+ </react_native_1.View>
120
+
121
+ <react_native_1.Text style={styles.deviceName}>{device?.name || device?.deviceId}</react_native_1.Text>
122
+ <react_native_1.Text style={styles.body}>
123
+ On the Mac where `yaver serve` is running, look for the 6-character code in the terminal output. Enter it here to adopt this machine.
124
+ </react_native_1.Text>
125
+
126
+ <react_native_1.TextInput style={styles.codeInput} value={code} onChangeText={(v) => setCode(v.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))} placeholder="ABCDEF" placeholderTextColor="#555" autoCapitalize="characters" autoCorrect={false} maxLength={6} keyboardType={react_native_1.Platform.OS === 'ios' ? 'ascii-capable' : 'visible-password'}/>
127
+
128
+ {error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
129
+ {success && <react_native_1.Text style={styles.success}>Paired ✓</react_native_1.Text>}
130
+
131
+ <react_native_1.Pressable onPress={handleSubmit} disabled={busy || success || code.length !== 6} style={({ pressed }) => [
132
+ styles.submit,
133
+ (busy || success || code.length !== 6) && styles.submitDisabled,
134
+ pressed && { opacity: 0.7 },
135
+ ]}>
136
+ {busy ? (<react_native_1.ActivityIndicator color="#fff"/>) : (<react_native_1.Text style={styles.submitText}>{success ? 'Paired' : 'Pair'}</react_native_1.Text>)}
137
+ </react_native_1.Pressable>
138
+ </react_native_1.Pressable>
139
+ </react_native_1.Pressable>
140
+ </react_native_1.Modal>);
141
+ };
142
+ exports.PairDeviceModal = PairDeviceModal;
143
+ const styles = react_native_1.StyleSheet.create({
144
+ overlay: {
145
+ flex: 1,
146
+ backgroundColor: 'rgba(0,0,0,0.55)',
147
+ justifyContent: 'flex-end',
148
+ },
149
+ card: {
150
+ backgroundColor: '#141422',
151
+ borderTopLeftRadius: 22,
152
+ borderTopRightRadius: 22,
153
+ padding: 24,
154
+ paddingBottom: 36,
155
+ gap: 14,
156
+ },
157
+ header: {
158
+ flexDirection: 'row',
159
+ alignItems: 'center',
160
+ justifyContent: 'space-between',
161
+ },
162
+ title: { fontSize: 20, fontWeight: '700', color: '#fff' },
163
+ closeBtn: {
164
+ width: 36,
165
+ height: 36,
166
+ borderRadius: 18,
167
+ alignItems: 'center',
168
+ justifyContent: 'center',
169
+ backgroundColor: 'rgba(255,255,255,0.08)',
170
+ },
171
+ closeIcon: { color: '#fff', fontSize: 22, lineHeight: 24 },
172
+ deviceName: { fontSize: 15, fontWeight: '600', color: '#c7c8ff' },
173
+ body: { fontSize: 13, color: '#9ca3af', lineHeight: 18 },
174
+ codeInput: {
175
+ backgroundColor: 'rgba(255,255,255,0.06)',
176
+ borderWidth: 1,
177
+ borderColor: 'rgba(255,255,255,0.14)',
178
+ borderRadius: 12,
179
+ paddingHorizontal: 16,
180
+ paddingVertical: 16,
181
+ fontSize: 22,
182
+ fontWeight: '700',
183
+ letterSpacing: 4,
184
+ textAlign: 'center',
185
+ color: '#fff',
186
+ fontFamily: react_native_1.Platform.OS === 'ios' ? 'Menlo' : 'monospace',
187
+ },
188
+ error: { color: '#ef4444', fontSize: 13 },
189
+ success: { color: '#22c55e', fontSize: 14, fontWeight: '600' },
190
+ submit: {
191
+ backgroundColor: '#818cf8',
192
+ borderRadius: 12,
193
+ paddingVertical: 14,
194
+ alignItems: 'center',
195
+ },
196
+ submitDisabled: { opacity: 0.35 },
197
+ submitText: { color: '#fff', fontSize: 16, fontWeight: '700' },
198
+ });
@@ -0,0 +1,79 @@
1
+ export interface CoreDevice {
2
+ /** Convex-issued device id. */
3
+ deviceId: string;
4
+ /** Display name — usually hostname. */
5
+ name: string;
6
+ /** OS family — "darwin", "linux", "windows". */
7
+ platform: string;
8
+ isOnline: boolean;
9
+ needsAuth: boolean;
10
+ runnerDown: boolean;
11
+ /** Unix ms of the latest heartbeat the agent sent to Convex. */
12
+ lastHeartbeat: number;
13
+ isGuest: boolean;
14
+ hostName?: string;
15
+ hostEmail?: string;
16
+ accessScope?: 'owner' | 'shared-scoped' | 'shared-legacy';
17
+ /** Primary LAN IP (or tunnel host) the agent advertised. */
18
+ quicHost: string;
19
+ quicPort: number;
20
+ /** HTTP port the agent listens on (usually 18080). */
21
+ httpPort?: number;
22
+ publicKey?: string;
23
+ /** Stable hardware identifier — dedup key for re-pair events. */
24
+ hwid?: string;
25
+ /**
26
+ * Every LAN IP the agent reported in its last heartbeat. Used by
27
+ * Discovery.raceProbe to try all interfaces in parallel.
28
+ */
29
+ localIps?: string[];
30
+ }
31
+ export declare function deviceIdentityKey(d: CoreDevice): string;
32
+ export declare function deviceAliasKey(d: CoreDevice): string | null;
33
+ export declare function deviceEndpointKey(d: CoreDevice): string | null;
34
+ export declare function mergeDeviceEntries(a: CoreDevice, b: CoreDevice): CoreDevice;
35
+ /**
36
+ * Collapse duplicate Convex rows so each physical machine appears
37
+ * exactly once. Three passes — identity key → alias key → endpoint key.
38
+ * Safe on empty lists; idempotent on already-deduped input.
39
+ */
40
+ export declare function collapseDevices(devices: CoreDevice[]): CoreDevice[];
41
+ /**
42
+ * "Fresh" matches the mobile app: online + heartbeat < 90 s. Clients
43
+ * read Convex's `isOnline` first (backend already applies its own 90 s
44
+ * gate from the server clock), then use this helper when they need the
45
+ * phone-side freshness opinion too — e.g. for auto-connect picks.
46
+ */
47
+ export declare function isDeviceFresh(d: CoreDevice, now?: number): boolean;
48
+ /**
49
+ * Choose the best candidate for an auto-connect attempt. Preference:
50
+ * 1. explicit `preferredDeviceId` that's still fresh
51
+ * 2. fresh (online + recent heartbeat) + has a quicHost
52
+ * 3. online + has a quicHost
53
+ * 4. first with a quicHost
54
+ */
55
+ export declare function pickTargetDevice(devices: CoreDevice[], preferredDeviceId?: string): CoreDevice | null;
56
+ /**
57
+ * Build the set of `/health` candidate URLs for a target device —
58
+ * `quicHost` plus every LAN IP the agent reported in `localIps`,
59
+ * uniqued and formatted. This is the thing that makes direct LAN
60
+ * reloads "just work" on multi-homed hosts (en0 + utun tailscale +
61
+ * docker0 etc.) — the mobile app races them in parallel via
62
+ * Promise.any.
63
+ */
64
+ export declare function buildProbeCandidates(target: CoreDevice, defaultHttpPort?: number): string[];
65
+ export interface ProbeResult {
66
+ url: string;
67
+ hostname?: string;
68
+ version?: string;
69
+ latency?: number;
70
+ }
71
+ /**
72
+ * Race `/health` probes across N URLs. First 200 wins; everything else
73
+ * is abandoned. Older Hermes doesn't have Promise.any, so we hand-roll
74
+ * the same semantic.
75
+ */
76
+ export declare function raceHealthProbes(urls: string[], opts?: {
77
+ timeoutMs?: number;
78
+ headers?: Record<string, string>;
79
+ }): Promise<ProbeResult | null>;