@promptowl/contextnest-community 1.24.0 → 1.26.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 +177 -14
- package/CONFIGURATION.md +577 -116
- package/README.md +4 -3
- package/STEWARDSHIP.md +24 -0
- package/dist/{chunk-XUIWAWDO.js → chunk-2QOJE7A5.js} +4 -4
- package/dist/{chunk-2TPQTN4Y.js → chunk-DGPTJXIV.js} +47 -15
- package/dist/chunk-I5EDEZBQ.js +523 -0
- package/dist/{chunk-ZTT4U4NE.js → chunk-IEAWRHMV.js} +99 -12
- package/dist/{chunk-YVMSM7LS.js → chunk-LHWOJPFE.js} +7 -0
- package/dist/{chunk-5EZOPA47.js → chunk-M6PGYMBD.js} +20 -11
- package/dist/{chunk-F66VCBLA.js → chunk-PICUHQEM.js} +51 -2
- package/dist/{chunk-I3CSD6CK.js → chunk-RPGFHESR.js} +2 -2
- package/dist/{chunk-KIAAEHWL.js → chunk-SWA5M5XX.js} +88 -546
- package/dist/{chunk-LA3VTQ22.js → chunk-TRSGPS27.js} +81 -14
- package/dist/{engine-S3QBQ7LH.js → engine-QNMFDIBV.js} +4 -3
- package/dist/{external-edit-service-6FNFPOJJ.js → external-edit-service-4MEGPT4D.js} +5 -4
- package/dist/{grants-service-UT3EQ3R7.js → grants-service-24GLUBY4.js} +3 -3
- package/dist/index.js +1739 -583
- package/dist/invited-user-AM6A3WGO.js +12 -0
- package/dist/{migrations.postgres-AIQ7WSU7.js → migrations.postgres-HGSQ7IA5.js} +16 -2
- package/dist/{review-service-HHGOTO6P.js → review-service-2EWN7WWO.js} +8 -7
- package/dist/{stewardship-service-4DHNRULG.js → stewardship-service-TDZUC2Q6.js} +7 -4
- package/dist/{version-service-KIOQME6U.js → version-service-C3JTZLJH.js} +5 -4
- package/dist/web3/assets/ActivityTracePage-DzwWIjjx.js +1 -0
- package/dist/web3/assets/{AgentDocsPage-CIiaBqMy.js → AgentDocsPage-ixP2UdAE.js} +1 -1
- package/dist/web3/assets/CollaboratorManager-C9VAXZ0h.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-tq4ZTexH.js +1 -0
- package/dist/web3/assets/DocumentEditor-C2CP2fiE.js +36 -0
- package/dist/web3/assets/DocumentsTab-Mh7KxUoB.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-B_CWKXZ_.js +1 -0
- package/dist/web3/assets/MarkdownEditor-CKh4WSn-.js +653 -0
- package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
- package/dist/web3/assets/NestPageHeader-Cizo8nUi.js +1 -0
- package/dist/web3/assets/NestView-EW-sraEb.js +73 -0
- package/dist/web3/assets/OverviewTab-CjidsMX_.js +6 -0
- package/dist/web3/assets/PersonCombobox-BKI7dff4.js +1 -0
- package/dist/web3/assets/{ReviewActions-OfOdqzCn.js → ReviewActions-CS8JXgXF.js} +2 -2
- package/dist/web3/assets/ReviewTab-CwVrXzw5.js +1 -0
- package/dist/web3/assets/StewardsTab-C-iP6gnh.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-Biq3m79i.js +1 -0
- package/dist/web3/assets/{alert-dialog-DTozKlmV.js → alert-dialog-BvF3NFr7.js} +2 -2
- package/dist/web3/assets/{arrow-left-BCKt4CwQ.js → arrow-left-CWIsv-sG.js} +1 -1
- package/dist/web3/assets/backlinks-Cqcn7n1J.js +19 -0
- package/dist/web3/assets/boxes-ClbgQqG2.js +6 -0
- package/dist/web3/assets/{chevron-left-BPwUT9DS.js → chevron-left-CK_ZPFm9.js} +1 -1
- package/dist/web3/assets/{circle-check-DI8IBGeo.js → circle-check-Bw_aTXiH.js} +1 -1
- package/dist/web3/assets/{circle-x-VKVhC88W.js → circle-x-DkIG68H7.js} +1 -1
- package/dist/web3/assets/client-attribution-BraarYaP.js +1 -0
- package/dist/web3/assets/{code-xml-CMabYZvH.js → code-xml-BlR7EN6l.js} +1 -1
- package/dist/web3/assets/{corner-down-right-BGxwK6wH.js → corner-down-right-DSVxK3tf.js} +1 -1
- package/dist/web3/assets/count-skeleton-DToLM74B.js +1 -0
- package/dist/web3/assets/{earth-BEp1EKTo.js → earth-BXhVPvCR.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-r8qZGIjP.js → file-exclamation-point-BevGZgtj.js} +1 -1
- package/dist/web3/assets/{folder-input-Cmtx1Jhk.js → folder-input-m8UBV4fA.js} +1 -1
- package/dist/web3/assets/{index-C1wTSjfP.js → index-BOVYgMvj.js} +1 -1
- package/dist/web3/assets/index-DVnaYqj9.css +1 -0
- package/dist/web3/assets/index-WYDmEOfO.js +404 -0
- package/dist/web3/assets/page-B6pkc9cd.js +2 -0
- package/dist/web3/assets/page-BJrFCKXB.js +1 -0
- package/dist/web3/assets/page-Bgy9Ng9p.js +1 -0
- package/dist/web3/assets/page-C2zoxumw.js +1 -0
- package/dist/web3/assets/page-CJWO06dN.js +1 -0
- package/dist/web3/assets/page-CptuAPJc.js +6 -0
- package/dist/web3/assets/page-DPY01SOj.js +1 -0
- package/dist/web3/assets/page-Dea2SblF.js +9 -0
- package/dist/web3/assets/page-Deq6IKiN.js +6 -0
- package/dist/web3/assets/page-Dfjg8p7Z.js +1 -0
- package/dist/web3/assets/{page-ByyVLDVo.js → page-Dh57L0RG.js} +5 -5
- package/dist/web3/assets/page-Dwbp4Seu.js +6 -0
- package/dist/web3/assets/{page-CHb0F5hV.js → page-DzKwP5Ds.js} +3 -3
- package/dist/web3/assets/page-YxjrE5b5.js +45 -0
- package/dist/web3/assets/{page-title-ClvvBsIo.js → page-title-DVq4qvMR.js} +1 -1
- package/dist/web3/assets/{play-C5YNKpAH.js → play-CXhS6Pgq.js} +1 -1
- package/dist/web3/assets/{send-0CHvmluT.js → send-DdfwnIrj.js} +1 -1
- package/dist/web3/assets/{settings-CeeF4LEb.js → settings-DKX0Q4gO.js} +1 -1
- package/dist/web3/assets/{share-2-CMnOlOt-.js → share-2-CxmXRt58.js} +1 -1
- package/dist/web3/assets/{tag-DQ_6J5Gv.js → tag-DeYC_mRv.js} +1 -1
- package/dist/web3/assets/{trash-2-HBi-Pslz.js → trash-2-B0HyPTnI.js} +1 -1
- package/dist/web3/assets/{user-plus-CvBNcpn4.js → user-plus-Ckm_Isyc.js} +1 -1
- package/dist/web3/assets/{x-By7piikG.js → x-mNtADYHc.js} +1 -1
- package/dist/web3/assets/zap-D2ifcnYb.js +16 -0
- package/dist/web3/index.html +2 -2
- package/package.json +3 -2
- package/dist/client-KEY4PYJH.js +0 -11
- package/dist/keys-CTOGAG3W.js +0 -20
- package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +0 -1
- package/dist/web3/assets/CollaboratorManager-By91wrIr.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +0 -1
- package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +0 -36
- package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +0 -1
- package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +0 -1
- package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +0 -643
- package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +0 -1
- package/dist/web3/assets/NestView-CTZ55tXD.js +0 -63
- package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +0 -1
- package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +0 -1
- package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +0 -1
- package/dist/web3/assets/ReviewTab-CyX8yxd3.js +0 -1
- package/dist/web3/assets/StewardsTab-DBEEfAQI.js +0 -1
- package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +0 -1
- package/dist/web3/assets/backlinks-CYd4xENL.js +0 -24
- package/dist/web3/assets/card-XG2dP5Xf.js +0 -1
- package/dist/web3/assets/count-skeleton-DMbdpscX.js +0 -1
- package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
- package/dist/web3/assets/index-BM-h3DwI.css +0 -1
- package/dist/web3/assets/index-EaX2yql0.js +0 -389
- package/dist/web3/assets/page-B3yvQvHy.js +0 -1
- package/dist/web3/assets/page-BExMvttJ.js +0 -1
- package/dist/web3/assets/page-BUOADv2X.js +0 -1
- package/dist/web3/assets/page-BYUFhYWZ.js +0 -1
- package/dist/web3/assets/page-Bicf02Cz.js +0 -1
- package/dist/web3/assets/page-BsPMDm5d.js +0 -45
- package/dist/web3/assets/page-CC0NJi8v.js +0 -1
- package/dist/web3/assets/page-CPjWQNKx.js +0 -11
- package/dist/web3/assets/page-CQt3ZGbf.js +0 -2
- package/dist/web3/assets/page-Cj1NetQ-.js +0 -1
- package/dist/web3/assets/page-CtaW65El.js +0 -1
- package/dist/web3/assets/page-DGOL9l8i.js +0 -6
- package/dist/web3/assets/refresh-cw-BhHzSH9m.js +0 -6
- package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
- package/dist/web3/assets/scroll-area-hz-xtayP.js +0 -1
- package/dist/web3/assets/select-DBFTrUmI.js +0 -6
- package/dist/web3/assets/triangle-alert-CDy8-7sv.js +0 -6
- package/dist/web3/assets/zap-C2T8riBp.js +0 -11
package/CONFIGURATION.md
CHANGED
|
@@ -11,7 +11,7 @@ The server supports two authentication modes. You pick one per instance with `AU
|
|
|
11
11
|
Every request requires an `Authorization: Bearer cnst_...` header. Use this for any multi-user or internet-facing deployment.
|
|
12
12
|
|
|
13
13
|
- Users register via `POST /auth/register` (email + password) or log in with PromptOwl via `POST /auth/device` → `POST /auth/promptowl`.
|
|
14
|
-
- API keys are per-user (`cnst_<64-hex>`) and can be scoped to a single nest for service-account use.
|
|
14
|
+
- API keys are per-user (`cnst_<64-hex>`) and can be scoped to a single nest for service-account use. A nest-scoped key reaches that nest (its REST routes and `/nests/<id>/mcp`), the server `/mcp` and `/index` filtered to it, and `GET /nests` / `/search` / `/stats` narrowed to it — and nothing else: other nests, `/auth/keys*` (so it can't mint itself a wider key), `/admin/*`, teammates, teams, people and `/me/*` answer `403` with `code: "nest_scoped_key"`, even when the key's owner is a server admin. Mint one on the API keys page (scope picker) or from a nest's Connect dialog ("Create a key for this nest only"). See `API.md → Nest-scoped API keys`.
|
|
15
15
|
- Rate-limited login / register / device-auth endpoints (sliding window, per IP + per email).
|
|
16
16
|
- First PromptOwl-authenticated user becomes the server admin (atomic claim). Admin can invite teammates at `POST /auth/invite`.
|
|
17
17
|
|
|
@@ -58,17 +58,19 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
58
58
|
| `PROMPTOWL_API_URL` | `https://app.promptowl.ai` | PromptOwl's API origin — used for device auth, license validation, telemetry. Override for air-gapped or test setups. |
|
|
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
|
-
| `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
|
|
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 **
|
|
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
|
-
| `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
|
|
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 Server settings → General. The server refuses `disabled` while PromptOwl sign-in is also `disabled`, unless SSO (OIDC) is on and fully configured (that would leave no way to log in — see [Making SSO the only way in](#making-sso-the-only-way-in)). Unknown values fall back to `open`. |
|
|
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 **Server 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
|
+
| `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. (5) It is rendered into the copy-paste setup snippets on **Server settings → Connecting agents** — a `bash` block and quoted strings in `config.toml` / `config.yaml` / `mcp.json`. Because those are pasted verbatim into other people's terminals, `PATCH /admin/settings` refuses a value that is not a plain `http(s)` URL, or that contains whitespace, quotes or shell characters (`" ' ` \ $ ; | & < > ( ) { }`). A value stored before that check is not rewritten, so re-save it if the panel shows it oddly. |
|
|
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 **Admin → Server settings → Single sign-on**. See the [Single sign-on (OIDC) admin guide](#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`. |
|
|
66
66
|
| `OIDC_CLIENT_ID` | `""` | Application (client) ID from your IdP app registration. |
|
|
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
|
-
| `
|
|
71
|
-
| `
|
|
70
|
+
| `NEST_CREATION` | `admins` | Who may create or import nests (REST, folder import, MCP `nest_create`). `admins`: server admins only — the default until a server is on a paid plan; everyone else sees "You don't have access to any nests yet. Ask [admin] to share one." instead of a create button. `everyone`: any signed-in user. Open mode is unaffected. Also editable from **Server settings → General → Nest creation**. |
|
|
71
|
+
| `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 **Server settings → General → People directory**. |
|
|
72
|
+
| `SHARED_DEPLOYMENT` | `false` | This server hosts **several unrelated organizations** (the PromptOwl-hosted deployment) rather than one company. A nest's `org` visibility means *every authenticated account on this deployment* — on a single-company self-hosted server that is the organization, so the tier flips in one click; on a shared server it is every customer. With this on: `PATCH /nests/:id/visibility` to `org` requires `acknowledge_public: true` exactly as `public` always does (enforced by the server, not only the UI), and the UI's visibility picker confirms before *Organization* and words both tiers to say who they really reach. Reported on `GET /health` as `shared_deployment`. Also editable from **Server settings → General → This server hosts several organizations**. **Turning it on guards future changes only** — nests already at `org` stay readable by every account until their admins reconfirm or narrow them. On an already-populated server, audit them first: the dashboard's *Organization* scope lists every one, or `SELECT id, name, user_id FROM nests WHERE visibility = 'org'`. |
|
|
73
|
+
| `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 Server settings → Single sign-on. |
|
|
72
74
|
| `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). |
|
|
73
75
|
| `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. |
|
|
74
76
|
| `MCP_TOKEN_TTL_SECONDS` | `300` | Lifetime of a minted MCP bearer, clamped to 30–3600. |
|
|
@@ -77,20 +79,24 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
77
79
|
| `ENV_FILE_PATH` | `$DATA_ROOT/.env` | Path to an optional `.env` file the server reads at boot (in addition to `$cwd/.env`). **No longer used for persistence** — the License Setup Page and Settings page now write to the database, not this file (see [Runtime settings persistence](#runtime-settings-persistence)). Kept for operators who bootstrap config from a mounted `.env`. |
|
|
78
80
|
| `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
|
|
79
81
|
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
|
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
|
|
81
|
-
| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog ingestion host. Set it to your own region or self-hosted PostHog. Also editable from
|
|
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
|
|
82
|
+
| `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 Server settings → Advanced. |
|
|
83
|
+
| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog ingestion host. Set it to your own region or self-hosted PostHog. Also editable from Server settings → Advanced. |
|
|
84
|
+
| `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 Server settings → Advanced. |
|
|
83
85
|
| `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. |
|
|
86
|
+
| `CONTEXTNEST_AGENT` | `""` | Overrides the `agent` this server derives for caller attribution (`client`, spec §9.4) when a call sends none of its own. Without it the name comes from the MCP `initialize` handshake's `clientInfo.name` when the connection reports one, else the label of the API key the call authenticated with. A caller's own `client.agent` always wins. |
|
|
87
|
+
| `CONTEXTNEST_SESSION_ID` | `""` | Same, for `session_id` — otherwise the MCP transport's session id, or an `Mcp-Session-Id` request header when the client sends one. Left absent when nothing reports one: the `/mcp` endpoints are stateless and issue no session, and a placeholder would be indistinguishable from a real id. |
|
|
88
|
+
| `CONTEXTNEST_NO_ATTRIBUTION` | `""` | Set to `1` to derive nothing at all. What this server derives lands in an append-only version history, so an operator who does not want an agent's name recorded there permanently needs to say so before the first write. A `client` the caller sent explicitly is still recorded — that is the caller's own record to make. |
|
|
84
89
|
| `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). |
|
|
85
90
|
| `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. |
|
|
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
|
|
91
|
+
| `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, and the PDF upload route (`POST /nests/:id/nodes/pdf`) up to `PDF_MAX_MB`. |
|
|
87
92
|
| `VIDEO_MAX_MB` | `30` | Max size (in MB) of a video uploaded into a doc. Default `30` keeps it under Cloud Run's ~32 MiB HTTP/1 request-body limit, so an oversized video is rejected with a clear message instead of a bare `413` from the platform. Raise only where the deployment can actually accept larger request bodies (not behind Cloud Run, or on HTTP/2 / direct-to-bucket upload). Images are fixed at 10 MB. |
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
93
|
+
| `PDF_MAX_MB` | `25` | Max size (in MB) of a PDF uploaded as a `pdf` document (`POST /nests/:id/nodes/pdf`). A larger file is refused with `413` and nothing is written. Default `25` keeps the request under Cloud Run's ~32 MiB HTTP/1 limit; raise it only where the deployment accepts larger request bodies. The PDF route is exempt from `MAX_BODY_BYTES` up to this cap. |
|
|
94
|
+
| `LOGO_URL` | _(unset)_ | Custom logo shown in the UI header + login screen. Must start with `https://`, `http://`, or `data:image/` — other schemes (`file://`, relative, `javascript:`) are rejected with a warning and the bundled icon is used. Server-wide: a nest's own reader branding (`PATCH /nests/:id/branding`, see API.md) overrides it for that nest's public reader only, and falls back to it when the nest sets no logo. |
|
|
95
|
+
| `PROMPTOWL_TEAMS_ENABLED` | _(unset — off)_ | Lets users who signed in with PromptOwl import their PromptOwl teams as local teams. Off by default; set `true` to enable, or toggle from Server settings → Advanced. When off, `GET/POST /teams/promptowl` return `404` and the PromptOwl Teams panel is hidden. Independent of `PROMPTOWL_SIGN_IN_GATE`. |
|
|
90
96
|
| `TYPE_ARTIFACT_ENABLED` | `true` | Set `false` to disable creation of **artifact** nodes server-wide (existing artifact nodes stay readable — never data loss). Runnable types (agent/skill/tool) are gated by `FEATURE_WORKFLOW_PLANE`, not here. Also editable from Settings (`/admin/settings`). |
|
|
91
97
|
| `TYPE_TABLE_ENABLED` | `true` | Same as above for **table** nodes. |
|
|
92
98
|
| `FEATURE_WORKFLOW_PLANE` | _(unset — off)_ | Enables the workflow plane: typed edges, edge-type registry, governed runs. Optional feature; also toggleable from Settings. |
|
|
93
|
-
| `FEATURE_SUBAGENT_RUNS` | _(unset — off)_ | Lets runs spawn nested sub-agent runs (recursion). Gated separately from the plane; requires `FEATURE_WORKFLOW_PLANE`. Also toggleable from
|
|
99
|
+
| `FEATURE_SUBAGENT_RUNS` | _(unset — off)_ | Lets runs spawn nested sub-agent runs (recursion). Gated separately from the plane; requires `FEATURE_WORKFLOW_PLANE`. Also toggleable from Server settings → Advanced (turning the plane off forces this off too). Opens a recursion surface — enable only after reviewing the depth/fan-out caps. |
|
|
94
100
|
| `SUBAGENT_MAX_DEPTH` | `8` | Max sub-agent nesting depth (clamped 1..32) — bounds the call tree's HEIGHT so it stays finite/haltable. |
|
|
95
101
|
| `SUBAGENT_MAX_CHILDREN` | `16` | Max direct children a single run may spawn (clamped 1..128) — bounds the call tree's WIDTH. Together with `SUBAGENT_MAX_DEPTH` this caps total tree size so a runner can't fork-bomb the DB. |
|
|
96
102
|
| `RUN_MAX_STEPS` | `10000` | Max steps a single run may accumulate (clamped 10..100000) — bounds a runaway/hostile runner's step log. |
|
|
@@ -98,7 +104,10 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
98
104
|
| `ANTHROPIC_API_KEY` | _(unset)_ | Server-wide default runner key for workflow-plane agent runs. Never returned by the API — the health endpoint reports presence only, and Settings shows a masked tail. |
|
|
99
105
|
| `SLACK_WEBHOOK_URL` | _(unset — connector off)_ | Slack incoming-webhook URL for governance-event notifications (review submitted/approved/rejected, collaborator added). `https://` only — the URL embeds a secret. Also editable from Settings in the UI. |
|
|
100
106
|
| `MSTEAMS_WEBHOOK_URL` | _(unset — connector off)_ | Microsoft Teams incoming-webhook URL for the same governance events, posted as Adaptive Cards. `https://` only — the URL embeds a secret. Also editable from Settings in the UI. See [Microsoft Teams notifications](#microsoft-teams-notifications). |
|
|
107
|
+
| `EMAIL_PROVIDER` | `smtp` | Email transport: `smtp` (uses `SMTP_URL`) or `ses` (AWS SES through the AWS SDK, for servers that can't use SMTP). Also editable from Settings. |
|
|
101
108
|
| `SMTP_URL` | _(unset — connector off)_ | SMTP connection URL for email notifications (`smtp://` or `smtps://`, credentials inline). Requires `NOTIFY_EMAIL_FROM` and `NOTIFY_EMAIL_TO`. Also editable from Settings. |
|
|
109
|
+
| `SES_REGION` | _(unset — connector off)_ | AWS region for SES (e.g. `us-east-1`) when `EMAIL_PROVIDER=ses`. The From address must be a verified SES identity. |
|
|
110
|
+
| `SES_ACCESS_KEY_ID` / `SES_SECRET_ACCESS_KEY` | _(unset)_ | Optional static SES credentials (need `ses:SendEmail`). Saving one email provider in Settings clears the other one's settings. Leave the keys unset to use the AWS default credential chain (IAM role, `AWS_*` env, shared profile). The secret is write-only in Settings. |
|
|
102
111
|
| `NOTIFY_EMAIL_FROM` | _(unset)_ | From address for notification emails. |
|
|
103
112
|
| `NOTIFY_EMAIL_TO` | _(unset)_ | Comma-separated recipients for notification emails. |
|
|
104
113
|
| `NOTIFY_DEBOUNCE_MS` | `15000` | Per-nest buffer window before notifications flush; bursts collapse into one digest message. |
|
|
@@ -107,7 +116,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
107
116
|
|
|
108
117
|
## Microsoft Teams notifications
|
|
109
118
|
|
|
110
|
-
Set `MSTEAMS_WEBHOOK_URL` (or paste the URL into **
|
|
119
|
+
Set `MSTEAMS_WEBHOOK_URL` (or paste the URL into **Server settings → Notifications →
|
|
111
120
|
Microsoft Teams notifications**) and the server posts the same governance
|
|
112
121
|
events the Slack connector covers — review requested / approved / rejected,
|
|
113
122
|
collaborator added, plus per-nest burst digests — to one Teams channel as
|
|
@@ -132,7 +141,7 @@ webhooks are now created with the **Workflows** (Power Automate) app:
|
|
|
132
141
|
confirm the team + channel.
|
|
133
142
|
3. Create the flow and **copy the HTTP POST URL** it shows (a
|
|
134
143
|
`https://….logic.azure.com/…` or `https://….powerplatform.com/…` address).
|
|
135
|
-
4. Paste that URL into **
|
|
144
|
+
4. Paste that URL into **Server settings → Notifications → Microsoft Teams
|
|
136
145
|
notifications** (or set `MSTEAMS_WEBHOOK_URL`).
|
|
137
146
|
5. Save, then click **Send test** on the card to post a test message and
|
|
138
147
|
confirm the channel receives it. (The button tests the *saved* URL — save
|
|
@@ -163,114 +172,566 @@ with `GET`/`PATCH`/`DELETE` on the same path.
|
|
|
163
172
|
|
|
164
173
|
## Single sign-on (OIDC)
|
|
165
174
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
This section is the admin guide to single sign-on. It covers what SSO does,
|
|
176
|
+
how to set it up for each identity provider, what happens to existing accounts,
|
|
177
|
+
API keys and agents, and how to fix each sign-in error.
|
|
178
|
+
|
|
179
|
+
**In this section:** [Overview](#overview) ·
|
|
180
|
+
[Prerequisites](#prerequisites) · [Settings reference](#settings-reference) ·
|
|
181
|
+
[Microsoft Entra ID](#connect-microsoft-entra-id) · [Google](#connect-google) ·
|
|
182
|
+
[Okta](#connect-okta) · [Keycloak](#connect-keycloak) ·
|
|
183
|
+
[Any other OIDC provider](#connect-any-other-oidc-provider) ·
|
|
184
|
+
[User lifecycle](#user-lifecycle) ·
|
|
185
|
+
[API keys, agents and MCP](#how-sso-relates-to-api-keys-agents-and-mcp) ·
|
|
186
|
+
[Shared deployments](#hosted-and-shared-deployments) ·
|
|
187
|
+
[Department auto-tagging](#department-auto-tagging) ·
|
|
188
|
+
[Troubleshooting](#troubleshooting-sso-sign-in)
|
|
189
|
+
|
|
190
|
+
### Overview
|
|
191
|
+
|
|
192
|
+
With SSO turned on, the login page shows one more button. Someone clicks it,
|
|
193
|
+
signs in at your identity provider (IdP), and comes back signed in to
|
|
194
|
+
ContextNest. The session is the same one a password login creates: an httpOnly
|
|
195
|
+
cookie that lasts 30 days.
|
|
196
|
+
|
|
197
|
+
- **Protocol:** OpenID Connect, authorization-code flow with PKCE (S256).
|
|
198
|
+
`GET /auth/oidc/login` sends the browser to your IdP.
|
|
199
|
+
`GET /auth/oidc/callback` gets the code back, redeems it, checks the ID token
|
|
200
|
+
(signature against the issuer's JWKS, issuer, audience, expiry, nonce) and
|
|
201
|
+
starts the session. Endpoint details are in `API.md → GET /auth/oidc/login`.
|
|
202
|
+
- **Providers:** any OIDC provider that meets the
|
|
203
|
+
[requirements below](#connect-any-other-oidc-provider). The settings page
|
|
204
|
+
has one-click presets for **Microsoft Entra ID** and **Google**. **Okta** and
|
|
205
|
+
**Keycloak** work as a custom issuer. SAML is not supported.
|
|
206
|
+
- **Button label:** "Sign in with Microsoft" when the issuer host is
|
|
207
|
+
`login.microsoftonline.com`, "Sign in with Google" for
|
|
208
|
+
`accounts.google.com`, and "Sign in with SSO" for anything else. The button
|
|
209
|
+
only appears when SSO is on **and** the issuer, client ID and client secret
|
|
210
|
+
are all set.
|
|
211
|
+
- **Where to configure:** **Admin → Server settings → Single sign-on** in the
|
|
212
|
+
left rail, or the `OIDC_*` environment variables. Changes made in the UI take
|
|
213
|
+
effect at once, without a restart.
|
|
214
|
+
- **Who can configure:** a server admin. That is the license admin or a
|
|
215
|
+
superadmin (granted on **Admin → Teammates**, or listed under `super_admins`
|
|
216
|
+
in `access.yaml`). In `AUTH_MODE=open` nobody signs in, so SSO has no effect
|
|
217
|
+
there. SSO is for `AUTH_MODE=key`, the default.
|
|
218
|
+
- **One setup per server:** there is a single issuer, client and domain
|
|
219
|
+
allowlist for the whole server. See
|
|
220
|
+
[Hosted and shared deployments](#hosted-and-shared-deployments).
|
|
221
|
+
|
|
222
|
+
SSO is separate from three other features with similar names:
|
|
223
|
+
|
|
224
|
+
- **Sign in with PromptOwl**, the device flow (`PROMPTOWL_SIGN_IN_GATE`).
|
|
225
|
+
- **One-click "Open Community" login** from PromptOwl (`GET /auth/sso`,
|
|
226
|
+
**Server settings → Community sites**).
|
|
227
|
+
- **Agent SSO** token exchange ([below](#agent-sso-token-exchange)).
|
|
228
|
+
|
|
229
|
+
Each has its own settings.
|
|
230
|
+
|
|
231
|
+
### Prerequisites
|
|
232
|
+
|
|
233
|
+
1. **An https address.** The issuer must be `https://` (the server refuses
|
|
234
|
+
anything else). Your ContextNest server should also be on https: the session
|
|
235
|
+
and flow cookies get the `Secure` flag on https, and most IdPs only accept an
|
|
236
|
+
`http://` redirect URI for `localhost`.
|
|
237
|
+
2. **`PUBLIC_BASE_URL` set to that address.** Set it on
|
|
238
|
+
**Server settings → General → Public base URL**, or in the environment, e.g.
|
|
239
|
+
`https://nest.acme.com`, with no trailing path. The redirect URI is built
|
|
240
|
+
from it. If it is unset, the server uses the origin of the incoming request,
|
|
241
|
+
which behind a reverse proxy is whatever `Host` header the proxy forwards.
|
|
242
|
+
See the `PUBLIC_BASE_URL` row in the env var table for why you should always
|
|
243
|
+
pin it.
|
|
244
|
+
3. **The redirect URI registered at your IdP, exactly:**
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
<PUBLIC_BASE_URL>/auth/oidc/callback
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The Single sign-on tab shows this value with a **Copy** button. Before you
|
|
251
|
+
save, it is built from the Public base URL you have typed, or from this
|
|
252
|
+
browser's address if that field is empty. People must reach ContextNest at
|
|
253
|
+
this same address. The flow cookies are set on the host where sign-in
|
|
254
|
+
starts, so starting on a different host fails with `state_mismatch`.
|
|
255
|
+
4. **Outbound access from the server to the IdP.** The server fetches
|
|
256
|
+
`<issuer>/.well-known/openid-configuration`, the JWKS and the token
|
|
257
|
+
endpoint. Each call times out after 10 seconds.
|
|
258
|
+
5. **A server clock that is correct (NTP).** ID-token expiry is checked with
|
|
259
|
+
no clock tolerance.
|
|
260
|
+
6. **A second way in while you test.** Keep password or PromptOwl sign-in on
|
|
261
|
+
until an SSO login has worked. The server will not let you turn off
|
|
262
|
+
password and PromptOwl sign-in unless SSO is on and fully configured, so
|
|
263
|
+
there is always a way in.
|
|
264
|
+
|
|
265
|
+
### Settings reference
|
|
266
|
+
|
|
267
|
+
All of these are on **Admin → Server settings → Single sign-on**, in the
|
|
268
|
+
**Single sign-on (OIDC)** card. The exception is `PUBLIC_BASE_URL`, which is on
|
|
269
|
+
the **General** tab. One **Save** button, next to the page title, saves every
|
|
270
|
+
tab. If a value is set both in the environment and in the UI, see
|
|
271
|
+
[Runtime settings persistence](#runtime-settings-persistence) for which one
|
|
272
|
+
wins.
|
|
273
|
+
|
|
274
|
+
| Env var | UI field | Default | What it does |
|
|
275
|
+
|---|---|---|---|
|
|
276
|
+
| `OIDC_ENABLED` | On/Off switch at the top of the card | `false` | Turns SSO on. The server refuses to turn it on until the issuer, client ID and client secret are all set. If one of them is cleared later while SSO is still on, the button disappears and the card warns *Enabled but not configured*. |
|
|
277
|
+
| `OIDC_ISSUER` | **Issuer URL**, plus the **Microsoft Entra ID** / **Google** preset buttons | `""` | The issuer, e.g. `https://login.microsoftonline.com/<tenant-id>/v2.0`, `https://accounts.google.com`, `https://acme.okta.com` or `https://sso.acme.com/realms/acme`. **https only.** In the environment, a non-https value is ignored, a warning is logged (`[config] OIDC_ISSUER rejected`) and SSO stays off. In the UI, saving it fails. A trailing slash is removed. The issuer must serve `/.well-known/openid-configuration`, and the `issuer` in that document must match this value exactly (a trailing slash aside). |
|
|
278
|
+
| `OIDC_CLIENT_ID` | **Client ID** | `""` | The client (application) ID from your IdP. The ID token's audience must equal it. |
|
|
279
|
+
| `OIDC_CLIENT_SECRET` | **Client secret** | `""` | The client secret from your IdP. **Write-only:** the UI and `GET /admin/settings` only show whether it is set. Paste a new one to replace it. **Clear secret** removes it. The server sends it in the body of the token request (`client_secret_post`). |
|
|
280
|
+
| `OIDC_ALLOWED_DOMAINS` | **Allowed email domains** | `""` (any domain) | A comma-separated list of domains, e.g. `acme.com, contractors.acme.com`, stored lowercase. The domain of the user's email (the part after the last `@`) must be on the list **exactly**. Subdomains are not included automatically: `acme.com` does not allow `eu.acme.com`. The UI rejects anything that is not a bare domain. |
|
|
281
|
+
| `OIDC_AUTO_PROVISION` | **Auto-provision new users** | `true` | On: anyone the IdP signs in, and the domain list allows, gets an account the first time. Off: only emails that already have an account can sign in. Others get `not_invited`. See [User lifecycle](#user-lifecycle). |
|
|
282
|
+
| `OIDC_DEPARTMENT_TAGGING` | **Department auto-tagging** | `false` | Adds `dept:<department>` to documents a user creates. See [Department auto-tagging](#department-auto-tagging). |
|
|
283
|
+
| `PUBLIC_BASE_URL` | **General → Public base URL** | `""` (request origin) | The base of the redirect URI and of the post-logout return address. |
|
|
284
|
+
|
|
285
|
+
The following are **fixed** and cannot be configured:
|
|
286
|
+
|
|
287
|
+
- Scopes: `openid email profile`.
|
|
288
|
+
- Response type: `code`, with PKCE `S256`.
|
|
289
|
+
- Client authentication: `client_secret_post`. `client_secret_basic` and
|
|
290
|
+
`private_key_jwt` are not supported.
|
|
291
|
+
- ID-token algorithm: `RS256` only.
|
|
292
|
+
- Flow cookies (state, nonce, PKCE verifier) last 10 minutes.
|
|
293
|
+
- Discovery documents are cached for 5 minutes.
|
|
294
|
+
- Calls to the IdP time out after 10 seconds.
|
|
295
|
+
- Sessions last 30 days.
|
|
296
|
+
- Rate limit: 100 requests per 15 minutes per client IP, counted separately for
|
|
297
|
+
`/auth/oidc/login` and `/auth/oidc/callback`.
|
|
298
|
+
|
|
299
|
+
The `/admin/settings` API names for the same fields are `oidc_enabled`,
|
|
300
|
+
`oidc_issuer`, `oidc_client_id`, `oidc_client_secret`, `oidc_allowed_domains`,
|
|
301
|
+
`oidc_auto_provision` and `oidc_department_tagging` (see `API.md`).
|
|
185
302
|
|
|
186
303
|
### Connect Microsoft Entra ID
|
|
187
304
|
|
|
188
|
-
1. **Register an app.** Entra admin center
|
|
189
|
-
registrations → New registration*. Name it
|
|
190
|
-
account type at *Accounts in this
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
305
|
+
1. **Register an app.** In the Entra admin center, go to *Identity →
|
|
306
|
+
Applications → App registrations → New registration*. Name it, e.g.
|
|
307
|
+
"ContextNest", and leave the account type at *Accounts in this
|
|
308
|
+
organizational directory only* (single tenant).
|
|
309
|
+
2. **Add the redirect URI.** In the registration, go to *Authentication → Add
|
|
310
|
+
a platform → Web* and paste the redirect URI from the Single sign-on tab
|
|
311
|
+
(`https://<your-server>/auth/oidc/callback`).
|
|
312
|
+
3. **Create a client secret.** Go to *Certificates & secrets → New client
|
|
313
|
+
secret*. Copy the secret **Value**, not the Secret ID. It is shown only
|
|
314
|
+
once. Note when it expires: after that date, sign-in fails with
|
|
315
|
+
`exchange_failed` until you paste a new secret.
|
|
316
|
+
4. **Collect the IDs.** On the app's *Overview* page, copy the **Application
|
|
197
317
|
(client) ID** and the **Directory (tenant) ID**.
|
|
198
|
-
5. **Fill in ContextNest.**
|
|
199
|
-
ID
|
|
200
|
-
`https://login.microsoftonline.com/<tenant>/v2.0
|
|
201
|
-
ID and the secret value.
|
|
202
|
-
your
|
|
203
|
-
the same save
|
|
204
|
-
6. **Test.** Open the login page in a private window
|
|
205
|
-
Microsoft** button
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
318
|
+
5. **Fill in ContextNest.** On **Admin → Server settings → Single sign-on**,
|
|
319
|
+
click **Microsoft Entra ID** and paste the tenant ID when asked. The issuer
|
|
320
|
+
becomes `https://login.microsoftonline.com/<tenant-id>/v2.0`. Paste the
|
|
321
|
+
client ID and the secret value. You can also set **Allowed email domains**
|
|
322
|
+
to your organization's domains. Turn the switch **On** and **Save**. The
|
|
323
|
+
switch and the fields can go in the same save.
|
|
324
|
+
6. **Test.** Open the login page in a private window. A **Sign in with
|
|
325
|
+
Microsoft** button should appear and take you through Entra.
|
|
326
|
+
|
|
327
|
+
Entra-specific notes:
|
|
328
|
+
|
|
329
|
+
- **Use the tenant ID (a GUID), not `common`, `organizations` or a domain
|
|
330
|
+
name.** The server requires the discovery document's `issuer` to match the
|
|
331
|
+
configured issuer exactly. For the multi-tenant endpoints, Entra's document
|
|
332
|
+
contains the placeholder `{tenantid}`, so the check fails and sign-in stops
|
|
333
|
+
with `discovery_failed`. Only single-tenant sign-in is supported.
|
|
334
|
+
- **Email claim.** Entra v2 ID tokens often have no `email` claim. The server
|
|
335
|
+
then uses `preferred_username` (the UPN). The address people sign in with
|
|
336
|
+
may therefore be their UPN, which may differ from their mail address.
|
|
337
|
+
Account matching uses this address (see [User lifecycle](#user-lifecycle)).
|
|
338
|
+
- **Only let assigned users in (optional).** Go to *Enterprise applications →
|
|
339
|
+
(your app) → Properties → Assignment required? → Yes*, then assign users or
|
|
340
|
+
groups. Entra then refuses anyone else. Depending on where Entra stops the
|
|
341
|
+
sign-in, the user sees either Entra's own error page or `provider_error`.
|
|
342
|
+
- **Sign-out:** Entra publishes an `end_session_endpoint`, so ContextNest
|
|
343
|
+
signs the user out of Entra as well (see
|
|
344
|
+
[Offboarding and sign-out](#revoking-access-offboarding)).
|
|
345
|
+
|
|
346
|
+
### Connect Google
|
|
347
|
+
|
|
348
|
+
1. **Choose the project.** In the Google Cloud console, pick (or create) the
|
|
349
|
+
project that will own the sign-in client.
|
|
350
|
+
2. **Set up the consent screen.** Go to *APIs & Services → OAuth consent
|
|
351
|
+
screen*. Newer consoles call this *Google Auth Platform → Branding /
|
|
352
|
+
Audience*.
|
|
353
|
+
- **Internal:** only accounts in your Google Workspace organization can
|
|
354
|
+
sign in. This is the right choice for a company server.
|
|
355
|
+
- **External:** any Google account can sign in. While the app is in
|
|
356
|
+
*Testing*, only the test users you list can sign in.
|
|
357
|
+
3. **Create the client.** Go to *APIs & Services → Credentials → Create
|
|
358
|
+
credentials → OAuth client ID* (newer consoles: *Google Auth Platform →
|
|
359
|
+
Clients → Create client*). Choose **Application type: Web application**.
|
|
360
|
+
4. **Add the redirect URI.** Under **Authorized redirect URIs**, add the
|
|
361
|
+
redirect URI from the Single sign-on tab exactly, then click **Create**.
|
|
362
|
+
5. **Copy the credentials.** Copy the **Client ID** and **Client secret**.
|
|
363
|
+
6. **Fill in ContextNest.** On **Admin → Server settings → Single sign-on**,
|
|
364
|
+
click **Google**. The issuer becomes `https://accounts.google.com`. Paste
|
|
365
|
+
the client ID and secret, and **set Allowed email domains** to your
|
|
366
|
+
Workspace domains. Turn the switch **On**, **Save**, and test in a private
|
|
367
|
+
window. A **Sign in with Google** button should appear.
|
|
368
|
+
|
|
369
|
+
Google-specific notes:
|
|
370
|
+
|
|
371
|
+
- **Set Allowed email domains.** `accounts.google.com` will sign in *any*
|
|
372
|
+
Google account. With auto-provision on and no domain list, anyone with a
|
|
373
|
+
Google account gets an account on your server. The Single sign-on tab shows
|
|
374
|
+
a warning when that combination is set.
|
|
375
|
+
- **Sign-out:** Google has no `end_session_endpoint`. Signing out of
|
|
376
|
+
ContextNest ends the ContextNest session only. The user stays signed in to
|
|
377
|
+
Google.
|
|
378
|
+
|
|
379
|
+
### Connect Okta
|
|
380
|
+
|
|
381
|
+
These are the standard steps in the Okta Admin Console. The Okta-side behavior
|
|
382
|
+
noted at the end has not been tested against a live Okta org, so check it on
|
|
383
|
+
yours.
|
|
384
|
+
|
|
385
|
+
1. **Create the app integration.** Go to *Applications → Applications → Create
|
|
386
|
+
App Integration*. Choose **Sign-in method: OIDC - OpenID Connect** and
|
|
387
|
+
**Application type: Web Application**, then **Next**.
|
|
388
|
+
2. **General settings.** Name it, e.g. "ContextNest". Under **Grant type**,
|
|
389
|
+
keep **Authorization Code** checked.
|
|
390
|
+
- **Sign-in redirect URIs:** the redirect URI from the Single sign-on tab,
|
|
391
|
+
e.g. `https://nest.acme.com/auth/oidc/callback`.
|
|
392
|
+
- **Sign-out redirect URIs:** your base URL with a trailing slash, e.g.
|
|
393
|
+
`https://nest.acme.com/`. This is the address ContextNest asks Okta to
|
|
394
|
+
return to after sign-out.
|
|
395
|
+
- **Assignments:** choose who may use the app (everyone, or selected
|
|
396
|
+
groups). **Save**.
|
|
397
|
+
3. **Copy the credentials.** On the app's **General** tab, copy the **Client
|
|
398
|
+
ID** and a **Client secret**. **Client authentication** should be **Client
|
|
399
|
+
secret**, not *Public key / Private key*.
|
|
400
|
+
4. **Find the issuer.** It is one of these:
|
|
401
|
+
- Your Okta org URL, e.g. `https://acme.okta.com`. This is the org
|
|
402
|
+
authorization server.
|
|
403
|
+
- A custom authorization server, e.g. `https://acme.okta.com/oauth2/default`
|
|
404
|
+
(*Security → API → Authorization Servers → Issuer URI*).
|
|
405
|
+
|
|
406
|
+
Either works, as long as the value you enter matches the `issuer` in
|
|
407
|
+
`<issuer>/.well-known/openid-configuration`. Open that URL in a browser to
|
|
408
|
+
check. If you use a custom domain, use the issuer the discovery document
|
|
409
|
+
shows.
|
|
410
|
+
5. **Fill in ContextNest.** On **Admin → Server settings → Single sign-on**,
|
|
411
|
+
paste the issuer into **Issuer URL**. There is no preset, and the page
|
|
412
|
+
shows *Custom issuer*. Paste the **Client ID** and **Client secret**, set
|
|
413
|
+
**Allowed email domains** if you want, turn it **On** and **Save**. The
|
|
414
|
+
login page shows **Sign in with SSO**.
|
|
415
|
+
|
|
416
|
+
Okta notes that were **not verified against a live Okta org:**
|
|
417
|
+
|
|
418
|
+
- **Client authentication method.** ContextNest sends the secret in the
|
|
419
|
+
request body (`client_secret_post`). If sign-in fails with `exchange_failed`
|
|
420
|
+
and the server log shows `[oidc] token exchange failed with status 401`,
|
|
421
|
+
check the app's `token_endpoint_auth_method`. The Admin Console may not show
|
|
422
|
+
it, so use Okta's Apps API to set it to `client_secret_post` and try again.
|
|
423
|
+
- **Sign-out.** ContextNest sends `post_logout_redirect_uri` and `client_id`
|
|
424
|
+
to Okta's logout endpoint, but not `id_token_hint`. If Okta shows an error
|
|
425
|
+
page on sign-out, the ContextNest session has **already** ended (it is
|
|
426
|
+
deleted before the redirect). Only the Okta session is left open.
|
|
427
|
+
|
|
428
|
+
### Connect Keycloak
|
|
429
|
+
|
|
430
|
+
These steps are for the current admin console (Keycloak 19 and later).
|
|
431
|
+
|
|
432
|
+
1. **Create the client.** In your realm, go to *Clients → Create client*.
|
|
433
|
+
Choose **Client type: OpenID Connect** and enter a **Client ID**, e.g.
|
|
434
|
+
`contextnest`. **Next**.
|
|
435
|
+
2. **Capability config.** Turn **Client authentication** **On**. This makes it
|
|
436
|
+
a confidential client, which ContextNest requires. Keep **Standard flow**
|
|
437
|
+
checked. **Next**.
|
|
438
|
+
3. **Login settings.**
|
|
439
|
+
- **Valid redirect URIs:** the redirect URI from the Single sign-on tab.
|
|
440
|
+
- **Valid post logout redirect URIs:** your base URL with a trailing slash,
|
|
441
|
+
e.g. `https://nest.acme.com/`.
|
|
442
|
+
|
|
443
|
+
**Save**.
|
|
444
|
+
4. **Copy the secret.** On the client's **Credentials** tab, keep **Client
|
|
445
|
+
Authenticator: Client Id and Secret** and copy the **Client Secret**.
|
|
446
|
+
5. **Find the issuer.** It is `https://<keycloak-host>/realms/<realm>`. Older
|
|
447
|
+
Keycloak versions (before 17) add `/auth`:
|
|
448
|
+
`https://<keycloak-host>/auth/realms/<realm>`. Open
|
|
449
|
+
`<issuer>/.well-known/openid-configuration` and use its `issuer` value
|
|
450
|
+
exactly. If Keycloak runs behind a proxy without a correct hostname
|
|
451
|
+
setting, the document may show an internal or `http://` issuer. That fails
|
|
452
|
+
with `discovery_failed`, so fix Keycloak's hostname configuration first.
|
|
453
|
+
6. **Fill in ContextNest.** Paste the issuer, client ID and client secret on
|
|
454
|
+
**Admin → Server settings → Single sign-on**, turn it **On** and **Save**.
|
|
455
|
+
The login page shows **Sign in with SSO**.
|
|
456
|
+
|
|
457
|
+
Keycloak-specific notes:
|
|
458
|
+
|
|
459
|
+
- **Verified email is required.** Keycloak includes `email_verified` in the ID
|
|
460
|
+
token, and ContextNest refuses a token where it is `false`
|
|
461
|
+
(`email_not_verified`). Users created in the admin console start with
|
|
462
|
+
*Email verified* **off**. Turn it on for each user, or have users verify
|
|
463
|
+
their address.
|
|
464
|
+
- **Users need an email address.** A user without one produces a token
|
|
465
|
+
ContextNest cannot use (`invalid_token`).
|
|
466
|
+
- **Lock down open registration.** If the realm allows self-registration,
|
|
467
|
+
anyone can create a Keycloak account with any address. Turn off *User
|
|
468
|
+
registration* or set **Allowed email domains**. See the
|
|
469
|
+
[trust assumption](#email-verification-trust-assumption).
|
|
470
|
+
- **Signature algorithm.** Keep the realm's default `RS256`. If the client's
|
|
471
|
+
*ID token signature algorithm* is set to anything else (`ES256`, `PS256`,
|
|
472
|
+
…), every login fails with `invalid_token`.
|
|
473
|
+
- **Department (optional).** For [department auto-tagging](#department-auto-tagging),
|
|
474
|
+
add a *User Attribute* mapper to the client's dedicated scope. Set the token
|
|
475
|
+
claim name to `department`, the claim type to String, and turn on *Add to
|
|
476
|
+
ID token*.
|
|
477
|
+
|
|
478
|
+
### Connect any other OIDC provider
|
|
479
|
+
|
|
480
|
+
Any provider that meets all of these requirements works. Each one is checked by
|
|
481
|
+
the server, and a failure shows up as the error code given.
|
|
482
|
+
|
|
483
|
+
| Requirement | Error if not met |
|
|
484
|
+
|---|---|
|
|
485
|
+
| Issuer is `https://` and serves `<issuer>/.well-known/openid-configuration` | Refused when saving (UI), or `not_configured` (env) / `discovery_failed` |
|
|
486
|
+
| The discovery document has `issuer`, `authorization_endpoint`, `token_endpoint` and `jwks_uri`, and its `issuer` matches the configured issuer exactly (a trailing slash aside) | `discovery_failed` |
|
|
487
|
+
| Authorization-code flow, with the client secret in the request body (`client_secret_post`). PKCE `S256` parameters are always sent. | `exchange_failed` |
|
|
488
|
+
| The token response includes an `id_token` | `exchange_failed` |
|
|
489
|
+
| The ID token is signed `RS256` with a key in `jwks_uri`, its `aud` is the client ID, it has not expired, and it returns the `nonce` that was sent | `invalid_token` |
|
|
490
|
+
| The ID token has an email-shaped `email` claim, or failing that a `preferred_username` claim | `invalid_token` |
|
|
491
|
+
| `email_verified` is not `false`. A missing claim is accepted. | `email_not_verified` |
|
|
492
|
+
|
|
493
|
+
Optional claims the server uses: `name` (display name for new accounts) and
|
|
494
|
+
`department` (see [Department auto-tagging](#department-auto-tagging)). If the
|
|
495
|
+
discovery document has an `end_session_endpoint`, sign-out also ends the IdP
|
|
496
|
+
session.
|
|
497
|
+
|
|
498
|
+
Enter the issuer in **Issuer URL** (the page shows *Custom issuer*), fill in the
|
|
499
|
+
client ID and secret, and register the redirect URI with the provider.
|
|
500
|
+
|
|
501
|
+
### User lifecycle
|
|
502
|
+
|
|
503
|
+
**How accounts are matched.** ContextNest links an SSO login to an account
|
|
504
|
+
**by email address only**. It does not store the IdP's subject ID (`sub`). The
|
|
505
|
+
address comes from `email`, or from `preferred_username` if `email` is missing.
|
|
506
|
+
It is trimmed and lowercased, and matched against existing accounts without
|
|
507
|
+
regard to case. As a result:
|
|
508
|
+
|
|
509
|
+
- **An existing password account with the same email is the same account.**
|
|
510
|
+
The first SSO login signs into it, with the same nests, teams, stewardship
|
|
511
|
+
roles, API keys and admin rights. SSO does not change or remove the
|
|
512
|
+
password, so the person can still sign in with it, unless you turn password
|
|
513
|
+
sign-in off (see [Making SSO the only way in](#making-sso-the-only-way-in)).
|
|
514
|
+
- **Invited people can use SSO straight away.** An invite (Teammates, a nest
|
|
515
|
+
share, a team, a steward assignment) creates the account and sends a
|
|
516
|
+
temporary password. If the invited email matches their IdP email, they can
|
|
517
|
+
click the SSO button instead. Any first sign-in clears the account's
|
|
518
|
+
*Invited* badge.
|
|
519
|
+
- **A changed email is a new account.** If someone's email or UPN changes at
|
|
520
|
+
the IdP, their next SSO login does not match the old account. With
|
|
521
|
+
auto-provision on, they get a new, empty account. With it off, they get
|
|
522
|
+
`not_invited`. Share their nests with the new address again, and remove the
|
|
523
|
+
old account on Teammates.
|
|
524
|
+
- **Admin rights belong to the account,** not to how it signed in. A
|
|
525
|
+
superadmin listed by email in `access.yaml` or granted on Teammates is still
|
|
526
|
+
an admin when signing in with SSO.
|
|
527
|
+
|
|
528
|
+
**Auto-provision on (the default).** The first successful SSO login by an
|
|
529
|
+
unknown email creates an account. The display name comes from the `name`
|
|
530
|
+
claim, and the password is random and unusable. A new account sees only
|
|
531
|
+
nests shared with it (directly or through a team) and nests whose visibility
|
|
532
|
+
is *Organization* or *Public*. The account
|
|
533
|
+
can sign in only through SSO, unless an admin sets a password with **Reset
|
|
534
|
+
password** on Teammates. Auto-provision also applies when **Manual sign-in**
|
|
535
|
+
is *Invite only*, because that setting governs email-and-password
|
|
536
|
+
registration, not SSO. Limit who can get in with **Allowed email domains**, or
|
|
537
|
+
by assigning the app to specific users or groups at the IdP.
|
|
538
|
+
|
|
539
|
+
**Auto-provision off (invite-only SSO).** Only emails that already have an
|
|
540
|
+
account can sign in. Everyone else is sent back with `not_invited`. Add people
|
|
541
|
+
first with **Admin → Teammates → Invite Teammate**, or by sharing a nest or
|
|
542
|
+
document with their email. They can then use the SSO button.
|
|
543
|
+
|
|
544
|
+
**Allowed email domains** apply to every SSO login, new and existing
|
|
545
|
+
accounts alike. An existing account whose domain is not on the list gets
|
|
546
|
+
`domain_not_allowed`. It can still use password or PromptOwl sign-in if those
|
|
547
|
+
are on.
|
|
548
|
+
|
|
549
|
+
#### Making SSO the only way in
|
|
550
|
+
|
|
551
|
+
On **Server settings → General**, set **Manual sign-in** to **Off** and
|
|
552
|
+
**Sign in with PromptOwl** to **Off**. The login page then shows only the SSO
|
|
553
|
+
button, and `POST /auth/login` / `POST /auth/register` return `403`. The server
|
|
554
|
+
refuses this combination unless SSO is on and has an issuer, client ID and
|
|
555
|
+
secret. It also refuses to turn SSO off or clear its settings while SSO is the
|
|
556
|
+
only way in.
|
|
557
|
+
|
|
558
|
+
Consider a break-glass option: set **Sign in with PromptOwl** to **Admins
|
|
559
|
+
only**. The license admin can then still sign in through `?admin=1` on the
|
|
560
|
+
login page if the IdP is down.
|
|
561
|
+
|
|
562
|
+
API keys keep working either way. They are not a sign-in method (see
|
|
563
|
+
[below](#how-sso-relates-to-api-keys-agents-and-mcp)).
|
|
212
564
|
|
|
213
|
-
###
|
|
565
|
+
### Revoking access (offboarding)
|
|
214
566
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
567
|
+
**Disabling someone at the IdP only blocks new SSO logins.** It does not
|
|
568
|
+
affect:
|
|
569
|
+
|
|
570
|
+
- ContextNest sessions they already have. These stay valid until they expire,
|
|
571
|
+
up to 30 days after login.
|
|
572
|
+
- Their **API keys**. These do not expire and never contact the IdP.
|
|
573
|
+
- Their password, if the account had one or an admin has set one, while
|
|
574
|
+
password sign-in is on.
|
|
575
|
+
|
|
576
|
+
To cut access completely, **remove the user in ContextNest**: go to
|
|
577
|
+
**Admin → Teammates**, find the person and click **Remove**. This deletes their
|
|
578
|
+
account, all their API keys, their active sessions, their stewardship roles
|
|
579
|
+
and their nest-collaborator grants in one step. Documents they wrote stay in
|
|
580
|
+
their nests. A few limits apply:
|
|
581
|
+
|
|
582
|
+
- Removal is refused while the user **owns** nests. Transfer or delete those
|
|
583
|
+
nests first.
|
|
584
|
+
- A server admin cannot be removed until their superadmin access is revoked.
|
|
585
|
+
- Nobody can remove their own account.
|
|
586
|
+
|
|
587
|
+
The full procedure has two steps:
|
|
588
|
+
|
|
589
|
+
1. **Disable the user at the IdP.** Do this first. With auto-provision on, a
|
|
590
|
+
user who is still active at the IdP can sign in again after removal and
|
|
591
|
+
get a new, empty account.
|
|
592
|
+
2. **Remove the user on Admin → Teammates.** This ends their sessions and
|
|
593
|
+
revokes their keys.
|
|
594
|
+
|
|
595
|
+
Bearers minted by [Agent SSO](#agent-sso-token-exchange) look up the user by
|
|
596
|
+
email on every request, so they stop working once the user is removed.
|
|
597
|
+
|
|
598
|
+
**Sign-out (RP-initiated logout).** When someone who signed in with SSO clicks
|
|
599
|
+
**Log out**, the app goes to `GET /auth/oidc/logout`. This deletes the
|
|
600
|
+
ContextNest session and cookies. Then, if the IdP publishes an
|
|
601
|
+
`end_session_endpoint` (Entra, Okta and Keycloak do; Google does not), it sends
|
|
602
|
+
the browser there with `post_logout_redirect_uri=<PUBLIC_BASE_URL>/` and
|
|
603
|
+
`client_id`, so the IdP session ends too. Without that, the next click on the
|
|
604
|
+
SSO button would sign the person straight back in. Register `<base URL>/` as a
|
|
605
|
+
sign-out / post-logout redirect URI at IdPs that require it (Okta, Keycloak).
|
|
606
|
+
If the IdP has no logout endpoint, or cannot be reached, the local session is
|
|
607
|
+
still cleared and the user lands on the login page.
|
|
608
|
+
|
|
609
|
+
### How SSO relates to API keys, agents and MCP
|
|
610
|
+
|
|
611
|
+
SSO controls how **people sign in to the web app**. It does not control how
|
|
612
|
+
programs connect. Turning SSO on changes nothing for existing keys or agent
|
|
613
|
+
connections.
|
|
614
|
+
|
|
615
|
+
| Way in | Effect of turning on SSO |
|
|
616
|
+
|---|---|
|
|
617
|
+
| **Browser, email and password** | Keeps working. Controlled by **Manual sign-in** (Server settings → General). |
|
|
618
|
+
| **Browser, Sign in with PromptOwl** | Keeps working. Controlled by **Sign in with PromptOwl** (Server settings → General). |
|
|
619
|
+
| **Browser, SSO button** | New. Creates the same 30-day session as a password login. |
|
|
620
|
+
| **API keys (`cnst_…`)**, used by the REST API, the `ctx` CLI and MCP clients | **No change.** Any signed-in user, however they signed in, creates keys on **Workspace → API keys**, and keys are checked without contacting the IdP. Disabling someone at the IdP does **not** revoke their keys. Removing them on Teammates does. |
|
|
621
|
+
| **MCP clients** (Claude Desktop, Claude Code, Cursor, VS Code…) | **No change.** They connect to `<server>/mcp` or `<server>/nests/<id>/mcp` with an API key. They do not sign in through your IdP. |
|
|
622
|
+
| **Agent SSO (token exchange)** | Separate and **off** by default. An agent that already holds an Entra or Google ID token for a user exchanges it at `POST /auth/token-exchange` for a short-lived MCP bearer. It has its own switches (Single sign-on tab → **Agent SSO — token exchange** card) but uses **Allowed email domains** and **Auto-provision new users** from the SSO card. See [Agent SSO (token exchange)](#agent-sso-token-exchange). |
|
|
623
|
+
| **One-click "Open Community" login from PromptOwl** (`GET /auth/sso`) | Not related. It uses signed tickets from PromptOwl (**Server settings → Community sites**), not your IdP. |
|
|
624
|
+
|
|
625
|
+
### Hosted and shared deployments
|
|
626
|
+
|
|
627
|
+
The SSO settings apply to the **whole server**: one issuer, one client, one
|
|
628
|
+
domain list and one auto-provision setting for every user. There is no
|
|
629
|
+
per-organization or per-nest IdP. On a server that hosts several
|
|
630
|
+
organizations (`SHARED_DEPLOYMENT=true`, such as the PromptOwl-hosted service):
|
|
631
|
+
|
|
632
|
+
- Turning on SSO puts the same button in front of **every** user of the
|
|
633
|
+
server, and it signs in against one organization's IdP.
|
|
634
|
+
- With auto-provision on, anyone that IdP signs in (and the domain list allows)
|
|
635
|
+
gets an account on the shared server. That account can read every nest
|
|
636
|
+
whose visibility is *Organization* or *Public*, which on a shared server
|
|
637
|
+
means nests belonging to other customers.
|
|
638
|
+
- Only a server admin, meaning the operator, can change these settings.
|
|
639
|
+
Customers of a shared server cannot bring their own IdP.
|
|
640
|
+
|
|
641
|
+
An organization that needs its own IdP should run its own ContextNest server.
|
|
642
|
+
Per-organization identity (SAML / SCIM, a multi-tenant admin console) is in the
|
|
643
|
+
Enterprise edition (see `README.md`).
|
|
239
644
|
|
|
240
|
-
###
|
|
645
|
+
### Department auto-tagging
|
|
241
646
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
647
|
+
With `OIDC_DEPARTMENT_TAGGING` on (**Admin → Server settings → Single sign-on →
|
|
648
|
+
Department auto-tagging**), every document a user **creates** is tagged
|
|
649
|
+
`dept:<slugified-department>` from their directory department. For example, a
|
|
650
|
+
user in *Customer Success* creates documents tagged `dept:customer-success`.
|
|
651
|
+
It applies to new documents only: edits never add or change the tag, and
|
|
652
|
+
existing documents are never tagged afterwards. Users without a department
|
|
653
|
+
(password accounts, or an IdP that does not send the claim) create untagged
|
|
654
|
+
documents, with no error.
|
|
655
|
+
|
|
656
|
+
The department comes from the **`department` claim** in the OIDC ID token. It
|
|
657
|
+
is saved on the user at every SSO login. A new value replaces the old one and a
|
|
658
|
+
missing claim clears it, so directory moves take effect at the user's next
|
|
659
|
+
sign-in. Whitespace is trimmed and collapsed, and the value is cut to 120
|
|
660
|
+
characters.
|
|
661
|
+
|
|
662
|
+
Microsoft Entra ID does **not** send the claim by default. Add it to the app
|
|
663
|
+
registration: *Token configuration → Add optional claim → Token type: **ID** →
|
|
664
|
+
select **department** → Add*. Grant the suggested Microsoft Graph permission if
|
|
665
|
+
asked, and make sure users' *Department* field is filled in in Entra. Other
|
|
666
|
+
IdPs work too, as long as they send a string `department` claim in the ID token
|
|
667
|
+
(e.g. a Keycloak user-attribute mapper, see [Keycloak](#connect-keycloak)).
|
|
668
|
+
|
|
669
|
+
> **Privacy note:** the `dept:<slug>` tag is part of the document's visible
|
|
670
|
+
> metadata. Anyone who can read the document (collaborators, shared nests,
|
|
671
|
+
> public nests) can see the creator's directory department. That is
|
|
672
|
+
> PII-adjacent organizational data. Think about this before turning it on for
|
|
673
|
+
> servers where documents are shared beyond the creator's own team or made
|
|
674
|
+
> public.
|
|
256
675
|
|
|
257
676
|
### Email-verification trust assumption
|
|
258
677
|
|
|
259
|
-
An ID token whose `email_verified` claim is **explicitly `false`**
|
|
260
|
-
|
|
261
|
-
trusts the email claim
|
|
262
|
-
both Entra and Google guarantee address
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
678
|
+
An ID token whose `email_verified` claim is **explicitly `false`** (the boolean
|
|
679
|
+
or the string `"false"`) is refused with `?sso_error=email_not_verified`. If
|
|
680
|
+
the claim is **missing**, the server trusts the email claim. Microsoft Entra ID
|
|
681
|
+
v2 tokens often leave it out, and both Entra and Google guarantee address
|
|
682
|
+
ownership, so refusing when it is missing would break the main providers.
|
|
683
|
+
|
|
684
|
+
This is a deliberate trust assumption. **If your IdP allows unverified,
|
|
685
|
+
self-registered emails (e.g. an open Keycloak realm), set Allowed email domains
|
|
686
|
+
and turn off self-registration at the IdP.** Otherwise anyone who can claim an
|
|
687
|
+
arbitrary email at your IdP can sign in as the local account with that email,
|
|
688
|
+
including an admin's.
|
|
689
|
+
|
|
690
|
+
### Troubleshooting SSO sign-in
|
|
691
|
+
|
|
692
|
+
When an SSO sign-in fails, the browser lands on `/?sso_error=<code>`. The app
|
|
693
|
+
shows the message below as a toast, removes the parameter from the address bar
|
|
694
|
+
and shows the normal sign-in options. For the codes that involve the IdP, the
|
|
695
|
+
server log has the details: `[oidc] discovery failed: …`,
|
|
696
|
+
`[oidc] token exchange failed with status <n>`,
|
|
697
|
+
`[oidc] ID token validation failed: …` and
|
|
698
|
+
`[oidc] provisioning/session failed: …`.
|
|
699
|
+
|
|
700
|
+
| `sso_error` | Message the user sees | Cause | Fix |
|
|
701
|
+
|---|---|---|---|
|
|
702
|
+
| `disabled` | Single sign-on is turned off on this server. | `OIDC_ENABLED` is off, but something (an old bookmark, a link) opened `/auth/oidc/login` or the callback. | Turn SSO on, or remove the link. |
|
|
703
|
+
| `not_configured` | Single sign-on isn't fully configured. Contact your admin. | SSO is on, but the issuer, client ID or client secret is empty. This also happens when an `OIDC_ISSUER` in the environment was ignored for not being `https://` (look for `[config] OIDC_ISSUER rejected` in the log). | Fill in all three on the Single sign-on tab. The issuer must be `https://`. |
|
|
704
|
+
| `rate_limited` | Too many sign-in attempts. Wait a moment and try again. | More than 100 sign-in requests from one client IP in 15 minutes. Behind a proxy that does not set `X-Forwarded-For` / `X-Real-IP`, every user shares the proxy's IP. | Wait up to 15 minutes. Make sure your proxy passes the real client IP. |
|
|
705
|
+
| `discovery_failed` | Couldn't reach the identity provider. Try again, or contact your admin. | The server could not load `<issuer>/.well-known/openid-configuration`: wrong issuer URL, no outbound network access, a timeout after 10 seconds, a non-200 response, missing required fields, or **an `issuer` in the document that differs from the configured issuer**. Entra `common` / `organizations`, or an Entra domain name in place of the tenant GUID, fail on that last check. So does a Keycloak that reports an internal hostname. | Open the discovery URL yourself and copy its `issuer` value exactly into **Issuer URL**. Check that the server can reach the IdP. |
|
|
706
|
+
| `provider_error` | Your identity provider declined the sign-in. Try again. | The IdP returned `?error=…` to the callback instead of a code. Typical reasons: the user cancelled or declined consent, the user is not assigned to the app, or a conditional-access policy blocked the sign-in. Some IdPs show their own error page for these cases instead of returning. | Check the IdP's sign-in logs for the reason. Assign the user or group to the app, or fix the policy. |
|
|
707
|
+
| `state_mismatch` | The sign-in attempt expired or didn't match this browser. Try again. | The flow cookies were missing or did not match the `state` sent back. The user took more than 10 minutes at the IdP, the browser blocked cookies, or sign-in started on a different host from the redirect URI (e.g. started at `http://10.0.0.5:3000` with a redirect URI on `https://nest.acme.com`). Opening an old callback URL again also causes it. | Start again from the login page. Make sure people use the same address as `PUBLIC_BASE_URL`. |
|
|
708
|
+
| `exchange_failed` | Sign-in couldn't be completed with the identity provider. Try again. | The code could not be exchanged for an ID token. The token endpoint returned an error (wrong or **expired client secret**, a client ID for a different app, a redirect URI that does not match, or an IdP that requires `client_secret_basic` or `private_key_jwt`), the call timed out, no `code` came back, or the response had no `id_token` (e.g. the `openid` scope is not allowed). | Paste a new client secret. Check the client ID and redirect URI. Set the IdP client to send the secret in the request body (`client_secret_post`). The log shows the HTTP status. |
|
|
709
|
+
| `invalid_token` | The identity provider returned an invalid sign-in token. Try again. | The ID token failed a check: signature, issuer, audience (`aud` is not the client ID), expiry or not-before (**server clock off**), algorithm not `RS256`, or the nonce did not match. It also happens when the token has no email-shaped `email` or `preferred_username` claim, e.g. a Keycloak user with no email address. | Check the client ID and the IdP's ID-token signing algorithm (`RS256`). Sync the server clock with NTP. Give the user an email address at the IdP. |
|
|
710
|
+
| `email_not_verified` | Your identity provider reports your email address as unverified. … | The ID token has `email_verified: false`. This is common with Keycloak users created by an admin. | Mark the email as verified at the IdP, or have the user verify it. See the [trust assumption](#email-verification-trust-assumption). |
|
|
711
|
+
| `domain_not_allowed` | Your email domain isn't allowed on this server. Contact your admin. | The domain of the user's email is not on **Allowed email domains**. Matching is exact, so subdomains must be listed separately. With Entra, the address may be the UPN rather than the mail address. | Add the domain to the list, or have the user sign in with an address on an allowed domain. |
|
|
712
|
+
| `not_invited` | No account exists for your email on this server. Ask your admin for an invite. | **Auto-provision new users** is off, and no account has this email. The email may also have changed at the IdP (see [User lifecycle](#user-lifecycle)). | Invite the person on **Admin → Teammates**, or share a nest with their email, or turn auto-provision on. |
|
|
713
|
+
| `service_error` | Something went wrong signing you in. Please try again. | A database error while finding or creating the account or starting the session. | Check the server log (`[oidc] provisioning/session failed`) and the database connection. |
|
|
714
|
+
|
|
715
|
+
**Errors that appear at the IdP, not in ContextNest.** Some failures stop at
|
|
716
|
+
the IdP's own error page, and the browser never comes back. The most common
|
|
717
|
+
one is a redirect URI that is not registered exactly: Entra `AADSTS50011`,
|
|
718
|
+
Google `Error 400: redirect_uri_mismatch`, or Okta and Keycloak "invalid
|
|
719
|
+
redirect uri". Google also blocks, on its own page, users outside an *Internal*
|
|
720
|
+
consent screen's organization, and users who are not on the test list while
|
|
721
|
+
the app is in *Testing*. Copy the redirect URI from the Single sign-on tab again and
|
|
722
|
+
compare it character by character: scheme, host, port, path, and no trailing
|
|
723
|
+
slash. Check that it matches `PUBLIC_BASE_URL`.
|
|
724
|
+
|
|
725
|
+
**No SSO button on the login page.** The button appears only when SSO is on
|
|
726
|
+
*and* the issuer, client ID and secret are all set. The Single sign-on tab
|
|
727
|
+
warns *Enabled but not configured* when one is missing. The page reads this
|
|
728
|
+
state from `GET /health` (`oidc_enabled`, `oidc_issuer`).
|
|
729
|
+
|
|
730
|
+
**Other `sso_error` codes.** `not_supported`, `missing_ticket`,
|
|
731
|
+
`invalid_ticket`, `bad_audience`, `ticket_used` and `sign_in_restricted` come
|
|
732
|
+
from the PromptOwl one-click login (`GET /auth/sso`), not from OIDC. See
|
|
733
|
+
`API.md → GET /auth/sso` and **Server settings → Community sites**.
|
|
734
|
+
`rate_limited` and `service_error` are used by both flows.
|
|
274
735
|
|
|
275
736
|
---
|
|
276
737
|
|
|
@@ -283,8 +744,8 @@ RFC 8693-shaped. **Off by default and security-critical**: it trusts a token
|
|
|
283
744
|
minted elsewhere and hands back access, so read this section before enabling it.
|
|
284
745
|
|
|
285
746
|
Configure it with the `SSO_TOKEN_EXCHANGE_ENABLED` / `MCP_SIGNING_SECRET` /
|
|
286
|
-
`MS_*` / `GOOGLE_*` vars above, or from **
|
|
287
|
-
(
|
|
747
|
+
`MS_*` / `GOOGLE_*` vars above, or from **Admin → Server settings → Single sign-on →
|
|
748
|
+
Agent SSO — token exchange** (server admins). It reuses `OIDC_ALLOWED_DOMAINS` and `OIDC_AUTO_PROVISION` — the
|
|
288
749
|
same who-may-sign-in policy as browser login.
|
|
289
750
|
|
|
290
751
|
**Who this is for — and who it is not.** The minted bearer is short-lived by
|
|
@@ -391,7 +852,7 @@ are printed to the server log.
|
|
|
391
852
|
|
|
392
853
|
### Runtime settings persistence
|
|
393
854
|
|
|
394
|
-
Settings you change at runtime — everything on
|
|
855
|
+
Settings you change at runtime — everything on **Admin → Server settings** (`/admin/settings`:
|
|
395
856
|
sign-in gate, logo, base URL, upload limit, feature flags, OIDC SSO, Slack/Teams/SMTP connectors)
|
|
396
857
|
plus the **installed license key** — are stored in the database (`server_settings`
|
|
397
858
|
table), **not** in a `.env` file. This is deliberate: on Cloud Run the container
|