@unbrained/pm-web 2026.6.4 → 2026.6.9

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/routes/pm.js CHANGED
@@ -7,6 +7,7 @@ import { verifyProjectAccess } from "./projects.js";
7
7
  import { addSSEClient, broadcastProjectEvent, setupSSEHeaders, broadcastPresence, updateClientView, getProjectPresence } from "../services/sse.js";
8
8
  import { v4 as uuidv4 } from "uuid";
9
9
  import neo4j from "neo4j-driver";
10
+ import { routeParam } from "./route-params.js";
10
11
  // Singleton Neo4j driver — reused across sync calls to avoid per-call connection overhead.
11
12
  let _neo4jDriver = null;
12
13
  let _neo4jDriverKey = "";
@@ -32,7 +33,7 @@ router.use(async (req, res, next) => {
32
33
  next();
33
34
  return;
34
35
  }
35
- const projectId = req.params["projectId"];
36
+ const projectId = routeParam(req, "projectId");
36
37
  if (!projectId) {
37
38
  next();
38
39
  return;
@@ -335,7 +336,7 @@ async function verifyProject(userId, projectId) {
335
336
  // Returns runtime types/statuses from `pm contracts --json` so the frontend
336
337
  // stays in sync with whatever pm CLI version + extensions are installed.
337
338
  router.get("/schema", async (req, res) => {
338
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
339
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
339
340
  if (!project) {
340
341
  res.status(404).json({ error: "Project not found" });
341
342
  return;
@@ -353,7 +354,7 @@ router.get("/schema", async (req, res) => {
353
354
  });
354
355
  // GET /api/projects/:projectId/pm/list
355
356
  router.get("/list", async (req, res) => {
356
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
357
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
357
358
  if (!project) {
358
359
  res.status(404).json({ error: "Project not found" });
359
360
  return;
@@ -379,7 +380,7 @@ router.get("/list", async (req, res) => {
379
380
  });
380
381
  // GET /api/projects/:projectId/pm/list-all
381
382
  router.get("/list-all", async (req, res) => {
382
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
383
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
383
384
  if (!project) {
384
385
  res.status(404).json({ error: "Project not found" });
385
386
  return;
@@ -397,7 +398,7 @@ router.get("/list-all", async (req, res) => {
397
398
  // Kanban board: items grouped into columns by the workspace's runtime statuses
398
399
  // (read live from `pm contracts`) so the board matches the installed CLI.
399
400
  router.get("/board", async (req, res) => {
400
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
401
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
401
402
  if (!project) {
402
403
  res.status(404).json({ error: "Project not found" });
403
404
  return;
@@ -418,7 +419,7 @@ router.get("/board", async (req, res) => {
418
419
  // GET /api/projects/:projectId/pm/search?q=...
419
420
  // Full-text search over id/title/tags/body via a single list-all read.
420
421
  router.get("/search", async (req, res) => {
421
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
422
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
422
423
  if (!project) {
423
424
  res.status(404).json({ error: "Project not found" });
424
425
  return;
@@ -434,7 +435,7 @@ router.get("/search", async (req, res) => {
434
435
  });
435
436
  // POST /api/projects/:projectId/pm/create
436
437
  router.post("/create", async (req, res) => {
437
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
438
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
438
439
  if (!project) {
439
440
  res.status(404).json({ error: "Project not found" });
440
441
  return;
@@ -513,22 +514,22 @@ router.post("/create", async (req, res) => {
513
514
  return;
514
515
  }
515
516
  // Broadcast SSE create event
516
- broadcastProjectEvent(req.params["projectId"], {
517
+ broadcastProjectEvent(routeParam(req, "projectId"), {
517
518
  type: "item-created",
518
519
  data: { result: result.parsed, userId: req.user.userId },
519
520
  });
520
- scheduleGraphSync(req.params["projectId"], project, "item-created");
521
+ scheduleGraphSync(routeParam(req, "projectId"), project, "item-created");
521
522
  res.status(201).json(result.parsed || {});
522
523
  });
523
524
  // GET /api/projects/:projectId/pm/get/:itemId
524
525
  router.get("/get/:itemId", async (req, res) => {
525
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
526
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
526
527
  if (!project) {
527
528
  res.status(404).json({ error: "Project not found" });
528
529
  return;
529
530
  }
530
531
  const result = runPm({
531
- args: ["get", req.params["itemId"]],
532
+ args: ["get", routeParam(req, "itemId")],
532
533
  userId: project.ownerUserId,
533
534
  slug: project.slug,
534
535
  jsonOutput: true,
@@ -541,13 +542,13 @@ router.get("/get/:itemId", async (req, res) => {
541
542
  });
542
543
  // PATCH /api/projects/:projectId/pm/update/:itemId
543
544
  router.patch("/update/:itemId", async (req, res) => {
544
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
545
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
545
546
  if (!project) {
546
547
  res.status(404).json({ error: "Project not found" });
547
548
  return;
548
549
  }
549
550
  const body = req.body;
550
- const args = ["update", req.params["itemId"]];
551
+ const args = ["update", routeParam(req, "itemId")];
551
552
  // String options
552
553
  const stringFlags = {
553
554
  title: "--title", description: "--description", status: "--status", priority: "--priority",
@@ -580,16 +581,16 @@ router.patch("/update/:itemId", async (req, res) => {
580
581
  return;
581
582
  }
582
583
  // Broadcast SSE update event
583
- broadcastProjectEvent(req.params["projectId"], {
584
+ broadcastProjectEvent(routeParam(req, "projectId"), {
584
585
  type: "item-updated",
585
- data: { itemId: req.params["itemId"], userId: req.user.userId },
586
+ data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
586
587
  });
587
- scheduleGraphSync(req.params["projectId"], project, "item-updated");
588
+ scheduleGraphSync(routeParam(req, "projectId"), project, "item-updated");
588
589
  res.json(result.parsed || {});
589
590
  });
590
591
  // POST /api/projects/:projectId/pm/close/:itemId
591
592
  router.post("/close/:itemId", async (req, res) => {
592
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
593
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
593
594
  if (!project) {
594
595
  res.status(404).json({ error: "Project not found" });
595
596
  return;
@@ -600,7 +601,7 @@ router.post("/close/:itemId", async (req, res) => {
600
601
  return;
601
602
  }
602
603
  const result = runPm({
603
- args: ["close", req.params["itemId"], reason.trim()],
604
+ args: ["close", routeParam(req, "itemId"), reason.trim()],
604
605
  userId: project.ownerUserId,
605
606
  slug: project.slug,
606
607
  jsonOutput: true,
@@ -609,22 +610,22 @@ router.post("/close/:itemId", async (req, res) => {
609
610
  res.status(400).json({ error: result.stderr || "Failed to close item" });
610
611
  return;
611
612
  }
612
- broadcastProjectEvent(req.params["projectId"], {
613
+ broadcastProjectEvent(routeParam(req, "projectId"), {
613
614
  type: "item-closed",
614
- data: { itemId: req.params["itemId"], userId: req.user.userId },
615
+ data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
615
616
  });
616
- scheduleGraphSync(req.params["projectId"], project, "item-closed");
617
+ scheduleGraphSync(routeParam(req, "projectId"), project, "item-closed");
617
618
  res.json(result.parsed || {});
618
619
  });
619
620
  // DELETE /api/projects/:projectId/pm/delete/:itemId
620
621
  router.delete("/delete/:itemId", async (req, res) => {
621
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
622
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
622
623
  if (!project) {
623
624
  res.status(404).json({ error: "Project not found" });
624
625
  return;
625
626
  }
626
627
  const result = runPm({
627
- args: ["delete", req.params["itemId"], "--yes"],
628
+ args: ["delete", routeParam(req, "itemId"), "--yes"],
628
629
  userId: project.ownerUserId,
629
630
  slug: project.slug,
630
631
  });
@@ -632,16 +633,16 @@ router.delete("/delete/:itemId", async (req, res) => {
632
633
  res.status(400).json({ error: result.stderr || "Failed to delete item" });
633
634
  return;
634
635
  }
635
- broadcastProjectEvent(req.params["projectId"], {
636
+ broadcastProjectEvent(routeParam(req, "projectId"), {
636
637
  type: "item-deleted",
637
- data: { itemId: req.params["itemId"], userId: req.user.userId },
638
+ data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
638
639
  });
639
- scheduleGraphSync(req.params["projectId"], project, "item-deleted");
640
+ scheduleGraphSync(routeParam(req, "projectId"), project, "item-deleted");
640
641
  res.json({ ok: true });
641
642
  });
642
643
  // POST /api/projects/:projectId/pm/comments/:itemId
643
644
  router.post("/comments/:itemId", async (req, res) => {
644
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
645
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
645
646
  if (!project) {
646
647
  res.status(404).json({ error: "Project not found" });
647
648
  return;
@@ -652,7 +653,7 @@ router.post("/comments/:itemId", async (req, res) => {
652
653
  return;
653
654
  }
654
655
  const result = runPm({
655
- args: ["comments", req.params["itemId"], text.trim()],
656
+ args: ["comments", routeParam(req, "itemId"), text.trim()],
656
657
  userId: project.ownerUserId,
657
658
  slug: project.slug,
658
659
  jsonOutput: true,
@@ -665,13 +666,13 @@ router.post("/comments/:itemId", async (req, res) => {
665
666
  });
666
667
  // GET /api/projects/:projectId/pm/comments/:itemId
667
668
  router.get("/comments/:itemId", async (req, res) => {
668
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
669
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
669
670
  if (!project) {
670
671
  res.status(404).json({ error: "Project not found" });
671
672
  return;
672
673
  }
673
674
  const result = runPm({
674
- args: ["comments", req.params["itemId"]],
675
+ args: ["comments", routeParam(req, "itemId")],
675
676
  userId: project.ownerUserId,
676
677
  slug: project.slug,
677
678
  jsonOutput: true,
@@ -680,13 +681,13 @@ router.get("/comments/:itemId", async (req, res) => {
680
681
  });
681
682
  // GET /api/projects/:projectId/pm/notes/:itemId
682
683
  router.get("/notes/:itemId", async (req, res) => {
683
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
684
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
684
685
  if (!project) {
685
686
  res.status(404).json({ error: "Project not found" });
686
687
  return;
687
688
  }
688
689
  const result = runPm({
689
- args: ["notes", req.params["itemId"]],
690
+ args: ["notes", routeParam(req, "itemId")],
690
691
  userId: project.ownerUserId,
691
692
  slug: project.slug,
692
693
  jsonOutput: true,
@@ -695,7 +696,7 @@ router.get("/notes/:itemId", async (req, res) => {
695
696
  });
696
697
  // POST /api/projects/:projectId/pm/notes/:itemId
697
698
  router.post("/notes/:itemId", async (req, res) => {
698
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
699
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
699
700
  if (!project) {
700
701
  res.status(404).json({ error: "Project not found" });
701
702
  return;
@@ -706,7 +707,7 @@ router.post("/notes/:itemId", async (req, res) => {
706
707
  return;
707
708
  }
708
709
  const result = runPm({
709
- args: ["notes", req.params["itemId"], text.trim()],
710
+ args: ["notes", routeParam(req, "itemId"), text.trim()],
710
711
  userId: project.ownerUserId,
711
712
  slug: project.slug,
712
713
  jsonOutput: true,
@@ -719,7 +720,7 @@ router.post("/notes/:itemId", async (req, res) => {
719
720
  });
720
721
  // GET /api/projects/:projectId/pm/context
721
722
  router.get("/context", async (req, res) => {
722
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
723
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
723
724
  if (!project) {
724
725
  res.status(404).json({ error: "Project not found" });
725
726
  return;
@@ -737,7 +738,7 @@ router.get("/context", async (req, res) => {
737
738
  });
738
739
  // GET /api/projects/:projectId/pm/activity
739
740
  router.get("/activity", async (req, res) => {
740
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
741
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
741
742
  if (!project) {
742
743
  res.status(404).json({ error: "Project not found" });
743
744
  return;
@@ -751,7 +752,7 @@ router.get("/activity", async (req, res) => {
751
752
  });
752
753
  // GET /api/projects/:projectId/pm/stats
753
754
  router.get("/stats", async (req, res) => {
754
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
755
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
755
756
  if (!project) {
756
757
  res.status(404).json({ error: "Project not found" });
757
758
  return;
@@ -766,7 +767,7 @@ router.get("/stats", async (req, res) => {
766
767
  });
767
768
  // GET /api/projects/:projectId/pm/aggregate
768
769
  router.get("/aggregate", async (req, res) => {
769
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
770
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
770
771
  if (!project) {
771
772
  res.status(404).json({ error: "Project not found" });
772
773
  return;
@@ -781,7 +782,7 @@ router.get("/aggregate", async (req, res) => {
781
782
  });
782
783
  // POST /api/projects/:projectId/pm/search
783
784
  router.post("/search", async (req, res) => {
784
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
785
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
785
786
  if (!project) {
786
787
  res.status(404).json({ error: "Project not found" });
787
788
  return;
@@ -810,7 +811,7 @@ router.post("/search", async (req, res) => {
810
811
  });
811
812
  // GET /api/projects/:projectId/pm/calendar
812
813
  router.get("/calendar", async (req, res) => {
813
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
814
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
814
815
  if (!project) {
815
816
  res.status(404).json({ error: "Project not found" });
816
817
  return;
@@ -829,7 +830,7 @@ router.get("/calendar", async (req, res) => {
829
830
  // calendar view. Auth works via the usual token (header/cookie) or a
830
831
  // `?token=` query param, since calendar clients cannot send cookies.
831
832
  router.get("/calendar.ics", async (req, res) => {
832
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
833
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
833
834
  if (!project) {
834
835
  res.status(404).json({ error: "Project not found" });
835
836
  return;
@@ -867,7 +868,7 @@ router.get("/calendar.ics", async (req, res) => {
867
868
  });
868
869
  // GET /api/projects/:projectId/pm/health
869
870
  router.get("/health", async (req, res) => {
870
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
871
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
871
872
  if (!project) {
872
873
  res.status(404).json({ error: "Project not found" });
873
874
  return;
@@ -882,7 +883,7 @@ router.get("/health", async (req, res) => {
882
883
  });
883
884
  // POST /api/projects/:projectId/pm/append/:itemId
884
885
  router.post("/append/:itemId", async (req, res) => {
885
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
886
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
886
887
  if (!project) {
887
888
  res.status(404).json({ error: "Project not found" });
888
889
  return;
@@ -893,7 +894,7 @@ router.post("/append/:itemId", async (req, res) => {
893
894
  return;
894
895
  }
895
896
  const result = runPm({
896
- args: ["append", req.params["itemId"], text.trim()],
897
+ args: ["append", routeParam(req, "itemId"), text.trim()],
897
898
  userId: project.ownerUserId,
898
899
  slug: project.slug,
899
900
  jsonOutput: true,
@@ -902,18 +903,18 @@ router.post("/append/:itemId", async (req, res) => {
902
903
  res.status(400).json({ error: result.stderr || "Failed to append" });
903
904
  return;
904
905
  }
905
- scheduleGraphSync(req.params["projectId"], project, "item-appended");
906
+ scheduleGraphSync(routeParam(req, "projectId"), project, "item-appended");
906
907
  res.json(result.parsed || { ok: true });
907
908
  });
908
909
  // GET /api/projects/:projectId/pm/history/:itemId
909
910
  router.get("/history/:itemId", async (req, res) => {
910
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
911
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
911
912
  if (!project) {
912
913
  res.status(404).json({ error: "Project not found" });
913
914
  return;
914
915
  }
915
916
  const result = runPm({
916
- args: ["history", req.params["itemId"]],
917
+ args: ["history", routeParam(req, "itemId")],
917
918
  userId: project.ownerUserId,
918
919
  slug: project.slug,
919
920
  jsonOutput: true,
@@ -922,13 +923,13 @@ router.get("/history/:itemId", async (req, res) => {
922
923
  });
923
924
  // GET /api/projects/:projectId/pm/deps/:itemId
924
925
  router.get("/deps/:itemId", async (req, res) => {
925
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
926
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
926
927
  if (!project) {
927
928
  res.status(404).json({ error: "Project not found" });
928
929
  return;
929
930
  }
930
931
  const result = runPm({
931
- args: ["deps", req.params["itemId"]],
932
+ args: ["deps", routeParam(req, "itemId")],
932
933
  userId: project.ownerUserId,
933
934
  slug: project.slug,
934
935
  jsonOutput: true,
@@ -937,7 +938,7 @@ router.get("/deps/:itemId", async (req, res) => {
937
938
  });
938
939
  // POST /api/projects/:projectId/pm/deps/:itemId
939
940
  router.post("/deps/:itemId", async (req, res) => {
940
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
941
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
941
942
  if (!project) {
942
943
  res.status(404).json({ error: "Project not found" });
943
944
  return;
@@ -949,7 +950,7 @@ router.post("/deps/:itemId", async (req, res) => {
949
950
  }
950
951
  const depRel = normalizeDependencyKind(rel);
951
952
  const result = runPm({
952
- args: ["update", req.params["itemId"], "--dep", `id=${targetId.trim()},kind=${depRel}`],
953
+ args: ["update", routeParam(req, "itemId"), "--dep", `id=${targetId.trim()},kind=${depRel}`],
953
954
  userId: project.ownerUserId,
954
955
  slug: project.slug,
955
956
  jsonOutput: true,
@@ -958,9 +959,9 @@ router.post("/deps/:itemId", async (req, res) => {
958
959
  res.status(400).json({ error: result.stderr || "Failed to add dependency" });
959
960
  return;
960
961
  }
961
- scheduleGraphSync(req.params["projectId"], project, "dependency-added");
962
- broadcastDependencyEvent(req.params["projectId"], "dependency-added", {
963
- from: req.params["itemId"],
962
+ scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-added");
963
+ broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-added", {
964
+ from: routeParam(req, "itemId"),
964
965
  to: targetId.trim(),
965
966
  rel: depRel,
966
967
  userId: req.user.userId,
@@ -969,7 +970,7 @@ router.post("/deps/:itemId", async (req, res) => {
969
970
  });
970
971
  // DELETE /api/projects/:projectId/pm/deps/:itemId
971
972
  router.delete("/deps/:itemId", async (req, res) => {
972
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
973
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
973
974
  if (!project) {
974
975
  res.status(404).json({ error: "Project not found" });
975
976
  return;
@@ -982,7 +983,7 @@ router.delete("/deps/:itemId", async (req, res) => {
982
983
  const depRel = normalizeDependencyKind(rel || "relates_to");
983
984
  const selector = `id=${targetId.trim()},kind=${depRel}`;
984
985
  const result = runPm({
985
- args: ["update", req.params["itemId"], "--dep-remove", selector],
986
+ args: ["update", routeParam(req, "itemId"), "--dep-remove", selector],
986
987
  userId: project.ownerUserId,
987
988
  slug: project.slug,
988
989
  jsonOutput: true,
@@ -991,18 +992,18 @@ router.delete("/deps/:itemId", async (req, res) => {
991
992
  res.status(400).json({ error: result.stderr || "Failed to remove dependency" });
992
993
  return;
993
994
  }
994
- scheduleGraphSync(req.params["projectId"], project, "dependency-removed");
995
- broadcastDependencyEvent(req.params["projectId"], "dependency-removed", {
996
- from: req.params["itemId"],
995
+ scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-removed");
996
+ broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-removed", {
997
+ from: routeParam(req, "itemId"),
997
998
  to: targetId.trim(),
998
999
  rel: depRel,
999
1000
  userId: req.user.userId,
1000
1001
  });
1001
- res.status(200).json({ ok: true, from: req.params["itemId"], to: targetId.trim(), type: depRel, result: result.parsed || null });
1002
+ res.status(200).json({ ok: true, from: routeParam(req, "itemId"), to: targetId.trim(), type: depRel, result: result.parsed || null });
1002
1003
  });
1003
1004
  // POST /api/projects/:projectId/pm/rel — Create a relationship between two items
1004
1005
  router.post("/rel", async (req, res) => {
1005
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1006
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1006
1007
  if (!project) {
1007
1008
  res.status(404).json({ error: "Project not found" });
1008
1009
  return;
@@ -1023,8 +1024,8 @@ router.post("/rel", async (req, res) => {
1023
1024
  res.status(400).json({ error: result.stderr || "Failed to create relationship" });
1024
1025
  return;
1025
1026
  }
1026
- scheduleGraphSync(req.params["projectId"], project, "rel-created");
1027
- broadcastDependencyEvent(req.params["projectId"], "dependency-added", {
1027
+ scheduleGraphSync(routeParam(req, "projectId"), project, "rel-created");
1028
+ broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-added", {
1028
1029
  from: from.trim(),
1029
1030
  to: to.trim(),
1030
1031
  rel: depRel,
@@ -1034,7 +1035,7 @@ router.post("/rel", async (req, res) => {
1034
1035
  });
1035
1036
  // DELETE /api/projects/:projectId/pm/rel — Remove a relationship between two items
1036
1037
  router.delete("/rel", async (req, res) => {
1037
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1038
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1038
1039
  if (!project) {
1039
1040
  res.status(404).json({ error: "Project not found" });
1040
1041
  return;
@@ -1056,8 +1057,8 @@ router.delete("/rel", async (req, res) => {
1056
1057
  res.status(400).json({ error: result.stderr || "Failed to remove relationship" });
1057
1058
  return;
1058
1059
  }
1059
- scheduleGraphSync(req.params["projectId"], project, "rel-removed");
1060
- broadcastDependencyEvent(req.params["projectId"], "dependency-removed", {
1060
+ scheduleGraphSync(routeParam(req, "projectId"), project, "rel-removed");
1061
+ broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-removed", {
1061
1062
  from: from.trim(),
1062
1063
  to: to.trim(),
1063
1064
  rel: depRel,
@@ -1067,7 +1068,7 @@ router.delete("/rel", async (req, res) => {
1067
1068
  });
1068
1069
  // GET /api/projects/:projectId/pm/graph
1069
1070
  router.get("/graph", async (req, res) => {
1070
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1071
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1071
1072
  if (!project) {
1072
1073
  res.status(404).json({ error: "Project not found" });
1073
1074
  return;
@@ -1095,7 +1096,7 @@ router.get("/graph", async (req, res) => {
1095
1096
  });
1096
1097
  // POST /api/projects/:projectId/pm/graph/sync
1097
1098
  router.post("/graph/sync", async (req, res) => {
1098
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1099
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1099
1100
  if (!project) {
1100
1101
  res.status(404).json({ error: "Project not found" });
1101
1102
  return;
@@ -1103,21 +1104,21 @@ router.post("/graph/sync", async (req, res) => {
1103
1104
  try {
1104
1105
  const syncResult = await syncProjectGraph(project);
1105
1106
  const payload = { reason: "manual-sync", ...syncResult, projectKey: graphProjectKey(project), source: "pm-web" };
1106
- broadcastProjectEvent(req.params["projectId"], {
1107
+ broadcastProjectEvent(routeParam(req, "projectId"), {
1107
1108
  type: "graph-synced",
1108
1109
  data: payload,
1109
1110
  });
1110
1111
  res.json({ ok: true, ...payload });
1111
1112
  }
1112
1113
  catch (err) {
1113
- broadcastProjectEvent(req.params["projectId"], {
1114
+ broadcastProjectEvent(routeParam(req, "projectId"), {
1114
1115
  type: "graph-sync-failed",
1115
1116
  data: {
1116
1117
  reason: "manual-sync",
1117
1118
  error: err instanceof Error ? err.message : String(err),
1118
1119
  },
1119
1120
  });
1120
- broadcastProjectEvent(req.params["projectId"], {
1121
+ broadcastProjectEvent(routeParam(req, "projectId"), {
1121
1122
  type: "graph_sync_failed",
1122
1123
  data: {
1123
1124
  reason: "manual-sync",
@@ -1129,12 +1130,12 @@ router.post("/graph/sync", async (req, res) => {
1129
1130
  });
1130
1131
  // GET /api/projects/:projectId/pm/graph/neighbors/:nodeId
1131
1132
  router.get("/graph/neighbors/:nodeId", async (req, res) => {
1132
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1133
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1133
1134
  if (!project) {
1134
1135
  res.status(404).json({ error: "Project not found" });
1135
1136
  return;
1136
1137
  }
1137
- const nodeId = req.params["nodeId"];
1138
+ const nodeId = routeParam(req, "nodeId");
1138
1139
  if (!nodeId) {
1139
1140
  res.status(400).json({ error: "nodeId is required" });
1140
1141
  return;
@@ -1160,7 +1161,7 @@ router.get("/graph/neighbors/:nodeId", async (req, res) => {
1160
1161
  });
1161
1162
  // POST /api/projects/:projectId/pm/graph/query
1162
1163
  router.post("/graph/query", async (req, res) => {
1163
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1164
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1164
1165
  if (!project) {
1165
1166
  res.status(404).json({ error: "Project not found" });
1166
1167
  return;
@@ -1190,13 +1191,13 @@ router.post("/graph/query", async (req, res) => {
1190
1191
  });
1191
1192
  // GET /api/projects/:projectId/pm/learnings/:itemId
1192
1193
  router.get("/learnings/:itemId", async (req, res) => {
1193
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1194
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1194
1195
  if (!project) {
1195
1196
  res.status(404).json({ error: "Project not found" });
1196
1197
  return;
1197
1198
  }
1198
1199
  const result = runPm({
1199
- args: ["learnings", req.params["itemId"]],
1200
+ args: ["learnings", routeParam(req, "itemId")],
1200
1201
  userId: project.ownerUserId,
1201
1202
  slug: project.slug,
1202
1203
  jsonOutput: true,
@@ -1205,7 +1206,7 @@ router.get("/learnings/:itemId", async (req, res) => {
1205
1206
  });
1206
1207
  // POST /api/projects/:projectId/pm/learnings/:itemId
1207
1208
  router.post("/learnings/:itemId", async (req, res) => {
1208
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1209
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1209
1210
  if (!project) {
1210
1211
  res.status(404).json({ error: "Project not found" });
1211
1212
  return;
@@ -1216,7 +1217,7 @@ router.post("/learnings/:itemId", async (req, res) => {
1216
1217
  return;
1217
1218
  }
1218
1219
  const result = runPm({
1219
- args: ["learnings", req.params["itemId"], text.trim()],
1220
+ args: ["learnings", routeParam(req, "itemId"), text.trim()],
1220
1221
  userId: project.ownerUserId,
1221
1222
  slug: project.slug,
1222
1223
  jsonOutput: true,
@@ -1229,13 +1230,13 @@ router.post("/learnings/:itemId", async (req, res) => {
1229
1230
  });
1230
1231
  // POST /api/projects/:projectId/pm/claim/:itemId
1231
1232
  router.post("/claim/:itemId", async (req, res) => {
1232
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1233
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1233
1234
  if (!project) {
1234
1235
  res.status(404).json({ error: "Project not found" });
1235
1236
  return;
1236
1237
  }
1237
1238
  const result = runPm({
1238
- args: ["claim", req.params["itemId"]],
1239
+ args: ["claim", routeParam(req, "itemId")],
1239
1240
  userId: project.ownerUserId,
1240
1241
  slug: project.slug,
1241
1242
  jsonOutput: true,
@@ -1244,18 +1245,18 @@ router.post("/claim/:itemId", async (req, res) => {
1244
1245
  res.status(400).json({ error: result.stderr || "Failed to claim item" });
1245
1246
  return;
1246
1247
  }
1247
- scheduleGraphSync(req.params["projectId"], project, "item-claimed");
1248
+ scheduleGraphSync(routeParam(req, "projectId"), project, "item-claimed");
1248
1249
  res.json(result.parsed || { ok: true });
1249
1250
  });
1250
1251
  // POST /api/projects/:projectId/pm/release/:itemId
1251
1252
  router.post("/release/:itemId", async (req, res) => {
1252
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1253
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1253
1254
  if (!project) {
1254
1255
  res.status(404).json({ error: "Project not found" });
1255
1256
  return;
1256
1257
  }
1257
1258
  const result = runPm({
1258
- args: ["release", req.params["itemId"]],
1259
+ args: ["release", routeParam(req, "itemId")],
1259
1260
  userId: project.ownerUserId,
1260
1261
  slug: project.slug,
1261
1262
  jsonOutput: true,
@@ -1264,18 +1265,18 @@ router.post("/release/:itemId", async (req, res) => {
1264
1265
  res.status(400).json({ error: result.stderr || "Failed to release item" });
1265
1266
  return;
1266
1267
  }
1267
- scheduleGraphSync(req.params["projectId"], project, "item-released");
1268
+ scheduleGraphSync(routeParam(req, "projectId"), project, "item-released");
1268
1269
  res.json(result.parsed || { ok: true });
1269
1270
  });
1270
1271
  // POST /api/projects/:projectId/pm/start-task/:itemId
1271
1272
  router.post("/start-task/:itemId", async (req, res) => {
1272
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1273
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1273
1274
  if (!project) {
1274
1275
  res.status(404).json({ error: "Project not found" });
1275
1276
  return;
1276
1277
  }
1277
1278
  const result = runPm({
1278
- args: ["start-task", req.params["itemId"]],
1279
+ args: ["start-task", routeParam(req, "itemId")],
1279
1280
  userId: project.ownerUserId,
1280
1281
  slug: project.slug,
1281
1282
  jsonOutput: true,
@@ -1284,18 +1285,18 @@ router.post("/start-task/:itemId", async (req, res) => {
1284
1285
  res.status(400).json({ error: result.stderr || "Failed to start task" });
1285
1286
  return;
1286
1287
  }
1287
- scheduleGraphSync(req.params["projectId"], project, "task-started");
1288
+ scheduleGraphSync(routeParam(req, "projectId"), project, "task-started");
1288
1289
  res.json(result.parsed || { ok: true });
1289
1290
  });
1290
1291
  // POST /api/projects/:projectId/pm/pause-task/:itemId
1291
1292
  router.post("/pause-task/:itemId", async (req, res) => {
1292
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1293
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1293
1294
  if (!project) {
1294
1295
  res.status(404).json({ error: "Project not found" });
1295
1296
  return;
1296
1297
  }
1297
1298
  const result = runPm({
1298
- args: ["pause-task", req.params["itemId"]],
1299
+ args: ["pause-task", routeParam(req, "itemId")],
1299
1300
  userId: project.ownerUserId,
1300
1301
  slug: project.slug,
1301
1302
  jsonOutput: true,
@@ -1304,18 +1305,18 @@ router.post("/pause-task/:itemId", async (req, res) => {
1304
1305
  res.status(400).json({ error: result.stderr || "Failed to pause task" });
1305
1306
  return;
1306
1307
  }
1307
- scheduleGraphSync(req.params["projectId"], project, "task-paused");
1308
+ scheduleGraphSync(routeParam(req, "projectId"), project, "task-paused");
1308
1309
  res.json(result.parsed || { ok: true });
1309
1310
  });
1310
1311
  // GET /api/projects/:projectId/pm/tests/:itemId
1311
1312
  router.get("/tests/:itemId", async (req, res) => {
1312
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1313
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1313
1314
  if (!project) {
1314
1315
  res.status(404).json({ error: "Project not found" });
1315
1316
  return;
1316
1317
  }
1317
1318
  const result = runPm({
1318
- args: ["test", req.params["itemId"]],
1319
+ args: ["test", routeParam(req, "itemId")],
1319
1320
  userId: project.ownerUserId,
1320
1321
  slug: project.slug,
1321
1322
  jsonOutput: true,
@@ -1324,7 +1325,7 @@ router.get("/tests/:itemId", async (req, res) => {
1324
1325
  });
1325
1326
  // POST /api/projects/:projectId/pm/tests/:itemId
1326
1327
  router.post("/tests/:itemId", async (req, res) => {
1327
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1328
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1328
1329
  if (!project) {
1329
1330
  res.status(404).json({ error: "Project not found" });
1330
1331
  return;
@@ -1334,7 +1335,7 @@ router.post("/tests/:itemId", async (req, res) => {
1334
1335
  res.status(400).json({ error: "Test command is required" });
1335
1336
  return;
1336
1337
  }
1337
- const args = ["test", req.params["itemId"], "--add", "--command", command.trim()];
1338
+ const args = ["test", routeParam(req, "itemId"), "--add", "--command", command.trim()];
1338
1339
  if (description)
1339
1340
  args.push("--description", description.trim());
1340
1341
  const result = runPm({
@@ -1351,7 +1352,7 @@ router.post("/tests/:itemId", async (req, res) => {
1351
1352
  });
1352
1353
  // GET /api/projects/:projectId/pm/dedupe-audit
1353
1354
  router.get("/dedupe-audit", async (req, res) => {
1354
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1355
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1355
1356
  if (!project) {
1356
1357
  res.status(404).json({ error: "Project not found" });
1357
1358
  return;
@@ -1366,7 +1367,7 @@ router.get("/dedupe-audit", async (req, res) => {
1366
1367
  });
1367
1368
  // GET /api/projects/:projectId/pm/validate
1368
1369
  router.get("/validate", async (req, res) => {
1369
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1370
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1370
1371
  if (!project) {
1371
1372
  res.status(404).json({ error: "Project not found" });
1372
1373
  return;
@@ -1381,7 +1382,7 @@ router.get("/validate", async (req, res) => {
1381
1382
  });
1382
1383
  // POST /api/projects/:projectId/pm/restore/:itemId
1383
1384
  router.post("/restore/:itemId", async (req, res) => {
1384
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1385
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1385
1386
  if (!project) {
1386
1387
  res.status(404).json({ error: "Project not found" });
1387
1388
  return;
@@ -1392,7 +1393,7 @@ router.post("/restore/:itemId", async (req, res) => {
1392
1393
  return;
1393
1394
  }
1394
1395
  const result = runPm({
1395
- args: ["restore", req.params["itemId"], target.trim()],
1396
+ args: ["restore", routeParam(req, "itemId"), target.trim()],
1396
1397
  userId: project.ownerUserId,
1397
1398
  slug: project.slug,
1398
1399
  jsonOutput: true,
@@ -1401,12 +1402,12 @@ router.post("/restore/:itemId", async (req, res) => {
1401
1402
  res.status(400).json({ error: result.stderr || "Failed to restore item" });
1402
1403
  return;
1403
1404
  }
1404
- scheduleGraphSync(req.params["projectId"], project, "item-restored");
1405
+ scheduleGraphSync(routeParam(req, "projectId"), project, "item-restored");
1405
1406
  res.json(result.parsed || { ok: true });
1406
1407
  });
1407
1408
  // POST /api/projects/:projectId/pm/close-task/:itemId
1408
1409
  router.post("/close-task/:itemId", async (req, res) => {
1409
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1410
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1410
1411
  if (!project) {
1411
1412
  res.status(404).json({ error: "Project not found" });
1412
1413
  return;
@@ -1417,7 +1418,7 @@ router.post("/close-task/:itemId", async (req, res) => {
1417
1418
  return;
1418
1419
  }
1419
1420
  const result = runPm({
1420
- args: ["close-task", req.params["itemId"], reason.trim()],
1421
+ args: ["close-task", routeParam(req, "itemId"), reason.trim()],
1421
1422
  userId: project.ownerUserId,
1422
1423
  slug: project.slug,
1423
1424
  jsonOutput: true,
@@ -1426,12 +1427,12 @@ router.post("/close-task/:itemId", async (req, res) => {
1426
1427
  res.status(400).json({ error: result.stderr || "Failed to close task" });
1427
1428
  return;
1428
1429
  }
1429
- scheduleGraphSync(req.params["projectId"], project, "task-closed");
1430
+ scheduleGraphSync(routeParam(req, "projectId"), project, "task-closed");
1430
1431
  res.json(result.parsed || { ok: true });
1431
1432
  });
1432
1433
  // POST /api/projects/:projectId/pm/reindex
1433
1434
  router.post("/reindex", async (req, res) => {
1434
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1435
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1435
1436
  if (!project) {
1436
1437
  res.status(404).json({ error: "Project not found" });
1437
1438
  return;
@@ -1454,7 +1455,7 @@ router.post("/reindex", async (req, res) => {
1454
1455
  });
1455
1456
  // POST /api/projects/:projectId/pm/normalize
1456
1457
  router.post("/normalize", async (req, res) => {
1457
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1458
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1458
1459
  if (!project) {
1459
1460
  res.status(404).json({ error: "Project not found" });
1460
1461
  return;
@@ -1469,7 +1470,7 @@ router.post("/normalize", async (req, res) => {
1469
1470
  });
1470
1471
  // GET /api/projects/:projectId/pm/comments-audit
1471
1472
  router.get("/comments-audit", async (req, res) => {
1472
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1473
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1473
1474
  if (!project) {
1474
1475
  res.status(404).json({ error: "Project not found" });
1475
1476
  return;
@@ -1484,7 +1485,7 @@ router.get("/comments-audit", async (req, res) => {
1484
1485
  });
1485
1486
  // POST /api/projects/:projectId/pm/files/:itemId
1486
1487
  router.post("/files/:itemId", async (req, res) => {
1487
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1488
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1488
1489
  if (!project) {
1489
1490
  res.status(404).json({ error: "Project not found" });
1490
1491
  return;
@@ -1497,7 +1498,7 @@ router.post("/files/:itemId", async (req, res) => {
1497
1498
  let addVal = `path=${filePath.trim()}`;
1498
1499
  if (scope)
1499
1500
  addVal += `,scope=${scope}`;
1500
- const args = ["files", req.params["itemId"], "--add", addVal];
1501
+ const args = ["files", routeParam(req, "itemId"), "--add", addVal];
1501
1502
  const result = runPm({
1502
1503
  args,
1503
1504
  userId: project.ownerUserId,
@@ -1508,18 +1509,18 @@ router.post("/files/:itemId", async (req, res) => {
1508
1509
  res.status(400).json({ error: result.stderr || "Failed to link file" });
1509
1510
  return;
1510
1511
  }
1511
- scheduleGraphSync(req.params["projectId"], project, "file-linked");
1512
+ scheduleGraphSync(routeParam(req, "projectId"), project, "file-linked");
1512
1513
  res.status(201).json(result.parsed || { ok: true });
1513
1514
  });
1514
1515
  // GET /api/projects/:projectId/pm/files/:itemId
1515
1516
  router.get("/files/:itemId", async (req, res) => {
1516
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1517
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1517
1518
  if (!project) {
1518
1519
  res.status(404).json({ error: "Project not found" });
1519
1520
  return;
1520
1521
  }
1521
1522
  const result = runPm({
1522
- args: ["files", req.params["itemId"]],
1523
+ args: ["files", routeParam(req, "itemId")],
1523
1524
  userId: project.ownerUserId,
1524
1525
  slug: project.slug,
1525
1526
  jsonOutput: true,
@@ -1528,7 +1529,7 @@ router.get("/files/:itemId", async (req, res) => {
1528
1529
  });
1529
1530
  // GET /api/projects/:projectId/pm/guide — list guide topics
1530
1531
  router.get("/guide", async (req, res) => {
1531
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1532
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1532
1533
  if (!project) {
1533
1534
  res.status(404).json({ error: "Project not found" });
1534
1535
  return;
@@ -1543,13 +1544,13 @@ router.get("/guide", async (req, res) => {
1543
1544
  });
1544
1545
  // GET /api/projects/:projectId/pm/guide/:topicId — get single guide topic
1545
1546
  router.get("/guide/:topicId", async (req, res) => {
1546
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1547
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1547
1548
  if (!project) {
1548
1549
  res.status(404).json({ error: "Project not found" });
1549
1550
  return;
1550
1551
  }
1551
1552
  const result = runPm({
1552
- args: ["guide", req.params["topicId"]],
1553
+ args: ["guide", routeParam(req, "topicId")],
1553
1554
  userId: project.ownerUserId,
1554
1555
  slug: project.slug,
1555
1556
  jsonOutput: true,
@@ -1567,7 +1568,7 @@ router.get("/guide/:topicId", async (req, res) => {
1567
1568
  // ─────────────────────────────────────────────────────────
1568
1569
  // GET /api/projects/:projectId/pm/export?format=json|csv|yaml
1569
1570
  router.get("/export", async (req, res) => {
1570
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1571
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1571
1572
  if (!project) {
1572
1573
  res.status(404).json({ error: "Project not found" });
1573
1574
  return;
@@ -1667,7 +1668,7 @@ router.get("/export", async (req, res) => {
1667
1668
  });
1668
1669
  // POST /api/projects/:projectId/pm/import — import JSON items
1669
1670
  router.post("/import", async (req, res) => {
1670
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1671
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1671
1672
  if (!project) {
1672
1673
  res.status(404).json({ error: "Project not found" });
1673
1674
  return;
@@ -1723,17 +1724,17 @@ router.post("/import", async (req, res) => {
1723
1724
  errors.push(`item[${i}]: ${result.stderr || "create failed"}`);
1724
1725
  }
1725
1726
  }
1726
- broadcastProjectEvent(req.params["projectId"], {
1727
+ broadcastProjectEvent(routeParam(req, "projectId"), {
1727
1728
  type: "items-imported",
1728
1729
  data: { count: created.length, userId: req.user.userId },
1729
1730
  });
1730
1731
  if (created.length > 0)
1731
- scheduleGraphSync(req.params["projectId"], project, "items-imported");
1732
+ scheduleGraphSync(routeParam(req, "projectId"), project, "items-imported");
1732
1733
  res.json({ created, errors, total: items.length });
1733
1734
  });
1734
1735
  // POST /api/projects/:projectId/pm/update-many
1735
1736
  router.post("/update-many", async (req, res) => {
1736
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1737
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1737
1738
  if (!project) {
1738
1739
  res.status(404).json({ error: "Project not found" });
1739
1740
  return;
@@ -1776,11 +1777,11 @@ router.post("/update-many", async (req, res) => {
1776
1777
  res.status(400).json({ error: result.stderr || "update-many failed" });
1777
1778
  return;
1778
1779
  }
1779
- broadcastProjectEvent(req.params["projectId"], {
1780
+ broadcastProjectEvent(routeParam(req, "projectId"), {
1780
1781
  type: "items-bulk-updated",
1781
1782
  data: { userId: req.user.userId },
1782
1783
  });
1783
- scheduleGraphSync(req.params["projectId"], project, "items-bulk-updated");
1784
+ scheduleGraphSync(routeParam(req, "projectId"), project, "items-bulk-updated");
1784
1785
  res.json(result.parsed || {});
1785
1786
  });
1786
1787
  // POST /api/projects/:projectId/pm/close-many
@@ -1788,7 +1789,7 @@ router.post("/update-many", async (req, res) => {
1788
1789
  // Accepts same filter options as update-many plus a required `reason` field.
1789
1790
  // Returns { closed_count, failed_count, skipped_count, rows }.
1790
1791
  router.post("/close-many", async (req, res) => {
1791
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1792
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1792
1793
  if (!project) {
1793
1794
  res.status(404).json({ error: "Project not found" });
1794
1795
  return;
@@ -1844,11 +1845,11 @@ router.post("/close-many", async (req, res) => {
1844
1845
  }
1845
1846
  }
1846
1847
  if (closedCount > 0) {
1847
- broadcastProjectEvent(req.params["projectId"], {
1848
+ broadcastProjectEvent(routeParam(req, "projectId"), {
1848
1849
  type: "items-bulk-updated",
1849
1850
  data: { userId: req.user.userId },
1850
1851
  });
1851
- scheduleGraphSync(req.params["projectId"], project, "items-bulk-updated");
1852
+ scheduleGraphSync(routeParam(req, "projectId"), project, "items-bulk-updated");
1852
1853
  }
1853
1854
  res.json({
1854
1855
  closed_count: closedCount,
@@ -1860,13 +1861,13 @@ router.post("/close-many", async (req, res) => {
1860
1861
  });
1861
1862
  // GET /api/projects/:projectId/pm/docs/:itemId
1862
1863
  router.get("/docs/:itemId", async (req, res) => {
1863
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1864
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1864
1865
  if (!project) {
1865
1866
  res.status(404).json({ error: "Project not found" });
1866
1867
  return;
1867
1868
  }
1868
1869
  const result = runPm({
1869
- args: ["docs", req.params["itemId"]],
1870
+ args: ["docs", routeParam(req, "itemId")],
1870
1871
  userId: project.ownerUserId,
1871
1872
  slug: project.slug,
1872
1873
  jsonOutput: true,
@@ -1875,13 +1876,13 @@ router.get("/docs/:itemId", async (req, res) => {
1875
1876
  });
1876
1877
  // POST /api/projects/:projectId/pm/docs/:itemId
1877
1878
  router.post("/docs/:itemId", async (req, res) => {
1878
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1879
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1879
1880
  if (!project) {
1880
1881
  res.status(404).json({ error: "Project not found" });
1881
1882
  return;
1882
1883
  }
1883
1884
  const { path: docPath, scope, note, remove, validatePaths } = req.body;
1884
- const args = ["docs", req.params["itemId"]];
1885
+ const args = ["docs", routeParam(req, "itemId")];
1885
1886
  if (remove) {
1886
1887
  args.push("--remove", remove);
1887
1888
  }
@@ -1905,12 +1906,12 @@ router.post("/docs/:itemId", async (req, res) => {
1905
1906
  res.status(400).json({ error: result.stderr || "Failed to update docs" });
1906
1907
  return;
1907
1908
  }
1908
- scheduleGraphSync(req.params["projectId"], project, "docs-updated");
1909
+ scheduleGraphSync(routeParam(req, "projectId"), project, "docs-updated");
1909
1910
  res.json(result.parsed || { ok: true });
1910
1911
  });
1911
1912
  // POST /api/projects/:projectId/pm/test-all
1912
1913
  router.post("/test-all", async (req, res) => {
1913
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1914
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1914
1915
  if (!project) {
1915
1916
  res.status(404).json({ error: "Project not found" });
1916
1917
  return;
@@ -1932,7 +1933,7 @@ router.post("/test-all", async (req, res) => {
1932
1933
  });
1933
1934
  // GET /api/projects/:projectId/pm/test-runs
1934
1935
  router.get("/test-runs", async (req, res) => {
1935
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1936
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1936
1937
  if (!project) {
1937
1938
  res.status(404).json({ error: "Project not found" });
1938
1939
  return;
@@ -1948,7 +1949,7 @@ router.get("/test-runs", async (req, res) => {
1948
1949
  });
1949
1950
  // POST /api/projects/:projectId/pm/gc
1950
1951
  router.post("/gc", async (req, res) => {
1951
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1952
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1952
1953
  if (!project) {
1953
1954
  res.status(404).json({ error: "Project not found" });
1954
1955
  return;
@@ -1958,7 +1959,7 @@ router.post("/gc", async (req, res) => {
1958
1959
  });
1959
1960
  // GET /api/projects/:projectId/pm/templates
1960
1961
  router.get("/templates", async (req, res) => {
1961
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1962
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1962
1963
  if (!project) {
1963
1964
  res.status(404).json({ error: "Project not found" });
1964
1965
  return;
@@ -1968,17 +1969,17 @@ router.get("/templates", async (req, res) => {
1968
1969
  });
1969
1970
  // GET /api/projects/:projectId/pm/templates/:name
1970
1971
  router.get("/templates/:name", async (req, res) => {
1971
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1972
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1972
1973
  if (!project) {
1973
1974
  res.status(404).json({ error: "Project not found" });
1974
1975
  return;
1975
1976
  }
1976
- const result = runPm({ args: ["templates", "show", req.params["name"]], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1977
+ const result = runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1977
1978
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
1978
1979
  });
1979
1980
  // GET /api/projects/:projectId/pm/config
1980
1981
  router.get("/config", async (req, res) => {
1981
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1982
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1982
1983
  if (!project) {
1983
1984
  res.status(404).json({ error: "Project not found" });
1984
1985
  return;
@@ -1988,23 +1989,23 @@ router.get("/config", async (req, res) => {
1988
1989
  });
1989
1990
  // GET /api/projects/:projectId/pm/config/:key
1990
1991
  router.get("/config/:key", async (req, res) => {
1991
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
1992
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
1992
1993
  if (!project) {
1993
1994
  res.status(404).json({ error: "Project not found" });
1994
1995
  return;
1995
1996
  }
1996
- const key = req.params["key"];
1997
+ const key = routeParam(req, "key");
1997
1998
  const result = runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1998
1999
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
1999
2000
  });
2000
2001
  // PATCH /api/projects/:projectId/pm/config/:key
2001
2002
  router.patch("/config/:key", async (req, res) => {
2002
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2003
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2003
2004
  if (!project) {
2004
2005
  res.status(404).json({ error: "Project not found" });
2005
2006
  return;
2006
2007
  }
2007
- const key = req.params["key"];
2008
+ const key = routeParam(req, "key");
2008
2009
  const body = req.body;
2009
2010
  const args = ["config", "project", "set", key];
2010
2011
  if (body.value)
@@ -2020,7 +2021,7 @@ router.patch("/config/:key", async (req, res) => {
2020
2021
  // These wrap pm list-draft, list-open, etc.
2021
2022
  function buildListShortcutRoute(pmCommand) {
2022
2023
  return async (req, res) => {
2023
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2024
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2024
2025
  if (!project) {
2025
2026
  res.status(404).json({ error: "Project not found" });
2026
2027
  return;
@@ -2058,7 +2059,7 @@ router.get("/list-canceled", buildListShortcutRoute("list-canceled"));
2058
2059
  // ─────────────────────────────────────────────────────────
2059
2060
  // POST /api/projects/:projectId/pm/plan
2060
2061
  router.post("/plan", async (req, res) => {
2061
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2062
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2062
2063
  if (!project) {
2063
2064
  res.status(404).json({ error: "Project not found" });
2064
2065
  return;
@@ -2084,7 +2085,7 @@ router.post("/plan", async (req, res) => {
2084
2085
  res.status(400).json({ error: result.stderr || "Failed to create plan" });
2085
2086
  return;
2086
2087
  }
2087
- broadcastProjectEvent(req.params["projectId"], {
2088
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2088
2089
  type: "item-created",
2089
2090
  data: { result: result.parsed, userId: req.user.userId },
2090
2091
  });
@@ -2092,13 +2093,13 @@ router.post("/plan", async (req, res) => {
2092
2093
  });
2093
2094
  // GET /api/projects/:projectId/pm/plan/:planId
2094
2095
  router.get("/plan/:planId", async (req, res) => {
2095
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2096
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2096
2097
  if (!project) {
2097
2098
  res.status(404).json({ error: "Project not found" });
2098
2099
  return;
2099
2100
  }
2100
2101
  const result = runPm({
2101
- args: ["plan", "show", req.params["planId"], "--depth", "standard"],
2102
+ args: ["plan", "show", routeParam(req, "planId"), "--depth", "standard"],
2102
2103
  userId: project.ownerUserId,
2103
2104
  slug: project.slug,
2104
2105
  jsonOutput: true,
@@ -2111,13 +2112,13 @@ router.get("/plan/:planId", async (req, res) => {
2111
2112
  });
2112
2113
  // PATCH /api/projects/:projectId/pm/plan/:planId
2113
2114
  router.patch("/plan/:planId", async (req, res) => {
2114
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2115
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2115
2116
  if (!project) {
2116
2117
  res.status(404).json({ error: "Project not found" });
2117
2118
  return;
2118
2119
  }
2119
2120
  const { title, description } = req.body;
2120
- const args = ["update", req.params["planId"]];
2121
+ const args = ["update", routeParam(req, "planId")];
2121
2122
  if (title?.trim())
2122
2123
  args.push("--title", title.trim());
2123
2124
  if (description !== undefined)
@@ -2127,21 +2128,21 @@ router.patch("/plan/:planId", async (req, res) => {
2127
2128
  res.status(400).json({ error: result.stderr || "Failed to update plan" });
2128
2129
  return;
2129
2130
  }
2130
- broadcastProjectEvent(req.params["projectId"], {
2131
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2131
2132
  type: "item-updated",
2132
- data: { itemId: req.params["planId"], userId: req.user.userId },
2133
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2133
2134
  });
2134
2135
  res.json(result.parsed || {});
2135
2136
  });
2136
2137
  // DELETE /api/projects/:projectId/pm/plan/:planId
2137
2138
  router.delete("/plan/:planId", async (req, res) => {
2138
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2139
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2139
2140
  if (!project) {
2140
2141
  res.status(404).json({ error: "Project not found" });
2141
2142
  return;
2142
2143
  }
2143
2144
  const result = runPm({
2144
- args: ["delete", req.params["planId"]],
2145
+ args: ["delete", routeParam(req, "planId")],
2145
2146
  userId: project.ownerUserId,
2146
2147
  slug: project.slug,
2147
2148
  jsonOutput: true,
@@ -2150,15 +2151,15 @@ router.delete("/plan/:planId", async (req, res) => {
2150
2151
  res.status(400).json({ error: result.stderr || "Failed to delete plan" });
2151
2152
  return;
2152
2153
  }
2153
- broadcastProjectEvent(req.params["projectId"], {
2154
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2154
2155
  type: "item-deleted",
2155
- data: { itemId: req.params["planId"], userId: req.user.userId },
2156
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2156
2157
  });
2157
2158
  res.json({ ok: true });
2158
2159
  });
2159
2160
  // POST /api/projects/:projectId/pm/plan/:planId/steps
2160
2161
  router.post("/plan/:planId/steps", async (req, res) => {
2161
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2162
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2162
2163
  if (!project) {
2163
2164
  res.status(404).json({ error: "Project not found" });
2164
2165
  return;
@@ -2168,7 +2169,7 @@ router.post("/plan/:planId/steps", async (req, res) => {
2168
2169
  res.status(400).json({ error: "Title is required" });
2169
2170
  return;
2170
2171
  }
2171
- const args = ["plan", "add-step", req.params["planId"], "--title", title.trim()];
2172
+ const args = ["plan", "add-step", routeParam(req, "planId"), "--title", title.trim()];
2172
2173
  if (description)
2173
2174
  args.push("--description", description);
2174
2175
  if (dependsOn)
@@ -2178,21 +2179,21 @@ router.post("/plan/:planId/steps", async (req, res) => {
2178
2179
  res.status(400).json({ error: result.stderr || "Failed to add step" });
2179
2180
  return;
2180
2181
  }
2181
- broadcastProjectEvent(req.params["projectId"], {
2182
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2182
2183
  type: "item-updated",
2183
- data: { itemId: req.params["planId"], userId: req.user.userId },
2184
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2184
2185
  });
2185
2186
  res.status(201).json(result.parsed || {});
2186
2187
  });
2187
2188
  // PATCH /api/projects/:projectId/pm/plan/:planId/steps/:stepRef
2188
2189
  router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
2189
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2190
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2190
2191
  if (!project) {
2191
2192
  res.status(404).json({ error: "Project not found" });
2192
2193
  return;
2193
2194
  }
2194
2195
  const { title, description } = req.body;
2195
- const args = ["plan", "update-step", req.params["planId"], req.params["stepRef"]];
2196
+ const args = ["plan", "update-step", routeParam(req, "planId"), routeParam(req, "stepRef")];
2196
2197
  if (title)
2197
2198
  args.push("--title", title);
2198
2199
  if (description)
@@ -2202,21 +2203,21 @@ router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
2202
2203
  res.status(400).json({ error: result.stderr || "Failed to update step" });
2203
2204
  return;
2204
2205
  }
2205
- broadcastProjectEvent(req.params["projectId"], {
2206
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2206
2207
  type: "item-updated",
2207
- data: { itemId: req.params["planId"], userId: req.user.userId },
2208
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2208
2209
  });
2209
2210
  res.json(result.parsed || {});
2210
2211
  });
2211
2212
  // POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/complete
2212
2213
  router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
2213
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2214
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2214
2215
  if (!project) {
2215
2216
  res.status(404).json({ error: "Project not found" });
2216
2217
  return;
2217
2218
  }
2218
2219
  const result = runPm({
2219
- args: ["plan", "complete-step", req.params["planId"], req.params["stepRef"]],
2220
+ args: ["plan", "complete-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
2220
2221
  userId: project.ownerUserId,
2221
2222
  slug: project.slug,
2222
2223
  jsonOutput: true,
@@ -2225,15 +2226,15 @@ router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
2225
2226
  res.status(400).json({ error: result.stderr || "Failed to complete step" });
2226
2227
  return;
2227
2228
  }
2228
- broadcastProjectEvent(req.params["projectId"], {
2229
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2229
2230
  type: "item-updated",
2230
- data: { itemId: req.params["planId"], userId: req.user.userId },
2231
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2231
2232
  });
2232
2233
  res.json(result.parsed || {});
2233
2234
  });
2234
2235
  // POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/block
2235
2236
  router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
2236
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2237
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2237
2238
  if (!project) {
2238
2239
  res.status(404).json({ error: "Project not found" });
2239
2240
  return;
@@ -2244,7 +2245,7 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
2244
2245
  return;
2245
2246
  }
2246
2247
  const result = runPm({
2247
- args: ["plan", "block-step", req.params["planId"], req.params["stepRef"], "--step-blocked-reason", reason.trim()],
2248
+ args: ["plan", "block-step", routeParam(req, "planId"), routeParam(req, "stepRef"), "--step-blocked-reason", reason.trim()],
2248
2249
  userId: project.ownerUserId,
2249
2250
  slug: project.slug,
2250
2251
  jsonOutput: true,
@@ -2253,21 +2254,21 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
2253
2254
  res.status(400).json({ error: result.stderr || "Failed to block step" });
2254
2255
  return;
2255
2256
  }
2256
- broadcastProjectEvent(req.params["projectId"], {
2257
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2257
2258
  type: "item-updated",
2258
- data: { itemId: req.params["planId"], userId: req.user.userId },
2259
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2259
2260
  });
2260
2261
  res.json(result.parsed || {});
2261
2262
  });
2262
2263
  // DELETE /api/projects/:projectId/pm/plan/:planId/steps/:stepRef
2263
2264
  router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
2264
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2265
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2265
2266
  if (!project) {
2266
2267
  res.status(404).json({ error: "Project not found" });
2267
2268
  return;
2268
2269
  }
2269
2270
  const result = runPm({
2270
- args: ["plan", "remove-step", req.params["planId"], req.params["stepRef"]],
2271
+ args: ["plan", "remove-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
2271
2272
  userId: project.ownerUserId,
2272
2273
  slug: project.slug,
2273
2274
  jsonOutput: true,
@@ -2276,21 +2277,21 @@ router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
2276
2277
  res.status(400).json({ error: result.stderr || "Failed to remove step" });
2277
2278
  return;
2278
2279
  }
2279
- broadcastProjectEvent(req.params["projectId"], {
2280
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2280
2281
  type: "item-updated",
2281
- data: { itemId: req.params["planId"], userId: req.user.userId },
2282
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2282
2283
  });
2283
2284
  res.json(result.parsed || {});
2284
2285
  });
2285
2286
  // POST /api/projects/:projectId/pm/plan/:planId/approve
2286
2287
  router.post("/plan/:planId/approve", async (req, res) => {
2287
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2288
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2288
2289
  if (!project) {
2289
2290
  res.status(404).json({ error: "Project not found" });
2290
2291
  return;
2291
2292
  }
2292
2293
  const result = runPm({
2293
- args: ["plan", "approve", req.params["planId"]],
2294
+ args: ["plan", "approve", routeParam(req, "planId")],
2294
2295
  userId: project.ownerUserId,
2295
2296
  slug: project.slug,
2296
2297
  jsonOutput: true,
@@ -2299,21 +2300,21 @@ router.post("/plan/:planId/approve", async (req, res) => {
2299
2300
  res.status(400).json({ error: result.stderr || "Failed to approve plan" });
2300
2301
  return;
2301
2302
  }
2302
- broadcastProjectEvent(req.params["projectId"], {
2303
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2303
2304
  type: "item-updated",
2304
- data: { itemId: req.params["planId"], userId: req.user.userId },
2305
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2305
2306
  });
2306
2307
  res.json(result.parsed || {});
2307
2308
  });
2308
2309
  // POST /api/projects/:projectId/pm/plan/:planId/materialize
2309
2310
  router.post("/plan/:planId/materialize", async (req, res) => {
2310
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2311
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2311
2312
  if (!project) {
2312
2313
  res.status(404).json({ error: "Project not found" });
2313
2314
  return;
2314
2315
  }
2315
2316
  const { materializeType, materializeParent, steps } = req.body;
2316
- const args = ["plan", "materialize", req.params["planId"]];
2317
+ const args = ["plan", "materialize", routeParam(req, "planId")];
2317
2318
  if (materializeType)
2318
2319
  args.push("--materialize-type", materializeType);
2319
2320
  if (materializeParent)
@@ -2325,7 +2326,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
2325
2326
  res.status(400).json({ error: result.stderr || "Failed to materialize plan" });
2326
2327
  return;
2327
2328
  }
2328
- broadcastProjectEvent(req.params["projectId"], {
2329
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2329
2330
  type: "item-created",
2330
2331
  data: { result: result.parsed, userId: req.user.userId },
2331
2332
  });
@@ -2333,7 +2334,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
2333
2334
  });
2334
2335
  // POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/reorder
2335
2336
  router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
2336
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2337
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2337
2338
  if (!project) {
2338
2339
  res.status(404).json({ error: "Project not found" });
2339
2340
  return;
@@ -2344,7 +2345,7 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
2344
2345
  return;
2345
2346
  }
2346
2347
  const result = runPm({
2347
- args: ["plan", "reorder-step", req.params["planId"], req.params["stepRef"], String(reorderTo)],
2348
+ args: ["plan", "reorder-step", routeParam(req, "planId"), routeParam(req, "stepRef"), String(reorderTo)],
2348
2349
  userId: project.ownerUserId,
2349
2350
  slug: project.slug,
2350
2351
  jsonOutput: true,
@@ -2353,15 +2354,15 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
2353
2354
  res.status(400).json({ error: result.stderr || "Failed to reorder step" });
2354
2355
  return;
2355
2356
  }
2356
- broadcastProjectEvent(req.params["projectId"], {
2357
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2357
2358
  type: "item-updated",
2358
- data: { itemId: req.params["planId"], userId: req.user.userId },
2359
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2359
2360
  });
2360
2361
  res.json(result.parsed || {});
2361
2362
  });
2362
2363
  // POST /api/projects/:projectId/pm/plan/:planId/link
2363
2364
  router.post("/plan/:planId/link", async (req, res) => {
2364
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2365
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2365
2366
  if (!project) {
2366
2367
  res.status(404).json({ error: "Project not found" });
2367
2368
  return;
@@ -2371,7 +2372,7 @@ router.post("/plan/:planId/link", async (req, res) => {
2371
2372
  res.status(400).json({ error: "link (item id) is required" });
2372
2373
  return;
2373
2374
  }
2374
- const args = ["plan", "link", req.params["planId"], "--link", link.trim()];
2375
+ const args = ["plan", "link", routeParam(req, "planId"), "--link", link.trim()];
2375
2376
  if (linkKind)
2376
2377
  args.push("--link-kind", linkKind);
2377
2378
  if (linkNote)
@@ -2383,15 +2384,15 @@ router.post("/plan/:planId/link", async (req, res) => {
2383
2384
  res.status(400).json({ error: result.stderr || "Failed to link plan" });
2384
2385
  return;
2385
2386
  }
2386
- broadcastProjectEvent(req.params["projectId"], {
2387
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2387
2388
  type: "item-updated",
2388
- data: { itemId: req.params["planId"], userId: req.user.userId },
2389
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2389
2390
  });
2390
2391
  res.status(201).json(result.parsed || {});
2391
2392
  });
2392
2393
  // DELETE /api/projects/:projectId/pm/plan/:planId/link
2393
2394
  router.delete("/plan/:planId/link", async (req, res) => {
2394
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2395
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2395
2396
  if (!project) {
2396
2397
  res.status(404).json({ error: "Project not found" });
2397
2398
  return;
@@ -2401,7 +2402,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
2401
2402
  res.status(400).json({ error: "link (item id) is required" });
2402
2403
  return;
2403
2404
  }
2404
- const args = ["plan", "unlink", req.params["planId"], "--link", link.trim()];
2405
+ const args = ["plan", "unlink", routeParam(req, "planId"), "--link", link.trim()];
2405
2406
  if (linkKind)
2406
2407
  args.push("--link-kind", linkKind);
2407
2408
  const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
@@ -2409,9 +2410,9 @@ router.delete("/plan/:planId/link", async (req, res) => {
2409
2410
  res.status(400).json({ error: result.stderr || "Failed to unlink plan" });
2410
2411
  return;
2411
2412
  }
2412
- broadcastProjectEvent(req.params["projectId"], {
2413
+ broadcastProjectEvent(routeParam(req, "projectId"), {
2413
2414
  type: "item-updated",
2414
- data: { itemId: req.params["planId"], userId: req.user.userId },
2415
+ data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
2415
2416
  });
2416
2417
  res.json(result.parsed || {});
2417
2418
  });
@@ -2420,7 +2421,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
2420
2421
  // POST /api/projects/:projectId/pm/upgrade
2421
2422
  // Runs pm upgrade --packages-only (never upgrades the CLI itself via the web UI).
2422
2423
  router.get("/upgrade", async (req, res) => {
2423
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2424
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2424
2425
  if (!project) {
2425
2426
  res.status(404).json({ error: "Project not found" });
2426
2427
  return;
@@ -2434,7 +2435,7 @@ router.get("/upgrade", async (req, res) => {
2434
2435
  res.json(result.ok ? (result.parsed || { dryRun: true }) : { error: result.stderr });
2435
2436
  });
2436
2437
  router.post("/upgrade", async (req, res) => {
2437
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2438
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2438
2439
  if (!project) {
2439
2440
  res.status(404).json({ error: "Project not found" });
2440
2441
  return;
@@ -2455,7 +2456,7 @@ router.post("/upgrade", async (req, res) => {
2455
2456
  // ─── Presence endpoints ───
2456
2457
  // GET /api/projects/:projectId/pm/presence
2457
2458
  router.get("/presence", async (req, res) => {
2458
- const projectId = req.params["projectId"];
2459
+ const projectId = routeParam(req, "projectId");
2459
2460
  res.json({ users: getProjectPresence(projectId) });
2460
2461
  });
2461
2462
  // PATCH /api/projects/:projectId/pm/presence/:clientId
@@ -2470,7 +2471,7 @@ router.patch("/presence/:clientId", async (req, res) => {
2470
2471
  // GET /api/projects/:projectId/pm/events
2471
2472
  router.get("/events", async (req, res) => {
2472
2473
  try {
2473
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2474
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2474
2475
  if (!project) {
2475
2476
  res.status(404).json({ error: "Project not found" });
2476
2477
  return;
@@ -2483,7 +2484,7 @@ router.get("/events", async (req, res) => {
2483
2484
  }
2484
2485
  setupSSEHeaders(res);
2485
2486
  const clientId = uuidv4();
2486
- const projectId = req.params["projectId"];
2487
+ const projectId = routeParam(req, "projectId");
2487
2488
  const userId = req.user.userId;
2488
2489
  // Client sends display name as query param; fall back to email
2489
2490
  const displayName = String(req.query["dn"] ?? req.user.email ?? userId);
@@ -2517,12 +2518,12 @@ router.get("/events", async (req, res) => {
2517
2518
  // ─── Presence endpoint ───
2518
2519
  // GET /api/projects/:projectId/pm/presence
2519
2520
  router.get("/presence", async (req, res) => {
2520
- const project = await verifyProject(req.user.userId, req.params["projectId"]);
2521
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
2521
2522
  if (!project) {
2522
2523
  res.status(404).json({ error: "Project not found" });
2523
2524
  return;
2524
2525
  }
2525
- const users = getProjectPresence(req.params["projectId"]);
2526
+ const users = getProjectPresence(routeParam(req, "projectId"));
2526
2527
  res.json({ users });
2527
2528
  });
2528
2529
  export { router as pmRouter };