@promptowl/contextnest-community 1.10.0 → 1.11.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/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  seedDefaultEdgeTypes,
29
29
  submitForReview,
30
30
  verifySmtp
31
- } from "./chunk-ZM4F7MY7.js";
31
+ } from "./chunk-XB7T3THE.js";
32
32
  import {
33
33
  createGrant,
34
34
  deleteGrant,
@@ -37,7 +37,7 @@ import {
37
37
  listGrants,
38
38
  listUserGrants,
39
39
  resolveNodeGrant
40
- } from "./chunk-7PQREKSM.js";
40
+ } from "./chunk-ZMSU7BCC.js";
41
41
  import {
42
42
  generateApiKey,
43
43
  getKeyPrefix,
@@ -46,9 +46,6 @@ import {
46
46
  parseBearerToken,
47
47
  verifyPassword
48
48
  } from "./chunk-XRK6SQSC.js";
49
- import {
50
- normalizeEmail
51
- } from "./chunk-FRQJWGN3.js";
52
49
  import {
53
50
  checkConflict,
54
51
  createVersion,
@@ -57,8 +54,10 @@ import {
57
54
  getDisplayStatus,
58
55
  getVersions,
59
56
  setApprovedVersion
60
- } from "./chunk-UHUU3VAK.js";
57
+ } from "./chunk-JHIU6RZU.js";
61
58
  import {
59
+ addMember,
60
+ buildTitleMap,
62
61
  canCreateInNest,
63
62
  canManageStewards,
64
63
  canUserAccess,
@@ -67,10 +66,14 @@ import {
67
66
  collabPermToRole,
68
67
  createNest,
69
68
  createStewardRecord,
69
+ createTeam,
70
70
  deleteNest,
71
+ deleteTeam,
71
72
  disableStewardshipAndWipeGovernance,
72
73
  docLink,
73
74
  engineCache,
75
+ findTeamByExternalId,
76
+ getAccessConfig,
74
77
  getCollaboratorRole,
75
78
  getCurrentLicense,
76
79
  getNest,
@@ -78,50 +81,68 @@ import {
78
81
  getStewardsForNest,
79
82
  getStewardsForUser,
80
83
  getSuspensionReason,
84
+ getTeam,
81
85
  importNest,
82
86
  installLicenseKey,
87
+ isConfigSuperAdmin,
83
88
  isLicenseAdminEmail,
84
89
  isLicenseAdminUserId,
85
90
  isPublicReader,
91
+ isServerAdminUserId,
86
92
  isStewardshipEnabled,
87
93
  isSuperAdmin,
88
94
  isSuspended,
95
+ listNestTeams,
89
96
  listNests,
90
97
  listPublicNests,
91
98
  listSharedNests,
92
99
  listStewards,
100
+ listTeamsForUser,
93
101
  loadAccessConfig,
102
+ loadGrantedSuperAdmins,
94
103
  loadServerSettings,
95
104
  nestAllowsSelfApprove,
96
105
  nestName,
97
106
  nestStorageRoot,
98
107
  permissionLevel,
99
108
  persistSetting,
109
+ removeMember,
100
110
  removeSteward,
101
111
  renameNest,
112
+ renameTeam,
113
+ replaceTeamRoster,
114
+ resolveMemberIdentity,
102
115
  resolveNestPath,
103
116
  resolveNestPermission,
104
117
  resolveStewardsForNode,
105
118
  resolveStewardsWithFallback,
119
+ resolveTeamRolesForUser,
106
120
  resolveUserRoles,
107
121
  setAllowSelfApprove,
108
122
  setStewardshipEnabled,
123
+ shareTeamWithNest,
109
124
  startLicenseSafetyPoll,
110
125
  startTelemetryLoop,
111
126
  syncFromConfig,
112
127
  trackEvent,
113
128
  uniqueNestName,
129
+ unshareTeamFromNest,
130
+ updateMemberRole,
114
131
  updateSteward,
115
132
  validateLicense
116
- } from "./chunk-KFGZIECB.js";
133
+ } from "./chunk-F7EKYNXG.js";
117
134
  import {
118
135
  AppError,
119
136
  ConflictError,
120
137
  ForbiddenError,
121
138
  LockedError,
122
139
  NotFoundError,
140
+ UnauthorizedError,
123
141
  ValidationError
124
- } from "./chunk-3JTODC3Y.js";
142
+ } from "./chunk-GUNJTORH.js";
143
+ import {
144
+ normalizeEmail
145
+ } from "./chunk-FRQJWGN3.js";
125
146
  import {
126
147
  insertOrIgnore,
127
148
  nowExpr
@@ -132,7 +153,7 @@ import {
132
153
  initDb,
133
154
  isEmailListish,
134
155
  isEmailish
135
- } from "./chunk-BC6KFUZH.js";
156
+ } from "./chunk-3M7677XW.js";
136
157
  import {
137
158
  ANON_EMAIL,
138
159
  ANON_USER_ID
@@ -142,14 +163,14 @@ import {
142
163
  import { serve } from "@hono/node-server";
143
164
 
144
165
  // src/app.ts
145
- import { Hono as Hono18 } from "hono";
166
+ import { Hono as Hono21 } from "hono";
146
167
  import { LinearRouter } from "hono/router/linear-router";
147
168
  import { createMiddleware as createMiddleware2 } from "hono/factory";
148
169
  import { cors } from "hono/cors";
149
170
 
150
171
  // src/auth/routes.ts
151
- import { Hono } from "hono";
152
- import { v4 as uuid } from "uuid";
172
+ import { Hono as Hono2 } from "hono";
173
+ import { v4 as uuid2 } from "uuid";
153
174
 
154
175
  // src/auth/middleware.ts
155
176
  import { createMiddleware } from "hono/factory";
@@ -227,6 +248,53 @@ function buildSessionCookie(id, opts = {}) {
227
248
  function buildClearSessionCookie(opts = {}) {
228
249
  return buildSessionCookie("", { secure: opts.secure, maxAgeSeconds: 0 });
229
250
  }
251
+ var OIDC_SESSION_MARKER_COOKIE = "cnst_sso_session";
252
+ var PROMPTOWL_TOKEN_COOKIE = "cnst_po_token";
253
+ var PROMPTOWL_TOKEN_COOKIE_PATH = "/teams";
254
+ function buildPromptowlTokenCookie(token, opts = {}) {
255
+ const parts = [
256
+ `${PROMPTOWL_TOKEN_COOKIE}=${opts.clear ? "" : encodeURIComponent(token)}`,
257
+ "HttpOnly",
258
+ `Path=${PROMPTOWL_TOKEN_COOKIE_PATH}`,
259
+ "SameSite=Lax",
260
+ `Max-Age=${opts.clear ? 0 : SESSION_TTL_SECS}`
261
+ ];
262
+ if (opts.secure) parts.push("Secure");
263
+ return parts.join("; ");
264
+ }
265
+ var PROMPTOWL_MARKER_COOKIE = "cnst_po_session";
266
+ function buildPromptowlMarkerCookie(opts = {}) {
267
+ const parts = [
268
+ `${PROMPTOWL_MARKER_COOKIE}=${opts.clear ? "" : "1"}`,
269
+ "Path=/",
270
+ "SameSite=Lax",
271
+ `Max-Age=${opts.clear ? 0 : SESSION_TTL_SECS}`
272
+ ];
273
+ if (opts.secure) parts.push("Secure");
274
+ return parts.join("; ");
275
+ }
276
+ function getPromptowlTokenFromRequest(c) {
277
+ const header = c.req.header("Cookie");
278
+ if (!header) return null;
279
+ for (const part of header.split(";")) {
280
+ const [k, ...rest] = part.trim().split("=");
281
+ if (k === PROMPTOWL_TOKEN_COOKIE) {
282
+ const v = decodeURIComponent(rest.join("="));
283
+ return /^po_device_[A-Za-z0-9_-]+$/.test(v) ? v : null;
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+ function buildOidcMarkerCookie(opts = {}) {
289
+ const parts = [
290
+ `${OIDC_SESSION_MARKER_COOKIE}=${opts.clear ? "" : "1"}`,
291
+ "Path=/",
292
+ "SameSite=Lax",
293
+ `Max-Age=${opts.clear ? 0 : SESSION_TTL_SECS}`
294
+ ];
295
+ if (opts.secure) parts.push("Secure");
296
+ return parts.join("; ");
297
+ }
230
298
  function isSecureRequest(c) {
231
299
  const proto = c.req.header("x-forwarded-proto");
232
300
  if (proto) return proto.split(",")[0].trim() === "https";
@@ -357,11 +425,349 @@ function verifySsoTicket(token, secret, nowSec = Math.floor(Date.now() / 1e3)) {
357
425
  return claims;
358
426
  }
359
427
 
360
- // src/auth/routes.ts
428
+ // src/auth/oidc.ts
429
+ import { Hono } from "hono";
430
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
431
+ import { v4 as uuid } from "uuid";
432
+ import { createRemoteJWKSet, jwtVerify } from "jose";
433
+
434
+ // src/shared/base-url.ts
435
+ function requestBaseUrl(reqUrl) {
436
+ if (config.PUBLIC_BASE_URL) return config.PUBLIC_BASE_URL;
437
+ try {
438
+ return new URL(reqUrl).origin;
439
+ } catch {
440
+ return "";
441
+ }
442
+ }
443
+
444
+ // src/shared/request-ip.ts
361
445
  import { getConnInfo } from "@hono/node-server/conninfo";
446
+ function clientIp(c) {
447
+ const xff = c.req.header("x-forwarded-for");
448
+ if (xff) return xff.split(",")[0].trim();
449
+ const realIp = c.req.header("x-real-ip");
450
+ if (realIp) return realIp.trim();
451
+ try {
452
+ const addr = getConnInfo(c).remote.address;
453
+ if (addr) return addr;
454
+ } catch {
455
+ }
456
+ return "unknown";
457
+ }
458
+
459
+ // src/auth/oidc.ts
460
+ var OIDC_LIMIT = { max: 100, windowMs: 15 * 6e4 };
461
+ var IDP_FETCH_TIMEOUT_MS = 1e4;
462
+ var STATE_COOKIE = "cnst_oidc_state";
463
+ var NONCE_COOKIE = "cnst_oidc_nonce";
464
+ var VERIFIER_COOKIE = "cnst_oidc_verifier";
465
+ var FLOW_COOKIE_TTL_SECS = 10 * 60;
466
+ var DISCOVERY_TTL_MS = 5 * 6e4;
467
+ var discoveryCache = null;
468
+ function isOidcReady() {
469
+ return config.OIDC_ENABLED && !!config.OIDC_ISSUER && !!config.OIDC_CLIENT_ID && !!config.OIDC_CLIENT_SECRET;
470
+ }
471
+ async function discover(issuer) {
472
+ const now = Date.now();
473
+ if (discoveryCache && discoveryCache.issuer === issuer && now - discoveryCache.fetchedAt < DISCOVERY_TTL_MS) {
474
+ return { doc: discoveryCache.doc, jwks: discoveryCache.jwks };
475
+ }
476
+ const res = await fetch(`${issuer}/.well-known/openid-configuration`, {
477
+ headers: { Accept: "application/json" },
478
+ signal: AbortSignal.timeout(IDP_FETCH_TIMEOUT_MS)
479
+ });
480
+ if (!res.ok) {
481
+ throw new Error(`discovery fetch failed with status ${res.status}`);
482
+ }
483
+ const doc = await res.json();
484
+ if (!doc.issuer || !doc.authorization_endpoint || !doc.token_endpoint || !doc.jwks_uri) {
485
+ throw new Error("discovery document is missing required fields");
486
+ }
487
+ if (doc.issuer.replace(/\/$/, "") !== issuer.replace(/\/$/, "")) {
488
+ throw new Error(
489
+ `discovery issuer mismatch: expected ${issuer}, got ${doc.issuer}`
490
+ );
491
+ }
492
+ const jwks = createRemoteJWKSet(new URL(doc.jwks_uri));
493
+ discoveryCache = {
494
+ issuer,
495
+ doc,
496
+ jwks,
497
+ fetchedAt: now
498
+ };
499
+ return { doc, jwks };
500
+ }
501
+ function base64url(buf) {
502
+ return buf.toString("base64url");
503
+ }
504
+ function pkceChallenge(verifier) {
505
+ return createHash("sha256").update(verifier).digest("base64url");
506
+ }
507
+ var DEPARTMENT_MAX_LENGTH = 120;
508
+ function normalizeDepartment(claim) {
509
+ if (typeof claim !== "string") return null;
510
+ const collapsed = claim.replace(/\s+/g, " ").trim();
511
+ if (!collapsed) return null;
512
+ return collapsed.slice(0, DEPARTMENT_MAX_LENGTH).trim();
513
+ }
514
+ function buildFlowCookie(name, value, opts) {
515
+ const parts = [
516
+ `${name}=${value}`,
517
+ "HttpOnly",
518
+ "Path=/auth/oidc",
519
+ "SameSite=Lax",
520
+ `Max-Age=${opts.clear ? 0 : FLOW_COOKIE_TTL_SECS}`
521
+ ];
522
+ if (opts.secure) parts.push("Secure");
523
+ return parts.join("; ");
524
+ }
525
+ function readCookie(c, name) {
526
+ const header = c.req.header("Cookie");
527
+ if (!header) return null;
528
+ for (const part of header.split(";")) {
529
+ const [k, ...rest] = part.trim().split("=");
530
+ if (k === name) {
531
+ const v = rest.join("=").trim();
532
+ return /^[A-Za-z0-9_-]{16,256}$/.test(v) ? v : null;
533
+ }
534
+ }
535
+ return null;
536
+ }
537
+ function oidcRedirectUri(reqUrl) {
538
+ return `${requestBaseUrl(reqUrl)}/auth/oidc/callback`;
539
+ }
540
+ var oidcRoutes = new Hono();
541
+ oidcRoutes.get("/login", async (c) => {
542
+ const bounce = (code) => c.redirect(`/?sso_error=${code}`, 302);
543
+ if (!config.OIDC_ENABLED) return bounce("disabled");
544
+ if (!isOidcReady()) return bounce("not_configured");
545
+ if (!tryConsume(`oidc-login:ip:${clientIp(c)}`, OIDC_LIMIT)) {
546
+ return bounce("rate_limited");
547
+ }
548
+ let doc;
549
+ try {
550
+ ({ doc } = await discover(config.OIDC_ISSUER));
551
+ } catch (err) {
552
+ console.error("[oidc] discovery failed:", err);
553
+ return bounce("discovery_failed");
554
+ }
555
+ const state = base64url(randomBytes2(24));
556
+ const nonce = base64url(randomBytes2(24));
557
+ const verifier = base64url(randomBytes2(32));
558
+ const secure = isSecureRequest(c);
559
+ c.header("Set-Cookie", buildFlowCookie(STATE_COOKIE, state, { secure }), {
560
+ append: true
561
+ });
562
+ c.header("Set-Cookie", buildFlowCookie(NONCE_COOKIE, nonce, { secure }), {
563
+ append: true
564
+ });
565
+ c.header(
566
+ "Set-Cookie",
567
+ buildFlowCookie(VERIFIER_COOKIE, verifier, { secure }),
568
+ { append: true }
569
+ );
570
+ const authorizeUrl = new URL(doc.authorization_endpoint);
571
+ authorizeUrl.searchParams.set("client_id", config.OIDC_CLIENT_ID);
572
+ authorizeUrl.searchParams.set("redirect_uri", oidcRedirectUri(c.req.url));
573
+ authorizeUrl.searchParams.set("response_type", "code");
574
+ authorizeUrl.searchParams.set("scope", "openid email profile");
575
+ authorizeUrl.searchParams.set("state", state);
576
+ authorizeUrl.searchParams.set("nonce", nonce);
577
+ authorizeUrl.searchParams.set("code_challenge", pkceChallenge(verifier));
578
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
579
+ return c.redirect(authorizeUrl.toString(), 302);
580
+ });
581
+ oidcRoutes.get("/callback", async (c) => {
582
+ const secure = isSecureRequest(c);
583
+ const clearFlowCookies = () => {
584
+ for (const name2 of [STATE_COOKIE, NONCE_COOKIE, VERIFIER_COOKIE]) {
585
+ c.header("Set-Cookie", buildFlowCookie(name2, "", { secure, clear: true }), {
586
+ append: true
587
+ });
588
+ }
589
+ };
590
+ const bounce = (code2) => {
591
+ clearFlowCookies();
592
+ return c.redirect(`/?sso_error=${code2}`, 302);
593
+ };
594
+ if (!config.OIDC_ENABLED) return bounce("disabled");
595
+ if (!isOidcReady()) return bounce("not_configured");
596
+ if (!tryConsume(`oidc-cb:ip:${clientIp(c)}`, OIDC_LIMIT)) {
597
+ return bounce("rate_limited");
598
+ }
599
+ if (c.req.query("error")) return bounce("provider_error");
600
+ const stateParam = c.req.query("state") || "";
601
+ const stateCookie = readCookie(c, STATE_COOKIE);
602
+ if (!stateCookie || !stateParam || stateParam !== stateCookie) {
603
+ return bounce("state_mismatch");
604
+ }
605
+ const code = c.req.query("code");
606
+ if (!code) return bounce("exchange_failed");
607
+ const nonceCookie = readCookie(c, NONCE_COOKIE);
608
+ const verifierCookie = readCookie(c, VERIFIER_COOKIE);
609
+ if (!nonceCookie || !verifierCookie) return bounce("state_mismatch");
610
+ let doc;
611
+ let jwks;
612
+ try {
613
+ ({ doc, jwks } = await discover(config.OIDC_ISSUER));
614
+ } catch (err) {
615
+ console.error("[oidc] discovery failed:", err);
616
+ return bounce("discovery_failed");
617
+ }
618
+ let idToken;
619
+ try {
620
+ const tokenRes = await fetch(doc.token_endpoint, {
621
+ method: "POST",
622
+ headers: {
623
+ "Content-Type": "application/x-www-form-urlencoded",
624
+ Accept: "application/json"
625
+ },
626
+ body: new URLSearchParams({
627
+ grant_type: "authorization_code",
628
+ code,
629
+ redirect_uri: oidcRedirectUri(c.req.url),
630
+ client_id: config.OIDC_CLIENT_ID,
631
+ client_secret: config.OIDC_CLIENT_SECRET,
632
+ code_verifier: verifierCookie
633
+ }).toString(),
634
+ signal: AbortSignal.timeout(IDP_FETCH_TIMEOUT_MS)
635
+ });
636
+ if (!tokenRes.ok) {
637
+ console.error(
638
+ `[oidc] token exchange failed with status ${tokenRes.status}`
639
+ );
640
+ return bounce("exchange_failed");
641
+ }
642
+ const tokens = await tokenRes.json();
643
+ if (!tokens.id_token || typeof tokens.id_token !== "string") {
644
+ return bounce("exchange_failed");
645
+ }
646
+ idToken = tokens.id_token;
647
+ } catch (err) {
648
+ console.error("[oidc] token exchange failed:", err);
649
+ return bounce("exchange_failed");
650
+ }
651
+ let claims;
652
+ try {
653
+ const { payload } = await jwtVerify(idToken, jwks, {
654
+ issuer: doc.issuer,
655
+ audience: config.OIDC_CLIENT_ID,
656
+ // Explicit allowlist on top of the JWKS constraint. createRemoteJWKSet
657
+ // already refuses keys the issuer doesn't publish, so alg-confusion is
658
+ // blocked either way; this pins the header alg so a JWKS that ever
659
+ // starts publishing an unexpected key type can't shift us onto it.
660
+ // RS256 is what every supported preset emits (Entra v2, Google, Okta,
661
+ // Keycloak default) and is the OIDC-mandated baseline.
662
+ algorithms: ["RS256"]
663
+ });
664
+ claims = payload;
665
+ } catch (err) {
666
+ console.error("[oidc] ID token validation failed:", err);
667
+ return bounce("invalid_token");
668
+ }
669
+ if (claims.nonce !== nonceCookie) return bounce("invalid_token");
670
+ if (claims.email_verified === false || claims.email_verified === "false") {
671
+ return bounce("email_not_verified");
672
+ }
673
+ const rawEmail = typeof claims.email === "string" && claims.email || typeof claims.preferred_username === "string" && claims.preferred_username || "";
674
+ const email = normalizeEmail(rawEmail);
675
+ if (!email || !isEmailish(email)) return bounce("invalid_token");
676
+ const allowed = config.OIDC_ALLOWED_DOMAINS;
677
+ if (allowed.length > 0) {
678
+ const domain = email.slice(email.lastIndexOf("@") + 1);
679
+ if (!allowed.includes(domain)) return bounce("domain_not_allowed");
680
+ }
681
+ const name = typeof claims.name === "string" && claims.name.trim() ? claims.name.trim() : null;
682
+ const department = normalizeDepartment(claims.department);
683
+ try {
684
+ const db = getDb();
685
+ let user = await db.get(
686
+ "SELECT id, email, name FROM users WHERE LOWER(email) = ?",
687
+ [email]
688
+ );
689
+ if (!user) {
690
+ if (!config.OIDC_AUTO_PROVISION) return bounce("not_invited");
691
+ const userId = uuid();
692
+ const placeholderHash = await hashPassword(uuid());
693
+ try {
694
+ await db.run(
695
+ "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
696
+ [userId, email, name, placeholderHash]
697
+ );
698
+ user = { id: userId, email, name };
699
+ trackEvent("user.register", { userId, email, method: "oidc" });
700
+ } catch (insErr) {
701
+ const raced = await db.get(
702
+ "SELECT id, email, name FROM users WHERE LOWER(email) = ?",
703
+ [email]
704
+ );
705
+ if (!raced) throw insErr;
706
+ user = raced;
707
+ trackEvent("user.login", { userId: user.id, method: "oidc" });
708
+ }
709
+ } else {
710
+ trackEvent("user.login", { userId: user.id, method: "oidc" });
711
+ }
712
+ try {
713
+ await db.run("UPDATE users SET department = ? WHERE id = ?", [
714
+ department,
715
+ user.id
716
+ ]);
717
+ } catch (err) {
718
+ console.error("[oidc] department upsert failed (login continues):", err);
719
+ }
720
+ const sessionId = await createSession(user.id, c.req.header("User-Agent"));
721
+ clearFlowCookies();
722
+ c.header("Set-Cookie", buildSessionCookie(sessionId, { secure }), {
723
+ append: true
724
+ });
725
+ c.header("Set-Cookie", buildOidcMarkerCookie({ secure }), { append: true });
726
+ return c.redirect("/", 302);
727
+ } catch (err) {
728
+ console.error("[oidc] provisioning/session failed:", err);
729
+ return bounce("service_error");
730
+ }
731
+ });
732
+ oidcRoutes.get("/logout", async (c) => {
733
+ const secure = isSecureRequest(c);
734
+ const sessionId = getSessionIdFromRequest(c);
735
+ if (sessionId) {
736
+ try {
737
+ await deleteSession(sessionId);
738
+ } catch (err) {
739
+ console.error("[oidc] logout: failed to delete session:", err);
740
+ }
741
+ }
742
+ c.header("Set-Cookie", buildClearSessionCookie({ secure }), { append: true });
743
+ c.header("Set-Cookie", buildOidcMarkerCookie({ secure, clear: true }), {
744
+ append: true
745
+ });
746
+ const home = `${requestBaseUrl(c.req.url)}/`;
747
+ if (isOidcReady()) {
748
+ try {
749
+ const { doc } = await discover(config.OIDC_ISSUER);
750
+ if (doc.end_session_endpoint) {
751
+ const url = new URL(doc.end_session_endpoint);
752
+ url.searchParams.set("post_logout_redirect_uri", home);
753
+ url.searchParams.set("client_id", config.OIDC_CLIENT_ID);
754
+ return c.redirect(url.toString(), 302);
755
+ }
756
+ } catch (err) {
757
+ console.error("[oidc] logout: discovery failed, local-only:", err);
758
+ }
759
+ }
760
+ return c.redirect(home, 302);
761
+ });
762
+
763
+ // src/auth/routes.ts
764
+ function isServerAdminEmail(email) {
765
+ return isLicenseAdminEmail(email) || isSuperAdmin(email);
766
+ }
362
767
  var LOGIN_LIMIT = { max: 5, windowMs: 15 * 6e4 };
363
768
  var REGISTER_LIMIT = { max: 3, windowMs: 60 * 6e4 };
364
769
  var DEVICE_LIMIT = { max: 10, windowMs: 15 * 6e4 };
770
+ var PROMPTOWL_SCOPES = ["user:read", "teams:read"];
365
771
  var warnedMissingSsoAudience = false;
366
772
  var MIN_PASSWORD_LENGTH = 8;
367
773
  function assertValidPassword(password, label = "password") {
@@ -374,18 +780,6 @@ function assertValidPassword(password, label = "password") {
374
780
  );
375
781
  }
376
782
  }
377
- function clientIp(c) {
378
- const xff = c.req.header("x-forwarded-for");
379
- if (xff) return xff.split(",")[0].trim();
380
- const realIp = c.req.header("x-real-ip");
381
- if (realIp) return realIp.trim();
382
- try {
383
- const addr = getConnInfo(c).remote.address;
384
- if (addr) return addr;
385
- } catch {
386
- }
387
- return "unknown";
388
- }
389
783
  async function resolveCallerUserId(c) {
390
784
  const sessionId = getSessionIdFromRequest(c);
391
785
  if (sessionId) {
@@ -423,6 +817,16 @@ function clearSessionCookie(c) {
423
817
  "Set-Cookie",
424
818
  buildClearSessionCookie({ secure: isSecureRequest(c) })
425
819
  );
820
+ c.header(
821
+ "Set-Cookie",
822
+ buildPromptowlTokenCookie("", { secure: isSecureRequest(c), clear: true }),
823
+ { append: true }
824
+ );
825
+ c.header(
826
+ "Set-Cookie",
827
+ buildPromptowlMarkerCookie({ secure: isSecureRequest(c), clear: true }),
828
+ { append: true }
829
+ );
426
830
  }
427
831
  async function provisionPromptowlUser(c, rawEmail, rawName) {
428
832
  const gate = config.PROMPTOWL_SIGN_IN_GATE;
@@ -443,8 +847,8 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
443
847
  [meEmail]
444
848
  );
445
849
  if (!user) {
446
- const userId = uuid();
447
- const placeholderHash = await hashPassword(uuid());
850
+ const userId = uuid2();
851
+ const placeholderHash = await hashPassword(uuid2());
448
852
  await db.run(
449
853
  "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
450
854
  [userId, meEmail, rawName || null, placeholderHash]
@@ -476,7 +880,8 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
476
880
  setSessionCookie(c, sessionId);
477
881
  return { ok: true, user, isAdmin, claimBlocked };
478
882
  }
479
- var authRoutes = new Hono();
883
+ var authRoutes = new Hono2();
884
+ authRoutes.route("/oidc", oidcRoutes);
480
885
  authRoutes.post("/register", async (c) => {
481
886
  if (config.MANUAL_SIGN_IN !== "open") {
482
887
  return c.json(
@@ -512,7 +917,7 @@ authRoutes.post("/register", async (c) => {
512
917
  }
513
918
  throw new ValidationError("Email already registered");
514
919
  }
515
- const userId = uuid();
920
+ const userId = uuid2();
516
921
  const passwordHash = await hashPassword(body.password);
517
922
  await db.run(
518
923
  "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
@@ -527,7 +932,7 @@ authRoutes.post("/register", async (c) => {
527
932
  id: userId,
528
933
  email,
529
934
  name: body.name || null,
530
- is_admin: isLicenseAdminEmail(email)
935
+ is_admin: isServerAdminEmail(email)
531
936
  }
532
937
  },
533
938
  201
@@ -593,7 +998,7 @@ authRoutes.post("/login", async (c) => {
593
998
  id: user.id,
594
999
  email: user.email,
595
1000
  name: user.name,
596
- is_admin: isLicenseAdminEmail(user.email)
1001
+ is_admin: isServerAdminEmail(user.email)
597
1002
  }
598
1003
  });
599
1004
  });
@@ -601,6 +1006,11 @@ authRoutes.post("/logout", async (c) => {
601
1006
  const sessionId = getSessionIdFromRequest(c);
602
1007
  if (sessionId) await deleteSession(sessionId);
603
1008
  clearSessionCookie(c);
1009
+ c.header(
1010
+ "Set-Cookie",
1011
+ buildOidcMarkerCookie({ secure: isSecureRequest(c), clear: true }),
1012
+ { append: true }
1013
+ );
604
1014
  return c.json({ ok: true });
605
1015
  });
606
1016
  authRoutes.post("/keys", authMiddleware, async (c) => {
@@ -623,7 +1033,7 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
623
1033
  );
624
1034
  }
625
1035
  const apiKey = generateApiKey();
626
- const keyId = uuid();
1036
+ const keyId = uuid2();
627
1037
  await db.run(
628
1038
  "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
629
1039
  [
@@ -647,7 +1057,7 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
647
1057
  const db = getDb();
648
1058
  const userId = c.get("userId");
649
1059
  const apiKey = generateApiKey();
650
- const keyId = uuid();
1060
+ const keyId = uuid2();
651
1061
  const prior = await db.get(
652
1062
  "SELECT label, nest_id FROM api_keys WHERE user_id = ?",
653
1063
  [userId]
@@ -693,14 +1103,21 @@ authRoutes.post("/device", async (c) => {
693
1103
  if (!tryConsume(`device:ip:${clientIp(c)}`, DEVICE_LIMIT)) {
694
1104
  return c.json({ error: "Too many device auth attempts, try again later" }, 429);
695
1105
  }
696
- const body = await c.req.json();
1106
+ const body = await c.req.json().catch(() => ({}));
697
1107
  const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
698
1108
  const res = await fetch(`${promptowlUrl}/api/auth/device`, {
699
1109
  method: "POST",
700
1110
  headers: config.PROMPTOWL_FETCH_HEADERS,
701
1111
  body: JSON.stringify({
702
1112
  deviceName: body.deviceName || "ContextNest Community",
703
- deviceType: body.deviceType || "webapp"
1113
+ deviceType: body.deviceType || "webapp",
1114
+ // Ask for the least this server can work with: read the profile (to
1115
+ // identify who signed in) and read teams (to import them). The token we
1116
+ // then hold — and park in the user's cookie — is refused by every other
1117
+ // PromptOwl endpoint, so it can't chat as the user or spend their
1118
+ // credits even if it leaks. An older PromptOwl ignores the field and
1119
+ // returns an unscoped token, which still works.
1120
+ scopes: PROMPTOWL_SCOPES
704
1121
  })
705
1122
  });
706
1123
  const data = await res.json();
@@ -753,6 +1170,16 @@ authRoutes.post("/promptowl", async (c) => {
753
1170
  if (!result.ok) {
754
1171
  return c.json({ error: result.error, gate: result.gate }, result.status);
755
1172
  }
1173
+ c.header(
1174
+ "Set-Cookie",
1175
+ buildPromptowlTokenCookie(body.token, { secure: isSecureRequest(c) }),
1176
+ { append: true }
1177
+ );
1178
+ c.header(
1179
+ "Set-Cookie",
1180
+ buildPromptowlMarkerCookie({ secure: isSecureRequest(c) }),
1181
+ { append: true }
1182
+ );
756
1183
  return c.json({
757
1184
  user: {
758
1185
  id: result.user.id,
@@ -843,7 +1270,7 @@ authRoutes.get("/admin-status", async (c) => {
843
1270
  me = {
844
1271
  email: row.email,
845
1272
  name: row.name,
846
- is_admin: isLicenseAdminEmail(row.email)
1273
+ is_admin: isServerAdminEmail(row.email)
847
1274
  };
848
1275
  }
849
1276
  }
@@ -890,9 +1317,9 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
890
1317
  if (!callerId) {
891
1318
  return c.json({ error: "Authentication required." }, 401);
892
1319
  }
893
- if (!await isLicenseAdminUserId(callerId)) {
1320
+ if (!await isServerAdminUserId(callerId)) {
894
1321
  return c.json(
895
- { error: "Only the license-admin user can reset passwords." },
1322
+ { error: "Only a server admin can reset passwords." },
896
1323
  403
897
1324
  );
898
1325
  }
@@ -903,6 +1330,12 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
903
1330
  [targetId]
904
1331
  );
905
1332
  if (!target) return c.json({ error: "User not found" }, 404);
1333
+ if (target.id !== callerId && isServerAdminEmail(target.email)) {
1334
+ return c.json(
1335
+ { error: "This user is a server admin \u2014 admins can't reset each other's passwords. They can change their own via their account." },
1336
+ 403
1337
+ );
1338
+ }
906
1339
  let supplied;
907
1340
  try {
908
1341
  supplied = (await c.req.json()).password;
@@ -912,7 +1345,7 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
912
1345
  if (supplied !== void 0) {
913
1346
  assertValidPassword(supplied);
914
1347
  }
915
- const generated = supplied ? null : uuid().replace(/-/g, "").slice(0, 16);
1348
+ const generated = supplied ? null : uuid2().replace(/-/g, "").slice(0, 16);
916
1349
  const newPassword = supplied ?? generated;
917
1350
  const newHash = await hashPassword(newPassword);
918
1351
  await db.transaction(async (tx) => {
@@ -936,9 +1369,9 @@ authRoutes.delete("/users/:userId", async (c) => {
936
1369
  if (!callerId) {
937
1370
  return c.json({ error: "Authentication required." }, 401);
938
1371
  }
939
- if (!await isLicenseAdminUserId(callerId)) {
1372
+ if (!await isServerAdminUserId(callerId)) {
940
1373
  return c.json(
941
- { error: "Only the license-admin user can remove users." },
1374
+ { error: "Only a server admin can remove users." },
942
1375
  403
943
1376
  );
944
1377
  }
@@ -952,6 +1385,12 @@ authRoutes.delete("/users/:userId", async (c) => {
952
1385
  [targetId]
953
1386
  );
954
1387
  if (!target) return c.json({ error: "User not found" }, 404);
1388
+ if (isServerAdminEmail(target.email)) {
1389
+ return c.json(
1390
+ { error: "This user is a server admin \u2014 admins can't remove each other. Revoke their superadmin access first." },
1391
+ 403
1392
+ );
1393
+ }
955
1394
  const ownedNests = (await db.get(
956
1395
  "SELECT COUNT(*) AS c FROM nests WHERE user_id = ?",
957
1396
  [target.id]
@@ -994,10 +1433,10 @@ authRoutes.post("/invite", async (c) => {
994
1433
  );
995
1434
  }
996
1435
  const db = getDb();
997
- if (!await isLicenseAdminUserId(callerId)) {
1436
+ if (!await isServerAdminUserId(callerId)) {
998
1437
  return c.json(
999
1438
  {
1000
- error: "Only the license-admin user can invite teammates. Contact the admin who installed the PromptOwl license on this server to issue invitations."
1439
+ error: "Only a server admin can invite teammates. Contact the admin who installed the PromptOwl license on this server to issue invitations."
1001
1440
  },
1002
1441
  403
1003
1442
  );
@@ -1014,7 +1453,7 @@ authRoutes.post("/invite", async (c) => {
1014
1453
  409
1015
1454
  );
1016
1455
  }
1017
- const tempPassword = uuid().replace(/-/g, "").slice(0, 16);
1456
+ const tempPassword = uuid2().replace(/-/g, "").slice(0, 16);
1018
1457
  const passwordHash = await hashPassword(tempPassword);
1019
1458
  let userId;
1020
1459
  if (existing) {
@@ -1024,7 +1463,7 @@ authRoutes.post("/invite", async (c) => {
1024
1463
  userId
1025
1464
  ]);
1026
1465
  } else {
1027
- userId = uuid();
1466
+ userId = uuid2();
1028
1467
  await db.run(
1029
1468
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
1030
1469
  [userId, email, null, passwordHash]
@@ -1051,10 +1490,10 @@ authRoutes.get("/teammates", async (c) => {
1051
1490
  );
1052
1491
  }
1053
1492
  const db = getDb();
1054
- if (!await isLicenseAdminUserId(callerId)) {
1493
+ if (!await isServerAdminUserId(callerId)) {
1055
1494
  return c.json(
1056
1495
  {
1057
- error: "Only the license-admin user can view the teammates list. Contact the admin who installed the PromptOwl license on this server."
1496
+ error: "Only a server admin can view the teammates list. Contact the admin who installed the PromptOwl license on this server."
1058
1497
  },
1059
1498
  403
1060
1499
  );
@@ -1084,7 +1523,7 @@ authRoutes.get("/teammates", async (c) => {
1084
1523
  )
1085
1524
  ORDER BY s.user_email`
1086
1525
  );
1087
- const enriched = teammates.map((t) => ({ ...t, is_admin: isLicenseAdminEmail(t.email) })).sort((a, b) => Number(b.is_admin) - Number(a.is_admin));
1526
+ const enriched = teammates.map((t) => ({ ...t, is_admin: isServerAdminEmail(t.email) })).sort((a, b) => Number(b.is_admin) - Number(a.is_admin));
1088
1527
  return c.json({
1089
1528
  teammates: enriched,
1090
1529
  pending_stewards: pendingStewards.map((p) => p.email)
@@ -1092,15 +1531,17 @@ authRoutes.get("/teammates", async (c) => {
1092
1531
  });
1093
1532
 
1094
1533
  // src/nests/routes.ts
1095
- import { Hono as Hono2 } from "hono";
1534
+ import { Hono as Hono3 } from "hono";
1535
+ import { streamText } from "hono/streaming";
1096
1536
 
1097
1537
  // src/nodes/service.ts
1098
1538
  import {
1099
1539
  serializeDocument,
1100
- parseDocument as parseDocument2
1540
+ parseDocument as parseDocument2,
1541
+ normalizeStatus
1101
1542
  } from "@promptowl/contextnest-engine";
1102
- import { rename, mkdir } from "fs/promises";
1103
- import { dirname, join as join2 } from "path";
1543
+ import { rename, mkdir, rm } from "fs/promises";
1544
+ import { dirname, join as join2, basename } from "path";
1104
1545
 
1105
1546
  // src/governance/tag-index-service.ts
1106
1547
  function normalizeTag(raw) {
@@ -1318,7 +1759,7 @@ async function approveExternalEdit(input) {
1318
1759
  const node = await storage.readDocument(input.documentId);
1319
1760
  const versionNum = result.versionEntry.version;
1320
1761
  const tags = node.frontmatter.tags || [];
1321
- const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-A2YAKYQW.js");
1762
+ const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-YQIIQ7FN.js");
1322
1763
  await createVersion2({
1323
1764
  nestId: input.nestId,
1324
1765
  nodeId: input.documentId,
@@ -1413,6 +1854,20 @@ function folderToSegments(folder) {
1413
1854
  return segments;
1414
1855
  }
1415
1856
  var stripUndefined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
1857
+ async function departmentTagFor(userEmail) {
1858
+ if (!config.OIDC_DEPARTMENT_TAGGING) return null;
1859
+ try {
1860
+ const row = await getDb().get(
1861
+ "SELECT department FROM users WHERE LOWER(email) = LOWER(?)",
1862
+ [userEmail]
1863
+ );
1864
+ const slug = slugify(String(row?.department ?? ""));
1865
+ return slug ? normalizeTag2(`dept:${slug}`) : null;
1866
+ } catch (err) {
1867
+ console.error("[dept-tag] department lookup failed (tag skipped):", err);
1868
+ return null;
1869
+ }
1870
+ }
1416
1871
  var nodeWriteChains = /* @__PURE__ */ new Map();
1417
1872
  function withNodeWriteLock(key, fn) {
1418
1873
  const prev = (nodeWriteChains.get(key) ?? Promise.resolve()).catch(() => {
@@ -1463,6 +1918,59 @@ function toNodeResponse(node) {
1463
1918
  schedule: fm.metadata?.schedule != null ? String(fm.metadata.schedule) : void 0
1464
1919
  };
1465
1920
  }
1921
+ async function resolveAuthorship(nestId, nodes) {
1922
+ const result = /* @__PURE__ */ new Map();
1923
+ if (nodes.length === 0) return result;
1924
+ const db = getDb();
1925
+ const single = nodes.length === 1 ? nodes[0].id : null;
1926
+ const versionRows = await db.all(
1927
+ `SELECT node_id, author, MIN(version) AS first_version
1928
+ FROM node_versions
1929
+ WHERE nest_id = ?${single ? " AND node_id = ?" : ""}
1930
+ AND author IS NOT NULL AND author NOT LIKE 'system:%'
1931
+ GROUP BY node_id, author`,
1932
+ single ? [nestId, single] : [nestId]
1933
+ );
1934
+ const grantRows = await db.all(
1935
+ `SELECT g.target AS node_id, u.email AS email
1936
+ FROM grants g
1937
+ JOIN users u ON u.id = g.user_id
1938
+ WHERE g.nest_id = ? AND g.target_type = 'document'${single ? " AND g.target = ?" : ""}`,
1939
+ single ? [nestId, single] : [nestId]
1940
+ );
1941
+ const editorsByNode = /* @__PURE__ */ new Map();
1942
+ for (const r of versionRows) {
1943
+ let list = editorsByNode.get(r.node_id);
1944
+ if (!list) editorsByNode.set(r.node_id, list = []);
1945
+ list.push({ author: r.author, first: Number(r.first_version) });
1946
+ }
1947
+ const granteesByNode = /* @__PURE__ */ new Map();
1948
+ for (const g of grantRows) {
1949
+ let list = granteesByNode.get(g.node_id);
1950
+ if (!list) granteesByNode.set(g.node_id, list = []);
1951
+ list.push(g.email);
1952
+ }
1953
+ for (const n of nodes) {
1954
+ const editors = editorsByNode.get(n.id) ?? [];
1955
+ const firstAuthor = editors.length ? editors.reduce((a, b) => b.first < a.first ? b : a).author : null;
1956
+ const author = n.frontmatterAuthor || firstAuthor;
1957
+ const authorKey = author ? author.toLowerCase() : null;
1958
+ const seen = /* @__PURE__ */ new Set();
1959
+ const collaborators = [];
1960
+ for (const email of [
1961
+ ...editors.map((e) => e.author),
1962
+ ...granteesByNode.get(n.id) ?? []
1963
+ ]) {
1964
+ const key = email.toLowerCase();
1965
+ if (key === authorKey || seen.has(key)) continue;
1966
+ seen.add(key);
1967
+ collaborators.push(email);
1968
+ }
1969
+ collaborators.sort((a, b) => a.localeCompare(b));
1970
+ result.set(n.id, { author: author ?? null, collaborators });
1971
+ }
1972
+ return result;
1973
+ }
1466
1974
  async function listNodesForCaller(nestId, userId, filters = {}) {
1467
1975
  const { storage, versions: versionManager } = await engineCache.get(nestId);
1468
1976
  let documents = await storage.discoverDocuments();
@@ -1485,9 +1993,21 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1485
1993
  accessible = grants.length ? documents.filter((d) => grantCoversNode(grants, d.id)) : accessible;
1486
1994
  }
1487
1995
  const publicReader = await isPublicReader(nestId, userId);
1996
+ const authorship = publicReader ? null : await resolveAuthorship(
1997
+ nestId,
1998
+ accessible.map((d) => ({
1999
+ id: d.id,
2000
+ frontmatterAuthor: d.frontmatter.author ?? null
2001
+ }))
2002
+ );
1488
2003
  const enriched = await Promise.all(
1489
2004
  accessible.map(async (doc) => {
1490
2005
  const r = toNodeResponse(doc);
2006
+ const people = authorship?.get(doc.id);
2007
+ if (people) {
2008
+ r.author = people.author;
2009
+ r.collaborators = people.collaborators;
2010
+ }
1491
2011
  if (publicReader) {
1492
2012
  const approved = await getApprovedVersion(nestId, doc.id);
1493
2013
  if (approved != null) {
@@ -1546,6 +2066,13 @@ async function createNode(nestId, input, userEmail) {
1546
2066
  }
1547
2067
  const now = (/* @__PURE__ */ new Date()).toISOString();
1548
2068
  const tags = (input.tags || []).map(normalizeTag2);
2069
+ const requestedType = input.type || "document";
2070
+ if (requestedType === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
2071
+ throw new ValidationError("Artifact nodes are disabled on this server");
2072
+ }
2073
+ if (requestedType === "table" && !config.TYPE_TABLE_ENABLED) {
2074
+ throw new ValidationError("Table nodes are disabled on this server");
2075
+ }
1549
2076
  if (input.schedule !== void 0 && !isRunnableType(input.type)) {
1550
2077
  throw new ValidationError(
1551
2078
  `schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(
@@ -1553,6 +2080,10 @@ async function createNode(nestId, input, userEmail) {
1553
2080
  )})`
1554
2081
  );
1555
2082
  }
2083
+ const deptTag = await departmentTagFor(userEmail);
2084
+ if (deptTag && !tags.some((t) => t.toLowerCase() === deptTag)) {
2085
+ tags.push(deptTag);
2086
+ }
1556
2087
  const hasStewards = await isStewardshipEnabled(nestId);
1557
2088
  const initialStatus = hasStewards ? "draft" : "published";
1558
2089
  const initialVersion = hasStewards ? 1 : 0;
@@ -1561,7 +2092,7 @@ async function createNode(nestId, input, userEmail) {
1561
2092
  filePath: "",
1562
2093
  frontmatter: {
1563
2094
  title: input.title,
1564
- type: input.type || "document",
2095
+ type: requestedType,
1565
2096
  tags,
1566
2097
  status: input.status || initialStatus,
1567
2098
  version: initialVersion,
@@ -1628,7 +2159,27 @@ async function createNode(nestId, input, userEmail) {
1628
2159
  await trackEvent("node.create", { nestId, nodeId: id });
1629
2160
  return { node, version: savedVersion };
1630
2161
  }
1631
- async function registerImportedDocuments(nestId, userEmail) {
2162
+ function explicitFrontmatterStatus(raw) {
2163
+ const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2164
+ if (!fm) return null;
2165
+ const m = fm[1].match(/^status:[ \t]*["']?([A-Za-z_-]+)["']?[ \t]*$/m);
2166
+ return m ? normalizeStatus(m[1]) : null;
2167
+ }
2168
+ async function resetImportedDocForPublish(storage, nestId, nodeId) {
2169
+ const versionsDir = join2(
2170
+ resolveNestPath(nestId),
2171
+ dirname(nodeId),
2172
+ ".versions",
2173
+ basename(nodeId)
2174
+ );
2175
+ await rm(versionsDir, { recursive: true, force: true });
2176
+ const node = await storage.readDocument(nodeId);
2177
+ const fm = { ...node.frontmatter };
2178
+ delete fm.version;
2179
+ delete fm.checksum;
2180
+ await storage.writeDocument(nodeId, serializeDocument({ ...node, frontmatter: fm }));
2181
+ }
2182
+ async function registerImportedDocuments(nestId, userEmail, onProgress) {
1632
2183
  const { storage } = await engineCache.get(nestId);
1633
2184
  let docs;
1634
2185
  try {
@@ -1637,24 +2188,52 @@ async function registerImportedDocuments(nestId, userEmail) {
1637
2188
  console.error("registerImportedDocuments: discovery failed", nestId, err);
1638
2189
  return 0;
1639
2190
  }
2191
+ const startedAt = Date.now();
2192
+ console.log(`[import] register: discovered=${docs.length} nest=${nestId}`);
2193
+ await onProgress?.(0, docs.length);
1640
2194
  let registered = 0;
2195
+ let alreadyTracked = 0;
2196
+ let publishFailed = 0;
2197
+ let publishRecovered = 0;
2198
+ let keptUnpublished = 0;
2199
+ let processed = 0;
1641
2200
  for (let doc of docs) {
1642
2201
  const nodeId = doc.id;
1643
- if (await getCurrentVersion(nestId, nodeId) > 0) continue;
1644
- if (!doc.frontmatter?.title) {
1645
- const title = nodeId.split("/").pop() || nodeId;
1646
- doc = { ...doc, frontmatter: { ...doc.frontmatter, title } };
1647
- try {
1648
- await storage.writeDocument(nodeId, serializeDocument(doc));
1649
- } catch (err) {
1650
- console.error("import: failed to persist filename title", nodeId, err);
1651
- }
2202
+ await onProgress?.(++processed, docs.length);
2203
+ if (await getCurrentVersion(nestId, nodeId) > 0) {
2204
+ alreadyTracked++;
2205
+ continue;
2206
+ }
2207
+ const title = doc.frontmatter?.title ?? (nodeId.split("/").pop() || nodeId);
2208
+ doc = { ...doc, frontmatter: { ...doc.frontmatter, title, author: userEmail } };
2209
+ try {
2210
+ await storage.writeDocument(nodeId, serializeDocument(doc));
2211
+ } catch (err) {
2212
+ console.error("import: failed to persist import metadata", nodeId, err);
1652
2213
  }
1653
2214
  const rawTags = Array.isArray(doc.frontmatter?.tags) ? doc.frontmatter.tags : [];
1654
2215
  const tags = rawTags.map((t) => normalizeTag2(String(t)));
1655
2216
  const fmVersion = Number(doc.frontmatter?.version);
1656
- let version = Number.isFinite(fmVersion) && fmVersion > 0 ? fmVersion : 1;
2217
+ let version = Number.isInteger(fmVersion) && fmVersion > 0 ? fmVersion : 1;
1657
2218
  let content = doc.body || "";
2219
+ const status = explicitFrontmatterStatus(doc.rawContent);
2220
+ if (status && status !== "published" && status !== "approved") {
2221
+ await createVersion({
2222
+ nestId,
2223
+ nodeId,
2224
+ version,
2225
+ content,
2226
+ author: userEmail,
2227
+ status: "draft",
2228
+ changeNote: "Imported from existing folder (kept unpublished)",
2229
+ tags
2230
+ });
2231
+ await syncNodeTags(nestId, nodeId, tags);
2232
+ keptUnpublished++;
2233
+ registered++;
2234
+ continue;
2235
+ }
2236
+ let published = false;
1658
2237
  try {
1659
2238
  const result = await safePublishDocument(storage, nodeId, {
1660
2239
  editedBy: userEmail,
@@ -1662,12 +2241,29 @@ async function registerImportedDocuments(nestId, userEmail) {
1662
2241
  });
1663
2242
  version = result.node.frontmatter.version || version;
1664
2243
  content = result.node.body || content;
2244
+ published = true;
1665
2245
  } catch (err) {
1666
- console.error(
1667
- "safePublishDocument failed (import register)",
1668
- nodeId,
1669
- err
2246
+ console.warn(
2247
+ `[import] publish retry needed for ${nodeId}: ${err?.message ?? err}`
1670
2248
  );
2249
+ try {
2250
+ await resetImportedDocForPublish(storage, nestId, nodeId);
2251
+ const result = await safePublishDocument(storage, nodeId, {
2252
+ editedBy: userEmail,
2253
+ note: "Imported from existing folder (reset chain)"
2254
+ });
2255
+ version = result.node.frontmatter.version || 1;
2256
+ content = result.node.body || content;
2257
+ published = true;
2258
+ publishRecovered++;
2259
+ } catch (err2) {
2260
+ publishFailed++;
2261
+ console.error(
2262
+ "reset+republish failed (import register)",
2263
+ nodeId,
2264
+ err2
2265
+ );
2266
+ }
1671
2267
  }
1672
2268
  await createVersion({
1673
2269
  nestId,
@@ -1675,14 +2271,19 @@ async function registerImportedDocuments(nestId, userEmail) {
1675
2271
  version,
1676
2272
  content,
1677
2273
  author: userEmail,
1678
- status: "published",
2274
+ status: published ? "published" : "draft",
1679
2275
  changeNote: "Imported from existing folder",
1680
2276
  tags
1681
2277
  });
1682
- await setApprovedVersion(nestId, nodeId, version, userEmail);
2278
+ if (published) {
2279
+ await setApprovedVersion(nestId, nodeId, version, userEmail);
2280
+ }
1683
2281
  await syncNodeTags(nestId, nodeId, tags);
1684
2282
  registered++;
1685
2283
  }
2284
+ console.log(
2285
+ `[import] register done: registered=${registered} alreadyTracked=${alreadyTracked} keptUnpublished=${keptUnpublished} publishRecovered=${publishRecovered} publishFailed=${publishFailed} tookMs=${Date.now() - startedAt}`
2286
+ );
1686
2287
  await trackEvent("nest.import.documents", { nestId, registered });
1687
2288
  return registered;
1688
2289
  }
@@ -2147,7 +2748,7 @@ async function effectivePermission(nestId, userId) {
2147
2748
  }
2148
2749
  return resolveNestPermission(nestId, userId);
2149
2750
  }
2150
- var nestRoutes = new Hono2();
2751
+ var nestRoutes = new Hono3();
2151
2752
  nestRoutes.get("/", async (c) => {
2152
2753
  const userId = c.get("userId");
2153
2754
  const owned = await listNests(userId);
@@ -2168,8 +2769,11 @@ nestRoutes.get("/", async (c) => {
2168
2769
  await getCollaboratorRole(n.id, callerEmail)
2169
2770
  );
2170
2771
  const stewardRoles = await getStewardRolesForUser(n.id, callerEmail);
2772
+ const teamRoles = await resolveTeamRolesForUser(n.id, userId);
2171
2773
  roles = [
2172
- ...new Set([grantRole, ...stewardRoles].filter(Boolean))
2774
+ ...new Set(
2775
+ [grantRole, ...stewardRoles, ...teamRoles].filter(Boolean)
2776
+ )
2173
2777
  ];
2174
2778
  }
2175
2779
  if (!is_owner && n.user_id !== ANON_USER_ID) {
@@ -2252,16 +2856,41 @@ nestRoutes.post("/:nestId", async (c) => {
2252
2856
  }
2253
2857
  const files = Array.isArray(body.files) ? body.files : [];
2254
2858
  const userId = c.get("userId");
2255
- const nest = await importNest(userId, body.name, files);
2256
- const documents = await registerImportedDocuments(
2257
- nest.id,
2258
- await resolveCallerEmail(userId)
2859
+ console.log(
2860
+ `[import] request: name=${JSON.stringify(body.name)} files=${files.length}`
2259
2861
  );
2260
- return c.json({ nest, documents }, 201);
2862
+ if (c.req.query("stream") === "1") {
2863
+ const name = body.name;
2864
+ return streamText(c, async (stream) => {
2865
+ try {
2866
+ await stream.writeln(JSON.stringify({ phase: "write" }));
2867
+ const nest2 = await importNest(userId, name, files);
2868
+ const email = await resolveCallerEmail(userId);
2869
+ const documents2 = await registerImportedDocuments(
2870
+ nest2.id,
2871
+ email,
2872
+ async (done, total) => {
2873
+ await stream.writeln(JSON.stringify({ phase: "register", done, total }));
2874
+ }
2875
+ );
2876
+ await stream.writeln(JSON.stringify({ complete: true, nest: nest2, documents: documents2 }));
2877
+ } catch (err) {
2878
+ await stream.writeln(
2879
+ JSON.stringify({ error: err?.message || "Import failed" })
2880
+ );
2881
+ }
2882
+ });
2883
+ }
2884
+ const nest = await importNest(userId, body.name, files);
2885
+ const documents = await registerImportedDocuments(
2886
+ nest.id,
2887
+ await resolveCallerEmail(userId)
2888
+ );
2889
+ return c.json({ nest, documents }, 201);
2261
2890
  });
2262
2891
  async function handleUnsyncedList(c) {
2263
2892
  const userId = c.get("userId");
2264
- if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
2893
+ if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
2265
2894
  throw new ForbiddenError("Only the server admin can list unsynced folders");
2266
2895
  }
2267
2896
  return c.json({ folders: listUnsyncedFolders() });
@@ -2271,7 +2900,7 @@ nestRoutes.post("/:nestId/sync", async (c) => {
2271
2900
  throw new NotFoundError("Not found");
2272
2901
  }
2273
2902
  const userId = c.get("userId");
2274
- if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
2903
+ if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
2275
2904
  throw new ForbiddenError("Only the server admin can sync folders");
2276
2905
  }
2277
2906
  const body = await c.req.json();
@@ -2391,10 +3020,10 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
2391
3020
  });
2392
3021
 
2393
3022
  // src/nests/sharing-routes.ts
2394
- import { Hono as Hono3 } from "hono";
3023
+ import { Hono as Hono4 } from "hono";
2395
3024
 
2396
3025
  // src/nests/sharing-service.ts
2397
- import { v4 as uuid2 } from "uuid";
3026
+ import { v4 as uuid3 } from "uuid";
2398
3027
  var VALID_PERMISSIONS = ["read", "write", "admin"];
2399
3028
  async function addCollaborator(params) {
2400
3029
  const { nestId } = params;
@@ -2418,10 +3047,10 @@ async function addCollaborator(params) {
2418
3047
  userId = existing.id;
2419
3048
  } else {
2420
3049
  const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
2421
- userId = uuid2();
3050
+ userId = uuid3();
2422
3051
  await db.run(
2423
3052
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
2424
- [userId, email, null, await hashPassword2(uuid2())]
3053
+ [userId, email, null, await hashPassword2(uuid3())]
2425
3054
  );
2426
3055
  }
2427
3056
  }
@@ -2459,7 +3088,7 @@ async function addCollaborator(params) {
2459
3088
  [params.grantedByEmail]
2460
3089
  ))?.id : void 0;
2461
3090
  const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
2462
- const collabId = uuid2();
3091
+ const collabId = uuid3();
2463
3092
  await db.run(
2464
3093
  "INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
2465
3094
  [collabId, nestId, userId, params.permission, granterId]
@@ -2484,18 +3113,8 @@ async function addCollaborator(params) {
2484
3113
  );
2485
3114
  }
2486
3115
 
2487
- // src/shared/base-url.ts
2488
- function requestBaseUrl(reqUrl) {
2489
- if (config.PUBLIC_BASE_URL) return config.PUBLIC_BASE_URL;
2490
- try {
2491
- return new URL(reqUrl).origin;
2492
- } catch {
2493
- return "";
2494
- }
2495
- }
2496
-
2497
3116
  // src/nests/sharing-routes.ts
2498
- var sharingRoutes = new Hono3();
3117
+ var sharingRoutes = new Hono4();
2499
3118
  sharingRoutes.get("/collaborators", async (c) => {
2500
3119
  const db = getDb();
2501
3120
  const nestId = c.req.param("nestId");
@@ -2582,9 +3201,9 @@ sharingRoutes.patch("/visibility", async (c) => {
2582
3201
  });
2583
3202
 
2584
3203
  // src/nests/grant-routes.ts
2585
- import { Hono as Hono4 } from "hono";
2586
- import { v4 as uuid3 } from "uuid";
2587
- var grantRoutes = new Hono4();
3204
+ import { Hono as Hono5 } from "hono";
3205
+ import { v4 as uuid4 } from "uuid";
3206
+ var grantRoutes = new Hono5();
2588
3207
  async function requireManage(c, nestId) {
2589
3208
  if (!await canManageStewards(nestId, c.get("userId"))) {
2590
3209
  throw new ForbiddenError(
@@ -2598,10 +3217,10 @@ async function resolveGrantee(email) {
2598
3217
  const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
2599
3218
  if (existing) return existing.id;
2600
3219
  const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
2601
- const id = uuid3();
3220
+ const id = uuid4();
2602
3221
  await db.run(
2603
3222
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
2604
- [id, e, null, await hashPassword2(uuid3())]
3223
+ [id, e, null, await hashPassword2(uuid4())]
2605
3224
  );
2606
3225
  return id;
2607
3226
  }
@@ -2652,10 +3271,347 @@ grantRoutes.delete("/:id", async (c) => {
2652
3271
  return c.json({ deleted: true });
2653
3272
  });
2654
3273
 
3274
+ // src/nests/nest-team-routes.ts
3275
+ import { Hono as Hono6 } from "hono";
3276
+ var nestTeamRoutes = new Hono6();
3277
+ nestTeamRoutes.get("/", async (c) => {
3278
+ const nestId = c.req.param("nestId");
3279
+ const refs = await listNestTeams(nestId);
3280
+ const teams = [];
3281
+ for (const ref of refs) {
3282
+ const team = await getTeam(ref.teamId).catch(() => null);
3283
+ teams.push({
3284
+ teamId: ref.teamId,
3285
+ teamName: team?.name ?? ref.teamName,
3286
+ members: team?.members ?? []
3287
+ });
3288
+ }
3289
+ return c.json({ teams });
3290
+ });
3291
+ nestTeamRoutes.post("/", async (c) => {
3292
+ const body = await c.req.json();
3293
+ if (!body.team_id) {
3294
+ throw new ValidationError("team_id is required");
3295
+ }
3296
+ const userId = c.get("userId");
3297
+ const teams = await shareTeamWithNest({
3298
+ nestId: c.req.param("nestId"),
3299
+ teamId: body.team_id,
3300
+ callerUserId: userId,
3301
+ allowAdmin: await isLicenseAdminUserId(userId)
3302
+ });
3303
+ return c.json({ teams }, 201);
3304
+ });
3305
+ nestTeamRoutes.delete("/:teamId", async (c) => {
3306
+ const teams = await unshareTeamFromNest({
3307
+ nestId: c.req.param("nestId"),
3308
+ teamId: c.req.param("teamId")
3309
+ });
3310
+ return c.json({ teams });
3311
+ });
3312
+
3313
+ // src/governance/team-routes.ts
3314
+ import { Hono as Hono7 } from "hono";
3315
+
3316
+ // src/governance/promptowl-teams.ts
3317
+ var PROMPTOWL_SOURCE = "promptowl";
3318
+ var IMPORTABLE_PROMPTOWL_ROLES = ["owner", "editor"];
3319
+ function canImportPromptowlTeam(team) {
3320
+ return IMPORTABLE_PROMPTOWL_ROLES.includes(
3321
+ (team.your_role || "").toLowerCase()
3322
+ );
3323
+ }
3324
+ function mapPromptowlRole(role) {
3325
+ switch ((role || "").toLowerCase()) {
3326
+ case "owner":
3327
+ return "admin";
3328
+ case "editor":
3329
+ return "editor";
3330
+ default:
3331
+ return "viewer";
3332
+ }
3333
+ }
3334
+ async function fetchOwnedPromptowlTeams(token) {
3335
+ if (!token?.trim()) throw new ValidationError("token is required");
3336
+ const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
3337
+ let res;
3338
+ try {
3339
+ res = await fetch(`${promptowlUrl}/api/user/teams`, {
3340
+ headers: {
3341
+ ...config.PROMPTOWL_FETCH_HEADERS,
3342
+ Authorization: `Bearer ${token}`
3343
+ }
3344
+ });
3345
+ } catch {
3346
+ throw new ValidationError("Could not reach PromptOwl. Check this server's network access.");
3347
+ }
3348
+ if (res.status === 401) {
3349
+ throw new UnauthorizedError(
3350
+ "PromptOwl rejected this approval. Approve again and retry."
3351
+ );
3352
+ }
3353
+ if (!res.ok) {
3354
+ throw new ValidationError(
3355
+ `PromptOwl could not list your teams (HTTP ${res.status}).`
3356
+ );
3357
+ }
3358
+ const data = await res.json();
3359
+ return Array.isArray(data.teams) ? data.teams : [];
3360
+ }
3361
+ async function syncPromptowlTeams(params) {
3362
+ if (!params.teamIds?.length) {
3363
+ throw new ValidationError("team_ids is required");
3364
+ }
3365
+ const owned = await fetchOwnedPromptowlTeams(params.token);
3366
+ const byId = new Map(owned.map((t) => [t.id, t]));
3367
+ const selected = [];
3368
+ for (const id of params.teamIds) {
3369
+ const team = byId.get(id);
3370
+ if (!team) {
3371
+ throw new ValidationError(
3372
+ "One of the selected teams isn't available to import. Refresh the list and try again."
3373
+ );
3374
+ }
3375
+ if (!canImportPromptowlTeam(team)) {
3376
+ throw new ForbiddenError(
3377
+ `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.`
3378
+ );
3379
+ }
3380
+ selected.push(team);
3381
+ }
3382
+ const db = getDb();
3383
+ const caller = await db.get("SELECT email FROM users WHERE id = ?", [
3384
+ params.callerUserId
3385
+ ]);
3386
+ if (!caller?.email) {
3387
+ throw new ValidationError("Could not resolve your account.");
3388
+ }
3389
+ const results = [];
3390
+ for (const poTeam of selected) {
3391
+ results.push(await syncOne(poTeam, params.callerUserId, caller.email));
3392
+ }
3393
+ return results;
3394
+ }
3395
+ async function syncOne(poTeam, callerUserId, callerEmail) {
3396
+ const existing = await findTeamByExternalId({
3397
+ ownerId: callerUserId,
3398
+ source: PROMPTOWL_SOURCE,
3399
+ externalId: poTeam.id
3400
+ });
3401
+ const team = existing ?? await createTeam({
3402
+ name: poTeam.name,
3403
+ ownerId: callerUserId,
3404
+ source: PROMPTOWL_SOURCE,
3405
+ externalId: poTeam.id
3406
+ });
3407
+ if (existing && existing.name !== poTeam.name) {
3408
+ await renameTeam({
3409
+ teamId: team.id,
3410
+ name: poTeam.name,
3411
+ callerUserId,
3412
+ allowAdmin: false
3413
+ });
3414
+ }
3415
+ const desired = await desiredRoster(poTeam, callerEmail);
3416
+ const before = new Map(team.members.map((m) => [m.email, m.role]));
3417
+ const after = new Map(desired.map((m) => [m.email, m.role]));
3418
+ const added = [...after.keys()].filter((e) => !before.has(e));
3419
+ const removed = [...before.keys()].filter((e) => !after.has(e));
3420
+ const updated = [...after.keys()].filter(
3421
+ (e) => before.has(e) && before.get(e) !== after.get(e)
3422
+ );
3423
+ if (added.length || removed.length || updated.length) {
3424
+ await replaceTeamRoster({
3425
+ teamId: team.id,
3426
+ members: desired,
3427
+ callerUserId,
3428
+ allowAdmin: false
3429
+ });
3430
+ }
3431
+ return {
3432
+ team_id: team.id,
3433
+ external_id: poTeam.id,
3434
+ name: poTeam.name,
3435
+ created: !existing,
3436
+ added,
3437
+ removed,
3438
+ updated
3439
+ };
3440
+ }
3441
+ async function desiredRoster(poTeam, callerEmail) {
3442
+ const caller = normalizeEmail(callerEmail);
3443
+ const roster = [];
3444
+ const seen = /* @__PURE__ */ new Set();
3445
+ for (const poMember of poTeam.members || []) {
3446
+ if (!poMember?.email) continue;
3447
+ const email = normalizeEmail(poMember.email);
3448
+ if (email === caller || seen.has(email)) continue;
3449
+ seen.add(email);
3450
+ const identity = await resolveMemberIdentity({ email });
3451
+ roster.push({
3452
+ userId: identity.userId,
3453
+ email: identity.email,
3454
+ role: mapPromptowlRole(poMember.role)
3455
+ });
3456
+ }
3457
+ return roster;
3458
+ }
3459
+
3460
+ // src/governance/team-routes.ts
3461
+ var teamRoutes = new Hono7();
3462
+ var PROMPTOWL_LIMIT = { max: 10, windowMs: 15 * 6e4 };
3463
+ var CONNECT_REQUIRED = "promptowl_connect_required";
3464
+ var CONNECT_REQUIRED_STATUS = 428;
3465
+ async function withPromptowlToken(c, run) {
3466
+ if (!config.PROMPTOWL_TEAMS_ENABLED) {
3467
+ return c.json({ error: "Not found" }, 404);
3468
+ }
3469
+ if (!tryConsume(`po-teams:ip:${clientIp(c)}`, PROMPTOWL_LIMIT)) {
3470
+ return c.json({ error: "Too many PromptOwl requests, try again later" }, 429);
3471
+ }
3472
+ const token = getPromptowlTokenFromRequest(c);
3473
+ if (!token) {
3474
+ return c.json(
3475
+ {
3476
+ error: "Sign in with PromptOwl to import teams.",
3477
+ code: CONNECT_REQUIRED
3478
+ },
3479
+ CONNECT_REQUIRED_STATUS
3480
+ );
3481
+ }
3482
+ try {
3483
+ return c.json(await run(token));
3484
+ } catch (err) {
3485
+ if (err instanceof UnauthorizedError) {
3486
+ c.header(
3487
+ "Set-Cookie",
3488
+ buildPromptowlTokenCookie("", { secure: isSecureRequest(c), clear: true }),
3489
+ { append: true }
3490
+ );
3491
+ return c.json(
3492
+ { error: err.message, code: CONNECT_REQUIRED },
3493
+ CONNECT_REQUIRED_STATUS
3494
+ );
3495
+ }
3496
+ throw err;
3497
+ }
3498
+ }
3499
+ teamRoutes.get(
3500
+ "/promptowl",
3501
+ async (c) => withPromptowlToken(c, async (token) => ({
3502
+ teams: await fetchOwnedPromptowlTeams(token)
3503
+ }))
3504
+ );
3505
+ teamRoutes.post("/promptowl/sync", async (c) => {
3506
+ const body = await c.req.json().catch(() => ({}));
3507
+ return withPromptowlToken(c, async (token) => ({
3508
+ results: await syncPromptowlTeams({
3509
+ callerUserId: c.get("userId"),
3510
+ token,
3511
+ teamIds: body.team_ids ?? []
3512
+ })
3513
+ }));
3514
+ });
3515
+ teamRoutes.post("/", async (c) => {
3516
+ const body = await c.req.json();
3517
+ const team = await createTeam({
3518
+ name: body.name ?? "",
3519
+ ownerId: c.get("userId")
3520
+ });
3521
+ return c.json({ team }, 201);
3522
+ });
3523
+ teamRoutes.get("/", async (c) => {
3524
+ const teams = await listTeamsForUser(c.get("userId"));
3525
+ return c.json({ teams });
3526
+ });
3527
+ teamRoutes.get("/:teamId", async (c) => {
3528
+ const userId = c.get("userId");
3529
+ const team = await getTeam(c.req.param("teamId"));
3530
+ const isMember = team.members.some((m) => m.userId === userId);
3531
+ if (team.owner_id !== userId && !isMember && !await isLicenseAdminUserId(userId)) {
3532
+ throw new ForbiddenError("You don't have access to this team.");
3533
+ }
3534
+ return c.json({ team });
3535
+ });
3536
+ teamRoutes.patch("/:teamId", async (c) => {
3537
+ const body = await c.req.json();
3538
+ const userId = c.get("userId");
3539
+ const team = await renameTeam({
3540
+ teamId: c.req.param("teamId"),
3541
+ name: body.name ?? "",
3542
+ callerUserId: userId,
3543
+ allowAdmin: await isLicenseAdminUserId(userId)
3544
+ });
3545
+ return c.json({ team });
3546
+ });
3547
+ teamRoutes.delete("/:teamId", async (c) => {
3548
+ const userId = c.get("userId");
3549
+ await deleteTeam({
3550
+ teamId: c.req.param("teamId"),
3551
+ callerUserId: userId,
3552
+ allowAdmin: await isLicenseAdminUserId(userId)
3553
+ });
3554
+ return c.json({ removed: true });
3555
+ });
3556
+ teamRoutes.post("/:teamId/members", async (c) => {
3557
+ const body = await c.req.json();
3558
+ const userId = c.get("userId");
3559
+ const team = await addMember({
3560
+ teamId: c.req.param("teamId"),
3561
+ email: body.email,
3562
+ userId: body.user_id,
3563
+ role: body.role ?? "",
3564
+ callerUserId: userId,
3565
+ allowAdmin: await isLicenseAdminUserId(userId)
3566
+ });
3567
+ return c.json({ team }, 201);
3568
+ });
3569
+ teamRoutes.patch("/:teamId/members/:userId", async (c) => {
3570
+ const body = await c.req.json();
3571
+ const userId = c.get("userId");
3572
+ const team = await updateMemberRole({
3573
+ teamId: c.req.param("teamId"),
3574
+ userId: c.req.param("userId"),
3575
+ role: body.role ?? "",
3576
+ callerUserId: userId,
3577
+ allowAdmin: await isLicenseAdminUserId(userId)
3578
+ });
3579
+ return c.json({ team });
3580
+ });
3581
+ teamRoutes.delete("/:teamId/members/:userId", async (c) => {
3582
+ const userId = c.get("userId");
3583
+ const team = await removeMember({
3584
+ teamId: c.req.param("teamId"),
3585
+ userId: c.req.param("userId"),
3586
+ callerUserId: userId,
3587
+ allowAdmin: await isLicenseAdminUserId(userId)
3588
+ });
3589
+ return c.json({ team });
3590
+ });
3591
+
2655
3592
  // src/nodes/routes.ts
2656
- import { Hono as Hono5 } from "hono";
3593
+ import { Hono as Hono8 } from "hono";
3594
+
3595
+ // src/shared/artifact-sandbox.ts
3596
+ var ARTIFACT_SANDBOX_CSP = [
3597
+ "sandbox allow-scripts allow-popups allow-modals",
3598
+ "default-src 'none'",
3599
+ "script-src 'unsafe-inline'",
3600
+ "style-src 'unsafe-inline'",
3601
+ "img-src data: blob:",
3602
+ "font-src data:",
3603
+ "connect-src 'none'",
3604
+ "base-uri 'none'",
3605
+ "form-action 'none'"
3606
+ ].join("; ");
3607
+ function applyArtifactSandboxHeaders(c) {
3608
+ c.header("Content-Security-Policy", ARTIFACT_SANDBOX_CSP);
3609
+ c.header("X-Content-Type-Options", "nosniff");
3610
+ c.header("Referrer-Policy", "no-referrer");
3611
+ }
2657
3612
 
2658
3613
  // src/nodes/markdown-export.ts
3614
+ import { parseDocument as parseDocument3 } from "@promptowl/contextnest-engine";
2659
3615
  function nodeToMarkdown(node) {
2660
3616
  const tags = node.tags ?? [];
2661
3617
  return [
@@ -2674,9 +3630,37 @@ function nodesToMarkdown(nodes) {
2674
3630
  function isMarkdownFormat(c) {
2675
3631
  return c.req.query("format") === "markdown";
2676
3632
  }
3633
+ function isRawFormat(c) {
3634
+ const format = c.req.query("format");
3635
+ if (format !== void 0) return format === "raw";
3636
+ const accept = c.req.header("Accept") ?? "";
3637
+ return accept.split(",").some((part) => part.trim().split(";")[0].trim().toLowerCase() === "text/markdown");
3638
+ }
3639
+ var RAW_CONTENT_TYPES = {
3640
+ artifact: "text/html; charset=utf-8",
3641
+ tool: "text/plain; charset=utf-8",
3642
+ table: "text/plain; charset=utf-8"
3643
+ };
3644
+ function rawContentType(nodeType) {
3645
+ return RAW_CONTENT_TYPES[nodeType || "document"] ?? "text/markdown; charset=utf-8";
3646
+ }
3647
+ function frontmatterBlockOf(raw) {
3648
+ if (!raw || !raw.startsWith("---")) return "";
3649
+ try {
3650
+ const body = parseDocument3("frontmatter-split.md", raw, "frontmatter-split").body ?? "";
3651
+ if (raw.endsWith(body)) return raw.slice(0, raw.length - body.length);
3652
+ } catch {
3653
+ }
3654
+ const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
3655
+ return m ? `---
3656
+ ${m[1]}
3657
+ ---
3658
+
3659
+ ` : "";
3660
+ }
2677
3661
 
2678
3662
  // src/annotations/service.ts
2679
- import { v4 as uuid4 } from "uuid";
3663
+ import { v4 as uuid5 } from "uuid";
2680
3664
 
2681
3665
  // src/annotations/projection.ts
2682
3666
  var MAX_CONTEXT_CHARS = 250;
@@ -2826,7 +3810,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
2826
3810
  throw new Error("comment body is required");
2827
3811
  }
2828
3812
  const db = getDb();
2829
- const id = uuid4();
3813
+ const id = uuid5();
2830
3814
  const anchor = clampAnchor(input.anchor);
2831
3815
  const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
2832
3816
  await db.transaction(async (tx) => {
@@ -2845,7 +3829,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
2845
3829
  );
2846
3830
  await tx.run(
2847
3831
  "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
2848
- [uuid4(), id, authorEmail, body]
3832
+ [uuid5(), id, authorEmail, body]
2849
3833
  );
2850
3834
  });
2851
3835
  return rowToThread(await getThreadRow(id));
@@ -2865,7 +3849,7 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
2865
3849
  await getScopedThreadRow(threadId, nestId, nodeId);
2866
3850
  await getDb().run(
2867
3851
  "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
2868
- [uuid4(), threadId, authorEmail, trimmed]
3852
+ [uuid5(), threadId, authorEmail, trimmed]
2869
3853
  );
2870
3854
  return rowToThread(await getThreadRow(threadId));
2871
3855
  }
@@ -2964,7 +3948,7 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
2964
3948
  }
2965
3949
 
2966
3950
  // src/nodes/routes.ts
2967
- var nodeRoutes = new Hono5();
3951
+ var nodeRoutes = new Hono8();
2968
3952
  function nodeAsMarkdown(response, nodeId) {
2969
3953
  return nodeToMarkdown({
2970
3954
  id: nodeId,
@@ -2974,6 +3958,17 @@ function nodeAsMarkdown(response, nodeId) {
2974
3958
  body: response.content
2975
3959
  });
2976
3960
  }
3961
+ function rawNodeBody(c, response, storedRaw) {
3962
+ const body = response.content ?? "";
3963
+ const payload = c.req.query("frontmatter") === "1" ? frontmatterBlockOf(storedRaw) + body : body;
3964
+ c.header("X-Content-Type-Options", "nosniff");
3965
+ if ((response.type || "document") === "artifact") {
3966
+ applyArtifactSandboxHeaders(c);
3967
+ }
3968
+ return c.body(payload, 200, {
3969
+ "Content-Type": rawContentType(response.type)
3970
+ });
3971
+ }
2977
3972
  nodeRoutes.get("/", async (c) => {
2978
3973
  const nestId = c.req.param("nestId");
2979
3974
  const userId = c.get("userId");
@@ -3017,7 +4012,7 @@ nodeRoutes.post("/", async (c) => {
3017
4012
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
3018
4013
  const nestId = c.req.param("nestId");
3019
4014
  const nodeId = c.req.param("nodeId");
3020
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-D5PDWXBR.js");
4015
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-7BHI2YXB.js");
3021
4016
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
3022
4017
  nestId,
3023
4018
  nodeId
@@ -3038,7 +4033,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
3038
4033
  nodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
3039
4034
  const nestId = c.req.param("nestId");
3040
4035
  const nodeId = c.req.param("nodeId");
3041
- const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-A2YAKYQW.js");
4036
+ const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-YQIIQ7FN.js");
3042
4037
  const allVersions = await getVersions2(nestId, nodeId);
3043
4038
  const approved = await getApprovedVersion2(nestId, nodeId);
3044
4039
  const db = getDb();
@@ -3070,7 +4065,7 @@ nodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
3070
4065
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
3071
4066
  const nestId = c.req.param("nestId");
3072
4067
  const nodeId = c.req.param("nodeId");
3073
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-VLSUT6KK.js");
4068
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-DPN26C47.js");
3074
4069
  const history = await getReviewHistory2(nestId, nodeId);
3075
4070
  return c.json({ reviews: history });
3076
4071
  });
@@ -3189,15 +4184,26 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
3189
4184
  }
3190
4185
  }
3191
4186
  const response = toNodeResponse(node);
3192
- if (await isPublicReader(nestId, userId)) {
4187
+ const publicReader = await isPublicReader(nestId, userId);
4188
+ if (!publicReader) {
4189
+ const people = (await resolveAuthorship(nestId, [
4190
+ { id: nodeId, frontmatterAuthor: node.frontmatter?.author ?? null }
4191
+ ])).get(nodeId);
4192
+ if (people) {
4193
+ response.author = people.author;
4194
+ response.collaborators = people.collaborators;
4195
+ }
4196
+ }
4197
+ if (publicReader) {
3193
4198
  const approved = await getApprovedVersion(nestId, nodeId);
3194
4199
  if (approved != null) {
4200
+ let approvedRaw = "";
3195
4201
  try {
3196
- const raw = await versionManager.reconstructVersion(
4202
+ approvedRaw = await versionManager.reconstructVersion(
3197
4203
  nodeId,
3198
4204
  approved
3199
4205
  );
3200
- response.content = bodyOnly(nodeId, raw);
4206
+ response.content = bodyOnly(nodeId, approvedRaw);
3201
4207
  } catch (err) {
3202
4208
  console.error(
3203
4209
  "reconstructVersion failed (public single)",
@@ -3209,6 +4215,9 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
3209
4215
  }
3210
4216
  response.version = approved;
3211
4217
  response.status = "published";
4218
+ if (isRawFormat(c)) {
4219
+ return rawNodeBody(c, response, approvedRaw);
4220
+ }
3212
4221
  if (isMarkdownFormat(c)) {
3213
4222
  return c.body(nodeAsMarkdown(response, nodeId), 200, {
3214
4223
  "Content-Type": "text/markdown; charset=utf-8"
@@ -3222,6 +4231,9 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
3222
4231
  const pending = await getPendingReview(nestId, nodeId);
3223
4232
  response.pendingReviewBy = pending?.requestedBy ?? null;
3224
4233
  }
4234
+ if (isRawFormat(c)) {
4235
+ return rawNodeBody(c, response, node.rawContent);
4236
+ }
3225
4237
  if (isMarkdownFormat(c)) {
3226
4238
  return c.body(nodeAsMarkdown(response, nodeId), 200, {
3227
4239
  "Content-Type": "text/markdown; charset=utf-8"
@@ -3347,13 +4359,13 @@ async function getUserEmail(c) {
3347
4359
  }
3348
4360
 
3349
4361
  // src/annotations/routes.ts
3350
- import { Hono as Hono6 } from "hono";
4362
+ import { Hono as Hono9 } from "hono";
3351
4363
 
3352
4364
  // src/annotations/types.ts
3353
4365
  var ARTIFACT_NODE_TYPE = "artifact";
3354
4366
 
3355
4367
  // src/annotations/routes.ts
3356
- var annotationRoutes = new Hono6();
4368
+ var annotationRoutes = new Hono9();
3357
4369
  function getNodeId(c) {
3358
4370
  const raw = c.req.param("nodeId");
3359
4371
  try {
@@ -3461,28 +4473,13 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
3461
4473
  if (type !== null && type !== ARTIFACT_NODE_TYPE) {
3462
4474
  return c.json({ error: "Not a hosted artifact" }, 404);
3463
4475
  }
3464
- c.header(
3465
- "Content-Security-Policy",
3466
- [
3467
- "sandbox allow-scripts allow-popups allow-modals",
3468
- "default-src 'none'",
3469
- "script-src 'unsafe-inline'",
3470
- "style-src 'unsafe-inline'",
3471
- "img-src data: blob:",
3472
- "font-src data:",
3473
- "connect-src 'none'",
3474
- "base-uri 'none'",
3475
- "form-action 'none'"
3476
- ].join("; ")
3477
- );
3478
- c.header("X-Content-Type-Options", "nosniff");
3479
- c.header("Referrer-Policy", "no-referrer");
4476
+ applyArtifactSandboxHeaders(c);
3480
4477
  if (served != null) c.header("X-Artifact-Snapshot", String(served));
3481
4478
  return c.html(html);
3482
4479
  });
3483
4480
 
3484
4481
  // src/nodes/query-routes.ts
3485
- import { Hono as Hono7 } from "hono";
4482
+ import { Hono as Hono10 } from "hono";
3486
4483
  import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
3487
4484
 
3488
4485
  // src/tables/service.ts
@@ -4053,7 +5050,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
4053
5050
  }
4054
5051
 
4055
5052
  // src/nodes/query-routes.ts
4056
- var queryRoutes = new Hono7();
5053
+ var queryRoutes = new Hono10();
4057
5054
  queryRoutes.get("/changes", async (c) => {
4058
5055
  const nestId = c.req.param("nestId");
4059
5056
  const userId = c.get("userId");
@@ -4517,6 +5514,15 @@ queryRoutes.post("/publish", async (c) => {
4517
5514
  if (!body.documents?.length && !body.context_md) {
4518
5515
  throw new ValidationError("documents array or context_md is required");
4519
5516
  }
5517
+ for (const doc of body.documents || []) {
5518
+ const t = doc.type || "document";
5519
+ if (t === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
5520
+ throw new ValidationError("Artifact nodes are disabled on this server");
5521
+ }
5522
+ if (t === "table" && !config.TYPE_TABLE_ENABLED) {
5523
+ throw new ValidationError("Table nodes are disabled on this server");
5524
+ }
5525
+ }
4520
5526
  const nestId = c.req.param("nestId");
4521
5527
  const { storage } = await engineCache.get(nestId);
4522
5528
  const created = [];
@@ -4565,7 +5571,7 @@ queryRoutes.post("/publish", async (c) => {
4565
5571
  });
4566
5572
 
4567
5573
  // src/mcp/routes.ts
4568
- import { Hono as Hono9 } from "hono";
5574
+ import { Hono as Hono12 } from "hono";
4569
5575
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4570
5576
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
4571
5577
 
@@ -4645,7 +5651,7 @@ async function listTraceEvents(filters = {}) {
4645
5651
  }
4646
5652
 
4647
5653
  // src/mcp/workflow-tools.ts
4648
- import { v4 as uuid7 } from "uuid";
5654
+ import { v4 as uuid8 } from "uuid";
4649
5655
 
4650
5656
  // src/shared/node-id.ts
4651
5657
  function assertSafeNodeId(rawId) {
@@ -4672,7 +5678,7 @@ function assertSafeNodeId(rawId) {
4672
5678
  }
4673
5679
 
4674
5680
  // src/workflow/run-service.ts
4675
- import { v4 as uuid5 } from "uuid";
5681
+ import { v4 as uuid6 } from "uuid";
4676
5682
  var RUNNABLE_TYPES = ["agent", "skill"];
4677
5683
  async function flowSubgraph(nestId, agentNode) {
4678
5684
  const db = getDb();
@@ -4802,7 +5808,7 @@ async function triggerRun(opts) {
4802
5808
  }
4803
5809
  const edges = await flowSubgraph(nestId, agentNode);
4804
5810
  const definitions = await referencedDefinitions(nestId, edges);
4805
- const id = `run_${uuid5()}`;
5811
+ const id = `run_${uuid6()}`;
4806
5812
  const db = getDb();
4807
5813
  await db.transaction(async (tx) => {
4808
5814
  if (parentRunId) {
@@ -4963,8 +5969,8 @@ async function listRuns(nestId, opts) {
4963
5969
  }
4964
5970
 
4965
5971
  // src/workflow/edge-routes.ts
4966
- import { Hono as Hono8 } from "hono";
4967
- import { v4 as uuid6 } from "uuid";
5972
+ import { Hono as Hono11 } from "hono";
5973
+ import { v4 as uuid7 } from "uuid";
4968
5974
  function toResponse(e, t) {
4969
5975
  return {
4970
5976
  id: e.id,
@@ -5066,7 +6072,7 @@ async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
5066
6072
  }
5067
6073
  }
5068
6074
  }
5069
- var edgeRoutes = new Hono8();
6075
+ var edgeRoutes = new Hono11();
5070
6076
  edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
5071
6077
  const nestId = c.req.param("nestId");
5072
6078
  const db = getDb();
@@ -5134,7 +6140,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
5134
6140
  );
5135
6141
  const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
5136
6142
  const now = (/* @__PURE__ */ new Date()).toISOString();
5137
- const id = uuid6();
6143
+ const id = uuid7();
5138
6144
  await db.transaction(async (tx) => {
5139
6145
  if (type.is_flow) {
5140
6146
  await assertNoFlowCycle(tx, nestId, fromNode, toNode);
@@ -5526,7 +6532,7 @@ async function createEdge(ctx, args) {
5526
6532
  );
5527
6533
  const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
5528
6534
  const now = (/* @__PURE__ */ new Date()).toISOString();
5529
- const id = uuid7();
6535
+ const id = uuid8();
5530
6536
  await db.transaction(async (tx) => {
5531
6537
  if (type.is_flow) {
5532
6538
  await assertNoFlowCycle(tx, ctx.nestId, fromNode, toNode);
@@ -5589,7 +6595,7 @@ async function upsertEdgeType(ctx, args) {
5589
6595
  `INSERT INTO edge_types
5590
6596
  (id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
5591
6597
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5592
- [uuid7(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
6598
+ [uuid8(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
5593
6599
  );
5594
6600
  }
5595
6601
  });
@@ -5765,7 +6771,7 @@ var TOOL_DEFINITIONS = [
5765
6771
  },
5766
6772
  type: {
5767
6773
  type: "string",
5768
- description: "Node type (default: document)"
6774
+ description: "Node type (default: document). Note: artifact and table can be disabled server-wide by an admin \u2014 creating a disabled type is rejected; existing nodes of that type stay readable."
5769
6775
  },
5770
6776
  tags: {
5771
6777
  type: "array",
@@ -6466,19 +7472,19 @@ ${list}`;
6466
7472
  if (!target) return "target is required (a node id or folder prefix).";
6467
7473
  if (!["read", "write"].includes(role)) return "role must be read or write.";
6468
7474
  try {
6469
- const { createGrant: createGrant2 } = await import("./grants-service-6H4WUA2F.js");
6470
- const db = (await import("./client-VZLX4THW.js")).getDb();
7475
+ const { createGrant: createGrant2 } = await import("./grants-service-PS5RMMEA.js");
7476
+ const db = (await import("./client-YUOEVUFU.js")).getDb();
6471
7477
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
6472
7478
  const e = normalizeEmail2(String(args.email || ""));
6473
7479
  if (!e) return "email is required.";
6474
7480
  let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
6475
7481
  if (!row) {
6476
7482
  const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
6477
- const { v4: uuid15 } = await import("uuid");
6478
- const id = uuid15();
7483
+ const { v4: uuid16 } = await import("uuid");
7484
+ const id = uuid16();
6479
7485
  await db.run(
6480
7486
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
6481
- [id, e, null, await hashPassword2(uuid15())]
7487
+ [id, e, null, await hashPassword2(uuid16())]
6482
7488
  );
6483
7489
  row = { id };
6484
7490
  }
@@ -6496,7 +7502,7 @@ ${list}`;
6496
7502
  }
6497
7503
  }
6498
7504
  case "context_unsynced_list": {
6499
- if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
7505
+ if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
6500
7506
  return "You don't have permission to list unsynced folders. Server admin only.";
6501
7507
  }
6502
7508
  const folders = listUnsyncedFolders();
@@ -6509,7 +7515,7 @@ ${list}`;
6509
7515
  ${list}`;
6510
7516
  }
6511
7517
  case "context_sync_folder": {
6512
- if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
7518
+ if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
6513
7519
  return "You don't have permission to sync folders. Server admin only.";
6514
7520
  }
6515
7521
  try {
@@ -6533,7 +7539,7 @@ ${list}`;
6533
7539
 
6534
7540
  // src/mcp/routes.ts
6535
7541
  import { z } from "zod";
6536
- var mcpRoutes = new Hono9();
7542
+ var mcpRoutes = new Hono12();
6537
7543
  async function getUserEmail2(userId) {
6538
7544
  const db = getDb();
6539
7545
  const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
@@ -6599,8 +7605,8 @@ mcpRoutes.all("/", async (c) => {
6599
7605
  });
6600
7606
 
6601
7607
  // src/workflow/run-routes.ts
6602
- import { Hono as Hono10 } from "hono";
6603
- import { v4 as uuid8 } from "uuid";
7608
+ import { Hono as Hono13 } from "hono";
7609
+ import { v4 as uuid9 } from "uuid";
6604
7610
  var RUNNABLE_TYPES2 = ["agent", "skill"];
6605
7611
  var safeJson2 = (s) => {
6606
7612
  if (!s) return null;
@@ -6718,7 +7724,7 @@ async function buildRunBundle(nestId, agentNode) {
6718
7724
  definitions
6719
7725
  };
6720
7726
  }
6721
- var runTriggerRoutes = new Hono10();
7727
+ var runTriggerRoutes = new Hono13();
6722
7728
  async function resolveParentRun2(nestId, parentRunId, childAgent) {
6723
7729
  if (!config.FEATURE_SUBAGENT_RUNS) {
6724
7730
  throw new ValidationError(
@@ -6797,7 +7803,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
6797
7803
  }
6798
7804
  const bundle = await buildRunBundle(nestId, agentNode);
6799
7805
  const triggeredBy = await resolveCallerEmail(c.get("userId"));
6800
- const id = `run_${uuid8()}`;
7806
+ const id = `run_${uuid9()}`;
6801
7807
  await getDb().run(
6802
7808
  `INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
6803
7809
  VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
@@ -6826,7 +7832,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
6826
7832
  201
6827
7833
  );
6828
7834
  });
6829
- var runListRoutes = new Hono10();
7835
+ var runListRoutes = new Hono13();
6830
7836
  runListRoutes.get("/", requireWorkflowPlane, async (c) => {
6831
7837
  const nestId = c.req.param("nestId");
6832
7838
  const db = getDb();
@@ -6858,7 +7864,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
6858
7864
  }))
6859
7865
  });
6860
7866
  });
6861
- var runDetailRoutes = new Hono10();
7867
+ var runDetailRoutes = new Hono13();
6862
7868
  async function loadRunWithPermission(c, runId, required) {
6863
7869
  const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
6864
7870
  runId
@@ -7001,8 +8007,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
7001
8007
  });
7002
8008
 
7003
8009
  // src/workflow/schedule-routes.ts
7004
- import { Hono as Hono11 } from "hono";
7005
- import { v4 as uuid9 } from "uuid";
8010
+ import { Hono as Hono14 } from "hono";
8011
+ import { v4 as uuid10 } from "uuid";
7006
8012
  var RUNNABLE_TYPES3 = ["agent", "skill"];
7007
8013
  var MIN_EVERY = 5;
7008
8014
  var MAX_EVERY = 7 * 24 * 60;
@@ -7039,7 +8045,7 @@ function parseEvery(v) {
7039
8045
  }
7040
8046
  return n;
7041
8047
  }
7042
- var scheduleRoutes = new Hono11();
8048
+ var scheduleRoutes = new Hono14();
7043
8049
  scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
7044
8050
  const nestId = c.req.param("nestId");
7045
8051
  const rows = await getDb().all(
@@ -7080,7 +8086,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
7080
8086
  );
7081
8087
  }
7082
8088
  const now = (/* @__PURE__ */ new Date()).toISOString();
7083
- const id = `sch_${uuid9()}`;
8089
+ const id = `sch_${uuid10()}`;
7084
8090
  await db.run(
7085
8091
  `INSERT INTO schedules (id, nest_id, agent_node, every_minutes, enabled, last_run_at, max_runs, runs_created, created_by, created_at, updated_at)
7086
8092
  VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
@@ -7135,8 +8141,8 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
7135
8141
  });
7136
8142
 
7137
8143
  // src/notify/connector-routes.ts
7138
- import { Hono as Hono12 } from "hono";
7139
- import { v4 as uuid10 } from "uuid";
8144
+ import { Hono as Hono15 } from "hono";
8145
+ import { v4 as uuid11 } from "uuid";
7140
8146
  var KNOWN_EVENTS = [
7141
8147
  "review_requested",
7142
8148
  "review_approved",
@@ -7182,7 +8188,7 @@ function parseUrl(v) {
7182
8188
  'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
7183
8189
  );
7184
8190
  }
7185
- var connectorRoutes = new Hono12();
8191
+ var connectorRoutes = new Hono15();
7186
8192
  connectorRoutes.get("/", async (c) => {
7187
8193
  const nestId = c.req.param("nestId");
7188
8194
  const rows = await getDb().all(
@@ -7201,7 +8207,7 @@ connectorRoutes.post("/", async (c) => {
7201
8207
  const url = parseUrl(body.url);
7202
8208
  const events = parseEvents(body.events);
7203
8209
  const now = (/* @__PURE__ */ new Date()).toISOString();
7204
- const id = `con_${uuid10()}`;
8210
+ const id = `con_${uuid11()}`;
7205
8211
  await getDb().run(
7206
8212
  `INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
7207
8213
  VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
@@ -7244,7 +8250,7 @@ connectorRoutes.delete("/:id", async (c) => {
7244
8250
  });
7245
8251
 
7246
8252
  // src/workflow/hook-routes.ts
7247
- import { Hono as Hono13 } from "hono";
8253
+ import { Hono as Hono16 } from "hono";
7248
8254
  import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
7249
8255
  var PRESETS = ["slack", "teams", "webhook"];
7250
8256
  var RUNNABLE_TYPES4 = ["agent", "skill"];
@@ -7267,7 +8273,7 @@ function toResponse4(c, r, opts = {}) {
7267
8273
  created_at: r.created_at
7268
8274
  };
7269
8275
  }
7270
- var hookNestRoutes = new Hono13();
8276
+ var hookNestRoutes = new Hono16();
7271
8277
  hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
7272
8278
  const nestId = c.req.param("nestId");
7273
8279
  const agent = c.req.query("agent");
@@ -7341,7 +8347,7 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
7341
8347
  function stripMentions(text) {
7342
8348
  return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
7343
8349
  }
7344
- var hookFireRoutes = new Hono13();
8350
+ var hookFireRoutes = new Hono16();
7345
8351
  hookFireRoutes.post("/:token", async (c) => {
7346
8352
  const token = c.req.param("token");
7347
8353
  if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
@@ -7441,9 +8447,9 @@ hookFireRoutes.post("/:token", async (c) => {
7441
8447
  });
7442
8448
 
7443
8449
  // src/definitions/routes.ts
7444
- import { Hono as Hono14 } from "hono";
7445
- import { v4 as uuid11 } from "uuid";
7446
- var definitionRoutes = new Hono14();
8450
+ import { Hono as Hono17 } from "hono";
8451
+ import { v4 as uuid12 } from "uuid";
8452
+ var definitionRoutes = new Hono17();
7447
8453
  definitionRoutes.get("/", async (c) => {
7448
8454
  const nestId = c.req.param("nestId");
7449
8455
  const db = getDb();
@@ -7522,7 +8528,7 @@ definitionRoutes.post("/", async (c) => {
7522
8528
  await applyUpdate(tx, existing.id);
7523
8529
  targetId = existing.id;
7524
8530
  } else {
7525
- targetId = uuid11();
8531
+ targetId = uuid12();
7526
8532
  wasCreated = true;
7527
8533
  await tx.run(
7528
8534
  `INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
@@ -7551,41 +8557,69 @@ definitionRoutes.delete("/:id", async (c) => {
7551
8557
  });
7552
8558
 
7553
8559
  // src/nests/asset-routes.ts
7554
- import { Hono as Hono15 } from "hono";
7555
- import { v4 as uuid12 } from "uuid";
8560
+ import { Hono as Hono18 } from "hono";
8561
+ import { v4 as uuid13 } from "uuid";
7556
8562
  import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
7557
8563
  import { join as join4 } from "path";
7558
- var assetRoutes = new Hono15();
8564
+ var assetRoutes = new Hono18();
7559
8565
  var CONTENT_TYPES = {
7560
8566
  png: "image/png",
7561
8567
  jpg: "image/jpeg",
7562
8568
  jpeg: "image/jpeg",
7563
8569
  gif: "image/gif",
7564
- webp: "image/webp"
8570
+ webp: "image/webp",
8571
+ mp4: "video/mp4",
8572
+ webm: "video/webm"
7565
8573
  };
7566
- var SAFE_NAME = /^[0-9a-f-]{36}\.(png|jpe?g|gif|webp)$/;
8574
+ var VIDEO_EXTS = /* @__PURE__ */ new Set(["mp4", "webm"]);
8575
+ var IMAGE_MAX_BYTES = 10 * 1024 * 1024;
8576
+ var VIDEO_MAX_BYTES = 100 * 1024 * 1024;
8577
+ var ASSET_MAX_REQUEST_BYTES = VIDEO_MAX_BYTES + 1024 * 1024;
8578
+ var SAFE_NAME = /^[0-9a-f-]{36}\.(png|jpe?g|gif|webp|mp4|webm)$/;
7567
8579
  assetRoutes.post("/", async (c) => {
7568
8580
  const nestId = c.req.param("nestId");
8581
+ const lenStr = c.req.header("Content-Length");
8582
+ if (lenStr) {
8583
+ const len = parseInt(lenStr, 10);
8584
+ if (Number.isFinite(len) && len > ASSET_MAX_REQUEST_BYTES) {
8585
+ throw new ValidationError(
8586
+ `upload too large (${(len / (1024 * 1024)).toFixed(1)} MB, max ${VIDEO_MAX_BYTES / (1024 * 1024)} MB for video, ${IMAGE_MAX_BYTES / (1024 * 1024)} MB for images)`
8587
+ );
8588
+ }
8589
+ }
7569
8590
  const body = await c.req.parseBody();
7570
8591
  const file = body["file"];
7571
8592
  if (!(file instanceof File)) {
7572
- throw new ValidationError('multipart field "file" (an image) is required');
8593
+ throw new ValidationError(
8594
+ 'multipart field "file" (an image or video) is required'
8595
+ );
7573
8596
  }
7574
8597
  const ext = (file.name.split(".").pop() || "").toLowerCase();
7575
8598
  if (!CONTENT_TYPES[ext]) {
7576
8599
  throw new ValidationError(
7577
- `unsupported image type ".${ext}" \u2014 allowed: ${Object.keys(CONTENT_TYPES).join(", ")}`
8600
+ `unsupported asset type ".${ext}" \u2014 allowed: ${Object.keys(CONTENT_TYPES).join(", ")}`
7578
8601
  );
7579
8602
  }
7580
- const name = `${uuid12()}.${ext === "jpeg" ? "jpg" : ext}`;
8603
+ const isVideo = VIDEO_EXTS.has(ext);
8604
+ const maxBytes = isVideo ? VIDEO_MAX_BYTES : IMAGE_MAX_BYTES;
8605
+ if (file.size > maxBytes) {
8606
+ throw new ValidationError(
8607
+ `${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
8608
+ );
8609
+ }
8610
+ const name = `${uuid13()}.${ext === "jpeg" ? "jpg" : ext}`;
7581
8611
  const dir = join4(resolveNestPath(nestId), "assets");
7582
8612
  await mkdir2(dir, { recursive: true });
7583
8613
  await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
8614
+ const url = `/nests/${nestId}/assets/${name}`;
7584
8615
  return c.json(
7585
8616
  {
7586
8617
  file: name,
7587
- url: `/nests/${nestId}/assets/${name}`,
7588
- markdown: `![${file.name.replace(/\.[^.]+$/, "")}](/nests/${nestId}/assets/${name})`
8618
+ url,
8619
+ // Ready-to-paste reference: image syntax for images; videos are
8620
+ // referenced by bare URL on its own line (the read view upgrades
8621
+ // same-origin asset video URLs to a <video> player).
8622
+ markdown: isVideo ? url : `![${file.name.replace(/\.[^.]+$/, "")}](${url})`
7589
8623
  },
7590
8624
  201
7591
8625
  );
@@ -7606,12 +8640,15 @@ assetRoutes.get("/:file", async (c) => {
7606
8640
  return c.body(new Uint8Array(bytes), 200, {
7607
8641
  "Content-Type": CONTENT_TYPES[ext],
7608
8642
  // uuid names never change content — cache hard.
7609
- "Cache-Control": "public, max-age=31536000, immutable"
8643
+ "Cache-Control": "public, max-age=31536000, immutable",
8644
+ // Never let the browser second-guess the declared type on direct
8645
+ // navigation (same posture as annotation attachments).
8646
+ "X-Content-Type-Options": "nosniff"
7610
8647
  });
7611
8648
  });
7612
8649
 
7613
8650
  // src/mcp/server-routes.ts
7614
- import { Hono as Hono16 } from "hono";
8651
+ import { Hono as Hono19 } from "hono";
7615
8652
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
7616
8653
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
7617
8654
  import { z as z2 } from "zod";
@@ -7852,7 +8889,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
7852
8889
  }
7853
8890
  return server;
7854
8891
  }
7855
- var serverMcpRoutes = new Hono16();
8892
+ var serverMcpRoutes = new Hono19();
7856
8893
  serverMcpRoutes.all("/", async (c) => {
7857
8894
  const userId = c.get("userId");
7858
8895
  const userEmail = await getUserEmail3(userId);
@@ -7874,7 +8911,7 @@ serverMcpRoutes.all("/", async (c) => {
7874
8911
  await server.server.close();
7875
8912
  }
7876
8913
  });
7877
- var nestIndexRoutes = new Hono16();
8914
+ var nestIndexRoutes = new Hono19();
7878
8915
  nestIndexRoutes.get("/", async (c) => {
7879
8916
  const userId = c.get("userId");
7880
8917
  const nests = await buildNestIndex(userId, c.get("nestScope"));
@@ -7944,6 +8981,7 @@ Other read endpoints (all under \`${B}/nests/<nest-id>\`):
7944
8981
  - \`GET /search?q=<terms>\` \u2014 full-text search
7945
8982
  - \`GET /overview\` \u2014 node count, types, tags
7946
8983
  - \`GET /nodes\` \u2014 list; \`GET /nodes/<node-id>?format=markdown\` \u2014 one document
8984
+ - \`GET /nodes/<node-id>?format=raw\` \u2014 direct consumption path: the node BODY as plain text (no JSON envelope; add \`&frontmatter=1\` for the stored YAML block)
7947
8985
  - \`GET /export?format=markdown\` \u2014 whole nest as one markdown file (optional \`selector\`, \`max_tokens\`)
7948
8986
  - \`GET /graph\` \u2014 ontology graph (nodes, links, tags, stewards)
7949
8987
 
@@ -7996,17 +9034,145 @@ npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
7996
9034
  through steward review before they become AI-visible.
7997
9035
  `;
7998
9036
  }
7999
- var llmsTxtRoutes = new Hono16();
9037
+ var llmsTxtRoutes = new Hono19();
8000
9038
  llmsTxtRoutes.get("/", (c) => {
8001
9039
  const md = renderLlmsTxt(requestBaseUrl(c.req.url));
8002
9040
  return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
8003
9041
  });
8004
9042
 
8005
9043
  // src/governance/routes.ts
8006
- import { Hono as Hono17 } from "hono";
9044
+ import { Hono as Hono20 } from "hono";
9045
+
9046
+ // src/governance/feed-service.ts
9047
+ var SNIPPET_CHARS = 200;
9048
+ function snippet(body) {
9049
+ const flat = body.replace(/\s+/g, " ").trim();
9050
+ return flat.length > SNIPPET_CHARS ? `${flat.slice(0, SNIPPET_CHARS)}\u2026` : flat;
9051
+ }
9052
+ async function getNestFeed(params) {
9053
+ const db = getDb();
9054
+ const filter = params.filter ?? "all";
9055
+ const limit = Number.isFinite(params.limit) && params.limit > 0 ? Math.min(params.limit, 200) : 50;
9056
+ const offset = Number.isFinite(params.offset) && params.offset > 0 ? params.offset : 0;
9057
+ const fetchWindow = offset + limit;
9058
+ const titles = await buildTitleMap(params.nestId);
9059
+ let changes = [];
9060
+ let changesTotal = 0;
9061
+ {
9062
+ changesTotal = (await db.get(
9063
+ "SELECT COUNT(*) AS c FROM review_requests WHERE nest_id = ? AND status = 'pending'",
9064
+ [params.nestId]
9065
+ )).c;
9066
+ if (filter !== "comments" && changesTotal > 0) {
9067
+ const rows = await db.all(
9068
+ `SELECT * FROM review_requests
9069
+ WHERE nest_id = ? AND status = 'pending'
9070
+ ORDER BY requested_at DESC, id DESC
9071
+ LIMIT ?`,
9072
+ [params.nestId, fetchWindow]
9073
+ );
9074
+ const distinctNodeIds = [
9075
+ ...new Set(rows.map((r) => r.node_id))
9076
+ ];
9077
+ const canReviewByNode = new Map(
9078
+ await Promise.all(
9079
+ distinctNodeIds.map(
9080
+ async (nodeId) => [
9081
+ nodeId,
9082
+ (await canUserApprove(params.nestId, nodeId, params.userEmail)).allowed
9083
+ ]
9084
+ )
9085
+ )
9086
+ );
9087
+ changes = rows.map((row) => ({
9088
+ type: "change",
9089
+ id: row.id,
9090
+ nestId: row.nest_id,
9091
+ nodeId: row.node_id,
9092
+ title: titles.get(row.node_id),
9093
+ version: row.version,
9094
+ requestedBy: row.requested_by,
9095
+ requestedAt: row.requested_at,
9096
+ requestNote: row.request_note || void 0,
9097
+ status: row.status,
9098
+ resolvedBy: row.resolved_by || void 0,
9099
+ resolvedAt: row.resolved_at || void 0,
9100
+ resolutionNote: row.resolution_note || void 0,
9101
+ priority: row.priority,
9102
+ canReview: canReviewByNode.get(row.node_id) ?? false,
9103
+ createdAt: row.requested_at
9104
+ }));
9105
+ }
9106
+ }
9107
+ let comments = [];
9108
+ let commentsTotal = 0;
9109
+ {
9110
+ const nodeRows = await db.all(
9111
+ "SELECT DISTINCT node_id AS id FROM annotation_threads WHERE nest_id = ?",
9112
+ [params.nestId]
9113
+ );
9114
+ const accessible = await filterAccessible(
9115
+ params.nestId,
9116
+ params.userId,
9117
+ params.userEmail,
9118
+ nodeRows
9119
+ );
9120
+ if (accessible.length > 0) {
9121
+ const placeholders = accessible.map(() => "?").join(",");
9122
+ const ids = accessible.map((n) => n.id);
9123
+ commentsTotal = (await db.get(
9124
+ `SELECT COUNT(*) AS c
9125
+ FROM annotation_comments c
9126
+ JOIN annotation_threads t ON t.id = c.thread_id
9127
+ WHERE t.nest_id = ? AND t.node_id IN (${placeholders})`,
9128
+ [params.nestId, ...ids]
9129
+ )).c;
9130
+ if (filter !== "changes" && commentsTotal > 0) {
9131
+ const rows = await db.all(
9132
+ `SELECT c.id, c.thread_id, c.author, c.body, c.created_at,
9133
+ t.node_id, t.status,
9134
+ CASE WHEN c.id = (
9135
+ SELECT c2.id FROM annotation_comments c2
9136
+ WHERE c2.thread_id = c.thread_id
9137
+ ORDER BY c2.created_at ASC, c2.id ASC LIMIT 1
9138
+ ) THEN 0 ELSE 1 END AS is_reply
9139
+ FROM annotation_comments c
9140
+ JOIN annotation_threads t ON t.id = c.thread_id
9141
+ WHERE t.nest_id = ? AND t.node_id IN (${placeholders})
9142
+ ORDER BY c.created_at DESC, c.id DESC
9143
+ LIMIT ?`,
9144
+ [params.nestId, ...ids, fetchWindow]
9145
+ );
9146
+ comments = rows.map((row) => ({
9147
+ type: "comment",
9148
+ id: row.id,
9149
+ threadId: row.thread_id,
9150
+ nodeId: row.node_id,
9151
+ title: titles.get(row.node_id),
9152
+ author: row.author,
9153
+ snippet: snippet(row.body),
9154
+ createdAt: row.created_at,
9155
+ resolved: row.status === "resolved",
9156
+ isReply: Number(row.is_reply) === 1
9157
+ }));
9158
+ }
9159
+ }
9160
+ }
9161
+ const merged = [...changes, ...comments].sort((a, b) => {
9162
+ const byTime = b.createdAt.localeCompare(a.createdAt);
9163
+ if (byTime !== 0) return byTime;
9164
+ return b.id.localeCompare(a.id);
9165
+ });
9166
+ const total = filter === "changes" ? changesTotal : filter === "comments" ? commentsTotal : changesTotal + commentsTotal;
9167
+ return {
9168
+ items: merged.slice(offset, offset + limit),
9169
+ total,
9170
+ counts: { changes: changesTotal, comments: commentsTotal }
9171
+ };
9172
+ }
8007
9173
 
8008
9174
  // src/governance/comment-service.ts
8009
- import { v4 as uuid13 } from "uuid";
9175
+ import { v4 as uuid14 } from "uuid";
8010
9176
  async function createComment(params) {
8011
9177
  const db = getDb();
8012
9178
  const body = (params.body ?? "").trim();
@@ -8022,7 +9188,7 @@ async function createComment(params) {
8022
9188
  throw new Error("Parent comment not found on this node");
8023
9189
  }
8024
9190
  }
8025
- const id = uuid13();
9191
+ const id = uuid14();
8026
9192
  await db.run(
8027
9193
  `INSERT INTO comments
8028
9194
  (id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
@@ -8307,7 +9473,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
8307
9473
  if (Number.isNaN(n) || n < 1) return def;
8308
9474
  return Math.min(n, max);
8309
9475
  }
8310
- var governanceRoutes = new Hono17();
9476
+ var governanceRoutes = new Hono20();
8311
9477
  governanceRoutes.get("/stewards", async (c) => {
8312
9478
  const nestId = c.req.param("nestId");
8313
9479
  const scope = c.req.query("scope");
@@ -8424,6 +9590,25 @@ governanceRoutes.get("/review-queue", async (c) => {
8424
9590
  }
8425
9591
  return c.json({ ...result, requests });
8426
9592
  });
9593
+ governanceRoutes.get("/feed", async (c) => {
9594
+ const nestId = c.req.param("nestId");
9595
+ const filterRaw = c.req.query("filter");
9596
+ const filter = filterRaw === "changes" || filterRaw === "comments" ? filterRaw : "all";
9597
+ const limit = parseLimit(c.req.query("limit"), 50, 200);
9598
+ const offsetRaw = parseInt(c.req.query("offset") ?? "", 10);
9599
+ const offset = Number.isNaN(offsetRaw) || offsetRaw < 0 ? 0 : Math.min(offsetRaw, 1e4);
9600
+ const userId = c.get("userId");
9601
+ const userEmail = await getUserEmail4(c);
9602
+ const result = await getNestFeed({
9603
+ nestId,
9604
+ userId,
9605
+ userEmail,
9606
+ filter,
9607
+ limit,
9608
+ offset
9609
+ });
9610
+ return c.json({ ...result, filter, limit, offset });
9611
+ });
8427
9612
  governanceRoutes.get("/external-edits", async (c) => {
8428
9613
  const nestId = c.req.param("nestId");
8429
9614
  const refresh = c.req.query("refresh") === "true";
@@ -8445,7 +9630,7 @@ governanceRoutes.get("/activity", async (c) => {
8445
9630
  const activity = await getActivity({ nestId, limit });
8446
9631
  return c.json({ activity });
8447
9632
  });
8448
- var governanceNodeRoutes = new Hono17();
9633
+ var governanceNodeRoutes = new Hono20();
8449
9634
  governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
8450
9635
  const nestId = c.req.param("nestId");
8451
9636
  const nodeId = c.req.param("nodeId");
@@ -8597,6 +9782,67 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
8597
9782
  201
8598
9783
  );
8599
9784
  });
9785
+ governanceNodeRoutes.post(
9786
+ "/:nodeId{.+}/external-edits/:suggestionId/approve",
9787
+ async (c) => {
9788
+ const nestId = c.req.param("nestId");
9789
+ const nodeId = c.req.param("nodeId");
9790
+ const suggestionId = c.req.param("suggestionId");
9791
+ const body = await c.req.json().catch(() => ({}));
9792
+ const actor = await getUserEmail4(c);
9793
+ try {
9794
+ const result = await approveExternalEdit({
9795
+ nestId,
9796
+ documentId: nodeId,
9797
+ suggestionId,
9798
+ actor,
9799
+ comment: body.comment
9800
+ });
9801
+ return c.json({
9802
+ approved: true,
9803
+ version: result.versionEntry.version,
9804
+ chainEvent: result.chainEvent.event_id
9805
+ });
9806
+ } catch (err) {
9807
+ console.error(
9808
+ `[external-edit-route] approve failed nest=${nestId} node=${nodeId} suggestion=${suggestionId}`,
9809
+ { message: err?.message, name: err?.name, stack: err?.stack }
9810
+ );
9811
+ return c.json(
9812
+ { error: err.message, name: err?.name },
9813
+ 400
9814
+ );
9815
+ }
9816
+ }
9817
+ );
9818
+ governanceNodeRoutes.post(
9819
+ "/:nodeId{.+}/external-edits/:suggestionId/reject",
9820
+ async (c) => {
9821
+ const nestId = c.req.param("nestId");
9822
+ const nodeId = c.req.param("nodeId");
9823
+ const suggestionId = c.req.param("suggestionId");
9824
+ const body = await c.req.json().catch(() => ({}));
9825
+ if (!body.reason) {
9826
+ throw new ValidationError("Rejection reason is required");
9827
+ }
9828
+ const actor = await getUserEmail4(c);
9829
+ try {
9830
+ const result = await rejectExternalEdit({
9831
+ nestId,
9832
+ documentId: nodeId,
9833
+ suggestionId,
9834
+ actor,
9835
+ reason: body.reason
9836
+ });
9837
+ return c.json({
9838
+ rejected: true,
9839
+ chainEvent: result.chainEvent.event_id
9840
+ });
9841
+ } catch (err) {
9842
+ return c.json({ error: err.message }, 400);
9843
+ }
9844
+ }
9845
+ );
8600
9846
  governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
8601
9847
  const nestId = c.req.param("nestId");
8602
9848
  const nodeId = c.req.param("nodeId");
@@ -8681,67 +9927,6 @@ governanceNodeRoutes.get(
8681
9927
  return c.json({ entry: detail });
8682
9928
  }
8683
9929
  );
8684
- governanceNodeRoutes.post(
8685
- "/:nodeId{.+}/external-edits/:suggestionId/approve",
8686
- async (c) => {
8687
- const nestId = c.req.param("nestId");
8688
- const nodeId = c.req.param("nodeId");
8689
- const suggestionId = c.req.param("suggestionId");
8690
- const body = await c.req.json().catch(() => ({}));
8691
- const actor = await getUserEmail4(c);
8692
- try {
8693
- const result = await approveExternalEdit({
8694
- nestId,
8695
- documentId: nodeId,
8696
- suggestionId,
8697
- actor,
8698
- comment: body.comment
8699
- });
8700
- return c.json({
8701
- approved: true,
8702
- version: result.versionEntry.version,
8703
- chainEvent: result.chainEvent.event_id
8704
- });
8705
- } catch (err) {
8706
- console.error(
8707
- `[external-edit-route] approve failed nest=${nestId} node=${nodeId} suggestion=${suggestionId}`,
8708
- { message: err?.message, name: err?.name, stack: err?.stack }
8709
- );
8710
- return c.json(
8711
- { error: err.message, name: err?.name },
8712
- 400
8713
- );
8714
- }
8715
- }
8716
- );
8717
- governanceNodeRoutes.post(
8718
- "/:nodeId{.+}/external-edits/:suggestionId/reject",
8719
- async (c) => {
8720
- const nestId = c.req.param("nestId");
8721
- const nodeId = c.req.param("nodeId");
8722
- const suggestionId = c.req.param("suggestionId");
8723
- const body = await c.req.json().catch(() => ({}));
8724
- if (!body.reason) {
8725
- throw new ValidationError("Rejection reason is required");
8726
- }
8727
- const actor = await getUserEmail4(c);
8728
- try {
8729
- const result = await rejectExternalEdit({
8730
- nestId,
8731
- documentId: nodeId,
8732
- suggestionId,
8733
- actor,
8734
- reason: body.reason
8735
- });
8736
- return c.json({
8737
- rejected: true,
8738
- chainEvent: result.chainEvent.event_id
8739
- });
8740
- } catch (err) {
8741
- return c.json({ error: err.message }, 400);
8742
- }
8743
- }
8744
- );
8745
9930
  governanceNodeRoutes.post("/:nodeId{.+}/cancel-review", async (c) => {
8746
9931
  const nestId = c.req.param("nestId");
8747
9932
  const nodeId = c.req.param("nodeId");
@@ -8833,20 +10018,22 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
8833
10018
  return c.json({ error: "Missing or invalid credentials" }, 401);
8834
10019
  });
8835
10020
  function createApp() {
8836
- const app = new Hono18({ router: new LinearRouter() });
10021
+ const app = new Hono21({ router: new LinearRouter() });
8837
10022
  const corsOrigins = config.CORS_ORIGINS;
8838
10023
  app.use(
8839
10024
  "*",
8840
10025
  corsOrigins === "*" ? cors({ origin: "*" }) : cors({ origin: corsOrigins, credentials: true })
8841
10026
  );
10027
+ const ASSET_UPLOAD_PATH = /^\/nests\/[^/]+\/assets\/?$/;
8842
10028
  app.use("*", async (c, next) => {
8843
10029
  const lenStr = c.req.header("Content-Length");
8844
10030
  if (lenStr) {
8845
10031
  const len = parseInt(lenStr, 10);
8846
- if (Number.isFinite(len) && len > config.MAX_BODY_BYTES) {
10032
+ const cap = c.req.method === "POST" && ASSET_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, ASSET_MAX_REQUEST_BYTES) : config.MAX_BODY_BYTES;
10033
+ if (Number.isFinite(len) && len > cap) {
8847
10034
  return c.json(
8848
10035
  {
8849
- error: `Request body too large (${len} bytes, max ${config.MAX_BODY_BYTES})`
10036
+ error: `Request body too large (${len} bytes, max ${cap})`
8850
10037
  },
8851
10038
  413
8852
10039
  );
@@ -8870,9 +10057,9 @@ function createApp() {
8870
10057
  durationMs: Date.now() - started
8871
10058
  });
8872
10059
  });
8873
- app.get(
8874
- "/health",
8875
- (c) => c.json({
10060
+ app.get("/health", (c) => {
10061
+ const oidcReady = isOidcReady();
10062
+ return c.json({
8876
10063
  status: isSuspended() ? "suspended" : "ok",
8877
10064
  service: "contextnest-community",
8878
10065
  version: SERVICE_VERSION,
@@ -8886,10 +10073,24 @@ function createApp() {
8886
10073
  // The login page reads this to show/hide the credential form, and to
8887
10074
  // switch the Register link to an "activate invite" affordance.
8888
10075
  manual_sign_in: config.MANUAL_SIGN_IN,
10076
+ // OIDC SSO — true only when enabled AND fully configured, so the login
10077
+ // page never shows a button that would bounce with an error. The issuer
10078
+ // is exposed (when live) so the client can label the button by IdP
10079
+ // (login.microsoftonline.com → Microsoft, accounts.google.com → Google).
10080
+ oidc_enabled: oidcReady,
10081
+ ...oidcReady && { oidc_issuer: config.OIDC_ISSUER },
8889
10082
  // Public feature flags the SPA needs before auth to decide what to
8890
10083
  // render. Mirrors the server-side route gate so UI and API agree:
8891
10084
  // flag off → the plane's routes 404 AND its UI stays hidden.
8892
10085
  workflow_plane_enabled: config.FEATURE_WORKFLOW_PLANE,
10086
+ // Import teams from PromptOwl (default OFF). Off → /teams/promptowl 404s
10087
+ // and the UI hides the PromptOwl Teams panel.
10088
+ promptowl_teams_enabled: config.PROMPTOWL_TEAMS_ENABLED,
10089
+ // Content-type toggles (default ON). The editor's type picker mirrors
10090
+ // these so a disabled type disappears from creation exactly when the
10091
+ // API would refuse it — existing nodes of the type stay readable.
10092
+ type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
10093
+ type_table_enabled: config.TYPE_TABLE_ENABLED,
8893
10094
  // Sub-agent runs gate its own recursion surface separately (env-only,
8894
10095
  // must clear a security review — NOT a UI toggle). Exposed read-only so
8895
10096
  // the Runs page shows the "spawn sub-agent" option only when it's live.
@@ -8901,8 +10102,8 @@ function createApp() {
8901
10102
  subagent_max_depth: config.SUBAGENT_MAX_DEPTH,
8902
10103
  subagent_max_children: config.SUBAGENT_MAX_CHILDREN,
8903
10104
  ...isSuspended() && { suspended_reason: getSuspensionReason() }
8904
- })
8905
- );
10105
+ });
10106
+ });
8906
10107
  app.use("/auth/invite", async (c, next) => {
8907
10108
  if (!getCurrentLicense()?.valid) {
8908
10109
  return c.json(
@@ -8983,8 +10184,8 @@ function createApp() {
8983
10184
  return c.json({ error: msg }, 500);
8984
10185
  }
8985
10186
  });
10187
+ const serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
8986
10188
  app.use("/admin/settings", flexAuthMiddleware);
8987
- const adminSettingsAllowed = async (c) => config.AUTH_MODE === "open" || await isLicenseAdminUserId(c.get("userId"));
8988
10189
  const currentServerSettings = () => ({
8989
10190
  promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
8990
10191
  manual_sign_in: config.MANUAL_SIGN_IN,
@@ -8993,10 +10194,27 @@ function createApp() {
8993
10194
  public_base_url: config.PUBLIC_BASE_URL,
8994
10195
  max_body_bytes: config.MAX_BODY_BYTES,
8995
10196
  workflow_plane_enabled: config.FEATURE_WORKFLOW_PLANE,
10197
+ promptowl_teams_enabled: config.PROMPTOWL_TEAMS_ENABLED,
10198
+ type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
10199
+ type_table_enabled: config.TYPE_TABLE_ENABLED,
8996
10200
  subagent_runs_enabled: config.FEATURE_SUBAGENT_RUNS,
8997
10201
  subagent_max_depth: config.SUBAGENT_MAX_DEPTH,
8998
10202
  subagent_max_children: config.SUBAGENT_MAX_CHILDREN,
8999
10203
  trace_retention_days: config.TRACE_RETENTION_DAYS,
10204
+ // OIDC SSO. The client secret is WRITE-ONLY: only its presence is
10205
+ // reported, never the value (same discipline as the Anthropic key below).
10206
+ oidc_enabled: config.OIDC_ENABLED,
10207
+ oidc_issuer: config.OIDC_ISSUER,
10208
+ oidc_client_id: config.OIDC_CLIENT_ID,
10209
+ oidc_client_secret_set: !!config.OIDC_CLIENT_SECRET,
10210
+ oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
10211
+ oidc_auto_provision: config.OIDC_AUTO_PROVISION,
10212
+ oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
10213
+ // Live readiness (enabled AND issuer+id+secret present) — the SAVED state,
10214
+ // i.e. whether the login button is currently live. The Settings UI derives
10215
+ // its own "enabled but not configured" warning from the unsaved draft
10216
+ // instead, so this is for API consumers (and tests) rather than that badge.
10217
+ oidc_ready: isOidcReady(),
9000
10218
  // Server-wide runner default. NEVER the value — masked tail only.
9001
10219
  anthropic_api_key_masked: process.env.ANTHROPIC_API_KEY ? `\u2022\u2022\u2022\u2022${process.env.ANTHROPIC_API_KEY.slice(-4)}` : null,
9002
10220
  slack_webhook_url: config.SLACK_WEBHOOK_URL ?? "",
@@ -9005,12 +10223,12 @@ function createApp() {
9005
10223
  notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
9006
10224
  });
9007
10225
  app.get("/admin/settings", async (c) => {
9008
- if (!await adminSettingsAllowed(c))
10226
+ if (!await serverAdminAllowed(c))
9009
10227
  return c.json({ error: "Only the server admin can view this." }, 403);
9010
10228
  return c.json(currentServerSettings());
9011
10229
  });
9012
10230
  app.patch("/admin/settings", async (c) => {
9013
- if (!await adminSettingsAllowed(c))
10231
+ if (!await serverAdminAllowed(c))
9014
10232
  return c.json({ error: "Only the server admin can change this." }, 403);
9015
10233
  let body;
9016
10234
  try {
@@ -9052,6 +10270,24 @@ function createApp() {
9052
10270
  value: body.workflow_plane_enabled ? "true" : null
9053
10271
  });
9054
10272
  }
10273
+ if ("promptowl_teams_enabled" in body) {
10274
+ pending.push({
10275
+ name: "PROMPTOWL_TEAMS_ENABLED",
10276
+ value: body.promptowl_teams_enabled ? "true" : null
10277
+ });
10278
+ }
10279
+ if ("type_artifact_enabled" in body) {
10280
+ pending.push({
10281
+ name: "TYPE_ARTIFACT_ENABLED",
10282
+ value: body.type_artifact_enabled ? null : "false"
10283
+ });
10284
+ }
10285
+ if ("type_table_enabled" in body) {
10286
+ pending.push({
10287
+ name: "TYPE_TABLE_ENABLED",
10288
+ value: body.type_table_enabled ? null : "false"
10289
+ });
10290
+ }
9055
10291
  if ("anthropic_api_key" in body) {
9056
10292
  const v = body.anthropic_api_key;
9057
10293
  if (v === null || v === "") {
@@ -9109,6 +10345,74 @@ function createApp() {
9109
10345
  else pending.push({ name: "TRACE_RETENTION_DAYS", value: String(n) });
9110
10346
  }
9111
10347
  }
10348
+ if ("oidc_issuer" in body) {
10349
+ const v = String(body.oidc_issuer ?? "").trim().replace(/\/$/, "");
10350
+ let ok = false;
10351
+ try {
10352
+ ok = !v || new URL(v).protocol === "https:";
10353
+ } catch {
10354
+ ok = false;
10355
+ }
10356
+ if (!ok)
10357
+ addError("oidc_issuer", "OIDC issuer must be a valid https:// URL (e.g. https://accounts.google.com), or empty to clear it.");
10358
+ else pending.push({ name: "OIDC_ISSUER", value: v || null });
10359
+ }
10360
+ if ("oidc_client_id" in body) {
10361
+ const v = String(body.oidc_client_id ?? "").trim();
10362
+ pending.push({ name: "OIDC_CLIENT_ID", value: v || null });
10363
+ }
10364
+ if ("oidc_client_secret" in body) {
10365
+ const v = body.oidc_client_secret;
10366
+ if (v === null || v === "") {
10367
+ pending.push({ name: "OIDC_CLIENT_SECRET", value: null });
10368
+ } else if (typeof v !== "string") {
10369
+ addError("oidc_client_secret", "OIDC client secret must be a string (or empty to clear it).");
10370
+ } else {
10371
+ pending.push({ name: "OIDC_CLIENT_SECRET", value: v.trim() });
10372
+ }
10373
+ }
10374
+ if ("oidc_allowed_domains" in body) {
10375
+ const raw = String(body.oidc_allowed_domains ?? "").trim();
10376
+ const entries = raw.split(",").map((d) => d.trim().toLowerCase()).filter(Boolean);
10377
+ const bad = entries.find((d) => !/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(d));
10378
+ if (bad !== void 0)
10379
+ addError("oidc_allowed_domains", `"${bad}" is not a valid domain \u2014 use bare domains, comma-separated (e.g. acme.com, contractors.acme.com), or leave empty to allow any.`);
10380
+ else
10381
+ pending.push({
10382
+ name: "OIDC_ALLOWED_DOMAINS",
10383
+ value: entries.length ? entries.join(",") : null
10384
+ });
10385
+ }
10386
+ if ("oidc_auto_provision" in body) {
10387
+ pending.push({
10388
+ name: "OIDC_AUTO_PROVISION",
10389
+ value: body.oidc_auto_provision ? "true" : "false"
10390
+ });
10391
+ }
10392
+ if ("oidc_department_tagging" in body) {
10393
+ pending.push({
10394
+ name: "OIDC_DEPARTMENT_TAGGING",
10395
+ value: body.oidc_department_tagging ? "true" : null
10396
+ });
10397
+ }
10398
+ const effOidcIssuer = "oidc_issuer" in body ? String(body.oidc_issuer ?? "").trim() : config.OIDC_ISSUER;
10399
+ const effOidcClientId = "oidc_client_id" in body ? String(body.oidc_client_id ?? "").trim() : config.OIDC_CLIENT_ID;
10400
+ const effOidcSecret = "oidc_client_secret" in body ? String(body.oidc_client_secret ?? "").trim() : config.OIDC_CLIENT_SECRET;
10401
+ const effOidcEnabled = "oidc_enabled" in body ? !!body.oidc_enabled : config.OIDC_ENABLED;
10402
+ const effOidcReady = effOidcEnabled && !!effOidcIssuer && !!effOidcClientId && !!effOidcSecret;
10403
+ if ("oidc_enabled" in body) {
10404
+ if (body.oidc_enabled && !effOidcReady) {
10405
+ addError(
10406
+ "oidc_enabled",
10407
+ "Set the OIDC issuer, client ID, and client secret before turning single sign-on on."
10408
+ );
10409
+ } else {
10410
+ pending.push({
10411
+ name: "OIDC_ENABLED",
10412
+ value: body.oidc_enabled ? "true" : null
10413
+ });
10414
+ }
10415
+ }
9112
10416
  if ("slack_webhook_url" in body) {
9113
10417
  const v = String(body.slack_webhook_url ?? "").trim();
9114
10418
  if (v && !/^https:\/\//i.test(v))
@@ -9143,10 +10447,13 @@ function createApp() {
9143
10447
  }
9144
10448
  const effManual = "manual_sign_in" in body ? String(body.manual_sign_in ?? "").trim().toLowerCase() : config.MANUAL_SIGN_IN;
9145
10449
  const effGate = "promptowl_sign_in_gate" in body ? String(body.promptowl_sign_in_gate ?? "").trim().toLowerCase() : config.PROMPTOWL_SIGN_IN_GATE;
9146
- if (effManual === "disabled" && effGate === "disabled") {
10450
+ if (effManual === "disabled" && effGate === "disabled" && !effOidcReady) {
10451
+ const touchedOidcOnly = !("manual_sign_in" in body) && !("promptowl_sign_in_gate" in body) && ["oidc_enabled", "oidc_issuer", "oidc_client_id", "oidc_client_secret"].some(
10452
+ (k) => k in body
10453
+ );
9147
10454
  addError(
9148
- "manual_sign_in",
9149
- "Cannot disable both manual and PromptOwl sign-in \u2014 no one would be able to log in."
10455
+ touchedOidcOnly ? "oidc_enabled" : "manual_sign_in",
10456
+ "Cannot disable both manual and PromptOwl sign-in unless SSO (OIDC) is enabled and configured \u2014 no one would be able to log in."
9150
10457
  );
9151
10458
  }
9152
10459
  const errorMessages = Object.values(fieldErrors);
@@ -9188,14 +10495,8 @@ function createApp() {
9188
10495
  return c.json({ settings: currentServerSettings() });
9189
10496
  });
9190
10497
  app.use("/admin/trace", flexAuthMiddleware);
9191
- const traceAllowed = async (c) => {
9192
- if (config.AUTH_MODE === "open") return true;
9193
- const userId = c.get("userId");
9194
- if (await isLicenseAdminUserId(userId)) return true;
9195
- return isSuperAdmin(await resolveCallerEmail(userId));
9196
- };
9197
10498
  app.get("/admin/trace", async (c) => {
9198
- if (!await traceAllowed(c))
10499
+ if (!await serverAdminAllowed(c))
9199
10500
  return c.json({ error: "Only the server admin can view this." }, 403);
9200
10501
  const limitRaw = Number(c.req.query("limit"));
9201
10502
  const offsetRaw = Number(c.req.query("offset"));
@@ -9210,6 +10511,124 @@ function createApp() {
9210
10511
  });
9211
10512
  return c.json({ count: events.length, total, limit, offset, events });
9212
10513
  });
10514
+ app.use("/admin/super-admins", flexAuthMiddleware);
10515
+ app.use("/admin/super-admins/*", flexAuthMiddleware);
10516
+ app.get("/admin/super-admins", async (c) => {
10517
+ if (!await serverAdminAllowed(c))
10518
+ return c.json({ error: "Only the server admin can view this." }, 403);
10519
+ const callerEmail = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
10520
+ const byEmail = /* @__PURE__ */ new Map();
10521
+ const add = (row) => {
10522
+ if (!byEmail.has(row.email)) byEmail.set(row.email, row);
10523
+ };
10524
+ const lic = getCurrentLicense();
10525
+ if (lic?.valid && lic.ownerEmail) {
10526
+ const email = lic.ownerEmail.toLowerCase();
10527
+ add({ email, source: "license", granted_by: null, granted_at: null, self: email === callerEmail });
10528
+ }
10529
+ for (const raw of getAccessConfig()?.super_admins ?? []) {
10530
+ const email = raw.toLowerCase();
10531
+ add({ email, source: "config", granted_by: null, granted_at: null, self: email === callerEmail });
10532
+ }
10533
+ const granted = await getDb().all(
10534
+ "SELECT email, granted_by, granted_at FROM super_admins ORDER BY granted_at, email"
10535
+ );
10536
+ for (const g of granted) {
10537
+ const email = g.email.toLowerCase();
10538
+ add({ email, source: "granted", granted_by: g.granted_by, granted_at: g.granted_at, self: email === callerEmail });
10539
+ }
10540
+ return c.json({ super_admins: [...byEmail.values()] });
10541
+ });
10542
+ app.post("/admin/super-admins", async (c) => {
10543
+ if (!await serverAdminAllowed(c))
10544
+ return c.json({ error: "Only the server admin can grant superadmin." }, 403);
10545
+ let body;
10546
+ try {
10547
+ body = await c.req.json();
10548
+ } catch {
10549
+ return c.json({ error: "Invalid JSON body" }, 400);
10550
+ }
10551
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
10552
+ if (!email || !isEmailish(email)) {
10553
+ return c.json({ error: "A valid email is required." }, 400);
10554
+ }
10555
+ const user = await getDb().get(
10556
+ "SELECT id FROM users WHERE LOWER(email) = ?",
10557
+ [email]
10558
+ );
10559
+ if (!user) {
10560
+ return c.json(
10561
+ { error: "No user with this email exists on this server. Invite them as a teammate first." },
10562
+ 400
10563
+ );
10564
+ }
10565
+ const lic = getCurrentLicense();
10566
+ const isLicenseOwner = !!lic?.valid && lic.ownerEmail?.toLowerCase() === email;
10567
+ if (isLicenseOwner || isSuperAdmin(email)) {
10568
+ return c.json({ error: "This user is already a superadmin." }, 409);
10569
+ }
10570
+ const grantedBy = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
10571
+ try {
10572
+ await getDb().run(
10573
+ "INSERT INTO super_admins (email, granted_by) VALUES (?, ?)",
10574
+ [email, grantedBy]
10575
+ );
10576
+ } catch (err) {
10577
+ const msg = err instanceof Error ? err.message : String(err);
10578
+ if (/unique|duplicate/i.test(msg)) {
10579
+ await loadGrantedSuperAdmins();
10580
+ return c.json({ error: "This user is already a superadmin." }, 409);
10581
+ }
10582
+ throw err;
10583
+ }
10584
+ await loadGrantedSuperAdmins();
10585
+ return c.json({ email, source: "granted", granted_by: grantedBy }, 201);
10586
+ });
10587
+ app.delete("/admin/super-admins/:email", async (c) => {
10588
+ if (!await serverAdminAllowed(c))
10589
+ return c.json({ error: "Only the server admin can revoke superadmin." }, 403);
10590
+ const email = c.req.param("email").trim().toLowerCase();
10591
+ const callerEmail = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
10592
+ const lic = getCurrentLicense();
10593
+ if (lic?.valid && lic.ownerEmail?.toLowerCase() === email) {
10594
+ return c.json(
10595
+ { error: "The license admin can't be revoked \u2014 their access comes from owning the installed PromptOwl license." },
10596
+ 400
10597
+ );
10598
+ }
10599
+ if (isConfigSuperAdmin(email)) {
10600
+ return c.json(
10601
+ { error: "This superadmin is defined in access.yaml and can't be revoked from here. Remove them from the file and restart the server." },
10602
+ 400
10603
+ );
10604
+ }
10605
+ const db = getDb();
10606
+ const row = await db.get(
10607
+ "SELECT email FROM super_admins WHERE LOWER(email) = ?",
10608
+ [email]
10609
+ );
10610
+ if (!row) return c.json({ error: "Not a granted superadmin." }, 404);
10611
+ if (email === callerEmail) {
10612
+ const hasLicenseAdmin = !!lic?.valid && !!lic.ownerEmail;
10613
+ const hasConfigAdmins = (getAccessConfig()?.super_admins?.length ?? 0) > 0;
10614
+ const others = await db.get(
10615
+ "SELECT COUNT(*) AS c FROM super_admins WHERE LOWER(email) != ?",
10616
+ [email]
10617
+ );
10618
+ if (!hasLicenseAdmin && !hasConfigAdmins && others.c === 0) {
10619
+ return c.json(
10620
+ { error: "You're the last superadmin on this server \u2014 grant someone else superadmin before revoking yourself." },
10621
+ 400
10622
+ );
10623
+ }
10624
+ }
10625
+ await db.run("DELETE FROM super_admins WHERE LOWER(email) = ?", [email]);
10626
+ await loadGrantedSuperAdmins();
10627
+ return c.json({ ok: true, email });
10628
+ });
10629
+ app.use("/teams/*", flexAuthMiddleware);
10630
+ app.use("/teams", flexAuthMiddleware);
10631
+ app.route("/teams", teamRoutes);
9213
10632
  app.use("/me/*", flexAuthMiddleware);
9214
10633
  app.get("/me/work", async (c) => {
9215
10634
  const userId = c.get("userId");
@@ -9283,7 +10702,7 @@ function createApp() {
9283
10702
  app.route("/index", nestIndexRoutes);
9284
10703
  app.use("/mcp", flexAuthMiddleware);
9285
10704
  app.route("/mcp", serverMcpRoutes);
9286
- const nestsApp = new Hono18();
10705
+ const nestsApp = new Hono21();
9287
10706
  nestsApp.use("*", flexAuthMiddleware);
9288
10707
  nestsApp.use("*", async (c, next) => {
9289
10708
  const localPath = c.req.path.replace(/^\/nests\//, "");
@@ -9310,7 +10729,8 @@ function createApp() {
9310
10729
  const path2 = c.req.path;
9311
10730
  const isCommentPath = /\/comments$/.test(path2) || /\/comments\/[^/]+\/resolve$/.test(path2);
9312
10731
  const isActivityPath = /\/activity$/.test(path2);
9313
- const isGovernance = path2.includes("/stewards") || path2.includes("/review-queue") || path2.includes("/versions") || path2.includes("/reviews") || path2.includes("/submit-review") || path2.includes("/cancel-review") || path2.includes("/approve") || path2.includes("/reject") || path2.includes("/can-access") || path2.includes("/can-approve") || path2.includes("/can-edit") || isCommentPath || isActivityPath;
10732
+ const isFeedPath = parts.length === 2 && parts[1] === "feed";
10733
+ const isGovernance = path2.includes("/stewards") || path2.includes("/review-queue") || path2.includes("/versions") || path2.includes("/reviews") || path2.includes("/submit-review") || path2.includes("/cancel-review") || path2.includes("/approve") || path2.includes("/reject") || path2.includes("/can-access") || path2.includes("/can-approve") || path2.includes("/can-edit") || isCommentPath || isActivityPath || isFeedPath;
9314
10734
  const needsLicense = c.req.method !== "GET" || isGovernance;
9315
10735
  if (needsLicense) {
9316
10736
  const lic = getCurrentLicense();
@@ -9372,6 +10792,8 @@ function createApp() {
9372
10792
  required = "admin";
9373
10793
  } else if (resource === "collaborators") {
9374
10794
  required = c.req.method === "GET" || c.req.method === "POST" ? "write" : "admin";
10795
+ } else if (resource === "teams") {
10796
+ required = c.req.method === "GET" ? "write" : "admin";
9375
10797
  } else if (parts[1] === "hooks") {
9376
10798
  required = "write";
9377
10799
  } else if (c.req.method !== "GET" && !isStewardActionPath && !isCommentAction && !isAnnotationAction && !isReadQuery) {
@@ -9454,6 +10876,7 @@ function createApp() {
9454
10876
  nestsApp.route("/:nestId", queryRoutes);
9455
10877
  nestsApp.route("/:nestId", sharingRoutes);
9456
10878
  nestsApp.route("/:nestId/grants", grantRoutes);
10879
+ nestsApp.route("/:nestId/teams", nestTeamRoutes);
9457
10880
  nestsApp.route("/:nestId/edge-types", edgeTypeRoutes);
9458
10881
  nestsApp.route("/:nestId/edges", edgeRoutes);
9459
10882
  nestsApp.route("/:nestId/run", runTriggerRoutes);
@@ -9655,7 +11078,7 @@ async function backfillNodeVersionsFromHistory(db) {
9655
11078
  }
9656
11079
 
9657
11080
  // src/workflow/scheduler.ts
9658
- import { v4 as uuid14 } from "uuid";
11081
+ import { v4 as uuid15 } from "uuid";
9659
11082
  function isDue(s, now) {
9660
11083
  if (!s.last_run_at) return true;
9661
11084
  const last = Date.parse(s.last_run_at);
@@ -9679,7 +11102,7 @@ async function schedulerTick(now = Date.now()) {
9679
11102
  );
9680
11103
  continue;
9681
11104
  }
9682
- const id = `run_${uuid14()}`;
11105
+ const id = `run_${uuid15()}`;
9683
11106
  const nowIso = new Date(now).toISOString();
9684
11107
  await db.run(
9685
11108
  `INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
@@ -9734,6 +11157,11 @@ async function main() {
9734
11157
  if (accessCfg) {
9735
11158
  console.log(` Loaded access.yaml (mode: ${accessCfg.mode || "open"})`);
9736
11159
  }
11160
+ try {
11161
+ await loadGrantedSuperAdmins();
11162
+ } catch (err) {
11163
+ console.error("[access] failed to load DB-granted superadmins:", err);
11164
+ }
9737
11165
  const license = await validateLicense();
9738
11166
  if (!license.valid) {
9739
11167
  console.warn(`