@musnows/scriverse 0.3.9 → 0.3.10

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/dist/app.js CHANGED
@@ -26,6 +26,7 @@ import { createRequestLoggingMiddleware, sanitizeRequestPath } from "./http-logg
26
26
  import { accountReference, logger, sanitizeError } from "./logger.js";
27
27
  import { runWithRequestActor } from "./request-context.js";
28
28
  import { APP_VERSION } from "./version.js";
29
+ import { fullWorkModulePermissions } from "./work-permissions.js";
29
30
  import { clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, setSessionCookie, UserAuthService } from "./user-auth.js";
30
31
  const nonEmpty = z.string().trim().min(1);
31
32
  const identifier = z.string().trim().min(1).max(200);
@@ -55,8 +56,29 @@ const loginSchema = z.object({
55
56
  ...captchaFields
56
57
  }).strict();
57
58
  const userUpdateSchema = z.object({ role: z.enum(["admin", "user"]).optional(), status: z.enum(["active", "disabled"]).optional() }).strict();
58
- const memberSchema = z.object({ userId: identifier, role: z.enum(["editor", "settings-editor", "viewer"]) }).strict();
59
- const memberRoleSchema = z.object({ role: z.enum(["editor", "settings-editor", "viewer"]) }).strict();
59
+ const memberRoleValueSchema = z.enum(["editor", "settings-editor", "viewer"]);
60
+ const moduleAccessSchema = z.enum(["none", "read", "write"]);
61
+ const modulePermissionsSchema = z.object({
62
+ prose: moduleAccessSchema,
63
+ settings: moduleAccessSchema,
64
+ characters: moduleAccessSchema,
65
+ races: moduleAccessSchema,
66
+ organizations: moduleAccessSchema,
67
+ timeline: moduleAccessSchema,
68
+ relationships: moduleAccessSchema,
69
+ outlines: moduleAccessSchema,
70
+ reviews: moduleAccessSchema,
71
+ ai: moduleAccessSchema,
72
+ "ai-settings": moduleAccessSchema
73
+ }).strict();
74
+ const memberSchema = z.union([
75
+ z.object({ userId: identifier, permissions: modulePermissionsSchema }).strict(),
76
+ z.object({ userId: identifier, role: memberRoleValueSchema }).strict()
77
+ ]);
78
+ const memberPermissionSchema = z.union([
79
+ z.object({ permissions: modulePermissionsSchema }).strict(),
80
+ z.object({ role: memberRoleValueSchema }).strict()
81
+ ]);
60
82
  const profileSchema = z.object({ displayName: z.string().trim().min(1).max(80) }).strict();
61
83
  const passwordChangeSchema = z.object({ currentPassword: z.string().max(200), newPassword: passwordSchema }).strict();
62
84
  const changeNoteSchema = z.string().trim().max(500).optional();
@@ -303,6 +325,55 @@ function noContent(response) {
303
325
  function parse(schema, value) {
304
326
  return schema.parse(value);
305
327
  }
328
+ function recordValue(value) {
329
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
330
+ }
331
+ function mapRecords(value, mapper) {
332
+ if (Array.isArray(value))
333
+ return value.map((item) => recordValue(item) ? mapper(item) : item);
334
+ const record = recordValue(value);
335
+ if (!record)
336
+ return value;
337
+ if (Array.isArray(record.items)) {
338
+ return { ...record, items: record.items.map((item) => recordValue(item) ? mapper(item) : item) };
339
+ }
340
+ return mapper(record);
341
+ }
342
+ function redactCharacterLinks(record, permissions) {
343
+ const result = { ...record };
344
+ if (permissions.races === "none") {
345
+ result.raceId = null;
346
+ result.race = null;
347
+ result.species = "";
348
+ }
349
+ if (permissions.organizations === "none") {
350
+ result.organizationIds = [];
351
+ result.organizations = [];
352
+ }
353
+ return result;
354
+ }
355
+ function redactRaceMembers(record, permissions) {
356
+ return permissions.characters === "none" ? { ...record, memberIds: [], members: [] } : record;
357
+ }
358
+ function redactOrganizationMembers(record, permissions) {
359
+ return permissions.characters === "none" ? { ...record, memberIds: [], members: [] } : record;
360
+ }
361
+ function redactMergeRecords(value, mapper) {
362
+ const record = recordValue(value);
363
+ if (!record)
364
+ return value;
365
+ return {
366
+ ...record,
367
+ ...(recordValue(record.target) ? { target: mapper(record.target) } : {}),
368
+ ...(recordValue(record.source) ? { source: mapper(record.source) } : {})
369
+ };
370
+ }
371
+ function redactVersionSnapshots(value, mapper) {
372
+ return mapRecords(value, (version) => {
373
+ const snapshot = recordValue(version.snapshot);
374
+ return snapshot ? { ...version, snapshot: mapper(snapshot) } : version;
375
+ });
376
+ }
306
377
  export function createRuntime(options) {
307
378
  logger.info("runtime.initializing", {
308
379
  databasePath: options.databasePath,
@@ -319,6 +390,14 @@ export function createRuntime(options) {
319
390
  mkdirSync(attachmentStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
320
391
  const auth = new UserAuthService(database);
321
392
  const store = new Store(database);
393
+ const requestPermissions = (request, workId) => {
394
+ if (!request.authUser)
395
+ return fullWorkModulePermissions();
396
+ const resolvedWorkId = workId ?? auth.resolveWorkId(request.path) ?? undefined;
397
+ if (!resolvedWorkId)
398
+ return fullWorkModulePermissions();
399
+ return auth.workModulePermissions(request.authUser, resolvedWorkId, request.authMethod !== "api-key") ?? fullWorkModulePermissions();
400
+ };
322
401
  const captcha = new ImageCaptchaService({ revealAnswer: options.revealCaptchaAnswer === true });
323
402
  const ai = new AiManager(store, new CredentialVault(options.masterSecret), options.fetchImpl ?? fetch, options.security ? (url) => assertSafeAiEndpoint(url, options.security?.allowPrivateAiEndpoints) : undefined);
324
403
  const app = express();
@@ -536,19 +615,34 @@ export function createRuntime(options) {
536
615
  if (!request.authUser)
537
616
  throw new AppError(401, "AUTH_REQUIRED", "请先登录");
538
617
  const input = parse(memberSchema, request.body);
539
- const members = auth.addMember(request.params.workId, input.userId, input.role, request.authUser.userId);
540
- store.audit(request.params.workId, "work.member-added", "user", input.userId, { role: input.role });
618
+ const permissionInput = "permissions" in input
619
+ ? { permissions: input.permissions }
620
+ : { role: input.role };
621
+ const members = database.transaction(() => {
622
+ const result = auth.addMember(request.params.workId, input.userId, permissionInput, request.authUser.userId);
623
+ store.audit(request.params.workId, "work.member-added", "user", input.userId, permissionInput);
624
+ return result;
625
+ });
541
626
  data(response, members, 201);
542
627
  });
543
628
  app.patch("/api/works/:workId/members/:userId", (request, response) => {
544
- const input = parse(memberRoleSchema, request.body);
545
- const members = auth.updateMemberRole(request.params.workId, request.params.userId, input.role);
546
- store.audit(request.params.workId, "work.member-role-updated", "user", request.params.userId, { role: input.role });
629
+ const input = parse(memberPermissionSchema, request.body);
630
+ const permissionInput = "permissions" in input
631
+ ? { permissions: input.permissions }
632
+ : { role: input.role };
633
+ const members = database.transaction(() => {
634
+ const result = auth.updateMemberPermissions(request.params.workId, request.params.userId, permissionInput);
635
+ store.audit(request.params.workId, "work.member-role-updated", "user", request.params.userId, permissionInput);
636
+ return result;
637
+ });
547
638
  data(response, members);
548
639
  });
549
640
  app.delete("/api/works/:workId/members/:userId", (request, response) => {
550
- const members = auth.removeMember(request.params.workId, request.params.userId);
551
- store.audit(request.params.workId, "work.member-removed", "user", request.params.userId);
641
+ const members = database.transaction(() => {
642
+ const result = auth.removeMember(request.params.workId, request.params.userId);
643
+ store.audit(request.params.workId, "work.member-removed", "user", request.params.userId);
644
+ return result;
645
+ });
552
646
  data(response, members);
553
647
  });
554
648
  app.patch("/api/works/:workId", (request, response) => {
@@ -731,23 +825,34 @@ export function createRuntime(options) {
731
825
  includeMerged: z.enum(["0", "1"]).default("0")
732
826
  }), request.query);
733
827
  const pagination = parsePagination(request.query);
734
- data(response, pagination
828
+ const permissions = requestPermissions(request, request.params.workId);
829
+ const characters = pagination
735
830
  ? store.listCharactersPage(request.params.workId, pagination, includeSections === "true", includeMerged === "1")
736
- : store.listCharacters(request.params.workId, includeSections === "true", includeMerged === "1"));
831
+ : store.listCharacters(request.params.workId, includeSections === "true", includeMerged === "1");
832
+ data(response, mapRecords(characters, (character) => redactCharacterLinks(character, permissions)));
833
+ });
834
+ app.post("/api/works/:workId/characters", (request, response) => {
835
+ const character = store.createCharacter(request.params.workId, parse(characterSchema, request.body));
836
+ data(response, redactCharacterLinks(character, requestPermissions(request, request.params.workId)), 201);
837
+ });
838
+ app.get("/api/characters/:characterId", (request, response) => {
839
+ data(response, redactCharacterLinks(store.getCharacter(request.params.characterId), requestPermissions(request)));
737
840
  });
738
- app.post("/api/works/:workId/characters", (request, response) => data(response, store.createCharacter(request.params.workId, parse(characterSchema, request.body)), 201));
739
- app.get("/api/characters/:characterId", (request, response) => data(response, store.getCharacter(request.params.characterId)));
740
841
  app.patch("/api/characters/:characterId", (request, response) => {
741
842
  const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
742
- data(response, store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo));
843
+ const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
844
+ data(response, redactCharacterLinks(character, requestPermissions(request)));
743
845
  });
744
846
  app.get("/api/characters/:characterId/versions", (request, response) => {
745
847
  const pagination = parsePagination(request.query);
746
- data(response, pagination ? store.listCharacterVersionsPage(request.params.characterId, pagination) : store.listCharacterVersions(request.params.characterId));
848
+ const versions = pagination ? store.listCharacterVersionsPage(request.params.characterId, pagination) : store.listCharacterVersions(request.params.characterId);
849
+ const permissions = requestPermissions(request);
850
+ data(response, redactVersionSnapshots(versions, (snapshot) => redactCharacterLinks(snapshot, permissions)));
747
851
  });
748
852
  app.post("/api/characters/:characterId/restore", (request, response) => {
749
853
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
750
- data(response, store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo));
854
+ const character = store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo);
855
+ data(response, redactCharacterLinks(character, requestPermissions(request)));
751
856
  });
752
857
  app.delete("/api/characters/:characterId", (request, response) => {
753
858
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
@@ -760,11 +865,13 @@ export function createRuntime(options) {
760
865
  expectedTargetVersionNo: z.number().int().positive(),
761
866
  expectedSourceVersionNo: z.number().int().positive()
762
867
  }).strict(), request.body);
763
- data(response, store.mergeCharacters({
868
+ const result = store.mergeCharacters({
764
869
  reviewId: null,
765
870
  sourceCharacterId: request.params.characterId,
766
871
  ...input
767
- }));
872
+ });
873
+ const permissions = requestPermissions(request);
874
+ data(response, redactMergeRecords(result, (character) => redactCharacterLinks(character, permissions)));
768
875
  });
769
876
  app.get("/api/characters/:characterId/sections", (request, response) => {
770
877
  const pagination = parsePagination(request.query);
@@ -844,15 +951,21 @@ export function createRuntime(options) {
844
951
  });
845
952
  app.get("/api/works/:workId/races", (request, response) => {
846
953
  const pagination = parsePagination(request.query);
847
- data(response, pagination ? store.listRacesPage(request.params.workId, pagination) : store.listRaces(request.params.workId));
954
+ const races = pagination ? store.listRacesPage(request.params.workId, pagination) : store.listRaces(request.params.workId);
955
+ const permissions = requestPermissions(request, request.params.workId);
956
+ data(response, mapRecords(races, (race) => redactRaceMembers(race, permissions)));
848
957
  });
849
958
  app.post("/api/works/:workId/races", (request, response) => {
850
- data(response, store.createRace(request.params.workId, parse(raceSchema, request.body)), 201);
959
+ const race = store.createRace(request.params.workId, parse(raceSchema, request.body));
960
+ data(response, redactRaceMembers(race, requestPermissions(request, request.params.workId)), 201);
961
+ });
962
+ app.get("/api/races/:raceId", (request, response) => {
963
+ data(response, redactRaceMembers(store.getRace(request.params.raceId), requestPermissions(request)));
851
964
  });
852
- app.get("/api/races/:raceId", (request, response) => data(response, store.getRace(request.params.raceId)));
853
965
  app.patch("/api/races/:raceId", (request, response) => {
854
966
  const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
855
- data(response, store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo));
967
+ const race = store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo);
968
+ data(response, redactRaceMembers(race, requestPermissions(request)));
856
969
  });
857
970
  app.delete("/api/races/:raceId", (request, response) => {
858
971
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
@@ -861,19 +974,27 @@ export function createRuntime(options) {
861
974
  });
862
975
  app.post("/api/races/:raceId/merge", (request, response) => {
863
976
  const input = parse(z.object({ targetRaceId: identifier }).strict(), request.body);
864
- data(response, store.mergeRaces(request.params.raceId, input.targetRaceId));
977
+ const result = store.mergeRaces(request.params.raceId, input.targetRaceId);
978
+ const permissions = requestPermissions(request);
979
+ data(response, redactMergeRecords(result, (race) => redactRaceMembers(race, permissions)));
865
980
  });
866
981
  app.get("/api/works/:workId/organizations", (request, response) => {
867
982
  const pagination = parsePagination(request.query);
868
- data(response, pagination ? store.listOrganizationsPage(request.params.workId, pagination) : store.listOrganizations(request.params.workId));
983
+ const organizations = pagination ? store.listOrganizationsPage(request.params.workId, pagination) : store.listOrganizations(request.params.workId);
984
+ const permissions = requestPermissions(request, request.params.workId);
985
+ data(response, mapRecords(organizations, (organization) => redactOrganizationMembers(organization, permissions)));
869
986
  });
870
987
  app.post("/api/works/:workId/organizations", (request, response) => {
871
- data(response, store.createOrganization(request.params.workId, parse(organizationSchema, request.body)), 201);
988
+ const organization = store.createOrganization(request.params.workId, parse(organizationSchema, request.body));
989
+ data(response, redactOrganizationMembers(organization, requestPermissions(request, request.params.workId)), 201);
990
+ });
991
+ app.get("/api/organizations/:organizationId", (request, response) => {
992
+ data(response, redactOrganizationMembers(store.getOrganization(request.params.organizationId), requestPermissions(request)));
872
993
  });
873
- app.get("/api/organizations/:organizationId", (request, response) => data(response, store.getOrganization(request.params.organizationId)));
874
994
  app.patch("/api/organizations/:organizationId", (request, response) => {
875
995
  const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
876
- data(response, store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo));
996
+ const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
997
+ data(response, redactOrganizationMembers(organization, requestPermissions(request)));
877
998
  });
878
999
  app.delete("/api/organizations/:organizationId", (request, response) => {
879
1000
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
@@ -882,7 +1003,9 @@ export function createRuntime(options) {
882
1003
  });
883
1004
  app.post("/api/organizations/:organizationId/merge", (request, response) => {
884
1005
  const input = parse(z.object({ targetOrganizationId: identifier }).strict(), request.body);
885
- data(response, store.mergeOrganizations(request.params.organizationId, input.targetOrganizationId));
1006
+ const result = store.mergeOrganizations(request.params.organizationId, input.targetOrganizationId);
1007
+ const permissions = requestPermissions(request);
1008
+ data(response, redactMergeRecords(result, (organization) => redactOrganizationMembers(organization, permissions)));
886
1009
  });
887
1010
  app.get("/api/works/:workId/timeline-tracks", (request, response) => {
888
1011
  const pagination = parsePagination(request.query);
@@ -961,9 +1084,19 @@ export function createRuntime(options) {
961
1084
  app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
962
1085
  const input = parse(z.object({ entityType: versionedEntityTypeSchema, entityId: identifier }), request.params);
963
1086
  const pagination = parsePagination(request.query);
964
- data(response, pagination
1087
+ const versions = pagination
965
1088
  ? store.listEntityVersionsPage(input.entityType, input.entityId, pagination)
966
- : store.listEntityVersions(input.entityType, input.entityId));
1089
+ : store.listEntityVersions(input.entityType, input.entityId);
1090
+ const permissions = requestPermissions(request);
1091
+ if (input.entityType === "race") {
1092
+ data(response, redactVersionSnapshots(versions, (snapshot) => redactRaceMembers(snapshot, permissions)));
1093
+ return;
1094
+ }
1095
+ if (input.entityType === "organization") {
1096
+ data(response, redactVersionSnapshots(versions, (snapshot) => redactOrganizationMembers(snapshot, permissions)));
1097
+ return;
1098
+ }
1099
+ data(response, versions);
967
1100
  });
968
1101
  app.post("/api/entity-versions/:entityType/:entityId/restore", (request, response) => {
969
1102
  const params = parse(z.object({ entityType: versionedEntityTypeSchema, entityId: identifier }), request.params);