@promptowl/contextnest-community 1.15.0 → 1.16.1
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-GWDOLYFJ.js → chunk-CLKZZZS2.js} +2 -2
- package/dist/{chunk-JWVZ3T35.js → chunk-D6HICFVW.js} +21 -2
- package/dist/{chunk-BCFKLY4H.js → chunk-JMJLNEXE.js} +24 -0
- package/dist/{chunk-7BOG3S5H.js → chunk-L7UCMGWZ.js} +89 -92
- package/dist/{chunk-5IAKUUZF.js → chunk-OTGZQZKK.js} +1 -1
- package/dist/{chunk-C4U75IUR.js → chunk-QJVRZT6M.js} +21 -58
- package/dist/{client-NFUHPLLM.js → client-OBS5HOTA.js} +1 -1
- package/dist/{grants-service-O4AO326A.js → grants-service-I535QVTW.js} +2 -2
- package/dist/index.js +961 -493
- package/dist/{review-service-JUAFYB73.js → review-service-V2GR6VM3.js} +6 -6
- package/dist/{stewardship-service-FHJ2RBKN.js → stewardship-service-LTREGVSJ.js} +3 -3
- package/dist/{version-service-ECYCBVSE.js → version-service-V6K5SZ3P.js} +7 -3
- package/dist/web3/assets/index-DnQARUL3.css +1 -0
- package/dist/web3/assets/index-_m7WyozM.js +1051 -0
- package/dist/web3/index.html +2 -2
- package/package.json +4 -3
- package/dist/web3/assets/index-BaMJ4tEz.css +0 -1
- package/dist/web3/assets/index-D950G4hR.js +0 -1052
package/CONFIGURATION.md
CHANGED
|
@@ -73,6 +73,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
73
73
|
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
|
74
74
|
| `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
|
|
75
75
|
| `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). |
|
|
76
|
+
| `FRAME_ANCESTORS` | `'self'` | Which origins may embed this server in an iframe, sent as CSP `frame-ancestors`. The default lets nothing but this origin frame the UI, which blocks clickjacking. Deployments that are meant to be embedded list the embedding origin — e.g. the PromptOwl Data Room iframes ContextNest, so that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`. Comma-separated; `'self'` is always included; `*` allows any site and disables the protection. Note the embedding page must be **same-site** (a sibling subdomain) for the session cookie to survive inside the frame — a genuinely cross-domain embed will render the login page no matter what this is set to. |
|
|
76
77
|
| `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. The asset-upload route (`POST /nests/:id/assets`) is exempt up to the video cap below. |
|
|
77
78
|
| `VIDEO_MAX_MB` | `30` | Max size (in MB) of a video uploaded into a doc. Default `30` keeps it under Cloud Run's ~32 MiB HTTP/1 request-body limit, so an oversized video is rejected with a clear message instead of a bare `413` from the platform. Raise only where the deployment can actually accept larger request bodies (not behind Cloud Run, or on HTTP/2 / direct-to-bucket upload). Images are fixed at 10 MB. |
|
|
78
79
|
| `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. |
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveNestPermission,
|
|
12
12
|
resolveTeamRolesForUser,
|
|
13
13
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-L7UCMGWZ.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";
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
buildTitleMap,
|
|
3
3
|
insertOrReplace,
|
|
4
4
|
nowExpr
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-L7UCMGWZ.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,
|
|
@@ -512,6 +512,30 @@ var config = {
|
|
|
512
512
|
if ((process.env.AUTH_MODE || "key") === "open") return "*";
|
|
513
513
|
return ["http://localhost:5173", "http://localhost:3838"];
|
|
514
514
|
},
|
|
515
|
+
/**
|
|
516
|
+
* Who may frame this server, as a CSP `frame-ancestors` source list.
|
|
517
|
+
*
|
|
518
|
+
* Defaults to `'self'`: no other site can embed the UI, which is what stops
|
|
519
|
+
* a hostile page from framing it invisibly and stealing clicks from a
|
|
520
|
+
* signed-in operator. Deployments that are *meant* to be embedded name the
|
|
521
|
+
* embedding origin — e.g. the PromptOwl Data Room iframes this server, so
|
|
522
|
+
* that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`.
|
|
523
|
+
*
|
|
524
|
+
* Comma-separated; `'self'` is always included. `*` allows any site to frame
|
|
525
|
+
* this server and disables the protection.
|
|
526
|
+
*
|
|
527
|
+
* Note we deliberately do NOT also send `X-Frame-Options` — it only
|
|
528
|
+
* understands "same origin", so it would block a sibling subdomain that
|
|
529
|
+
* `frame-ancestors` is configured to allow. Every browser that matters
|
|
530
|
+
* honours `frame-ancestors`, and it wins where both are present.
|
|
531
|
+
*/
|
|
532
|
+
get FRAME_ANCESTORS() {
|
|
533
|
+
const raw = process.env.FRAME_ANCESTORS?.trim();
|
|
534
|
+
if (!raw) return "'self'";
|
|
535
|
+
if (raw === "*") return "*";
|
|
536
|
+
const sources = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
537
|
+
return [.../* @__PURE__ */ new Set(["'self'", ...sources])].join(" ");
|
|
538
|
+
},
|
|
515
539
|
/**
|
|
516
540
|
* Max JSON body size in bytes. Prevents DoS via giant payloads.
|
|
517
541
|
* Default: 10 MB. Override via MAX_BODY_BYTES env.
|
|
@@ -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
|
|
@@ -1052,11 +995,29 @@ import {
|
|
|
1052
995
|
GraphQueryEngine,
|
|
1053
996
|
VersionManager
|
|
1054
997
|
} from "@promptowl/contextnest-engine";
|
|
998
|
+
import { createEngineApi } from "@promptowl/contextnest-engine/api";
|
|
1055
999
|
|
|
1056
1000
|
// src/nodes/flat-storage.ts
|
|
1057
|
-
import {
|
|
1058
|
-
import { join as
|
|
1001
|
+
import { readdir } from "fs/promises";
|
|
1002
|
+
import { join as join3, relative, sep } from "path";
|
|
1059
1003
|
import { NestStorage } from "@promptowl/contextnest-engine";
|
|
1004
|
+
|
|
1005
|
+
// src/fs/vault-import.ts
|
|
1006
|
+
function isSkippedSegment(segment) {
|
|
1007
|
+
return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// src/shared/batch.ts
|
|
1011
|
+
var IO_CONCURRENCY = 32;
|
|
1012
|
+
async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
|
|
1013
|
+
const out = [];
|
|
1014
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
1015
|
+
out.push(...await Promise.all(items.slice(i, i + concurrency).map(fn)));
|
|
1016
|
+
}
|
|
1017
|
+
return out;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/nodes/flat-storage.ts
|
|
1060
1021
|
var META_FILES = /* @__PURE__ */ new Set([
|
|
1061
1022
|
"INDEX.md",
|
|
1062
1023
|
"CONTEXT.md",
|
|
@@ -1070,7 +1031,7 @@ var FlatNestStorage = class extends NestStorage {
|
|
|
1070
1031
|
const root = this.root;
|
|
1071
1032
|
let entries;
|
|
1072
1033
|
try {
|
|
1073
|
-
entries =
|
|
1034
|
+
entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
1074
1035
|
} catch (err) {
|
|
1075
1036
|
console.error("[FlatNestStorage] cannot read folder", root, err);
|
|
1076
1037
|
return [];
|
|
@@ -1080,33 +1041,72 @@ var FlatNestStorage = class extends NestStorage {
|
|
|
1080
1041
|
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
1081
1042
|
if (META_FILES.has(e.name)) continue;
|
|
1082
1043
|
const dir = e.parentPath ?? e.path ?? root;
|
|
1083
|
-
const rel = relative(root,
|
|
1044
|
+
const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
|
|
1084
1045
|
if (rel.split("/").some(isSkippedSegment)) continue;
|
|
1085
1046
|
ids.push(rel.replace(/\.md$/, ""));
|
|
1086
1047
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1048
|
+
ids.sort();
|
|
1049
|
+
const read = await mapBatched(ids, async (id) => {
|
|
1089
1050
|
try {
|
|
1090
|
-
|
|
1051
|
+
return await this.readDocument(id);
|
|
1091
1052
|
} catch (err) {
|
|
1092
1053
|
console.error("[FlatNestStorage] skipped unreadable doc", id, err);
|
|
1054
|
+
return null;
|
|
1093
1055
|
}
|
|
1094
|
-
}
|
|
1095
|
-
return
|
|
1056
|
+
});
|
|
1057
|
+
return read.filter((n) => n !== null);
|
|
1096
1058
|
}
|
|
1097
1059
|
};
|
|
1098
1060
|
|
|
1099
1061
|
// src/nodes/engine.ts
|
|
1062
|
+
var DISCOVERY_TTL_MS = 3e4;
|
|
1063
|
+
var DOCUMENT_WRITERS = [
|
|
1064
|
+
"writeDocument",
|
|
1065
|
+
"deleteDocument",
|
|
1066
|
+
"writeVaultFile",
|
|
1067
|
+
"init"
|
|
1068
|
+
];
|
|
1069
|
+
function withDiscoveryCache(storage) {
|
|
1070
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1071
|
+
const discover = storage.discoverDocuments.bind(storage);
|
|
1072
|
+
let generation = 0;
|
|
1073
|
+
storage.discoverDocuments = async (options = {}) => {
|
|
1074
|
+
const key = options.includeRetired || options.includeSuperseded ? "all" : "live";
|
|
1075
|
+
const hit = entries.get(key);
|
|
1076
|
+
if (hit && Date.now() - hit.ts < DISCOVERY_TTL_MS) return hit.docs;
|
|
1077
|
+
const startedAt = generation;
|
|
1078
|
+
const docs = await discover(options);
|
|
1079
|
+
if (generation === startedAt) entries.set(key, { docs, ts: Date.now() });
|
|
1080
|
+
return docs;
|
|
1081
|
+
};
|
|
1082
|
+
const invalidate = () => {
|
|
1083
|
+
generation++;
|
|
1084
|
+
entries.clear();
|
|
1085
|
+
};
|
|
1086
|
+
for (const name of DOCUMENT_WRITERS) {
|
|
1087
|
+
const write = storage[name].bind(storage);
|
|
1088
|
+
storage[name] = async (...args) => {
|
|
1089
|
+
try {
|
|
1090
|
+
return await write(...args);
|
|
1091
|
+
} finally {
|
|
1092
|
+
invalidate();
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
return { storage, invalidate };
|
|
1097
|
+
}
|
|
1100
1098
|
var NestEngineCache = class {
|
|
1101
1099
|
cache = /* @__PURE__ */ new Map();
|
|
1102
1100
|
async get(nestId) {
|
|
1103
1101
|
let engine = this.cache.get(nestId);
|
|
1104
1102
|
if (!engine) {
|
|
1105
1103
|
const nestPath = resolveNestPath(nestId);
|
|
1106
|
-
const storage
|
|
1104
|
+
const { storage, invalidate } = withDiscoveryCache(
|
|
1105
|
+
await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage2(nestPath)
|
|
1106
|
+
);
|
|
1107
1107
|
const query = new GraphQueryEngine(storage);
|
|
1108
1108
|
const versions = new VersionManager(storage);
|
|
1109
|
-
engine = { storage, query, versions };
|
|
1109
|
+
engine = { storage, query, versions, invalidateDiscovery: invalidate };
|
|
1110
1110
|
this.cache.set(nestId, engine);
|
|
1111
1111
|
}
|
|
1112
1112
|
return engine;
|
|
@@ -1116,6 +1116,11 @@ var NestEngineCache = class {
|
|
|
1116
1116
|
}
|
|
1117
1117
|
};
|
|
1118
1118
|
var engineCache = new NestEngineCache();
|
|
1119
|
+
var engineApi = createEngineApi();
|
|
1120
|
+
async function opContext(nestId, actor, onProgress) {
|
|
1121
|
+
const { storage, query, versions } = await engineCache.get(nestId);
|
|
1122
|
+
return { storage, query, versions, actor, onProgress };
|
|
1123
|
+
}
|
|
1119
1124
|
|
|
1120
1125
|
// src/governance/title-resolver.ts
|
|
1121
1126
|
async function buildTitleMap(nestId) {
|
|
@@ -1775,6 +1780,7 @@ async function removeNodeFromTagIndex(nestId, nodeId) {
|
|
|
1775
1780
|
}
|
|
1776
1781
|
|
|
1777
1782
|
// src/nests/service.ts
|
|
1783
|
+
import { rm } from "fs/promises";
|
|
1778
1784
|
async function isImportedNest(nestId) {
|
|
1779
1785
|
const row = await getDb().get(
|
|
1780
1786
|
"SELECT is_imported FROM nests WHERE id = ?",
|
|
@@ -1954,27 +1960,16 @@ async function createNest(userId, name, description) {
|
|
|
1954
1960
|
trackEvent("nest.create", { nestId: id, userId });
|
|
1955
1961
|
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
1956
1962
|
}
|
|
1957
|
-
async function importNest(userId, name
|
|
1963
|
+
async function importNest(userId, name) {
|
|
1958
1964
|
const nm = (name || "").trim();
|
|
1959
1965
|
if (!nm) {
|
|
1960
1966
|
throw new ValidationError("name is required");
|
|
1961
1967
|
}
|
|
1962
|
-
if (!Array.isArray(files)) {
|
|
1963
|
-
throw new ValidationError("files are required");
|
|
1964
|
-
}
|
|
1965
1968
|
const db = getDb();
|
|
1966
1969
|
const id = uuid2();
|
|
1967
1970
|
const slug = toSlug(nm);
|
|
1968
1971
|
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
|
-
}
|
|
1972
|
+
mkdirSync(resolveNestPath(id), { recursive: true });
|
|
1978
1973
|
await db.run(
|
|
1979
1974
|
"INSERT INTO nests (id, user_id, name, slug, description, visibility, is_imported) VALUES (?, ?, ?, ?, ?, ?, 1)",
|
|
1980
1975
|
[id, userId, nm, slug, null, visibility]
|
|
@@ -2138,7 +2133,7 @@ async function deleteNest(nestId) {
|
|
|
2138
2133
|
});
|
|
2139
2134
|
const path = resolveNestPath(nestId);
|
|
2140
2135
|
try {
|
|
2141
|
-
|
|
2136
|
+
await rm(path, { recursive: true, force: true });
|
|
2142
2137
|
} catch (err) {
|
|
2143
2138
|
console.warn(`[nests] failed to remove nest directory ${path}:`, err);
|
|
2144
2139
|
}
|
|
@@ -2194,6 +2189,8 @@ export {
|
|
|
2194
2189
|
getNest,
|
|
2195
2190
|
deleteNest,
|
|
2196
2191
|
engineCache,
|
|
2192
|
+
engineApi,
|
|
2193
|
+
opContext,
|
|
2197
2194
|
buildTitleMap,
|
|
2198
2195
|
folderForNode,
|
|
2199
2196
|
describeNode,
|
|
@@ -2,22 +2,24 @@ import {
|
|
|
2
2
|
grantCoversNode,
|
|
3
3
|
listUserGrants,
|
|
4
4
|
resolveNodeGrant
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OTGZQZKK.js";
|
|
6
6
|
import {
|
|
7
7
|
createVersion,
|
|
8
8
|
getApprovedVersion,
|
|
9
|
+
getApprovedVersions,
|
|
9
10
|
getCurrentVersion,
|
|
10
11
|
setApprovedVersion
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-D6HICFVW.js";
|
|
12
13
|
import {
|
|
13
14
|
canUserAccess,
|
|
14
15
|
canUserApprove,
|
|
15
16
|
resolveStewardsForNode
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-CLKZZZS2.js";
|
|
17
18
|
import {
|
|
18
19
|
buildDocContext,
|
|
19
20
|
buildTitleMap,
|
|
20
21
|
describeNode,
|
|
22
|
+
engineApi,
|
|
21
23
|
engineCache,
|
|
22
24
|
insertOrReplace,
|
|
23
25
|
isPublicReader,
|
|
@@ -25,10 +27,11 @@ import {
|
|
|
25
27
|
nestName,
|
|
26
28
|
notifyEmailForNest,
|
|
27
29
|
nowExpr,
|
|
30
|
+
opContext,
|
|
28
31
|
prettyFolderPath,
|
|
29
32
|
sendEmailToRecipient,
|
|
30
33
|
titleForNode
|
|
31
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-L7UCMGWZ.js";
|
|
32
35
|
import {
|
|
33
36
|
ConflictError,
|
|
34
37
|
NotFoundError,
|
|
@@ -37,7 +40,7 @@ import {
|
|
|
37
40
|
import {
|
|
38
41
|
config,
|
|
39
42
|
getDb
|
|
40
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-JMJLNEXE.js";
|
|
41
44
|
|
|
42
45
|
// src/governance/review-service.ts
|
|
43
46
|
import { v4 as uuid3 } from "uuid";
|
|
@@ -222,15 +225,10 @@ async function canReadNode(nestId, nodeId, userId, userEmail) {
|
|
|
222
225
|
if ((await canUserAccess(nestId, nodeId, userEmail)).allowed) return true;
|
|
223
226
|
return await resolveNodeGrant(nestId, userId, nodeId) !== null;
|
|
224
227
|
}
|
|
225
|
-
async function filterAccessible(nestId, userId, userEmail, nodes) {
|
|
228
|
+
async function filterAccessible(nestId, userId, userEmail, nodes, approvedVersions) {
|
|
226
229
|
if (await isPublicReader(nestId, userId)) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
if (await getApprovedVersion(nestId, n.id) !== null) {
|
|
230
|
-
filtered.push(n);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return filtered;
|
|
230
|
+
const approved = approvedVersions ?? await getApprovedVersions(nestId);
|
|
231
|
+
return nodes.filter((n) => approved.has(n.id));
|
|
234
232
|
}
|
|
235
233
|
if (!await isStewardshipEnabled(nestId)) return nodes;
|
|
236
234
|
const grants = await listUserGrants(nestId, userId);
|
|
@@ -834,39 +832,6 @@ function withNodeWriteLock(key, fn) {
|
|
|
834
832
|
return run;
|
|
835
833
|
}
|
|
836
834
|
|
|
837
|
-
// src/governance/safe-publish.ts
|
|
838
|
-
import {
|
|
839
|
-
publishDocument,
|
|
840
|
-
serializeDocument
|
|
841
|
-
} from "@promptowl/contextnest-engine";
|
|
842
|
-
async function safePublishDocument(storage, docId, options) {
|
|
843
|
-
const node = await storage.readDocument(docId);
|
|
844
|
-
const cleanedFrontmatter = stripUndefinedDeep(node.frontmatter);
|
|
845
|
-
const cleanedNode = { ...node, frontmatter: cleanedFrontmatter };
|
|
846
|
-
await storage.writeDocument(docId, serializeDocument(cleanedNode));
|
|
847
|
-
return publishDocument(storage, docId, options);
|
|
848
|
-
}
|
|
849
|
-
function serializeNodeSafe(node) {
|
|
850
|
-
return serializeDocument({
|
|
851
|
-
...node,
|
|
852
|
-
frontmatter: stripUndefinedDeep(node.frontmatter)
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
function stripUndefinedDeep(value) {
|
|
856
|
-
if (Array.isArray(value)) {
|
|
857
|
-
return value.filter((v) => v !== void 0).map((v) => stripUndefinedDeep(v));
|
|
858
|
-
}
|
|
859
|
-
if (value && typeof value === "object") {
|
|
860
|
-
const out = {};
|
|
861
|
-
for (const [k, v] of Object.entries(value)) {
|
|
862
|
-
if (v === void 0) continue;
|
|
863
|
-
out[k] = stripUndefinedDeep(v);
|
|
864
|
-
}
|
|
865
|
-
return out;
|
|
866
|
-
}
|
|
867
|
-
return value;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
835
|
// src/governance/review-service.ts
|
|
871
836
|
async function submitForReview(params) {
|
|
872
837
|
const db = getDb();
|
|
@@ -1032,13 +997,14 @@ async function approve(params) {
|
|
|
1032
997
|
);
|
|
1033
998
|
try {
|
|
1034
999
|
const { storage } = await engineCache.get(params.nestId);
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1000
|
+
const note = params.note || `Approved review request ${pending.id}`;
|
|
1001
|
+
const { version: engineVersion } = await engineApi.run(
|
|
1002
|
+
"context_publish",
|
|
1003
|
+
{ id: params.nodeId, note },
|
|
1004
|
+
await opContext(params.nestId, params.approvedBy)
|
|
1005
|
+
);
|
|
1040
1006
|
if (engineVersion !== version) {
|
|
1041
|
-
const node =
|
|
1007
|
+
const node = await storage.readDocument(params.nodeId);
|
|
1042
1008
|
const tags = node.frontmatter.tags || [];
|
|
1043
1009
|
await createVersion({
|
|
1044
1010
|
nestId: params.nestId,
|
|
@@ -1048,7 +1014,7 @@ async function approve(params) {
|
|
|
1048
1014
|
author: params.approvedBy,
|
|
1049
1015
|
status: "published",
|
|
1050
1016
|
tags,
|
|
1051
|
-
changeNote:
|
|
1017
|
+
changeNote: note
|
|
1052
1018
|
});
|
|
1053
1019
|
await setApprovedVersion(
|
|
1054
1020
|
params.nestId,
|
|
@@ -1369,7 +1335,7 @@ async function notifyDeletionRequested(request, baseUrl) {
|
|
|
1369
1335
|
async function notifyDeletionResolved(params) {
|
|
1370
1336
|
try {
|
|
1371
1337
|
const { outcome, resolvedBy, note } = params;
|
|
1372
|
-
const verb = outcome === "deleted" ? "deleted" : "
|
|
1338
|
+
const verb = outcome === "deleted" ? "deleted" : "rejected the deletion of";
|
|
1373
1339
|
const line = `${resolvedBy} ${verb} *${params.docTitle}*${note ? ` \u2014 "${note}"` : ""}`;
|
|
1374
1340
|
const recipient = params.requestedBy.toLowerCase();
|
|
1375
1341
|
const canInbox = !(params.targetType === "nest" && outcome === "deleted");
|
|
@@ -1379,7 +1345,7 @@ async function notifyDeletionResolved(params) {
|
|
|
1379
1345
|
[recipient],
|
|
1380
1346
|
`deletion_${outcome}`,
|
|
1381
1347
|
params.requestId,
|
|
1382
|
-
outcome === "deleted" ? `"${params.docTitle}" was deleted by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was
|
|
1348
|
+
outcome === "deleted" ? `"${params.docTitle}" was deleted by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1383
1349
|
);
|
|
1384
1350
|
}
|
|
1385
1351
|
void dispatchEvent({
|
|
@@ -1582,9 +1548,6 @@ function rowToReviewRequest(row) {
|
|
|
1582
1548
|
}
|
|
1583
1549
|
|
|
1584
1550
|
export {
|
|
1585
|
-
safePublishDocument,
|
|
1586
|
-
serializeNodeSafe,
|
|
1587
|
-
stripUndefinedDeep,
|
|
1588
1551
|
listWatchers,
|
|
1589
1552
|
addWatcher,
|
|
1590
1553
|
removeWatcher,
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
listGrants,
|
|
7
7
|
listUserGrants,
|
|
8
8
|
resolveNodeGrant
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-OTGZQZKK.js";
|
|
10
10
|
import "./chunk-GUNJTORH.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-JMJLNEXE.js";
|
|
12
12
|
import "./chunk-SLTQACJW.js";
|
|
13
13
|
export {
|
|
14
14
|
createGrant,
|