aifastdb 2.2.2 → 2.2.6

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.
@@ -4,7 +4,7 @@
4
4
  * 提供 RESTful API 端点
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.hasAdminAccess = exports.isAdminRequest = exports.matchAdminRoute = exports.adminRoutes = exports.routes = exports.deleteDatabaseHandler = exports.createDatabaseHandler = exports.getDatabaseInfoHandler = exports.listDatabasesHandler = exports.detailedHealthHandler = exports.healthCheckHandler = exports.readinessHandler = exports.livenessHandler = exports.revokeApiKeyHandler = exports.createApiKeyHandler = exports.queryAuditLogsHandler = exports.listUsersHandler = exports.createUserHandler = exports.collectionStatsHandler = exports.listCollectionsHandler = exports.getHandler = exports.forgetHandler = exports.recallHandler = exports.rememberHandler = exports.refreshTokenHandler = exports.logoutHandler = exports.loginHandler = void 0;
7
+ exports.hasAdminAccess = exports.isAdminRequest = exports.matchAdminRoute = exports.adminRoutes = exports.routes = exports.getRecordActionsHandler = exports.updateProtectionHandler = exports.updateCollaboratorHandler = exports.removeCollaboratorHandler = exports.addCollaboratorHandler = exports.updateVisibilityHandler = exports.federationQueryWithAuthHandler = exports.federationWriteWithAuthHandler = exports.deleteDatabaseHandler = exports.createDatabaseHandler = exports.getDatabaseInfoHandler = exports.listDatabasesHandler = exports.detailedHealthHandler = exports.healthCheckHandler = exports.readinessHandler = exports.livenessHandler = exports.revokeApiKeyHandler = exports.createApiKeyHandler = exports.queryAuditLogsHandler = exports.listUsersHandler = exports.createUserHandler = exports.collectionStatsHandler = exports.listCollectionsHandler = exports.getHandler = exports.forgetHandler = exports.recallHandler = exports.rememberHandler = exports.refreshTokenHandler = exports.logoutHandler = exports.loginHandler = void 0;
8
8
  exports.matchRoute = matchRoute;
9
9
  const intranet_1 = require("../modes/intranet");
10
10
  const production_1 = require("../modes/production");
@@ -1062,6 +1062,554 @@ const deleteDatabaseHandler = async (ctx, security, db, root) => {
1062
1062
  };
1063
1063
  exports.deleteDatabaseHandler = deleteDatabaseHandler;
1064
1064
  // ============================================================================
1065
+ // 联邦存储 — 记录级权限管理路由 (T15.10)
1066
+ // ============================================================================
1067
+ /**
1068
+ * 联邦存储:带鉴权写入
1069
+ * POST /api/v2/federation/stores/:alias/write
1070
+ */
1071
+ const federationWriteWithAuthHandler = async (ctx, security, db, root) => {
1072
+ const federation = root?.federation;
1073
+ if (!federation) {
1074
+ return {
1075
+ status: 500,
1076
+ headers: { 'Content-Type': 'application/json' },
1077
+ body: { error: 'Internal Server Error', message: 'Federation not available' },
1078
+ };
1079
+ }
1080
+ const pathParts = ctx.path.split('/');
1081
+ const storesIdx = pathParts.indexOf('stores');
1082
+ const alias = pathParts[storesIdx + 1];
1083
+ const { op, accessContext } = ctx.body || {};
1084
+ if (!alias || !op) {
1085
+ return {
1086
+ status: 400,
1087
+ headers: { 'Content-Type': 'application/json' },
1088
+ body: { error: 'Bad Request', message: 'Store alias and write operation are required' },
1089
+ };
1090
+ }
1091
+ try {
1092
+ // Build access context from request body or authenticated user
1093
+ const authCtx = accessContext || {
1094
+ caller: ctx.security?.user?.id || 'anonymous',
1095
+ roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
1096
+ storeAlias: alias,
1097
+ systemOverride: false,
1098
+ };
1099
+ const result = federation.writeWithAuth(alias, op, authCtx);
1100
+ return {
1101
+ status: 201,
1102
+ headers: { 'Content-Type': 'application/json' },
1103
+ body: { success: true, data: result },
1104
+ };
1105
+ }
1106
+ catch (error) {
1107
+ const message = error?.message || String(error);
1108
+ const status = message.includes('Permission denied') || message.includes('permission') ? 403 : 400;
1109
+ return {
1110
+ status,
1111
+ headers: { 'Content-Type': 'application/json' },
1112
+ body: { error: status === 403 ? 'Forbidden' : 'Bad Request', message },
1113
+ };
1114
+ }
1115
+ };
1116
+ exports.federationWriteWithAuthHandler = federationWriteWithAuthHandler;
1117
+ /**
1118
+ * 联邦存储:带鉴权查询
1119
+ * POST /api/v2/federation/stores/:alias/query
1120
+ */
1121
+ const federationQueryWithAuthHandler = async (ctx, security, db, root) => {
1122
+ const federation = root?.federation;
1123
+ if (!federation) {
1124
+ return {
1125
+ status: 500,
1126
+ headers: { 'Content-Type': 'application/json' },
1127
+ body: { error: 'Internal Server Error', message: 'Federation not available' },
1128
+ };
1129
+ }
1130
+ const pathParts = ctx.path.split('/');
1131
+ const storesIdx = pathParts.indexOf('stores');
1132
+ const alias = pathParts[storesIdx + 1];
1133
+ const { target, filters, limit, offset, accessContext } = ctx.body || {};
1134
+ if (!alias || !target) {
1135
+ return {
1136
+ status: 400,
1137
+ headers: { 'Content-Type': 'application/json' },
1138
+ body: { error: 'Bad Request', message: 'Store alias and target type are required' },
1139
+ };
1140
+ }
1141
+ try {
1142
+ const authCtx = accessContext || {
1143
+ caller: ctx.security?.user?.id || 'anonymous',
1144
+ roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
1145
+ storeAlias: alias,
1146
+ systemOverride: false,
1147
+ };
1148
+ const result = federation.queryWithAuth(alias, target, authCtx, filters, limit, offset);
1149
+ return {
1150
+ status: 200,
1151
+ headers: { 'Content-Type': 'application/json' },
1152
+ body: { success: true, data: result },
1153
+ };
1154
+ }
1155
+ catch (error) {
1156
+ const message = error?.message || String(error);
1157
+ const status = message.includes('Permission denied') ? 403 : 500;
1158
+ return {
1159
+ status,
1160
+ headers: { 'Content-Type': 'application/json' },
1161
+ body: { error: status === 403 ? 'Forbidden' : 'Internal Server Error', message },
1162
+ };
1163
+ }
1164
+ };
1165
+ exports.federationQueryWithAuthHandler = federationQueryWithAuthHandler;
1166
+ /**
1167
+ * 修改记录可见性
1168
+ * PUT /api/v2/federation/stores/:alias/records/:id/visibility
1169
+ */
1170
+ const updateVisibilityHandler = async (ctx, security, db, root) => {
1171
+ const federation = root?.federation;
1172
+ if (!federation) {
1173
+ return {
1174
+ status: 500,
1175
+ headers: { 'Content-Type': 'application/json' },
1176
+ body: { error: 'Internal Server Error', message: 'Federation not available' },
1177
+ };
1178
+ }
1179
+ const pathParts = ctx.path.split('/');
1180
+ const storesIdx = pathParts.indexOf('stores');
1181
+ const alias = pathParts[storesIdx + 1];
1182
+ const recordsIdx = pathParts.indexOf('records');
1183
+ const recordId = pathParts[recordsIdx + 1];
1184
+ const { visibility, userIds, roles } = ctx.body || {};
1185
+ if (!alias || !recordId || !visibility) {
1186
+ return {
1187
+ status: 400,
1188
+ headers: { 'Content-Type': 'application/json' },
1189
+ body: { error: 'Bad Request', message: 'Store alias, record ID, and visibility are required' },
1190
+ };
1191
+ }
1192
+ try {
1193
+ // Get current record to check ownership
1194
+ const authCtx = {
1195
+ caller: ctx.security?.user?.id || 'anonymous',
1196
+ roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
1197
+ storeAlias: alias,
1198
+ systemOverride: false,
1199
+ };
1200
+ const actions = federation.getRecordActions(alias, recordId, authCtx);
1201
+ if (actions && !actions.canShare) {
1202
+ return {
1203
+ status: 403,
1204
+ headers: { 'Content-Type': 'application/json' },
1205
+ body: { error: 'Forbidden', message: 'Only the owner or admin can modify visibility' },
1206
+ };
1207
+ }
1208
+ // Persist visibility change via NAPI
1209
+ federation.updateVisibility(alias, recordId, {
1210
+ visibilityType: visibility,
1211
+ userIds: userIds || undefined,
1212
+ roles: roles || undefined,
1213
+ }, authCtx);
1214
+ return {
1215
+ status: 200,
1216
+ headers: { 'Content-Type': 'application/json' },
1217
+ body: {
1218
+ success: true,
1219
+ data: {
1220
+ id: recordId,
1221
+ visibility,
1222
+ userIds: userIds || [],
1223
+ roles: roles || [],
1224
+ updatedBy: authCtx.caller,
1225
+ },
1226
+ },
1227
+ };
1228
+ }
1229
+ catch (error) {
1230
+ return {
1231
+ status: 500,
1232
+ headers: { 'Content-Type': 'application/json' },
1233
+ body: { error: 'Internal Server Error', message: error?.message || String(error) },
1234
+ };
1235
+ }
1236
+ };
1237
+ exports.updateVisibilityHandler = updateVisibilityHandler;
1238
+ /**
1239
+ * 添加协作者
1240
+ * POST /api/v2/federation/stores/:alias/records/:id/collaborators
1241
+ */
1242
+ const addCollaboratorHandler = async (ctx, security, db, root) => {
1243
+ const federation = root?.federation;
1244
+ if (!federation) {
1245
+ return {
1246
+ status: 500,
1247
+ headers: { 'Content-Type': 'application/json' },
1248
+ body: { error: 'Internal Server Error', message: 'Federation not available' },
1249
+ };
1250
+ }
1251
+ const pathParts = ctx.path.split('/');
1252
+ const storesIdx = pathParts.indexOf('stores');
1253
+ const alias = pathParts[storesIdx + 1];
1254
+ const recordsIdx = pathParts.indexOf('records');
1255
+ const recordId = pathParts[recordsIdx + 1];
1256
+ const { userId, permissions } = ctx.body || {};
1257
+ if (!alias || !recordId || !userId || !permissions) {
1258
+ return {
1259
+ status: 400,
1260
+ headers: { 'Content-Type': 'application/json' },
1261
+ body: {
1262
+ error: 'Bad Request',
1263
+ message: 'Store alias, record ID, userId, and permissions are required',
1264
+ },
1265
+ };
1266
+ }
1267
+ try {
1268
+ const authCtx = {
1269
+ caller: ctx.security?.user?.id || 'anonymous',
1270
+ roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
1271
+ storeAlias: alias,
1272
+ systemOverride: false,
1273
+ };
1274
+ const actions = federation.getRecordActions(alias, recordId, authCtx);
1275
+ if (actions && !actions.canShare) {
1276
+ return {
1277
+ status: 403,
1278
+ headers: { 'Content-Type': 'application/json' },
1279
+ body: { error: 'Forbidden', message: 'Only the owner or admin can manage collaborators' },
1280
+ };
1281
+ }
1282
+ // Persist collaborator addition via NAPI
1283
+ const grantedAt = Date.now();
1284
+ federation.addCollaborator(alias, recordId, {
1285
+ userId,
1286
+ permissions,
1287
+ grantedAt,
1288
+ grantedBy: authCtx.caller,
1289
+ }, authCtx);
1290
+ return {
1291
+ status: 201,
1292
+ headers: { 'Content-Type': 'application/json' },
1293
+ body: {
1294
+ success: true,
1295
+ data: {
1296
+ id: recordId,
1297
+ collaborator: {
1298
+ userId,
1299
+ permissions,
1300
+ grantedAt,
1301
+ grantedBy: authCtx.caller,
1302
+ },
1303
+ },
1304
+ },
1305
+ };
1306
+ }
1307
+ catch (error) {
1308
+ return {
1309
+ status: 500,
1310
+ headers: { 'Content-Type': 'application/json' },
1311
+ body: { error: 'Internal Server Error', message: error?.message || String(error) },
1312
+ };
1313
+ }
1314
+ };
1315
+ exports.addCollaboratorHandler = addCollaboratorHandler;
1316
+ /**
1317
+ * 移除协作者
1318
+ * DELETE /api/v2/federation/stores/:alias/records/:id/collaborators/:userId
1319
+ */
1320
+ const removeCollaboratorHandler = async (ctx, security, db, root) => {
1321
+ const federation = root?.federation;
1322
+ if (!federation) {
1323
+ return {
1324
+ status: 500,
1325
+ headers: { 'Content-Type': 'application/json' },
1326
+ body: { error: 'Internal Server Error', message: 'Federation not available' },
1327
+ };
1328
+ }
1329
+ const pathParts = ctx.path.split('/');
1330
+ const storesIdx = pathParts.indexOf('stores');
1331
+ const alias = pathParts[storesIdx + 1];
1332
+ const recordsIdx = pathParts.indexOf('records');
1333
+ const recordId = pathParts[recordsIdx + 1];
1334
+ const collaboratorsIdx = pathParts.indexOf('collaborators');
1335
+ const targetUserId = pathParts[collaboratorsIdx + 1];
1336
+ if (!alias || !recordId || !targetUserId) {
1337
+ return {
1338
+ status: 400,
1339
+ headers: { 'Content-Type': 'application/json' },
1340
+ body: { error: 'Bad Request', message: 'Store alias, record ID, and user ID are required' },
1341
+ };
1342
+ }
1343
+ try {
1344
+ const authCtx = {
1345
+ caller: ctx.security?.user?.id || 'anonymous',
1346
+ roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
1347
+ storeAlias: alias,
1348
+ systemOverride: false,
1349
+ };
1350
+ const actions = federation.getRecordActions(alias, recordId, authCtx);
1351
+ if (actions && !actions.canShare) {
1352
+ return {
1353
+ status: 403,
1354
+ headers: { 'Content-Type': 'application/json' },
1355
+ body: { error: 'Forbidden', message: 'Only the owner or admin can manage collaborators' },
1356
+ };
1357
+ }
1358
+ // Persist collaborator removal via NAPI
1359
+ federation.removeCollaborator(alias, recordId, targetUserId, authCtx);
1360
+ return {
1361
+ status: 200,
1362
+ headers: { 'Content-Type': 'application/json' },
1363
+ body: {
1364
+ success: true,
1365
+ data: {
1366
+ id: recordId,
1367
+ removedUserId: targetUserId,
1368
+ removedBy: authCtx.caller,
1369
+ },
1370
+ },
1371
+ };
1372
+ }
1373
+ catch (error) {
1374
+ return {
1375
+ status: 500,
1376
+ headers: { 'Content-Type': 'application/json' },
1377
+ body: { error: 'Internal Server Error', message: error?.message || String(error) },
1378
+ };
1379
+ }
1380
+ };
1381
+ exports.removeCollaboratorHandler = removeCollaboratorHandler;
1382
+ /**
1383
+ * 修改协作者权限
1384
+ * PUT /api/v2/federation/stores/:alias/records/:id/collaborators/:userId
1385
+ *
1386
+ * 更新指定协作者的权限列表。仅记录所有者或管理员可操作。
1387
+ * 如果 permissions 为空数组,等同于移除该协作者。
1388
+ *
1389
+ * Request body: { permissions: string[] }
1390
+ * permissions: ['read', 'write', 'delete', 'share']
1391
+ */
1392
+ const updateCollaboratorHandler = async (ctx, security, db, root) => {
1393
+ const federation = root?.federation;
1394
+ if (!federation) {
1395
+ return {
1396
+ status: 500,
1397
+ headers: { 'Content-Type': 'application/json' },
1398
+ body: { error: 'Internal Server Error', message: 'Federation not available' },
1399
+ };
1400
+ }
1401
+ const pathParts = ctx.path.split('/');
1402
+ const storesIdx = pathParts.indexOf('stores');
1403
+ const alias = pathParts[storesIdx + 1];
1404
+ const recordsIdx = pathParts.indexOf('records');
1405
+ const recordId = pathParts[recordsIdx + 1];
1406
+ const collaboratorsIdx = pathParts.indexOf('collaborators');
1407
+ const targetUserId = pathParts[collaboratorsIdx + 1];
1408
+ const { permissions } = ctx.body || {};
1409
+ if (!alias || !recordId || !targetUserId || !Array.isArray(permissions)) {
1410
+ return {
1411
+ status: 400,
1412
+ headers: { 'Content-Type': 'application/json' },
1413
+ body: {
1414
+ error: 'Bad Request',
1415
+ message: 'Store alias, record ID, user ID, and permissions array are required',
1416
+ },
1417
+ };
1418
+ }
1419
+ // Validate permission values
1420
+ const validPermissions = ['read', 'write', 'delete', 'share'];
1421
+ const invalidPerms = permissions.filter((p) => !validPermissions.includes(p));
1422
+ if (invalidPerms.length > 0) {
1423
+ return {
1424
+ status: 400,
1425
+ headers: { 'Content-Type': 'application/json' },
1426
+ body: {
1427
+ error: 'Bad Request',
1428
+ message: `Invalid permissions: ${invalidPerms.join(', ')}. Valid values: ${validPermissions.join(', ')}`,
1429
+ },
1430
+ };
1431
+ }
1432
+ try {
1433
+ const authCtx = {
1434
+ caller: ctx.security?.user?.id || 'anonymous',
1435
+ roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
1436
+ storeAlias: alias,
1437
+ systemOverride: false,
1438
+ };
1439
+ // Use the NAPI updateCollaborator method which checks share permission internally
1440
+ federation.updateCollaborator(alias, recordId, {
1441
+ userId: targetUserId,
1442
+ permissions,
1443
+ grantedBy: authCtx.caller,
1444
+ }, authCtx);
1445
+ return {
1446
+ status: 200,
1447
+ headers: { 'Content-Type': 'application/json' },
1448
+ body: {
1449
+ success: true,
1450
+ data: {
1451
+ id: recordId,
1452
+ userId: targetUserId,
1453
+ permissions,
1454
+ updatedBy: authCtx.caller,
1455
+ },
1456
+ },
1457
+ };
1458
+ }
1459
+ catch (error) {
1460
+ // Distinguish permission errors from other errors
1461
+ if (error?.message?.includes('Permission denied')) {
1462
+ return {
1463
+ status: 403,
1464
+ headers: { 'Content-Type': 'application/json' },
1465
+ body: { error: 'Forbidden', message: error.message },
1466
+ };
1467
+ }
1468
+ if (error?.message?.includes('not found')) {
1469
+ return {
1470
+ status: 404,
1471
+ headers: { 'Content-Type': 'application/json' },
1472
+ body: { error: 'Not Found', message: error.message },
1473
+ };
1474
+ }
1475
+ return {
1476
+ status: 500,
1477
+ headers: { 'Content-Type': 'application/json' },
1478
+ body: { error: 'Internal Server Error', message: error?.message || String(error) },
1479
+ };
1480
+ }
1481
+ };
1482
+ exports.updateCollaboratorHandler = updateCollaboratorHandler;
1483
+ /**
1484
+ * 修改记录保护级别 (仅 Admin)
1485
+ * PUT /api/v2/federation/stores/:alias/records/:id/protection
1486
+ */
1487
+ const updateProtectionHandler = async (ctx, security, db, root) => {
1488
+ const federation = root?.federation;
1489
+ if (!federation) {
1490
+ return {
1491
+ status: 500,
1492
+ headers: { 'Content-Type': 'application/json' },
1493
+ body: { error: 'Internal Server Error', message: 'Federation not available' },
1494
+ };
1495
+ }
1496
+ const pathParts = ctx.path.split('/');
1497
+ const storesIdx = pathParts.indexOf('stores');
1498
+ const alias = pathParts[storesIdx + 1];
1499
+ const recordsIdx = pathParts.indexOf('records');
1500
+ const recordId = pathParts[recordsIdx + 1];
1501
+ const { protection } = ctx.body || {};
1502
+ if (!alias || !recordId || !protection) {
1503
+ return {
1504
+ status: 400,
1505
+ headers: { 'Content-Type': 'application/json' },
1506
+ body: { error: 'Bad Request', message: 'Store alias, record ID, and protection level are required' },
1507
+ };
1508
+ }
1509
+ // Reject setting protection to "system" via API
1510
+ if (protection === 'system') {
1511
+ return {
1512
+ status: 400,
1513
+ headers: { 'Content-Type': 'application/json' },
1514
+ body: {
1515
+ error: 'PROTECTION_ERROR',
1516
+ message: 'Cannot set protection level to "system" via API. Use system_override internally.',
1517
+ },
1518
+ };
1519
+ }
1520
+ try {
1521
+ const authCtx = {
1522
+ caller: ctx.security?.user?.id || 'anonymous',
1523
+ roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
1524
+ storeAlias: alias,
1525
+ systemOverride: false,
1526
+ };
1527
+ federation.downgradeProtection(alias, recordId, protection, authCtx);
1528
+ return {
1529
+ status: 200,
1530
+ headers: { 'Content-Type': 'application/json' },
1531
+ body: {
1532
+ success: true,
1533
+ data: {
1534
+ id: recordId,
1535
+ protection,
1536
+ updatedBy: authCtx.caller,
1537
+ },
1538
+ },
1539
+ };
1540
+ }
1541
+ catch (error) {
1542
+ const message = error?.message || String(error);
1543
+ const isProtectionError = message.includes('protection') || message.includes('Protection')
1544
+ || message.includes('admin') || message.includes('Admin');
1545
+ return {
1546
+ status: isProtectionError ? 403 : 500,
1547
+ headers: { 'Content-Type': 'application/json' },
1548
+ body: {
1549
+ error: isProtectionError ? 'PROTECTION_ERROR' : 'Internal Server Error',
1550
+ message,
1551
+ },
1552
+ };
1553
+ }
1554
+ };
1555
+ exports.updateProtectionHandler = updateProtectionHandler;
1556
+ /**
1557
+ * 获取记录可用操作
1558
+ * GET /api/v2/federation/stores/:alias/records/:id/actions
1559
+ */
1560
+ const getRecordActionsHandler = async (ctx, security, db, root) => {
1561
+ const federation = root?.federation;
1562
+ if (!federation) {
1563
+ return {
1564
+ status: 500,
1565
+ headers: { 'Content-Type': 'application/json' },
1566
+ body: { error: 'Internal Server Error', message: 'Federation not available' },
1567
+ };
1568
+ }
1569
+ const pathParts = ctx.path.split('/');
1570
+ const storesIdx = pathParts.indexOf('stores');
1571
+ const alias = pathParts[storesIdx + 1];
1572
+ const recordsIdx = pathParts.indexOf('records');
1573
+ const recordId = pathParts[recordsIdx + 1];
1574
+ if (!alias || !recordId) {
1575
+ return {
1576
+ status: 400,
1577
+ headers: { 'Content-Type': 'application/json' },
1578
+ body: { error: 'Bad Request', message: 'Store alias and record ID are required' },
1579
+ };
1580
+ }
1581
+ try {
1582
+ const authCtx = {
1583
+ caller: ctx.security?.user?.id || 'anonymous',
1584
+ roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
1585
+ storeAlias: alias,
1586
+ systemOverride: false,
1587
+ };
1588
+ const actions = federation.getRecordActions(alias, recordId, authCtx);
1589
+ return {
1590
+ status: 200,
1591
+ headers: { 'Content-Type': 'application/json' },
1592
+ body: { success: true, data: { actions } },
1593
+ };
1594
+ }
1595
+ catch (error) {
1596
+ const message = error?.message || String(error);
1597
+ if (message.includes('not found')) {
1598
+ return {
1599
+ status: 404,
1600
+ headers: { 'Content-Type': 'application/json' },
1601
+ body: { error: 'Not Found', message },
1602
+ };
1603
+ }
1604
+ return {
1605
+ status: 500,
1606
+ headers: { 'Content-Type': 'application/json' },
1607
+ body: { error: 'Internal Server Error', message },
1608
+ };
1609
+ }
1610
+ };
1611
+ exports.getRecordActionsHandler = getRecordActionsHandler;
1612
+ // ============================================================================
1065
1613
  // 路由表
1066
1614
  // ============================================================================
1067
1615
  /**
@@ -1098,6 +1646,25 @@ exports.routes = [
1098
1646
  { method: 'GET', path: '/api/v1/databases/:name', handler: exports.getDatabaseInfoHandler, requireAuth: true, permissions: ['read'] },
1099
1647
  { method: 'POST', path: '/api/v1/databases', handler: exports.createDatabaseHandler, requireAuth: true, permissions: ['write'] },
1100
1648
  { method: 'DELETE', path: '/api/v1/databases/:name', handler: exports.deleteDatabaseHandler, requireAuth: true, permissions: ['delete'] },
1649
+ // ================================================================
1650
+ // 联邦存储 v2 — 记录级权限管理路由 (T15.10)
1651
+ // ================================================================
1652
+ // 联邦带鉴权写入
1653
+ { method: 'POST', path: '/api/v2/federation/stores/:alias/write', handler: exports.federationWriteWithAuthHandler, requireAuth: true, permissions: ['write'] },
1654
+ // 联邦带鉴权查询
1655
+ { method: 'POST', path: '/api/v2/federation/stores/:alias/query', handler: exports.federationQueryWithAuthHandler, requireAuth: true, permissions: ['read'] },
1656
+ // 修改记录可见性
1657
+ { method: 'PUT', path: '/api/v2/federation/stores/:alias/records/:id/visibility', handler: exports.updateVisibilityHandler, requireAuth: true, permissions: ['write'] },
1658
+ // 添加协作者
1659
+ { method: 'POST', path: '/api/v2/federation/stores/:alias/records/:id/collaborators', handler: exports.addCollaboratorHandler, requireAuth: true, permissions: ['write'] },
1660
+ // 移除协作者
1661
+ { method: 'DELETE', path: '/api/v2/federation/stores/:alias/records/:id/collaborators/:userId', handler: exports.removeCollaboratorHandler, requireAuth: true, permissions: ['write'] },
1662
+ // 修改协作者权限
1663
+ { method: 'PUT', path: '/api/v2/federation/stores/:alias/records/:id/collaborators/:userId', handler: exports.updateCollaboratorHandler, requireAuth: true, permissions: ['write'] },
1664
+ // 修改记录保护级别
1665
+ { method: 'PUT', path: '/api/v2/federation/stores/:alias/records/:id/protection', handler: exports.updateProtectionHandler, requireAuth: true, permissions: ['write'] },
1666
+ // 获取记录可用操作
1667
+ { method: 'GET', path: '/api/v2/federation/stores/:alias/records/:id/actions', handler: exports.getRecordActionsHandler, requireAuth: true, permissions: ['read'] },
1101
1668
  ];
1102
1669
  /**
1103
1670
  * 路由匹配器