@promptowl/contextnest-community 1.25.0 → 1.27.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 +102 -9
- package/CONFIGURATION.md +604 -117
- package/README.md +3 -2
- package/dist/{chunk-JDAOA5KP.js → chunk-H3W72HN3.js} +67 -14
- package/dist/{chunk-2KHM5C7V.js → chunk-MB52DWWP.js} +13 -15
- package/dist/{chunk-TI7HMP64.js → chunk-NFFUZF2B.js} +23 -2
- package/dist/{chunk-F66VCBLA.js → chunk-O3JEI33N.js} +51 -2
- package/dist/{chunk-7FROMOZM.js → chunk-QMQDWRHN.js} +180 -550
- package/dist/chunk-RXCVXG34.js +523 -0
- package/dist/{chunk-32SZCH36.js → chunk-SE5U4J4T.js} +4 -4
- package/dist/{chunk-KPGNZLZL.js → chunk-VHEMGQ3I.js} +1 -1
- package/dist/{chunk-DTLMBF4W.js → chunk-VRLAP4HB.js} +42 -17
- package/dist/{engine-TER6FDBP.js → engine-HLYQLP5T.js} +3 -2
- package/dist/{external-edit-service-CAGENHDJ.js → external-edit-service-3DFQ4EZZ.js} +4 -3
- package/dist/{grants-service-2OTFS7EH.js → grants-service-WICV4Y2J.js} +2 -2
- package/dist/index.js +1669 -521
- package/dist/invited-user-XEBX6BM4.js +12 -0
- package/dist/{migrations.postgres-23VJJJJX.js → migrations.postgres-HGSQ7IA5.js} +4 -0
- package/dist/{review-service-4G2XUFS6.js → review-service-GB43UQVH.js} +7 -6
- package/dist/{stewardship-service-S22V2JKG.js → stewardship-service-KDBPWRZI.js} +4 -3
- package/dist/{version-service-X4FAG2OZ.js → version-service-JYHFPSYP.js} +8 -3
- package/dist/web3/assets/ActivityTracePage-CkRoyIc-.js +1 -0
- package/dist/web3/assets/AgentDocsPage-BTRXmliv.js +1 -0
- package/dist/web3/assets/CollaboratorManager-CqJIkvs-.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-JcO1P-qm.js +1 -0
- package/dist/web3/assets/DocumentEditor-D1GLeeEP.js +36 -0
- package/dist/web3/assets/DocumentsTab-BAgSxTX1.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-DuuTXZap.js +1 -0
- package/dist/web3/assets/{MarkdownEditor-Dfk5uIE5.js → MarkdownEditor-BELFww9q.js} +130 -135
- package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
- package/dist/web3/assets/NestPageHeader-Bbje2ncY.js +1 -0
- package/dist/web3/assets/NestView-D4OMXPQ7.js +73 -0
- package/dist/web3/assets/OverviewTab-FIDGjR3Q.js +6 -0
- package/dist/web3/assets/PersonCombobox-D52m_jhc.js +1 -0
- package/dist/web3/assets/{ReviewActions-BXDTDPp1.js → ReviewActions-DYqBcWlv.js} +2 -2
- package/dist/web3/assets/ReviewTab-DoxfygOb.js +1 -0
- package/dist/web3/assets/StewardsTab-CFsYjbEn.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-AW2OLNAE.js +1 -0
- package/dist/web3/assets/{alert-dialog-4SiLKlz8.js → alert-dialog-p-fBLSWx.js} +2 -2
- package/dist/web3/assets/{arrow-left-BAgIocLn.js → arrow-left-CiErCCTG.js} +1 -1
- package/dist/web3/assets/backlinks-DaVSYvhV.js +19 -0
- package/dist/web3/assets/boxes-VWrCCMzW.js +6 -0
- package/dist/web3/assets/{chevron-left-XR4ReJ9Z.js → chevron-left-CsD8gZwL.js} +1 -1
- package/dist/web3/assets/{circle-check-CSkZUEFK.js → circle-check-BBCXd9LI.js} +1 -1
- package/dist/web3/assets/{circle-x-in2o3aHU.js → circle-x-C9LsnORQ.js} +1 -1
- package/dist/web3/assets/{code-xml-pPQgdctI.js → code-xml-BaoV7HiQ.js} +1 -1
- package/dist/web3/assets/{corner-down-right-CuxZWscX.js → corner-down-right-DgiTXrtw.js} +1 -1
- package/dist/web3/assets/count-skeleton-CnEE4uF-.js +1 -0
- package/dist/web3/assets/{earth-CXOcThqn.js → earth-BmMbVt9k.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-CSphs1VD.js → file-exclamation-point-qJeca9-A.js} +1 -1
- package/dist/web3/assets/{folder-input-BAwK9tFL.js → folder-input-D7R93LJ6.js} +1 -1
- package/dist/web3/assets/index-DNmWTxN7.css +1 -0
- package/dist/web3/assets/index-DvuUIUP6.js +451 -0
- package/dist/web3/assets/{page-CUjuSs5Q.js → page-BJQuaTRW.js} +3 -3
- package/dist/web3/assets/page-Bb8xGOPQ.js +2 -0
- package/dist/web3/assets/page-BtM-qDzr.js +6 -0
- package/dist/web3/assets/page-Bxd0EG25.js +1 -0
- package/dist/web3/assets/page-C3QCO6IR.js +1 -0
- package/dist/web3/assets/page-CfoR0khy.js +6 -0
- package/dist/web3/assets/page-D7Zr5tAI.js +1 -0
- package/dist/web3/assets/page-DATSlHCW.js +6 -0
- package/dist/web3/assets/page-DTjPQizZ.js +45 -0
- package/dist/web3/assets/page-DspptlWr.js +1 -0
- package/dist/web3/assets/{page-xzYqkBUP.js → page-I6vAFJOj.js} +5 -5
- package/dist/web3/assets/page-QxI4Ud2s.js +9 -0
- package/dist/web3/assets/page-YcNNg-dX.js +1 -0
- package/dist/web3/assets/page-kM7eO5Kj.js +1 -0
- package/dist/web3/assets/{page-title-BkqlUA0j.js → page-title-Dn5jANiU.js} +1 -1
- package/dist/web3/assets/{play-B8xE9j5f.js → play-B2k_Ix-L.js} +1 -1
- package/dist/web3/assets/{send-CvAgOUPG.js → send-DPNxv3As.js} +1 -1
- package/dist/web3/assets/{settings-EY4A_DYv.js → settings-DZjq-OQA.js} +1 -1
- package/dist/web3/assets/{share-2-vGmZUl90.js → share-2-C-OnE2ru.js} +1 -1
- package/dist/web3/assets/{tag-Br_y1f00.js → tag-CwJl7FdI.js} +1 -1
- package/dist/web3/assets/{trash-2-B4hRXo-p.js → trash-2-aljmIff4.js} +1 -1
- package/dist/web3/assets/{user-plus-4CTYEkd3.js → user-plus-DPerZexX.js} +1 -1
- package/dist/web3/assets/{zap-BcsKR0ef.js → zap-CHZtiN7P.js} +1 -1
- package/dist/web3/index.html +2 -2
- package/package.json +8 -3
- package/dist/client-D44ZHDTJ.js +0 -11
- package/dist/keys-CTOGAG3W.js +0 -20
- package/dist/web3/assets/ActivityTracePage-D2iIbEph.js +0 -1
- package/dist/web3/assets/AgentDocsPage-Dkhgb1hk.js +0 -1
- package/dist/web3/assets/CollaboratorManager-Bo81D--Q.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-DRCitQuX.js +0 -1
- package/dist/web3/assets/DocumentEditor-DV2WHYbj.js +0 -36
- package/dist/web3/assets/DocumentsTab-BUxBCN8o.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-kIBimZyE.js +0 -1
- package/dist/web3/assets/MarkdownEditor-CsVusP2d.css +0 -1
- package/dist/web3/assets/NestPageHeader-yOK-OIYZ.js +0 -1
- package/dist/web3/assets/NestView-DwIagxwJ.js +0 -68
- package/dist/web3/assets/OverviewTab-DFOFMVyE.js +0 -1
- package/dist/web3/assets/PersonCombobox-D350WlrJ.js +0 -1
- package/dist/web3/assets/ReasonDialog-CN3ECAnQ.js +0 -1
- package/dist/web3/assets/ReviewTab-CZewAYiz.js +0 -1
- package/dist/web3/assets/StewardsTab-AagNWcWS.js +0 -1
- package/dist/web3/assets/SubmitForReviewModal-BOZJxwRN.js +0 -1
- package/dist/web3/assets/backlinks-Cb8Uf8mw.js +0 -24
- package/dist/web3/assets/card-UDyMRcps.js +0 -1
- package/dist/web3/assets/count-skeleton-DBWCDhRy.js +0 -1
- package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
- package/dist/web3/assets/index-A0_ymcqL.js +0 -29
- package/dist/web3/assets/index-B5hLclEq.js +0 -389
- package/dist/web3/assets/index-DrUAtoQM.css +0 -1
- package/dist/web3/assets/page-BHD4beun.js +0 -1
- package/dist/web3/assets/page-BSYUXYmO.js +0 -11
- package/dist/web3/assets/page-BYc2DIux.js +0 -45
- package/dist/web3/assets/page-BfibLg8e.js +0 -1
- package/dist/web3/assets/page-CGe8MtUu.js +0 -9
- package/dist/web3/assets/page-CIgPJmG6.js +0 -2
- package/dist/web3/assets/page-Ck31nx9b.js +0 -1
- package/dist/web3/assets/page-Cpdj_11Y.js +0 -1
- package/dist/web3/assets/page-D0sN3GO6.js +0 -1
- package/dist/web3/assets/page-DEpH91X2.js +0 -1
- package/dist/web3/assets/page-l8-B7CGt.js +0 -1
- package/dist/web3/assets/page-mnSHmk6A.js +0 -6
- package/dist/web3/assets/refresh-cw-CsCtmWwX.js +0 -6
- package/dist/web3/assets/scroll-area-CbcH6yrZ.js +0 -1
- package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
- package/dist/web3/assets/select-DrL0mpRN.js +0 -6
- package/dist/web3/assets/triangle-alert-CsYGyfGZ.js +0 -6
- package/dist/web3/assets/x-qxt1WrUi.js +0 -6
|
@@ -4,11 +4,16 @@ import {
|
|
|
4
4
|
NotFoundError,
|
|
5
5
|
ValidationError
|
|
6
6
|
} from "./chunk-LHWOJPFE.js";
|
|
7
|
+
import {
|
|
8
|
+
permissionLabel,
|
|
9
|
+
sendEmailToRecipient,
|
|
10
|
+
teamRoleToPermission
|
|
11
|
+
} from "./chunk-RXCVXG34.js";
|
|
7
12
|
import {
|
|
8
13
|
config,
|
|
9
14
|
getDb,
|
|
10
15
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-H3W72HN3.js";
|
|
12
17
|
import {
|
|
13
18
|
ANON_USER_ID
|
|
14
19
|
} from "./chunk-YB3LKF7U.js";
|
|
@@ -151,6 +156,102 @@ function insertOrReplace(db, insertSql, conflictCols, set) {
|
|
|
151
156
|
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
|
|
152
157
|
}
|
|
153
158
|
|
|
159
|
+
// src/shared/secret-box.ts
|
|
160
|
+
import {
|
|
161
|
+
createCipheriv,
|
|
162
|
+
createDecipheriv,
|
|
163
|
+
createHash as createHash2,
|
|
164
|
+
randomBytes
|
|
165
|
+
} from "crypto";
|
|
166
|
+
var PREFIX = "enc.v1.";
|
|
167
|
+
var b64 = (b) => b.toString("base64url");
|
|
168
|
+
var MASTER_KEY_SETTING = "SECRETS_MASTER_KEY";
|
|
169
|
+
var SECRET_SETTINGS = /* @__PURE__ */ new Set([
|
|
170
|
+
"ANTHROPIC_API_KEY",
|
|
171
|
+
"OIDC_CLIENT_SECRET",
|
|
172
|
+
"MCP_SIGNING_SECRET",
|
|
173
|
+
"SMTP_URL",
|
|
174
|
+
"SLACK_WEBHOOK_URL",
|
|
175
|
+
"MSTEAMS_WEBHOOK_URL",
|
|
176
|
+
"SES_SECRET_ACCESS_KEY",
|
|
177
|
+
"PROMPTOWL_KEY"
|
|
178
|
+
]);
|
|
179
|
+
function isSecretSetting(name) {
|
|
180
|
+
return SECRET_SETTINGS.has(name);
|
|
181
|
+
}
|
|
182
|
+
function isEncrypted(value) {
|
|
183
|
+
return value.startsWith(PREFIX);
|
|
184
|
+
}
|
|
185
|
+
async function encryptSecret(plain) {
|
|
186
|
+
if (isEncrypted(plain)) return plain;
|
|
187
|
+
const iv = randomBytes(12);
|
|
188
|
+
const cipher = createCipheriv("aes-256-gcm", await masterKey(), iv);
|
|
189
|
+
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
190
|
+
return PREFIX + [b64(iv), b64(cipher.getAuthTag()), b64(ct)].join(".");
|
|
191
|
+
}
|
|
192
|
+
async function decryptSecret(stored) {
|
|
193
|
+
if (!isEncrypted(stored)) return stored;
|
|
194
|
+
const [iv, tag, ct] = stored.slice(PREFIX.length).split(".");
|
|
195
|
+
if (!iv || !tag || !ct) throw new Error("malformed encrypted value");
|
|
196
|
+
const decipher = createDecipheriv(
|
|
197
|
+
"aes-256-gcm",
|
|
198
|
+
await masterKey(),
|
|
199
|
+
Buffer.from(iv, "base64url")
|
|
200
|
+
);
|
|
201
|
+
decipher.setAuthTag(Buffer.from(tag, "base64url"));
|
|
202
|
+
return Buffer.concat([
|
|
203
|
+
decipher.update(Buffer.from(ct, "base64url")),
|
|
204
|
+
decipher.final()
|
|
205
|
+
]).toString("utf8");
|
|
206
|
+
}
|
|
207
|
+
async function tryDecryptSecret(stored, label) {
|
|
208
|
+
try {
|
|
209
|
+
return await decryptSecret(stored);
|
|
210
|
+
} catch {
|
|
211
|
+
console.error(
|
|
212
|
+
`[secrets] cannot decrypt ${label} \u2014 SECRETS_KEY does not match the key this value was written with. Unset it to fall back to the stored key, or re-enter the value to replace it.`
|
|
213
|
+
);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
var cached = null;
|
|
218
|
+
var inFlight = null;
|
|
219
|
+
async function masterKey() {
|
|
220
|
+
const fromEnv = (process.env.SECRETS_KEY || "").trim();
|
|
221
|
+
if (fromEnv) {
|
|
222
|
+
if (cached?.id !== fromEnv) {
|
|
223
|
+
cached = { id: fromEnv, key: createHash2("sha256").update(fromEnv).digest() };
|
|
224
|
+
}
|
|
225
|
+
return cached.key;
|
|
226
|
+
}
|
|
227
|
+
const db = getDb();
|
|
228
|
+
const id = `db:${db.dialect}:${dbIdentity(db)}`;
|
|
229
|
+
if (cached?.id === id) return cached.key;
|
|
230
|
+
if (!inFlight) inFlight = loadOrCreateKey(id).finally(() => inFlight = null);
|
|
231
|
+
return inFlight;
|
|
232
|
+
}
|
|
233
|
+
async function loadOrCreateKey(id) {
|
|
234
|
+
const db = getDb();
|
|
235
|
+
const read = async () => await db.get("SELECT value FROM server_settings WHERE key = ?", [
|
|
236
|
+
MASTER_KEY_SETTING
|
|
237
|
+
]);
|
|
238
|
+
let row = await read();
|
|
239
|
+
if (!row?.value) {
|
|
240
|
+
const sql = db.dialect === "sqlite" ? `INSERT OR IGNORE INTO server_settings (key, value, env_at_write, updated_at)
|
|
241
|
+
VALUES (?, ?, NULL, ${nowExpr(db)})` : `INSERT INTO server_settings (key, value, env_at_write, updated_at)
|
|
242
|
+
VALUES (?, ?, NULL, ${nowExpr(db)}) ON CONFLICT (key) DO NOTHING`;
|
|
243
|
+
await db.run(sql, [MASTER_KEY_SETTING, randomBytes(32).toString("hex")]);
|
|
244
|
+
row = await read();
|
|
245
|
+
}
|
|
246
|
+
if (!row?.value) throw new Error("could not establish an encryption key");
|
|
247
|
+
const key = Buffer.from(row.value, "hex");
|
|
248
|
+
cached = { id, key };
|
|
249
|
+
return key;
|
|
250
|
+
}
|
|
251
|
+
function dbIdentity(db) {
|
|
252
|
+
return process.env.DATABASE_URL || process.env.DATA_ROOT || db.dialect;
|
|
253
|
+
}
|
|
254
|
+
|
|
154
255
|
// src/db/settings.ts
|
|
155
256
|
var deployBaseline = null;
|
|
156
257
|
function norm(v) {
|
|
@@ -163,6 +264,9 @@ function baseline() {
|
|
|
163
264
|
async function persistSetting(name, value) {
|
|
164
265
|
const db = getDb();
|
|
165
266
|
const envAtWrite = norm(baseline()[name]);
|
|
267
|
+
const seal = async (v) => v !== null && isSecretSetting(name) ? await encryptSecret(v) : v;
|
|
268
|
+
const sealedValue = await seal(value);
|
|
269
|
+
const sealedBaseline = await seal(envAtWrite);
|
|
166
270
|
await db.run(
|
|
167
271
|
insertOrReplace(
|
|
168
272
|
db,
|
|
@@ -171,7 +275,7 @@ async function persistSetting(name, value) {
|
|
|
171
275
|
["key"],
|
|
172
276
|
`value = excluded.value, env_at_write = excluded.env_at_write, updated_at = ${nowExpr(db)}`
|
|
173
277
|
),
|
|
174
|
-
[name,
|
|
278
|
+
[name, sealedValue, sealedBaseline]
|
|
175
279
|
);
|
|
176
280
|
}
|
|
177
281
|
async function loadServerSettings() {
|
|
@@ -183,7 +287,12 @@ async function loadServerSettings() {
|
|
|
183
287
|
);
|
|
184
288
|
let applied = 0;
|
|
185
289
|
for (const row of rows) {
|
|
290
|
+
if (row.key === MASTER_KEY_SETTING) continue;
|
|
186
291
|
const envVal = norm(base[row.key]);
|
|
292
|
+
if (isSecretSetting(row.key)) {
|
|
293
|
+
row.value = row.value === null ? null : await tryDecryptSecret(row.value, row.key);
|
|
294
|
+
row.env_at_write = row.env_at_write === null ? null : await tryDecryptSecret(row.env_at_write, `${row.key} (baseline)`);
|
|
295
|
+
}
|
|
187
296
|
if (envVal !== norm(row.env_at_write)) {
|
|
188
297
|
await persistSetting(row.key, envVal);
|
|
189
298
|
if (envVal === null) delete process.env[row.key];
|
|
@@ -226,7 +335,7 @@ async function installLicenseKey(key) {
|
|
|
226
335
|
}
|
|
227
336
|
const previousKey = process.env.PROMPTOWL_KEY || "";
|
|
228
337
|
process.env.PROMPTOWL_KEY = trimmed;
|
|
229
|
-
const info = await validateLicense({ forceFresh: true });
|
|
338
|
+
const info = await validateLicense({ forceFresh: true, strict: true });
|
|
230
339
|
if (!info.valid) {
|
|
231
340
|
process.env.PROMPTOWL_KEY = previousKey;
|
|
232
341
|
if (previousKey) {
|
|
@@ -309,12 +418,26 @@ function isSuspended() {
|
|
|
309
418
|
function getSuspensionReason() {
|
|
310
419
|
return suspensionReason;
|
|
311
420
|
}
|
|
421
|
+
var REJECTION_STRIKES_TO_REVOKE = 3;
|
|
422
|
+
var rejectionStreak = 0;
|
|
423
|
+
var rejectionKey = null;
|
|
424
|
+
function fromCache(cached2) {
|
|
425
|
+
return {
|
|
426
|
+
valid: true,
|
|
427
|
+
tier: cached2.tier,
|
|
428
|
+
org: cached2.org,
|
|
429
|
+
limits: cached2.limits_json ? JSON.parse(cached2.limits_json) : null,
|
|
430
|
+
suspended: suspensionConfirmed,
|
|
431
|
+
suspendedReason: suspensionReason,
|
|
432
|
+
ownerEmail: cached2.owner_email || null
|
|
433
|
+
};
|
|
434
|
+
}
|
|
312
435
|
async function validateLicense(opts = {}) {
|
|
313
|
-
const info = await _validateLicenseImpl(!!opts.forceFresh);
|
|
436
|
+
const info = await _validateLicenseImpl(!!opts.forceFresh, !!opts.strict);
|
|
314
437
|
currentLicense = info;
|
|
315
438
|
return info;
|
|
316
439
|
}
|
|
317
|
-
async function _validateLicenseImpl(forceFresh) {
|
|
440
|
+
async function _validateLicenseImpl(forceFresh, strict) {
|
|
318
441
|
const key = config.PROMPTOWL_KEY;
|
|
319
442
|
if (!key) {
|
|
320
443
|
return {
|
|
@@ -328,22 +451,14 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
328
451
|
};
|
|
329
452
|
}
|
|
330
453
|
const db = getDb();
|
|
331
|
-
const
|
|
454
|
+
const cached2 = await db.get(
|
|
332
455
|
"SELECT * FROM license_cache WHERE key = ?",
|
|
333
456
|
[key]
|
|
334
457
|
);
|
|
335
|
-
if (
|
|
336
|
-
const age = Date.now() - (/* @__PURE__ */ new Date(
|
|
458
|
+
if (cached2 && !forceFresh) {
|
|
459
|
+
const age = Date.now() - (/* @__PURE__ */ new Date(cached2.validated_at + "Z")).getTime();
|
|
337
460
|
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
|
-
};
|
|
461
|
+
return fromCache(cached2);
|
|
347
462
|
}
|
|
348
463
|
}
|
|
349
464
|
try {
|
|
@@ -354,19 +469,11 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
354
469
|
body: JSON.stringify({ key })
|
|
355
470
|
});
|
|
356
471
|
if (!res.ok) {
|
|
357
|
-
if (
|
|
472
|
+
if (cached2) {
|
|
358
473
|
console.warn(
|
|
359
474
|
" PromptOwl unreachable, using cached license (grace period)"
|
|
360
475
|
);
|
|
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
|
-
};
|
|
476
|
+
return fromCache(cached2);
|
|
370
477
|
}
|
|
371
478
|
return {
|
|
372
479
|
valid: false,
|
|
@@ -412,6 +519,20 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
412
519
|
`
|
|
413
520
|
);
|
|
414
521
|
}
|
|
522
|
+
if (data.valid !== true) {
|
|
523
|
+
rejectionStreak = 0;
|
|
524
|
+
rejectionKey = null;
|
|
525
|
+
if (cached2) return fromCache(cached2);
|
|
526
|
+
return {
|
|
527
|
+
valid: false,
|
|
528
|
+
tier: "none",
|
|
529
|
+
org: null,
|
|
530
|
+
limits: null,
|
|
531
|
+
suspended: suspensionConfirmed,
|
|
532
|
+
suspendedReason: suspensionReason,
|
|
533
|
+
ownerEmail: data.owner_email || null
|
|
534
|
+
};
|
|
535
|
+
}
|
|
415
536
|
} else {
|
|
416
537
|
if (suspensionFirstSeen) {
|
|
417
538
|
console.log(" Suspension flag cleared by PromptOwl.");
|
|
@@ -421,6 +542,18 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
421
542
|
suspensionReason = null;
|
|
422
543
|
}
|
|
423
544
|
if (!data.valid && !data.suspended) {
|
|
545
|
+
if (cached2 && !strict) {
|
|
546
|
+
rejectionStreak = rejectionKey === key ? rejectionStreak + 1 : 1;
|
|
547
|
+
rejectionKey = key;
|
|
548
|
+
if (rejectionStreak < REJECTION_STRIKES_TO_REVOKE) {
|
|
549
|
+
console.warn(
|
|
550
|
+
`[license] PromptOwl rejected the license (${rejectionStreak}/${REJECTION_STRIKES_TO_REVOKE}); keeping the cached license until the rejection is confirmed`
|
|
551
|
+
);
|
|
552
|
+
return fromCache(cached2);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
rejectionStreak = 0;
|
|
556
|
+
rejectionKey = null;
|
|
424
557
|
await db.run("DELETE FROM license_cache WHERE key = ?", [key]);
|
|
425
558
|
return {
|
|
426
559
|
valid: false,
|
|
@@ -433,6 +566,8 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
433
566
|
};
|
|
434
567
|
}
|
|
435
568
|
if (data.valid) {
|
|
569
|
+
rejectionStreak = 0;
|
|
570
|
+
rejectionKey = null;
|
|
436
571
|
const limitsJson = data.limits ? JSON.stringify(data.limits) : null;
|
|
437
572
|
await db.run(
|
|
438
573
|
insertOrReplace(
|
|
@@ -461,19 +596,11 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
461
596
|
ownerEmail: data.owner_email || null
|
|
462
597
|
};
|
|
463
598
|
} catch (err) {
|
|
464
|
-
if (
|
|
599
|
+
if (cached2) {
|
|
465
600
|
console.warn(
|
|
466
601
|
` PromptOwl validation failed (${err.message}), using cached license`
|
|
467
602
|
);
|
|
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
|
-
};
|
|
603
|
+
return fromCache(cached2);
|
|
477
604
|
}
|
|
478
605
|
return {
|
|
479
606
|
valid: false,
|
|
@@ -598,491 +725,6 @@ function parseAccessYaml(content) {
|
|
|
598
725
|
// src/governance/teams-service.ts
|
|
599
726
|
import { v4 as uuid2 } from "uuid";
|
|
600
727
|
|
|
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
728
|
// src/governance/title-resolver.ts
|
|
1087
729
|
async function buildTitleMap(nestId) {
|
|
1088
730
|
try {
|
|
@@ -1519,23 +1161,9 @@ async function resolveMemberIdentity(params) {
|
|
|
1519
1161
|
if (!requested) {
|
|
1520
1162
|
throw new ValidationError("user_id or email is required");
|
|
1521
1163
|
}
|
|
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 };
|
|
1164
|
+
const { findOrCreateInvitedUser } = await import("./invited-user-XEBX6BM4.js");
|
|
1165
|
+
const { id } = await findOrCreateInvitedUser(requested);
|
|
1166
|
+
return { userId: id, email: requested };
|
|
1539
1167
|
}
|
|
1540
1168
|
async function replaceTeamRoster(params) {
|
|
1541
1169
|
for (const m of params.members) assertValidRole(m.role);
|
|
@@ -2153,7 +1781,15 @@ async function renameNest(nestId, patch) {
|
|
|
2153
1781
|
);
|
|
2154
1782
|
return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]);
|
|
2155
1783
|
}
|
|
1784
|
+
async function assertCanCreateNest(userId) {
|
|
1785
|
+
if (config.AUTH_MODE === "open" || config.NEST_CREATION === "everyone" || await isServerAdminUserId(userId))
|
|
1786
|
+
return;
|
|
1787
|
+
throw new ForbiddenError(
|
|
1788
|
+
"Only a server admin can create nests on this server. Ask your admin to share a nest with you."
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
2156
1791
|
async function createNest(userId, name, description) {
|
|
1792
|
+
await assertCanCreateNest(userId);
|
|
2157
1793
|
const id = uuid3();
|
|
2158
1794
|
const db = getDb();
|
|
2159
1795
|
const trimmed = plainName(name);
|
|
@@ -2181,6 +1817,7 @@ async function createNest(userId, name, description) {
|
|
|
2181
1817
|
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
2182
1818
|
}
|
|
2183
1819
|
async function importNest(userId, name) {
|
|
1820
|
+
await assertCanCreateNest(userId);
|
|
2184
1821
|
const nm = plainName(name || "", "name is required");
|
|
2185
1822
|
const db = getDb();
|
|
2186
1823
|
const id = uuid3();
|
|
@@ -2637,8 +2274,8 @@ async function ensureNodeIndex(nestId) {
|
|
|
2637
2274
|
return run;
|
|
2638
2275
|
}
|
|
2639
2276
|
async function rebuildNodeIndex(nestId) {
|
|
2640
|
-
const { engineCache: engineCache2 } = await import("./engine-
|
|
2641
|
-
const { documentsWithSuggestions } = await import("./external-edit-service-
|
|
2277
|
+
const { engineCache: engineCache2 } = await import("./engine-HLYQLP5T.js");
|
|
2278
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-3DFQ4EZZ.js");
|
|
2642
2279
|
const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
|
|
2643
2280
|
dropDiscoveryCache();
|
|
2644
2281
|
const startedAt = writeGeneration.get(nestId) ?? 0;
|
|
@@ -2896,6 +2533,8 @@ export {
|
|
|
2896
2533
|
insertOrReplace,
|
|
2897
2534
|
trackEvent,
|
|
2898
2535
|
startTelemetryLoop,
|
|
2536
|
+
encryptSecret,
|
|
2537
|
+
tryDecryptSecret,
|
|
2899
2538
|
persistSetting,
|
|
2900
2539
|
loadServerSettings,
|
|
2901
2540
|
getCurrentLicense,
|
|
@@ -2911,15 +2550,6 @@ export {
|
|
|
2911
2550
|
getAccessConfig,
|
|
2912
2551
|
isSuperAdmin,
|
|
2913
2552
|
isConfigSuperAdmin,
|
|
2914
|
-
collabPermToRole,
|
|
2915
|
-
canViewWith,
|
|
2916
|
-
canEditWith,
|
|
2917
|
-
canApproveWith,
|
|
2918
|
-
primaryRole,
|
|
2919
|
-
verifySmtp,
|
|
2920
|
-
notifyEmailForNest,
|
|
2921
|
-
isEmailConfigured,
|
|
2922
|
-
sendEmailToRecipient,
|
|
2923
2553
|
nestStorageRoot,
|
|
2924
2554
|
resolveNestPath,
|
|
2925
2555
|
normalizeTag,
|