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.
Files changed (144) hide show
  1. package/README.md +102 -1
  2. package/app.plugin.js +126 -9
  3. package/dist/AuthOverlay.d.ts +1 -14
  4. package/dist/AuthOverlay.js +9 -62
  5. package/dist/BlackBox.js +9 -1
  6. package/dist/DeployPanel.js +65 -0
  7. package/dist/Discovery.js +13 -8
  8. package/dist/DogfoodRuntime.d.ts +124 -0
  9. package/dist/DogfoodRuntime.js +273 -0
  10. package/dist/FeedbackModal.js +505 -65
  11. package/dist/LoginScreen.d.ts +1 -5
  12. package/dist/LoginScreen.js +2 -6
  13. package/dist/MachinePickerScreen.d.ts +1 -3
  14. package/dist/MachinePickerScreen.js +6 -18
  15. package/dist/P2PClient.d.ts +178 -2
  16. package/dist/P2PClient.js +493 -3
  17. package/dist/P2PDogfoodDriver.d.ts +12 -0
  18. package/dist/P2PDogfoodDriver.js +118 -0
  19. package/dist/PairDeviceModal.d.ts +2 -3
  20. package/dist/ShakeDetector.js +2 -0
  21. package/dist/VibeChatScreen.d.ts +11 -1
  22. package/dist/VibeChatScreen.js +200 -41
  23. package/dist/YaverFeedback.d.ts +132 -0
  24. package/dist/YaverFeedback.js +633 -65
  25. package/dist/YaverModeBadge.d.ts +22 -0
  26. package/dist/YaverModeBadge.js +219 -0
  27. package/dist/__tests__/AuthDevices.test.js +1 -48
  28. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  29. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  30. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  31. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  32. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  33. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  34. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  35. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  36. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  37. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  38. package/dist/__tests__/P2PClient.test.js +2 -2
  39. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  40. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  41. package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
  42. package/dist/__tests__/ReportIdentity.test.js +180 -0
  43. package/dist/__tests__/SDKToken.test.js +1 -1
  44. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  45. package/dist/__tests__/ShakeToggle.test.js +121 -0
  46. package/dist/__tests__/YaverFeedback.test.js +13 -3
  47. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  48. package/dist/__tests__/deviceDogfood.test.js +86 -0
  49. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  50. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  51. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  52. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  53. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  54. package/dist/__tests__/reloadActions.test.js +129 -0
  55. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  56. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  57. package/dist/__tests__/types.test.js +5 -5
  58. package/dist/_core/ansi.d.ts +117 -0
  59. package/dist/_core/ansi.js +468 -0
  60. package/dist/_core/ansi.test.d.ts +1 -0
  61. package/dist/_core/ansi.test.js +225 -0
  62. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  63. package/dist/_core/buildFeedbackPrompt.js +18 -72
  64. package/dist/_core/constants.d.ts +19 -6
  65. package/dist/_core/constants.js +20 -7
  66. package/dist/_core/device.d.ts +1 -5
  67. package/dist/_core/device.js +5 -14
  68. package/dist/_core/endpoints.d.ts +0 -7
  69. package/dist/_core/endpoints.js +0 -7
  70. package/dist/_core/index.d.ts +4 -0
  71. package/dist/_core/index.js +4 -0
  72. package/dist/_core/remoteless.d.ts +44 -0
  73. package/dist/_core/remoteless.js +75 -0
  74. package/dist/_core/trace.d.ts +47 -0
  75. package/dist/_core/trace.js +38 -0
  76. package/dist/_core/trace.test.d.ts +1 -0
  77. package/dist/_core/trace.test.js +60 -0
  78. package/dist/auth.d.ts +3 -60
  79. package/dist/auth.js +6 -88
  80. package/dist/deviceDogfood.d.ts +53 -0
  81. package/dist/deviceDogfood.js +137 -0
  82. package/dist/dogfoodPolicy.d.ts +25 -0
  83. package/dist/dogfoodPolicy.js +24 -0
  84. package/dist/index.d.ts +17 -4
  85. package/dist/index.js +33 -7
  86. package/dist/reloadActions.d.ts +88 -0
  87. package/dist/reloadActions.js +200 -0
  88. package/dist/storeShots.d.ts +67 -0
  89. package/dist/storeShots.js +137 -0
  90. package/dist/types.d.ts +208 -8
  91. package/package.json +13 -4
  92. package/src/AuthOverlay.tsx +20 -106
  93. package/src/BlackBox.ts +10 -1
  94. package/src/DeployPanel.tsx +74 -0
  95. package/src/Discovery.ts +13 -8
  96. package/src/DogfoodRuntime.ts +373 -0
  97. package/src/FeedbackModal.tsx +619 -79
  98. package/src/LoginScreen.tsx +2 -22
  99. package/src/MachinePickerScreen.tsx +6 -21
  100. package/src/P2PClient.ts +580 -4
  101. package/src/P2PDogfoodDriver.ts +132 -0
  102. package/src/PairDeviceModal.tsx +2 -3
  103. package/src/ShakeDetector.ts +1 -0
  104. package/src/VibeChatScreen.tsx +232 -42
  105. package/src/YaverFeedback.ts +631 -68
  106. package/src/YaverModeBadge.tsx +234 -0
  107. package/src/__tests__/AuthDevices.test.ts +1 -52
  108. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  109. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  110. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  111. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  112. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  113. package/src/__tests__/P2PClient.test.ts +2 -2
  114. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  115. package/src/__tests__/ReportIdentity.test.ts +212 -0
  116. package/src/__tests__/SDKToken.test.ts +1 -1
  117. package/src/__tests__/ShakeToggle.test.ts +153 -0
  118. package/src/__tests__/YaverFeedback.test.ts +15 -3
  119. package/src/__tests__/deviceDogfood.test.ts +77 -0
  120. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  121. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  122. package/src/__tests__/reloadActions.test.ts +171 -0
  123. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  124. package/src/__tests__/types.test.ts +5 -5
  125. package/src/_core/ansi.test.ts +250 -0
  126. package/src/_core/ansi.ts +475 -0
  127. package/src/_core/buildFeedbackPrompt.ts +18 -95
  128. package/src/_core/constants.ts +20 -6
  129. package/src/_core/device.ts +5 -16
  130. package/src/_core/endpoints.ts +0 -7
  131. package/src/_core/index.ts +4 -0
  132. package/src/_core/remoteless.ts +110 -0
  133. package/src/_core/trace.test.ts +58 -0
  134. package/src/_core/trace.ts +75 -0
  135. package/src/auth.ts +7 -156
  136. package/src/deviceDogfood.ts +171 -0
  137. package/src/dogfoodPolicy.ts +40 -0
  138. package/src/index.ts +61 -11
  139. package/src/reloadActions.ts +273 -0
  140. package/src/storeShots.ts +189 -0
  141. package/src/types.ts +209 -8
  142. package/dist/GuestOnboardingScreen.d.ts +0 -8
  143. package/dist/GuestOnboardingScreen.js +0 -282
  144. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ // Runtime shake toggling + init() idempotency.
3
+ //
4
+ // Host apps put the feedback SDK behind a settings switch, which means init /
5
+ // enable / disable run repeatedly in one process. Two things used to break
6
+ // there:
7
+ //
8
+ // 1. There was no way to turn the shake catcher off short of
9
+ // setEnabled(false) (which also stops the flight recorder and the agent
10
+ // command channel) or a full re-init. `trigger` is only read inside
11
+ // init(). setShakeEnabled() fills that gap.
12
+ //
13
+ // 2. init() registered a BlackBox command handler and threw away the
14
+ // unsubscribe, so each re-init stacked another. Two inits meant two
15
+ // reloads per agent command; destroy() never cleared them either.
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ const YaverFeedback_1 = require("../YaverFeedback");
18
+ const BlackBox_1 = require("../BlackBox");
19
+ const mockShakeStart = jest.fn();
20
+ const mockShakeStop = jest.fn();
21
+ jest.mock('../ShakeDetector', () => ({
22
+ ShakeDetector: jest.fn().mockImplementation(() => ({
23
+ start: mockShakeStart,
24
+ stop: mockShakeStop,
25
+ })),
26
+ }));
27
+ jest.mock('react-native', () => ({
28
+ Platform: { OS: 'ios' },
29
+ DeviceEventEmitter: { emit: jest.fn(), addListener: jest.fn() },
30
+ NativeModules: {},
31
+ }));
32
+ const mockFetch = jest.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({}) }));
33
+ global.fetch = mockFetch;
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ YaverFeedback_1.YaverFeedback.destroy();
37
+ });
38
+ afterEach(() => {
39
+ YaverFeedback_1.YaverFeedback.destroy();
40
+ });
41
+ describe('setShakeEnabled', () => {
42
+ it('arms the shake listener on init when trigger is shake', () => {
43
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
44
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
45
+ });
46
+ it('disarms the listener without disabling the SDK', () => {
47
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
48
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
49
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
50
+ expect(mockShakeStop).toHaveBeenCalled();
51
+ // The point of a separate toggle: the rest of the SDK stays up.
52
+ expect(YaverFeedback_1.YaverFeedback.isEnabled()).toBe(true);
53
+ });
54
+ it('re-arms the listener', () => {
55
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
56
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
57
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
58
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
59
+ });
60
+ it('is idempotent — repeated calls do not stack detectors', () => {
61
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
62
+ mockShakeStart.mockClear();
63
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
64
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
65
+ expect(mockShakeStart).not.toHaveBeenCalled();
66
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
67
+ });
68
+ it('persists onto config so a later enable does not resurrect the listener', () => {
69
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
70
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
71
+ YaverFeedback_1.YaverFeedback.setEnabled(false);
72
+ YaverFeedback_1.YaverFeedback.setEnabled(true);
73
+ // The user said no shake. Cycling the master switch must not undo that.
74
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
75
+ expect(YaverFeedback_1.YaverFeedback.getConfig()?.disableShakeGesture).toBe(true);
76
+ });
77
+ it('does not arm the listener for a non-shake trigger', () => {
78
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'manual', agentUrl: 'http://x:18080', authToken: 't' });
79
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
80
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
81
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
82
+ });
83
+ it('no-ops before init rather than throwing', () => {
84
+ expect(() => YaverFeedback_1.YaverFeedback.setShakeEnabled(true)).not.toThrow();
85
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
86
+ });
87
+ });
88
+ describe('init() command-handler registration', () => {
89
+ it('does not stack command handlers across re-inits', () => {
90
+ const cfg = { trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' };
91
+ YaverFeedback_1.YaverFeedback.init(cfg);
92
+ YaverFeedback_1.YaverFeedback.init(cfg);
93
+ YaverFeedback_1.YaverFeedback.init(cfg);
94
+ // Three inits, one live handler. Before the fix this array grew by one
95
+ // each time, so a single agent 'reload' fired three reloads.
96
+ expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(1);
97
+ });
98
+ it('fires a reload exactly once per command after repeated inits', () => {
99
+ // The user-visible symptom the count above stands in for.
100
+ const onReload = jest.fn();
101
+ const cfg = { trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't', onReload };
102
+ YaverFeedback_1.YaverFeedback.init(cfg);
103
+ YaverFeedback_1.YaverFeedback.init(cfg);
104
+ for (const h of BlackBox_1.BlackBox.commandHandlers)
105
+ h({ command: 'reload' });
106
+ expect(onReload).toHaveBeenCalledTimes(1);
107
+ });
108
+ it('destroy() unregisters the command handler', () => {
109
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
110
+ const before = BlackBox_1.BlackBox.commandHandlers.length;
111
+ expect(before).toBeGreaterThan(0);
112
+ YaverFeedback_1.YaverFeedback.destroy();
113
+ expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(before - 1);
114
+ });
115
+ it('destroy() then init() leaves exactly one handler', () => {
116
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
117
+ YaverFeedback_1.YaverFeedback.destroy();
118
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
119
+ expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(1);
120
+ });
121
+ });
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_native_1 = require("react-native");
3
4
  const YaverFeedback_1 = require("../YaverFeedback");
4
5
  // Mock react-native: DeviceEventEmitter for event dispatch + Platform so
5
6
  // ShakeDetector.start() can branch on iOS without hitting a real RN runtime.
@@ -33,13 +34,10 @@ jest.mock('../auth', () => ({
33
34
  needsAuth: false,
34
35
  runnerDown: false,
35
36
  lastHeartbeat: Date.now(),
36
- isGuest: false,
37
- accessScope: 'owner',
38
37
  quicHost: '127.0.0.1',
39
38
  quicPort: 18080,
40
39
  },
41
40
  ],
42
- shared: [],
43
41
  })),
44
42
  DEFAULT_CONVEX_SITE_URL: 'https://example.convex.site',
45
43
  }));
@@ -51,6 +49,18 @@ beforeEach(() => {
51
49
  jest.clearAllMocks();
52
50
  });
53
51
  describe('YaverFeedback', () => {
52
+ describe('Dogfood onboarding', () => {
53
+ it('starts with Yaver OAuth when the host has no session', () => {
54
+ YaverFeedback_1.YaverFeedback.init({ enabled: true });
55
+ YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app', label: 'Example' });
56
+ expect(react_native_1.DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startLogin');
57
+ });
58
+ it('asks for a machine after an existing OAuth session', () => {
59
+ YaverFeedback_1.YaverFeedback.init({ enabled: true, authToken: 'owner-token' });
60
+ YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app', label: 'Example' });
61
+ expect(react_native_1.DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startMachinePicker');
62
+ });
63
+ });
54
64
  describe('init()', () => {
55
65
  it('sets config correctly with defaults', () => {
56
66
  YaverFeedback_1.YaverFeedback.init({
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
7
+ const buffer_1 = require("buffer");
8
+ const crypto_1 = require("crypto");
9
+ const deviceDogfood_1 = require("../deviceDogfood");
10
+ class MemorySecureStore {
11
+ constructor() {
12
+ this.values = new Map();
13
+ }
14
+ async getItemAsync(key) { return this.values.get(key) ?? null; }
15
+ async setItemAsync(key, value) { this.values.set(key, value); }
16
+ async deleteItemAsync(key) { this.values.delete(key); }
17
+ }
18
+ function response(body, status = 200) {
19
+ return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
20
+ }
21
+ describe('YaverDeviceDogfood', () => {
22
+ beforeAll(() => {
23
+ if (!globalThis.crypto)
24
+ globalThis.crypto = crypto_1.webcrypto;
25
+ });
26
+ afterEach(() => { jest.restoreAllMocks(); });
27
+ test('keeps identity stable and proves possession rather than trusting the UUID', async () => {
28
+ const store = new MemorySecureStore();
29
+ const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', secureStore: store, backendUrl: 'https://dogfood.test' });
30
+ const first = await client.enrollmentInfo();
31
+ const second = await client.enrollmentInfo();
32
+ expect(second).toEqual(first);
33
+ let proof;
34
+ jest.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
35
+ const url = String(input);
36
+ if (url.endsWith('/dogfood/enroll/start'))
37
+ return response({ status: 'pending', challenge: 'server-nonce' });
38
+ if (url.endsWith('/dogfood/enroll/prove')) {
39
+ proof = JSON.parse(String(init?.body));
40
+ return response({ status: 'pending', proofVerified: true });
41
+ }
42
+ throw new Error(`unexpected URL ${url}`);
43
+ });
44
+ await client.enroll('ios');
45
+ const message = new TextEncoder().encode(`yaver-dogfood-enroll-v1\nio.example.test\n${first.installationId}\nserver-nonce`);
46
+ expect(tweetnacl_1.default.sign.detached.verify(message, new Uint8Array(buffer_1.Buffer.from(proof.signature, 'base64')), new Uint8Array(buffer_1.Buffer.from(first.publicKey, 'base64')))).toBe(true);
47
+ const attacker = tweetnacl_1.default.sign.keyPair();
48
+ expect(tweetnacl_1.default.sign.detached.verify(message, new Uint8Array(buffer_1.Buffer.from(proof.signature, 'base64')), attacker.publicKey)).toBe(false);
49
+ });
50
+ test('re-register rotates key and installation while preserving only the logical slot', async () => {
51
+ const store = new MemorySecureStore();
52
+ const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', secureStore: store, backendUrl: 'https://dogfood.test' });
53
+ const before = await client.enrollmentInfo();
54
+ jest.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
55
+ if (String(input).endsWith('/dogfood/enroll/start'))
56
+ return response({ status: 'pending', challenge: 'rotate-nonce' });
57
+ return response({ status: 'pending', proofVerified: true });
58
+ });
59
+ await client.reRegister('ios');
60
+ const after = await client.enrollmentInfo();
61
+ expect(after.registrationSlot).toBe(before.registrationSlot);
62
+ expect(after.installationId).not.toBe(before.installationId);
63
+ expect(after.publicKey).not.toBe(before.publicKey);
64
+ });
65
+ test('exchanges an approved key proof for a short-lived scoped session', async () => {
66
+ const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', secureStore: new MemorySecureStore(), backendUrl: 'https://dogfood.test' });
67
+ const identity = await client.enrollmentInfo();
68
+ let sessionProof;
69
+ jest.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
70
+ const url = String(input);
71
+ if (url.includes('/dogfood/enroll/status'))
72
+ return response({ status: 'active' });
73
+ if (url.endsWith('/dogfood/session/challenge'))
74
+ return response({ challenge: 'session-nonce' });
75
+ if (url.endsWith('/dogfood/session')) {
76
+ sessionProof = JSON.parse(String(init?.body));
77
+ return response({ token: 'short-token', expiresAt: 123, scopes: ['feedback', 'blackbox'], projectSlug: 'example' });
78
+ }
79
+ throw new Error(`unexpected URL ${url}`);
80
+ });
81
+ const session = await client.session();
82
+ expect(session).toMatchObject({ active: true, token: 'short-token', scopes: ['feedback', 'blackbox'] });
83
+ const message = new TextEncoder().encode(`yaver-dogfood-session-v1\nio.example.test\n${identity.installationId}\nsession-nonce`);
84
+ expect(tweetnacl_1.default.sign.detached.verify(message, new Uint8Array(buffer_1.Buffer.from(sessionProof.signature, 'base64')), new Uint8Array(buffer_1.Buffer.from(identity.publicKey, 'base64')))).toBe(true);
85
+ });
86
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const dogfoodPolicy_1 = require("../dogfoodPolicy");
4
+ describe('resolveSDKDogfood', () => {
5
+ it('fails closed unless Dogfood is explicitly enabled', () => {
6
+ expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ accountIds: ['acct-1'], currentAccountId: 'acct-1' })).toMatchObject({
7
+ active: false,
8
+ code: 'SDK_DOGFOOD_DISABLED',
9
+ });
10
+ });
11
+ it('requires an exact approved app-account ID', () => {
12
+ expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, accountIds: ['acct-1'], currentAccountId: '' }).code)
13
+ .toBe('SDK_DOGFOOD_ACCOUNT_REQUIRED');
14
+ expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, accountIds: ['acct-1'], currentAccountId: 'acct-10' }).code)
15
+ .toBe('SDK_DOGFOOD_ACCOUNT_NOT_ALLOWED');
16
+ expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, accountIds: ['acct-1'], currentAccountId: 'acct-1' })).toMatchObject({
17
+ active: true,
18
+ code: 'SDK_DOGFOOD_ACTIVE',
19
+ accountId: 'acct-1',
20
+ });
21
+ });
22
+ it('supports a key-enrolled installation without an app account', () => {
23
+ expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, appId: 'io.example', installationStatus: 'active' }).code)
24
+ .toBe('SDK_DOGFOOD_INSTALLATION_REQUIRED');
25
+ expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, appId: 'io.example', installationId: 'install-1', installationStatus: 'pending' }).code)
26
+ .toBe('SDK_DOGFOOD_INSTALLATION_NOT_ACTIVE');
27
+ expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, appId: 'io.example', installationId: 'install-1', installationStatus: 'active' })).toMatchObject({
28
+ active: true,
29
+ code: 'SDK_DOGFOOD_ACTIVE',
30
+ accountId: 'install-1',
31
+ });
32
+ });
33
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ // Target-device selection contract.
3
+ //
4
+ // This exists because pickTargetDevice used to silently reroute: when the
5
+ // user's `preferredDeviceId` was absent from the list — or present but
6
+ // carrying an empty `quicHost` — the preference block fell through to the
7
+ // generic "first fresh machine" search. The user picked their Mac mini and
8
+ // the fix landed on their laptop, with no error on either end.
9
+ //
10
+ // The contract now: an explicit preference is honoured by id alone, or the
11
+ // pick fails. Not connecting is a better failure than connecting to the
12
+ // wrong host.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const device_1 = require("../_core/device");
15
+ const constants_1 = require("../_core/constants");
16
+ const NOW = 1700000000000;
17
+ function device(over) {
18
+ return {
19
+ name: over.deviceId,
20
+ platform: 'darwin',
21
+ isOnline: true,
22
+ lastHeartbeat: NOW,
23
+ quicHost: '100.64.0.1',
24
+ quicPort: 18080,
25
+ ...over,
26
+ };
27
+ }
28
+ beforeEach(() => {
29
+ jest.spyOn(Date, 'now').mockReturnValue(NOW);
30
+ });
31
+ afterEach(() => {
32
+ jest.restoreAllMocks();
33
+ });
34
+ describe('pickTargetDevice — explicit preference', () => {
35
+ it('returns the preferred device when it is fresh', () => {
36
+ const macmini = device({ deviceId: 'macmini' });
37
+ const laptop = device({ deviceId: 'laptop' });
38
+ expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
39
+ });
40
+ it('returns the preferred device even when it is STALE, rather than rerouting', () => {
41
+ const macmini = device({
42
+ deviceId: 'macmini',
43
+ lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
44
+ });
45
+ const laptop = device({ deviceId: 'laptop' });
46
+ expect((0, device_1.isDeviceFresh)(macmini)).toBe(false);
47
+ expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
48
+ });
49
+ it('returns the preferred device even with NO quicHost — relay addresses it by id', () => {
50
+ // This is the off-LAN shape: no LAN address is published, and the relay
51
+ // route is `<relay>/d/<deviceId>`. An empty quicHost is not a reason to
52
+ // reroute; it is the normal remote case.
53
+ const macmini = device({ deviceId: 'macmini', quicHost: '' });
54
+ const laptop = device({ deviceId: 'laptop' });
55
+ expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
56
+ });
57
+ it('returns null when the preferred id is absent — never falls through to another machine', () => {
58
+ const laptop = device({ deviceId: 'laptop' });
59
+ const desktop = device({ deviceId: 'desktop' });
60
+ expect((0, device_1.pickTargetDevice)([laptop, desktop], 'macmini')).toBeNull();
61
+ });
62
+ it('returns null for an unknown preference even when exactly one healthy machine exists', () => {
63
+ // The tempting "well, there's only one, just use it" case. Still wrong:
64
+ // the user asked for a specific host.
65
+ expect((0, device_1.pickTargetDevice)([device({ deviceId: 'laptop' })], 'macmini')).toBeNull();
66
+ });
67
+ });
68
+ describe('pickTargetDevice — no preference', () => {
69
+ it('prefers a fresh device with a quicHost over a stale one', () => {
70
+ const stale = device({
71
+ deviceId: 'stale',
72
+ lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
73
+ });
74
+ const fresh = device({ deviceId: 'fresh' });
75
+ expect((0, device_1.pickTargetDevice)([stale, fresh])).toBe(fresh);
76
+ });
77
+ it('falls back to an online device with a quicHost when none are fresh', () => {
78
+ const offline = device({ deviceId: 'offline', isOnline: false });
79
+ const online = device({
80
+ deviceId: 'online',
81
+ lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
82
+ });
83
+ expect((0, device_1.pickTargetDevice)([offline, online])).toBe(online);
84
+ });
85
+ it('returns null for an empty list', () => {
86
+ expect((0, device_1.pickTargetDevice)([], 'macmini')).toBeNull();
87
+ expect((0, device_1.pickTargetDevice)([])).toBeNull();
88
+ });
89
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const reloadActions_1 = require("../reloadActions");
4
+ const DEV = { isDevBuild: true, connected: true };
5
+ describe('reloadActions — the production guard', () => {
6
+ // THIS IS THE GUARD. Prove it by breaking it: flip `if (!opts.isDevBuild)`
7
+ // in reloadActions.ts to `if (opts.isDevBuild)` and this test fails while
8
+ // every other test in this file still passes.
9
+ it('returns NOTHING in a production build, even with a healthy dev server', () => {
10
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'vite' }, { isDevBuild: false, connected: true, includeRebuild: true });
11
+ expect(actions).toEqual([]);
12
+ });
13
+ it('returns actions in a dev build', () => {
14
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'vite' }, DEV);
15
+ expect(actions.map((a) => a.id)).toEqual(['hot', 'full']);
16
+ expect(actions.every((a) => a.enabled)).toBe(true);
17
+ });
18
+ });
19
+ describe('reloadFrameworkFamily', () => {
20
+ it.each([
21
+ ['flutter', 'flutter'],
22
+ ['expo', 'react-native'],
23
+ ['react-native', 'react-native'],
24
+ ['vite', 'web'],
25
+ ['nextjs', 'web'],
26
+ ['', 'unknown'],
27
+ ['godot', 'unknown'],
28
+ ])('maps %s → %s', (framework, family) => {
29
+ expect((0, reloadActions_1.reloadFrameworkFamily)(framework)).toBe(family);
30
+ });
31
+ });
32
+ describe('per-stack labels', () => {
33
+ it('calls the full action a Hot Restart on Flutter (stdin R), not a Full Reload', () => {
34
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'flutter' }, DEV);
35
+ expect(actions.find((a) => a.id === 'hot').label).toBe('Hot Reload');
36
+ expect(actions.find((a) => a.id === 'full').label).toBe('Hot Restart');
37
+ expect(actions.find((a) => a.id === 'full').hint).toContain('(R)');
38
+ });
39
+ it('calls it a Full Reload everywhere else', () => {
40
+ for (const framework of ['expo', 'vite', 'nextjs']) {
41
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework }, DEV);
42
+ expect(actions.find((a) => a.id === 'full').label).toBe('Full Reload');
43
+ }
44
+ });
45
+ });
46
+ describe('URL / payload construction', () => {
47
+ it('sends fast for hot and full for full, both to /dev/reload', () => {
48
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'flutter' }, DEV);
49
+ expect((0, reloadActions_1.reloadRequest)(actions[0])).toEqual({
50
+ method: 'POST',
51
+ path: reloadActions_1.RELOAD_PATH,
52
+ body: { mode: 'fast' },
53
+ });
54
+ expect((0, reloadActions_1.reloadRequest)(actions[1])).toEqual({
55
+ method: 'POST',
56
+ path: reloadActions_1.RELOAD_PATH,
57
+ body: { mode: 'full' },
58
+ });
59
+ });
60
+ it('routes the RN bundle rebuild to /dev/reload-app', () => {
61
+ const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, includeRebuild: true });
62
+ const rebuild = actions.find((a) => a.id === 'rebuild');
63
+ expect((0, reloadActions_1.reloadRequest)(rebuild)).toEqual({
64
+ method: 'POST',
65
+ path: reloadActions_1.RELOAD_APP_PATH,
66
+ body: { mode: 'bundle' },
67
+ });
68
+ });
69
+ });
70
+ describe('a blocked action NAMES the blocker', () => {
71
+ it('names the missing dev server and the command that starts it', () => {
72
+ const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, machineLabel: 'primary' });
73
+ expect(actions).toHaveLength(2);
74
+ for (const action of actions) {
75
+ expect(action.enabled).toBe(false);
76
+ expect(action.disabledReason).toContain('primary');
77
+ expect(action.disabledReason).toContain('yaver dev start');
78
+ }
79
+ });
80
+ it('names "still building" rather than pretending nothing is running', () => {
81
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, building: true, framework: 'expo' }, DEV);
82
+ expect(actions[0].disabledReason).toContain('still building');
83
+ });
84
+ it('names the missing machine when disconnected', () => {
85
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'expo' }, { isDevBuild: true, connected: false });
86
+ expect(actions[0].disabledReason).toContain('Not connected');
87
+ });
88
+ it('keeps Rebuild Bundle ENABLED with no dev server — that is its whole point', () => {
89
+ const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, includeRebuild: true });
90
+ const rebuild = actions.find((a) => a.id === 'rebuild');
91
+ expect(rebuild.enabled).toBe(true);
92
+ expect(rebuild.disabledReason).toBeUndefined();
93
+ });
94
+ it('disables Rebuild Bundle when there is no machine', () => {
95
+ const actions = (0, reloadActions_1.reloadActions)({ running: true }, { isDevBuild: true, connected: false, includeRebuild: true });
96
+ expect(actions.find((a) => a.id === 'rebuild').enabled).toBe(false);
97
+ });
98
+ });
99
+ describe('describeReloadFailure names a cause, never "failed"', () => {
100
+ it('503 → no dev server', () => {
101
+ expect((0, reloadActions_1.describeReloadFailure)(503, 'dev server not available')).toContain('No dev server is running');
102
+ });
103
+ it('framework cannot hot reload → says so and points at the alternative', () => {
104
+ const msg = (0, reloadActions_1.describeReloadFailure)(500, 'unity does not support hot reload', {
105
+ running: true,
106
+ framework: 'unity',
107
+ });
108
+ expect(msg).toContain('unity');
109
+ expect(msg).toContain('Rebuild Bundle');
110
+ });
111
+ it('loopback connection refused → the dev server is not listening', () => {
112
+ const msg = (0, reloadActions_1.describeReloadFailure)(502, 'Get "http://127.0.0.1:8081/reload": dial tcp 127.0.0.1:8081: connect: connection refused');
113
+ expect(msg).toContain('not listening');
114
+ expect(msg).toContain('yaver dev start');
115
+ });
116
+ it('401/403 → session, not server', () => {
117
+ expect((0, reloadActions_1.describeReloadFailure)(401, '')).toContain('sign in again');
118
+ expect((0, reloadActions_1.describeReloadFailure)(403, '')).toContain('sign in again');
119
+ });
120
+ it('404 → the agent is too old, and says how to update it', () => {
121
+ expect((0, reloadActions_1.describeReloadFailure)(404, 'not found')).toContain('yaver-cli@latest');
122
+ });
123
+ it('5xx → points at the agent log', () => {
124
+ expect((0, reloadActions_1.describeReloadFailure)(500, 'boom')).toContain('yaver logs');
125
+ });
126
+ it('status 0 (transport never answered) → machine reachability', () => {
127
+ expect((0, reloadActions_1.describeReloadFailure)(0, '')).toContain('yaver serve');
128
+ });
129
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fs_1 = require("fs");
4
+ const path_1 = require("path");
5
+ /**
6
+ * `reloadActions.ts` is duplicated into yaver-feedback-react-native and
7
+ * yaver-feedback-web on purpose — they are two independently published npm
8
+ * packages, and neither may depend on the other.
9
+ *
10
+ * Duplication without a guard is DRIFT, and drift in this file is the worst
11
+ * kind: it is a *policy* file. If the web copy stops returning [] for a
12
+ * production build and the RN copy still does, the two SDKs disagree about
13
+ * whether a shipped app may show a reload button — and nothing in `tsc`
14
+ * notices, because they are separate compilation units.
15
+ *
16
+ * This is the same shape as beaconParity.test.ts in mobile/: read both
17
+ * sources, assert they agree. Prove it by breaking it — change one word in
18
+ * either copy and this test fails.
19
+ */
20
+ const RN_COPY = (0, path_1.join)(__dirname, '..', 'reloadActions.ts');
21
+ const WEB_COPY = (0, path_1.join)(__dirname, '..', '..', '..', // sdk/feedback/react-native
22
+ 'web', 'src', 'reloadActions.ts');
23
+ describe('reloadActions parity between the RN and web SDKs', () => {
24
+ it('both copies exist', () => {
25
+ expect(() => (0, fs_1.readFileSync)(RN_COPY, 'utf8')).not.toThrow();
26
+ expect(() => (0, fs_1.readFileSync)(WEB_COPY, 'utf8')).not.toThrow();
27
+ });
28
+ it('is byte-identical across the two packages', () => {
29
+ const rn = (0, fs_1.readFileSync)(RN_COPY, 'utf8');
30
+ const web = (0, fs_1.readFileSync)(WEB_COPY, 'utf8');
31
+ expect(rn).toEqual(web);
32
+ });
33
+ it('still carries the production guard in BOTH copies', () => {
34
+ // Named separately from the byte comparison so that if someone
35
+ // legitimately reformats both files the failure still points at the one
36
+ // line that actually matters.
37
+ for (const [name, path] of [['react-native', RN_COPY], ['web', WEB_COPY]]) {
38
+ const source = (0, fs_1.readFileSync)(path, 'utf8');
39
+ expect(`${name}: ${source.includes('if (!opts.isDevBuild) return [];')}`).toEqual(`${name}: true`);
40
+ }
41
+ });
42
+ });
@@ -39,7 +39,7 @@ describe('React Native SDK types', () => {
39
39
  const bundle = {
40
40
  metadata: {
41
41
  timestamp: '2026-03-24T12:00:00Z',
42
- device: {
42
+ deviceInfo: {
43
43
  platform: 'ios',
44
44
  osVersion: '18.0',
45
45
  model: 'iPhone 16 Pro',
@@ -55,7 +55,7 @@ describe('React Native SDK types', () => {
55
55
  screenshots: [],
56
56
  };
57
57
  expect(bundle.metadata.timestamp).toBe('2026-03-24T12:00:00Z');
58
- expect(bundle.metadata.device.platform).toBe('ios');
58
+ expect(bundle.metadata.deviceInfo.platform).toBe('ios');
59
59
  expect(bundle.screenshots).toEqual([]);
60
60
  expect(bundle.video).toBeUndefined();
61
61
  });
@@ -63,7 +63,7 @@ describe('React Native SDK types', () => {
63
63
  const bundle = {
64
64
  metadata: {
65
65
  timestamp: '2026-03-24T12:00:00Z',
66
- device: {
66
+ deviceInfo: {
67
67
  platform: 'android',
68
68
  osVersion: '15',
69
69
  model: 'Pixel 9',
@@ -146,7 +146,7 @@ describe('React Native SDK types', () => {
146
146
  bundle: {
147
147
  metadata: {
148
148
  timestamp: 'now',
149
- device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
149
+ deviceInfo: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
150
150
  app: {},
151
151
  },
152
152
  screenshots: [],
@@ -164,7 +164,7 @@ describe('React Native SDK types', () => {
164
164
  bundle: {
165
165
  metadata: {
166
166
  timestamp: 'now',
167
- device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
167
+ deviceInfo: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
168
168
  app: {},
169
169
  },
170
170
  screenshots: [],