@bunizao/contracts 0.1.0 → 0.3.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/analytics.js CHANGED
@@ -11,15 +11,15 @@ 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
13
  export {
14
- NEWSLETTER_ANALYTICS_OPEN_ENDPOINT,
15
- NEWSLETTER_ANALYTICS_CLICK_ENDPOINT,
16
- LISTENING_ANALYTICS_EVENT_ENDPOINT,
17
- BLOG_ANALYTICS_SUMMARY_ENDPOINT,
18
- BLOG_ANALYTICS_READ_THRESHOLD_MS,
19
- BLOG_ANALYTICS_RANGE_OPTIONS,
20
- BLOG_ANALYTICS_EVENT_ENDPOINT,
21
- BLOG_ANALYTICS_EVENTS_ENDPOINT,
22
- BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
14
+ BLOG_ANALYTICS_ARTICLE_ENDPOINT,
23
15
  BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH,
24
- BLOG_ANALYTICS_ARTICLE_ENDPOINT
16
+ BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
17
+ BLOG_ANALYTICS_EVENTS_ENDPOINT,
18
+ BLOG_ANALYTICS_EVENT_ENDPOINT,
19
+ BLOG_ANALYTICS_RANGE_OPTIONS,
20
+ BLOG_ANALYTICS_READ_THRESHOLD_MS,
21
+ BLOG_ANALYTICS_SUMMARY_ENDPOINT,
22
+ LISTENING_ANALYTICS_EVENT_ENDPOINT,
23
+ NEWSLETTER_ANALYTICS_CLICK_ENDPOINT,
24
+ NEWSLETTER_ANALYTICS_OPEN_ENDPOINT
25
25
  };
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Blog comments, reactions, and reader identity on /blog/[slug]. v2 shape —
3
+ * see plans/blog-comments.md "Decisions taken (v2)" for the model this
4
+ * follows: anonymous-first participation through a risk stack, lazy and
5
+ * stateless email verification, one reader table shared with the newsletter.
6
+ *
7
+ * Reading is open to everyone. Posting a comment or reacting needs only a
8
+ * name, with an optional email — no session, no verification gate. Email verification
9
+ * is an upgrade path (grade L1/L2 in the PRD), never a door charge; the
10
+ * email address itself never crosses this boundary in a response body.
11
+ */
12
+ export declare const READER_PROVIDERS: readonly ['email', 'github', 'google'];
13
+ export type ReaderProvider = (typeof READER_PROVIDERS)[number];
14
+ export declare const COMMENT_LOCALES: readonly ['zh', 'en'];
15
+ export type CommentLocale = (typeof COMMENT_LOCALES)[number];
16
+ /** The reader's standing, from "Identity: three grades, one table":
17
+ * l0 — `reader_anon` cookie only, nothing verified
18
+ * l1 — verified email (lazy verification link)
19
+ * l2 — OAuth (GitHub or Google), phase 3 */
20
+ export declare const READER_GRADES: readonly ['l0', 'l1', 'l2'];
21
+ export type ReaderGrade = (typeof READER_GRADES)[number];
22
+ export declare const COMMENT_STATUSES: readonly ['published', 'held', 'rejected', 'deleted'];
23
+ export type CommentStatus = (typeof COMMENT_STATUSES)[number];
24
+ export declare const MODERATION_ACTIONS: readonly ['publish', 'hold', 'reject', 'unsure'];
25
+ export type ModerationAction = (typeof MODERATION_ACTIONS)[number];
26
+ export declare const MODERATION_REASONS: readonly ['ok', 'spam', 'promotional', 'abuse', 'off_topic', 'personal_info'];
27
+ export type ModerationReason = (typeof MODERATION_REASONS)[number];
28
+ /** Never sent to a reader. Rides along on the owner's Telegram notification. */
29
+ export interface ModerationVerdict {
30
+ action: ModerationAction;
31
+ reason: ModerationReason;
32
+ /** One short line explaining the call, in the language of the comment. */
33
+ note: string;
34
+ }
35
+ /** The calling browser's verified reader standing (L1/L2 only — an L0-only
36
+ visitor has no reader row and this is null). Never carries email or
37
+ email_hash; `avatarUrl` is always a same-origin proxy path. */
38
+ export interface ReaderMe {
39
+ readerId: string;
40
+ grade: ReaderGrade;
41
+ provider: ReaderProvider;
42
+ displayName: string;
43
+ avatarUrl: string;
44
+ notifyReplies: boolean;
45
+ /** Whether this address holds an active newsletter subscription. */
46
+ subscribed: boolean;
47
+ }
48
+ export interface ReaderMeResult {
49
+ reader: ReaderMe | null;
50
+ }
51
+ /** The confirm button's POST — plans/blog-comments.md "Lazy verification". */
52
+ export interface ReaderVerifyInput {
53
+ token: string;
54
+ /** Ticked "also subscribe" in the post-comment nudge. Activates the
55
+ subscription in the same POST — one email, one click, both confirmations. */
56
+ subscribe?: boolean;
57
+ }
58
+ export declare const READER_VERIFY_OUTCOMES: readonly ['confirmed', 'already_confirmed', 'expired', 'invalid'];
59
+ export type ReaderVerifyOutcome = (typeof READER_VERIFY_OUTCOMES)[number];
60
+ export interface ReaderVerifyResult {
61
+ outcome: ReaderVerifyOutcome;
62
+ reader: ReaderMe | null;
63
+ }
64
+ /** One conversation, quieted from the reply mail itself. The token is the
65
+ whole authentication: the reader is usually holding that mail on a device
66
+ that has never signed in here, and an off switch gated behind a sign-in is
67
+ one people reach by marking the sender spam instead. */
68
+ export interface ReaderMuteInput {
69
+ token: string;
70
+ /** Omitted or true mutes; false is the undo the landing page offers. */
71
+ muted?: boolean;
72
+ }
73
+ export declare const READER_MUTE_OUTCOMES: readonly ['muted', 'unmuted', 'invalid'];
74
+ export type ReaderMuteOutcome = (typeof READER_MUTE_OUTCOMES)[number];
75
+ export interface ReaderMuteResult {
76
+ outcome: ReaderMuteOutcome;
77
+ /** Carried so the page can offer the wider switches without another call. */
78
+ postId?: string;
79
+ }
80
+ /** Re-send the verification mail. Rate-limited per address; always answers
81
+ the same shape regardless of whether the address has a comment on file,
82
+ so this can never be used to probe which addresses have commented. */
83
+ export interface ReaderResendInput {
84
+ email: string;
85
+ /** Carries the original "notify me of replies" intent into the fresh
86
+ verification link, recovered from the stale token's payload. Optional;
87
+ a bare resend defaults to false. */
88
+ notifyReplies?: boolean;
89
+ locale?: CommentLocale;
90
+ }
91
+ export interface ReaderResendResult {
92
+ ok: boolean;
93
+ }
94
+ /** The two switches on the confirm page, and the only place a reader can move
95
+ them without an account. Session-authenticated (the cookie the verify POST
96
+ just set), so nothing here names an address. Both fields are optional: the
97
+ page sends only the one that was just toggled. */
98
+ export interface ReaderPreferencesInput {
99
+ notifyReplies?: boolean;
100
+ subscribed?: boolean;
101
+ }
102
+ export interface ReaderPreferencesResult {
103
+ reader: ReaderMe | null;
104
+ }
105
+ export declare const REACTION_TARGET_TYPES: readonly ['post', 'comment'];
106
+ export type ReactionTargetType = (typeof REACTION_TARGET_TYPES)[number];
107
+ /** The only reaction shipped at launch. The storage layer is emoji-keyed anyway. */
108
+ export declare const DEFAULT_REACTION_EMOJI = "\u2764\uFE0F";
109
+ /** A face in the avatar stack beside a reaction count. Only identified
110
+ reactors (L1/L2, or an L0 reactor whose claimed email resolves) ever
111
+ appear here — see "Reactions: anonymous counts, identified faces". */
112
+ export interface ReactorChip {
113
+ name: string;
114
+ avatarUrl: string | null;
115
+ }
116
+ export interface ReactionSummary {
117
+ emoji: string;
118
+ count: number;
119
+ /** Whether the calling browser (session or reader) holds this reaction. */
120
+ reacted: boolean;
121
+ /** Most recent identified reactors, newest first, capped server-side. */
122
+ reactors: ReactorChip[];
123
+ }
124
+ /** `${targetType}:${targetId}`, e.g. `post:abc123` or `comment:xyz789`. */
125
+ export type ReactionTargetKey = string;
126
+ export interface ReactionBatchResult {
127
+ reactions: Record<ReactionTargetKey, ReactionSummary[]>;
128
+ }
129
+ export interface ReactionToggleInput {
130
+ targetType: ReactionTargetType;
131
+ targetId: string;
132
+ emoji?: string;
133
+ /** Desired final state. Repeating the same request is safe. */
134
+ reacted: boolean;
135
+ /** `expectedAction: 'blog_reaction'` -- plans/blog-comments.md "The risk
136
+ stack" step 2. The widget solves invisibly (managed mode), so this
137
+ never costs the reader a prompt or a round trip of their own. */
138
+ turnstileToken: string;
139
+ }
140
+ export interface ReactionToggleResult {
141
+ reaction: ReactionSummary;
142
+ }
143
+ /** Public author view on a comment row. Never includes email or email_hash. */
144
+ export interface CommentAuthor {
145
+ name: string;
146
+ /** Same-origin proxy path to the writer's cached avatar, or empty when no
147
+ avatar has resolved for their address (or they left none). Empty means
148
+ "draw your own": the endpoint behind this path answers an identicon for
149
+ any key it does not know, so a path emitted for every address would make
150
+ every face an identicon. */
151
+ avatarUrl: string;
152
+ /** True when this row's writer is the blog owner. */
153
+ byAuthor: boolean;
154
+ }
155
+ export interface Comment {
156
+ id: string;
157
+ /** Ghost's post.id, stable across slug renames. */
158
+ postId: string;
159
+ /** Always a root comment id, or null. Threading is one level deep. */
160
+ parentId: string | null;
161
+ author: CommentAuthor;
162
+ /** Plain text, 1-2000 chars. Escaping and autolinking happen at render. */
163
+ body: string;
164
+ status: CommentStatus;
165
+ createdAt: string;
166
+ editedAt: string | null;
167
+ /** True when this browser wrote the row (verified reader match, or the
168
+ `reader_anon` session matches). Highlights the row as yours; mutation
169
+ rights are signalled by `editableUntil`/`deletable`, not by this. */
170
+ mine: boolean;
171
+ /** Server clock deadline for the 15-minute edit window, ms since epoch.
172
+ Present only when the viewer is the verified reader who owns the row
173
+ and the window has not closed. Always null for session-owned rows --
174
+ anonymous comments cannot be edited. */
175
+ editableUntil: number | null;
176
+ /** True when the viewer may delete this row: verified reader owns it and
177
+ it is not already a tombstone. Always false for session-owned rows. */
178
+ deletable: boolean;
179
+ /** Soft-deleted but kept as a shape-preserving placeholder because a reply
180
+ hangs underneath it. `body`/`author` are empty on a tombstone. */
181
+ tombstone: boolean;
182
+ }
183
+ export interface CommentListResult {
184
+ comments: Comment[];
185
+ hasMore: boolean;
186
+ /** Cursor for the next page, or null when `hasMore` is false. */
187
+ nextBefore: string | null;
188
+ /** Published comments on the post (excludes held/rejected/deleted). */
189
+ total: number;
190
+ }
191
+ export interface CommentCreateInput {
192
+ postId: string;
193
+ body: string;
194
+ /** Root comment id this replies to. Omitted or null for a root comment. */
195
+ parentId?: string | null;
196
+ displayName: string;
197
+ /** Optional. Supplied: must be valid; triggers lazy verification and
198
+ enables claiming and a Gravatar-backed avatar.
199
+ Omitted or empty: the comment is owned by its anon session only and
200
+ the client renders an identicon. */
201
+ email?: string;
202
+ turnstileToken: string;
203
+ /** Visually-hidden honeypot field. Must arrive empty. */
204
+ website?: string;
205
+ /** Signed server timestamp minted at first interaction with the compose
206
+ box — see "The risk stack" step 4 (dwell time). */
207
+ dwellToken: string;
208
+ /** Reader accepted the post-comment subscribe offer. Only takes effect
209
+ once the address is verified. */
210
+ notifyReplies?: boolean;
211
+ locale?: CommentLocale;
212
+ }
213
+ export declare const COMMENT_CREATE_OUTCOMES: readonly ['published', 'held'];
214
+ export type CommentCreateOutcome = (typeof COMMENT_CREATE_OUTCOMES)[number];
215
+ export interface CommentCreateResult {
216
+ outcome: CommentCreateOutcome;
217
+ comment: Comment;
218
+ /** True when a supplied `email` was not already a verified reader — the
219
+ client shows the verification nudge (and, when accepted, the subscribe
220
+ offer). Always false when no email was supplied; the add-an-email
221
+ nudge is driven client-side by the missing address, not by this flag. */
222
+ unverifiedEmail: boolean;
223
+ }
224
+ export interface CommentEditInput {
225
+ body: string;
226
+ }
227
+ export interface CommentEditResult {
228
+ comment: Comment;
229
+ }
230
+ export interface CommentDeleteResult {
231
+ ok: true;
232
+ /** True when the row became a tombstone instead of disappearing, because
233
+ a reply hangs underneath it. */
234
+ tombstone: boolean;
235
+ }
236
+ /** What the comment section does on a post.
237
+ *
238
+ * open — the ordinary thread: read, write, reply.
239
+ * readonly — everything already written stays readable; nothing new is
240
+ * accepted. The section says so instead of offering a box.
241
+ * off — no comment section at all.
242
+ *
243
+ * `off` and `readonly` differ only in what the page draws; to the API both
244
+ * mean "this post takes no writes" (see `acceptsComments`). */
245
+ export declare const COMMENTS_MODES: readonly ['open', 'readonly', 'off'];
246
+ export type CommentsMode = (typeof COMMENTS_MODES)[number];
247
+ export interface CommentPolicy {
248
+ mode: CommentsMode;
249
+ /** Whether the heart can be pressed — on the post and on its comments.
250
+ Independent of `mode`: a post can take reactions with comments off, and
251
+ an open thread can refuse them. */
252
+ reactions: boolean;
253
+ /** Accept a comment only from an address that has been verified. Anonymous
254
+ and unverified-email writers are refused rather than held. */
255
+ requireVerifiedEmail: boolean;
256
+ }
257
+ export declare const DEFAULT_COMMENT_POLICY: CommentPolicy;
258
+ /** Ghost internal tags that override the site-wide default, one knob each.
259
+ *
260
+ * Internal tags are the switch because they are the only per-post field the
261
+ * author already edits in Ghost, and both halves of the system can read them:
262
+ * the site sees them through the Admin API at build time, site-api through the
263
+ * Content API at request time (`include=tags` returns internal tags). One
264
+ * source of truth, no settings table, no admin screen to keep in sync.
265
+ *
266
+ * `#no-comments` predates the others and is kept: it always meant "this post
267
+ * takes no more comments", which is `readonly`. */
268
+ export declare const COMMENT_POLICY_TAGS: {
269
+ readonly 'comments-off': (policy: CommentPolicy) => {
270
+ /** Whether the heart can be pressed — on the post and on its comments.
271
+ Independent of `mode`: a post can take reactions with comments off, and
272
+ an open thread can refuse them. */
273
+ reactions: boolean;
274
+ /** Accept a comment only from an address that has been verified. Anonymous
275
+ and unverified-email writers are refused rather than held. */
276
+ requireVerifiedEmail: boolean;
277
+ mode: CommentsMode;
278
+ };
279
+ readonly 'comments-readonly': (policy: CommentPolicy) => {
280
+ /** Whether the heart can be pressed — on the post and on its comments.
281
+ Independent of `mode`: a post can take reactions with comments off, and
282
+ an open thread can refuse them. */
283
+ reactions: boolean;
284
+ /** Accept a comment only from an address that has been verified. Anonymous
285
+ and unverified-email writers are refused rather than held. */
286
+ requireVerifiedEmail: boolean;
287
+ mode: CommentsMode;
288
+ };
289
+ readonly 'no-comments': (policy: CommentPolicy) => {
290
+ /** Whether the heart can be pressed — on the post and on its comments.
291
+ Independent of `mode`: a post can take reactions with comments off, and
292
+ an open thread can refuse them. */
293
+ reactions: boolean;
294
+ /** Accept a comment only from an address that has been verified. Anonymous
295
+ and unverified-email writers are refused rather than held. */
296
+ requireVerifiedEmail: boolean;
297
+ mode: CommentsMode;
298
+ };
299
+ readonly 'reactions-off': (policy: CommentPolicy) => {
300
+ mode: CommentsMode;
301
+ /** Accept a comment only from an address that has been verified. Anonymous
302
+ and unverified-email writers are refused rather than held. */
303
+ requireVerifiedEmail: boolean;
304
+ reactions: false;
305
+ };
306
+ readonly 'comments-verified': (policy: CommentPolicy) => {
307
+ mode: CommentsMode;
308
+ /** Whether the heart can be pressed — on the post and on its comments.
309
+ Independent of `mode`: a post can take reactions with comments off, and
310
+ an open thread can refuse them. */
311
+ reactions: boolean;
312
+ requireVerifiedEmail: true;
313
+ };
314
+ };
315
+ /** A tag as either half of Ghost's pair: `#no-comments` is the name a person
316
+ types, `hash-no-comments` the slug the API returns beside it. */
317
+ export interface CommentPolicyTagLike {
318
+ name?: string | null;
319
+ slug?: string | null;
320
+ }
321
+ /** Fold a post's tags onto the site-wide default. Order does not matter:
322
+ every tag sets one field, and a tag the map does not know is ignored. */
323
+ export declare function commentPolicyFromTags(tags: readonly CommentPolicyTagLike[] | null | undefined, base?: CommentPolicy): CommentPolicy;
324
+ /** Whether the post takes new comments — the one question the API asks. Both
325
+ `readonly` and `off` answer no; the difference between them is drawn, not
326
+ enforced. */
327
+ export declare const acceptsComments: (policy: CommentPolicy) => boolean;
@@ -0,0 +1,72 @@
1
+ // src/comments.ts
2
+ var READER_PROVIDERS = ["email", "github", "google"];
3
+ var COMMENT_LOCALES = ["zh", "en"];
4
+ var READER_GRADES = ["l0", "l1", "l2"];
5
+ var COMMENT_STATUSES = ["published", "held", "rejected", "deleted"];
6
+ var MODERATION_ACTIONS = ["publish", "hold", "reject", "unsure"];
7
+ var MODERATION_REASONS = [
8
+ "ok",
9
+ "spam",
10
+ "promotional",
11
+ "abuse",
12
+ "off_topic",
13
+ "personal_info"
14
+ ];
15
+ var READER_VERIFY_OUTCOMES = ["confirmed", "already_confirmed", "expired", "invalid"];
16
+ var READER_MUTE_OUTCOMES = ["muted", "unmuted", "invalid"];
17
+ var REACTION_TARGET_TYPES = ["post", "comment"];
18
+ var DEFAULT_REACTION_EMOJI = "❤️";
19
+ var COMMENT_CREATE_OUTCOMES = ["published", "held"];
20
+ var COMMENTS_MODES = ["open", "readonly", "off"];
21
+ var DEFAULT_COMMENT_POLICY = {
22
+ mode: "open",
23
+ reactions: true,
24
+ requireVerifiedEmail: false
25
+ };
26
+ var COMMENT_POLICY_TAGS = {
27
+ "comments-off": (policy) => ({ ...policy, mode: "off" }),
28
+ "comments-readonly": (policy) => ({ ...policy, mode: "readonly" }),
29
+ "no-comments": (policy) => ({ ...policy, mode: "readonly" }),
30
+ "reactions-off": (policy) => ({ ...policy, reactions: false }),
31
+ "comments-verified": (policy) => ({ ...policy, requireVerifiedEmail: true })
32
+ };
33
+ var policyKeyOf = (tag) => {
34
+ const name = tag.name?.trim();
35
+ if (name?.startsWith("#"))
36
+ return name.slice(1).toLowerCase();
37
+ const slug = tag.slug?.trim().toLowerCase();
38
+ if (slug?.startsWith("hash-"))
39
+ return slug.slice(5);
40
+ return null;
41
+ };
42
+ function commentPolicyFromTags(tags, base = DEFAULT_COMMENT_POLICY) {
43
+ let policy = { ...base };
44
+ for (const tag of tags ?? []) {
45
+ const key = policyKeyOf(tag);
46
+ if (!key)
47
+ continue;
48
+ const apply = COMMENT_POLICY_TAGS[key];
49
+ if (apply)
50
+ policy = apply(policy);
51
+ }
52
+ return policy;
53
+ }
54
+ var acceptsComments = (policy) => policy.mode === "open";
55
+ export {
56
+ COMMENTS_MODES,
57
+ COMMENT_CREATE_OUTCOMES,
58
+ COMMENT_LOCALES,
59
+ COMMENT_POLICY_TAGS,
60
+ COMMENT_STATUSES,
61
+ DEFAULT_COMMENT_POLICY,
62
+ DEFAULT_REACTION_EMOJI,
63
+ MODERATION_ACTIONS,
64
+ MODERATION_REASONS,
65
+ REACTION_TARGET_TYPES,
66
+ READER_GRADES,
67
+ READER_MUTE_OUTCOMES,
68
+ READER_PROVIDERS,
69
+ READER_VERIFY_OUTCOMES,
70
+ acceptsComments,
71
+ commentPolicyFromTags
72
+ };
package/dist/content.js CHANGED
@@ -18,6 +18,6 @@ function parsePostLocaleTag(tagName) {
18
18
  return canonicalSlug ? { locale, canonicalSlug } : { locale };
19
19
  }
20
20
  export {
21
- parsePostLocaleTag,
22
- CONTENT_DOCUMENT_SOURCES
21
+ CONTENT_DOCUMENT_SOURCES,
22
+ parsePostLocaleTag
23
23
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './analytics';
2
2
  export * from './admin';
3
+ export * from './comments';
3
4
  export * from './content';
4
5
  export * from './listening';
5
6
  export * from './mood';
package/dist/index.js CHANGED
@@ -11,6 +11,61 @@ 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
13
 
14
+ // src/comments.ts
15
+ var READER_PROVIDERS = ["email", "github", "google"];
16
+ var COMMENT_LOCALES = ["zh", "en"];
17
+ var READER_GRADES = ["l0", "l1", "l2"];
18
+ var COMMENT_STATUSES = ["published", "held", "rejected", "deleted"];
19
+ var MODERATION_ACTIONS = ["publish", "hold", "reject", "unsure"];
20
+ var MODERATION_REASONS = [
21
+ "ok",
22
+ "spam",
23
+ "promotional",
24
+ "abuse",
25
+ "off_topic",
26
+ "personal_info"
27
+ ];
28
+ var READER_VERIFY_OUTCOMES = ["confirmed", "already_confirmed", "expired", "invalid"];
29
+ var READER_MUTE_OUTCOMES = ["muted", "unmuted", "invalid"];
30
+ var REACTION_TARGET_TYPES = ["post", "comment"];
31
+ var DEFAULT_REACTION_EMOJI = "❤️";
32
+ var COMMENT_CREATE_OUTCOMES = ["published", "held"];
33
+ var COMMENTS_MODES = ["open", "readonly", "off"];
34
+ var DEFAULT_COMMENT_POLICY = {
35
+ mode: "open",
36
+ reactions: true,
37
+ requireVerifiedEmail: false
38
+ };
39
+ var COMMENT_POLICY_TAGS = {
40
+ "comments-off": (policy) => ({ ...policy, mode: "off" }),
41
+ "comments-readonly": (policy) => ({ ...policy, mode: "readonly" }),
42
+ "no-comments": (policy) => ({ ...policy, mode: "readonly" }),
43
+ "reactions-off": (policy) => ({ ...policy, reactions: false }),
44
+ "comments-verified": (policy) => ({ ...policy, requireVerifiedEmail: true })
45
+ };
46
+ var policyKeyOf = (tag) => {
47
+ const name = tag.name?.trim();
48
+ if (name?.startsWith("#"))
49
+ return name.slice(1).toLowerCase();
50
+ const slug = tag.slug?.trim().toLowerCase();
51
+ if (slug?.startsWith("hash-"))
52
+ return slug.slice(5);
53
+ return null;
54
+ };
55
+ function commentPolicyFromTags(tags, base = DEFAULT_COMMENT_POLICY) {
56
+ let policy = { ...base };
57
+ for (const tag of tags ?? []) {
58
+ const key = policyKeyOf(tag);
59
+ if (!key)
60
+ continue;
61
+ const apply = COMMENT_POLICY_TAGS[key];
62
+ if (apply)
63
+ policy = apply(policy);
64
+ }
65
+ return policy;
66
+ }
67
+ var acceptsComments = (policy) => policy.mode === "open";
68
+
14
69
  // src/content.ts
15
70
  var CONTENT_DOCUMENT_SOURCES = ["mood", "post"];
16
71
  var POST_LOCALE_RE = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/;
@@ -52,6 +107,19 @@ var MOOD_SEARCH_PATH = "/v2/mood/search";
52
107
  var MOOD_IMAGE_PROXY_BASE_PATH = "/v2/images";
53
108
  var MOOD_MEDIA_PROXY_BASE_PATH = "/v2/media";
54
109
  var LISTENING_PATH = "/v2/listening";
110
+ var COMMENTS_PATH = "/v2/comments";
111
+ var COMMENT_PATH_PREFIX = "/v2/comments/";
112
+ var REACTIONS_PATH = "/v2/reactions";
113
+ var REACTIONS_TOGGLE_PATH = "/v2/reactions/toggle";
114
+ var READER_ME_PATH = "/v2/reader/me";
115
+ var READER_VERIFY_PATH = "/v2/reader/verify";
116
+ var READER_RESEND_PATH = "/v2/reader/resend";
117
+ var READER_PREFERENCES_PATH = "/v2/reader/preferences";
118
+ var READER_MUTE_PATH = "/v2/reader/mute";
119
+ var READER_AVATAR_PATH_PREFIX = "/v2/reader/avatar/";
120
+ var READER_OAUTH_PATH_PREFIX = "/oauth/reader/";
121
+ var READER_CONFIRM_PATH = "/reader/confirm";
122
+ var READER_MUTE_PAGE_PATH = "/reader/mute";
55
123
  var ADMIN_BASE_PATH = "/admin";
56
124
  var NOTIFY_BASE_PATH = "/notify";
57
125
  var GHOST_WEBHOOK_PATH = "/webhooks/ghost";
@@ -114,67 +182,96 @@ var TELEGRAM_OPS_BROADCAST_STATUSES = [
114
182
  "failed"
115
183
  ];
116
184
  export {
117
- telegramOpsSubscriberPath,
118
- telegramOpsReminderDeliveredPath,
119
- telegramOpsEventPath,
120
- telegramOpsEventActionPath,
121
- parsePostLocaleTag,
122
- TELEGRAM_WEBHOOK_PATH,
123
- TELEGRAM_OPS_WEBHOOK_PATH,
124
- TELEGRAM_OPS_SUBSCRIBER_STATUSES,
125
- TELEGRAM_OPS_SUBSCRIBERS_PATH,
126
- TELEGRAM_OPS_REMINDERS_PATH,
127
- TELEGRAM_OPS_REMINDERS_DUE_PATH,
128
- TELEGRAM_OPS_OVERVIEW_PATH,
129
- TELEGRAM_OPS_EVENTS_PATH,
130
- TELEGRAM_OPS_BROADCAST_STATUSES,
131
- TELEGRAM_OPS_BROADCAST_SEND_PATH,
132
- TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
133
- TELEGRAM_OPS_BROADCAST_DELIVERY_MODES,
134
- TELEGRAM_OPS_BROADCAST_CHANNELS,
135
- TELEGRAM_OPS_BROADCASTS_PATH,
136
- TELEGRAM_OPS_BASE_PATH,
137
- PING_PATH,
138
- NOTIFY_GATE_STATES,
139
- NOTIFY_GATE_RELEASE_PATH,
140
- NOTIFY_GATE_PATH,
141
- NOTIFY_GATE_DECISIONS,
142
- NOTIFY_CHANNELS,
143
- NOTIFY_BASE_PATH,
144
- NEWSLETTER_ANALYTICS_OPEN_ENDPOINT,
145
- NEWSLETTER_ANALYTICS_CLICK_ENDPOINT,
146
- MUSICKIT_TOKEN_PATH,
147
- MOOD_SENTIMENT_LABELS,
148
- MOOD_SEARCH_PATH,
149
- MOOD_PUBLIC_FEED_PATH,
150
- MOOD_PUBLIC_COMMENTS_PATH,
151
- MOOD_MEDIA_PROXY_BASE_PATH,
152
- MOOD_LIVE_META_PATH,
153
- MOOD_LIVE_FEED_PATH,
154
- MOOD_LIVE_COUNTS_PATH,
155
- MOOD_IMAGE_PROXY_BASE_PATH,
156
- MOOD_ARCHIVE_STATS_PATH,
157
- MOOD_ARCHIVE_FEED_PATH,
158
- MOOD_AI_MODELS,
159
- LISTENING_PATH,
160
- LISTENING_ANALYTICS_EVENT_ENDPOINT,
161
- LEGACY_NOTIFY_BASE_PATH,
162
- LEGACY_MUSICKIT_TOKEN_PATH,
163
- LEGACY_HEALTH_PATH,
164
- LEGACY_GHOST_WEBHOOK_PATH,
165
- LEGACY_ADMIN_BASE_PATH,
166
- HEALTH_PATH,
167
- GHOST_WEBHOOK_PATH,
168
- EVENT_STATUSES,
169
- CONTENT_DOCUMENT_SOURCES,
170
- BLOG_ANALYTICS_SUMMARY_ENDPOINT,
171
- BLOG_ANALYTICS_READ_THRESHOLD_MS,
172
- BLOG_ANALYTICS_RANGE_OPTIONS,
173
- BLOG_ANALYTICS_EVENT_ENDPOINT,
174
- BLOG_ANALYTICS_EVENTS_ENDPOINT,
175
- BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
176
- BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH,
177
- BLOG_ANALYTICS_ARTICLE_ENDPOINT,
185
+ ADMIN_BASE_PATH,
178
186
  API_PREFIX,
179
- ADMIN_BASE_PATH
187
+ BLOG_ANALYTICS_ARTICLE_ENDPOINT,
188
+ BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH,
189
+ BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
190
+ BLOG_ANALYTICS_EVENTS_ENDPOINT,
191
+ BLOG_ANALYTICS_EVENT_ENDPOINT,
192
+ BLOG_ANALYTICS_RANGE_OPTIONS,
193
+ BLOG_ANALYTICS_READ_THRESHOLD_MS,
194
+ BLOG_ANALYTICS_SUMMARY_ENDPOINT,
195
+ COMMENTS_MODES,
196
+ COMMENTS_PATH,
197
+ COMMENT_CREATE_OUTCOMES,
198
+ COMMENT_LOCALES,
199
+ COMMENT_PATH_PREFIX,
200
+ COMMENT_POLICY_TAGS,
201
+ COMMENT_STATUSES,
202
+ CONTENT_DOCUMENT_SOURCES,
203
+ DEFAULT_COMMENT_POLICY,
204
+ DEFAULT_REACTION_EMOJI,
205
+ EVENT_STATUSES,
206
+ GHOST_WEBHOOK_PATH,
207
+ HEALTH_PATH,
208
+ LEGACY_ADMIN_BASE_PATH,
209
+ LEGACY_GHOST_WEBHOOK_PATH,
210
+ LEGACY_HEALTH_PATH,
211
+ LEGACY_MUSICKIT_TOKEN_PATH,
212
+ LEGACY_NOTIFY_BASE_PATH,
213
+ LISTENING_ANALYTICS_EVENT_ENDPOINT,
214
+ LISTENING_PATH,
215
+ MODERATION_ACTIONS,
216
+ MODERATION_REASONS,
217
+ MOOD_AI_MODELS,
218
+ MOOD_ARCHIVE_FEED_PATH,
219
+ MOOD_ARCHIVE_STATS_PATH,
220
+ MOOD_IMAGE_PROXY_BASE_PATH,
221
+ MOOD_LIVE_COUNTS_PATH,
222
+ MOOD_LIVE_FEED_PATH,
223
+ MOOD_LIVE_META_PATH,
224
+ MOOD_MEDIA_PROXY_BASE_PATH,
225
+ MOOD_PUBLIC_COMMENTS_PATH,
226
+ MOOD_PUBLIC_FEED_PATH,
227
+ MOOD_SEARCH_PATH,
228
+ MOOD_SENTIMENT_LABELS,
229
+ MUSICKIT_TOKEN_PATH,
230
+ NEWSLETTER_ANALYTICS_CLICK_ENDPOINT,
231
+ NEWSLETTER_ANALYTICS_OPEN_ENDPOINT,
232
+ NOTIFY_BASE_PATH,
233
+ NOTIFY_CHANNELS,
234
+ NOTIFY_GATE_DECISIONS,
235
+ NOTIFY_GATE_PATH,
236
+ NOTIFY_GATE_RELEASE_PATH,
237
+ NOTIFY_GATE_STATES,
238
+ PING_PATH,
239
+ REACTIONS_PATH,
240
+ REACTIONS_TOGGLE_PATH,
241
+ REACTION_TARGET_TYPES,
242
+ READER_AVATAR_PATH_PREFIX,
243
+ READER_CONFIRM_PATH,
244
+ READER_GRADES,
245
+ READER_ME_PATH,
246
+ READER_MUTE_OUTCOMES,
247
+ READER_MUTE_PAGE_PATH,
248
+ READER_MUTE_PATH,
249
+ READER_OAUTH_PATH_PREFIX,
250
+ READER_PREFERENCES_PATH,
251
+ READER_PROVIDERS,
252
+ READER_RESEND_PATH,
253
+ READER_VERIFY_OUTCOMES,
254
+ READER_VERIFY_PATH,
255
+ TELEGRAM_OPS_BASE_PATH,
256
+ TELEGRAM_OPS_BROADCASTS_PATH,
257
+ TELEGRAM_OPS_BROADCAST_CHANNELS,
258
+ TELEGRAM_OPS_BROADCAST_DELIVERY_MODES,
259
+ TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
260
+ TELEGRAM_OPS_BROADCAST_SEND_PATH,
261
+ TELEGRAM_OPS_BROADCAST_STATUSES,
262
+ TELEGRAM_OPS_EVENTS_PATH,
263
+ TELEGRAM_OPS_OVERVIEW_PATH,
264
+ TELEGRAM_OPS_REMINDERS_DUE_PATH,
265
+ TELEGRAM_OPS_REMINDERS_PATH,
266
+ TELEGRAM_OPS_SUBSCRIBERS_PATH,
267
+ TELEGRAM_OPS_SUBSCRIBER_STATUSES,
268
+ TELEGRAM_OPS_WEBHOOK_PATH,
269
+ TELEGRAM_WEBHOOK_PATH,
270
+ acceptsComments,
271
+ commentPolicyFromTags,
272
+ parsePostLocaleTag,
273
+ telegramOpsEventActionPath,
274
+ telegramOpsEventPath,
275
+ telegramOpsReminderDeliveredPath,
276
+ telegramOpsSubscriberPath
180
277
  };
package/dist/mood.js CHANGED
@@ -2,6 +2,6 @@
2
2
  var MOOD_SENTIMENT_LABELS = ["joy", "calm", "melancholy", "anger", "anxiety", "neutral"];
3
3
  var MOOD_AI_MODELS = ["gpt-5.5", "gpt-5", "claude-sonnet-4.6"];
4
4
  export {
5
- MOOD_SENTIMENT_LABELS,
6
- MOOD_AI_MODELS
5
+ MOOD_AI_MODELS,
6
+ MOOD_SENTIMENT_LABELS
7
7
  };
package/dist/routes.d.ts CHANGED
@@ -12,6 +12,19 @@ export declare const MOOD_SEARCH_PATH: '/v2/mood/search';
12
12
  export declare const MOOD_IMAGE_PROXY_BASE_PATH: '/v2/images';
13
13
  export declare const MOOD_MEDIA_PROXY_BASE_PATH: '/v2/media';
14
14
  export declare const LISTENING_PATH: '/v2/listening';
15
+ export declare const COMMENTS_PATH: '/v2/comments';
16
+ export declare const COMMENT_PATH_PREFIX: '/v2/comments/';
17
+ export declare const REACTIONS_PATH: '/v2/reactions';
18
+ export declare const REACTIONS_TOGGLE_PATH: '/v2/reactions/toggle';
19
+ export declare const READER_ME_PATH: '/v2/reader/me';
20
+ export declare const READER_VERIFY_PATH: '/v2/reader/verify';
21
+ export declare const READER_RESEND_PATH: '/v2/reader/resend';
22
+ export declare const READER_PREFERENCES_PATH: '/v2/reader/preferences';
23
+ export declare const READER_MUTE_PATH: '/v2/reader/mute';
24
+ export declare const READER_AVATAR_PATH_PREFIX: '/v2/reader/avatar/';
25
+ export declare const READER_OAUTH_PATH_PREFIX: '/oauth/reader/';
26
+ export declare const READER_CONFIRM_PATH: '/reader/confirm';
27
+ export declare const READER_MUTE_PAGE_PATH: '/reader/mute';
15
28
  export declare const ADMIN_BASE_PATH: '/admin';
16
29
  export declare const NOTIFY_BASE_PATH: '/notify';
17
30
  export declare const GHOST_WEBHOOK_PATH: '/webhooks/ghost';
package/dist/routes.js CHANGED
@@ -13,6 +13,19 @@ var MOOD_SEARCH_PATH = "/v2/mood/search";
13
13
  var MOOD_IMAGE_PROXY_BASE_PATH = "/v2/images";
14
14
  var MOOD_MEDIA_PROXY_BASE_PATH = "/v2/media";
15
15
  var LISTENING_PATH = "/v2/listening";
16
+ var COMMENTS_PATH = "/v2/comments";
17
+ var COMMENT_PATH_PREFIX = "/v2/comments/";
18
+ var REACTIONS_PATH = "/v2/reactions";
19
+ var REACTIONS_TOGGLE_PATH = "/v2/reactions/toggle";
20
+ var READER_ME_PATH = "/v2/reader/me";
21
+ var READER_VERIFY_PATH = "/v2/reader/verify";
22
+ var READER_RESEND_PATH = "/v2/reader/resend";
23
+ var READER_PREFERENCES_PATH = "/v2/reader/preferences";
24
+ var READER_MUTE_PATH = "/v2/reader/mute";
25
+ var READER_AVATAR_PATH_PREFIX = "/v2/reader/avatar/";
26
+ var READER_OAUTH_PATH_PREFIX = "/oauth/reader/";
27
+ var READER_CONFIRM_PATH = "/reader/confirm";
28
+ var READER_MUTE_PAGE_PATH = "/reader/mute";
16
29
  var ADMIN_BASE_PATH = "/admin";
17
30
  var NOTIFY_BASE_PATH = "/notify";
18
31
  var GHOST_WEBHOOK_PATH = "/webhooks/ghost";
@@ -24,28 +37,41 @@ var LEGACY_GHOST_WEBHOOK_PATH = "/v2/ghost/webhook";
24
37
  var LEGACY_MUSICKIT_TOKEN_PATH = "/v2/musickit/token";
25
38
  var LEGACY_HEALTH_PATH = "/v2/health";
26
39
  export {
27
- TELEGRAM_WEBHOOK_PATH,
28
- PING_PATH,
29
- NOTIFY_BASE_PATH,
30
- MUSICKIT_TOKEN_PATH,
31
- MOOD_SEARCH_PATH,
32
- MOOD_PUBLIC_FEED_PATH,
33
- MOOD_PUBLIC_COMMENTS_PATH,
34
- MOOD_MEDIA_PROXY_BASE_PATH,
35
- MOOD_LIVE_META_PATH,
36
- MOOD_LIVE_FEED_PATH,
37
- MOOD_LIVE_COUNTS_PATH,
38
- MOOD_IMAGE_PROXY_BASE_PATH,
39
- MOOD_ARCHIVE_STATS_PATH,
40
- MOOD_ARCHIVE_FEED_PATH,
41
- LISTENING_PATH,
42
- LEGACY_NOTIFY_BASE_PATH,
43
- LEGACY_MUSICKIT_TOKEN_PATH,
44
- LEGACY_HEALTH_PATH,
45
- LEGACY_GHOST_WEBHOOK_PATH,
46
- LEGACY_ADMIN_BASE_PATH,
47
- HEALTH_PATH,
48
- GHOST_WEBHOOK_PATH,
40
+ ADMIN_BASE_PATH,
49
41
  API_PREFIX,
50
- ADMIN_BASE_PATH
42
+ COMMENTS_PATH,
43
+ COMMENT_PATH_PREFIX,
44
+ GHOST_WEBHOOK_PATH,
45
+ HEALTH_PATH,
46
+ LEGACY_ADMIN_BASE_PATH,
47
+ LEGACY_GHOST_WEBHOOK_PATH,
48
+ LEGACY_HEALTH_PATH,
49
+ LEGACY_MUSICKIT_TOKEN_PATH,
50
+ LEGACY_NOTIFY_BASE_PATH,
51
+ LISTENING_PATH,
52
+ MOOD_ARCHIVE_FEED_PATH,
53
+ MOOD_ARCHIVE_STATS_PATH,
54
+ MOOD_IMAGE_PROXY_BASE_PATH,
55
+ MOOD_LIVE_COUNTS_PATH,
56
+ MOOD_LIVE_FEED_PATH,
57
+ MOOD_LIVE_META_PATH,
58
+ MOOD_MEDIA_PROXY_BASE_PATH,
59
+ MOOD_PUBLIC_COMMENTS_PATH,
60
+ MOOD_PUBLIC_FEED_PATH,
61
+ MOOD_SEARCH_PATH,
62
+ MUSICKIT_TOKEN_PATH,
63
+ NOTIFY_BASE_PATH,
64
+ PING_PATH,
65
+ REACTIONS_PATH,
66
+ REACTIONS_TOGGLE_PATH,
67
+ READER_AVATAR_PATH_PREFIX,
68
+ READER_CONFIRM_PATH,
69
+ READER_ME_PATH,
70
+ READER_MUTE_PAGE_PATH,
71
+ READER_MUTE_PATH,
72
+ READER_OAUTH_PATH_PREFIX,
73
+ READER_PREFERENCES_PATH,
74
+ READER_RESEND_PATH,
75
+ READER_VERIFY_PATH,
76
+ TELEGRAM_WEBHOOK_PATH
51
77
  };
@@ -49,27 +49,27 @@ var TELEGRAM_OPS_BROADCAST_STATUSES = [
49
49
  "failed"
50
50
  ];
51
51
  export {
52
- telegramOpsSubscriberPath,
53
- telegramOpsReminderDeliveredPath,
54
- telegramOpsEventPath,
55
- telegramOpsEventActionPath,
56
- TELEGRAM_OPS_WEBHOOK_PATH,
57
- TELEGRAM_OPS_SUBSCRIBER_STATUSES,
58
- TELEGRAM_OPS_SUBSCRIBERS_PATH,
59
- TELEGRAM_OPS_REMINDERS_PATH,
60
- TELEGRAM_OPS_REMINDERS_DUE_PATH,
61
- TELEGRAM_OPS_OVERVIEW_PATH,
62
- TELEGRAM_OPS_EVENTS_PATH,
63
- TELEGRAM_OPS_BROADCAST_STATUSES,
64
- TELEGRAM_OPS_BROADCAST_SEND_PATH,
65
- TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
66
- TELEGRAM_OPS_BROADCAST_DELIVERY_MODES,
67
- TELEGRAM_OPS_BROADCAST_CHANNELS,
68
- TELEGRAM_OPS_BROADCASTS_PATH,
69
- TELEGRAM_OPS_BASE_PATH,
70
- NOTIFY_GATE_STATES,
71
- NOTIFY_GATE_RELEASE_PATH,
72
- NOTIFY_GATE_PATH,
52
+ EVENT_STATUSES,
73
53
  NOTIFY_GATE_DECISIONS,
74
- EVENT_STATUSES
54
+ NOTIFY_GATE_PATH,
55
+ NOTIFY_GATE_RELEASE_PATH,
56
+ NOTIFY_GATE_STATES,
57
+ TELEGRAM_OPS_BASE_PATH,
58
+ TELEGRAM_OPS_BROADCASTS_PATH,
59
+ TELEGRAM_OPS_BROADCAST_CHANNELS,
60
+ TELEGRAM_OPS_BROADCAST_DELIVERY_MODES,
61
+ TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
62
+ TELEGRAM_OPS_BROADCAST_SEND_PATH,
63
+ TELEGRAM_OPS_BROADCAST_STATUSES,
64
+ TELEGRAM_OPS_EVENTS_PATH,
65
+ TELEGRAM_OPS_OVERVIEW_PATH,
66
+ TELEGRAM_OPS_REMINDERS_DUE_PATH,
67
+ TELEGRAM_OPS_REMINDERS_PATH,
68
+ TELEGRAM_OPS_SUBSCRIBERS_PATH,
69
+ TELEGRAM_OPS_SUBSCRIBER_STATUSES,
70
+ TELEGRAM_OPS_WEBHOOK_PATH,
71
+ telegramOpsEventActionPath,
72
+ telegramOpsEventPath,
73
+ telegramOpsReminderDeliveredPath,
74
+ telegramOpsSubscriberPath
75
75
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunizao/contracts",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared TypeScript contracts for buxx.me services and clients.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -38,6 +38,11 @@
38
38
  "import": "./dist/admin.js",
39
39
  "default": "./dist/admin.js"
40
40
  },
41
+ "./comments": {
42
+ "types": "./dist/comments.d.ts",
43
+ "import": "./dist/comments.js",
44
+ "default": "./dist/comments.js"
45
+ },
41
46
  "./content": {
42
47
  "types": "./dist/content.d.ts",
43
48
  "import": "./dist/content.js",