@promptowl/contextnest-community 1.22.0 → 1.24.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.
Files changed (89) hide show
  1. package/API.md +2092 -0
  2. package/CONFIGURATION.md +5 -2
  3. package/README.md +62 -10
  4. package/STEWARDSHIP.md +229 -0
  5. package/dist/{chunk-PX3P4FTU.js → chunk-2TPQTN4Y.js} +5 -4
  6. package/dist/{chunk-OQXZ43HG.js → chunk-5EZOPA47.js} +40 -10
  7. package/dist/{chunk-MY4JIWQD.js → chunk-I3CSD6CK.js} +1 -1
  8. package/dist/{chunk-W5ILNGPD.js → chunk-KIAAEHWL.js} +80 -28
  9. package/dist/{chunk-J2OQ3MEB.js → chunk-LA3VTQ22.js} +63 -1
  10. package/dist/{chunk-ZAV4QUZA.js → chunk-XUIWAWDO.js} +28 -33
  11. package/dist/{chunk-6WESEA75.js → chunk-ZTT4U4NE.js} +2 -2
  12. package/dist/{client-CSCHXUNX.js → client-KEY4PYJH.js} +1 -1
  13. package/dist/{engine-XKRP7GOQ.js → engine-S3QBQ7LH.js} +2 -2
  14. package/dist/{external-edit-service-TXW7SXSF.js → external-edit-service-6FNFPOJJ.js} +3 -3
  15. package/dist/{grants-service-UZ2MGDO5.js → grants-service-UT3EQ3R7.js} +2 -2
  16. package/dist/index.js +1261 -305
  17. package/dist/{migrations.postgres-KKICIH2C.js → migrations.postgres-AIQ7WSU7.js} +60 -4
  18. package/dist/{review-service-XFWTENTN.js → review-service-HHGOTO6P.js} +6 -6
  19. package/dist/{stewardship-service-5HBQGDMK.js → stewardship-service-4DHNRULG.js} +3 -3
  20. package/dist/{version-service-TKNYJMCM.js → version-service-KIOQME6U.js} +3 -3
  21. package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +1 -0
  22. package/dist/web3/assets/AgentDocsPage-CIiaBqMy.js +1 -0
  23. package/dist/web3/assets/CollaboratorManager-By91wrIr.js +1 -0
  24. package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +1 -0
  25. package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +36 -0
  26. package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +6 -0
  27. package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +1 -0
  28. package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +1 -0
  29. package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +643 -0
  30. package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +1 -0
  31. package/dist/web3/assets/NestView-CTZ55tXD.js +63 -0
  32. package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +1 -0
  33. package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +1 -0
  34. package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +1 -0
  35. package/dist/web3/assets/ReviewActions-OfOdqzCn.js +6 -0
  36. package/dist/web3/assets/ReviewTab-CyX8yxd3.js +1 -0
  37. package/dist/web3/assets/StewardsTab-DBEEfAQI.js +1 -0
  38. package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +1 -0
  39. package/dist/web3/assets/alert-dialog-DTozKlmV.js +7 -0
  40. package/dist/web3/assets/arrow-left-BCKt4CwQ.js +6 -0
  41. package/dist/web3/assets/backlinks-CYd4xENL.js +24 -0
  42. package/dist/web3/assets/card-XG2dP5Xf.js +1 -0
  43. package/dist/web3/assets/chevron-left-BPwUT9DS.js +6 -0
  44. package/dist/web3/assets/circle-check-DI8IBGeo.js +6 -0
  45. package/dist/web3/assets/circle-x-VKVhC88W.js +6 -0
  46. package/dist/web3/assets/code-xml-CMabYZvH.js +6 -0
  47. package/dist/web3/assets/corner-down-right-BGxwK6wH.js +6 -0
  48. package/dist/web3/assets/count-skeleton-DMbdpscX.js +1 -0
  49. package/dist/web3/assets/dates-BCxbm4_q.js +1 -0
  50. package/dist/web3/assets/earth-BEp1EKTo.js +6 -0
  51. package/dist/web3/assets/file-exclamation-point-r8qZGIjP.js +6 -0
  52. package/dist/web3/assets/folder-input-Cmtx1Jhk.js +11 -0
  53. package/dist/web3/assets/folder-target-CUSWqImF.js +1 -0
  54. package/dist/web3/assets/index-BM-h3DwI.css +1 -0
  55. package/dist/web3/assets/index-C1wTSjfP.js +29 -0
  56. package/dist/web3/assets/index-EaX2yql0.js +389 -0
  57. package/dist/web3/assets/page-B3yvQvHy.js +1 -0
  58. package/dist/web3/assets/page-BExMvttJ.js +1 -0
  59. package/dist/web3/assets/page-BUOADv2X.js +1 -0
  60. package/dist/web3/assets/page-BYUFhYWZ.js +1 -0
  61. package/dist/web3/assets/page-Bicf02Cz.js +1 -0
  62. package/dist/web3/assets/page-BsPMDm5d.js +45 -0
  63. package/dist/web3/assets/page-ByyVLDVo.js +16 -0
  64. package/dist/web3/assets/page-CC0NJi8v.js +1 -0
  65. package/dist/web3/assets/page-CHb0F5hV.js +24 -0
  66. package/dist/web3/assets/page-CPjWQNKx.js +11 -0
  67. package/dist/web3/assets/page-CQt3ZGbf.js +2 -0
  68. package/dist/web3/assets/page-Cj1NetQ-.js +1 -0
  69. package/dist/web3/assets/page-CtaW65El.js +1 -0
  70. package/dist/web3/assets/page-DGOL9l8i.js +6 -0
  71. package/dist/web3/assets/page-title-ClvvBsIo.js +1 -0
  72. package/dist/web3/assets/play-C5YNKpAH.js +6 -0
  73. package/dist/web3/assets/refresh-cw-BhHzSH9m.js +6 -0
  74. package/dist/web3/assets/scroll-area-dRWncRqa.css +1 -0
  75. package/dist/web3/assets/scroll-area-hz-xtayP.js +1 -0
  76. package/dist/web3/assets/select-DBFTrUmI.js +6 -0
  77. package/dist/web3/assets/send-0CHvmluT.js +6 -0
  78. package/dist/web3/assets/settings-CeeF4LEb.js +6 -0
  79. package/dist/web3/assets/share-2-CMnOlOt-.js +6 -0
  80. package/dist/web3/assets/tag-DQ_6J5Gv.js +11 -0
  81. package/dist/web3/assets/trash-2-HBi-Pslz.js +6 -0
  82. package/dist/web3/assets/triangle-alert-CDy8-7sv.js +6 -0
  83. package/dist/web3/assets/user-plus-CvBNcpn4.js +6 -0
  84. package/dist/web3/assets/x-By7piikG.js +6 -0
  85. package/dist/web3/assets/zap-C2T8riBp.js +11 -0
  86. package/dist/web3/index.html +2 -2
  87. package/package.json +4 -2
  88. package/dist/web3/assets/index-C2hW_9j9.js +0 -1382
  89. 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` | `""` | **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. |
@@ -79,6 +80,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
79
80
  | `POSTHOG_KEY` | `""` | PostHog project API key for product analytics in the UI. Empty = analytics off (the default — a self-hosted install brings its own project). Served to the browser via `/health`, but only while `TELEMETRY_ENABLED` is on, so that switch turns off everything this server sends outward. Also editable from Settings → Advanced. |
80
81
  | `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog ingestion host. Set it to your own region or self-hosted PostHog. Also editable from Settings → Advanced. |
81
82
  | `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
83
+ | `DRIFT_SCAN_INTERVAL_MS` | `30000` (30 s) | How often the drift scanner walks every nest for files edited outside the app (external edits) and stages them as suggestions for review. Set `0` to disable — do this on a GCS FUSE or other network mount, where the walk is slow and costly. Unset, empty or non-numeric falls back to the default. |
82
84
  | `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
83
85
  | `FRAME_ANCESTORS` | `'self'` | Which origins may embed this server in an iframe, sent as CSP `frame-ancestors`. The default lets nothing but this origin frame the UI, which blocks clickjacking. Deployments that are meant to be embedded list the embedding origin — e.g. the PromptOwl Data Room iframes ContextNest, so that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`. Comma-separated; `'self'` is always included; `*` allows any site and disables the protection. Note the embedding page must be **same-site** (a sibling subdomain) for the session cookie to survive inside the frame — a genuinely cross-domain embed will render the login page no matter what this is set to. |
84
86
  | `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. The asset-upload route (`POST /nests/:id/assets`) is exempt up to the video cap below. |
@@ -152,7 +154,8 @@ plain-text `fallbackText` for clients that cannot render cards.
152
154
  Per-nest connector rows post the same Adaptive Card format to a nest-specific
153
155
  webhook, with per-event filtering: `POST /nests/:id/connectors` with a JSON
154
156
  body like `{"channel": "teams", "url": "https://…", "events":
155
- ["review_requested", "review_rejected"]}` (`events` may also be `["*"]`; the
157
+ ["review_requested", "review_rejected"]}` (`events` is any of `review_requested`,
158
+ `review_approved`, `review_rejected`, `run_failed`, `mention`, or `["*"]`; the
156
159
  `url` may be an `env:KEY` reference into the nest's env store). Manage rows
157
160
  with `GET`/`PATCH`/`DELETE` on the same path.
158
161
 
package/README.md CHANGED
@@ -8,17 +8,19 @@
8
8
 
9
9
  ## What it is
10
10
 
11
- ContextNest Community Edition is a self-hosted server that lets you:
11
+ A shared knowledge base for your team **and** your AI agents, running on your own machine or infrastructure. Documents live in **nests**; every save is a version; in a stewarded nest a change waits for a reviewer before agents can read it. People write in the browser, agents read and write the same documents over MCP, HTTP or the `ctx` CLI.
12
12
 
13
- - Store, version, and govern markdown-based context documents ("nests"), filed in folders you can reorganize as the vault grows
14
- - Import an existing folder or vault of markdown files in one step
15
- - Export a nest as a portable bundle and re-import it on another self-hosted host
16
- - Apply stewardship workflows — draft, pending review, approved
17
- - Share nests with collaborators or publish them read-only to the public
18
- - Serve approved context to AI agents via MCP or HTTP — connect one by pasting a generated setup prompt or downloading a ready `.env`, or register the nest in the ctx CLI (`ctx vault add <alias> --url <server>/nests/<id>/mcp --bearer-env CONTEXTNEST_API_KEY`, then `ctx query "#tag" --vault <alias>`)
19
- - Sync with the PromptOwl hosted platform for multi-user collaboration
13
+ Concretely, it lets you:
20
14
 
21
- The server runs locally or on your own infrastructure. Your PromptOwl account handles authentication, entitlement, and governance metadata.
15
+ - Write Markdown documents — plus hosted HTML artifacts and CSV tables agents can query row by row — filed in folders, linked with `[[Title]]`, tagged, commented on, and versioned
16
+ - Import an existing folder of Markdown in one step; export a nest as a portable bundle
17
+ - Govern changes — draft → pending review → approved — with stewards per nest, tag or document, and a cross-nest inbox (My Work, My Drafts) for what is waiting on you
18
+ - Share a nest with people or teams, share a single document or folder, or publish read-only to the public
19
+ - Give agents deterministic reads: the same selector returns the same context every time, with a trace of what was pulled
20
+ - Run agents against a nest (Workflows, Beta): an agent is a document whose body is instructions; its output lands as a pending review
21
+ - Sync with the PromptOwl hosted platform for accounts, licensing and teams
22
+
23
+ Your PromptOwl account handles authentication, entitlement, and governance metadata.
22
24
 
23
25
  ## Quickstart
24
26
 
@@ -73,6 +75,40 @@ The key is read at boot. The server validates against PromptOwl on startup; if v
73
75
 
74
76
  For redistribution, hosted-service, OEM, or regulated-industry licensing, contact **hoot@promptowl.ai**.
75
77
 
78
+ ## First five minutes
79
+
80
+ 1. **Create a nest** on the Nests page — or **Import folder** to bring in Markdown you already have.
81
+ 2. **Write a document.** `[[Title]]` links another document, `@name` mentions a person, tags go under the title. Every save is a version.
82
+ 3. **Turn on stewardship** (nest Settings) if you want review. Saves become drafts; **Submit for review**; a steward approves or rejects. Alone? Allow self-approve and you get the history without the ceremony.
83
+ 4. **Connect an agent.** Inside a nest, **Connect** (bottom of the sidebar) gives you the MCP URL and a REST snippet; **Add with AI** (nest overview) is a prompt that has an agent write context in. Mint a key under Workspace → API keys.
84
+ 5. **Share.** The whole nest with a person or team, one document or folder, or make it public with a read-only Reader mode.
85
+
86
+ The in-app **How it works** page (sidebar → Help) is the full tour; **Docs** (header) is the agent manual.
87
+
88
+ ## Around the app
89
+
90
+ One sidebar, grouped:
91
+
92
+ | Group | Pages | Who |
93
+ |---|---|---|
94
+ | — | **Nests** — every nest you can see, pinned first | everyone |
95
+ | Work | **My Work** (reviews and handoffs waiting on you, across nests) · **My Drafts** (yours, never submitted) | everyone |
96
+ | Workspace | **Teams** · **API keys** | everyone |
97
+ | Admin | **Teammates** · **Server settings** · **Activity trace** | server admins |
98
+ | Help | **How it works** | everyone |
99
+
100
+ Inside a nest: the document tree, plus **Board** (documents as cards by folder, status or tag), **Graph** (documents, links, tags, stewards), **Definitions** (the nest's glossary) and **Workflows** (Beta). The user menu (top right) holds **Account** (name, password), keyboard shortcuts and logout. `Ctrl`/`⌘` `K` searches every nest and document; `?` lists shortcuts.
101
+
102
+ ## Connect an agent
103
+
104
+ Point any agent at `<server>/llms.txt` — it is the complete manual (endpoints, MCP tools, selector grammar). The short version:
105
+
106
+ - **MCP** — `<server>/mcp` for everything the key can read, `<server>/nests/<id>/mcp` for one nest with the full toolset. Claude Code, Cursor and VS Code connect over HTTP with `Authorization: Bearer cnst_…`; Claude Desktop goes through `npx -y mcp-remote`.
107
+ - **REST** — `POST /nests/<id>/context` with a selector (`#tag`, `[[Title]]`, `type:document`, combined with `+ | -`) returns assembled context plus a trace. Full reference: [API.md](./API.md).
108
+ - **ctx CLI** — `ctx vault add <alias> --url <server>/nests/<id>/mcp --bearer-env CONTEXTNEST_API_KEY`, then `ctx query "#tag" --vault <alias>`.
109
+
110
+ Keys are minted in the app (Workspace → API keys, or the Connect dialog) — one per client, user-wide or scoped to a nest. Servers in `AUTH_MODE=open` need none.
111
+
76
112
  ## System requirements
77
113
 
78
114
  - **Node.js** 20.x or later
@@ -105,6 +141,14 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
105
141
  | MCP server for AI agents | ✅ | ✅ |
106
142
  | One-shot agent setup — generated connect prompt + `.env` download | ✅ | ✅ |
107
143
  | Several API keys per account — one credential per client, rotate one at a time | ✅ | ✅ |
144
+ | Comments on documents — anchored threads, resolve / reopen, same threads over MCP | ✅ | ✅ |
145
+ | Table nodes — CSV facts with deterministic row queries (`POST /table-query`, `context_table_query`) | ✅ | ✅ |
146
+ | Teams — share a nest with a group once; import rosters from PromptOwl | ✅ | ✅ |
147
+ | Cross-nest inbox — My Work (reviews, handoffs) and My Drafts | ✅ | ✅ |
148
+ | Nest views — Board (kanban by folder / status / tag), Graph, Definitions glossary | ✅ | ✅ |
149
+ | Workflow plane (Beta) — agents as documents, runs, schedules, inbound hooks, Slack / Teams / webhook connectors | ✅ | ✅ |
150
+ | Activity trace — every governance action, per nest and server-wide | ✅ | ✅ |
151
+ | Notifications — Slack, Microsoft Teams, email | ✅ | ✅ |
108
152
  | Centralized multi-tenant admin console | — | ✅ |
109
153
  | Single sign-on (OIDC — Entra ID, Google, Okta, Keycloak) | ✅ | ✅ |
110
154
  | SAML / SCIM provisioning | — | ✅ |
@@ -118,6 +162,14 @@ For Enterprise pricing and features, contact **hoot@promptowl.ai** or visit <htt
118
162
 
119
163
  Release notes live in [CHANGELOG.md](./CHANGELOG.md).
120
164
 
165
+ ## Documentation
166
+
167
+ - [CONFIGURATION.md](./CONFIGURATION.md) — every environment variable (port, auth mode, storage, notifications, telemetry)
168
+ - [API.md](./API.md) — complete REST reference with request and response bodies
169
+ - [STEWARDSHIP.md](./STEWARDSHIP.md) — the governance model: modes, stewards, scopes, prime documents, teams, super-admins
170
+ - `<server>/llms.txt` — the agent manual the running server publishes; rendered as **Docs** in the app
171
+ - **How it works** inside the app — a page-by-page tour
172
+
121
173
  ## Licensing
122
174
 
123
175
  ContextNest Community Edition is **commercial software**. It is **not open source**.
@@ -165,4 +217,4 @@ The Software injects content into large language models. AI output may be inaccu
165
217
  **Copyright © 2026 Promptowl LLC.** All rights reserved.
166
218
  "ContextNest" and "PromptOwl" are trademarks of Promptowl LLC.
167
219
 
168
- Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
220
+ Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
package/STEWARDSHIP.md ADDED
@@ -0,0 +1,229 @@
1
+ # Stewardship — Quick Guide
2
+
3
+ A short, practical guide to the governance model the community server implements. The deep strategy lives in `docs/contextnest-whitepaper.md`; this is the "how does this actually work in the product" version.
4
+
5
+ ## The two modes
6
+
7
+ Every nest is either **ungoverned** or **governed**, controlled by the `stewardship_enabled` flag on the nest.
8
+
9
+ | | Ungoverned (default) | Governed |
10
+ |---|---|---|
11
+ | New documents | Auto-approved, immediately available to AI | Start as **drafts**; need steward approval |
12
+ | Draft / pending / rejected lifecycle | Not shown | Shown, filterable |
13
+ | Review queue | Hidden | Visible, orderable |
14
+ | Stewards | Not shown | Assignable by scope |
15
+ | Reads gated by permission | No | Yes (in key mode) |
16
+
17
+ Flip the flag any time via the nest's **Settings → Stewardship**. Existing approved docs stay approved; existing drafts remain drafts.
18
+
19
+ ## What a steward is
20
+
21
+ A **steward** is a person (identified by email) who governs a subset of the nest. Stewards have one of three roles:
22
+
23
+ | Role | Can read | Can edit | Can approve / reject |
24
+ |---|---|---|---|
25
+ | **Viewer** | yes | no | no |
26
+ | **Editor** | yes | yes | no |
27
+ | **Reviewer** | yes | yes | **yes** |
28
+
29
+ > **Authors can't approve their own work.** Even a reviewer-level steward can't approve a version they themselves authored. Separation of duties, enforced server-side.
30
+
31
+ ## Scope — who governs what
32
+
33
+ Stewards are assigned at one of three scopes. When a document needs a steward (to approve, or to check access), the server resolves them in this order — **first match wins**:
34
+
35
+ | Priority | Scope | Target | Example |
36
+ |---|---|---|---|
37
+ | 1 | **Document** | Exact node id | `nodes/pricing-policy` — only this one doc |
38
+ | 2 | **Tag** | Tag name (lowercased, no `#`) | `security` — any doc tagged `#security` |
39
+ | 3 | **Nest** | (none — applies to the whole nest) | The fallback for anything not covered above |
40
+
41
+ If nothing matches and no steward resolves, the **nest owner** is the implicit steward (owner fallback). That keeps a freshly-enabled nest workable even with zero stewards configured.
42
+
43
+ ## The approval flow
44
+
45
+ 1. **Author** creates or edits a doc. In governed mode it saves as a **draft**.
46
+ 2. **Author** clicks **Submit for Review**. The doc becomes **pending** and appears in the review queue for anyone whose scope resolves to that doc — and in their **My Work** page across nests. Until someone acts the author can withdraw it back to draft.
47
+ 3. A **Reviewer steward** opens the review queue (or My Work), reads the doc, clicks **Approve** or **Reject**. Rejecting requires a note.
48
+ 4. On approve, the doc becomes **approved** and is now AI-readable. The approved version number is pinned (`approvedVersion`), so subsequent drafts don't automatically replace it — AI keeps getting the last blessed version until a new one is approved.
49
+
50
+ ### Auto-publish owner & admin edits (`allow_self_approve`)
51
+
52
+ A per-nest flag (**off** by default; nest Settings → "Auto-publish owner & admin edits"). When it's **on**, a create or edit by the nest **owner** or an **admin** publishes immediately — it skips steps 2–4 entirely instead of landing as a draft. This is a governance *bypass* for the two roles that already hold approval authority, not a separation-of-duties exception. **Editor and reviewer flows are unchanged**: their edits still save as drafts and go through Submit → Review → Approve regardless of the flag. The pending-review edit lock still applies, so an owner/admin can't overwrite a doc a teammate has in review. Turning the flag back off doesn't unpublish anything; it only affects future edits.
53
+
54
+ ## Prime documents
55
+
56
+ Most content in a nest doesn't need a review gate — call notes, social drafts, scratch research. A small set does: messaging architecture, the sales playbook, pricing policy. **Prime** marks that second set.
57
+
58
+ A prime document **always** goes through draft → pending → approved, no matter who writes it. It is the one thing `allow_self_approve` does not bypass: an owner editing a prime document still lands a draft.
59
+
60
+ ### How a document becomes prime
61
+
62
+ Resolution mirrors the steward scope order — **document → tag → nest**, first match wins:
63
+
64
+ | Priority | Scope | How it's set |
65
+ |---|---|---|
66
+ | 1 | **Document** | The Prime switch in the document editor (owner/admin only). `true` forces prime; `false` **exempts** this one doc from an otherwise-prime tag. |
67
+ | 2 | **Tag** | The doc carries one of the nest's **prime tags** (Settings → Prime tags; `prime-document` out of the box). |
68
+ | 3 | **Nest** | Nothing matched → not prime. |
69
+
70
+ Only the nest owner or an admin can set or clear the document flag — it's a governance decision, not an authoring one. Otherwise any editor could un-flag the playbook and walk their own edit past the gate.
71
+
72
+ ### Reviewing prime documents only (`prime_only_review`)
73
+
74
+ A per-nest flag (**off** by default; nest Settings → "Review prime documents only"). It changes what a governed nest's *default* is:
75
+
76
+ | | `prime_only_review` off (default) | on |
77
+ |---|---|---|
78
+ | Ordinary document, any author | Draft → review → approve (unless the author has a self-approve bypass) | **Publishes immediately** |
79
+ | Prime document, any author | Draft → review → approve | Draft → review → approve |
80
+
81
+ That's the "most content self-publishes, high-stakes content is gated" posture. Leaving it off preserves the original behaviour, so turning stewardship on doesn't silently change meaning for an existing nest.
82
+
83
+ While `prime_only_review` is on, `allow_self_approve` has no effect at all — non-prime documents already publish for everyone, and prime documents ignore the bypass. Settings disables that switch and says so rather than leaving a live control that changes nothing.
84
+
85
+ Prime only applies to **governed** nests. With stewardship off there are no stewards to approve anything, so a prime tag is inert until you turn stewardship on.
86
+
87
+ ### At a glance
88
+
89
+ Prime documents carry a **Prime** badge in the nest's document list, with a tooltip saying whether it came from the document flag or an inherited tag. To see every document a given prime tag covers, filter the list by that tag.
90
+
91
+ Flipping any of this only affects **future** writes. Marking a published document prime doesn't unpublish it; the next edit is what needs approval.
92
+
93
+ ## Who sees what
94
+
95
+ Governance gates two things: who can **approve**, and who can **read**.
96
+
97
+ - **Approve gate**: server-side, always on when stewardship is enabled. Non-stewards can't approve.
98
+ - **Read gate**: only active in **key mode** (`AUTH_MODE=key`, real user accounts with API keys). In **open mode** — which is the default for a solo/local server — everyone is the same anonymous admin, so read-gating is a no-op.
99
+
100
+ In key mode, when stewardship is enabled on a nest:
101
+ - Node list / single-read / search / query responses are filtered to docs the caller can access
102
+ - A non-steward gets an empty list or a 403 on the node they asked for
103
+ - The nest **owner** and any resolved **steward (any role)** can read; **super admins** bypass the check
104
+
105
+ ## Server super-admins
106
+
107
+ Super-admins administer **every** nest on the server without being added per-nest. The effective set is the union of three sources:
108
+
109
+ - **license** — the PromptOwl account that owns the installed license (always; never revocable)
110
+ - **config** — emails in `access.yaml: super_admins` (the bootstrap; edit the file + restart to change)
111
+ - **granted** — grants managed from **Teammates → Superadmins** in the UI, or `GET/POST /admin/super-admins` (see `API.md`)
112
+
113
+ A super-admin can:
114
+
115
+ - Read every document (bypasses the read gate, even with stewardship enabled)
116
+ - Change a nest's visibility (private / org / public)
117
+ - Add, remove, and re-role collaborators
118
+ - Add, remove, and re-role stewards
119
+ - Approve and reject pending versions in any nest's review queue
120
+ - Manage the official community sites trusted for SSO auto-login (**Server settings → Community sites**, or `GET/POST /admin/community-sites` — see `API.md`)
121
+
122
+ What a super-admin **cannot** do (owner-only operations):
123
+
124
+ - Delete a nest
125
+ - Transfer ownership
126
+
127
+ Use this for a small set of platform operators — a CEO, a head of governance, an internal compliance admin — not as a default broad-access mechanism. Day-to-day governance should still flow through per-nest stewards.
128
+
129
+ ## Teams — sharing with a group
130
+
131
+ Instead of adding people one at a time, you can create a **team** — a reusable,
132
+ owner-managed group — and share it onto a nest. Each team member carries a
133
+ **role** set on the membership (not on the share), and that role applies on
134
+ **every** nest the team is shared to:
135
+
136
+ | Member role | Nest access | Governance |
137
+ |---|---|---|
138
+ | **viewer** | read | viewer |
139
+ | **editor** | write | editor |
140
+ | **admin** | admin | admin |
141
+
142
+ Key points:
143
+
144
+ - A member's role is uniform across nests — set it once on the team. To vary
145
+ access per nest, use a different team (or a direct collaborator/steward grant).
146
+ - When a user reaches a nest by several paths (a direct collaborator grant **and**
147
+ team membership), the **highest** access wins.
148
+ - Sharing a team whose members carry a governance role (viewer/editor)
149
+ **enables stewardship** on the nest — same effect as assigning an individual
150
+ steward. An `admin`-only team is collaborator-style and doesn't.
151
+ - Removing a member, deleting the team, or unsharing it **revokes access
152
+ immediately** — there are no per-user rows to clean up.
153
+ - Managing a team is gated by your role on it: editor/admin members (and the
154
+ owner) can add people, capped to their own level; admins can also change
155
+ roles and remove members; only the owner can delete the team.
156
+
157
+ ### Importing a team from PromptOwl
158
+
159
+ If you already run teams in PromptOwl, the Teams page has a **PromptOwl Teams**
160
+ tab listing your teams there with their members. Each one has a **Sync** button
161
+ that copies it here as a normal team owned by you, instead of you retyping every
162
+ member. Members without an account here become invited placeholders, exactly as
163
+ if you'd added them by hand.
164
+
165
+ **The tab only appears if you signed in with PromptOwl** — that sign-in is what
166
+ gives this server permission to read your teams. Signing in another way (SSO,
167
+ your identity provider, or email and password) means no PromptOwl identity to
168
+ read teams for, so the tab isn't offered.
169
+
170
+ **You can import a team you're an owner or editor of in PromptOwl.** Teams you're
171
+ view-only on are listed for reference but can't be imported: the copy would be
172
+ yours to control here — rename it, rewrite its roster, share it onto a nest —
173
+ and that's more than PromptOwl lets you do with that team. Ask one of its owners
174
+ or editors to import it instead.
175
+
176
+ Already-imported teams are marked **Synced** in that tab, and show a **PromptOwl**
177
+ badge in the ContextNest Teams tab. Two things worth knowing before you sync
178
+ one again:
179
+
180
+ - **PromptOwl is the source of truth on every sync.** The roster is reconciled to
181
+ match: roles are reset, and anyone no longer in the PromptOwl team is removed —
182
+ including people you added to this team by hand here. If you need a group that
183
+ differs from PromptOwl, make it a separate team rather than editing an
184
+ imported one.
185
+ - **Your PromptOwl access isn't stored on this server.** The connection lives in
186
+ a cookie in your own browser and ends when your session does — this server's
187
+ database never holds anything that can reach your PromptOwl account, so a
188
+ stolen backup of it exposes nothing of yours. The connection is also limited
189
+ to reading your profile and your teams: it cannot chat as you or spend your
190
+ credits. Signing out ends it, and revoking the device in PromptOwl's account
191
+ settings ends it immediately from the other side.
192
+
193
+ PromptOwl's `User` role — its default membership — maps to **viewer** here, as
194
+ does any role this version doesn't recognize; `Owner` maps to admin, and
195
+ `Editor`/`Viewer` map across as themselves.
196
+
197
+ Members see the teams they belong to on the **Teams** page and any team-shared
198
+ nest on their dashboard. Manage teams there (create, add/remove members with a
199
+ role); share them from a nest's sharing panel. See `API.md` §3a for the endpoints.
200
+
201
+ ## Picking a scope when you assign a steward
202
+
203
+ Rules of thumb:
204
+
205
+ - **Nest scope** — use sparingly. "This person reviews everything in this nest."
206
+ - **Tag scope** — the most common. "Legal reviews anything with `#legal`. Security reviews `#auth` and `#pii`."
207
+ - **Document scope** — override. "This specific doc has a dedicated reviewer regardless of its tags."
208
+
209
+ You can stack them: a doc tagged `#legal` can have a doc-level steward that overrides the tag steward. Priority order resolves ties.
210
+
211
+ ## `stewards.yaml` (optional)
212
+
213
+ You can also declare stewards declaratively via a `stewards.yaml` file in the nest's vault directory and run `POST /nests/:id/stewards/sync`. Useful for version-controlled governance. Example:
214
+
215
+ ```yaml
216
+ version: 1
217
+ nest:
218
+ - email: governance-lead@acme.com
219
+ role: reviewer
220
+ tags:
221
+ "#legal":
222
+ - email: legal@acme.com
223
+ role: reviewer
224
+ "#security":
225
+ - email: security-team@acme.com
226
+ role: reviewer
227
+ ```
228
+
229
+ Sync replaces all stewards for that nest from the file. Prefer the UI for day-to-day adds; prefer the file for reproducible setups and code review.
@@ -8,10 +8,11 @@ import {
8
8
  nestAllowsSelfApprove,
9
9
  nestName,
10
10
  primaryRole,
11
+ resolveNestAccess,
11
12
  resolveNestPermission,
12
13
  resolveTeamRolesForUser,
13
14
  sendEmailToRecipient
14
- } from "./chunk-W5ILNGPD.js";
15
+ } from "./chunk-KIAAEHWL.js";
15
16
  import {
16
17
  ConflictError,
17
18
  ValidationError
@@ -20,7 +21,7 @@ import {
20
21
  config,
21
22
  getDb,
22
23
  isEmailish
23
- } from "./chunk-J2OQ3MEB.js";
24
+ } from "./chunk-LA3VTQ22.js";
24
25
 
25
26
  // src/governance/stewardship-service.ts
26
27
  import { v4 as uuid } from "uuid";
@@ -441,8 +442,8 @@ async function resolveUserRoles(nestId, userEmail, opts) {
441
442
  }
442
443
  async function canManageStewards(nestId, userId) {
443
444
  if (config.AUTH_MODE === "open") return true;
444
- const perm = await resolveNestPermission(nestId, userId);
445
- return perm === "owner" || perm === "admin";
445
+ const { rawPermission } = await resolveNestAccess(nestId, userId);
446
+ return rawPermission === "owner" || rawPermission === "admin";
446
447
  }
447
448
  async function canCreateInNest(nestId, userEmail) {
448
449
  if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
@@ -4,19 +4,19 @@ import {
4
4
  resolveNestWideRoles,
5
5
  resolveStewardsForNode,
6
6
  stewardCoverageForUser
7
- } from "./chunk-PX3P4FTU.js";
7
+ } from "./chunk-2TPQTN4Y.js";
8
8
  import {
9
9
  grantCoversNode,
10
10
  listUserGrants,
11
11
  resolveNodeGrant
12
- } from "./chunk-MY4JIWQD.js";
12
+ } from "./chunk-I3CSD6CK.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-ZTT4U4NE.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-KIAAEHWL.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-LA3VTQ22.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,
@@ -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-LA3VTQ22.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";