@vigilkids/cms-client 0.0.1

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present VigilKids
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @vigilkids/cms-client
2
+
3
+ TypeScript HTTP client for the OneX CMS delivery API. Framework-agnostic — works with any HTTP client (fetch, axios, etc.).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @vigilkids/cms-client
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createCmsClient } from '@vigilkids/cms-client'
15
+
16
+ const cms = createCmsClient({
17
+ baseUrl: 'https://api.example.com',
18
+ productCode: 'visiva',
19
+ })
20
+
21
+ // Fetch article by slug
22
+ const article = await cms.getArticleBySlug('my-article', { locale: 'en' })
23
+
24
+ // List articles
25
+ const { data, pagination } = await cms.listArticles({ page: 1, pageSize: 20 })
26
+ ```
27
+
28
+ ## Exports
29
+
30
+ | Entry Point | Import | Content |
31
+ |-------------|--------|---------|
32
+ | Main | `@vigilkids/cms-client` | Client factory and API methods |
33
+ | Types | `@vigilkids/cms-client/types` | Request/response type definitions |
34
+ | Webhook | `@vigilkids/cms-client/webhook` | Webhook payload types and validation |
35
+
36
+ ## Compatibility
37
+
38
+ - Node.js ≥ 18
39
+ - ESM only
40
+ - TypeScript ≥ 5.0
41
+
42
+ ## License
43
+
44
+ [MIT](./LICENSE)
@@ -0,0 +1,78 @@
1
+ import { ArticleListParams, Paginated, ArticleListItem, ArticleDetail, Author, CategoryNode, Comment, CreateCommentPayload, SitemapEntry, RedirectRule, Series } from './types.mjs';
2
+ export { HreflangEntry, LocalizedString, OgMeta, PaginationMeta, RevalidatePayload, SeoMeta, TwitterMeta, WebhookEventType } from './types.mjs';
3
+
4
+ /** CMS 客户端配置 */
5
+ interface CmsClientConfig {
6
+ /** CMS API 基础 URL */
7
+ baseUrl: string;
8
+ /** 产品编码,注入 X-Product-Code header */
9
+ productCode?: string;
10
+ /** 默认语言 */
11
+ defaultLocale?: string;
12
+ /** 预览令牌 */
13
+ previewToken?: string;
14
+ /** 自定义 fetch 实现 */
15
+ fetch?: typeof globalThis.fetch;
16
+ /** 请求超时(毫秒),默认 10000 */
17
+ timeout?: number;
18
+ /** 自定义请求头 */
19
+ headers?: Record<string, string>;
20
+ }
21
+
22
+ /** CMS Public API 客户端 */
23
+ declare class CmsClient {
24
+ private readonly config;
25
+ private readonly fetchFn;
26
+ constructor(config: CmsClientConfig);
27
+ /** 获取文章分页列表 */
28
+ getArticles(params?: ArticleListParams): Promise<Paginated<ArticleListItem>>;
29
+ /** 获取文章详情 */
30
+ getArticle(slug: string, locale?: string): Promise<ArticleDetail>;
31
+ /** 获取相关文章 */
32
+ getRelatedArticles(slug: string, options?: {
33
+ limit?: number;
34
+ locale?: string;
35
+ }): Promise<ArticleListItem[]>;
36
+ /** 获取所有作者 */
37
+ getAuthors(): Promise<Author[]>;
38
+ /** 获取作者详情 */
39
+ getAuthor(slug: string): Promise<Author>;
40
+ /** 获取作者的文章列表 */
41
+ getAuthorArticles(slug: string, options?: {
42
+ page?: number;
43
+ page_size?: number;
44
+ locale?: string;
45
+ }): Promise<ArticleListItem[]>;
46
+ /** 获取分类树 */
47
+ getCategories(): Promise<CategoryNode[]>;
48
+ /** 获取文章评论树 */
49
+ getComments(articleSlug: string): Promise<Comment[]>;
50
+ /** 发表评论 */
51
+ createComment(articleSlug: string, payload: CreateCommentPayload): Promise<void>;
52
+ /** 获取 Sitemap 条目 */
53
+ getSitemap(locale?: string): Promise<SitemapEntry[]>;
54
+ /** 获取重定向规则 */
55
+ getRedirects(): Promise<RedirectRule[]>;
56
+ /** 获取文章系列 */
57
+ getSeries(slug: string, locale?: string): Promise<Series>;
58
+ private request;
59
+ /** 构建完整 URL,过滤 undefined 参数 */
60
+ private buildUrl;
61
+ /** 构建请求头 */
62
+ private buildHeaders;
63
+ }
64
+ /** 创建 CMS 客户端实例 */
65
+ declare function createCmsClient(config: CmsClientConfig): CmsClient;
66
+
67
+ /** CMS API 错误 */
68
+ declare class CmsApiError extends Error {
69
+ readonly statusCode: number;
70
+ readonly errorCode: string;
71
+ readonly body: unknown;
72
+ constructor(statusCode: number, errorCode: string, message: string, body?: unknown);
73
+ get isNotFound(): boolean;
74
+ get isUnauthorized(): boolean;
75
+ }
76
+
77
+ export { ArticleDetail, ArticleListItem, ArticleListParams, Author, CategoryNode, CmsApiError, CmsClient, Comment, CreateCommentPayload, Paginated, RedirectRule, Series, SitemapEntry, createCmsClient };
78
+ export type { CmsClientConfig };
@@ -0,0 +1,78 @@
1
+ import { ArticleListParams, Paginated, ArticleListItem, ArticleDetail, Author, CategoryNode, Comment, CreateCommentPayload, SitemapEntry, RedirectRule, Series } from './types.js';
2
+ export { HreflangEntry, LocalizedString, OgMeta, PaginationMeta, RevalidatePayload, SeoMeta, TwitterMeta, WebhookEventType } from './types.js';
3
+
4
+ /** CMS 客户端配置 */
5
+ interface CmsClientConfig {
6
+ /** CMS API 基础 URL */
7
+ baseUrl: string;
8
+ /** 产品编码,注入 X-Product-Code header */
9
+ productCode?: string;
10
+ /** 默认语言 */
11
+ defaultLocale?: string;
12
+ /** 预览令牌 */
13
+ previewToken?: string;
14
+ /** 自定义 fetch 实现 */
15
+ fetch?: typeof globalThis.fetch;
16
+ /** 请求超时(毫秒),默认 10000 */
17
+ timeout?: number;
18
+ /** 自定义请求头 */
19
+ headers?: Record<string, string>;
20
+ }
21
+
22
+ /** CMS Public API 客户端 */
23
+ declare class CmsClient {
24
+ private readonly config;
25
+ private readonly fetchFn;
26
+ constructor(config: CmsClientConfig);
27
+ /** 获取文章分页列表 */
28
+ getArticles(params?: ArticleListParams): Promise<Paginated<ArticleListItem>>;
29
+ /** 获取文章详情 */
30
+ getArticle(slug: string, locale?: string): Promise<ArticleDetail>;
31
+ /** 获取相关文章 */
32
+ getRelatedArticles(slug: string, options?: {
33
+ limit?: number;
34
+ locale?: string;
35
+ }): Promise<ArticleListItem[]>;
36
+ /** 获取所有作者 */
37
+ getAuthors(): Promise<Author[]>;
38
+ /** 获取作者详情 */
39
+ getAuthor(slug: string): Promise<Author>;
40
+ /** 获取作者的文章列表 */
41
+ getAuthorArticles(slug: string, options?: {
42
+ page?: number;
43
+ page_size?: number;
44
+ locale?: string;
45
+ }): Promise<ArticleListItem[]>;
46
+ /** 获取分类树 */
47
+ getCategories(): Promise<CategoryNode[]>;
48
+ /** 获取文章评论树 */
49
+ getComments(articleSlug: string): Promise<Comment[]>;
50
+ /** 发表评论 */
51
+ createComment(articleSlug: string, payload: CreateCommentPayload): Promise<void>;
52
+ /** 获取 Sitemap 条目 */
53
+ getSitemap(locale?: string): Promise<SitemapEntry[]>;
54
+ /** 获取重定向规则 */
55
+ getRedirects(): Promise<RedirectRule[]>;
56
+ /** 获取文章系列 */
57
+ getSeries(slug: string, locale?: string): Promise<Series>;
58
+ private request;
59
+ /** 构建完整 URL,过滤 undefined 参数 */
60
+ private buildUrl;
61
+ /** 构建请求头 */
62
+ private buildHeaders;
63
+ }
64
+ /** 创建 CMS 客户端实例 */
65
+ declare function createCmsClient(config: CmsClientConfig): CmsClient;
66
+
67
+ /** CMS API 错误 */
68
+ declare class CmsApiError extends Error {
69
+ readonly statusCode: number;
70
+ readonly errorCode: string;
71
+ readonly body: unknown;
72
+ constructor(statusCode: number, errorCode: string, message: string, body?: unknown);
73
+ get isNotFound(): boolean;
74
+ get isUnauthorized(): boolean;
75
+ }
76
+
77
+ export { ArticleDetail, ArticleListItem, ArticleListParams, Author, CategoryNode, CmsApiError, CmsClient, Comment, CreateCommentPayload, Paginated, RedirectRule, Series, SitemapEntry, createCmsClient };
78
+ export type { CmsClientConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1,181 @@
1
+ const DEFAULT_CONFIG = {
2
+ defaultLocale: "en",
3
+ timeout: 1e4
4
+ };
5
+
6
+ class CmsApiError extends Error {
7
+ statusCode;
8
+ errorCode;
9
+ body;
10
+ constructor(statusCode, errorCode, message, body) {
11
+ super(message);
12
+ this.name = "CmsApiError";
13
+ this.statusCode = statusCode;
14
+ this.errorCode = errorCode;
15
+ this.body = body;
16
+ }
17
+ get isNotFound() {
18
+ return this.statusCode === 404;
19
+ }
20
+ get isUnauthorized() {
21
+ return this.statusCode === 401;
22
+ }
23
+ }
24
+
25
+ class CmsClient {
26
+ config;
27
+ fetchFn;
28
+ constructor(config) {
29
+ this.config = { ...DEFAULT_CONFIG, ...config };
30
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
31
+ }
32
+ // ─── 文章 ───────────────────────────────────────────
33
+ /** 获取文章分页列表 */
34
+ async getArticles(params) {
35
+ return this.request("GET", "/articles", params);
36
+ }
37
+ /** 获取文章详情 */
38
+ async getArticle(slug, locale) {
39
+ return this.request("GET", `/articles/${encodeURIComponent(slug)}`, { locale });
40
+ }
41
+ /** 获取相关文章 */
42
+ async getRelatedArticles(slug, options) {
43
+ return this.request(
44
+ "GET",
45
+ `/articles/${encodeURIComponent(slug)}/related`,
46
+ options
47
+ );
48
+ }
49
+ // ─── 作者 ───────────────────────────────────────────
50
+ /** 获取所有作者 */
51
+ async getAuthors() {
52
+ return this.request("GET", "/authors");
53
+ }
54
+ /** 获取作者详情 */
55
+ async getAuthor(slug) {
56
+ return this.request("GET", `/authors/${encodeURIComponent(slug)}`);
57
+ }
58
+ /** 获取作者的文章列表 */
59
+ async getAuthorArticles(slug, options) {
60
+ return this.request(
61
+ "GET",
62
+ `/authors/${encodeURIComponent(slug)}/articles`,
63
+ options
64
+ );
65
+ }
66
+ // ─── 分类 ───────────────────────────────────────────
67
+ /** 获取分类树 */
68
+ async getCategories() {
69
+ return this.request("GET", "/categories");
70
+ }
71
+ // ─── 评论 ───────────────────────────────────────────
72
+ /** 获取文章评论树 */
73
+ async getComments(articleSlug) {
74
+ return this.request(
75
+ "GET",
76
+ `/articles/${encodeURIComponent(articleSlug)}/comments`
77
+ );
78
+ }
79
+ /** 发表评论 */
80
+ async createComment(articleSlug, payload) {
81
+ return this.request(
82
+ "POST",
83
+ `/articles/${encodeURIComponent(articleSlug)}/comments`,
84
+ void 0,
85
+ payload
86
+ );
87
+ }
88
+ // ─── Sitemap / 重定向 / 系列 ──────────────────────
89
+ /** 获取 Sitemap 条目 */
90
+ async getSitemap(locale) {
91
+ return this.request("GET", "/sitemap", { locale });
92
+ }
93
+ /** 获取重定向规则 */
94
+ async getRedirects() {
95
+ return this.request("GET", "/redirects");
96
+ }
97
+ /** 获取文章系列 */
98
+ async getSeries(slug, locale) {
99
+ return this.request("GET", `/series/${encodeURIComponent(slug)}`, { locale });
100
+ }
101
+ // ─── 内部请求方法 ──────────────────────────────────
102
+ async request(method, path, query, body) {
103
+ if (method === "GET" && this.config.defaultLocale) {
104
+ const q = query ?? {};
105
+ if (q.locale === void 0 || q.locale === null) {
106
+ q.locale = this.config.defaultLocale;
107
+ }
108
+ query = q;
109
+ }
110
+ const url = this.buildUrl(path, query);
111
+ const headers = this.buildHeaders(method);
112
+ const controller = new AbortController();
113
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
114
+ try {
115
+ const response = await this.fetchFn(url, {
116
+ method,
117
+ headers,
118
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
119
+ signal: controller.signal
120
+ });
121
+ if (!response.ok) {
122
+ let errorBody;
123
+ let errorCode = "UNKNOWN_ERROR";
124
+ let errorMessage = `CMS API \u8BF7\u6C42\u5931\u8D25: ${response.status}`;
125
+ try {
126
+ errorBody = await response.json();
127
+ if (typeof errorBody === "object" && errorBody !== null && "code" in errorBody && "message" in errorBody) {
128
+ const parsed = errorBody;
129
+ errorCode = parsed.code;
130
+ errorMessage = parsed.message;
131
+ }
132
+ } catch {
133
+ }
134
+ throw new CmsApiError(response.status, errorCode, errorMessage, errorBody);
135
+ }
136
+ if (response.status === 204) {
137
+ return void 0;
138
+ }
139
+ return await response.json();
140
+ } finally {
141
+ clearTimeout(timeoutId);
142
+ }
143
+ }
144
+ /** 构建完整 URL,过滤 undefined 参数 */
145
+ buildUrl(path, query) {
146
+ const base = this.config.baseUrl.replace(/\/+$/, "");
147
+ const url = new URL(`${base}${path}`);
148
+ if (query) {
149
+ for (const [key, value] of Object.entries(query)) {
150
+ if (value !== void 0 && value !== null) {
151
+ url.searchParams.set(key, String(value));
152
+ }
153
+ }
154
+ }
155
+ return url.toString();
156
+ }
157
+ /** 构建请求头 */
158
+ buildHeaders(method) {
159
+ const headers = {
160
+ Accept: "application/json"
161
+ };
162
+ if (method === "POST" || method === "PUT") {
163
+ headers["Content-Type"] = "application/json";
164
+ }
165
+ if (this.config.productCode) {
166
+ headers["X-Product-Code"] = this.config.productCode;
167
+ }
168
+ if (this.config.previewToken) {
169
+ headers["X-Preview-Token"] = this.config.previewToken;
170
+ }
171
+ if (this.config.headers) {
172
+ Object.assign(headers, this.config.headers);
173
+ }
174
+ return headers;
175
+ }
176
+ }
177
+ function createCmsClient(config) {
178
+ return new CmsClient(config);
179
+ }
180
+
181
+ export { CmsApiError, CmsClient, createCmsClient };
@@ -0,0 +1,169 @@
1
+ /** 多语言字符串 (Go: domain.LocalizedString = map[string]string) */
2
+ type LocalizedString = Record<string, string>;
3
+ /** 分页元信息 (Go: response.PaginationMeta) */
4
+ interface PaginationMeta {
5
+ page: number;
6
+ page_size: number;
7
+ total: number;
8
+ total_pages: number;
9
+ }
10
+ /** 分页响应 (Go: response.Paginated[T]) */
11
+ interface Paginated<T> {
12
+ data: T[];
13
+ pagination: PaginationMeta;
14
+ }
15
+
16
+ /** 作者信息 (Go: delivery.DeliveryAuthorResponse) */
17
+ interface Author {
18
+ display_name: string;
19
+ slug: string;
20
+ avatar_url?: string;
21
+ bio?: string;
22
+ }
23
+
24
+ /** 文章详情 (Go: delivery.DeliveryArticleResponse) */
25
+ interface ArticleDetail {
26
+ slug: string;
27
+ title: string;
28
+ excerpt?: string;
29
+ content_html: string;
30
+ content_json?: unknown;
31
+ featured_image_url?: string;
32
+ featured_image_alt?: string;
33
+ category?: string;
34
+ tags: string[];
35
+ authors: Author[];
36
+ related_article_ids?: number[];
37
+ comment_count: number;
38
+ is_comment_enabled: boolean;
39
+ published_at: string | null;
40
+ last_published_at: string | null;
41
+ word_count: number;
42
+ reading_time_minutes: number;
43
+ seo_meta?: SeoMeta;
44
+ og_meta?: OgMeta;
45
+ twitter_meta?: TwitterMeta;
46
+ json_ld: Record<string, unknown> | null;
47
+ locale: string;
48
+ available_locales: string[];
49
+ }
50
+ /** 文章列表项 (Go: delivery.DeliveryArticleListItem) */
51
+ interface ArticleListItem {
52
+ slug: string;
53
+ title: string;
54
+ excerpt?: string;
55
+ featured_image_url?: string;
56
+ featured_image_alt?: string;
57
+ published_at: string | null;
58
+ reading_time_minutes: number;
59
+ category_slug?: string;
60
+ is_pinned: boolean;
61
+ tags: string[];
62
+ authors: Author[];
63
+ }
64
+ /** 文章列表查询参数 */
65
+ interface ArticleListParams {
66
+ locale?: string;
67
+ category_id?: number;
68
+ category_slug?: string;
69
+ tag?: string;
70
+ is_pinned?: boolean;
71
+ page?: number;
72
+ page_size?: number;
73
+ }
74
+ /** SEO 元信息 */
75
+ interface SeoMeta {
76
+ title: string | null;
77
+ description: string | null;
78
+ keywords: string | null;
79
+ robots: string | null;
80
+ canonical_url: string | null;
81
+ }
82
+ /** Open Graph 元信息 */
83
+ interface OgMeta {
84
+ title: string | null;
85
+ description: string | null;
86
+ image: string | null;
87
+ url: string | null;
88
+ type: string | null;
89
+ }
90
+ /** Twitter Card 元信息 */
91
+ interface TwitterMeta {
92
+ card: string | null;
93
+ title: string | null;
94
+ description: string | null;
95
+ image: string | null;
96
+ }
97
+
98
+ /** 分类树节点 (Go: delivery.CategoryNodeResponse) */
99
+ interface CategoryNode {
100
+ id: number;
101
+ slug: string;
102
+ name: LocalizedString;
103
+ description: LocalizedString;
104
+ article_count: number;
105
+ article_counts?: Record<string, number>;
106
+ children?: CategoryNode[];
107
+ }
108
+
109
+ /** 评论树节点 (Go: delivery.CommentTreeResponse) */
110
+ interface Comment {
111
+ id: number;
112
+ author_name: string;
113
+ author_url?: string;
114
+ content_html: string;
115
+ is_pinned: boolean;
116
+ like_count: number;
117
+ created_at: string;
118
+ children?: Comment[];
119
+ }
120
+ /** 发表评论请求 */
121
+ interface CreateCommentPayload {
122
+ author_name: string;
123
+ author_email: string;
124
+ content: string;
125
+ parent_id?: number;
126
+ }
127
+
128
+ /** 重定向规则 (Go: delivery.RedirectRuleResponse) */
129
+ interface RedirectRule {
130
+ source_path: string;
131
+ destination_path: string;
132
+ status_code: number;
133
+ match_type: string;
134
+ }
135
+
136
+ /** 文章系列 (Go: delivery.DeliverySeriesResponse) */
137
+ interface Series {
138
+ title: string;
139
+ slug: string;
140
+ description: string | null;
141
+ cover_image_url: string | null;
142
+ articles: ArticleListItem[];
143
+ }
144
+
145
+ /** Hreflang 条目 (Go: delivery.HreflangEntry) */
146
+ interface HreflangEntry {
147
+ lang: string;
148
+ href: string;
149
+ }
150
+ /** Sitemap 条目 (Go: delivery.SitemapEntryResponse) */
151
+ interface SitemapEntry {
152
+ loc: string;
153
+ last_mod: string;
154
+ change_freq: string;
155
+ priority: number;
156
+ hreflang?: HreflangEntry[];
157
+ }
158
+
159
+ /** Webhook 事件类型 */
160
+ type WebhookEventType = 'article.published' | 'article.unpublished' | 'article.updated' | 'article.archived' | 'redirect.changed' | 'comment.approved';
161
+ /** ISR 重验证请求体 */
162
+ interface RevalidatePayload {
163
+ event: WebhookEventType;
164
+ slug: string;
165
+ locale: string;
166
+ category_slug?: string;
167
+ }
168
+
169
+ export type { ArticleDetail, ArticleListItem, ArticleListParams, Author, CategoryNode, Comment, CreateCommentPayload, HreflangEntry, LocalizedString, OgMeta, Paginated, PaginationMeta, RedirectRule, RevalidatePayload, SeoMeta, Series, SitemapEntry, TwitterMeta, WebhookEventType };
@@ -0,0 +1,169 @@
1
+ /** 多语言字符串 (Go: domain.LocalizedString = map[string]string) */
2
+ type LocalizedString = Record<string, string>;
3
+ /** 分页元信息 (Go: response.PaginationMeta) */
4
+ interface PaginationMeta {
5
+ page: number;
6
+ page_size: number;
7
+ total: number;
8
+ total_pages: number;
9
+ }
10
+ /** 分页响应 (Go: response.Paginated[T]) */
11
+ interface Paginated<T> {
12
+ data: T[];
13
+ pagination: PaginationMeta;
14
+ }
15
+
16
+ /** 作者信息 (Go: delivery.DeliveryAuthorResponse) */
17
+ interface Author {
18
+ display_name: string;
19
+ slug: string;
20
+ avatar_url?: string;
21
+ bio?: string;
22
+ }
23
+
24
+ /** 文章详情 (Go: delivery.DeliveryArticleResponse) */
25
+ interface ArticleDetail {
26
+ slug: string;
27
+ title: string;
28
+ excerpt?: string;
29
+ content_html: string;
30
+ content_json?: unknown;
31
+ featured_image_url?: string;
32
+ featured_image_alt?: string;
33
+ category?: string;
34
+ tags: string[];
35
+ authors: Author[];
36
+ related_article_ids?: number[];
37
+ comment_count: number;
38
+ is_comment_enabled: boolean;
39
+ published_at: string | null;
40
+ last_published_at: string | null;
41
+ word_count: number;
42
+ reading_time_minutes: number;
43
+ seo_meta?: SeoMeta;
44
+ og_meta?: OgMeta;
45
+ twitter_meta?: TwitterMeta;
46
+ json_ld: Record<string, unknown> | null;
47
+ locale: string;
48
+ available_locales: string[];
49
+ }
50
+ /** 文章列表项 (Go: delivery.DeliveryArticleListItem) */
51
+ interface ArticleListItem {
52
+ slug: string;
53
+ title: string;
54
+ excerpt?: string;
55
+ featured_image_url?: string;
56
+ featured_image_alt?: string;
57
+ published_at: string | null;
58
+ reading_time_minutes: number;
59
+ category_slug?: string;
60
+ is_pinned: boolean;
61
+ tags: string[];
62
+ authors: Author[];
63
+ }
64
+ /** 文章列表查询参数 */
65
+ interface ArticleListParams {
66
+ locale?: string;
67
+ category_id?: number;
68
+ category_slug?: string;
69
+ tag?: string;
70
+ is_pinned?: boolean;
71
+ page?: number;
72
+ page_size?: number;
73
+ }
74
+ /** SEO 元信息 */
75
+ interface SeoMeta {
76
+ title: string | null;
77
+ description: string | null;
78
+ keywords: string | null;
79
+ robots: string | null;
80
+ canonical_url: string | null;
81
+ }
82
+ /** Open Graph 元信息 */
83
+ interface OgMeta {
84
+ title: string | null;
85
+ description: string | null;
86
+ image: string | null;
87
+ url: string | null;
88
+ type: string | null;
89
+ }
90
+ /** Twitter Card 元信息 */
91
+ interface TwitterMeta {
92
+ card: string | null;
93
+ title: string | null;
94
+ description: string | null;
95
+ image: string | null;
96
+ }
97
+
98
+ /** 分类树节点 (Go: delivery.CategoryNodeResponse) */
99
+ interface CategoryNode {
100
+ id: number;
101
+ slug: string;
102
+ name: LocalizedString;
103
+ description: LocalizedString;
104
+ article_count: number;
105
+ article_counts?: Record<string, number>;
106
+ children?: CategoryNode[];
107
+ }
108
+
109
+ /** 评论树节点 (Go: delivery.CommentTreeResponse) */
110
+ interface Comment {
111
+ id: number;
112
+ author_name: string;
113
+ author_url?: string;
114
+ content_html: string;
115
+ is_pinned: boolean;
116
+ like_count: number;
117
+ created_at: string;
118
+ children?: Comment[];
119
+ }
120
+ /** 发表评论请求 */
121
+ interface CreateCommentPayload {
122
+ author_name: string;
123
+ author_email: string;
124
+ content: string;
125
+ parent_id?: number;
126
+ }
127
+
128
+ /** 重定向规则 (Go: delivery.RedirectRuleResponse) */
129
+ interface RedirectRule {
130
+ source_path: string;
131
+ destination_path: string;
132
+ status_code: number;
133
+ match_type: string;
134
+ }
135
+
136
+ /** 文章系列 (Go: delivery.DeliverySeriesResponse) */
137
+ interface Series {
138
+ title: string;
139
+ slug: string;
140
+ description: string | null;
141
+ cover_image_url: string | null;
142
+ articles: ArticleListItem[];
143
+ }
144
+
145
+ /** Hreflang 条目 (Go: delivery.HreflangEntry) */
146
+ interface HreflangEntry {
147
+ lang: string;
148
+ href: string;
149
+ }
150
+ /** Sitemap 条目 (Go: delivery.SitemapEntryResponse) */
151
+ interface SitemapEntry {
152
+ loc: string;
153
+ last_mod: string;
154
+ change_freq: string;
155
+ priority: number;
156
+ hreflang?: HreflangEntry[];
157
+ }
158
+
159
+ /** Webhook 事件类型 */
160
+ type WebhookEventType = 'article.published' | 'article.unpublished' | 'article.updated' | 'article.archived' | 'redirect.changed' | 'comment.approved';
161
+ /** ISR 重验证请求体 */
162
+ interface RevalidatePayload {
163
+ event: WebhookEventType;
164
+ slug: string;
165
+ locale: string;
166
+ category_slug?: string;
167
+ }
168
+
169
+ export type { ArticleDetail, ArticleListItem, ArticleListParams, Author, CategoryNode, Comment, CreateCommentPayload, HreflangEntry, LocalizedString, OgMeta, Paginated, PaginationMeta, RedirectRule, RevalidatePayload, SeoMeta, Series, SitemapEntry, TwitterMeta, WebhookEventType };
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,10 @@
1
+ interface VerifyWebhookOptions {
2
+ rawBody: string;
3
+ signature: string;
4
+ secret: string;
5
+ }
6
+ /** 验证 Webhook 签名(HMAC-SHA256) */
7
+ declare function verifyWebhookSignature(options: VerifyWebhookOptions): boolean;
8
+
9
+ export { verifyWebhookSignature };
10
+ export type { VerifyWebhookOptions };
@@ -0,0 +1,10 @@
1
+ interface VerifyWebhookOptions {
2
+ rawBody: string;
3
+ signature: string;
4
+ secret: string;
5
+ }
6
+ /** 验证 Webhook 签名(HMAC-SHA256) */
7
+ declare function verifyWebhookSignature(options: VerifyWebhookOptions): boolean;
8
+
9
+ export { verifyWebhookSignature };
10
+ export type { VerifyWebhookOptions };
@@ -0,0 +1,13 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+
3
+ function verifyWebhookSignature(options) {
4
+ const { rawBody, signature, secret } = options;
5
+ if (!signature || !rawBody || !secret) return false;
6
+ const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
7
+ const sigBuffer = Buffer.from(signature);
8
+ const expectedBuffer = Buffer.from(expected);
9
+ if (sigBuffer.length !== expectedBuffer.length) return false;
10
+ return timingSafeEqual(sigBuffer, expectedBuffer);
11
+ }
12
+
13
+ export { verifyWebhookSignature };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@vigilkids/cms-client",
3
+ "version": "0.0.1",
4
+ "description": "Framework-agnostic TypeScript client for OnEx CMS Public API",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs"
12
+ },
13
+ "./types": {
14
+ "types": "./dist/types.d.ts",
15
+ "import": "./dist/types.mjs"
16
+ },
17
+ "./webhook": {
18
+ "types": "./dist/webhook.d.ts",
19
+ "import": "./dist/webhook.mjs"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "unbuild"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/1yhy/onex.git",
35
+ "directory": "packages/cms-client"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.0.0",
39
+ "unbuild": "^3.5.0"
40
+ }
41
+ }