@yimingliao/cms 0.0.14 → 0.0.16

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) {
@@ -194,6 +196,114 @@ function createCookieService(nextCookies, cryptoService) {
194
196
  };
195
197
  }
196
198
 
199
+ // src/server/infrastructure/cache/cache-key-delimiter.ts
200
+ var CACHE_KEY_DELIMITER = "|";
201
+
202
+ // src/server/infrastructure/cache/create-cache.ts
203
+ function createCache({
204
+ redisUrl,
205
+ namespace,
206
+ keyDelimiter = CACHE_KEY_DELIMITER
207
+ }) {
208
+ const redisStore = new KeyvRedis(redisUrl, {
209
+ keyPrefixSeparator: keyDelimiter
210
+ });
211
+ return new Keyv({
212
+ store: redisStore,
213
+ namespace,
214
+ useKeyPrefix: false
215
+ });
216
+ }
217
+
218
+ // src/server/infrastructure/cache/normalize-cache-key.ts
219
+ var sanitize = (k) => k.replaceAll(/[\u200B-\u200D\uFEFF]/g, "").replaceAll(/[\r\n]/g, "").trim();
220
+ var normalizeCacheKey = (key, delimiter = CACHE_KEY_DELIMITER) => {
221
+ if (!key) return null;
222
+ if (Array.isArray(key)) {
223
+ if (key.length === 0) return null;
224
+ const normalized = key.map((k) => {
225
+ if (k === null) return "__null";
226
+ if (k === void 0) return "__undefined";
227
+ if (typeof k === "boolean") return k ? "__true" : "__false";
228
+ return sanitize(String(k));
229
+ });
230
+ return normalized.join(delimiter);
231
+ }
232
+ if (typeof key === "boolean") return key ? "__true" : "__false";
233
+ return String(key);
234
+ };
235
+
236
+ // src/server/infrastructure/cache/create-cache-result.ts
237
+ var DAY = 1e3 * 60 * 60 * 24;
238
+ function createCacheResult(cache, logger) {
239
+ return async function cacheResult({
240
+ key: rawKey,
241
+ ttl = DAY,
242
+ load
243
+ }) {
244
+ const key = normalizeCacheKey(rawKey);
245
+ if (!key) {
246
+ logger.error("Cache skipped due to invalid key", {
247
+ rawKey,
248
+ scope: "cacheResult"
249
+ });
250
+ return load();
251
+ }
252
+ try {
253
+ const cached = await cache.get(key);
254
+ if (cached !== void 0) {
255
+ return cached;
256
+ }
257
+ const data = await load();
258
+ if (data !== void 0) {
259
+ await cache.set(key, data, ttl);
260
+ }
261
+ return data;
262
+ } catch (error) {
263
+ logger.error("Cache failure, falling back to loader", {
264
+ key,
265
+ error,
266
+ scope: "cacheResult"
267
+ });
268
+ return load();
269
+ }
270
+ };
271
+ }
272
+ var DEFAULT_MAX_ATTEMPTS = 10;
273
+ var DEFAULT_TIME_WINDOW = 60;
274
+ var lua = `
275
+ local current = redis.call('INCR', KEYS[1])
276
+ if current == 1 then
277
+ redis.call('EXPIRE', KEYS[1], ARGV[1])
278
+ end
279
+ return current
280
+ `;
281
+ function createIpRateLimiter(cache, appName) {
282
+ return async function ipRateLimiter({
283
+ key: rawKey,
284
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
285
+ timeWindow = DEFAULT_TIME_WINDOW
286
+ }) {
287
+ const secondaryStore = cache.store;
288
+ const redis = await secondaryStore.getClient();
289
+ if (!redis) return true;
290
+ const headersStore = await headers();
291
+ const ip = headersStore.get("cf-connecting-ip")?.trim() || headersStore.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
292
+ const key = normalizeCacheKey([
293
+ appName,
294
+ "ip-rate-limiter",
295
+ ...Array.isArray(rawKey) ? rawKey : [rawKey],
296
+ ip
297
+ ]);
298
+ const currentRaw = await redis.eval(lua, {
299
+ keys: [key],
300
+ arguments: [timeWindow.toString()]
301
+ });
302
+ const current = typeof currentRaw === "number" ? currentRaw : Number(currentRaw);
303
+ return current <= maxAttempts;
304
+ };
305
+ }
306
+
197
307
  // src/server/infrastructure/database/utils/connect.ts
198
308
  var ids = (items) => items.map(({ id }) => ({ id }));
199
309
  function connectOne(item) {
@@ -1307,4 +1417,4 @@ function createSeoMetadataCommandRepository(prisma) {
1307
1417
  };
1308
1418
  }
1309
1419
 
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 };
1420
+ 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.16",
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
  },