@neuralnomads/codenomad-dev 0.11.1-dev-20260216-1a0734c6 → 0.11.2-dev-20260218-127a1f62
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/dist/opencode-config/package.json +2 -2
- package/dist/server/http-server.js +119 -9
- package/dist/server/routes/auth-pages/login.html +2 -1
- package/dist/server/routes/auth.js +15 -1
- package/package.json +1 -1
- package/public/assets/index-9jdTcSzh.css +1 -0
- package/public/assets/index-B3GrzRpx.js +1 -0
- package/public/assets/{loading-C-ZTLd91.js → loading-BOqsz5th.js} +1 -1
- package/public/assets/main-CFk-S6k5.js +192 -0
- package/public/index.html +3 -3
- package/public/loading.html +3 -3
- package/public/sw.js +1 -1
- package/public/ui-version.json +1 -1
- package/public/assets/index-DIh36dcI.js +0 -1
- package/public/assets/index-IUhCGbWv.css +0 -1
- package/public/assets/main-D8EyTa5Y.js +0 -190
|
@@ -282,6 +282,20 @@ function registerInstanceProxyRoutes(app, deps) {
|
|
|
282
282
|
});
|
|
283
283
|
}
|
|
284
284
|
const INSTANCE_PROXY_HOST = "127.0.0.1";
|
|
285
|
+
// Special-case OpenCode directory override.
|
|
286
|
+
//
|
|
287
|
+
// UI clients may need to scope certain requests to an arbitrary directory that is not
|
|
288
|
+
// part of the Git worktree list. Since the OpenCode SDK does not reliably support
|
|
289
|
+
// injecting per-request headers, we encode an override into the *path* and strip it
|
|
290
|
+
// before proxying to the instance.
|
|
291
|
+
//
|
|
292
|
+
// Example proxied request path:
|
|
293
|
+
// /workspaces/:id/worktrees/:slug/instance/__dir/<base64url>/session/create
|
|
294
|
+
//
|
|
295
|
+
// The server will decode <base64url> -> absolute directory, validate it, then set
|
|
296
|
+
// x-opencode-directory accordingly and forward the request to /session/create.
|
|
297
|
+
const OPENCODE_DIR_OVERRIDE_PREFIX = "__dir/";
|
|
298
|
+
const OPENCODE_DIR_OVERRIDE_MAX_LEN = 4096;
|
|
285
299
|
async function proxyWorkspaceRequest(args) {
|
|
286
300
|
const { request, reply, workspaceManager, logger, worktreeSlug } = args;
|
|
287
301
|
const workspaceId = request.params.id;
|
|
@@ -357,17 +371,43 @@ async function proxyWorkspaceRequest(args) {
|
|
|
357
371
|
reply.code(400).send({ error: "Invalid worktree slug" });
|
|
358
372
|
return;
|
|
359
373
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
reply.code(404).send({ error: "Worktree not found" });
|
|
374
|
+
let extracted;
|
|
375
|
+
try {
|
|
376
|
+
extracted = extractOpencodeDirectoryOverride(args.pathSuffix);
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
const message = error instanceof Error ? error.message : "Invalid directory override";
|
|
380
|
+
reply.code(400).send({ error: message });
|
|
368
381
|
return;
|
|
369
382
|
}
|
|
370
|
-
|
|
383
|
+
let directory = null;
|
|
384
|
+
let forwardedSuffix = extracted.forwardedSuffix;
|
|
385
|
+
if (extracted.overrideDirectory) {
|
|
386
|
+
try {
|
|
387
|
+
directory = validateAndNormalizeOverrideDirectory({
|
|
388
|
+
overrideDirectory: extracted.overrideDirectory,
|
|
389
|
+
workspaceRoot: workspace.path,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
const message = error instanceof Error ? error.message : "Invalid directory override";
|
|
394
|
+
reply.code(400).send({ error: message });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
directory = await resolveWorktreeDirectory({
|
|
400
|
+
workspaceId,
|
|
401
|
+
workspacePath: workspace.path,
|
|
402
|
+
worktreeSlug,
|
|
403
|
+
logger,
|
|
404
|
+
});
|
|
405
|
+
if (!directory) {
|
|
406
|
+
reply.code(404).send({ error: "Worktree not found" });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const normalizedSuffix = normalizeInstanceSuffix(forwardedSuffix);
|
|
371
411
|
const queryIndex = (request.raw.url ?? "").indexOf("?");
|
|
372
412
|
const search = queryIndex >= 0 ? (request.raw.url ?? "").slice(queryIndex) : "";
|
|
373
413
|
const targetUrl = `http://${INSTANCE_PROXY_HOST}:${port}${normalizedSuffix}${search}`;
|
|
@@ -418,6 +458,76 @@ async function proxyWorkspaceRequest(args) {
|
|
|
418
458
|
},
|
|
419
459
|
});
|
|
420
460
|
}
|
|
461
|
+
function extractOpencodeDirectoryOverride(pathSuffix) {
|
|
462
|
+
if (!pathSuffix) {
|
|
463
|
+
return { overrideDirectory: null, forwardedSuffix: pathSuffix };
|
|
464
|
+
}
|
|
465
|
+
// Fastify wildcard param does not include a leading slash.
|
|
466
|
+
const trimmed = pathSuffix.replace(/^\/+/, "");
|
|
467
|
+
if (!trimmed.startsWith(OPENCODE_DIR_OVERRIDE_PREFIX)) {
|
|
468
|
+
return { overrideDirectory: null, forwardedSuffix: pathSuffix };
|
|
469
|
+
}
|
|
470
|
+
const rest = trimmed.slice(OPENCODE_DIR_OVERRIDE_PREFIX.length);
|
|
471
|
+
const slashIndex = rest.indexOf("/");
|
|
472
|
+
const encoded = (slashIndex >= 0 ? rest.slice(0, slashIndex) : rest).trim();
|
|
473
|
+
const remaining = slashIndex >= 0 ? rest.slice(slashIndex + 1) : "";
|
|
474
|
+
if (!encoded) {
|
|
475
|
+
throw new Error("Missing directory override");
|
|
476
|
+
}
|
|
477
|
+
if (encoded.length > OPENCODE_DIR_OVERRIDE_MAX_LEN) {
|
|
478
|
+
throw new Error("Directory override too large");
|
|
479
|
+
}
|
|
480
|
+
let overrideDirectory = "";
|
|
481
|
+
try {
|
|
482
|
+
overrideDirectory = decodeBase64Url(encoded);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
throw new Error("Invalid directory override");
|
|
486
|
+
}
|
|
487
|
+
const forwardedSuffix = remaining;
|
|
488
|
+
return { overrideDirectory, forwardedSuffix };
|
|
489
|
+
}
|
|
490
|
+
function decodeBase64Url(input) {
|
|
491
|
+
// base64url -> base64
|
|
492
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
493
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
|
|
494
|
+
const base64 = `${normalized}${padding}`;
|
|
495
|
+
return Buffer.from(base64, "base64").toString("utf-8");
|
|
496
|
+
}
|
|
497
|
+
function validateAndNormalizeOverrideDirectory(params) {
|
|
498
|
+
const raw = params.overrideDirectory.trim();
|
|
499
|
+
if (!raw) {
|
|
500
|
+
throw new Error("Override directory is empty");
|
|
501
|
+
}
|
|
502
|
+
if (!path.isAbsolute(raw)) {
|
|
503
|
+
throw new Error("Override directory must be an absolute path");
|
|
504
|
+
}
|
|
505
|
+
if (!fs.existsSync(raw)) {
|
|
506
|
+
throw new Error(`Override directory does not exist: ${raw}`);
|
|
507
|
+
}
|
|
508
|
+
const stats = fs.statSync(raw);
|
|
509
|
+
if (!stats.isDirectory()) {
|
|
510
|
+
throw new Error(`Override path is not a directory: ${raw}`);
|
|
511
|
+
}
|
|
512
|
+
const normalizedOverride = fs.realpathSync(raw);
|
|
513
|
+
const normalizedRoot = fs.realpathSync(params.workspaceRoot);
|
|
514
|
+
if (!isSubpath(normalizedOverride, normalizedRoot)) {
|
|
515
|
+
throw new Error("Override directory must be within the workspace root");
|
|
516
|
+
}
|
|
517
|
+
return normalizedOverride;
|
|
518
|
+
}
|
|
519
|
+
function isSubpath(candidate, root) {
|
|
520
|
+
const rel = path.relative(root, candidate);
|
|
521
|
+
if (rel === "")
|
|
522
|
+
return true;
|
|
523
|
+
if (rel === "..")
|
|
524
|
+
return false;
|
|
525
|
+
if (rel.startsWith(`..${path.sep}`))
|
|
526
|
+
return false;
|
|
527
|
+
if (path.isAbsolute(rel))
|
|
528
|
+
return false;
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
421
531
|
function normalizeInstanceSuffix(pathSuffix) {
|
|
422
532
|
if (!pathSuffix || pathSuffix === "/") {
|
|
423
533
|
return "/";
|
|
@@ -119,7 +119,8 @@
|
|
|
119
119
|
showError(message || `Login failed (${res.status})`)
|
|
120
120
|
return
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
// Replace history entry so Back doesn't return to /login.
|
|
123
|
+
window.location.replace("/")
|
|
123
124
|
} catch (e) {
|
|
124
125
|
showError(e && e.message ? e.message : String(e))
|
|
125
126
|
}
|
|
@@ -35,7 +35,17 @@ function getTokenHtml() {
|
|
|
35
35
|
return cachedTokenTemplate;
|
|
36
36
|
}
|
|
37
37
|
export function registerAuthRoutes(app, deps) {
|
|
38
|
-
app.get("/login", async (
|
|
38
|
+
app.get("/login", async (request, reply) => {
|
|
39
|
+
// If already authenticated, don't show the login page.
|
|
40
|
+
const session = deps.authManager.getSessionFromRequest(request);
|
|
41
|
+
if (session) {
|
|
42
|
+
reply.redirect("/");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Avoid caching the login page (helps with bfcache/back behavior).
|
|
46
|
+
reply.header("Cache-Control", "no-store");
|
|
47
|
+
reply.header("Pragma", "no-cache");
|
|
48
|
+
reply.header("Expires", "0");
|
|
39
49
|
const status = deps.authManager.getStatus();
|
|
40
50
|
reply.type("text/html").send(getLoginHtml(status.username));
|
|
41
51
|
});
|
|
@@ -48,6 +58,10 @@ export function registerAuthRoutes(app, deps) {
|
|
|
48
58
|
reply.code(404).send({ error: "Not found" });
|
|
49
59
|
return;
|
|
50
60
|
}
|
|
61
|
+
// Avoid caching the token bootstrap page.
|
|
62
|
+
reply.header("Cache-Control", "no-store");
|
|
63
|
+
reply.header("Pragma", "no-cache");
|
|
64
|
+
reply.header("Expires", "0");
|
|
51
65
|
reply.type("text/html").send(getTokenHtml());
|
|
52
66
|
});
|
|
53
67
|
app.get("/api/auth/status", async (request, reply) => {
|