@promptowl/contextnest-community 1.13.0 → 1.14.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.
@@ -2,7 +2,7 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-5QQ7WKAI.js";
5
+ } from "./chunk-I5KYGMIT.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-ZQS5W45U.js";
9
+ } from "./chunk-IHGPZ6NY.js";
10
10
  import "./chunk-GUNJTORH.js";
11
- import "./chunk-5QQ7WKAI.js";
11
+ import "./chunk-I5KYGMIT.js";
12
12
  import "./chunk-SLTQACJW.js";
13
13
  export {
14
14
  createGrant,
package/dist/index.js CHANGED
@@ -17,18 +17,23 @@ import {
17
17
  listNotifications,
18
18
  listWatchers,
19
19
  markNotificationsRead,
20
+ nodeWriteKey,
20
21
  notifyNestEvent,
22
+ notifyNestInvite,
21
23
  parseConditionSchema,
22
24
  reject,
23
25
  removeWatcher,
26
+ repointPendingReview,
24
27
  requireWorkflowPlane,
25
28
  resolveCallerEmail,
26
29
  safeJson,
27
30
  safePublishDocument,
28
31
  seedDefaultEdgeTypes,
32
+ sendTestNotification,
33
+ stripUndefinedDeep,
29
34
  submitForReview,
30
- verifySmtp
31
- } from "./chunk-PAIP5KHB.js";
35
+ withNodeWriteLock
36
+ } from "./chunk-MZ4IR5KH.js";
32
37
  import {
33
38
  createGrant,
34
39
  deleteGrant,
@@ -37,7 +42,7 @@ import {
37
42
  listGrants,
38
43
  listUserGrants,
39
44
  resolveNodeGrant
40
- } from "./chunk-ZQS5W45U.js";
45
+ } from "./chunk-IHGPZ6NY.js";
41
46
  import {
42
47
  generateApiKey,
43
48
  getKeyPrefix,
@@ -55,7 +60,7 @@ import {
55
60
  getVersions,
56
61
  setApprovedVersion,
57
62
  upsertVersion
58
- } from "./chunk-EG77SQHO.js";
63
+ } from "./chunk-JP67WXLA.js";
59
64
  import {
60
65
  addMember,
61
66
  buildTitleMap,
@@ -119,6 +124,7 @@ import {
119
124
  resolveStewardsForNode,
120
125
  resolveStewardsWithFallback,
121
126
  resolveUserRoles,
127
+ sendEmailToRecipient,
122
128
  setAllowSelfApprove,
123
129
  setStewardshipEnabled,
124
130
  shareTeamWithNest,
@@ -130,8 +136,9 @@ import {
130
136
  unshareTeamFromNest,
131
137
  updateMemberRole,
132
138
  updateSteward,
133
- validateLicense
134
- } from "./chunk-Q5NRCDD4.js";
139
+ validateLicense,
140
+ verifySmtp
141
+ } from "./chunk-KPK656BH.js";
135
142
  import {
136
143
  AppError,
137
144
  ConflictError,
@@ -154,7 +161,7 @@ import {
154
161
  initDb,
155
162
  isEmailListish,
156
163
  isEmailish
157
- } from "./chunk-5QQ7WKAI.js";
164
+ } from "./chunk-I5KYGMIT.js";
158
165
  import {
159
166
  ANON_EMAIL,
160
167
  ANON_USER_ID
@@ -1476,6 +1483,18 @@ authRoutes.post("/invite", async (c) => {
1476
1483
  );
1477
1484
  }
1478
1485
  trackEvent("admin.invite", { adminId: callerId, email });
1486
+ const inviter = await db.get("SELECT email FROM users WHERE id = ?", [
1487
+ callerId
1488
+ ]);
1489
+ void sendEmailToRecipient(email, {
1490
+ status: "invited",
1491
+ docTitle: "ContextNest",
1492
+ path: "",
1493
+ actor: email,
1494
+ by: inviter?.email || void 0,
1495
+ tempPassword,
1496
+ link: requestBaseUrl(c.req.url)
1497
+ });
1479
1498
  return c.json(
1480
1499
  {
1481
1500
  temporary_password: tempPassword,
@@ -1765,7 +1784,7 @@ async function approveExternalEdit(input) {
1765
1784
  const node = await storage.readDocument(input.documentId);
1766
1785
  const versionNum = result.versionEntry.version;
1767
1786
  const tags = node.frontmatter.tags || [];
1768
- const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-L52F4WVZ.js");
1787
+ const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-FGL47APL.js");
1769
1788
  await createVersion2({
1770
1789
  nestId: input.nestId,
1771
1790
  nodeId: input.documentId,
@@ -1874,18 +1893,6 @@ async function departmentTagFor(userEmail) {
1874
1893
  return null;
1875
1894
  }
1876
1895
  }
1877
- var nodeWriteChains = /* @__PURE__ */ new Map();
1878
- function withNodeWriteLock(key, fn) {
1879
- const prev = (nodeWriteChains.get(key) ?? Promise.resolve()).catch(() => {
1880
- });
1881
- const run = prev.then(fn);
1882
- nodeWriteChains.set(key, run);
1883
- void run.catch(() => {
1884
- }).finally(() => {
1885
- if (nodeWriteChains.get(key) === run) nodeWriteChains.delete(key);
1886
- });
1887
- return run;
1888
- }
1889
1896
  function toSafeError(err, context, message) {
1890
1897
  if (err instanceof AppError) return err;
1891
1898
  console.error(`${context}:`, err);
@@ -2300,7 +2307,7 @@ async function registerImportedDocuments(nestId, userEmail, onProgress) {
2300
2307
  return registered;
2301
2308
  }
2302
2309
  async function updateNode(nestId, nodeId, patch, userEmail) {
2303
- return withNodeWriteLock(`${nestId}::${nodeId}`, async () => {
2310
+ return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
2304
2311
  try {
2305
2312
  const { storage, versions: versionManager } = await engineCache.get(
2306
2313
  nestId
@@ -2352,13 +2359,15 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
2352
2359
  const hasStewards = await isStewardshipEnabled(nestId);
2353
2360
  const autoPublish = !hasStewards || await actorAutoPublishes(nestId, userEmail);
2354
2361
  const currentTags = node.frontmatter.tags || [];
2355
- if (hasStewards && await getPendingReview(nestId, nodeId)) {
2362
+ const pendingReview = hasStewards ? await getPendingReview(nestId, nodeId) : null;
2363
+ const isAuthorEditingOwnReview = !!pendingReview && pendingReview.requestedBy.toLowerCase() === userEmail.toLowerCase();
2364
+ if (pendingReview && !isAuthorEditingOwnReview) {
2356
2365
  throw new LockedError(
2357
2366
  "This document is awaiting steward review and is locked. Approve or reject the pending review before editing."
2358
2367
  );
2359
2368
  }
2360
2369
  let responseVersion;
2361
- if (!autoPublish) {
2370
+ if (!autoPublish || isAuthorEditingOwnReview) {
2362
2371
  const currentVersion = await getCurrentVersion(nestId, nodeId);
2363
2372
  const newVersion = currentVersion + 1;
2364
2373
  node = {
@@ -2396,6 +2405,9 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
2396
2405
  tags: currentTags,
2397
2406
  changeNote: patch.changeNote
2398
2407
  });
2408
+ if (isAuthorEditingOwnReview) {
2409
+ await repointPendingReview({ nestId, nodeId, newVersion });
2410
+ }
2399
2411
  responseVersion = newVersion;
2400
2412
  } else {
2401
2413
  node = {
@@ -3143,8 +3155,30 @@ async function addCollaborator(params) {
3143
3155
  actor: params.email || userId,
3144
3156
  permission: params.permission,
3145
3157
  by: params.grantedByEmail || void 0
3146
- }
3147
- );
3158
+ },
3159
+ // Invite email goes to the invitee directly (below), NOT the shared
3160
+ // NOTIFY_EMAIL_TO team inbox — so skip the team-channel email for this
3161
+ // event. Slack/Teams still fire.
3162
+ { skipEmail: true }
3163
+ );
3164
+ const inviteeEmail = params.email || (await db.get("SELECT email FROM users WHERE id = ?", [userId]))?.email;
3165
+ if (inviteeEmail) {
3166
+ void sendEmailToRecipient(inviteeEmail, {
3167
+ status: "shared",
3168
+ docTitle: shareNestName,
3169
+ path: shareNestName,
3170
+ link: docLink(nestId, void 0, params.baseUrl),
3171
+ actor: inviteeEmail,
3172
+ permission: params.permission,
3173
+ by: params.grantedByEmail || void 0
3174
+ });
3175
+ void notifyNestInvite({
3176
+ nestId,
3177
+ inviteeEmail,
3178
+ permission: params.permission,
3179
+ nestName: shareNestName
3180
+ });
3181
+ }
3148
3182
  return await db.get(
3149
3183
  "SELECT * FROM nest_collaborators WHERE id = ?",
3150
3184
  [collabId]
@@ -3336,7 +3370,8 @@ nestTeamRoutes.post("/", async (c) => {
3336
3370
  nestId: c.req.param("nestId"),
3337
3371
  teamId: body.team_id,
3338
3372
  callerUserId: userId,
3339
- allowAdmin: await isLicenseAdminUserId(userId)
3373
+ allowAdmin: await isLicenseAdminUserId(userId),
3374
+ baseUrl: requestBaseUrl(c.req.url)
3340
3375
  });
3341
3376
  return c.json({ teams }, 201);
3342
3377
  });
@@ -4050,7 +4085,7 @@ nodeRoutes.post("/", async (c) => {
4050
4085
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4051
4086
  const nestId = c.req.param("nestId");
4052
4087
  const nodeId = c.req.param("nodeId");
4053
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-B6L7UACX.js");
4088
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-G7IF7ZNP.js");
4054
4089
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
4055
4090
  nestId,
4056
4091
  nodeId
@@ -4071,7 +4106,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4071
4106
  nodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
4072
4107
  const nestId = c.req.param("nestId");
4073
4108
  const nodeId = c.req.param("nodeId");
4074
- const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-L52F4WVZ.js");
4109
+ const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-FGL47APL.js");
4075
4110
  const allVersions = await getVersions2(nestId, nodeId);
4076
4111
  const approved = await getApprovedVersion2(nestId, nodeId);
4077
4112
  const db = getDb();
@@ -4103,7 +4138,7 @@ nodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
4103
4138
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
4104
4139
  const nestId = c.req.param("nestId");
4105
4140
  const nodeId = c.req.param("nodeId");
4106
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-Q3NDK5TX.js");
4141
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-6G7MVTY6.js");
4107
4142
  const history = await getReviewHistory2(nestId, nodeId);
4108
4143
  return c.json({ reviews: history });
4109
4144
  });
@@ -4518,7 +4553,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
4518
4553
 
4519
4554
  // src/nodes/query-routes.ts
4520
4555
  import { Hono as Hono10 } from "hono";
4521
- import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
4556
+ import { serializeDocument as serializeDocument3 } from "@promptowl/contextnest-engine";
4522
4557
 
4523
4558
  // src/tables/service.ts
4524
4559
  function splitCsvLine(line) {
@@ -4937,6 +4972,68 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
4937
4972
  }
4938
4973
  }
4939
4974
 
4975
+ // src/nests/bundle-service.ts
4976
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
4977
+ import { join as join4, sep } from "path";
4978
+ import JSZip from "jszip";
4979
+ import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
4980
+ function walkFiles(root) {
4981
+ const out = [];
4982
+ let entries;
4983
+ try {
4984
+ entries = readdirSync2(root, { recursive: true });
4985
+ } catch {
4986
+ return out;
4987
+ }
4988
+ for (const rel of entries) {
4989
+ try {
4990
+ if (statSync2(join4(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
4991
+ } catch {
4992
+ }
4993
+ }
4994
+ return out;
4995
+ }
4996
+ async function exportNestBundle(nestId, includeDrafts) {
4997
+ const db = getDb();
4998
+ const approvedRows = await db.all(
4999
+ "SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
5000
+ [nestId]
5001
+ );
5002
+ const approvedByNode = new Map(
5003
+ approvedRows.map((r) => [r.node_id, r.approved_version])
5004
+ );
5005
+ const { storage } = await engineCache.get(nestId);
5006
+ const docs = await storage.discoverDocuments();
5007
+ const zip = new JSZip();
5008
+ let count = 0;
5009
+ for (const doc of docs) {
5010
+ const approved = approvedByNode.get(doc.id);
5011
+ const frontmatter = stripUndefinedDeep({ ...doc.frontmatter });
5012
+ let body = doc.body || "";
5013
+ if (approved != null) {
5014
+ const approvedBody = await resolveExportBody(nestId, doc.id, body);
5015
+ if (approvedBody == null) continue;
5016
+ body = approvedBody;
5017
+ frontmatter.status = "published";
5018
+ frontmatter.version = approved;
5019
+ } else {
5020
+ if (!includeDrafts) continue;
5021
+ frontmatter.status = "draft";
5022
+ }
5023
+ zip.file(`${doc.id}.md`, serializeDocument2({ ...doc, frontmatter, body }));
5024
+ count++;
5025
+ }
5026
+ const nestPath = resolveNestPath(nestId);
5027
+ for (const rel of walkFiles(nestPath)) {
5028
+ const segs = rel.split("/");
5029
+ const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
5030
+ if (!keep) continue;
5031
+ zip.file(rel, readFileSync2(join4(nestPath, rel)));
5032
+ }
5033
+ trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
5034
+ return zip.generateAsync({ type: "arraybuffer" });
5035
+ }
5036
+
4940
5037
  // src/nodes/graph-service.ts
4941
5038
  var MAX_GRAPH_NODES = 150;
4942
5039
  var WIKILINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
@@ -5490,10 +5587,24 @@ queryRoutes.get("/context", async (c) => {
5490
5587
  return c.json({ content: content || "" });
5491
5588
  });
5492
5589
  queryRoutes.get("/export", async (c) => {
5590
+ const nestId = c.req.param("nestId");
5591
+ if (c.req.query("format") === "bundle") {
5592
+ const perm = await resolveNestPermission(nestId, c.get("userId"));
5593
+ if (perm !== "owner" && perm !== "admin") {
5594
+ throw new ForbiddenError("Only the nest owner or an admin can export a bundle");
5595
+ }
5596
+ const draftsParam = c.req.query("includeDrafts");
5597
+ const includeDrafts = draftsParam === "1" || draftsParam === "true";
5598
+ const zip = await exportNestBundle(nestId, includeDrafts);
5599
+ const filename = `nest-${nestId}.zip`;
5600
+ return c.body(zip, 200, {
5601
+ "Content-Type": "application/zip",
5602
+ "Content-Disposition": `attachment; filename="${filename}"`
5603
+ });
5604
+ }
5493
5605
  if (!isMarkdownFormat(c)) {
5494
- throw new ValidationError("format=markdown is required");
5606
+ throw new ValidationError("format=markdown or format=bundle is required");
5495
5607
  }
5496
- const nestId = c.req.param("nestId");
5497
5608
  const { storage, query: queryEngine } = await engineCache.get(nestId);
5498
5609
  const selector = c.req.query("selector")?.trim() || null;
5499
5610
  let documents;
@@ -5592,7 +5703,7 @@ queryRoutes.post("/publish", async (c) => {
5592
5703
  body: doc.content,
5593
5704
  rawContent: ""
5594
5705
  };
5595
- const serialized = serializeDocument2(node);
5706
+ const serialized = serializeDocument3(node);
5596
5707
  await storage.writeDocument(id, serialized);
5597
5708
  await syncNodeTags(nestId, id, tags);
5598
5709
  created.push(id);
@@ -7397,17 +7508,15 @@ ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "
7397
7508
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
7398
7509
  );
7399
7510
  if (!node) return `Node not found: ${args.title}`;
7400
- const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
7401
7511
  try {
7402
7512
  const request = await approve({
7403
7513
  nestId: ctx.nestId,
7404
7514
  nodeId: node.id,
7405
- version: currentVersion,
7406
7515
  approvedBy: ctx.userEmail,
7407
7516
  note: args.note,
7408
7517
  baseUrl: ctx.baseUrl
7409
7518
  });
7410
- return `Approved "${args.title}" v${currentVersion}. This version is now live for AI queries.${args.note ? `
7519
+ return `Approved "${args.title}" v${request.version}. This version is now live for AI queries.${args.note ? `
7411
7520
  Note: ${args.note}` : ""}`;
7412
7521
  } catch (err) {
7413
7522
  return `Cannot approve: ${err.message}`;
@@ -7419,17 +7528,15 @@ Note: ${args.note}` : ""}`;
7419
7528
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
7420
7529
  );
7421
7530
  if (!node) return `Node not found: ${args.title}`;
7422
- const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
7423
7531
  try {
7424
7532
  const request = await reject({
7425
7533
  nestId: ctx.nestId,
7426
7534
  nodeId: node.id,
7427
- version: currentVersion,
7428
7535
  rejectedBy: ctx.userEmail,
7429
7536
  note: args.note,
7430
7537
  baseUrl: ctx.baseUrl
7431
7538
  });
7432
- return `Rejected "${args.title}" v${currentVersion}.
7539
+ return `Rejected "${args.title}" v${request.version}.
7433
7540
  Reason: ${args.note}`;
7434
7541
  } catch (err) {
7435
7542
  return `Cannot reject: ${err.message}`;
@@ -7510,8 +7617,8 @@ ${list}`;
7510
7617
  if (!target) return "target is required (a node id or folder prefix).";
7511
7618
  if (!["read", "write"].includes(role)) return "role must be read or write.";
7512
7619
  try {
7513
- const { createGrant: createGrant2 } = await import("./grants-service-FZD6QM6V.js");
7514
- const db = (await import("./client-AAMF22YJ.js")).getDb();
7620
+ const { createGrant: createGrant2 } = await import("./grants-service-5ZFISCBH.js");
7621
+ const db = (await import("./client-3MGIP57D.js")).getDb();
7515
7622
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
7516
7623
  const e = normalizeEmail2(String(args.email || ""));
7517
7624
  if (!e) return "email is required.";
@@ -8598,7 +8705,7 @@ definitionRoutes.delete("/:id", async (c) => {
8598
8705
  import { Hono as Hono18 } from "hono";
8599
8706
  import { v4 as uuid13 } from "uuid";
8600
8707
  import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
8601
- import { join as join4 } from "path";
8708
+ import { join as join5 } from "path";
8602
8709
  var assetRoutes = new Hono18();
8603
8710
  var CONTENT_TYPES = {
8604
8711
  png: "image/png",
@@ -8646,9 +8753,9 @@ assetRoutes.post("/", async (c) => {
8646
8753
  );
8647
8754
  }
8648
8755
  const name = `${uuid13()}.${ext === "jpeg" ? "jpg" : ext}`;
8649
- const dir = join4(resolveNestPath(nestId), "assets");
8756
+ const dir = join5(resolveNestPath(nestId), "assets");
8650
8757
  await mkdir2(dir, { recursive: true });
8651
- await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
8758
+ await writeFile(join5(dir, name), Buffer.from(await file.arrayBuffer()));
8652
8759
  const url = `/nests/${nestId}/assets/${name}`;
8653
8760
  return c.json(
8654
8761
  {
@@ -8670,7 +8777,7 @@ assetRoutes.get("/:file", async (c) => {
8670
8777
  }
8671
8778
  let bytes;
8672
8779
  try {
8673
- bytes = await readFile2(join4(resolveNestPath(nestId), "assets", name));
8780
+ bytes = await readFile2(join5(resolveNestPath(nestId), "assets", name));
8674
8781
  } catch {
8675
8782
  throw new NotFoundError("Asset not found");
8676
8783
  }
@@ -9425,8 +9532,8 @@ function rowToComment(row) {
9425
9532
  }
9426
9533
 
9427
9534
  // src/governance/stewards-parser.ts
9428
- import { readFileSync as readFileSync2, existsSync } from "fs";
9429
- import { join as join5 } from "path";
9535
+ import { readFileSync as readFileSync3, existsSync } from "fs";
9536
+ import { join as join6 } from "path";
9430
9537
  function parseStewardsYaml(content) {
9431
9538
  const result = { version: 1 };
9432
9539
  const lines = content.split("\n");
@@ -9492,13 +9599,13 @@ function parseEntry(str) {
9492
9599
  function loadStewardsConfig(nestId) {
9493
9600
  const nestPath = resolveNestPath(nestId);
9494
9601
  const candidates = [
9495
- join5(nestPath, "stewards.yaml"),
9496
- join5(nestPath, "stewards.yml"),
9497
- join5(nestPath, ".context", "stewards.yaml")
9602
+ join6(nestPath, "stewards.yaml"),
9603
+ join6(nestPath, "stewards.yml"),
9604
+ join6(nestPath, ".context", "stewards.yaml")
9498
9605
  ];
9499
9606
  for (const candidatePath of candidates) {
9500
9607
  if (existsSync(candidatePath)) {
9501
- const content = readFileSync2(candidatePath, "utf-8");
9608
+ const content = readFileSync3(candidatePath, "utf-8");
9502
9609
  return parseStewardsYaml(content);
9503
9610
  }
9504
9611
  }
@@ -9554,7 +9661,8 @@ governanceRoutes.post("/stewards", async (c) => {
9554
9661
  documentId: body.documentId,
9555
9662
  tagName: body.tagName,
9556
9663
  users: body.users,
9557
- assignedBy
9664
+ assignedBy,
9665
+ baseUrl: requestBaseUrl(c.req.url)
9558
9666
  });
9559
9667
  return c.json({ stewards: created2 }, 201);
9560
9668
  }
@@ -9572,7 +9680,8 @@ governanceRoutes.post("/stewards", async (c) => {
9572
9680
  role: body.role
9573
9681
  }
9574
9682
  ],
9575
- assignedBy
9683
+ assignedBy,
9684
+ baseUrl: requestBaseUrl(c.req.url)
9576
9685
  });
9577
9686
  return c.json({ steward: created[0] }, 201);
9578
9687
  });
@@ -9891,7 +10000,6 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
9891
10000
  const request = await approve({
9892
10001
  nestId,
9893
10002
  nodeId,
9894
- version: await getCurrentVersion(nestId, nodeId),
9895
10003
  approvedBy: userEmail,
9896
10004
  note: body.note,
9897
10005
  override: body.override && isAdmin,
@@ -9914,7 +10022,6 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
9914
10022
  const request = await reject({
9915
10023
  nestId,
9916
10024
  nodeId,
9917
- version: await getCurrentVersion(nestId, nodeId),
9918
10025
  rejectedBy: userEmail,
9919
10026
  note: body.note,
9920
10027
  baseUrl: requestBaseUrl(c.req.url)
@@ -10007,19 +10114,19 @@ async function ensureAnonymousUser() {
10007
10114
  // src/app.ts
10008
10115
  import { serveStatic } from "@hono/node-server/serve-static";
10009
10116
  import { fileURLToPath } from "url";
10010
- import { dirname as dirname2, join as join6, relative as relative2 } from "path";
10011
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
10117
+ import { dirname as dirname2, join as join7, relative as relative2 } from "path";
10118
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
10012
10119
  var HERE = dirname2(fileURLToPath(import.meta.url));
10013
10120
  var SERVICE_VERSION = (() => {
10014
10121
  try {
10015
- return JSON.parse(readFileSync3(join6(HERE, "..", "package.json"), "utf8")).version;
10122
+ return JSON.parse(readFileSync4(join7(HERE, "..", "package.json"), "utf8")).version;
10016
10123
  } catch {
10017
10124
  return "unknown";
10018
10125
  }
10019
10126
  })();
10020
10127
  var UI_DIR_CANDIDATES = [
10021
- join6(HERE, "web3"),
10022
- join6(process.cwd(), "dist", "web3")
10128
+ join7(HERE, "web3"),
10129
+ join7(process.cwd(), "dist", "web3")
10023
10130
  ];
10024
10131
  var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
10025
10132
  var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
@@ -10240,6 +10347,7 @@ function createApp() {
10240
10347
  });
10241
10348
  const serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
10242
10349
  app.use("/admin/settings", flexAuthMiddleware);
10350
+ app.use("/admin/settings/*", flexAuthMiddleware);
10243
10351
  const currentServerSettings = () => ({
10244
10352
  promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
10245
10353
  manual_sign_in: config.MANUAL_SIGN_IN,
@@ -10272,6 +10380,7 @@ function createApp() {
10272
10380
  // Server-wide runner default. NEVER the value — masked tail only.
10273
10381
  anthropic_api_key_masked: process.env.ANTHROPIC_API_KEY ? `\u2022\u2022\u2022\u2022${process.env.ANTHROPIC_API_KEY.slice(-4)}` : null,
10274
10382
  slack_webhook_url: config.SLACK_WEBHOOK_URL ?? "",
10383
+ msteams_webhook_url: config.MSTEAMS_WEBHOOK_URL ?? "",
10275
10384
  smtp_url: config.SMTP_URL ?? "",
10276
10385
  notify_email_from: config.NOTIFY_EMAIL_FROM ?? "",
10277
10386
  notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
@@ -10281,6 +10390,20 @@ function createApp() {
10281
10390
  return c.json({ error: "Only the server admin can view this." }, 403);
10282
10391
  return c.json(currentServerSettings());
10283
10392
  });
10393
+ app.post("/admin/settings/test-notification", async (c) => {
10394
+ if (!await serverAdminAllowed(c))
10395
+ return c.json({ error: "Only the server admin can do this." }, 403);
10396
+ let body;
10397
+ try {
10398
+ body = await c.req.json();
10399
+ } catch {
10400
+ return c.json({ error: "Invalid JSON body" }, 400);
10401
+ }
10402
+ if (body.channel !== "slack" && body.channel !== "teams")
10403
+ return c.json({ error: "channel must be 'slack' or 'teams'" }, 400);
10404
+ const result = await sendTestNotification(body.channel);
10405
+ return result.ok ? c.json({ ok: true }) : c.json({ error: result.error }, 502);
10406
+ });
10284
10407
  app.patch("/admin/settings", async (c) => {
10285
10408
  if (!await serverAdminAllowed(c))
10286
10409
  return c.json({ error: "Only the server admin can change this." }, 403);
@@ -10473,6 +10596,12 @@ function createApp() {
10473
10596
  addError("slack_webhook_url", "Slack webhook must be an https:// URL (or leave it empty to turn Slack notifications off).");
10474
10597
  else pending.push({ name: "SLACK_WEBHOOK_URL", value: v || null });
10475
10598
  }
10599
+ if ("msteams_webhook_url" in body) {
10600
+ const v = String(body.msteams_webhook_url ?? "").trim();
10601
+ if (v && !/^https:\/\//i.test(v))
10602
+ addError("msteams_webhook_url", "Microsoft Teams webhook must be an https:// URL (or leave it empty to turn Teams notifications off).");
10603
+ else pending.push({ name: "MSTEAMS_WEBHOOK_URL", value: v || null });
10604
+ }
10476
10605
  if ("smtp_url" in body) {
10477
10606
  const v = String(body.smtp_url ?? "").trim();
10478
10607
  if (v && !/^smtps?:\/\//i.test(v))
@@ -5,17 +5,18 @@ import {
5
5
  getReviewHistory,
6
6
  getReviewQueue,
7
7
  reject,
8
+ repointPendingReview,
8
9
  submitForReview
9
- } from "./chunk-PAIP5KHB.js";
10
- import "./chunk-ZQS5W45U.js";
11
- import "./chunk-EG77SQHO.js";
10
+ } from "./chunk-MZ4IR5KH.js";
11
+ import "./chunk-IHGPZ6NY.js";
12
+ import "./chunk-JP67WXLA.js";
12
13
  import {
13
14
  canUserApprove
14
- } from "./chunk-Q5NRCDD4.js";
15
+ } from "./chunk-KPK656BH.js";
15
16
  import "./chunk-GUNJTORH.js";
16
17
  import "./chunk-FRQJWGN3.js";
17
18
  import "./chunk-XQ46F76G.js";
18
- import "./chunk-5QQ7WKAI.js";
19
+ import "./chunk-I5KYGMIT.js";
19
20
  import "./chunk-SLTQACJW.js";
20
21
  export {
21
22
  approve,
@@ -25,5 +26,6 @@ export {
25
26
  getReviewHistory,
26
27
  getReviewQueue,
27
28
  reject,
29
+ repointPendingReview,
28
30
  submitForReview
29
31
  };
@@ -21,11 +21,11 @@ import {
21
21
  syncFromConfig,
22
22
  updateSteward,
23
23
  updateStewardRole
24
- } from "./chunk-Q5NRCDD4.js";
24
+ } from "./chunk-KPK656BH.js";
25
25
  import "./chunk-GUNJTORH.js";
26
26
  import "./chunk-FRQJWGN3.js";
27
27
  import "./chunk-XQ46F76G.js";
28
- import "./chunk-5QQ7WKAI.js";
28
+ import "./chunk-I5KYGMIT.js";
29
29
  import "./chunk-SLTQACJW.js";
30
30
  export {
31
31
  assignSteward,
@@ -12,9 +12,9 @@ import {
12
12
  setApprovedVersion,
13
13
  systemAuthor,
14
14
  upsertVersion
15
- } from "./chunk-EG77SQHO.js";
15
+ } from "./chunk-JP67WXLA.js";
16
16
  import "./chunk-XQ46F76G.js";
17
- import "./chunk-5QQ7WKAI.js";
17
+ import "./chunk-I5KYGMIT.js";
18
18
  import "./chunk-SLTQACJW.js";
19
19
  export {
20
20
  SYSTEM_AUTHOR_PREFIX,