@promptowl/contextnest-community 1.15.0 → 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/CONFIGURATION.md +415 -414
- package/LICENSE.md +142 -142
- package/README.md +164 -169
- package/dist/{chunk-C4U75IUR.js → chunk-4YGJSUJC.js} +52 -59
- package/dist/{chunk-5IAKUUZF.js → chunk-HWDPCPYT.js} +11 -10
- package/dist/{chunk-BCFKLY4H.js → chunk-JMJLNEXE.js} +24 -0
- package/dist/{chunk-7BOG3S5H.js → chunk-SNFGU5Q7.js} +244 -162
- package/dist/{chunk-JWVZ3T35.js → chunk-UXJSO54R.js} +21 -2
- package/dist/{chunk-GWDOLYFJ.js → chunk-YF4OFT2V.js} +2 -2
- package/dist/{client-NFUHPLLM.js → client-OBS5HOTA.js} +1 -1
- package/dist/{grants-service-O4AO326A.js → grants-service-4NXPBYWD.js} +4 -4
- package/dist/index.js +1160 -544
- package/dist/{review-service-JUAFYB73.js → review-service-3CCKDO5N.js} +10 -8
- package/dist/{stewardship-service-FHJ2RBKN.js → stewardship-service-OHTTZFND.js} +3 -3
- package/dist/{version-service-ECYCBVSE.js → version-service-EFZGGL3Y.js} +7 -3
- package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
- package/dist/web3/assets/index-D1299AIf.css +1 -0
- package/dist/web3/assets/index-W6tEA3RK.js +1046 -0
- package/dist/web3/index.html +14 -14
- package/package.json +165 -164
- package/dist/web3/assets/index-BaMJ4tEz.css +0 -1
- package/dist/web3/assets/index-D950G4hR.js +0 -1052
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
getDb,
|
|
10
10
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-JMJLNEXE.js";
|
|
12
12
|
import {
|
|
13
13
|
ANON_USER_ID
|
|
14
14
|
} from "./chunk-SLTQACJW.js";
|
|
@@ -600,7 +600,7 @@ var STATUS_META = {
|
|
|
600
600
|
},
|
|
601
601
|
deletion_declined: {
|
|
602
602
|
subjectWord: "kept",
|
|
603
|
-
label: "Deletion
|
|
603
|
+
label: "Deletion rejected",
|
|
604
604
|
color: "#d97706"
|
|
605
605
|
},
|
|
606
606
|
deletion_deleted: {
|
|
@@ -683,16 +683,16 @@ var TEMPLATES = {
|
|
|
683
683
|
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
684
684
|
detailsHeader: "Document details",
|
|
685
685
|
linkLabel: "Review the request",
|
|
686
|
-
lineT: (x) => `${x.title} has been flagged for deletion${x.byT}${x.noteT}. Delete it or
|
|
687
|
-
lineH: (x) => `${x.doc} has been flagged for deletion${x.byH}${x.noteH}. Delete it or
|
|
686
|
+
lineT: (x) => `${x.title} has been flagged for deletion${x.byT}${x.noteT}. Delete it or reject the request from the nest.`,
|
|
687
|
+
lineH: (x) => `${x.doc} has been flagged for deletion${x.byH}${x.noteH}. Delete it or reject the request from the nest.`,
|
|
688
688
|
rows: governanceRows
|
|
689
689
|
},
|
|
690
690
|
deletion_declined: {
|
|
691
|
-
heading: (x) => `Your deletion request for ${x.title} was
|
|
691
|
+
heading: (x) => `Your deletion request for ${x.title} was rejected`,
|
|
692
692
|
detailsHeader: "Document details",
|
|
693
693
|
linkLabel: "Open document",
|
|
694
|
-
lineT: (x) => `Your request to delete ${x.title} was
|
|
695
|
-
lineH: (x) => `Your request to delete ${x.doc} was
|
|
694
|
+
lineT: (x) => `Your request to delete ${x.title} was rejected${x.byT}${x.noteT}. The document stays.`,
|
|
695
|
+
lineH: (x) => `Your request to delete ${x.doc} was rejected${x.byH}${x.noteH}. The document stays.`,
|
|
696
696
|
rows: governanceRows
|
|
697
697
|
},
|
|
698
698
|
deletion_deleted: {
|
|
@@ -973,74 +973,17 @@ async function sendEmailToRecipient(to, details) {
|
|
|
973
973
|
}
|
|
974
974
|
|
|
975
975
|
// src/nests/service.ts
|
|
976
|
-
import {
|
|
976
|
+
import { mkdirSync } from "fs";
|
|
977
977
|
import { v4 as uuid2 } from "uuid";
|
|
978
978
|
import { NestStorage as NestStorage3 } from "@promptowl/contextnest-engine";
|
|
979
979
|
|
|
980
|
-
// src/fs/vault-import.ts
|
|
981
|
-
import { rmSync } from "fs";
|
|
982
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
983
|
-
import { join as join2, dirname, isAbsolute } from "path";
|
|
984
|
-
function isSkippedSegment(segment) {
|
|
985
|
-
return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
|
|
986
|
-
}
|
|
987
|
-
var ALLOWED_DOT_DIRS = /* @__PURE__ */ new Set([".versions", ".context"]);
|
|
988
|
-
function safeSegments(rel) {
|
|
989
|
-
if (!rel || isAbsolute(rel)) return null;
|
|
990
|
-
const segs = rel.split(/[\\/]/).filter(Boolean);
|
|
991
|
-
if (!segs.length) return null;
|
|
992
|
-
for (const s of segs) {
|
|
993
|
-
if (s === "..") return null;
|
|
994
|
-
if (!ALLOWED_DOT_DIRS.has(s) && isSkippedSegment(s)) return null;
|
|
995
|
-
}
|
|
996
|
-
return segs;
|
|
997
|
-
}
|
|
998
|
-
var WRITE_CONCURRENCY = 32;
|
|
999
|
-
async function writeImportedFiles(dest, files) {
|
|
1000
|
-
const targets = [];
|
|
1001
|
-
const skipped = [];
|
|
1002
|
-
for (const f of files) {
|
|
1003
|
-
const segs = safeSegments(f?.path || "");
|
|
1004
|
-
if (!segs) {
|
|
1005
|
-
skipped.push(f?.path || "(empty)");
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
|
-
targets.push({ full: join2(dest, ...segs), content: f.content ?? "" });
|
|
1009
|
-
}
|
|
1010
|
-
const dirs = new Set(targets.map((t) => dirname(t.full)));
|
|
1011
|
-
await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })));
|
|
1012
|
-
let written = 0;
|
|
1013
|
-
for (let i = 0; i < targets.length; i += WRITE_CONCURRENCY) {
|
|
1014
|
-
const batch = targets.slice(i, i + WRITE_CONCURRENCY);
|
|
1015
|
-
await Promise.all(
|
|
1016
|
-
batch.map(async (t) => {
|
|
1017
|
-
await writeFile(t.full, t.content, "utf-8");
|
|
1018
|
-
written++;
|
|
1019
|
-
})
|
|
1020
|
-
);
|
|
1021
|
-
}
|
|
1022
|
-
console.log(
|
|
1023
|
-
`[import] writeImportedFiles: received=${files.length} written=${written} skipped=${skipped.length}`
|
|
1024
|
-
);
|
|
1025
|
-
if (skipped.length) {
|
|
1026
|
-
console.log("[import] skipped (unsafe/hidden path):", skipped.join(", "));
|
|
1027
|
-
}
|
|
1028
|
-
return written;
|
|
1029
|
-
}
|
|
1030
|
-
function discardImportDir(dest) {
|
|
1031
|
-
try {
|
|
1032
|
-
rmSync(dest, { recursive: true, force: true });
|
|
1033
|
-
} catch {
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
980
|
// src/shared/paths.ts
|
|
1038
|
-
import { join as
|
|
981
|
+
import { join as join2 } from "path";
|
|
1039
982
|
function nestStorageRoot() {
|
|
1040
983
|
return config.NEST_STORAGE_ROOT;
|
|
1041
984
|
}
|
|
1042
985
|
function resolveNestPath(nestId) {
|
|
1043
|
-
return
|
|
986
|
+
return join2(nestStorageRoot(), nestId);
|
|
1044
987
|
}
|
|
1045
988
|
|
|
1046
989
|
// src/governance/teams-service.ts
|
|
@@ -1050,13 +993,32 @@ import { v4 as uuid } from "uuid";
|
|
|
1050
993
|
import {
|
|
1051
994
|
NestStorage as NestStorage2,
|
|
1052
995
|
GraphQueryEngine,
|
|
1053
|
-
VersionManager
|
|
996
|
+
VersionManager,
|
|
997
|
+
DocumentNotFoundError
|
|
1054
998
|
} from "@promptowl/contextnest-engine";
|
|
999
|
+
import { createEngineApi } from "@promptowl/contextnest-engine/api";
|
|
1055
1000
|
|
|
1056
1001
|
// src/nodes/flat-storage.ts
|
|
1057
|
-
import {
|
|
1058
|
-
import { join as
|
|
1002
|
+
import { readdir } from "fs/promises";
|
|
1003
|
+
import { join as join3, relative, sep } from "path";
|
|
1059
1004
|
import { NestStorage } from "@promptowl/contextnest-engine";
|
|
1005
|
+
|
|
1006
|
+
// src/fs/vault-import.ts
|
|
1007
|
+
function isSkippedSegment(segment) {
|
|
1008
|
+
return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/shared/batch.ts
|
|
1012
|
+
var IO_CONCURRENCY = 32;
|
|
1013
|
+
async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
|
|
1014
|
+
const out = [];
|
|
1015
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
1016
|
+
out.push(...await Promise.all(items.slice(i, i + concurrency).map(fn)));
|
|
1017
|
+
}
|
|
1018
|
+
return out;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/nodes/flat-storage.ts
|
|
1060
1022
|
var META_FILES = /* @__PURE__ */ new Set([
|
|
1061
1023
|
"INDEX.md",
|
|
1062
1024
|
"CONTEXT.md",
|
|
@@ -1070,7 +1032,7 @@ var FlatNestStorage = class extends NestStorage {
|
|
|
1070
1032
|
const root = this.root;
|
|
1071
1033
|
let entries;
|
|
1072
1034
|
try {
|
|
1073
|
-
entries =
|
|
1035
|
+
entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
1074
1036
|
} catch (err) {
|
|
1075
1037
|
console.error("[FlatNestStorage] cannot read folder", root, err);
|
|
1076
1038
|
return [];
|
|
@@ -1080,33 +1042,121 @@ var FlatNestStorage = class extends NestStorage {
|
|
|
1080
1042
|
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
1081
1043
|
if (META_FILES.has(e.name)) continue;
|
|
1082
1044
|
const dir = e.parentPath ?? e.path ?? root;
|
|
1083
|
-
const rel = relative(root,
|
|
1045
|
+
const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
|
|
1084
1046
|
if (rel.split("/").some(isSkippedSegment)) continue;
|
|
1085
1047
|
ids.push(rel.replace(/\.md$/, ""));
|
|
1086
1048
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1049
|
+
ids.sort();
|
|
1050
|
+
const read = await mapBatched(ids, async (id) => {
|
|
1089
1051
|
try {
|
|
1090
|
-
|
|
1052
|
+
return await this.readDocument(id);
|
|
1091
1053
|
} catch (err) {
|
|
1092
1054
|
console.error("[FlatNestStorage] skipped unreadable doc", id, err);
|
|
1055
|
+
return null;
|
|
1093
1056
|
}
|
|
1094
|
-
}
|
|
1095
|
-
return
|
|
1057
|
+
});
|
|
1058
|
+
return read.filter((n) => n !== null);
|
|
1096
1059
|
}
|
|
1097
1060
|
};
|
|
1098
1061
|
|
|
1099
1062
|
// src/nodes/engine.ts
|
|
1063
|
+
var DISCOVERY_TTL_MS = 3e4;
|
|
1064
|
+
var isLive = (node) => node.frontmatter.status !== "rejected";
|
|
1065
|
+
var DERIVED_VAULT_FILES = /* @__PURE__ */ new Set([
|
|
1066
|
+
"INDEX.md",
|
|
1067
|
+
"CONTEXT.md",
|
|
1068
|
+
"CLAUDE.md",
|
|
1069
|
+
"GEMINI.md",
|
|
1070
|
+
"AGENTS.md",
|
|
1071
|
+
"README.md"
|
|
1072
|
+
]);
|
|
1073
|
+
function withDiscoveryCache(storage) {
|
|
1074
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1075
|
+
const discover = storage.discoverDocuments.bind(storage);
|
|
1076
|
+
const readDoc = storage.readDocument.bind(storage);
|
|
1077
|
+
const filtersRetired = !(storage instanceof FlatNestStorage);
|
|
1078
|
+
let generation = 0;
|
|
1079
|
+
storage.discoverDocuments = async (options = {}) => {
|
|
1080
|
+
const key = options.includeRetired || options.includeSuperseded ? "all" : "live";
|
|
1081
|
+
const hit = entries.get(key);
|
|
1082
|
+
if (hit && Date.now() - hit.ts < DISCOVERY_TTL_MS) return hit.docs;
|
|
1083
|
+
const all = entries.get("all");
|
|
1084
|
+
if (all && Date.now() - all.ts < DISCOVERY_TTL_MS) {
|
|
1085
|
+
const docs2 = filtersRetired ? all.docs.filter(isLive) : all.docs;
|
|
1086
|
+
entries.set("live", { docs: docs2, ts: all.ts });
|
|
1087
|
+
return docs2;
|
|
1088
|
+
}
|
|
1089
|
+
const startedAt = generation;
|
|
1090
|
+
const crawled = await discover({ ...options, includeRetired: true });
|
|
1091
|
+
const docs = key === "all" || !filtersRetired ? crawled : crawled.filter(isLive);
|
|
1092
|
+
if (generation === startedAt) {
|
|
1093
|
+
const ts = Date.now();
|
|
1094
|
+
entries.set("all", { docs: crawled, ts });
|
|
1095
|
+
entries.set(key, { docs, ts });
|
|
1096
|
+
}
|
|
1097
|
+
return docs;
|
|
1098
|
+
};
|
|
1099
|
+
const invalidate = () => {
|
|
1100
|
+
generation++;
|
|
1101
|
+
entries.clear();
|
|
1102
|
+
};
|
|
1103
|
+
const patch = async (id) => {
|
|
1104
|
+
generation++;
|
|
1105
|
+
let node = null;
|
|
1106
|
+
try {
|
|
1107
|
+
node = await readDoc(id);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
if (!(err instanceof DocumentNotFoundError)) {
|
|
1110
|
+
invalidate();
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
node = null;
|
|
1114
|
+
}
|
|
1115
|
+
for (const [key, entry] of entries) {
|
|
1116
|
+
const docs = entry.docs.filter((d) => d.id !== id);
|
|
1117
|
+
if (node && (key === "all" || isLive(node))) {
|
|
1118
|
+
const at = docs.findIndex((d) => d.id > id);
|
|
1119
|
+
docs.splice(at === -1 ? docs.length : at, 0, node);
|
|
1120
|
+
}
|
|
1121
|
+
entry.docs = docs;
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
const wrap = (name, after) => {
|
|
1125
|
+
const write = storage[name].bind(storage);
|
|
1126
|
+
storage[name] = async (...args) => {
|
|
1127
|
+
try {
|
|
1128
|
+
return await write(...args);
|
|
1129
|
+
} finally {
|
|
1130
|
+
try {
|
|
1131
|
+
await after(args);
|
|
1132
|
+
} catch {
|
|
1133
|
+
invalidate();
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
};
|
|
1138
|
+
wrap("writeDocument", (args) => patch(String(args[0])));
|
|
1139
|
+
wrap("deleteDocument", (args) => patch(String(args[0])));
|
|
1140
|
+
wrap("init", () => invalidate());
|
|
1141
|
+
wrap("writeVaultFile", (args) => {
|
|
1142
|
+
const rel = String(args[0] ?? "");
|
|
1143
|
+
const base = rel.split(/[/\\]/).pop() || "";
|
|
1144
|
+
if (rel.endsWith(".md") && !DERIVED_VAULT_FILES.has(base)) invalidate();
|
|
1145
|
+
});
|
|
1146
|
+
return { storage, invalidate };
|
|
1147
|
+
}
|
|
1100
1148
|
var NestEngineCache = class {
|
|
1101
1149
|
cache = /* @__PURE__ */ new Map();
|
|
1102
1150
|
async get(nestId) {
|
|
1103
1151
|
let engine = this.cache.get(nestId);
|
|
1104
1152
|
if (!engine) {
|
|
1105
1153
|
const nestPath = resolveNestPath(nestId);
|
|
1106
|
-
const storage
|
|
1154
|
+
const { storage, invalidate } = withDiscoveryCache(
|
|
1155
|
+
await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage2(nestPath)
|
|
1156
|
+
);
|
|
1107
1157
|
const query = new GraphQueryEngine(storage);
|
|
1108
1158
|
const versions = new VersionManager(storage);
|
|
1109
|
-
engine = { storage, query, versions };
|
|
1159
|
+
engine = { storage, query, versions, invalidateDiscovery: invalidate };
|
|
1110
1160
|
this.cache.set(nestId, engine);
|
|
1111
1161
|
}
|
|
1112
1162
|
return engine;
|
|
@@ -1116,6 +1166,11 @@ var NestEngineCache = class {
|
|
|
1116
1166
|
}
|
|
1117
1167
|
};
|
|
1118
1168
|
var engineCache = new NestEngineCache();
|
|
1169
|
+
var engineApi = createEngineApi();
|
|
1170
|
+
async function opContext(nestId, actor, onProgress) {
|
|
1171
|
+
const { storage, query, versions } = await engineCache.get(nestId);
|
|
1172
|
+
return { storage, query, versions, actor, onProgress };
|
|
1173
|
+
}
|
|
1119
1174
|
|
|
1120
1175
|
// src/governance/title-resolver.ts
|
|
1121
1176
|
async function buildTitleMap(nestId) {
|
|
@@ -1601,14 +1656,18 @@ async function removeTeamRefs(tx, teamId) {
|
|
|
1601
1656
|
}
|
|
1602
1657
|
async function resolveTeamRolesForUser(nestId, userId) {
|
|
1603
1658
|
const refs = await listNestTeams(nestId).catch(() => []);
|
|
1659
|
+
return resolveTeamRolesFromShared(refs, userId);
|
|
1660
|
+
}
|
|
1661
|
+
async function resolveTeamRolesFromShared(refs, userId) {
|
|
1604
1662
|
if (refs.length === 0) return [];
|
|
1605
|
-
const
|
|
1663
|
+
const ids = [...new Set(refs.map((r) => r.teamId).filter(Boolean))];
|
|
1664
|
+
if (ids.length === 0) return [];
|
|
1665
|
+
const rows = await getDb().all(
|
|
1666
|
+
`SELECT members FROM teams WHERE id IN (${ids.map(() => "?").join(",")})`,
|
|
1667
|
+
ids
|
|
1668
|
+
);
|
|
1606
1669
|
const roles = /* @__PURE__ */ new Set();
|
|
1607
|
-
for (const
|
|
1608
|
-
const row = await db.get("SELECT members FROM teams WHERE id = ?", [
|
|
1609
|
-
ref.teamId
|
|
1610
|
-
]);
|
|
1611
|
-
if (!row) continue;
|
|
1670
|
+
for (const row of rows) {
|
|
1612
1671
|
const member = parseMembers(row.members).find((m) => m.userId === userId);
|
|
1613
1672
|
if (member) roles.add(member.role);
|
|
1614
1673
|
}
|
|
@@ -1674,71 +1733,91 @@ var PERMISSION_LEVELS = {
|
|
|
1674
1733
|
admin: 3,
|
|
1675
1734
|
owner: 4
|
|
1676
1735
|
};
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
for (const role of await resolveTeamRolesForUser(nestId, userId)) {
|
|
1699
|
-
best = higher(best, teamRoleToPermission(role));
|
|
1700
|
-
}
|
|
1701
|
-
if (best === "admin") return best;
|
|
1702
|
-
const stewardGrant = await db.get(
|
|
1703
|
-
`SELECT 1 FROM stewards s
|
|
1704
|
-
JOIN users u ON u.id = ?
|
|
1705
|
-
WHERE s.nest_id = ? AND s.is_active = 1
|
|
1706
|
-
AND LOWER(s.user_email) = LOWER(u.email)
|
|
1707
|
-
LIMIT 1`,
|
|
1708
|
-
[userId, nestId]
|
|
1736
|
+
var higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
|
|
1737
|
+
async function resolveNestAccess(nestId, userId) {
|
|
1738
|
+
const row = await getDb().get(
|
|
1739
|
+
`SELECT n.user_id AS owner_id,
|
|
1740
|
+
n.visibility AS visibility,
|
|
1741
|
+
n.shared_teams AS shared_teams,
|
|
1742
|
+
u.email AS caller_email,
|
|
1743
|
+
nc.permission AS collab_permission,
|
|
1744
|
+
(SELECT 1 FROM stewards s
|
|
1745
|
+
WHERE s.nest_id = n.id AND s.is_active = 1
|
|
1746
|
+
AND LOWER(s.user_email) = LOWER(u.email)
|
|
1747
|
+
LIMIT 1) AS steward_hit,
|
|
1748
|
+
(SELECT 1 FROM grants g
|
|
1749
|
+
WHERE g.nest_id = n.id AND g.user_id = ?
|
|
1750
|
+
LIMIT 1) AS grant_hit
|
|
1751
|
+
FROM nests n
|
|
1752
|
+
LEFT JOIN users u ON u.id = ?
|
|
1753
|
+
LEFT JOIN nest_collaborators nc
|
|
1754
|
+
ON nc.nest_id = n.id AND nc.user_id = ?
|
|
1755
|
+
WHERE n.id = ?`,
|
|
1756
|
+
[userId, userId, userId, nestId]
|
|
1709
1757
|
);
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1758
|
+
const absent = {
|
|
1759
|
+
exists: false,
|
|
1760
|
+
ownerId: null,
|
|
1761
|
+
visibility: null,
|
|
1762
|
+
callerEmail: null,
|
|
1763
|
+
collaboratorPermission: null,
|
|
1764
|
+
isSteward: false,
|
|
1765
|
+
hasGrant: false,
|
|
1766
|
+
permission: "none",
|
|
1767
|
+
isPublicReader: false
|
|
1768
|
+
};
|
|
1769
|
+
if (!row) return absent;
|
|
1770
|
+
const base = {
|
|
1771
|
+
exists: true,
|
|
1772
|
+
ownerId: row.owner_id,
|
|
1773
|
+
visibility: row.visibility,
|
|
1774
|
+
callerEmail: row.caller_email ?? null,
|
|
1775
|
+
collaboratorPermission: row.collab_permission ?? null,
|
|
1776
|
+
// Postgres returns integer 1, SQLite returns 1 — both truthy, neither a
|
|
1777
|
+
// boolean, so normalize rather than leaking the driver's shape.
|
|
1778
|
+
isSteward: !!row.steward_hit,
|
|
1779
|
+
hasGrant: !!row.grant_hit
|
|
1780
|
+
};
|
|
1781
|
+
if (row.owner_id === userId) {
|
|
1782
|
+
return { ...base, permission: "owner", isPublicReader: false };
|
|
1783
|
+
}
|
|
1784
|
+
const email = row.caller_email ?? null;
|
|
1785
|
+
const licenseAdmin = isLicenseAdminEmail(email);
|
|
1786
|
+
if (row.owner_id === ANON_USER_ID && licenseAdmin) {
|
|
1787
|
+
return { ...base, permission: "owner", isPublicReader: false };
|
|
1788
|
+
}
|
|
1789
|
+
if (licenseAdmin || email && isSuperAdmin(email)) {
|
|
1790
|
+
return { ...base, permission: "admin", isPublicReader: false };
|
|
1791
|
+
}
|
|
1792
|
+
let best = higher("none", row.collab_permission);
|
|
1793
|
+
if (best !== "admin") {
|
|
1794
|
+
const teamRoles = await resolveTeamRolesFromShared(
|
|
1795
|
+
parseSharedTeams(row.shared_teams),
|
|
1796
|
+
userId
|
|
1797
|
+
);
|
|
1798
|
+
for (const role of teamRoles) {
|
|
1799
|
+
best = higher(best, teamRoleToPermission(role));
|
|
1800
|
+
}
|
|
1801
|
+
if (teamRoles.length > 0) {
|
|
1802
|
+
return { ...base, permission: best, isPublicReader: false };
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (best === "admin") {
|
|
1806
|
+
return { ...base, permission: best, isPublicReader: false };
|
|
1807
|
+
}
|
|
1808
|
+
if (base.isSteward) best = higher(best, "read");
|
|
1809
|
+
if (row.visibility === "public") best = higher(best, "read");
|
|
1810
|
+
const isPublicReader2 = row.visibility === "public" && !row.collab_permission && !base.isSteward;
|
|
1811
|
+
return { ...base, permission: best, isPublicReader: isPublicReader2 };
|
|
1812
|
+
}
|
|
1813
|
+
async function resolveNestPermission(nestId, userId) {
|
|
1814
|
+
return (await resolveNestAccess(nestId, userId)).permission;
|
|
1713
1815
|
}
|
|
1714
1816
|
function permissionLevel(p) {
|
|
1715
1817
|
return PERMISSION_LEVELS[p] ?? 0;
|
|
1716
1818
|
}
|
|
1717
1819
|
async function isPublicReader(nestId, userId) {
|
|
1718
|
-
|
|
1719
|
-
const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
|
|
1720
|
-
if (!nest) return false;
|
|
1721
|
-
if (nest.visibility !== "public") return false;
|
|
1722
|
-
if (nest.user_id === userId) return false;
|
|
1723
|
-
const directGrant = await db.get(
|
|
1724
|
-
"SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
1725
|
-
[nestId, userId]
|
|
1726
|
-
);
|
|
1727
|
-
if (directGrant) return false;
|
|
1728
|
-
if (await isLicenseAdminUserId(userId)) return false;
|
|
1729
|
-
const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
1730
|
-
if (caller?.email && isSuperAdmin(caller.email)) return false;
|
|
1731
|
-
const stewardGrant = await db.get(
|
|
1732
|
-
`SELECT 1 FROM stewards s
|
|
1733
|
-
JOIN users u ON u.id = ?
|
|
1734
|
-
WHERE s.nest_id = ? AND s.is_active = 1
|
|
1735
|
-
AND LOWER(s.user_email) = LOWER(u.email)
|
|
1736
|
-
LIMIT 1`,
|
|
1737
|
-
[userId, nestId]
|
|
1738
|
-
);
|
|
1739
|
-
if (stewardGrant) return false;
|
|
1740
|
-
if ((await resolveTeamRolesForUser(nestId, userId)).length > 0) return false;
|
|
1741
|
-
return true;
|
|
1820
|
+
return (await resolveNestAccess(nestId, userId)).isPublicReader;
|
|
1742
1821
|
}
|
|
1743
1822
|
|
|
1744
1823
|
// src/governance/tag-index-service.ts
|
|
@@ -1775,6 +1854,7 @@ async function removeNodeFromTagIndex(nestId, nodeId) {
|
|
|
1775
1854
|
}
|
|
1776
1855
|
|
|
1777
1856
|
// src/nests/service.ts
|
|
1857
|
+
import { rm } from "fs/promises";
|
|
1778
1858
|
async function isImportedNest(nestId) {
|
|
1779
1859
|
const row = await getDb().get(
|
|
1780
1860
|
"SELECT is_imported FROM nests WHERE id = ?",
|
|
@@ -1954,27 +2034,16 @@ async function createNest(userId, name, description) {
|
|
|
1954
2034
|
trackEvent("nest.create", { nestId: id, userId });
|
|
1955
2035
|
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
1956
2036
|
}
|
|
1957
|
-
async function importNest(userId, name
|
|
2037
|
+
async function importNest(userId, name) {
|
|
1958
2038
|
const nm = (name || "").trim();
|
|
1959
2039
|
if (!nm) {
|
|
1960
2040
|
throw new ValidationError("name is required");
|
|
1961
2041
|
}
|
|
1962
|
-
if (!Array.isArray(files)) {
|
|
1963
|
-
throw new ValidationError("files are required");
|
|
1964
|
-
}
|
|
1965
2042
|
const db = getDb();
|
|
1966
2043
|
const id = uuid2();
|
|
1967
2044
|
const slug = toSlug(nm);
|
|
1968
2045
|
const visibility = userId === ANON_USER_ID ? "public" : "private";
|
|
1969
|
-
|
|
1970
|
-
mkdirSync(dest, { recursive: true });
|
|
1971
|
-
try {
|
|
1972
|
-
await writeImportedFiles(dest, files);
|
|
1973
|
-
} catch (err) {
|
|
1974
|
-
console.error("[nests] import write failed", dest, err);
|
|
1975
|
-
discardImportDir(dest);
|
|
1976
|
-
throw new ValidationError("Failed to import folder");
|
|
1977
|
-
}
|
|
2046
|
+
mkdirSync(resolveNestPath(id), { recursive: true });
|
|
1978
2047
|
await db.run(
|
|
1979
2048
|
"INSERT INTO nests (id, user_id, name, slug, description, visibility, is_imported) VALUES (?, ?, ?, ?, ?, ?, 1)",
|
|
1980
2049
|
[id, userId, nm, slug, null, visibility]
|
|
@@ -2045,10 +2114,12 @@ async function listSharedNests(userId) {
|
|
|
2045
2114
|
LEFT JOIN stewards s
|
|
2046
2115
|
ON s.nest_id = n.id AND s.is_active = 1
|
|
2047
2116
|
AND LOWER(s.user_email) = LOWER(u.email)
|
|
2117
|
+
LEFT JOIN grants g
|
|
2118
|
+
ON g.nest_id = n.id AND g.user_id = ?
|
|
2048
2119
|
WHERE n.user_id != ?
|
|
2049
|
-
AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL)
|
|
2120
|
+
AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL OR g.id IS NOT NULL)
|
|
2050
2121
|
ORDER BY n.created_at DESC`,
|
|
2051
|
-
[userId, userId, userId]
|
|
2122
|
+
[userId, userId, userId, userId]
|
|
2052
2123
|
);
|
|
2053
2124
|
const seen = new Set(direct.map((n) => n.id));
|
|
2054
2125
|
for (const n of await nestsSharedViaTeams(userId)) {
|
|
@@ -2074,10 +2145,16 @@ async function listSharedNests(userId) {
|
|
|
2074
2145
|
);
|
|
2075
2146
|
return direct;
|
|
2076
2147
|
}
|
|
2077
|
-
async function listVisibleNests(userId) {
|
|
2148
|
+
async function listVisibleNests(userId, opts = {}) {
|
|
2078
2149
|
const db = getDb();
|
|
2079
2150
|
const includeAnon = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
|
|
2080
|
-
const params =
|
|
2151
|
+
const params = [
|
|
2152
|
+
userId,
|
|
2153
|
+
userId,
|
|
2154
|
+
...opts.readable ? [userId] : [],
|
|
2155
|
+
userId,
|
|
2156
|
+
...includeAnon ? [ANON_USER_ID] : []
|
|
2157
|
+
];
|
|
2081
2158
|
const rows = await db.all(
|
|
2082
2159
|
`SELECT DISTINCT n.* FROM nests n
|
|
2083
2160
|
LEFT JOIN nest_collaborators nc
|
|
@@ -2086,9 +2163,11 @@ async function listVisibleNests(userId) {
|
|
|
2086
2163
|
LEFT JOIN stewards s
|
|
2087
2164
|
ON s.nest_id = n.id AND s.is_active = 1
|
|
2088
2165
|
AND LOWER(s.user_email) = LOWER(u.email)
|
|
2166
|
+
${opts.readable ? "LEFT JOIN grants g ON g.nest_id = n.id AND g.user_id = ?" : ""}
|
|
2089
2167
|
WHERE n.user_id = ?
|
|
2090
2168
|
OR nc.user_id IS NOT NULL
|
|
2091
2169
|
OR s.id IS NOT NULL
|
|
2170
|
+
${opts.readable ? "OR g.id IS NOT NULL OR n.visibility = 'public'" : ""}
|
|
2092
2171
|
${includeAnon ? "OR n.user_id = ?" : ""}
|
|
2093
2172
|
ORDER BY n.created_at DESC`,
|
|
2094
2173
|
params
|
|
@@ -2138,7 +2217,7 @@ async function deleteNest(nestId) {
|
|
|
2138
2217
|
});
|
|
2139
2218
|
const path = resolveNestPath(nestId);
|
|
2140
2219
|
try {
|
|
2141
|
-
|
|
2220
|
+
await rm(path, { recursive: true, force: true });
|
|
2142
2221
|
} catch (err) {
|
|
2143
2222
|
console.warn(`[nests] failed to remove nest directory ${path}:`, err);
|
|
2144
2223
|
}
|
|
@@ -2194,6 +2273,8 @@ export {
|
|
|
2194
2273
|
getNest,
|
|
2195
2274
|
deleteNest,
|
|
2196
2275
|
engineCache,
|
|
2276
|
+
engineApi,
|
|
2277
|
+
opContext,
|
|
2197
2278
|
buildTitleMap,
|
|
2198
2279
|
folderForNode,
|
|
2199
2280
|
describeNode,
|
|
@@ -2223,6 +2304,7 @@ export {
|
|
|
2223
2304
|
canApproveWith,
|
|
2224
2305
|
primaryRole,
|
|
2225
2306
|
isServerAdminUserId,
|
|
2307
|
+
resolveNestAccess,
|
|
2226
2308
|
resolveNestPermission,
|
|
2227
2309
|
permissionLevel,
|
|
2228
2310
|
isPublicReader
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
buildTitleMap,
|
|
3
3
|
insertOrReplace,
|
|
4
4
|
nowExpr
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-SNFGU5Q7.js";
|
|
6
6
|
import {
|
|
7
7
|
getDb
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-JMJLNEXE.js";
|
|
9
9
|
|
|
10
10
|
// src/governance/version-service.ts
|
|
11
11
|
import { createHash } from "crypto";
|
|
@@ -100,6 +100,15 @@ async function getCurrentVersion(nestId, nodeId) {
|
|
|
100
100
|
);
|
|
101
101
|
return row?.v || 0;
|
|
102
102
|
}
|
|
103
|
+
async function getTrackedNodeIds(nestId) {
|
|
104
|
+
const db = getDb();
|
|
105
|
+
const rows = await db.all(
|
|
106
|
+
`SELECT DISTINCT node_id FROM node_versions
|
|
107
|
+
WHERE nest_id = ? AND author NOT LIKE ?`,
|
|
108
|
+
[nestId, `${SYSTEM_AUTHOR_PREFIX}%`]
|
|
109
|
+
);
|
|
110
|
+
return new Set(rows.map((r) => r.node_id));
|
|
111
|
+
}
|
|
103
112
|
async function getApprovedVersion(nestId, nodeId) {
|
|
104
113
|
const db = getDb();
|
|
105
114
|
const row = await db.get(
|
|
@@ -108,6 +117,14 @@ async function getApprovedVersion(nestId, nodeId) {
|
|
|
108
117
|
);
|
|
109
118
|
return row?.approved_version ?? null;
|
|
110
119
|
}
|
|
120
|
+
async function getApprovedVersions(nestId) {
|
|
121
|
+
const db = getDb();
|
|
122
|
+
const rows = await db.all(
|
|
123
|
+
"SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
|
|
124
|
+
[nestId]
|
|
125
|
+
);
|
|
126
|
+
return new Map(rows.map((r) => [r.node_id, r.approved_version]));
|
|
127
|
+
}
|
|
111
128
|
async function setApprovedVersion(nestId, nodeId, version, approvedBy) {
|
|
112
129
|
const db = getDb();
|
|
113
130
|
const sql = insertOrReplace(
|
|
@@ -262,7 +279,9 @@ export {
|
|
|
262
279
|
getVersions,
|
|
263
280
|
getVersion,
|
|
264
281
|
getCurrentVersion,
|
|
282
|
+
getTrackedNodeIds,
|
|
265
283
|
getApprovedVersion,
|
|
284
|
+
getApprovedVersions,
|
|
266
285
|
setApprovedVersion,
|
|
267
286
|
checkConflict,
|
|
268
287
|
getNodeTags,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveNestPermission,
|
|
12
12
|
resolveTeamRolesForUser,
|
|
13
13
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-SNFGU5Q7.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-JMJLNEXE.js";
|
|
24
24
|
|
|
25
25
|
// src/governance/stewardship-service.ts
|
|
26
26
|
import { v4 as uuid } from "uuid";
|