@promptowl/contextnest-community 1.11.0 → 1.13.0

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.
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GUNJTORH.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-3M7677XW.js";
6
+ } from "./chunk-5QQ7WKAI.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -2,7 +2,7 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-3M7677XW.js";
5
+ } from "./chunk-5QQ7WKAI.js";
6
6
  import "./chunk-SLTQACJW.js";
7
7
  export {
8
8
  getDb,
@@ -6,9 +6,9 @@ import {
6
6
  listGrants,
7
7
  listUserGrants,
8
8
  resolveNodeGrant
9
- } from "./chunk-ZMSU7BCC.js";
9
+ } from "./chunk-ZQS5W45U.js";
10
10
  import "./chunk-GUNJTORH.js";
11
- import "./chunk-3M7677XW.js";
11
+ import "./chunk-5QQ7WKAI.js";
12
12
  import "./chunk-SLTQACJW.js";
13
13
  export {
14
14
  createGrant,
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  seedDefaultEdgeTypes,
29
29
  submitForReview,
30
30
  verifySmtp
31
- } from "./chunk-XB7T3THE.js";
31
+ } from "./chunk-PAIP5KHB.js";
32
32
  import {
33
33
  createGrant,
34
34
  deleteGrant,
@@ -37,7 +37,7 @@ import {
37
37
  listGrants,
38
38
  listUserGrants,
39
39
  resolveNodeGrant
40
- } from "./chunk-ZMSU7BCC.js";
40
+ } from "./chunk-ZQS5W45U.js";
41
41
  import {
42
42
  generateApiKey,
43
43
  getKeyPrefix,
@@ -53,8 +53,9 @@ import {
53
53
  getCurrentVersion,
54
54
  getDisplayStatus,
55
55
  getVersions,
56
- setApprovedVersion
57
- } from "./chunk-JHIU6RZU.js";
56
+ setApprovedVersion,
57
+ upsertVersion
58
+ } from "./chunk-EG77SQHO.js";
58
59
  import {
59
60
  addMember,
60
61
  buildTitleMap,
@@ -63,7 +64,6 @@ import {
63
64
  canUserAccess,
64
65
  canUserApprove,
65
66
  canUserEdit,
66
- collabPermToRole,
67
67
  createNest,
68
68
  createStewardRecord,
69
69
  createTeam,
@@ -98,6 +98,7 @@ import {
98
98
  listSharedNests,
99
99
  listStewards,
100
100
  listTeamsForUser,
101
+ listVisibleNests,
101
102
  loadAccessConfig,
102
103
  loadGrantedSuperAdmins,
103
104
  loadServerSettings,
@@ -111,12 +112,12 @@ import {
111
112
  renameNest,
112
113
  renameTeam,
113
114
  replaceTeamRoster,
115
+ resolveGrantRoles,
114
116
  resolveMemberIdentity,
115
117
  resolveNestPath,
116
118
  resolveNestPermission,
117
119
  resolveStewardsForNode,
118
120
  resolveStewardsWithFallback,
119
- resolveTeamRolesForUser,
120
121
  resolveUserRoles,
121
122
  setAllowSelfApprove,
122
123
  setStewardshipEnabled,
@@ -130,7 +131,7 @@ import {
130
131
  updateMemberRole,
131
132
  updateSteward,
132
133
  validateLicense
133
- } from "./chunk-F7EKYNXG.js";
134
+ } from "./chunk-Q5NRCDD4.js";
134
135
  import {
135
136
  AppError,
136
137
  ConflictError,
@@ -153,7 +154,7 @@ import {
153
154
  initDb,
154
155
  isEmailListish,
155
156
  isEmailish
156
- } from "./chunk-3M7677XW.js";
157
+ } from "./chunk-5QQ7WKAI.js";
157
158
  import {
158
159
  ANON_EMAIL,
159
160
  ANON_USER_ID
@@ -194,6 +195,12 @@ async function createSession(userId, userAgent) {
194
195
  VALUES (?, ?, ?, ?)`,
195
196
  [id, userId, expiryIso(), userAgent || null]
196
197
  );
198
+ try {
199
+ await db.run("UPDATE users SET is_invited = 0 WHERE id = ? AND is_invited = 1", [
200
+ userId
201
+ ]);
202
+ } catch {
203
+ }
197
204
  return id;
198
205
  }
199
206
  async function resolveSession(id) {
@@ -895,6 +902,9 @@ authRoutes.post("/register", async (c) => {
895
902
  if (!body.email || !body.password) {
896
903
  throw new ValidationError("email and password are required");
897
904
  }
905
+ if (!isEmailish(body.email)) {
906
+ throw new ValidationError("Enter a valid email address.");
907
+ }
898
908
  assertValidPassword(body.password);
899
909
  const email = normalizeEmail(body.email);
900
910
  const ip = clientIp(c);
@@ -974,12 +984,6 @@ authRoutes.post("/login", async (c) => {
974
984
  console.log(`[auth] login OK \u2014 counter reset ip=${ip} email=${emailLower}`);
975
985
  if (hasIp) clear(ipKey);
976
986
  clear(emailKey);
977
- if (user.is_invited === 1) {
978
- try {
979
- await db.run("UPDATE users SET is_invited = 0 WHERE id = ?", [user.id]);
980
- } catch {
981
- }
982
- }
983
987
  if (check.needsRehash) {
984
988
  try {
985
989
  const newHash = await hashPassword(body.password);
@@ -1051,9 +1055,7 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
1051
1055
  );
1052
1056
  });
1053
1057
  authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
1054
- const body = await c.req.json().catch(
1055
- () => ({})
1056
- );
1058
+ const body = await c.req.json().catch(() => ({}));
1057
1059
  const db = getDb();
1058
1060
  const userId = c.get("userId");
1059
1061
  const apiKey = generateApiKey();
@@ -1062,6 +1064,7 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
1062
1064
  "SELECT label, nest_id FROM api_keys WHERE user_id = ?",
1063
1065
  [userId]
1064
1066
  );
1067
+ const nestScope = "nest_id" in body ? body.nest_id || null : prior?.nest_id ?? null;
1065
1068
  await db.transaction(async (tx) => {
1066
1069
  await tx.run("DELETE FROM api_keys WHERE user_id = ?", [userId]);
1067
1070
  await tx.run(
@@ -1071,7 +1074,7 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
1071
1074
  userId,
1072
1075
  hashApiKey(apiKey),
1073
1076
  getKeyPrefix(apiKey),
1074
- body.nest_id ?? prior?.nest_id ?? null,
1077
+ nestScope,
1075
1078
  body.label ?? prior?.label ?? null
1076
1079
  ]
1077
1080
  );
@@ -1422,6 +1425,9 @@ authRoutes.delete("/users/:userId", async (c) => {
1422
1425
  authRoutes.post("/invite", async (c) => {
1423
1426
  const body = await c.req.json();
1424
1427
  if (!body.email) throw new ValidationError("email is required");
1428
+ if (!isEmailish(body.email)) {
1429
+ throw new ValidationError("Enter a valid email address.");
1430
+ }
1425
1431
  const email = normalizeEmail(body.email);
1426
1432
  const callerId = await resolveCallerUserId(c);
1427
1433
  if (!callerId) {
@@ -1759,7 +1765,7 @@ async function approveExternalEdit(input) {
1759
1765
  const node = await storage.readDocument(input.documentId);
1760
1766
  const versionNum = result.versionEntry.version;
1761
1767
  const tags = node.frontmatter.tags || [];
1762
- const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-YQIIQ7FN.js");
1768
+ const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-L52F4WVZ.js");
1763
1769
  await createVersion2({
1764
1770
  nestId: input.nestId,
1765
1771
  nodeId: input.documentId,
@@ -2051,6 +2057,11 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
2051
2057
  async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
2052
2058
  return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
2053
2059
  }
2060
+ async function actorAutoPublishes(nestId, userEmail) {
2061
+ if (!await nestAllowsSelfApprove(nestId)) return false;
2062
+ const roles = await resolveUserRoles(nestId, userEmail);
2063
+ return roles.includes("owner") || roles.includes("admin");
2064
+ }
2054
2065
  async function createNode(nestId, input, userEmail) {
2055
2066
  const { storage, versions: versionManager } = await engineCache.get(nestId);
2056
2067
  const slug = slugify(input.title);
@@ -2085,8 +2096,9 @@ async function createNode(nestId, input, userEmail) {
2085
2096
  tags.push(deptTag);
2086
2097
  }
2087
2098
  const hasStewards = await isStewardshipEnabled(nestId);
2088
- const initialStatus = hasStewards ? "draft" : "published";
2089
- const initialVersion = hasStewards ? 1 : 0;
2099
+ const autoPublish = !hasStewards || await actorAutoPublishes(nestId, userEmail);
2100
+ const initialStatus = autoPublish ? "published" : "draft";
2101
+ const initialVersion = autoPublish ? 0 : 1;
2090
2102
  let node = {
2091
2103
  id,
2092
2104
  filePath: "",
@@ -2110,7 +2122,7 @@ async function createNode(nestId, input, userEmail) {
2110
2122
  await storage.writeDocument(id, serializeDocument(node));
2111
2123
  await syncNodeTags(nestId, id, tags);
2112
2124
  let savedVersion = 1;
2113
- if (hasStewards) {
2125
+ if (!autoPublish) {
2114
2126
  try {
2115
2127
  await versionManager.createVersion(node, userEmail);
2116
2128
  } catch (err) {
@@ -2129,7 +2141,7 @@ async function createNode(nestId, input, userEmail) {
2129
2141
  try {
2130
2142
  const result = await safePublishDocument(storage, id, {
2131
2143
  editedBy: userEmail,
2132
- note: "Auto-published on create (no stewards configured)"
2144
+ note: hasStewards ? "Auto-published on create (owner/admin self-approve)" : "Auto-published on create (no stewards configured)"
2133
2145
  });
2134
2146
  savedVersion = result.node.frontmatter.version || 1;
2135
2147
  await createVersion({
@@ -2338,6 +2350,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
2338
2350
  node = { ...node, frontmatter: { ...node.frontmatter, metadata } };
2339
2351
  }
2340
2352
  const hasStewards = await isStewardshipEnabled(nestId);
2353
+ const autoPublish = !hasStewards || await actorAutoPublishes(nestId, userEmail);
2341
2354
  const currentTags = node.frontmatter.tags || [];
2342
2355
  if (hasStewards && await getPendingReview(nestId, nodeId)) {
2343
2356
  throw new LockedError(
@@ -2345,7 +2358,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
2345
2358
  );
2346
2359
  }
2347
2360
  let responseVersion;
2348
- if (hasStewards) {
2361
+ if (!autoPublish) {
2349
2362
  const currentVersion = await getCurrentVersion(nestId, nodeId);
2350
2363
  const newVersion = currentVersion + 1;
2351
2364
  node = {
@@ -2400,7 +2413,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
2400
2413
  try {
2401
2414
  const result = await safePublishDocument(storage, nodeId, {
2402
2415
  editedBy: userEmail,
2403
- note: patch.changeNote || "Auto-published on edit (no stewards)"
2416
+ note: patch.changeNote || (hasStewards ? "Auto-published on edit (owner/admin self-approve)" : "Auto-published on edit (no stewards)")
2404
2417
  });
2405
2418
  publishedVersion = result.node.frontmatter.version || publishedVersion;
2406
2419
  node = result.node;
@@ -2411,7 +2424,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
2411
2424
  );
2412
2425
  throw err;
2413
2426
  }
2414
- await createVersion({
2427
+ await upsertVersion({
2415
2428
  nestId,
2416
2429
  nodeId,
2417
2430
  version: publishedVersion,
@@ -2735,6 +2748,25 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
2735
2748
  }
2736
2749
  return { nest, documents };
2737
2750
  }
2751
+ function deleteUnsyncedFolder(folderName) {
2752
+ console.log(`[unsynced] delete requested folder="${folderName}"`);
2753
+ assertSafeFolderName(folderName);
2754
+ const src = join3(config.DATA_ROOT, folderName);
2755
+ if (isNestStorageRoot(src)) {
2756
+ throw new ValidationError("Folder is not eligible for delete");
2757
+ }
2758
+ let stat;
2759
+ try {
2760
+ stat = statSync(src);
2761
+ } catch {
2762
+ throw new NotFoundError(`Folder not found: ${folderName}`);
2763
+ }
2764
+ if (!stat.isDirectory()) {
2765
+ throw new ValidationError(`Not a directory: ${folderName}`);
2766
+ }
2767
+ rmSync(src, { recursive: true, force: true });
2768
+ console.log(`[unsynced] deleted folder ${src}`);
2769
+ }
2738
2770
 
2739
2771
  // src/nests/routes.ts
2740
2772
  async function effectivePermission(nestId, userId) {
@@ -2760,54 +2792,42 @@ nestRoutes.get("/", async (c) => {
2760
2792
  const annotate = async (n) => {
2761
2793
  const permission = await effectivePermission(n.id, userId);
2762
2794
  const is_owner = permission === "owner";
2763
- let owner_email = null;
2764
- let roles;
2765
- if (is_owner) {
2766
- roles = ["owner"];
2767
- } else {
2768
- const grantRole = collabPermToRole(
2769
- await getCollaboratorRole(n.id, callerEmail)
2770
- );
2771
- const stewardRoles = await getStewardRolesForUser(n.id, callerEmail);
2772
- const teamRoles = await resolveTeamRolesForUser(n.id, userId);
2773
- roles = [
2774
- ...new Set(
2775
- [grantRole, ...stewardRoles, ...teamRoles].filter(Boolean)
2776
- )
2777
- ];
2778
- }
2779
- if (!is_owner && n.user_id !== ANON_USER_ID) {
2780
- const row = await db.get(ownerEmailSql, [n.user_id]);
2781
- owner_email = row?.email ?? null;
2782
- }
2795
+ const needOwnerEmail = !is_owner && n.user_id !== ANON_USER_ID;
2796
+ const [roles, owner_email] = await Promise.all([
2797
+ is_owner ? Promise.resolve(["owner"]) : resolveGrantRoles(n.id, callerEmail, userId),
2798
+ needOwnerEmail ? db.get(ownerEmailSql, [n.user_id]).then(
2799
+ (row) => row?.email ?? null
2800
+ ) : Promise.resolve(null)
2801
+ ]);
2783
2802
  return {
2784
2803
  ...n,
2785
2804
  permission,
2786
2805
  is_owner,
2787
2806
  owner_email,
2788
2807
  roles,
2789
- stewardship_enabled: await isStewardshipEnabled(n.id)
2808
+ stewardship_enabled: !!n.stewardship_enabled
2790
2809
  };
2791
2810
  };
2792
2811
  const seen = /* @__PURE__ */ new Set();
2793
- const out = [];
2812
+ const deduped = [];
2794
2813
  for (const n of [...owned, ...shared, ...publicExtras]) {
2795
2814
  if (seen.has(n.id)) continue;
2796
2815
  seen.add(n.id);
2797
- out.push(await annotate(n));
2816
+ deduped.push(n);
2798
2817
  }
2818
+ const out = await Promise.all(deduped.map(annotate));
2799
2819
  const docRows = await db.all(
2800
2820
  "SELECT nest_id, COUNT(DISTINCT node_id) AS c FROM node_versions GROUP BY nest_id"
2801
2821
  );
2802
- const docByNest = new Map(docRows.map((r) => [r.nest_id, r.c]));
2822
+ const docByNest = new Map(docRows.map((r) => [r.nest_id, Number(r.c)]));
2803
2823
  const collabRows = await db.all(
2804
2824
  "SELECT nest_id, COUNT(*) AS c FROM nest_collaborators GROUP BY nest_id"
2805
2825
  );
2806
- const collabByNest = new Map(collabRows.map((r) => [r.nest_id, r.c]));
2826
+ const collabByNest = new Map(collabRows.map((r) => [r.nest_id, Number(r.c)]));
2807
2827
  const stewardRows = await db.all(
2808
2828
  "SELECT nest_id, COUNT(DISTINCT user_email) AS c FROM stewards WHERE is_active = 1 GROUP BY nest_id"
2809
2829
  );
2810
- const stewardByNest = new Map(stewardRows.map((r) => [r.nest_id, r.c]));
2830
+ const stewardByNest = new Map(stewardRows.map((r) => [r.nest_id, Number(r.c)]));
2811
2831
  const stewardEmailRows = await db.all(
2812
2832
  `SELECT nest_id, user_email FROM stewards WHERE is_active = 1
2813
2833
  GROUP BY nest_id, user_email ORDER BY MIN(assigned_at)`
@@ -2914,6 +2934,21 @@ nestRoutes.post("/:nestId/sync", async (c) => {
2914
2934
  );
2915
2935
  return c.json(result, 201);
2916
2936
  });
2937
+ nestRoutes.delete("/:nestId/sync", async (c) => {
2938
+ if (c.req.param("nestId") !== "unsynced") {
2939
+ throw new NotFoundError("Not found");
2940
+ }
2941
+ const userId = c.get("userId");
2942
+ if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
2943
+ throw new ForbiddenError("Only the server admin can delete folders");
2944
+ }
2945
+ const body = await c.req.json().catch(() => ({}));
2946
+ if (!body?.name) {
2947
+ throw new ValidationError("name is required");
2948
+ }
2949
+ deleteUnsyncedFolder(body.name);
2950
+ return c.json({ ok: true });
2951
+ });
2917
2952
  nestRoutes.get("/:nestId", async (c) => {
2918
2953
  const nestId = c.req.param("nestId");
2919
2954
  if (nestId === "unsynced") return handleUnsyncedList(c);
@@ -3038,6 +3073,9 @@ async function addCollaborator(params) {
3038
3073
  const db = getDb();
3039
3074
  let userId = params.userId;
3040
3075
  if (!userId && params.email) {
3076
+ if (!isEmailish(params.email)) {
3077
+ throw new ValidationError("Enter a valid email address.");
3078
+ }
3041
3079
  const email = normalizeEmail(params.email);
3042
3080
  const existing = await db.get(
3043
3081
  "SELECT id FROM users WHERE LOWER(email) = ?",
@@ -4012,7 +4050,7 @@ nodeRoutes.post("/", async (c) => {
4012
4050
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4013
4051
  const nestId = c.req.param("nestId");
4014
4052
  const nodeId = c.req.param("nodeId");
4015
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-7BHI2YXB.js");
4053
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-B6L7UACX.js");
4016
4054
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
4017
4055
  nestId,
4018
4056
  nodeId
@@ -4033,7 +4071,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4033
4071
  nodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
4034
4072
  const nestId = c.req.param("nestId");
4035
4073
  const nodeId = c.req.param("nodeId");
4036
- const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-YQIIQ7FN.js");
4074
+ const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-L52F4WVZ.js");
4037
4075
  const allVersions = await getVersions2(nestId, nodeId);
4038
4076
  const approved = await getApprovedVersion2(nestId, nodeId);
4039
4077
  const db = getDb();
@@ -4065,7 +4103,7 @@ nodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
4065
4103
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
4066
4104
  const nestId = c.req.param("nestId");
4067
4105
  const nodeId = c.req.param("nodeId");
4068
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-DPN26C47.js");
4106
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-Q3NDK5TX.js");
4069
4107
  const history = await getReviewHistory2(nestId, nodeId);
4070
4108
  return c.json({ reviews: history });
4071
4109
  });
@@ -7472,8 +7510,8 @@ ${list}`;
7472
7510
  if (!target) return "target is required (a node id or folder prefix).";
7473
7511
  if (!["read", "write"].includes(role)) return "role must be read or write.";
7474
7512
  try {
7475
- const { createGrant: createGrant2 } = await import("./grants-service-PS5RMMEA.js");
7476
- const db = (await import("./client-YUOEVUFU.js")).getDb();
7513
+ const { createGrant: createGrant2 } = await import("./grants-service-FZD6QM6V.js");
7514
+ const db = (await import("./client-AAMF22YJ.js")).getDb();
7477
7515
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
7478
7516
  const e = normalizeEmail2(String(args.email || ""));
7479
7517
  if (!e) return "email is required.";
@@ -8573,7 +8611,7 @@ var CONTENT_TYPES = {
8573
8611
  };
8574
8612
  var VIDEO_EXTS = /* @__PURE__ */ new Set(["mp4", "webm"]);
8575
8613
  var IMAGE_MAX_BYTES = 10 * 1024 * 1024;
8576
- var VIDEO_MAX_BYTES = 100 * 1024 * 1024;
8614
+ var VIDEO_MAX_BYTES = config.VIDEO_MAX_BYTES;
8577
8615
  var ASSET_MAX_REQUEST_BYTES = VIDEO_MAX_BYTES + 1024 * 1024;
8578
8616
  var SAFE_NAME = /^[0-9a-f-]{36}\.(png|jpe?g|gif|webp|mp4|webm)$/;
8579
8617
  assetRoutes.post("/", async (c) => {
@@ -10017,6 +10055,22 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
10017
10055
  }
10018
10056
  return c.json({ error: "Missing or invalid credentials" }, 401);
10019
10057
  });
10058
+ var APPROVE_CACHE_TTL_MS = 3e4;
10059
+ var approveCache = /* @__PURE__ */ new Map();
10060
+ async function canApproveInNest(nestId, email) {
10061
+ const key = `${nestId} ${email.toLowerCase()}`;
10062
+ const now = Date.now();
10063
+ const hit = approveCache.get(key);
10064
+ if (hit && hit.exp > now) return hit.ok;
10065
+ const roles = await resolveUserRoles(nestId, email);
10066
+ let ok = roles.includes("admin") || roles.includes("reviewer");
10067
+ if (!ok && roles.includes("owner")) ok = await nestAllowsSelfApprove(nestId);
10068
+ if (approveCache.size > 500) {
10069
+ for (const [k, v] of approveCache) if (v.exp <= now) approveCache.delete(k);
10070
+ }
10071
+ approveCache.set(key, { ok, exp: now + APPROVE_CACHE_TTL_MS });
10072
+ return ok;
10073
+ }
10020
10074
  function createApp() {
10021
10075
  const app = new Hono21({ router: new LinearRouter() });
10022
10076
  const corsOrigins = config.CORS_ORIGINS;
@@ -10634,12 +10688,16 @@ function createApp() {
10634
10688
  const userId = c.get("userId");
10635
10689
  const me = await resolveCallerEmail(userId);
10636
10690
  const nestName2 = /* @__PURE__ */ new Map();
10637
- for (const n of [
10638
- ...await listNests(userId),
10639
- ...await listSharedNests(userId)
10640
- ])
10691
+ for (const n of await listVisibleNests(userId))
10641
10692
  nestName2.set(n.id, n.name ?? n.id);
10642
- const { requests } = await getReviewQueue({ status: "pending", limit: 200 });
10693
+ const limit = Math.min(Math.max(Number(c.req.query("limit")) || 50, 1), 100);
10694
+ const offset = Math.max(Number(c.req.query("offset")) || 0, 0);
10695
+ const { requests, total } = await getReviewQueue({
10696
+ status: "pending",
10697
+ nestIds: [...nestName2.keys()],
10698
+ limit,
10699
+ offset
10700
+ });
10643
10701
  const toItem = (r) => ({
10644
10702
  type: "review",
10645
10703
  nest_id: r.nestId,
@@ -10650,12 +10708,29 @@ function createApp() {
10650
10708
  requested_at: r.requestedAt,
10651
10709
  priority: r.priority
10652
10710
  });
10653
- const mine = requests.filter((r) => nestName2.has(r.nestId));
10654
10711
  const byNewest = (a, b) => a.requested_at < b.requested_at ? 1 : -1;
10655
- const items = mine.filter((r) => r.requestedBy !== me).map(toItem).sort(byNewest);
10656
- const waiting = mine.filter((r) => r.requestedBy === me).map(toItem).sort(byNewest);
10712
+ const others = requests.filter((r) => r.requestedBy !== me);
10713
+ const nestApprove = new Map(
10714
+ await Promise.all(
10715
+ [...new Set(others.map((r) => r.nestId))].map(
10716
+ async (nestId) => [
10717
+ nestId,
10718
+ await canApproveInNest(nestId, me)
10719
+ ]
10720
+ )
10721
+ )
10722
+ );
10723
+ const items = others.filter((r) => nestApprove.get(r.nestId)).map(toItem).sort(byNewest);
10724
+ const waiting = requests.filter((r) => r.requestedBy === me).map(toItem).sort(byNewest);
10657
10725
  const notifications = await listNotifications(me, { unreadOnly: true, limit: 50 });
10658
- return c.json({ items, waiting, notifications });
10726
+ return c.json({
10727
+ items,
10728
+ waiting,
10729
+ notifications,
10730
+ // total = all pending across visible nests (both buckets); has_more drives
10731
+ // the "Load more" control on the client.
10732
+ pagination: { total, limit, offset, has_more: offset + requests.length < total }
10733
+ });
10659
10734
  });
10660
10735
  app.get("/me/notifications", async (c) => {
10661
10736
  const me = await resolveCallerEmail(c.get("userId"));
@@ -10673,9 +10748,7 @@ function createApp() {
10673
10748
  const db = getDb();
10674
10749
  const userId = c.get("userId");
10675
10750
  const userEmail = await resolveCallerEmail(userId);
10676
- const owned = await listNests(userId);
10677
- const sharedNests = await listSharedNests(userId);
10678
- const visibleNests = [...owned, ...sharedNests];
10751
+ const visibleNests = await listVisibleNests(userId);
10679
10752
  let documents = 0;
10680
10753
  for (const nest of visibleNests) {
10681
10754
  try {
@@ -10712,18 +10785,31 @@ function createApp() {
10712
10785
  const nestId = parts[0];
10713
10786
  const userId = c.get("userId");
10714
10787
  const nestScope = c.get("nestScope");
10788
+ const isMcpTransport = parts.length === 2 && parts[1] === "mcp";
10789
+ const deny = async (status, error, extra) => {
10790
+ if (!isMcpTransport) return c.json({ error, ...extra }, status);
10791
+ let id = null;
10792
+ try {
10793
+ id = (await c.req.json())?.id ?? null;
10794
+ } catch {
10795
+ }
10796
+ const message = typeof extra?.reason === "string" ? `${error} \u2014 ${extra.reason}` : error;
10797
+ return c.json(
10798
+ { jsonrpc: "2.0", id, error: { code: -32e3, message } },
10799
+ status
10800
+ );
10801
+ };
10715
10802
  if (nestScope && nestScope !== nestId) {
10716
- return c.json({ error: "API key not authorized for this nest" }, 403);
10803
+ return deny(
10804
+ 403,
10805
+ `API key not authorized for this nest \u2014 this key is scoped to nest "${nestScope}". Use that nest's MCP URL, or mint a user-level key in the Connect dialog.`
10806
+ );
10717
10807
  }
10718
10808
  if (isSuspended() && c.req.method !== "GET") {
10719
- return c.json(
10720
- {
10721
- error: "Server suspended by PromptOwl",
10722
- reason: getSuspensionReason(),
10723
- contact: "support@promptowl.ai"
10724
- },
10725
- 503
10726
- );
10809
+ return deny(503, "Server suspended by PromptOwl", {
10810
+ reason: getSuspensionReason(),
10811
+ contact: "support@promptowl.ai"
10812
+ });
10727
10813
  }
10728
10814
  {
10729
10815
  const path2 = c.req.path;
@@ -10735,14 +10821,10 @@ function createApp() {
10735
10821
  if (needsLicense) {
10736
10822
  const lic = getCurrentLicense();
10737
10823
  if (!lic?.valid) {
10738
- return c.json(
10739
- {
10740
- error: "License required",
10741
- reason: "Install a PromptOwl license key via the setup screen or POST /license/install.",
10742
- setup_url: "/setup"
10743
- },
10744
- 503
10745
- );
10824
+ return deny(503, "License required", {
10825
+ reason: "This server has no valid PromptOwl license installed. Ask the server admin to install one via the setup screen or POST /license/install.",
10826
+ setup_url: "/setup"
10827
+ });
10746
10828
  }
10747
10829
  }
10748
10830
  }
@@ -10770,7 +10852,9 @@ function createApp() {
10770
10852
  grantNestVisible = await hasAnyGrant(nestId, userId);
10771
10853
  }
10772
10854
  if (!grantRole && !grantNestVisible) {
10773
- return c.json({ error: "Nest not found" }, 404);
10855
+ return deny(404, "Nest not found", {
10856
+ reason: "The nest id may be wrong, or these credentials have no access to it. Ask the nest owner to share the nest with you, then reconnect."
10857
+ });
10774
10858
  }
10775
10859
  }
10776
10860
  let required = "read";
@@ -10796,7 +10880,7 @@ function createApp() {
10796
10880
  required = c.req.method === "GET" ? "write" : "admin";
10797
10881
  } else if (parts[1] === "hooks") {
10798
10882
  required = "write";
10799
- } else if (c.req.method !== "GET" && !isStewardActionPath && !isCommentAction && !isAnnotationAction && !isReadQuery) {
10883
+ } else if (c.req.method !== "GET" && !isStewardActionPath && !isCommentAction && !isAnnotationAction && !isReadQuery && !isMcpTransport) {
10800
10884
  required = "write";
10801
10885
  }
10802
10886
  const isNodeAction = c.req.method === "POST" && parts.length >= 4 && (parts[parts.length - 1] === "revert" || parts[parts.length - 1] === "move");
@@ -10825,13 +10909,13 @@ function createApp() {
10825
10909
  const grantLevel = grantRole ? permissionLevel(grantRole) : grantNestVisible ? permissionLevel("read") : 0;
10826
10910
  const effectiveLevel = Math.max(permissionLevel(permission), grantLevel);
10827
10911
  if (!stewardEditorBypass && effectiveLevel < permissionLevel(required)) {
10828
- return c.json(
10912
+ return deny(
10913
+ 403,
10914
+ `You don't have access to perform this action on this nest. Required permission: '${required}', your permission: '${permission}'. Ask the nest owner or a server admin to grant you ${required} access.`,
10829
10915
  {
10830
- error: `You don't have access to perform this action on this nest. Required permission: '${required}', your permission: '${permission}'. Ask the nest owner or a server admin to grant you ${required} access.`,
10831
10916
  required_permission: required,
10832
10917
  your_permission: permission
10833
- },
10834
- 403
10918
+ }
10835
10919
  );
10836
10920
  }
10837
10921
  c.set(
@@ -6,16 +6,16 @@ import {
6
6
  getReviewQueue,
7
7
  reject,
8
8
  submitForReview
9
- } from "./chunk-XB7T3THE.js";
10
- import "./chunk-ZMSU7BCC.js";
11
- import "./chunk-JHIU6RZU.js";
9
+ } from "./chunk-PAIP5KHB.js";
10
+ import "./chunk-ZQS5W45U.js";
11
+ import "./chunk-EG77SQHO.js";
12
12
  import {
13
13
  canUserApprove
14
- } from "./chunk-F7EKYNXG.js";
14
+ } from "./chunk-Q5NRCDD4.js";
15
15
  import "./chunk-GUNJTORH.js";
16
16
  import "./chunk-FRQJWGN3.js";
17
17
  import "./chunk-XQ46F76G.js";
18
- import "./chunk-3M7677XW.js";
18
+ import "./chunk-5QQ7WKAI.js";
19
19
  import "./chunk-SLTQACJW.js";
20
20
  export {
21
21
  approve,
@@ -14,17 +14,18 @@ import {
14
14
  getStewardsForUser,
15
15
  listStewards,
16
16
  removeSteward,
17
+ resolveGrantRoles,
17
18
  resolveStewardsForNode,
18
19
  resolveStewardsWithFallback,
19
20
  resolveUserRoles,
20
21
  syncFromConfig,
21
22
  updateSteward,
22
23
  updateStewardRole
23
- } from "./chunk-F7EKYNXG.js";
24
+ } from "./chunk-Q5NRCDD4.js";
24
25
  import "./chunk-GUNJTORH.js";
25
26
  import "./chunk-FRQJWGN3.js";
26
27
  import "./chunk-XQ46F76G.js";
27
- import "./chunk-3M7677XW.js";
28
+ import "./chunk-5QQ7WKAI.js";
28
29
  import "./chunk-SLTQACJW.js";
29
30
  export {
30
31
  assignSteward,
@@ -42,6 +43,7 @@ export {
42
43
  getStewardsForUser,
43
44
  listStewards,
44
45
  removeSteward,
46
+ resolveGrantRoles,
45
47
  resolveStewardsForNode,
46
48
  resolveStewardsWithFallback,
47
49
  resolveUserRoles,