@promptowl/contextnest-community 1.1.0 → 1.2.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/README.md +17 -0
- package/dist/{chunk-WCOUCBDJ.js → chunk-5MT4ZBVF.js} +8 -1
- package/dist/{chunk-S2EWN2VA.js → chunk-E7E3JMQR.js} +3 -3
- package/dist/{chunk-TDAX3JOT.js → chunk-G62P54ET.js} +14 -0
- package/dist/{chunk-7UTMBL6Z.js → chunk-LO54V4AU.js} +1 -1
- package/dist/index.js +323 -16
- package/dist/{review-service-3OJIPYNV.js → review-service-GYX3AW6E.js} +4 -4
- package/dist/{stewardship-service-3XGX7QIN.js → stewardship-service-VOD5HY3I.js} +2 -2
- package/dist/{version-service-UODXLAOJ.js → version-service-OCZUV2QP.js} +2 -2
- package/dist/web3/assets/index-72vKyivD.js +756 -0
- package/dist/web3/assets/index-JmSevkg_.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +13 -1
- package/dist/web3/assets/index-BLxRS7jD.js +0 -673
- package/dist/web3/assets/index-DszK6Vkc.css +0 -1
package/CONFIGURATION.md
CHANGED
|
@@ -49,6 +49,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
49
49
|
| `AUTH_MODE` | `key` | `key` or `open`. See above. |
|
|
50
50
|
| `PROMPTOWL_API_URL` | `https://app.promptowl.ai` | PromptOwl's API origin — used for device auth, license validation, telemetry. Override for air-gapped or test setups. |
|
|
51
51
|
| `PROMPTOWL_KEY` | `""` | Your PromptOwl Community License key (`pk_...`). Unlicensed instances still run and serve reads, but every write returns `503` until a valid key is installed. Can also be set via the browser License Setup Page, which persists it to `ENV_FILE_PATH`. |
|
|
52
|
+
| `PROMPTOWL_SIGN_IN_GATE` | `open` | Restrict "Sign in with PromptOwl". `open` = anyone may; `admin-only` = only the license owner (admin) may, everyone else uses email/password (admin opens the login page with `?admin=1`); `disabled` = nobody may. Enforced server-side at `POST /auth/promptowl` and surfaced on the health endpoint. Unknown values fall back to `open`. |
|
|
52
53
|
| `ENV_FILE_PATH` | `$cwd/.env` | Path to the `.env` file the license install flow writes `PROMPTOWL_KEY` into (alongside existing vars). Override when your `.env` lives outside the working directory. |
|
|
53
54
|
| `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
|
|
54
55
|
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
package/README.md
CHANGED
|
@@ -97,6 +97,10 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
|
|
|
97
97
|
| Per-nest sharing + collaborators | ✅ | ✅ |
|
|
98
98
|
| Public read-only nests | ✅ | ✅ |
|
|
99
99
|
| Custom logo / branding | ✅ | ✅ |
|
|
100
|
+
| Admin password reset + user removal (in-platform) | ✅ | ✅ |
|
|
101
|
+
| Wiki backlinks, outline, hover-preview, link health | ✅ | ✅ |
|
|
102
|
+
| Rich editor — tables, callouts, toggles, code highlight, find/replace | ✅ | ✅ |
|
|
103
|
+
| Steward version revert | ✅ | ✅ |
|
|
100
104
|
| MCP server for AI agents | ✅ | ✅ |
|
|
101
105
|
| Centralized multi-tenant admin console | — | ✅ |
|
|
102
106
|
| SSO / SAML / SCIM | — | ✅ |
|
|
@@ -106,6 +110,19 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
|
|
|
106
110
|
|
|
107
111
|
For Enterprise pricing and features, contact **hoot@promptowl.ai** or visit <https://promptowl.ai/contextnest/>.
|
|
108
112
|
|
|
113
|
+
## What's new in 1.2.0
|
|
114
|
+
|
|
115
|
+
- **Admin user management (no email server needed)** — `POST /auth/admin/reset-password/:userId` sets a user's password or returns a one-time temp password; `DELETE /auth/users/:userId` removes a user and revokes their API keys, sessions, and role rows. New self-service **Change password** in the user menu blocks reusing the current password.
|
|
116
|
+
- **`PROMPTOWL_SIGN_IN_GATE`** — `open` / `admin-only` / `disabled` restricts "Sign in with PromptOwl" (admin reaches it via `?admin=1`). See [CONFIGURATION.md](./CONFIGURATION.md).
|
|
117
|
+
- **Plain-text agent view** — `?format=markdown` on a node returns frontmatter + body as `text/markdown` for LLM-friendly consumption.
|
|
118
|
+
- **Steward version revert** — `POST .../revert` restores an earlier version as a new one; pending-review docs are now editable (saving withdraws the review).
|
|
119
|
+
- **Editor — always-editable surface** — Notion-style buttery headline, selection bubble toolbar (format-on-highlight), turn-into block conversion, Cmd+F find & replace, rich blocks (tables, callouts, toggles, syntax-highlighted code, columns), and wiki/code-safety fixes.
|
|
120
|
+
- **Wiki usability** — backlinks panel ("Linked from N documents"), outline/TOC for 3+-heading docs, hover-preview on `[[wiki links]]`, multi-tag AND filtering with clickable chips, per-doc copy link, rename-safety warning for title-form links, link-health report (broken links + orphans), recently-edited strip.
|
|
121
|
+
- **Sharing clarity** — share button reads "Share nest"; dialog clarifies nest-wide scope; deep links open documents directly.
|
|
122
|
+
- **Security** — hardened scanner gate (PolinRider + Shai-Hulud preflight) in `bin/`.
|
|
123
|
+
|
|
124
|
+
Full history in [CHANGELOG.md](./CHANGELOG.md).
|
|
125
|
+
|
|
109
126
|
## What's new in 1.1.0
|
|
110
127
|
|
|
111
128
|
- **Vault import** — import an existing folder of markdown files into a new nest in one step, from the dashboard ("Import folder") or via the API. Frontmatter, wiki links, and folder structure are preserved.
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ANON_USER_ID,
|
|
3
3
|
config,
|
|
4
4
|
getDb
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-G62P54ET.js";
|
|
6
6
|
|
|
7
7
|
// src/governance/stewardship-service.ts
|
|
8
8
|
import { v4 as uuid2 } from "uuid";
|
|
@@ -136,6 +136,12 @@ var ConflictError = class extends AppError {
|
|
|
136
136
|
this.name = "ConflictError";
|
|
137
137
|
}
|
|
138
138
|
};
|
|
139
|
+
var LockedError = class extends AppError {
|
|
140
|
+
constructor(message = "Locked") {
|
|
141
|
+
super(423, message);
|
|
142
|
+
this.name = "LockedError";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
139
145
|
|
|
140
146
|
// src/nodes/engine.ts
|
|
141
147
|
import {
|
|
@@ -1352,6 +1358,7 @@ export {
|
|
|
1352
1358
|
ForbiddenError,
|
|
1353
1359
|
ValidationError,
|
|
1354
1360
|
ConflictError,
|
|
1361
|
+
LockedError,
|
|
1355
1362
|
trackEvent,
|
|
1356
1363
|
startTelemetryLoop,
|
|
1357
1364
|
getCurrentLicense,
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createVersion,
|
|
3
3
|
setApprovedVersion
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LO54V4AU.js";
|
|
5
5
|
import {
|
|
6
6
|
buildTitleMap,
|
|
7
7
|
canUserApprove,
|
|
8
8
|
engineCache,
|
|
9
9
|
resolveStewardsForNode
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-5MT4ZBVF.js";
|
|
11
11
|
import {
|
|
12
12
|
getDb
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-G62P54ET.js";
|
|
14
14
|
|
|
15
15
|
// src/governance/review-service.ts
|
|
16
16
|
import { v4 as uuid } from "uuid";
|
|
@@ -38,6 +38,20 @@ var config = {
|
|
|
38
38
|
get PROMPTOWL_KEY() {
|
|
39
39
|
return process.env.PROMPTOWL_KEY || "";
|
|
40
40
|
},
|
|
41
|
+
/**
|
|
42
|
+
* Restrict "Sign in with PromptOwl":
|
|
43
|
+
* "open" — anyone may sign in with PromptOwl (default)
|
|
44
|
+
* "admin-only" — only the license owner (admin) may; everyone else
|
|
45
|
+
* uses email/password. Admin reaches it via the login
|
|
46
|
+
* page's admin route (?admin=1).
|
|
47
|
+
* "disabled" — nobody may sign in with PromptOwl.
|
|
48
|
+
* Enforced server-side at the device entry points (POST /auth/device,
|
|
49
|
+
* GET /auth/device/poll) and the identity point (POST /auth/promptowl).
|
|
50
|
+
*/
|
|
51
|
+
get PROMPTOWL_SIGN_IN_GATE() {
|
|
52
|
+
const v = (process.env.PROMPTOWL_SIGN_IN_GATE || "open").trim().toLowerCase();
|
|
53
|
+
return v === "admin-only" || v === "disabled" ? v : "open";
|
|
54
|
+
},
|
|
41
55
|
/**
|
|
42
56
|
* Path to the .env file the server reads its config from. Used by
|
|
43
57
|
* the license install flow to persist PROMPTOWL_KEY alongside any
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
reject,
|
|
17
17
|
safePublishDocument,
|
|
18
18
|
submitForReview
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-E7E3JMQR.js";
|
|
20
20
|
import {
|
|
21
21
|
checkConflict,
|
|
22
22
|
createVersion,
|
|
@@ -25,11 +25,12 @@ import {
|
|
|
25
25
|
getDisplayStatus,
|
|
26
26
|
getVersions,
|
|
27
27
|
setApprovedVersion
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-LO54V4AU.js";
|
|
29
29
|
import {
|
|
30
30
|
AppError,
|
|
31
31
|
ConflictError,
|
|
32
32
|
ForbiddenError,
|
|
33
|
+
LockedError,
|
|
33
34
|
NotFoundError,
|
|
34
35
|
ValidationError,
|
|
35
36
|
canCreateInNest,
|
|
@@ -78,13 +79,13 @@ import {
|
|
|
78
79
|
trackEvent,
|
|
79
80
|
updateSteward,
|
|
80
81
|
validateLicense
|
|
81
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-5MT4ZBVF.js";
|
|
82
83
|
import {
|
|
83
84
|
ANON_EMAIL,
|
|
84
85
|
ANON_USER_ID,
|
|
85
86
|
config,
|
|
86
87
|
getDb
|
|
87
|
-
} from "./chunk-
|
|
88
|
+
} from "./chunk-G62P54ET.js";
|
|
88
89
|
|
|
89
90
|
// src/index.ts
|
|
90
91
|
import { serve } from "@hono/node-server";
|
|
@@ -272,6 +273,15 @@ function resolveCallerUserId(c) {
|
|
|
272
273
|
}
|
|
273
274
|
return null;
|
|
274
275
|
}
|
|
276
|
+
function deviceGateBlocked(c) {
|
|
277
|
+
const gate = config.PROMPTOWL_SIGN_IN_GATE;
|
|
278
|
+
if (gate === "open") return null;
|
|
279
|
+
const error = "PromptOwl sign-in is restricted on this server. Use email and password, or contact your admin.";
|
|
280
|
+
if (gate === "disabled") return { error, gate };
|
|
281
|
+
const callerId = resolveCallerUserId(c);
|
|
282
|
+
if (callerId && !isLicenseAdminUserId(callerId)) return { error, gate };
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
275
285
|
function setSessionCookie(c, sessionId) {
|
|
276
286
|
c.header(
|
|
277
287
|
"Set-Cookie",
|
|
@@ -455,6 +465,8 @@ authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
|
|
|
455
465
|
return c.json({ deleted: true });
|
|
456
466
|
});
|
|
457
467
|
authRoutes.post("/device", async (c) => {
|
|
468
|
+
const blocked = deviceGateBlocked(c);
|
|
469
|
+
if (blocked) return c.json(blocked, 403);
|
|
458
470
|
if (!tryConsume(`device:ip:${clientIp(c)}`, DEVICE_LIMIT)) {
|
|
459
471
|
return c.json({ error: "Too many device auth attempts, try again later" }, 429);
|
|
460
472
|
}
|
|
@@ -476,6 +488,8 @@ authRoutes.post("/device", async (c) => {
|
|
|
476
488
|
return c.json(data);
|
|
477
489
|
});
|
|
478
490
|
authRoutes.get("/device/poll", async (c) => {
|
|
491
|
+
const blocked = deviceGateBlocked(c);
|
|
492
|
+
if (blocked) return c.json(blocked, 403);
|
|
479
493
|
const code = c.req.query("code");
|
|
480
494
|
const clientSecret = c.req.query("client_secret");
|
|
481
495
|
if (!code || !clientSecret) {
|
|
@@ -511,6 +525,18 @@ authRoutes.post("/promptowl", async (c) => {
|
|
|
511
525
|
401
|
|
512
526
|
);
|
|
513
527
|
}
|
|
528
|
+
const gate = config.PROMPTOWL_SIGN_IN_GATE;
|
|
529
|
+
if (gate !== "open") {
|
|
530
|
+
if (gate === "disabled" || !isLicenseAdminEmail(me.email)) {
|
|
531
|
+
return c.json(
|
|
532
|
+
{
|
|
533
|
+
error: "PromptOwl sign-in is restricted on this server. Use email and password, or contact your admin.",
|
|
534
|
+
gate
|
|
535
|
+
},
|
|
536
|
+
403
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
514
540
|
const db = getDb();
|
|
515
541
|
let user = db.prepare("SELECT id, email, name FROM users WHERE email = ?").get(me.email);
|
|
516
542
|
if (!user) {
|
|
@@ -592,9 +618,17 @@ authRoutes.post("/password", authMiddleware, async (c) => {
|
|
|
592
618
|
const db = getDb();
|
|
593
619
|
const userId = c.get("userId");
|
|
594
620
|
const user = db.prepare("SELECT password_hash FROM users WHERE id = ?").get(userId);
|
|
595
|
-
if (!user)
|
|
621
|
+
if (!user) throw new ValidationError("User not found");
|
|
596
622
|
const check = await verifyPassword(body.current, user.password_hash);
|
|
597
|
-
if (!check.ok)
|
|
623
|
+
if (!check.ok) {
|
|
624
|
+
throw new ValidationError("Current password is incorrect");
|
|
625
|
+
}
|
|
626
|
+
const sameAsCurrent = await verifyPassword(body.next, user.password_hash);
|
|
627
|
+
if (sameAsCurrent.ok) {
|
|
628
|
+
throw new ValidationError(
|
|
629
|
+
"new password must be different from the current password"
|
|
630
|
+
);
|
|
631
|
+
}
|
|
598
632
|
const newHash = await hashPassword(body.next);
|
|
599
633
|
db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
|
|
600
634
|
newHash,
|
|
@@ -604,6 +638,92 @@ authRoutes.post("/password", authMiddleware, async (c) => {
|
|
|
604
638
|
clearSessionCookie(c);
|
|
605
639
|
return c.json({ ok: true });
|
|
606
640
|
});
|
|
641
|
+
authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
642
|
+
const callerId = resolveCallerUserId(c);
|
|
643
|
+
if (!callerId) {
|
|
644
|
+
return c.json({ error: "Authentication required." }, 401);
|
|
645
|
+
}
|
|
646
|
+
if (!isLicenseAdminUserId(callerId)) {
|
|
647
|
+
return c.json(
|
|
648
|
+
{ error: "Only the license-admin user can reset passwords." },
|
|
649
|
+
403
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const targetId = c.req.param("userId");
|
|
653
|
+
const db = getDb();
|
|
654
|
+
const target = db.prepare("SELECT id, email FROM users WHERE id = ?").get(targetId);
|
|
655
|
+
if (!target) return c.json({ error: "User not found" }, 404);
|
|
656
|
+
let supplied;
|
|
657
|
+
try {
|
|
658
|
+
supplied = (await c.req.json()).password;
|
|
659
|
+
} catch {
|
|
660
|
+
supplied = void 0;
|
|
661
|
+
}
|
|
662
|
+
if (supplied && supplied.length < 8) {
|
|
663
|
+
throw new ValidationError("password must be at least 8 characters");
|
|
664
|
+
}
|
|
665
|
+
const generated = supplied ? null : uuid().replace(/-/g, "").slice(0, 16);
|
|
666
|
+
const newPassword = supplied ?? generated;
|
|
667
|
+
const newHash = await hashPassword(newPassword);
|
|
668
|
+
db.transaction(() => {
|
|
669
|
+
db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
|
|
670
|
+
newHash,
|
|
671
|
+
target.id
|
|
672
|
+
);
|
|
673
|
+
db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(target.id);
|
|
674
|
+
})();
|
|
675
|
+
deleteAllSessionsForUser(target.id);
|
|
676
|
+
trackEvent("admin.reset_password", { adminId: callerId, userId: target.id });
|
|
677
|
+
return c.json({
|
|
678
|
+
ok: true,
|
|
679
|
+
email: target.email,
|
|
680
|
+
keys_revoked: true,
|
|
681
|
+
// Plaintext returned ONCE, only when the server generated it.
|
|
682
|
+
temporary_password: generated ?? void 0
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
authRoutes.delete("/users/:userId", async (c) => {
|
|
686
|
+
const callerId = resolveCallerUserId(c);
|
|
687
|
+
if (!callerId) {
|
|
688
|
+
return c.json({ error: "Authentication required." }, 401);
|
|
689
|
+
}
|
|
690
|
+
if (!isLicenseAdminUserId(callerId)) {
|
|
691
|
+
return c.json(
|
|
692
|
+
{ error: "Only the license-admin user can remove users." },
|
|
693
|
+
403
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
const targetId = c.req.param("userId");
|
|
697
|
+
if (targetId === callerId) {
|
|
698
|
+
return c.json({ error: "You can't remove your own admin account." }, 400);
|
|
699
|
+
}
|
|
700
|
+
const db = getDb();
|
|
701
|
+
const target = db.prepare("SELECT id, email FROM users WHERE id = ?").get(targetId);
|
|
702
|
+
if (!target) return c.json({ error: "User not found" }, 404);
|
|
703
|
+
const ownedNests = db.prepare("SELECT COUNT(*) AS c FROM nests WHERE user_id = ?").get(target.id).c;
|
|
704
|
+
if (ownedNests > 0) {
|
|
705
|
+
return c.json(
|
|
706
|
+
{
|
|
707
|
+
error: `This user owns ${ownedNests} nest${ownedNests === 1 ? "" : "s"}. Transfer or delete them before removing the user.`,
|
|
708
|
+
owned_nests: ownedNests
|
|
709
|
+
},
|
|
710
|
+
409
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
db.transaction(() => {
|
|
714
|
+
db.prepare(
|
|
715
|
+
"DELETE FROM stewards WHERE user_id = ? OR lower(user_email) = lower(?)"
|
|
716
|
+
).run(target.id, target.email);
|
|
717
|
+
db.prepare("DELETE FROM nest_collaborators WHERE user_id = ?").run(
|
|
718
|
+
target.id
|
|
719
|
+
);
|
|
720
|
+
db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(target.id);
|
|
721
|
+
db.prepare("DELETE FROM users WHERE id = ?").run(target.id);
|
|
722
|
+
})();
|
|
723
|
+
deleteAllSessionsForUser(target.id);
|
|
724
|
+
trackEvent("admin.remove_user", { adminId: callerId, userId: target.id });
|
|
725
|
+
return c.json({ ok: true, email: target.email });
|
|
726
|
+
});
|
|
607
727
|
authRoutes.post("/invite", async (c) => {
|
|
608
728
|
const body = await c.req.json();
|
|
609
729
|
if (!body.email) throw new ValidationError("email is required");
|
|
@@ -949,7 +1069,7 @@ async function approveExternalEdit(input) {
|
|
|
949
1069
|
const node = await storage.readDocument(input.documentId);
|
|
950
1070
|
const versionNum = result.versionEntry.version;
|
|
951
1071
|
const tags = node.frontmatter.tags || [];
|
|
952
|
-
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-
|
|
1072
|
+
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-OCZUV2QP.js");
|
|
953
1073
|
createVersion2({
|
|
954
1074
|
nestId: input.nestId,
|
|
955
1075
|
nodeId: input.documentId,
|
|
@@ -1251,8 +1371,10 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
1251
1371
|
}
|
|
1252
1372
|
const hasStewards = isStewardshipEnabled(nestId);
|
|
1253
1373
|
const currentTags = node.frontmatter.tags || [];
|
|
1254
|
-
if (getPendingReview(nestId, nodeId)) {
|
|
1255
|
-
|
|
1374
|
+
if (hasStewards && getPendingReview(nestId, nodeId)) {
|
|
1375
|
+
throw new LockedError(
|
|
1376
|
+
"This document is awaiting steward review and is locked. Approve or reject the pending review before editing."
|
|
1377
|
+
);
|
|
1256
1378
|
}
|
|
1257
1379
|
let responseVersion;
|
|
1258
1380
|
if (hasStewards) {
|
|
@@ -1589,7 +1711,38 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
1589
1711
|
|
|
1590
1712
|
// src/nodes/routes.ts
|
|
1591
1713
|
import { Hono as Hono4 } from "hono";
|
|
1714
|
+
|
|
1715
|
+
// src/nodes/markdown-export.ts
|
|
1716
|
+
function nodeToMarkdown(node) {
|
|
1717
|
+
const tags = node.tags ?? [];
|
|
1718
|
+
return [
|
|
1719
|
+
"---",
|
|
1720
|
+
`title: ${JSON.stringify(node.title ?? "")}`,
|
|
1721
|
+
`tags: [${tags.map((t) => JSON.stringify(t)).join(", ")}]`,
|
|
1722
|
+
`status: ${JSON.stringify(node.status ?? "")}`,
|
|
1723
|
+
`id: ${JSON.stringify(node.id)}`,
|
|
1724
|
+
"---",
|
|
1725
|
+
""
|
|
1726
|
+
].join("\n") + (node.body ?? "");
|
|
1727
|
+
}
|
|
1728
|
+
function nodesToMarkdown(nodes) {
|
|
1729
|
+
return nodes.map(nodeToMarkdown).join("\n\n---\n\n");
|
|
1730
|
+
}
|
|
1731
|
+
function isMarkdownFormat(c) {
|
|
1732
|
+
return c.req.query("format") === "markdown";
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/nodes/routes.ts
|
|
1592
1736
|
var nodeRoutes = new Hono4();
|
|
1737
|
+
function nodeAsMarkdown(response, nodeId) {
|
|
1738
|
+
return nodeToMarkdown({
|
|
1739
|
+
id: nodeId,
|
|
1740
|
+
title: response.title,
|
|
1741
|
+
tags: response.tags,
|
|
1742
|
+
status: response.status,
|
|
1743
|
+
body: response.content
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1593
1746
|
nodeRoutes.get("/", async (c) => {
|
|
1594
1747
|
const nestId = c.req.param("nestId");
|
|
1595
1748
|
const userId = c.get("userId");
|
|
@@ -1628,7 +1781,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
1628
1781
|
nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
1629
1782
|
const nestId = c.req.param("nestId");
|
|
1630
1783
|
const nodeId = c.req.param("nodeId");
|
|
1631
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
1784
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-VOD5HY3I.js");
|
|
1632
1785
|
const { stewards, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback2(
|
|
1633
1786
|
nestId,
|
|
1634
1787
|
nodeId
|
|
@@ -1649,7 +1802,7 @@ nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
|
1649
1802
|
nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
1650
1803
|
const nestId = c.req.param("nestId");
|
|
1651
1804
|
const nodeId = c.req.param("nodeId");
|
|
1652
|
-
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-
|
|
1805
|
+
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-OCZUV2QP.js");
|
|
1653
1806
|
const allVersions = getVersions2(nestId, nodeId);
|
|
1654
1807
|
const approved = getApprovedVersion2(nestId, nodeId);
|
|
1655
1808
|
const db = getDb();
|
|
@@ -1680,10 +1833,45 @@ nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
|
1680
1833
|
nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
|
|
1681
1834
|
const nestId = c.req.param("nestId");
|
|
1682
1835
|
const nodeId = c.req.param("nodeId");
|
|
1683
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
1836
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-GYX3AW6E.js");
|
|
1684
1837
|
const history = getReviewHistory2(nestId, nodeId);
|
|
1685
1838
|
return c.json({ reviews: history });
|
|
1686
1839
|
});
|
|
1840
|
+
nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
1841
|
+
const nestId = c.req.param("nestId");
|
|
1842
|
+
const nodeId = c.req.param("nodeId");
|
|
1843
|
+
const { versions: versionManager } = engineCache.get(nestId);
|
|
1844
|
+
const userId = c.get("userId");
|
|
1845
|
+
const userEmail = resolveCallerEmail(userId);
|
|
1846
|
+
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
1847
|
+
return c.json(
|
|
1848
|
+
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
1849
|
+
403
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1853
|
+
const targetVersion = Number(body.targetVersion);
|
|
1854
|
+
if (!Number.isInteger(targetVersion) || targetVersion < 1) {
|
|
1855
|
+
throw new ValidationError("targetVersion (a positive integer) is required");
|
|
1856
|
+
}
|
|
1857
|
+
let raw;
|
|
1858
|
+
try {
|
|
1859
|
+
raw = await versionManager.reconstructVersion(nodeId, targetVersion);
|
|
1860
|
+
} catch {
|
|
1861
|
+
throw new NotFoundError(
|
|
1862
|
+
`Version ${targetVersion} not found for ${nodeId}`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
const content = bodyOnly(nodeId, raw);
|
|
1866
|
+
const { node, version } = await updateNode(
|
|
1867
|
+
nestId,
|
|
1868
|
+
nodeId,
|
|
1869
|
+
{ content, changeNote: `Restored from version ${targetVersion}` },
|
|
1870
|
+
userEmail
|
|
1871
|
+
);
|
|
1872
|
+
trackEvent("node.revert", { nestId, nodeId, targetVersion });
|
|
1873
|
+
return c.json({ ok: true, version, node: toNodeResponse(node) });
|
|
1874
|
+
});
|
|
1687
1875
|
nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
1688
1876
|
const nestId = c.req.param("nestId");
|
|
1689
1877
|
const nodeId = c.req.param("nodeId");
|
|
@@ -1740,6 +1928,11 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
1740
1928
|
}
|
|
1741
1929
|
response.version = approved;
|
|
1742
1930
|
response.status = "published";
|
|
1931
|
+
if (isMarkdownFormat(c)) {
|
|
1932
|
+
return c.body(nodeAsMarkdown(response, nodeId), 200, {
|
|
1933
|
+
"Content-Type": "text/markdown; charset=utf-8"
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1743
1936
|
return c.json({ node: response });
|
|
1744
1937
|
}
|
|
1745
1938
|
}
|
|
@@ -1748,6 +1941,11 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
1748
1941
|
const pending = getPendingReview(nestId, nodeId);
|
|
1749
1942
|
response.pendingReviewBy = pending?.requestedBy ?? null;
|
|
1750
1943
|
}
|
|
1944
|
+
if (isMarkdownFormat(c)) {
|
|
1945
|
+
return c.body(nodeAsMarkdown(response, nodeId), 200, {
|
|
1946
|
+
"Content-Type": "text/markdown; charset=utf-8"
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1751
1949
|
return c.json({ node: response });
|
|
1752
1950
|
});
|
|
1753
1951
|
nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
@@ -1792,6 +1990,11 @@ nodeRoutes.delete("/:nodeId{.+}", async (c) => {
|
|
|
1792
1990
|
const nestId = c.req.param("nestId");
|
|
1793
1991
|
const nodeId = c.req.param("nodeId");
|
|
1794
1992
|
const { storage } = engineCache.get(nestId);
|
|
1993
|
+
if (isStewardshipEnabled(nestId) && getPendingReview(nestId, nodeId)) {
|
|
1994
|
+
throw new LockedError(
|
|
1995
|
+
"This document is awaiting steward review and is locked. Approve or reject the pending review before deleting."
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1795
1998
|
try {
|
|
1796
1999
|
await storage.deleteDocument(nodeId);
|
|
1797
2000
|
} catch {
|
|
@@ -2017,6 +2220,32 @@ function compilePrompt(prompt, nestId, titles) {
|
|
|
2017
2220
|
};
|
|
2018
2221
|
}
|
|
2019
2222
|
|
|
2223
|
+
// src/nodes/readable-body.ts
|
|
2224
|
+
async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
|
|
2225
|
+
if (!isPublicReader(nestId, userId)) return workingBody;
|
|
2226
|
+
const approved = getApprovedVersion(nestId, nodeId);
|
|
2227
|
+
if (approved == null) return "";
|
|
2228
|
+
try {
|
|
2229
|
+
const { versions } = engineCache.get(nestId);
|
|
2230
|
+
const raw = await versions.reconstructVersion(nodeId, approved);
|
|
2231
|
+
return bodyOnly(nodeId, raw);
|
|
2232
|
+
} catch {
|
|
2233
|
+
return "";
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
2237
|
+
if (!isStewardshipEnabled(nestId)) return workingBody;
|
|
2238
|
+
const approved = getApprovedVersion(nestId, nodeId);
|
|
2239
|
+
if (approved == null) return null;
|
|
2240
|
+
try {
|
|
2241
|
+
const { versions } = engineCache.get(nestId);
|
|
2242
|
+
const raw = await versions.reconstructVersion(nodeId, approved);
|
|
2243
|
+
return bodyOnly(nodeId, raw);
|
|
2244
|
+
} catch {
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2020
2249
|
// src/nodes/query-routes.ts
|
|
2021
2250
|
var queryRoutes = new Hono5();
|
|
2022
2251
|
function approxTokens(text) {
|
|
@@ -2087,9 +2316,15 @@ queryRoutes.post("/context", async (c) => {
|
|
|
2087
2316
|
const beforePermission = documents.length;
|
|
2088
2317
|
const accessible = filterAccessible(nestId, userId, userEmail, documents);
|
|
2089
2318
|
const permissionFiltered = beforePermission - accessible.length;
|
|
2319
|
+
const readable = await Promise.all(
|
|
2320
|
+
accessible.map(async (doc) => ({
|
|
2321
|
+
...doc,
|
|
2322
|
+
body: await resolveReadableBody(nestId, doc.id, userId, doc.body || "")
|
|
2323
|
+
}))
|
|
2324
|
+
);
|
|
2090
2325
|
const included = [];
|
|
2091
2326
|
let tokenCount = 0;
|
|
2092
|
-
for (const doc of
|
|
2327
|
+
for (const doc of readable) {
|
|
2093
2328
|
const block = formatContextBlock(doc);
|
|
2094
2329
|
const blockTokens = approxTokens(block);
|
|
2095
2330
|
if (tokenCount + blockTokens > maxTokens && included.length > 0) break;
|
|
@@ -2220,6 +2455,64 @@ queryRoutes.get("/context", async (c) => {
|
|
|
2220
2455
|
const content = await storage.readContextMd();
|
|
2221
2456
|
return c.json({ content: content || "" });
|
|
2222
2457
|
});
|
|
2458
|
+
queryRoutes.get("/export", async (c) => {
|
|
2459
|
+
if (!isMarkdownFormat(c)) {
|
|
2460
|
+
throw new ValidationError("format=markdown is required");
|
|
2461
|
+
}
|
|
2462
|
+
const nestId = c.req.param("nestId");
|
|
2463
|
+
const { storage, query: queryEngine } = engineCache.get(nestId);
|
|
2464
|
+
const selector = c.req.query("selector")?.trim() || null;
|
|
2465
|
+
let documents;
|
|
2466
|
+
if (selector) {
|
|
2467
|
+
const result = await queryEngine.query(selector, { hops: 2, full: true });
|
|
2468
|
+
documents = result.documents;
|
|
2469
|
+
} else {
|
|
2470
|
+
documents = await storage.discoverDocuments();
|
|
2471
|
+
}
|
|
2472
|
+
const userId = c.get("userId");
|
|
2473
|
+
const userEmail = resolveCallerEmail(userId);
|
|
2474
|
+
const accessible = filterAccessible(nestId, userId, userEmail, documents);
|
|
2475
|
+
const governed = isStewardshipEnabled(nestId);
|
|
2476
|
+
const resolved = await Promise.all(
|
|
2477
|
+
accessible.map(async (n) => {
|
|
2478
|
+
const body = await resolveExportBody(nestId, n.id, n.body || "");
|
|
2479
|
+
if (body == null) return null;
|
|
2480
|
+
return {
|
|
2481
|
+
id: n.id,
|
|
2482
|
+
title: n.frontmatter.title,
|
|
2483
|
+
tags: n.frontmatter.tags || [],
|
|
2484
|
+
status: governed ? "published" : n.frontmatter.status,
|
|
2485
|
+
body
|
|
2486
|
+
};
|
|
2487
|
+
})
|
|
2488
|
+
);
|
|
2489
|
+
const fields = resolved.filter(
|
|
2490
|
+
(f) => f != null
|
|
2491
|
+
);
|
|
2492
|
+
const maxParam = parseInt(c.req.query("max_tokens") ?? "", 10);
|
|
2493
|
+
const maxTokens = Number.isFinite(maxParam) && maxParam > 0 ? maxParam : null;
|
|
2494
|
+
let included = fields;
|
|
2495
|
+
if (maxTokens) {
|
|
2496
|
+
const kept = [];
|
|
2497
|
+
let tokens = 0;
|
|
2498
|
+
for (const f of fields) {
|
|
2499
|
+
const t = approxTokens(nodeToMarkdown(f));
|
|
2500
|
+
if (tokens + t > maxTokens && kept.length > 0) break;
|
|
2501
|
+
kept.push(f);
|
|
2502
|
+
tokens += t;
|
|
2503
|
+
}
|
|
2504
|
+
included = kept;
|
|
2505
|
+
}
|
|
2506
|
+
trackEvent("nest.export", {
|
|
2507
|
+
nestId,
|
|
2508
|
+
count: included.length,
|
|
2509
|
+
selector,
|
|
2510
|
+
truncated_by_budget: fields.length - included.length
|
|
2511
|
+
});
|
|
2512
|
+
return c.body(nodesToMarkdown(included), 200, {
|
|
2513
|
+
"Content-Type": "text/markdown; charset=utf-8"
|
|
2514
|
+
});
|
|
2515
|
+
});
|
|
2223
2516
|
queryRoutes.post("/publish", async (c) => {
|
|
2224
2517
|
const body = await c.req.json();
|
|
2225
2518
|
if (!body.documents?.length && !body.context_md) {
|
|
@@ -3174,8 +3467,19 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
3174
3467
|
const nodeId = c.req.param("nodeId");
|
|
3175
3468
|
const allVersions = getVersions(nestId, nodeId);
|
|
3176
3469
|
const approved = getApprovedVersion(nestId, nodeId);
|
|
3470
|
+
const { versions: versionManager } = engineCache.get(nestId);
|
|
3471
|
+
const withContent = await Promise.all(
|
|
3472
|
+
allVersions.map(async (v) => {
|
|
3473
|
+
try {
|
|
3474
|
+
const raw = await versionManager.reconstructVersion(nodeId, v.version);
|
|
3475
|
+
return { ...v, content: bodyOnly(nodeId, raw) };
|
|
3476
|
+
} catch {
|
|
3477
|
+
return v;
|
|
3478
|
+
}
|
|
3479
|
+
})
|
|
3480
|
+
);
|
|
3177
3481
|
return c.json({
|
|
3178
|
-
versions:
|
|
3482
|
+
versions: withContent,
|
|
3179
3483
|
approvedVersion: approved,
|
|
3180
3484
|
currentVersion: allVersions[0]?.version || 0
|
|
3181
3485
|
});
|
|
@@ -3473,6 +3777,7 @@ function createApp() {
|
|
|
3473
3777
|
version: "0.1.0",
|
|
3474
3778
|
auth_mode: config.AUTH_MODE,
|
|
3475
3779
|
logo_url: config.LOGO_URL,
|
|
3780
|
+
promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
|
|
3476
3781
|
...isSuspended() && { suspended_reason: getSuspensionReason() }
|
|
3477
3782
|
})
|
|
3478
3783
|
);
|
|
@@ -3636,11 +3941,13 @@ function createApp() {
|
|
|
3636
3941
|
} else if (c.req.method !== "GET" && !isStewardActionPath) {
|
|
3637
3942
|
required = "write";
|
|
3638
3943
|
}
|
|
3944
|
+
const isNodeRevert = c.req.method === "POST" && parts.length >= 4 && parts[parts.length - 1] === "revert";
|
|
3639
3945
|
let stewardEditorBypass = false;
|
|
3640
3946
|
if (required === "write" && permission === "read" && parts[1] === "nodes") {
|
|
3641
3947
|
const userEmail = resolveCallerEmail(userId);
|
|
3642
|
-
if (parts.length >= 3 && (c.req.method === "PATCH" || c.req.method === "DELETE")) {
|
|
3643
|
-
const
|
|
3948
|
+
if (parts.length >= 3 && (c.req.method === "PATCH" || c.req.method === "DELETE" || isNodeRevert)) {
|
|
3949
|
+
const idParts = isNodeRevert ? parts.slice(2, -1) : parts.slice(2);
|
|
3950
|
+
const rawNodeId = idParts.join("/");
|
|
3644
3951
|
let nodeId = rawNodeId;
|
|
3645
3952
|
try {
|
|
3646
3953
|
nodeId = decodeURIComponent(rawNodeId);
|
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
getReviewQueue,
|
|
7
7
|
reject,
|
|
8
8
|
submitForReview
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-E7E3JMQR.js";
|
|
10
|
+
import "./chunk-LO54V4AU.js";
|
|
11
11
|
import {
|
|
12
12
|
canUserApprove
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-
|
|
13
|
+
} from "./chunk-5MT4ZBVF.js";
|
|
14
|
+
import "./chunk-G62P54ET.js";
|
|
15
15
|
export {
|
|
16
16
|
approve,
|
|
17
17
|
canUserApprove,
|