@promptowl/contextnest-community 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +34 -1
- package/CONFIGURATION.md +32 -1
- package/dist/{chunk-TRSGPS27.js → chunk-H3W72HN3.js} +28 -0
- package/dist/{chunk-DGPTJXIV.js → chunk-MB52DWWP.js} +4 -4
- package/dist/{chunk-IEAWRHMV.js → chunk-NFFUZF2B.js} +23 -2
- package/dist/{chunk-PICUHQEM.js → chunk-O3JEI33N.js} +2 -2
- package/dist/{chunk-SWA5M5XX.js → chunk-QMQDWRHN.js} +128 -22
- package/dist/{chunk-I5EDEZBQ.js → chunk-RXCVXG34.js} +1 -1
- package/dist/{chunk-2QOJE7A5.js → chunk-SE5U4J4T.js} +4 -4
- package/dist/{chunk-RPGFHESR.js → chunk-VHEMGQ3I.js} +1 -1
- package/dist/{chunk-M6PGYMBD.js → chunk-VRLAP4HB.js} +33 -15
- package/dist/{engine-QNMFDIBV.js → engine-HLYQLP5T.js} +3 -3
- package/dist/{external-edit-service-4MEGPT4D.js → external-edit-service-3DFQ4EZZ.js} +4 -4
- package/dist/{grants-service-24GLUBY4.js → grants-service-WICV4Y2J.js} +2 -2
- package/dist/index.js +804 -304
- package/dist/{invited-user-AM6A3WGO.js → invited-user-XEBX6BM4.js} +3 -3
- package/dist/{review-service-2EWN7WWO.js → review-service-GB43UQVH.js} +7 -7
- package/dist/{stewardship-service-TDZUC2Q6.js → stewardship-service-KDBPWRZI.js} +4 -4
- package/dist/{version-service-C3JTZLJH.js → version-service-JYHFPSYP.js} +8 -4
- package/dist/web3/assets/{ActivityTracePage-DzwWIjjx.js → ActivityTracePage-CkRoyIc-.js} +1 -1
- package/dist/web3/assets/AgentDocsPage-BTRXmliv.js +1 -0
- package/dist/web3/assets/CollaboratorManager-CqJIkvs-.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-JcO1P-qm.js +1 -0
- package/dist/web3/assets/DocumentEditor-D1GLeeEP.js +36 -0
- package/dist/web3/assets/DocumentsTab-BAgSxTX1.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-DuuTXZap.js +1 -0
- package/dist/web3/assets/{MarkdownEditor-CKh4WSn-.js → MarkdownEditor-BELFww9q.js} +116 -126
- package/dist/web3/assets/{NestPageHeader-Cizo8nUi.js → NestPageHeader-Bbje2ncY.js} +1 -1
- package/dist/web3/assets/NestView-D4OMXPQ7.js +73 -0
- package/dist/web3/assets/OverviewTab-FIDGjR3Q.js +6 -0
- package/dist/web3/assets/{PersonCombobox-BKI7dff4.js → PersonCombobox-D52m_jhc.js} +1 -1
- package/dist/web3/assets/{ReviewActions-CS8JXgXF.js → ReviewActions-DYqBcWlv.js} +2 -2
- package/dist/web3/assets/ReviewTab-DoxfygOb.js +1 -0
- package/dist/web3/assets/StewardsTab-CFsYjbEn.js +1 -0
- package/dist/web3/assets/{SubmitForReviewModal-Biq3m79i.js → SubmitForReviewModal-AW2OLNAE.js} +1 -1
- package/dist/web3/assets/{alert-dialog-BvF3NFr7.js → alert-dialog-p-fBLSWx.js} +2 -2
- package/dist/web3/assets/{arrow-left-CWIsv-sG.js → arrow-left-CiErCCTG.js} +1 -1
- package/dist/web3/assets/{backlinks-Cqcn7n1J.js → backlinks-DaVSYvhV.js} +2 -2
- package/dist/web3/assets/{boxes-ClbgQqG2.js → boxes-VWrCCMzW.js} +1 -1
- package/dist/web3/assets/{chevron-left-CK_ZPFm9.js → chevron-left-CsD8gZwL.js} +1 -1
- package/dist/web3/assets/{circle-check-Bw_aTXiH.js → circle-check-BBCXd9LI.js} +1 -1
- package/dist/web3/assets/{circle-x-DkIG68H7.js → circle-x-C9LsnORQ.js} +1 -1
- package/dist/web3/assets/{code-xml-BlR7EN6l.js → code-xml-BaoV7HiQ.js} +1 -1
- package/dist/web3/assets/{corner-down-right-DSVxK3tf.js → corner-down-right-DgiTXrtw.js} +1 -1
- package/dist/web3/assets/{count-skeleton-DToLM74B.js → count-skeleton-CnEE4uF-.js} +1 -1
- package/dist/web3/assets/{earth-BXhVPvCR.js → earth-BmMbVt9k.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-BevGZgtj.js → file-exclamation-point-qJeca9-A.js} +1 -1
- package/dist/web3/assets/{folder-input-m8UBV4fA.js → folder-input-D7R93LJ6.js} +1 -1
- package/dist/web3/assets/index-DNmWTxN7.css +1 -0
- package/dist/web3/assets/index-DvuUIUP6.js +451 -0
- package/dist/web3/assets/{page-DzKwP5Ds.js → page-BJQuaTRW.js} +2 -2
- package/dist/web3/assets/{page-B6pkc9cd.js → page-Bb8xGOPQ.js} +2 -2
- package/dist/web3/assets/{page-Deq6IKiN.js → page-BtM-qDzr.js} +2 -2
- package/dist/web3/assets/{page-DPY01SOj.js → page-Bxd0EG25.js} +1 -1
- package/dist/web3/assets/page-C3QCO6IR.js +1 -0
- package/dist/web3/assets/{page-CptuAPJc.js → page-CfoR0khy.js} +2 -2
- package/dist/web3/assets/page-D7Zr5tAI.js +1 -0
- package/dist/web3/assets/{page-Dwbp4Seu.js → page-DATSlHCW.js} +2 -2
- package/dist/web3/assets/page-DTjPQizZ.js +45 -0
- package/dist/web3/assets/{page-Bgy9Ng9p.js → page-DspptlWr.js} +1 -1
- package/dist/web3/assets/{page-Dh57L0RG.js → page-I6vAFJOj.js} +4 -4
- package/dist/web3/assets/page-QxI4Ud2s.js +9 -0
- package/dist/web3/assets/{page-BJrFCKXB.js → page-YcNNg-dX.js} +1 -1
- package/dist/web3/assets/page-kM7eO5Kj.js +1 -0
- package/dist/web3/assets/{page-title-DVq4qvMR.js → page-title-Dn5jANiU.js} +1 -1
- package/dist/web3/assets/{play-CXhS6Pgq.js → play-B2k_Ix-L.js} +1 -1
- package/dist/web3/assets/{send-DdfwnIrj.js → send-DPNxv3As.js} +1 -1
- package/dist/web3/assets/{settings-DKX0Q4gO.js → settings-DZjq-OQA.js} +1 -1
- package/dist/web3/assets/{share-2-CxmXRt58.js → share-2-C-OnE2ru.js} +1 -1
- package/dist/web3/assets/{tag-DeYC_mRv.js → tag-CwJl7FdI.js} +1 -1
- package/dist/web3/assets/{trash-2-B0HyPTnI.js → trash-2-aljmIff4.js} +1 -1
- package/dist/web3/assets/{user-plus-Ckm_Isyc.js → user-plus-DPerZexX.js} +1 -1
- package/dist/web3/assets/{zap-D2ifcnYb.js → zap-CHZtiN7P.js} +1 -1
- package/dist/web3/index.html +2 -2
- package/package.json +6 -2
- package/dist/web3/assets/AgentDocsPage-ixP2UdAE.js +0 -1
- package/dist/web3/assets/CollaboratorManager-C9VAXZ0h.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-tq4ZTexH.js +0 -1
- package/dist/web3/assets/DocumentEditor-C2CP2fiE.js +0 -36
- package/dist/web3/assets/DocumentsTab-Mh7KxUoB.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-B_CWKXZ_.js +0 -1
- package/dist/web3/assets/NestView-EW-sraEb.js +0 -73
- package/dist/web3/assets/OverviewTab-CjidsMX_.js +0 -6
- package/dist/web3/assets/ReviewTab-CwVrXzw5.js +0 -1
- package/dist/web3/assets/StewardsTab-C-iP6gnh.js +0 -1
- package/dist/web3/assets/index-BOVYgMvj.js +0 -29
- package/dist/web3/assets/index-DVnaYqj9.css +0 -1
- package/dist/web3/assets/index-WYDmEOfO.js +0 -404
- package/dist/web3/assets/page-C2zoxumw.js +0 -1
- package/dist/web3/assets/page-CJWO06dN.js +0 -1
- package/dist/web3/assets/page-Dea2SblF.js +0 -9
- package/dist/web3/assets/page-Dfjg8p7Z.js +0 -1
- package/dist/web3/assets/page-YxjrE5b5.js +0 -45
- package/dist/web3/assets/x-mNtADYHc.js +0 -6
package/API.md
CHANGED
|
@@ -1592,7 +1592,7 @@ Body (give `prompt` OR `selector`; everything else optional):
|
|
|
1592
1592
|
}
|
|
1593
1593
|
```
|
|
1594
1594
|
- `prompt` — natural language. Tokens are matched against tag names and titles, and every document is scored against them (title hits weigh most, then tags, then body words); documents under half the top score are dropped as noise. A prompt that matches nothing returns `nodes: []` — never the whole nest. `trace.compiled.fallback` is `"fulltext"` when body scoring alone produced the answer.
|
|
1595
|
-
- `selector` — explicit selector grammar (e.g. `#auth`). Wins over `prompt`.
|
|
1595
|
+
- `selector` — explicit selector grammar (e.g. `#auth`). Wins over `prompt`. May carry `[[Title]]` parts in an OR union (`#auth | [[Auth Overview]]`), and `[[nodes/auth-overview]]` names one document by id where a title may match several.
|
|
1596
1596
|
- `hops` — link expansion depth (`[[wikilinks]]` and `contextnest://` links, both directions). Defaults to `0` for a `prompt` (neighbours of a keyword match are not answers to the question) and `2` for a `selector`.
|
|
1597
1597
|
- `max_tokens` — budget, floored at 50, default 4000. A node that doesn't fit is skipped and the next tried (`trace.truncated_by_budget` counts the skips); only when nothing fits does the first node go through oversized.
|
|
1598
1598
|
- 400 `prompt or selector is required` if both missing.
|
|
@@ -1708,6 +1708,39 @@ Deterministic fact lookup against a **table** node (CSV body): fetch the exact r
|
|
|
1708
1708
|
```
|
|
1709
1709
|
Same operation over MCP: `context_table_query`.
|
|
1710
1710
|
|
|
1711
|
+
### POST `/nests/:nestId/chat`
|
|
1712
|
+
Hootie in the nest — ask a question about the nest and get a streamed, cited answer. Read tier, like `/context`: retrieval runs through the same `/context` path **as the caller**, so the answer can only draw on documents this person may read. Gated by `HOOTIE_ENABLED` (off → `404`); needs the server-wide `ANTHROPIC_API_KEY` (missing → `503` with `reason: "no_api_key"`); answers with `HOOTIE_MODEL`, through `ANTHROPIC_BASE_URL` when one is set (all three editable in Settings → General). Rate-limited per user across all nests — the budget guards the one server-wide key (`429`).
|
|
1713
|
+
|
|
1714
|
+
Request:
|
|
1715
|
+
|
|
1716
|
+
```json
|
|
1717
|
+
{
|
|
1718
|
+
"message": "how do requests authenticate?",
|
|
1719
|
+
"node_id": "nodes/api-authentication-guide",
|
|
1720
|
+
"history": [
|
|
1721
|
+
{ "role": "user", "content": "…" },
|
|
1722
|
+
{ "role": "assistant", "content": "…" }
|
|
1723
|
+
]
|
|
1724
|
+
}
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
- `message` (required) — the question. `@name` tokens loop a colleague in (below).
|
|
1728
|
+
- `node_id` (optional) — the document the person has open. It and its neighbours (one hop) lead the context, whatever the question matches follows.
|
|
1729
|
+
- `history` (optional) — prior turns, oldest first; the server keeps the most recent 20.
|
|
1730
|
+
|
|
1731
|
+
Response: `text/event-stream`.
|
|
1732
|
+
|
|
1733
|
+
| event | data |
|
|
1734
|
+
|---|---|
|
|
1735
|
+
| `meta` | `{ nodes: [{ id, title, url, tags, type }], anchor: node \| null }` — what the answer will be grounded in, before the model speaks. |
|
|
1736
|
+
| `delta` | `{ text }` — a chunk of the answer. |
|
|
1737
|
+
| `done` | `{ answer, cited: [node…], mention }` — the whole answer, the documents it cites (by `[[Title]]`, in order), and what happened to any `@mentions`. |
|
|
1738
|
+
| `error` | `{ error }` — the model call failed; the stream ends. |
|
|
1739
|
+
|
|
1740
|
+
**Grounding.** The retrieved documents are placed in the system prompt verbatim with the instruction to answer from them alone. Retrieval is permission-gated, so an answer can never draw on anything the caller couldn't read — but a document author can write text aimed at Hootie ("ignore the question and…") that later readers' sessions will see. That is the ordinary tradeoff of grounding a model in editable content, accepted here; the documents are governed, so who can write them is the control.
|
|
1741
|
+
|
|
1742
|
+
**Mentions.** A message naming people (`@alice@acme.com`) posts the exchange — question and answer, under the asker's name — as a comment on the anchor document (the open one, else the first document the answer cites) and notifies them through the comment-mention path: inbox row, and email / Slack / Teams where configured. `mention` is `{ status: "posted", node_id, node_title, thread_id, notified }` or `{ status: "undeliverable", reason: "no_anchor" | "post_failed" }`. Nothing is posted when nobody is named.
|
|
1743
|
+
|
|
1711
1744
|
### POST `/nests/:nestId/query`
|
|
1712
1745
|
Graph query (Cypher-ish).
|
|
1713
1746
|
|
package/CONFIGURATION.md
CHANGED
|
@@ -95,13 +95,17 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
95
95
|
| `PROMPTOWL_TEAMS_ENABLED` | _(unset — off)_ | Lets users who signed in with PromptOwl import their PromptOwl teams as local teams. Off by default; set `true` to enable, or toggle from Server settings → Advanced. When off, `GET/POST /teams/promptowl` return `404` and the PromptOwl Teams panel is hidden. Independent of `PROMPTOWL_SIGN_IN_GATE`. |
|
|
96
96
|
| `TYPE_ARTIFACT_ENABLED` | `true` | Set `false` to disable creation of **artifact** nodes server-wide (existing artifact nodes stay readable — never data loss). Runnable types (agent/skill/tool) are gated by `FEATURE_WORKFLOW_PLANE`, not here. Also editable from Settings (`/admin/settings`). |
|
|
97
97
|
| `TYPE_TABLE_ENABLED` | `true` | Same as above for **table** nodes. |
|
|
98
|
+
| `HOOTIE_ENABLED` | `true` | Hootie in the nest — the assistant on every page of the UI and its `POST /nests/:id/chat` route. Set `false` to switch it off (the launcher disappears and the route answers `404`). Also toggleable from Settings → General. Answering needs `ANTHROPIC_API_KEY`; without one the feature stays on but the panel explains what's missing and the route answers `503`. Questions are budgeted per user across all nests (30 per 5 minutes); in `AUTH_MODE=open` every caller is the one anonymous user, so the budget is shared by everyone on that server. |
|
|
99
|
+
| `HOOTIE_MODEL` | `claude-opus-5` | The model Hootie answers with. Also editable from Settings → General. Through a gateway, use the gateway's name for it (`ollama/llama3.2`, `gpt-5`, …). |
|
|
100
|
+
| `ANTHROPIC_BASE_URL` | _(unset — Anthropic directly)_ | A LiteLLM or PromptOwl gateway speaking `/v1/messages`. When set, **every** model call this server makes goes through it — Hootie, and agent runs via the run bundle's env floor — with `ANTHROPIC_API_KEY` as the gateway's virtual key: budgets, spend logs, and any model the gateway fronts. Also editable from Settings → General. |
|
|
98
101
|
| `FEATURE_WORKFLOW_PLANE` | _(unset — off)_ | Enables the workflow plane: typed edges, edge-type registry, governed runs. Optional feature; also toggleable from Settings. |
|
|
99
102
|
| `FEATURE_SUBAGENT_RUNS` | _(unset — off)_ | Lets runs spawn nested sub-agent runs (recursion). Gated separately from the plane; requires `FEATURE_WORKFLOW_PLANE`. Also toggleable from Server settings → Advanced (turning the plane off forces this off too). Opens a recursion surface — enable only after reviewing the depth/fan-out caps. |
|
|
100
103
|
| `SUBAGENT_MAX_DEPTH` | `8` | Max sub-agent nesting depth (clamped 1..32) — bounds the call tree's HEIGHT so it stays finite/haltable. |
|
|
101
104
|
| `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. |
|
|
102
105
|
| `RUN_MAX_STEPS` | `10000` | Max steps a single run may accumulate (clamped 10..100000) — bounds a runaway/hostile runner's step log. |
|
|
103
106
|
| `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. |
|
|
104
|
-
| `ANTHROPIC_API_KEY` | _(unset)_ | Server-wide default runner key for workflow-plane agent runs. Never returned by the API — the health endpoint reports presence only, and Settings shows a masked tail. |
|
|
107
|
+
| `ANTHROPIC_API_KEY` | _(unset)_ | Server-wide default runner key for workflow-plane agent runs, and the key Hootie answers with. Never returned by the API — the health endpoint reports presence only, and Settings shows a masked tail. |
|
|
108
|
+
| `SECRETS_KEY` | _(unset — a key is generated and stored in the database)_ | Master key for the credentials this server stores encrypted (the secret settings rows and every nest Tool-environment value). Any string; it is hashed to 32 bytes. Optional, and stronger than the default when your deployment has a secret manager: the key then lives outside the database it protects. Values already written under the generated key cannot be read after you set it. See [Runtime settings persistence](#runtime-settings-persistence). |
|
|
105
109
|
| `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. |
|
|
106
110
|
| `MSTEAMS_WEBHOOK_URL` | _(unset — connector off)_ | Microsoft Teams incoming-webhook URL for the same governance events, posted as Adaptive Cards. `https://` only — the URL embeds a secret. Also editable from Settings in the UI. See [Microsoft Teams notifications](#microsoft-teams-notifications). |
|
|
107
111
|
| `EMAIL_PROVIDER` | `smtp` | Email transport: `smtp` (uses `SMTP_URL`) or `ses` (AWS SES through the AWS SDK, for servers that can't use SMTP). Also editable from Settings. |
|
|
@@ -875,6 +879,33 @@ to keys you don't set in the environment stay put. Clearing a setting in the UI
|
|
|
875
879
|
writes a tombstone, so a value you removed is not resurrected from the environment
|
|
876
880
|
on the next boot.
|
|
877
881
|
|
|
882
|
+
**Credentials are encrypted at rest.** The secret rows — `ANTHROPIC_API_KEY`,
|
|
883
|
+
`OIDC_CLIENT_SECRET`, `MCP_SIGNING_SECRET`, `SMTP_URL`, `SLACK_WEBHOOK_URL`,
|
|
884
|
+
`MSTEAMS_WEBHOOK_URL`, `SES_SECRET_ACCESS_KEY`, `PROMPTOWL_KEY` — and every value in a
|
|
885
|
+
nest's Tool environment (`nest_env`) are stored AES-256-GCM encrypted, so a row read in
|
|
886
|
+
isolation is not the credential. Non-secret settings stay readable: what a server is
|
|
887
|
+
configured to do is worth being able to see. One known exception: the license
|
|
888
|
+
validation cache (`license_cache`) is still keyed by the license key itself.
|
|
889
|
+
|
|
890
|
+
The master key needs nothing configured: on first use the server generates one and
|
|
891
|
+
stores it in the database (`server_settings`, key `SECRETS_MASTER_KEY`), so it survives
|
|
892
|
+
redeploys, is shared by every instance, and travels with your database backups. That row
|
|
893
|
+
is not a setting — it is never loaded into the process environment and never returned by
|
|
894
|
+
the settings API.
|
|
895
|
+
|
|
896
|
+
Set **`SECRETS_KEY`** in the deploy environment to supply the key yourself instead; it
|
|
897
|
+
takes precedence over the stored one, and any string works (it is hashed to 32 bytes).
|
|
898
|
+
This is the stronger arrangement, because the key then lives somewhere the database is
|
|
899
|
+
not: a database dump alone cannot open the values. With the stored key, a dump that
|
|
900
|
+
includes that row includes the key — the encryption protects against a value being read
|
|
901
|
+
or logged incidentally, not against an attacker holding the whole database. If you set
|
|
902
|
+
`SECRETS_KEY` on a server that already wrote values under the stored key, it cannot read
|
|
903
|
+
them: the server still boots, logs which settings it could not open, and treats each as
|
|
904
|
+
unset so an admin can re-enter it.
|
|
905
|
+
|
|
906
|
+
Values written before any of this existed are read back as plaintext and re-encrypted the
|
|
907
|
+
next time they are saved — nothing to migrate.
|
|
908
|
+
|
|
878
909
|
### Cloud Run + Cloud SQL (PostgreSQL)
|
|
879
910
|
|
|
880
911
|
Attach the Cloud SQL instance to the service (`--add-cloudsql-instances`) so the
|
|
@@ -501,6 +501,34 @@ var config = {
|
|
|
501
501
|
get TYPE_TABLE_ENABLED() {
|
|
502
502
|
return process.env.TYPE_TABLE_ENABLED !== "false";
|
|
503
503
|
},
|
|
504
|
+
/**
|
|
505
|
+
* Hootie in the nest: the floating assistant on every page of the UI and
|
|
506
|
+
* its POST /nests/:id/chat route (default ON). Superadmin-editable at
|
|
507
|
+
* runtime via /admin/settings (hootie_enabled). Off → the route 404s and
|
|
508
|
+
* the launcher is hidden. Answering additionally needs the server-wide
|
|
509
|
+
* ANTHROPIC_API_KEY; without one the route answers 503, not 404 — the
|
|
510
|
+
* feature is on, it just isn't configured yet.
|
|
511
|
+
*/
|
|
512
|
+
get HOOTIE_ENABLED() {
|
|
513
|
+
return process.env.HOOTIE_ENABLED !== "false";
|
|
514
|
+
},
|
|
515
|
+
/** Model Hootie answers with. Superadmin-editable at runtime via
|
|
516
|
+
* /admin/settings (hootie_model). A gateway-side name (`ollama/…`,
|
|
517
|
+
* `gpt-…`) is fine when ANTHROPIC_BASE_URL points at one. */
|
|
518
|
+
get HOOTIE_MODEL() {
|
|
519
|
+
return process.env.HOOTIE_MODEL || "claude-opus-5";
|
|
520
|
+
},
|
|
521
|
+
/**
|
|
522
|
+
* Where the Anthropic-shaped model calls go. Empty = Anthropic directly.
|
|
523
|
+
* Set to a LiteLLM (or PromptOwl) gateway to route Hootie AND agent runs
|
|
524
|
+
* through it — one setting: the runner honours the same variable via the
|
|
525
|
+
* run bundle's env floor. Superadmin-editable (anthropic_base_url).
|
|
526
|
+
* Never a per-user vendor key: that is the shadow-AI hole the gateway
|
|
527
|
+
* exists to close.
|
|
528
|
+
*/
|
|
529
|
+
get ANTHROPIC_BASE_URL() {
|
|
530
|
+
return (process.env.ANTHROPIC_BASE_URL || "").trim().replace(/\/$/, "");
|
|
531
|
+
},
|
|
504
532
|
/**
|
|
505
533
|
* Sub-agent runs: lets a run spawn nested runs (an external runner calling
|
|
506
534
|
* back into POST /run for a child agent). Gated SEPARATELY from the base
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveNestAccess,
|
|
8
8
|
resolveNestPermission,
|
|
9
9
|
resolveTeamRolesForUser
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QMQDWRHN.js";
|
|
11
11
|
import {
|
|
12
12
|
ConflictError,
|
|
13
13
|
ValidationError
|
|
@@ -19,12 +19,12 @@ import {
|
|
|
19
19
|
collabPermToRole,
|
|
20
20
|
primaryRole,
|
|
21
21
|
sendEmailToRecipient
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-RXCVXG34.js";
|
|
23
23
|
import {
|
|
24
24
|
config,
|
|
25
25
|
getDb,
|
|
26
26
|
isEmailish
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-H3W72HN3.js";
|
|
28
28
|
|
|
29
29
|
// src/governance/stewardship-service.ts
|
|
30
30
|
import { v4 as uuid } from "uuid";
|
|
@@ -233,7 +233,7 @@ async function createStewardRecord(params) {
|
|
|
233
233
|
);
|
|
234
234
|
let userId = userRow?.id;
|
|
235
235
|
if (!userId) {
|
|
236
|
-
const { findOrCreateInvitedUser } = await import("./invited-user-
|
|
236
|
+
const { findOrCreateInvitedUser } = await import("./invited-user-XEBX6BM4.js");
|
|
237
237
|
userId = (await findOrCreateInvitedUser(email)).id;
|
|
238
238
|
}
|
|
239
239
|
const created = await assignSteward({
|
|
@@ -2,13 +2,13 @@ import {
|
|
|
2
2
|
buildTitleMap,
|
|
3
3
|
insertOrReplace,
|
|
4
4
|
nowExpr
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-QMQDWRHN.js";
|
|
6
6
|
import {
|
|
7
7
|
ValidationError
|
|
8
8
|
} from "./chunk-LHWOJPFE.js";
|
|
9
9
|
import {
|
|
10
10
|
getDb
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-H3W72HN3.js";
|
|
12
12
|
|
|
13
13
|
// src/governance/version-service.ts
|
|
14
14
|
import { createHash } from "crypto";
|
|
@@ -215,6 +215,25 @@ async function setApprovedVersion(nestId, nodeId, version, approvedBy) {
|
|
|
215
215
|
);
|
|
216
216
|
await db.run(sql, [nestId, nodeId, version, approvedBy]);
|
|
217
217
|
}
|
|
218
|
+
var APPROVED_HEAL_AUTHOR = "system:approved-heal";
|
|
219
|
+
async function moveApprovedVersion(nestId, nodeId, from, to) {
|
|
220
|
+
const db = getDb();
|
|
221
|
+
const moved = await db.run(
|
|
222
|
+
`UPDATE approved_versions
|
|
223
|
+
SET approved_version = ?, approved_by = ?, approved_at = ${nowExpr(db)}
|
|
224
|
+
WHERE nest_id = ? AND node_id = ? AND approved_version = ?`,
|
|
225
|
+
[to, APPROVED_HEAL_AUTHOR, nestId, nodeId, from]
|
|
226
|
+
);
|
|
227
|
+
if (moved.changes === 0) return false;
|
|
228
|
+
const note = `Approved version moved from v${from} to v${to} \u2014 v${from} could not be rebuilt`;
|
|
229
|
+
await db.run(
|
|
230
|
+
`UPDATE node_versions
|
|
231
|
+
SET change_note = CASE WHEN change_note IS NULL OR change_note = '' THEN ? ELSE change_note || ' \xB7 ' || ? END
|
|
232
|
+
WHERE nest_id = ? AND node_id = ? AND version = ?`,
|
|
233
|
+
[note, note, nestId, nodeId, to]
|
|
234
|
+
);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
218
237
|
async function getSealedVersion(nestId, nodeId) {
|
|
219
238
|
const approved = await getApprovedVersion(nestId, nodeId);
|
|
220
239
|
if (approved != null) return approved;
|
|
@@ -458,6 +477,8 @@ export {
|
|
|
458
477
|
getApprovedVersion,
|
|
459
478
|
getApprovedVersions,
|
|
460
479
|
setApprovedVersion,
|
|
480
|
+
APPROVED_HEAL_AUTHOR,
|
|
481
|
+
moveApprovedVersion,
|
|
461
482
|
getSealedVersion,
|
|
462
483
|
deleteSupersededDrafts,
|
|
463
484
|
deleteUnsealedVersions,
|
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
permissionLabel,
|
|
9
9
|
sendEmailToRecipient,
|
|
10
10
|
teamRoleToPermission
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RXCVXG34.js";
|
|
12
12
|
import {
|
|
13
13
|
config,
|
|
14
14
|
getDb,
|
|
15
15
|
isEmailish
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-H3W72HN3.js";
|
|
17
17
|
import {
|
|
18
18
|
ANON_USER_ID
|
|
19
19
|
} from "./chunk-YB3LKF7U.js";
|
|
@@ -156,6 +156,102 @@ function insertOrReplace(db, insertSql, conflictCols, set) {
|
|
|
156
156
|
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// src/shared/secret-box.ts
|
|
160
|
+
import {
|
|
161
|
+
createCipheriv,
|
|
162
|
+
createDecipheriv,
|
|
163
|
+
createHash as createHash2,
|
|
164
|
+
randomBytes
|
|
165
|
+
} from "crypto";
|
|
166
|
+
var PREFIX = "enc.v1.";
|
|
167
|
+
var b64 = (b) => b.toString("base64url");
|
|
168
|
+
var MASTER_KEY_SETTING = "SECRETS_MASTER_KEY";
|
|
169
|
+
var SECRET_SETTINGS = /* @__PURE__ */ new Set([
|
|
170
|
+
"ANTHROPIC_API_KEY",
|
|
171
|
+
"OIDC_CLIENT_SECRET",
|
|
172
|
+
"MCP_SIGNING_SECRET",
|
|
173
|
+
"SMTP_URL",
|
|
174
|
+
"SLACK_WEBHOOK_URL",
|
|
175
|
+
"MSTEAMS_WEBHOOK_URL",
|
|
176
|
+
"SES_SECRET_ACCESS_KEY",
|
|
177
|
+
"PROMPTOWL_KEY"
|
|
178
|
+
]);
|
|
179
|
+
function isSecretSetting(name) {
|
|
180
|
+
return SECRET_SETTINGS.has(name);
|
|
181
|
+
}
|
|
182
|
+
function isEncrypted(value) {
|
|
183
|
+
return value.startsWith(PREFIX);
|
|
184
|
+
}
|
|
185
|
+
async function encryptSecret(plain) {
|
|
186
|
+
if (isEncrypted(plain)) return plain;
|
|
187
|
+
const iv = randomBytes(12);
|
|
188
|
+
const cipher = createCipheriv("aes-256-gcm", await masterKey(), iv);
|
|
189
|
+
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
190
|
+
return PREFIX + [b64(iv), b64(cipher.getAuthTag()), b64(ct)].join(".");
|
|
191
|
+
}
|
|
192
|
+
async function decryptSecret(stored) {
|
|
193
|
+
if (!isEncrypted(stored)) return stored;
|
|
194
|
+
const [iv, tag, ct] = stored.slice(PREFIX.length).split(".");
|
|
195
|
+
if (!iv || !tag || !ct) throw new Error("malformed encrypted value");
|
|
196
|
+
const decipher = createDecipheriv(
|
|
197
|
+
"aes-256-gcm",
|
|
198
|
+
await masterKey(),
|
|
199
|
+
Buffer.from(iv, "base64url")
|
|
200
|
+
);
|
|
201
|
+
decipher.setAuthTag(Buffer.from(tag, "base64url"));
|
|
202
|
+
return Buffer.concat([
|
|
203
|
+
decipher.update(Buffer.from(ct, "base64url")),
|
|
204
|
+
decipher.final()
|
|
205
|
+
]).toString("utf8");
|
|
206
|
+
}
|
|
207
|
+
async function tryDecryptSecret(stored, label) {
|
|
208
|
+
try {
|
|
209
|
+
return await decryptSecret(stored);
|
|
210
|
+
} catch {
|
|
211
|
+
console.error(
|
|
212
|
+
`[secrets] cannot decrypt ${label} \u2014 SECRETS_KEY does not match the key this value was written with. Unset it to fall back to the stored key, or re-enter the value to replace it.`
|
|
213
|
+
);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
var cached = null;
|
|
218
|
+
var inFlight = null;
|
|
219
|
+
async function masterKey() {
|
|
220
|
+
const fromEnv = (process.env.SECRETS_KEY || "").trim();
|
|
221
|
+
if (fromEnv) {
|
|
222
|
+
if (cached?.id !== fromEnv) {
|
|
223
|
+
cached = { id: fromEnv, key: createHash2("sha256").update(fromEnv).digest() };
|
|
224
|
+
}
|
|
225
|
+
return cached.key;
|
|
226
|
+
}
|
|
227
|
+
const db = getDb();
|
|
228
|
+
const id = `db:${db.dialect}:${dbIdentity(db)}`;
|
|
229
|
+
if (cached?.id === id) return cached.key;
|
|
230
|
+
if (!inFlight) inFlight = loadOrCreateKey(id).finally(() => inFlight = null);
|
|
231
|
+
return inFlight;
|
|
232
|
+
}
|
|
233
|
+
async function loadOrCreateKey(id) {
|
|
234
|
+
const db = getDb();
|
|
235
|
+
const read = async () => await db.get("SELECT value FROM server_settings WHERE key = ?", [
|
|
236
|
+
MASTER_KEY_SETTING
|
|
237
|
+
]);
|
|
238
|
+
let row = await read();
|
|
239
|
+
if (!row?.value) {
|
|
240
|
+
const sql = db.dialect === "sqlite" ? `INSERT OR IGNORE INTO server_settings (key, value, env_at_write, updated_at)
|
|
241
|
+
VALUES (?, ?, NULL, ${nowExpr(db)})` : `INSERT INTO server_settings (key, value, env_at_write, updated_at)
|
|
242
|
+
VALUES (?, ?, NULL, ${nowExpr(db)}) ON CONFLICT (key) DO NOTHING`;
|
|
243
|
+
await db.run(sql, [MASTER_KEY_SETTING, randomBytes(32).toString("hex")]);
|
|
244
|
+
row = await read();
|
|
245
|
+
}
|
|
246
|
+
if (!row?.value) throw new Error("could not establish an encryption key");
|
|
247
|
+
const key = Buffer.from(row.value, "hex");
|
|
248
|
+
cached = { id, key };
|
|
249
|
+
return key;
|
|
250
|
+
}
|
|
251
|
+
function dbIdentity(db) {
|
|
252
|
+
return process.env.DATABASE_URL || process.env.DATA_ROOT || db.dialect;
|
|
253
|
+
}
|
|
254
|
+
|
|
159
255
|
// src/db/settings.ts
|
|
160
256
|
var deployBaseline = null;
|
|
161
257
|
function norm(v) {
|
|
@@ -168,6 +264,9 @@ function baseline() {
|
|
|
168
264
|
async function persistSetting(name, value) {
|
|
169
265
|
const db = getDb();
|
|
170
266
|
const envAtWrite = norm(baseline()[name]);
|
|
267
|
+
const seal = async (v) => v !== null && isSecretSetting(name) ? await encryptSecret(v) : v;
|
|
268
|
+
const sealedValue = await seal(value);
|
|
269
|
+
const sealedBaseline = await seal(envAtWrite);
|
|
171
270
|
await db.run(
|
|
172
271
|
insertOrReplace(
|
|
173
272
|
db,
|
|
@@ -176,7 +275,7 @@ async function persistSetting(name, value) {
|
|
|
176
275
|
["key"],
|
|
177
276
|
`value = excluded.value, env_at_write = excluded.env_at_write, updated_at = ${nowExpr(db)}`
|
|
178
277
|
),
|
|
179
|
-
[name,
|
|
278
|
+
[name, sealedValue, sealedBaseline]
|
|
180
279
|
);
|
|
181
280
|
}
|
|
182
281
|
async function loadServerSettings() {
|
|
@@ -188,7 +287,12 @@ async function loadServerSettings() {
|
|
|
188
287
|
);
|
|
189
288
|
let applied = 0;
|
|
190
289
|
for (const row of rows) {
|
|
290
|
+
if (row.key === MASTER_KEY_SETTING) continue;
|
|
191
291
|
const envVal = norm(base[row.key]);
|
|
292
|
+
if (isSecretSetting(row.key)) {
|
|
293
|
+
row.value = row.value === null ? null : await tryDecryptSecret(row.value, row.key);
|
|
294
|
+
row.env_at_write = row.env_at_write === null ? null : await tryDecryptSecret(row.env_at_write, `${row.key} (baseline)`);
|
|
295
|
+
}
|
|
192
296
|
if (envVal !== norm(row.env_at_write)) {
|
|
193
297
|
await persistSetting(row.key, envVal);
|
|
194
298
|
if (envVal === null) delete process.env[row.key];
|
|
@@ -317,15 +421,15 @@ function getSuspensionReason() {
|
|
|
317
421
|
var REJECTION_STRIKES_TO_REVOKE = 3;
|
|
318
422
|
var rejectionStreak = 0;
|
|
319
423
|
var rejectionKey = null;
|
|
320
|
-
function fromCache(
|
|
424
|
+
function fromCache(cached2) {
|
|
321
425
|
return {
|
|
322
426
|
valid: true,
|
|
323
|
-
tier:
|
|
324
|
-
org:
|
|
325
|
-
limits:
|
|
427
|
+
tier: cached2.tier,
|
|
428
|
+
org: cached2.org,
|
|
429
|
+
limits: cached2.limits_json ? JSON.parse(cached2.limits_json) : null,
|
|
326
430
|
suspended: suspensionConfirmed,
|
|
327
431
|
suspendedReason: suspensionReason,
|
|
328
|
-
ownerEmail:
|
|
432
|
+
ownerEmail: cached2.owner_email || null
|
|
329
433
|
};
|
|
330
434
|
}
|
|
331
435
|
async function validateLicense(opts = {}) {
|
|
@@ -347,14 +451,14 @@ async function _validateLicenseImpl(forceFresh, strict) {
|
|
|
347
451
|
};
|
|
348
452
|
}
|
|
349
453
|
const db = getDb();
|
|
350
|
-
const
|
|
454
|
+
const cached2 = await db.get(
|
|
351
455
|
"SELECT * FROM license_cache WHERE key = ?",
|
|
352
456
|
[key]
|
|
353
457
|
);
|
|
354
|
-
if (
|
|
355
|
-
const age = Date.now() - (/* @__PURE__ */ new Date(
|
|
458
|
+
if (cached2 && !forceFresh) {
|
|
459
|
+
const age = Date.now() - (/* @__PURE__ */ new Date(cached2.validated_at + "Z")).getTime();
|
|
356
460
|
if (age < CACHE_TTL_MS) {
|
|
357
|
-
return fromCache(
|
|
461
|
+
return fromCache(cached2);
|
|
358
462
|
}
|
|
359
463
|
}
|
|
360
464
|
try {
|
|
@@ -365,11 +469,11 @@ async function _validateLicenseImpl(forceFresh, strict) {
|
|
|
365
469
|
body: JSON.stringify({ key })
|
|
366
470
|
});
|
|
367
471
|
if (!res.ok) {
|
|
368
|
-
if (
|
|
472
|
+
if (cached2) {
|
|
369
473
|
console.warn(
|
|
370
474
|
" PromptOwl unreachable, using cached license (grace period)"
|
|
371
475
|
);
|
|
372
|
-
return fromCache(
|
|
476
|
+
return fromCache(cached2);
|
|
373
477
|
}
|
|
374
478
|
return {
|
|
375
479
|
valid: false,
|
|
@@ -418,7 +522,7 @@ async function _validateLicenseImpl(forceFresh, strict) {
|
|
|
418
522
|
if (data.valid !== true) {
|
|
419
523
|
rejectionStreak = 0;
|
|
420
524
|
rejectionKey = null;
|
|
421
|
-
if (
|
|
525
|
+
if (cached2) return fromCache(cached2);
|
|
422
526
|
return {
|
|
423
527
|
valid: false,
|
|
424
528
|
tier: "none",
|
|
@@ -438,14 +542,14 @@ async function _validateLicenseImpl(forceFresh, strict) {
|
|
|
438
542
|
suspensionReason = null;
|
|
439
543
|
}
|
|
440
544
|
if (!data.valid && !data.suspended) {
|
|
441
|
-
if (
|
|
545
|
+
if (cached2 && !strict) {
|
|
442
546
|
rejectionStreak = rejectionKey === key ? rejectionStreak + 1 : 1;
|
|
443
547
|
rejectionKey = key;
|
|
444
548
|
if (rejectionStreak < REJECTION_STRIKES_TO_REVOKE) {
|
|
445
549
|
console.warn(
|
|
446
550
|
`[license] PromptOwl rejected the license (${rejectionStreak}/${REJECTION_STRIKES_TO_REVOKE}); keeping the cached license until the rejection is confirmed`
|
|
447
551
|
);
|
|
448
|
-
return fromCache(
|
|
552
|
+
return fromCache(cached2);
|
|
449
553
|
}
|
|
450
554
|
}
|
|
451
555
|
rejectionStreak = 0;
|
|
@@ -492,11 +596,11 @@ async function _validateLicenseImpl(forceFresh, strict) {
|
|
|
492
596
|
ownerEmail: data.owner_email || null
|
|
493
597
|
};
|
|
494
598
|
} catch (err) {
|
|
495
|
-
if (
|
|
599
|
+
if (cached2) {
|
|
496
600
|
console.warn(
|
|
497
601
|
` PromptOwl validation failed (${err.message}), using cached license`
|
|
498
602
|
);
|
|
499
|
-
return fromCache(
|
|
603
|
+
return fromCache(cached2);
|
|
500
604
|
}
|
|
501
605
|
return {
|
|
502
606
|
valid: false,
|
|
@@ -1057,7 +1161,7 @@ async function resolveMemberIdentity(params) {
|
|
|
1057
1161
|
if (!requested) {
|
|
1058
1162
|
throw new ValidationError("user_id or email is required");
|
|
1059
1163
|
}
|
|
1060
|
-
const { findOrCreateInvitedUser } = await import("./invited-user-
|
|
1164
|
+
const { findOrCreateInvitedUser } = await import("./invited-user-XEBX6BM4.js");
|
|
1061
1165
|
const { id } = await findOrCreateInvitedUser(requested);
|
|
1062
1166
|
return { userId: id, email: requested };
|
|
1063
1167
|
}
|
|
@@ -2170,8 +2274,8 @@ async function ensureNodeIndex(nestId) {
|
|
|
2170
2274
|
return run;
|
|
2171
2275
|
}
|
|
2172
2276
|
async function rebuildNodeIndex(nestId) {
|
|
2173
|
-
const { engineCache: engineCache2 } = await import("./engine-
|
|
2174
|
-
const { documentsWithSuggestions } = await import("./external-edit-service-
|
|
2277
|
+
const { engineCache: engineCache2 } = await import("./engine-HLYQLP5T.js");
|
|
2278
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-3DFQ4EZZ.js");
|
|
2175
2279
|
const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
|
|
2176
2280
|
dropDiscoveryCache();
|
|
2177
2281
|
const startedAt = writeGeneration.get(nestId) ?? 0;
|
|
@@ -2429,6 +2533,8 @@ export {
|
|
|
2429
2533
|
insertOrReplace,
|
|
2430
2534
|
trackEvent,
|
|
2431
2535
|
startTelemetryLoop,
|
|
2536
|
+
encryptSecret,
|
|
2537
|
+
tryDecryptSecret,
|
|
2432
2538
|
persistSetting,
|
|
2433
2539
|
loadServerSettings,
|
|
2434
2540
|
getCurrentLicense,
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
markIndexStale,
|
|
7
7
|
resolveNestPath,
|
|
8
8
|
syncSuggestionRows
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-QMQDWRHN.js";
|
|
10
10
|
import {
|
|
11
11
|
getDb
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-H3W72HN3.js";
|
|
13
13
|
|
|
14
14
|
// src/governance/external-edit-service.ts
|
|
15
15
|
import { readFile, readdir } from "fs/promises";
|
|
@@ -83,7 +83,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
83
83
|
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
|
-
const { hasUnsealedDraft } = await import("./version-service-
|
|
86
|
+
const { hasUnsealedDraft } = await import("./version-service-JYHFPSYP.js");
|
|
87
87
|
if (await hasUnsealedDraft(nestId, documentId)) {
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
@@ -254,7 +254,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
|
|
|
254
254
|
async function mirrorVersion(input) {
|
|
255
255
|
const { storage } = await engineCache.get(input.nestId);
|
|
256
256
|
const node = await storage.readDocument(input.documentId);
|
|
257
|
-
const { upsertVersion, setApprovedVersion } = await import("./version-service-
|
|
257
|
+
const { upsertVersion, setApprovedVersion } = await import("./version-service-JYHFPSYP.js");
|
|
258
258
|
await upsertVersion({
|
|
259
259
|
nestId: input.nestId,
|
|
260
260
|
nodeId: input.documentId,
|
|
@@ -4,23 +4,24 @@ import {
|
|
|
4
4
|
resolveNestWideRoles,
|
|
5
5
|
resolveStewardsForNode,
|
|
6
6
|
stewardCoverageForUser
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-MB52DWWP.js";
|
|
8
8
|
import {
|
|
9
9
|
grantCoversNode,
|
|
10
10
|
listUserGrants,
|
|
11
11
|
resolveNodeGrant
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-VHEMGQ3I.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-NFFUZF2B.js";
|
|
20
20
|
import {
|
|
21
21
|
buildDocContext,
|
|
22
22
|
buildTitleMap,
|
|
23
23
|
describeNode,
|
|
24
|
+
encryptSecret,
|
|
24
25
|
engineApi,
|
|
25
26
|
engineCache,
|
|
26
27
|
insertOrReplace,
|
|
@@ -34,8 +35,9 @@ import {
|
|
|
34
35
|
opContext,
|
|
35
36
|
prettyFolderPath,
|
|
36
37
|
resolveNestPermission,
|
|
37
|
-
titleForNode
|
|
38
|
-
|
|
38
|
+
titleForNode,
|
|
39
|
+
tryDecryptSecret
|
|
40
|
+
} from "./chunk-QMQDWRHN.js";
|
|
39
41
|
import {
|
|
40
42
|
ConflictError,
|
|
41
43
|
NotFoundError,
|
|
@@ -45,11 +47,11 @@ import {
|
|
|
45
47
|
notifyEmailForNest,
|
|
46
48
|
sendEmailToRecipient,
|
|
47
49
|
transportSettings
|
|
48
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-RXCVXG34.js";
|
|
49
51
|
import {
|
|
50
52
|
config,
|
|
51
53
|
getDb
|
|
52
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-H3W72HN3.js";
|
|
53
55
|
|
|
54
56
|
// src/governance/review-service.ts
|
|
55
57
|
import { v4 as uuid2 } from "uuid";
|
|
@@ -362,12 +364,18 @@ envRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
362
364
|
);
|
|
363
365
|
return c.json({
|
|
364
366
|
count: rows.length,
|
|
365
|
-
env:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
env: await Promise.all(
|
|
368
|
+
rows.map(async (r) => ({
|
|
369
|
+
key: r.key,
|
|
370
|
+
// A value this server can't open still has a name worth listing, so
|
|
371
|
+
// the admin can see what to re-enter.
|
|
372
|
+
value_masked: mask(
|
|
373
|
+
await tryDecryptSecret(r.value, `${r.key} on nest ${nestId}`) ?? ""
|
|
374
|
+
),
|
|
375
|
+
updated_by: r.updated_by,
|
|
376
|
+
updated_at: r.updated_at
|
|
377
|
+
}))
|
|
378
|
+
)
|
|
371
379
|
});
|
|
372
380
|
});
|
|
373
381
|
envRoutes.put("/", requireWorkflowPlane, async (c) => {
|
|
@@ -385,11 +393,12 @@ envRoutes.put("/", requireWorkflowPlane, async (c) => {
|
|
|
385
393
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
386
394
|
const by = await resolveCallerEmail(c.get("userId"));
|
|
387
395
|
const db = getDb();
|
|
396
|
+
const sealed = await encryptSecret(body.value);
|
|
388
397
|
await db.run(
|
|
389
398
|
`INSERT INTO nest_env (nest_id, key, value, updated_by, updated_at)
|
|
390
399
|
VALUES (?, ?, ?, ?, ?)
|
|
391
400
|
ON CONFLICT(nest_id, key) DO UPDATE SET value = ?, updated_by = ?, updated_at = ?`,
|
|
392
|
-
[nestId, key,
|
|
401
|
+
[nestId, key, sealed, by, now, sealed, by, now]
|
|
393
402
|
);
|
|
394
403
|
return c.json({ key, value_masked: mask(body.value) }, 201);
|
|
395
404
|
});
|
|
@@ -412,7 +421,16 @@ async function envValues(nestId) {
|
|
|
412
421
|
);
|
|
413
422
|
const base = {};
|
|
414
423
|
if (process.env.ANTHROPIC_API_KEY) base.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
415
|
-
|
|
424
|
+
if (config.ANTHROPIC_BASE_URL) base.ANTHROPIC_BASE_URL = config.ANTHROPIC_BASE_URL;
|
|
425
|
+
const stored = (await Promise.all(
|
|
426
|
+
rows.map(
|
|
427
|
+
async (r) => [
|
|
428
|
+
r.key,
|
|
429
|
+
await tryDecryptSecret(r.value, `${r.key} on nest ${nestId}`)
|
|
430
|
+
]
|
|
431
|
+
)
|
|
432
|
+
)).filter((e) => e[1] !== null);
|
|
433
|
+
return { ...base, ...Object.fromEntries(stored) };
|
|
416
434
|
}
|
|
417
435
|
|
|
418
436
|
// src/notify/webhook.ts
|