@musnows/scriverse 0.8.7 → 0.9.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/README.en.md +3 -1
- package/README.md +3 -1
- package/dist/ai-analysis-timeout.js +12 -0
- package/dist/ai-analysis-timeout.js.map +1 -0
- package/dist/ai.js +943 -222
- package/dist/ai.js.map +1 -1
- package/dist/app.js +340 -28
- package/dist/app.js.map +1 -1
- package/dist/database.js +220 -2
- package/dist/database.js.map +1 -1
- package/dist/desktop-protocol.js +21 -0
- package/dist/desktop-protocol.js.map +1 -0
- package/dist/offline-sync.js +436 -0
- package/dist/offline-sync.js.map +1 -0
- package/dist/public/ai-connectivity-test.js +5 -2
- package/dist/public/app.js +470 -76
- package/dist/public/index.html +24 -11
- package/dist/public/roleplay-turn.js +100 -0
- package/dist/public/styles.css +76 -17
- package/dist/public/toast-layer.d.ts +4 -0
- package/dist/public/toast-layer.js +5 -0
- package/dist/roleplay-turn.js +98 -0
- package/dist/roleplay-turn.js.map +1 -0
- package/dist/security.js +36 -9
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +13 -3
- package/dist/server-runtime.js.map +1 -1
- package/dist/storage-manifest.js +116 -0
- package/dist/storage-manifest.js.map +1 -0
- package/dist/store.js +152 -24
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +194 -35
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/user-auth.js
CHANGED
|
@@ -25,8 +25,10 @@ export function relationshipAnalysisReadModules(scope) {
|
|
|
25
25
|
}
|
|
26
26
|
return [...modules];
|
|
27
27
|
}
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
export const SESSION_COOKIE_NAME = "scriverse_session";
|
|
29
|
+
export const SESSION_LIFETIME_MS = 30 * 24 * 60 * 60_000;
|
|
30
|
+
export const DESKTOP_SESSION_TOKEN_PREFIX = "scrvd_";
|
|
31
|
+
export const DESKTOP_SESSION_LIFETIME_MS = 30 * 24 * 60 * 60_000;
|
|
30
32
|
const apiKeyPrefix = "scrv_";
|
|
31
33
|
function membershipAccessRole(row) {
|
|
32
34
|
const role = String(row?.role ?? "");
|
|
@@ -72,30 +74,41 @@ function parseCookies(header) {
|
|
|
72
74
|
}
|
|
73
75
|
return result;
|
|
74
76
|
}
|
|
77
|
+
function bearerCredential(request) {
|
|
78
|
+
const authorization = request.get("authorization");
|
|
79
|
+
if (!authorization?.startsWith("Bearer "))
|
|
80
|
+
return null;
|
|
81
|
+
const token = authorization.slice(7).trim();
|
|
82
|
+
return token.length > 0 && token.length <= 200 ? token : null;
|
|
83
|
+
}
|
|
75
84
|
function apiKeyCredential(request) {
|
|
76
85
|
const direct = request.get("x-scriverse-api-key")?.trim();
|
|
77
86
|
if (direct)
|
|
78
87
|
return direct;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
const bearer = bearerCredential(request);
|
|
89
|
+
return bearer?.startsWith(apiKeyPrefix) ? bearer : null;
|
|
90
|
+
}
|
|
91
|
+
function desktopSessionCredential(request) {
|
|
92
|
+
const bearer = bearerCredential(request);
|
|
93
|
+
return bearer?.startsWith(DESKTOP_SESSION_TOKEN_PREFIX) ? bearer : null;
|
|
83
94
|
}
|
|
84
95
|
function mapUser(row) {
|
|
85
96
|
const avatarSha256 = row.avatar_sha256 === null || row.avatar_sha256 === undefined
|
|
86
97
|
? null
|
|
87
98
|
: String(row.avatar_sha256);
|
|
99
|
+
const isSystemAdmin = String(row.role) === "admin";
|
|
88
100
|
return {
|
|
89
101
|
userId: String(row.id),
|
|
90
102
|
username: String(row.username),
|
|
91
103
|
displayName: String(row.display_name),
|
|
92
|
-
role:
|
|
104
|
+
role: isSystemAdmin ? "admin" : "user",
|
|
93
105
|
status: String(row.status) === "disabled" ? "disabled" : "active",
|
|
94
106
|
createdAt: String(row.created_at),
|
|
95
107
|
avatarUrl: avatarSha256
|
|
96
108
|
? `/api/user-avatars/${encodeURIComponent(String(row.id))}?v=${encodeURIComponent(avatarSha256)}`
|
|
97
109
|
: null,
|
|
98
|
-
onboardingCompleted: row.onboarding_completed_at !== null && row.onboarding_completed_at !== undefined
|
|
110
|
+
onboardingCompleted: row.onboarding_completed_at !== null && row.onboarding_completed_at !== undefined,
|
|
111
|
+
isSystemAdmin
|
|
99
112
|
};
|
|
100
113
|
}
|
|
101
114
|
function passwordDigest(password, salt) {
|
|
@@ -116,6 +129,8 @@ function workIdFromPath(database, pathname) {
|
|
|
116
129
|
return null;
|
|
117
130
|
if (resource === "works" && decoded[3] && decoded[3].toLocaleLowerCase("en-US") !== "import")
|
|
118
131
|
return decoded[3];
|
|
132
|
+
if (resource === "sync" && decoded[3]?.toLocaleLowerCase("en-US") === "works" && decoded[4])
|
|
133
|
+
return decoded[4];
|
|
119
134
|
const tableByResource = {
|
|
120
135
|
volumes: "volumes",
|
|
121
136
|
chapters: "chapters",
|
|
@@ -173,10 +188,20 @@ function workIdFromPath(database, pathname) {
|
|
|
173
188
|
}
|
|
174
189
|
return null;
|
|
175
190
|
}
|
|
191
|
+
function apiKeyCiphertext(row) {
|
|
192
|
+
const encrypted = String(row?.key_encrypted ?? "");
|
|
193
|
+
const iv = String(row?.key_iv ?? "");
|
|
194
|
+
const tag = String(row?.key_tag ?? "");
|
|
195
|
+
if (!encrypted || !iv || !tag)
|
|
196
|
+
return null;
|
|
197
|
+
return { encrypted, iv, tag };
|
|
198
|
+
}
|
|
176
199
|
export class UserAuthService {
|
|
177
200
|
database;
|
|
178
|
-
|
|
201
|
+
vault;
|
|
202
|
+
constructor(database, vault) {
|
|
179
203
|
this.database = database;
|
|
204
|
+
this.vault = vault;
|
|
180
205
|
const revokedAt = new Date().toISOString();
|
|
181
206
|
const result = this.database.run("UPDATE user_sessions SET revoked_at = ? WHERE revoked_at IS NULL", revokedAt);
|
|
182
207
|
if (result.changes > 0) {
|
|
@@ -204,10 +229,33 @@ export class UserAuthService {
|
|
|
204
229
|
const sessionId = randomUUID();
|
|
205
230
|
const timestamp = new Date();
|
|
206
231
|
this.database.run(`INSERT INTO user_sessions (id, user_id, token_hash, csrf_token, created_at, expires_at, last_seen_at)
|
|
207
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`, sessionId, userId, sha256(token), csrfToken, timestamp.toISOString(), new Date(timestamp.getTime() +
|
|
232
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, sessionId, userId, sha256(token), csrfToken, timestamp.toISOString(), new Date(timestamp.getTime() + SESSION_LIFETIME_MS).toISOString(), timestamp.toISOString());
|
|
208
233
|
const user = this.getUser(userId);
|
|
209
234
|
return { token, session: { id: sessionId, user, csrfToken } };
|
|
210
235
|
}
|
|
236
|
+
createDesktopSession(userId, input) {
|
|
237
|
+
const token = `${DESKTOP_SESSION_TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
|
|
238
|
+
const sessionId = randomUUID();
|
|
239
|
+
const timestamp = new Date();
|
|
240
|
+
const createdAt = timestamp.toISOString();
|
|
241
|
+
const expiresAt = new Date(timestamp.getTime() + DESKTOP_SESSION_LIFETIME_MS).toISOString();
|
|
242
|
+
this.database.run(`UPDATE user_desktop_sessions SET revoked_at = ?
|
|
243
|
+
WHERE desktop_id = ? AND profile_id = ? AND revoked_at IS NULL`, createdAt, input.desktopId, input.profileId);
|
|
244
|
+
this.database.run(`INSERT INTO user_desktop_sessions (
|
|
245
|
+
id, user_id, token_hash, desktop_id, profile_id, client_version, created_at, expires_at, last_seen_at
|
|
246
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, sessionId, userId, sha256(token), input.desktopId, input.profileId, input.clientVersion, createdAt, expiresAt, createdAt);
|
|
247
|
+
return {
|
|
248
|
+
token,
|
|
249
|
+
session: {
|
|
250
|
+
id: sessionId,
|
|
251
|
+
user: this.getUser(userId),
|
|
252
|
+
desktopId: input.desktopId,
|
|
253
|
+
profileId: input.profileId,
|
|
254
|
+
clientVersion: input.clientVersion,
|
|
255
|
+
expiresAt
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
211
259
|
register(input) {
|
|
212
260
|
const normalizedUsername = normalizeUsername(input.username);
|
|
213
261
|
const timestamp = new Date().toISOString();
|
|
@@ -229,9 +277,8 @@ export class UserAuthService {
|
|
|
229
277
|
return this.createSession(userId);
|
|
230
278
|
});
|
|
231
279
|
}
|
|
232
|
-
|
|
280
|
+
validateLoginCredentials(username, password) {
|
|
233
281
|
const normalizedUsername = normalizeUsername(username);
|
|
234
|
-
const timestamp = new Date();
|
|
235
282
|
const row = this.database.get("SELECT * FROM users WHERE normalized_username = ?", normalizedUsername);
|
|
236
283
|
const fallbackSalt = "invalid-login-salt";
|
|
237
284
|
const calculated = passwordDigest(password, String(row?.password_salt ?? fallbackSalt));
|
|
@@ -245,15 +292,26 @@ export class UserAuthService {
|
|
|
245
292
|
logger.warn("auth.login.failed", { reason: "account_disabled", actorRef: accountReference(user.userId) });
|
|
246
293
|
throw new AppError(403, "ACCOUNT_DISABLED", "该账户已被停用");
|
|
247
294
|
}
|
|
295
|
+
return { user, normalizedUsername, loginAt: new Date().toISOString() };
|
|
296
|
+
}
|
|
297
|
+
login(username, password) {
|
|
298
|
+
const validated = this.validateLoginCredentials(username, password);
|
|
248
299
|
return this.database.transaction(() => {
|
|
249
|
-
|
|
250
|
-
this.database.run("
|
|
251
|
-
this.
|
|
252
|
-
|
|
300
|
+
this.database.run("DELETE FROM login_attempts WHERE normalized_username = ?", validated.normalizedUsername);
|
|
301
|
+
this.database.run("UPDATE users SET last_login_at = ?, updated_at = ? WHERE id = ?", validated.loginAt, validated.loginAt, validated.user.userId);
|
|
302
|
+
return this.createSession(validated.user.userId);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
loginDesktop(username, password, input) {
|
|
306
|
+
const validated = this.validateLoginCredentials(username, password);
|
|
307
|
+
return this.database.transaction(() => {
|
|
308
|
+
this.database.run("DELETE FROM login_attempts WHERE normalized_username = ?", validated.normalizedUsername);
|
|
309
|
+
this.database.run("UPDATE users SET last_login_at = ?, updated_at = ? WHERE id = ?", validated.loginAt, validated.loginAt, validated.user.userId);
|
|
310
|
+
return this.createDesktopSession(validated.user.userId, input);
|
|
253
311
|
});
|
|
254
312
|
}
|
|
255
313
|
authenticate(request) {
|
|
256
|
-
const token = parseCookies(request.get("cookie")).get(
|
|
314
|
+
const token = parseCookies(request.get("cookie")).get(SESSION_COOKIE_NAME);
|
|
257
315
|
if (!token)
|
|
258
316
|
return null;
|
|
259
317
|
const row = this.database.get(`SELECT session.id AS session_id, session.csrf_token, session.expires_at, session.revoked_at,
|
|
@@ -266,6 +324,30 @@ export class UserAuthService {
|
|
|
266
324
|
this.database.run("UPDATE user_sessions SET last_seen_at = ? WHERE id = ?", new Date().toISOString(), String(row.session_id));
|
|
267
325
|
return { id: String(row.session_id), csrfToken: String(row.csrf_token), user };
|
|
268
326
|
}
|
|
327
|
+
authenticateDesktop(request) {
|
|
328
|
+
const token = desktopSessionCredential(request);
|
|
329
|
+
if (!token || token.length !== DESKTOP_SESSION_TOKEN_PREFIX.length + 43)
|
|
330
|
+
return null;
|
|
331
|
+
const row = this.database.get(`SELECT session.id AS session_id, session.desktop_id, session.profile_id, session.client_version,
|
|
332
|
+
session.expires_at, session.revoked_at, user.*
|
|
333
|
+
FROM user_desktop_sessions session
|
|
334
|
+
JOIN users user ON user.id = session.user_id
|
|
335
|
+
WHERE session.token_hash = ?`, sha256(token));
|
|
336
|
+
if (!row || row.revoked_at || String(row.expires_at) <= new Date().toISOString())
|
|
337
|
+
return null;
|
|
338
|
+
const user = mapUser(row);
|
|
339
|
+
if (user.status !== "active")
|
|
340
|
+
return null;
|
|
341
|
+
this.database.run("UPDATE user_desktop_sessions SET last_seen_at = ? WHERE id = ?", new Date().toISOString(), String(row.session_id));
|
|
342
|
+
return {
|
|
343
|
+
id: String(row.session_id),
|
|
344
|
+
user,
|
|
345
|
+
desktopId: String(row.desktop_id),
|
|
346
|
+
profileId: String(row.profile_id),
|
|
347
|
+
clientVersion: String(row.client_version),
|
|
348
|
+
expiresAt: String(row.expires_at)
|
|
349
|
+
};
|
|
350
|
+
}
|
|
269
351
|
authenticateApiKey(request) {
|
|
270
352
|
const key = apiKeyCredential(request);
|
|
271
353
|
if (!key || !key.startsWith(apiKeyPrefix) || key.length > 200)
|
|
@@ -283,18 +365,22 @@ export class UserAuthService {
|
|
|
283
365
|
hasApiKeyCredential(request) {
|
|
284
366
|
return apiKeyCredential(request) !== null;
|
|
285
367
|
}
|
|
368
|
+
hasDesktopSessionCredential(request) {
|
|
369
|
+
return desktopSessionCredential(request) !== null;
|
|
370
|
+
}
|
|
286
371
|
getApiKeyStatus(userId) {
|
|
287
372
|
this.getUser(userId);
|
|
288
373
|
const row = this.database.get("SELECT * FROM user_api_keys WHERE user_id = ?", userId);
|
|
289
374
|
if (!row) {
|
|
290
|
-
return { configured: false, prefix: null, createdAt: null, rotatedAt: null, lastUsedAt: null };
|
|
375
|
+
return { configured: false, prefix: null, createdAt: null, rotatedAt: null, lastUsedAt: null, copyable: false };
|
|
291
376
|
}
|
|
292
377
|
return {
|
|
293
378
|
configured: true,
|
|
294
379
|
prefix: String(row.key_prefix),
|
|
295
380
|
createdAt: String(row.created_at),
|
|
296
381
|
rotatedAt: String(row.rotated_at),
|
|
297
|
-
lastUsedAt: row.last_used_at === null ? null : String(row.last_used_at)
|
|
382
|
+
lastUsedAt: row.last_used_at === null ? null : String(row.last_used_at),
|
|
383
|
+
copyable: apiKeyCiphertext(row) !== null
|
|
298
384
|
};
|
|
299
385
|
}
|
|
300
386
|
resetApiKey(userId) {
|
|
@@ -302,18 +388,40 @@ export class UserAuthService {
|
|
|
302
388
|
const apiKey = `${apiKeyPrefix}${randomBytes(32).toString("base64url")}`;
|
|
303
389
|
const prefix = apiKey.slice(0, 13);
|
|
304
390
|
const timestamp = new Date().toISOString();
|
|
305
|
-
this.
|
|
306
|
-
|
|
391
|
+
const encrypted = this.vault.encrypt(apiKey);
|
|
392
|
+
this.database.run(`INSERT INTO user_api_keys (user_id, key_hash, key_prefix, created_at, rotated_at, last_used_at, key_encrypted, key_iv, key_tag)
|
|
393
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
307
394
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
308
395
|
key_hash = excluded.key_hash,
|
|
309
396
|
key_prefix = excluded.key_prefix,
|
|
310
397
|
rotated_at = excluded.rotated_at,
|
|
311
|
-
last_used_at = NULL
|
|
398
|
+
last_used_at = NULL,
|
|
399
|
+
key_encrypted = excluded.key_encrypted,
|
|
400
|
+
key_iv = excluded.key_iv,
|
|
401
|
+
key_tag = excluded.key_tag`, userId, sha256(apiKey), prefix, timestamp, timestamp, encrypted.encrypted, encrypted.iv, encrypted.tag);
|
|
312
402
|
return { ...this.getApiKeyStatus(userId), apiKey };
|
|
313
403
|
}
|
|
404
|
+
revealApiKey(userId) {
|
|
405
|
+
this.getUser(userId);
|
|
406
|
+
const row = this.database.get("SELECT * FROM user_api_keys WHERE user_id = ?", userId);
|
|
407
|
+
if (!row)
|
|
408
|
+
throw new AppError(404, "API_KEY_NOT_CONFIGURED", "尚未生成 API Key");
|
|
409
|
+
const ciphertext = apiKeyCiphertext(row);
|
|
410
|
+
if (!ciphertext)
|
|
411
|
+
throw new AppError(409, "API_KEY_NOT_RECOVERABLE", "当前 API Key 无法复制,请重置后再试");
|
|
412
|
+
try {
|
|
413
|
+
return { apiKey: this.vault.decrypt(ciphertext), prefix: String(row.key_prefix) };
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
throw new AppError(409, "API_KEY_NOT_RECOVERABLE", "当前 API Key 无法复制,请重置后再试");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
314
419
|
revoke(sessionId) {
|
|
315
420
|
this.database.run("UPDATE user_sessions SET revoked_at = ? WHERE id = ?", new Date().toISOString(), sessionId);
|
|
316
421
|
}
|
|
422
|
+
revokeDesktop(sessionId) {
|
|
423
|
+
this.database.run("UPDATE user_desktop_sessions SET revoked_at = ? WHERE id = ?", new Date().toISOString(), sessionId);
|
|
424
|
+
}
|
|
317
425
|
getUser(userId) {
|
|
318
426
|
const row = this.database.get("SELECT * FROM users WHERE id = ?", userId);
|
|
319
427
|
if (!row)
|
|
@@ -365,7 +473,9 @@ export class UserAuthService {
|
|
|
365
473
|
return this.database.transaction(() => {
|
|
366
474
|
this.database.run("UPDATE users SET role = ?, status = ?, updated_at = ? WHERE id = ?", nextRole, nextStatus, new Date().toISOString(), userId);
|
|
367
475
|
if (nextStatus === "disabled") {
|
|
368
|
-
|
|
476
|
+
const revokedAt = new Date().toISOString();
|
|
477
|
+
this.database.run("UPDATE user_sessions SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL", revokedAt, userId);
|
|
478
|
+
this.database.run("UPDATE user_desktop_sessions SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL", revokedAt, userId);
|
|
369
479
|
}
|
|
370
480
|
return this.getUser(userId);
|
|
371
481
|
});
|
|
@@ -417,7 +527,7 @@ export class UserAuthService {
|
|
|
417
527
|
});
|
|
418
528
|
return this.getUser(userId);
|
|
419
529
|
}
|
|
420
|
-
changePassword(userId, sessionId, currentPassword, newPassword) {
|
|
530
|
+
changePassword(userId, sessionId, currentPassword, newPassword, sessionKind = "browser") {
|
|
421
531
|
const row = this.database.get("SELECT * FROM users WHERE id = ?", userId);
|
|
422
532
|
if (!row)
|
|
423
533
|
throw notFound("用户");
|
|
@@ -428,7 +538,10 @@ export class UserAuthService {
|
|
|
428
538
|
const timestamp = new Date().toISOString();
|
|
429
539
|
this.database.transaction(() => {
|
|
430
540
|
this.database.run("UPDATE users SET password_hash = ?, password_salt = ?, updated_at = ? WHERE id = ?", passwordDigest(newPassword, salt), salt, timestamp, userId);
|
|
431
|
-
this.database.run(
|
|
541
|
+
this.database.run(`UPDATE user_sessions SET revoked_at = ?
|
|
542
|
+
WHERE user_id = ? AND (? <> 'browser' OR id <> ?) AND revoked_at IS NULL`, timestamp, userId, sessionKind, sessionId);
|
|
543
|
+
this.database.run(`UPDATE user_desktop_sessions SET revoked_at = ?
|
|
544
|
+
WHERE user_id = ? AND (? <> 'desktop' OR id <> ?) AND revoked_at IS NULL`, timestamp, userId, sessionKind, sessionId);
|
|
432
545
|
this.database.run("DELETE FROM login_attempts WHERE normalized_username = ?", String(row.normalized_username));
|
|
433
546
|
});
|
|
434
547
|
}
|
|
@@ -580,16 +693,16 @@ export class UserAuthService {
|
|
|
580
693
|
}
|
|
581
694
|
}
|
|
582
695
|
export function setSessionCookie(response, token, secure) {
|
|
583
|
-
response.cookie(
|
|
696
|
+
response.cookie(SESSION_COOKIE_NAME, token, {
|
|
584
697
|
httpOnly: true,
|
|
585
698
|
sameSite: "lax",
|
|
586
699
|
secure,
|
|
587
700
|
path: "/",
|
|
588
|
-
maxAge:
|
|
701
|
+
maxAge: SESSION_LIFETIME_MS
|
|
589
702
|
});
|
|
590
703
|
}
|
|
591
704
|
export function clearSessionCookie(response, secure) {
|
|
592
|
-
response.clearCookie(
|
|
705
|
+
response.clearCookie(SESSION_COOKIE_NAME, { httpOnly: true, sameSite: "lax", secure, path: "/" });
|
|
593
706
|
}
|
|
594
707
|
export function createUserSessionMiddleware(auth, disabledOrOptions = false) {
|
|
595
708
|
const options = typeof disabledOrOptions === "boolean"
|
|
@@ -605,18 +718,36 @@ export function createUserSessionMiddleware(auth, disabledOrOptions = false) {
|
|
|
605
718
|
}
|
|
606
719
|
return runWithRequestActor(null, next);
|
|
607
720
|
}
|
|
608
|
-
const
|
|
609
|
-
|
|
721
|
+
const apiKeyProvided = auth.hasApiKeyCredential(request);
|
|
722
|
+
const desktopSessionProvided = auth.hasDesktopSessionCredential(request);
|
|
723
|
+
if (apiKeyProvided && desktopSessionProvided) {
|
|
724
|
+
logger.warn("auth.request.rejected", { reason: "conflicting_credentials", method: request.method, path: sanitizeRequestPath(request.path) });
|
|
725
|
+
response.status(400).json({ error: { code: "AUTH_CREDENTIAL_CONFLICT", message: "请求包含相互冲突的身份凭据" } });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const apiKey = apiKeyProvided ? auth.authenticateApiKey(request) : null;
|
|
729
|
+
if (apiKeyProvided && !apiKey) {
|
|
610
730
|
logger.warn("auth.request.rejected", { reason: "invalid_api_key", method: request.method, path: sanitizeRequestPath(request.path) });
|
|
611
731
|
response.status(401).json({ error: { code: "API_KEY_INVALID", message: "API Key 无效或已失效" } });
|
|
612
732
|
return;
|
|
613
733
|
}
|
|
614
|
-
const
|
|
734
|
+
const desktopSession = desktopSessionProvided ? auth.authenticateDesktop(request) : null;
|
|
735
|
+
if (desktopSessionProvided && !desktopSession) {
|
|
736
|
+
logger.warn("auth.request.rejected", { reason: "invalid_desktop_session", method: request.method, path: sanitizeRequestPath(request.path) });
|
|
737
|
+
response.status(401).json({ error: { code: "DESKTOP_SESSION_INVALID", message: "Desktop 登录已失效,请重新登录" } });
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const session = apiKey || desktopSession ? null : auth.authenticate(request);
|
|
615
741
|
if (apiKey) {
|
|
616
742
|
request.authApiKey = apiKey;
|
|
617
743
|
request.authUser = apiKey.user;
|
|
618
744
|
request.authMethod = "api-key";
|
|
619
745
|
}
|
|
746
|
+
else if (desktopSession) {
|
|
747
|
+
request.authDesktopSession = desktopSession;
|
|
748
|
+
request.authUser = desktopSession.user;
|
|
749
|
+
request.authMethod = "desktop-session";
|
|
750
|
+
}
|
|
620
751
|
else if (session) {
|
|
621
752
|
request.authSession = session;
|
|
622
753
|
request.authUser = session.user;
|
|
@@ -627,8 +758,9 @@ export function createUserSessionMiddleware(auth, disabledOrOptions = false) {
|
|
|
627
758
|
|| path === "/api/auth/session"
|
|
628
759
|
|| (path === "/api/auth/register" && request.method === "POST")
|
|
629
760
|
|| (path === "/api/auth/login" && request.method === "POST")
|
|
761
|
+
|| (path === "/api/desktop/auth/login" && request.method === "POST")
|
|
630
762
|
|| !path.startsWith("/api/");
|
|
631
|
-
if (!session && !apiKey && !isPublic) {
|
|
763
|
+
if (!session && !desktopSession && !apiKey && !isPublic) {
|
|
632
764
|
logger.warn("auth.request.rejected", { reason: "authentication_required", method: request.method, path: sanitizeRequestPath(request.path) });
|
|
633
765
|
response.status(401).json({ error: { code: "AUTH_REQUIRED", message: "请先登录" } });
|
|
634
766
|
return;
|
|
@@ -641,10 +773,11 @@ export function createUserSessionMiddleware(auth, disabledOrOptions = false) {
|
|
|
641
773
|
return;
|
|
642
774
|
}
|
|
643
775
|
}
|
|
644
|
-
const user = apiKey?.user ?? session?.user ?? null;
|
|
645
|
-
|
|
776
|
+
const user = apiKey?.user ?? desktopSession?.user ?? session?.user ?? null;
|
|
777
|
+
const authMethod = apiKey ? "api-key" : desktopSession ? "desktop-session" : "session";
|
|
778
|
+
return runWithRequestActor(user ? { ...user, authentication: authMethod } : null, () => {
|
|
646
779
|
if (user)
|
|
647
|
-
logger.debug("auth.request.authenticated", { authMethod
|
|
780
|
+
logger.debug("auth.request.authenticated", { authMethod });
|
|
648
781
|
next();
|
|
649
782
|
});
|
|
650
783
|
};
|
|
@@ -762,11 +895,37 @@ function globalReplaceWriteModules(request) {
|
|
|
762
895
|
return ["prose", "settings"];
|
|
763
896
|
return [];
|
|
764
897
|
}
|
|
898
|
+
function syncPushWriteModules(request) {
|
|
899
|
+
const mutations = requestBodyRecord(request).mutations;
|
|
900
|
+
if (!Array.isArray(mutations))
|
|
901
|
+
return [];
|
|
902
|
+
const modules = new Set();
|
|
903
|
+
for (const mutation of mutations) {
|
|
904
|
+
if (!mutation || typeof mutation !== "object" || Array.isArray(mutation))
|
|
905
|
+
continue;
|
|
906
|
+
const entityType = mutation.entityType;
|
|
907
|
+
if (entityType === "chapter")
|
|
908
|
+
modules.add("prose");
|
|
909
|
+
if (entityType === "setting")
|
|
910
|
+
modules.add("settings");
|
|
911
|
+
}
|
|
912
|
+
return [...modules];
|
|
913
|
+
}
|
|
765
914
|
function workModuleRequirements(request, write, annotationAccess) {
|
|
766
915
|
const pathname = normalizeApiPath(request.path);
|
|
767
916
|
const direct = (module, extraWrite = []) => (write ? { write: [module, ...extraWrite] } : { read: [module] });
|
|
917
|
+
if (/^\/api\/sync\/works\/[^/]+\/snapshots$/u.test(pathname))
|
|
918
|
+
return { read: ["prose", "settings"] };
|
|
919
|
+
if (/^\/api\/sync\/works\/[^/]+\/changes$/u.test(pathname))
|
|
920
|
+
return { read: ["prose", "settings"] };
|
|
921
|
+
if (/^\/api\/sync\/works\/[^/]+\/push$/u.test(pathname))
|
|
922
|
+
return { write: syncPushWriteModules(request) };
|
|
923
|
+
if (/^\/api\/sync\/works\/[^/]+\/mutations\/[^/]+$/u.test(pathname))
|
|
924
|
+
return { read: ["prose", "settings"] };
|
|
768
925
|
if (/^\/api\/works\/[^/]+$/u.test(pathname))
|
|
769
926
|
return write ? { ownerOnly: true } : {};
|
|
927
|
+
if (/^\/api\/works\/[^/]+\/offline-access$/u.test(pathname))
|
|
928
|
+
return { ownerOnly: true };
|
|
770
929
|
if (/^\/api\/works\/[^/]+\/cover$/u.test(pathname))
|
|
771
930
|
return write ? { ownerOnly: true } : {};
|
|
772
931
|
if (/^\/api\/works\/[^/]+\/members(?:\/[^/]+)?$/u.test(pathname))
|