@promptowl/contextnest-community 1.17.0 → 1.19.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 -1
- package/README.md +6 -2
- package/dist/{chunk-YF4OFT2V.js → chunk-3GMGLYTZ.js} +36 -5
- package/dist/{chunk-UXJSO54R.js → chunk-BXUVIAMP.js} +59 -14
- package/dist/{chunk-SNFGU5Q7.js → chunk-ENDHMQPD.js} +602 -327
- package/dist/chunk-F66VCBLA.js +136 -0
- package/dist/{chunk-4YGJSUJC.js → chunk-FBDEESNJ.js} +30 -22
- package/dist/{chunk-HWDPCPYT.js → chunk-GSYMJ3A4.js} +2 -2
- package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
- package/dist/chunk-TNAZJYL5.js +379 -0
- package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
- package/dist/{chunk-GUNJTORH.js → chunk-YVMSM7LS.js} +14 -1
- package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
- package/dist/engine-V4DY4PYN.js +14 -0
- package/dist/external-edit-service-3NK5DGLA.js +33 -0
- package/dist/{grants-service-4NXPBYWD.js → grants-service-CADI6LIB.js} +4 -4
- package/dist/index.js +580 -513
- 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-LK4M2C43.js} +8 -8
- package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-JAFSU5GN.js} +9 -5
- package/dist/{version-service-EFZGGL3Y.js → version-service-G7SFFKFO.js} +7 -5
- package/dist/web3/assets/index-C26nM5Kn.css +1 -0
- package/dist/web3/assets/index-C4oalc2K.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
package/CONFIGURATION.md
CHANGED
|
@@ -60,7 +60,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
60
60
|
| `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`. |
|
|
61
61
|
| `MANUAL_SIGN_IN` | `open` | Email + password sign-in mode. `open` = anyone may log in and self-register a new account; `invite-only` = existing/invited users may log in but brand-new self-registration returns `403` (the admin provisions accounts via invite/share/steward and shares the password — there is no self-service "set password", which would be account takeover without email verification); `disabled` = no email/password sign-in at all (`POST /auth/login` and `POST /auth/register` return `403`). Independent of `PROMPTOWL_SIGN_IN_GATE`, so the two methods are controlled separately (e.g. `invite-only` manual + `admin-only` PromptOwl). Also settable from Settings → General. The server refuses `disabled` while PromptOwl sign-in is also `disabled` (that would leave no way to log in). Unknown values fall back to `open`. |
|
|
62
62
|
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **Official deployment only — leave unset on self-hosted.** Shared HMAC secret enabling the one-click "Open Community" SSO auto-login from PromptOwl. Must exactly match the same-named var on PromptOwl. When unset, `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. |
|
|
63
|
-
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`).
|
|
63
|
+
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Three uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — only when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. (2) **Set this whenever `OIDC_ENABLED=true`.** It's the base for the OIDC `redirect_uri` sent to your IdP and replayed at token exchange. Unset, that URI is derived from the incoming request's `Host` header — fine when your proxy overwrites `Host` with a trusted value, but a proxy that passes an attacker-supplied `Host` through would feed it straight into the redirect URI. Setting this pins the value regardless of what the proxy forwards. (Your IdP's own redirect-URI allowlist is a second line of defence, not a substitute.) (3) **Set this wherever `POST /nests/:id/context` citations reach users.** The `url` on each node and the `_source:` line in each context block are built from it; unset, they fall back to the request origin, so a proxy forwarding an untrusted `Host` would put an attacker-influenced URL in front of both the model and the reader as a trustworthy citation. |
|
|
64
64
|
| `OIDC_ENABLED` | `false` | Turn on generic OIDC single sign-on (`GET /auth/oidc/login` / `GET /auth/oidc/callback`). Requires `OIDC_ISSUER`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET` — the login-page button only appears once all three are set. Also editable from Settings → Single sign-on. See [Single sign-on (OIDC)](#single-sign-on-oidc). |
|
|
65
65
|
| `OIDC_ISSUER` | `""` | OIDC issuer URL, e.g. `https://login.microsoftonline.com/<tenant>/v2.0` (Microsoft Entra ID) or `https://accounts.google.com` (Google). **`https://` only** — a non-https value is rejected with a warning and SSO stays off. Must serve `<issuer>/.well-known/openid-configuration`. |
|
|
66
66
|
| `OIDC_CLIENT_ID` | `""` | Application (client) ID from your IdP app registration. |
|
package/README.md
CHANGED
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
ContextNest Community Edition is a self-hosted server that lets you:
|
|
12
12
|
|
|
13
|
-
- Store, version, and govern markdown-based context documents ("nests")
|
|
13
|
+
- Store, version, and govern markdown-based context documents ("nests"), filed in folders you can reorganize as the vault grows
|
|
14
14
|
- Import an existing folder or vault of markdown files in one step
|
|
15
15
|
- Export a nest as a portable bundle and re-import it on another self-hosted host
|
|
16
16
|
- Apply stewardship workflows — draft, pending review, approved
|
|
17
17
|
- Share nests with collaborators or publish them read-only to the public
|
|
18
|
-
- Serve approved context to AI agents via MCP or HTTP
|
|
18
|
+
- Serve approved context to AI agents via MCP or HTTP — connect one by pasting a generated setup prompt or downloading a ready `.env`
|
|
19
19
|
- Sync with the PromptOwl hosted platform for multi-user collaboration
|
|
20
20
|
|
|
21
21
|
The server runs locally or on your own infrastructure. Your PromptOwl account handles authentication, entitlement, and governance metadata.
|
|
@@ -99,8 +99,12 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
|
|
|
99
99
|
| Admin password reset + user removal (in-platform) | ✅ | ✅ |
|
|
100
100
|
| Wiki backlinks, outline, hover-preview, link health | ✅ | ✅ |
|
|
101
101
|
| Rich editor — tables, callouts, toggles, code highlight, find/replace, image & video upload | ✅ | ✅ |
|
|
102
|
+
| Folder organization — nested folders, move documents, lazy folder tree | ✅ | ✅ |
|
|
103
|
+
| Scales to large vaults — nest listings served from a document index, not a disk crawl | ✅ | ✅ |
|
|
102
104
|
| Steward version revert | ✅ | ✅ |
|
|
103
105
|
| MCP server for AI agents | ✅ | ✅ |
|
|
106
|
+
| One-shot agent setup — generated connect prompt + `.env` download | ✅ | ✅ |
|
|
107
|
+
| Several API keys per account — one credential per client, rotate one at a time | ✅ | ✅ |
|
|
104
108
|
| Centralized multi-tenant admin console | — | ✅ |
|
|
105
109
|
| Single sign-on (OIDC — Entra ID, Google, Okta, Keycloak) | ✅ | ✅ |
|
|
106
110
|
| SAML / SCIM provisioning | — | ✅ |
|
|
@@ -11,16 +11,16 @@ import {
|
|
|
11
11
|
resolveNestPermission,
|
|
12
12
|
resolveTeamRolesForUser,
|
|
13
13
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-ENDHMQPD.js";
|
|
15
15
|
import {
|
|
16
16
|
ConflictError,
|
|
17
17
|
ValidationError
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-YVMSM7LS.js";
|
|
19
19
|
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,
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
buildTitleMap,
|
|
3
3
|
insertOrReplace,
|
|
4
4
|
nowExpr
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ENDHMQPD.js";
|
|
6
6
|
import {
|
|
7
7
|
getDb
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-HYSTFMFG.js";
|
|
9
9
|
|
|
10
10
|
// src/governance/version-service.ts
|
|
11
11
|
import { createHash } from "crypto";
|
|
@@ -178,32 +178,76 @@ async function getNodeTags(nestId, nodeId) {
|
|
|
178
178
|
return [];
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
function decideDisplayStatus(hasPendingReview, current, approvedVersion) {
|
|
182
|
+
if (hasPendingReview) return "pending_review";
|
|
183
|
+
if (!current) return "draft";
|
|
184
|
+
if (current.status === "published" || current.status === "approved") {
|
|
185
|
+
if (approvedVersion === current.version) {
|
|
186
|
+
return current.status === "published" ? "published" : "approved";
|
|
187
|
+
}
|
|
188
|
+
return "draft";
|
|
189
|
+
}
|
|
190
|
+
if (current.status === "rejected") return "rejected";
|
|
191
|
+
return "draft";
|
|
192
|
+
}
|
|
181
193
|
async function getDisplayStatus(nestId, nodeId) {
|
|
182
194
|
const db = getDb();
|
|
183
195
|
const pending = await db.get(
|
|
184
196
|
"SELECT 1 FROM review_requests WHERE nest_id = ? AND node_id = ? AND status = 'pending' LIMIT 1",
|
|
185
197
|
[nestId, nodeId]
|
|
186
198
|
);
|
|
187
|
-
if (pending) return "pending_review";
|
|
188
199
|
const current = await db.get(
|
|
189
200
|
`SELECT version, status FROM node_versions
|
|
190
201
|
WHERE nest_id = ? AND node_id = ?
|
|
191
202
|
ORDER BY version DESC LIMIT 1`,
|
|
192
203
|
[nestId, nodeId]
|
|
193
204
|
);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
205
|
+
const approved = current && (current.status === "published" || current.status === "approved") ? await db.get(
|
|
206
|
+
"SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?",
|
|
207
|
+
[nestId, nodeId]
|
|
208
|
+
) : void 0;
|
|
209
|
+
return decideDisplayStatus(!!pending, current, approved?.approved_version);
|
|
210
|
+
}
|
|
211
|
+
async function getDisplayStatuses(nestId) {
|
|
212
|
+
const db = getDb();
|
|
213
|
+
const [pendingRows, currentRows, approvedRows] = await Promise.all([
|
|
214
|
+
db.all(
|
|
215
|
+
"SELECT DISTINCT node_id FROM review_requests WHERE nest_id = ? AND status = 'pending'",
|
|
216
|
+
[nestId]
|
|
217
|
+
),
|
|
218
|
+
// The latest version per node, via a grouped self-join — portable across
|
|
219
|
+
// the SQLite and Postgres adapters (no window functions, no row values).
|
|
220
|
+
db.all(
|
|
221
|
+
`SELECT nv.node_id, nv.version, nv.status
|
|
222
|
+
FROM node_versions nv
|
|
223
|
+
JOIN (SELECT node_id, MAX(version) AS max_version
|
|
224
|
+
FROM node_versions WHERE nest_id = ? GROUP BY node_id) latest
|
|
225
|
+
ON latest.node_id = nv.node_id AND latest.max_version = nv.version
|
|
226
|
+
WHERE nv.nest_id = ?`,
|
|
227
|
+
[nestId, nestId]
|
|
228
|
+
),
|
|
229
|
+
db.all(
|
|
230
|
+
"SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
|
|
231
|
+
[nestId]
|
|
232
|
+
)
|
|
233
|
+
]);
|
|
234
|
+
const pending = new Set(pendingRows.map((r) => r.node_id));
|
|
235
|
+
const approved = new Map(
|
|
236
|
+
approvedRows.map((r) => [r.node_id, r.approved_version])
|
|
237
|
+
);
|
|
238
|
+
const current = new Map(currentRows.map((r) => [r.node_id, r]));
|
|
239
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const nodeId of /* @__PURE__ */ new Set([...pending, ...current.keys()])) {
|
|
241
|
+
statuses.set(
|
|
242
|
+
nodeId,
|
|
243
|
+
decideDisplayStatus(
|
|
244
|
+
pending.has(nodeId),
|
|
245
|
+
current.get(nodeId),
|
|
246
|
+
approved.get(nodeId)
|
|
247
|
+
)
|
|
199
248
|
);
|
|
200
|
-
if (approved?.approved_version === current.version) {
|
|
201
|
-
return current.status === "published" ? "published" : "approved";
|
|
202
|
-
}
|
|
203
|
-
return "draft";
|
|
204
249
|
}
|
|
205
|
-
|
|
206
|
-
return "draft";
|
|
250
|
+
return statuses;
|
|
207
251
|
}
|
|
208
252
|
async function listMyDrafts(params) {
|
|
209
253
|
if (params.nestIds.length === 0) return { drafts: [], total: 0 };
|
|
@@ -286,5 +330,6 @@ export {
|
|
|
286
330
|
checkConflict,
|
|
287
331
|
getNodeTags,
|
|
288
332
|
getDisplayStatus,
|
|
333
|
+
getDisplayStatuses,
|
|
289
334
|
listMyDrafts
|
|
290
335
|
};
|