@soulbatical/tetra-core 0.1.41 → 0.1.42
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/dist/frontend/storage/index.d.ts +5 -0
- package/dist/frontend/storage/index.d.ts.map +1 -0
- package/dist/frontend/storage/index.js +3 -0
- package/dist/frontend/storage/index.js.map +1 -0
- package/dist/frontend/storage/storageUrl.d.ts +32 -0
- package/dist/frontend/storage/storageUrl.d.ts.map +1 -0
- package/dist/frontend/storage/storageUrl.js +46 -0
- package/dist/frontend/storage/storageUrl.js.map +1 -0
- package/dist/frontend/storage/useStorageUpload.d.ts +41 -0
- package/dist/frontend/storage/useStorageUpload.d.ts.map +1 -0
- package/dist/frontend/storage/useStorageUpload.js +58 -0
- package/dist/frontend/storage/useStorageUpload.js.map +1 -0
- package/dist/shared/storage/ImageProcessingService.d.ts +32 -0
- package/dist/shared/storage/ImageProcessingService.d.ts.map +1 -0
- package/dist/shared/storage/ImageProcessingService.js +127 -0
- package/dist/shared/storage/ImageProcessingService.js.map +1 -0
- package/dist/shared/storage/StorageProxyService.d.ts +19 -0
- package/dist/shared/storage/StorageProxyService.d.ts.map +1 -0
- package/dist/shared/storage/StorageProxyService.js +63 -0
- package/dist/shared/storage/StorageProxyService.js.map +1 -0
- package/dist/shared/storage/index.d.ts +25 -0
- package/dist/shared/storage/index.d.ts.map +1 -0
- package/dist/shared/storage/index.js +24 -0
- package/dist/shared/storage/index.js.map +1 -0
- package/dist/shared/storage/routes.d.ts +42 -0
- package/dist/shared/storage/routes.d.ts.map +1 -0
- package/dist/shared/storage/routes.js +145 -0
- package/dist/shared/storage/routes.js.map +1 -0
- package/dist/shared/storage/types.d.ts +53 -0
- package/dist/shared/storage/types.d.ts.map +1 -0
- package/dist/shared/storage/types.js +2 -0
- package/dist/shared/storage/types.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { configureStorageUrls } from './storageUrl.js';
|
|
2
|
+
export type { StorageUrlHelpers, PhotoSize, Photo } from './storageUrl.js';
|
|
3
|
+
export { useStorageUpload } from './useStorageUpload.js';
|
|
4
|
+
export type { UseStorageUploadOptions, UseStorageUploadReturn, UploadResult } from './useStorageUpload.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/frontend/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/frontend/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage URL Builders — construct proxy URLs for stored images
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { configureStorageUrls } from '@soulbatical/tetra-core/frontend';
|
|
7
|
+
*
|
|
8
|
+
* const storage = configureStorageUrls('https://api.myapp.com');
|
|
9
|
+
* storage.buildStorageUrl('ad-creatives', orgId, 'abc.png');
|
|
10
|
+
* // → "https://api.myapp.com/api/public/storage/ad-creatives/{orgId}/abc.png"
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export type PhotoSize = 'thumb' | 'medium' | 'large' | 'original';
|
|
14
|
+
export interface Photo {
|
|
15
|
+
base_filename?: string;
|
|
16
|
+
storage_prefix?: string;
|
|
17
|
+
storage_path?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface StorageUrlHelpers {
|
|
20
|
+
/** Build a proxy URL for any stored file */
|
|
21
|
+
buildStorageUrl(bucket: string, orgId: string, fileId: string): string;
|
|
22
|
+
/** Build a photo URL for a specific size (expects {size}_{filename} convention) */
|
|
23
|
+
buildPhotoUrl(bucket: string, photo: Photo, size: PhotoSize): string;
|
|
24
|
+
/** Build a srcSet string for responsive images */
|
|
25
|
+
buildPhotoSrcSet(bucket: string, photo: Photo): string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Configure storage URL helpers with a base API URL.
|
|
29
|
+
* Call once at app startup, use the returned helpers everywhere.
|
|
30
|
+
*/
|
|
31
|
+
export declare function configureStorageUrls(apiBaseUrl: string): StorageUrlHelpers;
|
|
32
|
+
//# sourceMappingURL=storageUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storageUrl.d.ts","sourceRoot":"","sources":["../../../src/frontend/storage/storageUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAElE,MAAM,WAAW,KAAK;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACvE,mFAAmF;IACnF,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACrE,kDAAkD;IAClD,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;CACxD;AASD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,CA4B1E"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage URL Builders — construct proxy URLs for stored images
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { configureStorageUrls } from '@soulbatical/tetra-core/frontend';
|
|
7
|
+
*
|
|
8
|
+
* const storage = configureStorageUrls('https://api.myapp.com');
|
|
9
|
+
* storage.buildStorageUrl('ad-creatives', orgId, 'abc.png');
|
|
10
|
+
* // → "https://api.myapp.com/api/public/storage/ad-creatives/{orgId}/abc.png"
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
const SIZE_WIDTHS = {
|
|
14
|
+
thumb: 150,
|
|
15
|
+
medium: 600,
|
|
16
|
+
large: 1200,
|
|
17
|
+
original: 2000,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Configure storage URL helpers with a base API URL.
|
|
21
|
+
* Call once at app startup, use the returned helpers everywhere.
|
|
22
|
+
*/
|
|
23
|
+
export function configureStorageUrls(apiBaseUrl) {
|
|
24
|
+
const base = apiBaseUrl.replace(/\/$/, '');
|
|
25
|
+
function buildStorageUrl(bucket, orgId, fileId) {
|
|
26
|
+
return `${base}/api/public/storage/${bucket}/${orgId}/${fileId}`;
|
|
27
|
+
}
|
|
28
|
+
function buildPhotoUrl(bucket, photo, size) {
|
|
29
|
+
if (photo.storage_path) {
|
|
30
|
+
// Full path provided — use directly
|
|
31
|
+
return `${base}/api/public/storage/${bucket}/${photo.storage_path}`;
|
|
32
|
+
}
|
|
33
|
+
const prefix = photo.storage_prefix || '';
|
|
34
|
+
const filename = photo.base_filename || '';
|
|
35
|
+
const sizedFilename = size === 'original' ? filename : `${size}_${filename}`;
|
|
36
|
+
const path = prefix ? `${prefix}/${sizedFilename}` : sizedFilename;
|
|
37
|
+
return `${base}/api/public/storage/${bucket}/${path}`;
|
|
38
|
+
}
|
|
39
|
+
function buildPhotoSrcSet(bucket, photo) {
|
|
40
|
+
return Object.keys(SIZE_WIDTHS)
|
|
41
|
+
.map((size) => `${buildPhotoUrl(bucket, photo, size)} ${SIZE_WIDTHS[size]}w`)
|
|
42
|
+
.join(', ');
|
|
43
|
+
}
|
|
44
|
+
return { buildStorageUrl, buildPhotoUrl, buildPhotoSrcSet };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=storageUrl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storageUrl.js","sourceRoot":"","sources":["../../../src/frontend/storage/storageUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmBH,MAAM,WAAW,GAA8B;IAC7C,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,GAAG;IACX,KAAK,EAAE,IAAI;IACX,QAAQ,EAAE,IAAI;CACf,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAE3C,SAAS,eAAe,CAAC,MAAc,EAAE,KAAa,EAAE,MAAc;QACpE,OAAO,GAAG,IAAI,uBAAuB,MAAM,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;IACnE,CAAC;IAED,SAAS,aAAa,CAAC,MAAc,EAAE,KAAY,EAAE,IAAe;QAClE,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YACvB,oCAAoC;YACpC,OAAO,GAAG,IAAI,uBAAuB,MAAM,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QACtE,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,cAAc,IAAI,EAAE,CAAC;QAC1C,MAAM,QAAQ,GAAG,KAAK,CAAC,aAAa,IAAI,EAAE,CAAC;QAC3C,MAAM,aAAa,GAAG,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC7E,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;QAEnE,OAAO,GAAG,IAAI,uBAAuB,MAAM,IAAI,IAAI,EAAE,CAAC;IACxD,CAAC;IAED,SAAS,gBAAgB,CAAC,MAAc,EAAE,KAAY;QACpD,OAAQ,MAAM,CAAC,IAAI,CAAC,WAAW,CAAiB;aAC7C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;aAC5E,IAAI,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC;IAED,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hook for uploading files to storage
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { useStorageUpload } from '@soulbatical/tetra-core/frontend';
|
|
7
|
+
*
|
|
8
|
+
* const { upload, uploading, error, result } = useStorageUpload({
|
|
9
|
+
* apiBaseUrl: process.env.NEXT_PUBLIC_API_URL,
|
|
10
|
+
* bucket: 'ad-creatives',
|
|
11
|
+
* orgId: user.activeOrganizationId,
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* const handleFile = async (file: File) => {
|
|
15
|
+
* const uploaded = await upload(file);
|
|
16
|
+
* console.log(uploaded.proxyUrl);
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export interface UploadResult {
|
|
21
|
+
bucket: string;
|
|
22
|
+
path: string;
|
|
23
|
+
proxyUrl: string;
|
|
24
|
+
contentType: string;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
export interface UseStorageUploadOptions {
|
|
28
|
+
apiBaseUrl: string;
|
|
29
|
+
bucket: string;
|
|
30
|
+
orgId: string;
|
|
31
|
+
/** Upload endpoint path — defaults to '/api/admin/storage/upload' */
|
|
32
|
+
uploadPath?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface UseStorageUploadReturn {
|
|
35
|
+
upload: (file: File) => Promise<UploadResult>;
|
|
36
|
+
uploading: boolean;
|
|
37
|
+
error: string | null;
|
|
38
|
+
result: UploadResult | null;
|
|
39
|
+
}
|
|
40
|
+
export declare function useStorageUpload(options: UseStorageUploadOptions): UseStorageUploadReturn;
|
|
41
|
+
//# sourceMappingURL=useStorageUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStorageUpload.d.ts","sourceRoot":"","sources":["../../../src/frontend/storage/useStorageUpload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,sBAAsB,CAyCzF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hook for uploading files to storage
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { useStorageUpload } from '@soulbatical/tetra-core/frontend';
|
|
7
|
+
*
|
|
8
|
+
* const { upload, uploading, error, result } = useStorageUpload({
|
|
9
|
+
* apiBaseUrl: process.env.NEXT_PUBLIC_API_URL,
|
|
10
|
+
* bucket: 'ad-creatives',
|
|
11
|
+
* orgId: user.activeOrganizationId,
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* const handleFile = async (file: File) => {
|
|
15
|
+
* const uploaded = await upload(file);
|
|
16
|
+
* console.log(uploaded.proxyUrl);
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import { useState, useCallback } from 'react';
|
|
21
|
+
export function useStorageUpload(options) {
|
|
22
|
+
const { apiBaseUrl, bucket, orgId, uploadPath = '/api/admin/storage/upload' } = options;
|
|
23
|
+
const [uploading, setUploading] = useState(false);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const [result, setResult] = useState(null);
|
|
26
|
+
const upload = useCallback(async (file) => {
|
|
27
|
+
setUploading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
setResult(null);
|
|
30
|
+
try {
|
|
31
|
+
const formData = new FormData();
|
|
32
|
+
formData.append('file', file);
|
|
33
|
+
formData.append('bucket', bucket);
|
|
34
|
+
const base = apiBaseUrl.replace(/\/$/, '');
|
|
35
|
+
const res = await fetch(`${base}${uploadPath}?bucket=${encodeURIComponent(bucket)}`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
body: formData,
|
|
38
|
+
credentials: 'include',
|
|
39
|
+
});
|
|
40
|
+
const json = await res.json();
|
|
41
|
+
if (!res.ok || !json.success) {
|
|
42
|
+
throw new Error(json.error || 'Upload failed');
|
|
43
|
+
}
|
|
44
|
+
setResult(json.data);
|
|
45
|
+
return json.data;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const message = err instanceof Error ? err.message : 'Upload failed';
|
|
49
|
+
setError(message);
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
setUploading(false);
|
|
54
|
+
}
|
|
55
|
+
}, [apiBaseUrl, bucket, orgId, uploadPath]);
|
|
56
|
+
return { upload, uploading, error, result };
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=useStorageUpload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStorageUpload.js","sourceRoot":"","sources":["../../../src/frontend/storage/useStorageUpload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAyB9C,MAAM,UAAU,gBAAgB,CAAC,OAAgC;IAC/D,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,2BAA2B,EAAE,GAAG,OAAO,CAAC;IACxF,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IACxD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAC;IAEhE,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,EAAE,IAAU,EAAyB,EAAE;QACrE,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,SAAS,CAAC,IAAI,CAAC,CAAC;QAEhB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;YAChC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC9B,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAElC,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC3C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,GAAG,UAAU,WAAW,kBAAkB,CAAC,MAAM,CAAC,EAAE,EAAE;gBACnF,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,SAAS;aACvB,CAAC,CAAC;YAEH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAsD,CAAC;YAElF,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,eAAe,CAAC,CAAC;YACjD,CAAC;YAED,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrB,OAAO,IAAI,CAAC,IAAI,CAAC;QACnB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;YACrE,QAAQ,CAAC,OAAO,CAAC,CAAC;YAClB,MAAM,GAAG,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;IAE5C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Processing Service — sharp-based resize and format conversion
|
|
3
|
+
*
|
|
4
|
+
* Ported from SparkBuddy. Uses dynamic import so the module loads
|
|
5
|
+
* without sharp installed (fails only when you call a method).
|
|
6
|
+
*/
|
|
7
|
+
import type { ImageProcessingOptions, ImageSize, ProcessedImageResult } from './types.js';
|
|
8
|
+
/** Industry-standard image sizes (Amazon/Shopify pattern) */
|
|
9
|
+
export declare const STANDARD_IMAGE_SIZES: ImageSize[];
|
|
10
|
+
export declare class ImageProcessingService {
|
|
11
|
+
/**
|
|
12
|
+
* Generate a preview/thumbnail version of an image
|
|
13
|
+
*/
|
|
14
|
+
static generatePreview(buffer: Buffer, options?: ImageProcessingOptions): Promise<Buffer>;
|
|
15
|
+
/**
|
|
16
|
+
* Generate multiple sizes of an image for responsive display
|
|
17
|
+
*/
|
|
18
|
+
static generateMultipleSizes(buffer: Buffer, options?: {
|
|
19
|
+
format?: 'jpeg' | 'png' | 'webp';
|
|
20
|
+
sizes?: ImageSize[];
|
|
21
|
+
}): Promise<ProcessedImageResult[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Get image metadata without processing
|
|
24
|
+
*/
|
|
25
|
+
static getMetadata(buffer: Buffer): Promise<{
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
format: keyof import("sharp").FormatEnum;
|
|
29
|
+
size: number;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=ImageProcessingService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageProcessingService.d.ts","sourceRoot":"","sources":["../../../src/shared/storage/ImageProcessingService.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAc1F,6DAA6D;AAC7D,eAAO,MAAM,oBAAoB,EAAE,SAAS,EAK3C,CAAC;AAEF,qBAAa,sBAAsB;IACjC;;OAEG;WACU,eAAe,CAC1B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC;IA0ClB;;OAEG;WACU,qBAAqB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;KAAO,GACtE,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAsDlC;;OAEG;WACU,WAAW,CAAC,MAAM,EAAE,MAAM;;;;;;CAexC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Processing Service — sharp-based resize and format conversion
|
|
3
|
+
*
|
|
4
|
+
* Ported from SparkBuddy. Uses dynamic import so the module loads
|
|
5
|
+
* without sharp installed (fails only when you call a method).
|
|
6
|
+
*/
|
|
7
|
+
import { createLogger } from '../../utils/logger.js';
|
|
8
|
+
const logger = createLogger('storage:imageprocessing');
|
|
9
|
+
/** Dynamic sharp import — fails at call time, not at load time */
|
|
10
|
+
async function getSharp() {
|
|
11
|
+
try {
|
|
12
|
+
return (await import('sharp')).default;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new Error('sharp is required for image processing. Install: npm install sharp');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Industry-standard image sizes (Amazon/Shopify pattern) */
|
|
19
|
+
export const STANDARD_IMAGE_SIZES = [
|
|
20
|
+
{ name: 'thumb', width: 150, height: 150, quality: 80 },
|
|
21
|
+
{ name: 'medium', width: 600, height: 600, quality: 85 },
|
|
22
|
+
{ name: 'large', width: 1200, height: 1200, quality: 90 },
|
|
23
|
+
{ name: 'original', width: 2000, height: 2000, quality: 95 },
|
|
24
|
+
];
|
|
25
|
+
export class ImageProcessingService {
|
|
26
|
+
/**
|
|
27
|
+
* Generate a preview/thumbnail version of an image
|
|
28
|
+
*/
|
|
29
|
+
static async generatePreview(buffer, options = {}) {
|
|
30
|
+
const sharp = await getSharp();
|
|
31
|
+
const { maxWidth = 1024, maxHeight = 1366, quality = 85, format = 'jpeg', } = options;
|
|
32
|
+
try {
|
|
33
|
+
let processedImage = sharp(buffer).resize(maxWidth, maxHeight, {
|
|
34
|
+
fit: 'inside',
|
|
35
|
+
withoutEnlargement: true,
|
|
36
|
+
});
|
|
37
|
+
switch (format) {
|
|
38
|
+
case 'jpeg':
|
|
39
|
+
processedImage = processedImage.jpeg({ quality, progressive: true });
|
|
40
|
+
break;
|
|
41
|
+
case 'png':
|
|
42
|
+
processedImage = processedImage.png({ quality, compressionLevel: 9 });
|
|
43
|
+
break;
|
|
44
|
+
case 'webp':
|
|
45
|
+
processedImage = processedImage.webp({ quality });
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const previewBuffer = await processedImage.toBuffer();
|
|
49
|
+
logger.debug({
|
|
50
|
+
originalSize: buffer.length,
|
|
51
|
+
previewSize: previewBuffer.length,
|
|
52
|
+
reduction: `${Math.round((1 - previewBuffer.length / buffer.length) * 100)}%`,
|
|
53
|
+
}, 'Preview generated');
|
|
54
|
+
return previewBuffer;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
logger.error({ error }, 'Error generating preview');
|
|
58
|
+
throw new Error(`Failed to generate preview: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generate multiple sizes of an image for responsive display
|
|
63
|
+
*/
|
|
64
|
+
static async generateMultipleSizes(buffer, options = {}) {
|
|
65
|
+
const sharp = await getSharp();
|
|
66
|
+
const { sizes = STANDARD_IMAGE_SIZES } = options;
|
|
67
|
+
try {
|
|
68
|
+
const originalMetadata = await sharp(buffer).metadata();
|
|
69
|
+
if (!originalMetadata.width || !originalMetadata.height) {
|
|
70
|
+
throw new Error('Unable to read image dimensions');
|
|
71
|
+
}
|
|
72
|
+
const maxDimension = Math.max(originalMetadata.width, originalMetadata.height);
|
|
73
|
+
const minDimension = Math.min(originalMetadata.width, originalMetadata.height);
|
|
74
|
+
if (maxDimension < 800 && minDimension < 600) {
|
|
75
|
+
throw new Error('Image must be at least 800 pixels on one side, or 600x600 pixels minimum');
|
|
76
|
+
}
|
|
77
|
+
const results = await Promise.all(sizes.map(async (size) => {
|
|
78
|
+
const resizedBuffer = await sharp(buffer)
|
|
79
|
+
.resize(size.width, size.height, {
|
|
80
|
+
fit: 'inside',
|
|
81
|
+
withoutEnlargement: true,
|
|
82
|
+
})
|
|
83
|
+
.jpeg({ quality: size.quality, progressive: true })
|
|
84
|
+
.toBuffer();
|
|
85
|
+
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
|
86
|
+
return {
|
|
87
|
+
size: size.name,
|
|
88
|
+
buffer: resizedBuffer,
|
|
89
|
+
metadata: {
|
|
90
|
+
width: resizedMetadata.width || 0,
|
|
91
|
+
height: resizedMetadata.height || 0,
|
|
92
|
+
size: resizedBuffer.length,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}));
|
|
96
|
+
logger.debug({
|
|
97
|
+
originalSize: buffer.length,
|
|
98
|
+
sizesGenerated: results.length,
|
|
99
|
+
}, 'Multiple sizes generated');
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
logger.error({ error }, 'Error generating multiple sizes');
|
|
104
|
+
throw new Error(`Failed to generate multiple sizes: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get image metadata without processing
|
|
109
|
+
*/
|
|
110
|
+
static async getMetadata(buffer) {
|
|
111
|
+
const sharp = await getSharp();
|
|
112
|
+
try {
|
|
113
|
+
const metadata = await sharp(buffer).metadata();
|
|
114
|
+
return {
|
|
115
|
+
width: metadata.width,
|
|
116
|
+
height: metadata.height,
|
|
117
|
+
format: metadata.format,
|
|
118
|
+
size: buffer.length,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger.error({ error }, 'Error reading image metadata');
|
|
123
|
+
throw new Error(`Failed to read image metadata: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=ImageProcessingService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageProcessingService.js","sourceRoot":"","sources":["../../../src/shared/storage/ImageProcessingService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Proxy Service — streams files from Supabase through the API
|
|
3
|
+
*
|
|
4
|
+
* Sets proper CORS/CORP/Cache headers so images work cross-origin.
|
|
5
|
+
* Never exposes Supabase URLs to the client.
|
|
6
|
+
*/
|
|
7
|
+
import type { Response } from 'express';
|
|
8
|
+
import type { StorageConfig } from './types.js';
|
|
9
|
+
export declare class StorageProxyService {
|
|
10
|
+
private readonly supabaseUrl;
|
|
11
|
+
private readonly cacheMaxAge;
|
|
12
|
+
constructor(config: StorageConfig);
|
|
13
|
+
/**
|
|
14
|
+
* Stream a file from Supabase storage to the response.
|
|
15
|
+
* Sets Content-Type, Cache-Control, CORP, and CORS headers.
|
|
16
|
+
*/
|
|
17
|
+
streamFile(bucket: string, orgId: string, fileId: string, res: Response): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=StorageProxyService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StorageProxyService.d.ts","sourceRoot":"","sources":["../../../src/shared/storage/StorageProxyService.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAKhD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,MAAM,EAAE,aAAa;IAKjC;;;OAGG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CA+C9F"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Proxy Service — streams files from Supabase through the API
|
|
3
|
+
*
|
|
4
|
+
* Sets proper CORS/CORP/Cache headers so images work cross-origin.
|
|
5
|
+
* Never exposes Supabase URLs to the client.
|
|
6
|
+
*/
|
|
7
|
+
import { createLogger } from '../../utils/logger.js';
|
|
8
|
+
const logger = createLogger('storage:proxy');
|
|
9
|
+
export class StorageProxyService {
|
|
10
|
+
supabaseUrl;
|
|
11
|
+
cacheMaxAge;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.supabaseUrl = config.supabaseUrl || process.env.SUPABASE_URL || '';
|
|
14
|
+
this.cacheMaxAge = config.cacheMaxAge ?? 3600;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Stream a file from Supabase storage to the response.
|
|
18
|
+
* Sets Content-Type, Cache-Control, CORP, and CORS headers.
|
|
19
|
+
*/
|
|
20
|
+
async streamFile(bucket, orgId, fileId, res) {
|
|
21
|
+
if (!this.supabaseUrl) {
|
|
22
|
+
res.status(503).json({ success: false, error: 'Storage not configured' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const url = `${this.supabaseUrl}/storage/v1/object/public/${bucket}/${orgId}/${fileId}`;
|
|
26
|
+
try {
|
|
27
|
+
const upstream = await fetch(url);
|
|
28
|
+
if (!upstream.ok) {
|
|
29
|
+
res.status(upstream.status === 404 ? 404 : 502).json({
|
|
30
|
+
success: false,
|
|
31
|
+
error: upstream.status === 404 ? 'File not found' : 'Storage error',
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const contentType = upstream.headers.get('content-type') || 'application/octet-stream';
|
|
36
|
+
res.setHeader('Content-Type', contentType);
|
|
37
|
+
res.setHeader('Cache-Control', `public, max-age=${this.cacheMaxAge}, immutable`);
|
|
38
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
39
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
40
|
+
const contentLength = upstream.headers.get('content-length');
|
|
41
|
+
if (contentLength) {
|
|
42
|
+
res.setHeader('Content-Length', contentLength);
|
|
43
|
+
}
|
|
44
|
+
const reader = upstream.body?.getReader();
|
|
45
|
+
if (!reader) {
|
|
46
|
+
res.status(502).json({ success: false, error: 'No response body' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
while (true) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done)
|
|
52
|
+
break;
|
|
53
|
+
res.write(value);
|
|
54
|
+
}
|
|
55
|
+
res.end();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
logger.error({ error: err, bucket, orgId, fileId }, 'Failed to stream file');
|
|
59
|
+
res.status(502).json({ success: false, error: 'Failed to fetch from storage' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=StorageProxyService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StorageProxyService.js","sourceRoot":"","sources":["../../../src/shared/storage/StorageProxyService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @soulbatical/tetra-core/storage
|
|
3
|
+
*
|
|
4
|
+
* Shared storage module — Supabase streaming proxy, upload, and image processing.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import {
|
|
9
|
+
* addStorageProxyRoutes,
|
|
10
|
+
* addStorageUploadRoutes,
|
|
11
|
+
* ImageProcessingService,
|
|
12
|
+
* } from '@soulbatical/tetra-core';
|
|
13
|
+
*
|
|
14
|
+
* const publicRouter = Router();
|
|
15
|
+
* addStorageProxyRoutes(publicRouter, {
|
|
16
|
+
* config: { allowedBuckets: ['ad-creatives'] },
|
|
17
|
+
* });
|
|
18
|
+
* app.use('/api/public/storage', publicRouter);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export { addStorageProxyRoutes, addStorageUploadRoutes } from './routes.js';
|
|
22
|
+
export { StorageProxyService } from './StorageProxyService.js';
|
|
23
|
+
export { ImageProcessingService, STANDARD_IMAGE_SIZES } from './ImageProcessingService.js';
|
|
24
|
+
export type { StorageConfig, StorageProxyOptions, StorageUploadOptions, UploadResult, ImageSize, ProcessedImageResult, ImageProcessingOptions, } from './types.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/storage/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,SAAS,EACT,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @soulbatical/tetra-core/storage
|
|
3
|
+
*
|
|
4
|
+
* Shared storage module — Supabase streaming proxy, upload, and image processing.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import {
|
|
9
|
+
* addStorageProxyRoutes,
|
|
10
|
+
* addStorageUploadRoutes,
|
|
11
|
+
* ImageProcessingService,
|
|
12
|
+
* } from '@soulbatical/tetra-core';
|
|
13
|
+
*
|
|
14
|
+
* const publicRouter = Router();
|
|
15
|
+
* addStorageProxyRoutes(publicRouter, {
|
|
16
|
+
* config: { allowedBuckets: ['ad-creatives'] },
|
|
17
|
+
* });
|
|
18
|
+
* app.use('/api/public/storage', publicRouter);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export { addStorageProxyRoutes, addStorageUploadRoutes } from './routes.js';
|
|
22
|
+
export { StorageProxyService } from './StorageProxyService.js';
|
|
23
|
+
export { ImageProcessingService, STANDARD_IMAGE_SIZES } from './ImageProcessingService.js';
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/storage/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Route Factories — add storage proxy and upload endpoints
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { addStorageProxyRoutes, addStorageUploadRoutes } from '@soulbatical/tetra-core';
|
|
7
|
+
*
|
|
8
|
+
* // Public proxy (no auth)
|
|
9
|
+
* const publicRouter = Router();
|
|
10
|
+
* addStorageProxyRoutes(publicRouter, {
|
|
11
|
+
* config: { allowedBuckets: ['ad-creatives', 'ad-library'] },
|
|
12
|
+
* });
|
|
13
|
+
* app.use('/api/public/storage', publicRouter);
|
|
14
|
+
*
|
|
15
|
+
* // Authenticated upload
|
|
16
|
+
* const uploadRouter = Router();
|
|
17
|
+
* uploadRouter.use(authenticateToken);
|
|
18
|
+
* addStorageUploadRoutes(uploadRouter, {
|
|
19
|
+
* config: { allowedBuckets: ['ad-creatives'] },
|
|
20
|
+
* getOrgId: (req) => req.user?.activeOrganizationId,
|
|
21
|
+
* });
|
|
22
|
+
* app.use('/api/admin/storage', uploadRouter);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { Router } from 'express';
|
|
26
|
+
import type { StorageProxyOptions, StorageUploadOptions } from './types.js';
|
|
27
|
+
/**
|
|
28
|
+
* Add public storage proxy routes.
|
|
29
|
+
*
|
|
30
|
+
* Mounts: GET /:bucket/:orgId/:fileId
|
|
31
|
+
* Streams from Supabase with proper CORS/CORP/Cache headers.
|
|
32
|
+
*/
|
|
33
|
+
export declare function addStorageProxyRoutes(router: Router, options: StorageProxyOptions): void;
|
|
34
|
+
/**
|
|
35
|
+
* Add authenticated storage upload routes.
|
|
36
|
+
*
|
|
37
|
+
* Mounts: POST /upload
|
|
38
|
+
* Accepts multipart (multer) or base64 JSON body.
|
|
39
|
+
* Enforces org-scoping: user can only upload to their own org.
|
|
40
|
+
*/
|
|
41
|
+
export declare function addStorageUploadRoutes(router: Router, options: StorageUploadOptions): void;
|
|
42
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/shared/storage/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAGpD,OAAO,KAAK,EAAiB,mBAAmB,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAC;AAKzG;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAwBxF;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,IAAI,CA8F1F"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Route Factories — add storage proxy and upload endpoints
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { addStorageProxyRoutes, addStorageUploadRoutes } from '@soulbatical/tetra-core';
|
|
7
|
+
*
|
|
8
|
+
* // Public proxy (no auth)
|
|
9
|
+
* const publicRouter = Router();
|
|
10
|
+
* addStorageProxyRoutes(publicRouter, {
|
|
11
|
+
* config: { allowedBuckets: ['ad-creatives', 'ad-library'] },
|
|
12
|
+
* });
|
|
13
|
+
* app.use('/api/public/storage', publicRouter);
|
|
14
|
+
*
|
|
15
|
+
* // Authenticated upload
|
|
16
|
+
* const uploadRouter = Router();
|
|
17
|
+
* uploadRouter.use(authenticateToken);
|
|
18
|
+
* addStorageUploadRoutes(uploadRouter, {
|
|
19
|
+
* config: { allowedBuckets: ['ad-creatives'] },
|
|
20
|
+
* getOrgId: (req) => req.user?.activeOrganizationId,
|
|
21
|
+
* });
|
|
22
|
+
* app.use('/api/admin/storage', uploadRouter);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { randomUUID } from 'crypto';
|
|
26
|
+
import { StorageProxyService } from './StorageProxyService.js';
|
|
27
|
+
import { createLogger } from '../../utils/logger.js';
|
|
28
|
+
const logger = createLogger('storage:routes');
|
|
29
|
+
/**
|
|
30
|
+
* Add public storage proxy routes.
|
|
31
|
+
*
|
|
32
|
+
* Mounts: GET /:bucket/:orgId/:fileId
|
|
33
|
+
* Streams from Supabase with proper CORS/CORP/Cache headers.
|
|
34
|
+
*/
|
|
35
|
+
export function addStorageProxyRoutes(router, options) {
|
|
36
|
+
const { config } = options;
|
|
37
|
+
if (!config.allowedBuckets || config.allowedBuckets.length === 0) {
|
|
38
|
+
throw new Error('addStorageProxyRoutes: allowedBuckets must not be empty');
|
|
39
|
+
}
|
|
40
|
+
const service = new StorageProxyService(config);
|
|
41
|
+
const allowedSet = new Set(config.allowedBuckets);
|
|
42
|
+
router.get('/:bucket/:orgId/:fileId', async (req, res) => {
|
|
43
|
+
const bucket = req.params.bucket;
|
|
44
|
+
const orgId = req.params.orgId;
|
|
45
|
+
const fileId = req.params.fileId;
|
|
46
|
+
if (!allowedSet.has(bucket)) {
|
|
47
|
+
res.status(400).json({ success: false, error: 'Invalid bucket' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await service.streamFile(bucket, orgId, fileId, res);
|
|
51
|
+
});
|
|
52
|
+
logger.info({ buckets: config.allowedBuckets }, 'Storage proxy routes mounted');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Add authenticated storage upload routes.
|
|
56
|
+
*
|
|
57
|
+
* Mounts: POST /upload
|
|
58
|
+
* Accepts multipart (multer) or base64 JSON body.
|
|
59
|
+
* Enforces org-scoping: user can only upload to their own org.
|
|
60
|
+
*/
|
|
61
|
+
export function addStorageUploadRoutes(router, options) {
|
|
62
|
+
const { config, getOrgId, onAfterUpload } = options;
|
|
63
|
+
if (!config.allowedBuckets || config.allowedBuckets.length === 0) {
|
|
64
|
+
throw new Error('addStorageUploadRoutes: allowedBuckets must not be empty');
|
|
65
|
+
}
|
|
66
|
+
const supabaseUrl = config.supabaseUrl || process.env.SUPABASE_URL || '';
|
|
67
|
+
const supabaseKey = config.supabaseServiceKey || process.env.SUPABASE_SERVICE_ROLE_KEY || '';
|
|
68
|
+
const maxBytes = config.maxUploadBytes ?? 50 * 1024 * 1024;
|
|
69
|
+
const allowedSet = new Set(config.allowedBuckets);
|
|
70
|
+
router.post('/upload', async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
const orgId = getOrgId(req);
|
|
73
|
+
if (!orgId) {
|
|
74
|
+
res.status(400).json({ success: false, error: 'No active organization' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Extract file from multer (req.file) or base64 JSON body
|
|
78
|
+
let fileBuffer;
|
|
79
|
+
let contentType;
|
|
80
|
+
let originalName;
|
|
81
|
+
const multerFile = req.file;
|
|
82
|
+
if (multerFile) {
|
|
83
|
+
fileBuffer = multerFile.buffer;
|
|
84
|
+
contentType = multerFile.mimetype;
|
|
85
|
+
originalName = multerFile.originalname;
|
|
86
|
+
}
|
|
87
|
+
else if (req.body?.data && req.body?.contentType) {
|
|
88
|
+
fileBuffer = Buffer.from(req.body.data, 'base64');
|
|
89
|
+
contentType = req.body.contentType;
|
|
90
|
+
originalName = req.body.filename || 'upload';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
res.status(400).json({ success: false, error: 'No file provided. Use multipart or { data, contentType }' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (fileBuffer.length > maxBytes) {
|
|
97
|
+
res.status(413).json({ success: false, error: `File too large (max ${Math.round(maxBytes / 1024 / 1024)}MB)` });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const bucket = req.body?.bucket || req.query?.bucket;
|
|
101
|
+
if (!bucket || !allowedSet.has(bucket)) {
|
|
102
|
+
res.status(400).json({ success: false, error: `Invalid bucket. Allowed: ${config.allowedBuckets.join(', ')}` });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Generate unique file ID
|
|
106
|
+
const ext = originalName.split('.').pop() || contentType.split('/').pop() || 'bin';
|
|
107
|
+
const fileId = `${randomUUID()}.${ext}`;
|
|
108
|
+
const path = `${orgId}/${fileId}`;
|
|
109
|
+
// Upload to Supabase
|
|
110
|
+
const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucket}/${path}`;
|
|
111
|
+
const uploadRes = await fetch(uploadUrl, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Authorization': `Bearer ${supabaseKey}`,
|
|
115
|
+
'Content-Type': contentType,
|
|
116
|
+
'x-upsert': 'true',
|
|
117
|
+
},
|
|
118
|
+
body: fileBuffer,
|
|
119
|
+
});
|
|
120
|
+
if (!uploadRes.ok) {
|
|
121
|
+
const body = await uploadRes.text();
|
|
122
|
+
logger.error({ status: uploadRes.status, body, bucket, path }, 'Supabase upload failed');
|
|
123
|
+
res.status(502).json({ success: false, error: 'Upload to storage failed' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const result = {
|
|
127
|
+
bucket: bucket,
|
|
128
|
+
path,
|
|
129
|
+
proxyUrl: `/api/public/storage/${bucket}/${path}`,
|
|
130
|
+
contentType,
|
|
131
|
+
size: fileBuffer.length,
|
|
132
|
+
};
|
|
133
|
+
if (onAfterUpload) {
|
|
134
|
+
await onAfterUpload(result, req);
|
|
135
|
+
}
|
|
136
|
+
res.json({ success: true, data: result });
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
logger.error({ error: err }, 'Upload error');
|
|
140
|
+
res.status(500).json({ success: false, error: 'Upload failed' });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
logger.info({ buckets: config.allowedBuckets }, 'Storage upload routes mounted');
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/shared/storage/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
export interface StorageConfig {
|
|
3
|
+
/** Allowed bucket names — required, non-empty */
|
|
4
|
+
allowedBuckets: string[];
|
|
5
|
+
/** Supabase URL — defaults to process.env.SUPABASE_URL */
|
|
6
|
+
supabaseUrl?: string;
|
|
7
|
+
/** Supabase service role key — defaults to process.env.SUPABASE_SERVICE_ROLE_KEY */
|
|
8
|
+
supabaseServiceKey?: string;
|
|
9
|
+
/** Max upload size in bytes — defaults to 50MB */
|
|
10
|
+
maxUploadBytes?: number;
|
|
11
|
+
/** Cache-Control max-age in seconds — defaults to 3600 */
|
|
12
|
+
cacheMaxAge?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface StorageProxyOptions {
|
|
15
|
+
config: StorageConfig;
|
|
16
|
+
}
|
|
17
|
+
export interface StorageUploadOptions extends StorageProxyOptions {
|
|
18
|
+
/** Extract orgId from request (e.g. req.user.activeOrganizationId) */
|
|
19
|
+
getOrgId: (req: Request) => string | undefined;
|
|
20
|
+
/** Called after successful upload */
|
|
21
|
+
onAfterUpload?: (result: UploadResult, req: Request) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface UploadResult {
|
|
24
|
+
bucket: string;
|
|
25
|
+
/** Relative path within bucket: "{orgId}/{fileId}.png" */
|
|
26
|
+
path: string;
|
|
27
|
+
/** Proxy URL path: "/api/public/storage/{bucket}/{orgId}/{fileId}.png" */
|
|
28
|
+
proxyUrl: string;
|
|
29
|
+
contentType: string;
|
|
30
|
+
size: number;
|
|
31
|
+
}
|
|
32
|
+
export interface ImageSize {
|
|
33
|
+
name: string;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
quality: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ProcessedImageResult {
|
|
39
|
+
size: string;
|
|
40
|
+
buffer: Buffer;
|
|
41
|
+
metadata: {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
size: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface ImageProcessingOptions {
|
|
48
|
+
maxWidth?: number;
|
|
49
|
+
maxHeight?: number;
|
|
50
|
+
quality?: number;
|
|
51
|
+
format?: 'jpeg' | 'png' | 'webp';
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/shared/storage/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,WAAW,aAAa;IAC5B,iDAAiD;IACjD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oFAAoF;IACpF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAC/D,sEAAsE;IACtE,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/C,qCAAqC;IACrC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;CAClC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/shared/storage/types.ts"],"names":[],"mappings":""}
|