@bunizao/contracts 0.10.0 → 0.12.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.
package/dist/admin.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { ClientFingerprint, CommentAuthAtWrite, CommentClaimMethod, CommentStatus, Interaction } from './comments';
1
+ import type { ClientFingerprint, CommentAuthAtWrite, CommentClaimMethod, CommentsMode, CommentStatus, CommentSurface, Interaction } from './comments';
2
+ import type { MessageLocale, MessageState } from './messages';
2
3
  import type { DeliveryMode, NotifyAuditEventType, NotifyChannel, SubscriberRecord, SubscriberStatus } from './notify';
3
4
  export interface SubscriberChannelCount {
4
5
  total: number;
@@ -108,10 +109,30 @@ export interface AdminCommentRecord {
108
109
  country: string | null;
109
110
  createdAt: string;
110
111
  editedAt: string | null;
111
- /** Filled from the post registry; null when it could not say. */
112
+ /** Filled from the post registry; null when it could not say. A mood
113
+ post has no slug: `postSlug` is always null for one, and `postPath`
114
+ carries the link. */
112
115
  postTitle: string | null;
113
116
  postSlug: string | null;
114
117
  actor: AdminCommentActor;
118
+ surface?: CommentSurface;
119
+ /** When the latest verdict, automatic or the owner's, was written. */
120
+ moderatedAt?: string | null;
121
+ updatedAt?: string;
122
+ /** A deleted row the owner can still restore: the last instant it can.
123
+ Null for every other row. */
124
+ restorableUntil?: string | null;
125
+ /** When the owner pinned this root to the top of its post. A pin on a
126
+ row that is no longer published stays set and shows again with it. */
127
+ pinnedAt?: string | null;
128
+ /** When the owner locked replies under this root. */
129
+ lockedAt?: string | null;
130
+ /** The post's page on the public site, as a path: `/blog/<slug>` or
131
+ `/mood/<id>`. Null when the post could not be looked up. */
132
+ postPath?: string | null;
133
+ /** The owner wrote it: the public `CommentAuthor.byAuthor` rule, a row
134
+ written signed in by the reader whose address is the owner's. */
135
+ byAuthor?: boolean;
115
136
  }
116
137
  /** One page of the moderation queue. */
117
138
  export interface AdminCommentListResult {
@@ -121,6 +142,274 @@ export interface AdminCommentListResult {
121
142
  /** `offset + comments.length`, or null on the last page. */
122
143
  nextOffset: number | null;
123
144
  }
145
+ /** The numbers drawn above the queue. */
146
+ export interface AdminCommentSummary {
147
+ byStatus: Record<AdminCommentStatus, number>;
148
+ /** Comments written in the last 24 hours. */
149
+ today: number;
150
+ oldestHeldAt: string | null;
151
+ reasons: Array<{
152
+ reason: string;
153
+ count: number;
154
+ }>;
155
+ /** `slug` and `path` as on AdminCommentRecord's `postSlug` and
156
+ `postPath`; `path` is optional only for the portal's demo api. */
157
+ topPosts: Array<{
158
+ surface: CommentSurface;
159
+ postId: string;
160
+ count: number;
161
+ title: string | null;
162
+ slug: string | null;
163
+ path?: string | null;
164
+ }>;
165
+ daily: Array<{
166
+ date: string;
167
+ count: number;
168
+ }>;
169
+ }
170
+ export type AdminCommentQueueSort = 'newest' | 'oldest' | 'risk';
171
+ /** `akismet` alone, `llm` for Akismet plus the second opinion, `none` for
172
+ rows no model judged. */
173
+ export type AdminCommentModelFilter = 'akismet' | 'llm' | 'none';
174
+ /** The created-at window a queue page was read over. Search and the risk
175
+ sort always read one (30 days unless asked, 90 at most). */
176
+ export interface AdminCommentQueueWindow {
177
+ from: string | null;
178
+ to: string | null;
179
+ /** True when the window asked for was wider than 90 days and was cut to
180
+ the 90 before its end; `from` is then where the read started. */
181
+ clamped?: boolean;
182
+ }
183
+ /** GET /admin/comments. */
184
+ export interface AdminCommentQueueResult extends AdminCommentListResult {
185
+ summary: AdminCommentSummary;
186
+ window: AdminCommentQueueWindow | null;
187
+ }
188
+ export type AdminCommentAction = 'approve' | 'hide' | 'reject' | 'delete' | 'restore';
189
+ export declare const ADMIN_COMMENT_REJECT_REASONS: readonly ['spam', 'promotional', 'abuse', 'off_topic', 'personal_info'];
190
+ export type AdminCommentRejectReason = (typeof ADMIN_COMMENT_REJECT_REASONS)[number];
191
+ /** What one act did to one row. `already_*` is a repeat that changed
192
+ nothing; `not_restorable` is a deleted row past its window or removed in
193
+ a way restore cannot undo; `not_available` is a row gone, or in a state
194
+ the act cannot leave. */
195
+ export type AdminCommentActionResult = 'approved' | 'hidden' | 'rejected' | 'deleted' | 'restored' | 'already_approved' | 'already_hidden' | 'already_rejected' | 'already_deleted' | 'not_restorable' | 'not_available';
196
+ /** POST /admin/comments/:id. `reason` is required for `reject`. */
197
+ export interface AdminCommentActionRequest {
198
+ action: AdminCommentAction;
199
+ reason?: AdminCommentRejectReason;
200
+ }
201
+ export interface AdminCommentActionResponse {
202
+ result: AdminCommentActionResult;
203
+ comment: {
204
+ id: string;
205
+ status: AdminCommentStatus;
206
+ };
207
+ }
208
+ /** POST /admin/comments/bulk: at most 20 ids, applied as one batch. */
209
+ export interface AdminCommentBulkRequest {
210
+ ids: string[];
211
+ action: AdminCommentAction;
212
+ reason?: AdminCommentRejectReason;
213
+ }
214
+ /** One result per id, in the order sent. `status` is the row's status
215
+ after the call whatever the result, so a `not_available` row says where
216
+ it actually is; it is null only when no row has that id. */
217
+ export interface AdminCommentBulkResponse {
218
+ results: Array<{
219
+ id: string;
220
+ result: AdminCommentActionResult;
221
+ status: AdminCommentStatus | null;
222
+ }>;
223
+ }
224
+ /** POST /admin/comments/:id/reply. */
225
+ export interface AdminCommentReplyRequest {
226
+ body: string;
227
+ /** A UUID made once per reply and sent again on every retry: it becomes
228
+ the reply's id, so a retry answers the first reply instead of posting
229
+ a second. The same id with another body or comment is a 409
230
+ `reply_id_collision`. Omitted, every call posts a new reply. */
231
+ replyId?: string;
232
+ }
233
+ /** `parentId` is the thread root the reply joined. */
234
+ export interface AdminCommentReplyResponse {
235
+ comment: {
236
+ id: string;
237
+ parentId: string;
238
+ status: AdminCommentStatus;
239
+ };
240
+ }
241
+ /** The site-wide lockdown: every anonymous comment waits for an email
242
+ until `until`. */
243
+ export interface AdminCommentLockdown {
244
+ reason: string;
245
+ since: string;
246
+ until: string;
247
+ by: 'auto' | 'owner';
248
+ }
249
+ /** POST /admin/comments/lockdown: 1 to 10080 minutes, note up to 200
250
+ characters. */
251
+ export interface AdminCommentLockdownRequest {
252
+ minutes: number;
253
+ note?: string;
254
+ }
255
+ /** GET, POST and DELETE /admin/comments/lockdown. */
256
+ export interface AdminCommentLockdownResponse {
257
+ lockdown: AdminCommentLockdown | null;
258
+ }
259
+ /** A site-wide mode: every post at least this strict. */
260
+ export type AdminCommentSiteMode = Exclude<CommentsMode, 'open'>;
261
+ /** The owner's standing site-wide comment switches, on top of every post's
262
+ own mode (tags, then the portal's per-post override). Neither expires. */
263
+ export interface AdminCommentSitePolicy {
264
+ /** Null: each post's own mode decides. Otherwise every post, on every
265
+ surface, is at least this strict (open < readonly < off); a post that
266
+ is already stricter stays so. */
267
+ mode: AdminCommentSiteMode | null;
268
+ /** When `mode` was last set; null while it is null. */
269
+ modeSince: string | null;
270
+ /** Every anonymous comment waits for its writer to confirm an email, as
271
+ in a lockdown, until switched off. A writer who gives no address is
272
+ asked for one. Signed-in readers post as usual. */
273
+ requireEmail: boolean;
274
+ /** When `requireEmail` was turned on; null while it is off. */
275
+ requireEmailSince: string | null;
276
+ }
277
+ /** PUT /admin/comments/site-policy: the fields to change, at least one. */
278
+ export interface AdminCommentSitePolicyRequest {
279
+ mode?: AdminCommentSiteMode | null;
280
+ requireEmail?: boolean;
281
+ }
282
+ /** GET and PUT /admin/comments/site-policy. */
283
+ export interface AdminCommentSitePolicyResponse {
284
+ policy: AdminCommentSitePolicy;
285
+ }
286
+ /** PUT and DELETE /admin/comments/:id/pin. `replaced` is the comment the
287
+ PUT took the post's pin from, or null; always null on DELETE. */
288
+ export interface AdminCommentPinResponse {
289
+ comment: {
290
+ id: string;
291
+ surface: CommentSurface;
292
+ postId: string;
293
+ pinnedAt: string | null;
294
+ };
295
+ replaced: string | null;
296
+ }
297
+ /** PUT and DELETE /admin/comments/:id/lock. `comment` is the thread root,
298
+ which is not the id in the path when that was a reply. */
299
+ export interface AdminCommentLockResponse {
300
+ comment: {
301
+ id: string;
302
+ surface: CommentSurface;
303
+ postId: string;
304
+ lockedAt: string | null;
305
+ };
306
+ }
307
+ /** One post's comment mode as the portal shows it: the portal's override
308
+ beside what the post's tags give (the site default for a mood post). */
309
+ export interface AdminCommentModeState {
310
+ surface: CommentSurface;
311
+ postId: string;
312
+ /** Null when the tags decide. */
313
+ override: CommentsMode | null;
314
+ /** Null when the post could not be looked up. */
315
+ tagMode: CommentsMode | null;
316
+ /** What readers get: `override`, else `tagMode`. */
317
+ effectiveMode: CommentsMode | null;
318
+ /** When the override was last set; null without one. */
319
+ updatedAt: string | null;
320
+ title: string | null;
321
+ /** Blog only; null for a mood post, which has no slug. */
322
+ slug: string | null;
323
+ /** The post's page as a path, `/blog/<slug>` or `/mood/<id>`; null when
324
+ the post could not be looked up. Optional only for the portal's demo
325
+ api; site-api sends it. */
326
+ path?: string | null;
327
+ }
328
+ /** GET /admin/comment-modes: every overridden post. */
329
+ export interface AdminCommentModeListResult {
330
+ modes: AdminCommentModeState[];
331
+ }
332
+ /** PUT /admin/comment-modes/:surface/:postId. */
333
+ export interface AdminCommentModeRequest {
334
+ mode: CommentsMode;
335
+ }
336
+ /** GET, PUT and DELETE /admin/comment-modes/:surface/:postId. */
337
+ export interface AdminCommentModeResponse {
338
+ mode: AdminCommentModeState;
339
+ }
340
+ /** One message sent through /message, as the owner's inbox shows it. The
341
+ body is private: it never leaves the admin routes. */
342
+ export interface AdminOwnerMessage {
343
+ id: string;
344
+ state: MessageState;
345
+ displayName: string;
346
+ body: string;
347
+ locale: MessageLocale;
348
+ /** The reader the address resolves to. It proves who wrote the message
349
+ only when `authAtWrite` is `verified`. */
350
+ readerId: string | null;
351
+ /** `verified` when a signed-in reader's session sent it; `anonymous` when
352
+ the address was typed. Missing on older responses means unknown. */
353
+ authAtWrite?: CommentAuthAtWrite;
354
+ /** Null when the sender left no address. */
355
+ emailHash: string | null;
356
+ /** Why the risk stack filed it as spam, and which model said so. */
357
+ spamNote: string | null;
358
+ spamModel: string | null;
359
+ repliedAt: string | null;
360
+ country: string | null;
361
+ createdAt: string;
362
+ updatedAt: string;
363
+ }
364
+ /** GET /admin/messages `state`: one state, or `inbox` for new, read and
365
+ replied together -- everything not archived or spam. */
366
+ export type AdminOwnerMessageFilter = MessageState | 'inbox';
367
+ /** GET /admin/messages. `counts` covers the whole inbox whatever the
368
+ filter; `total` is the filtered count. */
369
+ export interface AdminOwnerMessageListResult {
370
+ messages: AdminOwnerMessage[];
371
+ counts: Record<MessageState, number>;
372
+ total: number;
373
+ nextOffset: number | null;
374
+ }
375
+ /** Whether a reply would reach the sender, and if not, why. */
376
+ export type AdminOwnerMessageReplyability = 'ok' | 'no_address' | 'unverified' | 'suppressed';
377
+ /** GET /admin/messages/:id. `history` is what else the same address sent,
378
+ newest first; `sender.email` is set only when a reply can reach it. */
379
+ export interface AdminOwnerMessageDetail {
380
+ message: AdminOwnerMessage;
381
+ sender: {
382
+ replyable: AdminOwnerMessageReplyability;
383
+ email: string | null;
384
+ readerId: string | null;
385
+ };
386
+ history: AdminOwnerMessage[];
387
+ /** The sender's keys and signals, in the comment writer's shape; `cluster`
388
+ counts comments, reactions and messages. Absent from a site-api that
389
+ predates it. */
390
+ actor?: AdminCommentActor;
391
+ }
392
+ export type AdminOwnerMessageAction = 'read' | 'archive' | 'unarchive' | 'spam' | 'unspam';
393
+ /** POST /admin/messages/:id. */
394
+ export interface AdminOwnerMessageActionRequest {
395
+ action: AdminOwnerMessageAction;
396
+ }
397
+ /** `changed` is false when the message was already past the act; `message`
398
+ is then what it is now. */
399
+ export interface AdminOwnerMessageActionResponse {
400
+ message: AdminOwnerMessage;
401
+ changed: boolean;
402
+ }
403
+ /** POST /admin/messages/:id/reply: 2 to 4000 characters. */
404
+ export interface AdminOwnerMessageReplyRequest {
405
+ body: string;
406
+ }
407
+ /** `recipientEmail` is masked. */
408
+ export interface AdminOwnerMessageReplyResponse {
409
+ message: AdminOwnerMessage;
410
+ recipientName: string;
411
+ recipientEmail: string;
412
+ }
124
413
  /** What a ban can hold onto. Values are hashes (or the ASN as text) for
125
414
  every kind but the two domain kinds, which are stored raw -- a domain is
126
415
  not personal data. `client_fp` matches either the exact or the stable
@@ -205,6 +494,8 @@ export interface AdminClusterCount {
205
494
  comments: number;
206
495
  held: number;
207
496
  reactions: number;
497
+ /** Owner messages sharing the key. Absent from a site-api that predates it. */
498
+ messages?: number;
208
499
  }
209
500
  /** Where a row came from, as far as the write path could tell. Shared by
210
501
  comments and reactions; a reaction's block has no email, no link domains
@@ -293,8 +584,21 @@ export interface AdminBanInput {
293
584
  note?: string;
294
585
  expiresAt?: string | null;
295
586
  purge?: boolean;
587
+ /** Keys a purge also matches without banning them, such as the writer's
588
+ device fingerprint. Ignored unless `purge` is set. */
589
+ sweepKeys?: Array<{
590
+ type: AdminBanKeyType;
591
+ value: string;
592
+ }>;
593
+ /** The comment the ban was raised from. It is removed in the same
594
+ restorable operation whatever `purge` says, and its writer is the one
595
+ signed-in reader a purge may remove published comments from. */
596
+ removeCommentId?: string | null;
296
597
  revokeReaderId?: string | null;
297
598
  }
599
+ /** `POST /admin/bans/preview`: the scope `POST /admin/bans` would reach with
600
+ the same fields. */
601
+ export type AdminBanPreviewInput = Pick<AdminBanInput, 'keys' | 'revokeReaderId' | 'purge' | 'sweepKeys' | 'removeCommentId'>;
298
602
  export interface AdminBanResult {
299
603
  bans: AdminBan[];
300
604
  purged: {
@@ -308,10 +612,21 @@ export interface AdminBanPreview {
308
612
  sessions: number;
309
613
  comments: Record<AdminCommentStatus | 'total', number>;
310
614
  reactions: number;
615
+ /** What the ban would really remove. The optional fields are absent from
616
+ a site-api that predates them. */
311
617
  purge: {
312
618
  comments: number;
313
619
  reactions: number;
620
+ /** Of `comments`, the ones readers can see now. */
621
+ published?: number;
622
+ /** Distinct sessions the removed rows came from. */
623
+ sessions?: number;
624
+ /** Reader accounts, other than the target comment's own, that lose a row. */
625
+ otherAccounts?: number;
314
626
  };
627
+ /** Published comments a purge leaves alone because another signed-in
628
+ reader wrote them. Absent from a site-api that predates the rule. */
629
+ spared?: number;
315
630
  windowDays: 90;
316
631
  purgeLimit: number;
317
632
  purgeAllowed: boolean;
@@ -346,8 +661,13 @@ export interface AdminBanRestoreResult {
346
661
  export interface AdminBanListResult {
347
662
  bans: AdminBan[];
348
663
  }
664
+ /** A reader a ban revoked -- `GET /admin/readers/revoked`. `emailHash` is
665
+ the value of an `email` ban key, for finding the key bans that outlive
666
+ a restore. `updatedAt` is when the row last changed: the revocation,
667
+ unless something touched the reader since. */
349
668
  export interface AdminBannedReader {
350
669
  readerId: string;
670
+ emailHash: string;
351
671
  email: string;
352
672
  displayName: string | null;
353
673
  updatedAt: string;
@@ -355,8 +675,31 @@ export interface AdminBannedReader {
355
675
  export interface AdminBannedReaderListResult {
356
676
  readers: AdminBannedReader[];
357
677
  }
678
+ /** POST /admin/readers/:readerId/restore. */
679
+ export interface AdminReaderRestoreResult {
680
+ readerId: string;
681
+ restoredAt: string;
682
+ }
358
683
  /** Everything about one key in one response --
359
684
  `GET /admin/sources/:type/:value`. */
685
+ export interface AdminSourceAddress {
686
+ emailHash: string;
687
+ /** Plaintext while a row still holds it. */
688
+ email: string | null;
689
+ confirmed: boolean;
690
+ comments: number;
691
+ messages: number;
692
+ lastSeenAt: string;
693
+ }
694
+ export interface AdminSourceDevice {
695
+ clientFp: string;
696
+ browser: string | null;
697
+ os: string | null;
698
+ comments: number;
699
+ reactions: number;
700
+ messages: number;
701
+ lastSeenAt: string;
702
+ }
360
703
  export interface AdminSourceProfile {
361
704
  key: {
362
705
  type: AdminSourceKeyType;
@@ -406,6 +749,19 @@ export interface AdminSourceProfile {
406
749
  held: number;
407
750
  reactions: number;
408
751
  };
752
+ /** Owner messages under this key. Absent from a site-api that predates it. */
753
+ messages?: {
754
+ total: number;
755
+ byState: Record<MessageState, number>;
756
+ /** Newest 50. */
757
+ rows: AdminOwnerMessage[];
758
+ };
759
+ /** Addresses written under this key, newest first, at most 10. `confirmed`
760
+ means a signed-in reader wrote with it; a typed address is evidence of
761
+ what this key claimed, never a link to that reader. */
762
+ addresses?: AdminSourceAddress[];
763
+ /** Device fingerprints seen under this key, newest first, at most 10. */
764
+ devices?: AdminSourceDevice[];
409
765
  /** Writes per hour over the source's last 7 days. */
410
766
  hourly: Array<{
411
767
  hour: string;
@@ -640,3 +996,46 @@ export interface AdminReactionInsights {
640
996
  sessions: number;
641
997
  }>;
642
998
  }
999
+ /** Reject and restore are logged as `comment.moderate`, with the status
1000
+ they left the row in. */
1001
+ export declare const ADMIN_ACTIVITY_EVENTS: readonly ['comment.create', 'comment.edit', 'comment.remove', 'comment.moderate', 'comment.approve', 'comment.hide', 'comment.delete', 'reaction.add', 'reaction.remove'];
1002
+ export type AdminActivityEvent = (typeof ADMIN_ACTIVITY_EVENTS)[number];
1003
+ export type AdminActivityActor = 'reader' | 'model' | 'owner';
1004
+ export type AdminActivitySource = 'web' | 'portal' | 'telegram' | 'cron';
1005
+ export type AdminActivityTargetType = 'comment' | 'post';
1006
+ export type AdminActivityFamily = 'comments' | 'reactions';
1007
+ export interface AdminActivityRecord {
1008
+ id: string;
1009
+ createdAt: string;
1010
+ event: AdminActivityEvent;
1011
+ actor: AdminActivityActor;
1012
+ source: AdminActivitySource;
1013
+ targetType: AdminActivityTargetType;
1014
+ targetId: string;
1015
+ postId: string | null;
1016
+ postTitle: string | null;
1017
+ postSlug: string | null;
1018
+ displayName: string | null;
1019
+ readerId: string | null;
1020
+ anonymous: boolean;
1021
+ emoji: string | null;
1022
+ status: string | null;
1023
+ reason: string | null;
1024
+ note: string | null;
1025
+ }
1026
+ export interface AdminActivitySummary {
1027
+ byEvent: Record<AdminActivityEvent, number>;
1028
+ today: number;
1029
+ reactionsNet: number;
1030
+ daily: Array<{
1031
+ date: string;
1032
+ comments: number;
1033
+ reactions: number;
1034
+ }>;
1035
+ }
1036
+ export interface AdminActivityListResult {
1037
+ summary: AdminActivitySummary;
1038
+ entries: AdminActivityRecord[];
1039
+ total: number;
1040
+ nextOffset: number | null;
1041
+ }
package/dist/admin.js CHANGED
@@ -0,0 +1,17 @@
1
+ // src/admin.ts
2
+ var ADMIN_COMMENT_REJECT_REASONS = ["spam", "promotional", "abuse", "off_topic", "personal_info"];
3
+ var ADMIN_ACTIVITY_EVENTS = [
4
+ "comment.create",
5
+ "comment.edit",
6
+ "comment.remove",
7
+ "comment.moderate",
8
+ "comment.approve",
9
+ "comment.hide",
10
+ "comment.delete",
11
+ "reaction.add",
12
+ "reaction.remove"
13
+ ];
14
+ export {
15
+ ADMIN_ACTIVITY_EVENTS,
16
+ ADMIN_COMMENT_REJECT_REASONS
17
+ };
@@ -64,7 +64,8 @@ export interface ReaderMe {
64
64
  has been handed out -- the client then draws from the name. */
65
65
  avatarSeed: number | null;
66
66
  notifyReplies: boolean;
67
- /** Whether this address holds an active newsletter subscription. */
67
+ /** Whether this address holds an active subscription that includes the blog
68
+ channel -- the reader switch is "latest posts", so mood alone reads false. */
68
69
  subscribed: boolean;
69
70
  }
70
71
  export interface ReaderMeResult {
@@ -348,14 +349,31 @@ export interface Comment {
348
349
  /** Soft-deleted but kept as a shape-preserving placeholder because a reply
349
350
  hangs underneath it. `body`/`author` are empty on a tombstone. */
350
351
  tombstone: boolean;
352
+ /** The owner pinned this root above the thread. At most one per post;
353
+ absent on every other row, replies included, and on every row from a
354
+ server that predates pins. */
355
+ pinned?: boolean;
356
+ /** The owner closed this root's thread to new replies; a reply under it
357
+ is refused with `403 thread_locked`. Set on the root only; absent
358
+ means open. */
359
+ locked?: boolean;
351
360
  }
352
361
  export interface CommentListResult {
362
+ /** The page's roots, newest first, then their replies. The first page
363
+ (no `before`) puts the post's pinned root ahead of the other roots when
364
+ it has one, on top of `limit` roots; no later page repeats it. */
353
365
  comments: Comment[];
354
366
  hasMore: boolean;
355
367
  /** Cursor for the next page, or null when `hasMore` is false. */
356
368
  nextBefore: string | null;
357
369
  /** Published comments on the post (excludes held/rejected/deleted). */
358
370
  total: number;
371
+ /** The post's policy when the owner overrode its mode in the portal or a
372
+ site-wide switch applies, on the first page only. Absent means the post
373
+ follows its tags, which the page was drawn from -- nothing to
374
+ reconcile. The page acts on `mode`, and on `requireVerifiedEmail` by
375
+ drawing the address as required. */
376
+ policy?: CommentPolicy;
359
377
  }
360
378
  export interface CommentCreateInput {
361
379
  /** Defaults to `blog`. `mood` requires the post to have a linked
@@ -436,8 +454,9 @@ export interface CommentPolicy {
436
454
  Independent of `mode`: a post can take reactions with comments off, and
437
455
  an open thread can refuse them. */
438
456
  reactions: boolean;
439
- /** Accept a comment only from an address that has been verified. Anonymous
440
- and unverified-email writers are refused rather than held. */
457
+ /** Publish a comment only from an address that has been verified. An
458
+ anonymous writer must give an address, and the comment is held until
459
+ they confirm it; one with no address is refused. */
441
460
  requireVerifiedEmail: boolean;
442
461
  }
443
462
  export declare const DEFAULT_COMMENT_POLICY: CommentPolicy;
@@ -447,8 +466,9 @@ export declare const COMMENT_POLICY_TAGS: {
447
466
  readonly 'no-comments': (policy: CommentPolicy) => CommentPolicy;
448
467
  readonly 'reactions-off': (policy: CommentPolicy) => {
449
468
  mode: CommentsMode;
450
- /** Accept a comment only from an address that has been verified. Anonymous
451
- and unverified-email writers are refused rather than held. */
469
+ /** Publish a comment only from an address that has been verified. An
470
+ anonymous writer must give an address, and the comment is held until
471
+ they confirm it; one with no address is refused. */
452
472
  requireVerifiedEmail: boolean;
453
473
  reactions: false;
454
474
  };
package/dist/content.d.ts CHANGED
@@ -1,5 +1,22 @@
1
1
  export declare const CONTENT_DOCUMENT_SOURCES: readonly ['mood', 'post'];
2
2
  export type ContentDocumentSource = (typeof CONTENT_DOCUMENT_SOURCES)[number];
3
+ /** Public read aggregates since analytics collection began. */
4
+ export interface BlogStats {
5
+ generatedAt: string;
6
+ /** Null until the first event has been recorded. */
7
+ since: string | null;
8
+ totals: {
9
+ reads: number;
10
+ readers: number;
11
+ completed: number;
12
+ };
13
+ posts: {
14
+ slug: string;
15
+ reads: number;
16
+ completed: number;
17
+ medianDwellMs: number;
18
+ }[];
19
+ }
3
20
  export interface PostLocaleTag {
4
21
  locale: string;
5
22
  canonicalSlug?: string;
package/dist/index.js CHANGED
@@ -10,6 +10,19 @@ var BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT = 50;
10
10
  var BLOG_ANALYTICS_READ_THRESHOLD_MS = 5000;
11
11
  var BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH = 0.9;
12
12
  var BLOG_ANALYTICS_RANGE_OPTIONS = [7, 30, 90];
13
+ // src/admin.ts
14
+ var ADMIN_COMMENT_REJECT_REASONS = ["spam", "promotional", "abuse", "off_topic", "personal_info"];
15
+ var ADMIN_ACTIVITY_EVENTS = [
16
+ "comment.create",
17
+ "comment.edit",
18
+ "comment.remove",
19
+ "comment.moderate",
20
+ "comment.approve",
21
+ "comment.hide",
22
+ "comment.delete",
23
+ "reaction.add",
24
+ "reaction.remove"
25
+ ];
13
26
  // src/comments.ts
14
27
  var READER_PROVIDERS = ["email", "github", "google"];
15
28
  var COMMENT_LOCALES = ["zh", "en"];
@@ -218,7 +231,9 @@ var TELEGRAM_OPS_BROADCAST_STATUSES = [
218
231
  "failed"
219
232
  ];
220
233
  export {
234
+ ADMIN_ACTIVITY_EVENTS,
221
235
  ADMIN_BASE_PATH,
236
+ ADMIN_COMMENT_REJECT_REASONS,
222
237
  API_PREFIX,
223
238
  AVATAR_CLASSES,
224
239
  AVATAR_OFFER_SIZE,
@@ -12,6 +12,7 @@
12
12
  * reader identity, and the ops-bot notification. See site-api's
13
13
  * features/messages/server/message-service.ts.
14
14
  */
15
+ import type { ClientEvidence } from './comments';
15
16
  export declare const MESSAGE_LOCALES: readonly ['zh', 'en'];
16
17
  export type MessageLocale = (typeof MESSAGE_LOCALES)[number];
17
18
  /** Body bounds, shared by the form's client-side validation and the service. */
@@ -31,7 +32,9 @@ export declare const MESSAGE_MAX_NAME_LENGTH = 32;
31
32
  */
32
33
  export declare const MESSAGE_STATES: readonly ['new', 'read', 'replied', 'archived', 'spam'];
33
34
  export type MessageState = (typeof MESSAGE_STATES)[number];
34
- export interface OwnerMessageCreateInput {
35
+ /** The fingerprint fields are the comment box's own evidence
36
+ (`ClientEvidence`): optional, bounded, and never a gate. */
37
+ export interface OwnerMessageCreateInput extends ClientEvidence {
35
38
  /** Plain text, MESSAGE_MIN_BODY_LENGTH–MESSAGE_MAX_BODY_LENGTH characters. */
36
39
  body: string;
37
40
  displayName: string;
package/dist/mood.d.ts CHANGED
@@ -22,16 +22,9 @@ export interface MoodStatsActivityBucket {
22
22
  date: string;
23
23
  count: number;
24
24
  }
25
- export interface MoodStatsSentimentBucket {
26
- bucketStart: string;
27
- avgValence: number | null;
28
- dominantLabel: MoodSentimentLabel | null;
29
- scoredCount: number;
30
- }
31
25
  export interface MoodStatsSnapshot {
32
26
  activity: MoodStatsActivityBucket[];
33
27
  rhythm: number[][];
34
- sentimentTimeline: MoodStatsSentimentBucket[];
35
28
  streaks: {
36
29
  current: number;
37
30
  longest: number;
package/dist/notify.d.ts CHANGED
@@ -1,8 +1,20 @@
1
+ import type { ClientEvidence } from './comments';
1
2
  export type SubscriberStatus = 'pending' | 'active' | 'unsubscribed';
2
3
  export type DeliveryMode = 'immediate' | 'every_5h' | 'daily';
3
4
  export type NotifyChannel = 'mood' | 'blog' | 'privacy' | 'announcement';
4
5
  export type NotifyAuditEventType = 'subscribe_requested' | 'subscription_confirmed' | 'unsubscribed' | 'email_change_requested' | 'email_changed' | 'admin_create' | 'admin_update' | 'admin_delete' | 'admin_resend_confirm' | 'broadcast_sent';
5
6
  export declare const NOTIFY_CHANNELS: readonly NotifyChannel[];
7
+ /** Optional risk evidence preserves compatibility with existing clients. */
8
+ export interface SubscribeRequest extends ClientEvidence {
9
+ email: string;
10
+ turnstileToken?: string;
11
+ channels?: NotifyChannel[];
12
+ deliveryMode?: DeliveryMode;
13
+ timezone?: string;
14
+ dailyHour?: number;
15
+ website?: string;
16
+ dwellToken?: string;
17
+ }
6
18
  export interface SubscriberRecord {
7
19
  email: string;
8
20
  emailHash: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunizao/contracts",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Shared TypeScript contracts for buxx.me services and clients.",
5
5
  "type": "module",
6
6
  "sideEffects": false,