@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.
@@ -4,6 +4,6 @@
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "dependencies": {
7
- "@opencode-ai/plugin": "1.2.4"
7
+ "@opencode-ai/plugin": "1.2.6"
8
8
  }
9
- }
9
+ }
@@ -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
- const directory = await resolveWorktreeDirectory({
361
- workspaceId,
362
- workspacePath: workspace.path,
363
- worktreeSlug,
364
- logger,
365
- });
366
- if (!directory) {
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
- const normalizedSuffix = normalizeInstanceSuffix(args.pathSuffix);
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
- window.location.href = "/"
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 (_request, reply) => {
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralnomads/codenomad-dev",
3
- "version": "0.11.1-dev-20260216-1a0734c6",
3
+ "version": "0.11.2-dev-20260218-127a1f62",
4
4
  "description": "CodeNomad Server",
5
5
  "license": "MIT",
6
6
  "author": {