@commonpub/server 2.88.0 → 2.90.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.
Files changed (183) hide show
  1. package/dist/admin/admin.d.ts +9 -0
  2. package/dist/admin/admin.d.ts.map +1 -1
  3. package/dist/admin/admin.js +50 -13
  4. package/dist/admin/admin.js.map +1 -1
  5. package/dist/content/content.d.ts +0 -5
  6. package/dist/content/content.d.ts.map +1 -1
  7. package/dist/content/content.js +118 -122
  8. package/dist/content/content.js.map +1 -1
  9. package/dist/content/index.d.ts +1 -1
  10. package/dist/content/index.d.ts.map +1 -1
  11. package/dist/content/index.js +1 -1
  12. package/dist/content/index.js.map +1 -1
  13. package/dist/contest/contest.d.ts +7 -300
  14. package/dist/contest/contest.d.ts.map +1 -1
  15. package/dist/contest/contest.js +99 -882
  16. package/dist/contest/contest.js.map +1 -1
  17. package/dist/contest/entries.d.ts +41 -0
  18. package/dist/contest/entries.d.ts.map +1 -0
  19. package/dist/contest/entries.js +285 -0
  20. package/dist/contest/entries.js.map +1 -0
  21. package/dist/contest/export.d.ts +20 -0
  22. package/dist/contest/export.d.ts.map +1 -0
  23. package/dist/contest/export.js +131 -0
  24. package/dist/contest/export.js.map +1 -0
  25. package/dist/contest/index.d.ts +12 -3
  26. package/dist/contest/index.d.ts.map +1 -1
  27. package/dist/contest/index.js +9 -2
  28. package/dist/contest/index.js.map +1 -1
  29. package/dist/contest/judges.js +9 -8
  30. package/dist/contest/judges.js.map +1 -1
  31. package/dist/contest/judging.d.ts +38 -0
  32. package/dist/contest/judging.d.ts.map +1 -0
  33. package/dist/contest/judging.js +274 -0
  34. package/dist/contest/judging.js.map +1 -0
  35. package/dist/contest/read.d.ts +44 -0
  36. package/dist/contest/read.d.ts.map +1 -0
  37. package/dist/contest/read.js +164 -0
  38. package/dist/contest/read.js.map +1 -0
  39. package/dist/contest/stages.d.ts +28 -0
  40. package/dist/contest/stages.d.ts.map +1 -0
  41. package/dist/contest/stages.js +52 -0
  42. package/dist/contest/stages.js.map +1 -0
  43. package/dist/contest/stakeholders.d.ts +32 -7
  44. package/dist/contest/stakeholders.d.ts.map +1 -1
  45. package/dist/contest/stakeholders.js +55 -14
  46. package/dist/contest/stakeholders.js.map +1 -1
  47. package/dist/contest/submissions.d.ts +90 -0
  48. package/dist/contest/submissions.d.ts.map +1 -0
  49. package/dist/contest/submissions.js +275 -0
  50. package/dist/contest/submissions.js.map +1 -0
  51. package/dist/contest/types.d.ts +197 -0
  52. package/dist/contest/types.d.ts.map +1 -0
  53. package/dist/contest/types.js +2 -0
  54. package/dist/contest/types.js.map +1 -0
  55. package/dist/contest/validation.d.ts +32 -0
  56. package/dist/contest/validation.d.ts.map +1 -0
  57. package/dist/contest/validation.js +132 -0
  58. package/dist/contest/validation.js.map +1 -0
  59. package/dist/docs/docs.d.ts +9 -3
  60. package/dist/docs/docs.d.ts.map +1 -1
  61. package/dist/docs/docs.js +16 -6
  62. package/dist/docs/docs.js.map +1 -1
  63. package/dist/events/events.d.ts.map +1 -1
  64. package/dist/events/events.js +12 -6
  65. package/dist/events/events.js.map +1 -1
  66. package/dist/federation/activityDedup.d.ts +14 -0
  67. package/dist/federation/activityDedup.d.ts.map +1 -0
  68. package/dist/federation/activityDedup.js +34 -0
  69. package/dist/federation/activityDedup.js.map +1 -0
  70. package/dist/federation/assertPublicHost.d.ts +14 -0
  71. package/dist/federation/assertPublicHost.d.ts.map +1 -0
  72. package/dist/federation/assertPublicHost.js +62 -0
  73. package/dist/federation/assertPublicHost.js.map +1 -0
  74. package/dist/federation/delivery.d.ts.map +1 -1
  75. package/dist/federation/delivery.js +37 -51
  76. package/dist/federation/delivery.js.map +1 -1
  77. package/dist/federation/federation.d.ts.map +1 -1
  78. package/dist/federation/federation.js +11 -7
  79. package/dist/federation/federation.js.map +1 -1
  80. package/dist/federation/hubMirroring.d.ts.map +1 -1
  81. package/dist/federation/hubMirroring.js +85 -66
  82. package/dist/federation/hubMirroring.js.map +1 -1
  83. package/dist/federation/inboxHandlers.d.ts.map +1 -1
  84. package/dist/federation/inboxHandlers.js +84 -73
  85. package/dist/federation/inboxHandlers.js.map +1 -1
  86. package/dist/federation/inboxParsing.d.ts +28 -0
  87. package/dist/federation/inboxParsing.d.ts.map +1 -0
  88. package/dist/federation/inboxParsing.js +71 -0
  89. package/dist/federation/inboxParsing.js.map +1 -0
  90. package/dist/federation/index.d.ts +2 -0
  91. package/dist/federation/index.d.ts.map +1 -1
  92. package/dist/federation/index.js +2 -0
  93. package/dist/federation/index.js.map +1 -1
  94. package/dist/federation/mastodonLogin.d.ts.map +1 -1
  95. package/dist/federation/mastodonLogin.js +19 -0
  96. package/dist/federation/mastodonLogin.js.map +1 -1
  97. package/dist/federation/outboxQueries.js +1 -1
  98. package/dist/federation/outboxQueries.js.map +1 -1
  99. package/dist/federation/timeline.d.ts +11 -0
  100. package/dist/federation/timeline.d.ts.map +1 -1
  101. package/dist/federation/timeline.js +101 -69
  102. package/dist/federation/timeline.js.map +1 -1
  103. package/dist/hub/hub.d.ts.map +1 -1
  104. package/dist/hub/hub.js +41 -3
  105. package/dist/hub/hub.js.map +1 -1
  106. package/dist/hub/index.d.ts +1 -1
  107. package/dist/hub/index.d.ts.map +1 -1
  108. package/dist/hub/index.js +1 -1
  109. package/dist/hub/index.js.map +1 -1
  110. package/dist/hub/members.d.ts +19 -0
  111. package/dist/hub/members.d.ts.map +1 -1
  112. package/dist/hub/members.js +158 -13
  113. package/dist/hub/members.js.map +1 -1
  114. package/dist/hub/moderation.d.ts +1 -1
  115. package/dist/hub/moderation.d.ts.map +1 -1
  116. package/dist/hub/moderation.js +25 -11
  117. package/dist/hub/moderation.js.map +1 -1
  118. package/dist/hub/posts.d.ts.map +1 -1
  119. package/dist/hub/posts.js +18 -10
  120. package/dist/hub/posts.js.map +1 -1
  121. package/dist/hub/resources.js +2 -2
  122. package/dist/hub/resources.js.map +1 -1
  123. package/dist/identity/mastodonFactory.d.ts.map +1 -1
  124. package/dist/identity/mastodonFactory.js +7 -0
  125. package/dist/identity/mastodonFactory.js.map +1 -1
  126. package/dist/index.d.ts +8 -8
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +6 -6
  129. package/dist/index.js.map +1 -1
  130. package/dist/learning/learning.d.ts.map +1 -1
  131. package/dist/learning/learning.js +42 -22
  132. package/dist/learning/learning.js.map +1 -1
  133. package/dist/messaging/messaging.d.ts.map +1 -1
  134. package/dist/messaging/messaging.js +3 -3
  135. package/dist/messaging/messaging.js.map +1 -1
  136. package/dist/notification/notification.d.ts.map +1 -1
  137. package/dist/notification/notification.js +4 -2
  138. package/dist/notification/notification.js.map +1 -1
  139. package/dist/product/product.d.ts.map +1 -1
  140. package/dist/product/product.js +75 -37
  141. package/dist/product/product.js.map +1 -1
  142. package/dist/profile/index.d.ts +2 -1
  143. package/dist/profile/index.d.ts.map +1 -1
  144. package/dist/profile/index.js +1 -1
  145. package/dist/profile/index.js.map +1 -1
  146. package/dist/profile/profile.d.ts +24 -2
  147. package/dist/profile/profile.d.ts.map +1 -1
  148. package/dist/profile/profile.js +34 -8
  149. package/dist/profile/profile.js.map +1 -1
  150. package/dist/query.d.ts +18 -2
  151. package/dist/query.d.ts.map +1 -1
  152. package/dist/query.js +24 -6
  153. package/dist/query.js.map +1 -1
  154. package/dist/rbac/admin.d.ts +43 -0
  155. package/dist/rbac/admin.d.ts.map +1 -0
  156. package/dist/rbac/admin.js +172 -0
  157. package/dist/rbac/admin.js.map +1 -0
  158. package/dist/rbac/index.d.ts +4 -0
  159. package/dist/rbac/index.d.ts.map +1 -1
  160. package/dist/rbac/index.js +2 -0
  161. package/dist/rbac/index.js.map +1 -1
  162. package/dist/rbac/seed.d.ts +30 -0
  163. package/dist/rbac/seed.d.ts.map +1 -0
  164. package/dist/rbac/seed.js +75 -0
  165. package/dist/rbac/seed.js.map +1 -0
  166. package/dist/search/contentSearch.d.ts +1 -1
  167. package/dist/search/contentSearch.d.ts.map +1 -1
  168. package/dist/search/contentSearch.js +22 -12
  169. package/dist/search/contentSearch.js.map +1 -1
  170. package/dist/social/social.d.ts +4 -2
  171. package/dist/social/social.d.ts.map +1 -1
  172. package/dist/social/social.js +25 -8
  173. package/dist/social/social.js.map +1 -1
  174. package/dist/types.d.ts +3 -0
  175. package/dist/types.d.ts.map +1 -1
  176. package/dist/video/video.d.ts +3 -0
  177. package/dist/video/video.d.ts.map +1 -1
  178. package/dist/video/video.js +17 -13
  179. package/dist/video/video.js.map +1 -1
  180. package/dist/voting/voting.d.ts.map +1 -1
  181. package/dist/voting/voting.js +39 -1
  182. package/dist/voting/voting.js.map +1 -1
  183. package/package.json +9 -7
@@ -0,0 +1,164 @@
1
+ import { eq, ne, and, or, desc, sql, inArray } from 'drizzle-orm';
2
+ import { contests, contestJudges, contestStakeholders } from '@commonpub/schema';
3
+ import { normalizePagination, countRows } from '../query.js';
4
+ import { isContestStakeholder } from './stakeholders.js';
5
+ import { isContestJudge } from './judges.js';
6
+ // Contest read path: listing (visibility-filtered), detail mapping, per-contest
7
+ // view authorization, and the score-reveal decision. Writers live in contest.ts.
8
+ export async function listContests(db, filters = {}, viewer) {
9
+ const conditions = [];
10
+ if (filters.status) {
11
+ conditions.push(eq(contests.status, filters.status));
12
+ }
13
+ // Visibility: admins see everything. Everyone else sees `public` contests,
14
+ // plus — when signed in — the ones they have a relationship to so they're not
15
+ // hidden in the listing: their own, ones they review (stakeholder), ones they
16
+ // judge, and private ones whose `visibleToRoles` includes their role. (`unlisted`
17
+ // stays link-only; mirrors canViewContest so the listing matches per-contest access.)
18
+ if (viewer?.role !== 'admin') {
19
+ const visConds = [eq(contests.visibility, 'public')];
20
+ if (viewer?.userId) {
21
+ visConds.push(eq(contests.createdById, viewer.userId));
22
+ visConds.push(inArray(contests.id, db.select({ id: contestStakeholders.contestId }).from(contestStakeholders).where(eq(contestStakeholders.userId, viewer.userId))));
23
+ visConds.push(inArray(contests.id, db.select({ id: contestJudges.contestId }).from(contestJudges).where(eq(contestJudges.userId, viewer.userId))));
24
+ }
25
+ if (viewer?.role) {
26
+ visConds.push(sql `${contests.visibleToRoles} @> ${JSON.stringify([viewer.role])}::jsonb`);
27
+ }
28
+ conditions.push(visConds.length > 1 ? or(...visConds) : visConds[0]);
29
+ // Drafts never appear in listings except to their own owner (admins, handled
30
+ // above, see everything). Orthogonal to visibility — a public draft is still hidden.
31
+ conditions.push(viewer?.userId
32
+ ? or(ne(contests.status, 'draft'), eq(contests.createdById, viewer.userId))
33
+ : ne(contests.status, 'draft'));
34
+ }
35
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
36
+ const { limit, offset } = normalizePagination(filters);
37
+ const [rows, total] = await Promise.all([
38
+ db
39
+ .select()
40
+ .from(contests)
41
+ .where(where)
42
+ .orderBy(desc(contests.startDate), desc(contests.id))
43
+ .limit(limit)
44
+ .offset(offset),
45
+ countRows(db, contests, where),
46
+ ]);
47
+ const items = rows.map((row) => ({
48
+ id: row.id,
49
+ title: row.title,
50
+ slug: row.slug,
51
+ subheading: row.subheading,
52
+ description: row.description,
53
+ bannerUrl: row.bannerUrl,
54
+ coverImageUrl: row.coverImageUrl,
55
+ status: row.status,
56
+ startDate: row.startDate,
57
+ endDate: row.endDate,
58
+ entryCount: row.entryCount,
59
+ createdAt: row.createdAt,
60
+ }));
61
+ return { items, total };
62
+ }
63
+ /** Map a contest row to the API detail shape. Shared by the CRUD writers. */
64
+ export function toContestDetail(row) {
65
+ return {
66
+ id: row.id,
67
+ title: row.title,
68
+ slug: row.slug,
69
+ description: row.description,
70
+ bannerUrl: row.bannerUrl,
71
+ coverImageUrl: row.coverImageUrl,
72
+ status: row.status,
73
+ startDate: row.startDate,
74
+ endDate: row.endDate,
75
+ entryCount: row.entryCount,
76
+ createdAt: row.createdAt,
77
+ subheading: row.subheading,
78
+ rules: row.rules,
79
+ prizesDescription: row.prizesDescription,
80
+ descriptionFormat: row.descriptionFormat,
81
+ rulesFormat: row.rulesFormat,
82
+ prizesDescriptionFormat: row.prizesDescriptionFormat,
83
+ descriptionBlocks: row.descriptionBlocks ?? null,
84
+ rulesBlocks: row.rulesBlocks ?? null,
85
+ prizesBlocks: row.prizesBlocks ?? null,
86
+ showPrizes: row.showPrizes,
87
+ stages: row.stages ?? [],
88
+ currentStageId: row.currentStageId ?? null,
89
+ prizes: row.prizes ?? null,
90
+ judgingCriteria: row.judgingCriteria ?? null,
91
+ judgingVisibility: row.judgingVisibility,
92
+ judgingEndDate: row.judgingEndDate,
93
+ communityVotingEnabled: row.communityVotingEnabled,
94
+ eligibleContentTypes: row.eligibleContentTypes ?? null,
95
+ maxEntriesPerUser: row.maxEntriesPerUser ?? null,
96
+ visibility: row.visibility,
97
+ visibleToRoles: row.visibleToRoles ?? null,
98
+ createdById: row.createdById,
99
+ };
100
+ }
101
+ /**
102
+ * Whether `user` may view this contest. `public`/`unlisted` are viewable by
103
+ * anyone (unlisted is simply hidden from listings). `private` is restricted to
104
+ * the owner, admins, stakeholders, panel judges, and users whose role is in
105
+ * `visibleToRoles`.
106
+ */
107
+ export async function canViewContest(db, contest, user) {
108
+ // Drafts are owner-only regardless of the visibility setting — an unlaunched
109
+ // contest must never be world-readable, even when its visibility is `public`.
110
+ if (contest.status === 'draft') {
111
+ if (!user)
112
+ return false;
113
+ if (user.id === contest.createdById || user.role === 'admin')
114
+ return true;
115
+ if (await isContestStakeholder(db, contest.id, user.id))
116
+ return true;
117
+ if (await isContestJudge(db, contest.id, user.id))
118
+ return true;
119
+ return false;
120
+ }
121
+ if (contest.visibility !== 'private')
122
+ return true;
123
+ if (!user)
124
+ return false;
125
+ if (user.id === contest.createdById || user.role === 'admin')
126
+ return true;
127
+ if (contest.visibleToRoles && contest.visibleToRoles.includes(user.role))
128
+ return true;
129
+ if (await isContestStakeholder(db, contest.id, user.id))
130
+ return true;
131
+ if (await isContestJudge(db, contest.id, user.id))
132
+ return true;
133
+ return false;
134
+ }
135
+ export async function getContestBySlug(db, slug) {
136
+ const rows = await db
137
+ .select()
138
+ .from(contests)
139
+ .where(eq(contests.slug, slug))
140
+ .limit(1);
141
+ if (rows.length === 0)
142
+ return null;
143
+ return toContestDetail(rows[0]);
144
+ }
145
+ /**
146
+ * Decide whether a viewer may see aggregate entry scores, honouring the
147
+ * contest's `judgingVisibility` setting. Pure + exhaustively testable.
148
+ *
149
+ * - Privileged viewers (owner / admin / panel judge) always see scores.
150
+ * - `public` → scores visible to everyone (during judging and after).
151
+ * - `judges-only`→ scores hidden from the public until the contest completes.
152
+ * - `private` → aggregate scores never exposed to the public (ranks may
153
+ * still be shown so winners can be announced).
154
+ */
155
+ export function shouldRevealScores(visibility, status, privileged) {
156
+ if (privileged)
157
+ return true;
158
+ if (visibility === 'public')
159
+ return true;
160
+ if (visibility === 'private')
161
+ return false;
162
+ return status === 'completed';
163
+ }
164
+ //# sourceMappingURL=read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/contest/read.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { ContestStage } from '@commonpub/schema';
2
+ import type { StageSource } from './types.js';
3
+ /**
4
+ * The classic Submissions → Judging → Results timeline, synthesized from the
5
+ * status + date columns for contests that haven't defined explicit stages.
6
+ * Stable ids let `currentStageId` reference them even for legacy contests.
7
+ */
8
+ export declare function synthesizeStages(c: StageSource): ContestStage[];
9
+ /**
10
+ * The contest's stage timeline: its explicit `stages` if any are defined,
11
+ * otherwise the synthesized classic flow. The standard flow is the zero-config
12
+ * default — a contest with no `stages` renders identically to pre-B1.
13
+ */
14
+ export declare function normalizeStages(c: StageSource): ContestStage[];
15
+ /**
16
+ * The stage that is currently "now": the one `currentStageId` points at (if it
17
+ * resolves), else derived from the coarse `status`. Null while draft/cancelled
18
+ * (nothing is running). `status` remains the behavioural source of truth for
19
+ * gating; this is for DISPLAY (hero pill, sidebar highlight, countdown label).
20
+ */
21
+ export declare function currentStage(c: StageSource): ContestStage | null;
22
+ /** True when an entry was culled at some review stage (Phase B2 cohort gate). */
23
+ export declare function isEliminated(entry: {
24
+ stageState?: Array<{
25
+ status: string;
26
+ }> | null;
27
+ }): boolean;
28
+ //# sourceMappingURL=stages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stages.d.ts","sourceRoot":"","sources":["../../src/contest/stages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAO9C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,WAAW,GAAG,YAAY,EAAE,CAM/D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,WAAW,GAAG,YAAY,EAAE,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,IAAI,CAiBhE;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAAE,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;CAAE,GAAG,OAAO,CAE9F"}
@@ -0,0 +1,52 @@
1
+ // --- Phase B1: stage timeline helpers (pure — operate on a contest-like object) ---
2
+ const toIso = (d) => d ? new Date(d).toISOString() : undefined;
3
+ /**
4
+ * The classic Submissions → Judging → Results timeline, synthesized from the
5
+ * status + date columns for contests that haven't defined explicit stages.
6
+ * Stable ids let `currentStageId` reference them even for legacy contests.
7
+ */
8
+ export function synthesizeStages(c) {
9
+ return [
10
+ { id: 'core-submission', name: 'Submissions', kind: 'submission', core: true, startsAt: toIso(c.startDate), endsAt: toIso(c.endDate) },
11
+ { id: 'core-review', name: 'Judging', kind: 'review', core: true, endsAt: toIso(c.judgingEndDate) ?? toIso(c.endDate) },
12
+ { id: 'core-results', name: 'Results', kind: 'results', core: true },
13
+ ];
14
+ }
15
+ /**
16
+ * The contest's stage timeline: its explicit `stages` if any are defined,
17
+ * otherwise the synthesized classic flow. The standard flow is the zero-config
18
+ * default — a contest with no `stages` renders identically to pre-B1.
19
+ */
20
+ export function normalizeStages(c) {
21
+ return c.stages && c.stages.length > 0 ? c.stages : synthesizeStages(c);
22
+ }
23
+ /**
24
+ * The stage that is currently "now": the one `currentStageId` points at (if it
25
+ * resolves), else derived from the coarse `status`. Null while draft/cancelled
26
+ * (nothing is running). `status` remains the behavioural source of truth for
27
+ * gating; this is for DISPLAY (hero pill, sidebar highlight, countdown label).
28
+ */
29
+ export function currentStage(c) {
30
+ const stages = normalizeStages(c);
31
+ if (c.currentStageId) {
32
+ const found = stages.find((s) => s.id === c.currentStageId);
33
+ if (found)
34
+ return found;
35
+ }
36
+ switch (c.status) {
37
+ case 'draft':
38
+ case 'cancelled':
39
+ return null;
40
+ case 'completed':
41
+ return stages.find((s) => s.kind === 'results') ?? stages[stages.length - 1] ?? null;
42
+ case 'judging':
43
+ return stages.find((s) => s.kind === 'review') ?? null;
44
+ default: // upcoming | active | paused
45
+ return stages.find((s) => s.kind === 'submission') ?? stages[0] ?? null;
46
+ }
47
+ }
48
+ /** True when an entry was culled at some review stage (Phase B2 cohort gate). */
49
+ export function isEliminated(entry) {
50
+ return !!entry.stageState?.some((s) => s.status === 'eliminated');
51
+ }
52
+ //# sourceMappingURL=stages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stages.js","sourceRoot":"","sources":["../../src/contest/stages.ts"],"names":[],"mappings":"AAGA,qFAAqF;AAErF,MAAM,KAAK,GAAG,CAAC,CAAmC,EAAsB,EAAE,CACxE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAE5C;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAc;IAC7C,OAAO;QACL,EAAE,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE;QACtI,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE;QACvH,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE;KACrE,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,CAAc;IAC5C,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,CAAc;IACzC,MAAM,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAClC,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,cAAc,CAAC,CAAC;QAC5D,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;QACjB,KAAK,OAAO,CAAC;QACb,KAAK,WAAW;YACd,OAAO,IAAI,CAAC;QACd,KAAK,WAAW;YACd,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;QACvF,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,IAAI,CAAC;QACzD,SAAS,6BAA6B;YACpC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC5E,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,YAAY,CAAC,KAAwD;IACnF,OAAO,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,YAAY,CAAC,CAAC;AACpE,CAAC"}
@@ -1,27 +1,52 @@
1
+ import type { StakeholderRole } from '@commonpub/schema';
1
2
  import type { DB } from '../types.js';
2
3
  /**
3
- * Stakeholders are view-only reviewers: they can see a contest (even private /
4
- * pre-publish) without admin-panel access or being a judge. Distinct from
5
- * judges they never appear in the judge list and cannot score.
4
+ * Per-contest collaborators, distinguished by `role`:
5
+ * - 'reviewer' — view-only: can see a contest (even private / pre-publish)
6
+ * without admin-panel access or being a judge. Never appears in the judge
7
+ * list and cannot score. (Original stakeholder semantics.)
8
+ * - 'editor' — full edit rights to THIS contest only (no system-wide access).
9
+ * Recognized by `isContestEditor`, which the edit/advance/transition routes
10
+ * fold into their canManage decision.
6
11
  */
7
12
  export interface ContestStakeholderItem {
8
13
  id: string;
9
14
  contestId: string;
10
15
  userId: string;
16
+ role: StakeholderRole;
11
17
  invitedAt: Date;
12
18
  userName: string;
13
19
  userUsername: string;
14
20
  userAvatar: string | null;
15
21
  }
16
22
  export declare function listContestStakeholders(db: DB, contestId: string): Promise<ContestStakeholderItem[]>;
17
- export declare function addContestStakeholder(db: DB, contestId: string, userId: string, context?: {
18
- contestSlug: string;
19
- contestTitle: string;
20
- invitedBy: string;
23
+ /**
24
+ * Grant (or update) a user's per-contest collaborator role. Upserts: if the
25
+ * user is already a stakeholder with a different role, their role is updated
26
+ * (so a reviewer can be promoted to editor) — `updated:true` is returned.
27
+ * Adding/promoting is gated to the contest owner / `contest.manage` at the route
28
+ * (an editor cannot mint more editors).
29
+ */
30
+ export declare function addContestStakeholder(db: DB, contestId: string, userId: string, opts?: {
31
+ role?: StakeholderRole;
32
+ contestSlug?: string;
33
+ contestTitle?: string;
34
+ invitedBy?: string;
21
35
  }): Promise<{
22
36
  added: boolean;
37
+ updated?: boolean;
23
38
  error?: string;
24
39
  }>;
25
40
  export declare function removeContestStakeholder(db: DB, contestId: string, userId: string): Promise<boolean>;
41
+ /**
42
+ * True if the user is a stakeholder of ANY role (reviewer or editor) — i.e. may
43
+ * VIEW a private/draft contest. Role-agnostic on purpose.
44
+ */
26
45
  export declare function isContestStakeholder(db: DB, contestId: string, userId: string): Promise<boolean>;
46
+ /**
47
+ * True if the user is an `editor` stakeholder of this contest — i.e. holds full
48
+ * edit rights to it (no system-wide access). Folded into the canManage decision
49
+ * on the edit/advance/transition routes alongside owner + `contest.manage`.
50
+ */
51
+ export declare function isContestEditor(db: DB, contestId: string, userId: string): Promise<boolean>;
27
52
  //# sourceMappingURL=stakeholders.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stakeholders.d.ts","sourceRoot":"","sources":["../../src/contest/stakeholders.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAGtC;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAmBnC;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzE,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA4B7C;AAED,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CASlB;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAOlB"}
1
+ {"version":3,"file":"stakeholders.d.ts","sourceRoot":"","sources":["../../src/contest/stakeholders.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAGtC;;;;;;;;GAQG;AACH,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAoBnC;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IACL,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+BhE;AAoBD,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CASlB;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,EAAE,EACN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAOlB"}
@@ -14,13 +14,22 @@ export async function listContestStakeholders(db, contestId) {
14
14
  id: sh.id,
15
15
  contestId: sh.contestId,
16
16
  userId: sh.userId,
17
+ role: sh.role ?? 'reviewer',
17
18
  invitedAt: sh.invitedAt,
18
19
  userName: user.displayName ?? user.username,
19
20
  userUsername: user.username,
20
21
  userAvatar: user.avatarUrl,
21
22
  }));
22
23
  }
23
- export async function addContestStakeholder(db, contestId, userId, context) {
24
+ /**
25
+ * Grant (or update) a user's per-contest collaborator role. Upserts: if the
26
+ * user is already a stakeholder with a different role, their role is updated
27
+ * (so a reviewer can be promoted to editor) — `updated:true` is returned.
28
+ * Adding/promoting is gated to the contest owner / `contest.manage` at the route
29
+ * (an editor cannot mint more editors).
30
+ */
31
+ export async function addContestStakeholder(db, contestId, userId, opts) {
32
+ const role = opts?.role ?? 'reviewer';
24
33
  const [contest] = await db.select({ id: contests.id }).from(contests).where(eq(contests.id, contestId)).limit(1);
25
34
  if (!contest)
26
35
  return { added: false, error: 'Contest not found' };
@@ -28,25 +37,40 @@ export async function addContestStakeholder(db, contestId, userId, context) {
28
37
  if (!user)
29
38
  return { added: false, error: 'User not found' };
30
39
  const [existing] = await db
31
- .select({ id: contestStakeholders.id })
40
+ .select({ id: contestStakeholders.id, role: contestStakeholders.role })
32
41
  .from(contestStakeholders)
33
42
  .where(and(eq(contestStakeholders.contestId, contestId), eq(contestStakeholders.userId, userId)))
34
43
  .limit(1);
35
- if (existing)
36
- return { added: false, error: 'User is already a stakeholder' };
37
- await db.insert(contestStakeholders).values({ contestId, userId });
38
- if (context) {
39
- createNotification(db, {
40
- userId,
41
- type: 'contest',
42
- title: 'Contest Access',
43
- message: `You've been granted review access to "${context.contestTitle}"`,
44
- link: `/contests/${context.contestSlug}`,
45
- actorId: context.invitedBy,
46
- }).catch(() => { });
44
+ if (existing) {
45
+ if (existing.role === role)
46
+ return { added: false, error: 'User already has that role' };
47
+ await db.update(contestStakeholders).set({ role }).where(eq(contestStakeholders.id, existing.id));
48
+ notifyStakeholder(db, userId, role, opts);
49
+ return { added: true, updated: true };
47
50
  }
51
+ // Conflict-safe insert: a concurrent double-submit can pass the `existing`
52
+ // check above and race the unique (contestId,userId) constraint — upsert the
53
+ // role instead of 500ing on the violation.
54
+ await db
55
+ .insert(contestStakeholders)
56
+ .values({ contestId, userId, role })
57
+ .onConflictDoUpdate({ target: [contestStakeholders.contestId, contestStakeholders.userId], set: { role } });
58
+ notifyStakeholder(db, userId, role, opts);
48
59
  return { added: true };
49
60
  }
61
+ function notifyStakeholder(db, userId, role, opts) {
62
+ if (!opts?.contestSlug || !opts.contestTitle || !opts.invitedBy)
63
+ return;
64
+ const access = role === 'editor' ? 'edit access' : 'review access';
65
+ createNotification(db, {
66
+ userId,
67
+ type: 'contest',
68
+ title: 'Contest Access',
69
+ message: `You've been granted ${access} to "${opts.contestTitle}"`,
70
+ link: `/contests/${opts.contestSlug}`,
71
+ actorId: opts.invitedBy,
72
+ }).catch(() => { });
73
+ }
50
74
  export async function removeContestStakeholder(db, contestId, userId) {
51
75
  const [existing] = await db
52
76
  .select({ id: contestStakeholders.id })
@@ -58,6 +82,10 @@ export async function removeContestStakeholder(db, contestId, userId) {
58
82
  await db.delete(contestStakeholders).where(eq(contestStakeholders.id, existing.id));
59
83
  return true;
60
84
  }
85
+ /**
86
+ * True if the user is a stakeholder of ANY role (reviewer or editor) — i.e. may
87
+ * VIEW a private/draft contest. Role-agnostic on purpose.
88
+ */
61
89
  export async function isContestStakeholder(db, contestId, userId) {
62
90
  const [row] = await db
63
91
  .select({ id: contestStakeholders.id })
@@ -66,4 +94,17 @@ export async function isContestStakeholder(db, contestId, userId) {
66
94
  .limit(1);
67
95
  return !!row;
68
96
  }
97
+ /**
98
+ * True if the user is an `editor` stakeholder of this contest — i.e. holds full
99
+ * edit rights to it (no system-wide access). Folded into the canManage decision
100
+ * on the edit/advance/transition routes alongside owner + `contest.manage`.
101
+ */
102
+ export async function isContestEditor(db, contestId, userId) {
103
+ const [row] = await db
104
+ .select({ role: contestStakeholders.role })
105
+ .from(contestStakeholders)
106
+ .where(and(eq(contestStakeholders.contestId, contestId), eq(contestStakeholders.userId, userId)))
107
+ .limit(1);
108
+ return row?.role === 'editor';
109
+ }
69
110
  //# sourceMappingURL=stakeholders.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stakeholders.js","sourceRoot":"","sources":["../../src/contest/stakeholders.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stakeholders.js","sourceRoot":"","sources":["../../src/contest/stakeholders.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import type { ContestStageSubmission } from '@commonpub/schema';
2
+ import type { DB } from '../types.js';
3
+ import type { AgreementAcceptanceInput, ContestTx } from './types.js';
4
+ /**
5
+ * Persist the PII + agreement halves of a partitioned submission within an open
6
+ * transaction. PII is upserted (one row per entry, merged with any prior PII so
7
+ * a later stage's PII doesn't wipe an earlier stage's); agreements are appended
8
+ * as immutable acceptance rows (terms hash + snapshot). No-op when both empty.
9
+ */
10
+ export declare function recordPrivateAndAgreements(tx: ContestTx, args: {
11
+ contestId: string;
12
+ entryId: string;
13
+ userId: string;
14
+ stageId: string;
15
+ pii: Record<string, string>;
16
+ agreements: AgreementAcceptanceInput[];
17
+ ip?: string | null;
18
+ }): Promise<void>;
19
+ /**
20
+ * Submit (or update) an entrant's per-stage artifact: the filled template
21
+ * values for one `submission` stage, snapshotted onto the entry's
22
+ * `stageSubmissions`. Owner-only. The stage must be the contest's CURRENT
23
+ * stage while the contest is `active` (status stays the gating truth — the
24
+ * organizer maps a later submission round back to `active` when advancing).
25
+ * Re-submitting while the stage is open replaces that stage's artifact.
26
+ * Cohort gate: an entry culled at a prior review stage can no longer submit.
27
+ */
28
+ export declare function submitStageArtifact(db: DB, entryId: string, stageId: string, fields: Record<string, string>, userId: string, ip?: string | null): Promise<{
29
+ submitted: boolean;
30
+ stageSubmissions?: ContestStageSubmission[];
31
+ error?: string;
32
+ }>;
33
+ export interface SubmitProposalArgs {
34
+ contestId: string;
35
+ stageId: string;
36
+ /** Raw entrant form values (template-key → string). */
37
+ fields: Record<string, string>;
38
+ userId: string;
39
+ /** Best-effort client IP, recorded with any agreement acceptances. */
40
+ ip?: string | null;
41
+ }
42
+ export type SubmitProposalResult = {
43
+ ok: true;
44
+ entryId: string;
45
+ projectSlug: string;
46
+ contentId: string;
47
+ contentType: string;
48
+ } | {
49
+ ok: false;
50
+ error: string;
51
+ };
52
+ /**
53
+ * Form-first proposal submission (Phase 4). For a `submission` stage in
54
+ * `proposal` mode: validate the form, create a DRAFT placeholder project, link a
55
+ * contest entry to it (relaxing the published-only gate that `submitContestEntry`
56
+ * enforces), snapshot the artifact, record agreement acceptances, store PII in
57
+ * the private table, and bump `entryCount`. The placeholder + the entry +
58
+ * PII/agreements are written atomically: the placeholder is created first, then
59
+ * the entry tx; if the entry tx fails the placeholder is removed (compensating
60
+ * delete) so a failed submit never leaves an orphan draft.
61
+ *
62
+ * Flag (`features.contestProposals`) is enforced at the route; this fn enforces
63
+ * the structural gate that the target stage is current + proposal-mode.
64
+ */
65
+ export declare function submitContestProposal(db: DB, args: SubmitProposalArgs): Promise<SubmitProposalResult>;
66
+ export interface EntryPrivateData {
67
+ contestId: string;
68
+ entryId: string;
69
+ userId: string;
70
+ fields: Record<string, string>;
71
+ updatedAt: Date;
72
+ agreements: Array<{
73
+ fieldKey: string;
74
+ stageId: string;
75
+ termsHash: string;
76
+ termsSnapshot: string;
77
+ acceptedAt: Date;
78
+ /** Consent-audit IP captured at acceptance. Surfaced so the subject can see
79
+ * the data held about them (transparency) and organizers have the audit trail. */
80
+ ip: string | null;
81
+ }>;
82
+ }
83
+ /**
84
+ * Read an entry's PII + agreement acceptances. NEVER reachable through the
85
+ * normal entries endpoints — the calling route gates this on the `contest.pii`
86
+ * permission OR the requester being the entrant. Returns null when the entry has
87
+ * no stored PII and no agreements.
88
+ */
89
+ export declare function getEntryPrivateData(db: DB, entryId: string): Promise<EntryPrivateData | null>;
90
+ //# sourceMappingURL=submissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissions.d.ts","sourceRoot":"","sources":["../../src/contest/submissions.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAe,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAKtC,OAAO,KAAK,EAAe,wBAAwB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAInF;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,SAAS,EACb,IAAI,EAAE;IACJ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,UAAU,EAAE,wBAAwB,EAAE,CAAC;IACvC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB,GACA,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,EAAE,EACN,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GACjB,OAAO,CAAC;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,gBAAgB,CAAC,EAAE,sBAAsB,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAkF9F;AAOD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,MAAM,oBAAoB,GAI5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC1F;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAiG3G;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,KAAK,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,IAAI,CAAC;QACjB;2FACmF;QACnF,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;KACnB,CAAC,CAAC;CACJ;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA6BnG"}