@promptowl/contextnest-community 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURATION.md +83 -1
- package/dist/{chunk-GSYMJ3A4.js → chunk-3LUUJUWS.js} +1 -1
- package/dist/{chunk-ENDHMQPD.js → chunk-7CPP6FIU.js} +251 -39
- package/dist/{chunk-TNAZJYL5.js → chunk-EY7WOKJR.js} +3 -3
- package/dist/{chunk-FBDEESNJ.js → chunk-GVUBZTI3.js} +26 -184
- package/dist/{chunk-BXUVIAMP.js → chunk-KHMSDTJJ.js} +2 -2
- package/dist/{chunk-3GMGLYTZ.js → chunk-O5DYL7ZG.js} +2 -2
- package/dist/{chunk-HYSTFMFG.js → chunk-XCFKS65Y.js} +107 -1
- package/dist/{client-SW5I6ZBK.js → client-7Z2IVCPW.js} +1 -1
- package/dist/{engine-V4DY4PYN.js → engine-VZTSDUPB.js} +2 -2
- package/dist/{external-edit-service-3NK5DGLA.js → external-edit-service-3FVK3OD7.js} +3 -3
- package/dist/{grants-service-CADI6LIB.js → grants-service-WC27TRY3.js} +2 -2
- package/dist/index.js +2203 -1056
- package/dist/{migrations.postgres-5VI4RDTS.js → migrations.postgres-L42OXR7K.js} +47 -1
- package/dist/{review-service-LK4M2C43.js → review-service-TJRHRMMZ.js} +6 -6
- package/dist/{stewardship-service-JAFSU5GN.js → stewardship-service-HRVD7SIS.js} +3 -3
- package/dist/{version-service-G7SFFKFO.js → version-service-5PIFT2OY.js} +3 -3
- package/dist/web3/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web3/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web3/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web3/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web3/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web3/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web3/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web3/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web3/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web3/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web3/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web3/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web3/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web3/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web3/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web3/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web3/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web3/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web3/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web3/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web3/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web3/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web3/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web3/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web3/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web3/assets/index-D4UG3T04.js +1365 -0
- package/dist/web3/assets/index-Dw1UTDIk.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +6 -1
- package/dist/web3/assets/index-C26nM5Kn.css +0 -1
- package/dist/web3/assets/index-C4oalc2K.js +0 -1065
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
addWatcher,
|
|
4
3
|
approve,
|
|
5
4
|
canReadNode,
|
|
6
5
|
canRequestDeletion,
|
|
@@ -21,16 +20,11 @@ import {
|
|
|
21
20
|
getReviewQueue,
|
|
22
21
|
isSafeConnectorUrl,
|
|
23
22
|
listDeletionRequests,
|
|
24
|
-
listNotifications,
|
|
25
|
-
listWatchers,
|
|
26
|
-
markNotificationsRead,
|
|
27
23
|
nodeWriteKey,
|
|
28
24
|
notifyDeletionResolved,
|
|
29
25
|
notifyNestEvent,
|
|
30
|
-
notifyNestInvite,
|
|
31
26
|
parseConditionSchema,
|
|
32
27
|
reject,
|
|
33
|
-
removeWatcher,
|
|
34
28
|
repointPendingReview,
|
|
35
29
|
requestDeletion,
|
|
36
30
|
requireWorkflowPlane,
|
|
@@ -41,7 +35,7 @@ import {
|
|
|
41
35
|
submitForReview,
|
|
42
36
|
withNodeWriteLock,
|
|
43
37
|
withdrawDeletionRequest
|
|
44
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-GVUBZTI3.js";
|
|
45
39
|
import {
|
|
46
40
|
canCreateInNest,
|
|
47
41
|
canManageStewards,
|
|
@@ -61,7 +55,7 @@ import {
|
|
|
61
55
|
resolveUserRoles,
|
|
62
56
|
syncFromConfig,
|
|
63
57
|
updateSteward
|
|
64
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-O5DYL7ZG.js";
|
|
65
59
|
import {
|
|
66
60
|
createGrant,
|
|
67
61
|
deleteGrant,
|
|
@@ -70,7 +64,7 @@ import {
|
|
|
70
64
|
listGrants,
|
|
71
65
|
listUserGrants,
|
|
72
66
|
resolveNodeGrant
|
|
73
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-3LUUJUWS.js";
|
|
74
68
|
import {
|
|
75
69
|
generateApiKey,
|
|
76
70
|
getKeyPrefix,
|
|
@@ -93,7 +87,7 @@ import {
|
|
|
93
87
|
listMyDrafts,
|
|
94
88
|
setApprovedVersion,
|
|
95
89
|
upsertVersion
|
|
96
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-KHMSDTJJ.js";
|
|
97
91
|
import {
|
|
98
92
|
approveExternalEdit,
|
|
99
93
|
getExternalEditDetail,
|
|
@@ -105,9 +99,10 @@ import {
|
|
|
105
99
|
scanDocumentForDrift,
|
|
106
100
|
scanNestForDrift,
|
|
107
101
|
startDriftScanner
|
|
108
|
-
} from "./chunk-
|
|
102
|
+
} from "./chunk-EY7WOKJR.js";
|
|
109
103
|
import {
|
|
110
104
|
addMember,
|
|
105
|
+
addWatcher,
|
|
111
106
|
assertSafeNodeId,
|
|
112
107
|
buildTitleMap,
|
|
113
108
|
canApproveWith,
|
|
@@ -135,6 +130,7 @@ import {
|
|
|
135
130
|
insertOrIgnore,
|
|
136
131
|
installLicenseKey,
|
|
137
132
|
isConfigSuperAdmin,
|
|
133
|
+
isEmailConfigured,
|
|
138
134
|
isLicenseAdminEmail,
|
|
139
135
|
isLicenseAdminUserId,
|
|
140
136
|
isPublicReader,
|
|
@@ -144,13 +140,16 @@ import {
|
|
|
144
140
|
isSuspended,
|
|
145
141
|
listNestTeams,
|
|
146
142
|
listNests,
|
|
143
|
+
listNotifications,
|
|
147
144
|
listPublicNests,
|
|
148
145
|
listSharedNests,
|
|
149
146
|
listTeamsForUser,
|
|
150
147
|
listVisibleNests,
|
|
148
|
+
listWatchers,
|
|
151
149
|
loadAccessConfig,
|
|
152
150
|
loadGrantedSuperAdmins,
|
|
153
151
|
loadServerSettings,
|
|
152
|
+
markNotificationsRead,
|
|
154
153
|
nestAllowsSelfApprove,
|
|
155
154
|
nestName,
|
|
156
155
|
nestPrimeTags,
|
|
@@ -158,6 +157,7 @@ import {
|
|
|
158
157
|
nestStorageRoot,
|
|
159
158
|
nodesWithSuggestions,
|
|
160
159
|
normalizeTag,
|
|
160
|
+
notifyNestInvite,
|
|
161
161
|
nowExpr,
|
|
162
162
|
opContext,
|
|
163
163
|
permissionLevel,
|
|
@@ -167,6 +167,7 @@ import {
|
|
|
167
167
|
rebuildNodeIndex,
|
|
168
168
|
removeMember,
|
|
169
169
|
removeNodeFromTagIndex,
|
|
170
|
+
removeWatcher,
|
|
170
171
|
renameNest,
|
|
171
172
|
renameTeam,
|
|
172
173
|
replaceTeamRoster,
|
|
@@ -180,6 +181,7 @@ import {
|
|
|
180
181
|
setAllowSelfApprove,
|
|
181
182
|
setPrimeOnlyReview,
|
|
182
183
|
setPrimeTags,
|
|
184
|
+
setReaderMode,
|
|
183
185
|
setStewardshipEnabled,
|
|
184
186
|
shareTeamWithNest,
|
|
185
187
|
startLicenseSafetyPoll,
|
|
@@ -191,7 +193,7 @@ import {
|
|
|
191
193
|
updateMemberRole,
|
|
192
194
|
validateLicense,
|
|
193
195
|
verifySmtp
|
|
194
|
-
} from "./chunk-
|
|
196
|
+
} from "./chunk-7CPP6FIU.js";
|
|
195
197
|
import {
|
|
196
198
|
AppError,
|
|
197
199
|
ConflictError,
|
|
@@ -209,7 +211,7 @@ import {
|
|
|
209
211
|
initDb,
|
|
210
212
|
isEmailListish,
|
|
211
213
|
isEmailish
|
|
212
|
-
} from "./chunk-
|
|
214
|
+
} from "./chunk-XCFKS65Y.js";
|
|
213
215
|
import {
|
|
214
216
|
ANON_EMAIL,
|
|
215
217
|
ANON_USER_ID,
|
|
@@ -223,7 +225,7 @@ import {
|
|
|
223
225
|
import { serve } from "@hono/node-server";
|
|
224
226
|
|
|
225
227
|
// src/app.ts
|
|
226
|
-
import { Hono as
|
|
228
|
+
import { Hono as Hono23 } from "hono";
|
|
227
229
|
import { LinearRouter } from "hono/router/linear-router";
|
|
228
230
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
229
231
|
import { cors } from "hono/cors";
|
|
@@ -253,7 +255,7 @@ function applyArtifactSandboxHeaders(c) {
|
|
|
253
255
|
|
|
254
256
|
// src/auth/routes.ts
|
|
255
257
|
import { Hono as Hono2 } from "hono";
|
|
256
|
-
import { v4 as
|
|
258
|
+
import { v4 as uuid3 } from "uuid";
|
|
257
259
|
|
|
258
260
|
// src/auth/middleware.ts
|
|
259
261
|
import { createMiddleware } from "hono/factory";
|
|
@@ -475,8 +477,39 @@ function b64urlToBuffer(input) {
|
|
|
475
477
|
const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - input.length % 4);
|
|
476
478
|
return Buffer.from(input.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
|
|
477
479
|
}
|
|
480
|
+
function bufferToB64url(buf) {
|
|
481
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
482
|
+
}
|
|
478
483
|
var SsoTokenError = class extends Error {
|
|
479
484
|
};
|
|
485
|
+
var MCP_TICKET_MIN_TTL = 30;
|
|
486
|
+
var MCP_TICKET_MAX_TTL = 3600;
|
|
487
|
+
function signMcpTicket(input, secret, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
488
|
+
if (!secret) throw new SsoTokenError("MCP signing secret not configured");
|
|
489
|
+
const sub = (input.sub ?? "").trim();
|
|
490
|
+
if (!isValidTicketSub(sub)) throw new SsoTokenError("Invalid ticket subject");
|
|
491
|
+
const ttl = Math.max(
|
|
492
|
+
MCP_TICKET_MIN_TTL,
|
|
493
|
+
Math.min(MCP_TICKET_MAX_TTL, Math.floor(input.ttlSeconds))
|
|
494
|
+
);
|
|
495
|
+
const headerB64 = bufferToB64url(
|
|
496
|
+
Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }))
|
|
497
|
+
);
|
|
498
|
+
const claims = {
|
|
499
|
+
sub,
|
|
500
|
+
scope: "mcp",
|
|
501
|
+
iat: nowSec,
|
|
502
|
+
exp: nowSec + ttl
|
|
503
|
+
};
|
|
504
|
+
if (input.aud) claims.aud = input.aud;
|
|
505
|
+
if (input.name) claims.name = input.name;
|
|
506
|
+
const payloadB64 = bufferToB64url(Buffer.from(JSON.stringify(claims)));
|
|
507
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
508
|
+
const sig = bufferToB64url(
|
|
509
|
+
createHmac("sha256", secret).update(signingInput).digest()
|
|
510
|
+
);
|
|
511
|
+
return `${signingInput}.${sig}`;
|
|
512
|
+
}
|
|
480
513
|
function verifySsoTicket(token, secret, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
481
514
|
if (!secret) throw new SsoTokenError("SSO secret not configured");
|
|
482
515
|
if (!token || typeof token !== "string") {
|
|
@@ -556,14 +589,15 @@ var NONCE_COOKIE = "cnst_oidc_nonce";
|
|
|
556
589
|
var VERIFIER_COOKIE = "cnst_oidc_verifier";
|
|
557
590
|
var FLOW_COOKIE_TTL_SECS = 10 * 60;
|
|
558
591
|
var DISCOVERY_TTL_MS = 5 * 6e4;
|
|
559
|
-
var discoveryCache =
|
|
592
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
560
593
|
function isOidcReady() {
|
|
561
594
|
return config.OIDC_ENABLED && !!config.OIDC_ISSUER && !!config.OIDC_CLIENT_ID && !!config.OIDC_CLIENT_SECRET;
|
|
562
595
|
}
|
|
563
596
|
async function discover(issuer) {
|
|
564
597
|
const now = Date.now();
|
|
565
|
-
|
|
566
|
-
|
|
598
|
+
const cached = discoveryCache.get(issuer);
|
|
599
|
+
if (cached && now - cached.fetchedAt < DISCOVERY_TTL_MS) {
|
|
600
|
+
return { doc: cached.doc, jwks: cached.jwks };
|
|
567
601
|
}
|
|
568
602
|
const res = await fetch(`${issuer}/.well-known/openid-configuration`, {
|
|
569
603
|
headers: { Accept: "application/json" },
|
|
@@ -582,12 +616,7 @@ async function discover(issuer) {
|
|
|
582
616
|
);
|
|
583
617
|
}
|
|
584
618
|
const jwks = createRemoteJWKSet(new URL(doc.jwks_uri));
|
|
585
|
-
discoveryCache
|
|
586
|
-
issuer,
|
|
587
|
-
doc,
|
|
588
|
-
jwks,
|
|
589
|
-
fetchedAt: now
|
|
590
|
-
};
|
|
619
|
+
discoveryCache.set(issuer, { doc, jwks, fetchedAt: now });
|
|
591
620
|
return { doc, jwks };
|
|
592
621
|
}
|
|
593
622
|
function base64url(buf) {
|
|
@@ -852,6 +881,184 @@ oidcRoutes.get("/logout", async (c) => {
|
|
|
852
881
|
return c.redirect(home, 302);
|
|
853
882
|
});
|
|
854
883
|
|
|
884
|
+
// src/auth/token-exchange.ts
|
|
885
|
+
import { v4 as uuid2 } from "uuid";
|
|
886
|
+
import { jwtVerify as jwtVerify2 } from "jose";
|
|
887
|
+
var TX_LIMIT = { max: 60, windowMs: 15 * 6e4 };
|
|
888
|
+
var ExchangeError = class extends Error {
|
|
889
|
+
constructor(code, status, message) {
|
|
890
|
+
super(message ?? code);
|
|
891
|
+
this.code = code;
|
|
892
|
+
this.status = status;
|
|
893
|
+
}
|
|
894
|
+
code;
|
|
895
|
+
status;
|
|
896
|
+
};
|
|
897
|
+
function expectedIssuer(provider, msTenantId) {
|
|
898
|
+
if (provider === "microsoft") {
|
|
899
|
+
return msTenantId ? `https://login.microsoftonline.com/${msTenantId}/v2.0` : "";
|
|
900
|
+
}
|
|
901
|
+
return "https://accounts.google.com";
|
|
902
|
+
}
|
|
903
|
+
function providerConfig(p) {
|
|
904
|
+
if (p === "microsoft") {
|
|
905
|
+
return {
|
|
906
|
+
enabled: config.MS_TOKEN_EXCHANGE_ENABLED,
|
|
907
|
+
issuer: expectedIssuer("microsoft", config.MS_TENANT_ID),
|
|
908
|
+
audience: config.MS_CLIENT_ID
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
enabled: config.GOOGLE_TOKEN_EXCHANGE_ENABLED,
|
|
913
|
+
issuer: expectedIssuer("google", ""),
|
|
914
|
+
audience: config.GOOGLE_CLIENT_ID
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
async function verifySubjectToken(provider, subjectToken) {
|
|
918
|
+
const pc = providerConfig(provider);
|
|
919
|
+
if (!pc.issuer || !pc.audience) {
|
|
920
|
+
throw new ExchangeError("provider_misconfigured", 400);
|
|
921
|
+
}
|
|
922
|
+
let discovered;
|
|
923
|
+
try {
|
|
924
|
+
discovered = await discover(pc.issuer);
|
|
925
|
+
} catch (err) {
|
|
926
|
+
console.error(`[token-exchange] discovery failed for ${pc.issuer}:`, err);
|
|
927
|
+
throw new ExchangeError("provider_misconfigured", 400);
|
|
928
|
+
}
|
|
929
|
+
let claims;
|
|
930
|
+
try {
|
|
931
|
+
const { payload } = await jwtVerify2(subjectToken, discovered.jwks, {
|
|
932
|
+
issuer: discovered.doc.issuer,
|
|
933
|
+
audience: pc.audience,
|
|
934
|
+
algorithms: ["RS256"],
|
|
935
|
+
// Same leeway verifySsoTicket allows, so modest clock skew doesn't
|
|
936
|
+
// bounce a legitimate token on one path and pass on the other.
|
|
937
|
+
clockTolerance: 30
|
|
938
|
+
});
|
|
939
|
+
claims = payload;
|
|
940
|
+
} catch (err) {
|
|
941
|
+
console.warn(
|
|
942
|
+
`[token-exchange] subject_token rejected (${provider}):`,
|
|
943
|
+
err instanceof Error ? err.message : err
|
|
944
|
+
);
|
|
945
|
+
throw new ExchangeError("invalid_subject_token", 401);
|
|
946
|
+
}
|
|
947
|
+
if (claims.email_verified === false || claims.email_verified === "false") {
|
|
948
|
+
throw new ExchangeError("email_not_verified", 403);
|
|
949
|
+
}
|
|
950
|
+
const raw = typeof claims.email === "string" && claims.email || typeof claims.preferred_username === "string" && claims.preferred_username || "";
|
|
951
|
+
const email = normalizeEmail(raw);
|
|
952
|
+
if (!email || !isEmailish(email)) {
|
|
953
|
+
throw new ExchangeError("invalid_subject_token", 401);
|
|
954
|
+
}
|
|
955
|
+
const allowed = config.OIDC_ALLOWED_DOMAINS;
|
|
956
|
+
if (allowed.length > 0 && !allowed.includes(email.slice(email.lastIndexOf("@") + 1))) {
|
|
957
|
+
throw new ExchangeError("domain_not_allowed", 403);
|
|
958
|
+
}
|
|
959
|
+
const name = typeof claims.name === "string" && claims.name.trim() ? claims.name.trim() : null;
|
|
960
|
+
return { email, name, department: normalizeDepartment(claims.department) };
|
|
961
|
+
}
|
|
962
|
+
async function resolveUser(email, name, department) {
|
|
963
|
+
const db = getDb();
|
|
964
|
+
const existing = await db.get(
|
|
965
|
+
"SELECT id FROM users WHERE LOWER(email) = ?",
|
|
966
|
+
[email]
|
|
967
|
+
);
|
|
968
|
+
if (existing) return withDepartment(existing, department);
|
|
969
|
+
if (!config.OIDC_AUTO_PROVISION || config.OIDC_ALLOWED_DOMAINS.length === 0) {
|
|
970
|
+
throw new ExchangeError("no_account", 403);
|
|
971
|
+
}
|
|
972
|
+
const userId = uuid2();
|
|
973
|
+
const placeholderHash = await hashPassword(uuid2());
|
|
974
|
+
try {
|
|
975
|
+
await db.run(
|
|
976
|
+
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
977
|
+
[userId, email, name, placeholderHash]
|
|
978
|
+
);
|
|
979
|
+
trackEvent("user.register", { userId, email, method: "token-exchange" });
|
|
980
|
+
return withDepartment({ id: userId }, department);
|
|
981
|
+
} catch (insErr) {
|
|
982
|
+
const raced = await db.get(
|
|
983
|
+
"SELECT id FROM users WHERE LOWER(email) = ?",
|
|
984
|
+
[email]
|
|
985
|
+
);
|
|
986
|
+
if (!raced) throw insErr;
|
|
987
|
+
return withDepartment(raced, department);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function withDepartment(user, department) {
|
|
991
|
+
try {
|
|
992
|
+
await getDb().run("UPDATE users SET department = ? WHERE id = ?", [
|
|
993
|
+
department,
|
|
994
|
+
user.id
|
|
995
|
+
]);
|
|
996
|
+
} catch (err) {
|
|
997
|
+
console.error("[token-exchange] department upsert failed:", err);
|
|
998
|
+
}
|
|
999
|
+
return user;
|
|
1000
|
+
}
|
|
1001
|
+
async function handleTokenExchange(c) {
|
|
1002
|
+
if (!config.SSO_TOKEN_EXCHANGE_ENABLED || !config.MCP_SIGNING_SECRET) {
|
|
1003
|
+
return c.json(
|
|
1004
|
+
{ error: "Token exchange is not enabled on this server", code: "disabled" },
|
|
1005
|
+
404
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
if (!tryConsume(`token-exchange:${clientIp(c)}`, TX_LIMIT)) {
|
|
1009
|
+
return c.json({ error: "Too many requests", code: "rate_limited" }, 429);
|
|
1010
|
+
}
|
|
1011
|
+
let body;
|
|
1012
|
+
try {
|
|
1013
|
+
const parsed = await c.req.json();
|
|
1014
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1015
|
+
throw new Error("body is not a JSON object");
|
|
1016
|
+
}
|
|
1017
|
+
body = parsed;
|
|
1018
|
+
} catch {
|
|
1019
|
+
return c.json({ error: "Invalid JSON body", code: "bad_request" }, 400);
|
|
1020
|
+
}
|
|
1021
|
+
const provider = body.provider;
|
|
1022
|
+
if (provider !== "microsoft" && provider !== "google") {
|
|
1023
|
+
return c.json({ error: "Unknown or missing provider", code: "bad_request" }, 400);
|
|
1024
|
+
}
|
|
1025
|
+
if (!providerConfig(provider).enabled) {
|
|
1026
|
+
return c.json({ error: "Provider not enabled", code: "provider_disabled" }, 400);
|
|
1027
|
+
}
|
|
1028
|
+
const subjectToken = typeof body.subject_token === "string" ? body.subject_token.trim() : "";
|
|
1029
|
+
if (!subjectToken) {
|
|
1030
|
+
return c.json({ error: "Missing subject_token", code: "bad_request" }, 400);
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
const { email, name, department } = await verifySubjectToken(
|
|
1034
|
+
provider,
|
|
1035
|
+
subjectToken
|
|
1036
|
+
);
|
|
1037
|
+
const user = await resolveUser(email, name, department);
|
|
1038
|
+
const token = signMcpTicket(
|
|
1039
|
+
{
|
|
1040
|
+
sub: email,
|
|
1041
|
+
aud: config.PUBLIC_BASE_URL || void 0,
|
|
1042
|
+
name: name ?? void 0,
|
|
1043
|
+
ttlSeconds: config.MCP_TOKEN_TTL_SECONDS
|
|
1044
|
+
},
|
|
1045
|
+
config.MCP_SIGNING_SECRET
|
|
1046
|
+
);
|
|
1047
|
+
trackEvent("token_exchange.mint", { userId: user.id, provider });
|
|
1048
|
+
return c.json({
|
|
1049
|
+
access_token: token,
|
|
1050
|
+
token_type: "Bearer",
|
|
1051
|
+
expires_in: config.MCP_TOKEN_TTL_SECONDS
|
|
1052
|
+
});
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
if (err instanceof ExchangeError) {
|
|
1055
|
+
return c.json({ error: err.message, code: err.code }, err.status);
|
|
1056
|
+
}
|
|
1057
|
+
console.error("[token-exchange] unexpected error:", err);
|
|
1058
|
+
return c.json({ error: "Token exchange failed", code: "server_error" }, 500);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
855
1062
|
// src/auth/routes.ts
|
|
856
1063
|
function isServerAdminEmail(email) {
|
|
857
1064
|
return isLicenseAdminEmail(email) || isSuperAdmin(email);
|
|
@@ -939,8 +1146,8 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
|
|
|
939
1146
|
[meEmail]
|
|
940
1147
|
);
|
|
941
1148
|
if (!user) {
|
|
942
|
-
const userId =
|
|
943
|
-
const placeholderHash = await hashPassword(
|
|
1149
|
+
const userId = uuid3();
|
|
1150
|
+
const placeholderHash = await hashPassword(uuid3());
|
|
944
1151
|
await db.run(
|
|
945
1152
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
946
1153
|
[userId, meEmail, rawName || null, placeholderHash]
|
|
@@ -974,6 +1181,7 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
|
|
|
974
1181
|
}
|
|
975
1182
|
var authRoutes = new Hono2();
|
|
976
1183
|
authRoutes.route("/oidc", oidcRoutes);
|
|
1184
|
+
authRoutes.post("/token-exchange", handleTokenExchange);
|
|
977
1185
|
authRoutes.post("/register", async (c) => {
|
|
978
1186
|
if (config.MANUAL_SIGN_IN !== "open") {
|
|
979
1187
|
return c.json(
|
|
@@ -1012,7 +1220,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1012
1220
|
}
|
|
1013
1221
|
throw new ValidationError("Email already registered");
|
|
1014
1222
|
}
|
|
1015
|
-
const userId =
|
|
1223
|
+
const userId = uuid3();
|
|
1016
1224
|
const passwordHash = await hashPassword(body.password);
|
|
1017
1225
|
await db.run(
|
|
1018
1226
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
@@ -1114,7 +1322,7 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1114
1322
|
});
|
|
1115
1323
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1116
1324
|
const apiKey = generateApiKey();
|
|
1117
|
-
const keyId =
|
|
1325
|
+
const keyId = uuid3();
|
|
1118
1326
|
await db.run(
|
|
1119
1327
|
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
|
|
1120
1328
|
[
|
|
@@ -1143,7 +1351,7 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1143
1351
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1144
1352
|
const prior = check.key;
|
|
1145
1353
|
const apiKey = generateApiKey();
|
|
1146
|
-
const keyId =
|
|
1354
|
+
const keyId = uuid3();
|
|
1147
1355
|
const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
|
|
1148
1356
|
const replaced = await db.transaction(async (tx) => {
|
|
1149
1357
|
const removed = await tx.run(
|
|
@@ -1451,7 +1659,7 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
|
1451
1659
|
if (supplied !== void 0) {
|
|
1452
1660
|
assertValidPassword(supplied);
|
|
1453
1661
|
}
|
|
1454
|
-
const generated = supplied ? null :
|
|
1662
|
+
const generated = supplied ? null : uuid3().replace(/-/g, "").slice(0, 16);
|
|
1455
1663
|
const newPassword = supplied ?? generated;
|
|
1456
1664
|
const newHash = await hashPassword(newPassword);
|
|
1457
1665
|
await db.transaction(async (tx) => {
|
|
@@ -1562,7 +1770,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1562
1770
|
409
|
|
1563
1771
|
);
|
|
1564
1772
|
}
|
|
1565
|
-
const tempPassword =
|
|
1773
|
+
const tempPassword = uuid3().replace(/-/g, "").slice(0, 16);
|
|
1566
1774
|
const passwordHash = await hashPassword(tempPassword);
|
|
1567
1775
|
let userId;
|
|
1568
1776
|
if (existing) {
|
|
@@ -1572,7 +1780,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1572
1780
|
userId
|
|
1573
1781
|
]);
|
|
1574
1782
|
} else {
|
|
1575
|
-
userId =
|
|
1783
|
+
userId = uuid3();
|
|
1576
1784
|
await db.run(
|
|
1577
1785
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1578
1786
|
[userId, email, null, passwordHash]
|
|
@@ -1661,9 +1869,10 @@ import {
|
|
|
1661
1869
|
isRejected,
|
|
1662
1870
|
filterDocuments,
|
|
1663
1871
|
DocumentNotFoundError,
|
|
1664
|
-
NODE_TYPES
|
|
1872
|
+
NODE_TYPES,
|
|
1873
|
+
generateIndexMd
|
|
1665
1874
|
} from "@promptowl/contextnest-engine";
|
|
1666
|
-
import { rename, mkdir } from "fs/promises";
|
|
1875
|
+
import { rename, mkdir, rm, stat } from "fs/promises";
|
|
1667
1876
|
import { dirname, join } from "path";
|
|
1668
1877
|
|
|
1669
1878
|
// src/governance/safe-publish.ts
|
|
@@ -1914,6 +2123,9 @@ function engineScopesFolders() {
|
|
|
1914
2123
|
}
|
|
1915
2124
|
return folderScopeSupported;
|
|
1916
2125
|
}
|
|
2126
|
+
function isDerivedAnnotationsNode(id) {
|
|
2127
|
+
return id.endsWith("--annotations");
|
|
2128
|
+
}
|
|
1917
2129
|
async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
|
|
1918
2130
|
if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
|
|
1919
2131
|
throw new ForbiddenError(
|
|
@@ -1992,6 +2204,9 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
1992
2204
|
includeRetired: true
|
|
1993
2205
|
});
|
|
1994
2206
|
}
|
|
2207
|
+
if (!filters.includeDerived) {
|
|
2208
|
+
documents = documents.filter((d) => !isDerivedAnnotationsNode(d.id));
|
|
2209
|
+
}
|
|
1995
2210
|
const scoped = filters.folder === void 0 ? documents : documents.filter((d) => folderOfId(d.id) === filters.folder);
|
|
1996
2211
|
const nodes = scoped.map((d) => ({
|
|
1997
2212
|
id: d.id,
|
|
@@ -2094,10 +2309,37 @@ function withSubtreeTotals(direct) {
|
|
|
2094
2309
|
)
|
|
2095
2310
|
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
2096
2311
|
}
|
|
2312
|
+
async function createEmptyFolder(nestId, folder) {
|
|
2313
|
+
const segments = folderToSegments(folder);
|
|
2314
|
+
if (segments.length === 0) {
|
|
2315
|
+
throw new ValidationError("folder is required");
|
|
2316
|
+
}
|
|
2317
|
+
const path = segments.join("/");
|
|
2318
|
+
const prefix = await storagePrefix(nestId);
|
|
2319
|
+
const vaultPath = prefix ? `${prefix}/${path}` : path;
|
|
2320
|
+
if (await vaultFolderExists(nestId, path)) return path;
|
|
2321
|
+
const title = segments[segments.length - 1].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2322
|
+
const { storage } = await engineCache.get(nestId);
|
|
2323
|
+
await storage.writeIndexMd(vaultPath, generateIndexMd(vaultPath, title, []));
|
|
2324
|
+
return path;
|
|
2325
|
+
}
|
|
2326
|
+
async function vaultFolderPaths(nestId, opts = {}) {
|
|
2327
|
+
const prefix = await storagePrefix(nestId);
|
|
2328
|
+
const scope = [prefix, opts.folder].filter(Boolean).join("/");
|
|
2329
|
+
const { storage } = await engineCache.get(nestId);
|
|
2330
|
+
const entries = await storage.listFolders({
|
|
2331
|
+
...scope ? { folder: scope } : {},
|
|
2332
|
+
...opts.recursive === false ? { recursive: false } : {}
|
|
2333
|
+
});
|
|
2334
|
+
return entries.map(
|
|
2335
|
+
(e) => prefix && e.path.startsWith(`${prefix}/`) ? e.path.slice(prefix.length + 1) : e.path
|
|
2336
|
+
).filter(Boolean);
|
|
2337
|
+
}
|
|
2097
2338
|
async function listNestFolders(nestId, opts = {}) {
|
|
2098
2339
|
if (opts.folder !== void 0) assertSafeFolder(opts.folder);
|
|
2099
2340
|
await ensureNodeIndex(nestId);
|
|
2100
2341
|
const counts = /* @__PURE__ */ new Map();
|
|
2342
|
+
for (const path of await vaultFolderPaths(nestId, opts)) counts.set(path, 0);
|
|
2101
2343
|
for (const { path, count } of await indexedFolderCounts(nestId)) {
|
|
2102
2344
|
counts.set(path, count);
|
|
2103
2345
|
let parent = path;
|
|
@@ -2122,7 +2364,9 @@ var RECENT_LIMIT = 3;
|
|
|
2122
2364
|
async function nestCensus(nestId, userId) {
|
|
2123
2365
|
const userEmail = await resolveCallerEmail(userId);
|
|
2124
2366
|
await ensureNodeIndex(nestId);
|
|
2125
|
-
const documents = await readIndexedNodes(nestId)
|
|
2367
|
+
const documents = (await readIndexedNodes(nestId)).filter(
|
|
2368
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
2369
|
+
);
|
|
2126
2370
|
const visible = await filterAccessible(nestId, userId, userEmail, documents);
|
|
2127
2371
|
const tags = {};
|
|
2128
2372
|
const types = {};
|
|
@@ -2143,6 +2387,7 @@ async function nestCensus(nestId, userId) {
|
|
|
2143
2387
|
const segs = folder.split("/");
|
|
2144
2388
|
for (let i = 1; i <= segs.length; i++) paths.add(segs.slice(0, i).join("/"));
|
|
2145
2389
|
}
|
|
2390
|
+
for (const path of await vaultFolderPaths(nestId)) paths.add(path);
|
|
2146
2391
|
const folders = withSubtreeTotals(
|
|
2147
2392
|
[...paths].map((path) => ({ path, count: direct.get(path) ?? 0 }))
|
|
2148
2393
|
);
|
|
@@ -2186,7 +2431,10 @@ async function nestCensus(nestId, userId) {
|
|
|
2186
2431
|
};
|
|
2187
2432
|
}
|
|
2188
2433
|
async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
|
|
2189
|
-
return listNodesForCaller(nestId, await userIdFromEmail(userEmail),
|
|
2434
|
+
return listNodesForCaller(nestId, await userIdFromEmail(userEmail), {
|
|
2435
|
+
includeDerived: true,
|
|
2436
|
+
...filters
|
|
2437
|
+
});
|
|
2190
2438
|
}
|
|
2191
2439
|
async function actorAutoPublishes(nestId, userEmail) {
|
|
2192
2440
|
if (!await nestAllowsSelfApprove(nestId)) return false;
|
|
@@ -2651,6 +2899,10 @@ var NODE_KEYED_TABLES = [
|
|
|
2651
2899
|
// A live capability token bound to one agent node — deleting the agent must
|
|
2652
2900
|
// revoke it, or a later node at that id answers the old webhook.
|
|
2653
2901
|
["trigger_hooks", "agent_node"],
|
|
2902
|
+
// A live public link pinned to one node. Deleting the document must kill the
|
|
2903
|
+
// link, and moving it must carry the link along — otherwise /p/<slug> serves
|
|
2904
|
+
// whatever node next takes that id.
|
|
2905
|
+
["publish", "node_id"],
|
|
2654
2906
|
["edges", "from_node"],
|
|
2655
2907
|
["edges", "to_node"]
|
|
2656
2908
|
];
|
|
@@ -2814,6 +3066,7 @@ async function deleteFolder(nestId, folder, userEmail) {
|
|
|
2814
3066
|
const docs = await storage.discoverDocuments();
|
|
2815
3067
|
const targets = docs.map((d) => d.id).filter((id) => isIdUnderFolder(id, rel));
|
|
2816
3068
|
if (targets.length === 0) {
|
|
3069
|
+
if (await removeVaultFolder(nestId, rel)) return [];
|
|
2817
3070
|
throw new NotFoundError(`No documents found under folder: ${folder}`);
|
|
2818
3071
|
}
|
|
2819
3072
|
const deleted = [];
|
|
@@ -2828,13 +3081,35 @@ async function deleteFolder(nestId, folder, userEmail) {
|
|
|
2828
3081
|
);
|
|
2829
3082
|
}
|
|
2830
3083
|
}
|
|
3084
|
+
await removeVaultFolder(nestId, rel);
|
|
2831
3085
|
return deleted;
|
|
2832
3086
|
}
|
|
3087
|
+
async function vaultFolderDir(nestId, rel) {
|
|
3088
|
+
const prefix = await storagePrefix(nestId);
|
|
3089
|
+
return join(
|
|
3090
|
+
resolveNestPath(nestId),
|
|
3091
|
+
...prefix ? [prefix] : [],
|
|
3092
|
+
...rel.split("/")
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
async function vaultFolderExists(nestId, rel) {
|
|
3096
|
+
if (!rel) return false;
|
|
3097
|
+
try {
|
|
3098
|
+
return (await stat(await vaultFolderDir(nestId, rel))).isDirectory();
|
|
3099
|
+
} catch {
|
|
3100
|
+
return false;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
async function removeVaultFolder(nestId, rel) {
|
|
3104
|
+
if (!await vaultFolderExists(nestId, rel)) return false;
|
|
3105
|
+
await rm(await vaultFolderDir(nestId, rel), { recursive: true, force: true });
|
|
3106
|
+
return true;
|
|
3107
|
+
}
|
|
2833
3108
|
|
|
2834
3109
|
// src/nests/unsynced-service.ts
|
|
2835
3110
|
import { readdirSync, readFileSync, statSync } from "fs";
|
|
2836
3111
|
import { join as join2, relative, resolve } from "path";
|
|
2837
|
-
import { rm } from "fs/promises";
|
|
3112
|
+
import { rm as rm2 } from "fs/promises";
|
|
2838
3113
|
var RESERVED = /* @__PURE__ */ new Set(["nests"]);
|
|
2839
3114
|
function isNestStorageRoot(absPath) {
|
|
2840
3115
|
return resolve(absPath) === resolve(nestStorageRoot());
|
|
@@ -2977,14 +3252,14 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
2977
3252
|
if (isNestStorageRoot(src)) {
|
|
2978
3253
|
throw new ValidationError("Folder is not eligible for sync");
|
|
2979
3254
|
}
|
|
2980
|
-
let
|
|
3255
|
+
let stat2;
|
|
2981
3256
|
try {
|
|
2982
|
-
|
|
3257
|
+
stat2 = statSync(src);
|
|
2983
3258
|
} catch {
|
|
2984
3259
|
console.warn(`[unsynced] source missing: ${src}`);
|
|
2985
3260
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
2986
3261
|
}
|
|
2987
|
-
if (!
|
|
3262
|
+
if (!stat2.isDirectory()) {
|
|
2988
3263
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
2989
3264
|
}
|
|
2990
3265
|
const files = collectMarkdownFiles(src, src);
|
|
@@ -3019,7 +3294,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3019
3294
|
`[unsynced] registered ${documents} document(s) for nest ${nest.id}`
|
|
3020
3295
|
);
|
|
3021
3296
|
try {
|
|
3022
|
-
await
|
|
3297
|
+
await rm2(src, { recursive: true, force: true });
|
|
3023
3298
|
console.log(`[unsynced] removed source folder ${src}`);
|
|
3024
3299
|
} catch (err) {
|
|
3025
3300
|
console.error("[unsynced] failed to remove source folder", src, err);
|
|
@@ -3033,16 +3308,16 @@ async function deleteUnsyncedFolder(folderName) {
|
|
|
3033
3308
|
if (isNestStorageRoot(src)) {
|
|
3034
3309
|
throw new ValidationError("Folder is not eligible for delete");
|
|
3035
3310
|
}
|
|
3036
|
-
let
|
|
3311
|
+
let stat2;
|
|
3037
3312
|
try {
|
|
3038
|
-
|
|
3313
|
+
stat2 = statSync(src);
|
|
3039
3314
|
} catch {
|
|
3040
3315
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
3041
3316
|
}
|
|
3042
|
-
if (!
|
|
3317
|
+
if (!stat2.isDirectory()) {
|
|
3043
3318
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
3044
3319
|
}
|
|
3045
|
-
await
|
|
3320
|
+
await rm2(src, { recursive: true, force: true });
|
|
3046
3321
|
console.log(`[unsynced] deleted folder ${src}`);
|
|
3047
3322
|
}
|
|
3048
3323
|
|
|
@@ -3421,7 +3696,7 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
|
3421
3696
|
import { Hono as Hono4 } from "hono";
|
|
3422
3697
|
|
|
3423
3698
|
// src/nests/sharing-service.ts
|
|
3424
|
-
import { v4 as
|
|
3699
|
+
import { v4 as uuid4 } from "uuid";
|
|
3425
3700
|
var VALID_PERMISSIONS = ["read", "write", "admin"];
|
|
3426
3701
|
async function addCollaborator(params) {
|
|
3427
3702
|
const { nestId } = params;
|
|
@@ -3448,10 +3723,10 @@ async function addCollaborator(params) {
|
|
|
3448
3723
|
userId = existing.id;
|
|
3449
3724
|
} else {
|
|
3450
3725
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3451
|
-
userId =
|
|
3726
|
+
userId = uuid4();
|
|
3452
3727
|
await db.run(
|
|
3453
3728
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
3454
|
-
[userId, email, null, await hashPassword2(
|
|
3729
|
+
[userId, email, null, await hashPassword2(uuid4())]
|
|
3455
3730
|
);
|
|
3456
3731
|
}
|
|
3457
3732
|
}
|
|
@@ -3489,7 +3764,7 @@ async function addCollaborator(params) {
|
|
|
3489
3764
|
[params.grantedByEmail]
|
|
3490
3765
|
))?.id : void 0;
|
|
3491
3766
|
const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
|
|
3492
|
-
const collabId =
|
|
3767
|
+
const collabId = uuid4();
|
|
3493
3768
|
await db.run(
|
|
3494
3769
|
"INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
|
|
3495
3770
|
[collabId, nestId, userId, params.permission, granterId]
|
|
@@ -3608,6 +3883,16 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
3608
3883
|
if (!body.visibility || !["private", "public"].includes(body.visibility)) {
|
|
3609
3884
|
throw new ValidationError("visibility must be private or public");
|
|
3610
3885
|
}
|
|
3886
|
+
if (body.visibility === "public" && body.acknowledge_public !== true) {
|
|
3887
|
+
return c.json(
|
|
3888
|
+
{
|
|
3889
|
+
error: "Publishing this nest requires acknowledgement.",
|
|
3890
|
+
requires_acknowledgement: true,
|
|
3891
|
+
warning: "This makes the nest readable by anyone on the internet, outside your organization."
|
|
3892
|
+
},
|
|
3893
|
+
409
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3611
3896
|
const db = getDb();
|
|
3612
3897
|
const nestId = c.req.param("nestId");
|
|
3613
3898
|
const result = await db.run("UPDATE nests SET visibility = ? WHERE id = ?", [
|
|
@@ -3622,10 +3907,55 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
3622
3907
|
]);
|
|
3623
3908
|
return c.json({ visibility: row.visibility });
|
|
3624
3909
|
});
|
|
3910
|
+
sharingRoutes.patch("/reader-mode", async (c) => {
|
|
3911
|
+
const userId = c.get("userId");
|
|
3912
|
+
if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
|
|
3913
|
+
throw new ForbiddenError(
|
|
3914
|
+
"Only the server admin can configure reader mode"
|
|
3915
|
+
);
|
|
3916
|
+
}
|
|
3917
|
+
const body = await c.req.json();
|
|
3918
|
+
if (body.enabled !== void 0 && typeof body.enabled !== "boolean") {
|
|
3919
|
+
throw new ValidationError("enabled must be a boolean");
|
|
3920
|
+
}
|
|
3921
|
+
let homeNode;
|
|
3922
|
+
if (body.home_node !== void 0) {
|
|
3923
|
+
if (body.home_node === null) {
|
|
3924
|
+
homeNode = null;
|
|
3925
|
+
} else if (typeof body.home_node === "string") {
|
|
3926
|
+
const trimmed = body.home_node.trim();
|
|
3927
|
+
homeNode = trimmed === "" ? null : trimmed;
|
|
3928
|
+
} else {
|
|
3929
|
+
throw new ValidationError("home_node must be a string or null");
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
if (body.enabled === void 0 && homeNode === void 0) {
|
|
3933
|
+
throw new ValidationError(
|
|
3934
|
+
"provide enabled and/or home_node to update"
|
|
3935
|
+
);
|
|
3936
|
+
}
|
|
3937
|
+
const db = getDb();
|
|
3938
|
+
const nestId = c.req.param("nestId");
|
|
3939
|
+
const exists = await db.get("SELECT id FROM nests WHERE id = ?", [
|
|
3940
|
+
nestId
|
|
3941
|
+
]);
|
|
3942
|
+
if (!exists) {
|
|
3943
|
+
throw new NotFoundError("Nest not found");
|
|
3944
|
+
}
|
|
3945
|
+
await setReaderMode(nestId, { enabled: body.enabled, homeNode });
|
|
3946
|
+
const saved = await db.get(
|
|
3947
|
+
"SELECT reader_mode, reader_home_node FROM nests WHERE id = ?",
|
|
3948
|
+
[nestId]
|
|
3949
|
+
);
|
|
3950
|
+
return c.json({
|
|
3951
|
+
reader_mode: saved.reader_mode,
|
|
3952
|
+
reader_home_node: saved.reader_home_node
|
|
3953
|
+
});
|
|
3954
|
+
});
|
|
3625
3955
|
|
|
3626
3956
|
// src/nests/grant-routes.ts
|
|
3627
3957
|
import { Hono as Hono5 } from "hono";
|
|
3628
|
-
import { v4 as
|
|
3958
|
+
import { v4 as uuid5 } from "uuid";
|
|
3629
3959
|
var grantRoutes = new Hono5();
|
|
3630
3960
|
async function requireManage(c, nestId) {
|
|
3631
3961
|
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
@@ -3640,10 +3970,10 @@ async function resolveGrantee(email) {
|
|
|
3640
3970
|
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
3641
3971
|
if (existing) return existing.id;
|
|
3642
3972
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3643
|
-
const id =
|
|
3973
|
+
const id = uuid5();
|
|
3644
3974
|
await db.run(
|
|
3645
3975
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
3646
|
-
[id, e, null, await hashPassword2(
|
|
3976
|
+
[id, e, null, await hashPassword2(uuid5())]
|
|
3647
3977
|
);
|
|
3648
3978
|
return id;
|
|
3649
3979
|
}
|
|
@@ -3741,1059 +4071,1718 @@ grantRoutes.delete("/:id", async (c) => {
|
|
|
3741
4071
|
return c.json({ deleted: true });
|
|
3742
4072
|
});
|
|
3743
4073
|
|
|
3744
|
-
// src/nests/
|
|
4074
|
+
// src/nests/publish-routes.ts
|
|
3745
4075
|
import { Hono as Hono6 } from "hono";
|
|
3746
|
-
var nestTeamRoutes = new Hono6();
|
|
3747
|
-
nestTeamRoutes.get("/", async (c) => {
|
|
3748
|
-
const nestId = c.req.param("nestId");
|
|
3749
|
-
const refs = await listNestTeams(nestId);
|
|
3750
|
-
const teams = [];
|
|
3751
|
-
for (const ref of refs) {
|
|
3752
|
-
const team = await getTeam(ref.teamId).catch(() => null);
|
|
3753
|
-
teams.push({
|
|
3754
|
-
teamId: ref.teamId,
|
|
3755
|
-
teamName: team?.name ?? ref.teamName,
|
|
3756
|
-
members: team?.members ?? []
|
|
3757
|
-
});
|
|
3758
|
-
}
|
|
3759
|
-
return c.json({ teams });
|
|
3760
|
-
});
|
|
3761
|
-
nestTeamRoutes.post("/", async (c) => {
|
|
3762
|
-
const body = await c.req.json();
|
|
3763
|
-
if (!body.team_id) {
|
|
3764
|
-
throw new ValidationError("team_id is required");
|
|
3765
|
-
}
|
|
3766
|
-
const userId = c.get("userId");
|
|
3767
|
-
const teams = await shareTeamWithNest({
|
|
3768
|
-
nestId: c.req.param("nestId"),
|
|
3769
|
-
teamId: body.team_id,
|
|
3770
|
-
callerUserId: userId,
|
|
3771
|
-
allowAdmin: await isLicenseAdminUserId(userId),
|
|
3772
|
-
baseUrl: requestBaseUrl(c.req.url)
|
|
3773
|
-
});
|
|
3774
|
-
return c.json({ teams }, 201);
|
|
3775
|
-
});
|
|
3776
|
-
nestTeamRoutes.delete("/:teamId", async (c) => {
|
|
3777
|
-
const teams = await unshareTeamFromNest({
|
|
3778
|
-
nestId: c.req.param("nestId"),
|
|
3779
|
-
teamId: c.req.param("teamId")
|
|
3780
|
-
});
|
|
3781
|
-
return c.json({ teams });
|
|
3782
|
-
});
|
|
3783
|
-
|
|
3784
|
-
// src/governance/team-routes.ts
|
|
3785
|
-
import { Hono as Hono7 } from "hono";
|
|
3786
4076
|
|
|
3787
|
-
// src/
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
case "owner":
|
|
3798
|
-
return "admin";
|
|
3799
|
-
case "editor":
|
|
3800
|
-
return "editor";
|
|
3801
|
-
default:
|
|
3802
|
-
return "viewer";
|
|
3803
|
-
}
|
|
3804
|
-
}
|
|
3805
|
-
async function fetchOwnedPromptowlTeams(token) {
|
|
3806
|
-
if (!token?.trim()) throw new ValidationError("token is required");
|
|
3807
|
-
const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
|
|
3808
|
-
let res;
|
|
4077
|
+
// src/nests/publish-service.ts
|
|
4078
|
+
import { v4 as uuid6 } from "uuid";
|
|
4079
|
+
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
4080
|
+
var MODES = ["public", "email", "code", "invite"];
|
|
4081
|
+
var LINK_TTL_SECONDS = 60 * 60;
|
|
4082
|
+
var SESSION_TTL_SECONDS = 24 * 60 * 60;
|
|
4083
|
+
function sha256Hex(s) {
|
|
4084
|
+
return createHash2("sha256").update(s).digest("hex");
|
|
4085
|
+
}
|
|
4086
|
+
async function nodeExists(nestId, nodeId) {
|
|
3809
4087
|
try {
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
Authorization: `Bearer ${token}`
|
|
3814
|
-
}
|
|
3815
|
-
});
|
|
4088
|
+
const { storage } = await engineCache.get(nestId);
|
|
4089
|
+
await storage.readDocument(nodeId);
|
|
4090
|
+
return true;
|
|
3816
4091
|
} catch {
|
|
3817
|
-
|
|
4092
|
+
return false;
|
|
3818
4093
|
}
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
4094
|
+
}
|
|
4095
|
+
function newSlug() {
|
|
4096
|
+
return randomBytes3(8).toString("base64url");
|
|
4097
|
+
}
|
|
4098
|
+
function newToken() {
|
|
4099
|
+
return randomBytes3(32).toString("hex");
|
|
4100
|
+
}
|
|
4101
|
+
function nowIso() {
|
|
4102
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4103
|
+
}
|
|
4104
|
+
function isPast(iso) {
|
|
4105
|
+
return !!iso && new Date(iso).getTime() < Date.now();
|
|
4106
|
+
}
|
|
4107
|
+
function isLivePublish(row) {
|
|
4108
|
+
return !!row && !row.revoked_at && !isPast(row.expires_at);
|
|
4109
|
+
}
|
|
4110
|
+
function toPublishResponse(row) {
|
|
4111
|
+
return {
|
|
4112
|
+
id: row.id,
|
|
4113
|
+
slug: row.slug,
|
|
4114
|
+
node_id: row.node_id,
|
|
4115
|
+
access_mode: row.access_mode,
|
|
4116
|
+
comments_enabled: row.comments_enabled === 1,
|
|
4117
|
+
watermark: row.watermark === 1,
|
|
4118
|
+
no_download: row.no_download === 1,
|
|
4119
|
+
created_at: row.created_at,
|
|
4120
|
+
expires_at: row.expires_at,
|
|
4121
|
+
revoked_at: row.revoked_at
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
async function createPublish(params) {
|
|
4125
|
+
const { nestId, mode, createdBy } = params;
|
|
4126
|
+
const nodeId = (params.nodeId || "").trim();
|
|
4127
|
+
if (!MODES.includes(mode)) {
|
|
4128
|
+
throw new ValidationError("access_mode must be public | email | code | invite");
|
|
3823
4129
|
}
|
|
3824
|
-
if (!
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
);
|
|
4130
|
+
if (!nodeId) throw new ValidationError("node_id is required");
|
|
4131
|
+
assertSafeNodeId(nodeId);
|
|
4132
|
+
if (mode === "code" && !params.code?.trim()) {
|
|
4133
|
+
throw new ValidationError("code is required for a code-gated publish");
|
|
3828
4134
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
}
|
|
3832
|
-
async function syncPromptowlTeams(params) {
|
|
3833
|
-
if (!params.teamIds?.length) {
|
|
3834
|
-
throw new ValidationError("team_ids is required");
|
|
4135
|
+
if (params.expiresAt && Number.isNaN(new Date(params.expiresAt).getTime())) {
|
|
4136
|
+
throw new ValidationError("expires_at must be an ISO date");
|
|
3835
4137
|
}
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
4138
|
+
if (!await nodeExists(nestId, nodeId)) {
|
|
4139
|
+
throw new ValidationError(`No such document in this nest: ${nodeId}`);
|
|
4140
|
+
}
|
|
4141
|
+
const db = getDb();
|
|
4142
|
+
const id = uuid6();
|
|
4143
|
+
const slug = newSlug();
|
|
4144
|
+
const created = nowIso();
|
|
4145
|
+
const codeHash = mode === "code" && params.code ? await hashPassword(params.code.trim()) : null;
|
|
4146
|
+
await db.run(
|
|
4147
|
+
`INSERT INTO publish
|
|
4148
|
+
(id, nest_id, node_id, slug, access_mode, code_hash, comments_enabled,
|
|
4149
|
+
watermark, no_download, created_by, created_at, expires_at, revoked_at)
|
|
4150
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`,
|
|
4151
|
+
[
|
|
4152
|
+
id,
|
|
4153
|
+
nestId,
|
|
4154
|
+
nodeId,
|
|
4155
|
+
slug,
|
|
4156
|
+
mode,
|
|
4157
|
+
codeHash,
|
|
4158
|
+
params.commentsEnabled ? 1 : 0,
|
|
4159
|
+
params.watermark ? 1 : 0,
|
|
4160
|
+
params.noDownload ? 1 : 0,
|
|
4161
|
+
createdBy,
|
|
4162
|
+
created,
|
|
4163
|
+
params.expiresAt ?? null
|
|
4164
|
+
]
|
|
4165
|
+
);
|
|
4166
|
+
if (mode === "invite") {
|
|
4167
|
+
for (const raw of params.allowlist ?? []) {
|
|
4168
|
+
const email = normalizeEmail(raw);
|
|
4169
|
+
if (!email) continue;
|
|
4170
|
+
await db.run(
|
|
4171
|
+
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4172
|
+
VALUES (?, ?, 'invite', ?, NULL, NULL, NULL, ?)`,
|
|
4173
|
+
[uuid6(), id, email, created]
|
|
3849
4174
|
);
|
|
3850
4175
|
}
|
|
3851
|
-
selected.push(team);
|
|
3852
4176
|
}
|
|
3853
|
-
const
|
|
3854
|
-
|
|
3855
|
-
|
|
4177
|
+
const row = await getPublishBySlug(slug);
|
|
4178
|
+
if (!row) throw new ValidationError("failed to create publish");
|
|
4179
|
+
return row;
|
|
4180
|
+
}
|
|
4181
|
+
async function getPublishBySlug(slug) {
|
|
4182
|
+
return await getDb().get("SELECT * FROM publish WHERE slug = ?", [slug]);
|
|
4183
|
+
}
|
|
4184
|
+
async function getPublishById(nestId, id) {
|
|
4185
|
+
return await getDb().get("SELECT * FROM publish WHERE id = ? AND nest_id = ?", [
|
|
4186
|
+
id,
|
|
4187
|
+
nestId
|
|
3856
4188
|
]);
|
|
3857
|
-
if (!caller?.email) {
|
|
3858
|
-
throw new ValidationError("Could not resolve your account.");
|
|
3859
|
-
}
|
|
3860
|
-
const results = [];
|
|
3861
|
-
for (const poTeam of selected) {
|
|
3862
|
-
results.push(await syncOne(poTeam, params.callerUserId, caller.email));
|
|
3863
|
-
}
|
|
3864
|
-
return results;
|
|
3865
4189
|
}
|
|
3866
|
-
async function
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
externalId: poTeam.id
|
|
3871
|
-
});
|
|
3872
|
-
const team = existing ?? await createTeam({
|
|
3873
|
-
name: poTeam.name,
|
|
3874
|
-
ownerId: callerUserId,
|
|
3875
|
-
source: PROMPTOWL_SOURCE,
|
|
3876
|
-
externalId: poTeam.id
|
|
3877
|
-
});
|
|
3878
|
-
if (existing && existing.name !== poTeam.name) {
|
|
3879
|
-
await renameTeam({
|
|
3880
|
-
teamId: team.id,
|
|
3881
|
-
name: poTeam.name,
|
|
3882
|
-
callerUserId,
|
|
3883
|
-
allowAdmin: false
|
|
3884
|
-
});
|
|
3885
|
-
}
|
|
3886
|
-
const desired = await desiredRoster(poTeam, callerEmail);
|
|
3887
|
-
const before = new Map(team.members.map((m) => [m.email, m.role]));
|
|
3888
|
-
const after = new Map(desired.map((m) => [m.email, m.role]));
|
|
3889
|
-
const added = [...after.keys()].filter((e) => !before.has(e));
|
|
3890
|
-
const removed = [...before.keys()].filter((e) => !after.has(e));
|
|
3891
|
-
const updated = [...after.keys()].filter(
|
|
3892
|
-
(e) => before.has(e) && before.get(e) !== after.get(e)
|
|
4190
|
+
async function listPublish(nestId) {
|
|
4191
|
+
return await getDb().all(
|
|
4192
|
+
"SELECT * FROM publish WHERE nest_id = ? ORDER BY created_at DESC",
|
|
4193
|
+
[nestId]
|
|
3893
4194
|
);
|
|
3894
|
-
if (added.length || removed.length || updated.length) {
|
|
3895
|
-
await replaceTeamRoster({
|
|
3896
|
-
teamId: team.id,
|
|
3897
|
-
members: desired,
|
|
3898
|
-
callerUserId,
|
|
3899
|
-
allowAdmin: false
|
|
3900
|
-
});
|
|
3901
|
-
}
|
|
3902
|
-
return {
|
|
3903
|
-
team_id: team.id,
|
|
3904
|
-
external_id: poTeam.id,
|
|
3905
|
-
name: poTeam.name,
|
|
3906
|
-
created: !existing,
|
|
3907
|
-
added,
|
|
3908
|
-
removed,
|
|
3909
|
-
updated
|
|
3910
|
-
};
|
|
3911
4195
|
}
|
|
3912
|
-
async function
|
|
3913
|
-
const
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4196
|
+
async function revokePublish(nestId, id) {
|
|
4197
|
+
const res = await getDb().run(
|
|
4198
|
+
"UPDATE publish SET revoked_at = ? WHERE id = ? AND nest_id = ? AND revoked_at IS NULL",
|
|
4199
|
+
[nowIso(), id, nestId]
|
|
4200
|
+
);
|
|
4201
|
+
return res.changes > 0;
|
|
4202
|
+
}
|
|
4203
|
+
async function verifyCode(row, code) {
|
|
4204
|
+
if (row.access_mode !== "code" || !row.code_hash) return false;
|
|
4205
|
+
return (await verifyPassword((code || "").trim(), row.code_hash)).ok;
|
|
4206
|
+
}
|
|
4207
|
+
async function isAllowlisted(publishId, email) {
|
|
4208
|
+
const e = normalizeEmail(email);
|
|
4209
|
+
if (!e) return false;
|
|
4210
|
+
const row = await getDb().get(
|
|
4211
|
+
"SELECT 1 AS x FROM access_grant WHERE publish_id = ? AND kind = 'invite' AND email = ? LIMIT 1",
|
|
4212
|
+
[publishId, e]
|
|
4213
|
+
);
|
|
4214
|
+
return !!row;
|
|
4215
|
+
}
|
|
4216
|
+
async function mintMagicLink(publishId, email) {
|
|
4217
|
+
const e = normalizeEmail(email);
|
|
4218
|
+
const token = newToken();
|
|
4219
|
+
const expires = new Date(Date.now() + LINK_TTL_SECONDS * 1e3).toISOString();
|
|
4220
|
+
await getDb().run(
|
|
4221
|
+
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4222
|
+
VALUES (?, ?, 'link', ?, ?, ?, NULL, ?)`,
|
|
4223
|
+
[uuid6(), publishId, e, sha256Hex(token), expires, nowIso()]
|
|
4224
|
+
);
|
|
4225
|
+
return token;
|
|
4226
|
+
}
|
|
4227
|
+
async function consumeMagicLink(publishId, token) {
|
|
4228
|
+
if (!token) return null;
|
|
4229
|
+
const row = await getDb().get(
|
|
4230
|
+
"SELECT id, email, expires_at, used_at FROM access_grant WHERE publish_id = ? AND kind = 'link' AND token_hash = ?",
|
|
4231
|
+
[publishId, sha256Hex(token)]
|
|
4232
|
+
);
|
|
4233
|
+
if (!row || row.used_at || isPast(row.expires_at)) return null;
|
|
4234
|
+
const publishRow = await getDb().get("SELECT access_mode FROM publish WHERE id = ?", [
|
|
4235
|
+
publishId
|
|
4236
|
+
]);
|
|
4237
|
+
if (publishRow?.access_mode === "invite") {
|
|
4238
|
+
if (!row.email || !await isAllowlisted(publishId, row.email)) return null;
|
|
3927
4239
|
}
|
|
3928
|
-
|
|
4240
|
+
const res = await getDb().run(
|
|
4241
|
+
"UPDATE access_grant SET used_at = ? WHERE id = ? AND used_at IS NULL",
|
|
4242
|
+
[nowIso(), row.id]
|
|
4243
|
+
);
|
|
4244
|
+
if (res.changes === 0) return null;
|
|
4245
|
+
return row.email;
|
|
4246
|
+
}
|
|
4247
|
+
async function createSession2(publishId, email) {
|
|
4248
|
+
const token = newToken();
|
|
4249
|
+
const expires = new Date(Date.now() + SESSION_TTL_SECONDS * 1e3).toISOString();
|
|
4250
|
+
await getDb().run(
|
|
4251
|
+
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4252
|
+
VALUES (?, ?, 'session', ?, ?, ?, NULL, ?)`,
|
|
4253
|
+
[uuid6(), publishId, email, sha256Hex(token), expires, nowIso()]
|
|
4254
|
+
);
|
|
4255
|
+
return token;
|
|
4256
|
+
}
|
|
4257
|
+
async function verifySession(publishId, token) {
|
|
4258
|
+
if (!token) return null;
|
|
4259
|
+
const row = await getDb().get(
|
|
4260
|
+
"SELECT email, expires_at FROM access_grant WHERE publish_id = ? AND kind = 'session' AND token_hash = ?",
|
|
4261
|
+
[publishId, sha256Hex(token)]
|
|
4262
|
+
);
|
|
4263
|
+
if (!row || isPast(row.expires_at)) return null;
|
|
4264
|
+
return { email: row.email };
|
|
4265
|
+
}
|
|
4266
|
+
async function logView(publishId, email) {
|
|
4267
|
+
await getDb().run(
|
|
4268
|
+
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4269
|
+
VALUES (?, ?, 'view', ?, NULL, NULL, NULL, ?)`,
|
|
4270
|
+
[uuid6(), publishId, email, nowIso()]
|
|
4271
|
+
);
|
|
4272
|
+
}
|
|
4273
|
+
async function listAllowlist(publishId) {
|
|
4274
|
+
const rows = await getDb().all(
|
|
4275
|
+
"SELECT email FROM access_grant WHERE publish_id = ? AND kind = 'invite' ORDER BY created_at ASC",
|
|
4276
|
+
[publishId]
|
|
4277
|
+
);
|
|
4278
|
+
return rows.map((r) => r.email ?? "").filter(Boolean);
|
|
4279
|
+
}
|
|
4280
|
+
async function listViews(publishId) {
|
|
4281
|
+
return await getDb().all(
|
|
4282
|
+
"SELECT email, created_at FROM access_grant WHERE publish_id = ? AND kind = 'view' ORDER BY created_at DESC LIMIT 500",
|
|
4283
|
+
[publishId]
|
|
4284
|
+
);
|
|
3929
4285
|
}
|
|
3930
4286
|
|
|
3931
|
-
// src/
|
|
3932
|
-
var
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
if (!config.PROMPTOWL_TEAMS_ENABLED) {
|
|
3938
|
-
return c.json({ error: "Not found" }, 404);
|
|
3939
|
-
}
|
|
3940
|
-
if (!tryConsume(`po-teams:ip:${clientIp(c)}`, PROMPTOWL_LIMIT)) {
|
|
3941
|
-
return c.json({ error: "Too many PromptOwl requests, try again later" }, 429);
|
|
3942
|
-
}
|
|
3943
|
-
const token = getPromptowlTokenFromRequest(c);
|
|
3944
|
-
if (!token) {
|
|
3945
|
-
return c.json(
|
|
3946
|
-
{
|
|
3947
|
-
error: "Sign in with PromptOwl to import teams.",
|
|
3948
|
-
code: CONNECT_REQUIRED
|
|
3949
|
-
},
|
|
3950
|
-
CONNECT_REQUIRED_STATUS
|
|
4287
|
+
// src/nests/publish-routes.ts
|
|
4288
|
+
var publishRoutes = new Hono6();
|
|
4289
|
+
async function requireManage2(c, nestId) {
|
|
4290
|
+
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
4291
|
+
throw new ForbiddenError(
|
|
4292
|
+
"Only a nest admin, the nest owner, or the server admin can publish documents."
|
|
3951
4293
|
);
|
|
3952
4294
|
}
|
|
3953
|
-
try {
|
|
3954
|
-
return c.json(await run(token));
|
|
3955
|
-
} catch (err) {
|
|
3956
|
-
if (err instanceof UnauthorizedError) {
|
|
3957
|
-
c.header(
|
|
3958
|
-
"Set-Cookie",
|
|
3959
|
-
buildPromptowlTokenCookie("", { secure: isSecureRequest(c), clear: true }),
|
|
3960
|
-
{ append: true }
|
|
3961
|
-
);
|
|
3962
|
-
return c.json(
|
|
3963
|
-
{ error: err.message, code: CONNECT_REQUIRED },
|
|
3964
|
-
CONNECT_REQUIRED_STATUS
|
|
3965
|
-
);
|
|
3966
|
-
}
|
|
3967
|
-
throw err;
|
|
3968
|
-
}
|
|
3969
4295
|
}
|
|
3970
|
-
|
|
3971
|
-
"
|
|
3972
|
-
|
|
3973
|
-
teams: await fetchOwnedPromptowlTeams(token)
|
|
3974
|
-
}))
|
|
3975
|
-
);
|
|
3976
|
-
teamRoutes.post("/promptowl/sync", async (c) => {
|
|
3977
|
-
const body = await c.req.json().catch(() => ({}));
|
|
3978
|
-
return withPromptowlToken(c, async (token) => ({
|
|
3979
|
-
results: await syncPromptowlTeams({
|
|
3980
|
-
callerUserId: c.get("userId"),
|
|
3981
|
-
token,
|
|
3982
|
-
teamIds: body.team_ids ?? []
|
|
3983
|
-
})
|
|
3984
|
-
}));
|
|
3985
|
-
});
|
|
3986
|
-
teamRoutes.post("/", async (c) => {
|
|
4296
|
+
publishRoutes.post("/", async (c) => {
|
|
4297
|
+
const nestId = c.req.param("nestId");
|
|
4298
|
+
await requireManage2(c, nestId);
|
|
3987
4299
|
const body = await c.req.json();
|
|
3988
|
-
const
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
});
|
|
3994
|
-
teamRoutes.get("/", async (c) => {
|
|
3995
|
-
const teams = await listTeamsForUser(c.get("userId"));
|
|
3996
|
-
return c.json({ teams });
|
|
3997
|
-
});
|
|
3998
|
-
teamRoutes.get("/:teamId", async (c) => {
|
|
3999
|
-
const userId = c.get("userId");
|
|
4000
|
-
const team = await getTeam(c.req.param("teamId"));
|
|
4001
|
-
const isMember = team.members.some((m) => m.userId === userId);
|
|
4002
|
-
if (team.owner_id !== userId && !isMember && !await isLicenseAdminUserId(userId)) {
|
|
4003
|
-
throw new ForbiddenError("You don't have access to this team.");
|
|
4300
|
+
const mode = body.access_mode ?? "public";
|
|
4301
|
+
if ((mode === "email" || mode === "invite") && !isEmailConfigured()) {
|
|
4302
|
+
throw new ValidationError(
|
|
4303
|
+
"Email and invite links need email set up on this server (SMTP_URL and NOTIFY_EMAIL_FROM). Use a public or passcode link instead."
|
|
4304
|
+
);
|
|
4004
4305
|
}
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
return c.json({ team });
|
|
4017
|
-
});
|
|
4018
|
-
teamRoutes.delete("/:teamId", async (c) => {
|
|
4019
|
-
const userId = c.get("userId");
|
|
4020
|
-
await deleteTeam({
|
|
4021
|
-
teamId: c.req.param("teamId"),
|
|
4022
|
-
callerUserId: userId,
|
|
4023
|
-
allowAdmin: await isLicenseAdminUserId(userId)
|
|
4306
|
+
const row = await createPublish({
|
|
4307
|
+
nestId,
|
|
4308
|
+
nodeId: (body.node_id ?? "").trim(),
|
|
4309
|
+
mode,
|
|
4310
|
+
commentsEnabled: !!body.comments_enabled,
|
|
4311
|
+
watermark: !!body.watermark,
|
|
4312
|
+
noDownload: !!body.no_download,
|
|
4313
|
+
code: body.code ?? null,
|
|
4314
|
+
allowlist: Array.isArray(body.allowlist) ? body.allowlist : [],
|
|
4315
|
+
expiresAt: body.expires_at ?? null,
|
|
4316
|
+
createdBy: c.get("userId")
|
|
4024
4317
|
});
|
|
4025
|
-
|
|
4318
|
+
const link = `${requestBaseUrl(c.req.url)}/p/${row.slug}`;
|
|
4319
|
+
if (mode === "invite") {
|
|
4320
|
+
for (const email of await listAllowlist(row.id)) {
|
|
4321
|
+
void sendEmailToRecipient(email, {
|
|
4322
|
+
status: "shared",
|
|
4323
|
+
docTitle: "A document has been shared with you",
|
|
4324
|
+
path: "PromptOwl \xB7 ContextNest",
|
|
4325
|
+
permission: "view",
|
|
4326
|
+
link
|
|
4327
|
+
});
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
return c.json({ publish: toPublishResponse(row), link }, 201);
|
|
4026
4331
|
});
|
|
4027
|
-
|
|
4028
|
-
const
|
|
4029
|
-
|
|
4030
|
-
const
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
callerUserId: userId,
|
|
4036
|
-
allowAdmin: await isLicenseAdminUserId(userId)
|
|
4332
|
+
publishRoutes.get("/", async (c) => {
|
|
4333
|
+
const nestId = c.req.param("nestId");
|
|
4334
|
+
await requireManage2(c, nestId);
|
|
4335
|
+
const base = requestBaseUrl(c.req.url);
|
|
4336
|
+
const rows = await listPublish(nestId);
|
|
4337
|
+
return c.json({
|
|
4338
|
+
count: rows.length,
|
|
4339
|
+
publishes: rows.map((r) => ({ ...toPublishResponse(r), link: `${base}/p/${r.slug}` }))
|
|
4037
4340
|
});
|
|
4038
|
-
return c.json({ team }, 201);
|
|
4039
4341
|
});
|
|
4040
|
-
|
|
4041
|
-
const
|
|
4042
|
-
|
|
4043
|
-
const
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
callerUserId: userId,
|
|
4048
|
-
allowAdmin: await isLicenseAdminUserId(userId)
|
|
4049
|
-
});
|
|
4050
|
-
return c.json({ team });
|
|
4342
|
+
publishRoutes.get("/:id/views", async (c) => {
|
|
4343
|
+
const nestId = c.req.param("nestId");
|
|
4344
|
+
await requireManage2(c, nestId);
|
|
4345
|
+
const row = await getPublishById(nestId, c.req.param("id"));
|
|
4346
|
+
if (!row) throw new NotFoundError("Publish not found");
|
|
4347
|
+
const views = await listViews(row.id);
|
|
4348
|
+
return c.json({ count: views.length, views });
|
|
4051
4349
|
});
|
|
4052
|
-
|
|
4053
|
-
const
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
allowAdmin: await isLicenseAdminUserId(userId)
|
|
4059
|
-
});
|
|
4060
|
-
return c.json({ team });
|
|
4350
|
+
publishRoutes.delete("/:id", async (c) => {
|
|
4351
|
+
const nestId = c.req.param("nestId");
|
|
4352
|
+
await requireManage2(c, nestId);
|
|
4353
|
+
const ok = await revokePublish(nestId, c.req.param("id"));
|
|
4354
|
+
if (!ok) throw new NotFoundError("Publish not found");
|
|
4355
|
+
return c.json({ revoked: true });
|
|
4061
4356
|
});
|
|
4062
4357
|
|
|
4063
|
-
// src/
|
|
4064
|
-
|
|
4065
|
-
|
|
4358
|
+
// src/nests/publish-render-routes.ts
|
|
4359
|
+
import { Hono as Hono7 } from "hono";
|
|
4360
|
+
import MarkdownIt from "markdown-it";
|
|
4361
|
+
|
|
4362
|
+
// src/annotations/service.ts
|
|
4363
|
+
import { v4 as uuid7 } from "uuid";
|
|
4364
|
+
|
|
4365
|
+
// src/annotations/projection.ts
|
|
4366
|
+
var MAX_CONTEXT_CHARS = 250;
|
|
4367
|
+
var MAX_QUOTE_CHARS = 1e3;
|
|
4368
|
+
function clampAnchor(raw) {
|
|
4369
|
+
if (!raw) return null;
|
|
4370
|
+
const quote = (raw.quote ?? "").trim().slice(0, MAX_QUOTE_CHARS);
|
|
4371
|
+
if (!quote) return null;
|
|
4372
|
+
const before = (raw.before ?? "").slice(-MAX_CONTEXT_CHARS);
|
|
4373
|
+
const after = (raw.after ?? "").slice(0, MAX_CONTEXT_CHARS);
|
|
4374
|
+
const line = typeof raw.line === "number" && Number.isFinite(raw.line) && raw.line > 0 ? Math.floor(raw.line) : void 0;
|
|
4375
|
+
return { quote, before, after, ...line !== void 0 ? { line } : {} };
|
|
4066
4376
|
}
|
|
4067
|
-
function
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
if (
|
|
4072
|
-
|
|
4073
|
-
for (let i = t.indexOf(q); i !== -1; i = t.indexOf(q, i + 1)) {
|
|
4074
|
-
if (i === 0) return 3;
|
|
4075
|
-
if (!/[a-z0-9]/.test(t[i - 1])) return 2;
|
|
4076
|
-
midWord = true;
|
|
4377
|
+
function oneLine(s) {
|
|
4378
|
+
return s.replace(/\s+/g, " ").replace(/--+>/g, "-\u2192").trim();
|
|
4379
|
+
}
|
|
4380
|
+
function renderAnchor(anchor) {
|
|
4381
|
+
if (!anchor) {
|
|
4382
|
+
return "<!-- anchor: whole-artifact -->";
|
|
4077
4383
|
}
|
|
4078
|
-
|
|
4079
|
-
const
|
|
4080
|
-
|
|
4081
|
-
|
|
4384
|
+
const linePart = anchor.line !== void 0 ? `line ${anchor.line} \xB7 ` : "";
|
|
4385
|
+
const context = `\u2026${oneLine(anchor.before)}\u3008${oneLine(anchor.quote)}\u3009${oneLine(
|
|
4386
|
+
anchor.after
|
|
4387
|
+
)}\u2026`;
|
|
4388
|
+
return `<!-- anchor: ${linePart}quote "${oneLine(anchor.quote)}" \xB7 context ${context} -->`;
|
|
4082
4389
|
}
|
|
4083
|
-
function
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
const
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4390
|
+
function statusHeading(thread) {
|
|
4391
|
+
if (thread.status === "resolved") {
|
|
4392
|
+
const who = thread.resolvedBy ? `${thread.resolvedBy}, ` : "";
|
|
4393
|
+
const when = thread.resolvedAt ? `${thread.resolvedAt}` : "";
|
|
4394
|
+
const meta = who || when ? ` (${who}${when})` : "";
|
|
4395
|
+
return `\u2705 RESOLVED${meta}`;
|
|
4396
|
+
}
|
|
4397
|
+
return "\u{1F7E0} OPEN";
|
|
4398
|
+
}
|
|
4399
|
+
function snapshotLabel(v) {
|
|
4400
|
+
return v == null ? "Unpinned" : `Snapshot v${v}`;
|
|
4401
|
+
}
|
|
4402
|
+
function orderThreads(threads) {
|
|
4403
|
+
return [...threads].sort((a, b) => {
|
|
4404
|
+
if (a.status !== b.status) return a.status === "open" ? -1 : 1;
|
|
4405
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
function projectThreadsToMarkdown(artifactTitle, threads) {
|
|
4409
|
+
const lines = [];
|
|
4410
|
+
lines.push(`# ${artifactTitle} \u2014 Annotations`);
|
|
4411
|
+
lines.push("");
|
|
4412
|
+
lines.push(
|
|
4413
|
+
"> Auto-generated from review comments, grouped by the artifact snapshot each was anchored to. **Open** threads are unresolved feedback for the next iteration; **resolved** threads were already addressed (don't re-break them)."
|
|
4096
4414
|
);
|
|
4415
|
+
lines.push("");
|
|
4416
|
+
if (threads.length === 0) {
|
|
4417
|
+
lines.push("_No annotations yet._");
|
|
4418
|
+
lines.push("");
|
|
4419
|
+
return lines.join("\n");
|
|
4420
|
+
}
|
|
4421
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
4422
|
+
for (const t of threads) {
|
|
4423
|
+
const key = t.snapshotVersion ?? null;
|
|
4424
|
+
(buckets2.get(key) ?? buckets2.set(key, []).get(key)).push(t);
|
|
4425
|
+
}
|
|
4426
|
+
const keys = [...buckets2.keys()].sort((a, b) => {
|
|
4427
|
+
if (a == null) return 1;
|
|
4428
|
+
if (b == null) return -1;
|
|
4429
|
+
return b - a;
|
|
4430
|
+
});
|
|
4431
|
+
for (const key of keys) {
|
|
4432
|
+
lines.push(`## ${snapshotLabel(key)}`);
|
|
4433
|
+
lines.push("");
|
|
4434
|
+
for (const thread of orderThreads(buckets2.get(key))) {
|
|
4435
|
+
lines.push(`### Thread \xB7 ${statusHeading(thread)}`);
|
|
4436
|
+
lines.push(renderAnchor(thread.anchor));
|
|
4437
|
+
for (const comment of thread.comments) {
|
|
4438
|
+
lines.push(
|
|
4439
|
+
`- **${comment.author}** \xB7 ${comment.createdAt} \u2014 ${comment.body}`
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
lines.push("");
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
return lines.join("\n");
|
|
4097
4446
|
}
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
const
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4447
|
+
|
|
4448
|
+
// src/annotations/service.ts
|
|
4449
|
+
async function loadComments(threadId) {
|
|
4450
|
+
const db = getDb();
|
|
4451
|
+
const rows = await db.all(
|
|
4452
|
+
"SELECT id, author, body, created_at FROM annotation_comments WHERE thread_id = ? ORDER BY created_at ASC, id ASC",
|
|
4453
|
+
[threadId]
|
|
4454
|
+
);
|
|
4455
|
+
return rows.map((r) => ({
|
|
4456
|
+
id: r.id,
|
|
4457
|
+
author: r.author,
|
|
4458
|
+
body: r.body,
|
|
4459
|
+
createdAt: r.created_at
|
|
4460
|
+
}));
|
|
4105
4461
|
}
|
|
4106
|
-
function
|
|
4462
|
+
async function rowToThread(row) {
|
|
4463
|
+
let anchor = null;
|
|
4464
|
+
if (row.anchor_json) {
|
|
4465
|
+
try {
|
|
4466
|
+
anchor = JSON.parse(row.anchor_json);
|
|
4467
|
+
} catch {
|
|
4468
|
+
anchor = null;
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4107
4471
|
return {
|
|
4108
|
-
id:
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4472
|
+
id: row.id,
|
|
4473
|
+
nestId: row.nest_id,
|
|
4474
|
+
nodeId: row.node_id,
|
|
4475
|
+
snapshotVersion: row.snapshot_version,
|
|
4476
|
+
anchor,
|
|
4477
|
+
status: row.status,
|
|
4478
|
+
createdBy: row.created_by,
|
|
4479
|
+
createdAt: row.created_at,
|
|
4480
|
+
resolvedBy: row.resolved_by,
|
|
4481
|
+
resolvedAt: row.resolved_at,
|
|
4482
|
+
comments: await loadComments(row.id)
|
|
4113
4483
|
};
|
|
4114
4484
|
}
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
import { Hono as Hono8 } from "hono";
|
|
4118
|
-
|
|
4119
|
-
// src/nodes/markdown-export.ts
|
|
4120
|
-
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
4121
|
-
function nodeToMarkdown(node) {
|
|
4122
|
-
const tags = node.tags ?? [];
|
|
4123
|
-
return [
|
|
4124
|
-
"---",
|
|
4125
|
-
`title: ${JSON.stringify(node.title ?? "")}`,
|
|
4126
|
-
`tags: [${tags.map((t) => JSON.stringify(t)).join(", ")}]`,
|
|
4127
|
-
`status: ${JSON.stringify(node.status ?? "")}`,
|
|
4128
|
-
`id: ${JSON.stringify(node.id)}`,
|
|
4129
|
-
"---",
|
|
4130
|
-
""
|
|
4131
|
-
].join("\n") + (node.body ?? "");
|
|
4132
|
-
}
|
|
4133
|
-
function nodesToMarkdown(nodes) {
|
|
4134
|
-
return nodes.map(nodeToMarkdown).join("\n\n---\n\n");
|
|
4485
|
+
async function getThreadRow(threadId) {
|
|
4486
|
+
return await getDb().get("SELECT * FROM annotation_threads WHERE id = ?", [threadId]);
|
|
4135
4487
|
}
|
|
4136
|
-
function
|
|
4137
|
-
|
|
4488
|
+
async function countThreadsByNode(nestId) {
|
|
4489
|
+
const rows = await getDb().all(
|
|
4490
|
+
`SELECT node_id, CAST(COUNT(*) AS INTEGER) AS count, MAX(created_at) AS last_at
|
|
4491
|
+
FROM annotation_threads
|
|
4492
|
+
WHERE nest_id = ?
|
|
4493
|
+
GROUP BY node_id`,
|
|
4494
|
+
[nestId]
|
|
4495
|
+
);
|
|
4496
|
+
const out = {};
|
|
4497
|
+
for (const r of rows) out[r.node_id] = { count: r.count, lastAt: r.last_at };
|
|
4498
|
+
return out;
|
|
4138
4499
|
}
|
|
4139
|
-
function
|
|
4140
|
-
const
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4500
|
+
async function listThreads(nestId, nodeId) {
|
|
4501
|
+
const rows = await getDb().all(
|
|
4502
|
+
"SELECT * FROM annotation_threads WHERE nest_id = ? AND node_id = ? ORDER BY created_at ASC, id ASC",
|
|
4503
|
+
[nestId, nodeId]
|
|
4504
|
+
);
|
|
4505
|
+
return Promise.all(rows.map(rowToThread));
|
|
4144
4506
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
}
|
|
4150
|
-
|
|
4151
|
-
|
|
4507
|
+
async function createThread(nestId, nodeId, input, authorEmail) {
|
|
4508
|
+
const body = (input.body ?? "").trim();
|
|
4509
|
+
if (!body) {
|
|
4510
|
+
throw new Error("comment body is required");
|
|
4511
|
+
}
|
|
4512
|
+
const db = getDb();
|
|
4513
|
+
const id = uuid7();
|
|
4514
|
+
const anchor = clampAnchor(input.anchor);
|
|
4515
|
+
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
4516
|
+
await db.transaction(async (tx) => {
|
|
4517
|
+
await tx.run(
|
|
4518
|
+
`INSERT INTO annotation_threads
|
|
4519
|
+
(id, nest_id, node_id, snapshot_version, anchor_json, status, created_by)
|
|
4520
|
+
VALUES (?, ?, ?, ?, ?, 'open', ?)`,
|
|
4521
|
+
[
|
|
4522
|
+
id,
|
|
4523
|
+
nestId,
|
|
4524
|
+
nodeId,
|
|
4525
|
+
snapshot,
|
|
4526
|
+
anchor ? JSON.stringify(anchor) : null,
|
|
4527
|
+
authorEmail
|
|
4528
|
+
]
|
|
4529
|
+
);
|
|
4530
|
+
await tx.run(
|
|
4531
|
+
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
|
|
4532
|
+
[uuid7(), id, authorEmail, body]
|
|
4533
|
+
);
|
|
4534
|
+
});
|
|
4535
|
+
return rowToThread(await getThreadRow(id));
|
|
4152
4536
|
}
|
|
4153
|
-
function
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
if (raw.endsWith(body)) return raw.slice(0, raw.length - body.length);
|
|
4158
|
-
} catch {
|
|
4537
|
+
async function getScopedThreadRow(threadId, nestId, nodeId) {
|
|
4538
|
+
const row = await getThreadRow(threadId);
|
|
4539
|
+
if (!row || row.nest_id !== nestId || row.node_id !== nodeId) {
|
|
4540
|
+
throw new NotFoundError(`Thread not found: ${threadId}`);
|
|
4159
4541
|
}
|
|
4160
|
-
|
|
4161
|
-
return m ? `---
|
|
4162
|
-
${m[1]}
|
|
4163
|
-
---
|
|
4164
|
-
|
|
4165
|
-
` : "";
|
|
4542
|
+
return row;
|
|
4166
4543
|
}
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4544
|
+
async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
4545
|
+
const trimmed = (body ?? "").trim();
|
|
4546
|
+
if (!trimmed) {
|
|
4547
|
+
throw new Error("comment body is required");
|
|
4548
|
+
}
|
|
4549
|
+
await getScopedThreadRow(threadId, nestId, nodeId);
|
|
4550
|
+
await getDb().run(
|
|
4551
|
+
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
|
|
4552
|
+
[uuid7(), threadId, authorEmail, trimmed]
|
|
4553
|
+
);
|
|
4554
|
+
return rowToThread(await getThreadRow(threadId));
|
|
4178
4555
|
}
|
|
4179
|
-
function
|
|
4180
|
-
|
|
4181
|
-
const
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4556
|
+
async function setThreadStatus(nestId, nodeId, threadId, status, byEmail) {
|
|
4557
|
+
await getScopedThreadRow(threadId, nestId, nodeId);
|
|
4558
|
+
const db = getDb();
|
|
4559
|
+
if (status === "resolved") {
|
|
4560
|
+
await db.run(
|
|
4561
|
+
`UPDATE annotation_threads SET status = 'resolved', resolved_by = ?, resolved_at = ${nowExpr(db)} WHERE id = ?`,
|
|
4562
|
+
[byEmail, threadId]
|
|
4563
|
+
);
|
|
4564
|
+
} else {
|
|
4565
|
+
await db.run(
|
|
4566
|
+
"UPDATE annotation_threads SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?",
|
|
4567
|
+
[threadId]
|
|
4568
|
+
);
|
|
4185
4569
|
}
|
|
4186
|
-
return
|
|
4187
|
-
"Content-Type": rawContentType(response.type)
|
|
4188
|
-
});
|
|
4570
|
+
return rowToThread(await getThreadRow(threadId));
|
|
4189
4571
|
}
|
|
4190
|
-
|
|
4191
|
-
const
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
// document tree. Absent = the whole nest, as every existing caller expects.
|
|
4205
|
-
...q.folder !== void 0 ? { folder: q.folder } : {},
|
|
4206
|
-
// Bodies are off by default — the list is served from the DB index, which
|
|
4207
|
-
// holds metadata only. Opt back in and the listing crawls the vault for
|
|
4208
|
-
// them; kept for any client that was reading `content` off this route.
|
|
4209
|
-
...q.content === "1" || q.content === "true" ? { withContent: true } : {}
|
|
4210
|
-
});
|
|
4211
|
-
return c.json({ count: nodes.length, nodes });
|
|
4212
|
-
});
|
|
4213
|
-
nodeRoutes.post("/", async (c) => {
|
|
4214
|
-
const body = await c.req.json();
|
|
4215
|
-
if (!body.title || !body.content) {
|
|
4216
|
-
throw new ValidationError("title and content are required");
|
|
4572
|
+
async function readArtifact(nestId, nodeId, version) {
|
|
4573
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
4574
|
+
const target = version ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
4575
|
+
let title = nodeId;
|
|
4576
|
+
let type = null;
|
|
4577
|
+
let liveHtml = "";
|
|
4578
|
+
let liveExists = true;
|
|
4579
|
+
try {
|
|
4580
|
+
const node = await storage.readDocument(nodeId);
|
|
4581
|
+
title = node.frontmatter?.title || nodeId;
|
|
4582
|
+
type = node.frontmatter?.type || null;
|
|
4583
|
+
liveHtml = node.body || "";
|
|
4584
|
+
} catch {
|
|
4585
|
+
liveExists = false;
|
|
4217
4586
|
}
|
|
4218
|
-
if (
|
|
4219
|
-
|
|
4587
|
+
if (target != null) {
|
|
4588
|
+
try {
|
|
4589
|
+
const raw = await versionManager.reconstructVersion(nodeId, target);
|
|
4590
|
+
return { title, html: bodyOnly(nodeId, raw), version: target, type };
|
|
4591
|
+
} catch (err) {
|
|
4592
|
+
console.warn(
|
|
4593
|
+
`[annotations] reconstruct v${target} failed for ${nestId}/${nodeId}; falling back to live body:`,
|
|
4594
|
+
err
|
|
4595
|
+
);
|
|
4596
|
+
}
|
|
4220
4597
|
}
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
});
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
}
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
4270
|
-
const nestId = c.req.param("nestId");
|
|
4271
|
-
const nodeId = c.req.param("nodeId");
|
|
4272
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-LK4M2C43.js");
|
|
4273
|
-
const history = await getReviewHistory2(nestId, nodeId);
|
|
4274
|
-
return c.json({ reviews: history });
|
|
4275
|
-
});
|
|
4276
|
-
nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
4277
|
-
const nestId = c.req.param("nestId");
|
|
4278
|
-
const nodeId = c.req.param("nodeId");
|
|
4279
|
-
const { versions: versionManager } = await engineCache.get(nestId);
|
|
4280
|
-
const userId = c.get("userId");
|
|
4281
|
-
const userEmail = await resolveCallerEmail(userId);
|
|
4282
|
-
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
4283
|
-
return c.json(
|
|
4284
|
-
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
4285
|
-
403
|
|
4598
|
+
if (!liveExists) {
|
|
4599
|
+
throw new NotFoundError(`Artifact not found: ${nodeId}`);
|
|
4600
|
+
}
|
|
4601
|
+
return { title, html: liveHtml, version: target, type };
|
|
4602
|
+
}
|
|
4603
|
+
function derivedAnnotationsId(sourceNodeId) {
|
|
4604
|
+
const base = sourceNodeId.replace(/^nodes\//, "");
|
|
4605
|
+
return `nodes/${base}--annotations`;
|
|
4606
|
+
}
|
|
4607
|
+
async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
4608
|
+
try {
|
|
4609
|
+
let title = nodeId;
|
|
4610
|
+
let type = null;
|
|
4611
|
+
try {
|
|
4612
|
+
const { storage } = await engineCache.get(nestId);
|
|
4613
|
+
const node = await storage.readDocument(nodeId);
|
|
4614
|
+
title = node.frontmatter?.title || nodeId;
|
|
4615
|
+
type = node.frontmatter?.type || "document";
|
|
4616
|
+
} catch {
|
|
4617
|
+
}
|
|
4618
|
+
if (type !== null && type !== "artifact") return;
|
|
4619
|
+
const threads = await listThreads(nestId, nodeId);
|
|
4620
|
+
const derivedTitle = `${title.slice(0, 100)} \u2014 Annotations`;
|
|
4621
|
+
const markdown2 = projectThreadsToMarkdown(title, threads);
|
|
4622
|
+
const derivedId = derivedAnnotationsId(nodeId);
|
|
4623
|
+
try {
|
|
4624
|
+
await updateNode(nestId, derivedId, { content: markdown2 }, userEmail);
|
|
4625
|
+
} catch (err) {
|
|
4626
|
+
if (err instanceof NotFoundError) {
|
|
4627
|
+
await createNode(
|
|
4628
|
+
nestId,
|
|
4629
|
+
{
|
|
4630
|
+
id: derivedId,
|
|
4631
|
+
title: derivedTitle,
|
|
4632
|
+
content: markdown2,
|
|
4633
|
+
type: "document",
|
|
4634
|
+
tags: ["annotations"]
|
|
4635
|
+
},
|
|
4636
|
+
userEmail
|
|
4637
|
+
);
|
|
4638
|
+
} else {
|
|
4639
|
+
throw err;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
} catch (err) {
|
|
4643
|
+
console.warn(
|
|
4644
|
+
`[annotations] failed to sync derived node for ${nestId}/${nodeId}:`,
|
|
4645
|
+
err
|
|
4286
4646
|
);
|
|
4287
4647
|
}
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4650
|
+
// src/nests/publish-render-routes.ts
|
|
4651
|
+
var publishRenderRoutes = new Hono7();
|
|
4652
|
+
var SESSION_TTL_SECONDS2 = 24 * 60 * 60;
|
|
4653
|
+
var GATE_LIMIT = { max: 10, windowMs: 10 * 6e4 };
|
|
4654
|
+
var COMMENT_LIMIT = { max: 5, windowMs: 10 * 6e4 };
|
|
4655
|
+
var COMMENT_MAX_CHARS = 4e3;
|
|
4656
|
+
var markdown = new MarkdownIt({ html: false, linkify: true, breaks: false });
|
|
4657
|
+
var PROSE_CSS = `
|
|
4658
|
+
body{margin:0;background:#fff;color:#1c1917;font:16px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
4659
|
+
main{max-width:820px;margin:0 auto;padding:48px 24px}
|
|
4660
|
+
h1,h2,h3,h4{color:#142a80;line-height:1.25;margin:1.8em 0 .6em}
|
|
4661
|
+
h1{font-size:2em;margin-top:0}h2{font-size:1.5em}h3{font-size:1.2em}h4{font-size:1.05em}
|
|
4662
|
+
p,ul,ol,blockquote,table{margin:0 0 1.1em}
|
|
4663
|
+
ul,ol{padding-left:1.5em}li{margin:.3em 0}
|
|
4664
|
+
a{color:#142a80}
|
|
4665
|
+
hr{border:0;border-top:1px solid #e7e5e4;margin:2em 0}
|
|
4666
|
+
blockquote{border-left:3px solid #d6d3d1;margin-left:0;padding:.2em 0 .2em 1em;color:#57534e}
|
|
4667
|
+
code{background:#f5f5f4;border-radius:4px;padding:.15em .35em;font:.9em/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
4668
|
+
pre{background:#f5f5f4;border-radius:9px;padding:14px 16px;overflow-x:auto}
|
|
4669
|
+
pre code{background:none;padding:0}
|
|
4670
|
+
table{border-collapse:collapse;width:100%;display:block;overflow-x:auto}
|
|
4671
|
+
th,td{border:1px solid #e7e5e4;padding:8px 10px;text-align:left}
|
|
4672
|
+
th{background:#fafaf9}
|
|
4673
|
+
img{max-width:100%}
|
|
4674
|
+
`;
|
|
4675
|
+
function cookieName(slug) {
|
|
4676
|
+
return `cnp_${slug}`;
|
|
4677
|
+
}
|
|
4678
|
+
function readCookie2(c, name) {
|
|
4679
|
+
const raw = c.req.header("cookie");
|
|
4680
|
+
if (!raw) return null;
|
|
4681
|
+
for (const part of raw.split(";")) {
|
|
4682
|
+
const eq = part.indexOf("=");
|
|
4683
|
+
if (eq === -1) continue;
|
|
4684
|
+
if (part.slice(0, eq).trim() === name) {
|
|
4685
|
+
try {
|
|
4686
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
4687
|
+
} catch {
|
|
4688
|
+
return part.slice(eq + 1).trim();
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4292
4691
|
}
|
|
4293
|
-
|
|
4692
|
+
return null;
|
|
4693
|
+
}
|
|
4694
|
+
function setSessionCookie2(c, slug, token) {
|
|
4695
|
+
const parts = [
|
|
4696
|
+
`${cookieName(slug)}=${encodeURIComponent(token)}`,
|
|
4697
|
+
"HttpOnly",
|
|
4698
|
+
`Path=/p/${slug}`,
|
|
4699
|
+
"SameSite=Lax",
|
|
4700
|
+
`Max-Age=${SESSION_TTL_SECONDS2}`
|
|
4701
|
+
];
|
|
4702
|
+
if (isSecureRequest(c)) parts.push("Secure");
|
|
4703
|
+
c.header("Set-Cookie", parts.join("; "));
|
|
4704
|
+
}
|
|
4705
|
+
function esc(s) {
|
|
4706
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4707
|
+
}
|
|
4708
|
+
var NO_DOWNLOAD_CSS = `<style>
|
|
4709
|
+
@media print{html,body{display:none !important}}
|
|
4710
|
+
html,body{-webkit-user-select:none;-ms-user-select:none;user-select:none}
|
|
4711
|
+
img,a,svg,canvas{-webkit-user-drag:none;user-drag:none}
|
|
4712
|
+
</style>`;
|
|
4713
|
+
var SHELL_PRINT_BLANK = `<style>@media print{html,body{display:none !important}}</style>`;
|
|
4714
|
+
var NO_DOWNLOAD_SCRIPT = `<script>
|
|
4715
|
+
(function () {
|
|
4716
|
+
function stop(e) { e.preventDefault(); return false; }
|
|
4717
|
+
["contextmenu", "copy", "cut", "dragstart", "selectstart"].forEach(function (evt) {
|
|
4718
|
+
document.addEventListener(evt, stop, { capture: true });
|
|
4719
|
+
});
|
|
4720
|
+
document.addEventListener("keydown", function (e) {
|
|
4721
|
+
var k = (e.key || "").toLowerCase();
|
|
4722
|
+
// Save, print, copy, cut, select-all, view-source.
|
|
4723
|
+
if ((e.ctrlKey || e.metaKey) && ["s", "p", "c", "x", "a", "u"].indexOf(k) !== -1) return stop(e);
|
|
4724
|
+
// DevTools. Blocking these is courtesy at best \u2014 the browser menu still
|
|
4725
|
+
// opens them \u2014 but it stops the reflex.
|
|
4726
|
+
if (k === "f12") return stop(e);
|
|
4727
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && ["i", "j", "c"].indexOf(k) !== -1) return stop(e);
|
|
4728
|
+
}, { capture: true });
|
|
4729
|
+
// Belt and braces with the print stylesheet, for engines that ignore it.
|
|
4730
|
+
window.addEventListener("beforeprint", function () { document.body.style.display = "none"; });
|
|
4731
|
+
window.addEventListener("afterprint", function () { document.body.style.display = ""; });
|
|
4732
|
+
})();
|
|
4733
|
+
</script>`;
|
|
4734
|
+
function overlayMarkup(row, viewerEmail) {
|
|
4735
|
+
const parts = [];
|
|
4736
|
+
if (row.no_download === 1) {
|
|
4737
|
+
parts.push(NO_DOWNLOAD_CSS, NO_DOWNLOAD_SCRIPT);
|
|
4738
|
+
}
|
|
4739
|
+
if (row.watermark === 1 && viewerEmail) {
|
|
4740
|
+
parts.push(
|
|
4741
|
+
`<style>#cn-watermark{position:fixed;inset:0;z-index:2147483647;pointer-events:none;overflow:hidden;display:flex;align-items:center;justify-content:center}#cn-watermark span{font:700 42px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:rgba(20,42,128,.10);transform:rotate(-24deg);white-space:nowrap}</style><div id="cn-watermark" aria-hidden="true"><span>${esc(viewerEmail)}</span></div>`
|
|
4742
|
+
);
|
|
4743
|
+
}
|
|
4744
|
+
return parts.join("");
|
|
4745
|
+
}
|
|
4746
|
+
async function publishedTitle(row) {
|
|
4294
4747
|
try {
|
|
4295
|
-
|
|
4748
|
+
return (await readArtifact(row.nest_id, row.node_id)).title || row.node_id;
|
|
4296
4749
|
} catch {
|
|
4297
|
-
|
|
4298
|
-
`Version ${targetVersion} not found for ${nodeId}`
|
|
4299
|
-
);
|
|
4750
|
+
return row.node_id;
|
|
4300
4751
|
}
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
const nodeId = c.req.param("nodeId");
|
|
4314
|
-
const userId = c.get("userId");
|
|
4315
|
-
const userEmail = await resolveCallerEmail(userId);
|
|
4316
|
-
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
4317
|
-
return c.json(
|
|
4318
|
-
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
4319
|
-
403
|
|
4320
|
-
);
|
|
4752
|
+
}
|
|
4753
|
+
function withOverlays(html, extra) {
|
|
4754
|
+
if (!extra) return html;
|
|
4755
|
+
const at = html.toLowerCase().lastIndexOf("</body>");
|
|
4756
|
+
return at === -1 ? html + extra : html.slice(0, at) + extra + html.slice(at);
|
|
4757
|
+
}
|
|
4758
|
+
async function renderPublished(c, row, viewerEmail) {
|
|
4759
|
+
let artifact;
|
|
4760
|
+
try {
|
|
4761
|
+
artifact = await readArtifact(row.nest_id, row.node_id);
|
|
4762
|
+
} catch {
|
|
4763
|
+
return c.html(unavailablePage(), 404);
|
|
4321
4764
|
}
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4765
|
+
if (artifact.version == null) return c.html(unavailablePage(), 404);
|
|
4766
|
+
const type = String(artifact.type ?? "document");
|
|
4767
|
+
const body = artifact.html ?? "";
|
|
4768
|
+
const isHtml = type === "artifact" || /^\s*<(?:!doctype|html)\b/i.test(body);
|
|
4769
|
+
await logView(row.id, viewerEmail);
|
|
4770
|
+
applyArtifactSandboxHeaders(c);
|
|
4771
|
+
const overlays = overlayMarkup(row, viewerEmail);
|
|
4772
|
+
if (isHtml) {
|
|
4773
|
+
return c.body(withOverlays(body, overlays), 200, {
|
|
4774
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
4775
|
+
});
|
|
4325
4776
|
}
|
|
4326
|
-
const
|
|
4327
|
-
|
|
4777
|
+
const title = esc(artifact.title || row.node_id);
|
|
4778
|
+
const heading = /^\s*#\s/.test(body) ? "" : `<h1>${title}</h1>`;
|
|
4779
|
+
const shell = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title><style>${PROSE_CSS}</style></head><body><main>${heading}${markdown.render(body)}</main>${overlays}</body></html>`;
|
|
4780
|
+
return c.body(shell, 200, { "Content-Type": "text/html; charset=utf-8" });
|
|
4781
|
+
}
|
|
4782
|
+
var EYE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`;
|
|
4783
|
+
var EYE_OFF_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.9 4.24A9.1 9.1 0 0 1 12 4c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.2 3.14M6.6 6.6A17.7 17.7 0 0 0 2 11s3.6 7 10 7a9 9 0 0 0 5.4-1.6"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/><path d="m2 2 20 20"/></svg>`;
|
|
4784
|
+
var GATE_EYE_SCRIPT = `<script>
|
|
4785
|
+
(function () {
|
|
4786
|
+
var input = document.getElementById("cn-code");
|
|
4787
|
+
var btn = document.getElementById("cn-eye");
|
|
4788
|
+
btn.addEventListener("click", function () {
|
|
4789
|
+
var show = input.type === "password";
|
|
4790
|
+
var at = input.selectionStart;
|
|
4791
|
+
input.type = show ? "text" : "password";
|
|
4792
|
+
btn.innerHTML = show ? ${JSON.stringify(EYE_OFF_SVG)} : ${JSON.stringify(EYE_SVG)};
|
|
4793
|
+
btn.setAttribute("aria-label", show ? "Hide code" : "Show code");
|
|
4794
|
+
btn.setAttribute("aria-pressed", show ? "true" : "false");
|
|
4795
|
+
input.focus();
|
|
4796
|
+
try { input.setSelectionRange(at, at); } catch (e) {}
|
|
4797
|
+
});
|
|
4798
|
+
})();
|
|
4799
|
+
</script>`;
|
|
4800
|
+
function gatePage(row, message) {
|
|
4801
|
+
const wantsCode = row.access_mode === "code";
|
|
4802
|
+
const field = wantsCode ? `<label>Access code<span class="wrap">
|
|
4803
|
+
<input id="cn-code" name="code" type="password" autocomplete="off" required autofocus>
|
|
4804
|
+
<button type="button" id="cn-eye" class="eye" aria-label="Show code" aria-pressed="false">${EYE_SVG}</button>
|
|
4805
|
+
</span></label>` : `<label>Your email<input name="email" type="email" autocomplete="email" required autofocus></label>`;
|
|
4806
|
+
const cta = wantsCode ? "Unlock" : "Send me a link";
|
|
4807
|
+
const note = wantsCode ? "This document is protected by a passcode." : "Enter your email and we'll send you a one-time link to open it.";
|
|
4808
|
+
const err = message ? `<p class="err">${esc(message)}</p>` : "";
|
|
4809
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Protected document</title><style>
|
|
4810
|
+
:root{--indigo:#142a80;--owl:#FF6500}
|
|
4811
|
+
body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f4f4f5;color:#1c1917;font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
4812
|
+
.card{background:#fff;border:1px solid #e7e5e4;border-radius:14px;box-shadow:0 4px 24px rgba(20,42,128,.08);padding:32px 34px;max-width:380px;width:calc(100% - 32px)}
|
|
4813
|
+
h1{font-size:20px;color:var(--indigo);margin:0 0 6px}
|
|
4814
|
+
p{color:#78716c;margin:0 0 18px;font-size:14px}
|
|
4815
|
+
label{display:block;font-size:13px;font-weight:600;color:#44403c;margin-bottom:16px}
|
|
4816
|
+
input{display:block;width:100%;margin-top:6px;padding:10px 12px;border:1px solid #d6d3d1;border-radius:9px;font:inherit;box-sizing:border-box}
|
|
4817
|
+
.wrap{display:block;position:relative}
|
|
4818
|
+
.wrap input{padding-right:44px}
|
|
4819
|
+
button{width:100%;background:var(--indigo);color:#fff;font-weight:700;border:0;border-radius:9px;padding:11px;font-size:15px;cursor:pointer}
|
|
4820
|
+
button.eye{position:absolute;top:6px;right:0;width:40px;height:calc(100% - 6px);background:none;border:0;padding:0;color:#78716c;display:grid;place-items:center;cursor:pointer}
|
|
4821
|
+
button.eye:hover{color:#44403c}
|
|
4822
|
+
button.eye svg{width:18px;height:18px;display:block}
|
|
4823
|
+
.err{color:#be123c;font-weight:600}
|
|
4824
|
+
.brand{font-size:12px;color:#a8a29e;margin:18px 0 0;text-align:center}
|
|
4825
|
+
</style></head><body><form class="card" method="POST" action="/p/${esc(row.slug)}/gate">
|
|
4826
|
+
<h1>Protected document</h1><p>${esc(note)}</p>${err}${field}
|
|
4827
|
+
<button type="submit">${cta}</button>
|
|
4828
|
+
<p class="brand">Secured by PromptOwl \xB7 ContextNest</p>
|
|
4829
|
+
</form>${wantsCode ? GATE_EYE_SCRIPT : ""}</body></html>`;
|
|
4830
|
+
}
|
|
4831
|
+
function unavailablePage() {
|
|
4832
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Link unavailable</title><style>
|
|
4833
|
+
body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f4f4f5;color:#1c1917;font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
4834
|
+
.card{background:#fff;border:1px solid #e7e5e4;border-radius:14px;box-shadow:0 4px 24px rgba(20,42,128,.08);padding:32px 34px;max-width:420px;width:calc(100% - 32px);text-align:center}
|
|
4835
|
+
h1{font-size:20px;color:#142a80;margin:0 0 8px}
|
|
4836
|
+
p{color:#78716c;margin:0;font-size:14px}
|
|
4837
|
+
.brand{font-size:12px;color:#a8a29e;margin:18px 0 0}
|
|
4838
|
+
</style></head><body><div class="card">
|
|
4839
|
+
<h1>This link is no longer available</h1>
|
|
4840
|
+
<p>It may have been revoked by its owner, or it may have expired. Ask whoever shared it with you for a new link.</p>
|
|
4841
|
+
<p class="brand">Secured by PromptOwl \xB7 ContextNest</p>
|
|
4842
|
+
</div></body></html>`;
|
|
4843
|
+
}
|
|
4844
|
+
function sentPage() {
|
|
4845
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Check your email</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f4f4f5;color:#1c1917;font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}.card{background:#fff;border:1px solid #e7e5e4;border-radius:14px;box-shadow:0 4px 24px rgba(20,42,128,.08);padding:32px 34px;max-width:380px;text-align:center}h1{font-size:20px;color:#142a80;margin:0 0 6px}p{color:#78716c;margin:0;font-size:14px}</style></head><body><div class="card"><h1>Check your email</h1><p>If your address has access, a one-time link to open this document is on its way.</p></div></body></html>`;
|
|
4846
|
+
}
|
|
4847
|
+
function readerShell(row, title, viewerEmail) {
|
|
4848
|
+
const emailField = viewerEmail ? `<p class="who">Commenting as <strong>${esc(viewerEmail)}</strong></p>` : `<label>Your email<input id="cn-email" type="email" autocomplete="email" required></label>`;
|
|
4849
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(title)}</title><style>
|
|
4850
|
+
html,body{margin:0;height:100%}
|
|
4851
|
+
#cn-frame{border:0;width:100%;height:100%;display:block}
|
|
4852
|
+
#cn-open{position:fixed;right:18px;bottom:18px;z-index:20;background:#142a80;color:#fff;font:600 14px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;padding:11px 16px;border:0;border-radius:9999px;cursor:pointer;box-shadow:0 4px 14px rgba(20,42,128,.32)}
|
|
4853
|
+
#cn-back{position:fixed;inset:0;z-index:30;background:rgba(28,25,23,.45);display:none;place-items:center}
|
|
4854
|
+
#cn-back[data-open]{display:grid}
|
|
4855
|
+
.card{background:#fff;border-radius:14px;box-shadow:0 10px 40px rgba(20,42,128,.22);padding:28px 30px;width:min(460px,calc(100% - 32px));font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:#1c1917;box-sizing:border-box}
|
|
4856
|
+
.card h2{font-size:20px;color:#142a80;margin:0 0 6px}
|
|
4857
|
+
.card p{color:#78716c;margin:0 0 18px;font-size:14px}
|
|
4858
|
+
.who strong{color:#44403c}
|
|
4859
|
+
label{display:block;font-size:13px;font-weight:600;color:#44403c;margin-bottom:16px}
|
|
4860
|
+
input,textarea{display:block;width:100%;margin-top:6px;padding:10px 12px;border:1px solid #d6d3d1;border-radius:9px;font:inherit;box-sizing:border-box}
|
|
4861
|
+
textarea{min-height:130px;resize:vertical}
|
|
4862
|
+
.row{display:flex;gap:10px}
|
|
4863
|
+
button{flex:1;background:#142a80;color:#fff;font-weight:700;border:0;border-radius:9px;padding:11px;font-size:15px;cursor:pointer}
|
|
4864
|
+
button.ghost{background:#f5f5f4;color:#44403c}
|
|
4865
|
+
button[disabled]{opacity:.6;cursor:default}
|
|
4866
|
+
.msg{font-weight:600;margin:0 0 14px;font-size:14px}
|
|
4867
|
+
.msg.err{color:#be123c}.msg.ok{color:#15803d}
|
|
4868
|
+
</style></head><body>
|
|
4869
|
+
<iframe id="cn-frame" src="/p/${esc(row.slug)}/view" title="${esc(title)}"></iframe>
|
|
4870
|
+
<button id="cn-open" type="button">Leave a comment</button>
|
|
4871
|
+
<div id="cn-back"><div class="card" role="dialog" aria-modal="true" aria-label="Leave a comment">
|
|
4872
|
+
<h2>Leave a comment</h2>
|
|
4873
|
+
<p>Your comment goes to this document's stewards for review \u2014 it is not posted publicly.</p>
|
|
4874
|
+
<p class="msg" id="cn-msg" hidden></p>
|
|
4875
|
+
${emailField}
|
|
4876
|
+
<label>Comment<textarea id="cn-body" maxlength="${COMMENT_MAX_CHARS}" required></textarea></label>
|
|
4877
|
+
<div class="row">
|
|
4878
|
+
<button type="button" class="ghost" id="cn-cancel">Cancel</button>
|
|
4879
|
+
<button type="button" id="cn-send">Send comment</button>
|
|
4880
|
+
</div>
|
|
4881
|
+
</div></div>
|
|
4882
|
+
<script>
|
|
4883
|
+
(function () {
|
|
4884
|
+
var back = document.getElementById("cn-back");
|
|
4885
|
+
var msg = document.getElementById("cn-msg");
|
|
4886
|
+
var send = document.getElementById("cn-send");
|
|
4887
|
+
var bodyEl = document.getElementById("cn-body");
|
|
4888
|
+
var emailEl = document.getElementById("cn-email");
|
|
4889
|
+
function show(text, ok) {
|
|
4890
|
+
msg.textContent = text;
|
|
4891
|
+
msg.className = "msg " + (ok ? "ok" : "err");
|
|
4892
|
+
msg.hidden = false;
|
|
4893
|
+
}
|
|
4894
|
+
function open() { msg.hidden = true; back.setAttribute("data-open", ""); (emailEl || bodyEl).focus(); }
|
|
4895
|
+
function close() { back.removeAttribute("data-open"); }
|
|
4896
|
+
document.getElementById("cn-open").addEventListener("click", open);
|
|
4897
|
+
document.getElementById("cn-cancel").addEventListener("click", close);
|
|
4898
|
+
back.addEventListener("click", function (e) { if (e.target === back) close(); });
|
|
4899
|
+
document.addEventListener("keydown", function (e) { if (e.key === "Escape") close(); });
|
|
4900
|
+
send.addEventListener("click", function () {
|
|
4901
|
+
var payload = { body: bodyEl.value };
|
|
4902
|
+
if (emailEl) payload.email = emailEl.value;
|
|
4903
|
+
send.disabled = true;
|
|
4904
|
+
fetch("/p/${esc(row.slug)}/comments", {
|
|
4905
|
+
method: "POST",
|
|
4906
|
+
headers: { "Content-Type": "application/json" },
|
|
4907
|
+
body: JSON.stringify(payload),
|
|
4908
|
+
})
|
|
4909
|
+
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
|
4910
|
+
.then(function (res) {
|
|
4911
|
+
if (!res.ok) { show(res.d.error || "Could not send that comment.", false); return; }
|
|
4912
|
+
bodyEl.value = "";
|
|
4913
|
+
show("Thanks \u2014 your comment is with the stewards.", true);
|
|
4914
|
+
setTimeout(close, 1600);
|
|
4915
|
+
})
|
|
4916
|
+
.catch(function () { show("Could not reach the server.", false); })
|
|
4917
|
+
.then(function () { send.disabled = false; });
|
|
4918
|
+
});
|
|
4919
|
+
})();
|
|
4920
|
+
</script>${row.no_download === 1 ? SHELL_PRINT_BLANK : ""}</body></html>`;
|
|
4921
|
+
}
|
|
4922
|
+
var SHELL_CSP = [
|
|
4923
|
+
"default-src 'none'",
|
|
4924
|
+
"script-src 'unsafe-inline'",
|
|
4925
|
+
"style-src 'unsafe-inline'",
|
|
4926
|
+
"frame-src 'self'",
|
|
4927
|
+
"connect-src 'self'"
|
|
4928
|
+
].join("; ");
|
|
4929
|
+
async function resolveViewer(c, row) {
|
|
4930
|
+
if (row.access_mode === "public") return { email: null };
|
|
4931
|
+
const token = readCookie2(c, cookieName(row.slug));
|
|
4932
|
+
return token ? await verifySession(row.id, token) : null;
|
|
4933
|
+
}
|
|
4934
|
+
publishRenderRoutes.get("/:slug", async (c) => {
|
|
4935
|
+
const slug = c.req.param("slug");
|
|
4936
|
+
const row = await getPublishBySlug(slug);
|
|
4937
|
+
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
4938
|
+
const viewer = await resolveViewer(c, row);
|
|
4939
|
+
if (!viewer) return c.html(gatePage(row));
|
|
4940
|
+
if (row.comments_enabled !== 1) return renderPublished(c, row, viewer.email);
|
|
4941
|
+
const title = await publishedTitle(row);
|
|
4942
|
+
c.header("Content-Security-Policy", SHELL_CSP);
|
|
4943
|
+
c.header("Referrer-Policy", "no-referrer");
|
|
4944
|
+
return c.html(readerShell(row, title, viewer.email));
|
|
4945
|
+
});
|
|
4946
|
+
publishRenderRoutes.get("/:slug/view", async (c) => {
|
|
4947
|
+
const slug = c.req.param("slug");
|
|
4948
|
+
const row = await getPublishBySlug(slug);
|
|
4949
|
+
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
4950
|
+
const viewer = await resolveViewer(c, row);
|
|
4951
|
+
if (!viewer) return c.html(unavailablePage(), 404);
|
|
4952
|
+
return renderPublished(c, row, viewer.email);
|
|
4953
|
+
});
|
|
4954
|
+
publishRenderRoutes.post("/:slug/comments", async (c) => {
|
|
4955
|
+
const slug = c.req.param("slug");
|
|
4956
|
+
const row = await getPublishBySlug(slug);
|
|
4957
|
+
if (!isLivePublish(row) || row.comments_enabled !== 1) {
|
|
4958
|
+
return c.json({ error: "Not found" }, 404);
|
|
4959
|
+
}
|
|
4960
|
+
const viewer = await resolveViewer(c, row);
|
|
4961
|
+
if (!viewer) return c.json({ error: "Not found" }, 404);
|
|
4962
|
+
if (!tryConsume(`publish-comment:${slug}:${clientIp(c)}`, COMMENT_LIMIT)) {
|
|
4963
|
+
return c.json({ error: "Too many comments. Try again in a few minutes." }, 429);
|
|
4964
|
+
}
|
|
4965
|
+
const input = await c.req.json().catch(() => ({}));
|
|
4966
|
+
const email = viewer.email ?? normalizeEmail(String(input.email ?? ""));
|
|
4967
|
+
if (!isEmailish(email)) {
|
|
4968
|
+
return c.json({ error: "Enter a valid email address." }, 400);
|
|
4969
|
+
}
|
|
4970
|
+
const body = String(input.body ?? "").trim().slice(0, COMMENT_MAX_CHARS);
|
|
4971
|
+
if (!body) {
|
|
4972
|
+
return c.json({ error: "Write a comment first." }, 400);
|
|
4973
|
+
}
|
|
4974
|
+
await createThread(row.nest_id, row.node_id, { body, anchor: null }, email);
|
|
4975
|
+
return c.json({ ok: true });
|
|
4328
4976
|
});
|
|
4329
|
-
|
|
4330
|
-
const
|
|
4331
|
-
const
|
|
4332
|
-
return c.
|
|
4977
|
+
publishRenderRoutes.post("/:slug/gate", async (c) => {
|
|
4978
|
+
const slug = c.req.param("slug");
|
|
4979
|
+
const row = await getPublishBySlug(slug);
|
|
4980
|
+
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
4981
|
+
if (!tryConsume(`publish-gate:${slug}:${clientIp(c)}`, GATE_LIMIT)) {
|
|
4982
|
+
return c.html(gatePage(row, "Too many attempts. Try again in a few minutes."), 429);
|
|
4983
|
+
}
|
|
4984
|
+
if (row.access_mode === "public") return c.redirect(`/p/${slug}`);
|
|
4985
|
+
const form = await c.req.parseBody();
|
|
4986
|
+
if (row.access_mode === "code") {
|
|
4987
|
+
if (!await verifyCode(row, String(form.code ?? ""))) {
|
|
4988
|
+
return c.html(gatePage(row, "That code is not correct."), 401);
|
|
4989
|
+
}
|
|
4990
|
+
setSessionCookie2(c, slug, await createSession2(row.id, null));
|
|
4991
|
+
return c.redirect(`/p/${slug}`);
|
|
4992
|
+
}
|
|
4993
|
+
const email = normalizeEmail(String(form.email ?? ""));
|
|
4994
|
+
if (!isEmailish(email)) {
|
|
4995
|
+
return c.html(gatePage(row, "Enter a valid email address."), 400);
|
|
4996
|
+
}
|
|
4997
|
+
const allowed = row.access_mode !== "invite" || await isAllowlisted(row.id, email);
|
|
4998
|
+
const linkToken = await mintMagicLink(row.id, email);
|
|
4999
|
+
if (!allowed) return c.html(sentPage());
|
|
5000
|
+
const link = `${requestBaseUrl(c.req.url)}/p/${slug}/verify?token=${linkToken}`;
|
|
5001
|
+
void sendEmailToRecipient(email, {
|
|
5002
|
+
status: "shared",
|
|
5003
|
+
docTitle: "A document has been shared with you",
|
|
5004
|
+
path: "PromptOwl \xB7 ContextNest",
|
|
5005
|
+
link
|
|
5006
|
+
});
|
|
5007
|
+
return c.html(sentPage());
|
|
4333
5008
|
});
|
|
4334
|
-
|
|
4335
|
-
const
|
|
4336
|
-
const
|
|
4337
|
-
|
|
4338
|
-
const email =
|
|
4339
|
-
if (!email
|
|
4340
|
-
|
|
5009
|
+
publishRenderRoutes.get("/:slug/verify", async (c) => {
|
|
5010
|
+
const slug = c.req.param("slug");
|
|
5011
|
+
const row = await getPublishBySlug(slug);
|
|
5012
|
+
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5013
|
+
const email = await consumeMagicLink(row.id, c.req.query("token") ?? "");
|
|
5014
|
+
if (!email) {
|
|
5015
|
+
return c.html(gatePage(row, "That link is invalid or has expired. Request a new one."), 401);
|
|
4341
5016
|
}
|
|
4342
|
-
|
|
4343
|
-
return c.
|
|
5017
|
+
setSessionCookie2(c, slug, await createSession2(row.id, email));
|
|
5018
|
+
return c.redirect(`/p/${slug}`);
|
|
4344
5019
|
});
|
|
4345
|
-
|
|
5020
|
+
|
|
5021
|
+
// src/nests/nest-team-routes.ts
|
|
5022
|
+
import { Hono as Hono8 } from "hono";
|
|
5023
|
+
var nestTeamRoutes = new Hono8();
|
|
5024
|
+
nestTeamRoutes.get("/", async (c) => {
|
|
4346
5025
|
const nestId = c.req.param("nestId");
|
|
4347
|
-
const
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
5026
|
+
const refs = await listNestTeams(nestId);
|
|
5027
|
+
const teams = [];
|
|
5028
|
+
for (const ref of refs) {
|
|
5029
|
+
const team = await getTeam(ref.teamId).catch(() => null);
|
|
5030
|
+
teams.push({
|
|
5031
|
+
teamId: ref.teamId,
|
|
5032
|
+
teamName: team?.name ?? ref.teamName,
|
|
5033
|
+
members: team?.members ?? []
|
|
5034
|
+
});
|
|
4352
5035
|
}
|
|
4353
|
-
return c.json({
|
|
5036
|
+
return c.json({ teams });
|
|
4354
5037
|
});
|
|
4355
|
-
|
|
4356
|
-
const
|
|
4357
|
-
|
|
4358
|
-
|
|
5038
|
+
nestTeamRoutes.post("/", async (c) => {
|
|
5039
|
+
const body = await c.req.json();
|
|
5040
|
+
if (!body.team_id) {
|
|
5041
|
+
throw new ValidationError("team_id is required");
|
|
5042
|
+
}
|
|
4359
5043
|
const userId = c.get("userId");
|
|
4360
|
-
const
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
userId,
|
|
4365
|
-
|
|
4366
|
-
|
|
5044
|
+
const teams = await shareTeamWithNest({
|
|
5045
|
+
nestId: c.req.param("nestId"),
|
|
5046
|
+
teamId: body.team_id,
|
|
5047
|
+
callerUserId: userId,
|
|
5048
|
+
allowAdmin: await isLicenseAdminUserId(userId),
|
|
5049
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
5050
|
+
});
|
|
5051
|
+
return c.json({ teams }, 201);
|
|
5052
|
+
});
|
|
5053
|
+
nestTeamRoutes.delete("/:teamId", async (c) => {
|
|
5054
|
+
const teams = await unshareTeamFromNest({
|
|
5055
|
+
nestId: c.req.param("nestId"),
|
|
5056
|
+
teamId: c.req.param("teamId")
|
|
5057
|
+
});
|
|
5058
|
+
return c.json({ teams });
|
|
5059
|
+
});
|
|
5060
|
+
|
|
5061
|
+
// src/governance/team-routes.ts
|
|
5062
|
+
import { Hono as Hono9 } from "hono";
|
|
5063
|
+
|
|
5064
|
+
// src/governance/promptowl-teams.ts
|
|
5065
|
+
var PROMPTOWL_SOURCE = "promptowl";
|
|
5066
|
+
var IMPORTABLE_PROMPTOWL_ROLES = ["owner", "editor"];
|
|
5067
|
+
function canImportPromptowlTeam(team) {
|
|
5068
|
+
return IMPORTABLE_PROMPTOWL_ROLES.includes(
|
|
5069
|
+
(team.your_role || "").toLowerCase()
|
|
4367
5070
|
);
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
5071
|
+
}
|
|
5072
|
+
function mapPromptowlRole(role) {
|
|
5073
|
+
switch ((role || "").toLowerCase()) {
|
|
5074
|
+
case "owner":
|
|
5075
|
+
return "admin";
|
|
5076
|
+
case "editor":
|
|
5077
|
+
return "editor";
|
|
5078
|
+
default:
|
|
5079
|
+
return "viewer";
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
async function fetchOwnedPromptowlTeams(token) {
|
|
5083
|
+
if (!token?.trim()) throw new ValidationError("token is required");
|
|
5084
|
+
const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
|
|
5085
|
+
let res;
|
|
5086
|
+
try {
|
|
5087
|
+
res = await fetch(`${promptowlUrl}/api/user/teams`, {
|
|
5088
|
+
headers: {
|
|
5089
|
+
...config.PROMPTOWL_FETCH_HEADERS,
|
|
5090
|
+
Authorization: `Bearer ${token}`
|
|
4380
5091
|
}
|
|
4381
|
-
}
|
|
4382
|
-
|
|
4383
|
-
|
|
5092
|
+
});
|
|
5093
|
+
} catch {
|
|
5094
|
+
throw new ValidationError("Could not reach PromptOwl. Check this server's network access.");
|
|
4384
5095
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
{ id: nodeId, frontmatterAuthor: node.frontmatter?.author ?? null }
|
|
4390
|
-
])).get(nodeId);
|
|
4391
|
-
if (people) {
|
|
4392
|
-
response.author = people.author;
|
|
4393
|
-
response.collaborators = people.collaborators;
|
|
4394
|
-
}
|
|
5096
|
+
if (res.status === 401) {
|
|
5097
|
+
throw new UnauthorizedError(
|
|
5098
|
+
"PromptOwl rejected this approval. Approve again and retry."
|
|
5099
|
+
);
|
|
4395
5100
|
}
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
return c.body(nodeAsMarkdown(response, nodeId), 200, {
|
|
4423
|
-
"Content-Type": "text/markdown; charset=utf-8"
|
|
4424
|
-
});
|
|
4425
|
-
}
|
|
4426
|
-
return c.json({ node: response });
|
|
5101
|
+
if (!res.ok) {
|
|
5102
|
+
throw new ValidationError(
|
|
5103
|
+
`PromptOwl could not list your teams (HTTP ${res.status}).`
|
|
5104
|
+
);
|
|
5105
|
+
}
|
|
5106
|
+
const data = await res.json();
|
|
5107
|
+
return Array.isArray(data.teams) ? data.teams : [];
|
|
5108
|
+
}
|
|
5109
|
+
async function syncPromptowlTeams(params) {
|
|
5110
|
+
if (!params.teamIds?.length) {
|
|
5111
|
+
throw new ValidationError("team_ids is required");
|
|
5112
|
+
}
|
|
5113
|
+
const owned = await fetchOwnedPromptowlTeams(params.token);
|
|
5114
|
+
const byId = new Map(owned.map((t) => [t.id, t]));
|
|
5115
|
+
const selected = [];
|
|
5116
|
+
for (const id of params.teamIds) {
|
|
5117
|
+
const team = byId.get(id);
|
|
5118
|
+
if (!team) {
|
|
5119
|
+
throw new ValidationError(
|
|
5120
|
+
"One of the selected teams isn't available to import. Refresh the list and try again."
|
|
5121
|
+
);
|
|
5122
|
+
}
|
|
5123
|
+
if (!canImportPromptowlTeam(team)) {
|
|
5124
|
+
throw new ForbiddenError(
|
|
5125
|
+
`You're view-only on "${team.name}" in PromptOwl, so you can't import it. Ask an owner or editor of that team to import it.`
|
|
5126
|
+
);
|
|
4427
5127
|
}
|
|
5128
|
+
selected.push(team);
|
|
4428
5129
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
5130
|
+
const db = getDb();
|
|
5131
|
+
const caller = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
5132
|
+
params.callerUserId
|
|
5133
|
+
]);
|
|
5134
|
+
if (!caller?.email) {
|
|
5135
|
+
throw new ValidationError("Could not resolve your account.");
|
|
4433
5136
|
}
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
5137
|
+
const results = [];
|
|
5138
|
+
for (const poTeam of selected) {
|
|
5139
|
+
results.push(await syncOne(poTeam, params.callerUserId, caller.email));
|
|
5140
|
+
}
|
|
5141
|
+
return results;
|
|
5142
|
+
}
|
|
5143
|
+
async function syncOne(poTeam, callerUserId, callerEmail) {
|
|
5144
|
+
const existing = await findTeamByExternalId({
|
|
5145
|
+
ownerId: callerUserId,
|
|
5146
|
+
source: PROMPTOWL_SOURCE,
|
|
5147
|
+
externalId: poTeam.id
|
|
5148
|
+
});
|
|
5149
|
+
const team = existing ?? await createTeam({
|
|
5150
|
+
name: poTeam.name,
|
|
5151
|
+
ownerId: callerUserId,
|
|
5152
|
+
source: PROMPTOWL_SOURCE,
|
|
5153
|
+
externalId: poTeam.id
|
|
5154
|
+
});
|
|
5155
|
+
if (existing && existing.name !== poTeam.name) {
|
|
5156
|
+
await renameTeam({
|
|
5157
|
+
teamId: team.id,
|
|
5158
|
+
name: poTeam.name,
|
|
5159
|
+
callerUserId,
|
|
5160
|
+
allowAdmin: false
|
|
5161
|
+
});
|
|
5162
|
+
}
|
|
5163
|
+
const desired = await desiredRoster(poTeam, callerEmail);
|
|
5164
|
+
const before = new Map(team.members.map((m) => [m.email, m.role]));
|
|
5165
|
+
const after = new Map(desired.map((m) => [m.email, m.role]));
|
|
5166
|
+
const added = [...after.keys()].filter((e) => !before.has(e));
|
|
5167
|
+
const removed = [...before.keys()].filter((e) => !after.has(e));
|
|
5168
|
+
const updated = [...after.keys()].filter(
|
|
5169
|
+
(e) => before.has(e) && before.get(e) !== after.get(e)
|
|
4437
5170
|
);
|
|
4438
|
-
if (
|
|
4439
|
-
|
|
5171
|
+
if (added.length || removed.length || updated.length) {
|
|
5172
|
+
await replaceTeamRoster({
|
|
5173
|
+
teamId: team.id,
|
|
5174
|
+
members: desired,
|
|
5175
|
+
callerUserId,
|
|
5176
|
+
allowAdmin: false
|
|
5177
|
+
});
|
|
4440
5178
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
5179
|
+
return {
|
|
5180
|
+
team_id: team.id,
|
|
5181
|
+
external_id: poTeam.id,
|
|
5182
|
+
name: poTeam.name,
|
|
5183
|
+
created: !existing,
|
|
5184
|
+
added,
|
|
5185
|
+
removed,
|
|
5186
|
+
updated
|
|
5187
|
+
};
|
|
5188
|
+
}
|
|
5189
|
+
async function desiredRoster(poTeam, callerEmail) {
|
|
5190
|
+
const caller = normalizeEmail(callerEmail);
|
|
5191
|
+
const roster = [];
|
|
5192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5193
|
+
for (const poMember of poTeam.members || []) {
|
|
5194
|
+
if (!poMember?.email) continue;
|
|
5195
|
+
const email = normalizeEmail(poMember.email);
|
|
5196
|
+
if (email === caller || seen.has(email)) continue;
|
|
5197
|
+
seen.add(email);
|
|
5198
|
+
const identity = await resolveMemberIdentity({ email });
|
|
5199
|
+
roster.push({
|
|
5200
|
+
userId: identity.userId,
|
|
5201
|
+
email: identity.email,
|
|
5202
|
+
role: mapPromptowlRole(poMember.role)
|
|
4444
5203
|
});
|
|
4445
5204
|
}
|
|
4446
|
-
return
|
|
4447
|
-
}
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
5205
|
+
return roster;
|
|
5206
|
+
}
|
|
5207
|
+
|
|
5208
|
+
// src/governance/team-routes.ts
|
|
5209
|
+
var teamRoutes = new Hono9();
|
|
5210
|
+
var PROMPTOWL_LIMIT = { max: 10, windowMs: 15 * 6e4 };
|
|
5211
|
+
var CONNECT_REQUIRED = "promptowl_connect_required";
|
|
5212
|
+
var CONNECT_REQUIRED_STATUS = 428;
|
|
5213
|
+
async function withPromptowlToken(c, run) {
|
|
5214
|
+
if (!config.PROMPTOWL_TEAMS_ENABLED) {
|
|
5215
|
+
return c.json({ error: "Not found" }, 404);
|
|
5216
|
+
}
|
|
5217
|
+
if (!tryConsume(`po-teams:ip:${clientIp(c)}`, PROMPTOWL_LIMIT)) {
|
|
5218
|
+
return c.json({ error: "Too many PromptOwl requests, try again later" }, 429);
|
|
5219
|
+
}
|
|
5220
|
+
const token = getPromptowlTokenFromRequest(c);
|
|
5221
|
+
if (!token) {
|
|
5222
|
+
return c.json(
|
|
5223
|
+
{
|
|
5224
|
+
error: "Sign in with PromptOwl to import teams.",
|
|
5225
|
+
code: CONNECT_REQUIRED
|
|
5226
|
+
},
|
|
5227
|
+
CONNECT_REQUIRED_STATUS
|
|
5228
|
+
);
|
|
5229
|
+
}
|
|
5230
|
+
try {
|
|
5231
|
+
return c.json(await run(token));
|
|
5232
|
+
} catch (err) {
|
|
5233
|
+
if (err instanceof UnauthorizedError) {
|
|
5234
|
+
c.header(
|
|
5235
|
+
"Set-Cookie",
|
|
5236
|
+
buildPromptowlTokenCookie("", { secure: isSecureRequest(c), clear: true }),
|
|
5237
|
+
{ append: true }
|
|
5238
|
+
);
|
|
4457
5239
|
return c.json(
|
|
4458
|
-
{
|
|
4459
|
-
|
|
4460
|
-
your_version: baseVersion,
|
|
4461
|
-
current_version: conflict.currentVersion,
|
|
4462
|
-
updated_by: conflict.updatedBy,
|
|
4463
|
-
updated_at: conflict.updatedAt,
|
|
4464
|
-
rejected_content: body.content || body.append || null
|
|
4465
|
-
},
|
|
4466
|
-
409
|
|
5240
|
+
{ error: err.message, code: CONNECT_REQUIRED },
|
|
5241
|
+
CONNECT_REQUIRED_STATUS
|
|
4467
5242
|
);
|
|
4468
5243
|
}
|
|
5244
|
+
throw err;
|
|
4469
5245
|
}
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
);
|
|
4486
|
-
return c.json({ node: toNodeResponse(node), version: responseVersion });
|
|
5246
|
+
}
|
|
5247
|
+
teamRoutes.get(
|
|
5248
|
+
"/promptowl",
|
|
5249
|
+
async (c) => withPromptowlToken(c, async (token) => ({
|
|
5250
|
+
teams: await fetchOwnedPromptowlTeams(token)
|
|
5251
|
+
}))
|
|
5252
|
+
);
|
|
5253
|
+
teamRoutes.post("/promptowl/sync", async (c) => {
|
|
5254
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5255
|
+
return withPromptowlToken(c, async (token) => ({
|
|
5256
|
+
results: await syncPromptowlTeams({
|
|
5257
|
+
callerUserId: c.get("userId"),
|
|
5258
|
+
token,
|
|
5259
|
+
teamIds: body.team_ids ?? []
|
|
5260
|
+
})
|
|
5261
|
+
}));
|
|
4487
5262
|
});
|
|
4488
|
-
|
|
4489
|
-
const
|
|
4490
|
-
const
|
|
4491
|
-
|
|
4492
|
-
|
|
5263
|
+
teamRoutes.post("/", async (c) => {
|
|
5264
|
+
const body = await c.req.json();
|
|
5265
|
+
const team = await createTeam({
|
|
5266
|
+
name: body.name ?? "",
|
|
5267
|
+
ownerId: c.get("userId")
|
|
5268
|
+
});
|
|
5269
|
+
return c.json({ team }, 201);
|
|
4493
5270
|
});
|
|
4494
|
-
async
|
|
5271
|
+
teamRoutes.get("/", async (c) => {
|
|
5272
|
+
const teams = await listTeamsForUser(c.get("userId"));
|
|
5273
|
+
return c.json({ teams });
|
|
5274
|
+
});
|
|
5275
|
+
teamRoutes.get("/:teamId", async (c) => {
|
|
4495
5276
|
const userId = c.get("userId");
|
|
4496
|
-
const
|
|
4497
|
-
const
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
5277
|
+
const team = await getTeam(c.req.param("teamId"));
|
|
5278
|
+
const isMember = team.members.some((m) => m.userId === userId);
|
|
5279
|
+
if (team.owner_id !== userId && !isMember && !await isLicenseAdminUserId(userId)) {
|
|
5280
|
+
throw new ForbiddenError("You don't have access to this team.");
|
|
5281
|
+
}
|
|
5282
|
+
return c.json({ team });
|
|
5283
|
+
});
|
|
5284
|
+
teamRoutes.patch("/:teamId", async (c) => {
|
|
5285
|
+
const body = await c.req.json();
|
|
5286
|
+
const userId = c.get("userId");
|
|
5287
|
+
const team = await renameTeam({
|
|
5288
|
+
teamId: c.req.param("teamId"),
|
|
5289
|
+
name: body.name ?? "",
|
|
5290
|
+
callerUserId: userId,
|
|
5291
|
+
allowAdmin: await isLicenseAdminUserId(userId)
|
|
5292
|
+
});
|
|
5293
|
+
return c.json({ team });
|
|
5294
|
+
});
|
|
5295
|
+
teamRoutes.delete("/:teamId", async (c) => {
|
|
5296
|
+
const userId = c.get("userId");
|
|
5297
|
+
await deleteTeam({
|
|
5298
|
+
teamId: c.req.param("teamId"),
|
|
5299
|
+
callerUserId: userId,
|
|
5300
|
+
allowAdmin: await isLicenseAdminUserId(userId)
|
|
5301
|
+
});
|
|
5302
|
+
return c.json({ removed: true });
|
|
5303
|
+
});
|
|
5304
|
+
teamRoutes.post("/:teamId/members", async (c) => {
|
|
5305
|
+
const body = await c.req.json();
|
|
5306
|
+
const userId = c.get("userId");
|
|
5307
|
+
const team = await addMember({
|
|
5308
|
+
teamId: c.req.param("teamId"),
|
|
5309
|
+
email: body.email,
|
|
5310
|
+
userId: body.user_id,
|
|
5311
|
+
role: body.role ?? "",
|
|
5312
|
+
callerUserId: userId,
|
|
5313
|
+
allowAdmin: await isLicenseAdminUserId(userId)
|
|
5314
|
+
});
|
|
5315
|
+
return c.json({ team }, 201);
|
|
5316
|
+
});
|
|
5317
|
+
teamRoutes.patch("/:teamId/members/:userId", async (c) => {
|
|
5318
|
+
const body = await c.req.json();
|
|
5319
|
+
const userId = c.get("userId");
|
|
5320
|
+
const team = await updateMemberRole({
|
|
5321
|
+
teamId: c.req.param("teamId"),
|
|
5322
|
+
userId: c.req.param("userId"),
|
|
5323
|
+
role: body.role ?? "",
|
|
5324
|
+
callerUserId: userId,
|
|
5325
|
+
allowAdmin: await isLicenseAdminUserId(userId)
|
|
5326
|
+
});
|
|
5327
|
+
return c.json({ team });
|
|
5328
|
+
});
|
|
5329
|
+
teamRoutes.delete("/:teamId/members/:userId", async (c) => {
|
|
5330
|
+
const userId = c.get("userId");
|
|
5331
|
+
const team = await removeMember({
|
|
5332
|
+
teamId: c.req.param("teamId"),
|
|
5333
|
+
userId: c.req.param("userId"),
|
|
5334
|
+
callerUserId: userId,
|
|
5335
|
+
allowAdmin: await isLicenseAdminUserId(userId)
|
|
5336
|
+
});
|
|
5337
|
+
return c.json({ team });
|
|
5338
|
+
});
|
|
4506
5339
|
|
|
4507
|
-
// src/
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
function clampAnchor(raw) {
|
|
4511
|
-
if (!raw) return null;
|
|
4512
|
-
const quote = (raw.quote ?? "").trim().slice(0, MAX_QUOTE_CHARS);
|
|
4513
|
-
if (!quote) return null;
|
|
4514
|
-
const before = (raw.before ?? "").slice(-MAX_CONTEXT_CHARS);
|
|
4515
|
-
const after = (raw.after ?? "").slice(0, MAX_CONTEXT_CHARS);
|
|
4516
|
-
const line = typeof raw.line === "number" && Number.isFinite(raw.line) && raw.line > 0 ? Math.floor(raw.line) : void 0;
|
|
4517
|
-
return { quote, before, after, ...line !== void 0 ? { line } : {} };
|
|
4518
|
-
}
|
|
4519
|
-
function oneLine(s) {
|
|
4520
|
-
return s.replace(/\s+/g, " ").replace(/--+>/g, "-\u2192").trim();
|
|
4521
|
-
}
|
|
4522
|
-
function renderAnchor(anchor) {
|
|
4523
|
-
if (!anchor) {
|
|
4524
|
-
return "<!-- anchor: whole-artifact -->";
|
|
4525
|
-
}
|
|
4526
|
-
const linePart = anchor.line !== void 0 ? `line ${anchor.line} \xB7 ` : "";
|
|
4527
|
-
const context = `\u2026${oneLine(anchor.before)}\u3008${oneLine(anchor.quote)}\u3009${oneLine(
|
|
4528
|
-
anchor.after
|
|
4529
|
-
)}\u2026`;
|
|
4530
|
-
return `<!-- anchor: ${linePart}quote "${oneLine(anchor.quote)}" \xB7 context ${context} -->`;
|
|
5340
|
+
// src/nodes/search.ts
|
|
5341
|
+
async function discoverForSearch(storage) {
|
|
5342
|
+
return storage.discoverDocuments({ includeRetired: true });
|
|
4531
5343
|
}
|
|
4532
|
-
function
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
5344
|
+
function titleScore(title, query) {
|
|
5345
|
+
const t = (title || "").toLowerCase();
|
|
5346
|
+
const q = query.trim().toLowerCase();
|
|
5347
|
+
if (!q) return 0;
|
|
5348
|
+
if (t === q) return 4;
|
|
5349
|
+
let midWord = false;
|
|
5350
|
+
for (let i = t.indexOf(q); i !== -1; i = t.indexOf(q, i + 1)) {
|
|
5351
|
+
if (i === 0) return 3;
|
|
5352
|
+
if (!/[a-z0-9]/.test(t[i - 1])) return 2;
|
|
5353
|
+
midWord = true;
|
|
4538
5354
|
}
|
|
4539
|
-
return
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
return
|
|
4543
|
-
}
|
|
4544
|
-
function orderThreads(threads) {
|
|
4545
|
-
return [...threads].sort((a, b) => {
|
|
4546
|
-
if (a.status !== b.status) return a.status === "open" ? -1 : 1;
|
|
4547
|
-
return a.createdAt.localeCompare(b.createdAt);
|
|
4548
|
-
});
|
|
5355
|
+
if (midWord) return 1;
|
|
5356
|
+
const terms = q.split(/\s+/).filter(Boolean);
|
|
5357
|
+
if (terms.length > 1 && terms.every((term) => t.includes(term))) return 1;
|
|
5358
|
+
return 0;
|
|
4549
5359
|
}
|
|
4550
|
-
function
|
|
4551
|
-
const
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
5360
|
+
function matchDocuments(documents, query) {
|
|
5361
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
5362
|
+
if (terms.length === 0) return [];
|
|
5363
|
+
return documents.filter((node) => {
|
|
5364
|
+
const haystack = [
|
|
5365
|
+
node.frontmatter.title,
|
|
5366
|
+
node.body || "",
|
|
5367
|
+
node.frontmatter.type || "",
|
|
5368
|
+
...node.frontmatter.tags || []
|
|
5369
|
+
].join(" ").toLowerCase();
|
|
5370
|
+
return terms.every((term) => haystack.includes(term));
|
|
5371
|
+
}).sort(
|
|
5372
|
+
(a, b) => titleScore(b.frontmatter.title, query) - titleScore(a.frontmatter.title, query)
|
|
4556
5373
|
);
|
|
4557
|
-
lines.push("");
|
|
4558
|
-
if (threads.length === 0) {
|
|
4559
|
-
lines.push("_No annotations yet._");
|
|
4560
|
-
lines.push("");
|
|
4561
|
-
return lines.join("\n");
|
|
4562
|
-
}
|
|
4563
|
-
const buckets2 = /* @__PURE__ */ new Map();
|
|
4564
|
-
for (const t of threads) {
|
|
4565
|
-
const key = t.snapshotVersion ?? null;
|
|
4566
|
-
(buckets2.get(key) ?? buckets2.set(key, []).get(key)).push(t);
|
|
4567
|
-
}
|
|
4568
|
-
const keys = [...buckets2.keys()].sort((a, b) => {
|
|
4569
|
-
if (a == null) return 1;
|
|
4570
|
-
if (b == null) return -1;
|
|
4571
|
-
return b - a;
|
|
4572
|
-
});
|
|
4573
|
-
for (const key of keys) {
|
|
4574
|
-
lines.push(`## ${snapshotLabel(key)}`);
|
|
4575
|
-
lines.push("");
|
|
4576
|
-
for (const thread of orderThreads(buckets2.get(key))) {
|
|
4577
|
-
lines.push(`### Thread \xB7 ${statusHeading(thread)}`);
|
|
4578
|
-
lines.push(renderAnchor(thread.anchor));
|
|
4579
|
-
for (const comment of thread.comments) {
|
|
4580
|
-
lines.push(
|
|
4581
|
-
`- **${comment.author}** \xB7 ${comment.createdAt} \u2014 ${comment.body}`
|
|
4582
|
-
);
|
|
4583
|
-
}
|
|
4584
|
-
lines.push("");
|
|
4585
|
-
}
|
|
4586
|
-
}
|
|
4587
|
-
return lines.join("\n");
|
|
4588
5374
|
}
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
const
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
);
|
|
4597
|
-
return rows.map((r) => ({
|
|
4598
|
-
id: r.id,
|
|
4599
|
-
author: r.author,
|
|
4600
|
-
body: r.body,
|
|
4601
|
-
createdAt: r.created_at
|
|
4602
|
-
}));
|
|
5375
|
+
function bodySnippet(body, query) {
|
|
5376
|
+
const flat = body.replace(/\s+/g, " ").trim();
|
|
5377
|
+
const terms = (query || "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
5378
|
+
const at = terms.map((t) => flat.toLowerCase().indexOf(t)).filter((i) => i >= 0).sort((a, b) => a - b)[0];
|
|
5379
|
+
if (at === void 0 || at < 80) return flat.slice(0, 200);
|
|
5380
|
+
const start = Math.max(0, at - 60);
|
|
5381
|
+
return `\u2026${flat.slice(start, start + 200)}`;
|
|
4603
5382
|
}
|
|
4604
|
-
|
|
4605
|
-
let anchor = null;
|
|
4606
|
-
if (row.anchor_json) {
|
|
4607
|
-
try {
|
|
4608
|
-
anchor = JSON.parse(row.anchor_json);
|
|
4609
|
-
} catch {
|
|
4610
|
-
anchor = null;
|
|
4611
|
-
}
|
|
4612
|
-
}
|
|
5383
|
+
function toSearchHit(node, query) {
|
|
4613
5384
|
return {
|
|
4614
|
-
id:
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
status: row.status,
|
|
4620
|
-
createdBy: row.created_by,
|
|
4621
|
-
createdAt: row.created_at,
|
|
4622
|
-
resolvedBy: row.resolved_by,
|
|
4623
|
-
resolvedAt: row.resolved_at,
|
|
4624
|
-
comments: await loadComments(row.id)
|
|
5385
|
+
id: node.id,
|
|
5386
|
+
title: node.frontmatter.title,
|
|
5387
|
+
type: node.frontmatter.type || "document",
|
|
5388
|
+
tags: node.frontmatter.tags || [],
|
|
5389
|
+
snippet: bodySnippet(node.body || "", query)
|
|
4625
5390
|
};
|
|
4626
5391
|
}
|
|
4627
|
-
|
|
4628
|
-
|
|
5392
|
+
|
|
5393
|
+
// src/nodes/routes.ts
|
|
5394
|
+
import { Hono as Hono10 } from "hono";
|
|
5395
|
+
|
|
5396
|
+
// src/nodes/markdown-export.ts
|
|
5397
|
+
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
5398
|
+
function nodeToMarkdown(node) {
|
|
5399
|
+
const tags = node.tags ?? [];
|
|
5400
|
+
return [
|
|
5401
|
+
"---",
|
|
5402
|
+
`title: ${JSON.stringify(node.title ?? "")}`,
|
|
5403
|
+
`tags: [${tags.map((t) => JSON.stringify(t)).join(", ")}]`,
|
|
5404
|
+
`status: ${JSON.stringify(node.status ?? "")}`,
|
|
5405
|
+
`id: ${JSON.stringify(node.id)}`,
|
|
5406
|
+
"---",
|
|
5407
|
+
""
|
|
5408
|
+
].join("\n") + (node.body ?? "");
|
|
4629
5409
|
}
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
`SELECT node_id, CAST(COUNT(*) AS INTEGER) AS count, MAX(created_at) AS last_at
|
|
4633
|
-
FROM annotation_threads
|
|
4634
|
-
WHERE nest_id = ?
|
|
4635
|
-
GROUP BY node_id`,
|
|
4636
|
-
[nestId]
|
|
4637
|
-
);
|
|
4638
|
-
const out = {};
|
|
4639
|
-
for (const r of rows) out[r.node_id] = { count: r.count, lastAt: r.last_at };
|
|
4640
|
-
return out;
|
|
5410
|
+
function nodesToMarkdown(nodes) {
|
|
5411
|
+
return nodes.map(nodeToMarkdown).join("\n\n---\n\n");
|
|
4641
5412
|
}
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
"SELECT * FROM annotation_threads WHERE nest_id = ? AND node_id = ? ORDER BY created_at ASC, id ASC",
|
|
4645
|
-
[nestId, nodeId]
|
|
4646
|
-
);
|
|
4647
|
-
return Promise.all(rows.map(rowToThread));
|
|
5413
|
+
function isMarkdownFormat(c) {
|
|
5414
|
+
return c.req.query("format") === "markdown";
|
|
4648
5415
|
}
|
|
4649
|
-
|
|
4650
|
-
const
|
|
4651
|
-
if (
|
|
4652
|
-
|
|
5416
|
+
function isRawFormat(c) {
|
|
5417
|
+
const format = c.req.query("format");
|
|
5418
|
+
if (format !== void 0) return format === "raw";
|
|
5419
|
+
const accept = c.req.header("Accept") ?? "";
|
|
5420
|
+
return accept.split(",").some((part) => part.trim().split(";")[0].trim().toLowerCase() === "text/markdown");
|
|
5421
|
+
}
|
|
5422
|
+
var RAW_CONTENT_TYPES = {
|
|
5423
|
+
artifact: "text/html; charset=utf-8",
|
|
5424
|
+
tool: "text/plain; charset=utf-8",
|
|
5425
|
+
table: "text/plain; charset=utf-8"
|
|
5426
|
+
};
|
|
5427
|
+
function rawContentType(nodeType) {
|
|
5428
|
+
return RAW_CONTENT_TYPES[nodeType || "document"] ?? "text/markdown; charset=utf-8";
|
|
5429
|
+
}
|
|
5430
|
+
function frontmatterBlockOf(raw) {
|
|
5431
|
+
if (!raw || !raw.startsWith("---")) return "";
|
|
5432
|
+
try {
|
|
5433
|
+
const body = parseDocument2("frontmatter-split.md", raw, "frontmatter-split").body ?? "";
|
|
5434
|
+
if (raw.endsWith(body)) return raw.slice(0, raw.length - body.length);
|
|
5435
|
+
} catch {
|
|
4653
5436
|
}
|
|
4654
|
-
const
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
);
|
|
4672
|
-
await tx.run(
|
|
4673
|
-
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
|
|
4674
|
-
[uuid5(), id, authorEmail, body]
|
|
4675
|
-
);
|
|
5437
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5438
|
+
return m ? `---
|
|
5439
|
+
${m[1]}
|
|
5440
|
+
---
|
|
5441
|
+
|
|
5442
|
+
` : "";
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5445
|
+
// src/nodes/routes.ts
|
|
5446
|
+
var nodeRoutes = new Hono10();
|
|
5447
|
+
function nodeAsMarkdown(response, nodeId) {
|
|
5448
|
+
return nodeToMarkdown({
|
|
5449
|
+
id: nodeId,
|
|
5450
|
+
title: response.title,
|
|
5451
|
+
tags: response.tags,
|
|
5452
|
+
status: response.status,
|
|
5453
|
+
body: response.content
|
|
4676
5454
|
});
|
|
4677
|
-
return rowToThread(await getThreadRow(id));
|
|
4678
5455
|
}
|
|
4679
|
-
|
|
4680
|
-
const
|
|
4681
|
-
|
|
4682
|
-
|
|
5456
|
+
function rawNodeBody(c, response, storedRaw) {
|
|
5457
|
+
const body = response.content ?? "";
|
|
5458
|
+
const payload = c.req.query("frontmatter") === "1" ? frontmatterBlockOf(storedRaw) + body : body;
|
|
5459
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
5460
|
+
if ((response.type || "document") === "artifact") {
|
|
5461
|
+
applyArtifactSandboxHeaders(c);
|
|
4683
5462
|
}
|
|
4684
|
-
return
|
|
5463
|
+
return c.body(payload, 200, {
|
|
5464
|
+
"Content-Type": rawContentType(response.type)
|
|
5465
|
+
});
|
|
4685
5466
|
}
|
|
4686
|
-
|
|
4687
|
-
const
|
|
4688
|
-
|
|
4689
|
-
|
|
5467
|
+
nodeRoutes.get("/", async (c) => {
|
|
5468
|
+
const nestId = c.req.param("nestId");
|
|
5469
|
+
const userId = c.get("userId");
|
|
5470
|
+
const q = c.req.query();
|
|
5471
|
+
const limit = q.limit ? parseInt(q.limit, 10) : void 0;
|
|
5472
|
+
const nodes = await listNodesForCaller(nestId, userId, {
|
|
5473
|
+
...q.type ? { type: q.type } : {},
|
|
5474
|
+
...q.tag ? { tag: q.tag } : {},
|
|
5475
|
+
...q.status ? { status: q.status } : {},
|
|
5476
|
+
// Lets an owner preview what making the nest public would expose.
|
|
5477
|
+
...q.approved_only === "1" || q.approved_only === "true" ? { approvedOnly: true } : {},
|
|
5478
|
+
// Ignore junk rather than 400 — a bad ?limit should not fail a listing.
|
|
5479
|
+
...limit !== void 0 && Number.isFinite(limit) && limit > 0 ? { limit } : {},
|
|
5480
|
+
// Present (even as "") = one folder level only, for the lazily-expanded
|
|
5481
|
+
// document tree. Absent = the whole nest, as every existing caller expects.
|
|
5482
|
+
...q.folder !== void 0 ? { folder: q.folder } : {},
|
|
5483
|
+
// Bodies are off by default — the list is served from the DB index, which
|
|
5484
|
+
// holds metadata only. Opt back in and the listing crawls the vault for
|
|
5485
|
+
// them; kept for any client that was reading `content` off this route.
|
|
5486
|
+
...q.content === "1" || q.content === "true" ? { withContent: true } : {}
|
|
5487
|
+
});
|
|
5488
|
+
return c.json({ count: nodes.length, nodes });
|
|
5489
|
+
});
|
|
5490
|
+
nodeRoutes.post("/", async (c) => {
|
|
5491
|
+
const body = await c.req.json();
|
|
5492
|
+
if (!body.title || !body.content) {
|
|
5493
|
+
throw new ValidationError("title and content are required");
|
|
4690
5494
|
}
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
5495
|
+
if (body.folder !== void 0 && typeof body.folder !== "string") {
|
|
5496
|
+
throw new ValidationError('folder must be a string path like "gtm/deals"');
|
|
5497
|
+
}
|
|
5498
|
+
const nestId = c.req.param("nestId");
|
|
5499
|
+
const authorEmail = await getUserEmail(c);
|
|
5500
|
+
const { node } = await createNode(
|
|
5501
|
+
nestId,
|
|
5502
|
+
{
|
|
5503
|
+
title: body.title,
|
|
5504
|
+
content: body.content,
|
|
5505
|
+
type: body.type,
|
|
5506
|
+
tags: body.tags,
|
|
5507
|
+
scope: body.scope,
|
|
5508
|
+
status: body.status,
|
|
5509
|
+
schedule: body.schedule,
|
|
5510
|
+
folder: body.folder,
|
|
5511
|
+
prime: body.prime
|
|
5512
|
+
},
|
|
5513
|
+
authorEmail
|
|
4695
5514
|
);
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
5515
|
+
const resolved = await resolveStewardsForNode(nestId, node.id);
|
|
5516
|
+
return c.json({
|
|
5517
|
+
node: toNodeResponse(node),
|
|
5518
|
+
stewards: resolved.length > 0 ? resolved.map((r) => ({
|
|
5519
|
+
email: r.steward.userEmail,
|
|
5520
|
+
role: r.steward.role,
|
|
5521
|
+
source: r.source
|
|
5522
|
+
})) : void 0
|
|
5523
|
+
}, 201);
|
|
5524
|
+
});
|
|
5525
|
+
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
5526
|
+
const nestId = c.req.param("nestId");
|
|
5527
|
+
const nodeId = c.req.param("nodeId");
|
|
5528
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-HRVD7SIS.js");
|
|
5529
|
+
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
5530
|
+
nestId,
|
|
5531
|
+
nodeId
|
|
5532
|
+
);
|
|
5533
|
+
return c.json({
|
|
5534
|
+
nodeId,
|
|
5535
|
+
stewards: stewards.map((r) => ({
|
|
5536
|
+
email: r.steward.userEmail,
|
|
5537
|
+
role: r.steward.role,
|
|
5538
|
+
scope: r.steward.scope,
|
|
5539
|
+
source: r.source,
|
|
5540
|
+
priority: r.priority
|
|
5541
|
+
})),
|
|
5542
|
+
fallbackToOwner,
|
|
5543
|
+
ownerEmail
|
|
5544
|
+
});
|
|
5545
|
+
});
|
|
5546
|
+
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
5547
|
+
const nestId = c.req.param("nestId");
|
|
5548
|
+
const nodeId = c.req.param("nodeId");
|
|
5549
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-TJRHRMMZ.js");
|
|
5550
|
+
const history = await getReviewHistory2(nestId, nodeId);
|
|
5551
|
+
return c.json({ reviews: history });
|
|
5552
|
+
});
|
|
5553
|
+
nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
5554
|
+
const nestId = c.req.param("nestId");
|
|
5555
|
+
const nodeId = c.req.param("nodeId");
|
|
5556
|
+
const { versions: versionManager } = await engineCache.get(nestId);
|
|
5557
|
+
const userId = c.get("userId");
|
|
5558
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
5559
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
5560
|
+
return c.json(
|
|
5561
|
+
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
5562
|
+
403
|
|
4710
5563
|
);
|
|
4711
5564
|
}
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
let
|
|
4718
|
-
let type = null;
|
|
4719
|
-
let liveHtml = "";
|
|
4720
|
-
let liveExists = true;
|
|
5565
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5566
|
+
const targetVersion = Number(body.targetVersion);
|
|
5567
|
+
if (!Number.isInteger(targetVersion) || targetVersion < 1) {
|
|
5568
|
+
throw new ValidationError("targetVersion (a positive integer) is required");
|
|
5569
|
+
}
|
|
5570
|
+
let raw;
|
|
4721
5571
|
try {
|
|
4722
|
-
|
|
4723
|
-
title = node.frontmatter?.title || nodeId;
|
|
4724
|
-
type = node.frontmatter?.type || null;
|
|
4725
|
-
liveHtml = node.body || "";
|
|
5572
|
+
raw = await versionManager.reconstructVersion(nodeId, targetVersion);
|
|
4726
5573
|
} catch {
|
|
4727
|
-
|
|
5574
|
+
throw new NotFoundError(
|
|
5575
|
+
`Version ${targetVersion} not found for ${nodeId}`
|
|
5576
|
+
);
|
|
4728
5577
|
}
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
5578
|
+
const content = bodyOnly(nodeId, raw);
|
|
5579
|
+
const { node, version } = await updateNode(
|
|
5580
|
+
nestId,
|
|
5581
|
+
nodeId,
|
|
5582
|
+
{ content, changeNote: `Restored from version ${targetVersion}` },
|
|
5583
|
+
userEmail
|
|
5584
|
+
);
|
|
5585
|
+
await trackEvent("node.revert", { nestId, nodeId, targetVersion });
|
|
5586
|
+
return c.json({ ok: true, version, node: toNodeResponse(node) });
|
|
5587
|
+
});
|
|
5588
|
+
nodeRoutes.post("/:nodeId{.+}/move", async (c) => {
|
|
5589
|
+
const nestId = c.req.param("nestId");
|
|
5590
|
+
const nodeId = c.req.param("nodeId");
|
|
5591
|
+
const userId = c.get("userId");
|
|
5592
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
5593
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
5594
|
+
return c.json(
|
|
5595
|
+
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
5596
|
+
403
|
|
5597
|
+
);
|
|
4739
5598
|
}
|
|
4740
|
-
|
|
4741
|
-
|
|
5599
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5600
|
+
if (typeof body.folder !== "string") {
|
|
5601
|
+
throw new ValidationError('folder (a string; "" for the nest root) is required');
|
|
4742
5602
|
}
|
|
4743
|
-
|
|
4744
|
-
}
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
5603
|
+
const { node, previousId } = await moveNode(nestId, nodeId, body.folder, userEmail);
|
|
5604
|
+
return c.json({ ok: true, node: toNodeResponse(node), previousId });
|
|
5605
|
+
});
|
|
5606
|
+
nodeRoutes.get("/:nodeId{.+}/watchers", async (c) => {
|
|
5607
|
+
const nestId = c.req.param("nestId");
|
|
5608
|
+
const nodeId = c.req.param("nodeId");
|
|
5609
|
+
return c.json({ watchers: await listWatchers(nestId, nodeId) });
|
|
5610
|
+
});
|
|
5611
|
+
nodeRoutes.post("/:nodeId{.+}/watchers", async (c) => {
|
|
5612
|
+
const nestId = c.req.param("nestId");
|
|
5613
|
+
const nodeId = c.req.param("nodeId");
|
|
5614
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5615
|
+
const email = typeof body.email === "string" ? body.email.trim() : "";
|
|
5616
|
+
if (!email || !email.includes("@")) {
|
|
5617
|
+
throw new ValidationError("email is required");
|
|
5618
|
+
}
|
|
5619
|
+
await addWatcher(nestId, nodeId, email, await getUserEmail(c));
|
|
5620
|
+
return c.json({ watchers: await listWatchers(nestId, nodeId) }, 201);
|
|
5621
|
+
});
|
|
5622
|
+
nodeRoutes.delete("/:nodeId{.+}/watchers", async (c) => {
|
|
5623
|
+
const nestId = c.req.param("nestId");
|
|
5624
|
+
const nodeId = c.req.param("nodeId");
|
|
5625
|
+
const email = c.req.query("email")?.trim() ?? "";
|
|
5626
|
+
if (!email) throw new ValidationError("email query param is required");
|
|
5627
|
+
if (!await removeWatcher(nestId, nodeId, email)) {
|
|
5628
|
+
throw new NotFoundError("Not watching");
|
|
5629
|
+
}
|
|
5630
|
+
return c.json({ watchers: await listWatchers(nestId, nodeId) });
|
|
5631
|
+
});
|
|
5632
|
+
nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
5633
|
+
const nestId = c.req.param("nestId");
|
|
5634
|
+
const nodeId = c.req.param("nodeId");
|
|
5635
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
5636
|
+
const userId = c.get("userId");
|
|
5637
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
5638
|
+
let node = await getNodeForCaller(
|
|
5639
|
+
nestId,
|
|
5640
|
+
{ id: nodeId },
|
|
5641
|
+
userId,
|
|
5642
|
+
userEmail,
|
|
5643
|
+
{ verifyChecksum: true }
|
|
5644
|
+
);
|
|
5645
|
+
if (node.pendingChange) {
|
|
4753
5646
|
try {
|
|
4754
|
-
|
|
4755
|
-
const
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
5647
|
+
await scanDocumentForDrift(nestId, nodeId, userEmail || "system:read");
|
|
5648
|
+
const refreshed = await getPendingChange(nestId, nodeId);
|
|
5649
|
+
if (refreshed) node.pendingChange = refreshed;
|
|
5650
|
+
} catch (err) {
|
|
5651
|
+
console.error("[external-edit] stage-on-read failed:", err);
|
|
4759
5652
|
}
|
|
4760
|
-
if (type !== null && type !== "artifact") return;
|
|
4761
|
-
const threads = await listThreads(nestId, nodeId);
|
|
4762
|
-
const derivedTitle = `${title.slice(0, 100)} \u2014 Annotations`;
|
|
4763
|
-
const markdown = projectThreadsToMarkdown(title, threads);
|
|
4764
|
-
const derivedId = derivedAnnotationsId(nodeId);
|
|
4765
5653
|
try {
|
|
4766
|
-
await
|
|
5654
|
+
const latest = await loadLatestApprovedNode(nestId, nodeId);
|
|
5655
|
+
if (latest) {
|
|
5656
|
+
node = { ...latest, pendingChange: node.pendingChange };
|
|
5657
|
+
}
|
|
4767
5658
|
} catch (err) {
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
5659
|
+
console.error("[external-edit] reconstruct-latest failed:", err);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
const response = toNodeResponse(node);
|
|
5663
|
+
const publicReader = await isPublicReader(nestId, userId);
|
|
5664
|
+
if (!publicReader) {
|
|
5665
|
+
const people = (await resolveAuthorship(nestId, [
|
|
5666
|
+
{ id: nodeId, frontmatterAuthor: node.frontmatter?.author ?? null }
|
|
5667
|
+
])).get(nodeId);
|
|
5668
|
+
if (people) {
|
|
5669
|
+
response.author = people.author;
|
|
5670
|
+
response.collaborators = people.collaborators;
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
|
|
5674
|
+
if (publicReader) {
|
|
5675
|
+
const approved = await getApprovedVersion(nestId, nodeId);
|
|
5676
|
+
if (approved != null) {
|
|
5677
|
+
let approvedRaw = "";
|
|
5678
|
+
try {
|
|
5679
|
+
approvedRaw = await versionManager.reconstructVersion(
|
|
5680
|
+
nodeId,
|
|
5681
|
+
approved
|
|
4779
5682
|
);
|
|
4780
|
-
|
|
4781
|
-
|
|
5683
|
+
response.content = bodyOnly(nodeId, approvedRaw);
|
|
5684
|
+
} catch (err) {
|
|
5685
|
+
console.error(
|
|
5686
|
+
"reconstructVersion failed (public single)",
|
|
5687
|
+
nodeId,
|
|
5688
|
+
approved,
|
|
5689
|
+
err
|
|
5690
|
+
);
|
|
5691
|
+
response.content = "";
|
|
5692
|
+
}
|
|
5693
|
+
response.version = approved;
|
|
5694
|
+
response.status = "published";
|
|
5695
|
+
if (isRawFormat(c)) {
|
|
5696
|
+
return rawNodeBody(c, response, approvedRaw);
|
|
5697
|
+
}
|
|
5698
|
+
if (isMarkdownFormat(c)) {
|
|
5699
|
+
return c.body(nodeAsMarkdown(response, nodeId), 200, {
|
|
5700
|
+
"Content-Type": "text/markdown; charset=utf-8"
|
|
5701
|
+
});
|
|
4782
5702
|
}
|
|
5703
|
+
return c.json({ node: response });
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
response.status = node.pendingChange ? "external_edit_pending" : await getDisplayStatus(nestId, nodeId);
|
|
5707
|
+
if (response.status === "pending_review") {
|
|
5708
|
+
const pending = await getPendingReview(nestId, nodeId);
|
|
5709
|
+
response.pendingReviewBy = pending?.requestedBy ?? null;
|
|
5710
|
+
}
|
|
5711
|
+
response.deletionRequest = await getLatestDeletionRequest(
|
|
5712
|
+
nestId,
|
|
5713
|
+
nodeId
|
|
5714
|
+
);
|
|
5715
|
+
if (isRawFormat(c)) {
|
|
5716
|
+
return rawNodeBody(c, response, node.rawContent);
|
|
5717
|
+
}
|
|
5718
|
+
if (isMarkdownFormat(c)) {
|
|
5719
|
+
return c.body(nodeAsMarkdown(response, nodeId), 200, {
|
|
5720
|
+
"Content-Type": "text/markdown; charset=utf-8"
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
return c.json({ node: response });
|
|
5724
|
+
});
|
|
5725
|
+
nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
5726
|
+
const nestId = c.req.param("nestId");
|
|
5727
|
+
const nodeId = c.req.param("nodeId");
|
|
5728
|
+
const body = await c.req.json();
|
|
5729
|
+
const baseVersionHeader = c.req.header("X-Base-Version");
|
|
5730
|
+
if (baseVersionHeader) {
|
|
5731
|
+
const baseVersion = parseInt(baseVersionHeader, 10);
|
|
5732
|
+
const conflict = await checkConflict(nestId, nodeId, baseVersion);
|
|
5733
|
+
if (conflict.conflict) {
|
|
5734
|
+
return c.json(
|
|
5735
|
+
{
|
|
5736
|
+
error: "Version conflict",
|
|
5737
|
+
your_version: baseVersion,
|
|
5738
|
+
current_version: conflict.currentVersion,
|
|
5739
|
+
updated_by: conflict.updatedBy,
|
|
5740
|
+
updated_at: conflict.updatedAt,
|
|
5741
|
+
rejected_content: body.content || body.append || null
|
|
5742
|
+
},
|
|
5743
|
+
409
|
|
5744
|
+
);
|
|
4783
5745
|
}
|
|
4784
|
-
} catch (err) {
|
|
4785
|
-
console.warn(
|
|
4786
|
-
`[annotations] failed to sync derived node for ${nestId}/${nodeId}:`,
|
|
4787
|
-
err
|
|
4788
|
-
);
|
|
4789
5746
|
}
|
|
5747
|
+
const authorEmail = await getUserEmail(c);
|
|
5748
|
+
const { node, version: responseVersion } = await updateNode(
|
|
5749
|
+
nestId,
|
|
5750
|
+
nodeId,
|
|
5751
|
+
{
|
|
5752
|
+
content: body.content,
|
|
5753
|
+
append: body.append,
|
|
5754
|
+
tags: body.tags,
|
|
5755
|
+
title: body.title,
|
|
5756
|
+
status: body.status,
|
|
5757
|
+
changeNote: body.changeNote,
|
|
5758
|
+
schedule: body.schedule,
|
|
5759
|
+
prime: body.prime
|
|
5760
|
+
},
|
|
5761
|
+
authorEmail
|
|
5762
|
+
);
|
|
5763
|
+
return c.json({ node: toNodeResponse(node), version: responseVersion });
|
|
5764
|
+
});
|
|
5765
|
+
nodeRoutes.delete("/:nodeId{.+}", async (c) => {
|
|
5766
|
+
const nestId = c.req.param("nestId");
|
|
5767
|
+
const nodeId = c.req.param("nodeId");
|
|
5768
|
+
await deleteNode(nestId, nodeId, await getUserEmail(c));
|
|
5769
|
+
return c.json({ deleted: true });
|
|
5770
|
+
});
|
|
5771
|
+
async function getUserEmail(c) {
|
|
5772
|
+
const userId = c.get("userId");
|
|
5773
|
+
const db = getDb();
|
|
5774
|
+
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
5775
|
+
return user?.email || "anonymous@localhost";
|
|
4790
5776
|
}
|
|
4791
5777
|
|
|
5778
|
+
// src/annotations/routes.ts
|
|
5779
|
+
import { Hono as Hono11 } from "hono";
|
|
5780
|
+
|
|
4792
5781
|
// src/annotations/types.ts
|
|
4793
5782
|
var ARTIFACT_NODE_TYPE = "artifact";
|
|
4794
5783
|
|
|
4795
5784
|
// src/annotations/routes.ts
|
|
4796
|
-
var annotationRoutes = new
|
|
5785
|
+
var annotationRoutes = new Hono11();
|
|
4797
5786
|
function getNodeId(c) {
|
|
4798
5787
|
const raw = c.req.param("nodeId");
|
|
4799
5788
|
try {
|
|
@@ -4907,7 +5896,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
4907
5896
|
});
|
|
4908
5897
|
|
|
4909
5898
|
// src/nodes/query-routes.ts
|
|
4910
|
-
import { Hono as
|
|
5899
|
+
import { Hono as Hono12 } from "hono";
|
|
4911
5900
|
|
|
4912
5901
|
// src/tables/service.ts
|
|
4913
5902
|
function splitCsvLine(line) {
|
|
@@ -5408,7 +6397,9 @@ function extractWikiTargets(body) {
|
|
|
5408
6397
|
}
|
|
5409
6398
|
async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
5410
6399
|
const { storage } = await engineCache.get(nestId);
|
|
5411
|
-
const docs = await storage.discoverDocuments()
|
|
6400
|
+
const docs = (await storage.discoverDocuments()).filter(
|
|
6401
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
6402
|
+
);
|
|
5412
6403
|
const accessible = await filterAccessible(nestId, userId, userEmail, docs);
|
|
5413
6404
|
const idSet = new Set(accessible.map((d) => d.id));
|
|
5414
6405
|
const titleToId = /* @__PURE__ */ new Map();
|
|
@@ -5539,7 +6530,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
5539
6530
|
}
|
|
5540
6531
|
|
|
5541
6532
|
// src/nodes/query-routes.ts
|
|
5542
|
-
var queryRoutes = new
|
|
6533
|
+
var queryRoutes = new Hono12();
|
|
5543
6534
|
queryRoutes.get("/changes", async (c) => {
|
|
5544
6535
|
const nestId = c.req.param("nestId");
|
|
5545
6536
|
const userId = c.get("userId");
|
|
@@ -5892,7 +6883,9 @@ queryRoutes.get("/search", async (c) => {
|
|
|
5892
6883
|
}
|
|
5893
6884
|
const nestId = c.req.param("nestId");
|
|
5894
6885
|
const { storage } = await engineCache.get(nestId);
|
|
5895
|
-
const matches = matchDocuments(await discoverForSearch(storage), q)
|
|
6886
|
+
const matches = matchDocuments(await discoverForSearch(storage), q).filter(
|
|
6887
|
+
(n) => !isDerivedAnnotationsNode(n.id)
|
|
6888
|
+
);
|
|
5896
6889
|
const userId = c.get("userId");
|
|
5897
6890
|
const userEmail = await resolveCallerEmail(userId);
|
|
5898
6891
|
const accessible = await filterAccessible(nestId, userId, userEmail, matches);
|
|
@@ -5904,7 +6897,9 @@ queryRoutes.get("/search", async (c) => {
|
|
|
5904
6897
|
});
|
|
5905
6898
|
queryRoutes.get("/overview", async (c) => {
|
|
5906
6899
|
const { storage } = await engineCache.get(c.req.param("nestId"));
|
|
5907
|
-
const documents = await storage.discoverDocuments()
|
|
6900
|
+
const documents = (await storage.discoverDocuments()).filter(
|
|
6901
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
6902
|
+
);
|
|
5908
6903
|
const types = {};
|
|
5909
6904
|
const tags = {};
|
|
5910
6905
|
for (const node of documents) {
|
|
@@ -5939,6 +6934,14 @@ queryRoutes.get("/folders", async (c) => {
|
|
|
5939
6934
|
});
|
|
5940
6935
|
return c.json({ folders });
|
|
5941
6936
|
});
|
|
6937
|
+
queryRoutes.post("/folders", async (c) => {
|
|
6938
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6939
|
+
if (typeof body.folder !== "string" || !body.folder.trim()) {
|
|
6940
|
+
throw new ValidationError('folder (a string like "gtm/deals") is required');
|
|
6941
|
+
}
|
|
6942
|
+
const folder = await createEmptyFolder(c.req.param("nestId"), body.folder);
|
|
6943
|
+
return c.json({ folder }, 201);
|
|
6944
|
+
});
|
|
5942
6945
|
queryRoutes.post("/reindex", async (c) => {
|
|
5943
6946
|
if (permissionLevel(c.get("nestPermission")) < permissionLevel("write")) {
|
|
5944
6947
|
throw new ForbiddenError("Reindexing a nest requires write access");
|
|
@@ -6089,7 +7092,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
6089
7092
|
});
|
|
6090
7093
|
|
|
6091
7094
|
// src/mcp/routes.ts
|
|
6092
|
-
import { Hono as
|
|
7095
|
+
import { Hono as Hono14 } from "hono";
|
|
6093
7096
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6094
7097
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
6095
7098
|
|
|
@@ -6169,10 +7172,10 @@ async function listTraceEvents(filters = {}) {
|
|
|
6169
7172
|
}
|
|
6170
7173
|
|
|
6171
7174
|
// src/mcp/workflow-tools.ts
|
|
6172
|
-
import { v4 as
|
|
7175
|
+
import { v4 as uuid10 } from "uuid";
|
|
6173
7176
|
|
|
6174
7177
|
// src/workflow/run-service.ts
|
|
6175
|
-
import { v4 as
|
|
7178
|
+
import { v4 as uuid8 } from "uuid";
|
|
6176
7179
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
6177
7180
|
async function flowSubgraph(nestId, agentNode) {
|
|
6178
7181
|
const db = getDb();
|
|
@@ -6302,7 +7305,7 @@ async function triggerRun(opts) {
|
|
|
6302
7305
|
}
|
|
6303
7306
|
const edges = await flowSubgraph(nestId, agentNode);
|
|
6304
7307
|
const definitions = await referencedDefinitions(nestId, edges);
|
|
6305
|
-
const id = `run_${
|
|
7308
|
+
const id = `run_${uuid8()}`;
|
|
6306
7309
|
const db = getDb();
|
|
6307
7310
|
await db.transaction(async (tx) => {
|
|
6308
7311
|
if (parentRunId) {
|
|
@@ -6488,8 +7491,8 @@ async function listRuns(nestId, opts) {
|
|
|
6488
7491
|
}
|
|
6489
7492
|
|
|
6490
7493
|
// src/workflow/edge-routes.ts
|
|
6491
|
-
import { Hono as
|
|
6492
|
-
import { v4 as
|
|
7494
|
+
import { Hono as Hono13 } from "hono";
|
|
7495
|
+
import { v4 as uuid9 } from "uuid";
|
|
6493
7496
|
function toResponse(e, t) {
|
|
6494
7497
|
return {
|
|
6495
7498
|
id: e.id,
|
|
@@ -6603,7 +7606,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
|
|
|
6603
7606
|
);
|
|
6604
7607
|
}
|
|
6605
7608
|
}
|
|
6606
|
-
var edgeRoutes = new
|
|
7609
|
+
var edgeRoutes = new Hono13();
|
|
6607
7610
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
6608
7611
|
const nestId = c.req.param("nestId");
|
|
6609
7612
|
const db = getDb();
|
|
@@ -6671,7 +7674,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
6671
7674
|
);
|
|
6672
7675
|
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
6673
7676
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6674
|
-
const id =
|
|
7677
|
+
const id = uuid9();
|
|
6675
7678
|
await db.transaction(async (tx) => {
|
|
6676
7679
|
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
6677
7680
|
if (type.is_flow) {
|
|
@@ -7072,7 +8075,7 @@ async function createEdge(ctx, args) {
|
|
|
7072
8075
|
);
|
|
7073
8076
|
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
7074
8077
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7075
|
-
const id =
|
|
8078
|
+
const id = uuid10();
|
|
7076
8079
|
await db.transaction(async (tx) => {
|
|
7077
8080
|
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
7078
8081
|
if (type.is_flow) {
|
|
@@ -7136,7 +8139,7 @@ async function upsertEdgeType(ctx, args) {
|
|
|
7136
8139
|
`INSERT INTO edge_types
|
|
7137
8140
|
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
7138
8141
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
7139
|
-
[
|
|
8142
|
+
[uuid10(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
7140
8143
|
);
|
|
7141
8144
|
}
|
|
7142
8145
|
});
|
|
@@ -8589,19 +9592,19 @@ ${list}`,
|
|
|
8589
9592
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
8590
9593
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
8591
9594
|
try {
|
|
8592
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
8593
|
-
const db = (await import("./client-
|
|
9595
|
+
const { createGrant: createGrant2 } = await import("./grants-service-WC27TRY3.js");
|
|
9596
|
+
const db = (await import("./client-7Z2IVCPW.js")).getDb();
|
|
8594
9597
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
8595
9598
|
const e = normalizeEmail2(String(args.email || ""));
|
|
8596
9599
|
if (!e) return "email is required.";
|
|
8597
9600
|
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
8598
9601
|
if (!row) {
|
|
8599
9602
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
8600
|
-
const { v4:
|
|
8601
|
-
const id =
|
|
9603
|
+
const { v4: uuid18 } = await import("uuid");
|
|
9604
|
+
const id = uuid18();
|
|
8602
9605
|
await db.run(
|
|
8603
9606
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
8604
|
-
[id, e, null, await hashPassword2(
|
|
9607
|
+
[id, e, null, await hashPassword2(uuid18())]
|
|
8605
9608
|
);
|
|
8606
9609
|
row = { id };
|
|
8607
9610
|
}
|
|
@@ -8656,7 +9659,7 @@ ${list}`;
|
|
|
8656
9659
|
|
|
8657
9660
|
// src/mcp/routes.ts
|
|
8658
9661
|
import { z } from "zod";
|
|
8659
|
-
var mcpRoutes = new
|
|
9662
|
+
var mcpRoutes = new Hono14();
|
|
8660
9663
|
async function getUserEmail2(userId) {
|
|
8661
9664
|
const db = getDb();
|
|
8662
9665
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
@@ -8722,8 +9725,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
8722
9725
|
});
|
|
8723
9726
|
|
|
8724
9727
|
// src/workflow/run-routes.ts
|
|
8725
|
-
import { Hono as
|
|
8726
|
-
import { v4 as
|
|
9728
|
+
import { Hono as Hono15 } from "hono";
|
|
9729
|
+
import { v4 as uuid11 } from "uuid";
|
|
8727
9730
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
8728
9731
|
var safeJson2 = (s) => {
|
|
8729
9732
|
if (!s) return null;
|
|
@@ -8841,7 +9844,7 @@ async function buildRunBundle(nestId, agentNode) {
|
|
|
8841
9844
|
definitions
|
|
8842
9845
|
};
|
|
8843
9846
|
}
|
|
8844
|
-
var runTriggerRoutes = new
|
|
9847
|
+
var runTriggerRoutes = new Hono15();
|
|
8845
9848
|
async function resolveParentRun2(nestId, parentRunId, childAgent) {
|
|
8846
9849
|
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
8847
9850
|
throw new ValidationError(
|
|
@@ -8920,7 +9923,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
8920
9923
|
}
|
|
8921
9924
|
const bundle = await buildRunBundle(nestId, agentNode);
|
|
8922
9925
|
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
8923
|
-
const id = `run_${
|
|
9926
|
+
const id = `run_${uuid11()}`;
|
|
8924
9927
|
await getDb().run(
|
|
8925
9928
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
8926
9929
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
@@ -8949,7 +9952,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
8949
9952
|
201
|
|
8950
9953
|
);
|
|
8951
9954
|
});
|
|
8952
|
-
var runListRoutes = new
|
|
9955
|
+
var runListRoutes = new Hono15();
|
|
8953
9956
|
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
8954
9957
|
const nestId = c.req.param("nestId");
|
|
8955
9958
|
const db = getDb();
|
|
@@ -8981,7 +9984,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
8981
9984
|
}))
|
|
8982
9985
|
});
|
|
8983
9986
|
});
|
|
8984
|
-
var runDetailRoutes = new
|
|
9987
|
+
var runDetailRoutes = new Hono15();
|
|
8985
9988
|
async function loadRunWithPermission(c, runId, required) {
|
|
8986
9989
|
const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
|
|
8987
9990
|
runId
|
|
@@ -9132,8 +10135,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
9132
10135
|
});
|
|
9133
10136
|
|
|
9134
10137
|
// src/workflow/schedule-routes.ts
|
|
9135
|
-
import { Hono as
|
|
9136
|
-
import { v4 as
|
|
10138
|
+
import { Hono as Hono16 } from "hono";
|
|
10139
|
+
import { v4 as uuid12 } from "uuid";
|
|
9137
10140
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
9138
10141
|
var MIN_EVERY = 5;
|
|
9139
10142
|
var MAX_EVERY = 7 * 24 * 60;
|
|
@@ -9170,7 +10173,7 @@ function parseEvery(v) {
|
|
|
9170
10173
|
}
|
|
9171
10174
|
return n;
|
|
9172
10175
|
}
|
|
9173
|
-
var scheduleRoutes = new
|
|
10176
|
+
var scheduleRoutes = new Hono16();
|
|
9174
10177
|
scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
9175
10178
|
const nestId = c.req.param("nestId");
|
|
9176
10179
|
const rows = await getDb().all(
|
|
@@ -9211,7 +10214,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
9211
10214
|
);
|
|
9212
10215
|
}
|
|
9213
10216
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9214
|
-
const id = `sch_${
|
|
10217
|
+
const id = `sch_${uuid12()}`;
|
|
9215
10218
|
await db.run(
|
|
9216
10219
|
`INSERT INTO schedules (id, nest_id, agent_node, every_minutes, enabled, last_run_at, max_runs, runs_created, created_by, created_at, updated_at)
|
|
9217
10220
|
VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
|
|
@@ -9266,8 +10269,8 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
9266
10269
|
});
|
|
9267
10270
|
|
|
9268
10271
|
// src/notify/connector-routes.ts
|
|
9269
|
-
import { Hono as
|
|
9270
|
-
import { v4 as
|
|
10272
|
+
import { Hono as Hono17 } from "hono";
|
|
10273
|
+
import { v4 as uuid13 } from "uuid";
|
|
9271
10274
|
var KNOWN_EVENTS = [
|
|
9272
10275
|
"review_requested",
|
|
9273
10276
|
"review_approved",
|
|
@@ -9313,7 +10316,7 @@ function parseUrl(v) {
|
|
|
9313
10316
|
'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
|
|
9314
10317
|
);
|
|
9315
10318
|
}
|
|
9316
|
-
var connectorRoutes = new
|
|
10319
|
+
var connectorRoutes = new Hono17();
|
|
9317
10320
|
connectorRoutes.get("/", async (c) => {
|
|
9318
10321
|
const nestId = c.req.param("nestId");
|
|
9319
10322
|
const rows = await getDb().all(
|
|
@@ -9332,7 +10335,7 @@ connectorRoutes.post("/", async (c) => {
|
|
|
9332
10335
|
const url = parseUrl(body.url);
|
|
9333
10336
|
const events = parseEvents(body.events);
|
|
9334
10337
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9335
|
-
const id = `con_${
|
|
10338
|
+
const id = `con_${uuid13()}`;
|
|
9336
10339
|
await getDb().run(
|
|
9337
10340
|
`INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
|
|
9338
10341
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
@@ -9375,7 +10378,7 @@ connectorRoutes.delete("/:id", async (c) => {
|
|
|
9375
10378
|
});
|
|
9376
10379
|
|
|
9377
10380
|
// src/workflow/hook-routes.ts
|
|
9378
|
-
import { Hono as
|
|
10381
|
+
import { Hono as Hono18 } from "hono";
|
|
9379
10382
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
|
|
9380
10383
|
var PRESETS = ["slack", "teams", "webhook"];
|
|
9381
10384
|
var RUNNABLE_TYPES4 = ["agent", "skill"];
|
|
@@ -9398,7 +10401,7 @@ function toResponse4(c, r, opts = {}) {
|
|
|
9398
10401
|
created_at: r.created_at
|
|
9399
10402
|
};
|
|
9400
10403
|
}
|
|
9401
|
-
var hookNestRoutes = new
|
|
10404
|
+
var hookNestRoutes = new Hono18();
|
|
9402
10405
|
hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
9403
10406
|
const nestId = c.req.param("nestId");
|
|
9404
10407
|
const agent = c.req.query("agent");
|
|
@@ -9472,7 +10475,7 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
|
|
|
9472
10475
|
function stripMentions(text) {
|
|
9473
10476
|
return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
|
|
9474
10477
|
}
|
|
9475
|
-
var hookFireRoutes = new
|
|
10478
|
+
var hookFireRoutes = new Hono18();
|
|
9476
10479
|
hookFireRoutes.post("/:token", async (c) => {
|
|
9477
10480
|
const token = c.req.param("token");
|
|
9478
10481
|
if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
|
|
@@ -9572,9 +10575,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
9572
10575
|
});
|
|
9573
10576
|
|
|
9574
10577
|
// src/definitions/routes.ts
|
|
9575
|
-
import { Hono as
|
|
9576
|
-
import { v4 as
|
|
9577
|
-
var definitionRoutes = new
|
|
10578
|
+
import { Hono as Hono19 } from "hono";
|
|
10579
|
+
import { v4 as uuid14 } from "uuid";
|
|
10580
|
+
var definitionRoutes = new Hono19();
|
|
9578
10581
|
definitionRoutes.get("/", async (c) => {
|
|
9579
10582
|
const nestId = c.req.param("nestId");
|
|
9580
10583
|
const db = getDb();
|
|
@@ -9653,7 +10656,7 @@ definitionRoutes.post("/", async (c) => {
|
|
|
9653
10656
|
await applyUpdate(tx, existing.id);
|
|
9654
10657
|
targetId = existing.id;
|
|
9655
10658
|
} else {
|
|
9656
|
-
targetId =
|
|
10659
|
+
targetId = uuid14();
|
|
9657
10660
|
wasCreated = true;
|
|
9658
10661
|
await tx.run(
|
|
9659
10662
|
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
@@ -9682,11 +10685,11 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
9682
10685
|
});
|
|
9683
10686
|
|
|
9684
10687
|
// src/nests/asset-routes.ts
|
|
9685
|
-
import { Hono as
|
|
9686
|
-
import { v4 as
|
|
10688
|
+
import { Hono as Hono20 } from "hono";
|
|
10689
|
+
import { v4 as uuid15 } from "uuid";
|
|
9687
10690
|
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
9688
10691
|
import { join as join4 } from "path";
|
|
9689
|
-
var assetRoutes = new
|
|
10692
|
+
var assetRoutes = new Hono20();
|
|
9690
10693
|
var CONTENT_TYPES = {
|
|
9691
10694
|
png: "image/png",
|
|
9692
10695
|
jpg: "image/jpeg",
|
|
@@ -9732,7 +10735,7 @@ assetRoutes.post("/", async (c) => {
|
|
|
9732
10735
|
`${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
|
|
9733
10736
|
);
|
|
9734
10737
|
}
|
|
9735
|
-
const name = `${
|
|
10738
|
+
const name = `${uuid15()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
9736
10739
|
const dir = join4(resolveNestPath(nestId), "assets");
|
|
9737
10740
|
await mkdir2(dir, { recursive: true });
|
|
9738
10741
|
await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
@@ -9773,7 +10776,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
9773
10776
|
});
|
|
9774
10777
|
|
|
9775
10778
|
// src/mcp/server-routes.ts
|
|
9776
|
-
import { Hono as
|
|
10779
|
+
import { Hono as Hono21 } from "hono";
|
|
9777
10780
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9778
10781
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
9779
10782
|
import { z as z2 } from "zod";
|
|
@@ -10072,7 +11075,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
10072
11075
|
}
|
|
10073
11076
|
return server;
|
|
10074
11077
|
}
|
|
10075
|
-
var serverMcpRoutes = new
|
|
11078
|
+
var serverMcpRoutes = new Hono21();
|
|
10076
11079
|
serverMcpRoutes.all("/", async (c) => {
|
|
10077
11080
|
const userId = c.get("userId");
|
|
10078
11081
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -10094,7 +11097,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
10094
11097
|
await server.server.close();
|
|
10095
11098
|
}
|
|
10096
11099
|
});
|
|
10097
|
-
var nestIndexRoutes = new
|
|
11100
|
+
var nestIndexRoutes = new Hono21();
|
|
10098
11101
|
nestIndexRoutes.get("/", async (c) => {
|
|
10099
11102
|
const userId = c.get("userId");
|
|
10100
11103
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -10213,14 +11216,14 @@ npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
|
|
|
10213
11216
|
through steward review before they become AI-visible.
|
|
10214
11217
|
`;
|
|
10215
11218
|
}
|
|
10216
|
-
var llmsTxtRoutes = new
|
|
11219
|
+
var llmsTxtRoutes = new Hono21();
|
|
10217
11220
|
llmsTxtRoutes.get("/", (c) => {
|
|
10218
11221
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
10219
11222
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
10220
11223
|
});
|
|
10221
11224
|
|
|
10222
11225
|
// src/governance/routes.ts
|
|
10223
|
-
import { Hono as
|
|
11226
|
+
import { Hono as Hono22 } from "hono";
|
|
10224
11227
|
|
|
10225
11228
|
// src/governance/feed-service.ts
|
|
10226
11229
|
var SNIPPET_CHARS = 200;
|
|
@@ -10351,7 +11354,7 @@ async function getNestFeed(params) {
|
|
|
10351
11354
|
}
|
|
10352
11355
|
|
|
10353
11356
|
// src/governance/comment-service.ts
|
|
10354
|
-
import { v4 as
|
|
11357
|
+
import { v4 as uuid16 } from "uuid";
|
|
10355
11358
|
async function createComment(params) {
|
|
10356
11359
|
const db = getDb();
|
|
10357
11360
|
const body = (params.body ?? "").trim();
|
|
@@ -10367,7 +11370,7 @@ async function createComment(params) {
|
|
|
10367
11370
|
throw new Error("Parent comment not found on this node");
|
|
10368
11371
|
}
|
|
10369
11372
|
}
|
|
10370
|
-
const id =
|
|
11373
|
+
const id = uuid16();
|
|
10371
11374
|
await db.run(
|
|
10372
11375
|
`INSERT INTO comments
|
|
10373
11376
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -10652,7 +11655,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
10652
11655
|
if (Number.isNaN(n) || n < 1) return def;
|
|
10653
11656
|
return Math.min(n, max);
|
|
10654
11657
|
}
|
|
10655
|
-
var governanceRoutes = new
|
|
11658
|
+
var governanceRoutes = new Hono22();
|
|
10656
11659
|
governanceRoutes.get("/stewards", async (c) => {
|
|
10657
11660
|
const nestId = c.req.param("nestId");
|
|
10658
11661
|
const scope = c.req.query("scope");
|
|
@@ -10771,6 +11774,60 @@ governanceRoutes.get("/review-queue", async (c) => {
|
|
|
10771
11774
|
}
|
|
10772
11775
|
return c.json({ ...result, requests });
|
|
10773
11776
|
});
|
|
11777
|
+
var BULK_REVIEW_LIMIT = 50;
|
|
11778
|
+
governanceRoutes.post("/review-queue/bulk", async (c) => {
|
|
11779
|
+
const nestId = c.req.param("nestId");
|
|
11780
|
+
const body = await c.req.json();
|
|
11781
|
+
const nodeIds = [...new Set(body.nodeIds ?? [])].map(assertSafeNodeId);
|
|
11782
|
+
if (nodeIds.length === 0) {
|
|
11783
|
+
throw new ValidationError("Select at least one document to review");
|
|
11784
|
+
}
|
|
11785
|
+
if (nodeIds.length > BULK_REVIEW_LIMIT) {
|
|
11786
|
+
throw new ValidationError(
|
|
11787
|
+
`Up to ${BULK_REVIEW_LIMIT} documents at a time \u2014 you selected ${nodeIds.length}.`
|
|
11788
|
+
);
|
|
11789
|
+
}
|
|
11790
|
+
if (body.action !== "approve" && body.action !== "reject") {
|
|
11791
|
+
throw new ValidationError("action must be approve or reject");
|
|
11792
|
+
}
|
|
11793
|
+
if (body.action === "reject" && !body.note) {
|
|
11794
|
+
throw new ValidationError("Rejection note is required");
|
|
11795
|
+
}
|
|
11796
|
+
const userEmail = await getUserEmail4(c);
|
|
11797
|
+
const baseUrl = requestBaseUrl(c.req.url);
|
|
11798
|
+
const results = [];
|
|
11799
|
+
for (const nodeId of nodeIds) {
|
|
11800
|
+
try {
|
|
11801
|
+
if (body.action === "approve") {
|
|
11802
|
+
await approve({
|
|
11803
|
+
nestId,
|
|
11804
|
+
nodeId,
|
|
11805
|
+
approvedBy: userEmail,
|
|
11806
|
+
note: body.note,
|
|
11807
|
+
override: false,
|
|
11808
|
+
baseUrl
|
|
11809
|
+
});
|
|
11810
|
+
} else {
|
|
11811
|
+
await reject({
|
|
11812
|
+
nestId,
|
|
11813
|
+
nodeId,
|
|
11814
|
+
rejectedBy: userEmail,
|
|
11815
|
+
note: body.note,
|
|
11816
|
+
baseUrl
|
|
11817
|
+
});
|
|
11818
|
+
}
|
|
11819
|
+
results.push({ nodeId, ok: true });
|
|
11820
|
+
} catch (err) {
|
|
11821
|
+
results.push({ nodeId, ok: false, error: err?.message || "Failed" });
|
|
11822
|
+
}
|
|
11823
|
+
}
|
|
11824
|
+
return c.json({
|
|
11825
|
+
action: body.action,
|
|
11826
|
+
succeeded: results.filter((r) => r.ok).length,
|
|
11827
|
+
failed: results.filter((r) => !r.ok).length,
|
|
11828
|
+
results
|
|
11829
|
+
});
|
|
11830
|
+
});
|
|
10774
11831
|
function denyUnlessNestManager(c, what) {
|
|
10775
11832
|
const permission = c.get("nestPermission");
|
|
10776
11833
|
if (permission === "owner" || permission === "admin") return null;
|
|
@@ -10963,7 +12020,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
10963
12020
|
const activity = await getActivity({ nestId, limit });
|
|
10964
12021
|
return c.json({ activity });
|
|
10965
12022
|
});
|
|
10966
|
-
var governanceNodeRoutes = new
|
|
12023
|
+
var governanceNodeRoutes = new Hono22();
|
|
10967
12024
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
10968
12025
|
const nestId = c.req.param("nestId");
|
|
10969
12026
|
const nodeId = c.req.param("nodeId");
|
|
@@ -11361,12 +12418,16 @@ governanceNodeRoutes.post("/:nodeId{.+}/cancel-review", async (c) => {
|
|
|
11361
12418
|
const nestId = c.req.param("nestId");
|
|
11362
12419
|
const nodeId = c.req.param("nodeId");
|
|
11363
12420
|
const userEmail = await getUserEmail4(c);
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
12421
|
+
try {
|
|
12422
|
+
const request = await cancelReview({
|
|
12423
|
+
nestId,
|
|
12424
|
+
nodeId,
|
|
12425
|
+
cancelledBy: userEmail
|
|
12426
|
+
});
|
|
12427
|
+
return c.json({ review: request });
|
|
12428
|
+
} catch (err) {
|
|
12429
|
+
return c.json({ error: err.message }, 403);
|
|
12430
|
+
}
|
|
11370
12431
|
});
|
|
11371
12432
|
async function getUserEmail4(c) {
|
|
11372
12433
|
const userId = c.get("userId");
|
|
@@ -11454,13 +12515,23 @@ var flexAuthWithPromptowlTicket = createMiddleware2(
|
|
|
11454
12515
|
async (c, next) => {
|
|
11455
12516
|
const header = c.req.header("Authorization") || "";
|
|
11456
12517
|
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
11457
|
-
const
|
|
12518
|
+
const secrets = [
|
|
12519
|
+
config.OFFICIAL_COMMUNITY_SSO_SECRET,
|
|
12520
|
+
config.MCP_SIGNING_SECRET
|
|
12521
|
+
].filter(Boolean);
|
|
11458
12522
|
const looksLikeJwt = !!token && !token.startsWith("cnst_") && token.split(".").length === 3;
|
|
11459
|
-
if (!looksLikeJwt ||
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
|
|
12523
|
+
if (!looksLikeJwt || secrets.length === 0) {
|
|
12524
|
+
return flexAuthMiddleware(c, next);
|
|
12525
|
+
}
|
|
12526
|
+
let claims = null;
|
|
12527
|
+
for (const secret of secrets) {
|
|
12528
|
+
try {
|
|
12529
|
+
claims = verifySsoTicket(token, secret);
|
|
12530
|
+
break;
|
|
12531
|
+
} catch {
|
|
12532
|
+
}
|
|
12533
|
+
}
|
|
12534
|
+
if (!claims) {
|
|
11464
12535
|
return c.json({ error: "Invalid service ticket" }, 401);
|
|
11465
12536
|
}
|
|
11466
12537
|
if (claims.scope !== "mcp" || claims.jti) {
|
|
@@ -11507,7 +12578,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
11507
12578
|
return ok;
|
|
11508
12579
|
}
|
|
11509
12580
|
function createApp() {
|
|
11510
|
-
const app = new
|
|
12581
|
+
const app = new Hono23({ router: new LinearRouter() });
|
|
11511
12582
|
const corsOrigins = config.CORS_ORIGINS;
|
|
11512
12583
|
app.use(
|
|
11513
12584
|
"*",
|
|
@@ -11564,6 +12635,9 @@ function createApp() {
|
|
|
11564
12635
|
// Presence only, never the value: lets the UI stop prompting for a
|
|
11565
12636
|
// runner key once the server-wide one exists.
|
|
11566
12637
|
runner_key_configured: !!process.env.ANTHROPIC_API_KEY,
|
|
12638
|
+
// Presence only: the publish dialog hides the email/invite gates when
|
|
12639
|
+
// this server cannot send mail, because their only way in is a mailed link.
|
|
12640
|
+
email_configured: isEmailConfigured(),
|
|
11567
12641
|
promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
|
|
11568
12642
|
// Manual (email/password) sign-in mode (open | invite-only | disabled).
|
|
11569
12643
|
// The login page reads this to show/hide the credential form, and to
|
|
@@ -11707,6 +12781,16 @@ function createApp() {
|
|
|
11707
12781
|
oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
|
|
11708
12782
|
oidc_auto_provision: config.OIDC_AUTO_PROVISION,
|
|
11709
12783
|
oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
|
|
12784
|
+
// Agent SSO / token-exchange. MCP_SIGNING_SECRET is WRITE-ONLY — only its
|
|
12785
|
+
// presence is reported, never the value (same discipline as the OIDC secret).
|
|
12786
|
+
sso_token_exchange_enabled: config.SSO_TOKEN_EXCHANGE_ENABLED,
|
|
12787
|
+
mcp_signing_secret_set: !!config.MCP_SIGNING_SECRET,
|
|
12788
|
+
mcp_token_ttl_seconds: config.MCP_TOKEN_TTL_SECONDS,
|
|
12789
|
+
ms_token_exchange_enabled: config.MS_TOKEN_EXCHANGE_ENABLED,
|
|
12790
|
+
ms_client_id: config.MS_CLIENT_ID,
|
|
12791
|
+
ms_tenant_id: config.MS_TENANT_ID,
|
|
12792
|
+
google_token_exchange_enabled: config.GOOGLE_TOKEN_EXCHANGE_ENABLED,
|
|
12793
|
+
google_client_id: config.GOOGLE_CLIENT_ID,
|
|
11710
12794
|
// Live readiness (enabled AND issuer+id+secret present) — the SAVED state,
|
|
11711
12795
|
// i.e. whether the login button is currently live. The Settings UI derives
|
|
11712
12796
|
// its own "enabled but not configured" warning from the unsaved draft
|
|
@@ -11907,6 +12991,67 @@ function createApp() {
|
|
|
11907
12991
|
value: body.oidc_department_tagging ? "true" : null
|
|
11908
12992
|
});
|
|
11909
12993
|
}
|
|
12994
|
+
if ("sso_token_exchange_enabled" in body) {
|
|
12995
|
+
pending.push({
|
|
12996
|
+
name: "SSO_TOKEN_EXCHANGE_ENABLED",
|
|
12997
|
+
value: body.sso_token_exchange_enabled ? "true" : null
|
|
12998
|
+
});
|
|
12999
|
+
}
|
|
13000
|
+
if ("mcp_signing_secret" in body) {
|
|
13001
|
+
const v = body.mcp_signing_secret;
|
|
13002
|
+
if (v === null || v === "") {
|
|
13003
|
+
pending.push({ name: "MCP_SIGNING_SECRET", value: null });
|
|
13004
|
+
} else if (typeof v !== "string") {
|
|
13005
|
+
addError("mcp_signing_secret", "MCP signing secret must be a string (or empty to clear it).");
|
|
13006
|
+
} else if (v.trim().length < 16) {
|
|
13007
|
+
addError("mcp_signing_secret", "MCP signing secret should be a long random string (16+ characters).");
|
|
13008
|
+
} else {
|
|
13009
|
+
pending.push({ name: "MCP_SIGNING_SECRET", value: v.trim() });
|
|
13010
|
+
}
|
|
13011
|
+
}
|
|
13012
|
+
if ("mcp_token_ttl_seconds" in body) {
|
|
13013
|
+
const n = Number(body.mcp_token_ttl_seconds);
|
|
13014
|
+
if (!Number.isFinite(n) || n < 30 || n > 3600) {
|
|
13015
|
+
addError("mcp_token_ttl_seconds", "Token lifetime must be between 30 and 3600 seconds.");
|
|
13016
|
+
} else {
|
|
13017
|
+
pending.push({ name: "MCP_TOKEN_TTL_SECONDS", value: String(Math.floor(n)) });
|
|
13018
|
+
}
|
|
13019
|
+
}
|
|
13020
|
+
if ("ms_token_exchange_enabled" in body) {
|
|
13021
|
+
pending.push({
|
|
13022
|
+
name: "MS_TOKEN_EXCHANGE_ENABLED",
|
|
13023
|
+
value: body.ms_token_exchange_enabled ? "true" : null
|
|
13024
|
+
});
|
|
13025
|
+
}
|
|
13026
|
+
if ("ms_client_id" in body) {
|
|
13027
|
+
pending.push({ name: "MS_CLIENT_ID", value: String(body.ms_client_id ?? "").trim() || null });
|
|
13028
|
+
}
|
|
13029
|
+
if ("ms_tenant_id" in body) {
|
|
13030
|
+
pending.push({ name: "MS_TENANT_ID", value: String(body.ms_tenant_id ?? "").trim() || null });
|
|
13031
|
+
}
|
|
13032
|
+
if ("google_token_exchange_enabled" in body) {
|
|
13033
|
+
pending.push({
|
|
13034
|
+
name: "GOOGLE_TOKEN_EXCHANGE_ENABLED",
|
|
13035
|
+
value: body.google_token_exchange_enabled ? "true" : null
|
|
13036
|
+
});
|
|
13037
|
+
}
|
|
13038
|
+
if ("google_client_id" in body) {
|
|
13039
|
+
pending.push({ name: "GOOGLE_CLIENT_ID", value: String(body.google_client_id ?? "").trim() || null });
|
|
13040
|
+
}
|
|
13041
|
+
const effAllowedDomains = "oidc_allowed_domains" in body ? String(body.oidc_allowed_domains ?? "").split(",").map((d) => d.trim()).filter(Boolean) : config.OIDC_ALLOWED_DOMAINS;
|
|
13042
|
+
const effAutoProvision = "oidc_auto_provision" in body ? !!body.oidc_auto_provision : config.OIDC_AUTO_PROVISION;
|
|
13043
|
+
if (effAutoProvision && effAllowedDomains.length === 0) {
|
|
13044
|
+
for (const field of [
|
|
13045
|
+
"ms_token_exchange_enabled",
|
|
13046
|
+
"google_token_exchange_enabled"
|
|
13047
|
+
]) {
|
|
13048
|
+
if (body[field])
|
|
13049
|
+
addError(
|
|
13050
|
+
field,
|
|
13051
|
+
"Set allowed email domains (or turn auto-provision off) before enabling token exchange \u2014 otherwise anyone with an account at that provider can sign themselves up and mint a token."
|
|
13052
|
+
);
|
|
13053
|
+
}
|
|
13054
|
+
}
|
|
11910
13055
|
const effOidcIssuer = "oidc_issuer" in body ? String(body.oidc_issuer ?? "").trim() : config.OIDC_ISSUER;
|
|
11911
13056
|
const effOidcClientId = "oidc_client_id" in body ? String(body.oidc_client_id ?? "").trim() : config.OIDC_CLIENT_ID;
|
|
11912
13057
|
const effOidcSecret = "oidc_client_secret" in body ? String(body.oidc_client_secret ?? "").trim() : config.OIDC_CLIENT_SECRET;
|
|
@@ -12336,11 +13481,12 @@ function createApp() {
|
|
|
12336
13481
|
app.route("/runs", runDetailRoutes);
|
|
12337
13482
|
app.route("/hooks", hookFireRoutes);
|
|
12338
13483
|
app.route("/llms.txt", llmsTxtRoutes);
|
|
13484
|
+
app.route("/p", publishRenderRoutes);
|
|
12339
13485
|
app.use("/index", flexAuthWithPromptowlTicket);
|
|
12340
13486
|
app.route("/index", nestIndexRoutes);
|
|
12341
13487
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
12342
13488
|
app.route("/mcp", serverMcpRoutes);
|
|
12343
|
-
const nestsApp = new
|
|
13489
|
+
const nestsApp = new Hono23();
|
|
12344
13490
|
nestsApp.use("*", async (c, next) => {
|
|
12345
13491
|
const parts = nestPathParts(c.req.path);
|
|
12346
13492
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -12431,7 +13577,7 @@ function createApp() {
|
|
|
12431
13577
|
}
|
|
12432
13578
|
let required = "read";
|
|
12433
13579
|
const path = c.req.path;
|
|
12434
|
-
const isStewardActionPath = path.includes("/approve") || path.includes("/reject") || path.includes("/submit-review") || path.includes("/cancel-review");
|
|
13580
|
+
const isStewardActionPath = path.includes("/approve") || path.includes("/reject") || path.includes("/submit-review") || path.includes("/cancel-review") || /\/review-queue\/bulk$/.test(path);
|
|
12435
13581
|
const isAnnotationAction = /\/annotations$/.test(path) || /\/annotations\/[^/]+\/(comments|resolve|reopen)$/.test(path);
|
|
12436
13582
|
const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
|
|
12437
13583
|
const isDeletionRequestAction = /\/request-deletion$/.test(path) || /\/deletion-requests\/[^/]+\/withdraw$/.test(path);
|
|
@@ -12445,7 +13591,7 @@ function createApp() {
|
|
|
12445
13591
|
);
|
|
12446
13592
|
}
|
|
12447
13593
|
const resource = parts[1];
|
|
12448
|
-
if (resource === "visibility") {
|
|
13594
|
+
if (resource === "visibility" || resource === "reader-mode") {
|
|
12449
13595
|
required = "admin";
|
|
12450
13596
|
} else if (resource === "collaborators") {
|
|
12451
13597
|
required = c.req.method === "GET" || c.req.method === "POST" ? "write" : "admin";
|
|
@@ -12533,6 +13679,7 @@ function createApp() {
|
|
|
12533
13679
|
nestsApp.route("/:nestId", queryRoutes);
|
|
12534
13680
|
nestsApp.route("/:nestId", sharingRoutes);
|
|
12535
13681
|
nestsApp.route("/:nestId/grants", grantRoutes);
|
|
13682
|
+
nestsApp.route("/:nestId/publications", publishRoutes);
|
|
12536
13683
|
nestsApp.route("/:nestId/teams", nestTeamRoutes);
|
|
12537
13684
|
nestsApp.route("/:nestId/edge-types", edgeTypeRoutes);
|
|
12538
13685
|
nestsApp.route("/:nestId/edges", edgeRoutes);
|
|
@@ -12853,7 +14000,7 @@ async function repairGraftedVersionChains(db) {
|
|
|
12853
14000
|
}
|
|
12854
14001
|
|
|
12855
14002
|
// src/workflow/scheduler.ts
|
|
12856
|
-
import { v4 as
|
|
14003
|
+
import { v4 as uuid17 } from "uuid";
|
|
12857
14004
|
function isDue(s, now) {
|
|
12858
14005
|
if (!s.last_run_at) return true;
|
|
12859
14006
|
const last = Date.parse(s.last_run_at);
|
|
@@ -12877,21 +14024,21 @@ async function schedulerTick(now = Date.now()) {
|
|
|
12877
14024
|
);
|
|
12878
14025
|
continue;
|
|
12879
14026
|
}
|
|
12880
|
-
const id = `run_${
|
|
12881
|
-
const
|
|
14027
|
+
const id = `run_${uuid17()}`;
|
|
14028
|
+
const nowIso2 = new Date(now).toISOString();
|
|
12882
14029
|
await db.run(
|
|
12883
14030
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
12884
14031
|
VALUES (?, ?, ?, ?, 'running', NULL, ?, NULL, 0)`,
|
|
12885
14032
|
// triggered_by names the schedule's creator with a schedule: prefix —
|
|
12886
14033
|
// honest provenance ("this fired on X's schedule, not X clicking").
|
|
12887
|
-
[id, s.nest_id, s.agent_node, `schedule:${s.created_by}`,
|
|
14034
|
+
[id, s.nest_id, s.agent_node, `schedule:${s.created_by}`, nowIso2]
|
|
12888
14035
|
);
|
|
12889
14036
|
const fired = s.runs_created + 1;
|
|
12890
14037
|
const done = s.max_runs !== null && fired >= s.max_runs;
|
|
12891
14038
|
await db.run(
|
|
12892
14039
|
`UPDATE schedules SET last_run_at = ?, runs_created = ?, enabled = ?, updated_at = ?
|
|
12893
14040
|
WHERE id = ?`,
|
|
12894
|
-
[
|
|
14041
|
+
[nowIso2, fired, done ? 0 : s.enabled, nowIso2, s.id]
|
|
12895
14042
|
);
|
|
12896
14043
|
created.push(id);
|
|
12897
14044
|
} catch (err) {
|