@promptowl/contextnest-community 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONFIGURATION.md CHANGED
@@ -60,7 +60,14 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
60
60
  | `PROMPTOWL_SIGN_IN_GATE` | `open` | Restrict "Sign in with PromptOwl". `open` = anyone may; `admin-only` = only the license owner (admin) may, everyone else uses email/password (admin opens the login page with `?admin=1`); `disabled` = nobody may. Enforced server-side at `POST /auth/promptowl` and surfaced on the health endpoint. Unknown values fall back to `open`. |
61
61
  | `MANUAL_SIGN_IN` | `open` | Email + password sign-in mode. `open` = anyone may log in and self-register a new account; `invite-only` = existing/invited users may log in but brand-new self-registration returns `403` (the admin provisions accounts via invite/share/steward and shares the password — there is no self-service "set password", which would be account takeover without email verification); `disabled` = no email/password sign-in at all (`POST /auth/login` and `POST /auth/register` return `403`). Independent of `PROMPTOWL_SIGN_IN_GATE`, so the two methods are controlled separately (e.g. `invite-only` manual + `admin-only` PromptOwl). Also settable from Settings → General. The server refuses `disabled` while PromptOwl sign-in is also `disabled` (that would leave no way to log in). Unknown values fall back to `open`. |
62
62
  | `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **Official deployment only — leave unset on self-hosted.** Shared HMAC secret enabling the one-click "Open Community" SSO auto-login from PromptOwl. Must exactly match the same-named var on PromptOwl. When unset, `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. |
63
- | `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another. Only relevant when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. |
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 only when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. (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.) |
64
+ | `OIDC_ENABLED` | `false` | Turn on generic OIDC single sign-on (`GET /auth/oidc/login` / `GET /auth/oidc/callback`). Requires `OIDC_ISSUER`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET` — the login-page button only appears once all three are set. Also editable from Settings → Single sign-on. See [Single sign-on (OIDC)](#single-sign-on-oidc). |
65
+ | `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
+ | `OIDC_CLIENT_ID` | `""` | Application (client) ID from your IdP app registration. |
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
+ | `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
+ | `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
+ | `OIDC_DEPARTMENT_TAGGING` | `false` | Auto-tag newly **created** documents with the creator's directory department: `dept:<slugified-department>` (lowercase, spaces → dashes, e.g. `dept:customer-success`) is appended to the document's tags, deduped against user-supplied tags. Applies on create only — never on update, never retroactively — and a user without a stored department is a silent no-op. The department is captured from the OIDC `department` ID-token claim on every SSO login (a login without the claim clears it, so directory moves propagate), so this is only meaningful when your IdP emits that claim — see [Department auto-tagging](#department-auto-tagging). Also editable from Settings → Single sign-on. |
64
71
  | `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`. |
65
72
  | `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
66
73
  | `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
@@ -68,11 +75,16 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
68
75
  | `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). |
69
76
  | `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. |
70
77
  | `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. |
78
+ | `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 Settings → Advanced. When off, `GET/POST /teams/promptowl` return `404` and the PromptOwl Teams panel is hidden. Independent of `PROMPTOWL_SIGN_IN_GATE`. |
79
+ | `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`). |
80
+ | `TYPE_TABLE_ENABLED` | `true` | Same as above for **table** nodes. |
71
81
  | `FEATURE_WORKFLOW_PLANE` | _(unset — off)_ | Enables the workflow plane: typed edges, edge-type registry, governed runs. Optional feature; also toggleable from Settings. |
72
82
  | `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 Settings → Advanced (turning the plane off forces this off too). Opens a recursion surface — enable only after reviewing the depth/fan-out caps. |
73
83
  | `SUBAGENT_MAX_DEPTH` | `8` | Max sub-agent nesting depth (clamped 1..32) — bounds the call tree's HEIGHT so it stays finite/haltable. |
74
84
  | `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. |
75
85
  | `RUN_MAX_STEPS` | `10000` | Max steps a single run may accumulate (clamped 10..100000) — bounds a runaway/hostile runner's step log. |
86
+ | `RUN_MAX_CONCURRENT_ROOTS` | `50` | Max concurrently-running root (depth-0) runs per nest (clamped 1..1000). The subagent caps bound a single tree; this bounds how many trees can run at once, so triggers can't flood a nest. |
87
+ | `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. |
76
88
  | `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. |
77
89
  | `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. |
78
90
  | `NOTIFY_EMAIL_FROM` | _(unset)_ | From address for notification emails. |
@@ -81,6 +93,119 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
81
93
 
82
94
  ---
83
95
 
96
+ ## Single sign-on (OIDC)
97
+
98
+ Generic OpenID Connect sign-in against any spec-compliant identity provider —
99
+ Microsoft Entra ID and Google are the first-class presets in **Settings →
100
+ Single sign-on**. The server runs a standard authorization-code flow with PKCE:
101
+ `GET /auth/oidc/login` redirects to your IdP, `GET /auth/oidc/callback` verifies
102
+ the returned ID token (issuer, audience, nonce, signature against the issuer's
103
+ JWKS) and starts a normal browser session. Users are looked up by email;
104
+ unknown emails are created automatically when `OIDC_AUTO_PROVISION` is on.
105
+
106
+ All seven `OIDC_*` values are also editable at runtime from **Settings → Single
107
+ sign-on** (superadmin only) — no restart needed. The Settings page shows the
108
+ exact **redirect URI** to register with your IdP:
109
+
110
+ ```
111
+ <PUBLIC_BASE_URL or server origin>/auth/oidc/callback
112
+ ```
113
+
114
+ Set `PUBLIC_BASE_URL` when the server sits behind a reverse proxy so the
115
+ redirect URI is derived from the canonical address rather than the incoming
116
+ Host header.
117
+
118
+ ### Connect Microsoft Entra ID
119
+
120
+ 1. **Register an app.** Entra admin center → *Identity → Applications → App
121
+ registrations → New registration*. Name it (e.g. "ContextNest"), leave the
122
+ account type at *Accounts in this organizational directory only*.
123
+ 2. **Add the redirect URI.** In the registration: *Authentication → Add a
124
+ platform → Web*, and paste the redirect URI shown on the ContextNest
125
+ Settings page (`https://<your-server>/auth/oidc/callback`).
126
+ 3. **Create a client secret.** *Certificates & secrets → New client secret*.
127
+ Copy the secret **value** (not the ID) immediately — it's shown once.
128
+ 4. **Collect the IDs.** On the app's *Overview* page copy the **Application
129
+ (client) ID** and the **Directory (tenant) ID**.
130
+ 5. **Fill in ContextNest.** Settings → Single sign-on: click **Microsoft Entra
131
+ ID**, paste the tenant ID when prompted (issuer becomes
132
+ `https://login.microsoftonline.com/<tenant>/v2.0`), then paste the client
133
+ ID and the secret value. Optionally restrict **Allowed email domains** to
134
+ your org's domain. Save, then flip the toggle **On** and save again (or in
135
+ the same save).
136
+ 6. **Test.** Open the login page in a private window — a **Sign in with
137
+ Microsoft** button appears and round-trips through Entra. Entra accounts
138
+ without an `email` claim fall back to `preferred_username` (the UPN).
139
+
140
+ For **Google**: create an OAuth client ID (type *Web application*) in the
141
+ Google Cloud console, add the same redirect URI under *Authorized redirect
142
+ URIs*, and use the **Google** preset (issuer `https://accounts.google.com`)
143
+ with that client ID/secret.
144
+
145
+ ### Department auto-tagging
146
+
147
+ With `OIDC_DEPARTMENT_TAGGING` on (Settings → Single sign-on → **Department
148
+ auto-tagging**), every document a user **creates** is tagged
149
+ `dept:<slugified-department>` from their directory department — e.g. a user in
150
+ *Customer Success* creates docs tagged `dept:customer-success`. Create-only:
151
+ edits never add or change the tag, and existing documents are never
152
+ retro-tagged. Users without a department (password accounts, or an IdP that
153
+ doesn't emit the claim) create untagged documents — never an error.
154
+
155
+ The department is read from the **`department` claim** in the OIDC ID token
156
+ and stored on the user at every SSO login: a new value updates it, an absent
157
+ claim clears it, so directory moves propagate on the user's next sign-in.
158
+
159
+ Microsoft Entra ID does **not** emit the claim by default — add it to the app
160
+ registration: *Token configuration → Add optional claim → Token type: **ID**
161
+ → select **department** → Add* (grant the suggested Microsoft Graph
162
+ permission if prompted), and make sure the users' *Department* field is
163
+ populated in Entra. Other IdPs work too as long as they emit a string
164
+ `department` claim in the ID token (e.g. a Keycloak user-attribute mapper).
165
+
166
+ > **Privacy note:** the `dept:<slug>` tag becomes part of the document's
167
+ > visible metadata — anyone who can read the document (collaborators, shared
168
+ > nests, public nests) can see the creator's directory department. That's
169
+ > PII-adjacent organizational data; consider this before enabling on servers
170
+ > where documents are shared beyond the creator's own team or made public.
171
+
172
+ ### Revoking access (offboarding)
173
+
174
+ Disabling a user **at the IdP** only blocks *new* sign-ins — an existing
175
+ ContextNest session stays valid until it expires (30 days). To cut access
176
+ immediately, **remove the user in ContextNest** (Settings → users): that wipes
177
+ all of their active sessions at once. Real offboarding is therefore two steps —
178
+ disable at the IdP *and* remove in ContextNest — with the ContextNest step being
179
+ the one that ends live sessions.
180
+
181
+ When a user signs themselves out, ContextNest performs an **RP-initiated
182
+ logout**: if the IdP advertises an `end_session_endpoint` (Entra, Okta,
183
+ Keycloak do; Google does not), the browser is bounced through it so the IdP
184
+ session ends too and the next "Sign in with SSO" click doesn't silently
185
+ re-authenticate. Where the IdP has no logout endpoint, the local session is
186
+ cleared and the user lands back on the login page (the IdP session persists —
187
+ that's the IdP's own timeout to manage).
188
+
189
+ ### Email-verification trust assumption
190
+
191
+ An ID token whose `email_verified` claim is **explicitly `false`** is refused
192
+ (`?sso_error=email_not_verified`). When the claim is **absent**, the server
193
+ trusts the email claim — Microsoft Entra ID v2 tokens frequently omit it, and
194
+ both Entra and Google guarantee address ownership, so refusing on absence
195
+ would break the primary providers. This is a deliberate trust assumption:
196
+ **when your IdP allows unverified self-registered emails (e.g. an open
197
+ Keycloak realm), configure `oidc_allowed_domains` and disable self-registration
198
+ at the IdP** — otherwise anyone able to assert an arbitrary email at your IdP
199
+ could sign in as the matching local account.
200
+
201
+ Sign-in failures bounce back to the app as `/?sso_error=<code>` and surface as
202
+ a toast; codes: `disabled`, `not_configured`, `discovery_failed`,
203
+ `provider_error`, `state_mismatch`, `exchange_failed`, `invalid_token`,
204
+ `email_not_verified`, `domain_not_allowed`, `not_invited`, `rate_limited`,
205
+ `service_error`.
206
+
207
+ ---
208
+
84
209
  ## Database backends
85
210
 
86
211
  The governance/auth metadata (users, sessions, nests registry, stewards, reviews,
@@ -108,7 +233,7 @@ Postgres backend starts empty.
108
233
  ### Runtime settings persistence
109
234
 
110
235
  Settings you change at runtime — everything on the **Settings page** (`/admin/settings`:
111
- sign-in gate, logo, base URL, upload limit, feature flags, Slack/SMTP connectors)
236
+ sign-in gate, logo, base URL, upload limit, feature flags, OIDC SSO, Slack/SMTP connectors)
112
237
  plus the **installed license key** — are stored in the database (`server_settings`
113
238
  table), **not** in a `.env` file. This is deliberate: on Cloud Run the container
114
239
  filesystem is ephemeral, so a file-based value was wiped by every rebuild, and each
package/README.md CHANGED
@@ -105,74 +105,17 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
105
105
  | Steward version revert | ✅ | ✅ |
106
106
  | MCP server for AI agents | ✅ | ✅ |
107
107
  | Centralized multi-tenant admin console | — | ✅ |
108
- | SSO / SAML / SCIM | | ✅ |
108
+ | Single sign-on (OIDC Entra ID, Google, Okta, Keycloak) | | ✅ |
109
+ | SAML / SCIM provisioning | — | ✅ |
109
110
  | Audit log streaming | — | ✅ |
110
111
  | Policy transforms (redaction, summarization) | — | ✅ |
111
112
  | Priority support and SLA | — | ✅ |
112
113
 
113
114
  For Enterprise pricing and features, contact **hoot@promptowl.ai** or visit <https://promptowl.ai/contextnest/>.
114
115
 
115
- ## What's new in 1.6.0
116
+ ## What's new
116
117
 
117
- - **PostgreSQL / Cloud SQL backend** — run the server on PostgreSQL alongside SQLite via an async DB adapter, so metadata survives on Cloud Run and other deployments where local disk isn't durable. Supports the Cloud SQL Auth Proxy (unix socket) and TCP + TLS. New `DB_*` env vars (`DB_DRIVER`, `DATABASE_URL`, `CLOUD_SQL_CONNECTION_NAME`, `DB_HOST`/`DB_PORT`/`DB_USER`/`DB_PASSWORD`/`DB_NAME`, `DB_POOL_MAX`, `DB_SSL`, `DB_SSL_CA`); SQLite stays the default when nothing is configured. See [CONFIGURATION.md](./CONFIGURATION.md).
118
- - **Configurable `NEST_STORAGE_ROOT`** — store nest vault files (markdown + version history) outside `DATA_ROOT` — e.g. on a Cloud Storage (GCS) volume mount — while the database stays on `DATA_ROOT`. Defaults to `<DATA_ROOT>/nests`, so existing deployments are unchanged. On a GCS FUSE mount, set `DRIFT_SCAN_INTERVAL_MS=0` and run a single instance.
119
- - **Security fix** — the admin guard on `/admin/settings` (GET/PATCH) is now correctly awaited; without it a non-admin could read or change server-wide settings in key mode.
120
- - **AdminLoginModal respects `PROMPTOWL_SIGN_IN_GATE`** — the "Sign in with PromptOwl" affordance follows the configured gate (`open` / `admin-only` / `disabled`).
121
- - **Drift scanner toggle** — `DRIFT_SCAN_INTERVAL_MS=0` now actually disables the drift scanner (previously reset to the 30s default).
122
- - **Safer folder sync** — unsynced-folder detection no longer treats a configured nest storage root as a sync candidate, preventing accidental duplication/removal of live nests when `NEST_STORAGE_ROOT` is nested under `DATA_ROOT`.
123
-
124
- Full history in [CHANGELOG.md](./CHANGELOG.md).
125
-
126
- ## What's new in 1.3.0
127
-
128
- - **Server super-admins** — emails listed in `access.yaml: super_admins` administer every nest (change visibility, manage collaborators and stewards) without being added per-nest. Owner-only operations (delete, transfer) still require the nest owner. See [STEWARDSHIP.md](./STEWARDSHIP.md).
129
- - **License persistence under `DATA_ROOT`** — `PROMPTOWL_KEY` now writes to `$DATA_ROOT/.env` (was `$cwd/.env`, which is root-owned `/app` in the official Docker image and silently lost the key on container recreate). The License Setup Page also surfaces write failures instead of failing quietly. Override via `ENV_FILE_PATH`. See [CONFIGURATION.md](./CONFIGURATION.md).
130
- - **Case-insensitive email** across registration, login, invite, and access checks. Migration dedupes existing accounts that differ only in case; fixes "I can't log in because I capitalized my email" and access-guard / lockout mismatches.
131
- - **Document version history + inline diff** — every saved version is browsable in the document detail view, and an inline diff highlights what changed between any two versions.
132
- - **Nest Overview / index landing** — every nest opens on a grouped table-of-contents with a "recently edited" strip instead of a flat list.
133
- - **Public/private toggle on the nest header** — visibility is set directly from the header, not buried in a dialog.
134
- - **Review queue surfaces submitted-for-review edits** — reviewers see the pending versions a contributor staged, not just net-new documents.
135
- - **Internal-folder sync over MCP** — agents can list a nest's unsynced filesystem folders and pull them into the nest on demand. New `unsynced-service.ts` + MCP tools, covered by `test/unsynced-folder.test.ts` and `test/mcp-unsynced.test.ts`.
136
- - **Telemetry `batch_id` + user email** — usage events carry a stable `batch_id` (de-duped on the receiver) and the authenticated user email alongside the user id, so the PromptOwl ingest bridge can meter credits to the right account.
137
- - **Hyperlink navigation fix** — hyperlinks in the editor and viewer navigate to their configured target URL correctly.
138
- - **Repo / DX** — `CLAUDE.md` at the repo root with project-specific guidance for Claude Code; `claude.yml` (PR assistant) and `claude-code-review.yml` (auto review on PRs) GitHub Actions workflows.
139
-
140
- Full history in [CHANGELOG.md](./CHANGELOG.md).
141
-
142
- ## What's new in 1.2.0
143
-
144
- - **Admin user management (no email server needed)** — `POST /auth/admin/reset-password/:userId` sets a user's password or returns a one-time temp password; `DELETE /auth/users/:userId` removes a user and revokes their API keys, sessions, and role rows. New self-service **Change password** in the user menu blocks reusing the current password.
145
- - **`PROMPTOWL_SIGN_IN_GATE`** — `open` / `admin-only` / `disabled` restricts "Sign in with PromptOwl" (admin reaches it via `?admin=1`). See [CONFIGURATION.md](./CONFIGURATION.md).
146
- - **Plain-text agent view** — `?format=markdown` on a node returns frontmatter + body as `text/markdown` for LLM-friendly consumption.
147
- - **Steward version revert** — `POST .../revert` restores an earlier version as a new one; pending-review docs are now editable (saving withdraws the review).
148
- - **Editor — always-editable surface** — Notion-style buttery headline, selection bubble toolbar (format-on-highlight), turn-into block conversion, Cmd+F find & replace, rich blocks (tables, callouts, toggles, syntax-highlighted code, columns), and wiki/code-safety fixes.
149
- - **Wiki usability** — backlinks panel ("Linked from N documents"), outline/TOC for 3+-heading docs, hover-preview on `[[wiki links]]`, multi-tag AND filtering with clickable chips, per-doc copy link, rename-safety warning for title-form links, link-health report (broken links + orphans), recently-edited strip.
150
- - **Sharing clarity** — share button reads "Share nest"; dialog clarifies nest-wide scope; deep links open documents directly.
151
- - **Security** — hardened scanner gate (PolinRider + Shai-Hulud preflight) in `bin/`.
152
-
153
- Full history in [CHANGELOG.md](./CHANGELOG.md).
154
-
155
- ## What's new in 1.1.0
156
-
157
- - **Vault import** — import an existing folder of markdown files into a new nest in one step, from the dashboard ("Import folder") or via the API. Frontmatter, wiki links, and folder structure are preserved.
158
- - **Nest sharing + collaborators** — set per-nest visibility and add collaborators with read or write access. A Share affordance is now inline in the document view.
159
- - **Public read-only nests** — flip a nest to `visibility=public` to serve it to unauthenticated readers (bypasses auth for GETs only); writes still require a key.
160
- - **Custom logo / branding** — set `LOGO_URL` to show your own logo in the UI header and login screen. See [CONFIGURATION.md](./CONFIGURATION.md).
161
- - **Governance** — nest owners can self-approve their own changes; new per-nest auto-approve toggle; role-based action gating (buttons reflect the viewer's role); stewards can edit scope during a role change.
162
- - **Editor** — `[[wikilink]]` autocomplete, broken-link click creates a new doc with the title pre-filled, tag chips filter the nest list, Notion-style H1 title, click-to-edit, and an unsaved-changes warning.
163
- - **Stability** — added an error boundary so a render error in one view no longer blanks the whole app; bumped `uuid` to 14.
164
-
165
- Full history in [CHANGELOG.md](./CHANGELOG.md).
166
-
167
- ## What's new in 1.0.1
168
-
169
- - **Document hashing pipeline** — external-edit detection, conflict-aware safe-publish, and inline version diffs powered by `@promptowl/contextnest-engine`. When a file is edited outside the UI, the editor shows an "External edit detected" banner with a side-by-side diff and an adopt / keep choice.
170
- - **Markdown rendering** — new `DocumentViewer` (react-markdown + remark-gfm + wikilink support) renders CLI/MCP-authored markdown correctly, with a view/edit toggle.
171
- - **License revocation now blocks writes synchronously** — revoke flips an in-process "writes blocked" flag immediately; the next write returns `503`.
172
- - **Dashboard stats** — new `GET /stats` endpoint surfaces nest / document / user counts.
173
- - **Docker fix** — Dockerfile now installs via npm against the shipped `package-lock.json`; base image bumped to `node:22-slim`.
174
-
175
- Full history in [CHANGELOG.md](./CHANGELOG.md).
118
+ Release notes live in [CHANGELOG.md](./CHANGELOG.md).
176
119
 
177
120
  ## Licensing
178
121
 
@@ -28,6 +28,7 @@ var canonicalEnvLoaded = null;
28
28
  var slackUrlWarned = false;
29
29
  var emailFromWarned = false;
30
30
  var emailToWarned = false;
31
+ var oidcIssuerWarned = false;
31
32
  function isEmailish(v) {
32
33
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) && !/[\r\n]/.test(v);
33
34
  }
@@ -124,6 +125,16 @@ var config = {
124
125
  get PROMPTOWL_KEY() {
125
126
  return process.env.PROMPTOWL_KEY || "";
126
127
  },
128
+ /**
129
+ * Whether importing/syncing teams FROM PromptOwl is offered. Default OFF
130
+ * (only an explicit "true" enables) — an operator opts the PO-team surface
131
+ * in from Settings or env. When off, the /teams/promptowl routes 404 and the
132
+ * UI hides the panel. Independent of PROMPTOWL_SIGN_IN_GATE: a user may sign
133
+ * in with PromptOwl yet not be allowed to pull their PO teams in.
134
+ */
135
+ get PROMPTOWL_TEAMS_ENABLED() {
136
+ return process.env.PROMPTOWL_TEAMS_ENABLED === "true";
137
+ },
127
138
  /** Vercel automation-bypass secret for PO calls; unset = no bypass. */
128
139
  get PROMPTOWL_BYPASS_SECRET() {
129
140
  return process.env.PROMPTOWL_BYPASS_SECRET || "";
@@ -194,6 +205,80 @@ var config = {
194
205
  const v = (process.env.MANUAL_SIGN_IN || "open").trim().toLowerCase();
195
206
  return v === "invite-only" || v === "disabled" ? v : "open";
196
207
  },
208
+ /**
209
+ * Generic OIDC single sign-on (Microsoft Entra ID, Google, any spec-
210
+ * compliant IdP). Off by default; superadmin-configurable at runtime via
211
+ * /admin/settings. When enabled AND fully configured (issuer + client id +
212
+ * client secret), GET /auth/oidc/login starts an authorization-code + PKCE
213
+ * flow and GET /auth/oidc/callback finishes it with a normal browser
214
+ * session — exactly like a password login.
215
+ */
216
+ get OIDC_ENABLED() {
217
+ return process.env.OIDC_ENABLED === "true";
218
+ },
219
+ /**
220
+ * OIDC issuer URL, e.g. "https://login.microsoftonline.com/<tenant>/v2.0"
221
+ * or "https://accounts.google.com". https only — tokens and the client
222
+ * secret ride this trust root, so a plaintext issuer is refused (warn once,
223
+ * treated as unset). Trailing slash stripped so discovery-doc comparison is
224
+ * canonical.
225
+ */
226
+ get OIDC_ISSUER() {
227
+ const raw = (process.env.OIDC_ISSUER || "").trim().replace(/\/$/, "");
228
+ if (!raw) return "";
229
+ let ok = false;
230
+ try {
231
+ ok = new URL(raw).protocol === "https:";
232
+ } catch {
233
+ ok = false;
234
+ }
235
+ if (!ok) {
236
+ if (!oidcIssuerWarned) {
237
+ oidcIssuerWarned = true;
238
+ console.warn(
239
+ "[config] OIDC_ISSUER rejected: must be a valid https:// URL. OIDC sign-in disabled."
240
+ );
241
+ }
242
+ return "";
243
+ }
244
+ return raw;
245
+ },
246
+ get OIDC_CLIENT_ID() {
247
+ return (process.env.OIDC_CLIENT_ID || "").trim();
248
+ },
249
+ get OIDC_CLIENT_SECRET() {
250
+ return (process.env.OIDC_CLIENT_SECRET || "").trim();
251
+ },
252
+ /**
253
+ * Comma-separated email-domain allowlist for OIDC sign-in (e.g.
254
+ * "acme.com, contractors.acme.com"). Empty = any domain the IdP asserts.
255
+ * Entries are normalized to lowercase; matching is on the part after the
256
+ * final "@" of the asserted email.
257
+ */
258
+ get OIDC_ALLOWED_DOMAINS() {
259
+ return (process.env.OIDC_ALLOWED_DOMAINS || "").split(",").map((d) => d.trim().toLowerCase()).filter(Boolean);
260
+ },
261
+ /**
262
+ * Auto-provision (JIT-create) an unknown user on first successful OIDC
263
+ * sign-in. Default true. When false, only emails that already have a user
264
+ * row (invited / registered) may sign in via OIDC — everyone else bounces
265
+ * with ?sso_error=not_invited.
266
+ */
267
+ get OIDC_AUTO_PROVISION() {
268
+ return process.env.OIDC_AUTO_PROVISION !== "false";
269
+ },
270
+ /**
271
+ * Auto-tag content created by SSO users with their directory department:
272
+ * when on, node CREATE appends `dept:<slugified-department>` to the new
273
+ * node's tags (never on update, never retroactively). The department is
274
+ * captured from the OIDC `department` claim at login (see oidc.ts), so
275
+ * this is only meaningful alongside an IdP that emits that claim — but
276
+ * there is no hard dependency on OIDC_ENABLED. Default off; superadmin-
277
+ * togglable at runtime via /admin/settings.
278
+ */
279
+ get OIDC_DEPARTMENT_TAGGING() {
280
+ return process.env.OIDC_DEPARTMENT_TAGGING === "true";
281
+ },
197
282
  /**
198
283
  * Path to the .env file the server reads its config from and the license
199
284
  * install flow persists PROMPTOWL_KEY into. Defaults UNDER DATA_ROOT (not
@@ -271,6 +356,21 @@ var config = {
271
356
  get FEATURE_WORKFLOW_PLANE() {
272
357
  return process.env.FEATURE_WORKFLOW_PLANE === "true";
273
358
  },
359
+ /**
360
+ * Content-type toggles — soft-disable a node type server-wide. DEFAULT ON:
361
+ * missing/unset/anything-but-"false" = enabled, so existing deployments see
362
+ * zero behavior change. Only CREATION of the type is refused when disabled;
363
+ * existing nodes of that type still read/render/version (never data loss).
364
+ * Runnable types (agent/skill/tool) are NOT governed here — they stay gated
365
+ * by FEATURE_WORKFLOW_PLANE. Superadmin-editable at runtime via
366
+ * /admin/settings (type_artifact_enabled / type_table_enabled).
367
+ */
368
+ get TYPE_ARTIFACT_ENABLED() {
369
+ return process.env.TYPE_ARTIFACT_ENABLED !== "false";
370
+ },
371
+ get TYPE_TABLE_ENABLED() {
372
+ return process.env.TYPE_TABLE_ENABLED !== "false";
373
+ },
274
374
  /**
275
375
  * Sub-agent runs: lets a run spawn nested runs (an external runner calling
276
376
  * back into POST /run for a child agent). Gated SEPARATELY from the base
@@ -1261,6 +1361,64 @@ function runMigrations(db) {
1261
1361
  })();
1262
1362
  recordMigration("026_api_events_nest_index");
1263
1363
  }
1364
+ if (!hasMigration("027_teams")) {
1365
+ db.transaction(() => {
1366
+ db.exec(`
1367
+ CREATE TABLE IF NOT EXISTS teams (
1368
+ id TEXT PRIMARY KEY,
1369
+ name TEXT NOT NULL,
1370
+ owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
1371
+ members TEXT NOT NULL DEFAULT '[]', -- JSON [{userId, email, role}]
1372
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1373
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1374
+ );
1375
+ CREATE INDEX IF NOT EXISTS idx_teams_owner ON teams(owner_id);
1376
+ `);
1377
+ const nestColsNow = db.prepare("PRAGMA table_info(nests)").all().map((c) => c.name);
1378
+ if (!nestColsNow.includes("shared_teams")) {
1379
+ db.exec("ALTER TABLE nests ADD COLUMN shared_teams TEXT NOT NULL DEFAULT '[]'");
1380
+ }
1381
+ })();
1382
+ recordMigration("027_teams");
1383
+ }
1384
+ if (!hasMigration("028_user_department")) {
1385
+ db.transaction(() => {
1386
+ const userColsNow = db.prepare("PRAGMA table_info(users)").all().map((c) => c.name);
1387
+ if (!userColsNow.includes("department")) {
1388
+ db.exec("ALTER TABLE users ADD COLUMN department TEXT");
1389
+ }
1390
+ })();
1391
+ recordMigration("028_user_department");
1392
+ }
1393
+ if (!hasMigration("029_super_admins")) {
1394
+ db.transaction(() => {
1395
+ db.exec(`
1396
+ CREATE TABLE IF NOT EXISTS super_admins (
1397
+ email TEXT PRIMARY KEY, -- stored lowercased
1398
+ granted_by TEXT NOT NULL, -- email of the granting admin
1399
+ granted_at TEXT NOT NULL DEFAULT (datetime('now'))
1400
+ );
1401
+ `);
1402
+ })();
1403
+ recordMigration("029_super_admins");
1404
+ }
1405
+ if (!hasMigration("031_team_source")) {
1406
+ db.transaction(() => {
1407
+ const teamCols = db.prepare("PRAGMA table_info(teams)").all().map((c) => c.name);
1408
+ if (!teamCols.includes("source")) {
1409
+ db.exec("ALTER TABLE teams ADD COLUMN source TEXT");
1410
+ }
1411
+ if (!teamCols.includes("external_id")) {
1412
+ db.exec("ALTER TABLE teams ADD COLUMN external_id TEXT");
1413
+ }
1414
+ db.exec(`
1415
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_teams_external
1416
+ ON teams(owner_id, source, external_id)
1417
+ WHERE external_id IS NOT NULL;
1418
+ `);
1419
+ })();
1420
+ recordMigration("031_team_source");
1421
+ }
1264
1422
  }
1265
1423
  function mergeCaseCollidingUsers(db) {
1266
1424
  const groups = db.prepare(
@@ -1478,7 +1636,7 @@ async function initDb() {
1478
1636
  if (config.DB_DRIVER === "postgres") {
1479
1637
  const { Pool } = await import("pg");
1480
1638
  const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
1481
- const { runPostgresMigrations } = await import("./migrations.postgres-4XYY3CTF.js");
1639
+ const { runPostgresMigrations } = await import("./migrations.postgres-BRXZY2GE.js");
1482
1640
  const pool = new Pool(buildPgConfig());
1483
1641
  adapter = new PostgresAdapter(pool);
1484
1642
  await runPostgresMigrations(adapter);