cabloy 5.1.102 → 5.1.103

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.
Files changed (127) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/CLAUDE.local.md +4 -0
  4. package/cabloy-docs/.vitepress/config.mjs +3 -0
  5. package/cabloy-docs/backend/file-guide.md +148 -0
  6. package/cabloy-docs/backend/image-guide.md +40 -40
  7. package/cabloy-docs/backend/jwt-guide.md +21 -0
  8. package/cabloy-docs/frontend/file-guide.md +147 -0
  9. package/cabloy-docs/frontend/image-guide.md +18 -9
  10. package/cabloy-docs/frontend/model-state-guide.md +12 -0
  11. package/cabloy-docs/frontend/use-state-data-best-practices.md +54 -0
  12. package/cabloy-docs/fullstack/file-workflow.md +135 -0
  13. package/cabloy-docs/fullstack/image-workflow.md +12 -3
  14. package/package.json +1 -1
  15. package/vona/packages-vona/vona/package.json +1 -1
  16. package/vona/src/suite/a-training/modules/training-record/src/.metadata/index.ts +30 -4
  17. package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +1 -1
  18. package/vona/src/suite/a-training/modules/training-record/src/bean/imageScene.sceneImage.ts +1 -0
  19. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +8 -6
  20. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +8 -6
  21. package/vona/src/suite/a-training/modules/training-record/src/index.ts +0 -1
  22. package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +0 -1
  23. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +10 -12
  24. package/vona/src/suite/a-training/modules/training-student/src/bean/imageScene.studentImage.ts +1 -0
  25. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +9 -10
  26. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +2 -2
  27. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +3 -2
  28. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +155 -35
  29. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +283 -54
  30. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +46 -63
  31. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.index.ts +12 -0
  32. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +1 -1
  33. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +3 -0
  34. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/schedule.fileDraftPrune.ts +29 -0
  35. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveView.ts +45 -0
  36. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveViews.ts +45 -0
  37. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +6 -0
  38. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +64 -56
  39. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeRequest.ts +13 -0
  40. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeResponse.ts +10 -0
  41. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +0 -3
  42. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +2 -14
  43. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +3 -0
  44. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +3 -0
  45. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +0 -25
  46. package/vona/src/{suite/a-training/modules/training-record/src/dto/recordDossierFileView.ts → suite-vendor/a-file/modules/a-file/src/dto/fileView.ts} +3 -15
  47. package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +10 -0
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadUrl.ts +329 -0
  49. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +1 -0
  50. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +51 -10
  51. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +8 -3
  52. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +1 -1
  53. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +1 -1
  54. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +12 -4
  55. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +52 -18
  56. package/vona/src/suite-vendor/a-file/modules/file-native/package.json +1 -1
  57. package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +5 -5
  58. package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +7 -8
  59. package/vona/src/suite-vendor/a-file/package.json +4 -4
  60. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
  61. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +16 -19
  62. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +99 -74
  63. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +28 -68
  64. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +1 -1
  65. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveView.ts +3 -0
  66. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveViews.ts +3 -0
  67. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +4 -0
  68. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +78 -34
  69. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDeliveryRequest.ts +3 -0
  70. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadRequest.ts +0 -3
  71. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +1 -27
  72. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +6 -0
  73. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +2 -36
  74. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadUrlRequest.ts +0 -3
  75. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +4 -27
  76. package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +1 -1
  77. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +36 -29
  78. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +4 -4
  79. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageScene.ts +1 -0
  80. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  81. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +8 -11
  82. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/lib/cloudflareImageUrl.ts +4 -16
  83. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +17 -16
  84. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +2 -6
  85. package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +0 -37
  86. package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +13 -36
  87. package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +100 -113
  88. package/vona/src/suite-vendor/a-image/package.json +1 -1
  89. package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
  90. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/config/config.ts +3 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +9 -4
  92. package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
  93. package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +113 -64
  94. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  95. package/zova/packages-zova/zova/package.json +2 -2
  96. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +50 -12
  97. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +610 -114
  98. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +44 -1
  99. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +50 -12
  100. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +610 -114
  101. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +16 -12
  102. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +25 -25
  103. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +18 -24
  104. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +228 -291
  105. package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +5 -5
  106. package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +23 -5
  107. package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +72 -43
  108. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +22 -4
  109. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +16 -42
  110. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +5 -25
  111. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +10 -24
  112. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +216 -333
  113. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +0 -5
  114. package/zova/src/suite/cabloy-basic/modules/basic-image/src/bean/tableCell.image.tsx +38 -12
  115. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +79 -32
  116. package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/preview.ts +22 -4
  117. package/zova/src/suite/cabloy-basic/modules/basic-image/src/types/image.ts +3 -4
  118. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  119. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +29 -1
  120. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +2 -2
  121. package/zova/src/suite-vendor/a-zova/package.json +2 -2
  122. package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +0 -34
  123. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenRequest.ts +0 -24
  124. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenResponse.ts +0 -16
  125. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +0 -57
  126. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +0 -31
  127. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +0 -13
@@ -1,21 +1,18 @@
1
1
  import type { SharpConstructor } from 'sharp';
2
2
  import type {
3
3
  EntityImage,
4
- IImageDeliveryOptions,
5
- IImageDirectUploadInput,
6
- IImageProviderDirectUploadResource,
4
+ IImageProviderDeliveryOptions,
7
5
  IImageProviderResolvedVariant,
8
6
  IImageProviderResource,
9
7
  IImageTransformOptions,
10
8
  IImageUploadInput,
11
9
  IImageVariantRequest,
12
10
  } from 'vona-module-a-image';
13
- import type { IUploadFile } from 'vona-module-a-upload';
14
11
 
15
12
  import fse from 'fs-extra';
16
13
  import { createHash } from 'node:crypto';
17
14
  import path from 'node:path';
18
- import { BeanBase, uuidv4 } from 'vona';
15
+ import { BeanBase, getRuntimePathPhysicalRoot, uuidv4 } from 'vona';
19
16
  import { Service } from 'vona-module-a-bean';
20
17
  import { resolveImageVariantRequestToTransform } from 'vona-module-a-image';
21
18
 
@@ -23,21 +20,7 @@ import type { IImageProviderNativeClientOptions } from '../bean/imageProvider.na
23
20
 
24
21
  type IImageNativeStoredImage = Pick<
25
22
  EntityImage,
26
- 'resourceId' | 'filename' | 'storagePath' | 'variants'
27
- >;
28
-
29
- type IImageNativeDraftImage = Pick<
30
- EntityImage,
31
- | 'resourceId'
32
- | 'filename'
33
- | 'contentType'
34
- | 'meta'
35
- | 'imageScene'
36
- | 'requireSignedURLs'
37
- | 'storagePath'
38
- | 'deliveryBaseUrl'
39
- | 'variants'
40
- | 'clientName'
23
+ 'id' | 'resourceId' | 'filename' | 'contentType' | 'public' | 'storagePath' | 'variants'
41
24
  >;
42
25
 
43
26
  @Service()
@@ -56,80 +39,24 @@ export class ServiceImageNative extends BeanBase {
56
39
  options: IImageProviderNativeClientOptions,
57
40
  ): Promise<IImageProviderResource> {
58
41
  const resourceId = uuidv4();
59
- const targetPath = await this._getFinalPath(resourceId, input.filename, options, input.file);
60
- await fse.copy(input.file, targetPath);
61
- return await this._buildStoredResource(targetPath, {
42
+ const isPublic = input.public ?? options.public;
43
+ const targetPath = await this._getFinalPath(
62
44
  resourceId,
63
- filename: input.filename,
64
- contentType: input.contentType,
65
- meta: input.meta,
66
- requireSignedURLs: input.requireSignedURLs ?? options.requireSignedURLs,
67
- variants: options.variants ?? this.scope.config.imageNative.variants,
68
- deliveryBaseUrl: options.deliveryBaseUrl,
69
- });
70
- }
71
-
72
- async createDirectUpload(
73
- input: IImageDirectUploadInput,
74
- options: IImageProviderNativeClientOptions,
75
- ): Promise<IImageProviderDirectUploadResource> {
76
- const resourceId = this._sanitizeResourceId(input.customId) ?? uuidv4();
77
- const storagePath = await this._getFinalPath(resourceId, input.filename, options);
78
- const routePath = this.scope.util.combineApiPath(
79
- `image-native/direct-upload/${encodeURIComponent(resourceId)}`,
80
- false,
81
- true,
45
+ input.filename,
46
+ isPublic,
47
+ options,
48
+ input.file,
82
49
  );
83
- const tokenPath = this.scope.util.combineApiPath(
84
- 'image-native/direct-upload/:resourceId',
85
- false,
86
- true,
87
- );
88
- const tokenPayload = await this.bean.imageUploadPolicy.createDirectUploadToken({
89
- resourceId,
90
- path: tokenPath,
91
- });
92
- const uploadUrl = new URL(this.app.util.getAbsoluteUrlByApiPath(routePath));
93
- uploadUrl.searchParams.set('token', tokenPayload.token);
94
- return {
50
+ await fse.ensureDir(path.dirname(targetPath));
51
+ await fse.copy(input.file, targetPath);
52
+ return await this._buildStoredResource(targetPath, {
95
53
  resourceId,
96
54
  filename: input.filename,
97
55
  contentType: input.contentType,
98
56
  meta: input.meta,
99
- requireSignedURLs: input.requireSignedURLs ?? options.requireSignedURLs,
57
+ public: isPublic,
100
58
  variants: options.variants ?? this.scope.config.imageNative.variants,
101
59
  deliveryBaseUrl: options.deliveryBaseUrl,
102
- storagePath,
103
- uploadUrl: uploadUrl.toString(),
104
- draft: true,
105
- };
106
- }
107
-
108
- async uploadDirectFile(image: IImageNativeDraftImage, file: IUploadFile) {
109
- const draftPath = this._getDraftPath(image.storagePath, image.resourceId);
110
- await fse.ensureDir(path.dirname(draftPath));
111
- await fse.move(file.file, draftPath, { overwrite: true });
112
- return draftPath;
113
- }
114
-
115
- async finalizeDirectUpload(
116
- image: IImageNativeDraftImage,
117
- options: IImageProviderNativeClientOptions,
118
- ): Promise<IImageProviderResource | undefined> {
119
- const storagePath =
120
- image.storagePath ?? (await this._getFinalPath(image.resourceId, image.filename, options));
121
- const draftPath = this._getDraftPath(storagePath, image.resourceId);
122
- if (!(await fse.pathExists(draftPath))) return undefined;
123
- await fse.ensureDir(path.dirname(storagePath));
124
- await fse.move(draftPath, storagePath, { overwrite: true });
125
- return await this._buildStoredResource(storagePath, {
126
- resourceId: image.resourceId,
127
- filename: image.filename,
128
- contentType: image.contentType,
129
- meta: image.meta,
130
- requireSignedURLs: image.requireSignedURLs ?? options.requireSignedURLs,
131
- variants: image.variants ?? options.variants ?? this.scope.config.imageNative.variants,
132
- deliveryBaseUrl: image.deliveryBaseUrl ?? options.deliveryBaseUrl,
133
60
  });
134
61
  }
135
62
 
@@ -137,8 +64,6 @@ export class ServiceImageNative extends BeanBase {
137
64
  if (!image.storagePath) return;
138
65
  const dir = path.dirname(image.storagePath);
139
66
  if (!(await fse.pathExists(dir))) return;
140
- const draftPath = this._getDraftPath(image.storagePath, image.resourceId);
141
- await fse.remove(draftPath);
142
67
  const entries = await fse.readdir(dir);
143
68
  await Promise.all(
144
69
  entries
@@ -154,8 +79,13 @@ export class ServiceImageNative extends BeanBase {
154
79
  image: IImageNativeStoredImage,
155
80
  request: IImageVariantRequest,
156
81
  options: IImageProviderNativeClientOptions,
157
- _deliveryOptions?: IImageDeliveryOptions,
82
+ _deliveryOptions?: IImageProviderDeliveryOptions,
158
83
  ) {
84
+ if (!(image.public ?? options.public)) {
85
+ throw new Error(
86
+ `Private image-native variant URL requires signed delivery: ${image.resourceId}`,
87
+ );
88
+ }
159
89
  const resolved = resolveImageVariantRequestToTransform(request, 'original', image.variants);
160
90
  if (
161
91
  resolved.variantName === 'original' &&
@@ -167,6 +97,34 @@ export class ServiceImageNative extends BeanBase {
167
97
  return await this._buildUrl(targetPath, options.deliveryBaseUrl);
168
98
  }
169
99
 
100
+ async downloadBuffer(
101
+ image: IImageNativeStoredImage,
102
+ request: IImageVariantRequest,
103
+ ): Promise<{
104
+ buffer: Buffer;
105
+ filename?: string;
106
+ contentType?: string;
107
+ } | null> {
108
+ if (!image.storagePath) {
109
+ throw new Error(`Image storage path missing: ${image.resourceId}`);
110
+ }
111
+ const resolved = resolveImageVariantRequestToTransform(request, 'original', image.variants);
112
+ const targetPath =
113
+ resolved.variantName === 'original' && this._isOriginalTransform(resolved.transformOptions)
114
+ ? image.storagePath
115
+ : await this._ensureVariantFile(image, resolved);
116
+ if (!(await fse.pathExists(targetPath))) return null;
117
+ return {
118
+ buffer: await fse.readFile(targetPath),
119
+ filename: image.filename,
120
+ contentType: this._resolveContentType(
121
+ targetPath,
122
+ image.contentType,
123
+ resolved.transformOptions,
124
+ ),
125
+ };
126
+ }
127
+
170
128
  private async _buildStoredResource(
171
129
  targetPath: string,
172
130
  data: {
@@ -174,7 +132,7 @@ export class ServiceImageNative extends BeanBase {
174
132
  filename?: string;
175
133
  contentType?: string;
176
134
  meta?: Record<string, unknown>;
177
- requireSignedURLs?: boolean;
135
+ public?: boolean;
178
136
  variants?: EntityImage['variants'];
179
137
  deliveryBaseUrl?: string;
180
138
  },
@@ -189,7 +147,7 @@ export class ServiceImageNative extends BeanBase {
189
147
  size: Number(stat.size),
190
148
  width: metadata.width,
191
149
  height: metadata.height,
192
- requireSignedURLs: data.requireSignedURLs,
150
+ public: data.public,
193
151
  variants: data.variants,
194
152
  meta: data.meta,
195
153
  storagePath: targetPath,
@@ -200,24 +158,34 @@ export class ServiceImageNative extends BeanBase {
200
158
  private async _getFinalPath(
201
159
  resourceId: string,
202
160
  filename: string | undefined,
161
+ isPublic: boolean | undefined,
203
162
  options: IImageProviderNativeClientOptions,
204
163
  filePath?: string,
205
164
  ) {
206
- const publicPath = await this.app.util.getPublicPathPhysical(
207
- path.join('image-native', options.subdir ?? 'default'),
208
- true,
209
- );
165
+ const basePath = await this._getBasePath(isPublic, options);
210
166
  const ext = this._getOriginalExt(filename, filePath ?? resourceId);
211
- return path.join(publicPath, `${resourceId}${ext}`);
167
+ return path.join(basePath, `${resourceId}${ext}`);
212
168
  }
213
169
 
214
- private _getDraftPath(storagePath: string | undefined, resourceId: string) {
215
- if (!storagePath) {
216
- throw new Error(`Image storage path missing: ${resourceId}`);
170
+ private async _getBasePath(
171
+ isPublic: boolean | undefined,
172
+ options: IImageProviderNativeClientOptions,
173
+ ) {
174
+ if (isPublic) {
175
+ return await this.app.util.getPublicPathPhysical(
176
+ path.join('image-native', options.subdir ?? 'default'),
177
+ true,
178
+ );
217
179
  }
218
- const dir = path.dirname(storagePath);
219
- const ext = path.extname(storagePath) || '.bin';
220
- return path.join(dir, `${resourceId}__draft${ext}`);
180
+ const runtimeRoot = getRuntimePathPhysicalRoot(this.app);
181
+ const basePath = path.join(
182
+ runtimeRoot,
183
+ this.ctx.instance.id.toString(),
184
+ 'image-native',
185
+ options.subdir ?? 'default',
186
+ );
187
+ await fse.ensureDir(basePath);
188
+ return basePath;
221
189
  }
222
190
 
223
191
  private async _ensureVariantFile(
@@ -308,8 +276,15 @@ export class ServiceImageNative extends BeanBase {
308
276
 
309
277
  private async _getVariantStaticPath(targetPath: string) {
310
278
  const publicRoot = await this.app.util.getPublicPathPhysical(undefined, true);
311
- const relativePath = path.relative(publicRoot, targetPath).split(path.sep).join(path.posix.sep);
312
- return path.posix.join('public', this.ctx.instance.id.toString(), relativePath);
279
+ const relativePath = path.relative(publicRoot, targetPath);
280
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
281
+ throw new Error(`Image target path is not publicly accessible: ${targetPath}`);
282
+ }
283
+ return path.posix.join(
284
+ 'public',
285
+ this.ctx.instance.id.toString(),
286
+ relativePath.split(path.sep).join(path.posix.sep),
287
+ );
313
288
  }
314
289
 
315
290
  private async _getVariantAbsolutePath(
@@ -323,8 +298,8 @@ export class ServiceImageNative extends BeanBase {
323
298
  const variantName = this._sanitizeVariantName(resolved.variantName);
324
299
  return path.join(dir, `${image.resourceId}__${variantName}${ext}`);
325
300
  }
326
- const cacheKey = this._buildTransformCacheKey(resolved.transformOptions);
327
- return path.join(dir, `${image.resourceId}__t_${cacheKey}${ext}`);
301
+ const transformKey = this._buildTransformKey(resolved.transformOptions);
302
+ return path.join(dir, `${image.resourceId}__t_${transformKey}${ext}`);
328
303
  }
329
304
 
330
305
  private _getVariantExt(
@@ -348,7 +323,25 @@ export class ServiceImageNative extends BeanBase {
348
323
  return format === 'jpeg' ? 'jpeg' : format;
349
324
  }
350
325
 
351
- private _buildTransformCacheKey(transformOptions: IImageTransformOptions) {
326
+ private _resolveContentType(
327
+ targetPath: string,
328
+ contentType: string | undefined,
329
+ transformOptions: IImageTransformOptions,
330
+ ) {
331
+ const format = this._normalizeFormat(transformOptions.format);
332
+ if (format === 'jpeg') return 'image/jpeg';
333
+ if (format === 'png') return 'image/png';
334
+ if (format === 'webp') return 'image/webp';
335
+ if (format === 'avif') return 'image/avif';
336
+ const ext = path.extname(targetPath).toLowerCase();
337
+ if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
338
+ if (ext === '.png') return 'image/png';
339
+ if (ext === '.webp') return 'image/webp';
340
+ if (ext === '.avif') return 'image/avif';
341
+ return contentType;
342
+ }
343
+
344
+ private _buildTransformKey(transformOptions: IImageTransformOptions) {
352
345
  const stable = this._stableStringify(transformOptions);
353
346
  return createHash('sha1').update(stable).digest('hex').slice(0, 12);
354
347
  }
@@ -368,12 +361,6 @@ export class ServiceImageNative extends BeanBase {
368
361
  return JSON.stringify(value);
369
362
  }
370
363
 
371
- private _sanitizeResourceId(resourceId: string | undefined) {
372
- if (!resourceId) return undefined;
373
- const sanitized = resourceId.replace(/[^\w.-]/g, '-');
374
- return sanitized || undefined;
375
- }
376
-
377
364
  private _sanitizeVariantName(variantName: string) {
378
365
  return variantName.replace(/[^\w.-]/g, '-');
379
366
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-image",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "files": [],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-jwt",
3
- "version": "5.1.15",
3
+ "version": "5.1.16",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,5 +1,7 @@
1
1
  import type { VonaApplication } from 'vona';
2
2
 
3
+ import { $protocolKey } from 'vona';
4
+
3
5
  import type { ConfigJwt } from '../types/jwt.ts';
4
6
 
5
7
  export function config(app: VonaApplication) {
@@ -15,7 +17,7 @@ export function config(app: VonaApplication) {
15
17
  header: '',
16
18
  headerAuth: 'authorization',
17
19
  headerAuthScheme: 'bearer',
18
- query: 'auth_token',
20
+ query: $protocolKey('x-vona-passport-code'),
19
21
  },
20
22
  },
21
23
  tempAuthToken: {
@@ -67,7 +67,9 @@ export class ServiceJwtClient extends BeanBase {
67
67
  [this.fieldClient]: this._clientName,
68
68
  [this.fieldData]: payloadData,
69
69
  };
70
- if (options?.path) payload[this.fieldPath] = options.path;
70
+ if (options?.path) {
71
+ payload[this.fieldPath] = options.path;
72
+ }
71
73
  let signOptions = this._clientOptions.signOptions;
72
74
  if (options?.dev) {
73
75
  signOptions = Object.assign({}, signOptions, {
@@ -123,8 +125,9 @@ export class ServiceJwtClient extends BeanBase {
123
125
  // check field client
124
126
  if (payload[this.fieldClient] !== this._clientName) return this.app.throw(401);
125
127
  // check field path
126
- if (!this._checkVerifyPath(payload[this.fieldPath], options?.path))
128
+ if (!this._checkVerifyPath(payload[this.fieldPath], options?.path)) {
127
129
  return this.app.throw(401);
130
+ }
128
131
  // passed
129
132
  resolve(payload[this.fieldData]);
130
133
  },
@@ -135,7 +138,9 @@ export class ServiceJwtClient extends BeanBase {
135
138
  _checkVerifyPath(pathTarget: string | string[] | undefined, pathReal: string | undefined) {
136
139
  if (!pathTarget) return true;
137
140
  const path = pathReal ?? String(this.ctx.route.routePathRaw);
138
- if (Array.isArray(pathTarget) && !pathTarget.includes(path)) return false;
139
- return pathTarget === path;
141
+ const targets = Array.isArray(pathTarget) ? pathTarget : [pathTarget];
142
+ return (
143
+ targets.every(target => typeof target === 'string') && targets.some(target => target === path)
144
+ );
140
145
  }
141
146
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-upload",
3
- "version": "5.1.14",
3
+ "version": "5.1.15",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -28,9 +28,7 @@ export class InterceptorUpload extends BeanBase implements IInterceptorExecute {
28
28
  res = await next();
29
29
  } finally {
30
30
  // delete temp files
31
- for (const fileCleanup of filesCleanup) {
32
- fileCleanup();
33
- }
31
+ this._cleanupFiles(filesCleanup);
34
32
  this.ctx[SymbolUploadValue] = undefined;
35
33
  }
36
34
  // ok
@@ -45,81 +43,132 @@ export class InterceptorUpload extends BeanBase implements IInterceptorExecute {
45
43
  const files: IUploadFile[] = [];
46
44
  const filesCleanup: Function[] = [];
47
45
  const fileWrites: Promise<void>[] = [];
46
+ const fileStreams: any[] = [];
47
+ const writeStreams: ReturnType<typeof createWriteStream>[] = [];
48
+ let bb: any;
48
49
  let settled = false;
49
50
  const rejectOnce = (err: Error) => {
50
51
  if (settled) return;
51
52
  settled = true;
53
+ for (const file of fileStreams) {
54
+ file.unpipe();
55
+ file.resume();
56
+ }
57
+ for (const writeStream of writeStreams) {
58
+ writeStream.destroy();
59
+ }
60
+ try {
61
+ this.ctx.req.unpipe(bb);
62
+ bb?.destroy();
63
+ this.ctx.req.resume();
64
+ } catch {
65
+ // ignore cleanup errors and preserve the receive error
66
+ }
67
+ this._cleanupFiles(filesCleanup);
52
68
  reject(err);
53
69
  };
54
- // bb
55
- const bb = Busboy(
56
- Object.assign({ defParamCharset: 'utf8' }, options.busboy, {
57
- headers: this.ctx.req.headers,
58
- }),
59
- );
60
- bb.on('file', (name, file, info) => {
61
- // temp
62
- const fileTempObj = tmp.fileSync({
63
- prefix: '.temp-upload-',
64
- });
65
- const fileTemp = fileTempObj.name;
66
- filesCleanup.push(fileTempObj.removeCallback);
67
- files.push({
68
- name,
69
- file: fileTemp,
70
- info,
71
- });
72
- // save
73
- const writeStream = createWriteStream(fileTemp);
74
- const fileWrite = new Promise<void>((resolveWrite, rejectWrite) => {
75
- writeStream.on('finish', () => {
76
- resolveWrite();
77
- });
78
- writeStream.on('error', err => {
79
- rejectWrite(err);
70
+ try {
71
+ // bb
72
+ bb = Busboy(
73
+ Object.assign({ defParamCharset: 'utf8' }, options.busboy, {
74
+ headers: this.ctx.req.headers,
75
+ }),
76
+ );
77
+ bb.on('file', (name, file, info) => {
78
+ if (settled) {
79
+ file.resume();
80
+ return;
81
+ }
82
+ fileStreams.push(file);
83
+ let fileTempObj: tmp.FileResult;
84
+ try {
85
+ fileTempObj = tmp.fileSync({
86
+ prefix: '.temp-upload-',
87
+ });
88
+ } catch (err) {
89
+ file.resume();
90
+ rejectOnce(err as Error);
91
+ return;
92
+ }
93
+ const fileTemp = fileTempObj.name;
94
+ filesCleanup.push(fileTempObj.removeCallback);
95
+ files.push({
96
+ name,
97
+ file: fileTemp,
98
+ info,
80
99
  });
81
- file.on('error', err => {
82
- rejectWrite(err);
100
+ // save
101
+ const writeStream = createWriteStream(fileTemp);
102
+ writeStreams.push(writeStream);
103
+ const fileWrite = new Promise<void>(resolveWrite => {
104
+ const complete = () => resolveWrite();
105
+ writeStream.on('finish', complete);
106
+ writeStream.on('error', err => {
107
+ rejectOnce(err);
108
+ complete();
109
+ });
110
+ file.on('error', err => {
111
+ rejectOnce(err);
112
+ complete();
113
+ });
114
+ file.on('limit', () => {
115
+ rejectOnce(this._createLimitError());
116
+ complete();
117
+ });
83
118
  });
119
+ fileWrites.push(fileWrite);
120
+ file.pipe(writeStream);
84
121
  });
85
- fileWrites.push(
86
- fileWrite.catch(err => {
87
- rejectOnce(err as Error);
88
- throw err;
89
- }),
90
- );
91
- file.pipe(writeStream);
92
- });
93
- bb.on('field', (name, value, info) => {
94
- fields.push({
95
- name,
96
- value,
97
- info,
122
+ bb.on('field', (name, value, info) => {
123
+ if (info.nameTruncated || info.valueTruncated) {
124
+ rejectOnce(this._createLimitError());
125
+ return;
126
+ }
127
+ fields.push({
128
+ name,
129
+ value,
130
+ info,
131
+ });
98
132
  });
99
- });
100
- bb.on('close', async () => {
101
- try {
133
+ bb.on('close', async () => {
102
134
  await Promise.all(fileWrites);
103
135
  if (settled) return;
104
136
  settled = true;
105
137
  resolve([fields, files, filesCleanup]);
106
- } catch {
107
- // handled by rejectOnce
108
- }
109
- });
110
- bb.on('error', (err: Error) => {
111
- rejectOnce(err);
112
- });
113
- bb.on('partsLimit', () => {
114
- rejectOnce(new Error('partsLimit'));
115
- });
116
- bb.on('filesLimit', () => {
117
- rejectOnce(new Error('filesLimit'));
118
- });
119
- bb.on('fieldsLimit', () => {
120
- rejectOnce(new Error('fieldsLimit'));
121
- });
122
- this.ctx.req.pipe(bb);
138
+ });
139
+ bb.on('error', (err: Error) => {
140
+ rejectOnce(err);
141
+ });
142
+ bb.on('partsLimit', () => {
143
+ rejectOnce(this._createLimitError());
144
+ });
145
+ bb.on('filesLimit', () => {
146
+ rejectOnce(this._createLimitError());
147
+ });
148
+ bb.on('fieldsLimit', () => {
149
+ rejectOnce(this._createLimitError());
150
+ });
151
+ this.ctx.req.pipe(bb);
152
+ } catch (err) {
153
+ rejectOnce(err as Error);
154
+ }
123
155
  });
124
156
  }
157
+
158
+ private _cleanupFiles(filesCleanup: Function[]) {
159
+ for (const fileCleanup of filesCleanup) {
160
+ try {
161
+ fileCleanup();
162
+ } catch {
163
+ // try every cleanup callback without masking the primary error
164
+ }
165
+ }
166
+ }
167
+
168
+ private _createLimitError() {
169
+ const err: any = new Error('upload limit exceeded');
170
+ err.status = 413;
171
+ err.code = 413;
172
+ return err as Error;
173
+ }
125
174
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.65",
3
+ "version": "5.1.66",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.132",
3
+ "version": "5.1.133",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A vue3 framework with ioc",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "zova-core": "^5.1.77",
49
- "zova-suite-a-zova": "^5.1.131"
49
+ "zova-suite-a-zova": "^5.1.132"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",