codex-agent-view 0.4.2 → 0.4.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.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  chmod,
3
+ link,
3
4
  lstat,
4
5
  mkdir,
5
6
  readFile,
@@ -15,6 +16,9 @@ export const LOOPBACK_HOST = "127.0.0.1";
15
16
  export const DEFAULT_PORT = 43127;
16
17
  export const MAX_EVENT_BODY_BYTES = 64 * 1024;
17
18
  export const RUNTIME_SCHEMA_VERSION = 1;
19
+ export const VIEWER_CREDENTIAL_SCHEMA_VERSION = 1;
20
+
21
+ const STRONG_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
18
22
 
19
23
  export function autoStartPort(env = process.env) {
20
24
  const configured = env.CODEX_AGENT_VIEW_AUTO_START_PORT;
@@ -41,10 +45,20 @@ export function runtimeFile(env = process.env) {
41
45
  return join(runtimeDirectory(env), "runtime.json");
42
46
  }
43
47
 
48
+ export function viewerCredentialFile(env = process.env) {
49
+ return join(runtimeDirectory(env), "viewer-auth.json");
50
+ }
51
+
44
52
  export function createRuntimeToken() {
45
53
  return randomBytes(32).toString("base64url");
46
54
  }
47
55
 
56
+ function assertStrongToken(token, message) {
57
+ if (typeof token !== "string" || !STRONG_TOKEN_PATTERN.test(token)) {
58
+ throw new Error(message);
59
+ }
60
+ }
61
+
48
62
  export async function ensurePrivateDirectory(directory) {
49
63
  await rejectSymlink(directory);
50
64
  await mkdir(directory, { recursive: true, mode: 0o700 });
@@ -65,6 +79,117 @@ async function rejectSymlink(path) {
65
79
  }
66
80
  }
67
81
 
82
+ async function requireRegularFile(path, description) {
83
+ const stats = await lstat(path);
84
+ if (stats.isSymbolicLink()) {
85
+ throw new Error(`refusing symbolic link ${description}: ${path}`);
86
+ }
87
+ if (!stats.isFile()) {
88
+ throw new Error(`refusing non-regular ${description}: ${path}`);
89
+ }
90
+ }
91
+
92
+ export async function readViewerToken(env = process.env) {
93
+ const path = viewerCredentialFile(env);
94
+ await rejectSymlink(dirname(path));
95
+ await requireRegularFile(path, "viewer credential path");
96
+ const raw = await readFile(path, "utf8");
97
+ let value;
98
+ try {
99
+ value = JSON.parse(raw);
100
+ } catch {
101
+ throw new Error("invalid Codex Agent View viewer credential file");
102
+ }
103
+ if (
104
+ value === null ||
105
+ typeof value !== "object" ||
106
+ Array.isArray(value) ||
107
+ value.schema_version !== VIEWER_CREDENTIAL_SCHEMA_VERSION
108
+ ) {
109
+ throw new Error("invalid Codex Agent View viewer credential file");
110
+ }
111
+ assertStrongToken(
112
+ value.token,
113
+ "invalid Codex Agent View viewer credential file",
114
+ );
115
+ return value.token;
116
+ }
117
+
118
+ export async function ensureViewerToken(
119
+ env = process.env,
120
+ { seedToken = createRuntimeToken() } = {},
121
+ ) {
122
+ assertStrongToken(seedToken, "invalid Codex Agent View viewer token");
123
+
124
+ const path = viewerCredentialFile(env);
125
+ const directory = dirname(path);
126
+ await ensurePrivateDirectory(directory);
127
+
128
+ try {
129
+ const token = await readViewerToken(env);
130
+ await chmod(path, 0o600);
131
+ return token;
132
+ } catch (error) {
133
+ if (error?.code !== "ENOENT") {
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ const temporaryPath = join(
139
+ directory,
140
+ `.viewer-auth-${process.pid}-${randomBytes(8).toString("hex")}.tmp`,
141
+ );
142
+ const serialized = `${JSON.stringify({
143
+ schema_version: VIEWER_CREDENTIAL_SCHEMA_VERSION,
144
+ token: seedToken,
145
+ }, null, 2)}\n`;
146
+
147
+ await writeFile(temporaryPath, serialized, {
148
+ encoding: "utf8",
149
+ mode: 0o600,
150
+ flag: "wx",
151
+ });
152
+ try {
153
+ await chmod(temporaryPath, 0o600);
154
+ try {
155
+ await link(temporaryPath, path);
156
+ await chmod(path, 0o600);
157
+ return seedToken;
158
+ } catch (error) {
159
+ if (error?.code !== "EEXIST") {
160
+ throw error;
161
+ }
162
+ const token = await readViewerToken(env);
163
+ await chmod(path, 0o600);
164
+ return token;
165
+ }
166
+ } finally {
167
+ await unlink(temporaryPath).catch((error) => {
168
+ if (error?.code !== "ENOENT") {
169
+ throw error;
170
+ }
171
+ });
172
+ }
173
+ }
174
+
175
+ export async function removeViewerToken(expectedToken, env = process.env) {
176
+ assertStrongToken(expectedToken, "invalid Codex Agent View viewer token");
177
+ const path = viewerCredentialFile(env);
178
+ try {
179
+ const currentToken = await readViewerToken(env);
180
+ if (currentToken !== expectedToken) {
181
+ return false;
182
+ }
183
+ await unlink(path);
184
+ return true;
185
+ } catch (error) {
186
+ if (error?.code === "ENOENT") {
187
+ return false;
188
+ }
189
+ throw error;
190
+ }
191
+ }
192
+
68
193
  export async function writeRuntimeInfo(info, env = process.env) {
69
194
  const path = runtimeFile(env);
70
195
  const directory = dirname(path);
@@ -105,6 +230,12 @@ export async function readRuntimeInfo(env = process.env) {
105
230
  ) {
106
231
  throw new Error("invalid Codex Agent View runtime file");
107
232
  }
233
+ if (value.viewer_token !== undefined) {
234
+ assertStrongToken(
235
+ value.viewer_token,
236
+ "invalid Codex Agent View runtime file",
237
+ );
238
+ }
108
239
  return value;
109
240
  }
110
241
 
@@ -90,6 +90,7 @@ export async function startMonitorServer({
90
90
  env = process.env,
91
91
  store = createMonitorStore(),
92
92
  token = createRuntimeToken(),
93
+ viewerToken = createRuntimeToken(),
93
94
  now = Date.now,
94
95
  } = {}) {
95
96
  if (host !== LOOPBACK_HOST) {
@@ -109,13 +110,20 @@ export async function startMonitorServer({
109
110
  return;
110
111
  }
111
112
 
112
- if (requestUrl.pathname.startsWith("/api/") && !hasToken(request, token)) {
113
- sendJson(response, 401, { error: "authorization required" });
113
+ if (
114
+ request.method === "GET" &&
115
+ requestUrl.pathname === "/api/state"
116
+ ) {
117
+ if (!hasToken(request, token) && !hasToken(request, viewerToken)) {
118
+ sendJson(response, 401, { error: "authorization required" });
119
+ return;
120
+ }
121
+ sendJson(response, 200, store.getSnapshot());
114
122
  return;
115
123
  }
116
124
 
117
- if (request.method === "GET" && requestUrl.pathname === "/api/state") {
118
- sendJson(response, 200, store.getSnapshot());
125
+ if (requestUrl.pathname.startsWith("/api/") && !hasToken(request, token)) {
126
+ sendJson(response, 401, { error: "authorization required" });
119
127
  return;
120
128
  }
121
129
 
@@ -182,6 +190,7 @@ export async function startMonitorServer({
182
190
  host,
183
191
  port: address.port,
184
192
  token,
193
+ viewer_token: viewerToken,
185
194
  pid: process.pid,
186
195
  started_at_ms: now(),
187
196
  };
@@ -214,6 +223,6 @@ export async function startMonitorServer({
214
223
  runtimeInfo,
215
224
  server,
216
225
  store,
217
- url: `http://${host}:${address.port}/#token=${encodeURIComponent(token)}`,
226
+ url: `http://${host}:${address.port}/#token=${encodeURIComponent(viewerToken)}`,
218
227
  };
219
228
  }