@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 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` | `""` | **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. |
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` may also be `["*"]`; the
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-W5ILNGPD.js";
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-J2OQ3MEB.js";
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-W5ILNGPD.js";
5
+ } from "./chunk-QLXC6542.js";
6
6
  import {
7
7
  getDb
8
- } from "./chunk-J2OQ3MEB.js";
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-PX3P4FTU.js";
7
+ } from "./chunk-2YPY2HGZ.js";
8
8
  import {
9
9
  grantCoversNode,
10
10
  listUserGrants,
11
11
  resolveNodeGrant
12
- } from "./chunk-MY4JIWQD.js";
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-6WESEA75.js";
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-W5ILNGPD.js";
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-J2OQ3MEB.js";
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])}. A flow can't return to a step it already ran.`
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 line = `Deletion requested for ${ctx.noun} *${ctx.label}* by ${request.requestedBy} \u2014 "${request.reason}"`;
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 verb = archivedInstead ? "archived" : outcome === "deleted" ? "deleted" : "rejected the deletion of";
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-J2OQ3MEB.js";
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 created_at DESC`,
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 created_at DESC`,
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 created_at DESC`,
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-XKRP7GOQ.js");
2595
- const { documentsWithSuggestions } = await import("./external-edit-service-TXW7SXSF.js");
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-KKICIH2C.js");
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);
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-YVMSM7LS.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-J2OQ3MEB.js";
6
+ } from "./chunk-QWNXWRWO.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -5,10 +5,10 @@ import {
5
5
  markIndexStale,
6
6
  resolveNestPath,
7
7
  setSuggestionFlag
8
- } from "./chunk-W5ILNGPD.js";
8
+ } from "./chunk-QLXC6542.js";
9
9
  import {
10
10
  getDb
11
- } from "./chunk-J2OQ3MEB.js";
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-TKNYJMCM.js");
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-TKNYJMCM.js");
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,7 +2,7 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-J2OQ3MEB.js";
5
+ } from "./chunk-QWNXWRWO.js";
6
6
  import "./chunk-YB3LKF7U.js";
7
7
  export {
8
8
  getDb,
@@ -2,9 +2,9 @@ import {
2
2
  engineApi,
3
3
  engineCache,
4
4
  opContext
5
- } from "./chunk-W5ILNGPD.js";
5
+ } from "./chunk-QLXC6542.js";
6
6
  import "./chunk-YVMSM7LS.js";
7
- import "./chunk-J2OQ3MEB.js";
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-ZAV4QUZA.js";
15
- import "./chunk-W5ILNGPD.js";
14
+ } from "./chunk-XYD6V2LH.js";
15
+ import "./chunk-QLXC6542.js";
16
16
  import "./chunk-YVMSM7LS.js";
17
- import "./chunk-J2OQ3MEB.js";
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-MY4JIWQD.js";
9
+ } from "./chunk-UOMW7TT6.js";
10
10
  import "./chunk-YVMSM7LS.js";
11
- import "./chunk-J2OQ3MEB.js";
11
+ import "./chunk-QWNXWRWO.js";
12
12
  import "./chunk-YB3LKF7U.js";
13
13
  export {
14
14
  createGrant,