@promptowl/contextnest-community 1.25.0 → 1.27.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 +102 -9
- package/CONFIGURATION.md +604 -117
- package/README.md +3 -2
- package/dist/{chunk-JDAOA5KP.js → chunk-H3W72HN3.js} +67 -14
- package/dist/{chunk-2KHM5C7V.js → chunk-MB52DWWP.js} +13 -15
- package/dist/{chunk-TI7HMP64.js → chunk-NFFUZF2B.js} +23 -2
- package/dist/{chunk-F66VCBLA.js → chunk-O3JEI33N.js} +51 -2
- package/dist/{chunk-7FROMOZM.js → chunk-QMQDWRHN.js} +180 -550
- package/dist/chunk-RXCVXG34.js +523 -0
- package/dist/{chunk-32SZCH36.js → chunk-SE5U4J4T.js} +4 -4
- package/dist/{chunk-KPGNZLZL.js → chunk-VHEMGQ3I.js} +1 -1
- package/dist/{chunk-DTLMBF4W.js → chunk-VRLAP4HB.js} +42 -17
- package/dist/{engine-TER6FDBP.js → engine-HLYQLP5T.js} +3 -2
- package/dist/{external-edit-service-CAGENHDJ.js → external-edit-service-3DFQ4EZZ.js} +4 -3
- package/dist/{grants-service-2OTFS7EH.js → grants-service-WICV4Y2J.js} +2 -2
- package/dist/index.js +1669 -521
- package/dist/invited-user-XEBX6BM4.js +12 -0
- package/dist/{migrations.postgres-23VJJJJX.js → migrations.postgres-HGSQ7IA5.js} +4 -0
- package/dist/{review-service-4G2XUFS6.js → review-service-GB43UQVH.js} +7 -6
- package/dist/{stewardship-service-S22V2JKG.js → stewardship-service-KDBPWRZI.js} +4 -3
- package/dist/{version-service-X4FAG2OZ.js → version-service-JYHFPSYP.js} +8 -3
- package/dist/web3/assets/ActivityTracePage-CkRoyIc-.js +1 -0
- package/dist/web3/assets/AgentDocsPage-BTRXmliv.js +1 -0
- package/dist/web3/assets/CollaboratorManager-CqJIkvs-.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-JcO1P-qm.js +1 -0
- package/dist/web3/assets/DocumentEditor-D1GLeeEP.js +36 -0
- package/dist/web3/assets/DocumentsTab-BAgSxTX1.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-DuuTXZap.js +1 -0
- package/dist/web3/assets/{MarkdownEditor-Dfk5uIE5.js → MarkdownEditor-BELFww9q.js} +130 -135
- package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
- package/dist/web3/assets/NestPageHeader-Bbje2ncY.js +1 -0
- package/dist/web3/assets/NestView-D4OMXPQ7.js +73 -0
- package/dist/web3/assets/OverviewTab-FIDGjR3Q.js +6 -0
- package/dist/web3/assets/PersonCombobox-D52m_jhc.js +1 -0
- package/dist/web3/assets/{ReviewActions-BXDTDPp1.js → ReviewActions-DYqBcWlv.js} +2 -2
- package/dist/web3/assets/ReviewTab-DoxfygOb.js +1 -0
- package/dist/web3/assets/StewardsTab-CFsYjbEn.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-AW2OLNAE.js +1 -0
- package/dist/web3/assets/{alert-dialog-4SiLKlz8.js → alert-dialog-p-fBLSWx.js} +2 -2
- package/dist/web3/assets/{arrow-left-BAgIocLn.js → arrow-left-CiErCCTG.js} +1 -1
- package/dist/web3/assets/backlinks-DaVSYvhV.js +19 -0
- package/dist/web3/assets/boxes-VWrCCMzW.js +6 -0
- package/dist/web3/assets/{chevron-left-XR4ReJ9Z.js → chevron-left-CsD8gZwL.js} +1 -1
- package/dist/web3/assets/{circle-check-CSkZUEFK.js → circle-check-BBCXd9LI.js} +1 -1
- package/dist/web3/assets/{circle-x-in2o3aHU.js → circle-x-C9LsnORQ.js} +1 -1
- package/dist/web3/assets/{code-xml-pPQgdctI.js → code-xml-BaoV7HiQ.js} +1 -1
- package/dist/web3/assets/{corner-down-right-CuxZWscX.js → corner-down-right-DgiTXrtw.js} +1 -1
- package/dist/web3/assets/count-skeleton-CnEE4uF-.js +1 -0
- package/dist/web3/assets/{earth-CXOcThqn.js → earth-BmMbVt9k.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-CSphs1VD.js → file-exclamation-point-qJeca9-A.js} +1 -1
- package/dist/web3/assets/{folder-input-BAwK9tFL.js → folder-input-D7R93LJ6.js} +1 -1
- package/dist/web3/assets/index-DNmWTxN7.css +1 -0
- package/dist/web3/assets/index-DvuUIUP6.js +451 -0
- package/dist/web3/assets/{page-CUjuSs5Q.js → page-BJQuaTRW.js} +3 -3
- package/dist/web3/assets/page-Bb8xGOPQ.js +2 -0
- package/dist/web3/assets/page-BtM-qDzr.js +6 -0
- package/dist/web3/assets/page-Bxd0EG25.js +1 -0
- package/dist/web3/assets/page-C3QCO6IR.js +1 -0
- package/dist/web3/assets/page-CfoR0khy.js +6 -0
- package/dist/web3/assets/page-D7Zr5tAI.js +1 -0
- package/dist/web3/assets/page-DATSlHCW.js +6 -0
- package/dist/web3/assets/page-DTjPQizZ.js +45 -0
- package/dist/web3/assets/page-DspptlWr.js +1 -0
- package/dist/web3/assets/{page-xzYqkBUP.js → page-I6vAFJOj.js} +5 -5
- package/dist/web3/assets/page-QxI4Ud2s.js +9 -0
- package/dist/web3/assets/page-YcNNg-dX.js +1 -0
- package/dist/web3/assets/page-kM7eO5Kj.js +1 -0
- package/dist/web3/assets/{page-title-BkqlUA0j.js → page-title-Dn5jANiU.js} +1 -1
- package/dist/web3/assets/{play-B8xE9j5f.js → play-B2k_Ix-L.js} +1 -1
- package/dist/web3/assets/{send-CvAgOUPG.js → send-DPNxv3As.js} +1 -1
- package/dist/web3/assets/{settings-EY4A_DYv.js → settings-DZjq-OQA.js} +1 -1
- package/dist/web3/assets/{share-2-vGmZUl90.js → share-2-C-OnE2ru.js} +1 -1
- package/dist/web3/assets/{tag-Br_y1f00.js → tag-CwJl7FdI.js} +1 -1
- package/dist/web3/assets/{trash-2-B4hRXo-p.js → trash-2-aljmIff4.js} +1 -1
- package/dist/web3/assets/{user-plus-4CTYEkd3.js → user-plus-DPerZexX.js} +1 -1
- package/dist/web3/assets/{zap-BcsKR0ef.js → zap-CHZtiN7P.js} +1 -1
- package/dist/web3/index.html +2 -2
- package/package.json +8 -3
- package/dist/client-D44ZHDTJ.js +0 -11
- package/dist/keys-CTOGAG3W.js +0 -20
- package/dist/web3/assets/ActivityTracePage-D2iIbEph.js +0 -1
- package/dist/web3/assets/AgentDocsPage-Dkhgb1hk.js +0 -1
- package/dist/web3/assets/CollaboratorManager-Bo81D--Q.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-DRCitQuX.js +0 -1
- package/dist/web3/assets/DocumentEditor-DV2WHYbj.js +0 -36
- package/dist/web3/assets/DocumentsTab-BUxBCN8o.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-kIBimZyE.js +0 -1
- package/dist/web3/assets/MarkdownEditor-CsVusP2d.css +0 -1
- package/dist/web3/assets/NestPageHeader-yOK-OIYZ.js +0 -1
- package/dist/web3/assets/NestView-DwIagxwJ.js +0 -68
- package/dist/web3/assets/OverviewTab-DFOFMVyE.js +0 -1
- package/dist/web3/assets/PersonCombobox-D350WlrJ.js +0 -1
- package/dist/web3/assets/ReasonDialog-CN3ECAnQ.js +0 -1
- package/dist/web3/assets/ReviewTab-CZewAYiz.js +0 -1
- package/dist/web3/assets/StewardsTab-AagNWcWS.js +0 -1
- package/dist/web3/assets/SubmitForReviewModal-BOZJxwRN.js +0 -1
- package/dist/web3/assets/backlinks-Cb8Uf8mw.js +0 -24
- package/dist/web3/assets/card-UDyMRcps.js +0 -1
- package/dist/web3/assets/count-skeleton-DBWCDhRy.js +0 -1
- package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
- package/dist/web3/assets/index-A0_ymcqL.js +0 -29
- package/dist/web3/assets/index-B5hLclEq.js +0 -389
- package/dist/web3/assets/index-DrUAtoQM.css +0 -1
- package/dist/web3/assets/page-BHD4beun.js +0 -1
- package/dist/web3/assets/page-BSYUXYmO.js +0 -11
- package/dist/web3/assets/page-BYc2DIux.js +0 -45
- package/dist/web3/assets/page-BfibLg8e.js +0 -1
- package/dist/web3/assets/page-CGe8MtUu.js +0 -9
- package/dist/web3/assets/page-CIgPJmG6.js +0 -2
- package/dist/web3/assets/page-Ck31nx9b.js +0 -1
- package/dist/web3/assets/page-Cpdj_11Y.js +0 -1
- package/dist/web3/assets/page-D0sN3GO6.js +0 -1
- package/dist/web3/assets/page-DEpH91X2.js +0 -1
- package/dist/web3/assets/page-l8-B7CGt.js +0 -1
- package/dist/web3/assets/page-mnSHmk6A.js +0 -6
- package/dist/web3/assets/refresh-cw-CsCtmWwX.js +0 -6
- package/dist/web3/assets/scroll-area-CbcH6yrZ.js +0 -1
- package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
- package/dist/web3/assets/select-DrL0mpRN.js +0 -6
- package/dist/web3/assets/triangle-alert-CsYGyfGZ.js +0 -6
- package/dist/web3/assets/x-qxt1WrUi.js +0 -6
package/API.md
CHANGED
|
@@ -12,6 +12,20 @@ Two methods, evaluated in order:
|
|
|
12
12
|
|
|
13
13
|
Anonymous open mode (`AUTH_MODE=open`) bypasses auth on most read endpoints.
|
|
14
14
|
|
|
15
|
+
### Nest-scoped API keys
|
|
16
|
+
|
|
17
|
+
A key minted with `nest_id` reaches **that one nest and nothing else**. It is deny-by-default: a request outside the list below answers **403** `{ "error": "This API key is scoped to one nest and can't …", "code": "nest_scoped_key" }`, before the route's own checks run, so a route added later is closed to nest keys until it is allowed on purpose.
|
|
18
|
+
|
|
19
|
+
| A nest-scoped key **can** reach | Notes |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `/nests/<its nest>` and everything under it, including `/nests/<its nest>/mcp` | The owner's permission on that nest still applies. |
|
|
22
|
+
| `GET /nests`, `GET /search`, `GET /stats` | Narrowed to its nest — other nests never appear. |
|
|
23
|
+
| `/mcp`, `GET /index` | Already filtered to its nest. |
|
|
24
|
+
| `/runs/<id>/*` | Only runs in its nest (403 otherwise). |
|
|
25
|
+
| `GET /health`, `GET /license/status`, `GET /llms.txt`, `GET /auth/admin-status` | Public / self-describing. |
|
|
26
|
+
|
|
27
|
+
Everything else is **403 `nest_scoped_key`** — notably `/auth/keys*` (a nest key can't mint, rotate, list or revoke keys, so it can't trade itself for a whole-account key), `POST /nests`, every other nest (`/nests/<other>/…` answers 403 "API key not authorized for this nest"; its MCP transport answers with a JSON-RPC error envelope), `/admin/*`, `/auth/teammates`, `/auth/invite`, `/auth/users/*`, `/auth/admin/*`, `/auth/profile`, `/auth/password`, `/teams*`, `/people*` and `/me/*` — even when the key's owner is a server admin. A session cookie sent alongside the key wins, as everywhere else. User-level keys (no `nest_id`) and sessions are unaffected.
|
|
28
|
+
|
|
15
29
|
All bodies are JSON unless noted. All errors return `{ "error": "msg" }` with appropriate HTTP status.
|
|
16
30
|
|
|
17
31
|
## Caller attribution (`client`)
|
|
@@ -121,16 +135,16 @@ Mints an API key for the caller. A user may hold **several** keys — one per pl
|
|
|
121
135
|
```
|
|
122
136
|
Response (201):
|
|
123
137
|
```json
|
|
124
|
-
{ "api_key": "cnst_...", "id": "uuid", "key_prefix": "cnst_abc123...", "label": "cli" }
|
|
138
|
+
{ "api_key": "cnst_...", "id": "uuid", "key_prefix": "cnst_abc123...", "label": "cli", "nest_id": null }
|
|
125
139
|
```
|
|
126
|
-
**Plaintext shown once.** Omit `nest_id` for a **user-level key** (works on every nest the user can access — what the Connect
|
|
140
|
+
**Plaintext shown once.** Omit `nest_id` for a **user-level key** (works on every nest the user can access — what the whole-brain Connect flow mints). With `nest_id` the key is **nest-scoped** — see [Nest-scoped API keys](#nest-scoped-api-keys) for exactly what it can reach. The caller must be able to reach that nest: an unknown nest, or one the caller has no access to, is **404** (the same answer for both, so this is no existence oracle). A nest-scoped key calling this endpoint gets **403 `nest_scoped_key`**.
|
|
127
141
|
|
|
128
142
|
### POST `/auth/keys/rotate` (auth required)
|
|
129
143
|
Replaces **one** key and returns its new plaintext; the caller's other keys are untouched.
|
|
130
144
|
|
|
131
145
|
`key_id` names the target. Omitting it is only accepted when the account holds exactly one key (pre-multi-key clients) — with several keys and no `key_id` the server answers **400** rather than guessing which to replace. **404** if the id isn't the caller's.
|
|
132
146
|
|
|
133
|
-
Scope resolution on the optional `nest_id` field: **absent** → keep the prior scope; **`null` (or `""`)** → clear to a user-level key; **a nest id** → scope to that nest. Same for `label` (absent keeps the prior label). Supplying a `label` that matches a *different* key of the caller's is **409**; renaming a key to what it is already called is fine.
|
|
147
|
+
Scope resolution on the optional `nest_id` field: **absent** → keep the prior scope; **`null` (or `""`)** → clear to a user-level key; **a nest id** → scope to that nest (**404** unless the caller can reach it, and the old key is left alone). The response reports the new key's `nest_id`. Un-scoping has to come from a session or a user-level key — a nest-scoped key calling any `/auth/keys*` route gets **403 `nest_scoped_key`**. Same for `label` (absent keeps the prior label). Supplying a `label` that matches a *different* key of the caller's is **409**; renaming a key to what it is already called is fine.
|
|
134
148
|
```json
|
|
135
149
|
{ "key_id": "uuid", "label": "optional", "nest_id": "optional" }
|
|
136
150
|
```
|
|
@@ -159,8 +173,7 @@ Creates an invited user and returns a **temporary password** (once) for the admi
|
|
|
159
173
|
{ "email": "teammate@x.com" }
|
|
160
174
|
```
|
|
161
175
|
- New email → user row with `is_invited = 1` and a temporary password.
|
|
162
|
-
-
|
|
163
|
-
- Existing, claimed email (has logged in or set their own password, `is_invited = 0`) → `409`; use Reset password instead.
|
|
176
|
+
- Any existing email → `409`, credentials untouched. The error says "already a member" once they've signed in (`is_invited = 0`), or "already invited but hasn't signed in yet" (`is_invited = 1`, including people a nest was shared with). Use Reset password to issue a new temporary password.
|
|
164
177
|
|
|
165
178
|
`is_invited` clears to `0` on the teammate's first successful login (or when they set their own password).
|
|
166
179
|
|
|
@@ -169,9 +182,12 @@ Response (201):
|
|
|
169
182
|
{
|
|
170
183
|
"temporary_password": "...",
|
|
171
184
|
"user": { "id": "...", "email": "..." },
|
|
185
|
+
"promptowl_sign_in": true,
|
|
186
|
+
"emailed": true,
|
|
172
187
|
"message": "Share this temporary password securely — it won't be shown again."
|
|
173
188
|
}
|
|
174
189
|
```
|
|
190
|
+
`emailed` — email is configured, so the invite email (password + sign-in link) was sent. `promptowl_sign_in` — PromptOwl sign-in is open to everyone, so the teammate may use it instead (the email says so too).
|
|
175
191
|
|
|
176
192
|
### GET `/auth/teammates` (admin only)
|
|
177
193
|
Returns all users + key counts. **Sorted: admins first, then `created_at DESC`.**
|
|
@@ -221,12 +237,12 @@ Starts generic **OIDC single sign-on** (Microsoft Entra ID, Google, or any spec-
|
|
|
221
237
|
When enabled and fully configured: fetches the issuer's discovery document (cached in-process ~5 min), mints `state` + `nonce` + a PKCE S256 verifier into three short-lived (10 min) httpOnly `SameSite=Lax` cookies scoped to `Path=/auth/oidc`, and `302`s to the IdP's authorization endpoint with `response_type=code&scope=openid email profile`. When disabled/unconfigured → `302` to `/?sso_error=disabled` / `not_configured`. Rate-limited per IP.
|
|
222
238
|
|
|
223
239
|
### GET `/auth/oidc/callback`
|
|
224
|
-
The redirect URI (`<base url>/auth/oidc/callback` — register this exact URI at your IdP). Verifies the `state` param against the flow cookie, exchanges the code at the token endpoint (client secret + PKCE verifier), validates the ID token with the issuer's JWKS (signature, `iss`, `aud` = client id, `exp`, and the `nonce` claim against the flow cookie), extracts the email (`email`, falling back to `preferred_username` for Entra; normalized trim+lowercase), and enforces `oidc_allowed_domains`. Existing users are signed in; unknown users are JIT-provisioned when `oidc_auto_provision` is on (name from the `name` claim), else refused. Every successful login also upserts `users.department` from the optional string `department` claim (trimmed, whitespace collapsed, capped at 120 chars; absent claim clears the stored value) — read by the `oidc_department_tagging` feature (see `CONFIGURATION.md → Department auto-tagging`). On success → session cookie (identical to a password login) + `302` to `/`. On any failure → `302` to `/?sso_error=<code>`; codes: `disabled`, `not_configured`, `discovery_failed`, `provider_error`, `state_mismatch`, `exchange_failed`, `invalid_token`, `email_not_verified` (ID token carries `email_verified: false` — an absent claim is trusted; see the trust assumption in `CONFIGURATION.md → Single sign-on (OIDC)`), `domain_not_allowed`, `not_invited`, `rate_limited`, `service_error
|
|
240
|
+
The redirect URI (`<base url>/auth/oidc/callback` — register this exact URI at your IdP). Verifies the `state` param against the flow cookie, exchanges the code at the token endpoint (client secret + PKCE verifier), validates the ID token with the issuer's JWKS (signature, `iss`, `aud` = client id, `exp`, and the `nonce` claim against the flow cookie), extracts the email (`email`, falling back to `preferred_username` for Entra; normalized trim+lowercase), and enforces `oidc_allowed_domains`. Existing users are signed in; unknown users are JIT-provisioned when `oidc_auto_provision` is on (name from the `name` claim), else refused. Every successful login also upserts `users.department` from the optional string `department` claim (trimmed, whitespace collapsed, capped at 120 chars; absent claim clears the stored value) — read by the `oidc_department_tagging` feature (see `CONFIGURATION.md → Department auto-tagging`). On success → session cookie (identical to a password login) + `302` to `/`. On any failure → `302` to `/?sso_error=<code>`; codes: `disabled`, `not_configured`, `discovery_failed`, `provider_error`, `state_mismatch`, `exchange_failed`, `invalid_token`, `email_not_verified` (ID token carries `email_verified: false` — an absent claim is trusted; see the trust assumption in `CONFIGURATION.md → Single sign-on (OIDC)`), `domain_not_allowed`, `not_invited`, `rate_limited`, `service_error` (cause and fix for each: `CONFIGURATION.md → Troubleshooting SSO sign-in`). Flow cookies are cleared on every outcome. Rate-limited per IP.
|
|
225
241
|
|
|
226
242
|
### GET `/auth/oidc/logout`
|
|
227
243
|
RP-initiated logout for an SSO session. Deletes the current session server-side and clears the session cookie plus the `cnst_sso_session` marker. If the issuer advertises an `end_session_endpoint` (Entra/Okta/Keycloak; Google does not), `302`s the browser through it with `post_logout_redirect_uri=<base url>/` so the IdP session ends too; otherwise `302`s straight to `/`. Always clears cookies and always lands on `/`, even if discovery is unreachable. The SPA navigates here (instead of `POST /auth/logout`) only when the non-httpOnly `cnst_sso_session` marker cookie — set on OIDC callback success — is present, so password sessions keep the plain logout.
|
|
228
244
|
|
|
229
|
-
Related `/admin/settings` fields (GET/PATCH,
|
|
245
|
+
Related `/admin/settings` fields (GET/PATCH, server admin only — the license admin or a superadmin): `oidc_enabled`, `oidc_issuer` (https URL), `oidc_client_id`, `oidc_client_secret` (**write-only** — PATCH sets/replaces, `null` clears; GET returns only `oidc_client_secret_set: boolean`), `oidc_allowed_domains` (comma-separated), `oidc_auto_provision`, `oidc_department_tagging` (append `dept:<slugified-department>` to a new document's tags on create — create-only, no-op for users without a stored department). Enabling is refused unless issuer + client id + secret are all present.
|
|
230
246
|
|
|
231
247
|
### POST `/auth/token-exchange`
|
|
232
248
|
Exchange an IdP ID token for a short-lived, scoped **MCP bearer** (RFC 8693-shaped) — for an external agent acting on behalf of a user your IdP already authenticated. No session, no API key. **Off by default; security-critical** — see `CONFIGURATION.md → Agent SSO (token exchange)`.
|
|
@@ -259,6 +275,8 @@ Returns owned + shared nests. Archived nests are **absent** — as they are from
|
|
|
259
275
|
```
|
|
260
276
|
Response (201): `{ "nest": { ... } }`
|
|
261
277
|
|
|
278
|
+
With `NEST_CREATION=admins` (the default) only a server admin may create or import a nest; anyone else gets `403` ("Only a server admin can create nests on this server…"). The same rule covers `POST /nests/import` and the MCP `nest_create` tool. `GET /health` reports it as `nest_creation`.
|
|
279
|
+
|
|
262
280
|
### GET `/nests/:nestId`
|
|
263
281
|
```json
|
|
264
282
|
{ "nest": { ... }, "permission": "owner" | "admin" | "write" | "read" }
|
|
@@ -371,6 +389,41 @@ Response (200) echoes the persisted row:
|
|
|
371
389
|
```
|
|
372
390
|
The two additive columns (`nests.reader_mode`, `nests.reader_home_node`) also surface on `GET /nests/:nestId` via the `SELECT *` nest payload.
|
|
373
391
|
|
|
392
|
+
### PATCH `/nests/:nestId/branding` (nest owner / nest admin / server admin)
|
|
393
|
+
Per-nest branding for the public reader (reader mode) and the nest's `/p` published pages. The body **replaces** the nest's branding; `null`, an empty body, or `{}` resets it to unbranded.
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"site_title": "Acme Docs",
|
|
397
|
+
"logo_url": "/nests/<nestId>/assets/<uuid>.png",
|
|
398
|
+
"banner_url": "https://cdn.example.com/banner.jpg",
|
|
399
|
+
"banner_headline": "Acme Handbook",
|
|
400
|
+
"banner_subtext": "Everything we know, versioned.",
|
|
401
|
+
"accent_color": "#e11d48",
|
|
402
|
+
"header_bg_color": "#0f172a",
|
|
403
|
+
"page_tint_color": "#fef2f2",
|
|
404
|
+
"footer_text": "© Acme Corp",
|
|
405
|
+
"footer_link_url": "https://acme.example.com/",
|
|
406
|
+
"footer_link_label": "acme.example.com",
|
|
407
|
+
"favicon_url": "/nests/<nestId>/assets/<uuid>.png",
|
|
408
|
+
"hide_powered_by": false
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
Every field is optional. Rules (any violation → `400`, nothing saved):
|
|
412
|
+
- **Colors** (`accent_color`, `header_bg_color`, `page_tint_color`): `#rgb` or `#rrggbb` only; stored as lowercase `#rrggbb`. Anything else — named colors, `red; background:url(…)`, `var()` — is refused.
|
|
413
|
+
- **Images** (`logo_url`, `banner_url`, `favicon_url`): an image asset uploaded to **this** nest (`POST /nests/:nestId/assets` → `/nests/<nestId>/assets/<uuid>.<png|jpg|gif|webp>`, must exist), or an `https://` URL (no credentials) whose path ends in `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp`. `javascript:`, `data:`, `http:`, protocol-relative, SVG, video assets and other nests' assets are refused.
|
|
414
|
+
- **`footer_link_url`**: an `https://` URL.
|
|
415
|
+
- **Text** (`site_title` ≤ 120, `banner_headline` ≤ 120, `banner_subtext` ≤ 280, `footer_text` ≤ 280, `footer_link_label` ≤ 80): plain text, no control characters; always rendered escaped.
|
|
416
|
+
- **Unknown keys** are refused — there is no free-form CSS or JS.
|
|
417
|
+
- **`hide_powered_by`** (boolean): hides "Powered by ContextNest" in the reader and "Secured by PromptOwl · ContextNest" on `/p`. Turning it on is **server-admin only** (`403` for a nest owner/admin). A non-admin edit that omits the field keeps the stored value — a non-admin full clear (`null` / `{}`) included, which then stores `{ "hide_powered_by": true }`; a non-admin may turn it off explicitly.
|
|
418
|
+
|
|
419
|
+
Gate: the access middleware requires the `admin` tier for this resource (owner, admin collaborator, or server admin); the handler re-checks it. Editors and viewers get `403`, anonymous callers `401`.
|
|
420
|
+
|
|
421
|
+
Response (200): `{ "branding": { …normalised… } }` or `{ "branding": null }`.
|
|
422
|
+
|
|
423
|
+
**Reading it.** There is no separate GET: `GET /nests/:nestId` returns `nest.branding` (parsed and re-validated against the write rules — a stored URL that no longer passes is dropped; `null` when unbranded) in place of the raw `branding_json` column. It is therefore readable exactly by whoever can read the nest — an anonymous reader of a public nest sees it; a private nest still `404`s.
|
|
424
|
+
|
|
425
|
+
**Where it applies.** The SPA reader (read-only callers in reader mode, and an owner's "Preview as reader") uses it for the header logo + title (falling back to the server `LOGO_URL` and the nest name), the banner on the home page, accent/header/page-tint colors, footer, favicon and tab title. On `/p`, the markdown document view, the gate page and the comment shell pick up the colors, title, logo, banner and footer; **the CSP is unchanged** — uploaded images are inlined as `data:` URIs (already allowed by `img-src data:`), and external https images are not rendered there. HTML/artifact publishes are served untouched.
|
|
426
|
+
|
|
374
427
|
### GET `/nests/unsynced` (server-admin only outside open mode)
|
|
375
428
|
Lists folders under `DATA_ROOT` that aren't nests yet — typically dropped in by the starter CLI or copied in manually. Each entry is a sync candidate. Reflects server-side filesystem state, so in non-open mode only a server admin — the license admin or any superadmin (`access.yaml` ∪ granted; same resolution as `/admin/*`) — sees it (everyone else gets `403`).
|
|
376
429
|
|
|
@@ -1136,6 +1189,12 @@ The UI deep-links a comment item to its document with the thread focused via
|
|
|
1136
1189
|
|
|
1137
1190
|
Each row carries `nest_id`, `nest_name` (resolved in the same read, so the inbox navigates without fetching the nest list), `kind`, `message`, `created_at`, `read_at`, and `node_id` when the row is about a document (review verdicts, document deletion flags, mentions); nest-level rows have `node_id: null`.
|
|
1138
1191
|
|
|
1192
|
+
Unread `nest_invite` rows are what the Nests page shows as **Pending invites**. Accept marks them read (`POST /me/notifications/read`) and opens the nest.
|
|
1193
|
+
|
|
1194
|
+
### POST `/me/nests/:nestId/leave`
|
|
1195
|
+
|
|
1196
|
+
Decline a nest invite. Removes the caller's **own** collaborator row on that nest (never anyone else's) and marks its `nest_invite` rows read. Response: `{ "left": true }`. Access that isn't a direct collaborator row — ownership, or a share through a team — isn't touched: `{ "left": false }`, and the invite rows are still marked read.
|
|
1197
|
+
|
|
1139
1198
|
### GET `/me/drafts`
|
|
1140
1199
|
|
|
1141
1200
|
Query: `?limit=25&offset=0&q=&sort=updated|title` (limit clamps to 1–100, offset to ≥ 0)
|
|
@@ -1533,8 +1592,8 @@ Body (give `prompt` OR `selector`; everything else optional):
|
|
|
1533
1592
|
}
|
|
1534
1593
|
```
|
|
1535
1594
|
- `prompt` — natural language. Tokens are matched against tag names and titles, and every document is scored against them (title hits weigh most, then tags, then body words); documents under half the top score are dropped as noise. A prompt that matches nothing returns `nodes: []` — never the whole nest. `trace.compiled.fallback` is `"fulltext"` when body scoring alone produced the answer.
|
|
1536
|
-
- `selector` — explicit selector grammar (e.g. `#auth`). Wins over `prompt`.
|
|
1537
|
-
- `hops` —
|
|
1595
|
+
- `selector` — explicit selector grammar (e.g. `#auth`). Wins over `prompt`. May carry `[[Title]]` parts in an OR union (`#auth | [[Auth Overview]]`), and `[[nodes/auth-overview]]` names one document by id where a title may match several.
|
|
1596
|
+
- `hops` — link expansion depth (`[[wikilinks]]` and `contextnest://` links, both directions). Defaults to `0` for a `prompt` (neighbours of a keyword match are not answers to the question) and `2` for a `selector`.
|
|
1538
1597
|
- `max_tokens` — budget, floored at 50, default 4000. A node that doesn't fit is skipped and the next tried (`trace.truncated_by_budget` counts the skips); only when nothing fits does the first node go through oversized.
|
|
1539
1598
|
- 400 `prompt or selector is required` if both missing.
|
|
1540
1599
|
- Each node carries a `url` deep link, and its context block a matching `_source: <url>_` line, so answers can cite sources. Built from `PUBLIC_BASE_URL`, falling back to the request origin; omitted when neither resolves.
|
|
@@ -1649,6 +1708,39 @@ Deterministic fact lookup against a **table** node (CSV body): fetch the exact r
|
|
|
1649
1708
|
```
|
|
1650
1709
|
Same operation over MCP: `context_table_query`.
|
|
1651
1710
|
|
|
1711
|
+
### POST `/nests/:nestId/chat`
|
|
1712
|
+
Hootie in the nest — ask a question about the nest and get a streamed, cited answer. Read tier, like `/context`: retrieval runs through the same `/context` path **as the caller**, so the answer can only draw on documents this person may read. Gated by `HOOTIE_ENABLED` (off → `404`); needs the server-wide `ANTHROPIC_API_KEY` (missing → `503` with `reason: "no_api_key"`); answers with `HOOTIE_MODEL`, through `ANTHROPIC_BASE_URL` when one is set (all three editable in Settings → General). Rate-limited per user across all nests — the budget guards the one server-wide key (`429`).
|
|
1713
|
+
|
|
1714
|
+
Request:
|
|
1715
|
+
|
|
1716
|
+
```json
|
|
1717
|
+
{
|
|
1718
|
+
"message": "how do requests authenticate?",
|
|
1719
|
+
"node_id": "nodes/api-authentication-guide",
|
|
1720
|
+
"history": [
|
|
1721
|
+
{ "role": "user", "content": "…" },
|
|
1722
|
+
{ "role": "assistant", "content": "…" }
|
|
1723
|
+
]
|
|
1724
|
+
}
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
- `message` (required) — the question. `@name` tokens loop a colleague in (below).
|
|
1728
|
+
- `node_id` (optional) — the document the person has open. It and its neighbours (one hop) lead the context, whatever the question matches follows.
|
|
1729
|
+
- `history` (optional) — prior turns, oldest first; the server keeps the most recent 20.
|
|
1730
|
+
|
|
1731
|
+
Response: `text/event-stream`.
|
|
1732
|
+
|
|
1733
|
+
| event | data |
|
|
1734
|
+
|---|---|
|
|
1735
|
+
| `meta` | `{ nodes: [{ id, title, url, tags, type }], anchor: node \| null }` — what the answer will be grounded in, before the model speaks. |
|
|
1736
|
+
| `delta` | `{ text }` — a chunk of the answer. |
|
|
1737
|
+
| `done` | `{ answer, cited: [node…], mention }` — the whole answer, the documents it cites (by `[[Title]]`, in order), and what happened to any `@mentions`. |
|
|
1738
|
+
| `error` | `{ error }` — the model call failed; the stream ends. |
|
|
1739
|
+
|
|
1740
|
+
**Grounding.** The retrieved documents are placed in the system prompt verbatim with the instruction to answer from them alone. Retrieval is permission-gated, so an answer can never draw on anything the caller couldn't read — but a document author can write text aimed at Hootie ("ignore the question and…") that later readers' sessions will see. That is the ordinary tradeoff of grounding a model in editable content, accepted here; the documents are governed, so who can write them is the control.
|
|
1741
|
+
|
|
1742
|
+
**Mentions.** A message naming people (`@alice@acme.com`) posts the exchange — question and answer, under the asker's name — as a comment on the anchor document (the open one, else the first document the answer cites) and notifies them through the comment-mention path: inbox row, and email / Slack / Teams where configured. `mention` is `{ status: "posted", node_id, node_title, thread_id, notified }` or `{ status: "undeliverable", reason: "no_anchor" | "post_failed" }`. Nothing is posted when nobody is named.
|
|
1743
|
+
|
|
1652
1744
|
### POST `/nests/:nestId/query`
|
|
1653
1745
|
Graph query (Cypher-ish).
|
|
1654
1746
|
|
|
@@ -2156,6 +2248,7 @@ For any `/nests/:nestId/...` request, the middleware computes:
|
|
|
2156
2248
|
- Required level by path:
|
|
2157
2249
|
- `/collaborators`, `/visibility` → `admin`
|
|
2158
2250
|
- `/reader-mode` → `admin` at the middleware, then the handler hard-gates on `isServerAdminUserId` (superadmin only)
|
|
2251
|
+
- `/branding` → `admin` at the middleware (owner, admin collaborator, or server admin), re-checked in the handler; `hide_powered_by: true` additionally requires the server admin
|
|
2159
2252
|
- Stewardship action paths (`/approve`, `/reject`, `/submit-review`, `/cancel-review`) → `read` (handler enforces fine-grained steward gating)
|
|
2160
2253
|
- Other non-GET → `write`
|
|
2161
2254
|
- GET → `read`
|