@promptowl/contextnest-community 1.21.1 → 1.23.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 +6 -2
- package/README.md +1 -1
- package/dist/{chunk-LL2JU5AA.js → chunk-2YPY2HGZ.js} +2 -2
- package/dist/{chunk-FXYFLUE7.js → chunk-5CVZMHHB.js} +2 -2
- package/dist/{chunk-NH7L2S6S.js → chunk-MLQU4I5J.js} +52 -12
- package/dist/{chunk-G4AL7ETF.js → chunk-QLXC6542.js} +43 -11
- package/dist/{chunk-SKZ7YYT4.js → chunk-QWNXWRWO.js} +114 -2
- package/dist/{chunk-IVXVOXYN.js → chunk-UOMW7TT6.js} +1 -1
- package/dist/{chunk-UVOAYQTU.js → chunk-XYD6V2LH.js} +4 -4
- package/dist/{client-KBK4VIWJ.js → client-LWDMNKX3.js} +1 -1
- package/dist/{engine-WXWER2V7.js → engine-QFF2IA3G.js} +2 -2
- package/dist/{external-edit-service-Y757EFMS.js → external-edit-service-F7D3VSA2.js} +3 -3
- package/dist/{grants-service-FKINSDLZ.js → grants-service-TBGTCO5K.js} +2 -2
- package/dist/index.js +920 -228
- package/dist/{migrations.postgres-JA2YGLS3.js → migrations.postgres-APCVSYUE.js} +32 -2
- package/dist/{review-service-PPR5BV5J.js → review-service-A7PNLWG5.js} +6 -6
- package/dist/{stewardship-service-IKC7MXLP.js → stewardship-service-32UHIRQ5.js} +3 -3
- package/dist/{version-service-GWSDSUZR.js → version-service-RJ6CWZ3O.js} +3 -3
- package/dist/web3/assets/index-BJ-LRNis.js +1382 -0
- package/dist/web3/assets/index-DPMEt-A_.css +1 -0
- package/dist/web3/assets/module-uADzTImp.js +3 -0
- package/dist/web3/index.html +2 -2
- package/package.json +2 -1
- package/dist/web3/assets/index-T4pilFNL.css +0 -1
- package/dist/web3/assets/index-deyhV93k.js +0 -1382
package/CONFIGURATION.md
CHANGED
|
@@ -59,7 +59,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
59
59
|
| `PROMPTOWL_KEY` | `""` | Your PromptOwl Community License key (`pk_...`). Unlicensed instances still run and serve reads, but every write returns `503` until a valid key is installed. Can also be set via the browser License Setup Page, which persists it to the database (`server_settings` table) so it survives a rebuild and reaches every instance — see [Runtime settings persistence](#runtime-settings-persistence). Setting it here in the deploy environment takes precedence on the next boot. |
|
|
60
60
|
| `PROMPTOWL_SIGN_IN_GATE` | `open` | Restrict "Sign in with PromptOwl". `open` = anyone may; `admin-only` = only the license owner (admin) may, everyone else uses email/password (admin opens the login page with `?admin=1`); `disabled` = nobody may. Enforced server-side at `POST /auth/promptowl` and surfaced on the health endpoint. Unknown values fall back to `open`. |
|
|
61
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
|
-
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **
|
|
62
|
+
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **Legacy — official deployment only, leave unset on self-hosted.** Shared HMAC secret enabling the one-click "Open Community" SSO auto-login from PromptOwl. Must exactly match the same-named var on PromptOwl. Superseded by the DB-backed **Settings → Community sites** list (`/admin/community-sites`), which supports multiple official sites each with their own name/url/secret/active flag — this env var still works as an implicit extra site for backward compatibility. When no site is configured (env var unset and no DB rows), `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. See `API.md → Community sites`. |
|
|
63
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`. |
|
|
@@ -67,6 +67,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
67
67
|
| `OIDC_CLIENT_SECRET` | `""` | Client secret from your IdP app registration. Write-only on the Settings API — `GET /admin/settings` reports only `oidc_client_secret_set: true/false`, never the value. |
|
|
68
68
|
| `OIDC_ALLOWED_DOMAINS` | `""` (any) | Comma-separated email-domain allowlist, e.g. `acme.com, contractors.acme.com`. When set, only accounts whose asserted email is on a listed domain may sign in (others bounce with `domain_not_allowed`). Empty allows any domain the IdP asserts. |
|
|
69
69
|
| `OIDC_AUTO_PROVISION` | `true` | Create a user automatically on first successful OIDC sign-in (display name from the `name` claim). Set `false` to allow only pre-existing (invited/registered) users — unknown emails bounce with `not_invited`. |
|
|
70
|
+
| `PEOPLE_SUGGEST_ALL_USERS` | `false` | Widen the add-a-person suggestions (`GET /people/suggest`, behind every "add a person" field) and the `@mention` pickers from *people the caller already shares a nest or team with* to **every registered account on this server**. **Leave this off on a server that hosts more than one organisation** — the PromptOwl-hosted deployment does, and turning it on there would offer one customer's staff another customer's email addresses. On a single-company self-hosted server the whole directory is the useful answer, which is what this is for. Suggestions never gate the invite either way: an address that appears in no list is still valid to type. In the `@` pickers the widening is an invite: a write+ author mentioning someone off the nest adds them as a `read` collaborator before notifying (a read-only author's mention of an outsider reaches nobody). A server admin always sees every account (they administer them). Also editable from **Settings → General → People directory**. |
|
|
70
71
|
| `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
72
|
| `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
73
|
| `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. |
|
|
@@ -76,6 +77,8 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
76
77
|
| `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`. |
|
|
77
78
|
| `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
|
|
78
79
|
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
|
80
|
+
| `POSTHOG_KEY` | `""` | PostHog project API key for product analytics in the UI. Empty = analytics off (the default — a self-hosted install brings its own project). Served to the browser via `/health`, but only while `TELEMETRY_ENABLED` is on, so that switch turns off everything this server sends outward. Also editable from Settings → Advanced. |
|
|
81
|
+
| `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog ingestion host. Set it to your own region or self-hosted PostHog. Also editable from Settings → Advanced. |
|
|
79
82
|
| `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
|
|
80
83
|
| `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). |
|
|
81
84
|
| `FRAME_ANCESTORS` | `'self'` | Which origins may embed this server in an iframe, sent as CSP `frame-ancestors`. The default lets nothing but this origin frame the UI, which blocks clickjacking. Deployments that are meant to be embedded list the embedding origin — e.g. the PromptOwl Data Room iframes ContextNest, so that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`. Comma-separated; `'self'` is always included; `*` allows any site and disables the protection. Note the embedding page must be **same-site** (a sibling subdomain) for the session cookie to survive inside the frame — a genuinely cross-domain embed will render the login page no matter what this is set to. |
|
|
@@ -150,7 +153,8 @@ plain-text `fallbackText` for clients that cannot render cards.
|
|
|
150
153
|
Per-nest connector rows post the same Adaptive Card format to a nest-specific
|
|
151
154
|
webhook, with per-event filtering: `POST /nests/:id/connectors` with a JSON
|
|
152
155
|
body like `{"channel": "teams", "url": "https://…", "events":
|
|
153
|
-
["review_requested", "review_rejected"]}` (`events`
|
|
156
|
+
["review_requested", "review_rejected"]}` (`events` is any of `review_requested`,
|
|
157
|
+
`review_approved`, `review_rejected`, `run_failed`, `mention`, or `["*"]`; the
|
|
154
158
|
`url` may be an `env:KEY` reference into the nest's env store). Manage rows
|
|
155
159
|
with `GET`/`PATCH`/`DELETE` on the same path.
|
|
156
160
|
|
package/README.md
CHANGED
|
@@ -165,4 +165,4 @@ The Software injects content into large language models. AI output may be inaccu
|
|
|
165
165
|
**Copyright © 2026 Promptowl LLC.** All rights reserved.
|
|
166
166
|
"ContextNest" and "PromptOwl" are trademarks of Promptowl LLC.
|
|
167
167
|
|
|
168
|
-
Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
|
|
168
|
+
Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveNestPermission,
|
|
12
12
|
resolveTeamRolesForUser,
|
|
13
13
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-QLXC6542.js";
|
|
15
15
|
import {
|
|
16
16
|
ConflictError,
|
|
17
17
|
ValidationError
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
config,
|
|
21
21
|
getDb,
|
|
22
22
|
isEmailish
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-QWNXWRWO.js";
|
|
24
24
|
|
|
25
25
|
// src/governance/stewardship-service.ts
|
|
26
26
|
import { v4 as uuid } from "uuid";
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
buildTitleMap,
|
|
3
3
|
insertOrReplace,
|
|
4
4
|
nowExpr
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-QLXC6542.js";
|
|
6
6
|
import {
|
|
7
7
|
getDb
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QWNXWRWO.js";
|
|
9
9
|
|
|
10
10
|
// src/governance/version-service.ts
|
|
11
11
|
import { createHash } from "crypto";
|
|
@@ -4,19 +4,19 @@ import {
|
|
|
4
4
|
resolveNestWideRoles,
|
|
5
5
|
resolveStewardsForNode,
|
|
6
6
|
stewardCoverageForUser
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2YPY2HGZ.js";
|
|
8
8
|
import {
|
|
9
9
|
grantCoversNode,
|
|
10
10
|
listUserGrants,
|
|
11
11
|
resolveNodeGrant
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-UOMW7TT6.js";
|
|
13
13
|
import {
|
|
14
14
|
createVersion,
|
|
15
15
|
getApprovedVersion,
|
|
16
16
|
getApprovedVersions,
|
|
17
17
|
getCurrentVersion,
|
|
18
18
|
setApprovedVersion
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-5CVZMHHB.js";
|
|
20
20
|
import {
|
|
21
21
|
buildDocContext,
|
|
22
22
|
buildTitleMap,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
insertOrReplace,
|
|
27
27
|
isPublicReader,
|
|
28
28
|
isStewardshipEnabled,
|
|
29
|
+
markNotificationsReadBySubject,
|
|
29
30
|
nestName,
|
|
30
31
|
notifyEmailForNest,
|
|
31
32
|
notifyReviewRequested,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
resolveNestPermission,
|
|
37
38
|
sendEmailToRecipient,
|
|
38
39
|
titleForNode
|
|
39
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-QLXC6542.js";
|
|
40
41
|
import {
|
|
41
42
|
ConflictError,
|
|
42
43
|
NotFoundError,
|
|
@@ -45,7 +46,7 @@ import {
|
|
|
45
46
|
import {
|
|
46
47
|
config,
|
|
47
48
|
getDb
|
|
48
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-QWNXWRWO.js";
|
|
49
50
|
|
|
50
51
|
// src/governance/review-service.ts
|
|
51
52
|
import { v4 as uuid2 } from "uuid";
|
|
@@ -131,7 +132,17 @@ async function nodeTrail(db, nestId, ids) {
|
|
|
131
132
|
for (const r of rows) if (r.title) titles.set(r.node_id, r.title);
|
|
132
133
|
} catch {
|
|
133
134
|
}
|
|
134
|
-
|
|
135
|
+
const label = (id) => titles.get(id) ?? id;
|
|
136
|
+
const idsPerLabel = /* @__PURE__ */ new Map();
|
|
137
|
+
for (const id of unique) {
|
|
138
|
+
const l = label(id);
|
|
139
|
+
(idsPerLabel.get(l) ?? idsPerLabel.set(l, /* @__PURE__ */ new Set()).get(l)).add(id);
|
|
140
|
+
}
|
|
141
|
+
return ids.map((id) => {
|
|
142
|
+
const l = label(id);
|
|
143
|
+
const ambiguous = (idsPerLabel.get(l)?.size ?? 0) > 1;
|
|
144
|
+
return ambiguous && l !== id ? `"${l}" (${id})` : `"${l}"`;
|
|
145
|
+
}).join(" \u2192 ");
|
|
135
146
|
}
|
|
136
147
|
async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
|
|
137
148
|
const db = getDb();
|
|
@@ -147,7 +158,9 @@ async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
|
|
|
147
158
|
for (const r of rows) {
|
|
148
159
|
if (r.from_node === r.to_node) {
|
|
149
160
|
throw new ConflictError(
|
|
150
|
-
`
|
|
161
|
+
`This step loops back on itself: ${await nodeTrail(db, nestId, [r.from_node])}.
|
|
162
|
+
|
|
163
|
+
A flow can't return to a step it already ran.`
|
|
151
164
|
);
|
|
152
165
|
}
|
|
153
166
|
nodes.add(r.from_node);
|
|
@@ -170,7 +183,7 @@ async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
|
|
|
170
183
|
}
|
|
171
184
|
if (removed < nodes.size) {
|
|
172
185
|
throw new ConflictError(
|
|
173
|
-
"
|
|
186
|
+
"Making this connection type a flow would loop these steps back on each other. A flow can't return to a step it already ran."
|
|
174
187
|
);
|
|
175
188
|
}
|
|
176
189
|
}
|
|
@@ -534,6 +547,7 @@ async function notifySlack(text) {
|
|
|
534
547
|
|
|
535
548
|
// src/notify/dispatch.ts
|
|
536
549
|
var EVENT_EMOJI = {
|
|
550
|
+
mention: ":speech_balloon:",
|
|
537
551
|
review_requested: ":memo:",
|
|
538
552
|
review_approved: ":white_check_mark:",
|
|
539
553
|
review_rejected: ":x:",
|
|
@@ -600,6 +614,27 @@ async function resolveUrl(nestId, raw) {
|
|
|
600
614
|
}
|
|
601
615
|
return isSafeConnectorUrl(raw) ? raw : null;
|
|
602
616
|
}
|
|
617
|
+
async function hasAnyConnector(nestId, kind) {
|
|
618
|
+
if ((config.SLACK_WEBHOOK_URL || config.MSTEAMS_WEBHOOK_URL) && !DIGEST_DELIVERED_KINDS.has(kind)) {
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const rows = await getDb().all(
|
|
623
|
+
"SELECT events FROM connectors WHERE nest_id = ? AND enabled = 1",
|
|
624
|
+
[nestId]
|
|
625
|
+
);
|
|
626
|
+
return rows.some((r) => {
|
|
627
|
+
try {
|
|
628
|
+
const kinds = JSON.parse(r.events);
|
|
629
|
+
return kinds.includes("*") || kinds.includes(kind);
|
|
630
|
+
} catch {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
603
638
|
async function dispatchEvent(ev) {
|
|
604
639
|
const db = getDb();
|
|
605
640
|
const nestName2 = await resolveNestName(ev.nestId);
|
|
@@ -1201,7 +1236,8 @@ async function deletionTargetContext(request, baseUrl) {
|
|
|
1201
1236
|
async function notifyDeletionRequested(request, baseUrl) {
|
|
1202
1237
|
try {
|
|
1203
1238
|
const ctx = await deletionTargetContext(request, baseUrl);
|
|
1204
|
-
const
|
|
1239
|
+
const isArchiveRequest = request.targetType === "nest_archive";
|
|
1240
|
+
const line = isArchiveRequest ? `Archive requested for ${ctx.noun} *${ctx.label}* by ${request.requestedBy} \u2014 "${request.reason}"` : `Deletion requested for ${ctx.noun} *${ctx.label}* by ${request.requestedBy} \u2014 "${request.reason}"`;
|
|
1205
1241
|
const admins = (await nestAdminEmails(request.nestId)).filter(
|
|
1206
1242
|
(e) => e !== request.requestedBy.toLowerCase()
|
|
1207
1243
|
);
|
|
@@ -1210,7 +1246,7 @@ async function notifyDeletionRequested(request, baseUrl) {
|
|
|
1210
1246
|
admins,
|
|
1211
1247
|
"deletion_requested",
|
|
1212
1248
|
request.id,
|
|
1213
|
-
`${ctx.noun === "document" ? "" : `${ctx.noun} `}"${ctx.docTitle}" was flagged for deletion by ${request.requestedBy} \u2014 "${request.reason}"`
|
|
1249
|
+
isArchiveRequest ? `${ctx.noun} "${ctx.docTitle}" was requested to be archived by ${request.requestedBy} \u2014 "${request.reason}"` : `${ctx.noun === "document" ? "" : `${ctx.noun} `}"${ctx.docTitle}" was flagged for deletion by ${request.requestedBy} \u2014 "${request.reason}"`
|
|
1214
1250
|
);
|
|
1215
1251
|
void dispatchEvent({
|
|
1216
1252
|
kind: "deletion_requested",
|
|
@@ -1257,9 +1293,11 @@ async function notifyDeletionResolved(params) {
|
|
|
1257
1293
|
try {
|
|
1258
1294
|
const { outcome, resolvedBy, note } = params;
|
|
1259
1295
|
const archivedInstead = outcome === "deleted" && params.targetType === "nest_archive";
|
|
1260
|
-
const
|
|
1296
|
+
const isArchiveRequest = params.targetType === "nest_archive";
|
|
1297
|
+
const verb = archivedInstead ? "archived" : outcome === "deleted" ? "deleted" : isArchiveRequest ? "declined the archive request for" : "rejected the deletion of";
|
|
1261
1298
|
const line = `${resolvedBy} ${verb} *${params.docTitle}*${note ? ` \u2014 "${note}"` : ""}`;
|
|
1262
1299
|
const recipient = params.requestedBy.toLowerCase();
|
|
1300
|
+
await markNotificationsReadBySubject(params.requestId, "deletion_requested");
|
|
1263
1301
|
const canInbox = !(params.targetType === "nest" && outcome === "deleted");
|
|
1264
1302
|
if (canInbox && recipient !== resolvedBy.toLowerCase()) {
|
|
1265
1303
|
await inboxInsert(
|
|
@@ -1267,7 +1305,7 @@ async function notifyDeletionResolved(params) {
|
|
|
1267
1305
|
[recipient],
|
|
1268
1306
|
`deletion_${outcome}`,
|
|
1269
1307
|
params.requestId,
|
|
1270
|
-
outcome === "deleted" ? `"${params.docTitle}" was ${archivedInstead ? "archived" : "deleted"} by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1308
|
+
outcome === "deleted" ? `"${params.docTitle}" was ${archivedInstead ? "archived" : "deleted"} by ${resolvedBy} \u2014 your request was accepted` : `Your ${isArchiveRequest ? "archive" : "deletion"} request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1271
1309
|
);
|
|
1272
1310
|
}
|
|
1273
1311
|
void dispatchEvent({
|
|
@@ -1436,6 +1474,7 @@ async function declineDeletion(params) {
|
|
|
1436
1474
|
requestedBy: declined.requestedBy,
|
|
1437
1475
|
resolvedBy: params.declinedBy,
|
|
1438
1476
|
requestId: declined.id,
|
|
1477
|
+
targetType: declined.targetType,
|
|
1439
1478
|
note: params.note,
|
|
1440
1479
|
baseUrl: params.baseUrl
|
|
1441
1480
|
});
|
|
@@ -1497,6 +1536,7 @@ export {
|
|
|
1497
1536
|
envRoutes,
|
|
1498
1537
|
envValues,
|
|
1499
1538
|
isSafeConnectorUrl,
|
|
1539
|
+
hasAnyConnector,
|
|
1500
1540
|
dispatchEvent,
|
|
1501
1541
|
sendTestNotification,
|
|
1502
1542
|
notifyNestEvent,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
getDb,
|
|
10
10
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QWNXWRWO.js";
|
|
12
12
|
import {
|
|
13
13
|
ANON_USER_ID
|
|
14
14
|
} from "./chunk-YB3LKF7U.js";
|
|
@@ -138,6 +138,12 @@ function startTelemetryLoop() {
|
|
|
138
138
|
function nowExpr(db) {
|
|
139
139
|
return db.dialect === "sqlite" ? "datetime('now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS')";
|
|
140
140
|
}
|
|
141
|
+
function suffixOf(db, column, length) {
|
|
142
|
+
return db.dialect === "sqlite" ? `substr(${column}, -${length})` : `right(${column}, ${length})`;
|
|
143
|
+
}
|
|
144
|
+
function nowMsExpr(db) {
|
|
145
|
+
return db.dialect === "sqlite" ? "strftime('%Y-%m-%d %H:%M:%f','now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS.MS')";
|
|
146
|
+
}
|
|
141
147
|
function insertOrIgnore(db, insertSql) {
|
|
142
148
|
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
|
|
143
149
|
}
|
|
@@ -657,6 +663,7 @@ function renderText(message) {
|
|
|
657
663
|
}
|
|
658
664
|
var STATUS_META = {
|
|
659
665
|
approved: { subjectWord: "approved", label: "Approved", color: "#16a34a" },
|
|
666
|
+
mention: { subjectWord: "mentioned you", label: "Mention", color: "#7c3aed" },
|
|
660
667
|
rejected: { subjectWord: "rejected", label: "Rejected", color: "#dc2626" },
|
|
661
668
|
pending_review: {
|
|
662
669
|
subjectWord: "awaiting review",
|
|
@@ -715,6 +722,17 @@ var TEMPLATES = {
|
|
|
715
722
|
lineH: (x) => `${x.doc}${x.vH} has been approved${x.byH}${x.noteH}.`,
|
|
716
723
|
rows: governanceRows
|
|
717
724
|
},
|
|
725
|
+
// A mention names one person, so this is the one template addressed to
|
|
726
|
+
// "you". `note` carries the comment's own words when there are any — a
|
|
727
|
+
// mention with no context reads as a summons.
|
|
728
|
+
mention: {
|
|
729
|
+
heading: (x) => `${x.d.actor || "Someone"} mentioned you in ${x.title}`,
|
|
730
|
+
detailsHeader: "Document details",
|
|
731
|
+
linkLabel: "Open document",
|
|
732
|
+
lineT: (x) => `You were mentioned in ${x.title}${x.byT}${x.noteT}.`,
|
|
733
|
+
lineH: (x) => `You were mentioned in ${x.doc}${x.byH}${x.noteH}.`,
|
|
734
|
+
rows: (x) => [plainRow("Nest", x.d.path)]
|
|
735
|
+
},
|
|
718
736
|
rejected: {
|
|
719
737
|
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
720
738
|
detailsHeader: "Document details",
|
|
@@ -1304,6 +1322,12 @@ async function markNotificationsRead(userEmail, ids) {
|
|
|
1304
1322
|
);
|
|
1305
1323
|
}
|
|
1306
1324
|
}
|
|
1325
|
+
async function markNotificationsReadBySubject(subjectId, kind) {
|
|
1326
|
+
await getDb().run(
|
|
1327
|
+
"UPDATE notifications SET read_at = ? WHERE subject_id = ? AND kind = ? AND read_at IS NULL",
|
|
1328
|
+
[(/* @__PURE__ */ new Date()).toISOString(), subjectId, kind]
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1307
1331
|
|
|
1308
1332
|
// src/governance/teams-service.ts
|
|
1309
1333
|
var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
|
|
@@ -1879,8 +1903,9 @@ async function resolveNestAccessRaw(nestId, userId) {
|
|
|
1879
1903
|
return { ...base, permission: best, isPublicReader: false };
|
|
1880
1904
|
}
|
|
1881
1905
|
if (base.isSteward) best = higher(best, "read");
|
|
1882
|
-
|
|
1883
|
-
|
|
1906
|
+
const visibilityRead = row.visibility === "public" || row.visibility === "org" && userId !== ANON_USER_ID;
|
|
1907
|
+
if (visibilityRead) best = higher(best, "read");
|
|
1908
|
+
const isPublicReader2 = visibilityRead && !row.collab_permission && !base.isSteward;
|
|
1884
1909
|
return { ...base, permission: best, isPublicReader: isPublicReader2 };
|
|
1885
1910
|
}
|
|
1886
1911
|
async function resolveNestPermission(nestId, userId) {
|
|
@@ -2194,22 +2219,23 @@ var LIVE_ONLY_N = "n.archived_at IS NULL";
|
|
|
2194
2219
|
async function listNests(userId, opts = {}) {
|
|
2195
2220
|
const db = getDb();
|
|
2196
2221
|
const scope = opts.archived ? "archived_at IS NOT NULL" : LIVE_ONLY;
|
|
2222
|
+
const orderBy = opts.archived ? "archived_at DESC" : "created_at DESC";
|
|
2197
2223
|
if (userId === ANON_USER_ID) {
|
|
2198
2224
|
return await db.all(
|
|
2199
|
-
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY
|
|
2225
|
+
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY ${orderBy}`,
|
|
2200
2226
|
[ANON_USER_ID]
|
|
2201
2227
|
);
|
|
2202
2228
|
}
|
|
2203
2229
|
const includeAnon = config.AUTH_MODE === "open" || await isLicenseAdminUserId(userId);
|
|
2204
2230
|
if (!includeAnon) {
|
|
2205
2231
|
return await db.all(
|
|
2206
|
-
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY
|
|
2232
|
+
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY ${orderBy}`,
|
|
2207
2233
|
[userId]
|
|
2208
2234
|
);
|
|
2209
2235
|
}
|
|
2210
2236
|
return await db.all(
|
|
2211
2237
|
`SELECT * FROM nests WHERE (user_id = ? OR user_id = ?) AND ${scope}
|
|
2212
|
-
ORDER BY
|
|
2238
|
+
ORDER BY ${orderBy}`,
|
|
2213
2239
|
[userId, ANON_USER_ID]
|
|
2214
2240
|
);
|
|
2215
2241
|
}
|
|
@@ -2309,11 +2335,13 @@ async function listSharedNests(userId) {
|
|
|
2309
2335
|
async function listVisibleNests(userId, opts = {}) {
|
|
2310
2336
|
const db = getDb();
|
|
2311
2337
|
const includeAnon = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
|
|
2338
|
+
const visibleByVisibility = userId === ANON_USER_ID ? ["public"] : ["public", "org"];
|
|
2312
2339
|
const params = [
|
|
2313
2340
|
userId,
|
|
2314
2341
|
userId,
|
|
2315
2342
|
...opts.readable ? [userId] : [],
|
|
2316
2343
|
userId,
|
|
2344
|
+
...opts.readable ? visibleByVisibility : [],
|
|
2317
2345
|
...includeAnon ? [ANON_USER_ID] : []
|
|
2318
2346
|
];
|
|
2319
2347
|
const rows = await db.all(
|
|
@@ -2329,7 +2357,7 @@ async function listVisibleNests(userId, opts = {}) {
|
|
|
2329
2357
|
AND (n.user_id = ?
|
|
2330
2358
|
OR nc.user_id IS NOT NULL
|
|
2331
2359
|
OR s.id IS NOT NULL
|
|
2332
|
-
${opts.readable ?
|
|
2360
|
+
${opts.readable ? `OR g.id IS NOT NULL OR n.visibility IN (${visibleByVisibility.map(() => "?").join(", ")})` : ""}
|
|
2333
2361
|
${includeAnon ? "OR n.user_id = ?" : ""})
|
|
2334
2362
|
ORDER BY n.created_at DESC`,
|
|
2335
2363
|
params
|
|
@@ -2345,9 +2373,10 @@ async function listVisibleNests(userId, opts = {}) {
|
|
|
2345
2373
|
}
|
|
2346
2374
|
async function listPublicNests(userId) {
|
|
2347
2375
|
const db = getDb();
|
|
2376
|
+
const visibilities = userId === ANON_USER_ID ? ["public"] : ["public", "org"];
|
|
2348
2377
|
return await db.all(
|
|
2349
2378
|
`SELECT n.* FROM nests n
|
|
2350
|
-
WHERE n.visibility
|
|
2379
|
+
WHERE n.visibility IN (${visibilities.map(() => "?").join(", ")})
|
|
2351
2380
|
AND ${LIVE_ONLY_N}
|
|
2352
2381
|
AND n.user_id != ?
|
|
2353
2382
|
AND NOT EXISTS (
|
|
@@ -2355,7 +2384,7 @@ async function listPublicNests(userId) {
|
|
|
2355
2384
|
WHERE nc.nest_id = n.id AND nc.user_id = ?
|
|
2356
2385
|
)
|
|
2357
2386
|
ORDER BY n.created_at DESC`,
|
|
2358
|
-
[userId, userId]
|
|
2387
|
+
[...visibilities, userId, userId]
|
|
2359
2388
|
);
|
|
2360
2389
|
}
|
|
2361
2390
|
async function getNest(nestId) {
|
|
@@ -2587,8 +2616,8 @@ async function ensureNodeIndex(nestId) {
|
|
|
2587
2616
|
return run;
|
|
2588
2617
|
}
|
|
2589
2618
|
async function rebuildNodeIndex(nestId) {
|
|
2590
|
-
const { engineCache: engineCache2 } = await import("./engine-
|
|
2591
|
-
const { documentsWithSuggestions } = await import("./external-edit-service-
|
|
2619
|
+
const { engineCache: engineCache2 } = await import("./engine-QFF2IA3G.js");
|
|
2620
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-F7D3VSA2.js");
|
|
2592
2621
|
const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
|
|
2593
2622
|
dropDiscoveryCache();
|
|
2594
2623
|
const startedAt = writeGeneration.get(nestId) ?? 0;
|
|
@@ -2822,6 +2851,8 @@ async function opContext(nestId, actor, onProgress) {
|
|
|
2822
2851
|
|
|
2823
2852
|
export {
|
|
2824
2853
|
nowExpr,
|
|
2854
|
+
suffixOf,
|
|
2855
|
+
nowMsExpr,
|
|
2825
2856
|
insertOrIgnore,
|
|
2826
2857
|
insertOrReplace,
|
|
2827
2858
|
trackEvent,
|
|
@@ -2910,6 +2941,7 @@ export {
|
|
|
2910
2941
|
notifyNestInvite,
|
|
2911
2942
|
listNotifications,
|
|
2912
2943
|
markNotificationsRead,
|
|
2944
|
+
markNotificationsReadBySubject,
|
|
2913
2945
|
createTeam,
|
|
2914
2946
|
findTeamByExternalId,
|
|
2915
2947
|
listTeamsForUser,
|
|
@@ -334,6 +334,28 @@ var config = {
|
|
|
334
334
|
get OIDC_DEPARTMENT_TAGGING() {
|
|
335
335
|
return process.env.OIDC_DEPARTMENT_TAGGING === "true";
|
|
336
336
|
},
|
|
337
|
+
/**
|
|
338
|
+
* Widen the add-a-person suggestions (GET /people/suggest) from "people you
|
|
339
|
+
* already work with" to every registered account on this server.
|
|
340
|
+
*
|
|
341
|
+
* OFF BY DEFAULT, and the default is the point. On a single-company
|
|
342
|
+
* self-hosted server every account is a colleague, so the whole directory is
|
|
343
|
+
* the useful answer and this saves asking someone for their address. On the
|
|
344
|
+
* PromptOwl-hosted deployment, one server holds many unrelated customers —
|
|
345
|
+
* so the same list is one customer's staff being offered another customer's
|
|
346
|
+
* email addresses. The hosted deployment leaves this unset; a self-hosted
|
|
347
|
+
* operator who knows their user table is their own staff turns it on.
|
|
348
|
+
*
|
|
349
|
+
* Narrows nothing when off: the default scoping (shared nests and teams,
|
|
350
|
+
* plus everything for a server admin, who administers the accounts anyway)
|
|
351
|
+
* is unchanged. Suggestions never gate the add either way — an address that
|
|
352
|
+
* appears in no list is still valid to type, so this only ever affects what
|
|
353
|
+
* is *offered*, never who can be invited. Superadmin-togglable at runtime
|
|
354
|
+
* via /admin/settings.
|
|
355
|
+
*/
|
|
356
|
+
get PEOPLE_SUGGEST_ALL_USERS() {
|
|
357
|
+
return process.env.PEOPLE_SUGGEST_ALL_USERS === "true";
|
|
358
|
+
},
|
|
337
359
|
/**
|
|
338
360
|
* Path to the .env file the server reads its config from and the license
|
|
339
361
|
* install flow persists PROMPTOWL_KEY into. Defaults UNDER DATA_ROOT (not
|
|
@@ -351,6 +373,19 @@ var config = {
|
|
|
351
373
|
get TELEMETRY_INTERVAL_MS() {
|
|
352
374
|
return parseInt(process.env.TELEMETRY_INTERVAL_MS || "3600000", 10);
|
|
353
375
|
},
|
|
376
|
+
/**
|
|
377
|
+
* PostHog product analytics. Empty key = off, which is the default: a
|
|
378
|
+
* self-hosted operator brings their own project. The key is a public client
|
|
379
|
+
* key by design (the browser sends it), so /health may hand it out — but
|
|
380
|
+
* only while TELEMETRY_ENABLED is on, so one switch stops all outbound
|
|
381
|
+
* usage data. Editable at runtime from the admin Settings page.
|
|
382
|
+
*/
|
|
383
|
+
get POSTHOG_KEY() {
|
|
384
|
+
return (process.env.POSTHOG_KEY || "").trim();
|
|
385
|
+
},
|
|
386
|
+
get POSTHOG_HOST() {
|
|
387
|
+
return (process.env.POSTHOG_HOST || "").trim() || "https://us.i.posthog.com";
|
|
388
|
+
},
|
|
354
389
|
/**
|
|
355
390
|
* Activity-trace retention window in days (the api_events table behind
|
|
356
391
|
* GET /admin/trace and GET /nests/:id/trace). 0 = keep forever (pruning is
|
|
@@ -626,7 +661,7 @@ function runMigrations(db) {
|
|
|
626
661
|
slug TEXT NOT NULL,
|
|
627
662
|
description TEXT,
|
|
628
663
|
visibility TEXT NOT NULL DEFAULT 'private'
|
|
629
|
-
CHECK(visibility IN ('private', 'public')),
|
|
664
|
+
CHECK(visibility IN ('private', 'org', 'public')),
|
|
630
665
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
631
666
|
UNIQUE(user_id, slug)
|
|
632
667
|
);
|
|
@@ -1702,6 +1737,83 @@ function runMigrations(db) {
|
|
|
1702
1737
|
})();
|
|
1703
1738
|
recordMigration("039_nest_pins");
|
|
1704
1739
|
}
|
|
1740
|
+
if (!hasMigration("041_nest_visibility_org")) {
|
|
1741
|
+
widenNestVisibilityWithGuard(db);
|
|
1742
|
+
recordMigration("041_nest_visibility_org");
|
|
1743
|
+
}
|
|
1744
|
+
if (!hasMigration("042_community_sites")) {
|
|
1745
|
+
db.transaction(() => {
|
|
1746
|
+
db.exec(`
|
|
1747
|
+
CREATE TABLE IF NOT EXISTS community_sites (
|
|
1748
|
+
id TEXT PRIMARY KEY,
|
|
1749
|
+
name TEXT NOT NULL,
|
|
1750
|
+
url TEXT NOT NULL UNIQUE,
|
|
1751
|
+
secret TEXT NOT NULL,
|
|
1752
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
1753
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1754
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1755
|
+
);
|
|
1756
|
+
`);
|
|
1757
|
+
recordMigration("042_community_sites");
|
|
1758
|
+
})();
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
function fkViolationCounts(db) {
|
|
1762
|
+
const rows = db.pragma("foreign_key_check");
|
|
1763
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1764
|
+
for (const r of rows) {
|
|
1765
|
+
const key = `${r.table}|${r.parent}|${r.fkid}`;
|
|
1766
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1767
|
+
}
|
|
1768
|
+
return counts;
|
|
1769
|
+
}
|
|
1770
|
+
function widenNestVisibilityWithGuard(db) {
|
|
1771
|
+
const before = fkViolationCounts(db);
|
|
1772
|
+
const fkWasOn = db.pragma("foreign_keys", { simple: true }) === 1;
|
|
1773
|
+
if (fkWasOn) db.pragma("foreign_keys = OFF");
|
|
1774
|
+
try {
|
|
1775
|
+
db.transaction(() => {
|
|
1776
|
+
widenNestVisibilityCheck(db);
|
|
1777
|
+
})();
|
|
1778
|
+
const after = fkViolationCounts(db);
|
|
1779
|
+
const introduced = [...after].filter(
|
|
1780
|
+
([key, n]) => n > (before.get(key) ?? 0)
|
|
1781
|
+
);
|
|
1782
|
+
if (introduced.length > 0) {
|
|
1783
|
+
throw new Error(
|
|
1784
|
+
`041_nest_visibility_org introduced foreign-key violations: ${introduced.map(([key]) => key).join(", ")}`
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
} finally {
|
|
1788
|
+
if (fkWasOn) db.pragma("foreign_keys = ON");
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
function widenNestVisibilityCheck(db) {
|
|
1792
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'nests'").get();
|
|
1793
|
+
const ddl = row?.sql;
|
|
1794
|
+
if (!ddl) return;
|
|
1795
|
+
const checkRe = /CHECK\s*\(\s*visibility\s+IN\s*\([^)]*\)\s*\)/i;
|
|
1796
|
+
const current = ddl.match(checkRe);
|
|
1797
|
+
if (!current) return;
|
|
1798
|
+
if (/'org'/i.test(current[0])) return;
|
|
1799
|
+
const newDdl = ddl.replace(
|
|
1800
|
+
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`\[]?nests["'`\]]?/i,
|
|
1801
|
+
"CREATE TABLE nests_new"
|
|
1802
|
+
).replace(checkRe, "CHECK(visibility IN ('private', 'org', 'public'))");
|
|
1803
|
+
if (!newDdl.startsWith("CREATE TABLE nests_new")) {
|
|
1804
|
+
throw new Error("041: could not rewrite the nests CREATE TABLE statement");
|
|
1805
|
+
}
|
|
1806
|
+
const cols = db.prepare("PRAGMA table_info(nests)").all().map((c) => `"${c.name}"`).join(", ");
|
|
1807
|
+
const indexes = db.prepare(
|
|
1808
|
+
"SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'nests' AND sql IS NOT NULL"
|
|
1809
|
+
).all().map((r) => `${r.sql};`);
|
|
1810
|
+
db.exec(`
|
|
1811
|
+
${newDdl};
|
|
1812
|
+
INSERT INTO nests_new (${cols}) SELECT ${cols} FROM nests;
|
|
1813
|
+
DROP TABLE nests;
|
|
1814
|
+
ALTER TABLE nests_new RENAME TO nests;
|
|
1815
|
+
${indexes.join("\n ")}
|
|
1816
|
+
`);
|
|
1705
1817
|
}
|
|
1706
1818
|
function mergeCaseCollidingUsers(db) {
|
|
1707
1819
|
const groups = db.prepare(
|
|
@@ -1919,7 +2031,7 @@ async function initDb() {
|
|
|
1919
2031
|
if (config.DB_DRIVER === "postgres") {
|
|
1920
2032
|
const { Pool } = await import("pg");
|
|
1921
2033
|
const { PostgresAdapter } = await import("./adapter.postgres-6VZCMPQL.js");
|
|
1922
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
2034
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-APCVSYUE.js");
|
|
1923
2035
|
const pool = new Pool(buildPgConfig());
|
|
1924
2036
|
adapter = new PostgresAdapter(pool);
|
|
1925
2037
|
await runPostgresMigrations(adapter);
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
markIndexStale,
|
|
6
6
|
resolveNestPath,
|
|
7
7
|
setSuggestionFlag
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QLXC6542.js";
|
|
9
9
|
import {
|
|
10
10
|
getDb
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QWNXWRWO.js";
|
|
12
12
|
|
|
13
13
|
// src/governance/external-edit-service.ts
|
|
14
14
|
import { readFile, readdir } from "fs/promises";
|
|
@@ -82,7 +82,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
82
82
|
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
-
const { hasUnsealedDraft } = await import("./version-service-
|
|
85
|
+
const { hasUnsealedDraft } = await import("./version-service-RJ6CWZ3O.js");
|
|
86
86
|
if (await hasUnsealedDraft(nestId, documentId)) {
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
@@ -259,7 +259,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
|
|
|
259
259
|
async function mirrorVersion(input) {
|
|
260
260
|
const { storage } = await engineCache.get(input.nestId);
|
|
261
261
|
const node = await storage.readDocument(input.documentId);
|
|
262
|
-
const { upsertVersion, setApprovedVersion } = await import("./version-service-
|
|
262
|
+
const { upsertVersion, setApprovedVersion } = await import("./version-service-RJ6CWZ3O.js");
|
|
263
263
|
await upsertVersion({
|
|
264
264
|
nestId: input.nestId,
|
|
265
265
|
nodeId: input.documentId,
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
engineApi,
|
|
3
3
|
engineCache,
|
|
4
4
|
opContext
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-QLXC6542.js";
|
|
6
6
|
import "./chunk-YVMSM7LS.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-QWNXWRWO.js";
|
|
8
8
|
import "./chunk-YB3LKF7U.js";
|
|
9
9
|
import "./chunk-FRQJWGN3.js";
|
|
10
10
|
export {
|