yaver-feedback-react-native 0.9.2 → 0.9.4
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/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/Discovery.js +0 -6
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +347 -61
- 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 +116 -3
- package/dist/P2PClient.js +273 -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 +31 -17
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +34 -0
- package/dist/YaverFeedback.js +124 -19
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- 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 -1
- package/dist/__tests__/ReportIdentity.test.js +34 -22
- package/dist/__tests__/ShakeDetectorWeb.test.d.ts +1 -0
- package/dist/__tests__/ShakeDetectorWeb.test.js +27 -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/_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 +9 -23
- package/dist/_core/device.js +13 -28
- 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 +13 -4
- package/dist/index.js +24 -8
- package/dist/reloadActions.js +2 -2
- package/dist/types.d.ts +50 -7
- package/package.json +12 -3
- package/src/AuthOverlay.tsx +20 -106
- package/src/Discovery.ts +0 -6
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +445 -67
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +347 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/ShakeDetector.ts +31 -18
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +133 -22
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- package/src/__tests__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +36 -27
- package/src/__tests__/ShakeDetectorWeb.test.ts +30 -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/_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 +13 -30
- 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 +40 -11
- package/src/reloadActions.ts +2 -2
- package/src/types.ts +45 -7
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// AUTO-SYNCED from shared/client-core/src/remoteless.ts.
|
|
3
|
+
// DO NOT EDIT IN PLACE. Edit the source and re-run
|
|
4
|
+
// scripts/sync-client-core.sh. CI checks drift via `--check`.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.remotelessCapability = remotelessCapability;
|
|
7
|
+
exports.resolveRemotelessPlacement = resolveRemotelessPlacement;
|
|
8
|
+
const LOCAL_SUPPORT = {
|
|
9
|
+
"analysis-chat": "supported", "code-edit": "bounded", "git-read": "supported", "git-commit": "bounded",
|
|
10
|
+
"git-push": "bounded", "static-preflight": "supported",
|
|
11
|
+
"existing-web-artifact": "supported", "dev-server": "unavailable",
|
|
12
|
+
"web-build": "unavailable", "flutter-render": "unavailable",
|
|
13
|
+
"native-build": "unavailable", simulator: "unavailable", shell: "unavailable",
|
|
14
|
+
test: "unavailable", deploy: "unavailable", container: "unavailable",
|
|
15
|
+
};
|
|
16
|
+
const LABELS = {
|
|
17
|
+
"analysis-chat": "analysis and chat", "code-edit": "editing", "git-read": "Git inspection", "git-commit": "Git commit",
|
|
18
|
+
"git-push": "Git push", "static-preflight": "static preflight",
|
|
19
|
+
"existing-web-artifact": "an existing web artifact", "dev-server": "a dev server",
|
|
20
|
+
"web-build": "a web build", "flutter-render": "Flutter rendering",
|
|
21
|
+
"native-build": "a native build", simulator: "a simulator", shell: "shell commands",
|
|
22
|
+
test: "tests", deploy: "deployment", container: "containers",
|
|
23
|
+
};
|
|
24
|
+
function remotelessCapability(capability, surface) {
|
|
25
|
+
const support = capability === "analysis-chat" ? "supported" : surface === "ios" || surface === "android"
|
|
26
|
+
? LOCAL_SUPPORT[capability]
|
|
27
|
+
: capability === "existing-web-artifact" ? "supported" : "unavailable";
|
|
28
|
+
const label = LABELS[capability];
|
|
29
|
+
const host = surface === "ios" ? "iPhone/iPad" : surface === "android" ? "Android device" : surface === "web" ? "browser" : "companion device";
|
|
30
|
+
if (support === "supported")
|
|
31
|
+
return {
|
|
32
|
+
capability, support, code: `remoteless.${capability}.supported`,
|
|
33
|
+
summary: `${label} can run on this device`,
|
|
34
|
+
detail: `${label} does not require a remote shell or build toolchain.`,
|
|
35
|
+
route: { label: "Choose a device", path: "/devices" },
|
|
36
|
+
};
|
|
37
|
+
if (support === "bounded")
|
|
38
|
+
return {
|
|
39
|
+
capability, support, code: `remoteless.${capability}.bounded`,
|
|
40
|
+
summary: `${label} is available on this device with limits`,
|
|
41
|
+
detail: `${label} can run locally, but background execution is bounded and interrupted work returns to Review.`,
|
|
42
|
+
route: { label: "Choose a device", path: "/devices" },
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
capability, support, code: `remoteless.${capability}.unavailable`,
|
|
46
|
+
summary: `${label} needs another execution target`,
|
|
47
|
+
detail: capability === "flutter-render"
|
|
48
|
+
? `The ${host} can display an already-built Flutter web artifact, but Yaver's remoteless runtime has no Flutter SDK, shell, dev server, or simulator. Build and serve it on your primary/secondary device or Cloud Workspace.`
|
|
49
|
+
: `Yaver's remoteless runtime on this ${host} cannot provide ${label}: it has no general shell, package manager, persistent process host, simulator, native SDK, or container runtime. Use an eligible primary/secondary device or Cloud Workspace.`,
|
|
50
|
+
route: { label: "Choose a capable device", path: "/devices" },
|
|
51
|
+
alternateRoute: { label: "Use Cloud Workspace", path: "/cloud-onboarding" },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function resolveRemotelessPlacement(input) {
|
|
55
|
+
const capability = remotelessCapability(input.capability, input.surface);
|
|
56
|
+
if (!input.forceLocal) {
|
|
57
|
+
// Enforce precedence here instead of trusting every surface to construct
|
|
58
|
+
// its array correctly. Within a role, caller order remains significant
|
|
59
|
+
// (for example project-assigned primary before account-wide primary).
|
|
60
|
+
const target = ["explicit", "primary", "secondary", "focused"]
|
|
61
|
+
.map((role) => input.candidates.find((candidate) => candidate.role === role && candidate.connected))
|
|
62
|
+
.find((candidate) => !!candidate);
|
|
63
|
+
if (target) {
|
|
64
|
+
const degraded = target.role === "secondary";
|
|
65
|
+
return { lane: "remote", target, degraded, banner: degraded ? `Using secondary · ${target.name} · primary unavailable` : null };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const configured = input.candidates.filter((candidate) => candidate.role !== "focused");
|
|
69
|
+
const reason = configured.length ? "Primary and secondary devices are unavailable." : "No eligible remote device is configured.";
|
|
70
|
+
if (capability.support === "unavailable")
|
|
71
|
+
return { lane: "blocked", capability, banner: `${reason} ${capability.summary}.` };
|
|
72
|
+
if (input.forceLocal)
|
|
73
|
+
return { lane: "remoteless", capability, banner: `No remote box selected · ${capability.summary}.` };
|
|
74
|
+
return { lane: "remoteless", capability, banner: `Remoteless fallback · ${reason} ${capability.summary}.` };
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trace.ts — one shared paste-ready trace assembler for every surface.
|
|
3
|
+
*
|
|
4
|
+
* WHY (2026-08-09): a crash/failure on web, mobile, or the console used to
|
|
5
|
+
* copy different things — RawFailureBanner copied just `failure.raw`, the
|
|
6
|
+
* mobile Logs sheet copied `combinedLogText`, RuntimeLab copied raw console
|
|
7
|
+
* lines. None carried the surface identity, versions, task id, runner/model,
|
|
8
|
+
* or the log tail, so pasting into an issue or a vibing follow-up lost the
|
|
9
|
+
* context that makes a bug reproducible. One assembler here gives every
|
|
10
|
+
* surface the SAME structured blob (surface → versions → device/relay →
|
|
11
|
+
* task → error → log tail), so web and mobile can never drift
|
|
12
|
+
* (AGENTS.md: one shared classifier, no copies). Mirrored into
|
|
13
|
+
* mobile/src/_core and web/lib/_core by scripts/sync-client-core.sh.
|
|
14
|
+
*/
|
|
15
|
+
export interface TraceContext {
|
|
16
|
+
/** Surface name: "web" | "mobile" | "console" | "cli" | "watch" ... */
|
|
17
|
+
surface: string;
|
|
18
|
+
/** e.g. web build label, mobile JS bundle version, cli version. */
|
|
19
|
+
surfaceVersion?: string;
|
|
20
|
+
/** Agent version reported by /info (e.g. "1.99.409"). */
|
|
21
|
+
agentVersion?: string;
|
|
22
|
+
/** Device the task ran on (name + id). */
|
|
23
|
+
device?: string;
|
|
24
|
+
/** Relay region / id if known. */
|
|
25
|
+
relay?: string;
|
|
26
|
+
/** Task identity + lifecycle. */
|
|
27
|
+
task?: {
|
|
28
|
+
id: string;
|
|
29
|
+
status?: string;
|
|
30
|
+
runner?: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
};
|
|
34
|
+
/** The human-readable error (already extracted/named). */
|
|
35
|
+
error?: string;
|
|
36
|
+
/** Raw failure blob (the undecorated original). */
|
|
37
|
+
raw?: string;
|
|
38
|
+
/** Log tail — last N lines of the relevant log (agent/relay/runtime). */
|
|
39
|
+
logTail?: string;
|
|
40
|
+
/** When the trace was captured (ms epoch). */
|
|
41
|
+
ts?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Assemble a paste-ready trace. Every present field becomes one labelled
|
|
45
|
+
* line; nothing is ever invented. Returns a plain-text blob.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assembleTrace(ctx: TraceContext): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// AUTO-SYNCED from shared/client-core/src/trace.ts.
|
|
3
|
+
// DO NOT EDIT IN PLACE. Edit the source and re-run
|
|
4
|
+
// scripts/sync-client-core.sh. CI checks drift via `--check`.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.assembleTrace = assembleTrace;
|
|
7
|
+
const MASK = /token\s*[=:]\s*\S+|Bearer\s+\S+|api[_-]?key\s*[=:]\s*\S+|password\s*[=:]\s*\S+/gi;
|
|
8
|
+
function sanitize(s) {
|
|
9
|
+
return String(s || "").replace(MASK, "[redacted]");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Assemble a paste-ready trace. Every present field becomes one labelled
|
|
13
|
+
* line; nothing is ever invented. Returns a plain-text blob.
|
|
14
|
+
*/
|
|
15
|
+
function assembleTrace(ctx) {
|
|
16
|
+
const out = [];
|
|
17
|
+
out.push("--- Yaver trace ---");
|
|
18
|
+
out.push(`surface: ${sanitize(ctx.surface)}`);
|
|
19
|
+
if (ctx.surfaceVersion)
|
|
20
|
+
out.push(`surface.version: ${sanitize(ctx.surfaceVersion)}`);
|
|
21
|
+
if (ctx.agentVersion)
|
|
22
|
+
out.push(`agent.version: ${sanitize(ctx.agentVersion)}`);
|
|
23
|
+
if (ctx.device)
|
|
24
|
+
out.push(`device: ${sanitize(ctx.device)}`);
|
|
25
|
+
if (ctx.relay)
|
|
26
|
+
out.push(`relay: ${sanitize(ctx.relay)}`);
|
|
27
|
+
if (ctx.task) {
|
|
28
|
+
out.push(`task: ${sanitize(ctx.task.id)}${ctx.task.status ? ` status=${sanitize(ctx.task.status)}` : ""}${ctx.task.runner ? ` runner=${sanitize(ctx.task.runner)}` : ""}${ctx.task.model ? ` model=${sanitize(ctx.task.model)}` : ""}${ctx.task.title ? ` title=${sanitize(ctx.task.title)}` : ""}`);
|
|
29
|
+
}
|
|
30
|
+
if (ctx.error)
|
|
31
|
+
out.push(`error: ${sanitize(ctx.error)}`);
|
|
32
|
+
if (ctx.raw)
|
|
33
|
+
out.push(`raw: ${sanitize(ctx.raw)}`);
|
|
34
|
+
if (ctx.logTail)
|
|
35
|
+
out.push(`log-tail:\n${sanitize(ctx.logTail)}`);
|
|
36
|
+
out.push(`ts: ${ctx.ts ?? Date.now()}`);
|
|
37
|
+
return out.join("\n");
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// AUTO-SYNCED from shared/client-core/src/trace.test.ts.
|
|
3
|
+
// DO NOT EDIT IN PLACE. Edit the source and re-run
|
|
4
|
+
// scripts/sync-client-core.sh. CI checks drift via `--check`.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* trace.test.ts — guards for the shared trace assembler.
|
|
8
|
+
*
|
|
9
|
+
* Run: npx tsx shared/client-core/src/trace.test.ts
|
|
10
|
+
*/
|
|
11
|
+
const trace_1 = require("./trace");
|
|
12
|
+
let failures = 0;
|
|
13
|
+
const eq = (got, want, label) => {
|
|
14
|
+
if (JSON.stringify(got) === JSON.stringify(want))
|
|
15
|
+
console.log(`ok ${label}`);
|
|
16
|
+
else {
|
|
17
|
+
console.error(`FAIL ${label}:\n got ${JSON.stringify(got)}\n want ${JSON.stringify(want)}`);
|
|
18
|
+
failures++;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const ok = (c, label) => eq(Boolean(c), true, label);
|
|
22
|
+
{
|
|
23
|
+
const t = (0, trace_1.assembleTrace)({
|
|
24
|
+
surface: "web",
|
|
25
|
+
surfaceVersion: "1.1.164",
|
|
26
|
+
agentVersion: "1.99.409",
|
|
27
|
+
device: "ubuntu-4gb-hel1-1 (2ed7da41…)",
|
|
28
|
+
relay: "public-free",
|
|
29
|
+
task: { id: "abc123", status: "failed", runner: "opencode", model: "deepseek/deepseek-v4-flash", title: "build" },
|
|
30
|
+
error: "flutter exited before becoming ready",
|
|
31
|
+
raw: "the raw failure bytes",
|
|
32
|
+
logTail: "line1\nline2",
|
|
33
|
+
ts: 1700000000000,
|
|
34
|
+
});
|
|
35
|
+
ok(t.includes("surface: web"), "surface line present");
|
|
36
|
+
ok(t.includes("agent.version: 1.99.409"), "agent version present");
|
|
37
|
+
ok(t.includes("task: abc123 status=failed runner=opencode"), "task line present");
|
|
38
|
+
ok(t.includes("error: flutter exited before becoming ready"), "error present");
|
|
39
|
+
ok(t.includes("log-tail:\nline1\nline2"), "log tail present under its label");
|
|
40
|
+
ok(t.includes("ts: 1700000000000"), "timestamp present");
|
|
41
|
+
}
|
|
42
|
+
{
|
|
43
|
+
// No invented fields when absent.
|
|
44
|
+
const t = (0, trace_1.assembleTrace)({ surface: "mobile", task: { id: "x" } });
|
|
45
|
+
ok(!t.includes("agent.version"), "no agent.version when absent");
|
|
46
|
+
ok(!t.includes("error:"), "no error when absent");
|
|
47
|
+
ok(t.includes("surface: mobile"), "surface still present");
|
|
48
|
+
}
|
|
49
|
+
{
|
|
50
|
+
// Secrets are redacted.
|
|
51
|
+
const t = (0, trace_1.assembleTrace)({ surface: "web", error: "failed with token=abc123xyz" });
|
|
52
|
+
ok(t.includes("[redacted]") && !t.includes("abc123xyz"), "token redacted from error");
|
|
53
|
+
const t2 = (0, trace_1.assembleTrace)({ surface: "mobile", raw: "Authorization: Bearer deadbeef" });
|
|
54
|
+
ok(t2.includes("[redacted]") && !t2.includes("deadbeef"), "bearer redacted from raw");
|
|
55
|
+
}
|
|
56
|
+
if (failures > 0) {
|
|
57
|
+
console.error(`\n${failures} FAILURE(s)`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
console.log("\nall trace tests pass");
|
package/dist/auth.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (`yaver://oauth-callback`) the Yaver mobile app uses.
|
|
11
11
|
* - Email / password sign-up + login (no 2FA flow — for SDK simplicity).
|
|
12
12
|
* - Token validation.
|
|
13
|
-
* - `/devices/list` →
|
|
13
|
+
* - `/devices/list` → owner remote dev machines.
|
|
14
14
|
*
|
|
15
15
|
* Mobile-only. A web equivalent will ship as a separate `yaver-web-feedback`
|
|
16
16
|
* package; do not import this module from a browser bundle.
|
|
@@ -99,12 +99,6 @@ export interface RemoteDevice {
|
|
|
99
99
|
needsAuth: boolean;
|
|
100
100
|
runnerDown: boolean;
|
|
101
101
|
lastHeartbeat: number;
|
|
102
|
-
isGuest: boolean;
|
|
103
|
-
hostUserId?: string;
|
|
104
|
-
hostName?: string;
|
|
105
|
-
hostEmail?: string;
|
|
106
|
-
hostUserIdString?: string;
|
|
107
|
-
accessScope: 'owner' | 'shared-scoped' | 'shared-legacy';
|
|
108
102
|
quicHost: string;
|
|
109
103
|
quicPort: number;
|
|
110
104
|
/** Agent HTTP port — preferred over quicPort when present. */
|
|
@@ -121,53 +115,14 @@ export interface RemoteDevice {
|
|
|
121
115
|
}
|
|
122
116
|
export interface DeviceList {
|
|
123
117
|
owned: RemoteDevice[];
|
|
124
|
-
shared: RemoteDevice[];
|
|
125
118
|
}
|
|
126
119
|
export interface DeviceReachability {
|
|
127
120
|
reachable: boolean;
|
|
128
121
|
url?: string;
|
|
129
122
|
}
|
|
130
|
-
export interface GuestInvitation {
|
|
131
|
-
hostUserId: string;
|
|
132
|
-
hostName: string;
|
|
133
|
-
hostEmail: string;
|
|
134
|
-
hostUserIdString?: string;
|
|
135
|
-
createdAt: number;
|
|
136
|
-
expiresAt: number;
|
|
137
|
-
inviteCode?: string;
|
|
138
|
-
}
|
|
139
|
-
export interface ActiveGuestHost {
|
|
140
|
-
hostUserId: string;
|
|
141
|
-
hostName: string;
|
|
142
|
-
hostEmail: string;
|
|
143
|
-
grantedAt: number;
|
|
144
|
-
}
|
|
145
|
-
export interface GuestHostsResponse {
|
|
146
|
-
pending: GuestInvitation[];
|
|
147
|
-
active: ActiveGuestHost[];
|
|
148
|
-
}
|
|
149
|
-
export interface InvitationHostDevice {
|
|
150
|
-
deviceId: string;
|
|
151
|
-
name: string;
|
|
152
|
-
platform: string;
|
|
153
|
-
lastHeartbeat?: number;
|
|
154
|
-
proposed: boolean;
|
|
155
|
-
}
|
|
156
|
-
export interface InvitationPreview {
|
|
157
|
-
inviteCode: string;
|
|
158
|
-
hostUserId: string;
|
|
159
|
-
hostName: string;
|
|
160
|
-
hostEmail: string;
|
|
161
|
-
hostUserIdString?: string;
|
|
162
|
-
proposedDeviceIds?: string[];
|
|
163
|
-
hostDevices: InvitationHostDevice[];
|
|
164
|
-
invitedByUserId?: boolean;
|
|
165
|
-
expiresAt: number;
|
|
166
|
-
createdAt: number;
|
|
167
|
-
}
|
|
168
123
|
/**
|
|
169
|
-
* Fetch the
|
|
170
|
-
*
|
|
124
|
+
* Fetch the signed-in owner's remote dev machines. Legacy non-owner rows are
|
|
125
|
+
* discarded even if a stale backend still returns them.
|
|
171
126
|
*
|
|
172
127
|
* Collapses duplicate rows before splitting — Convex can return multiple
|
|
173
128
|
* rows per physical machine after a re-pair or hostname change, and the
|
|
@@ -176,15 +131,3 @@ export interface InvitationPreview {
|
|
|
176
131
|
export declare function listReachableDevices(token: string): Promise<DeviceList>;
|
|
177
132
|
export declare function buildDeviceCandidateUrls(device: RemoteDevice): string[];
|
|
178
133
|
export declare function probeDeviceReachability(device: RemoteDevice, timeoutMs?: number): Promise<DeviceReachability>;
|
|
179
|
-
export declare function mintGuestSdkToken(token: string, hostUserId: string, targetDeviceId: string): Promise<{
|
|
180
|
-
token: string;
|
|
181
|
-
expiresAt: number;
|
|
182
|
-
allowedProjects?: string[];
|
|
183
|
-
}>;
|
|
184
|
-
export declare function fetchGuestHosts(token: string): Promise<GuestHostsResponse>;
|
|
185
|
-
export declare function findInviteByCode(token: string, code: string): Promise<InvitationPreview | null>;
|
|
186
|
-
export declare function acceptGuestByCode(token: string, code: string, approvedDeviceIds?: string[]): Promise<{
|
|
187
|
-
hostName: string;
|
|
188
|
-
hostEmail: string;
|
|
189
|
-
}>;
|
|
190
|
-
export declare function acceptGuestInvitation(token: string, hostUserId: string, approvedDeviceIds?: string[]): Promise<void>;
|
package/dist/auth.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* (`yaver://oauth-callback`) the Yaver mobile app uses.
|
|
12
12
|
* - Email / password sign-up + login (no 2FA flow — for SDK simplicity).
|
|
13
13
|
* - Token validation.
|
|
14
|
-
* - `/devices/list` →
|
|
14
|
+
* - `/devices/list` → owner remote dev machines.
|
|
15
15
|
*
|
|
16
16
|
* Mobile-only. A web equivalent will ship as a separate `yaver-web-feedback`
|
|
17
17
|
* package; do not import this module from a browser bundle.
|
|
@@ -39,11 +39,6 @@ exports.loginWithEmail = loginWithEmail;
|
|
|
39
39
|
exports.listReachableDevices = listReachableDevices;
|
|
40
40
|
exports.buildDeviceCandidateUrls = buildDeviceCandidateUrls;
|
|
41
41
|
exports.probeDeviceReachability = probeDeviceReachability;
|
|
42
|
-
exports.mintGuestSdkToken = mintGuestSdkToken;
|
|
43
|
-
exports.fetchGuestHosts = fetchGuestHosts;
|
|
44
|
-
exports.findInviteByCode = findInviteByCode;
|
|
45
|
-
exports.acceptGuestByCode = acceptGuestByCode;
|
|
46
|
-
exports.acceptGuestInvitation = acceptGuestInvitation;
|
|
47
42
|
// AsyncStorage is an optional peer dep — degrade gracefully if missing.
|
|
48
43
|
let AsyncStorage = null;
|
|
49
44
|
try {
|
|
@@ -358,8 +353,8 @@ async function loginWithEmail(email, password) {
|
|
|
358
353
|
return { token: data.token, userId: data.userId };
|
|
359
354
|
}
|
|
360
355
|
/**
|
|
361
|
-
* Fetch the
|
|
362
|
-
*
|
|
356
|
+
* Fetch the signed-in owner's remote dev machines. Legacy non-owner rows are
|
|
357
|
+
* discarded even if a stale backend still returns them.
|
|
363
358
|
*
|
|
364
359
|
* Collapses duplicate rows before splitting — Convex can return multiple
|
|
365
360
|
* rows per physical machine after a re-pair or hostname change, and the
|
|
@@ -371,7 +366,7 @@ async function listReachableDevices(token) {
|
|
|
371
366
|
headers: { Authorization: `Bearer ${token}` },
|
|
372
367
|
});
|
|
373
368
|
if (!res.ok)
|
|
374
|
-
return { owned: []
|
|
369
|
+
return { owned: [] };
|
|
375
370
|
const data = await res.json();
|
|
376
371
|
const raw = (data.devices ?? []);
|
|
377
372
|
// Normalise Convex field names → SDK's RemoteDevice shape. The
|
|
@@ -385,12 +380,6 @@ async function listReachableDevices(token) {
|
|
|
385
380
|
needsAuth: !!d.needsAuth,
|
|
386
381
|
runnerDown: !!d.runnerDown,
|
|
387
382
|
lastHeartbeat: d.lastHeartbeat ?? 0,
|
|
388
|
-
isGuest: !!d.isGuest,
|
|
389
|
-
hostUserId: d.hostUserId,
|
|
390
|
-
hostName: d.hostName,
|
|
391
|
-
hostEmail: d.hostEmail,
|
|
392
|
-
hostUserIdString: d.hostUserIdString,
|
|
393
|
-
accessScope: d.accessScope ?? 'owner',
|
|
394
383
|
quicHost: d.quicHost ?? d.host ?? '',
|
|
395
384
|
quicPort: d.quicPort ?? 0,
|
|
396
385
|
httpPort: d.httpPort ?? d.quicPort,
|
|
@@ -405,13 +394,10 @@ async function listReachableDevices(token) {
|
|
|
405
394
|
// Lazy require so Jest + tree-shakers don't choke on a circular import.
|
|
406
395
|
const { collapseRemoteDevices } = require('./deviceDedup');
|
|
407
396
|
const deduped = collapseRemoteDevices(normalised);
|
|
408
|
-
return {
|
|
409
|
-
owned: deduped.filter((d) => !d.isGuest),
|
|
410
|
-
shared: deduped.filter((d) => d.isGuest),
|
|
411
|
-
};
|
|
397
|
+
return { owned: deduped };
|
|
412
398
|
}
|
|
413
399
|
catch {
|
|
414
|
-
return { owned: []
|
|
400
|
+
return { owned: [] };
|
|
415
401
|
}
|
|
416
402
|
}
|
|
417
403
|
function buildDeviceCandidateUrls(device) {
|
|
@@ -453,71 +439,3 @@ async function probeDeviceReachability(device, timeoutMs = 2500) {
|
|
|
453
439
|
}
|
|
454
440
|
return { reachable: false };
|
|
455
441
|
}
|
|
456
|
-
async function mintGuestSdkToken(token, hostUserId, targetDeviceId) {
|
|
457
|
-
const res = await fetch(`${convexSiteUrl}/guests/sdk-token`, {
|
|
458
|
-
method: 'POST',
|
|
459
|
-
headers: {
|
|
460
|
-
Authorization: `Bearer ${token}`,
|
|
461
|
-
'Content-Type': 'application/json',
|
|
462
|
-
},
|
|
463
|
-
body: JSON.stringify({ hostUserId, targetDeviceId }),
|
|
464
|
-
});
|
|
465
|
-
if (!res.ok) {
|
|
466
|
-
const data = await res.json().catch(() => ({}));
|
|
467
|
-
throw new Error(data.error || 'Failed to mint delegated SDK token');
|
|
468
|
-
}
|
|
469
|
-
return res.json();
|
|
470
|
-
}
|
|
471
|
-
async function fetchGuestHosts(token) {
|
|
472
|
-
const res = await fetch(`${convexSiteUrl}/guests/hosts`, {
|
|
473
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
474
|
-
});
|
|
475
|
-
if (!res.ok) {
|
|
476
|
-
const data = await res.json().catch(() => ({}));
|
|
477
|
-
throw new Error(data.error || 'Failed to fetch guest hosts');
|
|
478
|
-
}
|
|
479
|
-
return res.json();
|
|
480
|
-
}
|
|
481
|
-
async function findInviteByCode(token, code) {
|
|
482
|
-
const cleaned = code.toUpperCase().trim();
|
|
483
|
-
const res = await fetch(`${convexSiteUrl}/guests/find-by-code?code=${encodeURIComponent(cleaned)}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
484
|
-
if (res.status === 404)
|
|
485
|
-
return null;
|
|
486
|
-
if (!res.ok) {
|
|
487
|
-
const data = await res.json().catch(() => ({}));
|
|
488
|
-
throw new Error(data.error || 'Failed to load invite');
|
|
489
|
-
}
|
|
490
|
-
return res.json();
|
|
491
|
-
}
|
|
492
|
-
async function acceptGuestByCode(token, code, approvedDeviceIds) {
|
|
493
|
-
const res = await fetch(`${convexSiteUrl}/guests/accept-code`, {
|
|
494
|
-
method: 'POST',
|
|
495
|
-
headers: {
|
|
496
|
-
Authorization: `Bearer ${token}`,
|
|
497
|
-
'Content-Type': 'application/json',
|
|
498
|
-
},
|
|
499
|
-
body: JSON.stringify({
|
|
500
|
-
code: code.toUpperCase().trim(),
|
|
501
|
-
approvedDeviceIds,
|
|
502
|
-
}),
|
|
503
|
-
});
|
|
504
|
-
if (!res.ok) {
|
|
505
|
-
const data = await res.json().catch(() => ({}));
|
|
506
|
-
throw new Error(data.error || 'Invalid invite code');
|
|
507
|
-
}
|
|
508
|
-
return res.json();
|
|
509
|
-
}
|
|
510
|
-
async function acceptGuestInvitation(token, hostUserId, approvedDeviceIds) {
|
|
511
|
-
const res = await fetch(`${convexSiteUrl}/guests/accept`, {
|
|
512
|
-
method: 'POST',
|
|
513
|
-
headers: {
|
|
514
|
-
Authorization: `Bearer ${token}`,
|
|
515
|
-
'Content-Type': 'application/json',
|
|
516
|
-
},
|
|
517
|
-
body: JSON.stringify({ hostUserId, approvedDeviceIds }),
|
|
518
|
-
});
|
|
519
|
-
if (!res.ok) {
|
|
520
|
-
const data = await res.json().catch(() => ({}));
|
|
521
|
-
throw new Error(data.error || 'Failed to accept invitation');
|
|
522
|
-
}
|
|
523
|
-
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
type SecureStoreLike = {
|
|
2
|
+
getItemAsync(key: string): Promise<string | null>;
|
|
3
|
+
setItemAsync(key: string, value: string): Promise<void>;
|
|
4
|
+
deleteItemAsync(key: string): Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export type DeviceDogfoodState = 'unregistered' | 'pending' | 'active' | 'cancelled' | 'revoked' | 'superseded';
|
|
7
|
+
export interface DeviceDogfoodOptions {
|
|
8
|
+
/** Public identifier registered by the app owner in Yaver Settings. */
|
|
9
|
+
appId: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
backendUrl?: string;
|
|
12
|
+
/** Advanced bare-RN integration. Expo apps use SecureStore automatically. */
|
|
13
|
+
secureStore?: SecureStoreLike;
|
|
14
|
+
}
|
|
15
|
+
export interface DeviceDogfoodSession {
|
|
16
|
+
active: true;
|
|
17
|
+
appId: string;
|
|
18
|
+
installationId: string;
|
|
19
|
+
token: string;
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
scopes: string[];
|
|
22
|
+
projectSlug?: string;
|
|
23
|
+
targetDeviceId?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Account-free third-party Dogfood enrollment. The UUID is a public lookup
|
|
26
|
+
* handle; possession is proven by the private key retained in this app. */
|
|
27
|
+
export declare class YaverDeviceDogfood {
|
|
28
|
+
private readonly options;
|
|
29
|
+
private readonly backendUrl;
|
|
30
|
+
private readonly store;
|
|
31
|
+
private readonly storageKey;
|
|
32
|
+
constructor(options: DeviceDogfoodOptions);
|
|
33
|
+
private identity;
|
|
34
|
+
enrollmentInfo(): Promise<{
|
|
35
|
+
appId: string;
|
|
36
|
+
installationId: string;
|
|
37
|
+
registrationSlot: string;
|
|
38
|
+
publicKey: string;
|
|
39
|
+
}>;
|
|
40
|
+
enroll(platform?: string): Promise<{
|
|
41
|
+
status: DeviceDogfoodState;
|
|
42
|
+
installationId: string;
|
|
43
|
+
}>;
|
|
44
|
+
status(): Promise<DeviceDogfoodState>;
|
|
45
|
+
session(): Promise<DeviceDogfoodSession | null>;
|
|
46
|
+
/** Rotate the key but keep the logical slot. Approval supersedes the prior
|
|
47
|
+
* generation, making its already-issued sessions fail immediately. */
|
|
48
|
+
reRegister(platform?: string): Promise<{
|
|
49
|
+
status: DeviceDogfoodState;
|
|
50
|
+
installationId: string;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
exports.YaverDeviceDogfood = void 0;
|
|
7
|
+
const tweetnacl_1 = __importDefault(require("tweetnacl"));
|
|
8
|
+
const buffer_1 = require("buffer");
|
|
9
|
+
const auth_1 = require("./auth");
|
|
10
|
+
function b64(bytes) { return buffer_1.Buffer.from(bytes).toString('base64'); }
|
|
11
|
+
function fromB64(value) { return new Uint8Array(buffer_1.Buffer.from(value, 'base64')); }
|
|
12
|
+
function b64url(bytes) {
|
|
13
|
+
return buffer_1.Buffer.from(bytes).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
14
|
+
}
|
|
15
|
+
function randomBytes(length) {
|
|
16
|
+
const cryptoObject = globalThis?.crypto;
|
|
17
|
+
if (cryptoObject?.getRandomValues) {
|
|
18
|
+
const out = new Uint8Array(length);
|
|
19
|
+
cryptoObject.getRandomValues(out);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return require('expo-crypto').getRandomBytes(length);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error('Secure randomness is unavailable. Install expo-crypto or provide global crypto.getRandomValues.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function defaultSecureStore() {
|
|
30
|
+
try {
|
|
31
|
+
const store = require('expo-secure-store');
|
|
32
|
+
if (store?.getItemAsync && store?.setItemAsync)
|
|
33
|
+
return store;
|
|
34
|
+
}
|
|
35
|
+
catch { /* named error below */ }
|
|
36
|
+
throw new Error('Secure Dogfood identity storage is unavailable. Install expo-secure-store or pass secureStore.');
|
|
37
|
+
}
|
|
38
|
+
async function responseJSON(response) {
|
|
39
|
+
const body = await response.json().catch(() => ({}));
|
|
40
|
+
if (!response.ok)
|
|
41
|
+
throw new Error(body?.error || `Yaver Dogfood request failed (HTTP ${response.status})`);
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
function enrollmentMessage(appId, installationId, challenge) {
|
|
45
|
+
return new TextEncoder().encode(`yaver-dogfood-enroll-v1\n${appId}\n${installationId}\n${challenge}`);
|
|
46
|
+
}
|
|
47
|
+
function sessionMessage(appId, installationId, challenge) {
|
|
48
|
+
return new TextEncoder().encode(`yaver-dogfood-session-v1\n${appId}\n${installationId}\n${challenge}`);
|
|
49
|
+
}
|
|
50
|
+
/** Account-free third-party Dogfood enrollment. The UUID is a public lookup
|
|
51
|
+
* handle; possession is proven by the private key retained in this app. */
|
|
52
|
+
class YaverDeviceDogfood {
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.options = options;
|
|
55
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{2,127}$/.test(options.appId))
|
|
56
|
+
throw new Error('A valid Dogfood appId is required');
|
|
57
|
+
this.backendUrl = (options.backendUrl || auth_1.DEFAULT_CONVEX_SITE_URL).replace(/\/$/, '');
|
|
58
|
+
this.store = options.secureStore || defaultSecureStore();
|
|
59
|
+
this.storageKey = `yaver_feedback_dogfood_v1_${options.appId}`;
|
|
60
|
+
}
|
|
61
|
+
async identity() {
|
|
62
|
+
const raw = await this.store.getItemAsync(this.storageKey);
|
|
63
|
+
if (raw) {
|
|
64
|
+
try {
|
|
65
|
+
const saved = JSON.parse(raw);
|
|
66
|
+
if (fromB64(saved.publicKey).length === tweetnacl_1.default.sign.publicKeyLength && fromB64(saved.secretKey).length === tweetnacl_1.default.sign.secretKeyLength)
|
|
67
|
+
return saved;
|
|
68
|
+
}
|
|
69
|
+
catch { /* rotate corrupt identity below */ }
|
|
70
|
+
}
|
|
71
|
+
const seed = randomBytes(32);
|
|
72
|
+
const pair = tweetnacl_1.default.sign.keyPair.fromSeed(seed);
|
|
73
|
+
seed.fill(0);
|
|
74
|
+
const identity = {
|
|
75
|
+
installationId: b64url(randomBytes(18)), registrationSlot: b64url(randomBytes(18)),
|
|
76
|
+
publicKey: b64(pair.publicKey), secretKey: b64(pair.secretKey),
|
|
77
|
+
};
|
|
78
|
+
await this.store.setItemAsync(this.storageKey, JSON.stringify(identity));
|
|
79
|
+
return identity;
|
|
80
|
+
}
|
|
81
|
+
async enrollmentInfo() {
|
|
82
|
+
const identity = await this.identity();
|
|
83
|
+
return { appId: this.options.appId, installationId: identity.installationId, registrationSlot: identity.registrationSlot, publicKey: identity.publicKey };
|
|
84
|
+
}
|
|
85
|
+
async enroll(platform = 'unknown') {
|
|
86
|
+
const identity = await this.identity();
|
|
87
|
+
const started = await responseJSON(await fetch(`${this.backendUrl}/dogfood/enroll/start`, {
|
|
88
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId, registrationSlot: identity.registrationSlot, publicKey: identity.publicKey, platform, label: this.options.label }),
|
|
90
|
+
}));
|
|
91
|
+
if (started.status === 'active')
|
|
92
|
+
return { status: 'active', installationId: identity.installationId };
|
|
93
|
+
const signature = b64(tweetnacl_1.default.sign.detached(enrollmentMessage(this.options.appId, identity.installationId, started.challenge), fromB64(identity.secretKey)));
|
|
94
|
+
await responseJSON(await fetch(`${this.backendUrl}/dogfood/enroll/prove`, {
|
|
95
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId, signature }),
|
|
97
|
+
}));
|
|
98
|
+
return { status: 'pending', installationId: identity.installationId };
|
|
99
|
+
}
|
|
100
|
+
async status() {
|
|
101
|
+
const identity = await this.identity();
|
|
102
|
+
const response = await fetch(`${this.backendUrl}/dogfood/enroll/status?appId=${encodeURIComponent(this.options.appId)}&installationId=${encodeURIComponent(identity.installationId)}`);
|
|
103
|
+
if (response.status === 404)
|
|
104
|
+
return 'unregistered';
|
|
105
|
+
return (await responseJSON(response)).status;
|
|
106
|
+
}
|
|
107
|
+
async session() {
|
|
108
|
+
const identity = await this.identity();
|
|
109
|
+
if (await this.status() !== 'active')
|
|
110
|
+
return null;
|
|
111
|
+
const challenge = await responseJSON(await fetch(`${this.backendUrl}/dogfood/session/challenge`, {
|
|
112
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId }),
|
|
114
|
+
}));
|
|
115
|
+
const signature = b64(tweetnacl_1.default.sign.detached(sessionMessage(this.options.appId, identity.installationId, challenge.challenge), fromB64(identity.secretKey)));
|
|
116
|
+
const result = await responseJSON(await fetch(`${this.backendUrl}/dogfood/session`, {
|
|
117
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId, signature }),
|
|
119
|
+
}));
|
|
120
|
+
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 };
|
|
121
|
+
}
|
|
122
|
+
/** Rotate the key but keep the logical slot. Approval supersedes the prior
|
|
123
|
+
* generation, making its already-issued sessions fail immediately. */
|
|
124
|
+
async reRegister(platform = 'unknown') {
|
|
125
|
+
const previous = await this.identity();
|
|
126
|
+
const seed = randomBytes(32);
|
|
127
|
+
const pair = tweetnacl_1.default.sign.keyPair.fromSeed(seed);
|
|
128
|
+
seed.fill(0);
|
|
129
|
+
const next = {
|
|
130
|
+
installationId: b64url(randomBytes(18)), registrationSlot: previous.registrationSlot,
|
|
131
|
+
publicKey: b64(pair.publicKey), secretKey: b64(pair.secretKey),
|
|
132
|
+
};
|
|
133
|
+
await this.store.setItemAsync(this.storageKey, JSON.stringify(next));
|
|
134
|
+
return this.enroll(platform);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.YaverDeviceDogfood = YaverDeviceDogfood;
|