@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.
- package/API.md +2092 -0
- package/CONFIGURATION.md +5 -2
- package/README.md +62 -10
- package/STEWARDSHIP.md +229 -0
- package/dist/{chunk-PX3P4FTU.js → chunk-2TPQTN4Y.js} +5 -4
- package/dist/{chunk-OQXZ43HG.js → chunk-5EZOPA47.js} +40 -10
- package/dist/{chunk-MY4JIWQD.js → chunk-I3CSD6CK.js} +1 -1
- package/dist/{chunk-W5ILNGPD.js → chunk-KIAAEHWL.js} +80 -28
- package/dist/{chunk-J2OQ3MEB.js → chunk-LA3VTQ22.js} +63 -1
- package/dist/{chunk-ZAV4QUZA.js → chunk-XUIWAWDO.js} +28 -33
- package/dist/{chunk-6WESEA75.js → chunk-ZTT4U4NE.js} +2 -2
- package/dist/{client-CSCHXUNX.js → client-KEY4PYJH.js} +1 -1
- package/dist/{engine-XKRP7GOQ.js → engine-S3QBQ7LH.js} +2 -2
- package/dist/{external-edit-service-TXW7SXSF.js → external-edit-service-6FNFPOJJ.js} +3 -3
- package/dist/{grants-service-UZ2MGDO5.js → grants-service-UT3EQ3R7.js} +2 -2
- package/dist/index.js +1261 -305
- package/dist/{migrations.postgres-KKICIH2C.js → migrations.postgres-AIQ7WSU7.js} +60 -4
- package/dist/{review-service-XFWTENTN.js → review-service-HHGOTO6P.js} +6 -6
- package/dist/{stewardship-service-5HBQGDMK.js → stewardship-service-4DHNRULG.js} +3 -3
- package/dist/{version-service-TKNYJMCM.js → version-service-KIOQME6U.js} +3 -3
- package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +1 -0
- package/dist/web3/assets/AgentDocsPage-CIiaBqMy.js +1 -0
- package/dist/web3/assets/CollaboratorManager-By91wrIr.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +1 -0
- package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +36 -0
- package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +1 -0
- package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +1 -0
- package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +643 -0
- package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +1 -0
- package/dist/web3/assets/NestView-CTZ55tXD.js +63 -0
- package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +1 -0
- package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +1 -0
- package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +1 -0
- package/dist/web3/assets/ReviewActions-OfOdqzCn.js +6 -0
- package/dist/web3/assets/ReviewTab-CyX8yxd3.js +1 -0
- package/dist/web3/assets/StewardsTab-DBEEfAQI.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +1 -0
- package/dist/web3/assets/alert-dialog-DTozKlmV.js +7 -0
- package/dist/web3/assets/arrow-left-BCKt4CwQ.js +6 -0
- package/dist/web3/assets/backlinks-CYd4xENL.js +24 -0
- package/dist/web3/assets/card-XG2dP5Xf.js +1 -0
- package/dist/web3/assets/chevron-left-BPwUT9DS.js +6 -0
- package/dist/web3/assets/circle-check-DI8IBGeo.js +6 -0
- package/dist/web3/assets/circle-x-VKVhC88W.js +6 -0
- package/dist/web3/assets/code-xml-CMabYZvH.js +6 -0
- package/dist/web3/assets/corner-down-right-BGxwK6wH.js +6 -0
- package/dist/web3/assets/count-skeleton-DMbdpscX.js +1 -0
- package/dist/web3/assets/dates-BCxbm4_q.js +1 -0
- package/dist/web3/assets/earth-BEp1EKTo.js +6 -0
- package/dist/web3/assets/file-exclamation-point-r8qZGIjP.js +6 -0
- package/dist/web3/assets/folder-input-Cmtx1Jhk.js +11 -0
- package/dist/web3/assets/folder-target-CUSWqImF.js +1 -0
- package/dist/web3/assets/index-BM-h3DwI.css +1 -0
- package/dist/web3/assets/index-C1wTSjfP.js +29 -0
- package/dist/web3/assets/index-EaX2yql0.js +389 -0
- package/dist/web3/assets/page-B3yvQvHy.js +1 -0
- package/dist/web3/assets/page-BExMvttJ.js +1 -0
- package/dist/web3/assets/page-BUOADv2X.js +1 -0
- package/dist/web3/assets/page-BYUFhYWZ.js +1 -0
- package/dist/web3/assets/page-Bicf02Cz.js +1 -0
- package/dist/web3/assets/page-BsPMDm5d.js +45 -0
- package/dist/web3/assets/page-ByyVLDVo.js +16 -0
- package/dist/web3/assets/page-CC0NJi8v.js +1 -0
- package/dist/web3/assets/page-CHb0F5hV.js +24 -0
- package/dist/web3/assets/page-CPjWQNKx.js +11 -0
- package/dist/web3/assets/page-CQt3ZGbf.js +2 -0
- package/dist/web3/assets/page-Cj1NetQ-.js +1 -0
- package/dist/web3/assets/page-CtaW65El.js +1 -0
- package/dist/web3/assets/page-DGOL9l8i.js +6 -0
- package/dist/web3/assets/page-title-ClvvBsIo.js +1 -0
- package/dist/web3/assets/play-C5YNKpAH.js +6 -0
- package/dist/web3/assets/refresh-cw-BhHzSH9m.js +6 -0
- package/dist/web3/assets/scroll-area-dRWncRqa.css +1 -0
- package/dist/web3/assets/scroll-area-hz-xtayP.js +1 -0
- package/dist/web3/assets/select-DBFTrUmI.js +6 -0
- package/dist/web3/assets/send-0CHvmluT.js +6 -0
- package/dist/web3/assets/settings-CeeF4LEb.js +6 -0
- package/dist/web3/assets/share-2-CMnOlOt-.js +6 -0
- package/dist/web3/assets/tag-DQ_6J5Gv.js +11 -0
- package/dist/web3/assets/trash-2-HBi-Pslz.js +6 -0
- package/dist/web3/assets/triangle-alert-CDy8-7sv.js +6 -0
- package/dist/web3/assets/user-plus-CvBNcpn4.js +6 -0
- package/dist/web3/assets/x-By7piikG.js +6 -0
- package/dist/web3/assets/zap-C2T8riBp.js +11 -0
- package/dist/web3/index.html +2 -2
- package/package.json +4 -2
- 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. |
|
|
@@ -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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
445
|
-
return
|
|
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-
|
|
7
|
+
} from "./chunk-2TPQTN4Y.js";
|
|
8
8
|
import {
|
|
9
9
|
grantCoversNode,
|
|
10
10
|
listUserGrants,
|
|
11
11
|
resolveNodeGrant
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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])}.
|
|
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,
|