@promptowl/contextnest-community 0.1.0-alpha.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURATION.md +118 -118
- package/dist/{chunk-Q2DCOS7V.js → chunk-2FXVMVZJ.js} +53 -4
- package/dist/{chunk-USIDOGVJ.js → chunk-2TW25QEA.js} +79 -3
- package/dist/{chunk-P6NG56CO.js → chunk-BLOPZDPL.js} +25 -2
- package/dist/{chunk-DJFEV4ET.js → chunk-XDCW4HTW.js} +2 -2
- package/dist/index.js +1014 -326
- package/dist/{review-service-5CLVZKAR.js → review-service-2JHZHZWJ.js} +3 -3
- package/dist/{stewardship-service-NC67XBYO.js → stewardship-service-ZJATH6OM.js} +2 -2
- package/dist/{version-service-Z6FYJRAG.js → version-service-2MZJGE3H.js} +4 -2
- package/dist/web3/assets/index-BlGzOlFt.css +1 -0
- package/dist/web3/assets/index-C3W5d7fT.js +591 -0
- package/dist/web3/index.html +2 -2
- package/package.json +125 -108
- package/dist/web3/assets/index-CemroDXg.css +0 -1
- package/dist/web3/assets/index-xLLf4lHJ.js +0 -332
package/dist/index.js
CHANGED
|
@@ -12,17 +12,19 @@ import {
|
|
|
12
12
|
createVersion,
|
|
13
13
|
getApprovedVersion,
|
|
14
14
|
getCurrentVersion,
|
|
15
|
+
getDisplayStatus,
|
|
15
16
|
getVersions,
|
|
16
17
|
setApprovedVersion
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-BLOPZDPL.js";
|
|
18
19
|
import {
|
|
19
20
|
approve,
|
|
20
21
|
cancelReview,
|
|
22
|
+
getPendingReview,
|
|
21
23
|
getReviewHistory,
|
|
22
24
|
getReviewQueue,
|
|
23
25
|
reject,
|
|
24
26
|
submitForReview
|
|
25
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-XDCW4HTW.js";
|
|
26
28
|
import {
|
|
27
29
|
assignSteward,
|
|
28
30
|
canUserAccess,
|
|
@@ -37,11 +39,11 @@ import {
|
|
|
37
39
|
resolveStewardsForNode,
|
|
38
40
|
resolveStewardsWithFallback,
|
|
39
41
|
syncFromConfig
|
|
40
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-2FXVMVZJ.js";
|
|
41
43
|
import {
|
|
42
44
|
config,
|
|
43
45
|
getDb
|
|
44
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-2TW25QEA.js";
|
|
45
47
|
|
|
46
48
|
// src/index.ts
|
|
47
49
|
import { serve } from "@hono/node-server";
|
|
@@ -49,7 +51,6 @@ import { serve } from "@hono/node-server";
|
|
|
49
51
|
// src/app.ts
|
|
50
52
|
import { Hono as Hono8 } from "hono";
|
|
51
53
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
52
|
-
import { logger } from "hono/logger";
|
|
53
54
|
import { cors } from "hono/cors";
|
|
54
55
|
|
|
55
56
|
// src/auth/routes.ts
|
|
@@ -58,23 +59,113 @@ import { v4 as uuid } from "uuid";
|
|
|
58
59
|
|
|
59
60
|
// src/auth/middleware.ts
|
|
60
61
|
import { createMiddleware } from "hono/factory";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
|
|
63
|
+
// src/auth/sessions.ts
|
|
64
|
+
import { randomBytes } from "crypto";
|
|
65
|
+
var SESSION_COOKIE = "cnst_session";
|
|
66
|
+
var SESSION_TTL_DAYS = 30;
|
|
67
|
+
var SESSION_TTL_SECS = SESSION_TTL_DAYS * 24 * 60 * 60;
|
|
68
|
+
function newSessionId() {
|
|
69
|
+
return randomBytes(32).toString("hex");
|
|
70
|
+
}
|
|
71
|
+
function expiryIso(ttlSeconds = SESSION_TTL_SECS) {
|
|
72
|
+
return new Date(Date.now() + ttlSeconds * 1e3).toISOString();
|
|
73
|
+
}
|
|
74
|
+
function createSession(userId, userAgent) {
|
|
75
|
+
const db = getDb();
|
|
76
|
+
const id = newSessionId();
|
|
77
|
+
db.prepare(
|
|
78
|
+
`INSERT INTO sessions (id, user_id, expires_at, user_agent)
|
|
79
|
+
VALUES (?, ?, ?, ?)`
|
|
80
|
+
).run(id, userId, expiryIso(), userAgent || null);
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
function resolveSession(id) {
|
|
67
84
|
const db = getDb();
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
const row = db.prepare(
|
|
86
|
+
`SELECT user_id, expires_at FROM sessions WHERE id = ?`
|
|
87
|
+
).get(id);
|
|
88
|
+
if (!row) return null;
|
|
89
|
+
if (new Date(row.expires_at).getTime() <= Date.now()) {
|
|
90
|
+
db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
91
|
+
return null;
|
|
71
92
|
}
|
|
72
93
|
db.prepare(
|
|
73
|
-
"UPDATE
|
|
74
|
-
).run(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
"UPDATE sessions SET last_seen_at = datetime('now') WHERE id = ?"
|
|
95
|
+
).run(id);
|
|
96
|
+
return row.user_id;
|
|
97
|
+
}
|
|
98
|
+
function deleteSession(id) {
|
|
99
|
+
const db = getDb();
|
|
100
|
+
db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
101
|
+
}
|
|
102
|
+
function deleteAllSessionsForUser(userId) {
|
|
103
|
+
const db = getDb();
|
|
104
|
+
db.prepare("DELETE FROM sessions WHERE user_id = ?").run(userId);
|
|
105
|
+
}
|
|
106
|
+
function getSessionIdFromRequest(c) {
|
|
107
|
+
const cookieHeader = c.req.header("Cookie");
|
|
108
|
+
if (!cookieHeader) return null;
|
|
109
|
+
for (const part of cookieHeader.split(";")) {
|
|
110
|
+
const [k, ...rest] = part.trim().split("=");
|
|
111
|
+
if (k === SESSION_COOKIE) {
|
|
112
|
+
const v = rest.join("=").trim();
|
|
113
|
+
return /^[0-9a-f]{64}$/.test(v) ? v : null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function buildSessionCookie(id, opts = {}) {
|
|
119
|
+
const maxAge = opts.maxAgeSeconds ?? SESSION_TTL_SECS;
|
|
120
|
+
const parts = [
|
|
121
|
+
`${SESSION_COOKIE}=${id}`,
|
|
122
|
+
"HttpOnly",
|
|
123
|
+
"Path=/",
|
|
124
|
+
"SameSite=Lax",
|
|
125
|
+
`Max-Age=${maxAge}`
|
|
126
|
+
];
|
|
127
|
+
if (opts.secure) parts.push("Secure");
|
|
128
|
+
return parts.join("; ");
|
|
129
|
+
}
|
|
130
|
+
function buildClearSessionCookie(opts = {}) {
|
|
131
|
+
return buildSessionCookie("", { secure: opts.secure, maxAgeSeconds: 0 });
|
|
132
|
+
}
|
|
133
|
+
function isSecureRequest(c) {
|
|
134
|
+
const proto = c.req.header("x-forwarded-proto");
|
|
135
|
+
if (proto) return proto.split(",")[0].trim() === "https";
|
|
136
|
+
try {
|
|
137
|
+
return new URL(c.req.url).protocol === "https:";
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/auth/middleware.ts
|
|
144
|
+
var authMiddleware = createMiddleware(async (c, next) => {
|
|
145
|
+
const db = getDb();
|
|
146
|
+
const sessionId = getSessionIdFromRequest(c);
|
|
147
|
+
if (sessionId) {
|
|
148
|
+
const userId = resolveSession(sessionId);
|
|
149
|
+
if (userId) {
|
|
150
|
+
c.set("userId", userId);
|
|
151
|
+
c.set("nestScope", null);
|
|
152
|
+
return next();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const key = parseBearerToken(c.req.header("Authorization"));
|
|
156
|
+
if (key) {
|
|
157
|
+
const keyHash = hashApiKey(key);
|
|
158
|
+
const record = db.prepare("SELECT user_id, nest_id FROM api_keys WHERE key_hash = ?").get(keyHash);
|
|
159
|
+
if (record) {
|
|
160
|
+
db.prepare(
|
|
161
|
+
"UPDATE api_keys SET last_used_at = datetime('now') WHERE key_hash = ?"
|
|
162
|
+
).run(keyHash);
|
|
163
|
+
c.set("userId", record.user_id);
|
|
164
|
+
c.set("nestScope", record.nest_id);
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return c.json({ error: "Missing or invalid credentials" }, 401);
|
|
78
169
|
});
|
|
79
170
|
|
|
80
171
|
// src/shared/errors.ts
|
|
@@ -84,6 +175,7 @@ var AppError = class extends Error {
|
|
|
84
175
|
this.statusCode = statusCode;
|
|
85
176
|
this.name = "AppError";
|
|
86
177
|
}
|
|
178
|
+
statusCode;
|
|
87
179
|
};
|
|
88
180
|
var NotFoundError = class extends AppError {
|
|
89
181
|
constructor(message = "Not found") {
|
|
@@ -97,6 +189,12 @@ var ValidationError = class extends AppError {
|
|
|
97
189
|
this.name = "ValidationError";
|
|
98
190
|
}
|
|
99
191
|
};
|
|
192
|
+
var ConflictError = class extends AppError {
|
|
193
|
+
constructor(message) {
|
|
194
|
+
super(409, message);
|
|
195
|
+
this.name = "ConflictError";
|
|
196
|
+
}
|
|
197
|
+
};
|
|
100
198
|
|
|
101
199
|
// src/telemetry/tracker.ts
|
|
102
200
|
function trackEvent(event, data) {
|
|
@@ -154,6 +252,338 @@ function startTelemetryLoop() {
|
|
|
154
252
|
);
|
|
155
253
|
}
|
|
156
254
|
|
|
255
|
+
// src/auth/license.ts
|
|
256
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
257
|
+
var currentLicense = null;
|
|
258
|
+
function getCurrentLicense() {
|
|
259
|
+
return currentLicense;
|
|
260
|
+
}
|
|
261
|
+
function isLicenseAdminEmail(email) {
|
|
262
|
+
if (!email) return false;
|
|
263
|
+
const lic = currentLicense;
|
|
264
|
+
if (!lic?.valid || !lic.ownerEmail) return false;
|
|
265
|
+
return lic.ownerEmail.toLowerCase() === email.toLowerCase();
|
|
266
|
+
}
|
|
267
|
+
function isLicenseAdminUserId(userId) {
|
|
268
|
+
try {
|
|
269
|
+
const row = getDb().prepare("SELECT email FROM users WHERE id = ?").get(userId);
|
|
270
|
+
return isLicenseAdminEmail(row?.email);
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function upsertEnvVar(filePath, varName, value) {
|
|
276
|
+
const prefix = `${varName}=`;
|
|
277
|
+
let lines = [];
|
|
278
|
+
if (existsSync(filePath)) {
|
|
279
|
+
lines = readFileSync(filePath, "utf8").split(/\r?\n/);
|
|
280
|
+
}
|
|
281
|
+
const filtered = lines.filter((line) => !line.trimStart().startsWith(prefix));
|
|
282
|
+
if (value !== null) {
|
|
283
|
+
filtered.push(`${prefix}${value}`);
|
|
284
|
+
}
|
|
285
|
+
while (filtered.length && filtered[filtered.length - 1] === "") {
|
|
286
|
+
filtered.pop();
|
|
287
|
+
}
|
|
288
|
+
writeFileSync(filePath, filtered.join("\n") + "\n", "utf8");
|
|
289
|
+
}
|
|
290
|
+
async function installLicenseKey(key) {
|
|
291
|
+
const trimmed = key.trim();
|
|
292
|
+
if (!trimmed.startsWith("pk_")) {
|
|
293
|
+
throw new Error("Invalid license key format. Must start with pk_.");
|
|
294
|
+
}
|
|
295
|
+
const previousKey = process.env.PROMPTOWL_KEY || "";
|
|
296
|
+
process.env.PROMPTOWL_KEY = trimmed;
|
|
297
|
+
const info = await validateLicense({ forceFresh: true });
|
|
298
|
+
if (!info.valid) {
|
|
299
|
+
process.env.PROMPTOWL_KEY = previousKey;
|
|
300
|
+
if (previousKey) {
|
|
301
|
+
await validateLicense({ forceFresh: true });
|
|
302
|
+
}
|
|
303
|
+
return info;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
upsertEnvVar(config.ENV_FILE_PATH, "PROMPTOWL_KEY", trimmed);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.warn("[license] failed to write .env:", err);
|
|
309
|
+
}
|
|
310
|
+
startLicenseWatcher();
|
|
311
|
+
return info;
|
|
312
|
+
}
|
|
313
|
+
var watcherActive = false;
|
|
314
|
+
var watcherAbort = null;
|
|
315
|
+
var WATCHER_BACKOFF_MIN_MS = 2 * 1e3;
|
|
316
|
+
var WATCHER_BACKOFF_MAX_MS = 60 * 1e3;
|
|
317
|
+
function startLicenseWatcher() {
|
|
318
|
+
if (watcherActive) {
|
|
319
|
+
watcherAbort?.abort();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
watcherActive = true;
|
|
323
|
+
void runLicenseWatcher();
|
|
324
|
+
}
|
|
325
|
+
async function runLicenseWatcher() {
|
|
326
|
+
let backoff = WATCHER_BACKOFF_MIN_MS;
|
|
327
|
+
while (watcherActive) {
|
|
328
|
+
const key = config.PROMPTOWL_KEY;
|
|
329
|
+
if (!key) {
|
|
330
|
+
await sleep(WATCHER_BACKOFF_MAX_MS);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
watcherAbort = new AbortController();
|
|
335
|
+
const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
|
|
336
|
+
const res = await fetch(`${promptowlUrl}/api/license/listen`, {
|
|
337
|
+
method: "POST",
|
|
338
|
+
headers: { "Content-Type": "application/json" },
|
|
339
|
+
body: JSON.stringify({
|
|
340
|
+
key,
|
|
341
|
+
since_updated_at: currentLicense ? (/* @__PURE__ */ new Date()).toISOString() : void 0
|
|
342
|
+
}),
|
|
343
|
+
signal: watcherAbort.signal
|
|
344
|
+
});
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
throw new Error(`listen returned ${res.status}`);
|
|
347
|
+
}
|
|
348
|
+
const data = await res.json();
|
|
349
|
+
backoff = WATCHER_BACKOFF_MIN_MS;
|
|
350
|
+
if (data.event && data.event !== "no_change") {
|
|
351
|
+
console.log(
|
|
352
|
+
`[license] event from PromptOwl: ${data.event} \u2014 revalidating`
|
|
353
|
+
);
|
|
354
|
+
const wasValid = !!currentLicense?.valid;
|
|
355
|
+
await validateLicense({ forceFresh: true });
|
|
356
|
+
const isValid = !!currentLicense?.valid;
|
|
357
|
+
if (wasValid && !isValid) {
|
|
358
|
+
handleLicenseRevoked();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
if (err.name === "AbortError") {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
console.warn(
|
|
366
|
+
`[license] watcher error: ${err.message}; backing off ${backoff}ms`
|
|
367
|
+
);
|
|
368
|
+
await sleep(backoff);
|
|
369
|
+
backoff = Math.min(backoff * 2, WATCHER_BACKOFF_MAX_MS);
|
|
370
|
+
} finally {
|
|
371
|
+
watcherAbort = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function sleep(ms) {
|
|
376
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
377
|
+
}
|
|
378
|
+
function handleLicenseRevoked() {
|
|
379
|
+
try {
|
|
380
|
+
const db = getDb();
|
|
381
|
+
const result = db.prepare("DELETE FROM sessions").run();
|
|
382
|
+
console.warn(
|
|
383
|
+
`[license] revoked \u2014 wiped ${result.changes} active session(s).`
|
|
384
|
+
);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
console.warn("[license] failed to wipe sessions:", err);
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
upsertEnvVar(config.ENV_FILE_PATH, "PROMPTOWL_KEY", null);
|
|
390
|
+
console.warn(
|
|
391
|
+
`[license] revoked \u2014 removed PROMPTOWL_KEY from ${config.ENV_FILE_PATH}`
|
|
392
|
+
);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
console.warn("[license] failed to strip key from .env:", err);
|
|
395
|
+
}
|
|
396
|
+
process.env.PROMPTOWL_KEY = "";
|
|
397
|
+
currentLicense = {
|
|
398
|
+
valid: false,
|
|
399
|
+
tier: "none",
|
|
400
|
+
org: null,
|
|
401
|
+
limits: null,
|
|
402
|
+
suspended: false,
|
|
403
|
+
suspendedReason: null,
|
|
404
|
+
ownerEmail: null
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
408
|
+
var suspensionFirstSeen = null;
|
|
409
|
+
var suspensionConfirmed = false;
|
|
410
|
+
var suspensionReason = null;
|
|
411
|
+
var SUSPENSION_CONFIRM_WINDOW_MS = 60 * 60 * 1e3;
|
|
412
|
+
function isSuspended() {
|
|
413
|
+
return suspensionConfirmed;
|
|
414
|
+
}
|
|
415
|
+
function getSuspensionReason() {
|
|
416
|
+
return suspensionReason;
|
|
417
|
+
}
|
|
418
|
+
async function validateLicense(opts = {}) {
|
|
419
|
+
const info = await _validateLicenseImpl(!!opts.forceFresh);
|
|
420
|
+
currentLicense = info;
|
|
421
|
+
return info;
|
|
422
|
+
}
|
|
423
|
+
async function _validateLicenseImpl(forceFresh) {
|
|
424
|
+
const key = config.PROMPTOWL_KEY;
|
|
425
|
+
if (!key) {
|
|
426
|
+
return {
|
|
427
|
+
valid: false,
|
|
428
|
+
tier: "none",
|
|
429
|
+
org: null,
|
|
430
|
+
limits: null,
|
|
431
|
+
suspended: false,
|
|
432
|
+
suspendedReason: null,
|
|
433
|
+
ownerEmail: null
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
const db = getDb();
|
|
437
|
+
const cached = db.prepare("SELECT * FROM license_cache WHERE key = ?").get(key);
|
|
438
|
+
if (cached && !forceFresh) {
|
|
439
|
+
const age = Date.now() - (/* @__PURE__ */ new Date(cached.validated_at + "Z")).getTime();
|
|
440
|
+
if (age < CACHE_TTL_MS) {
|
|
441
|
+
return {
|
|
442
|
+
valid: true,
|
|
443
|
+
tier: cached.tier,
|
|
444
|
+
org: cached.org,
|
|
445
|
+
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
446
|
+
suspended: suspensionConfirmed,
|
|
447
|
+
suspendedReason: suspensionReason,
|
|
448
|
+
ownerEmail: cached.owner_email || null
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
|
|
454
|
+
const res = await fetch(`${promptowlUrl}/api/license/validate`, {
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers: { "Content-Type": "application/json" },
|
|
457
|
+
body: JSON.stringify({ key })
|
|
458
|
+
});
|
|
459
|
+
if (!res.ok) {
|
|
460
|
+
if (cached) {
|
|
461
|
+
console.warn(
|
|
462
|
+
" PromptOwl unreachable, using cached license (grace period)"
|
|
463
|
+
);
|
|
464
|
+
return {
|
|
465
|
+
valid: true,
|
|
466
|
+
tier: cached.tier,
|
|
467
|
+
org: cached.org,
|
|
468
|
+
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
469
|
+
suspended: suspensionConfirmed,
|
|
470
|
+
suspendedReason: suspensionReason,
|
|
471
|
+
ownerEmail: cached.owner_email || null
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
valid: false,
|
|
476
|
+
tier: "none",
|
|
477
|
+
org: null,
|
|
478
|
+
limits: null,
|
|
479
|
+
suspended: false,
|
|
480
|
+
suspendedReason: null,
|
|
481
|
+
ownerEmail: null
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const data = await res.json();
|
|
485
|
+
if (data.suspended === true) {
|
|
486
|
+
if (!suspensionFirstSeen) {
|
|
487
|
+
suspensionFirstSeen = Date.now();
|
|
488
|
+
suspensionReason = data.suspended_reason || "Suspended by PromptOwl";
|
|
489
|
+
console.warn(
|
|
490
|
+
`
|
|
491
|
+
WARNING: PromptOwl has flagged this server for suspension.`
|
|
492
|
+
);
|
|
493
|
+
console.warn(
|
|
494
|
+
` Reason: ${suspensionReason}`
|
|
495
|
+
);
|
|
496
|
+
console.warn(
|
|
497
|
+
` This will be confirmed in ~1 hour. If this is an error,`
|
|
498
|
+
);
|
|
499
|
+
console.warn(
|
|
500
|
+
` contact support@promptowl.ai to reverse it.
|
|
501
|
+
`
|
|
502
|
+
);
|
|
503
|
+
} else if (Date.now() - suspensionFirstSeen >= SUSPENSION_CONFIRM_WINDOW_MS) {
|
|
504
|
+
suspensionConfirmed = true;
|
|
505
|
+
suspensionReason = data.suspended_reason || "Suspended by PromptOwl";
|
|
506
|
+
console.error(
|
|
507
|
+
`
|
|
508
|
+
SERVER SUSPENDED: ${suspensionReason}`
|
|
509
|
+
);
|
|
510
|
+
console.error(
|
|
511
|
+
` Write operations are disabled. Reads still work.`
|
|
512
|
+
);
|
|
513
|
+
console.error(
|
|
514
|
+
` Contact support@promptowl.ai to resolve.
|
|
515
|
+
`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
if (suspensionFirstSeen) {
|
|
520
|
+
console.log(" Suspension flag cleared by PromptOwl.");
|
|
521
|
+
}
|
|
522
|
+
suspensionFirstSeen = null;
|
|
523
|
+
suspensionConfirmed = false;
|
|
524
|
+
suspensionReason = null;
|
|
525
|
+
}
|
|
526
|
+
if (!data.valid && !data.suspended) {
|
|
527
|
+
db.prepare("DELETE FROM license_cache WHERE key = ?").run(key);
|
|
528
|
+
return {
|
|
529
|
+
valid: false,
|
|
530
|
+
tier: "none",
|
|
531
|
+
org: null,
|
|
532
|
+
limits: null,
|
|
533
|
+
suspended: false,
|
|
534
|
+
suspendedReason: null,
|
|
535
|
+
ownerEmail: data.owner_email || null
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (data.valid) {
|
|
539
|
+
const limitsJson = data.limits ? JSON.stringify(data.limits) : null;
|
|
540
|
+
db.prepare(
|
|
541
|
+
`INSERT OR REPLACE INTO license_cache (key, tier, org, limits_json, owner_email, validated_at)
|
|
542
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`
|
|
543
|
+
).run(
|
|
544
|
+
key,
|
|
545
|
+
data.tier || "community",
|
|
546
|
+
data.org || null,
|
|
547
|
+
limitsJson,
|
|
548
|
+
data.owner_email || null
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
valid: data.valid !== false,
|
|
553
|
+
tier: data.tier || "community",
|
|
554
|
+
org: data.org || null,
|
|
555
|
+
limits: data.limits || null,
|
|
556
|
+
suspended: suspensionConfirmed,
|
|
557
|
+
suspendedReason: suspensionReason,
|
|
558
|
+
ownerEmail: data.owner_email || null
|
|
559
|
+
};
|
|
560
|
+
} catch (err) {
|
|
561
|
+
if (cached) {
|
|
562
|
+
console.warn(
|
|
563
|
+
` PromptOwl validation failed (${err.message}), using cached license`
|
|
564
|
+
);
|
|
565
|
+
return {
|
|
566
|
+
valid: true,
|
|
567
|
+
tier: cached.tier,
|
|
568
|
+
org: cached.org,
|
|
569
|
+
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
570
|
+
suspended: suspensionConfirmed,
|
|
571
|
+
suspendedReason: suspensionReason,
|
|
572
|
+
ownerEmail: cached.owner_email || null
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
valid: false,
|
|
577
|
+
tier: "none",
|
|
578
|
+
org: null,
|
|
579
|
+
limits: null,
|
|
580
|
+
suspended: false,
|
|
581
|
+
suspendedReason: null,
|
|
582
|
+
ownerEmail: null
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
157
587
|
// src/shared/rate-limit.ts
|
|
158
588
|
var buckets = /* @__PURE__ */ new Map();
|
|
159
589
|
function tryConsume(key, cfg) {
|
|
@@ -185,6 +615,32 @@ function clientIp(c) {
|
|
|
185
615
|
if (realIp) return realIp.trim();
|
|
186
616
|
return "unknown";
|
|
187
617
|
}
|
|
618
|
+
function resolveCallerUserId(c) {
|
|
619
|
+
const sessionId = getSessionIdFromRequest(c);
|
|
620
|
+
if (sessionId) {
|
|
621
|
+
const uid = resolveSession(sessionId);
|
|
622
|
+
if (uid) return uid;
|
|
623
|
+
}
|
|
624
|
+
const key = parseBearerToken(c.req.header("Authorization"));
|
|
625
|
+
if (key) {
|
|
626
|
+
const db = getDb();
|
|
627
|
+
const row = db.prepare("SELECT user_id FROM api_keys WHERE key_hash = ?").get(hashApiKey(key));
|
|
628
|
+
if (row) return row.user_id;
|
|
629
|
+
}
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
function setSessionCookie(c, sessionId) {
|
|
633
|
+
c.header(
|
|
634
|
+
"Set-Cookie",
|
|
635
|
+
buildSessionCookie(sessionId, { secure: isSecureRequest(c) })
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
function clearSessionCookie(c) {
|
|
639
|
+
c.header(
|
|
640
|
+
"Set-Cookie",
|
|
641
|
+
buildClearSessionCookie({ secure: isSecureRequest(c) })
|
|
642
|
+
);
|
|
643
|
+
}
|
|
188
644
|
var authRoutes = new Hono();
|
|
189
645
|
authRoutes.post("/register", async (c) => {
|
|
190
646
|
const body = await c.req.json();
|
|
@@ -196,25 +652,34 @@ authRoutes.post("/register", async (c) => {
|
|
|
196
652
|
return c.json({ error: "Too many registration attempts, try again later" }, 429);
|
|
197
653
|
}
|
|
198
654
|
const db = getDb();
|
|
199
|
-
const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(body.email);
|
|
200
|
-
|
|
655
|
+
const existing = db.prepare("SELECT id, is_invited FROM users WHERE email = ?").get(body.email);
|
|
656
|
+
let userId;
|
|
657
|
+
const passwordHash = await hashPassword(body.password);
|
|
658
|
+
if (existing && existing.is_invited === 1) {
|
|
659
|
+
userId = existing.id;
|
|
660
|
+
db.prepare(
|
|
661
|
+
"UPDATE users SET password_hash = ?, name = COALESCE(?, name), is_invited = 0 WHERE id = ?"
|
|
662
|
+
).run(passwordHash, body.name || null, userId);
|
|
663
|
+
trackEvent("user.register", { userId, email: body.email, claimed: true });
|
|
664
|
+
} else if (existing) {
|
|
201
665
|
throw new ValidationError("Email already registered");
|
|
666
|
+
} else {
|
|
667
|
+
userId = uuid();
|
|
668
|
+
db.prepare(
|
|
669
|
+
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
|
|
670
|
+
).run(userId, body.email, body.name || null, passwordHash);
|
|
671
|
+
trackEvent("user.register", { userId, email: body.email });
|
|
202
672
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
db.prepare(
|
|
206
|
-
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
|
|
207
|
-
).run(userId, body.email, body.name || null, passwordHash);
|
|
208
|
-
const apiKey = generateApiKey();
|
|
209
|
-
const keyId = uuid();
|
|
210
|
-
db.prepare(
|
|
211
|
-
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, label) VALUES (?, ?, ?, ?, ?)"
|
|
212
|
-
).run(keyId, userId, hashApiKey(apiKey), getKeyPrefix(apiKey), "default");
|
|
213
|
-
trackEvent("user.register", { userId, email: body.email });
|
|
673
|
+
const sessionId = createSession(userId, c.req.header("User-Agent"));
|
|
674
|
+
setSessionCookie(c, sessionId);
|
|
214
675
|
return c.json(
|
|
215
676
|
{
|
|
216
|
-
user: {
|
|
217
|
-
|
|
677
|
+
user: {
|
|
678
|
+
id: userId,
|
|
679
|
+
email: body.email,
|
|
680
|
+
name: body.name || null,
|
|
681
|
+
is_admin: isLicenseAdminEmail(body.email)
|
|
682
|
+
}
|
|
218
683
|
},
|
|
219
684
|
201
|
|
220
685
|
);
|
|
@@ -231,7 +696,7 @@ authRoutes.post("/login", async (c) => {
|
|
|
231
696
|
}
|
|
232
697
|
const db = getDb();
|
|
233
698
|
const user = db.prepare(
|
|
234
|
-
"SELECT id, email, name, password_hash FROM users WHERE email = ?"
|
|
699
|
+
"SELECT id, email, name, password_hash, is_admin FROM users WHERE email = ?"
|
|
235
700
|
).get(body.email);
|
|
236
701
|
const check = user ? await verifyPassword(body.password, user.password_hash) : { ok: false, needsRehash: false };
|
|
237
702
|
if (!user || !check.ok) {
|
|
@@ -247,30 +712,47 @@ authRoutes.post("/login", async (c) => {
|
|
|
247
712
|
} catch {
|
|
248
713
|
}
|
|
249
714
|
}
|
|
250
|
-
const apiKey = generateApiKey();
|
|
251
|
-
const keyId = uuid();
|
|
252
|
-
db.prepare(
|
|
253
|
-
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, label) VALUES (?, ?, ?, ?, ?)"
|
|
254
|
-
).run(
|
|
255
|
-
keyId,
|
|
256
|
-
user.id,
|
|
257
|
-
hashApiKey(apiKey),
|
|
258
|
-
getKeyPrefix(apiKey),
|
|
259
|
-
body.label || "login"
|
|
260
|
-
);
|
|
261
715
|
trackEvent("user.login", { userId: user.id });
|
|
262
|
-
|
|
716
|
+
const sessionId = createSession(user.id, c.req.header("User-Agent"));
|
|
717
|
+
setSessionCookie(c, sessionId);
|
|
718
|
+
return c.json({
|
|
719
|
+
user: {
|
|
720
|
+
id: user.id,
|
|
721
|
+
email: user.email,
|
|
722
|
+
name: user.name,
|
|
723
|
+
is_admin: isLicenseAdminEmail(user.email)
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
authRoutes.post("/logout", async (c) => {
|
|
728
|
+
const sessionId = getSessionIdFromRequest(c);
|
|
729
|
+
if (sessionId) deleteSession(sessionId);
|
|
730
|
+
clearSessionCookie(c);
|
|
731
|
+
return c.json({ ok: true });
|
|
263
732
|
});
|
|
264
733
|
authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
265
|
-
const body = await c.req.json()
|
|
734
|
+
const body = await c.req.json().catch(
|
|
735
|
+
() => ({})
|
|
736
|
+
);
|
|
266
737
|
const db = getDb();
|
|
738
|
+
const userId = c.get("userId");
|
|
739
|
+
const existing = db.prepare("SELECT key_prefix FROM api_keys WHERE user_id = ?").get(userId);
|
|
740
|
+
if (existing) {
|
|
741
|
+
return c.json(
|
|
742
|
+
{
|
|
743
|
+
error: "An API key already exists for this user. Rotate it instead.",
|
|
744
|
+
key_prefix: existing.key_prefix
|
|
745
|
+
},
|
|
746
|
+
409
|
|
747
|
+
);
|
|
748
|
+
}
|
|
267
749
|
const apiKey = generateApiKey();
|
|
268
750
|
const keyId = uuid();
|
|
269
751
|
db.prepare(
|
|
270
752
|
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)"
|
|
271
753
|
).run(
|
|
272
754
|
keyId,
|
|
273
|
-
|
|
755
|
+
userId,
|
|
274
756
|
hashApiKey(apiKey),
|
|
275
757
|
getKeyPrefix(apiKey),
|
|
276
758
|
body.nest_id || null,
|
|
@@ -281,6 +763,30 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
281
763
|
201
|
|
282
764
|
);
|
|
283
765
|
});
|
|
766
|
+
authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
767
|
+
const body = await c.req.json().catch(
|
|
768
|
+
() => ({})
|
|
769
|
+
);
|
|
770
|
+
const db = getDb();
|
|
771
|
+
const userId = c.get("userId");
|
|
772
|
+
const apiKey = generateApiKey();
|
|
773
|
+
const keyId = uuid();
|
|
774
|
+
const prior = db.prepare("SELECT label, nest_id FROM api_keys WHERE user_id = ?").get(userId);
|
|
775
|
+
db.transaction(() => {
|
|
776
|
+
db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(userId);
|
|
777
|
+
db.prepare(
|
|
778
|
+
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)"
|
|
779
|
+
).run(
|
|
780
|
+
keyId,
|
|
781
|
+
userId,
|
|
782
|
+
hashApiKey(apiKey),
|
|
783
|
+
getKeyPrefix(apiKey),
|
|
784
|
+
body.nest_id ?? prior?.nest_id ?? null,
|
|
785
|
+
body.label ?? prior?.label ?? null
|
|
786
|
+
);
|
|
787
|
+
})();
|
|
788
|
+
return c.json({ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) });
|
|
789
|
+
});
|
|
284
790
|
authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
285
791
|
const db = getDb();
|
|
286
792
|
const keys = db.prepare(
|
|
@@ -370,97 +876,127 @@ authRoutes.post("/promptowl", async (c) => {
|
|
|
370
876
|
} else {
|
|
371
877
|
trackEvent("user.login", { userId: user.id, method: "promptowl" });
|
|
372
878
|
}
|
|
879
|
+
let claimBlocked = null;
|
|
880
|
+
const lic = getCurrentLicense();
|
|
373
881
|
let isAdmin = false;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
WHERE id = ?
|
|
378
|
-
AND NOT EXISTS (SELECT 1 FROM users WHERE is_admin = 1)`
|
|
379
|
-
).run(userId);
|
|
380
|
-
return result.changes > 0;
|
|
381
|
-
});
|
|
382
|
-
if (claim(user.id)) {
|
|
882
|
+
if (!lic?.valid) {
|
|
883
|
+
claimBlocked = { reason: "license_required" };
|
|
884
|
+
} else if (lic.ownerEmail && lic.ownerEmail.toLowerCase() === me.email.toLowerCase()) {
|
|
383
885
|
isAdmin = true;
|
|
384
886
|
trackEvent("admin.claim", { userId: user.id, email: user.email });
|
|
385
887
|
} else {
|
|
386
|
-
|
|
387
|
-
|
|
888
|
+
claimBlocked = {
|
|
889
|
+
reason: "email_mismatch",
|
|
890
|
+
license_owner_email: lic.ownerEmail
|
|
891
|
+
};
|
|
388
892
|
}
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
db.prepare(
|
|
392
|
-
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, label) VALUES (?, ?, ?, ?, ?)"
|
|
393
|
-
).run(
|
|
394
|
-
keyId,
|
|
395
|
-
user.id,
|
|
396
|
-
hashApiKey(apiKey),
|
|
397
|
-
getKeyPrefix(apiKey),
|
|
398
|
-
"promptowl"
|
|
399
|
-
);
|
|
893
|
+
const sessionId = createSession(user.id, c.req.header("User-Agent"));
|
|
894
|
+
setSessionCookie(c, sessionId);
|
|
400
895
|
return c.json({
|
|
401
|
-
api_key: apiKey,
|
|
402
896
|
user: {
|
|
403
897
|
id: user.id,
|
|
404
898
|
email: user.email,
|
|
405
899
|
name: user.name,
|
|
406
900
|
is_admin: isAdmin
|
|
407
|
-
}
|
|
901
|
+
},
|
|
902
|
+
...claimBlocked ? { claim_blocked: claimBlocked } : {}
|
|
408
903
|
});
|
|
409
904
|
});
|
|
410
905
|
authRoutes.get("/admin-status", async (c) => {
|
|
411
906
|
const db = getDb();
|
|
412
|
-
const
|
|
413
|
-
const
|
|
907
|
+
const lic = getCurrentLicense();
|
|
908
|
+
const ownerEmail = lic?.valid ? lic.ownerEmail : null;
|
|
909
|
+
let admin = null;
|
|
910
|
+
if (ownerEmail) {
|
|
911
|
+
const ownerRow = db.prepare("SELECT name FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1").get(ownerEmail);
|
|
912
|
+
admin = { email: ownerEmail, name: ownerRow?.name ?? null };
|
|
913
|
+
}
|
|
914
|
+
const callerId = resolveCallerUserId(c);
|
|
414
915
|
let me = null;
|
|
415
|
-
if (
|
|
416
|
-
const
|
|
417
|
-
const row = db.prepare(
|
|
418
|
-
`SELECT u.email, u.is_admin FROM api_keys k
|
|
419
|
-
JOIN users u ON u.id = k.user_id
|
|
420
|
-
WHERE k.key_hash = ?`
|
|
421
|
-
).get(keyHash);
|
|
916
|
+
if (callerId) {
|
|
917
|
+
const row = db.prepare("SELECT email, name FROM users WHERE id = ?").get(callerId);
|
|
422
918
|
if (row) {
|
|
423
|
-
me = {
|
|
919
|
+
me = {
|
|
920
|
+
email: row.email,
|
|
921
|
+
name: row.name,
|
|
922
|
+
is_admin: isLicenseAdminEmail(row.email)
|
|
923
|
+
};
|
|
424
924
|
}
|
|
425
925
|
}
|
|
426
926
|
return c.json({
|
|
427
927
|
claimed: !!admin,
|
|
428
|
-
admin
|
|
928
|
+
admin,
|
|
429
929
|
me
|
|
430
930
|
});
|
|
431
931
|
});
|
|
932
|
+
authRoutes.post("/password", authMiddleware, async (c) => {
|
|
933
|
+
const body = await c.req.json();
|
|
934
|
+
if (!body.current || !body.next) {
|
|
935
|
+
throw new ValidationError("current and next are required");
|
|
936
|
+
}
|
|
937
|
+
if (body.next.length < 8) {
|
|
938
|
+
throw new ValidationError("new password must be at least 8 characters");
|
|
939
|
+
}
|
|
940
|
+
const db = getDb();
|
|
941
|
+
const userId = c.get("userId");
|
|
942
|
+
const user = db.prepare("SELECT password_hash FROM users WHERE id = ?").get(userId);
|
|
943
|
+
if (!user) return c.json({ error: "User not found" }, 404);
|
|
944
|
+
const check = await verifyPassword(body.current, user.password_hash);
|
|
945
|
+
if (!check.ok) return c.json({ error: "Invalid current password" }, 401);
|
|
946
|
+
const newHash = await hashPassword(body.next);
|
|
947
|
+
db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
|
|
948
|
+
newHash,
|
|
949
|
+
userId
|
|
950
|
+
);
|
|
951
|
+
deleteAllSessionsForUser(userId);
|
|
952
|
+
clearSessionCookie(c);
|
|
953
|
+
return c.json({ ok: true });
|
|
954
|
+
});
|
|
432
955
|
authRoutes.post("/invite", async (c) => {
|
|
433
956
|
const body = await c.req.json();
|
|
434
957
|
if (!body.email) throw new ValidationError("email is required");
|
|
435
|
-
const
|
|
436
|
-
if (!
|
|
437
|
-
return c.json(
|
|
958
|
+
const callerId = resolveCallerUserId(c);
|
|
959
|
+
if (!callerId) {
|
|
960
|
+
return c.json(
|
|
961
|
+
{
|
|
962
|
+
error: "Authentication required. Sign in with the admin account that owns the installed PromptOwl license to invite teammates."
|
|
963
|
+
},
|
|
964
|
+
401
|
|
965
|
+
);
|
|
438
966
|
}
|
|
439
967
|
const db = getDb();
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return c.json({ error: "Admin only" }, 403);
|
|
968
|
+
if (!isLicenseAdminUserId(callerId)) {
|
|
969
|
+
return c.json(
|
|
970
|
+
{
|
|
971
|
+
error: "Only the license-admin user can invite teammates. Contact the admin who installed the PromptOwl license on this server to issue invitations."
|
|
972
|
+
},
|
|
973
|
+
403
|
|
974
|
+
);
|
|
448
975
|
}
|
|
449
976
|
let user = db.prepare("SELECT id, email FROM users WHERE email = ?").get(body.email);
|
|
450
977
|
if (!user) {
|
|
451
978
|
const userId = uuid();
|
|
452
979
|
const placeholderHash = await hashPassword(uuid());
|
|
453
980
|
db.prepare(
|
|
454
|
-
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?,
|
|
981
|
+
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
|
|
455
982
|
).run(userId, body.email, null, placeholderHash);
|
|
456
983
|
user = { id: userId, email: body.email };
|
|
457
984
|
}
|
|
458
985
|
const apiKey = generateApiKey();
|
|
459
986
|
const keyId = uuid();
|
|
460
|
-
db.
|
|
461
|
-
"
|
|
462
|
-
|
|
463
|
-
|
|
987
|
+
db.transaction(() => {
|
|
988
|
+
db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(user.id);
|
|
989
|
+
db.prepare(
|
|
990
|
+
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, label) VALUES (?, ?, ?, ?, ?)"
|
|
991
|
+
).run(
|
|
992
|
+
keyId,
|
|
993
|
+
user.id,
|
|
994
|
+
hashApiKey(apiKey),
|
|
995
|
+
getKeyPrefix(apiKey),
|
|
996
|
+
body.label || "teammate"
|
|
997
|
+
);
|
|
998
|
+
})();
|
|
999
|
+
trackEvent("admin.invite", { adminId: callerId, email: body.email });
|
|
464
1000
|
return c.json(
|
|
465
1001
|
{
|
|
466
1002
|
api_key: apiKey,
|
|
@@ -471,27 +1007,31 @@ authRoutes.post("/invite", async (c) => {
|
|
|
471
1007
|
);
|
|
472
1008
|
});
|
|
473
1009
|
authRoutes.get("/teammates", async (c) => {
|
|
474
|
-
const
|
|
475
|
-
if (!
|
|
476
|
-
return c.json(
|
|
1010
|
+
const callerId = resolveCallerUserId(c);
|
|
1011
|
+
if (!callerId) {
|
|
1012
|
+
return c.json(
|
|
1013
|
+
{
|
|
1014
|
+
error: "Authentication required. Sign in with the admin account that owns the installed PromptOwl license to view teammates."
|
|
1015
|
+
},
|
|
1016
|
+
401
|
|
1017
|
+
);
|
|
477
1018
|
}
|
|
478
1019
|
const db = getDb();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return c.json({ error: "Admin only" }, 403);
|
|
1020
|
+
if (!isLicenseAdminUserId(callerId)) {
|
|
1021
|
+
return c.json(
|
|
1022
|
+
{
|
|
1023
|
+
error: "Only the license-admin user can view the teammates list. Contact the admin who installed the PromptOwl license on this server."
|
|
1024
|
+
},
|
|
1025
|
+
403
|
|
1026
|
+
);
|
|
487
1027
|
}
|
|
488
1028
|
const teammates = db.prepare(
|
|
489
|
-
`SELECT u.id, u.email, u.name, u.
|
|
1029
|
+
`SELECT u.id, u.email, u.name, u.is_invited,
|
|
490
1030
|
(SELECT COUNT(*) FROM api_keys WHERE user_id = u.id) as key_count,
|
|
491
1031
|
(SELECT MAX(last_used_at) FROM api_keys WHERE user_id = u.id) as last_active
|
|
492
1032
|
FROM users u
|
|
493
1033
|
WHERE u.id != '00000000-0000-0000-0000-000000000000'
|
|
494
|
-
ORDER BY u.
|
|
1034
|
+
ORDER BY u.created_at DESC`
|
|
495
1035
|
).all();
|
|
496
1036
|
const pendingStewards = db.prepare(
|
|
497
1037
|
`SELECT DISTINCT s.user_email AS email
|
|
@@ -504,11 +1044,9 @@ authRoutes.get("/teammates", async (c) => {
|
|
|
504
1044
|
)
|
|
505
1045
|
ORDER BY s.user_email`
|
|
506
1046
|
).all();
|
|
1047
|
+
const enriched = teammates.map((t) => ({ ...t, is_admin: isLicenseAdminEmail(t.email) })).sort((a, b) => Number(b.is_admin) - Number(a.is_admin));
|
|
507
1048
|
return c.json({
|
|
508
|
-
teammates:
|
|
509
|
-
...t,
|
|
510
|
-
is_admin: !!t.is_admin
|
|
511
|
-
})),
|
|
1049
|
+
teammates: enriched,
|
|
512
1050
|
pending_stewards: pendingStewards.map((p) => p.email)
|
|
513
1051
|
});
|
|
514
1052
|
});
|
|
@@ -668,12 +1206,15 @@ nestRoutes.get("/:nestId/settings", async (c) => {
|
|
|
668
1206
|
nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
669
1207
|
const nestId = c.req.param("nestId");
|
|
670
1208
|
const userId = c.get("userId");
|
|
671
|
-
const
|
|
672
|
-
const userRow = db.prepare("SELECT is_admin FROM users WHERE id = ?").get(userId);
|
|
673
|
-
const isServerAdmin = !!userRow?.is_admin;
|
|
1209
|
+
const isServerAdmin = isLicenseAdminUserId(userId);
|
|
674
1210
|
const permission = effectivePermission(nestId, userId);
|
|
675
1211
|
if (!isServerAdmin && permission !== "owner") {
|
|
676
|
-
return c.json(
|
|
1212
|
+
return c.json(
|
|
1213
|
+
{
|
|
1214
|
+
error: "Only the nest owner or the server license-admin can update nest settings."
|
|
1215
|
+
},
|
|
1216
|
+
403
|
|
1217
|
+
);
|
|
677
1218
|
}
|
|
678
1219
|
const body = await c.req.json();
|
|
679
1220
|
if (typeof body.stewardship_enabled === "boolean") {
|
|
@@ -711,7 +1252,7 @@ sharingRoutes.post("/collaborators", async (c) => {
|
|
|
711
1252
|
const { hashPassword: hashPassword2 } = await import("./keys-YV33AJK3.js");
|
|
712
1253
|
userId = uuid3();
|
|
713
1254
|
db.prepare(
|
|
714
|
-
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?,
|
|
1255
|
+
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
|
|
715
1256
|
).run(userId, body.email, null, await hashPassword2(uuid3()));
|
|
716
1257
|
} else {
|
|
717
1258
|
userId = user.id;
|
|
@@ -771,7 +1312,11 @@ import { serializeDocument } from "@promptowl/contextnest-engine";
|
|
|
771
1312
|
|
|
772
1313
|
// src/nodes/engine.ts
|
|
773
1314
|
import { join as join2 } from "path";
|
|
774
|
-
import {
|
|
1315
|
+
import {
|
|
1316
|
+
NestStorage as NestStorage2,
|
|
1317
|
+
GraphQueryEngine,
|
|
1318
|
+
VersionManager
|
|
1319
|
+
} from "@promptowl/contextnest-engine";
|
|
775
1320
|
var NestEngineCache = class {
|
|
776
1321
|
cache = /* @__PURE__ */ new Map();
|
|
777
1322
|
get(nestId) {
|
|
@@ -780,7 +1325,8 @@ var NestEngineCache = class {
|
|
|
780
1325
|
const nestPath2 = join2(config.DATA_ROOT, "nests", nestId);
|
|
781
1326
|
const storage = new NestStorage2(nestPath2);
|
|
782
1327
|
const query = new GraphQueryEngine(storage);
|
|
783
|
-
|
|
1328
|
+
const versions = new VersionManager(storage);
|
|
1329
|
+
engine = { storage, query, versions };
|
|
784
1330
|
this.cache.set(nestId, engine);
|
|
785
1331
|
}
|
|
786
1332
|
return engine;
|
|
@@ -845,6 +1391,9 @@ function toNodeResponse(node) {
|
|
|
845
1391
|
title: node.frontmatter.title,
|
|
846
1392
|
type: node.frontmatter.type || "document",
|
|
847
1393
|
tags: node.frontmatter.tags || [],
|
|
1394
|
+
// Widen to string so callers can layer review-workflow states like
|
|
1395
|
+
// "pending_review" / "rejected" on top of the on-disk frontmatter
|
|
1396
|
+
// status. The engine's Status enum only knows draft/approved.
|
|
848
1397
|
status: node.frontmatter.status || "draft",
|
|
849
1398
|
version: node.frontmatter.version || 1,
|
|
850
1399
|
author: node.frontmatter.author,
|
|
@@ -862,7 +1411,11 @@ nodeRoutes.get("/", async (c) => {
|
|
|
862
1411
|
const accessible = filterAccessible(nestId, userEmail, documents);
|
|
863
1412
|
return c.json({
|
|
864
1413
|
count: accessible.length,
|
|
865
|
-
nodes: accessible.map(
|
|
1414
|
+
nodes: accessible.map((doc) => {
|
|
1415
|
+
const r = toNodeResponse(doc);
|
|
1416
|
+
r.status = getDisplayStatus(nestId, r.id);
|
|
1417
|
+
return r;
|
|
1418
|
+
})
|
|
866
1419
|
});
|
|
867
1420
|
});
|
|
868
1421
|
nodeRoutes.post("/", async (c) => {
|
|
@@ -871,7 +1424,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
871
1424
|
throw new ValidationError("title and content are required");
|
|
872
1425
|
}
|
|
873
1426
|
const nestId = c.req.param("nestId");
|
|
874
|
-
const { storage } = engineCache.get(nestId);
|
|
1427
|
+
const { storage, versions: versionManager } = engineCache.get(nestId);
|
|
875
1428
|
const slug = body.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
876
1429
|
const id = `nodes/${slug}`;
|
|
877
1430
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -901,6 +1454,11 @@ nodeRoutes.post("/", async (c) => {
|
|
|
901
1454
|
await storage.writeDocument(id, serialized);
|
|
902
1455
|
syncNodeTags(nestId, id, tags);
|
|
903
1456
|
const authorEmail = getUserEmail(c);
|
|
1457
|
+
try {
|
|
1458
|
+
await versionManager.createVersion(node, authorEmail);
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
console.error("VersionManager.createVersion failed (node create)", err);
|
|
1461
|
+
}
|
|
904
1462
|
createVersion({
|
|
905
1463
|
nestId,
|
|
906
1464
|
nodeId: id,
|
|
@@ -927,7 +1485,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
927
1485
|
nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
928
1486
|
const nestId = c.req.param("nestId");
|
|
929
1487
|
const nodeId = c.req.param("nodeId");
|
|
930
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
1488
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-ZJATH6OM.js");
|
|
931
1489
|
const { stewards, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback2(
|
|
932
1490
|
nestId,
|
|
933
1491
|
nodeId
|
|
@@ -949,11 +1507,30 @@ nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
|
949
1507
|
nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
950
1508
|
const nestId = c.req.param("nestId");
|
|
951
1509
|
const nodeId = c.req.param("nodeId");
|
|
952
|
-
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-
|
|
1510
|
+
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-2MZJGE3H.js");
|
|
953
1511
|
const allVersions = getVersions2(nestId, nodeId);
|
|
954
1512
|
const approved = getApprovedVersion2(nestId, nodeId);
|
|
1513
|
+
const db = getDb();
|
|
1514
|
+
const resolutions = db.prepare(
|
|
1515
|
+
`SELECT version, status, resolved_by, resolved_at
|
|
1516
|
+
FROM review_requests
|
|
1517
|
+
WHERE nest_id = ? AND node_id = ?
|
|
1518
|
+
AND status IN ('approved', 'rejected')
|
|
1519
|
+
AND resolved_by IS NOT NULL
|
|
1520
|
+
ORDER BY resolved_at DESC`
|
|
1521
|
+
).all(nestId, nodeId);
|
|
1522
|
+
const byVersion = /* @__PURE__ */ new Map();
|
|
1523
|
+
for (const r of resolutions) {
|
|
1524
|
+
if (!byVersion.has(r.version)) {
|
|
1525
|
+
byVersion.set(r.version, { status: r.status, resolvedBy: r.resolved_by });
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
const enriched = allVersions.map((v) => {
|
|
1529
|
+
const r = byVersion.get(v.version);
|
|
1530
|
+
return r ? { ...v, resolvedBy: r.resolvedBy, resolutionStatus: r.status } : v;
|
|
1531
|
+
});
|
|
955
1532
|
return c.json({
|
|
956
|
-
versions:
|
|
1533
|
+
versions: enriched,
|
|
957
1534
|
approvedVersion: approved,
|
|
958
1535
|
currentVersion: allVersions[0]?.version || 0
|
|
959
1536
|
});
|
|
@@ -961,7 +1538,7 @@ nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
|
961
1538
|
nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
|
|
962
1539
|
const nestId = c.req.param("nestId");
|
|
963
1540
|
const nodeId = c.req.param("nodeId");
|
|
964
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
1541
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-2JHZHZWJ.js");
|
|
965
1542
|
const history = getReviewHistory2(nestId, nodeId);
|
|
966
1543
|
return c.json({ reviews: history });
|
|
967
1544
|
});
|
|
@@ -976,17 +1553,20 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
976
1553
|
403
|
|
977
1554
|
);
|
|
978
1555
|
}
|
|
1556
|
+
let node;
|
|
979
1557
|
try {
|
|
980
|
-
|
|
981
|
-
return c.json({ node: toNodeResponse(node) });
|
|
1558
|
+
node = await storage.readDocument(nodeId);
|
|
982
1559
|
} catch {
|
|
983
1560
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
984
1561
|
}
|
|
1562
|
+
const response = toNodeResponse(node);
|
|
1563
|
+
response.status = getDisplayStatus(nestId, nodeId);
|
|
1564
|
+
return c.json({ node: response });
|
|
985
1565
|
});
|
|
986
1566
|
nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
987
1567
|
const nestId = c.req.param("nestId");
|
|
988
1568
|
const nodeId = c.req.param("nodeId");
|
|
989
|
-
const { storage } = engineCache.get(nestId);
|
|
1569
|
+
const { storage, versions: versionManager } = engineCache.get(nestId);
|
|
990
1570
|
const body = await c.req.json();
|
|
991
1571
|
const baseVersionHeader = c.req.header("X-Base-Version");
|
|
992
1572
|
if (baseVersionHeader) {
|
|
@@ -1055,6 +1635,13 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
1055
1635
|
const hasStewards = isStewardshipEnabled(nestId);
|
|
1056
1636
|
const currentTags = node.frontmatter.tags || [];
|
|
1057
1637
|
syncNodeTags(nestId, nodeId, currentTags);
|
|
1638
|
+
try {
|
|
1639
|
+
await versionManager.createVersion(node, authorEmail, {
|
|
1640
|
+
note: body.changeNote
|
|
1641
|
+
});
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
console.error("VersionManager.createVersion failed (node patch)", err);
|
|
1644
|
+
}
|
|
1058
1645
|
createVersion({
|
|
1059
1646
|
nestId,
|
|
1060
1647
|
nodeId,
|
|
@@ -1065,6 +1652,10 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
1065
1652
|
tags: currentTags,
|
|
1066
1653
|
changeNote: body.changeNote
|
|
1067
1654
|
});
|
|
1655
|
+
const { cancelReview: cancelReview2, getPendingReview: getPendingReview2 } = await import("./review-service-2JHZHZWJ.js");
|
|
1656
|
+
if (getPendingReview2(nestId, nodeId)) {
|
|
1657
|
+
cancelReview2({ nestId, nodeId, cancelledBy: authorEmail });
|
|
1658
|
+
}
|
|
1068
1659
|
if (!hasStewards) {
|
|
1069
1660
|
setApprovedVersion(nestId, nodeId, newVersion, authorEmail);
|
|
1070
1661
|
}
|
|
@@ -1080,6 +1671,18 @@ nodeRoutes.delete("/:nodeId{.+}", async (c) => {
|
|
|
1080
1671
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
1081
1672
|
}
|
|
1082
1673
|
removeNodeFromTagIndex(nestId, nodeId);
|
|
1674
|
+
const db = getDb();
|
|
1675
|
+
db.transaction(() => {
|
|
1676
|
+
db.prepare(
|
|
1677
|
+
"DELETE FROM node_versions WHERE nest_id = ? AND node_id = ?"
|
|
1678
|
+
).run(nestId, nodeId);
|
|
1679
|
+
db.prepare(
|
|
1680
|
+
"DELETE FROM review_requests WHERE nest_id = ? AND node_id = ?"
|
|
1681
|
+
).run(nestId, nodeId);
|
|
1682
|
+
db.prepare(
|
|
1683
|
+
"DELETE FROM approved_versions WHERE nest_id = ? AND node_id = ?"
|
|
1684
|
+
).run(nestId, nodeId);
|
|
1685
|
+
})();
|
|
1083
1686
|
trackEvent("node.delete", { nestId, nodeId });
|
|
1084
1687
|
return c.json({ deleted: true });
|
|
1085
1688
|
});
|
|
@@ -1756,8 +2359,19 @@ var TOOL_DEFINITIONS = [
|
|
|
1756
2359
|
}
|
|
1757
2360
|
}
|
|
1758
2361
|
];
|
|
2362
|
+
async function resolveLlmBody(ctx, node) {
|
|
2363
|
+
if (!isStewardshipEnabled(ctx.nestId)) return node.body || "";
|
|
2364
|
+
const approved = getApprovedVersion(ctx.nestId, node.id);
|
|
2365
|
+
if (approved == null) return null;
|
|
2366
|
+
try {
|
|
2367
|
+
return await ctx.versionManager.reconstructVersion(node.id, approved);
|
|
2368
|
+
} catch (err) {
|
|
2369
|
+
console.error("reconstructVersion failed", node.id, approved, err);
|
|
2370
|
+
return null;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
1759
2373
|
async function handleToolCall(toolName, args, ctx) {
|
|
1760
|
-
const { storage, queryEngine, nestId, userEmail } = ctx;
|
|
2374
|
+
const { storage, queryEngine, versionManager, nestId, userEmail } = ctx;
|
|
1761
2375
|
switch (toolName) {
|
|
1762
2376
|
case "context_init": {
|
|
1763
2377
|
const content = await storage.readContextMd();
|
|
@@ -1798,14 +2412,23 @@ ${nodeList}`;
|
|
|
1798
2412
|
case "context_search": {
|
|
1799
2413
|
const docs = await storage.discoverDocuments();
|
|
1800
2414
|
const terms = (args.query || "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
1801
|
-
const
|
|
1802
|
-
|
|
2415
|
+
const enriched = await Promise.all(
|
|
2416
|
+
docs.map(async (n) => ({ node: n, body: await resolveLlmBody(ctx, n) }))
|
|
2417
|
+
);
|
|
2418
|
+
const visible = enriched.filter((e) => e.body !== null);
|
|
2419
|
+
const matches = visible.filter(({ node: n, body }) => {
|
|
2420
|
+
const hay = [
|
|
2421
|
+
n.frontmatter.title,
|
|
2422
|
+
body || "",
|
|
2423
|
+
n.frontmatter.type || "",
|
|
2424
|
+
...n.frontmatter.tags || []
|
|
2425
|
+
].join(" ").toLowerCase();
|
|
1803
2426
|
return terms.every((t) => hay.includes(t));
|
|
1804
2427
|
});
|
|
1805
2428
|
if (!matches.length) return `No nodes matched search: "${args.query}"`;
|
|
1806
2429
|
const results = matches.map(
|
|
1807
|
-
(n, i) => `${i + 1}. **${n.frontmatter.title}** [${n.frontmatter.type || "document"}] ${(n.frontmatter.tags || []).join(" ")}
|
|
1808
|
-
${(
|
|
2430
|
+
({ node: n, body }, i) => `${i + 1}. **${n.frontmatter.title}** [${n.frontmatter.type || "document"}] ${(n.frontmatter.tags || []).join(" ")}
|
|
2431
|
+
${(body || "").slice(0, 200).replace(/\n/g, " ")}`
|
|
1809
2432
|
).join("\n\n");
|
|
1810
2433
|
return `Found ${matches.length} node(s) matching "${args.query}":
|
|
1811
2434
|
|
|
@@ -1833,6 +2456,10 @@ ${list}`;
|
|
|
1833
2456
|
node = docs.find((n) => n.id === args.id);
|
|
1834
2457
|
}
|
|
1835
2458
|
if (!node) return `Node not found: ${args.title || args.id}`;
|
|
2459
|
+
const body = await resolveLlmBody(ctx, node);
|
|
2460
|
+
if (body === null) {
|
|
2461
|
+
return `Node "${node.frontmatter.title}" has no approved version yet \u2014 not available to AI.`;
|
|
2462
|
+
}
|
|
1836
2463
|
const meta = [
|
|
1837
2464
|
`**Title:** ${node.frontmatter.title}`,
|
|
1838
2465
|
`**Type:** ${node.frontmatter.type || "document"}`,
|
|
@@ -1842,7 +2469,7 @@ ${list}`;
|
|
|
1842
2469
|
|
|
1843
2470
|
---
|
|
1844
2471
|
|
|
1845
|
-
${
|
|
2472
|
+
${body || "(no content)"}`;
|
|
1846
2473
|
}
|
|
1847
2474
|
case "context_list": {
|
|
1848
2475
|
let docs = await storage.discoverDocuments();
|
|
@@ -1852,6 +2479,11 @@ ${node.body || "(no content)"}`;
|
|
|
1852
2479
|
const tag = args.tag.startsWith("#") ? args.tag : `#${args.tag}`;
|
|
1853
2480
|
docs = docs.filter((n) => n.frontmatter.tags?.includes(tag));
|
|
1854
2481
|
}
|
|
2482
|
+
if (isStewardshipEnabled(nestId)) {
|
|
2483
|
+
docs = docs.filter(
|
|
2484
|
+
(n) => getApprovedVersion(nestId, n.id) != null
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
1855
2487
|
docs = docs.slice(0, args.limit || 50);
|
|
1856
2488
|
if (!docs.length) return "No nodes found with the given filters.";
|
|
1857
2489
|
const list = docs.map(
|
|
@@ -1904,6 +2536,11 @@ ${n.body || ""}`;
|
|
|
1904
2536
|
};
|
|
1905
2537
|
await storage.writeDocument(id, serializeDocument3(node));
|
|
1906
2538
|
syncNodeTags(nestId, id, tags);
|
|
2539
|
+
try {
|
|
2540
|
+
await versionManager.createVersion(node, userEmail);
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
console.error("VersionManager.createVersion failed (mcp create)", err);
|
|
2543
|
+
}
|
|
1907
2544
|
createVersion({
|
|
1908
2545
|
nestId,
|
|
1909
2546
|
nodeId: id,
|
|
@@ -1934,16 +2571,45 @@ ${n.body || ""}`;
|
|
|
1934
2571
|
);
|
|
1935
2572
|
tags = [.../* @__PURE__ */ new Set([...tags, ...newTags])];
|
|
1936
2573
|
}
|
|
2574
|
+
const prevVersion = getCurrentVersion(nestId, node.id);
|
|
2575
|
+
const newVersion = prevVersion + 1;
|
|
2576
|
+
const hasStewards = isStewardshipEnabled(nestId);
|
|
1937
2577
|
const updated = {
|
|
1938
2578
|
...node,
|
|
1939
2579
|
body,
|
|
1940
2580
|
frontmatter: {
|
|
1941
2581
|
...node.frontmatter,
|
|
1942
2582
|
tags,
|
|
2583
|
+
version: newVersion,
|
|
1943
2584
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1944
2585
|
}
|
|
1945
2586
|
};
|
|
1946
2587
|
await storage.writeDocument(node.id, serializeDocument3(updated));
|
|
2588
|
+
try {
|
|
2589
|
+
await versionManager.createVersion(updated, userEmail);
|
|
2590
|
+
} catch (err) {
|
|
2591
|
+
console.error("VersionManager.createVersion failed (mcp update)", err);
|
|
2592
|
+
}
|
|
2593
|
+
syncNodeTags(nestId, node.id, tags);
|
|
2594
|
+
createVersion({
|
|
2595
|
+
nestId,
|
|
2596
|
+
nodeId: node.id,
|
|
2597
|
+
version: newVersion,
|
|
2598
|
+
content: body,
|
|
2599
|
+
author: userEmail,
|
|
2600
|
+
status: hasStewards ? "draft" : "approved",
|
|
2601
|
+
tags
|
|
2602
|
+
});
|
|
2603
|
+
if (getPendingReview(nestId, node.id)) {
|
|
2604
|
+
cancelReview({
|
|
2605
|
+
nestId,
|
|
2606
|
+
nodeId: node.id,
|
|
2607
|
+
cancelledBy: userEmail
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
if (!hasStewards) {
|
|
2611
|
+
setApprovedVersion(nestId, node.id, newVersion, userEmail);
|
|
2612
|
+
}
|
|
1947
2613
|
return `Updated node: **${node.frontmatter.title}**`;
|
|
1948
2614
|
}
|
|
1949
2615
|
// ─── Governance Tool Handlers ──────────────────────────────────────
|
|
@@ -2153,6 +2819,7 @@ function createMcpServerForNest(nestId, userEmail) {
|
|
|
2153
2819
|
const text = await handleToolCall(tool.name, args, {
|
|
2154
2820
|
storage: engine.storage,
|
|
2155
2821
|
queryEngine: engine.query,
|
|
2822
|
+
versionManager: engine.versions,
|
|
2156
2823
|
nestId,
|
|
2157
2824
|
userEmail
|
|
2158
2825
|
});
|
|
@@ -2184,7 +2851,7 @@ mcpRoutes.all("/", async (c) => {
|
|
|
2184
2851
|
import { Hono as Hono7 } from "hono";
|
|
2185
2852
|
|
|
2186
2853
|
// src/governance/stewards-parser.ts
|
|
2187
|
-
import { readFileSync, existsSync } from "fs";
|
|
2854
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
2188
2855
|
import { join as join3 } from "path";
|
|
2189
2856
|
function parseStewardsYaml(content) {
|
|
2190
2857
|
const result = { version: 1 };
|
|
@@ -2266,8 +2933,8 @@ function loadStewardsConfig(nestId) {
|
|
|
2266
2933
|
join3(nestPath2, ".context", "stewards.yaml")
|
|
2267
2934
|
];
|
|
2268
2935
|
for (const candidatePath of candidates) {
|
|
2269
|
-
if (
|
|
2270
|
-
const content =
|
|
2936
|
+
if (existsSync2(candidatePath)) {
|
|
2937
|
+
const content = readFileSync2(candidatePath, "utf-8");
|
|
2271
2938
|
return parseStewardsYaml(content);
|
|
2272
2939
|
}
|
|
2273
2940
|
}
|
|
@@ -2293,7 +2960,7 @@ governanceRoutes.post("/stewards", async (c) => {
|
|
|
2293
2960
|
const assignedBy = getUserEmail3(c);
|
|
2294
2961
|
if (!body.scope) throw new ValidationError("scope is required");
|
|
2295
2962
|
if (Array.isArray(body.users)) {
|
|
2296
|
-
const created2 = createStewardRecord({
|
|
2963
|
+
const created2 = await createStewardRecord({
|
|
2297
2964
|
nestId,
|
|
2298
2965
|
scope: body.scope,
|
|
2299
2966
|
documentId: body.documentId,
|
|
@@ -2307,7 +2974,7 @@ governanceRoutes.post("/stewards", async (c) => {
|
|
|
2307
2974
|
if (!body.email) {
|
|
2308
2975
|
throw new ValidationError("users[] or email is required");
|
|
2309
2976
|
}
|
|
2310
|
-
const created = createStewardRecord({
|
|
2977
|
+
const created = await createStewardRecord({
|
|
2311
2978
|
nestId,
|
|
2312
2979
|
scope: body.scope,
|
|
2313
2980
|
documentId: body.scope === "document" ? body.nodePattern : void 0,
|
|
@@ -2397,14 +3064,23 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
|
|
|
2397
3064
|
throw new ValidationError("Node has no versions to review");
|
|
2398
3065
|
}
|
|
2399
3066
|
const userEmail = getUserEmail3(c);
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
3067
|
+
let request;
|
|
3068
|
+
try {
|
|
3069
|
+
request = submitForReview({
|
|
3070
|
+
nestId,
|
|
3071
|
+
nodeId,
|
|
3072
|
+
version: currentVersion,
|
|
3073
|
+
requestedBy: userEmail,
|
|
3074
|
+
note: body.note,
|
|
3075
|
+
priority: body.priority
|
|
3076
|
+
});
|
|
3077
|
+
} catch (err) {
|
|
3078
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3079
|
+
if (/already pending/i.test(msg)) {
|
|
3080
|
+
throw new ConflictError(msg);
|
|
3081
|
+
}
|
|
3082
|
+
throw err;
|
|
3083
|
+
}
|
|
2408
3084
|
const resolved = resolveStewardsForNode(nestId, nodeId);
|
|
2409
3085
|
return c.json(
|
|
2410
3086
|
{
|
|
@@ -2511,172 +3187,17 @@ function ensureAnonymousUser() {
|
|
|
2511
3187
|
return ANON_USER_ID3;
|
|
2512
3188
|
}
|
|
2513
3189
|
|
|
2514
|
-
// src/auth/license.ts
|
|
2515
|
-
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2516
|
-
var suspensionFirstSeen = null;
|
|
2517
|
-
var suspensionConfirmed = false;
|
|
2518
|
-
var suspensionReason = null;
|
|
2519
|
-
var SUSPENSION_CONFIRM_WINDOW_MS = 60 * 60 * 1e3;
|
|
2520
|
-
function isSuspended() {
|
|
2521
|
-
return suspensionConfirmed;
|
|
2522
|
-
}
|
|
2523
|
-
function getSuspensionReason() {
|
|
2524
|
-
return suspensionReason;
|
|
2525
|
-
}
|
|
2526
|
-
async function validateLicense() {
|
|
2527
|
-
const key = config.PROMPTOWL_KEY;
|
|
2528
|
-
if (!key) {
|
|
2529
|
-
return {
|
|
2530
|
-
valid: false,
|
|
2531
|
-
tier: "none",
|
|
2532
|
-
org: null,
|
|
2533
|
-
limits: null,
|
|
2534
|
-
suspended: false,
|
|
2535
|
-
suspendedReason: null
|
|
2536
|
-
};
|
|
2537
|
-
}
|
|
2538
|
-
const db = getDb();
|
|
2539
|
-
const cached = db.prepare("SELECT * FROM license_cache WHERE key = ?").get(key);
|
|
2540
|
-
if (cached) {
|
|
2541
|
-
const age = Date.now() - (/* @__PURE__ */ new Date(cached.validated_at + "Z")).getTime();
|
|
2542
|
-
if (age < CACHE_TTL_MS) {
|
|
2543
|
-
return {
|
|
2544
|
-
valid: true,
|
|
2545
|
-
tier: cached.tier,
|
|
2546
|
-
org: cached.org,
|
|
2547
|
-
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
2548
|
-
suspended: suspensionConfirmed,
|
|
2549
|
-
suspendedReason: suspensionReason
|
|
2550
|
-
};
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
try {
|
|
2554
|
-
const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
|
|
2555
|
-
const res = await fetch(`${promptowlUrl}/api/license/validate`, {
|
|
2556
|
-
method: "POST",
|
|
2557
|
-
headers: { "Content-Type": "application/json" },
|
|
2558
|
-
body: JSON.stringify({ key })
|
|
2559
|
-
});
|
|
2560
|
-
if (!res.ok) {
|
|
2561
|
-
if (cached) {
|
|
2562
|
-
console.warn(
|
|
2563
|
-
" PromptOwl unreachable, using cached license (grace period)"
|
|
2564
|
-
);
|
|
2565
|
-
return {
|
|
2566
|
-
valid: true,
|
|
2567
|
-
tier: cached.tier,
|
|
2568
|
-
org: cached.org,
|
|
2569
|
-
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
2570
|
-
suspended: suspensionConfirmed,
|
|
2571
|
-
suspendedReason: suspensionReason
|
|
2572
|
-
};
|
|
2573
|
-
}
|
|
2574
|
-
return {
|
|
2575
|
-
valid: false,
|
|
2576
|
-
tier: "none",
|
|
2577
|
-
org: null,
|
|
2578
|
-
limits: null,
|
|
2579
|
-
suspended: false,
|
|
2580
|
-
suspendedReason: null
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
const data = await res.json();
|
|
2584
|
-
if (data.suspended === true) {
|
|
2585
|
-
if (!suspensionFirstSeen) {
|
|
2586
|
-
suspensionFirstSeen = Date.now();
|
|
2587
|
-
suspensionReason = data.suspended_reason || "Suspended by PromptOwl";
|
|
2588
|
-
console.warn(
|
|
2589
|
-
`
|
|
2590
|
-
WARNING: PromptOwl has flagged this server for suspension.`
|
|
2591
|
-
);
|
|
2592
|
-
console.warn(
|
|
2593
|
-
` Reason: ${suspensionReason}`
|
|
2594
|
-
);
|
|
2595
|
-
console.warn(
|
|
2596
|
-
` This will be confirmed in ~1 hour. If this is an error,`
|
|
2597
|
-
);
|
|
2598
|
-
console.warn(
|
|
2599
|
-
` contact support@promptowl.ai to reverse it.
|
|
2600
|
-
`
|
|
2601
|
-
);
|
|
2602
|
-
} else if (Date.now() - suspensionFirstSeen >= SUSPENSION_CONFIRM_WINDOW_MS) {
|
|
2603
|
-
suspensionConfirmed = true;
|
|
2604
|
-
suspensionReason = data.suspended_reason || "Suspended by PromptOwl";
|
|
2605
|
-
console.error(
|
|
2606
|
-
`
|
|
2607
|
-
SERVER SUSPENDED: ${suspensionReason}`
|
|
2608
|
-
);
|
|
2609
|
-
console.error(
|
|
2610
|
-
` Write operations are disabled. Reads still work.`
|
|
2611
|
-
);
|
|
2612
|
-
console.error(
|
|
2613
|
-
` Contact support@promptowl.ai to resolve.
|
|
2614
|
-
`
|
|
2615
|
-
);
|
|
2616
|
-
}
|
|
2617
|
-
} else {
|
|
2618
|
-
if (suspensionFirstSeen) {
|
|
2619
|
-
console.log(" Suspension flag cleared by PromptOwl.");
|
|
2620
|
-
}
|
|
2621
|
-
suspensionFirstSeen = null;
|
|
2622
|
-
suspensionConfirmed = false;
|
|
2623
|
-
suspensionReason = null;
|
|
2624
|
-
}
|
|
2625
|
-
if (!data.valid && !data.suspended) {
|
|
2626
|
-
return {
|
|
2627
|
-
valid: false,
|
|
2628
|
-
tier: "none",
|
|
2629
|
-
org: null,
|
|
2630
|
-
limits: null,
|
|
2631
|
-
suspended: false,
|
|
2632
|
-
suspendedReason: null
|
|
2633
|
-
};
|
|
2634
|
-
}
|
|
2635
|
-
if (data.valid) {
|
|
2636
|
-
const limitsJson = data.limits ? JSON.stringify(data.limits) : null;
|
|
2637
|
-
db.prepare(
|
|
2638
|
-
`INSERT OR REPLACE INTO license_cache (key, tier, org, limits_json, validated_at)
|
|
2639
|
-
VALUES (?, ?, ?, ?, datetime('now'))`
|
|
2640
|
-
).run(key, data.tier || "community", data.org || null, limitsJson);
|
|
2641
|
-
}
|
|
2642
|
-
return {
|
|
2643
|
-
valid: data.valid !== false,
|
|
2644
|
-
tier: data.tier || "community",
|
|
2645
|
-
org: data.org || null,
|
|
2646
|
-
limits: data.limits || null,
|
|
2647
|
-
suspended: suspensionConfirmed,
|
|
2648
|
-
suspendedReason: suspensionReason
|
|
2649
|
-
};
|
|
2650
|
-
} catch (err) {
|
|
2651
|
-
if (cached) {
|
|
2652
|
-
console.warn(
|
|
2653
|
-
` PromptOwl validation failed (${err.message}), using cached license`
|
|
2654
|
-
);
|
|
2655
|
-
return {
|
|
2656
|
-
valid: true,
|
|
2657
|
-
tier: cached.tier,
|
|
2658
|
-
org: cached.org,
|
|
2659
|
-
limits: cached.limits_json ? JSON.parse(cached.limits_json) : null,
|
|
2660
|
-
suspended: suspensionConfirmed,
|
|
2661
|
-
suspendedReason: suspensionReason
|
|
2662
|
-
};
|
|
2663
|
-
}
|
|
2664
|
-
return {
|
|
2665
|
-
valid: false,
|
|
2666
|
-
tier: "none",
|
|
2667
|
-
org: null,
|
|
2668
|
-
limits: null,
|
|
2669
|
-
suspended: false,
|
|
2670
|
-
suspendedReason: null
|
|
2671
|
-
};
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
3190
|
// src/app.ts
|
|
2676
3191
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
2677
3192
|
import { fileURLToPath } from "url";
|
|
2678
3193
|
import { dirname, join as join4, relative } from "path";
|
|
2679
|
-
|
|
3194
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3195
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
3196
|
+
var UI_DIR_CANDIDATES = [
|
|
3197
|
+
join4(HERE, "web3"),
|
|
3198
|
+
join4(process.cwd(), "dist", "web3")
|
|
3199
|
+
];
|
|
3200
|
+
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync3(p)) || UI_DIR_CANDIDATES[0];
|
|
2680
3201
|
var UI_DIR_REL = relative(process.cwd(), UI_DIR_ABS) || ".";
|
|
2681
3202
|
var openModeMiddleware = createMiddleware2(async (c, next) => {
|
|
2682
3203
|
const anonId = ensureAnonymousUser();
|
|
@@ -2685,8 +3206,9 @@ var openModeMiddleware = createMiddleware2(async (c, next) => {
|
|
|
2685
3206
|
await next();
|
|
2686
3207
|
});
|
|
2687
3208
|
var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
2688
|
-
const
|
|
2689
|
-
|
|
3209
|
+
const hasBearer = c.req.header("Authorization")?.startsWith("Bearer cnst_");
|
|
3210
|
+
const hasCookie = !!c.req.header("Cookie")?.includes("cnst_session=");
|
|
3211
|
+
if (hasBearer || hasCookie) {
|
|
2690
3212
|
return authMiddleware(c, next);
|
|
2691
3213
|
}
|
|
2692
3214
|
if (config.AUTH_MODE === "open") {
|
|
@@ -2695,11 +3217,10 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
|
2695
3217
|
c.set("nestScope", null);
|
|
2696
3218
|
return next();
|
|
2697
3219
|
}
|
|
2698
|
-
return c.json({ error: "Missing or invalid
|
|
3220
|
+
return c.json({ error: "Missing or invalid credentials" }, 401);
|
|
2699
3221
|
});
|
|
2700
3222
|
function createApp() {
|
|
2701
3223
|
const app = new Hono8();
|
|
2702
|
-
app.use("*", logger());
|
|
2703
3224
|
const corsOrigins = config.CORS_ORIGINS;
|
|
2704
3225
|
app.use(
|
|
2705
3226
|
"*",
|
|
@@ -2730,7 +3251,79 @@ function createApp() {
|
|
|
2730
3251
|
...isSuspended() && { suspended_reason: getSuspensionReason() }
|
|
2731
3252
|
})
|
|
2732
3253
|
);
|
|
3254
|
+
app.use("/auth/invite", async (c, next) => {
|
|
3255
|
+
if (!getCurrentLicense()?.valid) {
|
|
3256
|
+
return c.json(
|
|
3257
|
+
{
|
|
3258
|
+
error: "License required",
|
|
3259
|
+
reason: "Install a valid PromptOwl license key before inviting teammates."
|
|
3260
|
+
},
|
|
3261
|
+
503
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
return next();
|
|
3265
|
+
});
|
|
3266
|
+
app.use("/auth/teammates", async (c, next) => {
|
|
3267
|
+
if (!getCurrentLicense()?.valid) {
|
|
3268
|
+
return c.json(
|
|
3269
|
+
{
|
|
3270
|
+
error: "License required",
|
|
3271
|
+
reason: "Install a valid PromptOwl license key to view teammates."
|
|
3272
|
+
},
|
|
3273
|
+
503
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
return next();
|
|
3277
|
+
});
|
|
2733
3278
|
app.route("/auth", authRoutes);
|
|
3279
|
+
app.get("/license/status", (c) => {
|
|
3280
|
+
const lic = getCurrentLicense();
|
|
3281
|
+
return c.json({
|
|
3282
|
+
valid: !!lic?.valid,
|
|
3283
|
+
tier: lic?.tier || "none",
|
|
3284
|
+
org: lic?.org || null,
|
|
3285
|
+
owner_email: lic?.ownerEmail || null,
|
|
3286
|
+
suspended: !!lic?.suspended,
|
|
3287
|
+
suspended_reason: lic?.suspendedReason || null,
|
|
3288
|
+
limits: lic?.limits || null
|
|
3289
|
+
});
|
|
3290
|
+
});
|
|
3291
|
+
app.post("/license/install", async (c) => {
|
|
3292
|
+
let body;
|
|
3293
|
+
try {
|
|
3294
|
+
body = await c.req.json();
|
|
3295
|
+
} catch {
|
|
3296
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
3297
|
+
}
|
|
3298
|
+
const key = body?.key?.trim();
|
|
3299
|
+
if (!key || !key.startsWith("pk_")) {
|
|
3300
|
+
return c.json(
|
|
3301
|
+
{ error: "Invalid license key. Must start with pk_." },
|
|
3302
|
+
400
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
try {
|
|
3306
|
+
const info = await installLicenseKey(key);
|
|
3307
|
+
if (!info.valid) {
|
|
3308
|
+
return c.json(
|
|
3309
|
+
{
|
|
3310
|
+
valid: false,
|
|
3311
|
+
error: "License key was saved but PromptOwl rejected it. Verify the key is correct and active."
|
|
3312
|
+
},
|
|
3313
|
+
400
|
|
3314
|
+
);
|
|
3315
|
+
}
|
|
3316
|
+
return c.json({
|
|
3317
|
+
valid: true,
|
|
3318
|
+
tier: info.tier,
|
|
3319
|
+
org: info.org,
|
|
3320
|
+
limits: info.limits
|
|
3321
|
+
});
|
|
3322
|
+
} catch (err) {
|
|
3323
|
+
const msg = err instanceof Error ? err.message : "Failed to install key";
|
|
3324
|
+
return c.json({ error: msg }, 500);
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
2734
3327
|
const nestsApp = new Hono8();
|
|
2735
3328
|
nestsApp.use("*", flexAuthMiddleware);
|
|
2736
3329
|
nestsApp.use("*", async (c, next) => {
|
|
@@ -2753,6 +3346,24 @@ function createApp() {
|
|
|
2753
3346
|
503
|
|
2754
3347
|
);
|
|
2755
3348
|
}
|
|
3349
|
+
{
|
|
3350
|
+
const path2 = c.req.path;
|
|
3351
|
+
const isGovernance = path2.includes("/stewards") || path2.includes("/review-queue") || path2.includes("/versions") || path2.includes("/reviews") || path2.includes("/submit-review") || path2.includes("/cancel-review") || path2.includes("/approve") || path2.includes("/reject") || path2.includes("/can-access") || path2.includes("/can-approve") || path2.includes("/can-edit");
|
|
3352
|
+
const needsLicense = c.req.method !== "GET" || isGovernance;
|
|
3353
|
+
if (needsLicense) {
|
|
3354
|
+
const lic = getCurrentLicense();
|
|
3355
|
+
if (!lic?.valid) {
|
|
3356
|
+
return c.json(
|
|
3357
|
+
{
|
|
3358
|
+
error: "License required",
|
|
3359
|
+
reason: "Install a PromptOwl license key via the setup screen or POST /license/install.",
|
|
3360
|
+
setup_url: "/setup"
|
|
3361
|
+
},
|
|
3362
|
+
503
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
2756
3367
|
if (config.AUTH_MODE === "open") {
|
|
2757
3368
|
c.set("nestPermission", "owner");
|
|
2758
3369
|
return next();
|
|
@@ -2763,13 +3374,21 @@ function createApp() {
|
|
|
2763
3374
|
}
|
|
2764
3375
|
let required = "read";
|
|
2765
3376
|
const path = c.req.path;
|
|
3377
|
+
const isStewardActionPath = path.includes("/approve") || path.includes("/reject") || path.includes("/submit-review") || path.includes("/cancel-review");
|
|
2766
3378
|
if (path.includes("/collaborators") || path.includes("/visibility")) {
|
|
2767
3379
|
required = "admin";
|
|
2768
|
-
} else if (c.req.method !== "GET") {
|
|
3380
|
+
} else if (c.req.method !== "GET" && !isStewardActionPath) {
|
|
2769
3381
|
required = "write";
|
|
2770
3382
|
}
|
|
2771
3383
|
if (permissionLevel(permission) < permissionLevel(required)) {
|
|
2772
|
-
return c.json(
|
|
3384
|
+
return c.json(
|
|
3385
|
+
{
|
|
3386
|
+
error: `You don't have access to perform this action on this nest. Required permission: '${required}', your permission: '${permission}'. Ask the nest owner or a server admin to grant you ${required} access.`,
|
|
3387
|
+
required_permission: required,
|
|
3388
|
+
your_permission: permission
|
|
3389
|
+
},
|
|
3390
|
+
403
|
|
3391
|
+
);
|
|
2773
3392
|
}
|
|
2774
3393
|
c.set("nestPermission", permission);
|
|
2775
3394
|
return next();
|
|
@@ -2788,8 +3407,74 @@ function createApp() {
|
|
|
2788
3407
|
if (err instanceof AppError) {
|
|
2789
3408
|
return c.json({ error: err.message }, err.statusCode);
|
|
2790
3409
|
}
|
|
3410
|
+
const e = err;
|
|
3411
|
+
const code = e.code;
|
|
3412
|
+
const msg = e.message || "";
|
|
3413
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE") {
|
|
3414
|
+
let friendly = "That conflicts with existing data. Please change a value and try again.";
|
|
3415
|
+
if (msg.includes("node_versions")) {
|
|
3416
|
+
friendly = "A version record for this node already exists. The document may have been recreated with the same name \u2014 pick a different title or remove the old node first.";
|
|
3417
|
+
} else if (msg.includes("review_requests")) {
|
|
3418
|
+
friendly = "A review request for this node already exists.";
|
|
3419
|
+
} else if (msg.includes("nests")) {
|
|
3420
|
+
friendly = "A nest with that name already exists.";
|
|
3421
|
+
} else if (msg.includes("users")) {
|
|
3422
|
+
friendly = "An account with that email already exists.";
|
|
3423
|
+
} else if (msg.includes("stewards")) {
|
|
3424
|
+
friendly = "That steward is already assigned with the same scope.";
|
|
3425
|
+
}
|
|
3426
|
+
return c.json({ error: friendly }, 409);
|
|
3427
|
+
}
|
|
3428
|
+
if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
|
|
3429
|
+
return c.json(
|
|
3430
|
+
{
|
|
3431
|
+
error: "Required related record is missing. Ensure the parent resource still exists."
|
|
3432
|
+
},
|
|
3433
|
+
409
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
if (code === "SQLITE_CONSTRAINT_CHECK") {
|
|
3437
|
+
return c.json(
|
|
3438
|
+
{
|
|
3439
|
+
error: "Provided value isn't allowed for one of the fields. Double-check your input."
|
|
3440
|
+
},
|
|
3441
|
+
400
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
if (code === "SQLITE_CONSTRAINT_NOTNULL") {
|
|
3445
|
+
return c.json(
|
|
3446
|
+
{ error: "A required field is missing. Please fill it in and retry." },
|
|
3447
|
+
400
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
if (code === "ENOENT") {
|
|
3451
|
+
return c.json(
|
|
3452
|
+
{ error: "The requested file or folder doesn't exist." },
|
|
3453
|
+
404
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
3457
|
+
return c.json(
|
|
3458
|
+
{ error: "Server doesn't have permission to access that file." },
|
|
3459
|
+
500
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
if (code === "ENOSPC") {
|
|
3463
|
+
return c.json(
|
|
3464
|
+
{ error: "Out of disk space on the server. Free up space and retry." },
|
|
3465
|
+
507
|
|
3466
|
+
);
|
|
3467
|
+
}
|
|
3468
|
+
if (err instanceof SyntaxError && msg.toLowerCase().includes("json")) {
|
|
3469
|
+
return c.json({ error: "Request body isn't valid JSON." }, 400);
|
|
3470
|
+
}
|
|
2791
3471
|
console.error("Unhandled error:", err);
|
|
2792
|
-
return c.json(
|
|
3472
|
+
return c.json(
|
|
3473
|
+
{
|
|
3474
|
+
error: "Something went wrong on the server. Try again \u2014 if it keeps failing, check the server log."
|
|
3475
|
+
},
|
|
3476
|
+
500
|
|
3477
|
+
);
|
|
2793
3478
|
});
|
|
2794
3479
|
return app;
|
|
2795
3480
|
}
|
|
@@ -2805,16 +3490,19 @@ async function main() {
|
|
|
2805
3490
|
if (!license.valid) {
|
|
2806
3491
|
console.warn(`
|
|
2807
3492
|
WARNING: No valid PromptOwl license key found.
|
|
2808
|
-
|
|
3493
|
+
Server boots in SETUP MODE \u2014 writes return 503 and admin features
|
|
3494
|
+
(stewards, teammates, governance) are locked until a key is installed.
|
|
2809
3495
|
|
|
2810
|
-
To
|
|
3496
|
+
To activate:
|
|
2811
3497
|
1. Sign up at https://app.promptowl.ai
|
|
2812
|
-
2.
|
|
2813
|
-
3.
|
|
2814
|
-
4.
|
|
3498
|
+
2. Open Overview > Community License
|
|
3499
|
+
3. Generate a key
|
|
3500
|
+
4. Paste it into the setup screen (UI) or POST /license/install,
|
|
3501
|
+
or set PROMPTOWL_KEY=pk_... in your environment and restart.
|
|
2815
3502
|
`);
|
|
2816
3503
|
}
|
|
2817
3504
|
const app = createApp();
|
|
3505
|
+
startLicenseWatcher();
|
|
2818
3506
|
startTelemetryLoop();
|
|
2819
3507
|
trackEvent("server.start", {
|
|
2820
3508
|
tier: license.tier,
|