@promptowl/contextnest-community 1.19.0 → 1.20.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 +83 -1
- package/dist/{chunk-GSYMJ3A4.js → chunk-3LUUJUWS.js} +1 -1
- package/dist/{chunk-ENDHMQPD.js → chunk-7CPP6FIU.js} +251 -39
- package/dist/{chunk-TNAZJYL5.js → chunk-EY7WOKJR.js} +3 -3
- package/dist/{chunk-FBDEESNJ.js → chunk-GVUBZTI3.js} +26 -184
- package/dist/{chunk-BXUVIAMP.js → chunk-KHMSDTJJ.js} +2 -2
- package/dist/{chunk-3GMGLYTZ.js → chunk-O5DYL7ZG.js} +2 -2
- package/dist/{chunk-HYSTFMFG.js → chunk-XCFKS65Y.js} +107 -1
- package/dist/{client-SW5I6ZBK.js → client-7Z2IVCPW.js} +1 -1
- package/dist/{engine-V4DY4PYN.js → engine-VZTSDUPB.js} +2 -2
- package/dist/{external-edit-service-3NK5DGLA.js → external-edit-service-3FVK3OD7.js} +3 -3
- package/dist/{grants-service-CADI6LIB.js → grants-service-WC27TRY3.js} +2 -2
- package/dist/index.js +2203 -1056
- package/dist/{migrations.postgres-5VI4RDTS.js → migrations.postgres-L42OXR7K.js} +47 -1
- package/dist/{review-service-LK4M2C43.js → review-service-TJRHRMMZ.js} +6 -6
- package/dist/{stewardship-service-JAFSU5GN.js → stewardship-service-HRVD7SIS.js} +3 -3
- package/dist/{version-service-G7SFFKFO.js → version-service-5PIFT2OY.js} +3 -3
- package/dist/web3/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web3/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web3/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web3/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web3/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web3/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web3/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web3/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web3/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web3/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web3/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web3/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web3/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web3/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web3/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web3/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web3/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web3/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web3/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web3/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web3/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web3/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web3/assets/index-D4UG3T04.js +1365 -0
- package/dist/web3/assets/index-Dw1UTDIk.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +6 -1
- package/dist/web3/assets/index-C26nM5Kn.css +0 -1
- package/dist/web3/assets/index-C4oalc2K.js +0 -1065
package/CONFIGURATION.md
CHANGED
|
@@ -60,7 +60,7 @@ 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`).
|
|
63
|
+
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Two uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — whenever a ticket-signing secret is set (`OFFICIAL_COMMUNITY_SSO_SECRET` on the official deployment, `MCP_SIGNING_SECRET` on a self-hosted one); with neither set no ticket verifies at all, so the audience check is moot. (2) **Set this whenever `OIDC_ENABLED=true`.** It's the base for the OIDC `redirect_uri` sent to your IdP and replayed at token exchange. Unset, that URI is derived from the incoming request's `Host` header — fine when your proxy overwrites `Host` with a trusted value, but a proxy that passes an attacker-supplied `Host` through would feed it straight into the redirect URI. Setting this pins the value regardless of what the proxy forwards. (Your IdP's own redirect-URI allowlist is a second line of defence, not a substitute.) (3) **Set this wherever email- or invite-gated publish links are used.** The magic link mailed by `POST /p/:slug/gate` is built from it; unset, it falls back to the request origin, so a proxy forwarding an attacker-supplied `Host` would send the recipient a one-time access token pointing at the attacker's domain — the classic reset-link poisoning. (4) **Set this wherever `POST /nests/:id/context` citations reach users.** The `url` on each node and the `_source:` line in each context block are built from it; unset, they fall back to the request origin, so a proxy forwarding an untrusted `Host` would put an attacker-influenced URL in front of both the model and the reader as a trustworthy citation. |
|
|
64
64
|
| `OIDC_ENABLED` | `false` | Turn on generic OIDC single sign-on (`GET /auth/oidc/login` / `GET /auth/oidc/callback`). Requires `OIDC_ISSUER`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET` — the login-page button only appears once all three are set. Also editable from Settings → Single sign-on. See [Single sign-on (OIDC)](#single-sign-on-oidc). |
|
|
65
65
|
| `OIDC_ISSUER` | `""` | OIDC issuer URL, e.g. `https://login.microsoftonline.com/<tenant>/v2.0` (Microsoft Entra ID) or `https://accounts.google.com` (Google). **`https://` only** — a non-https value is rejected with a warning and SSO stays off. Must serve `<issuer>/.well-known/openid-configuration`. |
|
|
66
66
|
| `OIDC_CLIENT_ID` | `""` | Application (client) ID from your IdP app registration. |
|
|
@@ -68,6 +68,11 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
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
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. |
|
|
71
|
+
| `SSO_TOKEN_EXCHANGE_ENABLED` | `false` | Master switch for `POST /auth/token-exchange` — an external agent exchanges an IdP ID token for a short-lived MCP bearer. Also needs `MCP_SIGNING_SECRET` and at least one provider; otherwise the endpoint returns `404`. Security-critical — see [Agent SSO (token exchange)](#agent-sso-token-exchange). |
|
|
72
|
+
| `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. |
|
|
73
|
+
| `MCP_TOKEN_TTL_SECONDS` | `300` | Lifetime of a minted MCP bearer, clamped to 30–3600. |
|
|
74
|
+
| `MS_TOKEN_EXCHANGE_ENABLED` / `MS_CLIENT_ID` / `MS_TENANT_ID` | `false` / `""` / `""` | Microsoft Entra provider. `MS_CLIENT_ID` is the audience an inbound token must carry; `MS_TENANT_ID` must be the directory **GUID** (it builds the required issuer `https://login.microsoftonline.com/<tenant>/v2.0` — `common`/`organizations`/a domain name will not match the discovery document and every exchange returns `provider_misconfigured`). |
|
|
75
|
+
| `GOOGLE_TOKEN_EXCHANGE_ENABLED` / `GOOGLE_CLIENT_ID` | `false` / `""` | Google provider. Issuer is fixed (`https://accounts.google.com`); `GOOGLE_CLIENT_ID` is the audience an inbound token must carry. |
|
|
71
76
|
| `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`. |
|
|
72
77
|
| `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
|
|
73
78
|
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
|
@@ -264,6 +269,83 @@ a toast; codes: `disabled`, `not_configured`, `discovery_failed`,
|
|
|
264
269
|
|
|
265
270
|
---
|
|
266
271
|
|
|
272
|
+
## Agent SSO (token exchange)
|
|
273
|
+
|
|
274
|
+
`POST /auth/token-exchange` lets an external agent, acting for a user your IdP
|
|
275
|
+
(Microsoft Entra / Google) has **already** authenticated, exchange that IdP's ID
|
|
276
|
+
token for a short-lived MCP bearer — no second login, no static API key.
|
|
277
|
+
RFC 8693-shaped. **Off by default and security-critical**: it trusts a token
|
|
278
|
+
minted elsewhere and hands back access, so read this section before enabling it.
|
|
279
|
+
|
|
280
|
+
Configure it with the `SSO_TOKEN_EXCHANGE_ENABLED` / `MCP_SIGNING_SECRET` /
|
|
281
|
+
`MS_*` / `GOOGLE_*` vars above, or from **Settings → Single sign-on → Agent SSO**
|
|
282
|
+
(superadmin). It reuses `OIDC_ALLOWED_DOMAINS` and `OIDC_AUTO_PROVISION` — the
|
|
283
|
+
same who-may-sign-in policy as browser login.
|
|
284
|
+
|
|
285
|
+
**Who this is for — and who it is not.** The minted bearer is short-lived by
|
|
286
|
+
design (`MCP_TOKEN_TTL_SECONDS`, default 300s, hard ceiling 3600s), and there is
|
|
287
|
+
no refresh token: renewal means calling `POST /auth/token-exchange` again with
|
|
288
|
+
the IdP token. So this fits a **programmatic** client — an agent runtime, a
|
|
289
|
+
backend service, a desktop app that already did an MSAL/Google sign-in — that
|
|
290
|
+
can re-exchange in code before a session or on a `401`, with no human involved.
|
|
291
|
+
|
|
292
|
+
It does **not** fit a client whose credential is typed once into a config file
|
|
293
|
+
(Claude Desktop, Cursor, any `mcpServers` JSON block): nothing here rewrites that
|
|
294
|
+
file, and the ceiling is an hour, so the connection would break repeatedly. Those
|
|
295
|
+
clients keep using a long-lived API key (`cnst_…`) — this endpoint is additive
|
|
296
|
+
and changes nothing about API-key or browser sign-in.
|
|
297
|
+
|
|
298
|
+
**What is verified** on the inbound `subject_token`, before anything is minted:
|
|
299
|
+
|
|
300
|
+
- **signature** against the provider's JWKS, algorithm pinned to `RS256`
|
|
301
|
+
- **issuer** — exact per provider (the tenant-scoped Entra issuer, or Google's)
|
|
302
|
+
- **audience** — your configured client id, so a token minted for *another*
|
|
303
|
+
application cannot be replayed here
|
|
304
|
+
- **expiry** (30s clock tolerance)
|
|
305
|
+
- the same `email_verified` + domain-allowlist policy as browser login
|
|
306
|
+
(see [Email-verification trust assumption](#email-verification-trust-assumption))
|
|
307
|
+
|
|
308
|
+
**Trust model / documented tradeoff.** Unlike the browser flow there is no
|
|
309
|
+
session to bind, so the OIDC `nonce` binding is intentionally dropped; trust
|
|
310
|
+
rests on signature + issuer + audience + expiry + `email_verified`. That is the
|
|
311
|
+
standard token-exchange model, and it is why the audience check (and a
|
|
312
|
+
correctly scoped IdP app registration) carries the weight here.
|
|
313
|
+
|
|
314
|
+
**Provisioning fails closed.** JIT-provisioning from a token minted elsewhere is
|
|
315
|
+
only allowed when `OIDC_ALLOWED_DOMAINS` bounds who may self-create. With
|
|
316
|
+
auto-provision on and an empty allowlist, every exchange for an unknown email is
|
|
317
|
+
refused with `no_account`, and the Settings page refuses to save that
|
|
318
|
+
combination — otherwise any account at a public IdP (Google = the whole
|
|
319
|
+
internet) could sign itself up and mint a bearer.
|
|
320
|
+
|
|
321
|
+
**Entra caveat (`preferred_username`).** Entra v2 tokens often omit `email` and
|
|
322
|
+
carry the address in `preferred_username`, which is a directory attribute — in a
|
|
323
|
+
tenant where guests or external identities can set their own mail attribute, a
|
|
324
|
+
token can assert an address that matches an existing local account. Pinning
|
|
325
|
+
`MS_TENANT_ID` to your own directory bounds that to identities your tenant
|
|
326
|
+
admits; treat inviting a guest as granting whatever that email address owns here.
|
|
327
|
+
|
|
328
|
+
**Rate limiting.** The per-IP cap on this endpoint reads the client IP from
|
|
329
|
+
`X-Forwarded-For`, so it is only a real cap behind a proxy that overwrites that
|
|
330
|
+
header. Deploy accordingly — an internet-facing server with a pass-through
|
|
331
|
+
header can be sprayed from a single host.
|
|
332
|
+
|
|
333
|
+
**Scope.** The minted bearer carries `scope: "mcp"`. That is a *route*
|
|
334
|
+
allowlist, not a reduced permission set: it reaches `/index`, `/mcp`, and the
|
|
335
|
+
ticket-eligible `/nests/:id/*` routes (including MCP write tools) with the
|
|
336
|
+
user's full stewardship rights. Keep `MCP_TOKEN_TTL_SECONDS` short.
|
|
337
|
+
|
|
338
|
+
**Revocation.** Turning the master flag off stops *minting* but does not revoke
|
|
339
|
+
bearers already issued — they remain valid until they expire (≤ 1h). Clear
|
|
340
|
+
`MCP_SIGNING_SECRET` to invalidate every outstanding bearer at once.
|
|
341
|
+
|
|
342
|
+
Error codes returned by the endpoint: `disabled` (404), `rate_limited` (429),
|
|
343
|
+
`bad_request`, `provider_disabled`, `provider_misconfigured` (400),
|
|
344
|
+
`invalid_subject_token` (401), `email_not_verified`, `domain_not_allowed`,
|
|
345
|
+
`no_account` (403), `server_error` (500).
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
267
349
|
## Database backends
|
|
268
350
|
|
|
269
351
|
The governance/auth metadata (users, sessions, nests registry, stewards, reviews,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
getDb,
|
|
10
10
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-XCFKS65Y.js";
|
|
12
12
|
import {
|
|
13
13
|
ANON_USER_ID
|
|
14
14
|
} from "./chunk-YB3LKF7U.js";
|
|
@@ -27,7 +27,7 @@ import { createEngineApi } from "@promptowl/contextnest-engine/api";
|
|
|
27
27
|
|
|
28
28
|
// src/nests/service.ts
|
|
29
29
|
import { mkdirSync } from "fs";
|
|
30
|
-
import { v4 as
|
|
30
|
+
import { v4 as uuid3 } from "uuid";
|
|
31
31
|
import { NestStorage } from "@promptowl/contextnest-engine";
|
|
32
32
|
|
|
33
33
|
// src/shared/paths.ts
|
|
@@ -590,7 +590,7 @@ function parseAccessYaml(content) {
|
|
|
590
590
|
}
|
|
591
591
|
|
|
592
592
|
// src/governance/teams-service.ts
|
|
593
|
-
import { v4 as
|
|
593
|
+
import { v4 as uuid2 } from "uuid";
|
|
594
594
|
|
|
595
595
|
// src/governance/roles.ts
|
|
596
596
|
function collabPermToRole(permission) {
|
|
@@ -997,6 +997,9 @@ async function notifyEmailForNest(nestId, message, details) {
|
|
|
997
997
|
}
|
|
998
998
|
}
|
|
999
999
|
}
|
|
1000
|
+
function isEmailConfigured() {
|
|
1001
|
+
return !!config.SMTP_URL && !!config.NOTIFY_EMAIL_FROM;
|
|
1002
|
+
}
|
|
1000
1003
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1001
1004
|
function recipientEnvelope(to, from) {
|
|
1002
1005
|
const recipients = Array.isArray(to) ? to : [to];
|
|
@@ -1118,6 +1121,172 @@ async function buildDocContext(nestId, nodeId, baseUrl) {
|
|
|
1118
1121
|
};
|
|
1119
1122
|
}
|
|
1120
1123
|
|
|
1124
|
+
// src/governance/notify-service.ts
|
|
1125
|
+
import { v4 as uuid } from "uuid";
|
|
1126
|
+
async function listWatchers(nestId, nodeId) {
|
|
1127
|
+
return await getDb().all(
|
|
1128
|
+
`SELECT id, node_id, user_email, created_by, created_at
|
|
1129
|
+
FROM watchers WHERE nest_id = ? AND node_id = ? ORDER BY created_at`,
|
|
1130
|
+
[nestId, nodeId]
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
async function addWatcher(nestId, nodeId, userEmail, createdBy) {
|
|
1134
|
+
await getDb().run(
|
|
1135
|
+
`INSERT INTO watchers (id, nest_id, node_id, user_email, created_by, created_at)
|
|
1136
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1137
|
+
ON CONFLICT(nest_id, node_id, user_email) DO NOTHING`,
|
|
1138
|
+
[uuid(), nestId, nodeId, userEmail.toLowerCase(), createdBy, (/* @__PURE__ */ new Date()).toISOString()]
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
async function removeWatcher(nestId, nodeId, userEmail) {
|
|
1142
|
+
const db = getDb();
|
|
1143
|
+
const row = await db.get(
|
|
1144
|
+
"SELECT id FROM watchers WHERE nest_id = ? AND node_id = ? AND LOWER(user_email) = LOWER(?)",
|
|
1145
|
+
[nestId, nodeId, userEmail]
|
|
1146
|
+
);
|
|
1147
|
+
if (!row) return false;
|
|
1148
|
+
await db.run("DELETE FROM watchers WHERE id = ?", [row.id]);
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
async function notifyReviewRequested(params) {
|
|
1152
|
+
const { nestId, nodeId, requestedBy, reviewId } = params;
|
|
1153
|
+
const db = getDb();
|
|
1154
|
+
try {
|
|
1155
|
+
const rows = await db.all(
|
|
1156
|
+
`SELECT DISTINCT w.user_email, w.node_id AS via
|
|
1157
|
+
FROM watchers w
|
|
1158
|
+
WHERE w.nest_id = ?
|
|
1159
|
+
AND (w.node_id = ?
|
|
1160
|
+
OR w.node_id IN (
|
|
1161
|
+
SELECT e.from_node FROM edges e
|
|
1162
|
+
JOIN edge_types t ON t.id = e.type_id
|
|
1163
|
+
WHERE e.nest_id = ? AND e.to_node = ? AND t.is_flow = 1
|
|
1164
|
+
))`,
|
|
1165
|
+
[nestId, nodeId, nestId, nodeId]
|
|
1166
|
+
);
|
|
1167
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1168
|
+
const label = await describeNode(nestId, nodeId);
|
|
1169
|
+
let created = 0;
|
|
1170
|
+
for (const r of rows) {
|
|
1171
|
+
if (r.user_email.toLowerCase() === requestedBy.toLowerCase()) continue;
|
|
1172
|
+
await db.run(
|
|
1173
|
+
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
1174
|
+
VALUES (?, ?, ?, 'review_requested', ?, ?, ?)`,
|
|
1175
|
+
[
|
|
1176
|
+
uuid(),
|
|
1177
|
+
nestId,
|
|
1178
|
+
r.user_email.toLowerCase(),
|
|
1179
|
+
reviewId,
|
|
1180
|
+
r.via === nodeId ? `"${label}" has a new version awaiting review (submitted by ${requestedBy})` : `Agent "${await describeNode(nestId, r.via)}" produced output on "${label}" \u2014 awaiting review`,
|
|
1181
|
+
now
|
|
1182
|
+
]
|
|
1183
|
+
);
|
|
1184
|
+
created++;
|
|
1185
|
+
}
|
|
1186
|
+
return created;
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
console.error("[notify] review fan-out failed", nestId, nodeId, err);
|
|
1189
|
+
return 0;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
async function notifyReviewResolved(params) {
|
|
1193
|
+
const { nestId, nodeId, status, resolvedBy, requestedBy, reviewId } = params;
|
|
1194
|
+
const db = getDb();
|
|
1195
|
+
try {
|
|
1196
|
+
const rows = await db.all(
|
|
1197
|
+
`SELECT DISTINCT w.user_email
|
|
1198
|
+
FROM watchers w
|
|
1199
|
+
WHERE w.nest_id = ?
|
|
1200
|
+
AND (w.node_id = ?
|
|
1201
|
+
OR w.node_id IN (
|
|
1202
|
+
SELECT e.from_node FROM edges e
|
|
1203
|
+
JOIN edge_types t ON t.id = e.type_id
|
|
1204
|
+
WHERE e.nest_id = ? AND e.to_node = ? AND t.is_flow = 1
|
|
1205
|
+
))`,
|
|
1206
|
+
[nestId, nodeId, nestId, nodeId]
|
|
1207
|
+
);
|
|
1208
|
+
const recipients = new Set(rows.map((r) => r.user_email.toLowerCase()));
|
|
1209
|
+
recipients.add(requestedBy.toLowerCase());
|
|
1210
|
+
recipients.delete(resolvedBy.toLowerCase());
|
|
1211
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1212
|
+
const label = await describeNode(nestId, nodeId);
|
|
1213
|
+
let created = 0;
|
|
1214
|
+
for (const email of recipients) {
|
|
1215
|
+
await db.run(
|
|
1216
|
+
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
1217
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1218
|
+
[
|
|
1219
|
+
uuid(),
|
|
1220
|
+
nestId,
|
|
1221
|
+
email,
|
|
1222
|
+
`review_${status}`,
|
|
1223
|
+
reviewId,
|
|
1224
|
+
`"${label}" was ${status} by ${resolvedBy}`,
|
|
1225
|
+
now
|
|
1226
|
+
]
|
|
1227
|
+
);
|
|
1228
|
+
created++;
|
|
1229
|
+
}
|
|
1230
|
+
return created;
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
console.error("[notify] resolution fan-out failed", nestId, nodeId, err);
|
|
1233
|
+
return 0;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
async function notifyNestInvite(params) {
|
|
1237
|
+
const { nestId, inviteeEmail, permission, nestName: nestName2, viaTeam } = params;
|
|
1238
|
+
const message = viaTeam ? `Your team "${viaTeam}" was given access to "${nestName2}" as ${permissionLabel(permission)}` : `You were added to "${nestName2}" as ${permissionLabel(permission)}`;
|
|
1239
|
+
try {
|
|
1240
|
+
await getDb().run(
|
|
1241
|
+
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
1242
|
+
VALUES (?, ?, ?, 'nest_invite', ?, ?, ?)`,
|
|
1243
|
+
[
|
|
1244
|
+
uuid(),
|
|
1245
|
+
nestId,
|
|
1246
|
+
inviteeEmail.toLowerCase(),
|
|
1247
|
+
nestId,
|
|
1248
|
+
message,
|
|
1249
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
1250
|
+
]
|
|
1251
|
+
);
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
console.error("[notify] invite inbox insert failed", nestId, inviteeEmail, err);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function listNotifications(userEmail, opts = {}) {
|
|
1257
|
+
const limit = Math.min(opts.limit ?? 50, 200);
|
|
1258
|
+
return await getDb().all(
|
|
1259
|
+
`SELECT n.id, n.nest_id, n.kind, n.subject_id, n.message, n.created_at, n.read_at,
|
|
1260
|
+
COALESCE(
|
|
1261
|
+
rr.node_id,
|
|
1262
|
+
CASE WHEN dr.target_type = 'document' THEN dr.node_id END
|
|
1263
|
+
) AS node_id
|
|
1264
|
+
FROM notifications n
|
|
1265
|
+
LEFT JOIN review_requests rr ON rr.id = n.subject_id
|
|
1266
|
+
LEFT JOIN deletion_requests dr ON dr.id = n.subject_id
|
|
1267
|
+
WHERE LOWER(n.user_email) = LOWER(?)${opts.unreadOnly ? " AND n.read_at IS NULL" : ""}
|
|
1268
|
+
ORDER BY n.created_at DESC LIMIT ${limit}`,
|
|
1269
|
+
[userEmail]
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
async function markNotificationsRead(userEmail, ids) {
|
|
1273
|
+
const db = getDb();
|
|
1274
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1275
|
+
if (ids === "all") {
|
|
1276
|
+
await db.run(
|
|
1277
|
+
"UPDATE notifications SET read_at = ? WHERE LOWER(user_email) = LOWER(?) AND read_at IS NULL",
|
|
1278
|
+
[now, userEmail]
|
|
1279
|
+
);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
for (const id of ids.slice(0, 200)) {
|
|
1283
|
+
await db.run(
|
|
1284
|
+
"UPDATE notifications SET read_at = ? WHERE id = ? AND LOWER(user_email) = LOWER(?)",
|
|
1285
|
+
[now, id, userEmail]
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1121
1290
|
// src/governance/teams-service.ts
|
|
1122
1291
|
var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
|
|
1123
1292
|
function parseMembers(json) {
|
|
@@ -1196,7 +1365,7 @@ async function createTeam(params) {
|
|
|
1196
1365
|
if (!name) throw new ValidationError("Team name is required");
|
|
1197
1366
|
const db = getDb();
|
|
1198
1367
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1199
|
-
const id =
|
|
1368
|
+
const id = uuid2();
|
|
1200
1369
|
await db.run(
|
|
1201
1370
|
"INSERT INTO teams (id, name, owner_id, members, source, external_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
1202
1371
|
[
|
|
@@ -1316,10 +1485,10 @@ async function resolveMemberIdentity(params) {
|
|
|
1316
1485
|
};
|
|
1317
1486
|
}
|
|
1318
1487
|
const { hashPassword } = await import("./keys-CTOGAG3W.js");
|
|
1319
|
-
const userId =
|
|
1488
|
+
const userId = uuid2();
|
|
1320
1489
|
await db.run(
|
|
1321
1490
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1322
|
-
[userId, requested, null, await hashPassword(
|
|
1491
|
+
[userId, requested, null, await hashPassword(uuid2())]
|
|
1323
1492
|
);
|
|
1324
1493
|
return { userId, email: requested };
|
|
1325
1494
|
}
|
|
@@ -1408,6 +1577,7 @@ async function listNestTeams(nestId) {
|
|
|
1408
1577
|
async function shareTeamWithNest(params) {
|
|
1409
1578
|
const db = getDb();
|
|
1410
1579
|
let sharedMembers = [];
|
|
1580
|
+
let sharedTeamName = "";
|
|
1411
1581
|
const refs = await db.transaction(async (tx) => {
|
|
1412
1582
|
const team = await tx.get(
|
|
1413
1583
|
"SELECT id, name, owner_id, members FROM teams WHERE id = ?",
|
|
@@ -1434,6 +1604,7 @@ async function shareTeamWithNest(params) {
|
|
|
1434
1604
|
]);
|
|
1435
1605
|
const members = parseMembers(team.members);
|
|
1436
1606
|
sharedMembers = members;
|
|
1607
|
+
sharedTeamName = team.name;
|
|
1437
1608
|
const hasGovernanceRole = members.some(
|
|
1438
1609
|
(m) => m.role === "viewer" || m.role === "editor"
|
|
1439
1610
|
);
|
|
@@ -1468,6 +1639,16 @@ async function shareTeamWithNest(params) {
|
|
|
1468
1639
|
permission: role,
|
|
1469
1640
|
by: caller?.email || void 0
|
|
1470
1641
|
});
|
|
1642
|
+
const permission = teamRoleToPermission(role) ?? role;
|
|
1643
|
+
for (const email of emails) {
|
|
1644
|
+
await notifyNestInvite({
|
|
1645
|
+
nestId: params.nestId,
|
|
1646
|
+
inviteeEmail: email,
|
|
1647
|
+
permission,
|
|
1648
|
+
nestName: shareNestName,
|
|
1649
|
+
viaTeam: sharedTeamName
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1471
1652
|
}
|
|
1472
1653
|
} catch (err) {
|
|
1473
1654
|
console.error("[notify] team-share email fan-out failed", params.nestId, err);
|
|
@@ -1696,6 +1877,17 @@ async function isImportedNest(nestId) {
|
|
|
1696
1877
|
);
|
|
1697
1878
|
return !!row?.is_imported;
|
|
1698
1879
|
}
|
|
1880
|
+
function plainName(name, emptyMessage = "Name cannot be empty") {
|
|
1881
|
+
const typed = (name ?? "").trim();
|
|
1882
|
+
if (!typed) throw new ValidationError(emptyMessage);
|
|
1883
|
+
const stripped = typed.replace(/<[^>]*>/g, "").trim();
|
|
1884
|
+
if (!stripped) {
|
|
1885
|
+
throw new ValidationError(
|
|
1886
|
+
"That's not a valid nest name \u2014 it's only HTML markup. Use plain text."
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
return stripped;
|
|
1890
|
+
}
|
|
1699
1891
|
function toSlug(name) {
|
|
1700
1892
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1701
1893
|
}
|
|
@@ -1730,6 +1922,22 @@ async function setStewardshipEnabled(nestId, enabled) {
|
|
|
1730
1922
|
nestId
|
|
1731
1923
|
]);
|
|
1732
1924
|
}
|
|
1925
|
+
async function setReaderMode(nestId, opts) {
|
|
1926
|
+
const db = getDb();
|
|
1927
|
+
const sets = [];
|
|
1928
|
+
const params = [];
|
|
1929
|
+
if (opts.enabled !== void 0) {
|
|
1930
|
+
sets.push("reader_mode = ?");
|
|
1931
|
+
params.push(opts.enabled ? 1 : 0);
|
|
1932
|
+
}
|
|
1933
|
+
if (opts.homeNode !== void 0) {
|
|
1934
|
+
sets.push("reader_home_node = ?");
|
|
1935
|
+
params.push(opts.homeNode ?? null);
|
|
1936
|
+
}
|
|
1937
|
+
if (sets.length === 0) return;
|
|
1938
|
+
params.push(nestId);
|
|
1939
|
+
await db.run(`UPDATE nests SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1940
|
+
}
|
|
1733
1941
|
async function nestAllowsSelfApprove(nestId) {
|
|
1734
1942
|
const db = getDb();
|
|
1735
1943
|
const row = await db.get(
|
|
@@ -1806,8 +2014,7 @@ async function renameNest(nestId, patch) {
|
|
|
1806
2014
|
const updates = [];
|
|
1807
2015
|
const params = [];
|
|
1808
2016
|
if (patch.name !== void 0) {
|
|
1809
|
-
const trimmed = patch.name
|
|
1810
|
-
if (!trimmed) throw new ValidationError("Name cannot be empty");
|
|
2017
|
+
const trimmed = plainName(patch.name);
|
|
1811
2018
|
const slug = toSlug(trimmed);
|
|
1812
2019
|
if (!slug) throw new ValidationError("Name must contain at least one alphanumeric character");
|
|
1813
2020
|
const conflict = await db.get(
|
|
@@ -1837,10 +2044,9 @@ async function renameNest(nestId, patch) {
|
|
|
1837
2044
|
return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]);
|
|
1838
2045
|
}
|
|
1839
2046
|
async function createNest(userId, name, description) {
|
|
1840
|
-
const id =
|
|
2047
|
+
const id = uuid3();
|
|
1841
2048
|
const db = getDb();
|
|
1842
|
-
const trimmed = name
|
|
1843
|
-
if (!trimmed) throw new ValidationError("Name cannot be empty");
|
|
2049
|
+
const trimmed = plainName(name);
|
|
1844
2050
|
const slug = toSlug(trimmed);
|
|
1845
2051
|
if (!slug) {
|
|
1846
2052
|
throw new ValidationError(
|
|
@@ -1869,12 +2075,9 @@ async function createNest(userId, name, description) {
|
|
|
1869
2075
|
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
1870
2076
|
}
|
|
1871
2077
|
async function importNest(userId, name) {
|
|
1872
|
-
const nm = (name || "")
|
|
1873
|
-
if (!nm) {
|
|
1874
|
-
throw new ValidationError("name is required");
|
|
1875
|
-
}
|
|
2078
|
+
const nm = plainName(name || "", "name is required");
|
|
1876
2079
|
const db = getDb();
|
|
1877
|
-
const id =
|
|
2080
|
+
const id = uuid3();
|
|
1878
2081
|
const slug = toSlug(nm);
|
|
1879
2082
|
const visibility = userId === ANON_USER_ID ? "public" : "private";
|
|
1880
2083
|
mkdirSync(resolveNestPath(id), { recursive: true });
|
|
@@ -2068,6 +2271,21 @@ function isSkippedSegment(segment) {
|
|
|
2068
2271
|
return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
|
|
2069
2272
|
}
|
|
2070
2273
|
|
|
2274
|
+
// src/nodes/meta-files.ts
|
|
2275
|
+
var META_BASENAMES = /* @__PURE__ */ new Set([
|
|
2276
|
+
"INDEX",
|
|
2277
|
+
"CONTEXT",
|
|
2278
|
+
"CLAUDE",
|
|
2279
|
+
"GEMINI",
|
|
2280
|
+
"AGENTS",
|
|
2281
|
+
"QWEN",
|
|
2282
|
+
"README"
|
|
2283
|
+
]);
|
|
2284
|
+
function isMetaFile(idOrPath) {
|
|
2285
|
+
const base = idOrPath.split(/[/\\]/).pop() ?? "";
|
|
2286
|
+
return META_BASENAMES.has(base.replace(/\.md$/i, ""));
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2071
2289
|
// src/shared/batch.ts
|
|
2072
2290
|
var IO_CONCURRENCY = 32;
|
|
2073
2291
|
async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
|
|
@@ -2079,14 +2297,6 @@ async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
|
|
|
2079
2297
|
}
|
|
2080
2298
|
|
|
2081
2299
|
// src/nodes/flat-storage.ts
|
|
2082
|
-
var META_FILES = /* @__PURE__ */ new Set([
|
|
2083
|
-
"INDEX.md",
|
|
2084
|
-
"CONTEXT.md",
|
|
2085
|
-
"CLAUDE.md",
|
|
2086
|
-
"GEMINI.md",
|
|
2087
|
-
"AGENTS.md",
|
|
2088
|
-
"QWEN.md"
|
|
2089
|
-
]);
|
|
2090
2300
|
var FlatNestStorage = class extends NestStorage2 {
|
|
2091
2301
|
async discoverDocuments() {
|
|
2092
2302
|
const root = this.root;
|
|
@@ -2100,7 +2310,7 @@ var FlatNestStorage = class extends NestStorage2 {
|
|
|
2100
2310
|
const ids = [];
|
|
2101
2311
|
for (const e of entries) {
|
|
2102
2312
|
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
2103
|
-
if (
|
|
2313
|
+
if (isMetaFile(e.name)) continue;
|
|
2104
2314
|
const dir = e.parentPath ?? e.path ?? root;
|
|
2105
2315
|
const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
|
|
2106
2316
|
if (rel.split("/").some(isSkippedSegment)) continue;
|
|
@@ -2247,8 +2457,8 @@ async function ensureNodeIndex(nestId) {
|
|
|
2247
2457
|
return run;
|
|
2248
2458
|
}
|
|
2249
2459
|
async function rebuildNodeIndex(nestId) {
|
|
2250
|
-
const { engineCache: engineCache2 } = await import("./engine-
|
|
2251
|
-
const { documentsWithSuggestions } = await import("./external-edit-service-
|
|
2460
|
+
const { engineCache: engineCache2 } = await import("./engine-VZTSDUPB.js");
|
|
2461
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-3FVK3OD7.js");
|
|
2252
2462
|
const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
|
|
2253
2463
|
dropDiscoveryCache();
|
|
2254
2464
|
const startedAt = writeGeneration.get(nestId) ?? 0;
|
|
@@ -2345,18 +2555,11 @@ async function indexedFolderCounts(nestId) {
|
|
|
2345
2555
|
// src/nodes/engine.ts
|
|
2346
2556
|
var DISCOVERY_TTL_MS = 3e4;
|
|
2347
2557
|
var isLive = (node) => node.frontmatter.status !== "rejected";
|
|
2348
|
-
var DERIVED_VAULT_FILES = /* @__PURE__ */ new Set([
|
|
2349
|
-
"INDEX.md",
|
|
2350
|
-
"CONTEXT.md",
|
|
2351
|
-
"CLAUDE.md",
|
|
2352
|
-
"GEMINI.md",
|
|
2353
|
-
"AGENTS.md",
|
|
2354
|
-
"README.md"
|
|
2355
|
-
]);
|
|
2356
2558
|
function withDiscoveryCache(nestId, storage) {
|
|
2357
2559
|
const entries = /* @__PURE__ */ new Map();
|
|
2358
2560
|
let inflight = null;
|
|
2359
|
-
const
|
|
2561
|
+
const rawDiscover = storage.discoverDocuments.bind(storage);
|
|
2562
|
+
const discover = async (options) => (await rawDiscover(options)).filter((d) => !isMetaFile(d.id));
|
|
2360
2563
|
const readDoc = storage.readDocument.bind(storage);
|
|
2361
2564
|
const filtersRetired = !(storage instanceof FlatNestStorage);
|
|
2362
2565
|
let generation = 0;
|
|
@@ -2407,6 +2610,7 @@ function withDiscoveryCache(nestId, storage) {
|
|
|
2407
2610
|
await markIndexStale(nestId);
|
|
2408
2611
|
};
|
|
2409
2612
|
const patch = async (id) => {
|
|
2613
|
+
if (isMetaFile(id)) return;
|
|
2410
2614
|
generation++;
|
|
2411
2615
|
inflight = null;
|
|
2412
2616
|
let node = null;
|
|
@@ -2448,8 +2652,7 @@ function withDiscoveryCache(nestId, storage) {
|
|
|
2448
2652
|
wrap("init", () => desync());
|
|
2449
2653
|
wrap("writeVaultFile", (args) => {
|
|
2450
2654
|
const rel = String(args[0] ?? "");
|
|
2451
|
-
|
|
2452
|
-
if (rel.endsWith(".md") && !DERIVED_VAULT_FILES.has(base)) return desync();
|
|
2655
|
+
if (rel.endsWith(".md") && !isMetaFile(rel)) return desync();
|
|
2453
2656
|
});
|
|
2454
2657
|
return { storage, invalidate, desync: () => void desync() };
|
|
2455
2658
|
}
|
|
@@ -2513,9 +2716,9 @@ export {
|
|
|
2513
2716
|
canEditWith,
|
|
2514
2717
|
canApproveWith,
|
|
2515
2718
|
primaryRole,
|
|
2516
|
-
permissionLabel,
|
|
2517
2719
|
verifySmtp,
|
|
2518
2720
|
notifyEmailForNest,
|
|
2721
|
+
isEmailConfigured,
|
|
2519
2722
|
sendEmailToRecipient,
|
|
2520
2723
|
nestStorageRoot,
|
|
2521
2724
|
resolveNestPath,
|
|
@@ -2525,6 +2728,7 @@ export {
|
|
|
2525
2728
|
uniqueNestName,
|
|
2526
2729
|
isStewardshipEnabled,
|
|
2527
2730
|
setStewardshipEnabled,
|
|
2731
|
+
setReaderMode,
|
|
2528
2732
|
nestAllowsSelfApprove,
|
|
2529
2733
|
setAllowSelfApprove,
|
|
2530
2734
|
nestReviewsPrimeOnly,
|
|
@@ -2563,6 +2767,14 @@ export {
|
|
|
2563
2767
|
docLink,
|
|
2564
2768
|
resolveNodeUrl,
|
|
2565
2769
|
buildDocContext,
|
|
2770
|
+
listWatchers,
|
|
2771
|
+
addWatcher,
|
|
2772
|
+
removeWatcher,
|
|
2773
|
+
notifyReviewRequested,
|
|
2774
|
+
notifyReviewResolved,
|
|
2775
|
+
notifyNestInvite,
|
|
2776
|
+
listNotifications,
|
|
2777
|
+
markNotificationsRead,
|
|
2566
2778
|
createTeam,
|
|
2567
2779
|
findTeamByExternalId,
|
|
2568
2780
|
listTeamsForUser,
|
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
markIndexStale,
|
|
5
5
|
resolveNestPath,
|
|
6
6
|
setSuggestionFlag
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7CPP6FIU.js";
|
|
8
8
|
import {
|
|
9
9
|
getDb
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-XCFKS65Y.js";
|
|
11
11
|
|
|
12
12
|
// src/governance/external-edit-service.ts
|
|
13
13
|
import { readFile, readdir } from "fs/promises";
|
|
@@ -253,7 +253,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
|
|
|
253
253
|
async function mirrorVersion(input) {
|
|
254
254
|
const { storage } = await engineCache.get(input.nestId);
|
|
255
255
|
const node = await storage.readDocument(input.documentId);
|
|
256
|
-
const { upsertVersion, setApprovedVersion } = await import("./version-service-
|
|
256
|
+
const { upsertVersion, setApprovedVersion } = await import("./version-service-5PIFT2OY.js");
|
|
257
257
|
await upsertVersion({
|
|
258
258
|
nestId: input.nestId,
|
|
259
259
|
nodeId: input.documentId,
|