@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 +127 -2
- package/README.md +4 -61
- package/dist/{chunk-BC6KFUZH.js → chunk-3M7677XW.js} +159 -1
- package/dist/{chunk-KFGZIECB.js → chunk-F7EKYNXG.js} +564 -47
- package/dist/{chunk-3JTODC3Y.js → chunk-GUNJTORH.js} +7 -0
- package/dist/{chunk-UHUU3VAK.js → chunk-JHIU6RZU.js} +1 -1
- package/dist/{chunk-ZM4F7MY7.js → chunk-XB7T3THE.js} +5 -5
- package/dist/{chunk-7PQREKSM.js → chunk-ZMSU7BCC.js} +2 -2
- package/dist/{client-VZLX4THW.js → client-YUOEVUFU.js} +1 -1
- package/dist/{grants-service-6H4WUA2F.js → grants-service-PS5RMMEA.js} +3 -3
- package/dist/index.js +1745 -307
- package/dist/{migrations.postgres-4XYY3CTF.js → migrations.postgres-BRXZY2GE.js} +50 -2
- package/dist/{review-service-VLSUT6KK.js → review-service-DPN26C47.js} +7 -6
- package/dist/{stewardship-service-D5PDWXBR.js → stewardship-service-7BHI2YXB.js} +4 -3
- package/dist/{version-service-A2YAKYQW.js → version-service-YQIIQ7FN.js} +2 -2
- package/dist/web3/assets/index-BiGiarBx.css +1 -0
- package/dist/web3/assets/index-CUVRdYvH.js +1026 -0
- package/dist/web3/index.html +2 -2
- package/package.json +2 -1
- package/dist/web3/assets/index-B2PkvzmY.js +0 -1007
- package/dist/web3/assets/index-C-tX933G.css +0 -1
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
|
|
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
|
-
|
|
|
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
|
|
116
|
+
## What's new
|
|
116
117
|
|
|
117
|
-
|
|
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-
|
|
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);
|