@prenta/core 7.7.1 → 7.8.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/CHANGELOG.md +12 -0
- package/dist/api/handler-factory.d.ts.map +1 -1
- package/dist/api/handler-factory.js +40 -14
- package/dist/api/handler-factory.js.map +1 -1
- package/dist/api/openapi.d.ts.map +1 -1
- package/dist/api/openapi.js +8 -2
- package/dist/api/openapi.js.map +1 -1
- package/dist/api/route-helpers/auth.d.ts +131 -0
- package/dist/api/route-helpers/auth.d.ts.map +1 -0
- package/dist/api/route-helpers/auth.js +505 -0
- package/dist/api/route-helpers/auth.js.map +1 -0
- package/dist/api/route-helpers/authoring.d.ts +32 -0
- package/dist/api/route-helpers/authoring.d.ts.map +1 -0
- package/dist/api/route-helpers/authoring.js +99 -0
- package/dist/api/route-helpers/authoring.js.map +1 -0
- package/dist/api/route-helpers/forms.d.ts +46 -0
- package/dist/api/route-helpers/forms.d.ts.map +1 -0
- package/dist/api/route-helpers/forms.js +435 -0
- package/dist/api/route-helpers/forms.js.map +1 -0
- package/dist/api/route-helpers/http.d.ts +87 -0
- package/dist/api/route-helpers/http.d.ts.map +1 -0
- package/dist/api/route-helpers/http.js +286 -0
- package/dist/api/route-helpers/http.js.map +1 -0
- package/dist/api/route-helpers/media.d.ts +37 -0
- package/dist/api/route-helpers/media.d.ts.map +1 -0
- package/dist/api/route-helpers/media.js +189 -0
- package/dist/api/route-helpers/media.js.map +1 -0
- package/dist/api/route-helpers/page-shape.d.ts +31 -0
- package/dist/api/route-helpers/page-shape.d.ts.map +1 -0
- package/dist/api/route-helpers/page-shape.js +98 -0
- package/dist/api/route-helpers/page-shape.js.map +1 -0
- package/dist/api/route-helpers.d.ts +8 -358
- package/dist/api/route-helpers.d.ts.map +1 -1
- package/dist/api/route-helpers.js +7 -1592
- package/dist/api/route-helpers.js.map +1 -1
- package/dist/api/routes/users/handlers/api-keys.d.ts +4 -0
- package/dist/api/routes/users/handlers/api-keys.d.ts.map +1 -0
- package/dist/api/routes/users/handlers/api-keys.js +164 -0
- package/dist/api/routes/users/handlers/api-keys.js.map +1 -0
- package/dist/api/routes/users/handlers/invites.d.ts +7 -0
- package/dist/api/routes/users/handlers/invites.d.ts.map +1 -0
- package/dist/api/routes/users/handlers/invites.js +201 -0
- package/dist/api/routes/users/handlers/invites.js.map +1 -0
- package/dist/api/routes/users/handlers/users-admin.d.ts +7 -0
- package/dist/api/routes/users/handlers/users-admin.d.ts.map +1 -0
- package/dist/api/routes/users/handlers/users-admin.js +310 -0
- package/dist/api/routes/users/handlers/users-admin.js.map +1 -0
- package/dist/api/routes/users/handlers/users-detail.d.ts +16 -0
- package/dist/api/routes/users/handlers/users-detail.d.ts.map +1 -0
- package/dist/api/routes/users/handlers/users-detail.js +525 -0
- package/dist/api/routes/users/handlers/users-detail.js.map +1 -0
- package/dist/api/routes/users.d.ts +4 -0
- package/dist/api/routes/users.d.ts.map +1 -1
- package/dist/api/routes/users.js +38 -1177
- package/dist/api/routes/users.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { assertCollectionAccess } from '../../actions.js';
|
|
2
|
+
import { verifySession } from '../../auth/session.js';
|
|
3
|
+
import { resolveEmailSender } from '../../email/sender.js';
|
|
4
|
+
import { workspaceDomainFromUrl } from '../../auth/team.js';
|
|
5
|
+
import { getDB } from '../../db.js';
|
|
6
|
+
import { createRateLimiter } from '../../security/rate-limit.js';
|
|
7
|
+
import { bindRequestLogUser } from '../../diagnostics/logger.js';
|
|
8
|
+
import { getClientIp } from '../../security/client-ip.js';
|
|
9
|
+
import { enforceSessionLimits } from '../../security/session-limits.js';
|
|
10
|
+
import { verifyReauth } from '../../security/reauth.js';
|
|
11
|
+
import { getPrentaConfig } from '../../config/runtime.js';
|
|
12
|
+
import { hashApiKey, looksLikeApiKey, validateApiKeyScope, validateApiKeyGlobalScope, validateApiKeyMediaScope, validateApiKeyIp, } from '../../security/api-key-enhanced.js';
|
|
13
|
+
import { guardedDb } from '../guarded-db.js';
|
|
14
|
+
import { SECURITY_HEADERS } from '../security-headers.js';
|
|
15
|
+
import { logger, json, errorResponse, hasModel } from './http.js';
|
|
16
|
+
let _secretMissing = false;
|
|
17
|
+
let _secretWarningLogged = false;
|
|
18
|
+
export function getSessionSecret() {
|
|
19
|
+
const secret = process.env.CMS_SECRET ?? process.env.CMS_SESSION_SECRET ?? getPrentaConfig()?.secret;
|
|
20
|
+
if (!secret) {
|
|
21
|
+
_secretMissing = true;
|
|
22
|
+
if (!_secretWarningLogged) {
|
|
23
|
+
_secretWarningLogged = true;
|
|
24
|
+
logger.error('Missing CMS secret. Set CMS_SECRET (min 32 characters) or pass secret in prenta.config.ts. Authenticated routes return 503 until it is set.');
|
|
25
|
+
}
|
|
26
|
+
throw new Error('CMS secret not configured');
|
|
27
|
+
}
|
|
28
|
+
if (secret.length < 32) {
|
|
29
|
+
throw new Error('[Prenta CMS] CMS secret must be at least 32 characters (got ' +
|
|
30
|
+
secret.length +
|
|
31
|
+
'). ' +
|
|
32
|
+
"Generate a secure value with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"");
|
|
33
|
+
}
|
|
34
|
+
_secretMissing = false;
|
|
35
|
+
return secret;
|
|
36
|
+
}
|
|
37
|
+
export function isSecretMissing() {
|
|
38
|
+
if (_secretMissing)
|
|
39
|
+
return true;
|
|
40
|
+
try {
|
|
41
|
+
getSessionSecret();
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function extractSession(request) {
|
|
49
|
+
if (isSecretMissing())
|
|
50
|
+
return null;
|
|
51
|
+
let token;
|
|
52
|
+
const authHeader = request.headers.get('Authorization');
|
|
53
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
54
|
+
token = authHeader.slice(7);
|
|
55
|
+
}
|
|
56
|
+
if (!token) {
|
|
57
|
+
const cookieHeader = request.headers.get('cookie') ?? '';
|
|
58
|
+
const cookies = parseCookieHeader(cookieHeader);
|
|
59
|
+
token = cookies['prenta_session'];
|
|
60
|
+
}
|
|
61
|
+
if (!token)
|
|
62
|
+
return null;
|
|
63
|
+
// API key path. Keys are recognized by the `act_sk_` prefix and looked up
|
|
64
|
+
// by SHA-256 hash. We never JWT-verify these tokens — they are opaque
|
|
65
|
+
// random secrets stored as hashes in `prenta_api_keys`.
|
|
66
|
+
if (looksLikeApiKey(token)) {
|
|
67
|
+
try {
|
|
68
|
+
const d = getDB();
|
|
69
|
+
if (!hasModel(d, 'apiKey'))
|
|
70
|
+
return null;
|
|
71
|
+
const hash = await hashApiKey(token);
|
|
72
|
+
const apiKey = await d.apiKey.findUnique({ where: { keyHash: hash } });
|
|
73
|
+
if (!apiKey)
|
|
74
|
+
return null;
|
|
75
|
+
if (apiKey.revokedAt)
|
|
76
|
+
return null;
|
|
77
|
+
if (apiKey.expiresAt && new Date(apiKey.expiresAt).getTime() < Date.now())
|
|
78
|
+
return null;
|
|
79
|
+
const ipRestrictions = Array.isArray(apiKey.ipRestrictions)
|
|
80
|
+
? apiKey.ipRestrictions
|
|
81
|
+
: apiKey.ipRestrictions
|
|
82
|
+
? (apiKey.ipRestrictions.allow ?? null)
|
|
83
|
+
: null;
|
|
84
|
+
if (ipRestrictions && ipRestrictions.length > 0) {
|
|
85
|
+
const ip = getClientIp(request);
|
|
86
|
+
if (!validateApiKeyIp(ipRestrictions, ip))
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
// Keys inherit the owner's current lifecycle + role. Suspension, lock,
|
|
90
|
+
// deactivation, or a demotion must take effect immediately — the key
|
|
91
|
+
// cannot outrank the user who minted it (CRIT-1).
|
|
92
|
+
if (!hasModel(d, 'user'))
|
|
93
|
+
return null;
|
|
94
|
+
const owner = await d.user.findUnique({
|
|
95
|
+
where: { id: apiKey.userId },
|
|
96
|
+
select: { id: true, role: true, isActive: true, suspendedAt: true, lockedAt: true },
|
|
97
|
+
});
|
|
98
|
+
if (!owner)
|
|
99
|
+
return null;
|
|
100
|
+
if (owner.isActive === false || owner.suspendedAt || owner.lockedAt)
|
|
101
|
+
return null;
|
|
102
|
+
const scopes = apiKey.scopes ?? {};
|
|
103
|
+
// Fire-and-forget lastUsedAt update; never block the request on it.
|
|
104
|
+
void d.apiKey
|
|
105
|
+
.update({ where: { id: apiKey.id }, data: { lastUsedAt: new Date() } })
|
|
106
|
+
.catch(() => { });
|
|
107
|
+
bindRequestLogUser(apiKey.userId);
|
|
108
|
+
return {
|
|
109
|
+
userId: apiKey.userId,
|
|
110
|
+
role: scopes.admin ? String(owner.role || 'ADMIN') : 'API_KEY',
|
|
111
|
+
sessionId: apiKey.id,
|
|
112
|
+
apiKey: { id: apiKey.id, scopes },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Session JWT path.
|
|
120
|
+
try {
|
|
121
|
+
const payload = await verifySession(token, { secret: getSessionSecret() });
|
|
122
|
+
const d = getDB();
|
|
123
|
+
if (hasModel(d, 'session')) {
|
|
124
|
+
const dbSession = await d.session.findUnique({
|
|
125
|
+
where: { id: payload.sessionId },
|
|
126
|
+
select: { revokedAt: true },
|
|
127
|
+
});
|
|
128
|
+
if (!dbSession || dbSession.revokedAt)
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (payload.userId)
|
|
132
|
+
bindRequestLogUser(payload.userId);
|
|
133
|
+
return payload;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function parseCookieHeader(cookieHeader) {
|
|
140
|
+
const cookies = {};
|
|
141
|
+
for (const pair of cookieHeader.split(';')) {
|
|
142
|
+
const [key, ...rest] = pair.split('=');
|
|
143
|
+
if (key) {
|
|
144
|
+
cookies[key.trim()] = rest.join('=').trim();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return cookies;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Verify a password (or "current password") posted in the X-Reauth-Password
|
|
151
|
+
* header against the authenticated user. Returns an error Response when the
|
|
152
|
+
* header is missing or the password is wrong; returns null on success.
|
|
153
|
+
*
|
|
154
|
+
* Use this for high-impact account changes (TOTP enable/disable, role change,
|
|
155
|
+
* delete user, etc.) so a stolen session alone cannot perform them.
|
|
156
|
+
*/
|
|
157
|
+
export async function requirePasswordReauth(request, userId) {
|
|
158
|
+
const password = request.headers.get('x-reauth-password');
|
|
159
|
+
if (!password) {
|
|
160
|
+
return new Response(JSON.stringify({
|
|
161
|
+
error: 'Re-authentication required for this action.',
|
|
162
|
+
code: 'REAUTH_REQUIRED',
|
|
163
|
+
method: 'password',
|
|
164
|
+
}), { status: 401, headers: { ...SECURITY_HEADERS } });
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const { getDB } = await import('../../db.js');
|
|
168
|
+
const ok = await verifyReauth(userId, password, 'password', getDB());
|
|
169
|
+
if (!ok) {
|
|
170
|
+
return errorResponse('Re-authentication failed.', 401);
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
logger.error('reauth verify failed', {
|
|
176
|
+
error: err instanceof Error ? err.message : String(err),
|
|
177
|
+
});
|
|
178
|
+
return errorResponse('Re-authentication failed.', 401);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export async function requireAuth(request) {
|
|
182
|
+
if (isSecretMissing()) {
|
|
183
|
+
return {
|
|
184
|
+
error: json({
|
|
185
|
+
error: 'CMS secret not configured. Set CMS_SECRET or CMS_SESSION_SECRET environment variable (min 32 characters).',
|
|
186
|
+
code: 'MISSING_SECRET',
|
|
187
|
+
}, 503),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const session = await extractSession(request);
|
|
191
|
+
if (!session) {
|
|
192
|
+
return { error: errorResponse('Unauthorized', 401) };
|
|
193
|
+
}
|
|
194
|
+
return { session };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* API-key collection-scope check as a plain denial reason (`null` = allowed).
|
|
198
|
+
* JWT sessions are never denied here — role checks are the route's job. Use
|
|
199
|
+
* this where the collection is only known deep inside a service (e.g. an SEO
|
|
200
|
+
* fix that writes a document) and a `Response` cannot be returned directly.
|
|
201
|
+
*/
|
|
202
|
+
export function apiKeyCollectionScopeDenial(session, collection, action) {
|
|
203
|
+
if (!session.apiKey)
|
|
204
|
+
return null;
|
|
205
|
+
return validateApiKeyScope(session.apiKey.scopes, collection, action)
|
|
206
|
+
? null
|
|
207
|
+
: `API key does not have permission to ${action} on collection "${collection}"`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Check that the request's auth context permits `action` on `collection`.
|
|
211
|
+
* - Session-authenticated requests fall through to `requireRole` semantics:
|
|
212
|
+
* write actions require WRITE_ROLES.
|
|
213
|
+
* - API-key-authenticated requests must have an explicit scope match.
|
|
214
|
+
*/
|
|
215
|
+
export function requireCollectionScope(session, collection, action) {
|
|
216
|
+
if (session.apiKey) {
|
|
217
|
+
const denial = apiKeyCollectionScopeDenial(session, collection, action);
|
|
218
|
+
return denial ? errorResponse(denial, 403) : null;
|
|
219
|
+
}
|
|
220
|
+
if (action === 'read')
|
|
221
|
+
return null;
|
|
222
|
+
return requireRole(session.role, WRITE_ROLES);
|
|
223
|
+
}
|
|
224
|
+
export function requireGlobalScope(session, slug) {
|
|
225
|
+
if (session.apiKey) {
|
|
226
|
+
if (!validateApiKeyGlobalScope(session.apiKey.scopes, slug)) {
|
|
227
|
+
return errorResponse(`API key does not have permission for global "${slug}"`, 403);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
export function requireMediaScope(session) {
|
|
233
|
+
if (session.apiKey) {
|
|
234
|
+
if (!validateApiKeyMediaScope(session.apiKey.scopes)) {
|
|
235
|
+
return errorResponse('API key does not have media scope', 403);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
export function requireAdminScope(session) {
|
|
241
|
+
if (session.apiKey) {
|
|
242
|
+
if (!session.apiKey.scopes.admin) {
|
|
243
|
+
return errorResponse('API key does not have admin scope', 403);
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return requireRole(session.role, ADMIN_ROLES);
|
|
248
|
+
}
|
|
249
|
+
export function buildActionContext(session, db, locale) {
|
|
250
|
+
return {
|
|
251
|
+
userId: session.userId,
|
|
252
|
+
role: session.role,
|
|
253
|
+
locale,
|
|
254
|
+
db,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
export const WRITE_ROLES = new Set(['OWNER', 'ADMIN', 'EDITOR', 'AUTHOR']);
|
|
258
|
+
export const ADMIN_ROLES = new Set(['OWNER', 'ADMIN']);
|
|
259
|
+
export function requireRole(role, allowedRoles) {
|
|
260
|
+
if (!allowedRoles.has(role)) {
|
|
261
|
+
return errorResponse('Insufficient permissions', 403);
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Build the shared {@link TeamOptions} for the Users endpoints from config +
|
|
267
|
+
* session: the workspace domain (for external-domain detection) and the seat
|
|
268
|
+
* cap (config-provided; null = unlimited until billing exists).
|
|
269
|
+
*/
|
|
270
|
+
export function teamOptionsFor(session) {
|
|
271
|
+
const cfg = getPrentaConfig();
|
|
272
|
+
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ??
|
|
273
|
+
cfg?.seo?.siteUrl ??
|
|
274
|
+
null;
|
|
275
|
+
const seatLimit = cfg?.users?.seatLimit ??
|
|
276
|
+
cfg?.admin?.seatLimit ??
|
|
277
|
+
null;
|
|
278
|
+
return {
|
|
279
|
+
workspaceDomain: workspaceDomainFromUrl(siteUrl),
|
|
280
|
+
seatLimit,
|
|
281
|
+
currentUserId: session.userId,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/** Whether the configured email provider can actually send mail. */
|
|
285
|
+
function isEmailConfigured() {
|
|
286
|
+
const cfg = getPrentaConfig();
|
|
287
|
+
if (cfg?.platform?.email)
|
|
288
|
+
return true;
|
|
289
|
+
return !!resolveEmailSender();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* DB-aware variant of {@link isEmailConfigured} — also true when a provider
|
|
293
|
+
* is configured through Settings > Email.
|
|
294
|
+
*/
|
|
295
|
+
export async function isEmailConfiguredAsync(db) {
|
|
296
|
+
if (isEmailConfigured())
|
|
297
|
+
return true;
|
|
298
|
+
const { resolveEmailSenderAsync } = await import('../../email/resolve.js');
|
|
299
|
+
return Boolean(await resolveEmailSenderAsync(db));
|
|
300
|
+
}
|
|
301
|
+
export const loginLimiter = createRateLimiter({
|
|
302
|
+
maxRequests: 5,
|
|
303
|
+
windowMs: 15 * 60 * 1000,
|
|
304
|
+
failClosed: true,
|
|
305
|
+
});
|
|
306
|
+
// Fallback limiter for auth requests whose client IP can't be resolved (no
|
|
307
|
+
// trusted proxy header) — those all share ONE bucket. The strict 5/15min
|
|
308
|
+
// `loginLimiter` there would let a handful of anonymous requests lock every
|
|
309
|
+
// user out of login (see #536), so the shared "unknown" bucket gets a much
|
|
310
|
+
// higher ceiling. The real fix is resolving IPs (PRENTA_TRUST_PROXY); this
|
|
311
|
+
// just bounds abuse without turning a mis-config into a global outage.
|
|
312
|
+
export const loginUnknownLimiter = createRateLimiter({
|
|
313
|
+
maxRequests: 100,
|
|
314
|
+
windowMs: 15 * 60 * 1000,
|
|
315
|
+
failClosed: true,
|
|
316
|
+
});
|
|
317
|
+
/** Per-email-hash login limiter so account protection does not depend on IP. */
|
|
318
|
+
export const loginEmailLimiter = createRateLimiter({
|
|
319
|
+
maxRequests: 5,
|
|
320
|
+
windowMs: 15 * 60 * 1000,
|
|
321
|
+
failClosed: true,
|
|
322
|
+
});
|
|
323
|
+
// Admin-gated, but bounds invitation-email volume (and accidental loops) per
|
|
324
|
+
// admin: enough to onboard a team, low enough to deter abuse.
|
|
325
|
+
export const inviteLimiter = createRateLimiter({ maxRequests: 20, windowMs: 60 * 60 * 1000 });
|
|
326
|
+
export const totpLimiter = createRateLimiter({
|
|
327
|
+
maxRequests: 10,
|
|
328
|
+
windowMs: 15 * 60 * 1000,
|
|
329
|
+
failClosed: true,
|
|
330
|
+
});
|
|
331
|
+
// Admin-gated test sends from Settings > Email — enough to debug a provider
|
|
332
|
+
// setup, low enough that a compromised admin session can't spray mail.
|
|
333
|
+
export const emailTestLimiter = createRateLimiter({ maxRequests: 10, windowMs: 60 * 60 * 1000 });
|
|
334
|
+
export const aiGenerateLimiter = createRateLimiter({
|
|
335
|
+
maxRequests: 20,
|
|
336
|
+
windowMs: 60 * 60 * 1000,
|
|
337
|
+
failClosed: true,
|
|
338
|
+
});
|
|
339
|
+
// Strict per-user upload cap (security rule: uploads strict at 20/hour) — guards
|
|
340
|
+
// storage cost and optimization abuse beyond the global per-IP API bucket.
|
|
341
|
+
export const uploadLimiter = createRateLimiter({
|
|
342
|
+
maxRequests: 20,
|
|
343
|
+
windowMs: 60 * 60 * 1000,
|
|
344
|
+
failClosed: true,
|
|
345
|
+
});
|
|
346
|
+
// Deterministic (no provider) AI-adjacent routes that scan/iterate documents.
|
|
347
|
+
// Cheaper than the generation budget but still bounded against cost/DoS.
|
|
348
|
+
export const aiAnalysisLimiter = createRateLimiter({
|
|
349
|
+
maxRequests: 60,
|
|
350
|
+
windowMs: 60 * 60 * 1000,
|
|
351
|
+
failClosed: true,
|
|
352
|
+
});
|
|
353
|
+
// Upper bound on free-text inputs to the deterministic analysis routes
|
|
354
|
+
// (document body / link-candidate content) — caps memory + scan work.
|
|
355
|
+
export const AI_ANALYSIS_MAX_CHARS = 50_000;
|
|
356
|
+
export const linkHealthLimiter = createRateLimiter({ maxRequests: 4, windowMs: 60 * 60 * 1000 });
|
|
357
|
+
export const seoAuditLimiter = createRateLimiter({ maxRequests: 10, windowMs: 60 * 60 * 1000 });
|
|
358
|
+
// Admin-gated live checks against the Google Search Console API ("Test
|
|
359
|
+
// connection") — enough to debug a setup, bounded against key-guessing.
|
|
360
|
+
export const searchConsoleTestLimiter = createRateLimiter({
|
|
361
|
+
maxRequests: 10,
|
|
362
|
+
windowMs: 60 * 60 * 1000,
|
|
363
|
+
failClosed: true,
|
|
364
|
+
});
|
|
365
|
+
// Manual search-data syncs hit Google quotas and write big row sets; the
|
|
366
|
+
// nightly cron is the normal path, so the manual button is strictly bounded.
|
|
367
|
+
export const searchSyncLimiter = createRateLimiter({
|
|
368
|
+
maxRequests: 5,
|
|
369
|
+
windowMs: 60 * 60 * 1000,
|
|
370
|
+
failClosed: true,
|
|
371
|
+
});
|
|
372
|
+
// Digest test-sends dispatch real email to the caller — strictly bounded.
|
|
373
|
+
export const digestTestSendLimiter = createRateLimiter({
|
|
374
|
+
maxRequests: 5,
|
|
375
|
+
windowMs: 60 * 60 * 1000,
|
|
376
|
+
failClosed: true,
|
|
377
|
+
});
|
|
378
|
+
export async function checkRateLimitAsync(limiter, key) {
|
|
379
|
+
// Explicit, environment-gated bypass for test harnesses. Production never
|
|
380
|
+
// sets this — Vercel + the deploy guide both omit it. We deliberately do
|
|
381
|
+
// NOT key off `NODE_ENV === 'test'` because Next.js builds run with
|
|
382
|
+
// NODE_ENV=test in some CI configurations and we want rate limiting
|
|
383
|
+
// exercised by unit tests that explicitly opt in.
|
|
384
|
+
if (process.env.PRENTA_DISABLE_RATE_LIMIT === '1') {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
const result = await limiter.check(key);
|
|
388
|
+
return result.allowed;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Resolve the client IP from trusted-proxy headers (Vercel first, then x-real-ip,
|
|
392
|
+
* then x-forwarded-for only when PRENTA_TRUST_PROXY=1). Returns 'unknown' when
|
|
393
|
+
* no trustworthy source is available — callers MUST treat that case as a hard
|
|
394
|
+
* failure for security-sensitive decisions like rate-limit keys and IP allowlists.
|
|
395
|
+
*/
|
|
396
|
+
export function clientIp(request) {
|
|
397
|
+
return getClientIp(request);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Compute a before/after diff of top-level scalar (string/number/boolean) keys
|
|
401
|
+
* between a stored record and an incoming patch. Used to audit settings changes
|
|
402
|
+
* without ever logging nested objects (which may hold secret blobs).
|
|
403
|
+
*/
|
|
404
|
+
export function diffScalarKeys(previous, next) {
|
|
405
|
+
const isScalar = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
|
|
406
|
+
const changes = [];
|
|
407
|
+
for (const key of Object.keys(next)) {
|
|
408
|
+
const to = next[key];
|
|
409
|
+
if (!isScalar(to))
|
|
410
|
+
continue;
|
|
411
|
+
const from = previous[key];
|
|
412
|
+
if (isScalar(from) && from === to)
|
|
413
|
+
continue;
|
|
414
|
+
if (!isScalar(from) && from !== undefined)
|
|
415
|
+
continue;
|
|
416
|
+
changes.push({ key, from: isScalar(from) ? from : undefined, to });
|
|
417
|
+
}
|
|
418
|
+
return changes;
|
|
419
|
+
}
|
|
420
|
+
const MAX_CONCURRENT_SESSIONS = 5;
|
|
421
|
+
/**
|
|
422
|
+
* After a successful primary-credential check, prune the user's active sessions
|
|
423
|
+
* down to the configured concurrent maximum (default 5; configurable via
|
|
424
|
+
* `auth.maxConcurrentSessions`). Strategy is "revoke oldest" so a user can
|
|
425
|
+
* always sign in.
|
|
426
|
+
*/
|
|
427
|
+
export async function enforceSessionLimitsForUser(d, userId) {
|
|
428
|
+
if (!hasModel(d, 'session'))
|
|
429
|
+
return;
|
|
430
|
+
// Precedence: explicitly-saved Security setting (database) ▸ auth config ▸
|
|
431
|
+
// default. We read the stored value directly (rather than the parsed
|
|
432
|
+
// defaults) so an unsaved policy doesn't shadow a config-provided override.
|
|
433
|
+
let storedMax;
|
|
434
|
+
try {
|
|
435
|
+
if (hasModel(d, 'document')) {
|
|
436
|
+
const row = await d.document.findFirst({
|
|
437
|
+
where: { collection: 'settings', deletedAt: null },
|
|
438
|
+
select: { data: true },
|
|
439
|
+
});
|
|
440
|
+
const stored = row?.data?.security;
|
|
441
|
+
const v = stored?.session?.maxConcurrentSessions;
|
|
442
|
+
if (typeof v === 'number' && Number.isInteger(v) && v >= 0)
|
|
443
|
+
storedMax = v;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
/* fall back to config/default below */
|
|
448
|
+
}
|
|
449
|
+
const auth = getPrentaConfig()?.auth;
|
|
450
|
+
const max = storedMax !== undefined
|
|
451
|
+
? storedMax
|
|
452
|
+
: typeof auth?.maxConcurrentSessions === 'number' && auth.maxConcurrentSessions > 0
|
|
453
|
+
? auth.maxConcurrentSessions
|
|
454
|
+
: MAX_CONCURRENT_SESSIONS;
|
|
455
|
+
// 0 ⇒ unlimited: skip pruning entirely.
|
|
456
|
+
if (max <= 0)
|
|
457
|
+
return;
|
|
458
|
+
const active = await d.session.findMany({
|
|
459
|
+
where: { userId, revokedAt: null, expiresAt: { gt: new Date() } },
|
|
460
|
+
select: { id: true, createdAt: true },
|
|
461
|
+
});
|
|
462
|
+
const decision = enforceSessionLimits(active.map((s) => ({ sessionId: s.id, userId, createdAt: s.createdAt })), { maxConcurrentSessions: max, strategy: 'revoke_oldest' });
|
|
463
|
+
if (decision.sessionsToRevoke.length > 0) {
|
|
464
|
+
await d.session.updateMany({
|
|
465
|
+
where: { id: { in: decision.sessionsToRevoke } },
|
|
466
|
+
data: { revokedAt: new Date() },
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/** Revoke every live API key for a user. Called with session revocation. */
|
|
471
|
+
export async function revokeUserApiKeys(d, userId) {
|
|
472
|
+
if (!hasModel(d, 'apiKey'))
|
|
473
|
+
return 0;
|
|
474
|
+
const result = await d.apiKey.updateMany({
|
|
475
|
+
where: { userId, revokedAt: null },
|
|
476
|
+
data: { revokedAt: new Date() },
|
|
477
|
+
});
|
|
478
|
+
return typeof result?.count === 'number' ? result.count : 0;
|
|
479
|
+
}
|
|
480
|
+
export function getAdminPath() {
|
|
481
|
+
return process.env.PRENTA_ADMIN_PATH ?? getPrentaConfig()?.admin?.path ?? '/admin';
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Gate a per-document read for endpoints (e.g. SEO analysis) that fetch a
|
|
485
|
+
* document directly rather than through the collection CRUD routes. Enforces
|
|
486
|
+
* both the API-key collection scope and the collection's `access.read` rule
|
|
487
|
+
* so an AUTHOR scoped to one collection cannot read another's content.
|
|
488
|
+
* Returns a `Response` to short-circuit with, or `null` when allowed.
|
|
489
|
+
*/
|
|
490
|
+
export async function enforceDocumentReadAccess(session, collection) {
|
|
491
|
+
const scopeErr = requireCollectionScope(session, collection, 'read');
|
|
492
|
+
if (scopeErr)
|
|
493
|
+
return scopeErr;
|
|
494
|
+
try {
|
|
495
|
+
await assertCollectionAccess(collection, 'read', buildActionContext(session, guardedDb()));
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
if (err instanceof Error && err.message.includes('Access denied')) {
|
|
499
|
+
return errorResponse('Insufficient permissions', 403);
|
|
500
|
+
}
|
|
501
|
+
throw err;
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/api/route-helpers/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,sBAAsB,EAAoB,MAAM,oBAAoB,CAAA;AAC7E,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EACL,UAAU,EACV,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,GAEjB,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEzD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEjE,IAAI,cAAc,GAAG,KAAK,CAAA;AAE1B,IAAI,oBAAoB,GAAG,KAAK,CAAA;AAEhC,MAAM,UAAU,gBAAgB;IAC9B,MAAM,MAAM,GACV,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,eAAe,EAAE,EAAE,MAAM,CAAA;IACvF,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,cAAc,GAAG,IAAI,CAAA;QACrB,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC1B,oBAAoB,GAAG,IAAI,CAAA;YAC3B,MAAM,CAAC,KAAK,CACV,6IAA6I,CAC9I,CAAA;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAA;IAC9C,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,8DAA8D;YAC5D,MAAM,CAAC,MAAM;YACb,KAAK;YACL,0GAA0G,CAC7G,CAAA;IACH,CAAC;IACD,cAAc,GAAG,KAAK,CAAA;IACtB,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,IAAI,cAAc;QAAE,OAAO,IAAI,CAAA;IAC/B,IAAI,CAAC;QACH,gBAAgB,EAAE,CAAA;QAClB,OAAO,KAAK,CAAA;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAkBD,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAgB;IACnD,IAAI,eAAe,EAAE;QAAE,OAAO,IAAI,CAAA;IAElC,IAAI,KAAyB,CAAA;IAE7B,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;IACvD,IAAI,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACtC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;QACxD,MAAM,OAAO,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAA;QAC/C,KAAK,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACnC,CAAC;IAED,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IAEvB,0EAA0E;IAC1E,sEAAsE;IACtE,wDAAwD;IACxD,IAAI,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,KAAK,EAAY,CAAA;YAC3B,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC;gBAAE,OAAO,IAAI,CAAA;YACvC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,CAAA;YACpC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;YACtE,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAA;YACxB,IAAI,MAAM,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAA;YACjC,IAAI,MAAM,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE;gBAAE,OAAO,IAAI,CAAA;YAEtF,MAAM,cAAc,GAAoB,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC;gBAC1E,CAAC,CAAE,MAAM,CAAC,cAA2B;gBACrC,CAAC,CAAC,MAAM,CAAC,cAAc;oBACrB,CAAC,CAAC,CAAE,MAAM,CAAC,cAAuC,CAAC,KAAK,IAAI,IAAI,CAAC;oBACjE,CAAC,CAAC,IAAI,CAAA;YACV,IAAI,cAAc,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChD,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;gBAC/B,IAAI,CAAC,gBAAgB,CAAC,cAAc,EAAE,EAAE,CAAC;oBAAE,OAAO,IAAI,CAAA;YACxD,CAAC;YAED,uEAAuE;YACvE,qEAAqE;YACrE,kDAAkD;YAClD,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;gBAAE,OAAO,IAAI,CAAA;YACrC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;gBACpC,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE;gBAC5B,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE;aACpF,CAAC,CAAA;YACF,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAA;YACvB,IAAI,KAAK,CAAC,QAAQ,KAAK,KAAK,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAA;YAEhF,MAAM,MAAM,GAAI,MAAM,CAAC,MAA6B,IAAI,EAAE,CAAA;YAC1D,oEAAoE;YACpE,KAAK,CAAC,CAAC,MAAM;iBACV,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,EAAE,CAAC;iBACtE,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YAElB,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YACjC,OAAO;gBACL,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;gBAC9D,SAAS,EAAE,MAAM,CAAC,EAAE;gBACpB,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE;aAClC,CAAA;QACH,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,oBAAoB;IACpB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAA;QAC1E,MAAM,CAAC,GAAG,KAAK,EAAY,CAAA;QAC3B,IAAI,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,EAAE,CAAC;YAC3B,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;gBAC3C,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,SAAS,EAAE;gBAChC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aAC5B,CAAC,CAAA;YACF,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAA;QACpD,CAAC;QACD,IAAI,OAAO,CAAC,MAAM;YAAE,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACtD,OAAO,OAAO,CAAA;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,YAAoB;IACpD,MAAM,OAAO,GAA2B,EAAE,CAAA;IAC1C,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACtC,IAAI,GAAG,EAAE,CAAC;YACR,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;QAC7C,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,OAAgB,EAChB,MAAc;IAEd,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;IACzD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC;YACb,KAAK,EAAE,6CAA6C;YACpD,IAAI,EAAE,iBAAiB;YACvB,MAAM,EAAE,UAAU;SACnB,CAAC,EACF,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,gBAAgB,EAAE,EAAE,CAClD,CAAA;IACH,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAA;QAC7C,MAAM,EAAE,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;QACpE,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,OAAO,aAAa,CAAC,2BAA2B,EAAE,GAAG,CAAC,CAAA;QACxD,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,sBAAsB,EAAE;YACnC,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACxD,CAAC,CAAA;QACF,OAAO,aAAa,CAAC,2BAA2B,EAAE,GAAG,CAAC,CAAA;IACxD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAgB;IAEhB,IAAI,eAAe,EAAE,EAAE,CAAC;QACtB,OAAO;YACL,KAAK,EAAE,IAAI,CACT;gBACE,KAAK,EACH,2GAA2G;gBAC7G,IAAI,EAAE,gBAAgB;aACvB,EACD,GAAG,CACJ;SACF,CAAA;IACH,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAA;IAC7C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,cAAc,EAAE,GAAG,CAAC,EAAE,CAAA;IACtD,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAA;AACpB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B,CACzC,OAAoB,EACpB,UAAkB,EAClB,MAA+C;IAE/C,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAChC,OAAO,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC;QACnE,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,uCAAuC,MAAM,mBAAmB,UAAU,GAAG,CAAA;AACnF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAoB,EACpB,UAAkB,EAClB,MAA+C;IAE/C,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,2BAA2B,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,CAAA;QACvE,OAAO,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACnD,CAAC;IACD,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAClC,OAAO,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;AAC/C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAoB,EAAE,IAAY;IACnE,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;YAC5D,OAAO,aAAa,CAAC,gDAAgD,IAAI,GAAG,EAAE,GAAG,CAAC,CAAA;QACpF,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAoB;IACpD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACrD,OAAO,aAAa,CAAC,mCAAmC,EAAE,GAAG,CAAC,CAAA;QAChE,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAoB;IACpD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACjC,OAAO,aAAa,CAAC,mCAAmC,EAAE,GAAG,CAAC,CAAA;QAChE,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;AAC/C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,OAAyC,EACzC,EAAY,EACZ,MAAe;IAEf,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM;QACN,EAAE;KACH,CAAA;AACH,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAA;AAE1E,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;AAEtD,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,YAAyB;IACjE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,OAAO,aAAa,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAA;IACvD,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,OAAoB;IACjD,MAAM,GAAG,GAAG,eAAe,EAAE,CAAA;IAC7B,MAAM,OAAO,GACX,OAAO,CAAC,GAAG,CAAC,oBAAoB;QAC/B,GAA6C,EAAE,GAAG,EAAE,OAAO;QAC5D,IAAI,CAAA;IACN,MAAM,SAAS,GACZ,GAAiD,EAAE,KAAK,EAAE,SAAS;QACnE,GAAiD,EAAE,KAAK,EAAE,SAAS;QACpE,IAAI,CAAA;IACN,OAAO;QACL,eAAe,EAAE,sBAAsB,CAAC,OAAO,CAAC;QAChD,SAAS;QACT,aAAa,EAAE,OAAO,CAAC,MAAM;KAC9B,CAAA;AACH,CAAC;AAED,oEAAoE;AACpE,SAAS,iBAAiB;IACxB,MAAM,GAAG,GAAG,eAAe,EAAE,CAAA;IAC7B,IAAK,GAAiD,EAAE,QAAQ,EAAE,KAAK;QAAE,OAAO,IAAI,CAAA;IACpF,OAAO,CAAC,CAAC,kBAAkB,EAAE,CAAA;AAC/B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,EAAY;IACvD,IAAI,iBAAiB,EAAE;QAAE,OAAO,IAAI,CAAA;IACpC,MAAM,EAAE,uBAAuB,EAAE,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAA;IAC1E,OAAO,OAAO,CAAC,MAAM,uBAAuB,CAAC,EAAE,CAAC,CAAC,CAAA;AACnD,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,iBAAiB,CAAC;IAC5C,WAAW,EAAE,CAAC;IACd,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,2EAA2E;AAC3E,yEAAyE;AACzE,4EAA4E;AAC5E,2EAA2E;AAC3E,2EAA2E;AAC3E,uEAAuE;AACvE,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;IACnD,WAAW,EAAE,GAAG;IAChB,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,gFAAgF;AAChF,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;IACjD,WAAW,EAAE,CAAC;IACd,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,6EAA6E;AAC7E,8DAA8D;AAC9D,MAAM,CAAC,MAAM,aAAa,GAAG,iBAAiB,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;AAE7F,MAAM,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAC;IAC3C,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,4EAA4E;AAC5E,uEAAuE;AACvE,MAAM,CAAC,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;AAEhG,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;IACjD,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,iFAAiF;AACjF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,aAAa,GAAG,iBAAiB,CAAC;IAC7C,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,8EAA8E;AAC9E,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;IACjD,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,uEAAuE;AACvE,sEAAsE;AACtE,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAA;AAE3C,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;AAEhG,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;AAE/F,uEAAuE;AACvE,wEAAwE;AACxE,MAAM,CAAC,MAAM,wBAAwB,GAAG,iBAAiB,CAAC;IACxD,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,yEAAyE;AACzE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;IACjD,WAAW,EAAE,CAAC;IACd,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,0EAA0E;AAC1E,MAAM,CAAC,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;IACrD,WAAW,EAAE,CAAC;IACd,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,UAAU,EAAE,IAAI;CACjB,CAAC,CAAA;AAEF,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAkE,EAClE,GAAW;IAEX,0EAA0E;IAC1E,yEAAyE;IACzE,oEAAoE;IACpE,oEAAoE;IACpE,kDAAkD;IAClD,IAAI,OAAO,CAAC,GAAG,CAAC,yBAAyB,KAAK,GAAG,EAAE,CAAC;QAClD,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACvC,OAAO,MAAM,CAAC,OAAO,CAAA;AACvB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAgB;IACvC,OAAO,WAAW,CAAC,OAAO,CAAC,CAAA;AAC7B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAiC,EACjC,IAA6B;IAE7B,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAE,EAAE,CAC9B,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,CAAA;IACxF,MAAM,OAAO,GAAuD,EAAE,CAAA;IACtE,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACpB,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,SAAQ;QAC3B,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;QAC1B,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,EAAE;YAAE,SAAQ;QAC3C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,SAAS;YAAE,SAAQ;QACnD,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,EAAE,EAAE,CAAC,CAAA;IACpE,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,uBAAuB,GAAG,CAAC,CAAA;AAEjC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,CAAM,EAAE,MAAc;IACtE,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC;QAAE,OAAM;IACnC,2EAA2E;IAC3E,qEAAqE;IACrE,4EAA4E;IAC5E,IAAI,SAA6B,CAAA;IACjC,IAAI,CAAC;QACH,IAAI,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC;gBACrC,KAAK,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE;gBAClD,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;aACvB,CAAC,CAAA;YACF,MAAM,MAAM,GAAI,GAAG,EAAE,IAAuC,EAAE,QAEjD,CAAA;YACb,MAAM,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,qBAAqB,CAAA;YAChD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,SAAS,GAAG,CAAC,CAAA;QAC3E,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,uCAAuC;IACzC,CAAC;IACD,MAAM,IAAI,GAAG,eAAe,EAAE,EAAE,IAAI,CAAA;IACpC,MAAM,GAAG,GACP,SAAS,KAAK,SAAS;QACrB,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,OAAO,IAAI,EAAE,qBAAqB,KAAK,QAAQ,IAAI,IAAI,CAAC,qBAAqB,GAAG,CAAC;YACjF,CAAC,CAAC,IAAI,CAAC,qBAAqB;YAC5B,CAAC,CAAC,uBAAuB,CAAA;IAE/B,wCAAwC;IACxC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAM;IAEpB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;QACtC,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,EAAE,EAAE;QACjE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;KACtC,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,oBAAoB,CACnC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,EAC7E,EAAE,qBAAqB,EAAE,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE,CAC1D,CAAA;IACD,IAAI,QAAQ,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;YACzB,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,gBAAgB,EAAE,EAAE;YAChD,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE;SAChC,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,CAAM,EAAE,MAAc;IAC5D,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC;QAAE,OAAO,CAAC,CAAA;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;QACvC,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE;QAClC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE;KAChC,CAAC,CAAA;IACF,OAAO,OAAO,MAAM,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7D,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,eAAe,EAAE,EAAE,KAAK,EAAE,IAAI,IAAI,QAAQ,CAAA;AACpF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAAoB,EACpB,UAAkB;IAElB,MAAM,QAAQ,GAAG,sBAAsB,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,CAAA;IACpE,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC7B,IAAI,CAAC;QACH,MAAM,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,kBAAkB,CAAC,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;IAC5F,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;YAClE,OAAO,aAAa,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAA;QACvD,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PrentaDB } from '../../db/client-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the system/user prompt for an inline co-author action. Kept
|
|
4
|
+
* deterministic and small so the governed runtime can route it to any
|
|
5
|
+
* configured provider/model.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildCoauthorPrompt(action: 'rewrite' | 'expand' | 'compress' | 'proofread', body: {
|
|
8
|
+
text?: string;
|
|
9
|
+
style?: string;
|
|
10
|
+
tone?: string;
|
|
11
|
+
instructions?: string;
|
|
12
|
+
targetLength?: number;
|
|
13
|
+
}): {
|
|
14
|
+
system: string;
|
|
15
|
+
user: string;
|
|
16
|
+
maxTokens: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Common helper for the read endpoints. Reads the `?populate=` query param,
|
|
20
|
+
* fetches the target collection's field schema, and expands every relation
|
|
21
|
+
* reference into a {@link RelationSummary}. When the caller didn't ask for
|
|
22
|
+
* population (or the doc has no `data` field) we short-circuit and return
|
|
23
|
+
* the doc untouched.
|
|
24
|
+
*
|
|
25
|
+
* Lives here (not in `actions.ts`) because the rest of the actions API
|
|
26
|
+
* stays purely DB-shaped — population is an HTTP concern.
|
|
27
|
+
*/
|
|
28
|
+
export declare function maybePopulate(request: Request, collection: string, doc: Record<string, unknown> | null, dbInstance: PrentaDB, viewer?: {
|
|
29
|
+
id?: string;
|
|
30
|
+
role?: string;
|
|
31
|
+
}): Promise<Record<string, unknown> | null>;
|
|
32
|
+
//# sourceMappingURL=authoring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../../../src/api/route-helpers/authoring.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAGxD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,EACvD,IAAI,EAAE;IACJ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GACA;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAuDrD;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACnC,UAAU,EAAE,QAAQ,EACpB,MAAM,CAAC,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CA4BzC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { buildRelationLookup } from '../../actions.js';
|
|
2
|
+
import { populateRelations, parsePopulateParam } from '../../fields/relations.js';
|
|
3
|
+
import { getPrentaConfig } from '../../config/runtime.js';
|
|
4
|
+
import { logger } from './http.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build the system/user prompt for an inline co-author action. Kept
|
|
7
|
+
* deterministic and small so the governed runtime can route it to any
|
|
8
|
+
* configured provider/model.
|
|
9
|
+
*/
|
|
10
|
+
export function buildCoauthorPrompt(action, body) {
|
|
11
|
+
const text = body.text ?? '';
|
|
12
|
+
switch (action) {
|
|
13
|
+
case 'expand': {
|
|
14
|
+
const hint = body.instructions ? ` Follow this guidance: ${body.instructions}.` : '';
|
|
15
|
+
return {
|
|
16
|
+
system: 'You are an expert writer. Expand the user\u2019s text with more detail and depth ' +
|
|
17
|
+
'while preserving its meaning and voice.' +
|
|
18
|
+
hint +
|
|
19
|
+
' Return only the expanded text, with no preamble.',
|
|
20
|
+
user: text,
|
|
21
|
+
maxTokens: 2048,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
case 'compress': {
|
|
25
|
+
const hint = typeof body.targetLength === 'number' && body.targetLength > 0
|
|
26
|
+
? ` Aim for about ${body.targetLength} words.`
|
|
27
|
+
: '';
|
|
28
|
+
return {
|
|
29
|
+
system: 'You are an expert editor. Tighten the user\u2019s text, removing redundancy while ' +
|
|
30
|
+
'preserving the key information and voice.' +
|
|
31
|
+
hint +
|
|
32
|
+
' Return only the edited text, with no preamble.',
|
|
33
|
+
user: text,
|
|
34
|
+
maxTokens: 1024,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
case 'proofread':
|
|
38
|
+
return {
|
|
39
|
+
system: 'You are a meticulous proofreader. Correct grammar, spelling, and punctuation in the ' +
|
|
40
|
+
'user\u2019s text. Preserve its meaning and voice. Return only the corrected text, ' +
|
|
41
|
+
'with no preamble.',
|
|
42
|
+
user: text,
|
|
43
|
+
maxTokens: 2048,
|
|
44
|
+
};
|
|
45
|
+
case 'rewrite':
|
|
46
|
+
default: {
|
|
47
|
+
const style = body.style ? ` Write in a ${body.style} style.` : '';
|
|
48
|
+
const tone = body.tone ? ` Use a ${body.tone} tone.` : '';
|
|
49
|
+
return {
|
|
50
|
+
system: 'You are an expert editor. Rewrite the user\u2019s text to improve clarity and flow ' +
|
|
51
|
+
'while preserving its meaning.' +
|
|
52
|
+
style +
|
|
53
|
+
tone +
|
|
54
|
+
' Return only the rewritten text, with no preamble.',
|
|
55
|
+
user: text,
|
|
56
|
+
maxTokens: 2048,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Common helper for the read endpoints. Reads the `?populate=` query param,
|
|
63
|
+
* fetches the target collection's field schema, and expands every relation
|
|
64
|
+
* reference into a {@link RelationSummary}. When the caller didn't ask for
|
|
65
|
+
* population (or the doc has no `data` field) we short-circuit and return
|
|
66
|
+
* the doc untouched.
|
|
67
|
+
*
|
|
68
|
+
* Lives here (not in `actions.ts`) because the rest of the actions API
|
|
69
|
+
* stays purely DB-shaped — population is an HTTP concern.
|
|
70
|
+
*/
|
|
71
|
+
export async function maybePopulate(request, collection, doc, dbInstance, viewer) {
|
|
72
|
+
if (!doc)
|
|
73
|
+
return doc;
|
|
74
|
+
const url = new URL(request.url);
|
|
75
|
+
const opts = parsePopulateParam(url.searchParams.get('populate'));
|
|
76
|
+
if (!opts)
|
|
77
|
+
return doc;
|
|
78
|
+
const cfg = getPrentaConfig();
|
|
79
|
+
const fields = cfg?.collections?.[collection]?.fields;
|
|
80
|
+
if (!fields)
|
|
81
|
+
return doc;
|
|
82
|
+
const data = doc.data && typeof doc.data === 'object' ? doc.data : null;
|
|
83
|
+
if (!data)
|
|
84
|
+
return doc;
|
|
85
|
+
try {
|
|
86
|
+
const populated = await populateRelations(fields, data, buildRelationLookup(dbInstance, { viewer }), opts);
|
|
87
|
+
return { ...doc, data: populated };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Never let a populate failure mask the underlying document — log and
|
|
91
|
+
// return the unpopulated doc.
|
|
92
|
+
logger.error('populate failed', {
|
|
93
|
+
collection,
|
|
94
|
+
error: err instanceof Error ? err.message : String(err),
|
|
95
|
+
});
|
|
96
|
+
return doc;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=authoring.js.map
|