@promptowl/contextnest-community 1.17.0 → 1.18.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/README.md +6 -2
- package/dist/{chunk-UXJSO54R.js → chunk-34UXRZXD.js} +59 -14
- package/dist/chunk-F66VCBLA.js +136 -0
- package/dist/{chunk-SNFGU5Q7.js → chunk-FUUJG6XP.js} +597 -326
- package/dist/{chunk-4YGJSUJC.js → chunk-HGN45I5P.js} +29 -21
- package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
- package/dist/chunk-OOBZG6BW.js +379 -0
- package/dist/{chunk-HWDPCPYT.js → chunk-QUZXRLUA.js} +1 -1
- package/dist/{chunk-YF4OFT2V.js → chunk-TQACHVVA.js} +35 -4
- package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
- package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
- package/dist/engine-34E3YNX7.js +14 -0
- package/dist/external-edit-service-6Q6SHEOF.js +33 -0
- package/dist/{grants-service-4NXPBYWD.js → grants-service-ME7HIVHU.js} +3 -3
- package/dist/index.js +496 -470
- package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
- package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
- package/dist/{review-service-3CCKDO5N.js → review-service-H2M5ACPQ.js} +7 -7
- package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-U4CKL7Y5.js} +8 -4
- package/dist/{version-service-EFZGGL3Y.js → version-service-JWEN5UUW.js} +6 -4
- package/dist/web3/assets/index-DONv5_Ni.css +1 -0
- package/dist/web3/assets/index-jSGrnboh.js +1065 -0
- package/dist/web3/index.html +2 -2
- package/package.json +3 -3
- package/dist/chunk-XRK6SQSC.js +0 -58
- package/dist/web3/assets/index-D1299AIf.css +0 -1
- package/dist/web3/assets/index-W6tEA3RK.js +0 -1046
|
@@ -1,20 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canUserAccess,
|
|
3
|
+
canUserApprove,
|
|
4
|
+
resolveNestWideRoles,
|
|
5
|
+
resolveStewardsForNode,
|
|
6
|
+
stewardCoverageForUser
|
|
7
|
+
} from "./chunk-TQACHVVA.js";
|
|
1
8
|
import {
|
|
2
9
|
grantCoversNode,
|
|
3
10
|
listUserGrants,
|
|
4
11
|
resolveNodeGrant
|
|
5
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-QUZXRLUA.js";
|
|
6
13
|
import {
|
|
7
14
|
createVersion,
|
|
8
15
|
getApprovedVersion,
|
|
9
16
|
getApprovedVersions,
|
|
10
17
|
getCurrentVersion,
|
|
11
18
|
setApprovedVersion
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import {
|
|
14
|
-
canUserAccess,
|
|
15
|
-
canUserApprove,
|
|
16
|
-
resolveStewardsForNode
|
|
17
|
-
} from "./chunk-YF4OFT2V.js";
|
|
19
|
+
} from "./chunk-34UXRZXD.js";
|
|
18
20
|
import {
|
|
19
21
|
buildDocContext,
|
|
20
22
|
buildTitleMap,
|
|
@@ -28,11 +30,12 @@ import {
|
|
|
28
30
|
notifyEmailForNest,
|
|
29
31
|
nowExpr,
|
|
30
32
|
opContext,
|
|
33
|
+
permissionLabel,
|
|
31
34
|
prettyFolderPath,
|
|
32
35
|
resolveNestPermission,
|
|
33
36
|
sendEmailToRecipient,
|
|
34
37
|
titleForNode
|
|
35
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-FUUJG6XP.js";
|
|
36
39
|
import {
|
|
37
40
|
ConflictError,
|
|
38
41
|
NotFoundError,
|
|
@@ -41,7 +44,7 @@ import {
|
|
|
41
44
|
import {
|
|
42
45
|
config,
|
|
43
46
|
getDb
|
|
44
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-HYSTFMFG.js";
|
|
45
48
|
|
|
46
49
|
// src/governance/review-service.ts
|
|
47
50
|
import { v4 as uuid3 } from "uuid";
|
|
@@ -169,7 +172,7 @@ async function notifyNestInvite(params) {
|
|
|
169
172
|
nestId,
|
|
170
173
|
inviteeEmail.toLowerCase(),
|
|
171
174
|
nestId,
|
|
172
|
-
`You were added to "${nestName2}" as ${permission}`,
|
|
175
|
+
`You were added to "${nestName2}" as ${permissionLabel(permission)}`,
|
|
173
176
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
174
177
|
]
|
|
175
178
|
);
|
|
@@ -180,10 +183,16 @@ async function notifyNestInvite(params) {
|
|
|
180
183
|
async function listNotifications(userEmail, opts = {}) {
|
|
181
184
|
const limit = Math.min(opts.limit ?? 50, 200);
|
|
182
185
|
return await getDb().all(
|
|
183
|
-
`SELECT id, nest_id, kind, subject_id, message, created_at, read_at
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
`SELECT n.id, n.nest_id, n.kind, n.subject_id, n.message, n.created_at, n.read_at,
|
|
187
|
+
COALESCE(
|
|
188
|
+
rr.node_id,
|
|
189
|
+
CASE WHEN dr.target_type = 'document' THEN dr.node_id END
|
|
190
|
+
) AS node_id
|
|
191
|
+
FROM notifications n
|
|
192
|
+
LEFT JOIN review_requests rr ON rr.id = n.subject_id
|
|
193
|
+
LEFT JOIN deletion_requests dr ON dr.id = n.subject_id
|
|
194
|
+
WHERE LOWER(n.user_email) = LOWER(?)${opts.unreadOnly ? " AND n.read_at IS NULL" : ""}
|
|
195
|
+
ORDER BY n.created_at DESC LIMIT ${limit}`,
|
|
187
196
|
[userEmail]
|
|
188
197
|
);
|
|
189
198
|
}
|
|
@@ -251,13 +260,12 @@ async function filterAccessible(nestId, userId, userEmail, nodes, approvedVersio
|
|
|
251
260
|
if (await resolveNestPermission(nestId, userId) !== "none") return nodes;
|
|
252
261
|
return nodes.filter((n) => grantCoversNode(grants, n.id));
|
|
253
262
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return accessible;
|
|
263
|
+
if ((await resolveNestWideRoles(nestId, userEmail)).length > 0) return nodes;
|
|
264
|
+
const coverage = await stewardCoverageForUser(nestId, userEmail);
|
|
265
|
+
if (coverage.nestWide) return nodes;
|
|
266
|
+
return nodes.filter(
|
|
267
|
+
(n) => coverage.nodeIds.has(n.id) || grantCoversNode(grants, n.id)
|
|
268
|
+
);
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
// src/workflow/edge-type-routes.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ANON_USER_ID
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YB3LKF7U.js";
|
|
4
4
|
|
|
5
5
|
// src/db/client.ts
|
|
6
6
|
import Database from "better-sqlite3";
|
|
@@ -741,6 +741,9 @@ function runMigrations(db) {
|
|
|
741
741
|
if (!nestCols.includes("prime_tags")) {
|
|
742
742
|
db.exec("ALTER TABLE nests ADD COLUMN prime_tags TEXT NOT NULL DEFAULT 'prime-document'");
|
|
743
743
|
}
|
|
744
|
+
if (!nestCols.includes("index_synced_at")) {
|
|
745
|
+
db.exec("ALTER TABLE nests ADD COLUMN index_synced_at TEXT");
|
|
746
|
+
}
|
|
744
747
|
const userCols = db.prepare("PRAGMA table_info(users)").all().map((c) => c.name);
|
|
745
748
|
if (!userCols.includes("is_admin")) {
|
|
746
749
|
db.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0");
|
|
@@ -1531,6 +1534,44 @@ function runMigrations(db) {
|
|
|
1531
1534
|
`);
|
|
1532
1535
|
recordMigration("034_versions_author_status_index");
|
|
1533
1536
|
}
|
|
1537
|
+
if (!hasMigration("035_multiple_api_keys_per_user")) {
|
|
1538
|
+
db.exec(`DROP INDEX IF EXISTS idx_api_keys_user_unique;`);
|
|
1539
|
+
recordMigration("035_multiple_api_keys_per_user");
|
|
1540
|
+
}
|
|
1541
|
+
if (!hasMigration("036_node_index")) {
|
|
1542
|
+
db.transaction(() => {
|
|
1543
|
+
db.exec(`
|
|
1544
|
+
CREATE TABLE IF NOT EXISTS node_index (
|
|
1545
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1546
|
+
node_id TEXT NOT NULL,
|
|
1547
|
+
folder TEXT NOT NULL DEFAULT '',
|
|
1548
|
+
title TEXT NOT NULL DEFAULT '',
|
|
1549
|
+
type TEXT NOT NULL DEFAULT 'document',
|
|
1550
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
1551
|
+
updated_at TEXT,
|
|
1552
|
+
frontmatter_json TEXT NOT NULL,
|
|
1553
|
+
PRIMARY KEY (nest_id, node_id)
|
|
1554
|
+
);
|
|
1555
|
+
CREATE INDEX IF NOT EXISTS idx_node_index_folder
|
|
1556
|
+
ON node_index(nest_id, folder);
|
|
1557
|
+
|
|
1558
|
+
-- Which documents have an external edit staged against them.
|
|
1559
|
+
--
|
|
1560
|
+
-- Its own table rather than a node_index column: node_index rows are
|
|
1561
|
+
-- upserted with INSERT OR REPLACE on every document write, which
|
|
1562
|
+
-- resets any column the write does not carry \u2014 and a suggestion's
|
|
1563
|
+
-- existence has nothing to do with the document write that would
|
|
1564
|
+
-- clobber it. Kept in step at the three points that stage, approve or
|
|
1565
|
+
-- reject a suggestion, and refreshed wholesale by a rebuild.
|
|
1566
|
+
CREATE TABLE IF NOT EXISTS node_suggestions (
|
|
1567
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1568
|
+
node_id TEXT NOT NULL,
|
|
1569
|
+
PRIMARY KEY (nest_id, node_id)
|
|
1570
|
+
);
|
|
1571
|
+
`);
|
|
1572
|
+
})();
|
|
1573
|
+
recordMigration("036_node_index");
|
|
1574
|
+
}
|
|
1534
1575
|
}
|
|
1535
1576
|
function mergeCaseCollidingUsers(db) {
|
|
1536
1577
|
const groups = db.prepare(
|
|
@@ -1748,7 +1789,7 @@ async function initDb() {
|
|
|
1748
1789
|
if (config.DB_DRIVER === "postgres") {
|
|
1749
1790
|
const { Pool } = await import("pg");
|
|
1750
1791
|
const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
|
|
1751
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
1792
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-5VI4RDTS.js");
|
|
1752
1793
|
const pool = new Pool(buildPgConfig());
|
|
1753
1794
|
adapter = new PostgresAdapter(pool);
|
|
1754
1795
|
await runPostgresMigrations(adapter);
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import {
|
|
2
|
+
engineCache,
|
|
3
|
+
indexedNodeIds,
|
|
4
|
+
markIndexStale,
|
|
5
|
+
resolveNestPath,
|
|
6
|
+
setSuggestionFlag
|
|
7
|
+
} from "./chunk-FUUJG6XP.js";
|
|
8
|
+
import {
|
|
9
|
+
getDb
|
|
10
|
+
} from "./chunk-HYSTFMFG.js";
|
|
11
|
+
|
|
12
|
+
// src/governance/external-edit-service.ts
|
|
13
|
+
import { readFile, readdir } from "fs/promises";
|
|
14
|
+
import { join, relative, sep } from "path";
|
|
15
|
+
import {
|
|
16
|
+
detectDrift,
|
|
17
|
+
stageSuggestion,
|
|
18
|
+
ChainEventLog,
|
|
19
|
+
approveSuggestion,
|
|
20
|
+
rejectSuggestion,
|
|
21
|
+
listSuggestions,
|
|
22
|
+
readSuggestion,
|
|
23
|
+
parseDocument,
|
|
24
|
+
VersionManager,
|
|
25
|
+
computeContentHash,
|
|
26
|
+
getChecksumContent
|
|
27
|
+
} from "@promptowl/contextnest-engine";
|
|
28
|
+
var communityRbac = {
|
|
29
|
+
isCzar: () => false,
|
|
30
|
+
canIngest: () => true,
|
|
31
|
+
isDocOwner: () => true
|
|
32
|
+
};
|
|
33
|
+
function docPath(nestId, documentId) {
|
|
34
|
+
return join(resolveNestPath(nestId), `${documentId}.md`);
|
|
35
|
+
}
|
|
36
|
+
async function readRaw(nestId, documentId) {
|
|
37
|
+
try {
|
|
38
|
+
return await readFile(docPath(nestId, documentId), "utf-8");
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function loadChainHead(storage, documentId) {
|
|
44
|
+
const history = await storage.readHistory(documentId);
|
|
45
|
+
if (!history || history.versions.length === 0) return null;
|
|
46
|
+
const latest = history.versions[history.versions.length - 1];
|
|
47
|
+
try {
|
|
48
|
+
const content = await new VersionManager(storage).reconstructVersion(
|
|
49
|
+
documentId,
|
|
50
|
+
latest.version
|
|
51
|
+
);
|
|
52
|
+
return { version: latest.version, content };
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function loadLatestApprovedNode(nestId, documentId) {
|
|
58
|
+
const { storage } = await engineCache.get(nestId);
|
|
59
|
+
const head = await loadChainHead(storage, documentId);
|
|
60
|
+
if (!head) return null;
|
|
61
|
+
return parseDocument(docPath(nestId, documentId), head.content, documentId);
|
|
62
|
+
}
|
|
63
|
+
async function bodyMatchesLatestVersion(storage, documentId, liveBodyHash) {
|
|
64
|
+
const head = await loadChainHead(storage, documentId);
|
|
65
|
+
if (!head) return false;
|
|
66
|
+
const headBodyHash = computeContentHash(getChecksumContent(head.content));
|
|
67
|
+
return headBodyHash === liveBodyHash;
|
|
68
|
+
}
|
|
69
|
+
async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner") {
|
|
70
|
+
const res = await scanDocumentForDriftInternal(nestId, documentId, actor);
|
|
71
|
+
return res?.meta ?? null;
|
|
72
|
+
}
|
|
73
|
+
async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
74
|
+
const { storage } = await engineCache.get(nestId);
|
|
75
|
+
const node = await storage.readDocument(documentId).catch(() => null);
|
|
76
|
+
if (!node) return null;
|
|
77
|
+
const raw = await readRaw(nestId, documentId);
|
|
78
|
+
if (raw == null) return null;
|
|
79
|
+
const drift = detectDrift(raw, node.frontmatter.checksum);
|
|
80
|
+
if (!drift.drifted) return null;
|
|
81
|
+
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const approved = await loadChainHead(storage, documentId);
|
|
85
|
+
if (!approved) return null;
|
|
86
|
+
const existing = await listSuggestions(storage, documentId);
|
|
87
|
+
const dup = existing.find((s) => s.proposed_hash === drift.actualHash);
|
|
88
|
+
if (dup) {
|
|
89
|
+
await setSuggestionFlag(nestId, documentId, true);
|
|
90
|
+
return { meta: dup, created: false };
|
|
91
|
+
}
|
|
92
|
+
const result = await stageSuggestion({
|
|
93
|
+
storage,
|
|
94
|
+
documentId,
|
|
95
|
+
approvedRawContent: approved.content,
|
|
96
|
+
proposedRawContent: raw,
|
|
97
|
+
source: "out-of-band-edit",
|
|
98
|
+
actor,
|
|
99
|
+
docTier: "standard"
|
|
100
|
+
});
|
|
101
|
+
await setSuggestionFlag(nestId, documentId, true);
|
|
102
|
+
return { meta: result.meta, created: true };
|
|
103
|
+
}
|
|
104
|
+
async function refreshSuggestionFlag(nestId, storage, documentId) {
|
|
105
|
+
try {
|
|
106
|
+
const remaining = await listSuggestions(storage, documentId);
|
|
107
|
+
await setSuggestionFlag(nestId, documentId, remaining.length > 0);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error("[external-edit] flag refresh failed", nestId, documentId, err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function scanNestForDrift(nestId, actor = "system:scanner") {
|
|
113
|
+
const { storage, dropDiscoveryCache } = await engineCache.get(nestId);
|
|
114
|
+
dropDiscoveryCache();
|
|
115
|
+
const docs = await storage.discoverDocuments();
|
|
116
|
+
const results = await Promise.all(
|
|
117
|
+
docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
|
|
118
|
+
);
|
|
119
|
+
const staged = results.filter((r) => r?.created).length;
|
|
120
|
+
const onDisk = await storage.discoverDocuments({ includeRetired: true });
|
|
121
|
+
const indexed = await indexedNodeIds(nestId);
|
|
122
|
+
const membershipMoved = indexed.size !== onDisk.length || onDisk.some((d) => !indexed.has(d.id));
|
|
123
|
+
if (staged > 0 || membershipMoved) await markIndexStale(nestId);
|
|
124
|
+
return { scanned: docs.length, staged };
|
|
125
|
+
}
|
|
126
|
+
async function documentsWithSuggestions(nestId) {
|
|
127
|
+
const root = resolveNestPath(nestId);
|
|
128
|
+
const ids = /* @__PURE__ */ new Set();
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err?.code !== "ENOENT") {
|
|
134
|
+
console.error("[external-edit] suggestion scan failed", nestId, err);
|
|
135
|
+
}
|
|
136
|
+
return ids;
|
|
137
|
+
}
|
|
138
|
+
for (const e of entries) {
|
|
139
|
+
if (!e.isFile() || !e.name.endsWith(".meta.yaml")) continue;
|
|
140
|
+
const dir = e.parentPath ?? e.path ?? root;
|
|
141
|
+
const rel = relative(root, dir).split(sep).join("/");
|
|
142
|
+
const segs = rel.split("/");
|
|
143
|
+
const marker = segs.lastIndexOf("_suggestions");
|
|
144
|
+
if (marker === -1 || segs.length !== marker + 2) continue;
|
|
145
|
+
ids.add([...segs.slice(0, marker), segs[marker + 1]].filter(Boolean).join("/"));
|
|
146
|
+
}
|
|
147
|
+
return ids;
|
|
148
|
+
}
|
|
149
|
+
async function getPendingChange(nestId, documentId) {
|
|
150
|
+
const { storage } = await engineCache.get(nestId);
|
|
151
|
+
const list = await listSuggestions(storage, documentId);
|
|
152
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
153
|
+
const meta = list[i];
|
|
154
|
+
if (await bodyMatchesLatestVersion(storage, documentId, meta.proposed_hash)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
suggestion_id: meta.suggestion_id,
|
|
159
|
+
detected_at: meta.detected_at,
|
|
160
|
+
source: meta.source,
|
|
161
|
+
proposed_hash: meta.proposed_hash
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
async function listNestExternalEdits(nestId) {
|
|
167
|
+
const { storage } = await engineCache.get(nestId);
|
|
168
|
+
const docs = await storage.discoverDocuments();
|
|
169
|
+
const lists = await Promise.all(
|
|
170
|
+
docs.map(async (doc) => {
|
|
171
|
+
const metas = await listSuggestions(storage, doc.id);
|
|
172
|
+
const fresh = [];
|
|
173
|
+
for (const meta of metas) {
|
|
174
|
+
if (await bodyMatchesLatestVersion(storage, doc.id, meta.proposed_hash)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
fresh.push(meta);
|
|
178
|
+
}
|
|
179
|
+
return fresh;
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
const entries = lists.flat().map((meta) => ({
|
|
183
|
+
suggestion_id: meta.suggestion_id,
|
|
184
|
+
nest_id: nestId,
|
|
185
|
+
document_id: meta.document_id,
|
|
186
|
+
source: meta.source,
|
|
187
|
+
detected_at: meta.detected_at,
|
|
188
|
+
actor: meta.actor,
|
|
189
|
+
target_hash: meta.target_hash,
|
|
190
|
+
proposed_hash: meta.proposed_hash,
|
|
191
|
+
note: meta.note
|
|
192
|
+
}));
|
|
193
|
+
return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
|
|
194
|
+
}
|
|
195
|
+
async function getExternalEditDetail(nestId, documentId, suggestionId) {
|
|
196
|
+
const { storage } = await engineCache.get(nestId);
|
|
197
|
+
const found = await readSuggestion(storage, documentId, suggestionId);
|
|
198
|
+
if (!found) return null;
|
|
199
|
+
return {
|
|
200
|
+
suggestion_id: found.meta.suggestion_id,
|
|
201
|
+
nest_id: nestId,
|
|
202
|
+
document_id: found.meta.document_id,
|
|
203
|
+
source: found.meta.source,
|
|
204
|
+
detected_at: found.meta.detected_at,
|
|
205
|
+
actor: found.meta.actor,
|
|
206
|
+
target_hash: found.meta.target_hash,
|
|
207
|
+
proposed_hash: found.meta.proposed_hash,
|
|
208
|
+
note: found.meta.note,
|
|
209
|
+
patch: found.patch
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function recordVerdict(nestId, event) {
|
|
213
|
+
try {
|
|
214
|
+
const { storage } = await engineCache.get(nestId);
|
|
215
|
+
await new ChainEventLog(storage).append(event);
|
|
216
|
+
return true;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(
|
|
219
|
+
`[external-edit] failed to record verdict ${event.event_id} for nest ${nestId}:`,
|
|
220
|
+
err
|
|
221
|
+
);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function listExternalEditVerdicts(nestId, documentId) {
|
|
226
|
+
try {
|
|
227
|
+
const { storage } = await engineCache.get(nestId);
|
|
228
|
+
const events = await new ChainEventLog(storage).readByDocument(documentId);
|
|
229
|
+
return events.flatMap((e) => {
|
|
230
|
+
const meta = e.action_metadata || {};
|
|
231
|
+
if (typeof meta.suggestion_id !== "string") return [];
|
|
232
|
+
const reason = meta.rejection_reason;
|
|
233
|
+
const comment = meta.approval_comment;
|
|
234
|
+
return [
|
|
235
|
+
{
|
|
236
|
+
suggestion_id: meta.suggestion_id,
|
|
237
|
+
status: typeof reason === "string" ? "rejected" : "approved",
|
|
238
|
+
actor: e.actor,
|
|
239
|
+
at: e.timestamp,
|
|
240
|
+
note: typeof reason === "string" ? reason : typeof comment === "string" ? comment : void 0,
|
|
241
|
+
source: typeof meta.source === "string" ? meta.source : void 0
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error(
|
|
247
|
+
`[external-edit] failed to read verdicts for ${nestId}/${documentId}:`,
|
|
248
|
+
err
|
|
249
|
+
);
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function mirrorVersion(input) {
|
|
254
|
+
const { storage } = await engineCache.get(input.nestId);
|
|
255
|
+
const node = await storage.readDocument(input.documentId);
|
|
256
|
+
const { upsertVersion, setApprovedVersion } = await import("./version-service-JWEN5UUW.js");
|
|
257
|
+
await upsertVersion({
|
|
258
|
+
nestId: input.nestId,
|
|
259
|
+
nodeId: input.documentId,
|
|
260
|
+
version: input.version,
|
|
261
|
+
content: node.body || "",
|
|
262
|
+
author: input.actor,
|
|
263
|
+
status: "published",
|
|
264
|
+
tags: node.frontmatter.tags || [],
|
|
265
|
+
changeNote: input.note
|
|
266
|
+
});
|
|
267
|
+
await setApprovedVersion(
|
|
268
|
+
input.nestId,
|
|
269
|
+
input.documentId,
|
|
270
|
+
input.version,
|
|
271
|
+
input.actor
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
async function approveExternalEdit(input) {
|
|
275
|
+
const { storage } = await engineCache.get(input.nestId);
|
|
276
|
+
let result;
|
|
277
|
+
try {
|
|
278
|
+
result = await approveSuggestion({
|
|
279
|
+
storage,
|
|
280
|
+
rbac: communityRbac,
|
|
281
|
+
documentId: input.documentId,
|
|
282
|
+
suggestionId: input.suggestionId,
|
|
283
|
+
actor: input.actor,
|
|
284
|
+
zone: "default",
|
|
285
|
+
comment: input.comment
|
|
286
|
+
});
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error(
|
|
289
|
+
`[external-edit] approveSuggestion failed for ${input.nestId}/${input.documentId} suggestion=${input.suggestionId}:`,
|
|
290
|
+
err
|
|
291
|
+
);
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
await mirrorVersion({
|
|
296
|
+
nestId: input.nestId,
|
|
297
|
+
documentId: input.documentId,
|
|
298
|
+
version: result.versionEntry.version,
|
|
299
|
+
actor: input.actor,
|
|
300
|
+
note: input.comment ? `External edit approved: ${input.comment}` : "External edit approved"
|
|
301
|
+
});
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.error(
|
|
304
|
+
`[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
|
|
305
|
+
err
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
309
|
+
await refreshSuggestionFlag(input.nestId, storage, input.documentId);
|
|
310
|
+
return { ...result, historyRecorded };
|
|
311
|
+
}
|
|
312
|
+
async function rejectExternalEdit(input) {
|
|
313
|
+
const { storage } = await engineCache.get(input.nestId);
|
|
314
|
+
const result = await rejectSuggestion({
|
|
315
|
+
storage,
|
|
316
|
+
rbac: communityRbac,
|
|
317
|
+
documentId: input.documentId,
|
|
318
|
+
suggestionId: input.suggestionId,
|
|
319
|
+
actor: input.actor,
|
|
320
|
+
zone: "default",
|
|
321
|
+
reason: input.reason
|
|
322
|
+
});
|
|
323
|
+
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
324
|
+
try {
|
|
325
|
+
const approved = await loadChainHead(storage, input.documentId);
|
|
326
|
+
if (approved) {
|
|
327
|
+
await storage.writeDocument(input.documentId, approved.content);
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error(
|
|
331
|
+
`[external-edit] revert-on-reject failed for ${input.nestId}/${input.documentId}:`,
|
|
332
|
+
err
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
await refreshSuggestionFlag(input.nestId, storage, input.documentId);
|
|
336
|
+
return { ...result, historyRecorded };
|
|
337
|
+
}
|
|
338
|
+
var scannerTimer = null;
|
|
339
|
+
async function scanAllNests() {
|
|
340
|
+
const db = getDb();
|
|
341
|
+
const rows = await db.all("SELECT id FROM nests");
|
|
342
|
+
await Promise.all(
|
|
343
|
+
rows.map(
|
|
344
|
+
({ id }) => scanNestForDrift(id).catch(
|
|
345
|
+
(err) => console.error(`[external-edit] scan failed for nest ${id}:`, err)
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
function startDriftScanner(intervalMs = 3e4) {
|
|
351
|
+
if (scannerTimer) return;
|
|
352
|
+
scannerTimer = setInterval(() => {
|
|
353
|
+
scanAllNests().catch(
|
|
354
|
+
(err) => console.error("[external-edit] scanner tick failed:", err)
|
|
355
|
+
);
|
|
356
|
+
}, intervalMs);
|
|
357
|
+
scannerTimer.unref?.();
|
|
358
|
+
}
|
|
359
|
+
function stopDriftScanner() {
|
|
360
|
+
if (scannerTimer) {
|
|
361
|
+
clearInterval(scannerTimer);
|
|
362
|
+
scannerTimer = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export {
|
|
367
|
+
loadLatestApprovedNode,
|
|
368
|
+
scanDocumentForDrift,
|
|
369
|
+
scanNestForDrift,
|
|
370
|
+
documentsWithSuggestions,
|
|
371
|
+
getPendingChange,
|
|
372
|
+
listNestExternalEdits,
|
|
373
|
+
getExternalEditDetail,
|
|
374
|
+
listExternalEditVerdicts,
|
|
375
|
+
approveExternalEdit,
|
|
376
|
+
rejectExternalEdit,
|
|
377
|
+
startDriftScanner,
|
|
378
|
+
stopDriftScanner
|
|
379
|
+
};
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveNestPermission,
|
|
12
12
|
resolveTeamRolesForUser,
|
|
13
13
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-FUUJG6XP.js";
|
|
15
15
|
import {
|
|
16
16
|
ConflictError,
|
|
17
17
|
ValidationError
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
config,
|
|
21
21
|
getDb,
|
|
22
22
|
isEmailish
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-HYSTFMFG.js";
|
|
24
24
|
|
|
25
25
|
// src/governance/stewardship-service.ts
|
|
26
26
|
import { v4 as uuid } from "uuid";
|
|
@@ -229,7 +229,7 @@ async function createStewardRecord(params) {
|
|
|
229
229
|
);
|
|
230
230
|
let userId = userRow?.id;
|
|
231
231
|
if (!userId) {
|
|
232
|
-
const { hashPassword } = await import("./keys-
|
|
232
|
+
const { hashPassword } = await import("./keys-CTOGAG3W.js");
|
|
233
233
|
userId = uuid();
|
|
234
234
|
await db.run(
|
|
235
235
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
@@ -389,7 +389,7 @@ async function resolveGrantRoles(nestId, userEmail, userId) {
|
|
|
389
389
|
)
|
|
390
390
|
];
|
|
391
391
|
}
|
|
392
|
-
async function
|
|
392
|
+
async function resolveNestWideRoles(nestId, userEmail) {
|
|
393
393
|
const roles = /* @__PURE__ */ new Set();
|
|
394
394
|
if (isSuperAdmin(userEmail)) roles.add("admin");
|
|
395
395
|
const owner = await getNestOwnerEmail(nestId);
|
|
@@ -404,6 +404,35 @@ async function resolveUserRoles(nestId, userEmail, opts) {
|
|
|
404
404
|
roles.add(role);
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
|
+
return [...roles];
|
|
408
|
+
}
|
|
409
|
+
async function stewardCoverageForUser(nestId, userEmail) {
|
|
410
|
+
const db = getDb();
|
|
411
|
+
const rows = await db.all(
|
|
412
|
+
`SELECT scope, node_pattern, tag_name FROM stewards
|
|
413
|
+
WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)`,
|
|
414
|
+
[nestId, userEmail]
|
|
415
|
+
);
|
|
416
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
417
|
+
const tags = [];
|
|
418
|
+
for (const r of rows) {
|
|
419
|
+
if (r.scope === "nest") return { nestWide: true, nodeIds };
|
|
420
|
+
if (r.scope === "document" && r.node_pattern) nodeIds.add(r.node_pattern);
|
|
421
|
+
else if (r.scope === "tag" && r.tag_name) tags.push(r.tag_name);
|
|
422
|
+
}
|
|
423
|
+
if (tags.length) {
|
|
424
|
+
const placeholders = tags.map(() => "?").join(", ");
|
|
425
|
+
const tagged = await db.all(
|
|
426
|
+
`SELECT DISTINCT node_id FROM node_tag_index
|
|
427
|
+
WHERE nest_id = ? AND tag_name IN (${placeholders})`,
|
|
428
|
+
[nestId, ...tags]
|
|
429
|
+
);
|
|
430
|
+
for (const t of tagged) nodeIds.add(t.node_id);
|
|
431
|
+
}
|
|
432
|
+
return { nestWide: false, nodeIds };
|
|
433
|
+
}
|
|
434
|
+
async function resolveUserRoles(nestId, userEmail, opts) {
|
|
435
|
+
const roles = new Set(await resolveNestWideRoles(nestId, userEmail));
|
|
407
436
|
const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
|
|
408
437
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
|
|
409
438
|
).map((r) => r.steward.role) : await getStewardRolesForUser(nestId, userEmail);
|
|
@@ -614,6 +643,8 @@ export {
|
|
|
614
643
|
getStewardsForUser,
|
|
615
644
|
getCollaboratorRole,
|
|
616
645
|
resolveGrantRoles,
|
|
646
|
+
resolveNestWideRoles,
|
|
647
|
+
stewardCoverageForUser,
|
|
617
648
|
resolveUserRoles,
|
|
618
649
|
canManageStewards,
|
|
619
650
|
canCreateInNest,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
engineApi,
|
|
3
|
+
engineCache,
|
|
4
|
+
opContext
|
|
5
|
+
} from "./chunk-FUUJG6XP.js";
|
|
6
|
+
import "./chunk-GUNJTORH.js";
|
|
7
|
+
import "./chunk-HYSTFMFG.js";
|
|
8
|
+
import "./chunk-YB3LKF7U.js";
|
|
9
|
+
import "./chunk-FRQJWGN3.js";
|
|
10
|
+
export {
|
|
11
|
+
engineApi,
|
|
12
|
+
engineCache,
|
|
13
|
+
opContext
|
|
14
|
+
};
|