yaver-feedback-react-native 0.9.0 → 0.9.3
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 +102 -1
- package/app.plugin.js +126 -9
- package/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/BlackBox.js +9 -1
- package/dist/DeployPanel.js +65 -0
- package/dist/Discovery.js +13 -8
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +505 -65
- package/dist/LoginScreen.d.ts +1 -5
- package/dist/LoginScreen.js +2 -6
- package/dist/MachinePickerScreen.d.ts +1 -3
- package/dist/MachinePickerScreen.js +6 -18
- package/dist/P2PClient.d.ts +178 -2
- package/dist/P2PClient.js +493 -3
- package/dist/P2PDogfoodDriver.d.ts +12 -0
- package/dist/P2PDogfoodDriver.js +118 -0
- package/dist/PairDeviceModal.d.ts +2 -3
- package/dist/ShakeDetector.js +2 -0
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +132 -0
- package/dist/YaverFeedback.js +633 -65
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
- package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
- package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
- package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
- package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
- package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- package/dist/__tests__/P2PClient.test.js +2 -2
- package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
- package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
- package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
- package/dist/__tests__/ReportIdentity.test.js +180 -0
- package/dist/__tests__/SDKToken.test.js +1 -1
- package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
- package/dist/__tests__/ShakeToggle.test.js +121 -0
- package/dist/__tests__/YaverFeedback.test.js +13 -3
- package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
- package/dist/__tests__/deviceDogfood.test.js +86 -0
- package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
- package/dist/__tests__/dogfoodPolicy.test.js +33 -0
- package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
- package/dist/__tests__/pickTargetDevice.test.js +89 -0
- package/dist/__tests__/reloadActions.test.d.ts +1 -0
- package/dist/__tests__/reloadActions.test.js +129 -0
- package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
- package/dist/__tests__/reloadActionsParity.test.js +42 -0
- package/dist/__tests__/types.test.js +5 -5
- package/dist/_core/ansi.d.ts +117 -0
- package/dist/_core/ansi.js +468 -0
- package/dist/_core/ansi.test.d.ts +1 -0
- package/dist/_core/ansi.test.js +225 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
- package/dist/_core/buildFeedbackPrompt.js +18 -72
- package/dist/_core/constants.d.ts +19 -6
- package/dist/_core/constants.js +20 -7
- package/dist/_core/device.d.ts +1 -5
- package/dist/_core/device.js +5 -14
- package/dist/_core/endpoints.d.ts +0 -7
- package/dist/_core/endpoints.js +0 -7
- package/dist/_core/index.d.ts +4 -0
- package/dist/_core/index.js +4 -0
- package/dist/_core/remoteless.d.ts +44 -0
- package/dist/_core/remoteless.js +75 -0
- package/dist/_core/trace.d.ts +47 -0
- package/dist/_core/trace.js +38 -0
- package/dist/_core/trace.test.d.ts +1 -0
- package/dist/_core/trace.test.js +60 -0
- package/dist/auth.d.ts +3 -60
- package/dist/auth.js +6 -88
- package/dist/deviceDogfood.d.ts +53 -0
- package/dist/deviceDogfood.js +137 -0
- package/dist/dogfoodPolicy.d.ts +25 -0
- package/dist/dogfoodPolicy.js +24 -0
- package/dist/index.d.ts +17 -4
- package/dist/index.js +33 -7
- package/dist/reloadActions.d.ts +88 -0
- package/dist/reloadActions.js +200 -0
- package/dist/storeShots.d.ts +67 -0
- package/dist/storeShots.js +137 -0
- package/dist/types.d.ts +208 -8
- package/package.json +13 -4
- package/src/AuthOverlay.tsx +20 -106
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -8
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +619 -79
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +580 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/ShakeDetector.ts +1 -0
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +631 -68
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
- package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
- package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
- package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
- package/src/__tests__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PClient.test.ts +2 -2
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +212 -0
- package/src/__tests__/SDKToken.test.ts +1 -1
- package/src/__tests__/ShakeToggle.test.ts +153 -0
- package/src/__tests__/YaverFeedback.test.ts +15 -3
- package/src/__tests__/deviceDogfood.test.ts +77 -0
- package/src/__tests__/dogfoodPolicy.test.ts +34 -0
- package/src/__tests__/pickTargetDevice.test.ts +101 -0
- package/src/__tests__/reloadActions.test.ts +171 -0
- package/src/__tests__/reloadActionsParity.test.ts +49 -0
- package/src/__tests__/types.test.ts +5 -5
- package/src/_core/ansi.test.ts +250 -0
- package/src/_core/ansi.ts +475 -0
- package/src/_core/buildFeedbackPrompt.ts +18 -95
- package/src/_core/constants.ts +20 -6
- package/src/_core/device.ts +5 -16
- package/src/_core/endpoints.ts +0 -7
- package/src/_core/index.ts +4 -0
- package/src/_core/remoteless.ts +110 -0
- package/src/_core/trace.test.ts +58 -0
- package/src/_core/trace.ts +75 -0
- package/src/auth.ts +7 -156
- package/src/deviceDogfood.ts +171 -0
- package/src/dogfoodPolicy.ts +40 -0
- package/src/index.ts +61 -11
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +209 -8
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import nacl from 'tweetnacl';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
import { DEFAULT_CONVEX_SITE_URL } from './auth';
|
|
4
|
+
|
|
5
|
+
type SecureStoreLike = {
|
|
6
|
+
getItemAsync(key: string): Promise<string | null>;
|
|
7
|
+
setItemAsync(key: string, value: string): Promise<void>;
|
|
8
|
+
deleteItemAsync(key: string): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type DeviceDogfoodState = 'unregistered' | 'pending' | 'active' | 'cancelled' | 'revoked' | 'superseded';
|
|
12
|
+
|
|
13
|
+
export interface DeviceDogfoodOptions {
|
|
14
|
+
/** Public identifier registered by the app owner in Yaver Settings. */
|
|
15
|
+
appId: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
backendUrl?: string;
|
|
18
|
+
/** Advanced bare-RN integration. Expo apps use SecureStore automatically. */
|
|
19
|
+
secureStore?: SecureStoreLike;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DeviceDogfoodSession {
|
|
23
|
+
active: true;
|
|
24
|
+
appId: string;
|
|
25
|
+
installationId: string;
|
|
26
|
+
token: string;
|
|
27
|
+
expiresAt: number;
|
|
28
|
+
scopes: string[];
|
|
29
|
+
projectSlug?: string;
|
|
30
|
+
targetDeviceId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface StoredIdentity {
|
|
34
|
+
installationId: string;
|
|
35
|
+
registrationSlot: string;
|
|
36
|
+
publicKey: string;
|
|
37
|
+
secretKey: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function b64(bytes: Uint8Array): string { return Buffer.from(bytes).toString('base64'); }
|
|
41
|
+
function fromB64(value: string): Uint8Array { return new Uint8Array(Buffer.from(value, 'base64')); }
|
|
42
|
+
function b64url(bytes: Uint8Array): string {
|
|
43
|
+
return Buffer.from(bytes).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function randomBytes(length: number): Uint8Array {
|
|
47
|
+
const cryptoObject = (globalThis as any)?.crypto;
|
|
48
|
+
if (cryptoObject?.getRandomValues) {
|
|
49
|
+
const out = new Uint8Array(length);
|
|
50
|
+
cryptoObject.getRandomValues(out);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return require('expo-crypto').getRandomBytes(length);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error('Secure randomness is unavailable. Install expo-crypto or provide global crypto.getRandomValues.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function defaultSecureStore(): SecureStoreLike {
|
|
61
|
+
try {
|
|
62
|
+
const store = require('expo-secure-store');
|
|
63
|
+
if (store?.getItemAsync && store?.setItemAsync) return store;
|
|
64
|
+
} catch { /* named error below */ }
|
|
65
|
+
throw new Error('Secure Dogfood identity storage is unavailable. Install expo-secure-store or pass secureStore.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function responseJSON(response: Response): Promise<any> {
|
|
69
|
+
const body = await response.json().catch(() => ({}));
|
|
70
|
+
if (!response.ok) throw new Error(body?.error || `Yaver Dogfood request failed (HTTP ${response.status})`);
|
|
71
|
+
return body;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function enrollmentMessage(appId: string, installationId: string, challenge: string): Uint8Array {
|
|
75
|
+
return new TextEncoder().encode(`yaver-dogfood-enroll-v1\n${appId}\n${installationId}\n${challenge}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sessionMessage(appId: string, installationId: string, challenge: string): Uint8Array {
|
|
79
|
+
return new TextEncoder().encode(`yaver-dogfood-session-v1\n${appId}\n${installationId}\n${challenge}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Account-free third-party Dogfood enrollment. The UUID is a public lookup
|
|
83
|
+
* handle; possession is proven by the private key retained in this app. */
|
|
84
|
+
export class YaverDeviceDogfood {
|
|
85
|
+
private readonly backendUrl: string;
|
|
86
|
+
private readonly store: SecureStoreLike;
|
|
87
|
+
private readonly storageKey: string;
|
|
88
|
+
|
|
89
|
+
constructor(private readonly options: DeviceDogfoodOptions) {
|
|
90
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{2,127}$/.test(options.appId)) throw new Error('A valid Dogfood appId is required');
|
|
91
|
+
this.backendUrl = (options.backendUrl || DEFAULT_CONVEX_SITE_URL).replace(/\/$/, '');
|
|
92
|
+
this.store = options.secureStore || defaultSecureStore();
|
|
93
|
+
this.storageKey = `yaver_feedback_dogfood_v1_${options.appId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async identity(): Promise<StoredIdentity> {
|
|
97
|
+
const raw = await this.store.getItemAsync(this.storageKey);
|
|
98
|
+
if (raw) {
|
|
99
|
+
try {
|
|
100
|
+
const saved = JSON.parse(raw) as StoredIdentity;
|
|
101
|
+
if (fromB64(saved.publicKey).length === nacl.sign.publicKeyLength && fromB64(saved.secretKey).length === nacl.sign.secretKeyLength) return saved;
|
|
102
|
+
} catch { /* rotate corrupt identity below */ }
|
|
103
|
+
}
|
|
104
|
+
const seed = randomBytes(32);
|
|
105
|
+
const pair = nacl.sign.keyPair.fromSeed(seed);
|
|
106
|
+
seed.fill(0);
|
|
107
|
+
const identity: StoredIdentity = {
|
|
108
|
+
installationId: b64url(randomBytes(18)), registrationSlot: b64url(randomBytes(18)),
|
|
109
|
+
publicKey: b64(pair.publicKey), secretKey: b64(pair.secretKey),
|
|
110
|
+
};
|
|
111
|
+
await this.store.setItemAsync(this.storageKey, JSON.stringify(identity));
|
|
112
|
+
return identity;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async enrollmentInfo(): Promise<{ appId: string; installationId: string; registrationSlot: string; publicKey: string }> {
|
|
116
|
+
const identity = await this.identity();
|
|
117
|
+
return { appId: this.options.appId, installationId: identity.installationId, registrationSlot: identity.registrationSlot, publicKey: identity.publicKey };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async enroll(platform = 'unknown'): Promise<{ status: DeviceDogfoodState; installationId: string }> {
|
|
121
|
+
const identity = await this.identity();
|
|
122
|
+
const started = await responseJSON(await fetch(`${this.backendUrl}/dogfood/enroll/start`, {
|
|
123
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId, registrationSlot: identity.registrationSlot, publicKey: identity.publicKey, platform, label: this.options.label }),
|
|
125
|
+
}));
|
|
126
|
+
if (started.status === 'active') return { status: 'active', installationId: identity.installationId };
|
|
127
|
+
const signature = b64(nacl.sign.detached(enrollmentMessage(this.options.appId, identity.installationId, started.challenge), fromB64(identity.secretKey)));
|
|
128
|
+
await responseJSON(await fetch(`${this.backendUrl}/dogfood/enroll/prove`, {
|
|
129
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId, signature }),
|
|
131
|
+
}));
|
|
132
|
+
return { status: 'pending', installationId: identity.installationId };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async status(): Promise<DeviceDogfoodState> {
|
|
136
|
+
const identity = await this.identity();
|
|
137
|
+
const response = await fetch(`${this.backendUrl}/dogfood/enroll/status?appId=${encodeURIComponent(this.options.appId)}&installationId=${encodeURIComponent(identity.installationId)}`);
|
|
138
|
+
if (response.status === 404) return 'unregistered';
|
|
139
|
+
return (await responseJSON(response)).status as DeviceDogfoodState;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async session(): Promise<DeviceDogfoodSession | null> {
|
|
143
|
+
const identity = await this.identity();
|
|
144
|
+
if (await this.status() !== 'active') return null;
|
|
145
|
+
const challenge = await responseJSON(await fetch(`${this.backendUrl}/dogfood/session/challenge`, {
|
|
146
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId }),
|
|
148
|
+
}));
|
|
149
|
+
const signature = b64(nacl.sign.detached(sessionMessage(this.options.appId, identity.installationId, challenge.challenge), fromB64(identity.secretKey)));
|
|
150
|
+
const result = await responseJSON(await fetch(`${this.backendUrl}/dogfood/session`, {
|
|
151
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId, signature }),
|
|
153
|
+
}));
|
|
154
|
+
return { active: true, appId: this.options.appId, installationId: identity.installationId, token: result.token, expiresAt: result.expiresAt, scopes: result.scopes || [], projectSlug: result.projectSlug, targetDeviceId: result.targetDeviceId };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Rotate the key but keep the logical slot. Approval supersedes the prior
|
|
158
|
+
* generation, making its already-issued sessions fail immediately. */
|
|
159
|
+
async reRegister(platform = 'unknown'): Promise<{ status: DeviceDogfoodState; installationId: string }> {
|
|
160
|
+
const previous = await this.identity();
|
|
161
|
+
const seed = randomBytes(32);
|
|
162
|
+
const pair = nacl.sign.keyPair.fromSeed(seed);
|
|
163
|
+
seed.fill(0);
|
|
164
|
+
const next: StoredIdentity = {
|
|
165
|
+
installationId: b64url(randomBytes(18)), registrationSlot: previous.registrationSlot,
|
|
166
|
+
publicKey: b64(pair.publicKey), secretKey: b64(pair.secretKey),
|
|
167
|
+
};
|
|
168
|
+
await this.store.setItemAsync(this.storageKey, JSON.stringify(next));
|
|
169
|
+
return this.enroll(platform);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface SDKDogfoodConfig {
|
|
2
|
+
/** Explicit opt-in. Omitted/false keeps the normal Feedback SDK. */
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
/** App-account IDs allowed to enter Dogfood. Public identifiers, not secrets. */
|
|
5
|
+
accountIds?: string[];
|
|
6
|
+
/** The third-party app's currently authenticated account ID. */
|
|
7
|
+
currentAccountId?: string;
|
|
8
|
+
/** Account-free installation identity resolved by YaverDeviceDogfood. */
|
|
9
|
+
appId?: string;
|
|
10
|
+
installationId?: string;
|
|
11
|
+
installationStatus?: 'pending' | 'active' | 'cancelled' | 'revoked' | 'superseded';
|
|
12
|
+
/** Called after the user confirms Exit Dogfood mode. */
|
|
13
|
+
onExit?: () => void | Promise<void>;
|
|
14
|
+
/** Optional app label shown beside Dogfood mode. */
|
|
15
|
+
label?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SDKDogfoodStatus {
|
|
19
|
+
active: boolean;
|
|
20
|
+
code: 'SDK_DOGFOOD_ACTIVE' | 'SDK_DOGFOOD_DISABLED' | 'SDK_DOGFOOD_ACCOUNT_REQUIRED' | 'SDK_DOGFOOD_ACCOUNT_NOT_ALLOWED' | 'SDK_DOGFOOD_INSTALLATION_REQUIRED' | 'SDK_DOGFOOD_INSTALLATION_NOT_ACTIVE';
|
|
21
|
+
accountId?: string;
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Pure, fail-closed account gate shared by init, the Y badge and tests.
|
|
26
|
+
* This controls SDK UX only; every task/reload still requires Yaver auth. */
|
|
27
|
+
export function resolveSDKDogfood(config?: SDKDogfoodConfig | null): SDKDogfoodStatus {
|
|
28
|
+
const label = String(config?.label || 'Dogfood').trim() || 'Dogfood';
|
|
29
|
+
if (config?.enabled !== true) return { active: false, code: 'SDK_DOGFOOD_DISABLED', label };
|
|
30
|
+
if (config.appId) {
|
|
31
|
+
if (!config.installationId) return { active: false, code: 'SDK_DOGFOOD_INSTALLATION_REQUIRED', label };
|
|
32
|
+
if (config.installationStatus !== 'active') return { active: false, code: 'SDK_DOGFOOD_INSTALLATION_NOT_ACTIVE', label };
|
|
33
|
+
return { active: true, code: 'SDK_DOGFOOD_ACTIVE', accountId: config.installationId, label };
|
|
34
|
+
}
|
|
35
|
+
const accountId = String(config.currentAccountId || '').trim();
|
|
36
|
+
if (!accountId) return { active: false, code: 'SDK_DOGFOOD_ACCOUNT_REQUIRED', label };
|
|
37
|
+
const allowed = new Set((config.accountIds || []).map((id) => String(id).trim()).filter(Boolean));
|
|
38
|
+
if (!allowed.has(accountId)) return { active: false, code: 'SDK_DOGFOOD_ACCOUNT_NOT_ALLOWED', accountId, label };
|
|
39
|
+
return { active: true, code: 'SDK_DOGFOOD_ACTIVE', accountId, label };
|
|
40
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -29,24 +29,83 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
export { YaverFeedback } from './YaverFeedback';
|
|
32
|
+
export type { DogfoodOnboardingOptions } from './YaverFeedback';
|
|
33
|
+
export { captureStoreScreenshots } from './storeShots';
|
|
34
|
+
export type {
|
|
35
|
+
CaptureStoreScreenshotsOptions,
|
|
36
|
+
CaptureStoreScreenshotsResult,
|
|
37
|
+
StoreShotFrame,
|
|
38
|
+
} from './storeShots';
|
|
32
39
|
export { BlackBox } from './BlackBox';
|
|
33
40
|
export { YaverUpdates } from './YaverUpdates';
|
|
34
41
|
export type { YaverUpdatesConfig, PendingUpdate } from './YaverUpdates';
|
|
35
42
|
export { initExpo } from './expo';
|
|
36
43
|
export { YaverDiscovery } from './Discovery';
|
|
37
44
|
export { P2PClient } from './P2PClient';
|
|
45
|
+
export type {
|
|
46
|
+
DogfoodDevEvent,
|
|
47
|
+
DogfoodDevServerStatus,
|
|
48
|
+
DogfoodRemoteRuntimeCapabilities,
|
|
49
|
+
DogfoodRemoteRuntimeSession,
|
|
50
|
+
DogfoodRemoteRuntimeTarget,
|
|
51
|
+
} from './P2PClient';
|
|
52
|
+
export { createP2PDogfoodDriver, type P2PDogfoodDriverOptions } from './P2PDogfoodDriver';
|
|
53
|
+
export {
|
|
54
|
+
reloadActions,
|
|
55
|
+
reloadRequest,
|
|
56
|
+
reloadFrameworkFamily,
|
|
57
|
+
describeReloadFailure,
|
|
58
|
+
RELOAD_PATH,
|
|
59
|
+
RELOAD_APP_PATH,
|
|
60
|
+
} from './reloadActions';
|
|
61
|
+
export type {
|
|
62
|
+
ReloadAction,
|
|
63
|
+
ReloadActionId,
|
|
64
|
+
ReloadActionsOptions,
|
|
65
|
+
ReloadWireMode,
|
|
66
|
+
DevServerSnapshot,
|
|
67
|
+
} from './reloadActions';
|
|
38
68
|
export { YaverConnectionScreen } from './ConnectionScreen';
|
|
39
69
|
export { YaverLoginScreen } from './LoginScreen';
|
|
40
70
|
export type { YaverLoginScreenProps } from './LoginScreen';
|
|
41
71
|
export { YaverMachinePickerScreen } from './MachinePickerScreen';
|
|
42
72
|
export type { YaverMachinePickerProps } from './MachinePickerScreen';
|
|
43
|
-
export { YaverGuestOnboardingScreen } from './GuestOnboardingScreen';
|
|
44
|
-
export type { YaverGuestOnboardingScreenProps } from './GuestOnboardingScreen';
|
|
45
73
|
export { PairDeviceModal } from './PairDeviceModal';
|
|
46
74
|
export type { PairDeviceModalProps } from './PairDeviceModal';
|
|
47
75
|
export { AuthOverlay } from './AuthOverlay';
|
|
48
76
|
export { ShakeDetector } from './ShakeDetector';
|
|
49
77
|
export { FloatingButton } from './FloatingButton';
|
|
78
|
+
// The polite "you're inside Yaver" mark. Rendered automatically by the SDK's
|
|
79
|
+
// overlay host when config.modeBadge !== false; exported so an app that manages
|
|
80
|
+
// its own overlay tree can place it itself.
|
|
81
|
+
export { YaverModeBadge, hideYaverModeBadge, showYaverModeBadge, isYaverModeBadgeHidden } from './YaverModeBadge';
|
|
82
|
+
export type { YaverModeBadgeProps } from './YaverModeBadge';
|
|
83
|
+
export { resolveSDKDogfood } from './dogfoodPolicy';
|
|
84
|
+
export type { SDKDogfoodConfig, SDKDogfoodStatus } from './dogfoodPolicy';
|
|
85
|
+
export { YaverDeviceDogfood } from './deviceDogfood';
|
|
86
|
+
export type { DeviceDogfoodOptions, DeviceDogfoodSession, DeviceDogfoodState } from './deviceDogfood';
|
|
87
|
+
export {
|
|
88
|
+
DogfoodController,
|
|
89
|
+
DogfoodRuntimeError,
|
|
90
|
+
defaultDogfoodLane,
|
|
91
|
+
dogfoodLaneOptions,
|
|
92
|
+
dogfoodLogLinesFromDevEvent,
|
|
93
|
+
runtimeLogLinesFromDevEvent,
|
|
94
|
+
validateDogfoodProject,
|
|
95
|
+
} from './DogfoodRuntime';
|
|
96
|
+
export type {
|
|
97
|
+
DogfoodControllerOptions,
|
|
98
|
+
DogfoodDriver,
|
|
99
|
+
DogfoodFailure,
|
|
100
|
+
DogfoodLane,
|
|
101
|
+
DogfoodLaneOption,
|
|
102
|
+
DogfoodLogLine,
|
|
103
|
+
DogfoodPhase,
|
|
104
|
+
DogfoodProject,
|
|
105
|
+
DogfoodResult,
|
|
106
|
+
DogfoodRunContext,
|
|
107
|
+
DogfoodSnapshot,
|
|
108
|
+
} from './DogfoodRuntime';
|
|
50
109
|
export { FeedbackModal } from './FeedbackModal';
|
|
51
110
|
export { QuickActionIcon } from './QuickActionIcon';
|
|
52
111
|
export type { QuickActionIconProps } from './QuickActionIcon';
|
|
@@ -74,10 +133,6 @@ export {
|
|
|
74
133
|
signupWithEmail,
|
|
75
134
|
loginWithEmail,
|
|
76
135
|
listReachableDevices,
|
|
77
|
-
fetchGuestHosts,
|
|
78
|
-
findInviteByCode,
|
|
79
|
-
acceptGuestByCode,
|
|
80
|
-
acceptGuestInvitation,
|
|
81
136
|
DEFAULT_CONVEX_SITE_URL,
|
|
82
137
|
DEFAULT_WEB_BASE_URL,
|
|
83
138
|
DEFAULT_OAUTH_REDIRECT,
|
|
@@ -87,11 +142,6 @@ export type {
|
|
|
87
142
|
User,
|
|
88
143
|
RemoteDevice,
|
|
89
144
|
DeviceList,
|
|
90
|
-
GuestInvitation,
|
|
91
|
-
ActiveGuestHost,
|
|
92
|
-
GuestHostsResponse,
|
|
93
|
-
InvitationHostDevice,
|
|
94
|
-
InvitationPreview,
|
|
95
145
|
} from './auth';
|
|
96
146
|
export {
|
|
97
147
|
captureScreenshot,
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// ─── Reload actions — the ONE decision seam every Yaver feedback SDK mirrors ──
|
|
2
|
+
//
|
|
3
|
+
// The in-app overlay offers the developer a way to reload the app they are
|
|
4
|
+
// looking at, without leaving it. This file answers the three questions that
|
|
5
|
+
// have to be answered IDENTICALLY on every stack, so that a bug fixed in one
|
|
6
|
+
// SDK is not still shipping in the other five:
|
|
7
|
+
//
|
|
8
|
+
// 1. WHICH actions may be shown at all (production build ⇒ none, ever)?
|
|
9
|
+
// 2. WHICH request does each action make (path + body)?
|
|
10
|
+
// 3. WHEN a reload fails, WHAT do we tell the user?
|
|
11
|
+
//
|
|
12
|
+
// It is deliberately PURE — no fetch, no DOM, no globals. That is what makes
|
|
13
|
+
// it unit-testable, and the unit test is the guard: a production build must
|
|
14
|
+
// yield an empty action list. Break `isDevBuild` and the test fails.
|
|
15
|
+
//
|
|
16
|
+
// ── Wire contract (desktop/agent/devserver_http.go) ──────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// POST /dev/reload { "mode": "fast" | "full" }
|
|
19
|
+
// fast — the framework's cheapest refresh. Flutter stdin "r" (hot
|
|
20
|
+
// reload, keeps state). Metro/Expo fast refresh. Vite/Next HMR.
|
|
21
|
+
// full — framework-level restart. Flutter stdin "R" (hot RESTART,
|
|
22
|
+
// resets state). Web lane additionally forces a warm re-export.
|
|
23
|
+
// NEVER a cache clear or a process cold-start.
|
|
24
|
+
// Absent/unknown mode is normalised to "fast" by the agent, so an old
|
|
25
|
+
// client keeps its exact old behaviour.
|
|
26
|
+
//
|
|
27
|
+
// POST /dev/reload-app { "mode": "bundle", ...identity }
|
|
28
|
+
// Rebuild the Hermes bytecode bundle on the agent and push it over the
|
|
29
|
+
// BlackBox channel. React Native only, and the ONLY action that still
|
|
30
|
+
// works when no dev server is running — which is why it stays enabled
|
|
31
|
+
// in exactly that case.
|
|
32
|
+
//
|
|
33
|
+
// ── Auth ─────────────────────────────────────────────────────────────────────
|
|
34
|
+
//
|
|
35
|
+
// None of this needs a new secret. `/dev/reload` and `/dev/reload-app` are
|
|
36
|
+
// registered under `authSDK` (desktop/agent/httpserver.go), the same
|
|
37
|
+
// middleware that already admits the bearer this SDK sends with its feedback
|
|
38
|
+
// POST. A scope-limited SDK token needs the existing `reload` scope,
|
|
39
|
+
// whose path list already contains both routes — no widening, no new gate.
|
|
40
|
+
|
|
41
|
+
/** Stable identifier for each action the overlay can render. */
|
|
42
|
+
export type ReloadActionId = 'hot' | 'full' | 'rebuild';
|
|
43
|
+
|
|
44
|
+
/** Wire value of the `mode` field (or `bundle` for /dev/reload-app). */
|
|
45
|
+
export type ReloadWireMode = 'fast' | 'full' | 'bundle';
|
|
46
|
+
|
|
47
|
+
/** The part of GET /dev/status this decision depends on. */
|
|
48
|
+
export interface DevServerSnapshot {
|
|
49
|
+
/** Is a dev server process alive on the machine? */
|
|
50
|
+
running: boolean;
|
|
51
|
+
/** Is it still compiling? A reload now would race the build. */
|
|
52
|
+
building?: boolean;
|
|
53
|
+
/** Agent's framework name: expo | react-native | flutter | vite | nextjs. */
|
|
54
|
+
framework?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ReloadActionsOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Is the HOST APP a development build?
|
|
60
|
+
*
|
|
61
|
+
* There is no honest default here, so there is no default: every stack has
|
|
62
|
+
* its own signal (`__DEV__`, `kDebugMode`, `Debug.isDebugBuild`,
|
|
63
|
+
* `FLAG_DEBUGGABLE`, `#if DEBUG`) and the caller passes it. False means the
|
|
64
|
+
* list is EMPTY — a shipped app never gets a reload button.
|
|
65
|
+
*/
|
|
66
|
+
isDevBuild: boolean;
|
|
67
|
+
/** Do we have an agent connection / selected machine at all? */
|
|
68
|
+
connected: boolean;
|
|
69
|
+
/** Human label for the machine, used inside the disabled reason. */
|
|
70
|
+
machineLabel?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Offer the React Native bundle rebuild as a third action. Only the RN SDK
|
|
73
|
+
* can act on the pushed bundle, so only the RN SDK sets this.
|
|
74
|
+
*/
|
|
75
|
+
includeRebuild?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ReloadAction {
|
|
79
|
+
id: ReloadActionId;
|
|
80
|
+
/** Button label — stack-idiomatic wording lives here, not at the call site. */
|
|
81
|
+
label: string;
|
|
82
|
+
/** One line under/next to the button explaining what it actually does. */
|
|
83
|
+
hint: string;
|
|
84
|
+
mode: ReloadWireMode;
|
|
85
|
+
/** Agent path this action POSTs to. */
|
|
86
|
+
path: string;
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Present exactly when `enabled` is false. Names the specific blocker and
|
|
90
|
+
* the fix — never "unavailable".
|
|
91
|
+
*/
|
|
92
|
+
disabledReason?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type FrameworkFamily = 'flutter' | 'react-native' | 'web' | 'unknown';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Map the agent's framework name onto the family whose reload vocabulary we
|
|
99
|
+
* borrow. Unknown frameworks still get generic actions: the agent is the
|
|
100
|
+
* authority on what it can do, and refusing to offer a reload because we did
|
|
101
|
+
* not recognise a name would be us inventing a limit the product does not have.
|
|
102
|
+
*/
|
|
103
|
+
export function reloadFrameworkFamily(framework?: string): FrameworkFamily {
|
|
104
|
+
const f = (framework || '').trim().toLowerCase();
|
|
105
|
+
if (!f) return 'unknown';
|
|
106
|
+
if (f.indexOf('flutter') >= 0) return 'flutter';
|
|
107
|
+
if (f === 'expo' || f.indexOf('react-native') >= 0 || f.indexOf('metro') >= 0) {
|
|
108
|
+
return 'react-native';
|
|
109
|
+
}
|
|
110
|
+
if (f === 'vite' || f === 'next' || f === 'nextjs' || f === 'web' || f === 'webpack') {
|
|
111
|
+
return 'web';
|
|
112
|
+
}
|
|
113
|
+
return 'unknown';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const LABELS: Record<FrameworkFamily, { hot: [string, string]; full: [string, string] }> = {
|
|
117
|
+
flutter: {
|
|
118
|
+
hot: ['Hot Reload', 'Flutter hot reload (r) — keeps the current app state.'],
|
|
119
|
+
full: ['Hot Restart', 'Flutter hot restart (R) — restarts the app and resets state.'],
|
|
120
|
+
},
|
|
121
|
+
'react-native': {
|
|
122
|
+
hot: ['Hot Reload', 'Fast Refresh through Metro — keeps component state.'],
|
|
123
|
+
full: ['Full Reload', 'Reloads the whole JS bundle and resets state.'],
|
|
124
|
+
},
|
|
125
|
+
web: {
|
|
126
|
+
hot: ['Hot Reload', 'Hot module replacement through the dev server.'],
|
|
127
|
+
full: ['Full Reload', 'Re-exports the bundle and reloads the page.'],
|
|
128
|
+
},
|
|
129
|
+
unknown: {
|
|
130
|
+
hot: ['Hot Reload', "The dev server's cheapest refresh."],
|
|
131
|
+
full: ['Full Reload', 'Framework-level restart of the running app.'],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Path constants — mirrored from AGENT_ENDPOINTS so a typo cannot diverge. */
|
|
136
|
+
export const RELOAD_PATH = '/dev/reload';
|
|
137
|
+
export const RELOAD_APP_PATH = '/dev/reload-app';
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The whole decision, in one pure function.
|
|
141
|
+
*
|
|
142
|
+
* Returns the ordered list the overlay should render. An EMPTY list means
|
|
143
|
+
* "render no reload UI at all" — that is the production-build answer, and it
|
|
144
|
+
* is deliberately indistinguishable from "this SDK has no reload feature",
|
|
145
|
+
* because to a shipped app it doesn't.
|
|
146
|
+
*
|
|
147
|
+
* A NON-empty list may still contain disabled entries: showing a greyed
|
|
148
|
+
* "Hot Reload — no dev server is running on primary" teaches the user what
|
|
149
|
+
* to fix. Hiding it teaches them nothing.
|
|
150
|
+
*/
|
|
151
|
+
export function reloadActions(
|
|
152
|
+
snapshot: DevServerSnapshot | null | undefined,
|
|
153
|
+
opts: ReloadActionsOptions,
|
|
154
|
+
): ReloadAction[] {
|
|
155
|
+
// 1. Production build — never, under any circumstance.
|
|
156
|
+
if (!opts.isDevBuild) return [];
|
|
157
|
+
|
|
158
|
+
const snap: DevServerSnapshot = snapshot || { running: false };
|
|
159
|
+
const family = reloadFrameworkFamily(snap.framework);
|
|
160
|
+
const labels = LABELS[family];
|
|
161
|
+
const machine = (opts.machineLabel || '').trim() || 'the selected machine';
|
|
162
|
+
|
|
163
|
+
let blocked: string | undefined;
|
|
164
|
+
if (!opts.connected) {
|
|
165
|
+
blocked = 'Not connected to a machine yet — pick one first.';
|
|
166
|
+
} else if (snap.building) {
|
|
167
|
+
blocked = 'The dev server is still building — reload works once it finishes.';
|
|
168
|
+
} else if (!snap.running) {
|
|
169
|
+
blocked =
|
|
170
|
+
`No dev server is running on ${machine}. ` +
|
|
171
|
+
'Start one from the Yaver app, or run `yaver dev start` there.';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const actions: ReloadAction[] = [
|
|
175
|
+
{
|
|
176
|
+
id: 'hot',
|
|
177
|
+
label: labels.hot[0],
|
|
178
|
+
hint: labels.hot[1],
|
|
179
|
+
mode: 'fast',
|
|
180
|
+
path: RELOAD_PATH,
|
|
181
|
+
enabled: !blocked,
|
|
182
|
+
disabledReason: blocked,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'full',
|
|
186
|
+
label: labels.full[0],
|
|
187
|
+
hint: labels.full[1],
|
|
188
|
+
mode: 'full',
|
|
189
|
+
path: RELOAD_PATH,
|
|
190
|
+
enabled: !blocked,
|
|
191
|
+
disabledReason: blocked,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
if (opts.includeRebuild) {
|
|
196
|
+
// Deliberately NOT gated on `running`: rebuilding the bundle is exactly
|
|
197
|
+
// what you do when Metro is not up. It is gated on `connected`, because
|
|
198
|
+
// without a machine there is nothing to rebuild on.
|
|
199
|
+
const rebuildBlocked = opts.connected
|
|
200
|
+
? undefined
|
|
201
|
+
: 'Not connected to a machine yet — pick one first.';
|
|
202
|
+
actions.push({
|
|
203
|
+
id: 'rebuild',
|
|
204
|
+
label: 'Rebuild Bundle',
|
|
205
|
+
hint: 'Recompiles the Hermes bundle on the machine. Works with no dev server.',
|
|
206
|
+
mode: 'bundle',
|
|
207
|
+
path: RELOAD_APP_PATH,
|
|
208
|
+
enabled: !rebuildBlocked,
|
|
209
|
+
disabledReason: rebuildBlocked,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return actions;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The exact request an action makes. Kept next to the labels on purpose. */
|
|
217
|
+
export function reloadRequest(action: Pick<ReloadAction, 'mode' | 'path'>): {
|
|
218
|
+
method: 'POST';
|
|
219
|
+
path: string;
|
|
220
|
+
body: Record<string, unknown>;
|
|
221
|
+
} {
|
|
222
|
+
return { method: 'POST', path: action.path, body: { mode: action.mode } };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Turn a failed reload into a sentence that names the cause AND the fix.
|
|
227
|
+
*
|
|
228
|
+
* "Reload failed" is the shape of error this codebase keeps paying whole
|
|
229
|
+
* sessions for. Every branch below exists because the raw text the agent (or
|
|
230
|
+
* Go's net stack) produces is accurate and unreadable.
|
|
231
|
+
*/
|
|
232
|
+
export function describeReloadFailure(
|
|
233
|
+
status: number,
|
|
234
|
+
body: string,
|
|
235
|
+
snapshot?: DevServerSnapshot | null,
|
|
236
|
+
): string {
|
|
237
|
+
const lower = (body || '').toLowerCase();
|
|
238
|
+
const framework = (snapshot?.framework || '').trim();
|
|
239
|
+
|
|
240
|
+
if (lower.indexOf('does not support hot reload') >= 0) {
|
|
241
|
+
const name = framework || 'This dev server';
|
|
242
|
+
return `${name} cannot hot reload. Use Rebuild Bundle, or restart the dev server.`;
|
|
243
|
+
}
|
|
244
|
+
if (
|
|
245
|
+
status === 503 ||
|
|
246
|
+
lower.indexOf('no dev server') >= 0 ||
|
|
247
|
+
lower.indexOf('dev server not available') >= 0
|
|
248
|
+
) {
|
|
249
|
+
return 'No dev server is running on the machine. Start one before reloading.';
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
(lower.indexOf('connection refused') >= 0 || lower.indexOf('econnrefused') >= 0) &&
|
|
253
|
+
(lower.indexOf('127.0.0.1') >= 0 || lower.indexOf('localhost') >= 0)
|
|
254
|
+
) {
|
|
255
|
+
return 'The dev server is not listening on the machine. Start it with `yaver dev start`.';
|
|
256
|
+
}
|
|
257
|
+
if (status === 401 || status === 403) {
|
|
258
|
+
return 'The machine rejected this session — sign in again, or re-pair this device.';
|
|
259
|
+
}
|
|
260
|
+
if (status === 404) {
|
|
261
|
+
return (
|
|
262
|
+
'This machine’s agent has no /dev/reload route — it is too old. ' +
|
|
263
|
+
'Update it with `npm install -g yaver-cli@latest`.'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (status >= 500) {
|
|
267
|
+
return 'The agent hit an internal error while reloading. Check `yaver logs` on the machine.';
|
|
268
|
+
}
|
|
269
|
+
if (status === 0) {
|
|
270
|
+
return 'Could not reach the machine. Check that it is online and `yaver serve` is running.';
|
|
271
|
+
}
|
|
272
|
+
return `Reload failed (HTTP ${status}).`;
|
|
273
|
+
}
|