@bunizao/contracts 0.1.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Copyright (c) 2026 bunizao.
2
+ All rights reserved.
3
+
4
+ This repository is source-available. The source code is published for inspection only.
5
+
6
+ No license is granted to use this software or any associated assets, including without limitation any right to:
7
+ - run, deploy, host, or provide the software as a service;
8
+ - copy, reproduce, modify, or create derivative works;
9
+ - distribute, sublicense, sell, lease, or otherwise exploit this software or any associated assets.
10
+
11
+ Any use requires prior written permission from bunizao.
12
+
13
+ If you obtained an earlier version of this project under a different license, that version remains governed by the license terms that were included with that version.
14
+
15
+ Third-party components and assets remain subject to their own licenses. This notice does not replace or modify those third-party terms.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL BUNIZAO BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @bunizao/contracts
2
+
3
+ Shared TypeScript contracts used by the buxx.me site and API Worker.
4
+
5
+ The package is published from `packages/contracts` in the
6
+ [bunizao/site](https://github.com/bunizao/site) repository. Consumers should
7
+ depend on an exact version so the public site and private API Worker upgrade
8
+ the contract deliberately together.
9
+
10
+ ```bash
11
+ bun add @bunizao/contracts@0.1.0
12
+ # or
13
+ npm install @bunizao/contracts@0.1.0
14
+ ```
15
+
16
+ The package exports the complete contract from `@bunizao/contracts` and the
17
+ feature modules from `@bunizao/contracts/{analytics,admin,content,listening,mood,notify,routes,telegram-ops}`.
18
+
19
+ The package is source-available but not licensed for redistribution. See
20
+ [`LICENSE`](LICENSE) for the applicable terms.
@@ -0,0 +1,87 @@
1
+ import type { DeliveryMode, NotifyAuditEventType, NotifyChannel, SubscriberRecord, SubscriberStatus } from './notify';
2
+ export interface SubscriberChannelCount {
3
+ total: number;
4
+ pendingCount: number;
5
+ activeCount: number;
6
+ unsubscribedCount: number;
7
+ }
8
+ export type SubscriberChannelCounts = Record<NotifyChannel, SubscriberChannelCount>;
9
+ export type BroadcastPreviewChannelCounts = Partial<Record<NotifyChannel, number>>;
10
+ export interface SubscriberFilter {
11
+ status?: SubscriberStatus | 'all';
12
+ channel?: NotifyChannel;
13
+ deliveryMode?: DeliveryMode;
14
+ search?: string;
15
+ limit?: number;
16
+ offset?: number;
17
+ }
18
+ export interface SubscriberListResult {
19
+ rows: SubscriberRecord[];
20
+ total: number;
21
+ pendingCount: number;
22
+ activeCount: number;
23
+ unsubscribedCount: number;
24
+ channelCounts?: SubscriberChannelCounts;
25
+ }
26
+ export interface AuditEntry {
27
+ id: number;
28
+ eventType: NotifyAuditEventType;
29
+ email: string;
30
+ emailHash: string;
31
+ source: string;
32
+ createdAt: string;
33
+ userAgent?: string;
34
+ }
35
+ export interface AdminSubscriberInput {
36
+ email: string;
37
+ status: SubscriberStatus;
38
+ channels: NotifyChannel[];
39
+ deliveryMode: DeliveryMode;
40
+ timezone?: string;
41
+ dailyHour?: number;
42
+ }
43
+ export interface AdminSubscriberPatch {
44
+ status?: SubscriberStatus;
45
+ channels?: NotifyChannel[];
46
+ deliveryMode?: DeliveryMode;
47
+ timezone?: string | null;
48
+ dailyHour?: number | null;
49
+ }
50
+ export interface BroadcastAudience {
51
+ status: SubscriberStatus | 'active';
52
+ channels: NotifyChannel[];
53
+ deliveryModes?: DeliveryMode[];
54
+ }
55
+ export interface BroadcastInput {
56
+ subject: string;
57
+ body: string;
58
+ audience: BroadcastAudience;
59
+ }
60
+ export interface BroadcastRecord {
61
+ id: string;
62
+ subject: string;
63
+ bodyHtml: string;
64
+ bodyText: string | null;
65
+ audience: BroadcastAudience;
66
+ recipientCount: number;
67
+ sentCount: number;
68
+ failedCount: number;
69
+ status: 'draft' | 'sending' | 'sent' | 'failed';
70
+ createdAt: string;
71
+ sentAt: string | null;
72
+ sentBy: string;
73
+ }
74
+ export interface BroadcastPreviewResult {
75
+ subject: string;
76
+ html: string;
77
+ text: string;
78
+ recipientCount: number;
79
+ channelCounts?: BroadcastPreviewChannelCounts;
80
+ }
81
+ export interface BroadcastSendResult {
82
+ id: string;
83
+ recipientCount: number;
84
+ sentCount: number;
85
+ failedCount: number;
86
+ status: BroadcastRecord['status'];
87
+ }
package/dist/admin.js ADDED
File without changes
@@ -0,0 +1,208 @@
1
+ export declare const BLOG_ANALYTICS_EVENT_ENDPOINT: '/api/analytics/event';
2
+ export declare const BLOG_ANALYTICS_SUMMARY_ENDPOINT: '/api/analytics/summary';
3
+ export declare const BLOG_ANALYTICS_EVENTS_ENDPOINT: '/api/analytics/events';
4
+ export declare const BLOG_ANALYTICS_ARTICLE_ENDPOINT: '/api/analytics/article';
5
+ export declare const NEWSLETTER_ANALYTICS_OPEN_ENDPOINT: '/api/analytics/newsletter/open';
6
+ export declare const NEWSLETTER_ANALYTICS_CLICK_ENDPOINT: '/api/analytics/newsletter/click';
7
+ export declare const LISTENING_ANALYTICS_EVENT_ENDPOINT: '/api/v2/analytics/listening';
8
+ export declare const BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT = 50;
9
+ export declare const BLOG_ANALYTICS_READ_THRESHOLD_MS = 5000;
10
+ export declare const BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH = 0.9;
11
+ export declare const BLOG_ANALYTICS_RANGE_OPTIONS: readonly [7, 30, 90];
12
+ export type BlogAnalyticsPlatform = 'wechat' | 'wechat_mini' | 'weibo' | 'qq' | 'dingtalk' | 'edge' | 'chrome' | 'firefox' | 'safari' | 'other' | (string & {});
13
+ export type BlogAnalyticsRefSource = 'direct' | 'telegram' | 'search' | 'twitter' | 'internal' | 'external' | (string & {});
14
+ export type BlogAnalyticsDeviceType = 'mobile' | 'tablet' | 'desktop' | 'other' | (string & {});
15
+ export type NewsletterAnalyticsEventType = 'sent' | 'open' | 'click';
16
+ export type NewsletterAnalyticsEmailType = 'blog_welcome' | 'blog_newsletter';
17
+ export type ListeningAnalyticsAction = 'play_request' | 'play' | 'progress' | 'pause' | 'seek' | 'complete';
18
+ export type ListeningAnalyticsSurface = 'home' | 'blog' | 'mood' | 'components' | 'other';
19
+ export interface BlogAnalyticsEventInput {
20
+ eventId: string;
21
+ slug: string;
22
+ visitorId: string;
23
+ sessionId?: string | null;
24
+ dwellMs: number;
25
+ scrollDepth: number;
26
+ completed: boolean;
27
+ referrer?: string | null;
28
+ }
29
+ export interface BlogAnalyticsTotals {
30
+ views: number;
31
+ reads: number;
32
+ uniqueVisitors: number;
33
+ avgReadMs: number;
34
+ avgVisitorReadMs: number;
35
+ completionRate: number;
36
+ }
37
+ export interface BlogAnalyticsArticleStats extends BlogAnalyticsTotals {
38
+ slug: string;
39
+ topPlatform: BlogAnalyticsPlatform | null;
40
+ }
41
+ export interface BlogAnalyticsBreakdown {
42
+ key: string;
43
+ label?: string;
44
+ views: number;
45
+ reads: number;
46
+ uniqueVisitors?: number;
47
+ avgReadMs?: number;
48
+ completionRate?: number;
49
+ }
50
+ export interface BlogAnalyticsDailyStats {
51
+ day: string;
52
+ views: number;
53
+ reads: number;
54
+ uniqueVisitors: number;
55
+ avgReadMs: number;
56
+ completionRate: number;
57
+ }
58
+ export interface NewsletterAnalyticsTotals {
59
+ sent: number;
60
+ opened: number;
61
+ clicked: number;
62
+ uniqueSubscribers: number;
63
+ openRate: number;
64
+ clickRate: number;
65
+ }
66
+ export interface NewsletterAnalyticsBreakdown extends NewsletterAnalyticsTotals {
67
+ key: string;
68
+ label?: string;
69
+ }
70
+ export interface NewsletterAnalyticsCampaignStats extends NewsletterAnalyticsTotals {
71
+ campaignId: string;
72
+ emailType: NewsletterAnalyticsEmailType;
73
+ postId?: string | null;
74
+ lastEventAt: string | null;
75
+ }
76
+ export interface NewsletterAnalyticsDailyStats {
77
+ day: string;
78
+ sent: number;
79
+ opened: number;
80
+ clicked: number;
81
+ }
82
+ export interface NewsletterAnalyticsSummary {
83
+ totals: NewsletterAnalyticsTotals;
84
+ byEmailType: NewsletterAnalyticsBreakdown[];
85
+ campaigns: NewsletterAnalyticsCampaignStats[];
86
+ daily: NewsletterAnalyticsDailyStats[];
87
+ }
88
+ export interface ListeningAnalyticsEventInput {
89
+ playbackId: string;
90
+ visitorId: string;
91
+ sessionId?: string | null;
92
+ action: ListeningAnalyticsAction;
93
+ trackId?: string | null;
94
+ trackTitle: string;
95
+ trackArtist?: string | null;
96
+ pagePath: string;
97
+ surface: ListeningAnalyticsSurface;
98
+ listenedMs: number;
99
+ mediaTimeMs: number;
100
+ durationMs: number;
101
+ requestCount: number;
102
+ playCount: number;
103
+ pauseCount: number;
104
+ seekCount: number;
105
+ completed: boolean;
106
+ }
107
+ export interface ListeningAnalyticsTotals {
108
+ requests: number;
109
+ plays: number;
110
+ uniqueListeners: number;
111
+ totalListenedMs: number;
112
+ avgListenedMs: number;
113
+ completionRate: number;
114
+ }
115
+ export interface ListeningAnalyticsTrackStats extends ListeningAnalyticsTotals {
116
+ trackId: string | null;
117
+ trackTitle: string;
118
+ trackArtist: string | null;
119
+ }
120
+ export interface ListeningAnalyticsSurfaceStats extends ListeningAnalyticsTotals {
121
+ surface: ListeningAnalyticsSurface;
122
+ }
123
+ export interface ListeningAnalyticsDailyStats extends ListeningAnalyticsTotals {
124
+ day: string;
125
+ }
126
+ export interface ListeningAnalyticsSessionRecord extends ListeningAnalyticsEventInput {
127
+ startedAt: string;
128
+ lastEventAt: string;
129
+ ip: string | null;
130
+ country: string | null;
131
+ city: string | null;
132
+ browser: string | null;
133
+ os: string | null;
134
+ deviceType: BlogAnalyticsDeviceType | null;
135
+ platform: BlogAnalyticsPlatform;
136
+ }
137
+ export interface ListeningAnalyticsSummary {
138
+ totals: ListeningAnalyticsTotals;
139
+ tracks: ListeningAnalyticsTrackStats[];
140
+ surfaces: ListeningAnalyticsSurfaceStats[];
141
+ daily: ListeningAnalyticsDailyStats[];
142
+ recent: ListeningAnalyticsSessionRecord[];
143
+ }
144
+ export interface BlogAnalyticsSummaryResult {
145
+ range: {
146
+ from: string | null;
147
+ to: string | null;
148
+ days: number;
149
+ };
150
+ totals: BlogAnalyticsTotals;
151
+ articles: BlogAnalyticsArticleStats[];
152
+ platforms: BlogAnalyticsBreakdown[];
153
+ countries: BlogAnalyticsBreakdown[];
154
+ referrers: BlogAnalyticsBreakdown[];
155
+ daily: BlogAnalyticsDailyStats[];
156
+ newsletter?: NewsletterAnalyticsSummary;
157
+ listening?: ListeningAnalyticsSummary;
158
+ }
159
+ export interface BlogAnalyticsEventRecord {
160
+ eventId: string;
161
+ slug: string;
162
+ visitorId: string;
163
+ sessionId: string | null;
164
+ openedAt: string;
165
+ dwellMs: number;
166
+ scrollDepth: number;
167
+ completed: boolean;
168
+ ip: string | null;
169
+ country: string | null;
170
+ region: string | null;
171
+ city: string | null;
172
+ asn: number | null;
173
+ asOrg: string | null;
174
+ colo: string | null;
175
+ ua: string | null;
176
+ browser: string | null;
177
+ os: string | null;
178
+ deviceType: BlogAnalyticsDeviceType | null;
179
+ platform: BlogAnalyticsPlatform;
180
+ lang: string | null;
181
+ referrer: string | null;
182
+ refSource: BlogAnalyticsRefSource;
183
+ createdAt: string;
184
+ updatedAt: string;
185
+ }
186
+ export interface BlogAnalyticsEventsResult {
187
+ events: BlogAnalyticsEventRecord[];
188
+ total?: number;
189
+ nextCursor?: string | null;
190
+ }
191
+ export interface BlogAnalyticsScrollBucket {
192
+ bucket: string;
193
+ count: number;
194
+ }
195
+ export interface BlogAnalyticsArticleDetailResult {
196
+ slug: string;
197
+ range: {
198
+ from: string | null;
199
+ to: string | null;
200
+ days: number;
201
+ };
202
+ totals: BlogAnalyticsTotals;
203
+ daily: BlogAnalyticsDailyStats[];
204
+ referrers: BlogAnalyticsBreakdown[];
205
+ platforms: BlogAnalyticsBreakdown[];
206
+ countries: BlogAnalyticsBreakdown[];
207
+ scrollBuckets: BlogAnalyticsScrollBucket[];
208
+ }
@@ -0,0 +1,25 @@
1
+ // src/analytics.ts
2
+ var BLOG_ANALYTICS_EVENT_ENDPOINT = "/api/analytics/event";
3
+ var BLOG_ANALYTICS_SUMMARY_ENDPOINT = "/api/analytics/summary";
4
+ var BLOG_ANALYTICS_EVENTS_ENDPOINT = "/api/analytics/events";
5
+ var BLOG_ANALYTICS_ARTICLE_ENDPOINT = "/api/analytics/article";
6
+ var NEWSLETTER_ANALYTICS_OPEN_ENDPOINT = "/api/analytics/newsletter/open";
7
+ var NEWSLETTER_ANALYTICS_CLICK_ENDPOINT = "/api/analytics/newsletter/click";
8
+ var LISTENING_ANALYTICS_EVENT_ENDPOINT = "/api/v2/analytics/listening";
9
+ var BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT = 50;
10
+ var BLOG_ANALYTICS_READ_THRESHOLD_MS = 5000;
11
+ var BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH = 0.9;
12
+ var BLOG_ANALYTICS_RANGE_OPTIONS = [7, 30, 90];
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,
23
+ BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH,
24
+ BLOG_ANALYTICS_ARTICLE_ENDPOINT
25
+ };
@@ -0,0 +1,96 @@
1
+ export declare const CONTENT_DOCUMENT_SOURCES: readonly ['mood', 'post'];
2
+ export type ContentDocumentSource = (typeof CONTENT_DOCUMENT_SOURCES)[number];
3
+ export interface PostLocaleTag {
4
+ locale: string;
5
+ canonicalSlug?: string;
6
+ }
7
+ /**
8
+ * Parses Ghost's internal post locale tag from tag.name.
9
+ *
10
+ * `#<locale>` marks the post's own locale. `#<locale>:<canonical>` marks a
11
+ * translated version of the canonical post. Ghost removes the colon from
12
+ * tag.slug, so callers must pass tag.name.
13
+ */
14
+ export declare function parsePostLocaleTag(tagName: string): PostLocaleTag | null;
15
+ export type ContentMediaType = 'image' | 'video' | 'audio' | 'document' | 'embed' | 'sticker' | 'link-preview' | 'location' | 'poll';
16
+ export type ContentMediaLayout = 'landscape' | 'portrait' | 'ultra-tall';
17
+ export interface MediaItem {
18
+ id?: string;
19
+ type: ContentMediaType;
20
+ src?: string;
21
+ href?: string;
22
+ fallbackSrc?: string | null;
23
+ posterSrc?: string | null;
24
+ thumbnailSrc?: string | null;
25
+ width?: number | null;
26
+ height?: number | null;
27
+ layout?: ContentMediaLayout | null;
28
+ alt?: string;
29
+ title?: string;
30
+ description?: string;
31
+ siteName?: string;
32
+ linkPreviewLayout?: 'large' | 'compact';
33
+ fileName?: string;
34
+ fileSizeLabel?: string;
35
+ mimeType?: string;
36
+ durationSeconds?: number | null;
37
+ originalUrl?: string;
38
+ }
39
+ export interface Reaction {
40
+ emoji: string;
41
+ emojiId?: string;
42
+ emojiImage?: string;
43
+ count: string;
44
+ isPaid: boolean;
45
+ }
46
+ export interface ForwardedFrom {
47
+ name: string;
48
+ href?: string;
49
+ author?: string;
50
+ }
51
+ export interface QuoteRef {
52
+ text: string;
53
+ author?: string;
54
+ href?: string;
55
+ thumbnailSrc?: string;
56
+ }
57
+ export interface ContentDocument {
58
+ id: string;
59
+ source: ContentDocumentSource;
60
+ datetime: string;
61
+ updatedAt?: string;
62
+ url?: string;
63
+ slug?: string;
64
+ title?: string;
65
+ excerpt?: string;
66
+ tag?: string;
67
+ tags?: string[];
68
+ bodyHtml: string;
69
+ previewText?: string;
70
+ previewHtml?: string;
71
+ hero?: MediaItem | null;
72
+ media: MediaItem[];
73
+ forwardedFrom?: ForwardedFrom | null;
74
+ quote?: QuoteRef | null;
75
+ reactions?: Reaction[];
76
+ commentsCount?: number | string;
77
+ }
78
+ export interface MoodContentDocument extends ContentDocument {
79
+ source: 'mood';
80
+ groupIds?: string[];
81
+ channel?: ContentChannelSummary;
82
+ }
83
+ export interface PostContentDocument extends ContentDocument {
84
+ source: 'post';
85
+ title: string;
86
+ slug: string;
87
+ }
88
+ export interface ContentChannelSummary {
89
+ slug?: string;
90
+ title?: string;
91
+ titleHTML?: string;
92
+ emojiId?: string;
93
+ avatar?: string;
94
+ description?: string;
95
+ descriptionHTML?: string;
96
+ }
@@ -0,0 +1,23 @@
1
+ // src/content.ts
2
+ var CONTENT_DOCUMENT_SOURCES = ["mood", "post"];
3
+ var POST_LOCALE_RE = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/;
4
+ var POST_CANONICAL_SLUG_RE = /^[^\s:/?#]+$/;
5
+ function parsePostLocaleTag(tagName) {
6
+ const normalized = tagName.trim().toLowerCase();
7
+ if (!normalized.startsWith("#"))
8
+ return null;
9
+ const value = normalized.slice(1);
10
+ const separatorIndex = value.indexOf(":");
11
+ const locale = separatorIndex === -1 ? value : value.slice(0, separatorIndex);
12
+ const canonicalSlug = separatorIndex === -1 ? undefined : value.slice(separatorIndex + 1);
13
+ if (!POST_LOCALE_RE.test(locale))
14
+ return null;
15
+ if (canonicalSlug !== undefined && !POST_CANONICAL_SLUG_RE.test(canonicalSlug)) {
16
+ return null;
17
+ }
18
+ return canonicalSlug ? { locale, canonicalSlug } : { locale };
19
+ }
20
+ export {
21
+ parsePostLocaleTag,
22
+ CONTENT_DOCUMENT_SOURCES
23
+ };
@@ -0,0 +1,8 @@
1
+ export * from './analytics';
2
+ export * from './admin';
3
+ export * from './content';
4
+ export * from './listening';
5
+ export * from './mood';
6
+ export * from './notify';
7
+ export * from './routes';
8
+ export * from './telegram-ops';
package/dist/index.js ADDED
@@ -0,0 +1,180 @@
1
+ // src/analytics.ts
2
+ var BLOG_ANALYTICS_EVENT_ENDPOINT = "/api/analytics/event";
3
+ var BLOG_ANALYTICS_SUMMARY_ENDPOINT = "/api/analytics/summary";
4
+ var BLOG_ANALYTICS_EVENTS_ENDPOINT = "/api/analytics/events";
5
+ var BLOG_ANALYTICS_ARTICLE_ENDPOINT = "/api/analytics/article";
6
+ var NEWSLETTER_ANALYTICS_OPEN_ENDPOINT = "/api/analytics/newsletter/open";
7
+ var NEWSLETTER_ANALYTICS_CLICK_ENDPOINT = "/api/analytics/newsletter/click";
8
+ var LISTENING_ANALYTICS_EVENT_ENDPOINT = "/api/v2/analytics/listening";
9
+ var BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT = 50;
10
+ var BLOG_ANALYTICS_READ_THRESHOLD_MS = 5000;
11
+ var BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH = 0.9;
12
+ var BLOG_ANALYTICS_RANGE_OPTIONS = [7, 30, 90];
13
+
14
+ // src/content.ts
15
+ var CONTENT_DOCUMENT_SOURCES = ["mood", "post"];
16
+ var POST_LOCALE_RE = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/;
17
+ var POST_CANONICAL_SLUG_RE = /^[^\s:/?#]+$/;
18
+ function parsePostLocaleTag(tagName) {
19
+ const normalized = tagName.trim().toLowerCase();
20
+ if (!normalized.startsWith("#"))
21
+ return null;
22
+ const value = normalized.slice(1);
23
+ const separatorIndex = value.indexOf(":");
24
+ const locale = separatorIndex === -1 ? value : value.slice(0, separatorIndex);
25
+ const canonicalSlug = separatorIndex === -1 ? undefined : value.slice(separatorIndex + 1);
26
+ if (!POST_LOCALE_RE.test(locale))
27
+ return null;
28
+ if (canonicalSlug !== undefined && !POST_CANONICAL_SLUG_RE.test(canonicalSlug)) {
29
+ return null;
30
+ }
31
+ return canonicalSlug ? { locale, canonicalSlug } : { locale };
32
+ }
33
+ // src/mood.ts
34
+ var MOOD_SENTIMENT_LABELS = ["joy", "calm", "melancholy", "anger", "anxiety", "neutral"];
35
+ var MOOD_AI_MODELS = ["gpt-5.5", "gpt-5", "claude-sonnet-4.6"];
36
+
37
+ // src/notify.ts
38
+ var NOTIFY_CHANNELS = ["mood", "blog", "privacy", "announcement"];
39
+
40
+ // src/routes.ts
41
+ var API_PREFIX = "/api";
42
+ var HEALTH_PATH = "/health";
43
+ var PING_PATH = "/ping";
44
+ var MOOD_PUBLIC_FEED_PATH = "/api/moods";
45
+ var MOOD_PUBLIC_COMMENTS_PATH = "/api/comments";
46
+ var MOOD_LIVE_FEED_PATH = "/v1/mood";
47
+ var MOOD_LIVE_META_PATH = "/v1/mood/meta";
48
+ var MOOD_ARCHIVE_FEED_PATH = "/v2/mood";
49
+ var MOOD_LIVE_COUNTS_PATH = "/v2/moods/live-counts";
50
+ var MOOD_ARCHIVE_STATS_PATH = "/v2/mood/stats";
51
+ var MOOD_SEARCH_PATH = "/v2/mood/search";
52
+ var MOOD_IMAGE_PROXY_BASE_PATH = "/v2/images";
53
+ var MOOD_MEDIA_PROXY_BASE_PATH = "/v2/media";
54
+ var LISTENING_PATH = "/v2/listening";
55
+ var ADMIN_BASE_PATH = "/admin";
56
+ var NOTIFY_BASE_PATH = "/notify";
57
+ var GHOST_WEBHOOK_PATH = "/webhooks/ghost";
58
+ var TELEGRAM_WEBHOOK_PATH = "/webhooks/telegram";
59
+ var MUSICKIT_TOKEN_PATH = "/musickit/token";
60
+ var LEGACY_ADMIN_BASE_PATH = "/v2/admin";
61
+ var LEGACY_NOTIFY_BASE_PATH = "/v2/notify";
62
+ var LEGACY_GHOST_WEBHOOK_PATH = "/v2/ghost/webhook";
63
+ var LEGACY_MUSICKIT_TOKEN_PATH = "/v2/musickit/token";
64
+ var LEGACY_HEALTH_PATH = "/v2/health";
65
+
66
+ // src/telegram-ops.ts
67
+ var TELEGRAM_OPS_WEBHOOK_PATH = "/webhooks/telegram-ops";
68
+ var NOTIFY_GATE_PATH = "/admin/notify-gate";
69
+ var NOTIFY_GATE_RELEASE_PATH = `${NOTIFY_GATE_PATH}/release`;
70
+ var NOTIFY_GATE_STATES = ["open", "held"];
71
+ var NOTIFY_GATE_DECISIONS = ["digest", "individual", "drop"];
72
+ var TELEGRAM_OPS_BASE_PATH = "/admin/integrations/telegram-ops";
73
+ var TELEGRAM_OPS_OVERVIEW_PATH = `${TELEGRAM_OPS_BASE_PATH}/overview`;
74
+ var TELEGRAM_OPS_EVENTS_PATH = `${TELEGRAM_OPS_BASE_PATH}/events`;
75
+ var TELEGRAM_OPS_SUBSCRIBERS_PATH = `${TELEGRAM_OPS_BASE_PATH}/subscribers`;
76
+ var TELEGRAM_OPS_BROADCASTS_PATH = `${TELEGRAM_OPS_BASE_PATH}/broadcasts`;
77
+ var TELEGRAM_OPS_BROADCAST_PREVIEW_PATH = `${TELEGRAM_OPS_BROADCASTS_PATH}/preview`;
78
+ var TELEGRAM_OPS_BROADCAST_SEND_PATH = `${TELEGRAM_OPS_BROADCASTS_PATH}/send`;
79
+ var TELEGRAM_OPS_REMINDERS_PATH = `${TELEGRAM_OPS_BASE_PATH}/reminders`;
80
+ var TELEGRAM_OPS_REMINDERS_DUE_PATH = `${TELEGRAM_OPS_REMINDERS_PATH}/due`;
81
+ function telegramOpsEventPath(id) {
82
+ return `${TELEGRAM_OPS_EVENTS_PATH}/${encodeURIComponent(id)}`;
83
+ }
84
+ function telegramOpsEventActionPath(id, action) {
85
+ return `${telegramOpsEventPath(id)}/${action}`;
86
+ }
87
+ function telegramOpsSubscriberPath(emailHash) {
88
+ return `${TELEGRAM_OPS_SUBSCRIBERS_PATH}/${encodeURIComponent(emailHash)}`;
89
+ }
90
+ function telegramOpsReminderDeliveredPath(eventId) {
91
+ return `${TELEGRAM_OPS_REMINDERS_PATH}/${encodeURIComponent(eventId)}/delivered`;
92
+ }
93
+ var EVENT_STATUSES = ["draft", "published", "cancelled"];
94
+ var TELEGRAM_OPS_SUBSCRIBER_STATUSES = [
95
+ "pending",
96
+ "active",
97
+ "unsubscribed"
98
+ ];
99
+ var TELEGRAM_OPS_BROADCAST_CHANNELS = [
100
+ "mood",
101
+ "blog",
102
+ "privacy",
103
+ "announcement"
104
+ ];
105
+ var TELEGRAM_OPS_BROADCAST_DELIVERY_MODES = [
106
+ "immediate",
107
+ "every_5h",
108
+ "daily"
109
+ ];
110
+ var TELEGRAM_OPS_BROADCAST_STATUSES = [
111
+ "draft",
112
+ "sending",
113
+ "sent",
114
+ "failed"
115
+ ];
116
+ 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,
178
+ API_PREFIX,
179
+ ADMIN_BASE_PATH
180
+ };