dextunnel 0.1.0
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/LICENSE +211 -0
- package/README.md +112 -0
- package/SECURITY.md +27 -0
- package/SUPPORT.md +43 -0
- package/package.json +44 -0
- package/public/client-shared.js +1831 -0
- package/public/favicon.svg +11 -0
- package/public/host.html +29 -0
- package/public/host.js +2079 -0
- package/public/index.html +28 -0
- package/public/index.js +98 -0
- package/public/live-bridge-lifecycle.js +258 -0
- package/public/live-bridge-retry-state.js +61 -0
- package/public/live-selection-intent.js +79 -0
- package/public/remote-operator-state.js +316 -0
- package/public/remote.html +167 -0
- package/public/remote.js +3967 -0
- package/public/styles.css +2793 -0
- package/public/surface-view-state.js +89 -0
- package/public/voice-dictation.js +45 -0
- package/src/bin/desktop-rehydration-smoke.mjs +111 -0
- package/src/bin/dextunnel.mjs +41 -0
- package/src/bin/doctor.mjs +48 -0
- package/src/bin/launch-attest.mjs +39 -0
- package/src/bin/launch-status.mjs +49 -0
- package/src/bin/mobile-link-proxy.mjs +221 -0
- package/src/bin/mobile-proof.mjs +164 -0
- package/src/bin/mobile-transport-smoke.mjs +200 -0
- package/src/bin/probe-codex-app-server-write.mjs +36 -0
- package/src/bin/probe-codex-app-server.mjs +30 -0
- package/src/lib/agent-room-context.mjs +54 -0
- package/src/lib/agent-room-runtime.mjs +355 -0
- package/src/lib/agent-room-service.mjs +335 -0
- package/src/lib/agent-room-state.mjs +406 -0
- package/src/lib/agent-room-store.mjs +71 -0
- package/src/lib/agent-room-text.mjs +48 -0
- package/src/lib/app-server-contract.mjs +66 -0
- package/src/lib/app-server-runtime.mjs +60 -0
- package/src/lib/attachment-service.mjs +119 -0
- package/src/lib/bridge-api-handler.mjs +719 -0
- package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
- package/src/lib/bridge-status-builder.mjs +60 -0
- package/src/lib/codex-app-server-client.mjs +1511 -0
- package/src/lib/companion-state.mjs +453 -0
- package/src/lib/control-lease-service.mjs +180 -0
- package/src/lib/debug-harness-service.mjs +173 -0
- package/src/lib/desktop-integration.mjs +146 -0
- package/src/lib/desktop-rehydration-smoke.mjs +269 -0
- package/src/lib/dextunnel-cli.mjs +122 -0
- package/src/lib/discovery-docs.mjs +1321 -0
- package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
- package/src/lib/install-preflight.mjs +373 -0
- package/src/lib/interaction-resolution-service.mjs +185 -0
- package/src/lib/interaction-state.mjs +360 -0
- package/src/lib/launch-release-bar.mjs +158 -0
- package/src/lib/live-control-state.mjs +107 -0
- package/src/lib/live-payload-builder.mjs +298 -0
- package/src/lib/live-selection-transition-state.mjs +49 -0
- package/src/lib/live-transcript-state.mjs +549 -0
- package/src/lib/mobile-network-profile.mjs +39 -0
- package/src/lib/mock-codex-adapter.mjs +62 -0
- package/src/lib/operator-diagnostics.mjs +82 -0
- package/src/lib/repo-changes-service.mjs +527 -0
- package/src/lib/runtime-config.mjs +106 -0
- package/src/lib/selection-state-service.mjs +214 -0
- package/src/lib/session-store.mjs +355 -0
- package/src/lib/shared-room-state.mjs +473 -0
- package/src/lib/shared-selection-state.mjs +40 -0
- package/src/lib/sse-hub.mjs +35 -0
- package/src/lib/static-surface-service.mjs +71 -0
- package/src/lib/surface-access.mjs +189 -0
- package/src/lib/surface-presence-service.mjs +118 -0
- package/src/lib/surface-request-guard.mjs +52 -0
- package/src/lib/thread-sync-state.mjs +536 -0
- package/src/lib/watcher-lifecycle.mjs +287 -0
- package/src/server.mjs +1446 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { normalizeSurfaceName } from "./shared-room-state.mjs";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_SURFACE_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
export const SURFACE_CAPABILITIES = {
|
|
10
|
+
agent: [
|
|
11
|
+
"read_room",
|
|
12
|
+
"select_room",
|
|
13
|
+
"refresh_room",
|
|
14
|
+
"respond_interaction",
|
|
15
|
+
"control_remote",
|
|
16
|
+
"send_turn"
|
|
17
|
+
],
|
|
18
|
+
host: [
|
|
19
|
+
"read_room",
|
|
20
|
+
"select_room",
|
|
21
|
+
"sync_presence",
|
|
22
|
+
"refresh_room",
|
|
23
|
+
"open_in_codex",
|
|
24
|
+
"respond_interaction",
|
|
25
|
+
"release_remote_control",
|
|
26
|
+
"debug_tools"
|
|
27
|
+
],
|
|
28
|
+
remote: [
|
|
29
|
+
"read_room",
|
|
30
|
+
"select_room",
|
|
31
|
+
"sync_presence",
|
|
32
|
+
"refresh_room",
|
|
33
|
+
"open_in_codex",
|
|
34
|
+
"respond_interaction",
|
|
35
|
+
"control_remote",
|
|
36
|
+
"send_turn",
|
|
37
|
+
"use_companion",
|
|
38
|
+
"use_agent_room"
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function defaultSurfaceAccessSecretPath({ cwd = process.cwd() } = {}) {
|
|
43
|
+
return path.join(cwd, ".agent", "artifacts", "runtime", "surface-access-secret.txt");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function signatureFor(secret, payload) {
|
|
47
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function encodePayload(value) {
|
|
51
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function decodePayload(value) {
|
|
55
|
+
return JSON.parse(Buffer.from(String(value || ""), "base64url").toString("utf8"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function surfaceAccessError(message, statusCode = 403) {
|
|
59
|
+
const error = new Error(message);
|
|
60
|
+
error.statusCode = statusCode;
|
|
61
|
+
return error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function loadOrCreateSurfaceAccessSecret({
|
|
65
|
+
secretPath = defaultSurfaceAccessSecretPath()
|
|
66
|
+
} = {}) {
|
|
67
|
+
try {
|
|
68
|
+
const existing = (await readFile(secretPath, "utf8")).trim();
|
|
69
|
+
if (existing) {
|
|
70
|
+
return existing;
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
|
|
74
|
+
const secret = randomBytes(32).toString("base64url");
|
|
75
|
+
await mkdir(path.dirname(secretPath), { recursive: true });
|
|
76
|
+
await writeFile(secretPath, `${secret}\n`, "utf8");
|
|
77
|
+
return secret;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createSurfaceAccessRegistry({
|
|
81
|
+
now = () => new Date().toISOString(),
|
|
82
|
+
nowMs = () => Date.now(),
|
|
83
|
+
ttlMs = DEFAULT_SURFACE_TOKEN_TTL_MS,
|
|
84
|
+
secret
|
|
85
|
+
} = {}) {
|
|
86
|
+
if (!secret) {
|
|
87
|
+
throw new Error("createSurfaceAccessRegistry requires a signing secret.");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function issueBootstrap(surface = "remote") {
|
|
91
|
+
const nextSurface = normalizeSurfaceName(surface);
|
|
92
|
+
const capabilities = SURFACE_CAPABILITIES[nextSurface];
|
|
93
|
+
if (!capabilities) {
|
|
94
|
+
throw surfaceAccessError(`Unsupported surface: ${surface}`, 400);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const payload = {
|
|
98
|
+
clientId: `${nextSurface}-${randomBytes(10).toString("base64url")}`,
|
|
99
|
+
issuedAt: now(),
|
|
100
|
+
expiresAt: new Date(nowMs() + ttlMs).toISOString(),
|
|
101
|
+
nonce: randomBytes(12).toString("base64url"),
|
|
102
|
+
surface: nextSurface
|
|
103
|
+
};
|
|
104
|
+
const encoded = encodePayload(payload);
|
|
105
|
+
const signature = signatureFor(secret, encoded);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
accessToken: `${encoded}.${signature}`,
|
|
109
|
+
capabilities: [...capabilities],
|
|
110
|
+
clientId: payload.clientId,
|
|
111
|
+
expiresAt: payload.expiresAt,
|
|
112
|
+
issuedAt: payload.issuedAt,
|
|
113
|
+
surface: payload.surface
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolve({ headers = {}, searchParams = null } = {}) {
|
|
118
|
+
const bearerToken = String(headers.authorization || "")
|
|
119
|
+
.replace(/^Bearer\s+/i, "")
|
|
120
|
+
.trim();
|
|
121
|
+
const headerToken = String(headers["x-dextunnel-surface-token"] || "").trim();
|
|
122
|
+
const queryToken = String(searchParams?.get?.("surfaceToken") || "").trim();
|
|
123
|
+
const token = bearerToken || headerToken || queryToken;
|
|
124
|
+
if (!token || !token.includes(".")) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const [encoded, signature] = token.split(".", 2);
|
|
129
|
+
if (!encoded || !signature) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const expected = signatureFor(secret, encoded);
|
|
134
|
+
if (signature !== expected) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let payload = null;
|
|
139
|
+
try {
|
|
140
|
+
payload = decodePayload(encoded);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const surface = normalizeSurfaceName(payload.surface);
|
|
146
|
+
const capabilities = SURFACE_CAPABILITIES[surface];
|
|
147
|
+
if (!capabilities) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (payload.expiresAt && new Date(payload.expiresAt).getTime() <= nowMs()) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
accessToken: token,
|
|
156
|
+
capabilities: [...capabilities],
|
|
157
|
+
clientId: String(payload.clientId || "").trim() || null,
|
|
158
|
+
expiresAt: payload.expiresAt || null,
|
|
159
|
+
issuedAt: payload.issuedAt || null,
|
|
160
|
+
surface
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function requireCapability({ capability, headers = {}, searchParams = null } = {}) {
|
|
165
|
+
const access = resolve({ headers, searchParams });
|
|
166
|
+
if (!access) {
|
|
167
|
+
throw surfaceAccessError("Dextunnel surface access is missing or expired.");
|
|
168
|
+
}
|
|
169
|
+
if (!access.capabilities.includes(capability)) {
|
|
170
|
+
throw surfaceAccessError(
|
|
171
|
+
`${access.surface} surface is not allowed to ${capability.replaceAll("_", " ")}.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return access;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
issueBootstrap,
|
|
179
|
+
requireCapability,
|
|
180
|
+
resolve
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function injectSurfaceBootstrap(html, bootstrap) {
|
|
185
|
+
const script = `<script>window.__DEXTUNNEL_SURFACE_BOOTSTRAP__ = ${JSON.stringify(bootstrap)};</script>`;
|
|
186
|
+
return html.includes("</body>")
|
|
187
|
+
? html.replace("</body>", ` ${script}\n </body>`)
|
|
188
|
+
: `${html}\n${script}\n`;
|
|
189
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export function createSurfacePresenceService({
|
|
2
|
+
appServerState,
|
|
3
|
+
applySurfacePresenceUpdateState,
|
|
4
|
+
buildSelectedAttachmentsState,
|
|
5
|
+
countSurfacePresenceState,
|
|
6
|
+
defaultStaleMs = null,
|
|
7
|
+
liveState,
|
|
8
|
+
normalizeSurfaceName,
|
|
9
|
+
nowIso = () => new Date().toISOString(),
|
|
10
|
+
pruneStaleSurfacePresenceState,
|
|
11
|
+
randomId = () => `${Date.now()}`
|
|
12
|
+
} = {}) {
|
|
13
|
+
function recordSurfaceEvent({ action, cause = "", surface = "", threadId = null } = {}) {
|
|
14
|
+
const nextThreadId = String(threadId || "").trim();
|
|
15
|
+
const nextAction = action === "attach" ? "attach" : action === "detach" ? "detach" : "";
|
|
16
|
+
const nextSurface = normalizeSurfaceName(surface);
|
|
17
|
+
if (!nextThreadId || !nextAction || !nextSurface) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const event = {
|
|
22
|
+
action: nextAction,
|
|
23
|
+
at: nowIso(),
|
|
24
|
+
cause: String(cause || "").trim() || null,
|
|
25
|
+
id: randomId(),
|
|
26
|
+
surface: nextSurface,
|
|
27
|
+
threadId: nextThreadId
|
|
28
|
+
};
|
|
29
|
+
appServerState.lastSurfaceEvent = event;
|
|
30
|
+
return event;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pruneStaleSurfacePresence({ now = Date.now(), staleMs = defaultStaleMs } = {}) {
|
|
34
|
+
if (!Number.isFinite(staleMs)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result = pruneStaleSurfacePresenceState(liveState.surfacePresenceByClientId, {
|
|
39
|
+
now,
|
|
40
|
+
staleMs
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (result.changed) {
|
|
44
|
+
liveState.surfacePresenceByClientId = result.nextPresenceByClientId;
|
|
45
|
+
for (const event of result.events) {
|
|
46
|
+
recordSurfaceEvent(event);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result.changed;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function countSurfacePresence(threadId, surface) {
|
|
54
|
+
return countSurfacePresenceState(liveState.surfacePresenceByClientId, threadId, surface);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function applySurfacePresenceUpdate(payload = {}, { now = Date.now(), selectedThreadId = "" } = {}) {
|
|
58
|
+
const result = applySurfacePresenceUpdateState(liveState.surfacePresenceByClientId, payload, {
|
|
59
|
+
now,
|
|
60
|
+
selectedThreadId
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (result.changed) {
|
|
64
|
+
liveState.surfacePresenceByClientId = result.nextPresenceByClientId;
|
|
65
|
+
for (const event of result.events) {
|
|
66
|
+
recordSurfaceEvent(event);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result.changed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function upsertSurfacePresence(payload = {}, { now = Date.now(), selectedThreadId = liveState.selectedThreadId || "" } = {}) {
|
|
74
|
+
pruneStaleSurfacePresence({ now });
|
|
75
|
+
return applySurfacePresenceUpdate(
|
|
76
|
+
{
|
|
77
|
+
...payload,
|
|
78
|
+
detach: false,
|
|
79
|
+
threadId: payload.threadId || liveState.selectedThreadId || ""
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
now,
|
|
83
|
+
selectedThreadId
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function removeSurfacePresence(clientId, { now = Date.now(), selectedThreadId = liveState.selectedThreadId || "" } = {}) {
|
|
89
|
+
return applySurfacePresenceUpdate(
|
|
90
|
+
{
|
|
91
|
+
clientId,
|
|
92
|
+
detach: true
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
now,
|
|
96
|
+
selectedThreadId
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildSelectedAttachments(
|
|
102
|
+
threadId = liveState.selectedThreadId || liveState.selectedThreadSnapshot?.thread?.id || null,
|
|
103
|
+
{ now = Date.now(), staleMs = defaultStaleMs } = {}
|
|
104
|
+
) {
|
|
105
|
+
pruneStaleSurfacePresence({ now, staleMs });
|
|
106
|
+
return buildSelectedAttachmentsState(liveState.surfacePresenceByClientId, threadId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
applySurfacePresenceUpdate,
|
|
111
|
+
buildSelectedAttachments,
|
|
112
|
+
countSurfacePresence,
|
|
113
|
+
pruneStaleSurfacePresence,
|
|
114
|
+
recordSurfaceEvent,
|
|
115
|
+
removeSurfacePresence,
|
|
116
|
+
upsertSurfacePresence
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function normalizeAddress(value) {
|
|
2
|
+
return String(value || "").trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function normalizeComparableAddress(value) {
|
|
6
|
+
const address = normalizeAddress(value);
|
|
7
|
+
if (address.startsWith("::ffff:")) {
|
|
8
|
+
return address.slice(7);
|
|
9
|
+
}
|
|
10
|
+
return address;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isLoopbackAddress(value) {
|
|
14
|
+
const address = normalizeComparableAddress(value);
|
|
15
|
+
if (!address) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
address === "::1" ||
|
|
21
|
+
address === "::ffff:127.0.0.1" ||
|
|
22
|
+
address === "127.0.0.1" ||
|
|
23
|
+
address.startsWith("127.")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isSameMachineAddress(remoteAddress, localAddress) {
|
|
28
|
+
const remote = normalizeComparableAddress(remoteAddress);
|
|
29
|
+
const local = normalizeComparableAddress(localAddress);
|
|
30
|
+
if (!remote || !local) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return remote === local;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function canServeSurfaceBootstrap({
|
|
37
|
+
exposeHostSurface = false,
|
|
38
|
+
localAddress = "",
|
|
39
|
+
pathname = "",
|
|
40
|
+
remoteAddress = ""
|
|
41
|
+
} = {}) {
|
|
42
|
+
const nextPath = String(pathname || "").trim();
|
|
43
|
+
if (nextPath !== "/host.html" && nextPath !== "host.html") {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
Boolean(exposeHostSurface) ||
|
|
49
|
+
isLoopbackAddress(remoteAddress) ||
|
|
50
|
+
isSameMachineAddress(remoteAddress, localAddress)
|
|
51
|
+
);
|
|
52
|
+
}
|