@codingfactory/socialkit-vue 0.7.10 → 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
@@ -880,6 +893,10 @@ function circleUserRole(circle: Circle | null | undefined): CircleRole | null {
880
893
  return isCircleRole(circle.user_role) ? circle.user_role : null
881
894
  }
882
895
 
896
+ function hasValue(value: unknown): boolean {
897
+ return value !== undefined && value !== null
898
+ }
899
+
883
900
  function normalizeCollectionResponse<T>(
884
901
  payload: unknown,
885
902
  normalizeItem: (value: unknown) => T | null,
@@ -925,6 +942,12 @@ function normalizeInlineCollection<T>(
925
942
  }
926
943
  }
927
944
 
945
+ function normalizeBootstrapRoleCollection(value: unknown): PaginationResponse<CircleRoleDefinition> {
946
+ return Array.isArray(value)
947
+ ? normalizeInlineCollection(value, normalizeRoleDefinition)
948
+ : normalizeCollectionResponse(value ?? {}, normalizeRoleDefinition)
949
+ }
950
+
928
951
  function normalizeCircle(value: unknown): Circle | null {
929
952
  if (!isRecord(value)) {
930
953
  return null
@@ -1714,6 +1737,73 @@ class CirclesService {
1714
1737
  return matchedCircle ? normalizeCircle(matchedCircle) : null
1715
1738
  }
1716
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
+
1717
1807
  public async list(cursor?: string | null, filters?: CircleFilters): Promise<PaginationResponse<Circle>> {
1718
1808
  try {
1719
1809
  const params: Record<string, string> = {
@@ -2265,21 +2355,12 @@ class CirclesService {
2265
2355
  }
2266
2356
  }
2267
2357
 
2268
- public async reportMember(
2358
+ public async submitModerationReport(
2269
2359
  circleId: string,
2270
- subjectUserId: string,
2271
- category: string,
2272
- notes?: string,
2273
- ): Promise<{ report_id: string; status: string }> {
2360
+ input: CircleModerationReportSubmissionInput,
2361
+ ): Promise<CircleModerationReportSubmissionResult> {
2274
2362
  try {
2275
- const payload: Record<string, string> = {
2276
- subject_user_id: subjectUserId,
2277
- category: this.sanitizeInput(category),
2278
- }
2279
- if (typeof notes === 'string' && notes.trim().length > 0) {
2280
- payload.notes = this.sanitizeInput(notes)
2281
- }
2282
-
2363
+ const payload = this.buildModerationReportPayload(input)
2283
2364
  const response = await this.client.post(`${this.baseURL}/${circleId}/moderation/reports`, payload)
2284
2365
  const data = readRecord(response.data, 'data') ?? {}
2285
2366
  const reportId = readString(data.report_id)
@@ -2303,6 +2384,28 @@ class CirclesService {
2303
2384
  }
2304
2385
  }
2305
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
+
2306
2409
  public async listModerationReports(
2307
2410
  circleId: string,
2308
2411
  cursor?: string | null,
@@ -2508,6 +2611,10 @@ class CirclesService {
2508
2611
  public async getManagementBootstrap(circleId: string): Promise<CircleManagementBootstrap> {
2509
2612
  const identifier = this.sanitizeInput(circleId)
2510
2613
  let resolvedCircle: Circle | null = null
2614
+ let bootstrapPermissionCatalog = createEmptyPagination<CirclePermissionCatalogEntry>().data
2615
+ let bootstrapManagementSections: CircleManagementSection[] = []
2616
+ let bootstrapCounts = createEmptyManagementCounts()
2617
+ let hasBootstrapCounts = false
2511
2618
  const bootstrapIdentifier = this.isUuid(identifier)
2512
2619
  ? identifier
2513
2620
  : await (async (): Promise<string> => {
@@ -2520,7 +2627,14 @@ class CirclesService {
2520
2627
  const data = readRecord(response.data, 'data')
2521
2628
  const bootstrapCircle = data?.circle
2522
2629
  const bootstrapActorCapabilities = readRecord(data, 'actor_capabilities')
2523
- const bootstrapManagementSections = normalizeManagementSections(data?.management_sections)
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)
2524
2638
  const directCircle = normalizeCircle(bootstrapCircle ?? data)
2525
2639
  const directCircleRole = circleUserRole(directCircle)
2526
2640
  const fallbackRole = directCircleRole ?? circleUserRole(resolvedCircle)
@@ -2546,38 +2660,100 @@ class CirclesService {
2546
2660
  const circle = normalizeCircle(mergedCircleSource)
2547
2661
 
2548
2662
  if (circle !== null) {
2549
- const bootstrapHasCollections = data?.members !== undefined && data?.members !== null
2550
-
2551
- if (bootstrapHasCollections) {
2552
- const actor = circle.actor ?? buildActorContext(circle)
2663
+ const actor = circle.actor ?? buildActorContext(circle)
2664
+ if (!actor.capabilities.canViewManagement) {
2553
2665
  return {
2554
2666
  circle,
2555
2667
  actor,
2556
2668
  management_sections: bootstrapManagementSections.length > 0
2557
2669
  ? bootstrapManagementSections
2558
2670
  : actor.managementSections,
2559
- members: normalizeCollectionResponse(data?.members ?? {}, normalizeCircleMember),
2560
- requests: normalizeCollectionResponse(data?.requests ?? {}, normalizeJoinRequest),
2561
- roles: Array.isArray(data?.roles)
2562
- ? normalizeInlineCollection(data.roles, normalizeRoleDefinition)
2563
- : normalizeCollectionResponse(data?.roles ?? {}, normalizeRoleDefinition),
2564
- counts: normalizeManagementCounts(data?.counts),
2565
- permission_catalog: Array.isArray(data?.permission_catalog)
2566
- ? data.permission_catalog
2567
- .map((entry) => normalizePermissionCatalogEntry(entry))
2568
- .filter((entry): entry is CirclePermissionCatalogEntry => entry !== null)
2569
- : [],
2570
- reports: normalizeCollectionResponse(data?.reports ?? {}, normalizeModerationReport),
2571
- bans: normalizeCollectionResponse(data?.bans ?? {}, normalizeBanRecord),
2572
- mutes: normalizeCollectionResponse(data?.mutes ?? {}, normalizeMuteRecord),
2573
- audit: normalizeCollectionResponse(data?.audit ?? {}, normalizeAuditLogEntry),
2574
- 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>(),
2575
2681
  }
2576
2682
  }
2577
2683
 
2578
- // Bootstrap returned circle + counts but no collection data arrays.
2579
- // Save the circle so the individual-fetch fallback path below can use it.
2580
- 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
+ }
2581
2757
  }
2582
2758
  } catch (error: unknown) {
2583
2759
  if (!this.isHttpStatus(error, 404)) {
@@ -2597,7 +2773,7 @@ class CirclesService {
2597
2773
  requests: createEmptyPagination<JoinRequest>(),
2598
2774
  roles: createEmptyPagination<CircleRoleDefinition>(),
2599
2775
  counts: createEmptyManagementCounts(),
2600
- permission_catalog: [],
2776
+ permission_catalog: bootstrapPermissionCatalog,
2601
2777
  reports: createEmptyPagination<CircleModerationReport>(),
2602
2778
  bans: createEmptyPagination<CircleBanRecord>(),
2603
2779
  mutes: createEmptyPagination<CircleMuteRecord>(),
@@ -2664,7 +2840,7 @@ class CirclesService {
2664
2840
  bans_active: bans.data.length,
2665
2841
  mutes_active: mutes.data.length,
2666
2842
  },
2667
- permission_catalog: [],
2843
+ permission_catalog: bootstrapPermissionCatalog,
2668
2844
  reports,
2669
2845
  bans,
2670
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