@promptowl/contextnest-community 1.22.0 → 1.23.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 +4 -2
- package/README.md +1 -1
- package/dist/{chunk-PX3P4FTU.js → chunk-2YPY2HGZ.js} +2 -2
- package/dist/{chunk-6WESEA75.js → chunk-5CVZMHHB.js} +2 -2
- package/dist/{chunk-OQXZ43HG.js → chunk-MLQU4I5J.js} +40 -10
- package/dist/{chunk-W5ILNGPD.js → chunk-QLXC6542.js} +34 -6
- package/dist/{chunk-J2OQ3MEB.js → chunk-QWNXWRWO.js} +39 -1
- package/dist/{chunk-MY4JIWQD.js → chunk-UOMW7TT6.js} +1 -1
- package/dist/{chunk-ZAV4QUZA.js → chunk-XYD6V2LH.js} +4 -4
- package/dist/{client-CSCHXUNX.js → client-LWDMNKX3.js} +1 -1
- package/dist/{engine-XKRP7GOQ.js → engine-QFF2IA3G.js} +2 -2
- package/dist/{external-edit-service-TXW7SXSF.js → external-edit-service-F7D3VSA2.js} +3 -3
- package/dist/{grants-service-UZ2MGDO5.js → grants-service-TBGTCO5K.js} +2 -2
- package/dist/index.js +901 -226
- package/dist/{migrations.postgres-KKICIH2C.js → migrations.postgres-APCVSYUE.js} +16 -1
- package/dist/{review-service-XFWTENTN.js → review-service-A7PNLWG5.js} +6 -6
- package/dist/{stewardship-service-5HBQGDMK.js → stewardship-service-32UHIRQ5.js} +3 -3
- package/dist/{version-service-TKNYJMCM.js → version-service-RJ6CWZ3O.js} +3 -3
- package/dist/web3/assets/index-BJ-LRNis.js +1382 -0
- package/dist/web3/assets/index-DPMEt-A_.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +1 -1
- package/dist/web3/assets/index-C2hW_9j9.js +0 -1382
- package/dist/web3/assets/index-T4pilFNL.css +0 -1
package/CONFIGURATION.md
CHANGED
|
@@ -59,7 +59,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
59
59
|
| `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 the database (`server_settings` table) so it survives a rebuild and reaches every instance — see [Runtime settings persistence](#runtime-settings-persistence). Setting it here in the deploy environment takes precedence on the next boot. |
|
|
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
|
-
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **
|
|
62
|
+
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **Legacy — 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. Superseded by the DB-backed **Settings → Community sites** list (`/admin/community-sites`), which supports multiple official sites each with their own name/url/secret/active flag — this env var still works as an implicit extra site for backward compatibility. When no site is configured (env var unset and no DB rows), `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. See `API.md → Community sites`. |
|
|
63
63
|
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Two uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — whenever a ticket-signing secret is set (`OFFICIAL_COMMUNITY_SSO_SECRET` on the official deployment, `MCP_SIGNING_SECRET` on a self-hosted one); with neither set no ticket verifies at all, so the audience check is moot. (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 email- or invite-gated publish links are used.** The magic link mailed by `POST /p/:slug/gate` is built from it; unset, it falls back to the request origin, so a proxy forwarding an attacker-supplied `Host` would send the recipient a one-time access token pointing at the attacker's domain — the classic reset-link poisoning. (4) **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`. |
|
|
@@ -67,6 +67,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
67
67
|
| `OIDC_CLIENT_SECRET` | `""` | Client secret from your IdP app registration. Write-only on the Settings API — `GET /admin/settings` reports only `oidc_client_secret_set: true/false`, never the value. |
|
|
68
68
|
| `OIDC_ALLOWED_DOMAINS` | `""` (any) | Comma-separated email-domain allowlist, e.g. `acme.com, contractors.acme.com`. When set, only accounts whose asserted email is on a listed domain may sign in (others bounce with `domain_not_allowed`). Empty allows any domain the IdP asserts. |
|
|
69
69
|
| `OIDC_AUTO_PROVISION` | `true` | Create a user automatically on first successful OIDC sign-in (display name from the `name` claim). Set `false` to allow only pre-existing (invited/registered) users — unknown emails bounce with `not_invited`. |
|
|
70
|
+
| `PEOPLE_SUGGEST_ALL_USERS` | `false` | Widen the add-a-person suggestions (`GET /people/suggest`, behind every "add a person" field) and the `@mention` pickers from *people the caller already shares a nest or team with* to **every registered account on this server**. **Leave this off on a server that hosts more than one organisation** — the PromptOwl-hosted deployment does, and turning it on there would offer one customer's staff another customer's email addresses. On a single-company self-hosted server the whole directory is the useful answer, which is what this is for. Suggestions never gate the invite either way: an address that appears in no list is still valid to type. In the `@` pickers the widening is an invite: a write+ author mentioning someone off the nest adds them as a `read` collaborator before notifying (a read-only author's mention of an outsider reaches nobody). A server admin always sees every account (they administer them). Also editable from **Settings → General → People directory**. |
|
|
70
71
|
| `OIDC_DEPARTMENT_TAGGING` | `false` | Auto-tag newly **created** documents with the creator's directory department: `dept:<slugified-department>` (lowercase, spaces → dashes, e.g. `dept:customer-success`) is appended to the document's tags, deduped against user-supplied tags. Applies on create only — never on update, never retroactively — and a user without a stored department is a silent no-op. The department is captured from the OIDC `department` ID-token claim on every SSO login (a login without the claim clears it, so directory moves propagate), so this is only meaningful when your IdP emits that claim — see [Department auto-tagging](#department-auto-tagging). Also editable from Settings → Single sign-on. |
|
|
71
72
|
| `SSO_TOKEN_EXCHANGE_ENABLED` | `false` | Master switch for `POST /auth/token-exchange` — an external agent exchanges an IdP ID token for a short-lived MCP bearer. Also needs `MCP_SIGNING_SECRET` and at least one provider; otherwise the endpoint returns `404`. Security-critical — see [Agent SSO (token exchange)](#agent-sso-token-exchange). |
|
|
72
73
|
| `MCP_SIGNING_SECRET` | `""` | HMAC secret **this** server signs its minted MCP bearers with (distinct from `OFFICIAL_COMMUNITY_SSO_SECRET`, so self-hosted deployments can issue their own). Long random string. `/index` and `/mcp` accept a ticket signed with either secret. Write-only on the Settings API. **Clearing it is the revocation switch** — turning `SSO_TOKEN_EXCHANGE_ENABLED` off stops minting but already-minted bearers stay valid until they expire. |
|
|
@@ -152,7 +153,8 @@ plain-text `fallbackText` for clients that cannot render cards.
|
|
|
152
153
|
Per-nest connector rows post the same Adaptive Card format to a nest-specific
|
|
153
154
|
webhook, with per-event filtering: `POST /nests/:id/connectors` with a JSON
|
|
154
155
|
body like `{"channel": "teams", "url": "https://…", "events":
|
|
155
|
-
["review_requested", "review_rejected"]}` (`events`
|
|
156
|
+
["review_requested", "review_rejected"]}` (`events` is any of `review_requested`,
|
|
157
|
+
`review_approved`, `review_rejected`, `run_failed`, `mention`, or `["*"]`; the
|
|
156
158
|
`url` may be an `env:KEY` reference into the nest's env store). Manage rows
|
|
157
159
|
with `GET`/`PATCH`/`DELETE` on the same path.
|
|
158
160
|
|
package/README.md
CHANGED
|
@@ -165,4 +165,4 @@ The Software injects content into large language models. AI output may be inaccu
|
|
|
165
165
|
**Copyright © 2026 Promptowl LLC.** All rights reserved.
|
|
166
166
|
"ContextNest" and "PromptOwl" are trademarks of Promptowl LLC.
|
|
167
167
|
|
|
168
|
-
Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
|
|
168
|
+
Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveNestPermission,
|
|
12
12
|
resolveTeamRolesForUser,
|
|
13
13
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-QLXC6542.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-QWNXWRWO.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-QLXC6542.js";
|
|
6
6
|
import {
|
|
7
7
|
getDb
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QWNXWRWO.js";
|
|
9
9
|
|
|
10
10
|
// src/governance/version-service.ts
|
|
11
11
|
import { createHash } from "crypto";
|
|
@@ -4,19 +4,19 @@ import {
|
|
|
4
4
|
resolveNestWideRoles,
|
|
5
5
|
resolveStewardsForNode,
|
|
6
6
|
stewardCoverageForUser
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2YPY2HGZ.js";
|
|
8
8
|
import {
|
|
9
9
|
grantCoversNode,
|
|
10
10
|
listUserGrants,
|
|
11
11
|
resolveNodeGrant
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-UOMW7TT6.js";
|
|
13
13
|
import {
|
|
14
14
|
createVersion,
|
|
15
15
|
getApprovedVersion,
|
|
16
16
|
getApprovedVersions,
|
|
17
17
|
getCurrentVersion,
|
|
18
18
|
setApprovedVersion
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-5CVZMHHB.js";
|
|
20
20
|
import {
|
|
21
21
|
buildDocContext,
|
|
22
22
|
buildTitleMap,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
insertOrReplace,
|
|
27
27
|
isPublicReader,
|
|
28
28
|
isStewardshipEnabled,
|
|
29
|
+
markNotificationsReadBySubject,
|
|
29
30
|
nestName,
|
|
30
31
|
notifyEmailForNest,
|
|
31
32
|
notifyReviewRequested,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
resolveNestPermission,
|
|
37
38
|
sendEmailToRecipient,
|
|
38
39
|
titleForNode
|
|
39
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-QLXC6542.js";
|
|
40
41
|
import {
|
|
41
42
|
ConflictError,
|
|
42
43
|
NotFoundError,
|
|
@@ -45,7 +46,7 @@ import {
|
|
|
45
46
|
import {
|
|
46
47
|
config,
|
|
47
48
|
getDb
|
|
48
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-QWNXWRWO.js";
|
|
49
50
|
|
|
50
51
|
// src/governance/review-service.ts
|
|
51
52
|
import { v4 as uuid2 } from "uuid";
|
|
@@ -157,7 +158,9 @@ async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
|
|
|
157
158
|
for (const r of rows) {
|
|
158
159
|
if (r.from_node === r.to_node) {
|
|
159
160
|
throw new ConflictError(
|
|
160
|
-
`This step loops back on itself: ${await nodeTrail(db, nestId, [r.from_node])}.
|
|
161
|
+
`This step loops back on itself: ${await nodeTrail(db, nestId, [r.from_node])}.
|
|
162
|
+
|
|
163
|
+
A flow can't return to a step it already ran.`
|
|
161
164
|
);
|
|
162
165
|
}
|
|
163
166
|
nodes.add(r.from_node);
|
|
@@ -544,6 +547,7 @@ async function notifySlack(text) {
|
|
|
544
547
|
|
|
545
548
|
// src/notify/dispatch.ts
|
|
546
549
|
var EVENT_EMOJI = {
|
|
550
|
+
mention: ":speech_balloon:",
|
|
547
551
|
review_requested: ":memo:",
|
|
548
552
|
review_approved: ":white_check_mark:",
|
|
549
553
|
review_rejected: ":x:",
|
|
@@ -610,6 +614,27 @@ async function resolveUrl(nestId, raw) {
|
|
|
610
614
|
}
|
|
611
615
|
return isSafeConnectorUrl(raw) ? raw : null;
|
|
612
616
|
}
|
|
617
|
+
async function hasAnyConnector(nestId, kind) {
|
|
618
|
+
if ((config.SLACK_WEBHOOK_URL || config.MSTEAMS_WEBHOOK_URL) && !DIGEST_DELIVERED_KINDS.has(kind)) {
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const rows = await getDb().all(
|
|
623
|
+
"SELECT events FROM connectors WHERE nest_id = ? AND enabled = 1",
|
|
624
|
+
[nestId]
|
|
625
|
+
);
|
|
626
|
+
return rows.some((r) => {
|
|
627
|
+
try {
|
|
628
|
+
const kinds = JSON.parse(r.events);
|
|
629
|
+
return kinds.includes("*") || kinds.includes(kind);
|
|
630
|
+
} catch {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
613
638
|
async function dispatchEvent(ev) {
|
|
614
639
|
const db = getDb();
|
|
615
640
|
const nestName2 = await resolveNestName(ev.nestId);
|
|
@@ -1211,7 +1236,8 @@ async function deletionTargetContext(request, baseUrl) {
|
|
|
1211
1236
|
async function notifyDeletionRequested(request, baseUrl) {
|
|
1212
1237
|
try {
|
|
1213
1238
|
const ctx = await deletionTargetContext(request, baseUrl);
|
|
1214
|
-
const
|
|
1239
|
+
const isArchiveRequest = request.targetType === "nest_archive";
|
|
1240
|
+
const line = isArchiveRequest ? `Archive requested for ${ctx.noun} *${ctx.label}* by ${request.requestedBy} \u2014 "${request.reason}"` : `Deletion requested for ${ctx.noun} *${ctx.label}* by ${request.requestedBy} \u2014 "${request.reason}"`;
|
|
1215
1241
|
const admins = (await nestAdminEmails(request.nestId)).filter(
|
|
1216
1242
|
(e) => e !== request.requestedBy.toLowerCase()
|
|
1217
1243
|
);
|
|
@@ -1220,7 +1246,7 @@ async function notifyDeletionRequested(request, baseUrl) {
|
|
|
1220
1246
|
admins,
|
|
1221
1247
|
"deletion_requested",
|
|
1222
1248
|
request.id,
|
|
1223
|
-
`${ctx.noun === "document" ? "" : `${ctx.noun} `}"${ctx.docTitle}" was flagged for deletion by ${request.requestedBy} \u2014 "${request.reason}"`
|
|
1249
|
+
isArchiveRequest ? `${ctx.noun} "${ctx.docTitle}" was requested to be archived by ${request.requestedBy} \u2014 "${request.reason}"` : `${ctx.noun === "document" ? "" : `${ctx.noun} `}"${ctx.docTitle}" was flagged for deletion by ${request.requestedBy} \u2014 "${request.reason}"`
|
|
1224
1250
|
);
|
|
1225
1251
|
void dispatchEvent({
|
|
1226
1252
|
kind: "deletion_requested",
|
|
@@ -1267,9 +1293,11 @@ async function notifyDeletionResolved(params) {
|
|
|
1267
1293
|
try {
|
|
1268
1294
|
const { outcome, resolvedBy, note } = params;
|
|
1269
1295
|
const archivedInstead = outcome === "deleted" && params.targetType === "nest_archive";
|
|
1270
|
-
const
|
|
1296
|
+
const isArchiveRequest = params.targetType === "nest_archive";
|
|
1297
|
+
const verb = archivedInstead ? "archived" : outcome === "deleted" ? "deleted" : isArchiveRequest ? "declined the archive request for" : "rejected the deletion of";
|
|
1271
1298
|
const line = `${resolvedBy} ${verb} *${params.docTitle}*${note ? ` \u2014 "${note}"` : ""}`;
|
|
1272
1299
|
const recipient = params.requestedBy.toLowerCase();
|
|
1300
|
+
await markNotificationsReadBySubject(params.requestId, "deletion_requested");
|
|
1273
1301
|
const canInbox = !(params.targetType === "nest" && outcome === "deleted");
|
|
1274
1302
|
if (canInbox && recipient !== resolvedBy.toLowerCase()) {
|
|
1275
1303
|
await inboxInsert(
|
|
@@ -1277,7 +1305,7 @@ async function notifyDeletionResolved(params) {
|
|
|
1277
1305
|
[recipient],
|
|
1278
1306
|
`deletion_${outcome}`,
|
|
1279
1307
|
params.requestId,
|
|
1280
|
-
outcome === "deleted" ? `"${params.docTitle}" was ${archivedInstead ? "archived" : "deleted"} by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1308
|
+
outcome === "deleted" ? `"${params.docTitle}" was ${archivedInstead ? "archived" : "deleted"} by ${resolvedBy} \u2014 your request was accepted` : `Your ${isArchiveRequest ? "archive" : "deletion"} request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1281
1309
|
);
|
|
1282
1310
|
}
|
|
1283
1311
|
void dispatchEvent({
|
|
@@ -1446,6 +1474,7 @@ async function declineDeletion(params) {
|
|
|
1446
1474
|
requestedBy: declined.requestedBy,
|
|
1447
1475
|
resolvedBy: params.declinedBy,
|
|
1448
1476
|
requestId: declined.id,
|
|
1477
|
+
targetType: declined.targetType,
|
|
1449
1478
|
note: params.note,
|
|
1450
1479
|
baseUrl: params.baseUrl
|
|
1451
1480
|
});
|
|
@@ -1507,6 +1536,7 @@ export {
|
|
|
1507
1536
|
envRoutes,
|
|
1508
1537
|
envValues,
|
|
1509
1538
|
isSafeConnectorUrl,
|
|
1539
|
+
hasAnyConnector,
|
|
1510
1540
|
dispatchEvent,
|
|
1511
1541
|
sendTestNotification,
|
|
1512
1542
|
notifyNestEvent,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
getDb,
|
|
10
10
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QWNXWRWO.js";
|
|
12
12
|
import {
|
|
13
13
|
ANON_USER_ID
|
|
14
14
|
} from "./chunk-YB3LKF7U.js";
|
|
@@ -138,6 +138,12 @@ function startTelemetryLoop() {
|
|
|
138
138
|
function nowExpr(db) {
|
|
139
139
|
return db.dialect === "sqlite" ? "datetime('now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS')";
|
|
140
140
|
}
|
|
141
|
+
function suffixOf(db, column, length) {
|
|
142
|
+
return db.dialect === "sqlite" ? `substr(${column}, -${length})` : `right(${column}, ${length})`;
|
|
143
|
+
}
|
|
144
|
+
function nowMsExpr(db) {
|
|
145
|
+
return db.dialect === "sqlite" ? "strftime('%Y-%m-%d %H:%M:%f','now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS.MS')";
|
|
146
|
+
}
|
|
141
147
|
function insertOrIgnore(db, insertSql) {
|
|
142
148
|
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
|
|
143
149
|
}
|
|
@@ -657,6 +663,7 @@ function renderText(message) {
|
|
|
657
663
|
}
|
|
658
664
|
var STATUS_META = {
|
|
659
665
|
approved: { subjectWord: "approved", label: "Approved", color: "#16a34a" },
|
|
666
|
+
mention: { subjectWord: "mentioned you", label: "Mention", color: "#7c3aed" },
|
|
660
667
|
rejected: { subjectWord: "rejected", label: "Rejected", color: "#dc2626" },
|
|
661
668
|
pending_review: {
|
|
662
669
|
subjectWord: "awaiting review",
|
|
@@ -715,6 +722,17 @@ var TEMPLATES = {
|
|
|
715
722
|
lineH: (x) => `${x.doc}${x.vH} has been approved${x.byH}${x.noteH}.`,
|
|
716
723
|
rows: governanceRows
|
|
717
724
|
},
|
|
725
|
+
// A mention names one person, so this is the one template addressed to
|
|
726
|
+
// "you". `note` carries the comment's own words when there are any — a
|
|
727
|
+
// mention with no context reads as a summons.
|
|
728
|
+
mention: {
|
|
729
|
+
heading: (x) => `${x.d.actor || "Someone"} mentioned you in ${x.title}`,
|
|
730
|
+
detailsHeader: "Document details",
|
|
731
|
+
linkLabel: "Open document",
|
|
732
|
+
lineT: (x) => `You were mentioned in ${x.title}${x.byT}${x.noteT}.`,
|
|
733
|
+
lineH: (x) => `You were mentioned in ${x.doc}${x.byH}${x.noteH}.`,
|
|
734
|
+
rows: (x) => [plainRow("Nest", x.d.path)]
|
|
735
|
+
},
|
|
718
736
|
rejected: {
|
|
719
737
|
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
720
738
|
detailsHeader: "Document details",
|
|
@@ -1304,6 +1322,12 @@ async function markNotificationsRead(userEmail, ids) {
|
|
|
1304
1322
|
);
|
|
1305
1323
|
}
|
|
1306
1324
|
}
|
|
1325
|
+
async function markNotificationsReadBySubject(subjectId, kind) {
|
|
1326
|
+
await getDb().run(
|
|
1327
|
+
"UPDATE notifications SET read_at = ? WHERE subject_id = ? AND kind = ? AND read_at IS NULL",
|
|
1328
|
+
[(/* @__PURE__ */ new Date()).toISOString(), subjectId, kind]
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1307
1331
|
|
|
1308
1332
|
// src/governance/teams-service.ts
|
|
1309
1333
|
var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
|
|
@@ -2195,22 +2219,23 @@ var LIVE_ONLY_N = "n.archived_at IS NULL";
|
|
|
2195
2219
|
async function listNests(userId, opts = {}) {
|
|
2196
2220
|
const db = getDb();
|
|
2197
2221
|
const scope = opts.archived ? "archived_at IS NOT NULL" : LIVE_ONLY;
|
|
2222
|
+
const orderBy = opts.archived ? "archived_at DESC" : "created_at DESC";
|
|
2198
2223
|
if (userId === ANON_USER_ID) {
|
|
2199
2224
|
return await db.all(
|
|
2200
|
-
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY
|
|
2225
|
+
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY ${orderBy}`,
|
|
2201
2226
|
[ANON_USER_ID]
|
|
2202
2227
|
);
|
|
2203
2228
|
}
|
|
2204
2229
|
const includeAnon = config.AUTH_MODE === "open" || await isLicenseAdminUserId(userId);
|
|
2205
2230
|
if (!includeAnon) {
|
|
2206
2231
|
return await db.all(
|
|
2207
|
-
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY
|
|
2232
|
+
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY ${orderBy}`,
|
|
2208
2233
|
[userId]
|
|
2209
2234
|
);
|
|
2210
2235
|
}
|
|
2211
2236
|
return await db.all(
|
|
2212
2237
|
`SELECT * FROM nests WHERE (user_id = ? OR user_id = ?) AND ${scope}
|
|
2213
|
-
ORDER BY
|
|
2238
|
+
ORDER BY ${orderBy}`,
|
|
2214
2239
|
[userId, ANON_USER_ID]
|
|
2215
2240
|
);
|
|
2216
2241
|
}
|
|
@@ -2591,8 +2616,8 @@ async function ensureNodeIndex(nestId) {
|
|
|
2591
2616
|
return run;
|
|
2592
2617
|
}
|
|
2593
2618
|
async function rebuildNodeIndex(nestId) {
|
|
2594
|
-
const { engineCache: engineCache2 } = await import("./engine-
|
|
2595
|
-
const { documentsWithSuggestions } = await import("./external-edit-service-
|
|
2619
|
+
const { engineCache: engineCache2 } = await import("./engine-QFF2IA3G.js");
|
|
2620
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-F7D3VSA2.js");
|
|
2596
2621
|
const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
|
|
2597
2622
|
dropDiscoveryCache();
|
|
2598
2623
|
const startedAt = writeGeneration.get(nestId) ?? 0;
|
|
@@ -2826,6 +2851,8 @@ async function opContext(nestId, actor, onProgress) {
|
|
|
2826
2851
|
|
|
2827
2852
|
export {
|
|
2828
2853
|
nowExpr,
|
|
2854
|
+
suffixOf,
|
|
2855
|
+
nowMsExpr,
|
|
2829
2856
|
insertOrIgnore,
|
|
2830
2857
|
insertOrReplace,
|
|
2831
2858
|
trackEvent,
|
|
@@ -2914,6 +2941,7 @@ export {
|
|
|
2914
2941
|
notifyNestInvite,
|
|
2915
2942
|
listNotifications,
|
|
2916
2943
|
markNotificationsRead,
|
|
2944
|
+
markNotificationsReadBySubject,
|
|
2917
2945
|
createTeam,
|
|
2918
2946
|
findTeamByExternalId,
|
|
2919
2947
|
listTeamsForUser,
|
|
@@ -334,6 +334,28 @@ var config = {
|
|
|
334
334
|
get OIDC_DEPARTMENT_TAGGING() {
|
|
335
335
|
return process.env.OIDC_DEPARTMENT_TAGGING === "true";
|
|
336
336
|
},
|
|
337
|
+
/**
|
|
338
|
+
* Widen the add-a-person suggestions (GET /people/suggest) from "people you
|
|
339
|
+
* already work with" to every registered account on this server.
|
|
340
|
+
*
|
|
341
|
+
* OFF BY DEFAULT, and the default is the point. On a single-company
|
|
342
|
+
* self-hosted server every account is a colleague, so the whole directory is
|
|
343
|
+
* the useful answer and this saves asking someone for their address. On the
|
|
344
|
+
* PromptOwl-hosted deployment, one server holds many unrelated customers —
|
|
345
|
+
* so the same list is one customer's staff being offered another customer's
|
|
346
|
+
* email addresses. The hosted deployment leaves this unset; a self-hosted
|
|
347
|
+
* operator who knows their user table is their own staff turns it on.
|
|
348
|
+
*
|
|
349
|
+
* Narrows nothing when off: the default scoping (shared nests and teams,
|
|
350
|
+
* plus everything for a server admin, who administers the accounts anyway)
|
|
351
|
+
* is unchanged. Suggestions never gate the add either way — an address that
|
|
352
|
+
* appears in no list is still valid to type, so this only ever affects what
|
|
353
|
+
* is *offered*, never who can be invited. Superadmin-togglable at runtime
|
|
354
|
+
* via /admin/settings.
|
|
355
|
+
*/
|
|
356
|
+
get PEOPLE_SUGGEST_ALL_USERS() {
|
|
357
|
+
return process.env.PEOPLE_SUGGEST_ALL_USERS === "true";
|
|
358
|
+
},
|
|
337
359
|
/**
|
|
338
360
|
* Path to the .env file the server reads its config from and the license
|
|
339
361
|
* install flow persists PROMPTOWL_KEY into. Defaults UNDER DATA_ROOT (not
|
|
@@ -1719,6 +1741,22 @@ function runMigrations(db) {
|
|
|
1719
1741
|
widenNestVisibilityWithGuard(db);
|
|
1720
1742
|
recordMigration("041_nest_visibility_org");
|
|
1721
1743
|
}
|
|
1744
|
+
if (!hasMigration("042_community_sites")) {
|
|
1745
|
+
db.transaction(() => {
|
|
1746
|
+
db.exec(`
|
|
1747
|
+
CREATE TABLE IF NOT EXISTS community_sites (
|
|
1748
|
+
id TEXT PRIMARY KEY,
|
|
1749
|
+
name TEXT NOT NULL,
|
|
1750
|
+
url TEXT NOT NULL UNIQUE,
|
|
1751
|
+
secret TEXT NOT NULL,
|
|
1752
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
1753
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1754
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1755
|
+
);
|
|
1756
|
+
`);
|
|
1757
|
+
recordMigration("042_community_sites");
|
|
1758
|
+
})();
|
|
1759
|
+
}
|
|
1722
1760
|
}
|
|
1723
1761
|
function fkViolationCounts(db) {
|
|
1724
1762
|
const rows = db.pragma("foreign_key_check");
|
|
@@ -1993,7 +2031,7 @@ async function initDb() {
|
|
|
1993
2031
|
if (config.DB_DRIVER === "postgres") {
|
|
1994
2032
|
const { Pool } = await import("pg");
|
|
1995
2033
|
const { PostgresAdapter } = await import("./adapter.postgres-6VZCMPQL.js");
|
|
1996
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
2034
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-APCVSYUE.js");
|
|
1997
2035
|
const pool = new Pool(buildPgConfig());
|
|
1998
2036
|
adapter = new PostgresAdapter(pool);
|
|
1999
2037
|
await runPostgresMigrations(adapter);
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
markIndexStale,
|
|
6
6
|
resolveNestPath,
|
|
7
7
|
setSuggestionFlag
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QLXC6542.js";
|
|
9
9
|
import {
|
|
10
10
|
getDb
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QWNXWRWO.js";
|
|
12
12
|
|
|
13
13
|
// src/governance/external-edit-service.ts
|
|
14
14
|
import { readFile, readdir } from "fs/promises";
|
|
@@ -82,7 +82,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
82
82
|
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
-
const { hasUnsealedDraft } = await import("./version-service-
|
|
85
|
+
const { hasUnsealedDraft } = await import("./version-service-RJ6CWZ3O.js");
|
|
86
86
|
if (await hasUnsealedDraft(nestId, documentId)) {
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
@@ -259,7 +259,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
|
|
|
259
259
|
async function mirrorVersion(input) {
|
|
260
260
|
const { storage } = await engineCache.get(input.nestId);
|
|
261
261
|
const node = await storage.readDocument(input.documentId);
|
|
262
|
-
const { upsertVersion, setApprovedVersion } = await import("./version-service-
|
|
262
|
+
const { upsertVersion, setApprovedVersion } = await import("./version-service-RJ6CWZ3O.js");
|
|
263
263
|
await upsertVersion({
|
|
264
264
|
nestId: input.nestId,
|
|
265
265
|
nodeId: input.documentId,
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
engineApi,
|
|
3
3
|
engineCache,
|
|
4
4
|
opContext
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-QLXC6542.js";
|
|
6
6
|
import "./chunk-YVMSM7LS.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-QWNXWRWO.js";
|
|
8
8
|
import "./chunk-YB3LKF7U.js";
|
|
9
9
|
import "./chunk-FRQJWGN3.js";
|
|
10
10
|
export {
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
scanNestForDrift,
|
|
12
12
|
startDriftScanner,
|
|
13
13
|
stopDriftScanner
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-XYD6V2LH.js";
|
|
15
|
+
import "./chunk-QLXC6542.js";
|
|
16
16
|
import "./chunk-YVMSM7LS.js";
|
|
17
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-QWNXWRWO.js";
|
|
18
18
|
import "./chunk-YB3LKF7U.js";
|
|
19
19
|
import "./chunk-FRQJWGN3.js";
|
|
20
20
|
export {
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
listGrants,
|
|
7
7
|
listUserGrants,
|
|
8
8
|
resolveNodeGrant
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-UOMW7TT6.js";
|
|
10
10
|
import "./chunk-YVMSM7LS.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-QWNXWRWO.js";
|
|
12
12
|
import "./chunk-YB3LKF7U.js";
|
|
13
13
|
export {
|
|
14
14
|
createGrant,
|