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.
Files changed (76) hide show
  1. package/LICENSE +211 -0
  2. package/README.md +112 -0
  3. package/SECURITY.md +27 -0
  4. package/SUPPORT.md +43 -0
  5. package/package.json +44 -0
  6. package/public/client-shared.js +1831 -0
  7. package/public/favicon.svg +11 -0
  8. package/public/host.html +29 -0
  9. package/public/host.js +2079 -0
  10. package/public/index.html +28 -0
  11. package/public/index.js +98 -0
  12. package/public/live-bridge-lifecycle.js +258 -0
  13. package/public/live-bridge-retry-state.js +61 -0
  14. package/public/live-selection-intent.js +79 -0
  15. package/public/remote-operator-state.js +316 -0
  16. package/public/remote.html +167 -0
  17. package/public/remote.js +3967 -0
  18. package/public/styles.css +2793 -0
  19. package/public/surface-view-state.js +89 -0
  20. package/public/voice-dictation.js +45 -0
  21. package/src/bin/desktop-rehydration-smoke.mjs +111 -0
  22. package/src/bin/dextunnel.mjs +41 -0
  23. package/src/bin/doctor.mjs +48 -0
  24. package/src/bin/launch-attest.mjs +39 -0
  25. package/src/bin/launch-status.mjs +49 -0
  26. package/src/bin/mobile-link-proxy.mjs +221 -0
  27. package/src/bin/mobile-proof.mjs +164 -0
  28. package/src/bin/mobile-transport-smoke.mjs +200 -0
  29. package/src/bin/probe-codex-app-server-write.mjs +36 -0
  30. package/src/bin/probe-codex-app-server.mjs +30 -0
  31. package/src/lib/agent-room-context.mjs +54 -0
  32. package/src/lib/agent-room-runtime.mjs +355 -0
  33. package/src/lib/agent-room-service.mjs +335 -0
  34. package/src/lib/agent-room-state.mjs +406 -0
  35. package/src/lib/agent-room-store.mjs +71 -0
  36. package/src/lib/agent-room-text.mjs +48 -0
  37. package/src/lib/app-server-contract.mjs +66 -0
  38. package/src/lib/app-server-runtime.mjs +60 -0
  39. package/src/lib/attachment-service.mjs +119 -0
  40. package/src/lib/bridge-api-handler.mjs +719 -0
  41. package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
  42. package/src/lib/bridge-status-builder.mjs +60 -0
  43. package/src/lib/codex-app-server-client.mjs +1511 -0
  44. package/src/lib/companion-state.mjs +453 -0
  45. package/src/lib/control-lease-service.mjs +180 -0
  46. package/src/lib/debug-harness-service.mjs +173 -0
  47. package/src/lib/desktop-integration.mjs +146 -0
  48. package/src/lib/desktop-rehydration-smoke.mjs +269 -0
  49. package/src/lib/dextunnel-cli.mjs +122 -0
  50. package/src/lib/discovery-docs.mjs +1321 -0
  51. package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
  52. package/src/lib/install-preflight.mjs +373 -0
  53. package/src/lib/interaction-resolution-service.mjs +185 -0
  54. package/src/lib/interaction-state.mjs +360 -0
  55. package/src/lib/launch-release-bar.mjs +158 -0
  56. package/src/lib/live-control-state.mjs +107 -0
  57. package/src/lib/live-payload-builder.mjs +298 -0
  58. package/src/lib/live-selection-transition-state.mjs +49 -0
  59. package/src/lib/live-transcript-state.mjs +549 -0
  60. package/src/lib/mobile-network-profile.mjs +39 -0
  61. package/src/lib/mock-codex-adapter.mjs +62 -0
  62. package/src/lib/operator-diagnostics.mjs +82 -0
  63. package/src/lib/repo-changes-service.mjs +527 -0
  64. package/src/lib/runtime-config.mjs +106 -0
  65. package/src/lib/selection-state-service.mjs +214 -0
  66. package/src/lib/session-store.mjs +355 -0
  67. package/src/lib/shared-room-state.mjs +473 -0
  68. package/src/lib/shared-selection-state.mjs +40 -0
  69. package/src/lib/sse-hub.mjs +35 -0
  70. package/src/lib/static-surface-service.mjs +71 -0
  71. package/src/lib/surface-access.mjs +189 -0
  72. package/src/lib/surface-presence-service.mjs +118 -0
  73. package/src/lib/surface-request-guard.mjs +52 -0
  74. package/src/lib/thread-sync-state.mjs +536 -0
  75. package/src/lib/watcher-lifecycle.mjs +287 -0
  76. 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
+ }