@promptowl/contextnest-community 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +177 -14
- package/CONFIGURATION.md +577 -116
- package/README.md +4 -3
- package/STEWARDSHIP.md +24 -0
- package/dist/{chunk-XUIWAWDO.js → chunk-2QOJE7A5.js} +4 -4
- package/dist/{chunk-2TPQTN4Y.js → chunk-DGPTJXIV.js} +47 -15
- package/dist/chunk-I5EDEZBQ.js +523 -0
- package/dist/{chunk-ZTT4U4NE.js → chunk-IEAWRHMV.js} +99 -12
- package/dist/{chunk-YVMSM7LS.js → chunk-LHWOJPFE.js} +7 -0
- package/dist/{chunk-5EZOPA47.js → chunk-M6PGYMBD.js} +20 -11
- package/dist/{chunk-F66VCBLA.js → chunk-PICUHQEM.js} +51 -2
- package/dist/{chunk-I3CSD6CK.js → chunk-RPGFHESR.js} +2 -2
- package/dist/{chunk-KIAAEHWL.js → chunk-SWA5M5XX.js} +88 -546
- package/dist/{chunk-LA3VTQ22.js → chunk-TRSGPS27.js} +81 -14
- package/dist/{engine-S3QBQ7LH.js → engine-QNMFDIBV.js} +4 -3
- package/dist/{external-edit-service-6FNFPOJJ.js → external-edit-service-4MEGPT4D.js} +5 -4
- package/dist/{grants-service-UT3EQ3R7.js → grants-service-24GLUBY4.js} +3 -3
- package/dist/index.js +1739 -583
- package/dist/invited-user-AM6A3WGO.js +12 -0
- package/dist/{migrations.postgres-AIQ7WSU7.js → migrations.postgres-HGSQ7IA5.js} +16 -2
- package/dist/{review-service-HHGOTO6P.js → review-service-2EWN7WWO.js} +8 -7
- package/dist/{stewardship-service-4DHNRULG.js → stewardship-service-TDZUC2Q6.js} +7 -4
- package/dist/{version-service-KIOQME6U.js → version-service-C3JTZLJH.js} +5 -4
- package/dist/web3/assets/ActivityTracePage-DzwWIjjx.js +1 -0
- package/dist/web3/assets/{AgentDocsPage-CIiaBqMy.js → AgentDocsPage-ixP2UdAE.js} +1 -1
- package/dist/web3/assets/CollaboratorManager-C9VAXZ0h.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-tq4ZTexH.js +1 -0
- package/dist/web3/assets/DocumentEditor-C2CP2fiE.js +36 -0
- package/dist/web3/assets/DocumentsTab-Mh7KxUoB.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-B_CWKXZ_.js +1 -0
- package/dist/web3/assets/MarkdownEditor-CKh4WSn-.js +653 -0
- package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
- package/dist/web3/assets/NestPageHeader-Cizo8nUi.js +1 -0
- package/dist/web3/assets/NestView-EW-sraEb.js +73 -0
- package/dist/web3/assets/OverviewTab-CjidsMX_.js +6 -0
- package/dist/web3/assets/PersonCombobox-BKI7dff4.js +1 -0
- package/dist/web3/assets/{ReviewActions-OfOdqzCn.js → ReviewActions-CS8JXgXF.js} +2 -2
- package/dist/web3/assets/ReviewTab-CwVrXzw5.js +1 -0
- package/dist/web3/assets/StewardsTab-C-iP6gnh.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-Biq3m79i.js +1 -0
- package/dist/web3/assets/{alert-dialog-DTozKlmV.js → alert-dialog-BvF3NFr7.js} +2 -2
- package/dist/web3/assets/{arrow-left-BCKt4CwQ.js → arrow-left-CWIsv-sG.js} +1 -1
- package/dist/web3/assets/backlinks-Cqcn7n1J.js +19 -0
- package/dist/web3/assets/boxes-ClbgQqG2.js +6 -0
- package/dist/web3/assets/{chevron-left-BPwUT9DS.js → chevron-left-CK_ZPFm9.js} +1 -1
- package/dist/web3/assets/{circle-check-DI8IBGeo.js → circle-check-Bw_aTXiH.js} +1 -1
- package/dist/web3/assets/{circle-x-VKVhC88W.js → circle-x-DkIG68H7.js} +1 -1
- package/dist/web3/assets/client-attribution-BraarYaP.js +1 -0
- package/dist/web3/assets/{code-xml-CMabYZvH.js → code-xml-BlR7EN6l.js} +1 -1
- package/dist/web3/assets/{corner-down-right-BGxwK6wH.js → corner-down-right-DSVxK3tf.js} +1 -1
- package/dist/web3/assets/count-skeleton-DToLM74B.js +1 -0
- package/dist/web3/assets/{earth-BEp1EKTo.js → earth-BXhVPvCR.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-r8qZGIjP.js → file-exclamation-point-BevGZgtj.js} +1 -1
- package/dist/web3/assets/{folder-input-Cmtx1Jhk.js → folder-input-m8UBV4fA.js} +1 -1
- package/dist/web3/assets/{index-C1wTSjfP.js → index-BOVYgMvj.js} +1 -1
- package/dist/web3/assets/index-DVnaYqj9.css +1 -0
- package/dist/web3/assets/index-WYDmEOfO.js +404 -0
- package/dist/web3/assets/page-B6pkc9cd.js +2 -0
- package/dist/web3/assets/page-BJrFCKXB.js +1 -0
- package/dist/web3/assets/page-Bgy9Ng9p.js +1 -0
- package/dist/web3/assets/page-C2zoxumw.js +1 -0
- package/dist/web3/assets/page-CJWO06dN.js +1 -0
- package/dist/web3/assets/page-CptuAPJc.js +6 -0
- package/dist/web3/assets/page-DPY01SOj.js +1 -0
- package/dist/web3/assets/page-Dea2SblF.js +9 -0
- package/dist/web3/assets/page-Deq6IKiN.js +6 -0
- package/dist/web3/assets/page-Dfjg8p7Z.js +1 -0
- package/dist/web3/assets/{page-ByyVLDVo.js → page-Dh57L0RG.js} +5 -5
- package/dist/web3/assets/page-Dwbp4Seu.js +6 -0
- package/dist/web3/assets/{page-CHb0F5hV.js → page-DzKwP5Ds.js} +3 -3
- package/dist/web3/assets/page-YxjrE5b5.js +45 -0
- package/dist/web3/assets/{page-title-ClvvBsIo.js → page-title-DVq4qvMR.js} +1 -1
- package/dist/web3/assets/{play-C5YNKpAH.js → play-CXhS6Pgq.js} +1 -1
- package/dist/web3/assets/{send-0CHvmluT.js → send-DdfwnIrj.js} +1 -1
- package/dist/web3/assets/{settings-CeeF4LEb.js → settings-DKX0Q4gO.js} +1 -1
- package/dist/web3/assets/{share-2-CMnOlOt-.js → share-2-CxmXRt58.js} +1 -1
- package/dist/web3/assets/{tag-DQ_6J5Gv.js → tag-DeYC_mRv.js} +1 -1
- package/dist/web3/assets/{trash-2-HBi-Pslz.js → trash-2-B0HyPTnI.js} +1 -1
- package/dist/web3/assets/{user-plus-CvBNcpn4.js → user-plus-Ckm_Isyc.js} +1 -1
- package/dist/web3/assets/{x-By7piikG.js → x-mNtADYHc.js} +1 -1
- package/dist/web3/assets/zap-D2ifcnYb.js +16 -0
- package/dist/web3/index.html +2 -2
- package/package.json +3 -2
- package/dist/client-KEY4PYJH.js +0 -11
- package/dist/keys-CTOGAG3W.js +0 -20
- package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +0 -1
- package/dist/web3/assets/CollaboratorManager-By91wrIr.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +0 -1
- package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +0 -36
- package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +0 -1
- package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +0 -1
- package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +0 -643
- package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +0 -1
- package/dist/web3/assets/NestView-CTZ55tXD.js +0 -63
- package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +0 -1
- package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +0 -1
- package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +0 -1
- package/dist/web3/assets/ReviewTab-CyX8yxd3.js +0 -1
- package/dist/web3/assets/StewardsTab-DBEEfAQI.js +0 -1
- package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +0 -1
- package/dist/web3/assets/backlinks-CYd4xENL.js +0 -24
- package/dist/web3/assets/card-XG2dP5Xf.js +0 -1
- package/dist/web3/assets/count-skeleton-DMbdpscX.js +0 -1
- package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
- package/dist/web3/assets/index-BM-h3DwI.css +0 -1
- package/dist/web3/assets/index-EaX2yql0.js +0 -389
- package/dist/web3/assets/page-B3yvQvHy.js +0 -1
- package/dist/web3/assets/page-BExMvttJ.js +0 -1
- package/dist/web3/assets/page-BUOADv2X.js +0 -1
- package/dist/web3/assets/page-BYUFhYWZ.js +0 -1
- package/dist/web3/assets/page-Bicf02Cz.js +0 -1
- package/dist/web3/assets/page-BsPMDm5d.js +0 -45
- package/dist/web3/assets/page-CC0NJi8v.js +0 -1
- package/dist/web3/assets/page-CPjWQNKx.js +0 -11
- package/dist/web3/assets/page-CQt3ZGbf.js +0 -2
- package/dist/web3/assets/page-Cj1NetQ-.js +0 -1
- package/dist/web3/assets/page-CtaW65El.js +0 -1
- package/dist/web3/assets/page-DGOL9l8i.js +0 -6
- package/dist/web3/assets/refresh-cw-BhHzSH9m.js +0 -6
- package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
- package/dist/web3/assets/scroll-area-hz-xtayP.js +0 -1
- package/dist/web3/assets/select-DBFTrUmI.js +0 -6
- package/dist/web3/assets/triangle-alert-CDy8-7sv.js +0 -6
- package/dist/web3/assets/zap-C2T8riBp.js +0 -11
|
@@ -3,12 +3,17 @@ import {
|
|
|
3
3
|
ForbiddenError,
|
|
4
4
|
NotFoundError,
|
|
5
5
|
ValidationError
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LHWOJPFE.js";
|
|
7
|
+
import {
|
|
8
|
+
permissionLabel,
|
|
9
|
+
sendEmailToRecipient,
|
|
10
|
+
teamRoleToPermission
|
|
11
|
+
} from "./chunk-I5EDEZBQ.js";
|
|
7
12
|
import {
|
|
8
13
|
config,
|
|
9
14
|
getDb,
|
|
10
15
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-TRSGPS27.js";
|
|
12
17
|
import {
|
|
13
18
|
ANON_USER_ID
|
|
14
19
|
} from "./chunk-YB3LKF7U.js";
|
|
@@ -226,7 +231,7 @@ async function installLicenseKey(key) {
|
|
|
226
231
|
}
|
|
227
232
|
const previousKey = process.env.PROMPTOWL_KEY || "";
|
|
228
233
|
process.env.PROMPTOWL_KEY = trimmed;
|
|
229
|
-
const info = await validateLicense({ forceFresh: true });
|
|
234
|
+
const info = await validateLicense({ forceFresh: true, strict: true });
|
|
230
235
|
if (!info.valid) {
|
|
231
236
|
process.env.PROMPTOWL_KEY = previousKey;
|
|
232
237
|
if (previousKey) {
|
|
@@ -309,12 +314,26 @@ function isSuspended() {
|
|
|
309
314
|
function getSuspensionReason() {
|
|
310
315
|
return suspensionReason;
|
|
311
316
|
}
|
|
317
|
+
var REJECTION_STRIKES_TO_REVOKE = 3;
|
|
318
|
+
var rejectionStreak = 0;
|
|
319
|
+
var rejectionKey = null;
|
|
320
|
+
function fromCache(cached) {
|
|
321
|
+
return {
|
|
322
|
+
valid: true,
|
|
323
|
+
tier: cached.tier,
|
|
324
|
+
org: cached.org,
|
|
325
|
+
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
326
|
+
suspended: suspensionConfirmed,
|
|
327
|
+
suspendedReason: suspensionReason,
|
|
328
|
+
ownerEmail: cached.owner_email || null
|
|
329
|
+
};
|
|
330
|
+
}
|
|
312
331
|
async function validateLicense(opts = {}) {
|
|
313
|
-
const info = await _validateLicenseImpl(!!opts.forceFresh);
|
|
332
|
+
const info = await _validateLicenseImpl(!!opts.forceFresh, !!opts.strict);
|
|
314
333
|
currentLicense = info;
|
|
315
334
|
return info;
|
|
316
335
|
}
|
|
317
|
-
async function _validateLicenseImpl(forceFresh) {
|
|
336
|
+
async function _validateLicenseImpl(forceFresh, strict) {
|
|
318
337
|
const key = config.PROMPTOWL_KEY;
|
|
319
338
|
if (!key) {
|
|
320
339
|
return {
|
|
@@ -335,15 +354,7 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
335
354
|
if (cached && !forceFresh) {
|
|
336
355
|
const age = Date.now() - (/* @__PURE__ */ new Date(cached.validated_at + "Z")).getTime();
|
|
337
356
|
if (age < CACHE_TTL_MS) {
|
|
338
|
-
return
|
|
339
|
-
valid: true,
|
|
340
|
-
tier: cached.tier,
|
|
341
|
-
org: cached.org,
|
|
342
|
-
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
343
|
-
suspended: suspensionConfirmed,
|
|
344
|
-
suspendedReason: suspensionReason,
|
|
345
|
-
ownerEmail: cached.owner_email || null
|
|
346
|
-
};
|
|
357
|
+
return fromCache(cached);
|
|
347
358
|
}
|
|
348
359
|
}
|
|
349
360
|
try {
|
|
@@ -358,15 +369,7 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
358
369
|
console.warn(
|
|
359
370
|
" PromptOwl unreachable, using cached license (grace period)"
|
|
360
371
|
);
|
|
361
|
-
return
|
|
362
|
-
valid: true,
|
|
363
|
-
tier: cached.tier,
|
|
364
|
-
org: cached.org,
|
|
365
|
-
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
366
|
-
suspended: suspensionConfirmed,
|
|
367
|
-
suspendedReason: suspensionReason,
|
|
368
|
-
ownerEmail: cached.owner_email || null
|
|
369
|
-
};
|
|
372
|
+
return fromCache(cached);
|
|
370
373
|
}
|
|
371
374
|
return {
|
|
372
375
|
valid: false,
|
|
@@ -412,6 +415,20 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
412
415
|
`
|
|
413
416
|
);
|
|
414
417
|
}
|
|
418
|
+
if (data.valid !== true) {
|
|
419
|
+
rejectionStreak = 0;
|
|
420
|
+
rejectionKey = null;
|
|
421
|
+
if (cached) return fromCache(cached);
|
|
422
|
+
return {
|
|
423
|
+
valid: false,
|
|
424
|
+
tier: "none",
|
|
425
|
+
org: null,
|
|
426
|
+
limits: null,
|
|
427
|
+
suspended: suspensionConfirmed,
|
|
428
|
+
suspendedReason: suspensionReason,
|
|
429
|
+
ownerEmail: data.owner_email || null
|
|
430
|
+
};
|
|
431
|
+
}
|
|
415
432
|
} else {
|
|
416
433
|
if (suspensionFirstSeen) {
|
|
417
434
|
console.log(" Suspension flag cleared by PromptOwl.");
|
|
@@ -421,6 +438,18 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
421
438
|
suspensionReason = null;
|
|
422
439
|
}
|
|
423
440
|
if (!data.valid && !data.suspended) {
|
|
441
|
+
if (cached && !strict) {
|
|
442
|
+
rejectionStreak = rejectionKey === key ? rejectionStreak + 1 : 1;
|
|
443
|
+
rejectionKey = key;
|
|
444
|
+
if (rejectionStreak < REJECTION_STRIKES_TO_REVOKE) {
|
|
445
|
+
console.warn(
|
|
446
|
+
`[license] PromptOwl rejected the license (${rejectionStreak}/${REJECTION_STRIKES_TO_REVOKE}); keeping the cached license until the rejection is confirmed`
|
|
447
|
+
);
|
|
448
|
+
return fromCache(cached);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
rejectionStreak = 0;
|
|
452
|
+
rejectionKey = null;
|
|
424
453
|
await db.run("DELETE FROM license_cache WHERE key = ?", [key]);
|
|
425
454
|
return {
|
|
426
455
|
valid: false,
|
|
@@ -433,6 +462,8 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
433
462
|
};
|
|
434
463
|
}
|
|
435
464
|
if (data.valid) {
|
|
465
|
+
rejectionStreak = 0;
|
|
466
|
+
rejectionKey = null;
|
|
436
467
|
const limitsJson = data.limits ? JSON.stringify(data.limits) : null;
|
|
437
468
|
await db.run(
|
|
438
469
|
insertOrReplace(
|
|
@@ -465,15 +496,7 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
465
496
|
console.warn(
|
|
466
497
|
` PromptOwl validation failed (${err.message}), using cached license`
|
|
467
498
|
);
|
|
468
|
-
return
|
|
469
|
-
valid: true,
|
|
470
|
-
tier: cached.tier,
|
|
471
|
-
org: cached.org,
|
|
472
|
-
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
473
|
-
suspended: suspensionConfirmed,
|
|
474
|
-
suspendedReason: suspensionReason,
|
|
475
|
-
ownerEmail: cached.owner_email || null
|
|
476
|
-
};
|
|
499
|
+
return fromCache(cached);
|
|
477
500
|
}
|
|
478
501
|
return {
|
|
479
502
|
valid: false,
|
|
@@ -598,491 +621,6 @@ function parseAccessYaml(content) {
|
|
|
598
621
|
// src/governance/teams-service.ts
|
|
599
622
|
import { v4 as uuid2 } from "uuid";
|
|
600
623
|
|
|
601
|
-
// src/governance/roles.ts
|
|
602
|
-
function collabPermToRole(permission) {
|
|
603
|
-
switch (permission) {
|
|
604
|
-
case "owner":
|
|
605
|
-
return "owner";
|
|
606
|
-
case "admin":
|
|
607
|
-
return "admin";
|
|
608
|
-
case "write":
|
|
609
|
-
return "editor";
|
|
610
|
-
case "read":
|
|
611
|
-
return "viewer";
|
|
612
|
-
default:
|
|
613
|
-
return null;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
function teamRoleToPermission(role) {
|
|
617
|
-
switch (role) {
|
|
618
|
-
case "admin":
|
|
619
|
-
return "admin";
|
|
620
|
-
case "editor":
|
|
621
|
-
return "write";
|
|
622
|
-
case "viewer":
|
|
623
|
-
return "read";
|
|
624
|
-
default:
|
|
625
|
-
return null;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
|
|
629
|
-
var canViewWith = (roles) => roles.length > 0;
|
|
630
|
-
var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
|
|
631
|
-
var canApproveWith = (roles) => includesAny(roles, ["owner", "admin", "reviewer"]);
|
|
632
|
-
var PRECEDENCE = [
|
|
633
|
-
"owner",
|
|
634
|
-
"admin",
|
|
635
|
-
"editor",
|
|
636
|
-
"reviewer",
|
|
637
|
-
"viewer"
|
|
638
|
-
];
|
|
639
|
-
function primaryRole(roles) {
|
|
640
|
-
for (const r of PRECEDENCE) if (roles.includes(r)) return r;
|
|
641
|
-
return null;
|
|
642
|
-
}
|
|
643
|
-
function permissionLabel(permission) {
|
|
644
|
-
const role = collabPermToRole(permission);
|
|
645
|
-
return role ? role[0].toUpperCase() + role.slice(1) : "Collaborator";
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// src/notify/email-render.ts
|
|
649
|
-
function escapeHtml(s) {
|
|
650
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
651
|
-
}
|
|
652
|
-
var EVENT = {
|
|
653
|
-
":white_check_mark:": { icon: "\u2705", color: "#16a34a" },
|
|
654
|
-
":x:": { icon: "\u274C", color: "#dc2626" },
|
|
655
|
-
":memo:": { icon: "\u{1F4DD}", color: "#d97706" },
|
|
656
|
-
":key:": { icon: "\u{1F511}", color: "#2563eb" }
|
|
657
|
-
};
|
|
658
|
-
var ALL_EMOJI = Object.fromEntries(
|
|
659
|
-
Object.entries(EVENT).map(([code, m]) => [code, m.icon])
|
|
660
|
-
);
|
|
661
|
-
function renderText(message) {
|
|
662
|
-
return message.replace(/:[a-z_]+:/g, (m) => ALL_EMOJI[m] ?? "").replace(/\*/g, "").replace(/[ \t]{2,}/g, " ").trim();
|
|
663
|
-
}
|
|
664
|
-
var STATUS_META = {
|
|
665
|
-
approved: { subjectWord: "approved", label: "Approved", color: "#16a34a" },
|
|
666
|
-
mention: { subjectWord: "mentioned you", label: "Mention", color: "#7c3aed" },
|
|
667
|
-
rejected: { subjectWord: "rejected", label: "Rejected", color: "#dc2626" },
|
|
668
|
-
pending_review: {
|
|
669
|
-
subjectWord: "awaiting review",
|
|
670
|
-
label: "Pending review",
|
|
671
|
-
color: "#d97706"
|
|
672
|
-
},
|
|
673
|
-
shared: { subjectWord: "shared with you", label: "Shared", color: "#2563eb" },
|
|
674
|
-
invited: { subjectWord: "invited you", label: "Invitation", color: "#2563eb" },
|
|
675
|
-
steward: { subjectWord: "steward", label: "Steward", color: "#7c3aed" },
|
|
676
|
-
deletion_requested: {
|
|
677
|
-
subjectWord: "flagged for deletion",
|
|
678
|
-
label: "Deletion requested",
|
|
679
|
-
color: "#d97706"
|
|
680
|
-
},
|
|
681
|
-
deletion_declined: {
|
|
682
|
-
subjectWord: "kept",
|
|
683
|
-
label: "Deletion rejected",
|
|
684
|
-
color: "#d97706"
|
|
685
|
-
},
|
|
686
|
-
deletion_deleted: {
|
|
687
|
-
subjectWord: "deleted",
|
|
688
|
-
label: "Deleted",
|
|
689
|
-
color: "#dc2626"
|
|
690
|
-
},
|
|
691
|
-
// An honoured ARCHIVE request. Deliberately not the deletion palette: the
|
|
692
|
-
// nest is intact and restorable, and red "Deleted" would tell the requester
|
|
693
|
-
// the opposite of what happened.
|
|
694
|
-
deletion_archived: {
|
|
695
|
-
subjectWord: "archived",
|
|
696
|
-
label: "Archived",
|
|
697
|
-
color: "#0891b2"
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
701
|
-
var shareRole = (permission) => permission ? permissionLabel(permission) : "collaborator";
|
|
702
|
-
var detailRow = (label, valueHtml) => `<tr>
|
|
703
|
-
<td style="padding:5px 0;font-size:13px;color:#6b7280;width:90px;vertical-align:top;">${label}</td>
|
|
704
|
-
<td style="padding:5px 0;font-size:13px;color:#111827;vertical-align:top;">${valueHtml}</td>
|
|
705
|
-
</tr>`;
|
|
706
|
-
var plainRow = (label, value) => ({
|
|
707
|
-
label,
|
|
708
|
-
text: value,
|
|
709
|
-
html: escapeHtml(value)
|
|
710
|
-
});
|
|
711
|
-
var governanceRows = (x) => [
|
|
712
|
-
plainRow("Path", x.d.path),
|
|
713
|
-
{ label: "Status", text: x.meta.label, html: x.statusBadge },
|
|
714
|
-
...x.d.version != null ? [plainRow("Version", `v${x.d.version}`)] : []
|
|
715
|
-
];
|
|
716
|
-
var TEMPLATES = {
|
|
717
|
-
approved: {
|
|
718
|
-
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
719
|
-
detailsHeader: "Document details",
|
|
720
|
-
linkLabel: "Open document",
|
|
721
|
-
lineT: (x) => `${x.title}${x.vT} has been approved${x.byT}${x.noteT}.`,
|
|
722
|
-
lineH: (x) => `${x.doc}${x.vH} has been approved${x.byH}${x.noteH}.`,
|
|
723
|
-
rows: governanceRows
|
|
724
|
-
},
|
|
725
|
-
// A mention names one person, so this is the one template addressed to
|
|
726
|
-
// "you". `note` carries the comment's own words when there are any — a
|
|
727
|
-
// mention with no context reads as a summons.
|
|
728
|
-
mention: {
|
|
729
|
-
heading: (x) => `${x.d.actor || "Someone"} mentioned you in ${x.title}`,
|
|
730
|
-
detailsHeader: "Document details",
|
|
731
|
-
linkLabel: "Open document",
|
|
732
|
-
lineT: (x) => `You were mentioned in ${x.title}${x.byT}${x.noteT}.`,
|
|
733
|
-
lineH: (x) => `You were mentioned in ${x.doc}${x.byH}${x.noteH}.`,
|
|
734
|
-
rows: (x) => [plainRow("Nest", x.d.path)]
|
|
735
|
-
},
|
|
736
|
-
rejected: {
|
|
737
|
-
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
738
|
-
detailsHeader: "Document details",
|
|
739
|
-
linkLabel: "Open document",
|
|
740
|
-
lineT: (x) => `${x.title}${x.vT} has been rejected${x.byT}${x.noteT}.`,
|
|
741
|
-
lineH: (x) => `${x.doc}${x.vH} has been rejected${x.byH}${x.noteH}.`,
|
|
742
|
-
rows: governanceRows
|
|
743
|
-
},
|
|
744
|
-
pending_review: {
|
|
745
|
-
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
746
|
-
detailsHeader: "Document details",
|
|
747
|
-
linkLabel: "Open document",
|
|
748
|
-
lineT: (x) => `A review has been requested for ${x.title}${x.vT}${x.byT}${x.noteT}.`,
|
|
749
|
-
lineH: (x) => `A review has been requested for ${x.doc}${x.vH}${x.byH}${x.noteH}.`,
|
|
750
|
-
rows: governanceRows
|
|
751
|
-
},
|
|
752
|
-
shared: {
|
|
753
|
-
heading: (x) => `You've been added to ${x.title}`,
|
|
754
|
-
detailsHeader: "Details",
|
|
755
|
-
linkLabel: "Open nest",
|
|
756
|
-
lineT: (x) => `You've been given ${shareRole(x.d.permission)} access to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open it below to start collaborating.`,
|
|
757
|
-
lineH: (x) => `You've been given <strong>${escapeHtml(shareRole(x.d.permission))}</strong> access to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open it below to start collaborating.`,
|
|
758
|
-
rows: (x) => [
|
|
759
|
-
plainRow("Nest", x.d.path),
|
|
760
|
-
...x.d.permission ? [plainRow("Role", shareRole(x.d.permission))] : []
|
|
761
|
-
]
|
|
762
|
-
},
|
|
763
|
-
invited: {
|
|
764
|
-
heading: (x) => `You've been invited to ${x.title}`,
|
|
765
|
-
detailsHeader: "Sign-in details",
|
|
766
|
-
linkLabel: "Sign in",
|
|
767
|
-
lineT: (x) => `You've been invited to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Sign in with your email and the temporary password below, then choose your own password.`,
|
|
768
|
-
lineH: (x) => `You've been invited to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Sign in with your email and the temporary password below, then choose your own password.`,
|
|
769
|
-
rows: (x) => [
|
|
770
|
-
plainRow("Email", x.d.actor || ""),
|
|
771
|
-
...x.d.tempPassword ? [
|
|
772
|
-
{
|
|
773
|
-
label: "Temporary password",
|
|
774
|
-
text: x.d.tempPassword,
|
|
775
|
-
html: x.codeValue(x.d.tempPassword)
|
|
776
|
-
}
|
|
777
|
-
] : []
|
|
778
|
-
]
|
|
779
|
-
},
|
|
780
|
-
// Deletion requests reuse the governance details block — same "what/where/
|
|
781
|
-
// status" shape, and the reason/note rides in `note` like a review note.
|
|
782
|
-
deletion_requested: {
|
|
783
|
-
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
784
|
-
detailsHeader: "Document details",
|
|
785
|
-
linkLabel: "Review the request",
|
|
786
|
-
lineT: (x) => `${x.title} has been flagged for deletion${x.byT}${x.noteT}. Delete it or reject the request from the nest.`,
|
|
787
|
-
lineH: (x) => `${x.doc} has been flagged for deletion${x.byH}${x.noteH}. Delete it or reject the request from the nest.`,
|
|
788
|
-
rows: governanceRows
|
|
789
|
-
},
|
|
790
|
-
deletion_declined: {
|
|
791
|
-
heading: (x) => `Your deletion request for ${x.title} was rejected`,
|
|
792
|
-
detailsHeader: "Document details",
|
|
793
|
-
linkLabel: "Open document",
|
|
794
|
-
lineT: (x) => `Your request to delete ${x.title} was rejected${x.byT}${x.noteT}. The document stays.`,
|
|
795
|
-
lineH: (x) => `Your request to delete ${x.doc} was rejected${x.byH}${x.noteH}. The document stays.`,
|
|
796
|
-
rows: governanceRows
|
|
797
|
-
},
|
|
798
|
-
deletion_deleted: {
|
|
799
|
-
heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
|
|
800
|
-
detailsHeader: "Document details",
|
|
801
|
-
linkLabel: "Open nest",
|
|
802
|
-
lineT: (x) => `${x.title} has been deleted${x.byT}${x.noteT} \u2014 your deletion request was accepted.`,
|
|
803
|
-
lineH: (x) => `${x.doc} has been deleted${x.byH}${x.noteH} \u2014 your deletion request was accepted.`,
|
|
804
|
-
rows: governanceRows
|
|
805
|
-
},
|
|
806
|
-
deletion_archived: {
|
|
807
|
-
heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
|
|
808
|
-
detailsHeader: "Nest details",
|
|
809
|
-
linkLabel: "Open nest",
|
|
810
|
-
lineT: (x) => `${x.title} has been archived${x.byT}${x.noteT} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
|
|
811
|
-
lineH: (x) => `${x.doc} has been archived${x.byH}${x.noteH} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
|
|
812
|
-
rows: governanceRows
|
|
813
|
-
},
|
|
814
|
-
steward: {
|
|
815
|
-
heading: (x) => `You're now a steward of ${x.title}`,
|
|
816
|
-
detailsHeader: "Details",
|
|
817
|
-
linkLabel: "Open nest",
|
|
818
|
-
lineT: (x) => `You've been assigned as a ${x.d.permission || "reviewer"} steward of ${x.d.scopeLabel || x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open the nest below to start reviewing.`,
|
|
819
|
-
lineH: (x) => `You've been assigned as a <strong>${escapeHtml(x.d.permission || "reviewer")}</strong> steward of ${x.d.scopeLabel ? escapeHtml(x.d.scopeLabel) : x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open the nest below to start reviewing.`,
|
|
820
|
-
rows: (x) => [
|
|
821
|
-
plainRow("Nest", x.d.path),
|
|
822
|
-
...x.d.permission ? [plainRow("Role", cap(x.d.permission))] : [],
|
|
823
|
-
...x.d.scopeLabel ? [plainRow("Scope", cap(x.d.scopeLabel))] : []
|
|
824
|
-
]
|
|
825
|
-
}
|
|
826
|
-
};
|
|
827
|
-
function renderEmail(d) {
|
|
828
|
-
const meta = STATUS_META[d.status];
|
|
829
|
-
const t = TEMPLATES[d.status];
|
|
830
|
-
const title = d.docTitle.replace(/[\r\n]+/g, " ").trim();
|
|
831
|
-
const vLabel = d.version != null ? `v${d.version}` : "";
|
|
832
|
-
const ctx = {
|
|
833
|
-
d,
|
|
834
|
-
title,
|
|
835
|
-
doc: `<strong>${escapeHtml(title)}</strong>`,
|
|
836
|
-
byT: d.actor ? ` by ${d.actor}` : "",
|
|
837
|
-
byH: d.actor ? ` by ${escapeHtml(d.actor)}` : "",
|
|
838
|
-
noteT: d.note ? ` with the note: "${d.note}"` : "",
|
|
839
|
-
noteH: d.note ? ` with the note: “${escapeHtml(d.note)}”` : "",
|
|
840
|
-
vT: vLabel ? ` ${vLabel}` : "",
|
|
841
|
-
vH: vLabel ? ` ${vLabel}` : "",
|
|
842
|
-
meta,
|
|
843
|
-
statusBadge: `<span style="display:inline-block;padding:2px 9px;border-radius:9999px;background:#f3f4f6;color:${meta.color};font-size:12px;font-weight:600;">${meta.label}</span>`,
|
|
844
|
-
codeValue: (v) => `<span style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-weight:600;color:#111827;background:#f3f4f6;padding:2px 8px;border-radius:6px;">${escapeHtml(v)}</span>`
|
|
845
|
-
};
|
|
846
|
-
const heading = t.heading(ctx).replace(/[\r\n]+/g, " ").trim();
|
|
847
|
-
const subject = heading;
|
|
848
|
-
const detailsHeader = t.detailsHeader;
|
|
849
|
-
const linkLabel = t.linkLabel;
|
|
850
|
-
const lineH = t.lineH(ctx);
|
|
851
|
-
const rows = t.rows(ctx);
|
|
852
|
-
const detailLines = [
|
|
853
|
-
`${detailsHeader}:`,
|
|
854
|
-
...rows.map((r) => ` ${r.label}: ${r.text}`),
|
|
855
|
-
...d.link ? [` ${linkLabel}: ${d.link}`] : []
|
|
856
|
-
];
|
|
857
|
-
const text = ["Hi,", "", t.lineT(ctx), "", ...detailLines, "", "Regards,", "Community Nest"].join("\n");
|
|
858
|
-
const detailRows = rows.map((r) => detailRow(r.label, r.html)).join("");
|
|
859
|
-
const buttonRow = d.link ? `<tr><td style="padding:20px 24px 0;">
|
|
860
|
-
<table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
|
|
861
|
-
<td bgcolor="${meta.color}" style="border-radius:6px;">
|
|
862
|
-
<a href="${escapeHtml(d.link)}" target="_blank" style="display:inline-block;padding:10px 20px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;">${linkLabel}</a>
|
|
863
|
-
</td>
|
|
864
|
-
</tr></table>
|
|
865
|
-
</td></tr>` : "";
|
|
866
|
-
const html = `<div style="margin:0;padding:24px;background:#f6f8fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#111827;">
|
|
867
|
-
<table role="presentation" width="480" cellpadding="0" cellspacing="0" border="0" align="center" style="width:100%;max-width:480px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;">
|
|
868
|
-
<tr><td style="padding:20px 24px 0;">
|
|
869
|
-
<div style="font-size:12px;font-weight:700;color:#6b7280;letter-spacing:0.06em;text-transform:uppercase;">ContextNest</div>
|
|
870
|
-
</td></tr>
|
|
871
|
-
<tr><td style="padding:10px 24px 0;">
|
|
872
|
-
<h1 style="margin:0;font-size:19px;line-height:1.35;font-weight:700;color:#111827;">${escapeHtml(heading)}</h1>
|
|
873
|
-
</td></tr>
|
|
874
|
-
<tr><td style="padding:16px 24px 0;font-size:14px;line-height:1.6;color:#374151;">
|
|
875
|
-
Hi,<br><br>${lineH}
|
|
876
|
-
</td></tr>
|
|
877
|
-
<tr><td style="padding:20px 24px 0;">
|
|
878
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #eef0f2;border-radius:8px;">
|
|
879
|
-
<tr><td style="padding:12px 16px;">
|
|
880
|
-
<div style="font-size:11px;font-weight:600;color:#9ca3af;letter-spacing:0.04em;text-transform:uppercase;padding-bottom:4px;">${detailsHeader}</div>
|
|
881
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">${detailRows}</table>
|
|
882
|
-
</td></tr>
|
|
883
|
-
</table>
|
|
884
|
-
</td></tr>
|
|
885
|
-
${buttonRow}
|
|
886
|
-
<tr><td style="padding:22px 24px 24px;font-size:14px;line-height:1.6;color:#374151;">
|
|
887
|
-
Regards,<br>Community Nest
|
|
888
|
-
</td></tr>
|
|
889
|
-
</table>
|
|
890
|
-
</div>`;
|
|
891
|
-
return { subject, text, html };
|
|
892
|
-
}
|
|
893
|
-
function renderHtml(message, nestName2) {
|
|
894
|
-
const lead = message.match(/^(:[a-z_]+:)\s*/);
|
|
895
|
-
const meta = lead && EVENT[lead[1]] || { icon: "\u{1F514}", color: "#475569" };
|
|
896
|
-
const rest = lead ? message.slice(lead[0].length) : message;
|
|
897
|
-
const body = escapeHtml(rest).replace(/\*([^*]+)\*/g, "<strong>$1</strong>").replace(/\n/g, "<br>");
|
|
898
|
-
const safeNest = escapeHtml(nestName2);
|
|
899
|
-
return `<div style="background:#f6f8fa;padding:24px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
|
900
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;">
|
|
901
|
-
<tr><td style="padding:20px 24px 16px;border-bottom:1px solid #f0f0f0;">
|
|
902
|
-
<div style="font-size:15px;font-weight:700;color:#111827;letter-spacing:-0.01em;">ContextNest</div>
|
|
903
|
-
<div style="font-size:13px;color:#6b7280;margin-top:2px;">${safeNest}</div>
|
|
904
|
-
</td></tr>
|
|
905
|
-
<tr><td style="padding:24px;">
|
|
906
|
-
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
|
|
907
|
-
<td valign="top" style="font-size:22px;line-height:1;padding-right:14px;">${meta.icon}</td>
|
|
908
|
-
<td valign="top" style="font-size:15px;line-height:1.55;color:#111827;border-left:3px solid ${meta.color};padding-left:14px;">${body}</td>
|
|
909
|
-
</tr></table>
|
|
910
|
-
</td></tr>
|
|
911
|
-
</table>
|
|
912
|
-
</div>`;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// src/notify/email.ts
|
|
916
|
-
var transporterPromise = null;
|
|
917
|
-
var configuredUrl = null;
|
|
918
|
-
var warnedOnce = false;
|
|
919
|
-
var SMTP_TIMEOUT_DEFAULTS = [
|
|
920
|
-
["connectionTimeout", 5e3],
|
|
921
|
-
["greetingTimeout", 5e3],
|
|
922
|
-
["socketTimeout", 1e4]
|
|
923
|
-
];
|
|
924
|
-
function withTimeouts(url) {
|
|
925
|
-
const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
|
|
926
|
-
const missing = SMTP_TIMEOUT_DEFAULTS.filter(
|
|
927
|
-
([name]) => !new RegExp(`(^|&)${name}=`, "i").test(query)
|
|
928
|
-
).map(([name, ms]) => `${name}=${ms}`);
|
|
929
|
-
if (missing.length === 0) return url;
|
|
930
|
-
return url + (url.includes("?") ? "&" : "?") + missing.join("&");
|
|
931
|
-
}
|
|
932
|
-
var POOL_DEFAULTS = [
|
|
933
|
-
["pool", "true"],
|
|
934
|
-
["maxConnections", "1"],
|
|
935
|
-
["rateDelta", "1000"],
|
|
936
|
-
["rateLimit", "8"]
|
|
937
|
-
// messages per second
|
|
938
|
-
];
|
|
939
|
-
function withPool(url) {
|
|
940
|
-
const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
|
|
941
|
-
const missing = POOL_DEFAULTS.filter(
|
|
942
|
-
([name]) => !new RegExp(`(^|&)${name}=`, "i").test(query)
|
|
943
|
-
).map(([name, val]) => `${name}=${val}`);
|
|
944
|
-
if (missing.length === 0) return url;
|
|
945
|
-
return url + (url.includes("?") ? "&" : "?") + missing.join("&");
|
|
946
|
-
}
|
|
947
|
-
async function verifySmtp(url) {
|
|
948
|
-
let transporter = null;
|
|
949
|
-
try {
|
|
950
|
-
const nodemailer = await import("nodemailer");
|
|
951
|
-
transporter = nodemailer.default.createTransport(withTimeouts(url), {
|
|
952
|
-
disableFileAccess: true,
|
|
953
|
-
disableUrlAccess: true
|
|
954
|
-
});
|
|
955
|
-
await transporter.verify();
|
|
956
|
-
return { ok: true };
|
|
957
|
-
} catch (err) {
|
|
958
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
959
|
-
} finally {
|
|
960
|
-
try {
|
|
961
|
-
transporter?.close?.();
|
|
962
|
-
} catch {
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
async function getTransporter() {
|
|
967
|
-
const url = config.SMTP_URL;
|
|
968
|
-
if (!url) return null;
|
|
969
|
-
if (!transporterPromise || configuredUrl !== url) {
|
|
970
|
-
configuredUrl = url;
|
|
971
|
-
transporterPromise = import("nodemailer").then(
|
|
972
|
-
(nodemailer) => (
|
|
973
|
-
// withPool → connection pool + per-second rate limit, so a burst queues
|
|
974
|
-
// instead of tripping the provider's "too many emails" 550.
|
|
975
|
-
nodemailer.default.createTransport(withPool(withTimeouts(url)), {
|
|
976
|
-
// Per-message defaults (belt-and-braces; also set on each sendMail).
|
|
977
|
-
disableFileAccess: true,
|
|
978
|
-
disableUrlAccess: true
|
|
979
|
-
})
|
|
980
|
-
)
|
|
981
|
-
);
|
|
982
|
-
transporterPromise.catch(() => {
|
|
983
|
-
transporterPromise = null;
|
|
984
|
-
configuredUrl = null;
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
return transporterPromise;
|
|
988
|
-
}
|
|
989
|
-
async function notifyEmailForNest(nestId, message, details) {
|
|
990
|
-
const from = config.NOTIFY_EMAIL_FROM;
|
|
991
|
-
const to = config.NOTIFY_EMAIL_TO;
|
|
992
|
-
if (!config.SMTP_URL || !from || !to) return;
|
|
993
|
-
try {
|
|
994
|
-
const transporter = await getTransporter();
|
|
995
|
-
if (!transporter) return;
|
|
996
|
-
let subject;
|
|
997
|
-
let text;
|
|
998
|
-
let html;
|
|
999
|
-
if (details) {
|
|
1000
|
-
({ subject, text, html } = renderEmail(details));
|
|
1001
|
-
} else {
|
|
1002
|
-
let name = nestId;
|
|
1003
|
-
try {
|
|
1004
|
-
const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
|
|
1005
|
-
nestId
|
|
1006
|
-
]);
|
|
1007
|
-
if (row?.name) name = row.name;
|
|
1008
|
-
} catch {
|
|
1009
|
-
}
|
|
1010
|
-
const safeName = name.replace(/[\r\n]+/g, " ").trim();
|
|
1011
|
-
subject = `ContextNest \xB7 ${safeName}`;
|
|
1012
|
-
text = renderText(message);
|
|
1013
|
-
html = renderHtml(message, safeName);
|
|
1014
|
-
}
|
|
1015
|
-
await transporter.sendMail({
|
|
1016
|
-
from,
|
|
1017
|
-
to,
|
|
1018
|
-
subject,
|
|
1019
|
-
text,
|
|
1020
|
-
html,
|
|
1021
|
-
disableFileAccess: true,
|
|
1022
|
-
disableUrlAccess: true
|
|
1023
|
-
});
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
if (!warnedOnce) {
|
|
1026
|
-
warnedOnce = true;
|
|
1027
|
-
console.warn(
|
|
1028
|
-
"[email] notification failed (suppressing further warnings):",
|
|
1029
|
-
err instanceof Error ? err.message : err
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
function isEmailConfigured() {
|
|
1035
|
-
return !!config.SMTP_URL && !!config.NOTIFY_EMAIL_FROM;
|
|
1036
|
-
}
|
|
1037
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1038
|
-
function recipientEnvelope(to, from) {
|
|
1039
|
-
const recipients = Array.isArray(to) ? to : [to];
|
|
1040
|
-
return recipients.length > 1 ? { to: from, bcc: recipients } : { to: recipients[0] };
|
|
1041
|
-
}
|
|
1042
|
-
function isRateLimited(err) {
|
|
1043
|
-
const msg = String(err?.message ?? err);
|
|
1044
|
-
const code = Number(err?.responseCode);
|
|
1045
|
-
return /too many|rate limit|throttl|try again|4\.7\.\d|too many emails/i.test(msg) || code === 421 || code === 429 || code === 450 || code === 451 || code === 550 && /too many|rate/i.test(msg);
|
|
1046
|
-
}
|
|
1047
|
-
async function sendMailWithRetry(transporter, msg, attempts = 5) {
|
|
1048
|
-
for (let i = 0; ; i++) {
|
|
1049
|
-
try {
|
|
1050
|
-
return await transporter.sendMail(msg);
|
|
1051
|
-
} catch (err) {
|
|
1052
|
-
if (i >= attempts - 1 || !isRateLimited(err)) throw err;
|
|
1053
|
-
const wait = 1e3 * 2 ** i;
|
|
1054
|
-
await sleep(wait);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
async function sendEmailToRecipient(to, details) {
|
|
1059
|
-
const from = config.NOTIFY_EMAIL_FROM;
|
|
1060
|
-
const hasRecipient = Array.isArray(to) ? to.length > 0 : !!to;
|
|
1061
|
-
if (!config.SMTP_URL || !from || !hasRecipient) return;
|
|
1062
|
-
try {
|
|
1063
|
-
const transporter = await getTransporter();
|
|
1064
|
-
if (!transporter) return;
|
|
1065
|
-
const { subject, text, html } = renderEmail(details);
|
|
1066
|
-
await sendMailWithRetry(transporter, {
|
|
1067
|
-
from,
|
|
1068
|
-
...recipientEnvelope(to, from),
|
|
1069
|
-
subject,
|
|
1070
|
-
text,
|
|
1071
|
-
html,
|
|
1072
|
-
disableFileAccess: true,
|
|
1073
|
-
disableUrlAccess: true
|
|
1074
|
-
});
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
if (!warnedOnce) {
|
|
1077
|
-
warnedOnce = true;
|
|
1078
|
-
console.warn(
|
|
1079
|
-
"[email] notification failed (suppressing further warnings):",
|
|
1080
|
-
err instanceof Error ? err.message : err
|
|
1081
|
-
);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
624
|
// src/governance/title-resolver.ts
|
|
1087
625
|
async function buildTitleMap(nestId) {
|
|
1088
626
|
try {
|
|
@@ -1519,23 +1057,9 @@ async function resolveMemberIdentity(params) {
|
|
|
1519
1057
|
if (!requested) {
|
|
1520
1058
|
throw new ValidationError("user_id or email is required");
|
|
1521
1059
|
}
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
);
|
|
1526
|
-
if (existing) {
|
|
1527
|
-
return {
|
|
1528
|
-
userId: existing.id,
|
|
1529
|
-
email: normalizeEmail(existing.email ?? requested)
|
|
1530
|
-
};
|
|
1531
|
-
}
|
|
1532
|
-
const { hashPassword } = await import("./keys-CTOGAG3W.js");
|
|
1533
|
-
const userId = uuid2();
|
|
1534
|
-
await db.run(
|
|
1535
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1536
|
-
[userId, requested, null, await hashPassword(uuid2())]
|
|
1537
|
-
);
|
|
1538
|
-
return { userId, email: requested };
|
|
1060
|
+
const { findOrCreateInvitedUser } = await import("./invited-user-AM6A3WGO.js");
|
|
1061
|
+
const { id } = await findOrCreateInvitedUser(requested);
|
|
1062
|
+
return { userId: id, email: requested };
|
|
1539
1063
|
}
|
|
1540
1064
|
async function replaceTeamRoster(params) {
|
|
1541
1065
|
for (const m of params.members) assertValidRole(m.role);
|
|
@@ -2066,6 +1590,21 @@ async function setPrimeOnlyReview(nestId, enabled) {
|
|
|
2066
1590
|
nestId
|
|
2067
1591
|
]);
|
|
2068
1592
|
}
|
|
1593
|
+
async function nestCreatorIsReviewer(nestId) {
|
|
1594
|
+
const db = getDb();
|
|
1595
|
+
const row = await db.get(
|
|
1596
|
+
"SELECT creator_is_reviewer FROM nests WHERE id = ?",
|
|
1597
|
+
[nestId]
|
|
1598
|
+
);
|
|
1599
|
+
return !!row?.creator_is_reviewer;
|
|
1600
|
+
}
|
|
1601
|
+
async function setCreatorIsReviewer(nestId, enabled) {
|
|
1602
|
+
const db = getDb();
|
|
1603
|
+
await db.run("UPDATE nests SET creator_is_reviewer = ? WHERE id = ?", [
|
|
1604
|
+
enabled ? 1 : 0,
|
|
1605
|
+
nestId
|
|
1606
|
+
]);
|
|
1607
|
+
}
|
|
2069
1608
|
async function nestPrimeTags(nestId) {
|
|
2070
1609
|
const db = getDb();
|
|
2071
1610
|
const row = await db.get("SELECT prime_tags FROM nests WHERE id = ?", [
|
|
@@ -2138,7 +1677,15 @@ async function renameNest(nestId, patch) {
|
|
|
2138
1677
|
);
|
|
2139
1678
|
return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]);
|
|
2140
1679
|
}
|
|
1680
|
+
async function assertCanCreateNest(userId) {
|
|
1681
|
+
if (config.AUTH_MODE === "open" || config.NEST_CREATION === "everyone" || await isServerAdminUserId(userId))
|
|
1682
|
+
return;
|
|
1683
|
+
throw new ForbiddenError(
|
|
1684
|
+
"Only a server admin can create nests on this server. Ask your admin to share a nest with you."
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
2141
1687
|
async function createNest(userId, name, description) {
|
|
1688
|
+
await assertCanCreateNest(userId);
|
|
2142
1689
|
const id = uuid3();
|
|
2143
1690
|
const db = getDb();
|
|
2144
1691
|
const trimmed = plainName(name);
|
|
@@ -2166,6 +1713,7 @@ async function createNest(userId, name, description) {
|
|
|
2166
1713
|
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
2167
1714
|
}
|
|
2168
1715
|
async function importNest(userId, name) {
|
|
1716
|
+
await assertCanCreateNest(userId);
|
|
2169
1717
|
const nm = plainName(name || "", "name is required");
|
|
2170
1718
|
const db = getDb();
|
|
2171
1719
|
const id = uuid3();
|
|
@@ -2622,8 +2170,8 @@ async function ensureNodeIndex(nestId) {
|
|
|
2622
2170
|
return run;
|
|
2623
2171
|
}
|
|
2624
2172
|
async function rebuildNodeIndex(nestId) {
|
|
2625
|
-
const { engineCache: engineCache2 } = await import("./engine-
|
|
2626
|
-
const { documentsWithSuggestions } = await import("./external-edit-service-
|
|
2173
|
+
const { engineCache: engineCache2 } = await import("./engine-QNMFDIBV.js");
|
|
2174
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-4MEGPT4D.js");
|
|
2627
2175
|
const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
|
|
2628
2176
|
dropDiscoveryCache();
|
|
2629
2177
|
const startedAt = writeGeneration.get(nestId) ?? 0;
|
|
@@ -2869,7 +2417,8 @@ var engineCache = new NestEngineCache();
|
|
|
2869
2417
|
var engineApi = createEngineApi();
|
|
2870
2418
|
async function opContext(nestId, actor, onProgress) {
|
|
2871
2419
|
const { storage, query, versions } = await engineCache.get(nestId);
|
|
2872
|
-
|
|
2420
|
+
const limits = { pdfMaxBytes: config.PDF_MAX_BYTES };
|
|
2421
|
+
return { storage, query, versions, actor, onProgress, limits };
|
|
2873
2422
|
}
|
|
2874
2423
|
|
|
2875
2424
|
export {
|
|
@@ -2895,15 +2444,6 @@ export {
|
|
|
2895
2444
|
getAccessConfig,
|
|
2896
2445
|
isSuperAdmin,
|
|
2897
2446
|
isConfigSuperAdmin,
|
|
2898
|
-
collabPermToRole,
|
|
2899
|
-
canViewWith,
|
|
2900
|
-
canEditWith,
|
|
2901
|
-
canApproveWith,
|
|
2902
|
-
primaryRole,
|
|
2903
|
-
verifySmtp,
|
|
2904
|
-
notifyEmailForNest,
|
|
2905
|
-
isEmailConfigured,
|
|
2906
|
-
sendEmailToRecipient,
|
|
2907
2447
|
nestStorageRoot,
|
|
2908
2448
|
resolveNestPath,
|
|
2909
2449
|
normalizeTag,
|
|
@@ -2917,6 +2457,8 @@ export {
|
|
|
2917
2457
|
setAllowSelfApprove,
|
|
2918
2458
|
nestReviewsPrimeOnly,
|
|
2919
2459
|
setPrimeOnlyReview,
|
|
2460
|
+
nestCreatorIsReviewer,
|
|
2461
|
+
setCreatorIsReviewer,
|
|
2920
2462
|
nestPrimeTags,
|
|
2921
2463
|
setPrimeTags,
|
|
2922
2464
|
disableStewardshipAndWipeGovernance,
|