@yimingliao/cms 0.0.14 → 0.0.15

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.
@@ -204,4 +204,30 @@ var formatFileSize = (size, decimals = 2) => {
204
204
  return `${display} ${units[unitIndex]}`;
205
205
  };
206
206
 
207
- export { ADMIN_ROLES, FILE_TYPES, POST_TYPES, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension };
207
+ // src/shared/result/result.ts
208
+ function success({
209
+ message,
210
+ data,
211
+ meta
212
+ }) {
213
+ return {
214
+ success: true,
215
+ ...message !== void 0 ? { message } : {},
216
+ ...data !== void 0 ? { data } : {},
217
+ ...meta !== void 0 ? { meta } : {}
218
+ };
219
+ }
220
+ function error({ message, errors, code }) {
221
+ return {
222
+ success: false,
223
+ ...message !== void 0 ? { message } : {},
224
+ ...errors !== void 0 ? { errors } : {},
225
+ ...code !== void 0 ? { code } : {}
226
+ };
227
+ }
228
+ var result = {
229
+ success,
230
+ error
231
+ };
232
+
233
+ export { ADMIN_ROLES, FILE_TYPES, POST_TYPES, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension, result };
package/dist/index.d.ts CHANGED
@@ -27,4 +27,44 @@ interface BlobFile extends File {
27
27
  duration: number | null;
28
28
  }
29
29
 
30
- export { type BlobFile, FolderFull, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension };
30
+ interface SuccessResult<D = unknown> {
31
+ success: true;
32
+ message?: string;
33
+ data?: D;
34
+ meta?: Record<string, unknown>;
35
+ }
36
+ interface ErrorResult {
37
+ success: false;
38
+ message?: string;
39
+ errors?: ErrorDetail[];
40
+ code?: string;
41
+ }
42
+ interface ErrorDetail {
43
+ field?: string;
44
+ message?: string;
45
+ code?: string;
46
+ }
47
+ type Result<D = unknown> = SuccessResult<D> | ErrorResult;
48
+
49
+ interface SuccessResultParams<D = unknown> {
50
+ message?: string;
51
+ data?: D;
52
+ meta?: Record<string, unknown>;
53
+ }
54
+ declare function success<D = unknown>({ message, data, meta, }: SuccessResultParams<D>): SuccessResult<D>;
55
+ interface ErrorResultParams {
56
+ message?: string;
57
+ errors?: ErrorDetail[];
58
+ code?: string;
59
+ }
60
+ declare function error({ message, errors, code }: ErrorResultParams): ErrorResult;
61
+ /**
62
+ * Generic result interface for API and action responses.
63
+ * Provides a unified structure for success and error outputs.
64
+ */
65
+ declare const result: {
66
+ success: typeof success;
67
+ error: typeof error;
68
+ };
69
+
70
+ export { type BlobFile, type ErrorDetail, type ErrorResult, type ErrorResultParams, FolderFull, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, type Result, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, type SuccessResult, type SuccessResultParams, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension, result };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { ADMIN_ROLES, FILE_TYPES, POST_TYPES, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension } from './chunk-KEQXXUK2.js';
1
+ export { ADMIN_ROLES, FILE_TYPES, POST_TYPES, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension, result } from './chunk-SEX4DOKX.js';
@@ -1,6 +1,8 @@
1
1
  import jwt from 'jsonwebtoken';
2
2
  import { BinaryLike } from 'node:crypto';
3
3
  import { cookies } from 'next/headers';
4
+ import Keyv from 'keyv';
5
+ import { Logger } from 'logry';
4
6
  import { e as AdminRole, v as SingleItem, B as BaseTranslation, a as Admin, c as AdminFull, f as AdminSafe, D as DeviceInfo, d as AdminRefreshToken, k as File, m as FileFull, o as FileType, p as Folder, F as FolderFull, u as PostType, M as MultiItems, E as ExternalLink, j as Faq, T as TocItem, q as Post, s as PostListCard, t as PostTranslation, r as PostFull, S as SeoMetadata, g as AdminTranslation, n as FileTranslation, h as Alternate } from '../base-DbGnfZr6.js';
5
7
 
6
8
  interface JwtServiceConfig {
@@ -84,6 +86,29 @@ declare function createCookieService(nextCookies: () => Promise<Awaited<ReturnTy
84
86
  }) => Promise<void>;
85
87
  };
86
88
 
89
+ interface CreateServerCacheOptions {
90
+ redisUrl: string;
91
+ namespace: string;
92
+ keyDelimiter: string;
93
+ }
94
+ declare function createCache({ redisUrl, namespace, keyDelimiter, }: CreateServerCacheOptions): Keyv<unknown>;
95
+
96
+ type RawCacheKey = string | Array<string | number | boolean | undefined | null>;
97
+
98
+ interface CacheResultOptions<T> {
99
+ key: RawCacheKey;
100
+ ttl?: number;
101
+ load: () => Promise<T>;
102
+ }
103
+ declare function createCacheResult(cache: Keyv<unknown>, logger: Logger): <T>({ key: rawKey, ttl, load, }: CacheResultOptions<T>) => Promise<T>;
104
+
105
+ interface RateLimiterOptions {
106
+ key: RawCacheKey;
107
+ maxAttempts?: number;
108
+ timeWindow?: number;
109
+ }
110
+ declare function createIpRateLimiter(cache: Keyv<unknown>, appName: string): ({ key: rawKey, maxAttempts, timeWindow, }: RateLimiterOptions) => Promise<boolean>;
111
+
87
112
  interface CreateParams$4 {
88
113
  email: string;
89
114
  role: AdminRole;
@@ -585,4 +610,4 @@ declare const POST_ORDER_BY: ({
585
610
  index: "asc";
586
611
  })[];
587
612
 
588
- export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };
613
+ export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCache, createCacheResult, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createIpRateLimiter, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };
@@ -1,8 +1,10 @@
1
- import { ADMIN_ROLES, mimeToExtension, classifyFileType, ROOT_FOLDER_ID } from '../chunk-KEQXXUK2.js';
1
+ import { ADMIN_ROLES, mimeToExtension, classifyFileType, ROOT_FOLDER_ID } from '../chunk-SEX4DOKX.js';
2
2
  import jwt from 'jsonwebtoken';
3
3
  import argon2, { argon2id } from 'argon2';
4
4
  import crypto, { timingSafeEqual } from 'crypto';
5
- import 'next/headers';
5
+ import { headers } from 'next/headers';
6
+ import KeyvRedis from '@keyv/redis';
7
+ import Keyv from 'keyv';
6
8
  import { ulid } from 'ulid';
7
9
 
8
10
  function createJwtService(config) {
@@ -193,6 +195,112 @@ function createCookieService(nextCookies, cryptoService) {
193
195
  delete: deleteCokkie
194
196
  };
195
197
  }
198
+ function createCache({
199
+ redisUrl,
200
+ namespace,
201
+ keyDelimiter
202
+ }) {
203
+ const redisStore = new KeyvRedis(redisUrl, {
204
+ keyPrefixSeparator: keyDelimiter
205
+ });
206
+ return new Keyv({
207
+ store: redisStore,
208
+ namespace,
209
+ useKeyPrefix: false
210
+ });
211
+ }
212
+
213
+ // src/server/infrastructure/cache/cache-key-delimiter.ts
214
+ var CACHE_KEY_DELIMITER = "|";
215
+
216
+ // src/server/infrastructure/cache/normalize-cache-key.ts
217
+ var sanitize = (k) => k.replaceAll(/[\u200B-\u200D\uFEFF]/g, "").replaceAll(/[\r\n]/g, "").trim();
218
+ var normalizeCacheKey = (key, delimiter = CACHE_KEY_DELIMITER) => {
219
+ if (!key) return null;
220
+ if (Array.isArray(key)) {
221
+ if (key.length === 0) return null;
222
+ const normalized = key.map((k) => {
223
+ if (k === null) return "__null";
224
+ if (k === void 0) return "__undefined";
225
+ if (typeof k === "boolean") return k ? "__true" : "__false";
226
+ return sanitize(String(k));
227
+ });
228
+ return normalized.join(delimiter);
229
+ }
230
+ if (typeof key === "boolean") return key ? "__true" : "__false";
231
+ return String(key);
232
+ };
233
+
234
+ // src/server/infrastructure/cache/create-cache-result.ts
235
+ var DAY = 1e3 * 60 * 60 * 24;
236
+ function createCacheResult(cache, logger) {
237
+ return async function cacheResult({
238
+ key: rawKey,
239
+ ttl = DAY,
240
+ load
241
+ }) {
242
+ const key = normalizeCacheKey(rawKey);
243
+ if (!key) {
244
+ logger.error("Cache skipped due to invalid key", {
245
+ rawKey,
246
+ scope: "cacheResult"
247
+ });
248
+ return load();
249
+ }
250
+ try {
251
+ const cached = await cache.get(key);
252
+ if (cached !== void 0) {
253
+ return cached;
254
+ }
255
+ const data = await load();
256
+ if (data !== void 0) {
257
+ await cache.set(key, data, ttl);
258
+ }
259
+ return data;
260
+ } catch (error) {
261
+ logger.error("Cache failure, falling back to loader", {
262
+ key,
263
+ error,
264
+ scope: "cacheResult"
265
+ });
266
+ return load();
267
+ }
268
+ };
269
+ }
270
+ var DEFAULT_MAX_ATTEMPTS = 10;
271
+ var DEFAULT_TIME_WINDOW = 60;
272
+ var lua = `
273
+ local current = redis.call('INCR', KEYS[1])
274
+ if current == 1 then
275
+ redis.call('EXPIRE', KEYS[1], ARGV[1])
276
+ end
277
+ return current
278
+ `;
279
+ function createIpRateLimiter(cache, appName) {
280
+ return async function ipRateLimiter({
281
+ key: rawKey,
282
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
283
+ timeWindow = DEFAULT_TIME_WINDOW
284
+ }) {
285
+ const secondaryStore = cache.store;
286
+ const redis = await secondaryStore.getClient();
287
+ if (!redis) return true;
288
+ const headersStore = await headers();
289
+ const ip = headersStore.get("cf-connecting-ip")?.trim() || headersStore.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
290
+ const key = normalizeCacheKey([
291
+ appName,
292
+ "ip-rate-limiter",
293
+ ...Array.isArray(rawKey) ? rawKey : [rawKey],
294
+ ip
295
+ ]);
296
+ const currentRaw = await redis.eval(lua, {
297
+ keys: [key],
298
+ arguments: [timeWindow.toString()]
299
+ });
300
+ const current = typeof currentRaw === "number" ? currentRaw : Number(currentRaw);
301
+ return current <= maxAttempts;
302
+ };
303
+ }
196
304
 
197
305
  // src/server/infrastructure/database/utils/connect.ts
198
306
  var ids = (items) => items.map(({ id }) => ({ id }));
@@ -1307,4 +1415,4 @@ function createSeoMetadataCommandRepository(prisma) {
1307
1415
  };
1308
1416
  }
1309
1417
 
1310
- export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };
1418
+ export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCache, createCacheResult, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createIpRateLimiter, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yimingliao/cms",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "author": "Yiming Liao",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,8 +26,11 @@
26
26
  "prepublishOnly": "yarn build"
27
27
  },
28
28
  "dependencies": {
29
+ "@keyv/redis": "^5.1.6",
29
30
  "argon2": "^0.44.0",
30
31
  "jsonwebtoken": "^9.0.3",
32
+ "keyv": "^5.6.0",
33
+ "logry": "^2.1.6",
31
34
  "mime-types": "^3.0.2",
32
35
  "ulid": "^3.0.2"
33
36
  },