@promptowl/contextnest-community 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONFIGURATION.md CHANGED
@@ -60,7 +60,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`). Two uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — only when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. (2) **Set this whenever `OIDC_ENABLED=true`.** It's the base for the OIDC `redirect_uri` sent to your IdP and replayed at token exchange. Unset, that URI is derived from the incoming request's `Host` header — fine when your proxy overwrites `Host` with a trusted value, but a proxy that passes an attacker-supplied `Host` through would feed it straight into the redirect URI. Setting this pins the value regardless of what the proxy forwards. (Your IdP's own redirect-URI allowlist is a second line of defence, not a substitute.) |
63
+ | `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Three uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — only when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. (2) **Set this whenever `OIDC_ENABLED=true`.** It's the base for the OIDC `redirect_uri` sent to your IdP and replayed at token exchange. Unset, that URI is derived from the incoming request's `Host` header — fine when your proxy overwrites `Host` with a trusted value, but a proxy that passes an attacker-supplied `Host` through would feed it straight into the redirect URI. Setting this pins the value regardless of what the proxy forwards. (Your IdP's own redirect-URI allowlist is a second line of defence, not a substitute.) (3) **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. |
@@ -11,11 +11,11 @@ import {
11
11
  resolveNestPermission,
12
12
  resolveTeamRolesForUser,
13
13
  sendEmailToRecipient
14
- } from "./chunk-FUUJG6XP.js";
14
+ } from "./chunk-ENDHMQPD.js";
15
15
  import {
16
16
  ConflictError,
17
17
  ValidationError
18
- } from "./chunk-GUNJTORH.js";
18
+ } from "./chunk-YVMSM7LS.js";
19
19
  import {
20
20
  config,
21
21
  getDb,
@@ -2,7 +2,7 @@ import {
2
2
  buildTitleMap,
3
3
  insertOrReplace,
4
4
  nowExpr
5
- } from "./chunk-FUUJG6XP.js";
5
+ } from "./chunk-ENDHMQPD.js";
6
6
  import {
7
7
  getDb
8
8
  } from "./chunk-HYSTFMFG.js";
@@ -3,7 +3,7 @@ import {
3
3
  ForbiddenError,
4
4
  NotFoundError,
5
5
  ValidationError
6
- } from "./chunk-GUNJTORH.js";
6
+ } from "./chunk-YVMSM7LS.js";
7
7
  import {
8
8
  config,
9
9
  getDb,
@@ -1101,6 +1101,9 @@ function docLink(nestId, nodeId, baseUrl) {
1101
1101
  if (nodeId) q.set("doc", nodeId);
1102
1102
  return `${base.replace(/\/$/, "")}/?${q.toString()}`;
1103
1103
  }
1104
+ function resolveNodeUrl(node, opts) {
1105
+ return docLink(opts.nestId, node.id, opts.baseUrl) || void 0;
1106
+ }
1104
1107
  async function buildDocContext(nestId, nodeId, baseUrl) {
1105
1108
  const titles = await buildTitleMap(nestId);
1106
1109
  const docTitle = titles.get(nodeId) || nodeId;
@@ -2244,8 +2247,8 @@ async function ensureNodeIndex(nestId) {
2244
2247
  return run;
2245
2248
  }
2246
2249
  async function rebuildNodeIndex(nestId) {
2247
- const { engineCache: engineCache2 } = await import("./engine-34E3YNX7.js");
2248
- const { documentsWithSuggestions } = await import("./external-edit-service-6Q6SHEOF.js");
2250
+ const { engineCache: engineCache2 } = await import("./engine-V4DY4PYN.js");
2251
+ const { documentsWithSuggestions } = await import("./external-edit-service-3NK5DGLA.js");
2249
2252
  const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
2250
2253
  dropDiscoveryCache();
2251
2254
  const startedAt = writeGeneration.get(nestId) ?? 0;
@@ -2558,6 +2561,7 @@ export {
2558
2561
  prettyFolderPath,
2559
2562
  nestName,
2560
2563
  docLink,
2564
+ resolveNodeUrl,
2561
2565
  buildDocContext,
2562
2566
  createTeam,
2563
2567
  findTeamByExternalId,
@@ -4,19 +4,19 @@ import {
4
4
  resolveNestWideRoles,
5
5
  resolveStewardsForNode,
6
6
  stewardCoverageForUser
7
- } from "./chunk-TQACHVVA.js";
7
+ } from "./chunk-3GMGLYTZ.js";
8
8
  import {
9
9
  grantCoversNode,
10
10
  listUserGrants,
11
11
  resolveNodeGrant
12
- } from "./chunk-QUZXRLUA.js";
12
+ } from "./chunk-GSYMJ3A4.js";
13
13
  import {
14
14
  createVersion,
15
15
  getApprovedVersion,
16
16
  getApprovedVersions,
17
17
  getCurrentVersion,
18
18
  setApprovedVersion
19
- } from "./chunk-34UXRZXD.js";
19
+ } from "./chunk-BXUVIAMP.js";
20
20
  import {
21
21
  buildDocContext,
22
22
  buildTitleMap,
@@ -35,12 +35,12 @@ import {
35
35
  resolveNestPermission,
36
36
  sendEmailToRecipient,
37
37
  titleForNode
38
- } from "./chunk-FUUJG6XP.js";
38
+ } from "./chunk-ENDHMQPD.js";
39
39
  import {
40
40
  ConflictError,
41
41
  NotFoundError,
42
42
  ValidationError
43
- } from "./chunk-GUNJTORH.js";
43
+ } from "./chunk-YVMSM7LS.js";
44
44
  import {
45
45
  config,
46
46
  getDb
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ValidationError
3
- } from "./chunk-GUNJTORH.js";
3
+ } from "./chunk-YVMSM7LS.js";
4
4
  import {
5
5
  getDb
6
6
  } from "./chunk-HYSTFMFG.js";
@@ -4,7 +4,7 @@ import {
4
4
  markIndexStale,
5
5
  resolveNestPath,
6
6
  setSuggestionFlag
7
- } from "./chunk-FUUJG6XP.js";
7
+ } from "./chunk-ENDHMQPD.js";
8
8
  import {
9
9
  getDb
10
10
  } from "./chunk-HYSTFMFG.js";
@@ -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-JWEN5UUW.js");
256
+ const { upsertVersion, setApprovedVersion } = await import("./version-service-G7SFFKFO.js");
257
257
  await upsertVersion({
258
258
  nestId: input.nestId,
259
259
  nodeId: input.documentId,
@@ -43,6 +43,17 @@ var LockedError = class extends AppError {
43
43
  this.name = "LockedError";
44
44
  }
45
45
  };
46
+ var VaultBusyError = class extends AppError {
47
+ retryAfterSeconds = 5;
48
+ constructor(message = "The vault is busy with another write. Retry shortly.") {
49
+ super(503, message);
50
+ this.name = "VaultBusyError";
51
+ }
52
+ };
53
+ function isVaultLockTimeout(err) {
54
+ const e = err;
55
+ return e?.name === "VaultLockTimeoutError" || e?.code === "VAULT_LOCK_TIMEOUT";
56
+ }
46
57
 
47
58
  export {
48
59
  AppError,
@@ -51,5 +62,7 @@ export {
51
62
  UnauthorizedError,
52
63
  ValidationError,
53
64
  ConflictError,
54
- LockedError
65
+ LockedError,
66
+ VaultBusyError,
67
+ isVaultLockTimeout
55
68
  };
@@ -2,8 +2,8 @@ import {
2
2
  engineApi,
3
3
  engineCache,
4
4
  opContext
5
- } from "./chunk-FUUJG6XP.js";
6
- import "./chunk-GUNJTORH.js";
5
+ } from "./chunk-ENDHMQPD.js";
6
+ import "./chunk-YVMSM7LS.js";
7
7
  import "./chunk-HYSTFMFG.js";
8
8
  import "./chunk-YB3LKF7U.js";
9
9
  import "./chunk-FRQJWGN3.js";
@@ -11,9 +11,9 @@ import {
11
11
  scanNestForDrift,
12
12
  startDriftScanner,
13
13
  stopDriftScanner
14
- } from "./chunk-OOBZG6BW.js";
15
- import "./chunk-FUUJG6XP.js";
16
- import "./chunk-GUNJTORH.js";
14
+ } from "./chunk-TNAZJYL5.js";
15
+ import "./chunk-ENDHMQPD.js";
16
+ import "./chunk-YVMSM7LS.js";
17
17
  import "./chunk-HYSTFMFG.js";
18
18
  import "./chunk-YB3LKF7U.js";
19
19
  import "./chunk-FRQJWGN3.js";
@@ -6,8 +6,8 @@ import {
6
6
  listGrants,
7
7
  listUserGrants,
8
8
  resolveNodeGrant
9
- } from "./chunk-QUZXRLUA.js";
10
- import "./chunk-GUNJTORH.js";
9
+ } from "./chunk-GSYMJ3A4.js";
10
+ import "./chunk-YVMSM7LS.js";
11
11
  import "./chunk-HYSTFMFG.js";
12
12
  import "./chunk-YB3LKF7U.js";
13
13
  export {
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  submitForReview,
42
42
  withNodeWriteLock,
43
43
  withdrawDeletionRequest
44
- } from "./chunk-HGN45I5P.js";
44
+ } from "./chunk-FBDEESNJ.js";
45
45
  import {
46
46
  canCreateInNest,
47
47
  canManageStewards,
@@ -61,7 +61,7 @@ import {
61
61
  resolveUserRoles,
62
62
  syncFromConfig,
63
63
  updateSteward
64
- } from "./chunk-TQACHVVA.js";
64
+ } from "./chunk-3GMGLYTZ.js";
65
65
  import {
66
66
  createGrant,
67
67
  deleteGrant,
@@ -70,7 +70,7 @@ import {
70
70
  listGrants,
71
71
  listUserGrants,
72
72
  resolveNodeGrant
73
- } from "./chunk-QUZXRLUA.js";
73
+ } from "./chunk-GSYMJ3A4.js";
74
74
  import {
75
75
  generateApiKey,
76
76
  getKeyPrefix,
@@ -93,7 +93,7 @@ import {
93
93
  listMyDrafts,
94
94
  setApprovedVersion,
95
95
  upsertVersion
96
- } from "./chunk-34UXRZXD.js";
96
+ } from "./chunk-BXUVIAMP.js";
97
97
  import {
98
98
  approveExternalEdit,
99
99
  getExternalEditDetail,
@@ -105,7 +105,7 @@ import {
105
105
  scanDocumentForDrift,
106
106
  scanNestForDrift,
107
107
  startDriftScanner
108
- } from "./chunk-OOBZG6BW.js";
108
+ } from "./chunk-TNAZJYL5.js";
109
109
  import {
110
110
  addMember,
111
111
  assertSafeNodeId,
@@ -174,6 +174,7 @@ import {
174
174
  resolveNestAccess,
175
175
  resolveNestPath,
176
176
  resolveNestPermission,
177
+ resolveNodeUrl,
177
178
  resolveTeamRolesForUser,
178
179
  sendEmailToRecipient,
179
180
  setAllowSelfApprove,
@@ -190,7 +191,7 @@ import {
190
191
  updateMemberRole,
191
192
  validateLicense,
192
193
  verifySmtp
193
- } from "./chunk-FUUJG6XP.js";
194
+ } from "./chunk-ENDHMQPD.js";
194
195
  import {
195
196
  AppError,
196
197
  ConflictError,
@@ -198,8 +199,10 @@ import {
198
199
  LockedError,
199
200
  NotFoundError,
200
201
  UnauthorizedError,
201
- ValidationError
202
- } from "./chunk-GUNJTORH.js";
202
+ ValidationError,
203
+ VaultBusyError,
204
+ isVaultLockTimeout
205
+ } from "./chunk-YVMSM7LS.js";
203
206
  import {
204
207
  config,
205
208
  getDb,
@@ -1657,16 +1660,14 @@ import {
1657
1660
  normalizeStatus,
1658
1661
  isRejected,
1659
1662
  filterDocuments,
1660
- DocumentNotFoundError
1663
+ DocumentNotFoundError,
1664
+ NODE_TYPES
1661
1665
  } from "@promptowl/contextnest-engine";
1662
1666
  import { rename, mkdir } from "fs/promises";
1663
1667
  import { dirname, join } from "path";
1664
1668
 
1665
1669
  // src/governance/safe-publish.ts
1666
- import {
1667
- publishDocument,
1668
- serializeDocument
1669
- } from "@promptowl/contextnest-engine";
1670
+ import { serializeDocument } from "@promptowl/contextnest-engine";
1670
1671
  function serializeNodeSafe(node) {
1671
1672
  return serializeDocument({
1672
1673
  ...node,
@@ -1804,6 +1805,10 @@ function toSafeError(err, context, message) {
1804
1805
  "This document is rejected. Change its status before saving."
1805
1806
  );
1806
1807
  }
1808
+ if (isVaultLockTimeout(err)) {
1809
+ console.error(`${context}:`, err);
1810
+ return new VaultBusyError();
1811
+ }
1807
1812
  console.error(`${context}:`, err);
1808
1813
  return new AppError(500, message);
1809
1814
  }
@@ -1818,6 +1823,12 @@ var RUNNABLE_NODE_TYPES = ["agent", "skill"];
1818
1823
  function isRunnableType(type) {
1819
1824
  return RUNNABLE_NODE_TYPES.includes(type || "");
1820
1825
  }
1826
+ var ACCEPTED_NODE_TYPES = /* @__PURE__ */ new Set([
1827
+ ...NODE_TYPES,
1828
+ ...RUNNABLE_NODE_TYPES,
1829
+ "artifact",
1830
+ "table"
1831
+ ]);
1821
1832
  function toNodeResponse(node) {
1822
1833
  const fm = node.frontmatter;
1823
1834
  const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
@@ -1947,12 +1958,12 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1947
1958
  if (filters.folder !== void 0) assertSafeFolder(filters.folder);
1948
1959
  const { versions: versionManager } = await engineCache.get(nestId);
1949
1960
  const userEmail = await resolveCallerEmail(userId);
1950
- const frontmatterFilters = {
1961
+ const listFilters = {
1951
1962
  ...filters.type ? { type: filters.type } : {},
1952
1963
  ...filters.tag ? { tag: filters.tag } : {},
1953
- ...filters.status ? { status: filters.status } : {},
1954
- includeRetired: true
1964
+ ...filters.status ? { status: filters.status } : {}
1955
1965
  };
1966
+ const frontmatterFilters = { ...listFilters, includeRetired: true };
1956
1967
  let documents;
1957
1968
  if (filters.withContent) {
1958
1969
  const scopeInEngine = filters.folder !== void 0 && engineScopesFolders();
@@ -1960,7 +1971,7 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1960
1971
  ({ documents } = await engineApi.run(
1961
1972
  "context_list",
1962
1973
  {
1963
- ...frontmatterFilters,
1974
+ ...listFilters,
1964
1975
  include_retired: true,
1965
1976
  ...scopeInEngine ? {
1966
1977
  folder: [scopePrefix, filters.folder].filter(Boolean).join("/"),
@@ -1976,7 +1987,10 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1976
1987
  nestId,
1977
1988
  filters.folder !== void 0 ? { folder: filters.folder } : {}
1978
1989
  );
1979
- documents = filterDocuments(indexed, frontmatterFilters);
1990
+ documents = filterDocuments(indexed, {
1991
+ ...frontmatterFilters,
1992
+ includeRetired: true
1993
+ });
1980
1994
  }
1981
1995
  const scoped = filters.folder === void 0 ? documents : documents.filter((d) => folderOfId(d.id) === filters.folder);
1982
1996
  const nodes = scoped.map((d) => ({
@@ -2226,6 +2240,11 @@ async function createNodeOnce(nestId, input, userEmail) {
2226
2240
  if (requestedType === "table" && !config.TYPE_TABLE_ENABLED) {
2227
2241
  throw new ValidationError("Table nodes are disabled on this server");
2228
2242
  }
2243
+ if (!ACCEPTED_NODE_TYPES.has(requestedType)) {
2244
+ throw new ValidationError(
2245
+ `unknown node type "${requestedType}"; allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")}`
2246
+ );
2247
+ }
2229
2248
  if (input.schedule !== void 0 && !isRunnableType(input.type)) {
2230
2249
  throw new ValidationError(
2231
2250
  `schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(
@@ -4229,7 +4248,7 @@ nodeRoutes.post("/", async (c) => {
4229
4248
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4230
4249
  const nestId = c.req.param("nestId");
4231
4250
  const nodeId = c.req.param("nodeId");
4232
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-U4CKL7Y5.js");
4251
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-JAFSU5GN.js");
4233
4252
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
4234
4253
  nestId,
4235
4254
  nodeId
@@ -4250,7 +4269,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4250
4269
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
4251
4270
  const nestId = c.req.param("nestId");
4252
4271
  const nodeId = c.req.param("nodeId");
4253
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-H2M5ACPQ.js");
4272
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-LK4M2C43.js");
4254
4273
  const history = await getReviewHistory2(nestId, nodeId);
4255
4274
  return c.json({ reviews: history });
4256
4275
  });
@@ -4889,7 +4908,6 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
4889
4908
 
4890
4909
  // src/nodes/query-routes.ts
4891
4910
  import { Hono as Hono10 } from "hono";
4892
- import { serializeDocument as serializeDocument3 } from "@promptowl/contextnest-engine";
4893
4911
 
4894
4912
  // src/tables/service.ts
4895
4913
  function splitCsvLine(line) {
@@ -5649,11 +5667,15 @@ queryRoutes.get("/graph", async (c) => {
5649
5667
  function approxTokens(text) {
5650
5668
  return Math.ceil(text.length / 4);
5651
5669
  }
5652
- function formatContextBlock(node) {
5670
+ function formatContextBlock(node, url) {
5653
5671
  const tags = (node.frontmatter.tags || []).join(" ");
5654
- const header = tags ? `# ${node.frontmatter.title}
5672
+ const meta = [
5673
+ tags ? `_tags: ${tags}_` : null,
5674
+ url ? `_source: ${url}_` : null
5675
+ ].filter(Boolean);
5676
+ const header = meta.length ? `# ${node.frontmatter.title}
5655
5677
 
5656
- _tags: ${tags}_
5678
+ ${meta.join("\n")}
5657
5679
 
5658
5680
  ` : `# ${node.frontmatter.title}
5659
5681
 
@@ -5666,6 +5688,7 @@ queryRoutes.post("/context", async (c) => {
5666
5688
  throw new ValidationError("prompt or selector is required");
5667
5689
  }
5668
5690
  const nestId = c.req.param("nestId");
5691
+ const baseUrl = requestBaseUrl(c.req.url);
5669
5692
  const { query: queryEngine, storage } = await engineCache.get(nestId);
5670
5693
  const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
5671
5694
  const hops = body.hops ?? 2;
@@ -5774,20 +5797,21 @@ queryRoutes.post("/context", async (c) => {
5774
5797
  const readable = await Promise.all(
5775
5798
  accessible.map(async (doc) => ({
5776
5799
  ...doc,
5777
- body: await resolveReadableBody(nestId, doc.id, userId, doc.body || "")
5800
+ body: await resolveReadableBody(nestId, doc.id, userId, doc.body || ""),
5801
+ url: resolveNodeUrl(doc, { nestId, baseUrl })
5778
5802
  }))
5779
5803
  );
5780
5804
  const included = [];
5781
5805
  let tokenCount = 0;
5782
5806
  for (const doc of readable) {
5783
- const block = formatContextBlock(doc);
5807
+ const block = formatContextBlock(doc, doc.url);
5784
5808
  const blockTokens = approxTokens(block);
5785
5809
  if (tokenCount + blockTokens > maxTokens && included.length > 0) break;
5786
5810
  included.push(doc);
5787
5811
  tokenCount += blockTokens;
5788
5812
  }
5789
5813
  const truncatedByBudget = accessible.length - included.length;
5790
- const contextMarkdown = included.map(formatContextBlock).join("\n\n---\n\n");
5814
+ const contextMarkdown = included.map((n) => formatContextBlock(n, n.url)).join("\n\n---\n\n");
5791
5815
  trackEvent("context.resolve", {
5792
5816
  nestId,
5793
5817
  selector_from_prompt: !!compileDetail,
@@ -5800,6 +5824,7 @@ queryRoutes.post("/context", async (c) => {
5800
5824
  nodes: included.map((n) => ({
5801
5825
  id: n.id,
5802
5826
  title: n.frontmatter.title,
5827
+ url: n.url,
5803
5828
  tags: n.frontmatter.tags || [],
5804
5829
  type: n.frontmatter.type || "document"
5805
5830
  })),
@@ -6013,48 +6038,51 @@ queryRoutes.post("/publish", async (c) => {
6013
6038
  }
6014
6039
  }
6015
6040
  const nestId = c.req.param("nestId");
6016
- const { storage } = await engineCache.get(nestId);
6041
+ const userEmail = await resolveCallerEmail(c.get("userId"));
6017
6042
  const created = [];
6043
+ const skipped = [];
6044
+ const failed = [];
6018
6045
  if (body.context_md) {
6046
+ const { storage } = await engineCache.get(nestId);
6019
6047
  await storage.writeContextMd(body.context_md);
6048
+ await storage.regenerateIndex();
6020
6049
  }
6021
6050
  for (const doc of body.documents || []) {
6022
6051
  if (!doc.title || !doc.content) continue;
6023
- const slug = doc.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
6024
- const id = `nodes/${slug}`;
6025
- const now = (/* @__PURE__ */ new Date()).toISOString();
6026
- const tags = doc.tags?.map((t) => t.startsWith("#") ? t : `#${t}`) || [];
6027
- const node = {
6028
- id,
6029
- filePath: "",
6030
- frontmatter: {
6052
+ try {
6053
+ const result = await createNode(
6054
+ nestId,
6055
+ {
6056
+ title: doc.title,
6057
+ content: doc.content,
6058
+ type: doc.type,
6059
+ tags: doc.tags,
6060
+ scope: doc.scope
6061
+ },
6062
+ userEmail
6063
+ );
6064
+ created.push(result.node.id);
6065
+ } catch (err) {
6066
+ if (err instanceof ConflictError) {
6067
+ skipped.push(doc.title);
6068
+ continue;
6069
+ }
6070
+ if (isVaultLockTimeout(err) || err instanceof VaultBusyError) throw err;
6071
+ if (!(err instanceof AppError)) console.error("bulk publish row failed", err);
6072
+ failed.push({
6031
6073
  title: doc.title,
6032
- type: doc.type || "document",
6033
- tags,
6034
- status: "draft",
6035
- version: 1,
6036
- created_at: now,
6037
- updated_at: now,
6038
- metadata: {
6039
- owners: ["*"],
6040
- scope: doc.scope || "team"
6041
- }
6042
- },
6043
- body: doc.content,
6044
- rawContent: ""
6045
- };
6046
- const serialized = serializeDocument3(node);
6047
- await storage.writeDocument(id, serialized);
6048
- await syncNodeTags(nestId, id, tags);
6049
- created.push(id);
6074
+ error: err instanceof AppError ? err.message : "Could not create this document"
6075
+ });
6076
+ }
6050
6077
  }
6051
- if (created.length > 0 || body.context_md) await storage.regenerateIndex();
6052
6078
  trackEvent("nest.publish", { nestId, count: created.length });
6053
6079
  return c.json(
6054
6080
  {
6055
6081
  published: created.length,
6056
6082
  context_md_updated: !!body.context_md,
6057
- node_ids: created
6083
+ node_ids: created,
6084
+ skipped,
6085
+ failed
6058
6086
  },
6059
6087
  201
6060
6088
  );
@@ -7664,6 +7692,13 @@ async function handleToolCall(toolName, args, ctx) {
7664
7692
  try {
7665
7693
  return await runTool(toolName, args, ctx);
7666
7694
  } catch (err) {
7695
+ if (isVaultLockTimeout(err) || err instanceof VaultBusyError) {
7696
+ status = 503;
7697
+ return toolError(
7698
+ "The vault is busy with another write. Retry in a few seconds.",
7699
+ "VAULT_BUSY"
7700
+ );
7701
+ }
7667
7702
  status = 500;
7668
7703
  throw err;
7669
7704
  } finally {
@@ -8554,7 +8589,7 @@ ${list}`,
8554
8589
  if (!target) return "target is required (a node id or folder prefix).";
8555
8590
  if (!["read", "write"].includes(role)) return "role must be read or write.";
8556
8591
  try {
8557
- const { createGrant: createGrant2 } = await import("./grants-service-ME7HIVHU.js");
8592
+ const { createGrant: createGrant2 } = await import("./grants-service-CADI6LIB.js");
8558
8593
  const db = (await import("./client-SW5I6ZBK.js")).getDb();
8559
8594
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
8560
8595
  const e = normalizeEmail2(String(args.email || ""));
@@ -12534,6 +12569,12 @@ function createApp() {
12534
12569
  );
12535
12570
  app.notFound((c) => c.json({ error: "Not found" }, 404));
12536
12571
  app.onError((err, c) => {
12572
+ if (isVaultLockTimeout(err) || err instanceof VaultBusyError) {
12573
+ const busy = err instanceof VaultBusyError ? err : new VaultBusyError();
12574
+ return c.json({ error: busy.message }, 503, {
12575
+ "Retry-After": String(busy.retryAfterSeconds)
12576
+ });
12577
+ }
12537
12578
  if (err instanceof AppError) {
12538
12579
  return c.json({ error: err.message }, err.statusCode);
12539
12580
  }
@@ -16,14 +16,14 @@ import {
16
16
  requestDeletion,
17
17
  submitForReview,
18
18
  withdrawDeletionRequest
19
- } from "./chunk-HGN45I5P.js";
19
+ } from "./chunk-FBDEESNJ.js";
20
20
  import {
21
21
  canUserApprove
22
- } from "./chunk-TQACHVVA.js";
23
- import "./chunk-QUZXRLUA.js";
24
- import "./chunk-34UXRZXD.js";
25
- import "./chunk-FUUJG6XP.js";
26
- import "./chunk-GUNJTORH.js";
22
+ } from "./chunk-3GMGLYTZ.js";
23
+ import "./chunk-GSYMJ3A4.js";
24
+ import "./chunk-BXUVIAMP.js";
25
+ import "./chunk-ENDHMQPD.js";
26
+ import "./chunk-YVMSM7LS.js";
27
27
  import "./chunk-HYSTFMFG.js";
28
28
  import "./chunk-YB3LKF7U.js";
29
29
  import "./chunk-FRQJWGN3.js";
@@ -23,9 +23,9 @@ import {
23
23
  syncFromConfig,
24
24
  updateSteward,
25
25
  updateStewardRole
26
- } from "./chunk-TQACHVVA.js";
27
- import "./chunk-FUUJG6XP.js";
28
- import "./chunk-GUNJTORH.js";
26
+ } from "./chunk-3GMGLYTZ.js";
27
+ import "./chunk-ENDHMQPD.js";
28
+ import "./chunk-YVMSM7LS.js";
29
29
  import "./chunk-HYSTFMFG.js";
30
30
  import "./chunk-YB3LKF7U.js";
31
31
  import "./chunk-FRQJWGN3.js";
@@ -16,9 +16,9 @@ import {
16
16
  setApprovedVersion,
17
17
  systemAuthor,
18
18
  upsertVersion
19
- } from "./chunk-34UXRZXD.js";
20
- import "./chunk-FUUJG6XP.js";
21
- import "./chunk-GUNJTORH.js";
19
+ } from "./chunk-BXUVIAMP.js";
20
+ import "./chunk-ENDHMQPD.js";
21
+ import "./chunk-YVMSM7LS.js";
22
22
  import "./chunk-HYSTFMFG.js";
23
23
  import "./chunk-YB3LKF7U.js";
24
24
  import "./chunk-FRQJWGN3.js";