@codingfactory/socialkit-vue 0.7.8 → 0.7.11

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.
@@ -287,6 +287,19 @@ export interface CircleModerationReport {
287
287
  resolution_notes?: string | null
288
288
  }
289
289
 
290
+ export interface CircleModerationReportSubmissionInput {
291
+ category: string
292
+ notes?: string | null
293
+ subject_user_id?: string | null
294
+ subject_type?: CircleModerationReportSubjectType
295
+ subject_id?: string | null
296
+ }
297
+
298
+ export interface CircleModerationReportSubmissionResult {
299
+ report_id: string
300
+ status: string
301
+ }
302
+
290
303
  export interface CircleBanRecord {
291
304
  id: string
292
305
  user_id: string
@@ -851,6 +864,39 @@ function buildActorContext(circle: Circle): CircleActorContext {
851
864
  }
852
865
  }
853
866
 
867
+ function resolveBootstrapActorRole(
868
+ actorCapabilities: UnknownRecord | null,
869
+ fallbackRole: CircleRole | null,
870
+ ): CircleRole | null {
871
+ if (actorCapabilities === null) {
872
+ return fallbackRole
873
+ }
874
+
875
+ const displayRole = readString(actorCapabilities.display_role)
876
+ if (isCircleRole(displayRole)) {
877
+ return displayRole
878
+ }
879
+
880
+ const moderationLevel = readString(actorCapabilities.moderation_level)
881
+ if (isCircleRole(moderationLevel)) {
882
+ return moderationLevel
883
+ }
884
+
885
+ return fallbackRole
886
+ }
887
+
888
+ function circleUserRole(circle: Circle | null | undefined): CircleRole | null {
889
+ if (circle === null || circle === undefined) {
890
+ return null
891
+ }
892
+
893
+ return isCircleRole(circle.user_role) ? circle.user_role : null
894
+ }
895
+
896
+ function hasValue(value: unknown): boolean {
897
+ return value !== undefined && value !== null
898
+ }
899
+
854
900
  function normalizeCollectionResponse<T>(
855
901
  payload: unknown,
856
902
  normalizeItem: (value: unknown) => T | null,
@@ -896,6 +942,12 @@ function normalizeInlineCollection<T>(
896
942
  }
897
943
  }
898
944
 
945
+ function normalizeBootstrapRoleCollection(value: unknown): PaginationResponse<CircleRoleDefinition> {
946
+ return Array.isArray(value)
947
+ ? normalizeInlineCollection(value, normalizeRoleDefinition)
948
+ : normalizeCollectionResponse(value ?? {}, normalizeRoleDefinition)
949
+ }
950
+
899
951
  function normalizeCircle(value: unknown): Circle | null {
900
952
  if (!isRecord(value)) {
901
953
  return null
@@ -1685,6 +1737,73 @@ class CirclesService {
1685
1737
  return matchedCircle ? normalizeCircle(matchedCircle) : null
1686
1738
  }
1687
1739
 
1740
+ private normalizeModerationReportSubmissionInput(
1741
+ subjectOrInput: string | CircleModerationReportSubmissionInput,
1742
+ category?: string,
1743
+ notes?: string,
1744
+ ): CircleModerationReportSubmissionInput {
1745
+ if (typeof subjectOrInput !== 'string') {
1746
+ return subjectOrInput
1747
+ }
1748
+
1749
+ const input: CircleModerationReportSubmissionInput = {
1750
+ category: category ?? '',
1751
+ subject_user_id: subjectOrInput,
1752
+ subject_type: 'user',
1753
+ subject_id: subjectOrInput,
1754
+ }
1755
+
1756
+ if (typeof notes === 'string') {
1757
+ input.notes = notes
1758
+ }
1759
+
1760
+ return input
1761
+ }
1762
+
1763
+ private buildModerationReportPayload(input: CircleModerationReportSubmissionInput): Record<string, string> {
1764
+ const category = this.sanitizeInput(input.category)
1765
+ if (category.length === 0) {
1766
+ throw new Error('Moderation report category is required.')
1767
+ }
1768
+
1769
+ const subjectUserId = typeof input.subject_user_id === 'string' && input.subject_user_id.trim().length > 0
1770
+ ? this.sanitizeInput(input.subject_user_id)
1771
+ : null
1772
+ const subjectType = input.subject_type ?? (subjectUserId !== null ? 'user' : undefined)
1773
+ const derivedSubjectId = typeof input.subject_id === 'string' && input.subject_id.trim().length > 0
1774
+ ? this.sanitizeInput(input.subject_id)
1775
+ : subjectType === 'user'
1776
+ ? subjectUserId
1777
+ : null
1778
+
1779
+ if (subjectType !== undefined && derivedSubjectId === null) {
1780
+ throw new Error('Moderation report subject_id is required when subject_type is provided.')
1781
+ }
1782
+
1783
+ if (subjectType === undefined && subjectUserId === null) {
1784
+ throw new Error('Moderation report subject details are required.')
1785
+ }
1786
+
1787
+ const payload: Record<string, string> = {
1788
+ category,
1789
+ }
1790
+
1791
+ if (subjectUserId !== null) {
1792
+ payload.subject_user_id = subjectUserId
1793
+ }
1794
+
1795
+ if (subjectType !== undefined && derivedSubjectId !== null) {
1796
+ payload.subject_type = subjectType
1797
+ payload.subject_id = derivedSubjectId
1798
+ }
1799
+
1800
+ if (typeof input.notes === 'string' && input.notes.trim().length > 0) {
1801
+ payload.notes = this.sanitizeInput(input.notes)
1802
+ }
1803
+
1804
+ return payload
1805
+ }
1806
+
1688
1807
  public async list(cursor?: string | null, filters?: CircleFilters): Promise<PaginationResponse<Circle>> {
1689
1808
  try {
1690
1809
  const params: Record<string, string> = {
@@ -2236,21 +2355,12 @@ class CirclesService {
2236
2355
  }
2237
2356
  }
2238
2357
 
2239
- public async reportMember(
2358
+ public async submitModerationReport(
2240
2359
  circleId: string,
2241
- subjectUserId: string,
2242
- category: string,
2243
- notes?: string,
2244
- ): Promise<{ report_id: string; status: string }> {
2360
+ input: CircleModerationReportSubmissionInput,
2361
+ ): Promise<CircleModerationReportSubmissionResult> {
2245
2362
  try {
2246
- const payload: Record<string, string> = {
2247
- subject_user_id: subjectUserId,
2248
- category: this.sanitizeInput(category),
2249
- }
2250
- if (typeof notes === 'string' && notes.trim().length > 0) {
2251
- payload.notes = this.sanitizeInput(notes)
2252
- }
2253
-
2363
+ const payload = this.buildModerationReportPayload(input)
2254
2364
  const response = await this.client.post(`${this.baseURL}/${circleId}/moderation/reports`, payload)
2255
2365
  const data = readRecord(response.data, 'data') ?? {}
2256
2366
  const reportId = readString(data.report_id)
@@ -2274,6 +2384,28 @@ class CirclesService {
2274
2384
  }
2275
2385
  }
2276
2386
 
2387
+ public async reportMember(
2388
+ circleId: string,
2389
+ subjectUserId: string,
2390
+ category: string,
2391
+ notes?: string,
2392
+ ): Promise<CircleModerationReportSubmissionResult>
2393
+ public async reportMember(
2394
+ circleId: string,
2395
+ input: CircleModerationReportSubmissionInput,
2396
+ ): Promise<CircleModerationReportSubmissionResult>
2397
+ public async reportMember(
2398
+ circleId: string,
2399
+ subjectOrInput: string | CircleModerationReportSubmissionInput,
2400
+ category?: string,
2401
+ notes?: string,
2402
+ ): Promise<CircleModerationReportSubmissionResult> {
2403
+ return await this.submitModerationReport(
2404
+ circleId,
2405
+ this.normalizeModerationReportSubmissionInput(subjectOrInput, category, notes),
2406
+ )
2407
+ }
2408
+
2277
2409
  public async listModerationReports(
2278
2410
  circleId: string,
2279
2411
  cursor?: string | null,
@@ -2479,6 +2611,10 @@ class CirclesService {
2479
2611
  public async getManagementBootstrap(circleId: string): Promise<CircleManagementBootstrap> {
2480
2612
  const identifier = this.sanitizeInput(circleId)
2481
2613
  let resolvedCircle: Circle | null = null
2614
+ let bootstrapPermissionCatalog = createEmptyPagination<CirclePermissionCatalogEntry>().data
2615
+ let bootstrapManagementSections: CircleManagementSection[] = []
2616
+ let bootstrapCounts = createEmptyManagementCounts()
2617
+ let hasBootstrapCounts = false
2482
2618
  const bootstrapIdentifier = this.isUuid(identifier)
2483
2619
  ? identifier
2484
2620
  : await (async (): Promise<string> => {
@@ -2490,12 +2626,24 @@ class CirclesService {
2490
2626
  const response = await this.client.get(`${this.baseURL}/${bootstrapIdentifier}/management/bootstrap`)
2491
2627
  const data = readRecord(response.data, 'data')
2492
2628
  const bootstrapCircle = data?.circle
2493
- const bootstrapManagementSections = normalizeManagementSections(data?.management_sections)
2629
+ const bootstrapActorCapabilities = readRecord(data, 'actor_capabilities')
2630
+ bootstrapManagementSections = normalizeManagementSections(data?.management_sections)
2631
+ bootstrapPermissionCatalog = Array.isArray(data?.permission_catalog)
2632
+ ? data.permission_catalog
2633
+ .map((entry) => normalizePermissionCatalogEntry(entry))
2634
+ .filter((entry): entry is CirclePermissionCatalogEntry => entry !== null)
2635
+ : []
2636
+ hasBootstrapCounts = hasValue(data?.counts)
2637
+ bootstrapCounts = normalizeManagementCounts(data?.counts)
2494
2638
  const directCircle = normalizeCircle(bootstrapCircle ?? data)
2639
+ const directCircleRole = circleUserRole(directCircle)
2640
+ const fallbackRole = directCircleRole ?? circleUserRole(resolvedCircle)
2641
+ const bootstrapActorRole = resolveBootstrapActorRole(bootstrapActorCapabilities, fallbackRole)
2495
2642
  const mergedCircleSource = directCircle === null
2496
2643
  ? {
2497
2644
  ...(resolvedCircle ?? await this.get(bootstrapIdentifier)),
2498
2645
  ...(bootstrapCircle ?? {}),
2646
+ ...(bootstrapActorRole !== null ? { user_role: bootstrapActorRole } : {}),
2499
2647
  actor_capabilities: data?.actor_capabilities,
2500
2648
  management_sections: bootstrapManagementSections.length > 0
2501
2649
  ? bootstrapManagementSections
@@ -2503,6 +2651,7 @@ class CirclesService {
2503
2651
  }
2504
2652
  : {
2505
2653
  ...directCircle,
2654
+ ...(bootstrapActorRole !== null ? { user_role: bootstrapActorRole } : {}),
2506
2655
  actor_capabilities: data?.actor_capabilities ?? directCircle.actor_capabilities,
2507
2656
  management_sections: bootstrapManagementSections.length > 0
2508
2657
  ? bootstrapManagementSections
@@ -2511,38 +2660,100 @@ class CirclesService {
2511
2660
  const circle = normalizeCircle(mergedCircleSource)
2512
2661
 
2513
2662
  if (circle !== null) {
2514
- const bootstrapHasCollections = data?.members !== undefined && data?.members !== null
2515
-
2516
- if (bootstrapHasCollections) {
2517
- const actor = circle.actor ?? buildActorContext(circle)
2663
+ const actor = circle.actor ?? buildActorContext(circle)
2664
+ if (!actor.capabilities.canViewManagement) {
2518
2665
  return {
2519
2666
  circle,
2520
2667
  actor,
2521
2668
  management_sections: bootstrapManagementSections.length > 0
2522
2669
  ? bootstrapManagementSections
2523
2670
  : actor.managementSections,
2524
- members: normalizeCollectionResponse(data?.members ?? {}, normalizeCircleMember),
2525
- requests: normalizeCollectionResponse(data?.requests ?? {}, normalizeJoinRequest),
2526
- roles: Array.isArray(data?.roles)
2527
- ? normalizeInlineCollection(data.roles, normalizeRoleDefinition)
2528
- : normalizeCollectionResponse(data?.roles ?? {}, normalizeRoleDefinition),
2529
- counts: normalizeManagementCounts(data?.counts),
2530
- permission_catalog: Array.isArray(data?.permission_catalog)
2531
- ? data.permission_catalog
2532
- .map((entry) => normalizePermissionCatalogEntry(entry))
2533
- .filter((entry): entry is CirclePermissionCatalogEntry => entry !== null)
2534
- : [],
2535
- reports: normalizeCollectionResponse(data?.reports ?? {}, normalizeModerationReport),
2536
- bans: normalizeCollectionResponse(data?.bans ?? {}, normalizeBanRecord),
2537
- mutes: normalizeCollectionResponse(data?.mutes ?? {}, normalizeMuteRecord),
2538
- audit: normalizeCollectionResponse(data?.audit ?? {}, normalizeAuditLogEntry),
2539
- automod: normalizeCollectionResponse(data?.automod ?? {}, normalizeAutomodRule),
2671
+ members: createEmptyPagination<CircleMember>(),
2672
+ requests: createEmptyPagination<JoinRequest>(),
2673
+ roles: createEmptyPagination<CircleRoleDefinition>(),
2674
+ counts: createEmptyManagementCounts(),
2675
+ permission_catalog: [],
2676
+ reports: createEmptyPagination<CircleModerationReport>(),
2677
+ bans: createEmptyPagination<CircleBanRecord>(),
2678
+ mutes: createEmptyPagination<CircleMuteRecord>(),
2679
+ audit: createEmptyPagination<CircleModerationAuditLogEntry>(),
2680
+ automod: createEmptyPagination<CircleAutomodRule>(),
2540
2681
  }
2541
2682
  }
2542
2683
 
2543
- // Bootstrap returned circle + counts but no collection data arrays.
2544
- // Save the circle so the individual-fetch fallback path below can use it.
2545
- resolvedCircle = circle
2684
+ const caps = actor.capabilities
2685
+ const emptyJoinRequests = createEmptyPagination<JoinRequest>()
2686
+ const emptyRoles = createEmptyPagination<CircleRoleDefinition>()
2687
+ const emptyReports = createEmptyPagination<CircleModerationReport>()
2688
+ const emptyBans = createEmptyPagination<CircleBanRecord>()
2689
+ const emptyMutes = createEmptyPagination<CircleMuteRecord>()
2690
+ const emptyAudit = createEmptyPagination<CircleModerationAuditLogEntry>()
2691
+ const emptyAutomod = createEmptyPagination<CircleAutomodRule>()
2692
+ const members = hasValue(data?.members)
2693
+ ? normalizeCollectionResponse(data?.members ?? {}, normalizeCircleMember)
2694
+ : await this.getMembers(circle.id)
2695
+ const requests = hasValue(data?.requests)
2696
+ ? normalizeCollectionResponse(data?.requests ?? {}, normalizeJoinRequest)
2697
+ : caps.canReviewRequests
2698
+ ? await this.listJoinRequests(circle.id, null, 'pending')
2699
+ : emptyJoinRequests
2700
+ const roles = hasValue(data?.roles)
2701
+ ? normalizeBootstrapRoleCollection(data?.roles)
2702
+ : caps.canManageRoles
2703
+ ? await this.listRoles(circle.id)
2704
+ : emptyRoles
2705
+ const reports = hasValue(data?.reports)
2706
+ ? normalizeCollectionResponse(data?.reports ?? {}, normalizeModerationReport)
2707
+ : caps.canManageReports
2708
+ ? await this.listModerationReports(circle.id, null, 'pending')
2709
+ : emptyReports
2710
+ const bans = hasValue(data?.bans)
2711
+ ? normalizeCollectionResponse(data?.bans ?? {}, normalizeBanRecord)
2712
+ : caps.canManageBans
2713
+ ? await this.listBans(circle.id)
2714
+ : emptyBans
2715
+ const mutes = hasValue(data?.mutes)
2716
+ ? normalizeCollectionResponse(data?.mutes ?? {}, normalizeMuteRecord)
2717
+ : caps.canManageMutes
2718
+ ? await this.listMutes(circle.id)
2719
+ : emptyMutes
2720
+ const audit = hasValue(data?.audit)
2721
+ ? normalizeCollectionResponse(data?.audit ?? {}, normalizeAuditLogEntry)
2722
+ : caps.canViewAuditLog
2723
+ ? await this.getModerationAuditLog(circle.id)
2724
+ : emptyAudit
2725
+ const automod = hasValue(data?.automod)
2726
+ ? normalizeCollectionResponse(data?.automod ?? {}, normalizeAutomodRule)
2727
+ : caps.canManageAutomod
2728
+ ? await this.listAutomodRules(circle.id)
2729
+ : emptyAutomod
2730
+
2731
+ return {
2732
+ circle,
2733
+ actor,
2734
+ management_sections: bootstrapManagementSections.length > 0
2735
+ ? bootstrapManagementSections
2736
+ : actor.managementSections,
2737
+ members,
2738
+ requests,
2739
+ roles,
2740
+ counts: hasBootstrapCounts
2741
+ ? bootstrapCounts
2742
+ : {
2743
+ members: circle.member_count ?? members.data.length,
2744
+ requests_pending: requests.data.length,
2745
+ reports_pending: reports.data.length,
2746
+ roles: roles.data.length,
2747
+ bans_active: bans.data.length,
2748
+ mutes_active: mutes.data.length,
2749
+ },
2750
+ permission_catalog: bootstrapPermissionCatalog,
2751
+ reports,
2752
+ bans,
2753
+ mutes,
2754
+ audit,
2755
+ automod,
2756
+ }
2546
2757
  }
2547
2758
  } catch (error: unknown) {
2548
2759
  if (!this.isHttpStatus(error, 404)) {
@@ -2562,7 +2773,7 @@ class CirclesService {
2562
2773
  requests: createEmptyPagination<JoinRequest>(),
2563
2774
  roles: createEmptyPagination<CircleRoleDefinition>(),
2564
2775
  counts: createEmptyManagementCounts(),
2565
- permission_catalog: [],
2776
+ permission_catalog: bootstrapPermissionCatalog,
2566
2777
  reports: createEmptyPagination<CircleModerationReport>(),
2567
2778
  bans: createEmptyPagination<CircleBanRecord>(),
2568
2779
  mutes: createEmptyPagination<CircleMuteRecord>(),
@@ -2629,7 +2840,7 @@ class CirclesService {
2629
2840
  bans_active: bans.data.length,
2630
2841
  mutes_active: mutes.data.length,
2631
2842
  },
2632
- permission_catalog: [],
2843
+ permission_catalog: bootstrapPermissionCatalog,
2633
2844
  reports,
2634
2845
  bans,
2635
2846
  mutes,
@@ -460,10 +460,27 @@ const deriveRoleAssignmentsFromMembers = (members: CircleMemberState[]): CircleR
460
460
  roleAssignment.user = member.user
461
461
  }
462
462
 
463
- return roleAssignment
463
+ return roleAssignment
464
464
  })
465
465
  }
466
466
 
467
+ const resolveCircleActorContext = (
468
+ state: Pick<CirclesState, 'managementActors' | 'currentCircle' | 'circles'>,
469
+ circleId: string,
470
+ ): CircleActorContext | null => {
471
+ const managementActor = state.managementActors[circleId]
472
+ if (managementActor) {
473
+ return managementActor
474
+ }
475
+
476
+ if (state.currentCircle?.id === circleId || state.currentCircle?.slug === circleId) {
477
+ return state.currentCircle.actor ?? null
478
+ }
479
+
480
+ const matchingCircle = state.circles.find((candidate) => candidate.id === circleId || candidate.slug === circleId)
481
+ return matchingCircle?.actor ?? null
482
+ }
483
+
467
484
  const getRoleStoreService = (circlesService: CirclesServiceInstance): Partial<CircleRoleStoreService> => {
468
485
  // The shared service layer is mid-migration; runtime method checks let the store
469
486
  // support both the legacy member-role API and the new RBAC role-definition API.
@@ -1669,10 +1686,20 @@ export function createCirclesStoreDefinition(config: CirclesStoreConfig) {
1669
1686
 
1670
1687
  try {
1671
1688
  await circlesService.reportMember(circleId, subjectUserId, category, notes)
1672
- await Promise.allSettled([
1673
- this.fetchModerationReports(circleId, true),
1674
- this.fetchAuditLog(circleId, true),
1675
- ])
1689
+ const actor = resolveCircleActorContext(this, circleId)
1690
+ const followUpTasks: Promise<unknown>[] = []
1691
+
1692
+ if (actor?.capabilities.canManageReports === true) {
1693
+ followUpTasks.push(this.fetchModerationReports(circleId, true))
1694
+ }
1695
+
1696
+ if (actor?.capabilities.canViewAuditLog === true) {
1697
+ followUpTasks.push(this.fetchAuditLog(circleId, true))
1698
+ }
1699
+
1700
+ if (followUpTasks.length > 0) {
1701
+ await Promise.allSettled(followUpTasks)
1702
+ }
1676
1703
  } catch (error: unknown) {
1677
1704
  this.error = getErrorMessage(error)
1678
1705
  throw error