secure-upload-fastify-sdk 1.0.1 → 1.0.3

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/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  - **可自定义 Session 校验** — 默认 base64-decode JWT payload,可注入 `jwt.verify()` 完整验签
18
18
  - **后台任务** — 过期 `uploadId` 清理 + 孤儿文件扫描,全部可关闭
19
19
  - **严格秒传隔离(v1.0.0+)** — 按 `(contentHash, isPublic, userId)` 三元组匹配,公开池/私有池/不同用户之间完全隔离
20
+ - **可配置 /smart/init 批量上限(v1.0.2+)** — `rateLimit.smartMaxFilesPerInit` 动态生效,工厂函数生成 zod schema,业务校验 + 启动日志同源
20
21
 
21
22
  ---
22
23
 
@@ -308,6 +309,102 @@ await app.register(secureUploadPlugin, {
308
309
 
309
310
  ---
310
311
 
312
+ ## 🔧 /smart/init 批量上限配置链路(v1.0.2+)
313
+
314
+ `/smart/init` 是"一次接收 N 个文件 → 智能分流 ONE_SHOT / MULTIPART"的批量入口。
315
+ `N` 的上限在 **v1.0.2 之前**是 SDK 内部写死的 `z.array(...).max(50)`,业务方即便配置 `smartMaxFilesPerInit: 200` 也会被 zod 校验直接拒绝。
316
+
317
+ **v1.0.2 修复**:`/smart/init` 的请求体 schema 改为工厂函数,`maxFiles` 从 `rt.config.smartMaxFiles` 读取,与业务校验、`rateLimit`、启动日志**同源**。
318
+
319
+ ```ts
320
+ // src/server/routes/smart.ts
321
+ function makeSmartInitBodySchema(maxFiles: number) {
322
+ return z.object({
323
+ files: z.array(FileInitItemSchema).min(1).max(maxFiles),
324
+ }).strict();
325
+ }
326
+
327
+ // handler 里动态生成
328
+ const schema = makeSmartInitBodySchema(rt.config.smartMaxFiles);
329
+ const body = schema.parse(req.body ?? {});
330
+
331
+ // 业务侧防御性兜底(同源,防止 rt.config 异常时被绕过)
332
+ if (body.files.length > rt.config.smartMaxFiles) {
333
+ throw new SecureError(ErrorCode.BATCH_TOO_MANY_FILES, ...);
334
+ }
335
+ ```
336
+
337
+ ### 完整配置链路
338
+
339
+ ```
340
+ ┌────────────────────────────────────────────────────────────────┐
341
+ │ ① backend/.env │
342
+ │ UPLOAD_SMART_MAX_FILES_PER_INIT=200 # 单次 /smart/init │
343
+ │ UPLOAD_SMART_INIT_RATE_LIMIT=200 # 60s 配额 │
344
+ └─────────────────────────┬──────────────────────────────────────┘
345
+ │ dotenv 加载
346
+
347
+ ┌────────────────────────────────────────────────────────────────┐
348
+ │ ② backend/apps/upload-service/src/_bootstrap.ts │
349
+ │ getUPLOADConfig() │
350
+ │ → cfg.UPLOAD_SMART_MAX_FILES_PER_INIT = 200 │
351
+ │ → cfg.UPLOAD_SMART_INIT_RATE_LIMIT = 200 │
352
+ └─────────────────────────┬──────────────────────────────────────┘
353
+ │ app.ts 第 130 行
354
+
355
+ ┌────────────────────────────────────────────────────────────────┐
356
+ │ ③ upload-service/src/app.ts │
357
+ │ sdkOptions.rateLimit = { │
358
+ │ smartMaxFilesPerInit: cfg.UPLOAD_SMART_MAX_FILES_PER_INIT,│
359
+ │ } │
360
+ │ app.register(secureUploadPlugin, sdkOptions) │
361
+ └─────────────────────────┬──────────────────────────────────────┘
362
+ │ 插件 option → 运行时配置
363
+
364
+ ┌────────────────────────────────────────────────────────────────┐
365
+ │ ④ SDK 内部 (this SDK) │
366
+ │ options.rateLimit.smartMaxFilesPerInit │
367
+ │ → rt.config.smartMaxFiles │
368
+ │ → rt.config.smartInitRateLimit │
369
+ └─────────────────────────┬──────────────────────────────────────┘
370
+ │ 三处消费
371
+
372
+ ┌────────────────────────────────────────────────────────────────┐
373
+ │ ⑤ src/server/routes/smart.ts │
374
+ │ a) 启动日志(app.log.info): │
375
+ │ maxFiles: 200, rateLimit: 200 │
376
+ │ b) zod schema(工厂函数): │
377
+ │ makeSmartInitBodySchema(rt.config.smartMaxFiles) │
378
+ │ c) 业务校验兜底: │
379
+ │ if (body.files.length > rt.config.smartMaxFiles) │
380
+ │ throw BATCH_TOO_MANY_FILES │
381
+ │ d) 滑动窗口限流: │
382
+ │ slidingWindowRateLimit(..., rt.config.smartInitRateLimit)│
383
+ └────────────────────────────────────────────────────────────────┘
384
+ ```
385
+
386
+ ### 启动时确认
387
+
388
+ 启动 `upload-service` 后,日志里应看到:
389
+
390
+ ```text
391
+ 🔧 smart-routes: 注册 smart 上传路由(...)
392
+ maxFiles: 200 ← 从 .env UPLOAD_SMART_MAX_FILES_PER_INIT 透传
393
+ rateLimit: 200 ← 从 .env UPLOAD_SMART_INIT_RATE_LIMIT 透传
394
+ ```
395
+
396
+ > **为什么不用默认值?**
397
+ > 批量上传场景下,前端 `SmartClient` 一次发 60~200 个小文件很常见;
398
+ > 默认 50 适合"小批量场景",需要批量化时应**主动**调大,避免被 zod 静默拒在 400。
399
+
400
+ ### 升级到 v1.0.2 的 3 步
401
+
402
+ 1. 升级 SDK 版本:`secure-upload-fastify-sdk: ^1.0.1` → `^1.0.2`
403
+ 2. 业务侧 `pnpm install`(workspace 重链接 symlink)
404
+ 3. 重建 SDK:`pnpm build`(否则后端拿到的还是旧版 dist)
405
+
406
+ ---
407
+
311
408
  ## ⚡ 秒传隔离
312
409
 
313
410
  `/multipart/init` 和 `/smart/init` 都支持秒传:传 `expectedHash` 时,服务端按 `(contentHash, isPublic, userId)` 三元组查询已有 `File` 记录。
@@ -342,7 +439,7 @@ await app.register(secureUploadPlugin, {
342
439
  | `suggestedClientConcurrency` | `number` | `3` | 建议客户端并发 part 数 |
343
440
  | `rateLimit.initPerMin` | `number` | `30` | `/multipart/init` 限流 |
344
441
  | `rateLimit.partPerMin` | `number` | `600` | `/multipart/part` 限流 |
345
- | `rateLimit.smartMaxFilesPerInit` | `number` | `50` | `/smart/init` 单次最多文件数 |
442
+ | `rateLimit.smartMaxFilesPerInit` | `number` | `50` | `/smart/init` 单次最多文件数(**v1.0.2+ 可配置**,从 `.env UPLOAD_SMART_MAX_FILES_PER_INIT` 透传,详见下文"配置链路"章节) |
346
443
  | `pathPrefix.multipart` | `string` | `'/multipart'` | 分片路由挂载前缀 |
347
444
  | `pathPrefix.smart` | `string` | `'/smart'` | smart 路由挂载前缀 |
348
445
  | `fileUrlPrefix` | `string` | `'/files'` | complete 响应 `url` 字段前缀 |
@@ -462,6 +559,47 @@ HTTP 状态码映射由 `getHttpStatus(code)` 给出,用户业务侧可直接用
462
559
 
463
560
  ---
464
561
 
562
+ ## 📝 Changelog
563
+
564
+ ### v1.0.2 — /smart/init 批量上限可配置化
565
+
566
+ **🛠 Fix / Feature**
567
+
568
+ - **`/smart/init` 请求体 zod schema 改为工厂函数**
569
+ `makeSmartInitBodySchema(maxFiles: number)` 接收 `maxFiles` 参数,handler 里用 `rt.config.smartMaxFiles` 动态生成。
570
+ 修掉了 v1.0.1 写死 `z.array(...).max(50)` 导致 `rateLimit.smartMaxFilesPerInit` 配置**完全不生效**的 bug。
571
+
572
+ - **业务校验保留为防御性兜底(同源)**
573
+ `if (body.files.length > rt.config.smartMaxFiles)` 仍在 handler 里显式抛 `BATCH_TOO_MANY_FILES`,防止 `rt.config` 异常时被绕过。
574
+
575
+ - **启动日志暴露 `maxFiles` + `rateLimit`**
576
+ `app.log.info({ routes, maxFiles, rateLimit }, '🔧 smart-routes: ...')`,便于运维一眼确认配置是否生效。
577
+
578
+ **⚙️ 影响面**
579
+
580
+ | 组件 | 行为 | 是否需要改动 |
581
+ |------|------|-------------|
582
+ | SDK consumer(上传插件调用方) | 自动透传 `rateLimit.smartMaxFilesPerInit` → 运行时 `smartMaxFiles` | ❌ 无需改代码,只需升级 SDK 版本 |
583
+ | `backend/.env` | 新增 `UPLOAD_SMART_MAX_FILES_PER_INIT` / `UPLOAD_SMART_INIT_RATE_LIMIT` | ✅ 已在 1.0.2 升级时加上 |
584
+ | `upload-service` `_bootstrap.ts` | 新增 cfg 字段读取 | ✅ 已在 1.0.2 升级时加上 |
585
+ | 前端 `SmartClient` | 调大 `maxFilesPerBatch` 配合后端上限 | ⚠️ 按需,见 `frontend-demo/index.html` |
586
+
587
+ **🔄 升级步骤**
588
+
589
+ ```bash
590
+ # 1) 升级 SDK 版本号(package.json)
591
+ # secure-upload-fastify-sdk: ^1.0.1 → ^1.0.2
592
+ cd secure-upload-fastify-sdk && pnpm build # 重建 dist
593
+ cd ../backend && pnpm install # workspace 重链接
594
+ pnpm dev:all # 启动后端,看启动日志 maxFiles: 200
595
+ ```
596
+
597
+ **🔗 完整配置链路**
598
+
599
+ 见上文「🔧 /smart/init 批量上限配置链路(v1.0.2+)」章节的链路图。
600
+
601
+ ---
602
+
465
603
  ## 📄 License
466
604
 
467
605
  MIT
package/dist/index.js CHANGED
@@ -1615,9 +1615,11 @@ var FileInitItemSchema = import_zod2.z.object({
1615
1615
  expectedHash: import_zod2.z.string().regex(/^[a-f0-9]{64}$/i).optional(),
1616
1616
  meta: import_zod2.z.record(import_zod2.z.unknown()).optional()
1617
1617
  }).strict();
1618
- var SmartInitBodySchema = import_zod2.z.object({
1619
- files: import_zod2.z.array(FileInitItemSchema).min(1).max(50)
1620
- }).strict();
1618
+ function makeSmartInitBodySchema(maxFiles) {
1619
+ return import_zod2.z.object({
1620
+ files: import_zod2.z.array(FileInitItemSchema).min(1).max(maxFiles)
1621
+ }).strict();
1622
+ }
1621
1623
  async function processOneFile(req, rt, file, index) {
1622
1624
  try {
1623
1625
  const { userId } = await resolveSession(req, file.isPublic, rt.validateSession);
@@ -1731,7 +1733,8 @@ async function registerSmartRoutes(app, rt) {
1731
1733
  "\u{1F527} smart-routes: \u6CE8\u518C smart \u4E0A\u4F20\u8DEF\u7531(part/complete/abort/status \u590D\u7528 multipart/*)..."
1732
1734
  );
1733
1735
  app.post(`${base}/init`, async (req) => {
1734
- const body = SmartInitBodySchema.parse(req.body ?? {});
1736
+ const schema = makeSmartInitBodySchema(rt.config.smartMaxFiles);
1737
+ const body = schema.parse(req.body ?? {});
1735
1738
  if (body.files.length > rt.config.smartMaxFiles) {
1736
1739
  throw new SecureError(
1737
1740
  1511 /* BATCH_TOO_MANY_FILES */,
@@ -1972,7 +1975,7 @@ async function secureUploadPluginImpl(app, options) {
1972
1975
  initRateLimit: options.rateLimit?.initPerMin ?? DEFAULTS.initRateLimit,
1973
1976
  partRateLimit: options.rateLimit?.partPerMin ?? DEFAULTS.partRateLimit,
1974
1977
  smartMaxFiles: options.rateLimit?.smartMaxFilesPerInit ?? DEFAULTS.smartMaxFiles,
1975
- smartInitRateLimit: DEFAULTS.smartInitRateLimit,
1978
+ smartInitRateLimit: options.rateLimit?.smartInitRateLimit ?? DEFAULTS.smartInitRateLimit,
1976
1979
  multipartPath: options.pathPrefix?.multipart ?? DEFAULTS.multipartPath,
1977
1980
  smartPath: options.pathPrefix?.smart ?? DEFAULTS.smartPath,
1978
1981
  fileUrlPrefix: options.fileUrlPrefix ?? DEFAULTS.fileUrlPrefix,