esedre 0.1.7 → 0.1.8

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/README.md CHANGED
@@ -88,6 +88,8 @@ ese start
88
88
  # Open dashboard: http://localhost:5674/app
89
89
  ```
90
90
 
91
+ The server launches with unconstrained portfolio visibility across all registered data hubs and projects, allowing the main Esedre Web UI (as opposed to embedded component and LLM agent project-scoped behavior) to manage them seamlessly. Incoming agent requests enforce project scoping dynamically via the `x-esedre-allowed-projects` header or `?allowedProjects=...` query parameter.
92
+
91
93
  ### 4. Create Your First Ticket
92
94
 
93
95
  ```bash
@@ -260,6 +262,7 @@ Esedre enforces clean project isolation so each LLM agent is informed only of th
260
262
  - Unauthorized requests throw `EsedreAuthorizationError`:
261
263
  - **CLI**: Prints `Access Denied: ...` and exits with status code 1.
262
264
  - **MCP**: Responds with standard JSON-RPC error `-32603`.
265
+ - **REST API**: Responds with HTTP status code `403 Forbidden`.
263
266
 
264
267
  ---
265
268
 
package/dist/esedre.mjs CHANGED
@@ -9,7 +9,7 @@ import fs3 from "node:fs";
9
9
  import path3 from "node:path";
10
10
 
11
11
  // src/types.ts
12
- var CURRENT_ESEDRE_VERSION = "0.1.7";
12
+ var CURRENT_ESEDRE_VERSION = "0.1.8";
13
13
  var EsedreConflictError = class extends Error {
14
14
  constructor(ticketId, currentHash, lastHash) {
15
15
  super(
@@ -539,12 +539,12 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
539
539
  }
540
540
 
541
541
  // src/storage/filesystem.ts
542
- function isProjectMatch(code, slug, filter) {
542
+ function isProjectMatch(code, filter) {
543
+ if (!code || !filter) return false;
543
544
  const f = filter.trim().toLowerCase();
544
545
  const c = code.trim().toLowerCase();
545
- const s = slug.trim().toLowerCase();
546
- if (c === f || s === f) return true;
547
- if ((f === "prof" || f === "core" || f === "pasrc") && (c === "prof" || c === "core")) return true;
546
+ if (c === f) return true;
547
+ if ((f === "profe" || f === "prof" || f === "core" || f === "pasrc") && (c === "profe" || c === "prof" || c === "core")) return true;
548
548
  if ((f === "esedre" || f === "ese" || f === "docs") && (c === "esedre" || c === "docs")) return true;
549
549
  if ((f === "alce" || f === "web") && (c === "alce" || c === "web")) return true;
550
550
  return false;
@@ -572,6 +572,9 @@ function writeSafeFile(filePath, content) {
572
572
  var FilesystemStorageAdapter = class {
573
573
  workspaceRoot;
574
574
  config;
575
+ isConfigExplicit = false;
576
+ lastConfigCheck = 0;
577
+ configTtlMs = 3e3;
575
578
  duplicateProjectWarnings = [];
576
579
  getDuplicateProjectWarnings() {
577
580
  this.resolveProjectLocations();
@@ -581,13 +584,39 @@ var FilesystemStorageAdapter = class {
581
584
  this.workspaceRoot = workspaceRoot || this.resolveWorkspaceRoot();
582
585
  if (config) {
583
586
  this.config = config;
587
+ this.isConfigExplicit = true;
588
+ this.lastConfigCheck = Date.now();
584
589
  } else {
585
- try {
586
- const discovered = findEsedreConfig(this.workspaceRoot);
587
- this.config = discovered.config || {};
588
- } catch {
589
- this.config = {};
590
+ this.config = {};
591
+ this.refreshConfig();
592
+ }
593
+ }
594
+ refreshConfig() {
595
+ try {
596
+ const discovered = findEsedreConfig(this.workspaceRoot);
597
+ this.config = discovered.config || {};
598
+ } catch {
599
+ this.config = {};
600
+ }
601
+ this.lastConfigCheck = Date.now();
602
+ }
603
+ ensureFreshConfig() {
604
+ if (this.isConfigExplicit && this.config.allowedProjects?.includes("*")) {
605
+ if (Date.now() - this.lastConfigCheck > this.configTtlMs) {
606
+ try {
607
+ const discovered = findEsedreConfig(this.workspaceRoot, { fallbackToGlobal: true });
608
+ this.config = {
609
+ ...discovered.config,
610
+ allowedProjects: ["*"]
611
+ };
612
+ } catch {
613
+ }
614
+ this.lastConfigCheck = Date.now();
590
615
  }
616
+ return;
617
+ }
618
+ if (!this.isConfigExplicit && Date.now() - this.lastConfigCheck > this.configTtlMs) {
619
+ this.refreshConfig();
591
620
  }
592
621
  }
593
622
  resolveWorkspaceRoot() {
@@ -606,6 +635,7 @@ var FilesystemStorageAdapter = class {
606
635
  return process.cwd();
607
636
  }
608
637
  resolveProjectLocations() {
638
+ this.ensureFreshConfig();
609
639
  const locations = [];
610
640
  const seenCodes = /* @__PURE__ */ new Set();
611
641
  const codeToSource = /* @__PURE__ */ new Map();
@@ -649,9 +679,6 @@ var FilesystemStorageAdapter = class {
649
679
  if (fs3.existsSync(pJsonPath)) {
650
680
  try {
651
681
  desc = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
652
- if (desc && !desc.slug) {
653
- desc.slug = (desc.code || ent.name).toLowerCase();
654
- }
655
682
  } catch {
656
683
  }
657
684
  }
@@ -663,7 +690,7 @@ var FilesystemStorageAdapter = class {
663
690
  try {
664
691
  const hubProjs = JSON.parse(fs3.readFileSync(f, "utf-8"));
665
692
  desc = hubProjs.find(
666
- (p) => p.code.toLowerCase() === ent.name.toLowerCase() || p.slug.toLowerCase() === ent.name.toLowerCase()
693
+ (p) => p.code.toLowerCase() === ent.name.toLowerCase()
667
694
  );
668
695
  if (desc) break;
669
696
  } catch {
@@ -675,7 +702,6 @@ var FilesystemStorageAdapter = class {
675
702
  desc = {
676
703
  id: locations.length + 1,
677
704
  code: ent.name,
678
- slug: ent.name.toLowerCase(),
679
705
  name: ent.name,
680
706
  description: `${ent.name} project`
681
707
  };
@@ -727,7 +753,6 @@ var FilesystemStorageAdapter = class {
727
753
  descs = [{
728
754
  id: 1,
729
755
  code: pCode,
730
- slug: pCode.toLowerCase(),
731
756
  name: pCode,
732
757
  description: `${pCode} project`
733
758
  }];
@@ -782,9 +807,6 @@ var FilesystemStorageAdapter = class {
782
807
  if (fs3.existsSync(cand)) {
783
808
  try {
784
809
  desc = JSON.parse(fs3.readFileSync(cand, "utf-8"));
785
- if (desc && !desc.slug) {
786
- desc.slug = (desc.code || code).toLowerCase();
787
- }
788
810
  break;
789
811
  } catch {
790
812
  }
@@ -794,7 +816,6 @@ var FilesystemStorageAdapter = class {
794
816
  desc = {
795
817
  id: locations.length + 1,
796
818
  code,
797
- slug: code.toLowerCase(),
798
819
  name: code,
799
820
  description: `${code} project`
800
821
  };
@@ -862,7 +883,6 @@ var FilesystemStorageAdapter = class {
862
883
  descs = [{
863
884
  id: 1,
864
885
  code: this.config.projectCode,
865
- slug: this.config.projectCode.toLowerCase(),
866
886
  name: pName,
867
887
  description: `${pName} project`
868
888
  }];
@@ -894,7 +914,7 @@ var FilesystemStorageAdapter = class {
894
914
  const cleanCode = rawCode;
895
915
  const cleanName = input.name?.trim() || cleanCode;
896
916
  const cleanDesc = input.description?.trim() || `${cleanName} project`;
897
- const slug = cleanCode.toLowerCase();
917
+ const cleanLower = cleanCode.toLowerCase();
898
918
  const colors2 = input.colors || {
899
919
  badge: "border-cyan-500/30 bg-cyan-500/10 text-cyan-300",
900
920
  dot: "bg-cyan-400",
@@ -978,7 +998,7 @@ ${formattedList}`
978
998
  const normalizedTargetHub = path3.resolve(targetHub).toLowerCase();
979
999
  const existingLocations = this.resolveProjectLocations();
980
1000
  const existingConflict = existingLocations.find((l) => {
981
- if (l.project.code.toLowerCase() !== slug) return false;
1001
+ if (l.project.code.toLowerCase() !== cleanLower) return false;
982
1002
  if (l.sourceType === "hub" && l.hubDir) {
983
1003
  return path3.resolve(l.hubDir).toLowerCase() !== normalizedTargetHub;
984
1004
  }
@@ -996,7 +1016,7 @@ ${formattedList}`
996
1016
  try {
997
1017
  const entries = fs3.readdirSync(hubProjectsDir, { withFileTypes: true });
998
1018
  const matchedDir = entries.find(
999
- (e) => e.isDirectory() && e.name.toLowerCase() === slug
1019
+ (e) => e.isDirectory() && e.name.toLowerCase() === cleanLower
1000
1020
  );
1001
1021
  if (matchedDir) {
1002
1022
  resolvedDirName = matchedDir.name;
@@ -1024,7 +1044,6 @@ ${formattedList}`
1024
1044
  const projectDesc2 = {
1025
1045
  id: id2,
1026
1046
  code: resolvedCode2,
1027
- slug,
1028
1047
  name: cleanName,
1029
1048
  description: cleanDesc,
1030
1049
  colors: colors2
@@ -1034,7 +1053,7 @@ ${formattedList}`
1034
1053
  if (fs3.existsSync(hubProjectsFile)) {
1035
1054
  try {
1036
1055
  const arr = JSON.parse(fs3.readFileSync(hubProjectsFile, "utf-8"));
1037
- const idx = arr.findIndex((p) => p.code.toLowerCase() === slug);
1056
+ const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
1038
1057
  if (idx >= 0) {
1039
1058
  arr[idx] = projectDesc2;
1040
1059
  } else {
@@ -1059,12 +1078,11 @@ ${formattedList}`
1059
1078
  } catch {
1060
1079
  }
1061
1080
  }
1062
- const resolvedCode = existingPJson?.code && existingPJson.code.toLowerCase() === slug ? existingPJson.code : cleanCode;
1081
+ const resolvedCode = existingPJson?.code && existingPJson.code.toLowerCase() === cleanLower ? existingPJson.code : cleanCode;
1063
1082
  const id = existingPJson?.id || 1;
1064
1083
  const projectDesc = {
1065
1084
  id,
1066
1085
  code: resolvedCode,
1067
- slug,
1068
1086
  name: cleanName,
1069
1087
  description: cleanDesc,
1070
1088
  colors: colors2
@@ -1074,7 +1092,7 @@ ${formattedList}`
1074
1092
  if (fs3.existsSync(projectsJsonPath)) {
1075
1093
  try {
1076
1094
  const arr = JSON.parse(fs3.readFileSync(projectsJsonPath, "utf-8"));
1077
- const idx = arr.findIndex((p) => p.code.toLowerCase() === slug);
1095
+ const idx = arr.findIndex((p) => p.code.toLowerCase() === cleanLower);
1078
1096
  if (idx >= 0) {
1079
1097
  arr[idx] = projectDesc;
1080
1098
  } else {
@@ -1094,7 +1112,7 @@ ${formattedList}`
1094
1112
  if (prefixMatch) {
1095
1113
  const code = prefixMatch[1].toLowerCase();
1096
1114
  const num2 = parseInt(prefixMatch[2], 10);
1097
- const loc = locations.find((l) => isProjectMatch(l.project.code, l.project.slug, code));
1115
+ const loc = locations.find((l) => isProjectMatch(l.project.code, code));
1098
1116
  if (!loc) return null;
1099
1117
  const ticketDir = path3.join(loc.ticketsDir, String(num2));
1100
1118
  const metaPath = path3.join(ticketDir, "meta.json");
@@ -1106,7 +1124,7 @@ ${formattedList}`
1106
1124
  meta.type = meta.type || meta.category || "Feature";
1107
1125
  meta.category = meta.type;
1108
1126
  const mProj = (meta.project || "").toLowerCase();
1109
- if (mProj && mProj !== code && mProj !== loc.project.slug.toLowerCase()) {
1127
+ if (mProj && mProj !== code && mProj !== loc.project.code.toLowerCase()) {
1110
1128
  return null;
1111
1129
  }
1112
1130
  } catch {
@@ -1131,7 +1149,7 @@ ${formattedList}`
1131
1149
  const metaRaw = fs3.readFileSync(metaPath, "utf-8");
1132
1150
  const meta = JSON.parse(metaRaw);
1133
1151
  const matchedLoc = locations.find(
1134
- (l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && (l.project.code.toLowerCase() === meta.project.toLowerCase() || l.project.slug.toLowerCase() === meta.project.toLowerCase())
1152
+ (l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
1135
1153
  ) || loc;
1136
1154
  matches.push({ loc: matchedLoc, ticketDir, id: num });
1137
1155
  } catch {
@@ -1144,7 +1162,7 @@ ${formattedList}`
1144
1162
  if (this.config.projectCode) {
1145
1163
  const prefLower = this.config.projectCode.toLowerCase();
1146
1164
  const preferredMatch = matches.find(
1147
- (m) => m.loc.project.code.toLowerCase() === prefLower || m.loc.project.slug.toLowerCase() === prefLower
1165
+ (m) => m.loc.project.code.toLowerCase() === prefLower
1148
1166
  );
1149
1167
  if (preferredMatch) return preferredMatch;
1150
1168
  }
@@ -1166,7 +1184,7 @@ ${formattedList}`
1166
1184
  for (const loc of locations) {
1167
1185
  if (filter?.project && filter.project !== "all") {
1168
1186
  const pLower = filter.project.toLowerCase();
1169
- if (!isProjectMatch(loc.project.code, loc.project.slug, filter.project)) {
1187
+ if (!isProjectMatch(loc.project.code, filter.project)) {
1170
1188
  continue;
1171
1189
  }
1172
1190
  }
@@ -1198,7 +1216,7 @@ ${formattedList}`
1198
1216
  let effectiveLoc = loc;
1199
1217
  if (loc.isSharedDir) {
1200
1218
  const found = locations.find(
1201
- (l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && (l.project.code.toLowerCase() === meta.project.toLowerCase() || l.project.slug.toLowerCase() === meta.project.toLowerCase())
1219
+ (l) => meta.projectId !== void 0 && l.project.id === meta.projectId || meta.project && l.project.code.toLowerCase() === meta.project.toLowerCase()
1202
1220
  );
1203
1221
  if (found) effectiveLoc = found;
1204
1222
  }
@@ -1206,7 +1224,7 @@ ${formattedList}`
1206
1224
  meta.projectId = effectiveLoc.project.id;
1207
1225
  if (filter?.project && filter.project !== "all") {
1208
1226
  const pLower = filter.project.toLowerCase();
1209
- if (!isProjectMatch(meta.project, effectiveLoc.project.slug, filter.project)) {
1227
+ if (!isProjectMatch(meta.project, filter.project)) {
1210
1228
  continue;
1211
1229
  }
1212
1230
  }
@@ -1373,7 +1391,7 @@ ${formattedList}`
1373
1391
  if (requestedIdentifier) {
1374
1392
  const lower = String(requestedIdentifier).trim().toLowerCase();
1375
1393
  targetLoc = locations.find(
1376
- (l) => l.project.code?.toLowerCase() === lower || l.project.slug?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
1394
+ (l) => l.project.code?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
1377
1395
  );
1378
1396
  if (!targetLoc) {
1379
1397
  throw new Error(`Project '${requestedIdentifier}' is invalid or not registered in this workspace.`);
@@ -2579,11 +2597,13 @@ async function startDaemon(options = {}) {
2579
2597
  if (!pid) {
2580
2598
  throw new Error("Failed to spawn Esedre daemon background process.");
2581
2599
  }
2600
+ const timeoutMs = process.platform === "win32" ? 1e4 : 5e3;
2601
+ const pingTimeout = process.platform === "win32" ? 500 : 300;
2582
2602
  const startTime = Date.now();
2583
2603
  let ready = false;
2584
- while (Date.now() - startTime < 3500) {
2585
- await new Promise((r) => setTimeout(r, 100));
2586
- const ping = await pingDaemon(port, 150);
2604
+ while (Date.now() - startTime < timeoutMs) {
2605
+ await new Promise((r) => setTimeout(r, 150));
2606
+ const ping = await pingDaemon(port, pingTimeout);
2587
2607
  if (ping.responding && ping.isEsedre) {
2588
2608
  ready = true;
2589
2609
  break;
@@ -3228,7 +3248,6 @@ function configureWorkspace(targetDir, options = {}) {
3228
3248
  const projDesc = {
3229
3249
  id: 1,
3230
3250
  code: projectCode.toUpperCase(),
3231
- slug: projectCode.toLowerCase(),
3232
3251
  name: projectName || projectCode.toUpperCase(),
3233
3252
  description: `${projectName || projectCode.toUpperCase()} project`,
3234
3253
  colors: {
@@ -3401,16 +3420,30 @@ function sendJson(res, status, data) {
3401
3420
  "Content-Type": "application/json; charset=utf-8",
3402
3421
  "Access-Control-Allow-Origin": "*",
3403
3422
  "Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
3404
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
3423
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, x-esedre-allowed-projects"
3405
3424
  });
3406
3425
  res.end(JSON.stringify(data));
3407
3426
  }
3408
- var cachedAllData = null;
3409
- var cachedAllTimestamp = 0;
3427
+ var cachedAllData = /* @__PURE__ */ new Map();
3410
3428
  var CACHE_TTL_MS = 5e3;
3411
3429
  function invalidateApiCache() {
3412
- cachedAllData = null;
3413
- cachedAllTimestamp = 0;
3430
+ cachedAllData.clear();
3431
+ }
3432
+ function getRequestStorage(baseStorage, req, url) {
3433
+ const headerAllowed = req.headers["x-esedre-allowed-projects"];
3434
+ const queryAllowed = url.searchParams.get("allowedProjects");
3435
+ const rawAllowed = (typeof headerAllowed === "string" ? headerAllowed : Array.isArray(headerAllowed) ? headerAllowed[0] : null) || queryAllowed;
3436
+ if (rawAllowed && typeof rawAllowed === "string") {
3437
+ const list = rawAllowed.split(",").map((s) => s.trim()).filter(Boolean);
3438
+ if (list.length > 0 && !list.includes("*")) {
3439
+ const sortedKey = [...list].sort().join(",");
3440
+ return {
3441
+ storage: new SecurityFilter(baseStorage, { allowedProjects: list }),
3442
+ cacheKey: sortedKey
3443
+ };
3444
+ }
3445
+ }
3446
+ return { storage: baseStorage, cacheKey: "*" };
3414
3447
  }
3415
3448
  function createApiHandler(storage, workspaceRoot) {
3416
3449
  return async (req, res) => {
@@ -3436,6 +3469,7 @@ function createApiHandler(storage, workspaceRoot) {
3436
3469
  if (pathname.startsWith("/esedre/api")) {
3437
3470
  pathname = pathname.slice("/esedre".length);
3438
3471
  }
3472
+ const { storage: reqStorage, cacheKey } = getRequestStorage(storage, req, url);
3439
3473
  try {
3440
3474
  if (req.method === "GET") {
3441
3475
  if (pathname === "/api/ping") {
@@ -3443,7 +3477,7 @@ function createApiHandler(storage, workspaceRoot) {
3443
3477
  return true;
3444
3478
  }
3445
3479
  if (pathname === "/api/planning/projects") {
3446
- const projects = await storage.getProjects();
3480
+ const projects = await reqStorage.getProjects();
3447
3481
  sendJson(res, 200, projects);
3448
3482
  return true;
3449
3483
  }
@@ -3453,7 +3487,7 @@ function createApiHandler(storage, workspaceRoot) {
3453
3487
  const type = url.searchParams.get("type") || url.searchParams.get("category") || void 0;
3454
3488
  const category = type;
3455
3489
  const search = url.searchParams.get("search") || void 0;
3456
- const tickets = await storage.listTickets({
3490
+ const tickets = await reqStorage.listTickets({
3457
3491
  project: project === "all" ? void 0 : project,
3458
3492
  status,
3459
3493
  type,
@@ -3469,7 +3503,7 @@ function createApiHandler(storage, workspaceRoot) {
3469
3503
  sendJson(res, 400, { error: "Invalid ticket ID" });
3470
3504
  return true;
3471
3505
  }
3472
- const ticket = await storage.getTicket(idStr);
3506
+ const ticket = await reqStorage.getTicket(idStr);
3473
3507
  if (!ticket) {
3474
3508
  sendJson(res, 400, { error: "Ticket not found" });
3475
3509
  return true;
@@ -3479,11 +3513,12 @@ function createApiHandler(storage, workspaceRoot) {
3479
3513
  }
3480
3514
  if (pathname === "/api/planning/all") {
3481
3515
  const now = Date.now();
3482
- if (cachedAllData && now - cachedAllTimestamp < CACHE_TTL_MS) {
3483
- sendJson(res, 200, cachedAllData);
3516
+ const cached = cachedAllData.get(cacheKey);
3517
+ if (cached && now - cached.timestamp < CACHE_TTL_MS) {
3518
+ sendJson(res, 200, cached.data);
3484
3519
  return true;
3485
3520
  }
3486
- const tickets = await storage.listTickets({});
3521
+ const tickets = await reqStorage.listTickets({});
3487
3522
  const metasMap = {};
3488
3523
  const detailsMap = {};
3489
3524
  const plansMap = {};
@@ -3513,7 +3548,7 @@ function createApiHandler(storage, workspaceRoot) {
3513
3548
  }
3514
3549
  }
3515
3550
  }
3516
- const projects = await storage.getProjects();
3551
+ const projects = await reqStorage.getProjects();
3517
3552
  const responsePayload = {
3518
3553
  success: true,
3519
3554
  projects,
@@ -3526,13 +3561,12 @@ function createApiHandler(storage, workspaceRoot) {
3526
3561
  planHistory: {},
3527
3562
  ticketHistory: {}
3528
3563
  };
3529
- cachedAllData = responsePayload;
3530
- cachedAllTimestamp = now;
3564
+ cachedAllData.set(cacheKey, { data: responsePayload, timestamp: now });
3531
3565
  sendJson(res, 200, responsePayload);
3532
3566
  return true;
3533
3567
  }
3534
3568
  if (pathname === "/api/planning/metas") {
3535
- const tickets = await storage.listTickets({});
3569
+ const tickets = await reqStorage.listTickets({});
3536
3570
  const metasMap = {};
3537
3571
  for (const t of tickets) {
3538
3572
  metasMap[String(t.meta.id)] = t.meta;
@@ -3541,7 +3575,7 @@ function createApiHandler(storage, workspaceRoot) {
3541
3575
  return true;
3542
3576
  }
3543
3577
  if (pathname === "/api/planning/details") {
3544
- const tickets = await storage.listTickets({});
3578
+ const tickets = await reqStorage.listTickets({});
3545
3579
  const detailsMap = {};
3546
3580
  for (const t of tickets) {
3547
3581
  if (t.detail?.raw) {
@@ -3552,7 +3586,7 @@ function createApiHandler(storage, workspaceRoot) {
3552
3586
  return true;
3553
3587
  }
3554
3588
  if (pathname === "/api/planning/plans") {
3555
- const tickets = await storage.listTickets({});
3589
+ const tickets = await reqStorage.listTickets({});
3556
3590
  const plansMap = {};
3557
3591
  for (const t of tickets) {
3558
3592
  if (t.planMarkdown) {
@@ -3563,7 +3597,7 @@ function createApiHandler(storage, workspaceRoot) {
3563
3597
  return true;
3564
3598
  }
3565
3599
  if (pathname === "/api/planning/comments") {
3566
- const tickets = await storage.listTickets({});
3600
+ const tickets = await reqStorage.listTickets({});
3567
3601
  const commentsMap = {};
3568
3602
  for (const t of tickets) {
3569
3603
  commentsMap[String(t.meta.id)] = t.comments || [];
@@ -3593,7 +3627,7 @@ function createApiHandler(storage, workspaceRoot) {
3593
3627
  sendJson(res, 400, { error: "Project query parameter is required for snapshot generation (no default fallback)." });
3594
3628
  return true;
3595
3629
  }
3596
- const snapshot = await generateProjectSnapshot(storage, projectCode, workspaceRoot);
3630
+ const snapshot = await generateProjectSnapshot(reqStorage, projectCode, workspaceRoot);
3597
3631
  sendJson(res, 200, snapshot);
3598
3632
  return true;
3599
3633
  }
@@ -3602,7 +3636,7 @@ function createApiHandler(storage, workspaceRoot) {
3602
3636
  const body = await readJsonBody(req);
3603
3637
  if (pathname === "/api/planning/tickets") {
3604
3638
  invalidateApiCache();
3605
- const created = await storage.createTicket(body);
3639
+ const created = await reqStorage.createTicket(body);
3606
3640
  sendJson(res, 201, { ticketId: String(created.meta.id), meta: created.meta });
3607
3641
  return true;
3608
3642
  }
@@ -3623,7 +3657,7 @@ function createApiHandler(storage, workspaceRoot) {
3623
3657
  return true;
3624
3658
  }
3625
3659
  invalidateApiCache();
3626
- const project = await storage.registerProject({
3660
+ const project = await reqStorage.registerProject({
3627
3661
  code,
3628
3662
  name,
3629
3663
  description,
@@ -3636,27 +3670,27 @@ function createApiHandler(storage, workspaceRoot) {
3636
3670
  if (pathname === "/api/planning/plans") {
3637
3671
  const { ticketId, planMarkdown, lastHash } = body;
3638
3672
  invalidateApiCache();
3639
- await storage.savePlan(ticketId, planMarkdown, lastHash);
3673
+ await reqStorage.savePlan(ticketId, planMarkdown, lastHash);
3640
3674
  sendJson(res, 200, { success: true });
3641
3675
  return true;
3642
3676
  }
3643
3677
  if (pathname === "/api/planning/comments") {
3644
3678
  const { ticketId, text, author } = body;
3645
3679
  invalidateApiCache();
3646
- await storage.addComment(ticketId, { author: author || "Developer", text });
3647
- const ticket = await storage.getTicket(ticketId);
3680
+ await reqStorage.addComment(ticketId, { author: author || "Developer", text });
3681
+ const ticket = await reqStorage.getTicket(ticketId);
3648
3682
  sendJson(res, 200, { comments: ticket?.comments || [] });
3649
3683
  return true;
3650
3684
  }
3651
3685
  if (pathname === "/api/planning/update-meta") {
3652
3686
  const { ticketId, updates, lastHash } = body;
3653
- const updated = await storage.updateTicket(ticketId, updates, lastHash);
3687
+ const updated = await reqStorage.updateTicket(ticketId, updates, lastHash);
3654
3688
  sendJson(res, 200, { meta: updated.meta });
3655
3689
  return true;
3656
3690
  }
3657
3691
  if (pathname === "/api/planning/toggle-flag") {
3658
3692
  const { ticketId, flagged } = body;
3659
- const updated = await storage.updateTicket(ticketId, {
3693
+ const updated = await reqStorage.updateTicket(ticketId, {
3660
3694
  featureFlag: flagged ? "chat_enhanced" : ""
3661
3695
  });
3662
3696
  sendJson(res, 200, { meta: updated.meta });
@@ -3665,7 +3699,7 @@ function createApiHandler(storage, workspaceRoot) {
3665
3699
  if (pathname === "/api/planning/details") {
3666
3700
  const { ticketId, detailMarkdown, metaUpdates } = body;
3667
3701
  if (metaUpdates) {
3668
- await storage.updateTicket(ticketId, metaUpdates);
3702
+ await reqStorage.updateTicket(ticketId, metaUpdates);
3669
3703
  }
3670
3704
  sendJson(res, 200, { success: true, detail: detailMarkdown });
3671
3705
  return true;
@@ -3674,6 +3708,10 @@ function createApiHandler(storage, workspaceRoot) {
3674
3708
  sendJson(res, 404, { error: `Endpoint not found: ${pathname}` });
3675
3709
  return true;
3676
3710
  } catch (err) {
3711
+ if (err.name === "EsedreAuthorizationError" || err.message?.includes("outside this workspace's authorized scope") || err.message?.includes("Access Denied")) {
3712
+ sendJson(res, 403, { error: err.message });
3713
+ return true;
3714
+ }
3677
3715
  console.error("[Esedre API Error]:", err);
3678
3716
  sendJson(res, 500, { error: err.message });
3679
3717
  return true;
@@ -4435,17 +4473,22 @@ ${colors.bold}Commands:${colors.reset}`);
4435
4473
  return;
4436
4474
  }
4437
4475
  case "start": {
4438
- printDuplicateProjectWarnings(storage, isJson);
4439
4476
  const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
4440
4477
  const ports = resolvePorts(discovered.config, { port: flagPort });
4441
4478
  const port = ports.gateway;
4442
4479
  const isForeground = Boolean(flags["foreground"] || flags["f"]);
4480
+ const serverConfig = {
4481
+ ...discovered.config,
4482
+ allowedProjects: ["*"]
4483
+ };
4484
+ const serverStorage = new FilesystemStorageAdapter(discovered.workspaceRoot, serverConfig);
4485
+ printDuplicateProjectWarnings(serverStorage, isJson);
4443
4486
  if (isForeground) {
4444
4487
  const cluster = startGatewayCluster({
4445
4488
  gatewayPort: ports.gateway,
4446
4489
  uiPort: ports.ui,
4447
4490
  apiPort: ports.api,
4448
- storage,
4491
+ storage: serverStorage,
4449
4492
  workspaceRoot: discovered.workspaceRoot
4450
4493
  });
4451
4494
  console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${ports.gateway}${colors.reset}`);