founder-schema 0.3.0 → 0.5.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 (106) hide show
  1. package/README.md +44 -16
  2. package/dist/access/access.d.ts +76 -0
  3. package/dist/access/access.d.ts.map +1 -0
  4. package/dist/access/access.js +66 -0
  5. package/dist/access/access.js.map +1 -0
  6. package/dist/access/permissions.d.ts +153 -0
  7. package/dist/access/permissions.d.ts.map +1 -0
  8. package/dist/access/permissions.js +80 -0
  9. package/dist/access/permissions.js.map +1 -0
  10. package/dist/access/roles.d.ts +71 -0
  11. package/dist/access/roles.d.ts.map +1 -0
  12. package/dist/access/roles.js +72 -0
  13. package/dist/access/roles.js.map +1 -0
  14. package/dist/chat.d.ts +4 -4
  15. package/dist/chat.d.ts.map +1 -1
  16. package/dist/chat.js +7 -2
  17. package/dist/chat.js.map +1 -1
  18. package/dist/core/common.d.ts +304 -0
  19. package/dist/core/common.d.ts.map +1 -0
  20. package/dist/core/common.js +340 -0
  21. package/dist/core/common.js.map +1 -0
  22. package/dist/core/errors.d.ts +117 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +139 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/limits.d.ts +122 -0
  27. package/dist/core/limits.d.ts.map +1 -0
  28. package/dist/core/limits.js +132 -0
  29. package/dist/core/limits.js.map +1 -0
  30. package/dist/core/responses.d.ts +207 -0
  31. package/dist/core/responses.d.ts.map +1 -0
  32. package/dist/core/responses.js +73 -0
  33. package/dist/core/responses.js.map +1 -0
  34. package/dist/core/validation.d.ts +146 -0
  35. package/dist/core/validation.d.ts.map +1 -0
  36. package/dist/core/validation.js +152 -0
  37. package/dist/core/validation.js.map +1 -0
  38. package/dist/domains/account/account.d.ts +42 -0
  39. package/dist/domains/account/account.d.ts.map +1 -0
  40. package/dist/domains/account/account.js +41 -0
  41. package/dist/domains/account/account.js.map +1 -0
  42. package/dist/domains/audit/audit.d.ts +169 -0
  43. package/dist/domains/audit/audit.d.ts.map +1 -0
  44. package/dist/domains/audit/audit.js +84 -0
  45. package/dist/domains/audit/audit.js.map +1 -0
  46. package/dist/domains/chat/chat.d.ts +187 -0
  47. package/dist/domains/chat/chat.d.ts.map +1 -0
  48. package/dist/domains/chat/chat.js +189 -0
  49. package/dist/domains/chat/chat.js.map +1 -0
  50. package/dist/domains/dashboard/dashboard.d.ts +48 -0
  51. package/dist/domains/dashboard/dashboard.d.ts.map +1 -0
  52. package/dist/domains/dashboard/dashboard.js +58 -0
  53. package/dist/domains/dashboard/dashboard.js.map +1 -0
  54. package/dist/domains/discover/discover.d.ts +245 -0
  55. package/dist/domains/discover/discover.d.ts.map +1 -0
  56. package/dist/domains/discover/discover.js +68 -0
  57. package/dist/domains/discover/discover.js.map +1 -0
  58. package/dist/domains/feed/feed.d.ts +1695 -0
  59. package/dist/domains/feed/feed.d.ts.map +1 -0
  60. package/dist/domains/feed/feed.js +626 -0
  61. package/dist/domains/feed/feed.js.map +1 -0
  62. package/dist/domains/media/media.d.ts +134 -0
  63. package/dist/domains/media/media.d.ts.map +1 -0
  64. package/dist/domains/media/media.js +110 -0
  65. package/dist/domains/media/media.js.map +1 -0
  66. package/dist/domains/members/members.d.ts +175 -0
  67. package/dist/domains/members/members.d.ts.map +1 -0
  68. package/dist/domains/members/members.js +106 -0
  69. package/dist/domains/members/members.js.map +1 -0
  70. package/dist/domains/moderation/moderation.d.ts +184 -0
  71. package/dist/domains/moderation/moderation.d.ts.map +1 -0
  72. package/dist/domains/moderation/moderation.js +90 -0
  73. package/dist/domains/moderation/moderation.js.map +1 -0
  74. package/dist/domains/onboarding/onboarding.d.ts +1018 -0
  75. package/dist/domains/onboarding/onboarding.d.ts.map +1 -0
  76. package/dist/domains/onboarding/onboarding.js +717 -0
  77. package/dist/domains/onboarding/onboarding.js.map +1 -0
  78. package/dist/domains/profile/founder-profile.d.ts +411 -0
  79. package/dist/domains/profile/founder-profile.d.ts.map +1 -0
  80. package/dist/domains/profile/founder-profile.js +202 -0
  81. package/dist/domains/profile/founder-profile.js.map +1 -0
  82. package/dist/domains/support/support.d.ts +305 -0
  83. package/dist/domains/support/support.d.ts.map +1 -0
  84. package/dist/domains/support/support.js +129 -0
  85. package/dist/domains/support/support.js.map +1 -0
  86. package/dist/domains/team/team.d.ts +79 -0
  87. package/dist/domains/team/team.d.ts.map +1 -0
  88. package/dist/domains/team/team.js +54 -0
  89. package/dist/domains/team/team.js.map +1 -0
  90. package/dist/domains/uploads/uploads.d.ts +101 -0
  91. package/dist/domains/uploads/uploads.d.ts.map +1 -0
  92. package/dist/domains/uploads/uploads.js +89 -0
  93. package/dist/domains/uploads/uploads.js.map +1 -0
  94. package/dist/env.d.ts +85 -0
  95. package/dist/env.d.ts.map +1 -0
  96. package/dist/env.js +150 -0
  97. package/dist/env.js.map +1 -0
  98. package/dist/index.d.ts +22 -12
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +32 -19
  101. package/dist/index.js.map +1 -1
  102. package/dist/onboarding.d.ts +58 -32
  103. package/dist/onboarding.d.ts.map +1 -1
  104. package/dist/onboarding.js +155 -32
  105. package/dist/onboarding.js.map +1 -1
  106. package/package.json +14 -2
@@ -0,0 +1,106 @@
1
+ /**
2
+ * founder-schema: the staff member directory
3
+ *
4
+ * GET /api/v1/members ?q&status&role&limit&cursor
5
+ * GET /api/v1/members/:memberId one member, as staff see them
6
+ * POST /api/v1/members/:memberId/suspension { reason } suspend
7
+ * DELETE /api/v1/members/:memberId/suspension lift it
8
+ *
9
+ * Every staff role has a Members tab: a moderator looks up who wrote
10
+ * something, a support agent finds the person who wrote in, a manager decides
11
+ * on a suspension. What each sees is the server's call, per field: a
12
+ * moderator gets no email (`email: null`), because finding a member's
13
+ * address is support's job, not moderation's.
14
+ *
15
+ * A SUSPENDED member cannot use the product at all, on any device, from the
16
+ * next request on (every call answers `ACCOUNT_SUSPENDED`); their posts stay
17
+ * up unless moderated separately, and a staff role they were acting in is
18
+ * switched off. Lifting it restores everything else.
19
+ *
20
+ * SEARCH (`q`) finds people by the start of their name or handle among listed
21
+ * founders, or by their exact email among everyone (only for a role that may
22
+ * see emails: a moderator's email search finds nothing). It returns up to
23
+ * `limit` matches in one page (type more to narrow), not a pageable list. Who may suspend whom is the access policy's: never
24
+ * yourself, never a superadmin, and a manager never a fellow manager.
25
+ *
26
+ * Each detail carries a `viewer` block of what the asking staff member may do
27
+ * to THIS member, worked out by the server, so the screen shows only the
28
+ * buttons that will work.
29
+ *
30
+ * @module founder-schema/domains/members
31
+ */
32
+ import { z } from 'zod';
33
+ import { platformRoleSchema } from '../../access/roles.js';
34
+ import { emptyStringToUndefined, objectIdSchema, optionalTrimmedString, paginationQuerySchema, usernameSchema, } from '../../core/common.js';
35
+ import { MEMBER_SEARCH_MAX, SUSPENSION_REASON_MAX, SUSPENSION_REASON_MIN, } from '../../core/limits.js';
36
+ import { paginatedSchema } from '../../core/responses.js';
37
+ /** What staff can see an account's state as. A deleted account is not listed. */
38
+ export const MEMBER_STATUS_VALUES = ['active', 'suspended'];
39
+ export const memberStatusSchema = z.enum(MEMBER_STATUS_VALUES);
40
+ /** `:memberId` in the paths above. */
41
+ export const memberParamsSchema = z.object({ memberId: objectIdSchema });
42
+ /**
43
+ * The directory's query. `q` is a name or handle prefix, or a whole email
44
+ * address (anything with an `@` is matched as an email, exactly). An emptied
45
+ * box is "no search", not an error.
46
+ */
47
+ export const memberDirectoryQuerySchema = paginationQuerySchema.extend({
48
+ q: optionalTrimmedString(MEMBER_SEARCH_MAX),
49
+ status: z.preprocess(emptyStringToUndefined, memberStatusSchema.optional()),
50
+ /** Only people holding this staff role. */
51
+ role: z.preprocess(emptyStringToUndefined, platformRoleSchema.optional()),
52
+ });
53
+ /** One row of the directory. */
54
+ export const memberSummarySchema = z.object({
55
+ id: objectIdSchema,
56
+ displayName: z.string(),
57
+ /** Null when they have no founder profile (never finished onboarding). */
58
+ username: usernameSchema.nullable(),
59
+ avatarUrl: z.url().nullable(),
60
+ /** Null when the asking role may not see it. */
61
+ email: z.string().nullable(),
62
+ status: memberStatusSchema,
63
+ /** Staff roles held, highest first. Empty for an ordinary member. */
64
+ roles: z.array(platformRoleSchema),
65
+ joinedAt: z.iso.datetime(),
66
+ lastActiveAt: z.iso.datetime().nullable(),
67
+ });
68
+ export const memberDirectoryPageSchema = paginatedSchema(memberSummarySchema);
69
+ /** A suspension in force. */
70
+ export const suspensionSchema = z.object({
71
+ since: z.iso.datetime(),
72
+ reason: z.string(),
73
+ /** Who suspended them, by name at the time. */
74
+ by: z.object({ id: objectIdSchema, displayName: z.string() }).nullable(),
75
+ });
76
+ /** One member, as staff see them. */
77
+ export const memberDetailSchema = memberSummarySchema.extend({
78
+ headline: z.string().nullable(),
79
+ /** Their city, as declared on their profile. */
80
+ city: z.string().nullable(),
81
+ counts: z.object({
82
+ posts: z.number().int().min(0),
83
+ polls: z.number().int().min(0),
84
+ comments: z.number().int().min(0),
85
+ /** Reports filed about their posts, polls and comments that are still up. */
86
+ reportsAgainst: z.number().int().min(0),
87
+ /** Tickets they have raised. */
88
+ tickets: z.number().int().min(0),
89
+ }),
90
+ suspension: suspensionSchema.nullable(),
91
+ /** What the asking staff member may do to this member. */
92
+ viewer: z.object({
93
+ canSuspend: z.boolean(),
94
+ /** Roles the asker may grant or revoke on this member (never their own). */
95
+ grantableRoles: z.array(platformRoleSchema),
96
+ /** May clear this member's forgotten MPIN (superadmin, someone else). */
97
+ canResetMpin: z.boolean(),
98
+ }),
99
+ /** Whether they have set an MPIN. Only meaningful for staff. */
100
+ mpinSet: z.boolean(),
101
+ });
102
+ /** `POST /members/:memberId/suspension`. The reason is on record in the audit log. */
103
+ export const suspendMemberBodySchema = z.object({
104
+ reason: z.string().trim().min(SUSPENSION_REASON_MIN).max(SUSPENSION_REASON_MAX),
105
+ });
106
+ //# sourceMappingURL=members.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"members.js","sourceRoot":"","sources":["../../../src/domains/members/members.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema: the staff member directory\n *\n * GET /api/v1/members ?q&status&role&limit&cursor\n * GET /api/v1/members/:memberId one member, as staff see them\n * POST /api/v1/members/:memberId/suspension { reason } suspend\n * DELETE /api/v1/members/:memberId/suspension lift it\n *\n * Every staff role has a Members tab: a moderator looks up who wrote\n * something, a support agent finds the person who wrote in, a manager decides\n * on a suspension. What each sees is the server's call, per field: a\n * moderator gets no email (`email: null`), because finding a member's\n * address is support's job, not moderation's.\n *\n * A SUSPENDED member cannot use the product at all, on any device, from the\n * next request on (every call answers `ACCOUNT_SUSPENDED`); their posts stay\n * up unless moderated separately, and a staff role they were acting in is\n * switched off. Lifting it restores everything else.\n *\n * SEARCH (`q`) finds people by the start of their name or handle among listed\n * founders, or by their exact email among everyone (only for a role that may\n * see emails: a moderator's email search finds nothing). It returns up to\n * `limit` matches in one page (type more to narrow), not a pageable list. Who may suspend whom is the access policy's: never\n * yourself, never a superadmin, and a manager never a fellow manager.\n *\n * Each detail carries a `viewer` block of what the asking staff member may do\n * to THIS member, worked out by the server, so the screen shows only the\n * buttons that will work.\n *\n * @module founder-schema/domains/members\n */\n\nimport { z } from 'zod';\nimport { platformRoleSchema } from '../../access/roles.js';\nimport {\n emptyStringToUndefined,\n objectIdSchema,\n optionalTrimmedString,\n paginationQuerySchema,\n usernameSchema,\n} from '../../core/common.js';\nimport {\n MEMBER_SEARCH_MAX,\n SUSPENSION_REASON_MAX,\n SUSPENSION_REASON_MIN,\n} from '../../core/limits.js';\nimport { paginatedSchema } from '../../core/responses.js';\n\n/** What staff can see an account's state as. A deleted account is not listed. */\nexport const MEMBER_STATUS_VALUES = ['active', 'suspended'] as const;\nexport const memberStatusSchema = z.enum(MEMBER_STATUS_VALUES);\nexport type MemberStatus = z.infer<typeof memberStatusSchema>;\n\n/** `:memberId` in the paths above. */\nexport const memberParamsSchema = z.object({ memberId: objectIdSchema });\nexport type MemberParams = z.infer<typeof memberParamsSchema>;\n\n/**\n * The directory's query. `q` is a name or handle prefix, or a whole email\n * address (anything with an `@` is matched as an email, exactly). An emptied\n * box is \"no search\", not an error.\n */\nexport const memberDirectoryQuerySchema = paginationQuerySchema.extend({\n q: optionalTrimmedString(MEMBER_SEARCH_MAX),\n status: z.preprocess(emptyStringToUndefined, memberStatusSchema.optional()),\n /** Only people holding this staff role. */\n role: z.preprocess(emptyStringToUndefined, platformRoleSchema.optional()),\n});\nexport type MemberDirectoryQuery = z.infer<typeof memberDirectoryQuerySchema>;\nexport type MemberDirectoryQueryInput = z.input<typeof memberDirectoryQuerySchema>;\n\n/** One row of the directory. */\nexport const memberSummarySchema = z.object({\n id: objectIdSchema,\n displayName: z.string(),\n /** Null when they have no founder profile (never finished onboarding). */\n username: usernameSchema.nullable(),\n avatarUrl: z.url().nullable(),\n /** Null when the asking role may not see it. */\n email: z.string().nullable(),\n status: memberStatusSchema,\n /** Staff roles held, highest first. Empty for an ordinary member. */\n roles: z.array(platformRoleSchema),\n joinedAt: z.iso.datetime(),\n lastActiveAt: z.iso.datetime().nullable(),\n});\nexport type MemberSummary = z.infer<typeof memberSummarySchema>;\n\nexport const memberDirectoryPageSchema = paginatedSchema(memberSummarySchema);\nexport type MemberDirectoryPage = z.infer<typeof memberDirectoryPageSchema>;\n\n/** A suspension in force. */\nexport const suspensionSchema = z.object({\n since: z.iso.datetime(),\n reason: z.string(),\n /** Who suspended them, by name at the time. */\n by: z.object({ id: objectIdSchema, displayName: z.string() }).nullable(),\n});\nexport type Suspension = z.infer<typeof suspensionSchema>;\n\n/** One member, as staff see them. */\nexport const memberDetailSchema = memberSummarySchema.extend({\n headline: z.string().nullable(),\n /** Their city, as declared on their profile. */\n city: z.string().nullable(),\n counts: z.object({\n posts: z.number().int().min(0),\n polls: z.number().int().min(0),\n comments: z.number().int().min(0),\n /** Reports filed about their posts, polls and comments that are still up. */\n reportsAgainst: z.number().int().min(0),\n /** Tickets they have raised. */\n tickets: z.number().int().min(0),\n }),\n suspension: suspensionSchema.nullable(),\n /** What the asking staff member may do to this member. */\n viewer: z.object({\n canSuspend: z.boolean(),\n /** Roles the asker may grant or revoke on this member (never their own). */\n grantableRoles: z.array(platformRoleSchema),\n /** May clear this member's forgotten MPIN (superadmin, someone else). */\n canResetMpin: z.boolean(),\n }),\n /** Whether they have set an MPIN. Only meaningful for staff. */\n mpinSet: z.boolean(),\n});\nexport type MemberDetail = z.infer<typeof memberDetailSchema>;\n\n/** `POST /members/:memberId/suspension`. The reason is on record in the audit log. */\nexport const suspendMemberBodySchema = z.object({\n reason: z.string().trim().min(SUSPENSION_REASON_MIN).max(SUSPENSION_REASON_MAX),\n});\nexport type SuspendMemberBody = z.infer<typeof suspendMemberBodySchema>;\n"]}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * founder-schema: moderation
3
+ *
4
+ * GET /api/v1/moderation/queue ?limit&cursor
5
+ * what members reported and nobody has decided on, oldest report first
6
+ * GET /api/v1/moderation/content ?state&limit&cursor
7
+ * by state, newest first: visible is posts and polls by when posted;
8
+ * hidden and removed also hold comments (so a hidden comment can be
9
+ * found and put back), by when hidden or removed
10
+ * A decision that changes nothing (hiding what is already hidden, removing
11
+ * what its author deleted) leaves the reports open and writes nothing: for
12
+ * those, dismiss.
13
+ * POST /api/v1/moderation/:contentKind/:contentId/decision { action, note? }
14
+ * decide on one post, poll or comment; answers the item as it now is
15
+ *
16
+ * ONE ITEM PER THING, NOT PER REPORT. Five members reporting one post is one
17
+ * row with five reports, so a moderator decides once. The queue's order is
18
+ * the oldest open report first: whoever has waited longest is seen first, and
19
+ * a pile-on cannot jump the line.
20
+ *
21
+ * THE DECISIONS
22
+ * dismiss it stays up; its open reports close as dismissed
23
+ * hide out of every feed and thread, kept, restorable; reports actioned
24
+ * restore a hidden item goes back up
25
+ * remove deleted as its author deleting it would (counts move), not
26
+ * restorable from here; reports actioned
27
+ * Every decision on someone else's content is written to the audit log, with
28
+ * the note when there is one.
29
+ *
30
+ * A report's `note` was written by a member: untrusted text, shown as text.
31
+ *
32
+ * @module founder-schema/domains/moderation
33
+ */
34
+ import { z } from 'zod';
35
+ /** Where a post, poll or comment stands. */
36
+ export declare const MODERATION_STATE_VALUES: readonly ["visible", "hidden", "removed"];
37
+ export declare const moderationStateSchema: z.ZodEnum<{
38
+ visible: "visible";
39
+ hidden: "hidden";
40
+ removed: "removed";
41
+ }>;
42
+ export type ModerationState = z.infer<typeof moderationStateSchema>;
43
+ export declare const MODERATION_ACTION_VALUES: readonly ["dismiss", "hide", "restore", "remove"];
44
+ export declare const moderationActionSchema: z.ZodEnum<{
45
+ dismiss: "dismiss";
46
+ hide: "hide";
47
+ restore: "restore";
48
+ remove: "remove";
49
+ }>;
50
+ export type ModerationAction = z.infer<typeof moderationActionSchema>;
51
+ /** One post, poll or comment as moderation sees it. */
52
+ export declare const moderationItemSchema: z.ZodObject<{
53
+ target: z.ZodObject<{
54
+ kind: z.ZodEnum<{
55
+ post: "post";
56
+ poll: "poll";
57
+ comment: "comment";
58
+ }>;
59
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
60
+ }, z.core.$strip>;
61
+ content: z.ZodObject<{
62
+ kind: z.ZodEnum<{
63
+ post: "post";
64
+ poll: "poll";
65
+ }>;
66
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
67
+ }, z.core.$strip>;
68
+ excerpt: z.ZodString;
69
+ author: z.ZodObject<{
70
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
71
+ displayName: z.ZodString;
72
+ avatarUrl: z.ZodNullable<z.ZodURL>;
73
+ }, z.core.$strip>;
74
+ createdAt: z.ZodISODateTime;
75
+ state: z.ZodEnum<{
76
+ visible: "visible";
77
+ hidden: "hidden";
78
+ removed: "removed";
79
+ }>;
80
+ reports: z.ZodObject<{
81
+ open: z.ZodNumber;
82
+ total: z.ZodNumber;
83
+ reasons: z.ZodArray<z.ZodObject<{
84
+ reason: z.ZodEnum<{
85
+ other: "other";
86
+ spam: "spam";
87
+ harassment: "harassment";
88
+ misinformation: "misinformation";
89
+ off_topic: "off_topic";
90
+ impersonation: "impersonation";
91
+ }>;
92
+ count: z.ZodNumber;
93
+ }, z.core.$strip>>;
94
+ notes: z.ZodArray<z.ZodString>;
95
+ oldestOpenAt: z.ZodNullable<z.ZodISODateTime>;
96
+ }, z.core.$strip>;
97
+ viewer: z.ZodObject<{
98
+ canDismiss: z.ZodBoolean;
99
+ canHide: z.ZodBoolean;
100
+ canRestore: z.ZodBoolean;
101
+ canRemove: z.ZodBoolean;
102
+ }, z.core.$strip>;
103
+ }, z.core.$strip>;
104
+ export type ModerationItem = z.infer<typeof moderationItemSchema>;
105
+ export declare const moderationPageSchema: z.ZodObject<{
106
+ items: z.ZodArray<z.ZodObject<{
107
+ target: z.ZodObject<{
108
+ kind: z.ZodEnum<{
109
+ post: "post";
110
+ poll: "poll";
111
+ comment: "comment";
112
+ }>;
113
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
114
+ }, z.core.$strip>;
115
+ content: z.ZodObject<{
116
+ kind: z.ZodEnum<{
117
+ post: "post";
118
+ poll: "poll";
119
+ }>;
120
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
121
+ }, z.core.$strip>;
122
+ excerpt: z.ZodString;
123
+ author: z.ZodObject<{
124
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
125
+ displayName: z.ZodString;
126
+ avatarUrl: z.ZodNullable<z.ZodURL>;
127
+ }, z.core.$strip>;
128
+ createdAt: z.ZodISODateTime;
129
+ state: z.ZodEnum<{
130
+ visible: "visible";
131
+ hidden: "hidden";
132
+ removed: "removed";
133
+ }>;
134
+ reports: z.ZodObject<{
135
+ open: z.ZodNumber;
136
+ total: z.ZodNumber;
137
+ reasons: z.ZodArray<z.ZodObject<{
138
+ reason: z.ZodEnum<{
139
+ other: "other";
140
+ spam: "spam";
141
+ harassment: "harassment";
142
+ misinformation: "misinformation";
143
+ off_topic: "off_topic";
144
+ impersonation: "impersonation";
145
+ }>;
146
+ count: z.ZodNumber;
147
+ }, z.core.$strip>>;
148
+ notes: z.ZodArray<z.ZodString>;
149
+ oldestOpenAt: z.ZodNullable<z.ZodISODateTime>;
150
+ }, z.core.$strip>;
151
+ viewer: z.ZodObject<{
152
+ canDismiss: z.ZodBoolean;
153
+ canHide: z.ZodBoolean;
154
+ canRestore: z.ZodBoolean;
155
+ canRemove: z.ZodBoolean;
156
+ }, z.core.$strip>;
157
+ }, z.core.$strip>>;
158
+ nextCursor: z.ZodNullable<z.ZodString>;
159
+ }, z.core.$strip>;
160
+ export type ModerationPage = z.infer<typeof moderationPageSchema>;
161
+ /** `GET /moderation/content`. Visible when not said. */
162
+ export declare const moderationContentQuerySchema: z.ZodObject<{
163
+ limit: z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
164
+ cursor: z.ZodOptional<z.ZodString>;
165
+ state: z.ZodPreprocess<z.ZodDefault<z.ZodEnum<{
166
+ visible: "visible";
167
+ hidden: "hidden";
168
+ removed: "removed";
169
+ }>>, unknown>;
170
+ }, z.core.$strip>;
171
+ export type ModerationContentQuery = z.infer<typeof moderationContentQuerySchema>;
172
+ export type ModerationContentQueryInput = z.input<typeof moderationContentQuerySchema>;
173
+ /** `POST /moderation/:contentKind/:contentId/decision`. */
174
+ export declare const moderationDecisionBodySchema: z.ZodObject<{
175
+ action: z.ZodEnum<{
176
+ dismiss: "dismiss";
177
+ hide: "hide";
178
+ restore: "restore";
179
+ remove: "remove";
180
+ }>;
181
+ note: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
182
+ }, z.core.$strip>;
183
+ export type ModerationDecisionBody = z.infer<typeof moderationDecisionBodySchema>;
184
+ //# sourceMappingURL=moderation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moderation.d.ts","sourceRoot":"","sources":["../../../src/domains/moderation/moderation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,4CAA4C;AAC5C,eAAO,MAAM,uBAAuB,2CAA4C,CAAC;AACjF,eAAO,MAAM,qBAAqB;;;;EAAkC,CAAC;AACrE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,wBAAwB,mDAAoD,CAAC;AAC1F,eAAO,MAAM,sBAAsB;;;;;EAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,uDAAuD;AACvD,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgC/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAwC,CAAC;AAC1E,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,wDAAwD;AACxD,eAAO,MAAM,4BAA4B;;;;;;;;iBAEvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAClF,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAEvF,2DAA2D;AAC3D,eAAO,MAAM,4BAA4B;;;;;;;;iBAIvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * founder-schema: moderation
3
+ *
4
+ * GET /api/v1/moderation/queue ?limit&cursor
5
+ * what members reported and nobody has decided on, oldest report first
6
+ * GET /api/v1/moderation/content ?state&limit&cursor
7
+ * by state, newest first: visible is posts and polls by when posted;
8
+ * hidden and removed also hold comments (so a hidden comment can be
9
+ * found and put back), by when hidden or removed
10
+ * A decision that changes nothing (hiding what is already hidden, removing
11
+ * what its author deleted) leaves the reports open and writes nothing: for
12
+ * those, dismiss.
13
+ * POST /api/v1/moderation/:contentKind/:contentId/decision { action, note? }
14
+ * decide on one post, poll or comment; answers the item as it now is
15
+ *
16
+ * ONE ITEM PER THING, NOT PER REPORT. Five members reporting one post is one
17
+ * row with five reports, so a moderator decides once. The queue's order is
18
+ * the oldest open report first: whoever has waited longest is seen first, and
19
+ * a pile-on cannot jump the line.
20
+ *
21
+ * THE DECISIONS
22
+ * dismiss it stays up; its open reports close as dismissed
23
+ * hide out of every feed and thread, kept, restorable; reports actioned
24
+ * restore a hidden item goes back up
25
+ * remove deleted as its author deleting it would (counts move), not
26
+ * restorable from here; reports actioned
27
+ * Every decision on someone else's content is written to the audit log, with
28
+ * the note when there is one.
29
+ *
30
+ * A report's `note` was written by a member: untrusted text, shown as text.
31
+ *
32
+ * @module founder-schema/domains/moderation
33
+ */
34
+ import { z } from 'zod';
35
+ import { emptyStringToUndefined, objectIdSchema, optionalTrimmedString, paginationQuerySchema, } from '../../core/common.js';
36
+ import { MODERATION_NOTE_MAX } from '../../core/limits.js';
37
+ import { paginatedSchema } from '../../core/responses.js';
38
+ import { contentKindSchema, engagementTargetKindSchema, reportReasonSchema } from '../feed/feed.js';
39
+ /** Where a post, poll or comment stands. */
40
+ export const MODERATION_STATE_VALUES = ['visible', 'hidden', 'removed'];
41
+ export const moderationStateSchema = z.enum(MODERATION_STATE_VALUES);
42
+ export const MODERATION_ACTION_VALUES = ['dismiss', 'hide', 'restore', 'remove'];
43
+ export const moderationActionSchema = z.enum(MODERATION_ACTION_VALUES);
44
+ const count = z.number().int().min(0);
45
+ /** One post, poll or comment as moderation sees it. */
46
+ export const moderationItemSchema = z.object({
47
+ target: z.object({ kind: engagementTargetKindSchema, id: objectIdSchema }),
48
+ /** The post or poll itself, or the one a comment is on: where it is read in the app. */
49
+ content: z.object({ kind: contentKindSchema, id: objectIdSchema }),
50
+ /** Its first words (a poll's question), cut to `MODERATION_EXCERPT_MAX`. */
51
+ excerpt: z.string(),
52
+ author: z.object({
53
+ id: objectIdSchema,
54
+ displayName: z.string(),
55
+ avatarUrl: z.url().nullable(),
56
+ }),
57
+ createdAt: z.iso.datetime(),
58
+ state: moderationStateSchema,
59
+ reports: z.object({
60
+ /** Reports nobody has decided on. */
61
+ open: count,
62
+ /** Every report ever filed about it. */
63
+ total: count,
64
+ /** Why it was reported, among the open ones, most-given first. */
65
+ reasons: z.array(z.object({ reason: reportReasonSchema, count })),
66
+ /** The reporters' notes, among the open ones, newest first (a few, not all). */
67
+ notes: z.array(z.string()),
68
+ /** When the oldest open report was filed. Null when none is open. */
69
+ oldestOpenAt: z.iso.datetime().nullable(),
70
+ }),
71
+ /** What the asking staff member may do to it now. */
72
+ viewer: z.object({
73
+ canDismiss: z.boolean(),
74
+ canHide: z.boolean(),
75
+ canRestore: z.boolean(),
76
+ canRemove: z.boolean(),
77
+ }),
78
+ });
79
+ export const moderationPageSchema = paginatedSchema(moderationItemSchema);
80
+ /** `GET /moderation/content`. Visible when not said. */
81
+ export const moderationContentQuerySchema = paginationQuerySchema.extend({
82
+ state: z.preprocess(emptyStringToUndefined, moderationStateSchema.default('visible')),
83
+ });
84
+ /** `POST /moderation/:contentKind/:contentId/decision`. */
85
+ export const moderationDecisionBodySchema = z.object({
86
+ action: moderationActionSchema,
87
+ /** Why, for the audit log. Optional: an obvious spam removal needs no essay. */
88
+ note: optionalTrimmedString(MODERATION_NOTE_MAX),
89
+ });
90
+ //# sourceMappingURL=moderation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moderation.js","sourceRoot":"","sources":["../../../src/domains/moderation/moderation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema: moderation\n *\n * GET /api/v1/moderation/queue ?limit&cursor\n * what members reported and nobody has decided on, oldest report first\n * GET /api/v1/moderation/content ?state&limit&cursor\n * by state, newest first: visible is posts and polls by when posted;\n * hidden and removed also hold comments (so a hidden comment can be\n * found and put back), by when hidden or removed\n * A decision that changes nothing (hiding what is already hidden, removing\n * what its author deleted) leaves the reports open and writes nothing: for\n * those, dismiss.\n * POST /api/v1/moderation/:contentKind/:contentId/decision { action, note? }\n * decide on one post, poll or comment; answers the item as it now is\n *\n * ONE ITEM PER THING, NOT PER REPORT. Five members reporting one post is one\n * row with five reports, so a moderator decides once. The queue's order is\n * the oldest open report first: whoever has waited longest is seen first, and\n * a pile-on cannot jump the line.\n *\n * THE DECISIONS\n * dismiss it stays up; its open reports close as dismissed\n * hide out of every feed and thread, kept, restorable; reports actioned\n * restore a hidden item goes back up\n * remove deleted as its author deleting it would (counts move), not\n * restorable from here; reports actioned\n * Every decision on someone else's content is written to the audit log, with\n * the note when there is one.\n *\n * A report's `note` was written by a member: untrusted text, shown as text.\n *\n * @module founder-schema/domains/moderation\n */\n\nimport { z } from 'zod';\nimport {\n emptyStringToUndefined,\n objectIdSchema,\n optionalTrimmedString,\n paginationQuerySchema,\n} from '../../core/common.js';\nimport { MODERATION_NOTE_MAX } from '../../core/limits.js';\nimport { paginatedSchema } from '../../core/responses.js';\nimport { contentKindSchema, engagementTargetKindSchema, reportReasonSchema } from '../feed/feed.js';\n\n/** Where a post, poll or comment stands. */\nexport const MODERATION_STATE_VALUES = ['visible', 'hidden', 'removed'] as const;\nexport const moderationStateSchema = z.enum(MODERATION_STATE_VALUES);\nexport type ModerationState = z.infer<typeof moderationStateSchema>;\n\nexport const MODERATION_ACTION_VALUES = ['dismiss', 'hide', 'restore', 'remove'] as const;\nexport const moderationActionSchema = z.enum(MODERATION_ACTION_VALUES);\nexport type ModerationAction = z.infer<typeof moderationActionSchema>;\n\nconst count = z.number().int().min(0);\n\n/** One post, poll or comment as moderation sees it. */\nexport const moderationItemSchema = z.object({\n target: z.object({ kind: engagementTargetKindSchema, id: objectIdSchema }),\n /** The post or poll itself, or the one a comment is on: where it is read in the app. */\n content: z.object({ kind: contentKindSchema, id: objectIdSchema }),\n /** Its first words (a poll's question), cut to `MODERATION_EXCERPT_MAX`. */\n excerpt: z.string(),\n author: z.object({\n id: objectIdSchema,\n displayName: z.string(),\n avatarUrl: z.url().nullable(),\n }),\n createdAt: z.iso.datetime(),\n state: moderationStateSchema,\n reports: z.object({\n /** Reports nobody has decided on. */\n open: count,\n /** Every report ever filed about it. */\n total: count,\n /** Why it was reported, among the open ones, most-given first. */\n reasons: z.array(z.object({ reason: reportReasonSchema, count })),\n /** The reporters' notes, among the open ones, newest first (a few, not all). */\n notes: z.array(z.string()),\n /** When the oldest open report was filed. Null when none is open. */\n oldestOpenAt: z.iso.datetime().nullable(),\n }),\n /** What the asking staff member may do to it now. */\n viewer: z.object({\n canDismiss: z.boolean(),\n canHide: z.boolean(),\n canRestore: z.boolean(),\n canRemove: z.boolean(),\n }),\n});\nexport type ModerationItem = z.infer<typeof moderationItemSchema>;\n\nexport const moderationPageSchema = paginatedSchema(moderationItemSchema);\nexport type ModerationPage = z.infer<typeof moderationPageSchema>;\n\n/** `GET /moderation/content`. Visible when not said. */\nexport const moderationContentQuerySchema = paginationQuerySchema.extend({\n state: z.preprocess(emptyStringToUndefined, moderationStateSchema.default('visible')),\n});\nexport type ModerationContentQuery = z.infer<typeof moderationContentQuerySchema>;\nexport type ModerationContentQueryInput = z.input<typeof moderationContentQuerySchema>;\n\n/** `POST /moderation/:contentKind/:contentId/decision`. */\nexport const moderationDecisionBodySchema = z.object({\n action: moderationActionSchema,\n /** Why, for the audit log. Optional: an obvious spam removal needs no essay. */\n note: optionalTrimmedString(MODERATION_NOTE_MAX),\n});\nexport type ModerationDecisionBody = z.infer<typeof moderationDecisionBodySchema>;\n"]}