@promptowl/contextnest-community 1.16.1 → 1.17.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.
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  addWatcher,
4
4
  approve,
5
5
  canReadNode,
6
+ canRequestDeletion,
6
7
  cancelReview,
7
8
  declineDeletion,
8
9
  deletionTargetTitle,
@@ -38,17 +39,18 @@ import {
38
39
  seedDefaultEdgeTypes,
39
40
  sendTestNotification,
40
41
  submitForReview,
41
- withNodeWriteLock
42
- } from "./chunk-QJVRZT6M.js";
42
+ withNodeWriteLock,
43
+ withdrawDeletionRequest
44
+ } from "./chunk-4YGJSUJC.js";
43
45
  import {
44
46
  createGrant,
45
47
  deleteGrant,
46
48
  grantCoversNode,
47
- hasAnyGrant,
49
+ grantRoleForNode,
48
50
  listGrants,
49
51
  listUserGrants,
50
52
  resolveNodeGrant
51
- } from "./chunk-OTGZQZKK.js";
53
+ } from "./chunk-HWDPCPYT.js";
52
54
  import {
53
55
  generateApiKey,
54
56
  getKeyPrefix,
@@ -69,7 +71,7 @@ import {
69
71
  listMyDrafts,
70
72
  setApprovedVersion,
71
73
  upsertVersion
72
- } from "./chunk-D6HICFVW.js";
74
+ } from "./chunk-UXJSO54R.js";
73
75
  import {
74
76
  canCreateInNest,
75
77
  canManageStewards,
@@ -89,7 +91,7 @@ import {
89
91
  resolveUserRoles,
90
92
  syncFromConfig,
91
93
  updateSteward
92
- } from "./chunk-CLKZZZS2.js";
94
+ } from "./chunk-YF4OFT2V.js";
93
95
  import {
94
96
  addMember,
95
97
  buildTitleMap,
@@ -145,6 +147,7 @@ import {
145
147
  renameTeam,
146
148
  replaceTeamRoster,
147
149
  resolveMemberIdentity,
150
+ resolveNestAccess,
148
151
  resolveNestPath,
149
152
  resolveNestPermission,
150
153
  sendEmailToRecipient,
@@ -162,7 +165,7 @@ import {
162
165
  updateMemberRole,
163
166
  validateLicense,
164
167
  verifySmtp
165
- } from "./chunk-L7UCMGWZ.js";
168
+ } from "./chunk-SNFGU5Q7.js";
166
169
  import {
167
170
  AppError,
168
171
  ConflictError,
@@ -1863,7 +1866,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
1863
1866
  async function mirrorVersion(input) {
1864
1867
  const { storage } = await engineCache.get(input.nestId);
1865
1868
  const node = await storage.readDocument(input.documentId);
1866
- const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-V6K5SZ3P.js");
1869
+ const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-EFZGGL3Y.js");
1867
1870
  await upsertVersion2({
1868
1871
  nestId: input.nestId,
1869
1872
  nodeId: input.documentId,
@@ -2199,10 +2202,7 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
2199
2202
  nodes,
2200
2203
  approvedVersions ?? void 0
2201
2204
  );
2202
- if (await resolveNestPermission(nestId, userId) === "none" && !await isStewardshipEnabled(nestId)) {
2203
- const grants = await listUserGrants(nestId, userId);
2204
- accessible = grants.length ? nodes.filter((d) => grantCoversNode(grants, d.id)) : accessible;
2205
- }
2205
+ const myGrants = await listUserGrants(nestId, userId);
2206
2206
  if (approvedVersions && !publicReader) {
2207
2207
  accessible = accessible.filter((d) => approvedVersions.has(d.id));
2208
2208
  }
@@ -2222,6 +2222,9 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
2222
2222
  r.author = people.author;
2223
2223
  r.collaborators = people.collaborators;
2224
2224
  }
2225
+ if (myGrants.length) {
2226
+ r.myGrant = grantRoleForNode(myGrants, doc.id);
2227
+ }
2225
2228
  if (approvedVersions) {
2226
2229
  const approved = approvedVersions.get(doc.id);
2227
2230
  if (approved != null) {
@@ -3099,16 +3102,22 @@ async function deleteUnsyncedFolder(folderName) {
3099
3102
  }
3100
3103
 
3101
3104
  // src/nests/routes.ts
3102
- async function effectivePermission(nestId, userId) {
3103
- if (config.AUTH_MODE === "open") {
3104
- const db = getDb();
3105
- const nest = await db.get(
3106
- "SELECT user_id FROM nests WHERE id = ?",
3107
- [nestId]
3108
- );
3109
- if (nest && nest.user_id === ANON_USER_ID) return "owner";
3105
+ async function effectiveAccess(nestId, userId) {
3106
+ const access = await resolveNestAccess(nestId, userId);
3107
+ if (config.AUTH_MODE === "open" && access.ownerId === ANON_USER_ID) {
3108
+ return { permission: "owner", access };
3110
3109
  }
3111
- return resolveNestPermission(nestId, userId);
3110
+ const permission = access.permission;
3111
+ if (permission === "none" && access.hasGrant) {
3112
+ return { permission: "read", access };
3113
+ }
3114
+ return { permission, access };
3115
+ }
3116
+ async function effectivePermission(nestId, userId) {
3117
+ return (await effectiveAccess(nestId, userId)).permission;
3118
+ }
3119
+ function isGrantOnly(access) {
3120
+ return access.permission === "none" && access.hasGrant;
3112
3121
  }
3113
3122
  var nestRoutes = new Hono3();
3114
3123
  nestRoutes.get("/", async (c) => {
@@ -3119,8 +3128,31 @@ nestRoutes.get("/", async (c) => {
3119
3128
  const db = getDb();
3120
3129
  const ownerEmailSql = "SELECT email FROM users WHERE id = ?";
3121
3130
  const callerEmail = await resolveCallerEmail(userId);
3131
+ const myGrantRows = await db.all(
3132
+ "SELECT nest_id, target FROM grants WHERE user_id = ?",
3133
+ [userId]
3134
+ );
3135
+ const grantTargetsByNest = /* @__PURE__ */ new Map();
3136
+ for (const r of myGrantRows) {
3137
+ const arr = grantTargetsByNest.get(r.nest_id) ?? [];
3138
+ arr.push({ target: r.target });
3139
+ grantTargetsByNest.set(r.nest_id, arr);
3140
+ }
3122
3141
  const annotate = async (n) => {
3123
- const permission = await effectivePermission(n.id, userId);
3142
+ const { permission, access } = await effectiveAccess(n.id, userId);
3143
+ if (permission !== "owner" && // open-mode commons promotion outranks a grant
3144
+ isGrantOnly(access)) {
3145
+ const owner_email2 = n.user_id === ANON_USER_ID ? null : (await db.get(ownerEmailSql, [n.user_id]))?.email ?? null;
3146
+ return {
3147
+ ...n,
3148
+ permission: "read",
3149
+ is_owner: false,
3150
+ owner_email: owner_email2,
3151
+ roles: ["viewer"],
3152
+ grant_only: true,
3153
+ stewardship_enabled: !!n.stewardship_enabled
3154
+ };
3155
+ }
3124
3156
  const is_owner = permission === "owner";
3125
3157
  const needOwnerEmail = !is_owner && n.user_id !== ANON_USER_ID;
3126
3158
  const [roles, owner_email] = await Promise.all([
@@ -3135,6 +3167,7 @@ nestRoutes.get("/", async (c) => {
3135
3167
  is_owner,
3136
3168
  owner_email,
3137
3169
  roles,
3170
+ grant_only: false,
3138
3171
  stewardship_enabled: !!n.stewardship_enabled
3139
3172
  };
3140
3173
  };
@@ -3178,14 +3211,38 @@ nestRoutes.get("/", async (c) => {
3178
3211
  if (arr.length < 8) arr.push(r.email);
3179
3212
  collabEmailsByNest.set(r.nest_id, arr);
3180
3213
  }
3181
- const withMeta = out.map((n) => ({
3182
- ...n,
3183
- document_count: docByNest.get(n.id) ?? 0,
3184
- collaborator_count: collabByNest.get(n.id) ?? 0,
3185
- steward_count: stewardByNest.get(n.id) ?? 0,
3186
- steward_emails: stewardEmailsByNest.get(n.id) ?? [],
3187
- collaborator_emails: collabEmailsByNest.get(n.id) ?? []
3188
- }));
3214
+ const grantOnlyIds = out.filter((n) => n.grant_only).map((n) => n.id);
3215
+ const grantedDocsByNest = /* @__PURE__ */ new Map();
3216
+ if (grantOnlyIds.length) {
3217
+ const rows = await db.all(
3218
+ `SELECT DISTINCT nest_id, node_id FROM node_versions
3219
+ WHERE nest_id IN (${grantOnlyIds.map(() => "?").join(",")})`,
3220
+ grantOnlyIds
3221
+ );
3222
+ for (const r of rows) {
3223
+ const targets = grantTargetsByNest.get(r.nest_id) ?? [];
3224
+ if (grantCoversNode(targets, r.node_id)) {
3225
+ grantedDocsByNest.set(r.nest_id, (grantedDocsByNest.get(r.nest_id) ?? 0) + 1);
3226
+ }
3227
+ }
3228
+ }
3229
+ const withMeta = out.map(
3230
+ (n) => n.grant_only ? {
3231
+ ...n,
3232
+ document_count: grantedDocsByNest.get(n.id) ?? 0,
3233
+ collaborator_count: 0,
3234
+ steward_count: 0,
3235
+ steward_emails: [],
3236
+ collaborator_emails: []
3237
+ } : {
3238
+ ...n,
3239
+ document_count: docByNest.get(n.id) ?? 0,
3240
+ collaborator_count: collabByNest.get(n.id) ?? 0,
3241
+ steward_count: stewardByNest.get(n.id) ?? 0,
3242
+ steward_emails: stewardEmailsByNest.get(n.id) ?? [],
3243
+ collaborator_emails: collabEmailsByNest.get(n.id) ?? []
3244
+ }
3245
+ );
3189
3246
  return c.json({ nests: withMeta });
3190
3247
  });
3191
3248
  nestRoutes.post("/", async (c) => {
@@ -3291,7 +3348,7 @@ nestRoutes.get("/:nestId", async (c) => {
3291
3348
  const nestId = c.req.param("nestId");
3292
3349
  if (nestId === "unsynced") return handleUnsyncedList(c);
3293
3350
  const userId = c.get("userId");
3294
- const permission = await effectivePermission(nestId, userId);
3351
+ const { permission, access } = await effectiveAccess(nestId, userId);
3295
3352
  if (permission === "none") {
3296
3353
  throw new NotFoundError("Nest not found");
3297
3354
  }
@@ -3301,8 +3358,15 @@ nestRoutes.get("/:nestId", async (c) => {
3301
3358
  roles = ["owner", ...roles];
3302
3359
  }
3303
3360
  const myStewards = await getStewardsForUser(nestId, email);
3361
+ const grant_only = roles.length === 0 && isGrantOnly(access);
3304
3362
  const nest = await getNest(nestId);
3305
- return c.json({ nest, permission, roles, myStewards });
3363
+ return c.json({
3364
+ nest,
3365
+ permission,
3366
+ roles: grant_only ? ["viewer"] : roles,
3367
+ myStewards,
3368
+ grant_only
3369
+ });
3306
3370
  });
3307
3371
  nestRoutes.patch("/:nestId", async (c) => {
3308
3372
  const nestId = c.req.param("nestId");
@@ -4002,6 +4066,25 @@ teamRoutes.delete("/:teamId/members/:userId", async (c) => {
4002
4066
  });
4003
4067
 
4004
4068
  // src/nodes/search.ts
4069
+ async function discoverForSearch(storage) {
4070
+ return storage.discoverDocuments({ includeRetired: true });
4071
+ }
4072
+ function titleScore(title, query) {
4073
+ const t = (title || "").toLowerCase();
4074
+ const q = query.trim().toLowerCase();
4075
+ if (!q) return 0;
4076
+ if (t === q) return 4;
4077
+ let midWord = false;
4078
+ for (let i = t.indexOf(q); i !== -1; i = t.indexOf(q, i + 1)) {
4079
+ if (i === 0) return 3;
4080
+ if (!/[a-z0-9]/.test(t[i - 1])) return 2;
4081
+ midWord = true;
4082
+ }
4083
+ if (midWord) return 1;
4084
+ const terms = q.split(/\s+/).filter(Boolean);
4085
+ if (terms.length > 1 && terms.every((term) => t.includes(term))) return 1;
4086
+ return 0;
4087
+ }
4005
4088
  function matchDocuments(documents, query) {
4006
4089
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
4007
4090
  if (terms.length === 0) return [];
@@ -4013,15 +4096,25 @@ function matchDocuments(documents, query) {
4013
4096
  ...node.frontmatter.tags || []
4014
4097
  ].join(" ").toLowerCase();
4015
4098
  return terms.every((term) => haystack.includes(term));
4016
- });
4099
+ }).sort(
4100
+ (a, b) => titleScore(b.frontmatter.title, query) - titleScore(a.frontmatter.title, query)
4101
+ );
4017
4102
  }
4018
- function toSearchHit(node) {
4103
+ function bodySnippet(body, query) {
4104
+ const flat = body.replace(/\s+/g, " ").trim();
4105
+ const terms = (query || "").toLowerCase().split(/\s+/).filter(Boolean);
4106
+ const at = terms.map((t) => flat.toLowerCase().indexOf(t)).filter((i) => i >= 0).sort((a, b) => a - b)[0];
4107
+ if (at === void 0 || at < 80) return flat.slice(0, 200);
4108
+ const start = Math.max(0, at - 60);
4109
+ return `\u2026${flat.slice(start, start + 200)}`;
4110
+ }
4111
+ function toSearchHit(node, query) {
4019
4112
  return {
4020
4113
  id: node.id,
4021
4114
  title: node.frontmatter.title,
4022
4115
  type: node.frontmatter.type || "document",
4023
4116
  tags: node.frontmatter.tags || [],
4024
- snippet: (node.body || "").slice(0, 200).replace(/\n/g, " ")
4117
+ snippet: bodySnippet(node.body || "", query)
4025
4118
  };
4026
4119
  }
4027
4120
 
@@ -4153,7 +4246,7 @@ nodeRoutes.post("/", async (c) => {
4153
4246
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4154
4247
  const nestId = c.req.param("nestId");
4155
4248
  const nodeId = c.req.param("nodeId");
4156
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-LTREGVSJ.js");
4249
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-OHTTZFND.js");
4157
4250
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
4158
4251
  nestId,
4159
4252
  nodeId
@@ -4174,7 +4267,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4174
4267
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
4175
4268
  const nestId = c.req.param("nestId");
4176
4269
  const nodeId = c.req.param("nodeId");
4177
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-V2GR6VM3.js");
4270
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-3CCKDO5N.js");
4178
4271
  const history = await getReviewHistory2(nestId, nodeId);
4179
4272
  return c.json({ reviews: history });
4180
4273
  });
@@ -4298,6 +4391,7 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
4298
4391
  response.collaborators = people.collaborators;
4299
4392
  }
4300
4393
  }
4394
+ response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
4301
4395
  if (publicReader) {
4302
4396
  const approved = await getApprovedVersion(nestId, nodeId);
4303
4397
  if (approved != null) {
@@ -5785,15 +5879,14 @@ queryRoutes.get("/search", async (c) => {
5785
5879
  }
5786
5880
  const nestId = c.req.param("nestId");
5787
5881
  const { storage } = await engineCache.get(nestId);
5788
- const documents = await storage.discoverDocuments();
5789
- const matches = matchDocuments(documents, q);
5882
+ const matches = matchDocuments(await discoverForSearch(storage), q);
5790
5883
  const userId = c.get("userId");
5791
5884
  const userEmail = await resolveCallerEmail(userId);
5792
5885
  const accessible = await filterAccessible(nestId, userId, userEmail, matches);
5793
5886
  return c.json({
5794
5887
  query: q,
5795
5888
  count: accessible.length,
5796
- nodes: accessible.map(toSearchHit)
5889
+ nodes: accessible.map((n) => toSearchHit(n, q))
5797
5890
  });
5798
5891
  });
5799
5892
  queryRoutes.get("/overview", async (c) => {
@@ -5833,10 +5926,6 @@ queryRoutes.get("/context", async (c) => {
5833
5926
  queryRoutes.get("/export", async (c) => {
5834
5927
  const nestId = c.req.param("nestId");
5835
5928
  if (c.req.query("format") === "bundle") {
5836
- const perm = await resolveNestPermission(nestId, c.get("userId"));
5837
- if (perm !== "owner" && perm !== "admin") {
5838
- throw new ForbiddenError("Only the nest owner or an admin can export a bundle");
5839
- }
5840
5929
  const draftsParam = c.req.query("includeDrafts");
5841
5930
  const includeDrafts = draftsParam === "1" || draftsParam === "true";
5842
5931
  const zip = await exportNestBundle(nestId, includeDrafts);
@@ -7923,6 +8012,13 @@ ${sections}`;
7923
8012
  (n) => String(n.frontmatter.title ?? "").toLowerCase() === wanted
7924
8013
  );
7925
8014
  if (twin) {
8015
+ const twinVersions = await getVersions(nestId, twin.id);
8016
+ if (twinVersions.length === 0) {
8017
+ return toolError(
8018
+ `A node titled "${twin.frontmatter.title}" already exists (${twin.id}) but has no version history, which is why it doesn't show up in a read or a draft list. It may be an interrupted create or a document whose import never finished. Write into it with context_update "${twin.id}" to keep whatever is there, or remove it with context_delete "${twin.id}" and create again \u2014 check its content first.`,
8019
+ "DOCUMENT_ALREADY_EXISTS"
8020
+ );
8021
+ }
7926
8022
  return toolError(
7927
8023
  `A node titled "${twin.frontmatter.title}" already exists (${twin.id}). Titles must be unique \u2014 MCP tools resolve documents by title, so a second one would be unreachable. Use context_update to change it, context_move to refile it, or create this one under a different title.`,
7928
8024
  "DOCUMENT_ALREADY_EXISTS"
@@ -8076,6 +8172,9 @@ ${sections}`;
8076
8172
  return "reason is required \u2014 say why it should be deleted; that is what the admin judges.";
8077
8173
  }
8078
8174
  if (args.nest === true) {
8175
+ if (!await canRequestDeletion({ nestId, userId, userEmail, nodeId: "" })) {
8176
+ return "Only an editor, a nest admin, or the nest owner can request the deletion of a nest.";
8177
+ }
8079
8178
  const request2 = await requestDeletion({
8080
8179
  nestId,
8081
8180
  nodeId: nestId,
@@ -8088,6 +8187,9 @@ ${sections}`;
8088
8187
  Request id: \`${request2.id}\``;
8089
8188
  }
8090
8189
  if (typeof args.folder === "string" && args.folder.trim()) {
8190
+ if (!await canRequestDeletion({ nestId, userId, userEmail, nodeId: "" })) {
8191
+ return "Only an editor, a nest admin, or the nest owner can request the deletion of a folder.";
8192
+ }
8091
8193
  const folder = args.folder.trim().replace(/^\/+|\/+$/g, "").replace(/^nodes\//, "");
8092
8194
  const docs = await storage.discoverDocuments();
8093
8195
  const inside = docs.filter((d) => isIdUnderFolder(d.id, folder));
@@ -8110,6 +8212,14 @@ Request id: \`${request2.id}\``;
8110
8212
  if (!await canReadNode(nestId, node.id, userId, userEmail)) {
8111
8213
  return `Node not found: ${node.id}`;
8112
8214
  }
8215
+ if (!await canRequestDeletion({
8216
+ nestId,
8217
+ userId,
8218
+ userEmail,
8219
+ nodeId: node.id
8220
+ })) {
8221
+ return "Only an editor, a nest admin, or the nest owner can request the deletion of a document.";
8222
+ }
8113
8223
  const request = await requestDeletion({
8114
8224
  nestId,
8115
8225
  nodeId: node.id,
@@ -8434,7 +8544,7 @@ ${list}`,
8434
8544
  if (!target) return "target is required (a node id or folder prefix).";
8435
8545
  if (!["read", "write"].includes(role)) return "role must be read or write.";
8436
8546
  try {
8437
- const { createGrant: createGrant2 } = await import("./grants-service-I535QVTW.js");
8547
+ const { createGrant: createGrant2 } = await import("./grants-service-4NXPBYWD.js");
8438
8548
  const db = (await import("./client-OBS5HOTA.js")).getDb();
8439
8549
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
8440
8550
  const e = normalizeEmail2(String(args.email || ""));
@@ -10024,6 +10134,11 @@ Streamable HTTP, stateless. Two endpoints:
10024
10134
  (new document in a writable nest; a \`folder\` path creates the folder).
10025
10135
  - \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, full toolset including writes and
10026
10136
  governance (create/update, submit/approve/reject, stewards, sharing).
10137
+ Sharing a slice is one call: \`context_grant\` with
10138
+ \`{ "email", "target_type": "document" | "folder", "target", "role": "read" | "write" }\`
10139
+ \u2014 \`target\` is a node id (\`nodes/win-loss-log\`) or a folder prefix
10140
+ (\`nodes/gtm/deals\`). The recipient reaches exactly that document, or
10141
+ everything under that folder, and nothing else in the nest. Owner/admin only.
10027
10142
 
10028
10143
  Claude Code / Cursor / VS Code connect natively via HTTP with the
10029
10144
  \`Authorization: Bearer\` header. Claude Desktop needs the mcp-remote bridge:
@@ -10032,20 +10147,9 @@ Claude Code / Cursor / VS Code connect natively via HTTP with the
10032
10147
  npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
10033
10148
  \`\`\`
10034
10149
 
10035
- ## ctx CLI (npm: @promptowl/contextnest-cli)
10036
-
10037
- \`npm i -g @promptowl/contextnest-cli\`
10038
-
10039
- - \`ctx push --server ${B} --nest <nest-id> --key <api-key>\` \u2014 upload a local
10040
- vault into a nest (add \`--include-drafts\` for unpublished nodes).
10041
- - All other ctx commands (\`query\`, \`add\`, \`search\`, \`checkpoint\`) operate on
10042
- a LOCAL vault directory only. For remote reads use the API or MCP above \u2014
10043
- there is currently no remote \`ctx query\`.
10044
-
10045
10150
  ## npm packages
10046
10151
 
10047
10152
  - \`@promptowl/contextnest-community\` \u2014 this server
10048
- - \`@promptowl/contextnest-cli\` \u2014 the ctx CLI
10049
10153
  - \`@promptowl/contextnest-engine\` \u2014 vault storage + selector/graph engine
10050
10154
 
10051
10155
  ## Conventions
@@ -10624,6 +10728,25 @@ function denyUnlessNestManager(c, what) {
10624
10728
  403
10625
10729
  );
10626
10730
  }
10731
+ async function mayRequestDeletion(c, nestId, nodeId) {
10732
+ return canRequestDeletion({
10733
+ nestId,
10734
+ userId: c.get("userId"),
10735
+ userEmail: await getUserEmail4(c),
10736
+ nodeId,
10737
+ // The middleware promotes open-mode callers; resolving fresh would miss it.
10738
+ permission: c.get("nestPermission")
10739
+ });
10740
+ }
10741
+ function denyUnlessEditor(allowed, c, what) {
10742
+ if (allowed) return null;
10743
+ return c.json(
10744
+ {
10745
+ error: `Only an editor, a nest admin, or the nest owner can request the deletion of ${what}.`
10746
+ },
10747
+ 403
10748
+ );
10749
+ }
10627
10750
  governanceRoutes.post("/delete-folder", async (c) => {
10628
10751
  const nestId = c.req.param("nestId");
10629
10752
  const denied = denyUnlessNestManager(c, "a folder");
@@ -10641,6 +10764,12 @@ governanceRoutes.post("/delete-folder", async (c) => {
10641
10764
  });
10642
10765
  governanceRoutes.post("/request-deletion", async (c) => {
10643
10766
  const nestId = c.req.param("nestId");
10767
+ const denied = denyUnlessEditor(
10768
+ await mayRequestDeletion(c, nestId, ""),
10769
+ c,
10770
+ "a folder or a nest"
10771
+ );
10772
+ if (denied) return denied;
10644
10773
  const body = await c.req.json().catch(() => ({}));
10645
10774
  const reason = body.reason?.trim();
10646
10775
  if (!reason) {
@@ -10731,6 +10860,16 @@ governanceRoutes.post("/deletion-requests/:id/decline", async (c) => {
10731
10860
  if (!request) return c.json({ error: "Deletion request not found" }, 404);
10732
10861
  return c.json({ request });
10733
10862
  });
10863
+ governanceRoutes.post("/deletion-requests/:id/withdraw", async (c) => {
10864
+ const nestId = c.req.param("nestId");
10865
+ const request = await withdrawDeletionRequest({
10866
+ nestId,
10867
+ id: c.req.param("id"),
10868
+ withdrawnBy: await getUserEmail4(c)
10869
+ });
10870
+ if (!request) return c.json({ error: "Deletion request not found or not yours" }, 404);
10871
+ return c.json({ withdrawn: true, request });
10872
+ });
10734
10873
  governanceRoutes.get("/feed", async (c) => {
10735
10874
  const nestId = c.req.param("nestId");
10736
10875
  const filterRaw = c.req.query("filter");
@@ -10988,6 +11127,12 @@ governanceNodeRoutes.post("/:nodeId{.+}/request-deletion", async (c) => {
10988
11127
  if (!await canReadNode(nestId, nodeId, c.get("userId"), userEmail)) {
10989
11128
  return c.json({ error: `Node not found: ${nodeId}` }, 404);
10990
11129
  }
11130
+ const denied = denyUnlessEditor(
11131
+ await mayRequestDeletion(c, nestId, nodeId),
11132
+ c,
11133
+ "a document"
11134
+ );
11135
+ if (denied) return denied;
10991
11136
  const { storage } = await engineCache.get(nestId);
10992
11137
  try {
10993
11138
  await storage.readDocument(nodeId);
@@ -12102,14 +12247,14 @@ function createApp() {
12102
12247
  const userId = c.get("userId");
12103
12248
  const userEmail = await resolveCallerEmail(userId);
12104
12249
  const perNest = await Promise.all(
12105
- (await listVisibleNests(userId)).map(async (nest) => {
12250
+ (await listVisibleNests(userId, { readable: true })).map(async (nest) => {
12106
12251
  try {
12107
12252
  const { storage } = await engineCache.get(nest.id);
12108
- const matches = matchDocuments(await storage.discoverDocuments(), q);
12253
+ const matches = matchDocuments(await discoverForSearch(storage), q);
12109
12254
  if (matches.length === 0) return [];
12110
12255
  const accessible = await filterAccessible(nest.id, userId, userEmail, matches);
12111
12256
  return accessible.map((n) => ({
12112
- ...toSearchHit(n),
12257
+ ...toSearchHit(n, q),
12113
12258
  nest_id: nest.id,
12114
12259
  nest_name: nest.name ?? nest.id
12115
12260
  }));
@@ -12119,7 +12264,7 @@ function createApp() {
12119
12264
  }
12120
12265
  })
12121
12266
  );
12122
- const hits = perNest.flat();
12267
+ const hits = perNest.flat().sort((a, b) => titleScore(b.title, q) - titleScore(a.title, q));
12123
12268
  return c.json({
12124
12269
  query: q,
12125
12270
  count: hits.length,
@@ -12195,7 +12340,8 @@ function createApp() {
12195
12340
  c.set("nestPermission", "owner");
12196
12341
  return next();
12197
12342
  }
12198
- const permission = await resolveNestPermission(nestId, userId);
12343
+ const access = await resolveNestAccess(nestId, userId);
12344
+ const permission = access.permission;
12199
12345
  const isReadQuery = parts.length === 2 && ["context", "query", "search", "export"].includes(parts[1]);
12200
12346
  let grantRole = null;
12201
12347
  let grantNestVisible = false;
@@ -12208,11 +12354,13 @@ function createApp() {
12208
12354
  }
12209
12355
  grantRole = await resolveNodeGrant(nestId, userId, targetTail);
12210
12356
  } else if (parts.length === 1 && c.req.method === "GET") {
12211
- grantNestVisible = await hasAnyGrant(nestId, userId);
12357
+ grantNestVisible = access.hasGrant;
12212
12358
  } else if (parts[1] === "nodes" && parts.length === 2 && c.req.method === "GET") {
12213
- grantNestVisible = await hasAnyGrant(nestId, userId);
12359
+ grantNestVisible = access.hasGrant;
12214
12360
  } else if (isReadQuery) {
12215
- grantNestVisible = await hasAnyGrant(nestId, userId);
12361
+ grantNestVisible = access.hasGrant;
12362
+ } else if (parts[1] === "definitions" && parts.length === 2 && c.req.method === "GET") {
12363
+ grantNestVisible = access.hasGrant;
12216
12364
  }
12217
12365
  if (!grantRole && !grantNestVisible) {
12218
12366
  return deny(404, "Nest not found", {
@@ -12225,7 +12373,7 @@ function createApp() {
12225
12373
  const isStewardActionPath = path.includes("/approve") || path.includes("/reject") || path.includes("/submit-review") || path.includes("/cancel-review");
12226
12374
  const isAnnotationAction = /\/annotations$/.test(path) || /\/annotations\/[^/]+\/(comments|resolve|reopen)$/.test(path);
12227
12375
  const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
12228
- const isDeletionRequestAction = /\/request-deletion$/.test(path);
12376
+ const isDeletionRequestAction = /\/request-deletion$/.test(path) || /\/deletion-requests\/[^/]+\/withdraw$/.test(path);
12229
12377
  const isStewardRoster = path.includes("/stewards") && !path.includes("/nodes/");
12230
12378
  if (isStewardRoster && permission !== "owner" && permission !== "admin") {
12231
12379
  return c.json(
@@ -14,14 +14,15 @@ import {
14
14
  reject,
15
15
  repointPendingReview,
16
16
  requestDeletion,
17
- submitForReview
18
- } from "./chunk-QJVRZT6M.js";
19
- import "./chunk-OTGZQZKK.js";
20
- import "./chunk-D6HICFVW.js";
17
+ submitForReview,
18
+ withdrawDeletionRequest
19
+ } from "./chunk-4YGJSUJC.js";
20
+ import "./chunk-HWDPCPYT.js";
21
+ import "./chunk-UXJSO54R.js";
21
22
  import {
22
23
  canUserApprove
23
- } from "./chunk-CLKZZZS2.js";
24
- import "./chunk-L7UCMGWZ.js";
24
+ } from "./chunk-YF4OFT2V.js";
25
+ import "./chunk-SNFGU5Q7.js";
25
26
  import "./chunk-GUNJTORH.js";
26
27
  import "./chunk-JMJLNEXE.js";
27
28
  import "./chunk-SLTQACJW.js";
@@ -43,5 +44,6 @@ export {
43
44
  reject,
44
45
  repointPendingReview,
45
46
  requestDeletion,
46
- submitForReview
47
+ submitForReview,
48
+ withdrawDeletionRequest
47
49
  };
@@ -21,8 +21,8 @@ import {
21
21
  syncFromConfig,
22
22
  updateSteward,
23
23
  updateStewardRole
24
- } from "./chunk-CLKZZZS2.js";
25
- import "./chunk-L7UCMGWZ.js";
24
+ } from "./chunk-YF4OFT2V.js";
25
+ import "./chunk-SNFGU5Q7.js";
26
26
  import "./chunk-GUNJTORH.js";
27
27
  import "./chunk-JMJLNEXE.js";
28
28
  import "./chunk-SLTQACJW.js";
@@ -15,8 +15,8 @@ import {
15
15
  setApprovedVersion,
16
16
  systemAuthor,
17
17
  upsertVersion
18
- } from "./chunk-D6HICFVW.js";
19
- import "./chunk-L7UCMGWZ.js";
18
+ } from "./chunk-UXJSO54R.js";
19
+ import "./chunk-SNFGU5Q7.js";
20
20
  import "./chunk-GUNJTORH.js";
21
21
  import "./chunk-JMJLNEXE.js";
22
22
  import "./chunk-SLTQACJW.js";