@promptowl/contextnest-community 1.6.0 → 1.8.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/CONFIGURATION.md +1 -0
- package/dist/{chunk-2TUMMVBG.js → chunk-PJTAUVD7.js} +15 -5
- package/dist/{chunk-7V33Z6CS.js → chunk-PLAHCDQG.js} +184 -1
- package/dist/{chunk-T5L4LYU4.js → chunk-XNGOO6RI.js} +1 -1
- package/dist/{chunk-MZGFKBOK.js → chunk-Z3Y46ZGU.js} +62 -3
- package/dist/index.js +1523 -522
- package/dist/{migrations.postgres-NVJAGBSF.js → migrations.postgres-VCG45LMK.js} +41 -1
- package/dist/{review-service-QJ3FBOML.js → review-service-G5US3SMG.js} +4 -4
- package/dist/{stewardship-service-VIKF3VZB.js → stewardship-service-ZXAMFTXO.js} +2 -2
- package/dist/{version-service-MUZYTYMW.js → version-service-B3SDOJQE.js} +2 -2
- package/dist/web3/assets/index-Den-5nzY.js +904 -0
- package/dist/web3/assets/index-Jfj-2MHm.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +2 -2
- package/dist/web3/assets/index-C2Tf2ZFz.js +0 -887
- package/dist/web3/assets/index-C2dfT3Et.css +0 -1
package/CONFIGURATION.md
CHANGED
|
@@ -66,6 +66,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
66
66
|
| `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
|
|
67
67
|
| `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. |
|
|
68
68
|
| `LOGO_URL` | _(unset)_ | Custom logo shown in the UI header + login screen. Must start with `https://`, `http://`, or `data:image/` — other schemes (`file://`, relative, `javascript:`) are rejected with a warning and the bundled icon is used. |
|
|
69
|
+
| `SLACK_WEBHOOK_URL` | _(unset — connector off)_ | Slack incoming-webhook URL for governance-event notifications (review submitted/approved/rejected, collaborator added). `https://` only — the URL embeds a secret. Also editable from Settings in the UI. |
|
|
69
70
|
|
|
70
71
|
---
|
|
71
72
|
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
getDb,
|
|
4
4
|
insertOrReplace,
|
|
5
5
|
nowExpr
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-PLAHCDQG.js";
|
|
7
7
|
import {
|
|
8
8
|
ANON_USER_ID
|
|
9
9
|
} from "./chunk-SLTQACJW.js";
|
|
@@ -921,6 +921,15 @@ async function buildTitleMap(nestId) {
|
|
|
921
921
|
return /* @__PURE__ */ new Map();
|
|
922
922
|
}
|
|
923
923
|
}
|
|
924
|
+
function folderForNode(nodeId) {
|
|
925
|
+
return nodeId.replace(/^nodes\//, "").split("/").slice(0, -1).join("/");
|
|
926
|
+
}
|
|
927
|
+
async function describeNode(nestId, nodeId) {
|
|
928
|
+
const titles = await buildTitleMap(nestId);
|
|
929
|
+
const title = titles.get(nodeId) || nodeId;
|
|
930
|
+
const folder = folderForNode(nodeId);
|
|
931
|
+
return folder ? `${title} (in ${folder})` : title;
|
|
932
|
+
}
|
|
924
933
|
|
|
925
934
|
// src/shared/access.ts
|
|
926
935
|
var PERMISSION_LEVELS = {
|
|
@@ -1007,7 +1016,6 @@ function collabPermToRole(permission) {
|
|
|
1007
1016
|
var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
|
|
1008
1017
|
var canViewWith = (roles) => roles.length > 0;
|
|
1009
1018
|
var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
|
|
1010
|
-
var canManageWith = (roles) => includesAny(roles, ["owner", "admin"]);
|
|
1011
1019
|
var PRECEDENCE = [
|
|
1012
1020
|
"owner",
|
|
1013
1021
|
"admin",
|
|
@@ -1352,9 +1360,10 @@ async function resolveUserRoles(nestId, userEmail, opts) {
|
|
|
1352
1360
|
for (const role of stewardRoles) roles.add(role);
|
|
1353
1361
|
return [...roles];
|
|
1354
1362
|
}
|
|
1355
|
-
function canManageStewards(
|
|
1363
|
+
async function canManageStewards(nestId, userId) {
|
|
1356
1364
|
if (config.AUTH_MODE === "open") return true;
|
|
1357
|
-
|
|
1365
|
+
const perm = await resolveNestPermission(nestId, userId);
|
|
1366
|
+
return perm === "owner" || perm === "admin";
|
|
1358
1367
|
}
|
|
1359
1368
|
async function canCreateInNest(nestId, userEmail) {
|
|
1360
1369
|
if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
|
|
@@ -1580,7 +1589,8 @@ export {
|
|
|
1580
1589
|
deleteNest,
|
|
1581
1590
|
engineCache,
|
|
1582
1591
|
buildTitleMap,
|
|
1583
|
-
|
|
1592
|
+
describeNode,
|
|
1593
|
+
collabPermToRole,
|
|
1584
1594
|
assignSteward,
|
|
1585
1595
|
removeSteward,
|
|
1586
1596
|
updateSteward,
|
|
@@ -20,6 +20,7 @@ if (envFileLoaded && !isTestRun) {
|
|
|
20
20
|
}
|
|
21
21
|
var canonicalEnvFile = process.env.ENV_FILE_PATH || join(dataRoot(), ".env");
|
|
22
22
|
var canonicalEnvLoaded = null;
|
|
23
|
+
var slackUrlWarned = false;
|
|
23
24
|
if (!isTestRun && canonicalEnvFile !== envFileLoaded && existsSync(canonicalEnvFile)) {
|
|
24
25
|
dotenv.config({ path: canonicalEnvFile, override: true });
|
|
25
26
|
canonicalEnvLoaded = canonicalEnvFile;
|
|
@@ -189,6 +190,26 @@ var config = {
|
|
|
189
190
|
get AUTH_MODE() {
|
|
190
191
|
return process.env.AUTH_MODE || "key";
|
|
191
192
|
},
|
|
193
|
+
/**
|
|
194
|
+
* Optional Slack incoming-webhook URL for team notifications (review
|
|
195
|
+
* submitted/approved/rejected, nest shared). Empty/unset = connector off.
|
|
196
|
+
* https only — a webhook carries an implicit secret in its path, so it
|
|
197
|
+
* never travels plaintext.
|
|
198
|
+
*/
|
|
199
|
+
get SLACK_WEBHOOK_URL() {
|
|
200
|
+
const raw = process.env.SLACK_WEBHOOK_URL?.trim();
|
|
201
|
+
if (!raw) return null;
|
|
202
|
+
if (!/^https:\/\//i.test(raw)) {
|
|
203
|
+
if (!slackUrlWarned) {
|
|
204
|
+
slackUrlWarned = true;
|
|
205
|
+
console.warn(
|
|
206
|
+
"[config] SLACK_WEBHOOK_URL rejected: must be an https:// URL. Slack notifications disabled."
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return raw;
|
|
212
|
+
},
|
|
192
213
|
/**
|
|
193
214
|
* CORS origin allowlist. Comma-separated list of origins or "*".
|
|
194
215
|
* Defaults to "*" in open mode (read-heavy, no credentials to steal)
|
|
@@ -764,6 +785,168 @@ function runMigrations(db) {
|
|
|
764
785
|
})();
|
|
765
786
|
recordMigration("011_comments");
|
|
766
787
|
}
|
|
788
|
+
if (!hasMigration("012_merge_case_colliding_users")) {
|
|
789
|
+
db.transaction(() => {
|
|
790
|
+
const { groups, merged } = mergeCaseCollidingUsers(db);
|
|
791
|
+
if (merged > 0) {
|
|
792
|
+
console.warn(
|
|
793
|
+
`[migration 012] merged ${merged} duplicate user row(s) across ${groups} case-collision group(s).`
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
db.exec(
|
|
797
|
+
"UPDATE users SET email = LOWER(email) WHERE email <> LOWER(email)"
|
|
798
|
+
);
|
|
799
|
+
db.exec(
|
|
800
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_nocase ON users(email COLLATE NOCASE)"
|
|
801
|
+
);
|
|
802
|
+
})();
|
|
803
|
+
recordMigration("012_merge_case_colliding_users");
|
|
804
|
+
}
|
|
805
|
+
if (!hasMigration("013_node_deletion_tombstones")) {
|
|
806
|
+
db.transaction(() => {
|
|
807
|
+
db.exec(`
|
|
808
|
+
CREATE TABLE IF NOT EXISTS node_deletions (
|
|
809
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
810
|
+
node_id TEXT NOT NULL,
|
|
811
|
+
deleted_by TEXT,
|
|
812
|
+
deleted_at TEXT NOT NULL,
|
|
813
|
+
PRIMARY KEY (nest_id, node_id)
|
|
814
|
+
);
|
|
815
|
+
CREATE INDEX IF NOT EXISTS idx_node_deletions_time
|
|
816
|
+
ON node_deletions(nest_id, deleted_at);
|
|
817
|
+
`);
|
|
818
|
+
})();
|
|
819
|
+
recordMigration("013_node_deletion_tombstones");
|
|
820
|
+
}
|
|
821
|
+
if (!hasMigration("014_api_events")) {
|
|
822
|
+
db.transaction(() => {
|
|
823
|
+
db.exec(`
|
|
824
|
+
CREATE TABLE IF NOT EXISTS api_events (
|
|
825
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
826
|
+
ts TEXT NOT NULL,
|
|
827
|
+
kind TEXT NOT NULL CHECK(kind IN ('api', 'mcp')),
|
|
828
|
+
method TEXT, -- HTTP method (api rows)
|
|
829
|
+
path TEXT, -- request path (api rows)
|
|
830
|
+
tool TEXT, -- tool name (mcp rows)
|
|
831
|
+
nest_id TEXT,
|
|
832
|
+
user_id TEXT,
|
|
833
|
+
user_email TEXT,
|
|
834
|
+
status INTEGER, -- HTTP status (api rows)
|
|
835
|
+
duration_ms INTEGER
|
|
836
|
+
);
|
|
837
|
+
CREATE INDEX IF NOT EXISTS idx_api_events_ts ON api_events(ts);
|
|
838
|
+
`);
|
|
839
|
+
})();
|
|
840
|
+
recordMigration("014_api_events");
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function mergeCaseCollidingUsers(db) {
|
|
844
|
+
const groups = db.prepare(
|
|
845
|
+
`SELECT LOWER(email) AS le
|
|
846
|
+
FROM users
|
|
847
|
+
WHERE id != ?
|
|
848
|
+
GROUP BY LOWER(email)
|
|
849
|
+
HAVING COUNT(*) > 1`
|
|
850
|
+
).all(ANON_USER_ID);
|
|
851
|
+
const emailCols = [
|
|
852
|
+
["annotation_threads", "created_by"],
|
|
853
|
+
["annotation_threads", "resolved_by"],
|
|
854
|
+
["annotation_comments", "author"],
|
|
855
|
+
["review_requests", "requested_by"],
|
|
856
|
+
["review_requests", "resolved_by"],
|
|
857
|
+
["node_versions", "author"],
|
|
858
|
+
["approved_versions", "approved_by"],
|
|
859
|
+
["comments", "author"],
|
|
860
|
+
["comments", "resolved_by"],
|
|
861
|
+
["license_cache", "owner_email"],
|
|
862
|
+
["stewards", "assigned_by"]
|
|
863
|
+
];
|
|
864
|
+
let merged = 0;
|
|
865
|
+
for (const { le } of groups) {
|
|
866
|
+
const rows = db.prepare(
|
|
867
|
+
`SELECT u.id, u.email, u.is_invited, u.is_admin, u.name, u.created_at,
|
|
868
|
+
(SELECT COUNT(*) FROM api_keys k WHERE k.user_id = u.id) AS keys,
|
|
869
|
+
(SELECT COUNT(*) FROM sessions s WHERE s.user_id = u.id) AS sessions
|
|
870
|
+
FROM users u
|
|
871
|
+
WHERE LOWER(u.email) = ? AND u.id != ?`
|
|
872
|
+
).all(le, ANON_USER_ID);
|
|
873
|
+
if (rows.length < 2) continue;
|
|
874
|
+
rows.sort((a, b) => {
|
|
875
|
+
if (a.is_invited !== b.is_invited) return a.is_invited - b.is_invited;
|
|
876
|
+
const aCred = a.keys + a.sessions > 0 ? 1 : 0;
|
|
877
|
+
const bCred = b.keys + b.sessions > 0 ? 1 : 0;
|
|
878
|
+
if (aCred !== bCred) return bCred - aCred;
|
|
879
|
+
return b.created_at.localeCompare(a.created_at);
|
|
880
|
+
});
|
|
881
|
+
const winner = rows[0];
|
|
882
|
+
const losers = rows.slice(1);
|
|
883
|
+
const anyAdmin = rows.some((r) => r.is_admin) ? 1 : 0;
|
|
884
|
+
const winnerName = winner.name ?? rows.find((r) => r.name)?.name ?? null;
|
|
885
|
+
for (const loser of losers) {
|
|
886
|
+
const w = winner.id;
|
|
887
|
+
const l = loser.id;
|
|
888
|
+
const loserLower = loser.email.toLowerCase();
|
|
889
|
+
db.prepare("UPDATE sessions SET user_id = ? WHERE user_id = ?").run(w, l);
|
|
890
|
+
db.prepare(
|
|
891
|
+
"UPDATE nest_collaborators SET granted_by = ? WHERE granted_by = ?"
|
|
892
|
+
).run(w, l);
|
|
893
|
+
db.prepare("UPDATE stewards SET user_id = ? WHERE user_id = ?").run(w, l);
|
|
894
|
+
db.prepare(
|
|
895
|
+
`UPDATE nests SET user_id = ?
|
|
896
|
+
WHERE user_id = ?
|
|
897
|
+
AND slug NOT IN (SELECT slug FROM nests WHERE user_id = ?)`
|
|
898
|
+
).run(w, l, w);
|
|
899
|
+
db.prepare(
|
|
900
|
+
`UPDATE nests
|
|
901
|
+
SET slug = slug || '-merged-' || substr(id, 1, 8), user_id = ?
|
|
902
|
+
WHERE user_id = ?`
|
|
903
|
+
).run(w, l);
|
|
904
|
+
db.prepare(
|
|
905
|
+
`DELETE FROM api_keys
|
|
906
|
+
WHERE user_id = ?
|
|
907
|
+
AND EXISTS (SELECT 1 FROM api_keys WHERE user_id = ?)`
|
|
908
|
+
).run(l, w);
|
|
909
|
+
db.prepare("UPDATE api_keys SET user_id = ? WHERE user_id = ?").run(w, l);
|
|
910
|
+
db.prepare(
|
|
911
|
+
`DELETE FROM nest_collaborators
|
|
912
|
+
WHERE user_id = ?
|
|
913
|
+
AND nest_id IN (SELECT nest_id FROM nest_collaborators WHERE user_id = ?)`
|
|
914
|
+
).run(l, w);
|
|
915
|
+
db.prepare(
|
|
916
|
+
"UPDATE nest_collaborators SET user_id = ? WHERE user_id = ?"
|
|
917
|
+
).run(w, l);
|
|
918
|
+
db.prepare(
|
|
919
|
+
`DELETE FROM nest_collaborators
|
|
920
|
+
WHERE user_id = ?
|
|
921
|
+
AND nest_id IN (SELECT id FROM nests WHERE user_id = ?)`
|
|
922
|
+
).run(w, w);
|
|
923
|
+
for (const [table, col] of emailCols) {
|
|
924
|
+
db.prepare(
|
|
925
|
+
`UPDATE ${table} SET ${col} = ? WHERE LOWER(${col}) = ?`
|
|
926
|
+
).run(le, loserLower);
|
|
927
|
+
}
|
|
928
|
+
db.prepare(
|
|
929
|
+
`DELETE FROM stewards
|
|
930
|
+
WHERE is_active = 1
|
|
931
|
+
AND LOWER(user_email) = ?
|
|
932
|
+
AND id NOT IN (
|
|
933
|
+
SELECT MIN(id) FROM stewards
|
|
934
|
+
WHERE is_active = 1 AND LOWER(user_email) = ?
|
|
935
|
+
GROUP BY nest_id, scope,
|
|
936
|
+
IFNULL(node_pattern, ''), IFNULL(tag_name, '')
|
|
937
|
+
)`
|
|
938
|
+
).run(le, le);
|
|
939
|
+
db.prepare(
|
|
940
|
+
"UPDATE stewards SET user_email = ?, user_id = ? WHERE LOWER(user_email) = ?"
|
|
941
|
+
).run(le, w, le);
|
|
942
|
+
db.prepare("DELETE FROM users WHERE id = ?").run(l);
|
|
943
|
+
merged++;
|
|
944
|
+
}
|
|
945
|
+
db.prepare(
|
|
946
|
+
"UPDATE users SET email = ?, is_admin = ?, name = ? WHERE id = ?"
|
|
947
|
+
).run(le, anyAdmin, winnerName, winner.id);
|
|
948
|
+
}
|
|
949
|
+
return { groups: groups.length, merged };
|
|
767
950
|
}
|
|
768
951
|
|
|
769
952
|
// src/db/adapter.sqlite.ts
|
|
@@ -873,7 +1056,7 @@ async function initDb() {
|
|
|
873
1056
|
if (config.DB_DRIVER === "postgres") {
|
|
874
1057
|
const { Pool } = await import("pg");
|
|
875
1058
|
const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
|
|
876
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
1059
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-VCG45LMK.js");
|
|
877
1060
|
const pool = new Pool(buildPgConfig());
|
|
878
1061
|
adapter = new PostgresAdapter(pool);
|
|
879
1062
|
await runPostgresMigrations(adapter);
|
|
@@ -1,22 +1,68 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createVersion,
|
|
3
3
|
setApprovedVersion
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XNGOO6RI.js";
|
|
5
5
|
import {
|
|
6
6
|
buildTitleMap,
|
|
7
7
|
canUserApprove,
|
|
8
|
+
describeNode,
|
|
8
9
|
engineCache,
|
|
9
10
|
resolveStewardsForNode
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-PJTAUVD7.js";
|
|
11
12
|
import {
|
|
13
|
+
config,
|
|
12
14
|
getDb,
|
|
13
15
|
insertOrReplace,
|
|
14
16
|
nowExpr
|
|
15
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-PLAHCDQG.js";
|
|
16
18
|
|
|
17
19
|
// src/governance/review-service.ts
|
|
18
20
|
import { v4 as uuid } from "uuid";
|
|
19
21
|
|
|
22
|
+
// src/notify/slack.ts
|
|
23
|
+
var warnedOnce = false;
|
|
24
|
+
async function notifySlackForNest(nestId, message) {
|
|
25
|
+
if (!config.SLACK_WEBHOOK_URL) return;
|
|
26
|
+
let nestName = nestId;
|
|
27
|
+
try {
|
|
28
|
+
const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
|
|
29
|
+
nestId
|
|
30
|
+
]);
|
|
31
|
+
if (row?.name) nestName = row.name;
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
return notifySlack(`*[${nestName}]* ${message}`);
|
|
35
|
+
}
|
|
36
|
+
function escapeMrkdwn(s) {
|
|
37
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
38
|
+
}
|
|
39
|
+
async function notifySlack(text) {
|
|
40
|
+
const url = config.SLACK_WEBHOOK_URL;
|
|
41
|
+
if (!url) return;
|
|
42
|
+
try {
|
|
43
|
+
const ctrl = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => ctrl.abort(), 3e3);
|
|
45
|
+
try {
|
|
46
|
+
await fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify({ text: escapeMrkdwn(text) }),
|
|
50
|
+
signal: ctrl.signal
|
|
51
|
+
});
|
|
52
|
+
} finally {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (!warnedOnce) {
|
|
57
|
+
warnedOnce = true;
|
|
58
|
+
console.warn(
|
|
59
|
+
"[slack] notification failed (suppressing further warnings):",
|
|
60
|
+
err instanceof Error ? err.message : err
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
20
66
|
// src/governance/safe-publish.ts
|
|
21
67
|
import {
|
|
22
68
|
publishDocument,
|
|
@@ -73,6 +119,10 @@ async function submitForReview(params) {
|
|
|
73
119
|
"UPDATE node_versions SET status = 'pending_review' WHERE nest_id = ? AND node_id = ? AND version = ?",
|
|
74
120
|
[params.nestId, params.nodeId, params.version]
|
|
75
121
|
);
|
|
122
|
+
void notifySlackForNest(
|
|
123
|
+
params.nestId,
|
|
124
|
+
`:memo: Review requested on *${await describeNode(params.nestId, params.nodeId)}* (v${params.version}) by ${params.requestedBy}${params.note ? ` \u2014 "${params.note}"` : ""}`
|
|
125
|
+
);
|
|
76
126
|
return await getReviewRequest(id);
|
|
77
127
|
}
|
|
78
128
|
async function approve(params) {
|
|
@@ -148,6 +198,10 @@ async function approve(params) {
|
|
|
148
198
|
err
|
|
149
199
|
);
|
|
150
200
|
}
|
|
201
|
+
void notifySlackForNest(
|
|
202
|
+
params.nestId,
|
|
203
|
+
`:white_check_mark: *${await describeNode(params.nestId, params.nodeId)}* v${params.version} approved by ${params.approvedBy}`
|
|
204
|
+
);
|
|
151
205
|
return await getReviewRequest(pending.id);
|
|
152
206
|
}
|
|
153
207
|
async function reject(params) {
|
|
@@ -177,6 +231,10 @@ async function reject(params) {
|
|
|
177
231
|
"UPDATE node_versions SET status = 'rejected' WHERE nest_id = ? AND node_id = ? AND version = ?",
|
|
178
232
|
[params.nestId, params.nodeId, params.version]
|
|
179
233
|
);
|
|
234
|
+
void notifySlackForNest(
|
|
235
|
+
params.nestId,
|
|
236
|
+
`:x: *${await describeNode(params.nestId, params.nodeId)}* v${params.version} rejected by ${params.rejectedBy} \u2014 "${params.note}"`
|
|
237
|
+
);
|
|
180
238
|
return await getReviewRequest(pending.id);
|
|
181
239
|
}
|
|
182
240
|
async function cancelReview(params) {
|
|
@@ -293,6 +351,7 @@ function rowToReviewRequest(row) {
|
|
|
293
351
|
|
|
294
352
|
export {
|
|
295
353
|
safePublishDocument,
|
|
354
|
+
notifySlackForNest,
|
|
296
355
|
submitForReview,
|
|
297
356
|
approve,
|
|
298
357
|
reject,
|