@promptowl/contextnest-community 1.18.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-QUZXRLUA.js → chunk-3LUUJUWS.js} +2 -2
- package/dist/{chunk-FUUJG6XP.js → chunk-7CPP6FIU.js} +256 -40
- package/dist/{chunk-OOBZG6BW.js → chunk-EY7WOKJR.js} +3 -3
- package/dist/{chunk-HGN45I5P.js → chunk-GVUBZTI3.js} +27 -185
- package/dist/{chunk-34UXRZXD.js → chunk-KHMSDTJJ.js} +2 -2
- package/dist/{chunk-TQACHVVA.js → chunk-O5DYL7ZG.js} +3 -3
- package/dist/{chunk-HYSTFMFG.js → chunk-XCFKS65Y.js} +107 -1
- package/dist/{chunk-GUNJTORH.js → chunk-YVMSM7LS.js} +14 -1
- package/dist/{client-SW5I6ZBK.js → client-7Z2IVCPW.js} +1 -1
- package/dist/{engine-34E3YNX7.js → engine-VZTSDUPB.js} +3 -3
- package/dist/{external-edit-service-6Q6SHEOF.js → external-edit-service-3FVK3OD7.js} +4 -4
- package/dist/{grants-service-ME7HIVHU.js → grants-service-WC27TRY3.js} +3 -3
- package/dist/index.js +2294 -1106
- package/dist/{migrations.postgres-5VI4RDTS.js → migrations.postgres-L42OXR7K.js} +47 -1
- package/dist/{review-service-H2M5ACPQ.js → review-service-TJRHRMMZ.js} +7 -7
- package/dist/{stewardship-service-U4CKL7Y5.js → stewardship-service-HRVD7SIS.js} +4 -4
- package/dist/{version-service-JWEN5UUW.js → version-service-5PIFT2OY.js} +4 -4
- 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 +7 -2
- package/dist/web3/assets/index-DONv5_Ni.css +0 -1
- package/dist/web3/assets/index-jSGrnboh.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,
|
|
@@ -174,11 +175,13 @@ import {
|
|
|
174
175
|
resolveNestAccess,
|
|
175
176
|
resolveNestPath,
|
|
176
177
|
resolveNestPermission,
|
|
178
|
+
resolveNodeUrl,
|
|
177
179
|
resolveTeamRolesForUser,
|
|
178
180
|
sendEmailToRecipient,
|
|
179
181
|
setAllowSelfApprove,
|
|
180
182
|
setPrimeOnlyReview,
|
|
181
183
|
setPrimeTags,
|
|
184
|
+
setReaderMode,
|
|
182
185
|
setStewardshipEnabled,
|
|
183
186
|
shareTeamWithNest,
|
|
184
187
|
startLicenseSafetyPoll,
|
|
@@ -190,7 +193,7 @@ import {
|
|
|
190
193
|
updateMemberRole,
|
|
191
194
|
validateLicense,
|
|
192
195
|
verifySmtp
|
|
193
|
-
} from "./chunk-
|
|
196
|
+
} from "./chunk-7CPP6FIU.js";
|
|
194
197
|
import {
|
|
195
198
|
AppError,
|
|
196
199
|
ConflictError,
|
|
@@ -198,15 +201,17 @@ import {
|
|
|
198
201
|
LockedError,
|
|
199
202
|
NotFoundError,
|
|
200
203
|
UnauthorizedError,
|
|
201
|
-
ValidationError
|
|
202
|
-
|
|
204
|
+
ValidationError,
|
|
205
|
+
VaultBusyError,
|
|
206
|
+
isVaultLockTimeout
|
|
207
|
+
} from "./chunk-YVMSM7LS.js";
|
|
203
208
|
import {
|
|
204
209
|
config,
|
|
205
210
|
getDb,
|
|
206
211
|
initDb,
|
|
207
212
|
isEmailListish,
|
|
208
213
|
isEmailish
|
|
209
|
-
} from "./chunk-
|
|
214
|
+
} from "./chunk-XCFKS65Y.js";
|
|
210
215
|
import {
|
|
211
216
|
ANON_EMAIL,
|
|
212
217
|
ANON_USER_ID,
|
|
@@ -220,7 +225,7 @@ import {
|
|
|
220
225
|
import { serve } from "@hono/node-server";
|
|
221
226
|
|
|
222
227
|
// src/app.ts
|
|
223
|
-
import { Hono as
|
|
228
|
+
import { Hono as Hono23 } from "hono";
|
|
224
229
|
import { LinearRouter } from "hono/router/linear-router";
|
|
225
230
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
226
231
|
import { cors } from "hono/cors";
|
|
@@ -250,7 +255,7 @@ function applyArtifactSandboxHeaders(c) {
|
|
|
250
255
|
|
|
251
256
|
// src/auth/routes.ts
|
|
252
257
|
import { Hono as Hono2 } from "hono";
|
|
253
|
-
import { v4 as
|
|
258
|
+
import { v4 as uuid3 } from "uuid";
|
|
254
259
|
|
|
255
260
|
// src/auth/middleware.ts
|
|
256
261
|
import { createMiddleware } from "hono/factory";
|
|
@@ -472,8 +477,39 @@ function b64urlToBuffer(input) {
|
|
|
472
477
|
const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - input.length % 4);
|
|
473
478
|
return Buffer.from(input.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
|
|
474
479
|
}
|
|
480
|
+
function bufferToB64url(buf) {
|
|
481
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
482
|
+
}
|
|
475
483
|
var SsoTokenError = class extends Error {
|
|
476
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
|
+
}
|
|
477
513
|
function verifySsoTicket(token, secret, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
478
514
|
if (!secret) throw new SsoTokenError("SSO secret not configured");
|
|
479
515
|
if (!token || typeof token !== "string") {
|
|
@@ -553,14 +589,15 @@ var NONCE_COOKIE = "cnst_oidc_nonce";
|
|
|
553
589
|
var VERIFIER_COOKIE = "cnst_oidc_verifier";
|
|
554
590
|
var FLOW_COOKIE_TTL_SECS = 10 * 60;
|
|
555
591
|
var DISCOVERY_TTL_MS = 5 * 6e4;
|
|
556
|
-
var discoveryCache =
|
|
592
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
557
593
|
function isOidcReady() {
|
|
558
594
|
return config.OIDC_ENABLED && !!config.OIDC_ISSUER && !!config.OIDC_CLIENT_ID && !!config.OIDC_CLIENT_SECRET;
|
|
559
595
|
}
|
|
560
596
|
async function discover(issuer) {
|
|
561
597
|
const now = Date.now();
|
|
562
|
-
|
|
563
|
-
|
|
598
|
+
const cached = discoveryCache.get(issuer);
|
|
599
|
+
if (cached && now - cached.fetchedAt < DISCOVERY_TTL_MS) {
|
|
600
|
+
return { doc: cached.doc, jwks: cached.jwks };
|
|
564
601
|
}
|
|
565
602
|
const res = await fetch(`${issuer}/.well-known/openid-configuration`, {
|
|
566
603
|
headers: { Accept: "application/json" },
|
|
@@ -579,12 +616,7 @@ async function discover(issuer) {
|
|
|
579
616
|
);
|
|
580
617
|
}
|
|
581
618
|
const jwks = createRemoteJWKSet(new URL(doc.jwks_uri));
|
|
582
|
-
discoveryCache
|
|
583
|
-
issuer,
|
|
584
|
-
doc,
|
|
585
|
-
jwks,
|
|
586
|
-
fetchedAt: now
|
|
587
|
-
};
|
|
619
|
+
discoveryCache.set(issuer, { doc, jwks, fetchedAt: now });
|
|
588
620
|
return { doc, jwks };
|
|
589
621
|
}
|
|
590
622
|
function base64url(buf) {
|
|
@@ -849,6 +881,184 @@ oidcRoutes.get("/logout", async (c) => {
|
|
|
849
881
|
return c.redirect(home, 302);
|
|
850
882
|
});
|
|
851
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
|
+
|
|
852
1062
|
// src/auth/routes.ts
|
|
853
1063
|
function isServerAdminEmail(email) {
|
|
854
1064
|
return isLicenseAdminEmail(email) || isSuperAdmin(email);
|
|
@@ -936,8 +1146,8 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
|
|
|
936
1146
|
[meEmail]
|
|
937
1147
|
);
|
|
938
1148
|
if (!user) {
|
|
939
|
-
const userId =
|
|
940
|
-
const placeholderHash = await hashPassword(
|
|
1149
|
+
const userId = uuid3();
|
|
1150
|
+
const placeholderHash = await hashPassword(uuid3());
|
|
941
1151
|
await db.run(
|
|
942
1152
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
943
1153
|
[userId, meEmail, rawName || null, placeholderHash]
|
|
@@ -971,6 +1181,7 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
|
|
|
971
1181
|
}
|
|
972
1182
|
var authRoutes = new Hono2();
|
|
973
1183
|
authRoutes.route("/oidc", oidcRoutes);
|
|
1184
|
+
authRoutes.post("/token-exchange", handleTokenExchange);
|
|
974
1185
|
authRoutes.post("/register", async (c) => {
|
|
975
1186
|
if (config.MANUAL_SIGN_IN !== "open") {
|
|
976
1187
|
return c.json(
|
|
@@ -1009,7 +1220,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1009
1220
|
}
|
|
1010
1221
|
throw new ValidationError("Email already registered");
|
|
1011
1222
|
}
|
|
1012
|
-
const userId =
|
|
1223
|
+
const userId = uuid3();
|
|
1013
1224
|
const passwordHash = await hashPassword(body.password);
|
|
1014
1225
|
await db.run(
|
|
1015
1226
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
@@ -1111,7 +1322,7 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1111
1322
|
});
|
|
1112
1323
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1113
1324
|
const apiKey = generateApiKey();
|
|
1114
|
-
const keyId =
|
|
1325
|
+
const keyId = uuid3();
|
|
1115
1326
|
await db.run(
|
|
1116
1327
|
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
|
|
1117
1328
|
[
|
|
@@ -1140,7 +1351,7 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1140
1351
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1141
1352
|
const prior = check.key;
|
|
1142
1353
|
const apiKey = generateApiKey();
|
|
1143
|
-
const keyId =
|
|
1354
|
+
const keyId = uuid3();
|
|
1144
1355
|
const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
|
|
1145
1356
|
const replaced = await db.transaction(async (tx) => {
|
|
1146
1357
|
const removed = await tx.run(
|
|
@@ -1448,7 +1659,7 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
|
1448
1659
|
if (supplied !== void 0) {
|
|
1449
1660
|
assertValidPassword(supplied);
|
|
1450
1661
|
}
|
|
1451
|
-
const generated = supplied ? null :
|
|
1662
|
+
const generated = supplied ? null : uuid3().replace(/-/g, "").slice(0, 16);
|
|
1452
1663
|
const newPassword = supplied ?? generated;
|
|
1453
1664
|
const newHash = await hashPassword(newPassword);
|
|
1454
1665
|
await db.transaction(async (tx) => {
|
|
@@ -1559,7 +1770,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1559
1770
|
409
|
|
1560
1771
|
);
|
|
1561
1772
|
}
|
|
1562
|
-
const tempPassword =
|
|
1773
|
+
const tempPassword = uuid3().replace(/-/g, "").slice(0, 16);
|
|
1563
1774
|
const passwordHash = await hashPassword(tempPassword);
|
|
1564
1775
|
let userId;
|
|
1565
1776
|
if (existing) {
|
|
@@ -1569,7 +1780,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1569
1780
|
userId
|
|
1570
1781
|
]);
|
|
1571
1782
|
} else {
|
|
1572
|
-
userId =
|
|
1783
|
+
userId = uuid3();
|
|
1573
1784
|
await db.run(
|
|
1574
1785
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1575
1786
|
[userId, email, null, passwordHash]
|
|
@@ -1657,16 +1868,15 @@ import {
|
|
|
1657
1868
|
normalizeStatus,
|
|
1658
1869
|
isRejected,
|
|
1659
1870
|
filterDocuments,
|
|
1660
|
-
DocumentNotFoundError
|
|
1871
|
+
DocumentNotFoundError,
|
|
1872
|
+
NODE_TYPES,
|
|
1873
|
+
generateIndexMd
|
|
1661
1874
|
} from "@promptowl/contextnest-engine";
|
|
1662
|
-
import { rename, mkdir } from "fs/promises";
|
|
1875
|
+
import { rename, mkdir, rm, stat } from "fs/promises";
|
|
1663
1876
|
import { dirname, join } from "path";
|
|
1664
1877
|
|
|
1665
1878
|
// src/governance/safe-publish.ts
|
|
1666
|
-
import {
|
|
1667
|
-
publishDocument,
|
|
1668
|
-
serializeDocument
|
|
1669
|
-
} from "@promptowl/contextnest-engine";
|
|
1879
|
+
import { serializeDocument } from "@promptowl/contextnest-engine";
|
|
1670
1880
|
function serializeNodeSafe(node) {
|
|
1671
1881
|
return serializeDocument({
|
|
1672
1882
|
...node,
|
|
@@ -1804,6 +2014,10 @@ function toSafeError(err, context, message) {
|
|
|
1804
2014
|
"This document is rejected. Change its status before saving."
|
|
1805
2015
|
);
|
|
1806
2016
|
}
|
|
2017
|
+
if (isVaultLockTimeout(err)) {
|
|
2018
|
+
console.error(`${context}:`, err);
|
|
2019
|
+
return new VaultBusyError();
|
|
2020
|
+
}
|
|
1807
2021
|
console.error(`${context}:`, err);
|
|
1808
2022
|
return new AppError(500, message);
|
|
1809
2023
|
}
|
|
@@ -1818,6 +2032,12 @@ var RUNNABLE_NODE_TYPES = ["agent", "skill"];
|
|
|
1818
2032
|
function isRunnableType(type) {
|
|
1819
2033
|
return RUNNABLE_NODE_TYPES.includes(type || "");
|
|
1820
2034
|
}
|
|
2035
|
+
var ACCEPTED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
2036
|
+
...NODE_TYPES,
|
|
2037
|
+
...RUNNABLE_NODE_TYPES,
|
|
2038
|
+
"artifact",
|
|
2039
|
+
"table"
|
|
2040
|
+
]);
|
|
1821
2041
|
function toNodeResponse(node) {
|
|
1822
2042
|
const fm = node.frontmatter;
|
|
1823
2043
|
const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
|
|
@@ -1903,6 +2123,9 @@ function engineScopesFolders() {
|
|
|
1903
2123
|
}
|
|
1904
2124
|
return folderScopeSupported;
|
|
1905
2125
|
}
|
|
2126
|
+
function isDerivedAnnotationsNode(id) {
|
|
2127
|
+
return id.endsWith("--annotations");
|
|
2128
|
+
}
|
|
1906
2129
|
async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
|
|
1907
2130
|
if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
|
|
1908
2131
|
throw new ForbiddenError(
|
|
@@ -1947,12 +2170,12 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
1947
2170
|
if (filters.folder !== void 0) assertSafeFolder(filters.folder);
|
|
1948
2171
|
const { versions: versionManager } = await engineCache.get(nestId);
|
|
1949
2172
|
const userEmail = await resolveCallerEmail(userId);
|
|
1950
|
-
const
|
|
2173
|
+
const listFilters = {
|
|
1951
2174
|
...filters.type ? { type: filters.type } : {},
|
|
1952
2175
|
...filters.tag ? { tag: filters.tag } : {},
|
|
1953
|
-
...filters.status ? { status: filters.status } : {}
|
|
1954
|
-
includeRetired: true
|
|
2176
|
+
...filters.status ? { status: filters.status } : {}
|
|
1955
2177
|
};
|
|
2178
|
+
const frontmatterFilters = { ...listFilters, includeRetired: true };
|
|
1956
2179
|
let documents;
|
|
1957
2180
|
if (filters.withContent) {
|
|
1958
2181
|
const scopeInEngine = filters.folder !== void 0 && engineScopesFolders();
|
|
@@ -1960,7 +2183,7 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
1960
2183
|
({ documents } = await engineApi.run(
|
|
1961
2184
|
"context_list",
|
|
1962
2185
|
{
|
|
1963
|
-
...
|
|
2186
|
+
...listFilters,
|
|
1964
2187
|
include_retired: true,
|
|
1965
2188
|
...scopeInEngine ? {
|
|
1966
2189
|
folder: [scopePrefix, filters.folder].filter(Boolean).join("/"),
|
|
@@ -1976,7 +2199,13 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
1976
2199
|
nestId,
|
|
1977
2200
|
filters.folder !== void 0 ? { folder: filters.folder } : {}
|
|
1978
2201
|
);
|
|
1979
|
-
documents = filterDocuments(indexed,
|
|
2202
|
+
documents = filterDocuments(indexed, {
|
|
2203
|
+
...frontmatterFilters,
|
|
2204
|
+
includeRetired: true
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
if (!filters.includeDerived) {
|
|
2208
|
+
documents = documents.filter((d) => !isDerivedAnnotationsNode(d.id));
|
|
1980
2209
|
}
|
|
1981
2210
|
const scoped = filters.folder === void 0 ? documents : documents.filter((d) => folderOfId(d.id) === filters.folder);
|
|
1982
2211
|
const nodes = scoped.map((d) => ({
|
|
@@ -2080,10 +2309,37 @@ function withSubtreeTotals(direct) {
|
|
|
2080
2309
|
)
|
|
2081
2310
|
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
2082
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
|
+
}
|
|
2083
2338
|
async function listNestFolders(nestId, opts = {}) {
|
|
2084
2339
|
if (opts.folder !== void 0) assertSafeFolder(opts.folder);
|
|
2085
2340
|
await ensureNodeIndex(nestId);
|
|
2086
2341
|
const counts = /* @__PURE__ */ new Map();
|
|
2342
|
+
for (const path of await vaultFolderPaths(nestId, opts)) counts.set(path, 0);
|
|
2087
2343
|
for (const { path, count } of await indexedFolderCounts(nestId)) {
|
|
2088
2344
|
counts.set(path, count);
|
|
2089
2345
|
let parent = path;
|
|
@@ -2108,7 +2364,9 @@ var RECENT_LIMIT = 3;
|
|
|
2108
2364
|
async function nestCensus(nestId, userId) {
|
|
2109
2365
|
const userEmail = await resolveCallerEmail(userId);
|
|
2110
2366
|
await ensureNodeIndex(nestId);
|
|
2111
|
-
const documents = await readIndexedNodes(nestId)
|
|
2367
|
+
const documents = (await readIndexedNodes(nestId)).filter(
|
|
2368
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
2369
|
+
);
|
|
2112
2370
|
const visible = await filterAccessible(nestId, userId, userEmail, documents);
|
|
2113
2371
|
const tags = {};
|
|
2114
2372
|
const types = {};
|
|
@@ -2129,6 +2387,7 @@ async function nestCensus(nestId, userId) {
|
|
|
2129
2387
|
const segs = folder.split("/");
|
|
2130
2388
|
for (let i = 1; i <= segs.length; i++) paths.add(segs.slice(0, i).join("/"));
|
|
2131
2389
|
}
|
|
2390
|
+
for (const path of await vaultFolderPaths(nestId)) paths.add(path);
|
|
2132
2391
|
const folders = withSubtreeTotals(
|
|
2133
2392
|
[...paths].map((path) => ({ path, count: direct.get(path) ?? 0 }))
|
|
2134
2393
|
);
|
|
@@ -2172,7 +2431,10 @@ async function nestCensus(nestId, userId) {
|
|
|
2172
2431
|
};
|
|
2173
2432
|
}
|
|
2174
2433
|
async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
|
|
2175
|
-
return listNodesForCaller(nestId, await userIdFromEmail(userEmail),
|
|
2434
|
+
return listNodesForCaller(nestId, await userIdFromEmail(userEmail), {
|
|
2435
|
+
includeDerived: true,
|
|
2436
|
+
...filters
|
|
2437
|
+
});
|
|
2176
2438
|
}
|
|
2177
2439
|
async function actorAutoPublishes(nestId, userEmail) {
|
|
2178
2440
|
if (!await nestAllowsSelfApprove(nestId)) return false;
|
|
@@ -2226,6 +2488,11 @@ async function createNodeOnce(nestId, input, userEmail) {
|
|
|
2226
2488
|
if (requestedType === "table" && !config.TYPE_TABLE_ENABLED) {
|
|
2227
2489
|
throw new ValidationError("Table nodes are disabled on this server");
|
|
2228
2490
|
}
|
|
2491
|
+
if (!ACCEPTED_NODE_TYPES.has(requestedType)) {
|
|
2492
|
+
throw new ValidationError(
|
|
2493
|
+
`unknown node type "${requestedType}"; allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")}`
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2229
2496
|
if (input.schedule !== void 0 && !isRunnableType(input.type)) {
|
|
2230
2497
|
throw new ValidationError(
|
|
2231
2498
|
`schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(
|
|
@@ -2632,6 +2899,10 @@ var NODE_KEYED_TABLES = [
|
|
|
2632
2899
|
// A live capability token bound to one agent node — deleting the agent must
|
|
2633
2900
|
// revoke it, or a later node at that id answers the old webhook.
|
|
2634
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"],
|
|
2635
2906
|
["edges", "from_node"],
|
|
2636
2907
|
["edges", "to_node"]
|
|
2637
2908
|
];
|
|
@@ -2795,6 +3066,7 @@ async function deleteFolder(nestId, folder, userEmail) {
|
|
|
2795
3066
|
const docs = await storage.discoverDocuments();
|
|
2796
3067
|
const targets = docs.map((d) => d.id).filter((id) => isIdUnderFolder(id, rel));
|
|
2797
3068
|
if (targets.length === 0) {
|
|
3069
|
+
if (await removeVaultFolder(nestId, rel)) return [];
|
|
2798
3070
|
throw new NotFoundError(`No documents found under folder: ${folder}`);
|
|
2799
3071
|
}
|
|
2800
3072
|
const deleted = [];
|
|
@@ -2809,13 +3081,35 @@ async function deleteFolder(nestId, folder, userEmail) {
|
|
|
2809
3081
|
);
|
|
2810
3082
|
}
|
|
2811
3083
|
}
|
|
3084
|
+
await removeVaultFolder(nestId, rel);
|
|
2812
3085
|
return deleted;
|
|
2813
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
|
+
}
|
|
2814
3108
|
|
|
2815
3109
|
// src/nests/unsynced-service.ts
|
|
2816
3110
|
import { readdirSync, readFileSync, statSync } from "fs";
|
|
2817
3111
|
import { join as join2, relative, resolve } from "path";
|
|
2818
|
-
import { rm } from "fs/promises";
|
|
3112
|
+
import { rm as rm2 } from "fs/promises";
|
|
2819
3113
|
var RESERVED = /* @__PURE__ */ new Set(["nests"]);
|
|
2820
3114
|
function isNestStorageRoot(absPath) {
|
|
2821
3115
|
return resolve(absPath) === resolve(nestStorageRoot());
|
|
@@ -2958,14 +3252,14 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
2958
3252
|
if (isNestStorageRoot(src)) {
|
|
2959
3253
|
throw new ValidationError("Folder is not eligible for sync");
|
|
2960
3254
|
}
|
|
2961
|
-
let
|
|
3255
|
+
let stat2;
|
|
2962
3256
|
try {
|
|
2963
|
-
|
|
3257
|
+
stat2 = statSync(src);
|
|
2964
3258
|
} catch {
|
|
2965
3259
|
console.warn(`[unsynced] source missing: ${src}`);
|
|
2966
3260
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
2967
3261
|
}
|
|
2968
|
-
if (!
|
|
3262
|
+
if (!stat2.isDirectory()) {
|
|
2969
3263
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
2970
3264
|
}
|
|
2971
3265
|
const files = collectMarkdownFiles(src, src);
|
|
@@ -3000,7 +3294,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3000
3294
|
`[unsynced] registered ${documents} document(s) for nest ${nest.id}`
|
|
3001
3295
|
);
|
|
3002
3296
|
try {
|
|
3003
|
-
await
|
|
3297
|
+
await rm2(src, { recursive: true, force: true });
|
|
3004
3298
|
console.log(`[unsynced] removed source folder ${src}`);
|
|
3005
3299
|
} catch (err) {
|
|
3006
3300
|
console.error("[unsynced] failed to remove source folder", src, err);
|
|
@@ -3014,16 +3308,16 @@ async function deleteUnsyncedFolder(folderName) {
|
|
|
3014
3308
|
if (isNestStorageRoot(src)) {
|
|
3015
3309
|
throw new ValidationError("Folder is not eligible for delete");
|
|
3016
3310
|
}
|
|
3017
|
-
let
|
|
3311
|
+
let stat2;
|
|
3018
3312
|
try {
|
|
3019
|
-
|
|
3313
|
+
stat2 = statSync(src);
|
|
3020
3314
|
} catch {
|
|
3021
3315
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
3022
3316
|
}
|
|
3023
|
-
if (!
|
|
3317
|
+
if (!stat2.isDirectory()) {
|
|
3024
3318
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
3025
3319
|
}
|
|
3026
|
-
await
|
|
3320
|
+
await rm2(src, { recursive: true, force: true });
|
|
3027
3321
|
console.log(`[unsynced] deleted folder ${src}`);
|
|
3028
3322
|
}
|
|
3029
3323
|
|
|
@@ -3402,7 +3696,7 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
|
3402
3696
|
import { Hono as Hono4 } from "hono";
|
|
3403
3697
|
|
|
3404
3698
|
// src/nests/sharing-service.ts
|
|
3405
|
-
import { v4 as
|
|
3699
|
+
import { v4 as uuid4 } from "uuid";
|
|
3406
3700
|
var VALID_PERMISSIONS = ["read", "write", "admin"];
|
|
3407
3701
|
async function addCollaborator(params) {
|
|
3408
3702
|
const { nestId } = params;
|
|
@@ -3429,10 +3723,10 @@ async function addCollaborator(params) {
|
|
|
3429
3723
|
userId = existing.id;
|
|
3430
3724
|
} else {
|
|
3431
3725
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3432
|
-
userId =
|
|
3726
|
+
userId = uuid4();
|
|
3433
3727
|
await db.run(
|
|
3434
3728
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
3435
|
-
[userId, email, null, await hashPassword2(
|
|
3729
|
+
[userId, email, null, await hashPassword2(uuid4())]
|
|
3436
3730
|
);
|
|
3437
3731
|
}
|
|
3438
3732
|
}
|
|
@@ -3470,7 +3764,7 @@ async function addCollaborator(params) {
|
|
|
3470
3764
|
[params.grantedByEmail]
|
|
3471
3765
|
))?.id : void 0;
|
|
3472
3766
|
const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
|
|
3473
|
-
const collabId =
|
|
3767
|
+
const collabId = uuid4();
|
|
3474
3768
|
await db.run(
|
|
3475
3769
|
"INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
|
|
3476
3770
|
[collabId, nestId, userId, params.permission, granterId]
|
|
@@ -3589,6 +3883,16 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
3589
3883
|
if (!body.visibility || !["private", "public"].includes(body.visibility)) {
|
|
3590
3884
|
throw new ValidationError("visibility must be private or public");
|
|
3591
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
|
+
}
|
|
3592
3896
|
const db = getDb();
|
|
3593
3897
|
const nestId = c.req.param("nestId");
|
|
3594
3898
|
const result = await db.run("UPDATE nests SET visibility = ? WHERE id = ?", [
|
|
@@ -3603,10 +3907,55 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
3603
3907
|
]);
|
|
3604
3908
|
return c.json({ visibility: row.visibility });
|
|
3605
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
|
+
});
|
|
3606
3955
|
|
|
3607
3956
|
// src/nests/grant-routes.ts
|
|
3608
3957
|
import { Hono as Hono5 } from "hono";
|
|
3609
|
-
import { v4 as
|
|
3958
|
+
import { v4 as uuid5 } from "uuid";
|
|
3610
3959
|
var grantRoutes = new Hono5();
|
|
3611
3960
|
async function requireManage(c, nestId) {
|
|
3612
3961
|
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
@@ -3621,10 +3970,10 @@ async function resolveGrantee(email) {
|
|
|
3621
3970
|
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
3622
3971
|
if (existing) return existing.id;
|
|
3623
3972
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3624
|
-
const id =
|
|
3973
|
+
const id = uuid5();
|
|
3625
3974
|
await db.run(
|
|
3626
3975
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
3627
|
-
[id, e, null, await hashPassword2(
|
|
3976
|
+
[id, e, null, await hashPassword2(uuid5())]
|
|
3628
3977
|
);
|
|
3629
3978
|
return id;
|
|
3630
3979
|
}
|
|
@@ -3722,1059 +4071,1718 @@ grantRoutes.delete("/:id", async (c) => {
|
|
|
3722
4071
|
return c.json({ deleted: true });
|
|
3723
4072
|
});
|
|
3724
4073
|
|
|
3725
|
-
// src/nests/
|
|
4074
|
+
// src/nests/publish-routes.ts
|
|
3726
4075
|
import { Hono as Hono6 } from "hono";
|
|
3727
|
-
var nestTeamRoutes = new Hono6();
|
|
3728
|
-
nestTeamRoutes.get("/", async (c) => {
|
|
3729
|
-
const nestId = c.req.param("nestId");
|
|
3730
|
-
const refs = await listNestTeams(nestId);
|
|
3731
|
-
const teams = [];
|
|
3732
|
-
for (const ref of refs) {
|
|
3733
|
-
const team = await getTeam(ref.teamId).catch(() => null);
|
|
3734
|
-
teams.push({
|
|
3735
|
-
teamId: ref.teamId,
|
|
3736
|
-
teamName: team?.name ?? ref.teamName,
|
|
3737
|
-
members: team?.members ?? []
|
|
3738
|
-
});
|
|
3739
|
-
}
|
|
3740
|
-
return c.json({ teams });
|
|
3741
|
-
});
|
|
3742
|
-
nestTeamRoutes.post("/", async (c) => {
|
|
3743
|
-
const body = await c.req.json();
|
|
3744
|
-
if (!body.team_id) {
|
|
3745
|
-
throw new ValidationError("team_id is required");
|
|
3746
|
-
}
|
|
3747
|
-
const userId = c.get("userId");
|
|
3748
|
-
const teams = await shareTeamWithNest({
|
|
3749
|
-
nestId: c.req.param("nestId"),
|
|
3750
|
-
teamId: body.team_id,
|
|
3751
|
-
callerUserId: userId,
|
|
3752
|
-
allowAdmin: await isLicenseAdminUserId(userId),
|
|
3753
|
-
baseUrl: requestBaseUrl(c.req.url)
|
|
3754
|
-
});
|
|
3755
|
-
return c.json({ teams }, 201);
|
|
3756
|
-
});
|
|
3757
|
-
nestTeamRoutes.delete("/:teamId", async (c) => {
|
|
3758
|
-
const teams = await unshareTeamFromNest({
|
|
3759
|
-
nestId: c.req.param("nestId"),
|
|
3760
|
-
teamId: c.req.param("teamId")
|
|
3761
|
-
});
|
|
3762
|
-
return c.json({ teams });
|
|
3763
|
-
});
|
|
3764
|
-
|
|
3765
|
-
// src/governance/team-routes.ts
|
|
3766
|
-
import { Hono as Hono7 } from "hono";
|
|
3767
4076
|
|
|
3768
|
-
// src/
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
case "owner":
|
|
3779
|
-
return "admin";
|
|
3780
|
-
case "editor":
|
|
3781
|
-
return "editor";
|
|
3782
|
-
default:
|
|
3783
|
-
return "viewer";
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
async function fetchOwnedPromptowlTeams(token) {
|
|
3787
|
-
if (!token?.trim()) throw new ValidationError("token is required");
|
|
3788
|
-
const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
|
|
3789
|
-
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) {
|
|
3790
4087
|
try {
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
Authorization: `Bearer ${token}`
|
|
3795
|
-
}
|
|
3796
|
-
});
|
|
4088
|
+
const { storage } = await engineCache.get(nestId);
|
|
4089
|
+
await storage.readDocument(nodeId);
|
|
4090
|
+
return true;
|
|
3797
4091
|
} catch {
|
|
3798
|
-
|
|
4092
|
+
return false;
|
|
3799
4093
|
}
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
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");
|
|
3804
4129
|
}
|
|
3805
|
-
if (!
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
);
|
|
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");
|
|
3809
4134
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
}
|
|
3813
|
-
async function syncPromptowlTeams(params) {
|
|
3814
|
-
if (!params.teamIds?.length) {
|
|
3815
|
-
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");
|
|
3816
4137
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
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]
|
|
3830
4174
|
);
|
|
3831
4175
|
}
|
|
3832
|
-
selected.push(team);
|
|
3833
4176
|
}
|
|
3834
|
-
const
|
|
3835
|
-
|
|
3836
|
-
|
|
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
|
|
3837
4188
|
]);
|
|
3838
|
-
if (!caller?.email) {
|
|
3839
|
-
throw new ValidationError("Could not resolve your account.");
|
|
3840
|
-
}
|
|
3841
|
-
const results = [];
|
|
3842
|
-
for (const poTeam of selected) {
|
|
3843
|
-
results.push(await syncOne(poTeam, params.callerUserId, caller.email));
|
|
3844
|
-
}
|
|
3845
|
-
return results;
|
|
3846
4189
|
}
|
|
3847
|
-
async function
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
externalId: poTeam.id
|
|
3852
|
-
});
|
|
3853
|
-
const team = existing ?? await createTeam({
|
|
3854
|
-
name: poTeam.name,
|
|
3855
|
-
ownerId: callerUserId,
|
|
3856
|
-
source: PROMPTOWL_SOURCE,
|
|
3857
|
-
externalId: poTeam.id
|
|
3858
|
-
});
|
|
3859
|
-
if (existing && existing.name !== poTeam.name) {
|
|
3860
|
-
await renameTeam({
|
|
3861
|
-
teamId: team.id,
|
|
3862
|
-
name: poTeam.name,
|
|
3863
|
-
callerUserId,
|
|
3864
|
-
allowAdmin: false
|
|
3865
|
-
});
|
|
3866
|
-
}
|
|
3867
|
-
const desired = await desiredRoster(poTeam, callerEmail);
|
|
3868
|
-
const before = new Map(team.members.map((m) => [m.email, m.role]));
|
|
3869
|
-
const after = new Map(desired.map((m) => [m.email, m.role]));
|
|
3870
|
-
const added = [...after.keys()].filter((e) => !before.has(e));
|
|
3871
|
-
const removed = [...before.keys()].filter((e) => !after.has(e));
|
|
3872
|
-
const updated = [...after.keys()].filter(
|
|
3873
|
-
(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]
|
|
3874
4194
|
);
|
|
3875
|
-
if (added.length || removed.length || updated.length) {
|
|
3876
|
-
await replaceTeamRoster({
|
|
3877
|
-
teamId: team.id,
|
|
3878
|
-
members: desired,
|
|
3879
|
-
callerUserId,
|
|
3880
|
-
allowAdmin: false
|
|
3881
|
-
});
|
|
3882
|
-
}
|
|
3883
|
-
return {
|
|
3884
|
-
team_id: team.id,
|
|
3885
|
-
external_id: poTeam.id,
|
|
3886
|
-
name: poTeam.name,
|
|
3887
|
-
created: !existing,
|
|
3888
|
-
added,
|
|
3889
|
-
removed,
|
|
3890
|
-
updated
|
|
3891
|
-
};
|
|
3892
4195
|
}
|
|
3893
|
-
async function
|
|
3894
|
-
const
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
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;
|
|
3908
4239
|
}
|
|
3909
|
-
|
|
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
|
+
);
|
|
3910
4285
|
}
|
|
3911
4286
|
|
|
3912
|
-
// src/
|
|
3913
|
-
var
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
if (!config.PROMPTOWL_TEAMS_ENABLED) {
|
|
3919
|
-
return c.json({ error: "Not found" }, 404);
|
|
3920
|
-
}
|
|
3921
|
-
if (!tryConsume(`po-teams:ip:${clientIp(c)}`, PROMPTOWL_LIMIT)) {
|
|
3922
|
-
return c.json({ error: "Too many PromptOwl requests, try again later" }, 429);
|
|
3923
|
-
}
|
|
3924
|
-
const token = getPromptowlTokenFromRequest(c);
|
|
3925
|
-
if (!token) {
|
|
3926
|
-
return c.json(
|
|
3927
|
-
{
|
|
3928
|
-
error: "Sign in with PromptOwl to import teams.",
|
|
3929
|
-
code: CONNECT_REQUIRED
|
|
3930
|
-
},
|
|
3931
|
-
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."
|
|
3932
4293
|
);
|
|
3933
4294
|
}
|
|
3934
|
-
try {
|
|
3935
|
-
return c.json(await run(token));
|
|
3936
|
-
} catch (err) {
|
|
3937
|
-
if (err instanceof UnauthorizedError) {
|
|
3938
|
-
c.header(
|
|
3939
|
-
"Set-Cookie",
|
|
3940
|
-
buildPromptowlTokenCookie("", { secure: isSecureRequest(c), clear: true }),
|
|
3941
|
-
{ append: true }
|
|
3942
|
-
);
|
|
3943
|
-
return c.json(
|
|
3944
|
-
{ error: err.message, code: CONNECT_REQUIRED },
|
|
3945
|
-
CONNECT_REQUIRED_STATUS
|
|
3946
|
-
);
|
|
3947
|
-
}
|
|
3948
|
-
throw err;
|
|
3949
|
-
}
|
|
3950
4295
|
}
|
|
3951
|
-
|
|
3952
|
-
"
|
|
3953
|
-
|
|
3954
|
-
teams: await fetchOwnedPromptowlTeams(token)
|
|
3955
|
-
}))
|
|
3956
|
-
);
|
|
3957
|
-
teamRoutes.post("/promptowl/sync", async (c) => {
|
|
3958
|
-
const body = await c.req.json().catch(() => ({}));
|
|
3959
|
-
return withPromptowlToken(c, async (token) => ({
|
|
3960
|
-
results: await syncPromptowlTeams({
|
|
3961
|
-
callerUserId: c.get("userId"),
|
|
3962
|
-
token,
|
|
3963
|
-
teamIds: body.team_ids ?? []
|
|
3964
|
-
})
|
|
3965
|
-
}));
|
|
3966
|
-
});
|
|
3967
|
-
teamRoutes.post("/", async (c) => {
|
|
4296
|
+
publishRoutes.post("/", async (c) => {
|
|
4297
|
+
const nestId = c.req.param("nestId");
|
|
4298
|
+
await requireManage2(c, nestId);
|
|
3968
4299
|
const body = await c.req.json();
|
|
3969
|
-
const
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
});
|
|
3975
|
-
teamRoutes.get("/", async (c) => {
|
|
3976
|
-
const teams = await listTeamsForUser(c.get("userId"));
|
|
3977
|
-
return c.json({ teams });
|
|
3978
|
-
});
|
|
3979
|
-
teamRoutes.get("/:teamId", async (c) => {
|
|
3980
|
-
const userId = c.get("userId");
|
|
3981
|
-
const team = await getTeam(c.req.param("teamId"));
|
|
3982
|
-
const isMember = team.members.some((m) => m.userId === userId);
|
|
3983
|
-
if (team.owner_id !== userId && !isMember && !await isLicenseAdminUserId(userId)) {
|
|
3984
|
-
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
|
+
);
|
|
3985
4305
|
}
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
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")
|
|
3996
4317
|
});
|
|
3997
|
-
|
|
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);
|
|
3998
4331
|
});
|
|
3999
|
-
|
|
4000
|
-
const
|
|
4001
|
-
await
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
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}` }))
|
|
4005
4340
|
});
|
|
4006
|
-
return c.json({ removed: true });
|
|
4007
4341
|
});
|
|
4008
|
-
|
|
4009
|
-
const
|
|
4010
|
-
|
|
4011
|
-
const
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
role: body.role ?? "",
|
|
4016
|
-
callerUserId: userId,
|
|
4017
|
-
allowAdmin: await isLicenseAdminUserId(userId)
|
|
4018
|
-
});
|
|
4019
|
-
return c.json({ team }, 201);
|
|
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 });
|
|
4020
4349
|
});
|
|
4021
|
-
|
|
4022
|
-
const
|
|
4023
|
-
|
|
4024
|
-
const
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
role: body.role ?? "",
|
|
4028
|
-
callerUserId: userId,
|
|
4029
|
-
allowAdmin: await isLicenseAdminUserId(userId)
|
|
4030
|
-
});
|
|
4031
|
-
return c.json({ team });
|
|
4032
|
-
});
|
|
4033
|
-
teamRoutes.delete("/:teamId/members/:userId", async (c) => {
|
|
4034
|
-
const userId = c.get("userId");
|
|
4035
|
-
const team = await removeMember({
|
|
4036
|
-
teamId: c.req.param("teamId"),
|
|
4037
|
-
userId: c.req.param("userId"),
|
|
4038
|
-
callerUserId: userId,
|
|
4039
|
-
allowAdmin: await isLicenseAdminUserId(userId)
|
|
4040
|
-
});
|
|
4041
|
-
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 });
|
|
4042
4356
|
});
|
|
4043
4357
|
|
|
4044
|
-
// src/
|
|
4045
|
-
|
|
4046
|
-
|
|
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 } : {} };
|
|
4047
4376
|
}
|
|
4048
|
-
function
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
if (
|
|
4053
|
-
|
|
4054
|
-
for (let i = t.indexOf(q); i !== -1; i = t.indexOf(q, i + 1)) {
|
|
4055
|
-
if (i === 0) return 3;
|
|
4056
|
-
if (!/[a-z0-9]/.test(t[i - 1])) return 2;
|
|
4057
|
-
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 -->";
|
|
4058
4383
|
}
|
|
4059
|
-
|
|
4060
|
-
const
|
|
4061
|
-
|
|
4062
|
-
|
|
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} -->`;
|
|
4063
4389
|
}
|
|
4064
|
-
function
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
const
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
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)."
|
|
4077
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");
|
|
4078
4446
|
}
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
const
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
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
|
+
}));
|
|
4086
4461
|
}
|
|
4087
|
-
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
|
+
}
|
|
4088
4471
|
return {
|
|
4089
|
-
id:
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
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)
|
|
4094
4483
|
};
|
|
4095
4484
|
}
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
import { Hono as Hono8 } from "hono";
|
|
4099
|
-
|
|
4100
|
-
// src/nodes/markdown-export.ts
|
|
4101
|
-
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
4102
|
-
function nodeToMarkdown(node) {
|
|
4103
|
-
const tags = node.tags ?? [];
|
|
4104
|
-
return [
|
|
4105
|
-
"---",
|
|
4106
|
-
`title: ${JSON.stringify(node.title ?? "")}`,
|
|
4107
|
-
`tags: [${tags.map((t) => JSON.stringify(t)).join(", ")}]`,
|
|
4108
|
-
`status: ${JSON.stringify(node.status ?? "")}`,
|
|
4109
|
-
`id: ${JSON.stringify(node.id)}`,
|
|
4110
|
-
"---",
|
|
4111
|
-
""
|
|
4112
|
-
].join("\n") + (node.body ?? "");
|
|
4485
|
+
async function getThreadRow(threadId) {
|
|
4486
|
+
return await getDb().get("SELECT * FROM annotation_threads WHERE id = ?", [threadId]);
|
|
4113
4487
|
}
|
|
4114
|
-
function
|
|
4115
|
-
|
|
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;
|
|
4116
4499
|
}
|
|
4117
|
-
function
|
|
4118
|
-
|
|
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));
|
|
4119
4506
|
}
|
|
4120
|
-
function
|
|
4121
|
-
const
|
|
4122
|
-
if (
|
|
4123
|
-
|
|
4124
|
-
|
|
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));
|
|
4125
4536
|
}
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
return RAW_CONTENT_TYPES[nodeType || "document"] ?? "text/markdown; charset=utf-8";
|
|
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}`);
|
|
4541
|
+
}
|
|
4542
|
+
return row;
|
|
4133
4543
|
}
|
|
4134
|
-
function
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
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));
|
|
4555
|
+
}
|
|
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
|
+
);
|
|
4569
|
+
}
|
|
4570
|
+
return rowToThread(await getThreadRow(threadId));
|
|
4571
|
+
}
|
|
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 || "";
|
|
4139
4584
|
} catch {
|
|
4585
|
+
liveExists = false;
|
|
4586
|
+
}
|
|
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
|
+
}
|
|
4597
|
+
}
|
|
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
|
|
4646
|
+
);
|
|
4140
4647
|
}
|
|
4141
|
-
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4142
|
-
return m ? `---
|
|
4143
|
-
${m[1]}
|
|
4144
|
-
---
|
|
4145
|
-
|
|
4146
|
-
` : "";
|
|
4147
4648
|
}
|
|
4148
4649
|
|
|
4149
|
-
// src/
|
|
4150
|
-
var
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
}
|
|
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}`;
|
|
4159
4677
|
}
|
|
4160
|
-
function
|
|
4161
|
-
const
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
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
|
+
}
|
|
4166
4691
|
}
|
|
4167
|
-
return
|
|
4168
|
-
"Content-Type": rawContentType(response.type)
|
|
4169
|
-
});
|
|
4692
|
+
return null;
|
|
4170
4693
|
}
|
|
4171
|
-
|
|
4172
|
-
const
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
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) {
|
|
4747
|
+
try {
|
|
4748
|
+
return (await readArtifact(row.nest_id, row.node_id)).title || row.node_id;
|
|
4749
|
+
} catch {
|
|
4750
|
+
return row.node_id;
|
|
4198
4751
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
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);
|
|
4201
4764
|
}
|
|
4202
|
-
|
|
4203
|
-
const
|
|
4204
|
-
const
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
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
|
+
});
|
|
4776
|
+
}
|
|
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 });
|
|
4228
4976
|
});
|
|
4229
|
-
|
|
4230
|
-
const
|
|
4231
|
-
const
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
);
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
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
|
|
4248
5006
|
});
|
|
5007
|
+
return c.html(sentPage());
|
|
4249
5008
|
});
|
|
4250
|
-
|
|
4251
|
-
const
|
|
4252
|
-
const
|
|
4253
|
-
|
|
4254
|
-
const
|
|
4255
|
-
|
|
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);
|
|
5016
|
+
}
|
|
5017
|
+
setSessionCookie2(c, slug, await createSession2(row.id, email));
|
|
5018
|
+
return c.redirect(`/p/${slug}`);
|
|
4256
5019
|
});
|
|
4257
|
-
|
|
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) => {
|
|
4258
5025
|
const nestId = c.req.param("nestId");
|
|
4259
|
-
const
|
|
4260
|
-
const
|
|
4261
|
-
const
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
);
|
|
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
|
+
});
|
|
4268
5035
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
5036
|
+
return c.json({ teams });
|
|
5037
|
+
});
|
|
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");
|
|
4273
5042
|
}
|
|
4274
|
-
|
|
5043
|
+
const userId = c.get("userId");
|
|
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()
|
|
5070
|
+
);
|
|
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;
|
|
4275
5086
|
try {
|
|
4276
|
-
|
|
5087
|
+
res = await fetch(`${promptowlUrl}/api/user/teams`, {
|
|
5088
|
+
headers: {
|
|
5089
|
+
...config.PROMPTOWL_FETCH_HEADERS,
|
|
5090
|
+
Authorization: `Bearer ${token}`
|
|
5091
|
+
}
|
|
5092
|
+
});
|
|
4277
5093
|
} catch {
|
|
4278
|
-
throw new
|
|
4279
|
-
`Version ${targetVersion} not found for ${nodeId}`
|
|
4280
|
-
);
|
|
5094
|
+
throw new ValidationError("Could not reach PromptOwl. Check this server's network access.");
|
|
4281
5095
|
}
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
nodeId,
|
|
4286
|
-
{ content, changeNote: `Restored from version ${targetVersion}` },
|
|
4287
|
-
userEmail
|
|
4288
|
-
);
|
|
4289
|
-
await trackEvent("node.revert", { nestId, nodeId, targetVersion });
|
|
4290
|
-
return c.json({ ok: true, version, node: toNodeResponse(node) });
|
|
4291
|
-
});
|
|
4292
|
-
nodeRoutes.post("/:nodeId{.+}/move", async (c) => {
|
|
4293
|
-
const nestId = c.req.param("nestId");
|
|
4294
|
-
const nodeId = c.req.param("nodeId");
|
|
4295
|
-
const userId = c.get("userId");
|
|
4296
|
-
const userEmail = await resolveCallerEmail(userId);
|
|
4297
|
-
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
4298
|
-
return c.json(
|
|
4299
|
-
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
4300
|
-
403
|
|
5096
|
+
if (res.status === 401) {
|
|
5097
|
+
throw new UnauthorizedError(
|
|
5098
|
+
"PromptOwl rejected this approval. Approve again and retry."
|
|
4301
5099
|
);
|
|
4302
5100
|
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
const { node, previousId } = await moveNode(nestId, nodeId, body.folder, userEmail);
|
|
4308
|
-
return c.json({ ok: true, node: toNodeResponse(node), previousId });
|
|
4309
|
-
});
|
|
4310
|
-
nodeRoutes.get("/:nodeId{.+}/watchers", async (c) => {
|
|
4311
|
-
const nestId = c.req.param("nestId");
|
|
4312
|
-
const nodeId = c.req.param("nodeId");
|
|
4313
|
-
return c.json({ watchers: await listWatchers(nestId, nodeId) });
|
|
4314
|
-
});
|
|
4315
|
-
nodeRoutes.post("/:nodeId{.+}/watchers", async (c) => {
|
|
4316
|
-
const nestId = c.req.param("nestId");
|
|
4317
|
-
const nodeId = c.req.param("nodeId");
|
|
4318
|
-
const body = await c.req.json().catch(() => ({}));
|
|
4319
|
-
const email = typeof body.email === "string" ? body.email.trim() : "";
|
|
4320
|
-
if (!email || !email.includes("@")) {
|
|
4321
|
-
throw new ValidationError("email is required");
|
|
5101
|
+
if (!res.ok) {
|
|
5102
|
+
throw new ValidationError(
|
|
5103
|
+
`PromptOwl could not list your teams (HTTP ${res.status}).`
|
|
5104
|
+
);
|
|
4322
5105
|
}
|
|
4323
|
-
|
|
4324
|
-
return
|
|
4325
|
-
}
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
const email = c.req.query("email")?.trim() ?? "";
|
|
4330
|
-
if (!email) throw new ValidationError("email query param is required");
|
|
4331
|
-
if (!await removeWatcher(nestId, nodeId, email)) {
|
|
4332
|
-
throw new NotFoundError("Not watching");
|
|
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");
|
|
4333
5112
|
}
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
const
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
nestId,
|
|
4344
|
-
{ id: nodeId },
|
|
4345
|
-
userId,
|
|
4346
|
-
userEmail,
|
|
4347
|
-
{ verifyChecksum: true }
|
|
4348
|
-
);
|
|
4349
|
-
if (node.pendingChange) {
|
|
4350
|
-
try {
|
|
4351
|
-
await scanDocumentForDrift(nestId, nodeId, userEmail || "system:read");
|
|
4352
|
-
const refreshed = await getPendingChange(nestId, nodeId);
|
|
4353
|
-
if (refreshed) node.pendingChange = refreshed;
|
|
4354
|
-
} catch (err) {
|
|
4355
|
-
console.error("[external-edit] stage-on-read failed:", err);
|
|
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
|
+
);
|
|
4356
5122
|
}
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
}
|
|
4362
|
-
} catch (err) {
|
|
4363
|
-
console.error("[external-edit] reconstruct-latest failed:", err);
|
|
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
|
+
);
|
|
4364
5127
|
}
|
|
5128
|
+
selected.push(team);
|
|
4365
5129
|
}
|
|
4366
|
-
const
|
|
4367
|
-
const
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
if (people) {
|
|
4373
|
-
response.author = people.author;
|
|
4374
|
-
response.collaborators = people.collaborators;
|
|
4375
|
-
}
|
|
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.");
|
|
4376
5136
|
}
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
if (approved != null) {
|
|
4381
|
-
let approvedRaw = "";
|
|
4382
|
-
try {
|
|
4383
|
-
approvedRaw = await versionManager.reconstructVersion(
|
|
4384
|
-
nodeId,
|
|
4385
|
-
approved
|
|
4386
|
-
);
|
|
4387
|
-
response.content = bodyOnly(nodeId, approvedRaw);
|
|
4388
|
-
} catch (err) {
|
|
4389
|
-
console.error(
|
|
4390
|
-
"reconstructVersion failed (public single)",
|
|
4391
|
-
nodeId,
|
|
4392
|
-
approved,
|
|
4393
|
-
err
|
|
4394
|
-
);
|
|
4395
|
-
response.content = "";
|
|
4396
|
-
}
|
|
4397
|
-
response.version = approved;
|
|
4398
|
-
response.status = "published";
|
|
4399
|
-
if (isRawFormat(c)) {
|
|
4400
|
-
return rawNodeBody(c, response, approvedRaw);
|
|
4401
|
-
}
|
|
4402
|
-
if (isMarkdownFormat(c)) {
|
|
4403
|
-
return c.body(nodeAsMarkdown(response, nodeId), 200, {
|
|
4404
|
-
"Content-Type": "text/markdown; charset=utf-8"
|
|
4405
|
-
});
|
|
4406
|
-
}
|
|
4407
|
-
return c.json({ node: response });
|
|
4408
|
-
}
|
|
5137
|
+
const results = [];
|
|
5138
|
+
for (const poTeam of selected) {
|
|
5139
|
+
results.push(await syncOne(poTeam, params.callerUserId, caller.email));
|
|
4409
5140
|
}
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
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
|
+
});
|
|
4414
5162
|
}
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
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)
|
|
4418
5170
|
);
|
|
4419
|
-
if (
|
|
4420
|
-
|
|
5171
|
+
if (added.length || removed.length || updated.length) {
|
|
5172
|
+
await replaceTeamRoster({
|
|
5173
|
+
teamId: team.id,
|
|
5174
|
+
members: desired,
|
|
5175
|
+
callerUserId,
|
|
5176
|
+
allowAdmin: false
|
|
5177
|
+
});
|
|
4421
5178
|
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
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)
|
|
4425
5203
|
});
|
|
4426
5204
|
}
|
|
4427
|
-
return
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
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
|
+
);
|
|
4438
5239
|
return c.json(
|
|
4439
|
-
{
|
|
4440
|
-
|
|
4441
|
-
your_version: baseVersion,
|
|
4442
|
-
current_version: conflict.currentVersion,
|
|
4443
|
-
updated_by: conflict.updatedBy,
|
|
4444
|
-
updated_at: conflict.updatedAt,
|
|
4445
|
-
rejected_content: body.content || body.append || null
|
|
4446
|
-
},
|
|
4447
|
-
409
|
|
5240
|
+
{ error: err.message, code: CONNECT_REQUIRED },
|
|
5241
|
+
CONNECT_REQUIRED_STATUS
|
|
4448
5242
|
);
|
|
4449
5243
|
}
|
|
5244
|
+
throw err;
|
|
4450
5245
|
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
);
|
|
4467
|
-
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
|
+
}));
|
|
4468
5262
|
});
|
|
4469
|
-
|
|
4470
|
-
const
|
|
4471
|
-
const
|
|
4472
|
-
|
|
4473
|
-
|
|
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);
|
|
4474
5270
|
});
|
|
4475
|
-
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) => {
|
|
4476
5276
|
const userId = c.get("userId");
|
|
4477
|
-
const
|
|
4478
|
-
const
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
// src/annotations/routes.ts
|
|
4483
|
-
import { Hono as Hono9 } from "hono";
|
|
4484
|
-
|
|
4485
|
-
// src/annotations/service.ts
|
|
4486
|
-
import { v4 as uuid5 } from "uuid";
|
|
4487
|
-
|
|
4488
|
-
// src/annotations/projection.ts
|
|
4489
|
-
var MAX_CONTEXT_CHARS = 250;
|
|
4490
|
-
var MAX_QUOTE_CHARS = 1e3;
|
|
4491
|
-
function clampAnchor(raw) {
|
|
4492
|
-
if (!raw) return null;
|
|
4493
|
-
const quote = (raw.quote ?? "").trim().slice(0, MAX_QUOTE_CHARS);
|
|
4494
|
-
if (!quote) return null;
|
|
4495
|
-
const before = (raw.before ?? "").slice(-MAX_CONTEXT_CHARS);
|
|
4496
|
-
const after = (raw.after ?? "").slice(0, MAX_CONTEXT_CHARS);
|
|
4497
|
-
const line = typeof raw.line === "number" && Number.isFinite(raw.line) && raw.line > 0 ? Math.floor(raw.line) : void 0;
|
|
4498
|
-
return { quote, before, after, ...line !== void 0 ? { line } : {} };
|
|
4499
|
-
}
|
|
4500
|
-
function oneLine(s) {
|
|
4501
|
-
return s.replace(/\s+/g, " ").replace(/--+>/g, "-\u2192").trim();
|
|
4502
|
-
}
|
|
4503
|
-
function renderAnchor(anchor) {
|
|
4504
|
-
if (!anchor) {
|
|
4505
|
-
return "<!-- anchor: whole-artifact -->";
|
|
4506
|
-
}
|
|
4507
|
-
const linePart = anchor.line !== void 0 ? `line ${anchor.line} \xB7 ` : "";
|
|
4508
|
-
const context = `\u2026${oneLine(anchor.before)}\u3008${oneLine(anchor.quote)}\u3009${oneLine(
|
|
4509
|
-
anchor.after
|
|
4510
|
-
)}\u2026`;
|
|
4511
|
-
return `<!-- anchor: ${linePart}quote "${oneLine(anchor.quote)}" \xB7 context ${context} -->`;
|
|
4512
|
-
}
|
|
4513
|
-
function statusHeading(thread) {
|
|
4514
|
-
if (thread.status === "resolved") {
|
|
4515
|
-
const who = thread.resolvedBy ? `${thread.resolvedBy}, ` : "";
|
|
4516
|
-
const when = thread.resolvedAt ? `${thread.resolvedAt}` : "";
|
|
4517
|
-
const meta = who || when ? ` (${who}${when})` : "";
|
|
4518
|
-
return `\u2705 RESOLVED${meta}`;
|
|
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.");
|
|
4519
5281
|
}
|
|
4520
|
-
return
|
|
4521
|
-
}
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
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)
|
|
4529
5292
|
});
|
|
4530
|
-
}
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
lines.push("");
|
|
4539
|
-
if (threads.length === 0) {
|
|
4540
|
-
lines.push("_No annotations yet._");
|
|
4541
|
-
lines.push("");
|
|
4542
|
-
return lines.join("\n");
|
|
4543
|
-
}
|
|
4544
|
-
const buckets2 = /* @__PURE__ */ new Map();
|
|
4545
|
-
for (const t of threads) {
|
|
4546
|
-
const key = t.snapshotVersion ?? null;
|
|
4547
|
-
(buckets2.get(key) ?? buckets2.set(key, []).get(key)).push(t);
|
|
4548
|
-
}
|
|
4549
|
-
const keys = [...buckets2.keys()].sort((a, b) => {
|
|
4550
|
-
if (a == null) return 1;
|
|
4551
|
-
if (b == null) return -1;
|
|
4552
|
-
return b - a;
|
|
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)
|
|
4553
5301
|
});
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
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
|
+
});
|
|
5339
|
+
|
|
5340
|
+
// src/nodes/search.ts
|
|
5341
|
+
async function discoverForSearch(storage) {
|
|
5342
|
+
return storage.discoverDocuments({ includeRetired: true });
|
|
5343
|
+
}
|
|
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;
|
|
4567
5354
|
}
|
|
4568
|
-
|
|
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;
|
|
4569
5359
|
}
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
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)
|
|
4577
5373
|
);
|
|
4578
|
-
return rows.map((r) => ({
|
|
4579
|
-
id: r.id,
|
|
4580
|
-
author: r.author,
|
|
4581
|
-
body: r.body,
|
|
4582
|
-
createdAt: r.created_at
|
|
4583
|
-
}));
|
|
4584
5374
|
}
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
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)}`;
|
|
5382
|
+
}
|
|
5383
|
+
function toSearchHit(node, query) {
|
|
4594
5384
|
return {
|
|
4595
|
-
id:
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
status: row.status,
|
|
4601
|
-
createdBy: row.created_by,
|
|
4602
|
-
createdAt: row.created_at,
|
|
4603
|
-
resolvedBy: row.resolved_by,
|
|
4604
|
-
resolvedAt: row.resolved_at,
|
|
4605
|
-
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)
|
|
4606
5390
|
};
|
|
4607
5391
|
}
|
|
4608
|
-
|
|
4609
|
-
|
|
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 ?? "");
|
|
4610
5409
|
}
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
`SELECT node_id, CAST(COUNT(*) AS INTEGER) AS count, MAX(created_at) AS last_at
|
|
4614
|
-
FROM annotation_threads
|
|
4615
|
-
WHERE nest_id = ?
|
|
4616
|
-
GROUP BY node_id`,
|
|
4617
|
-
[nestId]
|
|
4618
|
-
);
|
|
4619
|
-
const out = {};
|
|
4620
|
-
for (const r of rows) out[r.node_id] = { count: r.count, lastAt: r.last_at };
|
|
4621
|
-
return out;
|
|
5410
|
+
function nodesToMarkdown(nodes) {
|
|
5411
|
+
return nodes.map(nodeToMarkdown).join("\n\n---\n\n");
|
|
4622
5412
|
}
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
"SELECT * FROM annotation_threads WHERE nest_id = ? AND node_id = ? ORDER BY created_at ASC, id ASC",
|
|
4626
|
-
[nestId, nodeId]
|
|
4627
|
-
);
|
|
4628
|
-
return Promise.all(rows.map(rowToThread));
|
|
5413
|
+
function isMarkdownFormat(c) {
|
|
5414
|
+
return c.req.query("format") === "markdown";
|
|
4629
5415
|
}
|
|
4630
|
-
|
|
4631
|
-
const
|
|
4632
|
-
if (
|
|
4633
|
-
|
|
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 {
|
|
4634
5436
|
}
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
);
|
|
4653
|
-
await tx.run(
|
|
4654
|
-
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
|
|
4655
|
-
[uuid5(), id, authorEmail, body]
|
|
4656
|
-
);
|
|
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
|
|
4657
5454
|
});
|
|
4658
|
-
return rowToThread(await getThreadRow(id));
|
|
4659
5455
|
}
|
|
4660
|
-
|
|
4661
|
-
const
|
|
4662
|
-
|
|
4663
|
-
|
|
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);
|
|
4664
5462
|
}
|
|
4665
|
-
return
|
|
5463
|
+
return c.body(payload, 200, {
|
|
5464
|
+
"Content-Type": rawContentType(response.type)
|
|
5465
|
+
});
|
|
4666
5466
|
}
|
|
4667
|
-
|
|
4668
|
-
const
|
|
4669
|
-
|
|
4670
|
-
|
|
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");
|
|
4671
5494
|
}
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
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
|
|
4676
5514
|
);
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
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
|
|
4691
5563
|
);
|
|
4692
5564
|
}
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
let
|
|
4699
|
-
let type = null;
|
|
4700
|
-
let liveHtml = "";
|
|
4701
|
-
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;
|
|
4702
5571
|
try {
|
|
4703
|
-
|
|
4704
|
-
title = node.frontmatter?.title || nodeId;
|
|
4705
|
-
type = node.frontmatter?.type || null;
|
|
4706
|
-
liveHtml = node.body || "";
|
|
5572
|
+
raw = await versionManager.reconstructVersion(nodeId, targetVersion);
|
|
4707
5573
|
} catch {
|
|
4708
|
-
|
|
5574
|
+
throw new NotFoundError(
|
|
5575
|
+
`Version ${targetVersion} not found for ${nodeId}`
|
|
5576
|
+
);
|
|
4709
5577
|
}
|
|
4710
|
-
|
|
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
|
+
);
|
|
5598
|
+
}
|
|
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');
|
|
5602
|
+
}
|
|
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) {
|
|
4711
5646
|
try {
|
|
4712
|
-
|
|
4713
|
-
|
|
5647
|
+
await scanDocumentForDrift(nestId, nodeId, userEmail || "system:read");
|
|
5648
|
+
const refreshed = await getPendingChange(nestId, nodeId);
|
|
5649
|
+
if (refreshed) node.pendingChange = refreshed;
|
|
4714
5650
|
} catch (err) {
|
|
4715
|
-
console.
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
);
|
|
5651
|
+
console.error("[external-edit] stage-on-read failed:", err);
|
|
5652
|
+
}
|
|
5653
|
+
try {
|
|
5654
|
+
const latest = await loadLatestApprovedNode(nestId, nodeId);
|
|
5655
|
+
if (latest) {
|
|
5656
|
+
node = { ...latest, pendingChange: node.pendingChange };
|
|
5657
|
+
}
|
|
5658
|
+
} catch (err) {
|
|
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
|
|
5682
|
+
);
|
|
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
|
+
});
|
|
5702
|
+
}
|
|
5703
|
+
return c.json({ node: response });
|
|
4719
5704
|
}
|
|
4720
5705
|
}
|
|
4721
|
-
|
|
4722
|
-
|
|
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;
|
|
4723
5710
|
}
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
const
|
|
4745
|
-
const
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
},
|
|
4759
|
-
userEmail
|
|
4760
|
-
);
|
|
4761
|
-
} else {
|
|
4762
|
-
throw err;
|
|
4763
|
-
}
|
|
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
|
+
);
|
|
4764
5745
|
}
|
|
4765
|
-
} catch (err) {
|
|
4766
|
-
console.warn(
|
|
4767
|
-
`[annotations] failed to sync derived node for ${nestId}/${nodeId}:`,
|
|
4768
|
-
err
|
|
4769
|
-
);
|
|
4770
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";
|
|
4771
5776
|
}
|
|
4772
5777
|
|
|
5778
|
+
// src/annotations/routes.ts
|
|
5779
|
+
import { Hono as Hono11 } from "hono";
|
|
5780
|
+
|
|
4773
5781
|
// src/annotations/types.ts
|
|
4774
5782
|
var ARTIFACT_NODE_TYPE = "artifact";
|
|
4775
5783
|
|
|
4776
5784
|
// src/annotations/routes.ts
|
|
4777
|
-
var annotationRoutes = new
|
|
5785
|
+
var annotationRoutes = new Hono11();
|
|
4778
5786
|
function getNodeId(c) {
|
|
4779
5787
|
const raw = c.req.param("nodeId");
|
|
4780
5788
|
try {
|
|
@@ -4888,8 +5896,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
4888
5896
|
});
|
|
4889
5897
|
|
|
4890
5898
|
// src/nodes/query-routes.ts
|
|
4891
|
-
import { Hono as
|
|
4892
|
-
import { serializeDocument as serializeDocument3 } from "@promptowl/contextnest-engine";
|
|
5899
|
+
import { Hono as Hono12 } from "hono";
|
|
4893
5900
|
|
|
4894
5901
|
// src/tables/service.ts
|
|
4895
5902
|
function splitCsvLine(line) {
|
|
@@ -5390,7 +6397,9 @@ function extractWikiTargets(body) {
|
|
|
5390
6397
|
}
|
|
5391
6398
|
async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
5392
6399
|
const { storage } = await engineCache.get(nestId);
|
|
5393
|
-
const docs = await storage.discoverDocuments()
|
|
6400
|
+
const docs = (await storage.discoverDocuments()).filter(
|
|
6401
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
6402
|
+
);
|
|
5394
6403
|
const accessible = await filterAccessible(nestId, userId, userEmail, docs);
|
|
5395
6404
|
const idSet = new Set(accessible.map((d) => d.id));
|
|
5396
6405
|
const titleToId = /* @__PURE__ */ new Map();
|
|
@@ -5521,7 +6530,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
5521
6530
|
}
|
|
5522
6531
|
|
|
5523
6532
|
// src/nodes/query-routes.ts
|
|
5524
|
-
var queryRoutes = new
|
|
6533
|
+
var queryRoutes = new Hono12();
|
|
5525
6534
|
queryRoutes.get("/changes", async (c) => {
|
|
5526
6535
|
const nestId = c.req.param("nestId");
|
|
5527
6536
|
const userId = c.get("userId");
|
|
@@ -5649,11 +6658,15 @@ queryRoutes.get("/graph", async (c) => {
|
|
|
5649
6658
|
function approxTokens(text) {
|
|
5650
6659
|
return Math.ceil(text.length / 4);
|
|
5651
6660
|
}
|
|
5652
|
-
function formatContextBlock(node) {
|
|
6661
|
+
function formatContextBlock(node, url) {
|
|
5653
6662
|
const tags = (node.frontmatter.tags || []).join(" ");
|
|
5654
|
-
const
|
|
6663
|
+
const meta = [
|
|
6664
|
+
tags ? `_tags: ${tags}_` : null,
|
|
6665
|
+
url ? `_source: ${url}_` : null
|
|
6666
|
+
].filter(Boolean);
|
|
6667
|
+
const header = meta.length ? `# ${node.frontmatter.title}
|
|
5655
6668
|
|
|
5656
|
-
|
|
6669
|
+
${meta.join("\n")}
|
|
5657
6670
|
|
|
5658
6671
|
` : `# ${node.frontmatter.title}
|
|
5659
6672
|
|
|
@@ -5666,6 +6679,7 @@ queryRoutes.post("/context", async (c) => {
|
|
|
5666
6679
|
throw new ValidationError("prompt or selector is required");
|
|
5667
6680
|
}
|
|
5668
6681
|
const nestId = c.req.param("nestId");
|
|
6682
|
+
const baseUrl = requestBaseUrl(c.req.url);
|
|
5669
6683
|
const { query: queryEngine, storage } = await engineCache.get(nestId);
|
|
5670
6684
|
const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
|
|
5671
6685
|
const hops = body.hops ?? 2;
|
|
@@ -5774,20 +6788,21 @@ queryRoutes.post("/context", async (c) => {
|
|
|
5774
6788
|
const readable = await Promise.all(
|
|
5775
6789
|
accessible.map(async (doc) => ({
|
|
5776
6790
|
...doc,
|
|
5777
|
-
body: await resolveReadableBody(nestId, doc.id, userId, doc.body || "")
|
|
6791
|
+
body: await resolveReadableBody(nestId, doc.id, userId, doc.body || ""),
|
|
6792
|
+
url: resolveNodeUrl(doc, { nestId, baseUrl })
|
|
5778
6793
|
}))
|
|
5779
6794
|
);
|
|
5780
6795
|
const included = [];
|
|
5781
6796
|
let tokenCount = 0;
|
|
5782
6797
|
for (const doc of readable) {
|
|
5783
|
-
const block = formatContextBlock(doc);
|
|
6798
|
+
const block = formatContextBlock(doc, doc.url);
|
|
5784
6799
|
const blockTokens = approxTokens(block);
|
|
5785
6800
|
if (tokenCount + blockTokens > maxTokens && included.length > 0) break;
|
|
5786
6801
|
included.push(doc);
|
|
5787
6802
|
tokenCount += blockTokens;
|
|
5788
6803
|
}
|
|
5789
6804
|
const truncatedByBudget = accessible.length - included.length;
|
|
5790
|
-
const contextMarkdown = included.map(formatContextBlock).join("\n\n---\n\n");
|
|
6805
|
+
const contextMarkdown = included.map((n) => formatContextBlock(n, n.url)).join("\n\n---\n\n");
|
|
5791
6806
|
trackEvent("context.resolve", {
|
|
5792
6807
|
nestId,
|
|
5793
6808
|
selector_from_prompt: !!compileDetail,
|
|
@@ -5800,6 +6815,7 @@ queryRoutes.post("/context", async (c) => {
|
|
|
5800
6815
|
nodes: included.map((n) => ({
|
|
5801
6816
|
id: n.id,
|
|
5802
6817
|
title: n.frontmatter.title,
|
|
6818
|
+
url: n.url,
|
|
5803
6819
|
tags: n.frontmatter.tags || [],
|
|
5804
6820
|
type: n.frontmatter.type || "document"
|
|
5805
6821
|
})),
|
|
@@ -5867,7 +6883,9 @@ queryRoutes.get("/search", async (c) => {
|
|
|
5867
6883
|
}
|
|
5868
6884
|
const nestId = c.req.param("nestId");
|
|
5869
6885
|
const { storage } = await engineCache.get(nestId);
|
|
5870
|
-
const matches = matchDocuments(await discoverForSearch(storage), q)
|
|
6886
|
+
const matches = matchDocuments(await discoverForSearch(storage), q).filter(
|
|
6887
|
+
(n) => !isDerivedAnnotationsNode(n.id)
|
|
6888
|
+
);
|
|
5871
6889
|
const userId = c.get("userId");
|
|
5872
6890
|
const userEmail = await resolveCallerEmail(userId);
|
|
5873
6891
|
const accessible = await filterAccessible(nestId, userId, userEmail, matches);
|
|
@@ -5879,7 +6897,9 @@ queryRoutes.get("/search", async (c) => {
|
|
|
5879
6897
|
});
|
|
5880
6898
|
queryRoutes.get("/overview", async (c) => {
|
|
5881
6899
|
const { storage } = await engineCache.get(c.req.param("nestId"));
|
|
5882
|
-
const documents = await storage.discoverDocuments()
|
|
6900
|
+
const documents = (await storage.discoverDocuments()).filter(
|
|
6901
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
6902
|
+
);
|
|
5883
6903
|
const types = {};
|
|
5884
6904
|
const tags = {};
|
|
5885
6905
|
for (const node of documents) {
|
|
@@ -5914,6 +6934,14 @@ queryRoutes.get("/folders", async (c) => {
|
|
|
5914
6934
|
});
|
|
5915
6935
|
return c.json({ folders });
|
|
5916
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
|
+
});
|
|
5917
6945
|
queryRoutes.post("/reindex", async (c) => {
|
|
5918
6946
|
if (permissionLevel(c.get("nestPermission")) < permissionLevel("write")) {
|
|
5919
6947
|
throw new ForbiddenError("Reindexing a nest requires write access");
|
|
@@ -6013,55 +7041,58 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
6013
7041
|
}
|
|
6014
7042
|
}
|
|
6015
7043
|
const nestId = c.req.param("nestId");
|
|
6016
|
-
const
|
|
7044
|
+
const userEmail = await resolveCallerEmail(c.get("userId"));
|
|
6017
7045
|
const created = [];
|
|
7046
|
+
const skipped = [];
|
|
7047
|
+
const failed = [];
|
|
6018
7048
|
if (body.context_md) {
|
|
7049
|
+
const { storage } = await engineCache.get(nestId);
|
|
6019
7050
|
await storage.writeContextMd(body.context_md);
|
|
7051
|
+
await storage.regenerateIndex();
|
|
6020
7052
|
}
|
|
6021
7053
|
for (const doc of body.documents || []) {
|
|
6022
7054
|
if (!doc.title || !doc.content) continue;
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
7055
|
+
try {
|
|
7056
|
+
const result = await createNode(
|
|
7057
|
+
nestId,
|
|
7058
|
+
{
|
|
7059
|
+
title: doc.title,
|
|
7060
|
+
content: doc.content,
|
|
7061
|
+
type: doc.type,
|
|
7062
|
+
tags: doc.tags,
|
|
7063
|
+
scope: doc.scope
|
|
7064
|
+
},
|
|
7065
|
+
userEmail
|
|
7066
|
+
);
|
|
7067
|
+
created.push(result.node.id);
|
|
7068
|
+
} catch (err) {
|
|
7069
|
+
if (err instanceof ConflictError) {
|
|
7070
|
+
skipped.push(doc.title);
|
|
7071
|
+
continue;
|
|
7072
|
+
}
|
|
7073
|
+
if (isVaultLockTimeout(err) || err instanceof VaultBusyError) throw err;
|
|
7074
|
+
if (!(err instanceof AppError)) console.error("bulk publish row failed", err);
|
|
7075
|
+
failed.push({
|
|
6031
7076
|
title: doc.title,
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
version: 1,
|
|
6036
|
-
created_at: now,
|
|
6037
|
-
updated_at: now,
|
|
6038
|
-
metadata: {
|
|
6039
|
-
owners: ["*"],
|
|
6040
|
-
scope: doc.scope || "team"
|
|
6041
|
-
}
|
|
6042
|
-
},
|
|
6043
|
-
body: doc.content,
|
|
6044
|
-
rawContent: ""
|
|
6045
|
-
};
|
|
6046
|
-
const serialized = serializeDocument3(node);
|
|
6047
|
-
await storage.writeDocument(id, serialized);
|
|
6048
|
-
await syncNodeTags(nestId, id, tags);
|
|
6049
|
-
created.push(id);
|
|
7077
|
+
error: err instanceof AppError ? err.message : "Could not create this document"
|
|
7078
|
+
});
|
|
7079
|
+
}
|
|
6050
7080
|
}
|
|
6051
|
-
if (created.length > 0 || body.context_md) await storage.regenerateIndex();
|
|
6052
7081
|
trackEvent("nest.publish", { nestId, count: created.length });
|
|
6053
7082
|
return c.json(
|
|
6054
7083
|
{
|
|
6055
7084
|
published: created.length,
|
|
6056
7085
|
context_md_updated: !!body.context_md,
|
|
6057
|
-
node_ids: created
|
|
7086
|
+
node_ids: created,
|
|
7087
|
+
skipped,
|
|
7088
|
+
failed
|
|
6058
7089
|
},
|
|
6059
7090
|
201
|
|
6060
7091
|
);
|
|
6061
7092
|
});
|
|
6062
7093
|
|
|
6063
7094
|
// src/mcp/routes.ts
|
|
6064
|
-
import { Hono as
|
|
7095
|
+
import { Hono as Hono14 } from "hono";
|
|
6065
7096
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6066
7097
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
6067
7098
|
|
|
@@ -6141,10 +7172,10 @@ async function listTraceEvents(filters = {}) {
|
|
|
6141
7172
|
}
|
|
6142
7173
|
|
|
6143
7174
|
// src/mcp/workflow-tools.ts
|
|
6144
|
-
import { v4 as
|
|
7175
|
+
import { v4 as uuid10 } from "uuid";
|
|
6145
7176
|
|
|
6146
7177
|
// src/workflow/run-service.ts
|
|
6147
|
-
import { v4 as
|
|
7178
|
+
import { v4 as uuid8 } from "uuid";
|
|
6148
7179
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
6149
7180
|
async function flowSubgraph(nestId, agentNode) {
|
|
6150
7181
|
const db = getDb();
|
|
@@ -6274,7 +7305,7 @@ async function triggerRun(opts) {
|
|
|
6274
7305
|
}
|
|
6275
7306
|
const edges = await flowSubgraph(nestId, agentNode);
|
|
6276
7307
|
const definitions = await referencedDefinitions(nestId, edges);
|
|
6277
|
-
const id = `run_${
|
|
7308
|
+
const id = `run_${uuid8()}`;
|
|
6278
7309
|
const db = getDb();
|
|
6279
7310
|
await db.transaction(async (tx) => {
|
|
6280
7311
|
if (parentRunId) {
|
|
@@ -6460,8 +7491,8 @@ async function listRuns(nestId, opts) {
|
|
|
6460
7491
|
}
|
|
6461
7492
|
|
|
6462
7493
|
// src/workflow/edge-routes.ts
|
|
6463
|
-
import { Hono as
|
|
6464
|
-
import { v4 as
|
|
7494
|
+
import { Hono as Hono13 } from "hono";
|
|
7495
|
+
import { v4 as uuid9 } from "uuid";
|
|
6465
7496
|
function toResponse(e, t) {
|
|
6466
7497
|
return {
|
|
6467
7498
|
id: e.id,
|
|
@@ -6575,7 +7606,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
|
|
|
6575
7606
|
);
|
|
6576
7607
|
}
|
|
6577
7608
|
}
|
|
6578
|
-
var edgeRoutes = new
|
|
7609
|
+
var edgeRoutes = new Hono13();
|
|
6579
7610
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
6580
7611
|
const nestId = c.req.param("nestId");
|
|
6581
7612
|
const db = getDb();
|
|
@@ -6643,7 +7674,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
6643
7674
|
);
|
|
6644
7675
|
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
6645
7676
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6646
|
-
const id =
|
|
7677
|
+
const id = uuid9();
|
|
6647
7678
|
await db.transaction(async (tx) => {
|
|
6648
7679
|
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
6649
7680
|
if (type.is_flow) {
|
|
@@ -7044,7 +8075,7 @@ async function createEdge(ctx, args) {
|
|
|
7044
8075
|
);
|
|
7045
8076
|
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
7046
8077
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7047
|
-
const id =
|
|
8078
|
+
const id = uuid10();
|
|
7048
8079
|
await db.transaction(async (tx) => {
|
|
7049
8080
|
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
7050
8081
|
if (type.is_flow) {
|
|
@@ -7108,7 +8139,7 @@ async function upsertEdgeType(ctx, args) {
|
|
|
7108
8139
|
`INSERT INTO edge_types
|
|
7109
8140
|
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
7110
8141
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
7111
|
-
[
|
|
8142
|
+
[uuid10(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
7112
8143
|
);
|
|
7113
8144
|
}
|
|
7114
8145
|
});
|
|
@@ -7664,6 +8695,13 @@ async function handleToolCall(toolName, args, ctx) {
|
|
|
7664
8695
|
try {
|
|
7665
8696
|
return await runTool(toolName, args, ctx);
|
|
7666
8697
|
} catch (err) {
|
|
8698
|
+
if (isVaultLockTimeout(err) || err instanceof VaultBusyError) {
|
|
8699
|
+
status = 503;
|
|
8700
|
+
return toolError(
|
|
8701
|
+
"The vault is busy with another write. Retry in a few seconds.",
|
|
8702
|
+
"VAULT_BUSY"
|
|
8703
|
+
);
|
|
8704
|
+
}
|
|
7667
8705
|
status = 500;
|
|
7668
8706
|
throw err;
|
|
7669
8707
|
} finally {
|
|
@@ -8554,19 +9592,19 @@ ${list}`,
|
|
|
8554
9592
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
8555
9593
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
8556
9594
|
try {
|
|
8557
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
8558
|
-
const db = (await import("./client-
|
|
9595
|
+
const { createGrant: createGrant2 } = await import("./grants-service-WC27TRY3.js");
|
|
9596
|
+
const db = (await import("./client-7Z2IVCPW.js")).getDb();
|
|
8559
9597
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
8560
9598
|
const e = normalizeEmail2(String(args.email || ""));
|
|
8561
9599
|
if (!e) return "email is required.";
|
|
8562
9600
|
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
8563
9601
|
if (!row) {
|
|
8564
9602
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
8565
|
-
const { v4:
|
|
8566
|
-
const id =
|
|
9603
|
+
const { v4: uuid18 } = await import("uuid");
|
|
9604
|
+
const id = uuid18();
|
|
8567
9605
|
await db.run(
|
|
8568
9606
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
8569
|
-
[id, e, null, await hashPassword2(
|
|
9607
|
+
[id, e, null, await hashPassword2(uuid18())]
|
|
8570
9608
|
);
|
|
8571
9609
|
row = { id };
|
|
8572
9610
|
}
|
|
@@ -8621,7 +9659,7 @@ ${list}`;
|
|
|
8621
9659
|
|
|
8622
9660
|
// src/mcp/routes.ts
|
|
8623
9661
|
import { z } from "zod";
|
|
8624
|
-
var mcpRoutes = new
|
|
9662
|
+
var mcpRoutes = new Hono14();
|
|
8625
9663
|
async function getUserEmail2(userId) {
|
|
8626
9664
|
const db = getDb();
|
|
8627
9665
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
@@ -8687,8 +9725,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
8687
9725
|
});
|
|
8688
9726
|
|
|
8689
9727
|
// src/workflow/run-routes.ts
|
|
8690
|
-
import { Hono as
|
|
8691
|
-
import { v4 as
|
|
9728
|
+
import { Hono as Hono15 } from "hono";
|
|
9729
|
+
import { v4 as uuid11 } from "uuid";
|
|
8692
9730
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
8693
9731
|
var safeJson2 = (s) => {
|
|
8694
9732
|
if (!s) return null;
|
|
@@ -8806,7 +9844,7 @@ async function buildRunBundle(nestId, agentNode) {
|
|
|
8806
9844
|
definitions
|
|
8807
9845
|
};
|
|
8808
9846
|
}
|
|
8809
|
-
var runTriggerRoutes = new
|
|
9847
|
+
var runTriggerRoutes = new Hono15();
|
|
8810
9848
|
async function resolveParentRun2(nestId, parentRunId, childAgent) {
|
|
8811
9849
|
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
8812
9850
|
throw new ValidationError(
|
|
@@ -8885,7 +9923,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
8885
9923
|
}
|
|
8886
9924
|
const bundle = await buildRunBundle(nestId, agentNode);
|
|
8887
9925
|
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
8888
|
-
const id = `run_${
|
|
9926
|
+
const id = `run_${uuid11()}`;
|
|
8889
9927
|
await getDb().run(
|
|
8890
9928
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
8891
9929
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
@@ -8914,7 +9952,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
8914
9952
|
201
|
|
8915
9953
|
);
|
|
8916
9954
|
});
|
|
8917
|
-
var runListRoutes = new
|
|
9955
|
+
var runListRoutes = new Hono15();
|
|
8918
9956
|
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
8919
9957
|
const nestId = c.req.param("nestId");
|
|
8920
9958
|
const db = getDb();
|
|
@@ -8946,7 +9984,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
8946
9984
|
}))
|
|
8947
9985
|
});
|
|
8948
9986
|
});
|
|
8949
|
-
var runDetailRoutes = new
|
|
9987
|
+
var runDetailRoutes = new Hono15();
|
|
8950
9988
|
async function loadRunWithPermission(c, runId, required) {
|
|
8951
9989
|
const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
|
|
8952
9990
|
runId
|
|
@@ -9097,8 +10135,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
9097
10135
|
});
|
|
9098
10136
|
|
|
9099
10137
|
// src/workflow/schedule-routes.ts
|
|
9100
|
-
import { Hono as
|
|
9101
|
-
import { v4 as
|
|
10138
|
+
import { Hono as Hono16 } from "hono";
|
|
10139
|
+
import { v4 as uuid12 } from "uuid";
|
|
9102
10140
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
9103
10141
|
var MIN_EVERY = 5;
|
|
9104
10142
|
var MAX_EVERY = 7 * 24 * 60;
|
|
@@ -9135,7 +10173,7 @@ function parseEvery(v) {
|
|
|
9135
10173
|
}
|
|
9136
10174
|
return n;
|
|
9137
10175
|
}
|
|
9138
|
-
var scheduleRoutes = new
|
|
10176
|
+
var scheduleRoutes = new Hono16();
|
|
9139
10177
|
scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
9140
10178
|
const nestId = c.req.param("nestId");
|
|
9141
10179
|
const rows = await getDb().all(
|
|
@@ -9176,7 +10214,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
9176
10214
|
);
|
|
9177
10215
|
}
|
|
9178
10216
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9179
|
-
const id = `sch_${
|
|
10217
|
+
const id = `sch_${uuid12()}`;
|
|
9180
10218
|
await db.run(
|
|
9181
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)
|
|
9182
10220
|
VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
|
|
@@ -9231,8 +10269,8 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
9231
10269
|
});
|
|
9232
10270
|
|
|
9233
10271
|
// src/notify/connector-routes.ts
|
|
9234
|
-
import { Hono as
|
|
9235
|
-
import { v4 as
|
|
10272
|
+
import { Hono as Hono17 } from "hono";
|
|
10273
|
+
import { v4 as uuid13 } from "uuid";
|
|
9236
10274
|
var KNOWN_EVENTS = [
|
|
9237
10275
|
"review_requested",
|
|
9238
10276
|
"review_approved",
|
|
@@ -9278,7 +10316,7 @@ function parseUrl(v) {
|
|
|
9278
10316
|
'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
|
|
9279
10317
|
);
|
|
9280
10318
|
}
|
|
9281
|
-
var connectorRoutes = new
|
|
10319
|
+
var connectorRoutes = new Hono17();
|
|
9282
10320
|
connectorRoutes.get("/", async (c) => {
|
|
9283
10321
|
const nestId = c.req.param("nestId");
|
|
9284
10322
|
const rows = await getDb().all(
|
|
@@ -9297,7 +10335,7 @@ connectorRoutes.post("/", async (c) => {
|
|
|
9297
10335
|
const url = parseUrl(body.url);
|
|
9298
10336
|
const events = parseEvents(body.events);
|
|
9299
10337
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9300
|
-
const id = `con_${
|
|
10338
|
+
const id = `con_${uuid13()}`;
|
|
9301
10339
|
await getDb().run(
|
|
9302
10340
|
`INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
|
|
9303
10341
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
@@ -9340,7 +10378,7 @@ connectorRoutes.delete("/:id", async (c) => {
|
|
|
9340
10378
|
});
|
|
9341
10379
|
|
|
9342
10380
|
// src/workflow/hook-routes.ts
|
|
9343
|
-
import { Hono as
|
|
10381
|
+
import { Hono as Hono18 } from "hono";
|
|
9344
10382
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
|
|
9345
10383
|
var PRESETS = ["slack", "teams", "webhook"];
|
|
9346
10384
|
var RUNNABLE_TYPES4 = ["agent", "skill"];
|
|
@@ -9363,7 +10401,7 @@ function toResponse4(c, r, opts = {}) {
|
|
|
9363
10401
|
created_at: r.created_at
|
|
9364
10402
|
};
|
|
9365
10403
|
}
|
|
9366
|
-
var hookNestRoutes = new
|
|
10404
|
+
var hookNestRoutes = new Hono18();
|
|
9367
10405
|
hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
9368
10406
|
const nestId = c.req.param("nestId");
|
|
9369
10407
|
const agent = c.req.query("agent");
|
|
@@ -9437,7 +10475,7 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
|
|
|
9437
10475
|
function stripMentions(text) {
|
|
9438
10476
|
return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
|
|
9439
10477
|
}
|
|
9440
|
-
var hookFireRoutes = new
|
|
10478
|
+
var hookFireRoutes = new Hono18();
|
|
9441
10479
|
hookFireRoutes.post("/:token", async (c) => {
|
|
9442
10480
|
const token = c.req.param("token");
|
|
9443
10481
|
if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
|
|
@@ -9537,9 +10575,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
9537
10575
|
});
|
|
9538
10576
|
|
|
9539
10577
|
// src/definitions/routes.ts
|
|
9540
|
-
import { Hono as
|
|
9541
|
-
import { v4 as
|
|
9542
|
-
var definitionRoutes = new
|
|
10578
|
+
import { Hono as Hono19 } from "hono";
|
|
10579
|
+
import { v4 as uuid14 } from "uuid";
|
|
10580
|
+
var definitionRoutes = new Hono19();
|
|
9543
10581
|
definitionRoutes.get("/", async (c) => {
|
|
9544
10582
|
const nestId = c.req.param("nestId");
|
|
9545
10583
|
const db = getDb();
|
|
@@ -9618,7 +10656,7 @@ definitionRoutes.post("/", async (c) => {
|
|
|
9618
10656
|
await applyUpdate(tx, existing.id);
|
|
9619
10657
|
targetId = existing.id;
|
|
9620
10658
|
} else {
|
|
9621
|
-
targetId =
|
|
10659
|
+
targetId = uuid14();
|
|
9622
10660
|
wasCreated = true;
|
|
9623
10661
|
await tx.run(
|
|
9624
10662
|
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
@@ -9647,11 +10685,11 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
9647
10685
|
});
|
|
9648
10686
|
|
|
9649
10687
|
// src/nests/asset-routes.ts
|
|
9650
|
-
import { Hono as
|
|
9651
|
-
import { v4 as
|
|
10688
|
+
import { Hono as Hono20 } from "hono";
|
|
10689
|
+
import { v4 as uuid15 } from "uuid";
|
|
9652
10690
|
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
9653
10691
|
import { join as join4 } from "path";
|
|
9654
|
-
var assetRoutes = new
|
|
10692
|
+
var assetRoutes = new Hono20();
|
|
9655
10693
|
var CONTENT_TYPES = {
|
|
9656
10694
|
png: "image/png",
|
|
9657
10695
|
jpg: "image/jpeg",
|
|
@@ -9697,7 +10735,7 @@ assetRoutes.post("/", async (c) => {
|
|
|
9697
10735
|
`${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
|
|
9698
10736
|
);
|
|
9699
10737
|
}
|
|
9700
|
-
const name = `${
|
|
10738
|
+
const name = `${uuid15()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
9701
10739
|
const dir = join4(resolveNestPath(nestId), "assets");
|
|
9702
10740
|
await mkdir2(dir, { recursive: true });
|
|
9703
10741
|
await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
@@ -9738,7 +10776,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
9738
10776
|
});
|
|
9739
10777
|
|
|
9740
10778
|
// src/mcp/server-routes.ts
|
|
9741
|
-
import { Hono as
|
|
10779
|
+
import { Hono as Hono21 } from "hono";
|
|
9742
10780
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9743
10781
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
9744
10782
|
import { z as z2 } from "zod";
|
|
@@ -10037,7 +11075,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
10037
11075
|
}
|
|
10038
11076
|
return server;
|
|
10039
11077
|
}
|
|
10040
|
-
var serverMcpRoutes = new
|
|
11078
|
+
var serverMcpRoutes = new Hono21();
|
|
10041
11079
|
serverMcpRoutes.all("/", async (c) => {
|
|
10042
11080
|
const userId = c.get("userId");
|
|
10043
11081
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -10059,7 +11097,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
10059
11097
|
await server.server.close();
|
|
10060
11098
|
}
|
|
10061
11099
|
});
|
|
10062
|
-
var nestIndexRoutes = new
|
|
11100
|
+
var nestIndexRoutes = new Hono21();
|
|
10063
11101
|
nestIndexRoutes.get("/", async (c) => {
|
|
10064
11102
|
const userId = c.get("userId");
|
|
10065
11103
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -10178,14 +11216,14 @@ npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
|
|
|
10178
11216
|
through steward review before they become AI-visible.
|
|
10179
11217
|
`;
|
|
10180
11218
|
}
|
|
10181
|
-
var llmsTxtRoutes = new
|
|
11219
|
+
var llmsTxtRoutes = new Hono21();
|
|
10182
11220
|
llmsTxtRoutes.get("/", (c) => {
|
|
10183
11221
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
10184
11222
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
10185
11223
|
});
|
|
10186
11224
|
|
|
10187
11225
|
// src/governance/routes.ts
|
|
10188
|
-
import { Hono as
|
|
11226
|
+
import { Hono as Hono22 } from "hono";
|
|
10189
11227
|
|
|
10190
11228
|
// src/governance/feed-service.ts
|
|
10191
11229
|
var SNIPPET_CHARS = 200;
|
|
@@ -10316,7 +11354,7 @@ async function getNestFeed(params) {
|
|
|
10316
11354
|
}
|
|
10317
11355
|
|
|
10318
11356
|
// src/governance/comment-service.ts
|
|
10319
|
-
import { v4 as
|
|
11357
|
+
import { v4 as uuid16 } from "uuid";
|
|
10320
11358
|
async function createComment(params) {
|
|
10321
11359
|
const db = getDb();
|
|
10322
11360
|
const body = (params.body ?? "").trim();
|
|
@@ -10332,7 +11370,7 @@ async function createComment(params) {
|
|
|
10332
11370
|
throw new Error("Parent comment not found on this node");
|
|
10333
11371
|
}
|
|
10334
11372
|
}
|
|
10335
|
-
const id =
|
|
11373
|
+
const id = uuid16();
|
|
10336
11374
|
await db.run(
|
|
10337
11375
|
`INSERT INTO comments
|
|
10338
11376
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -10617,7 +11655,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
10617
11655
|
if (Number.isNaN(n) || n < 1) return def;
|
|
10618
11656
|
return Math.min(n, max);
|
|
10619
11657
|
}
|
|
10620
|
-
var governanceRoutes = new
|
|
11658
|
+
var governanceRoutes = new Hono22();
|
|
10621
11659
|
governanceRoutes.get("/stewards", async (c) => {
|
|
10622
11660
|
const nestId = c.req.param("nestId");
|
|
10623
11661
|
const scope = c.req.query("scope");
|
|
@@ -10736,6 +11774,60 @@ governanceRoutes.get("/review-queue", async (c) => {
|
|
|
10736
11774
|
}
|
|
10737
11775
|
return c.json({ ...result, requests });
|
|
10738
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
|
+
});
|
|
10739
11831
|
function denyUnlessNestManager(c, what) {
|
|
10740
11832
|
const permission = c.get("nestPermission");
|
|
10741
11833
|
if (permission === "owner" || permission === "admin") return null;
|
|
@@ -10928,7 +12020,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
10928
12020
|
const activity = await getActivity({ nestId, limit });
|
|
10929
12021
|
return c.json({ activity });
|
|
10930
12022
|
});
|
|
10931
|
-
var governanceNodeRoutes = new
|
|
12023
|
+
var governanceNodeRoutes = new Hono22();
|
|
10932
12024
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
10933
12025
|
const nestId = c.req.param("nestId");
|
|
10934
12026
|
const nodeId = c.req.param("nodeId");
|
|
@@ -11326,12 +12418,16 @@ governanceNodeRoutes.post("/:nodeId{.+}/cancel-review", async (c) => {
|
|
|
11326
12418
|
const nestId = c.req.param("nestId");
|
|
11327
12419
|
const nodeId = c.req.param("nodeId");
|
|
11328
12420
|
const userEmail = await getUserEmail4(c);
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
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
|
+
}
|
|
11335
12431
|
});
|
|
11336
12432
|
async function getUserEmail4(c) {
|
|
11337
12433
|
const userId = c.get("userId");
|
|
@@ -11419,13 +12515,23 @@ var flexAuthWithPromptowlTicket = createMiddleware2(
|
|
|
11419
12515
|
async (c, next) => {
|
|
11420
12516
|
const header = c.req.header("Authorization") || "";
|
|
11421
12517
|
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
11422
|
-
const
|
|
12518
|
+
const secrets = [
|
|
12519
|
+
config.OFFICIAL_COMMUNITY_SSO_SECRET,
|
|
12520
|
+
config.MCP_SIGNING_SECRET
|
|
12521
|
+
].filter(Boolean);
|
|
11423
12522
|
const looksLikeJwt = !!token && !token.startsWith("cnst_") && token.split(".").length === 3;
|
|
11424
|
-
if (!looksLikeJwt ||
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
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) {
|
|
11429
12535
|
return c.json({ error: "Invalid service ticket" }, 401);
|
|
11430
12536
|
}
|
|
11431
12537
|
if (claims.scope !== "mcp" || claims.jti) {
|
|
@@ -11472,7 +12578,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
11472
12578
|
return ok;
|
|
11473
12579
|
}
|
|
11474
12580
|
function createApp() {
|
|
11475
|
-
const app = new
|
|
12581
|
+
const app = new Hono23({ router: new LinearRouter() });
|
|
11476
12582
|
const corsOrigins = config.CORS_ORIGINS;
|
|
11477
12583
|
app.use(
|
|
11478
12584
|
"*",
|
|
@@ -11529,6 +12635,9 @@ function createApp() {
|
|
|
11529
12635
|
// Presence only, never the value: lets the UI stop prompting for a
|
|
11530
12636
|
// runner key once the server-wide one exists.
|
|
11531
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(),
|
|
11532
12641
|
promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
|
|
11533
12642
|
// Manual (email/password) sign-in mode (open | invite-only | disabled).
|
|
11534
12643
|
// The login page reads this to show/hide the credential form, and to
|
|
@@ -11672,6 +12781,16 @@ function createApp() {
|
|
|
11672
12781
|
oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
|
|
11673
12782
|
oidc_auto_provision: config.OIDC_AUTO_PROVISION,
|
|
11674
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,
|
|
11675
12794
|
// Live readiness (enabled AND issuer+id+secret present) — the SAVED state,
|
|
11676
12795
|
// i.e. whether the login button is currently live. The Settings UI derives
|
|
11677
12796
|
// its own "enabled but not configured" warning from the unsaved draft
|
|
@@ -11872,6 +12991,67 @@ function createApp() {
|
|
|
11872
12991
|
value: body.oidc_department_tagging ? "true" : null
|
|
11873
12992
|
});
|
|
11874
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
|
+
}
|
|
11875
13055
|
const effOidcIssuer = "oidc_issuer" in body ? String(body.oidc_issuer ?? "").trim() : config.OIDC_ISSUER;
|
|
11876
13056
|
const effOidcClientId = "oidc_client_id" in body ? String(body.oidc_client_id ?? "").trim() : config.OIDC_CLIENT_ID;
|
|
11877
13057
|
const effOidcSecret = "oidc_client_secret" in body ? String(body.oidc_client_secret ?? "").trim() : config.OIDC_CLIENT_SECRET;
|
|
@@ -12301,11 +13481,12 @@ function createApp() {
|
|
|
12301
13481
|
app.route("/runs", runDetailRoutes);
|
|
12302
13482
|
app.route("/hooks", hookFireRoutes);
|
|
12303
13483
|
app.route("/llms.txt", llmsTxtRoutes);
|
|
13484
|
+
app.route("/p", publishRenderRoutes);
|
|
12304
13485
|
app.use("/index", flexAuthWithPromptowlTicket);
|
|
12305
13486
|
app.route("/index", nestIndexRoutes);
|
|
12306
13487
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
12307
13488
|
app.route("/mcp", serverMcpRoutes);
|
|
12308
|
-
const nestsApp = new
|
|
13489
|
+
const nestsApp = new Hono23();
|
|
12309
13490
|
nestsApp.use("*", async (c, next) => {
|
|
12310
13491
|
const parts = nestPathParts(c.req.path);
|
|
12311
13492
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -12396,7 +13577,7 @@ function createApp() {
|
|
|
12396
13577
|
}
|
|
12397
13578
|
let required = "read";
|
|
12398
13579
|
const path = c.req.path;
|
|
12399
|
-
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);
|
|
12400
13581
|
const isAnnotationAction = /\/annotations$/.test(path) || /\/annotations\/[^/]+\/(comments|resolve|reopen)$/.test(path);
|
|
12401
13582
|
const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
|
|
12402
13583
|
const isDeletionRequestAction = /\/request-deletion$/.test(path) || /\/deletion-requests\/[^/]+\/withdraw$/.test(path);
|
|
@@ -12410,7 +13591,7 @@ function createApp() {
|
|
|
12410
13591
|
);
|
|
12411
13592
|
}
|
|
12412
13593
|
const resource = parts[1];
|
|
12413
|
-
if (resource === "visibility") {
|
|
13594
|
+
if (resource === "visibility" || resource === "reader-mode") {
|
|
12414
13595
|
required = "admin";
|
|
12415
13596
|
} else if (resource === "collaborators") {
|
|
12416
13597
|
required = c.req.method === "GET" || c.req.method === "POST" ? "write" : "admin";
|
|
@@ -12498,6 +13679,7 @@ function createApp() {
|
|
|
12498
13679
|
nestsApp.route("/:nestId", queryRoutes);
|
|
12499
13680
|
nestsApp.route("/:nestId", sharingRoutes);
|
|
12500
13681
|
nestsApp.route("/:nestId/grants", grantRoutes);
|
|
13682
|
+
nestsApp.route("/:nestId/publications", publishRoutes);
|
|
12501
13683
|
nestsApp.route("/:nestId/teams", nestTeamRoutes);
|
|
12502
13684
|
nestsApp.route("/:nestId/edge-types", edgeTypeRoutes);
|
|
12503
13685
|
nestsApp.route("/:nestId/edges", edgeRoutes);
|
|
@@ -12534,6 +13716,12 @@ function createApp() {
|
|
|
12534
13716
|
);
|
|
12535
13717
|
app.notFound((c) => c.json({ error: "Not found" }, 404));
|
|
12536
13718
|
app.onError((err, c) => {
|
|
13719
|
+
if (isVaultLockTimeout(err) || err instanceof VaultBusyError) {
|
|
13720
|
+
const busy = err instanceof VaultBusyError ? err : new VaultBusyError();
|
|
13721
|
+
return c.json({ error: busy.message }, 503, {
|
|
13722
|
+
"Retry-After": String(busy.retryAfterSeconds)
|
|
13723
|
+
});
|
|
13724
|
+
}
|
|
12537
13725
|
if (err instanceof AppError) {
|
|
12538
13726
|
return c.json({ error: err.message }, err.statusCode);
|
|
12539
13727
|
}
|
|
@@ -12812,7 +14000,7 @@ async function repairGraftedVersionChains(db) {
|
|
|
12812
14000
|
}
|
|
12813
14001
|
|
|
12814
14002
|
// src/workflow/scheduler.ts
|
|
12815
|
-
import { v4 as
|
|
14003
|
+
import { v4 as uuid17 } from "uuid";
|
|
12816
14004
|
function isDue(s, now) {
|
|
12817
14005
|
if (!s.last_run_at) return true;
|
|
12818
14006
|
const last = Date.parse(s.last_run_at);
|
|
@@ -12836,21 +14024,21 @@ async function schedulerTick(now = Date.now()) {
|
|
|
12836
14024
|
);
|
|
12837
14025
|
continue;
|
|
12838
14026
|
}
|
|
12839
|
-
const id = `run_${
|
|
12840
|
-
const
|
|
14027
|
+
const id = `run_${uuid17()}`;
|
|
14028
|
+
const nowIso2 = new Date(now).toISOString();
|
|
12841
14029
|
await db.run(
|
|
12842
14030
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
12843
14031
|
VALUES (?, ?, ?, ?, 'running', NULL, ?, NULL, 0)`,
|
|
12844
14032
|
// triggered_by names the schedule's creator with a schedule: prefix —
|
|
12845
14033
|
// honest provenance ("this fired on X's schedule, not X clicking").
|
|
12846
|
-
[id, s.nest_id, s.agent_node, `schedule:${s.created_by}`,
|
|
14034
|
+
[id, s.nest_id, s.agent_node, `schedule:${s.created_by}`, nowIso2]
|
|
12847
14035
|
);
|
|
12848
14036
|
const fired = s.runs_created + 1;
|
|
12849
14037
|
const done = s.max_runs !== null && fired >= s.max_runs;
|
|
12850
14038
|
await db.run(
|
|
12851
14039
|
`UPDATE schedules SET last_run_at = ?, runs_created = ?, enabled = ?, updated_at = ?
|
|
12852
14040
|
WHERE id = ?`,
|
|
12853
|
-
[
|
|
14041
|
+
[nowIso2, fired, done ? 0 : s.enabled, nowIso2, s.id]
|
|
12854
14042
|
);
|
|
12855
14043
|
created.push(id);
|
|
12856
14044
|
} catch (err) {
|