@open-mercato/storage-s3 0.6.2-canary.3404.1.2312fbb315

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 (55) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/build.mjs +70 -0
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +7 -0
  5. package/dist/modules/storage_s3/acl.js +13 -0
  6. package/dist/modules/storage_s3/acl.js.map +7 -0
  7. package/dist/modules/storage_s3/api/delete/storage-providers/s3/delete.js +68 -0
  8. package/dist/modules/storage_s3/api/delete/storage-providers/s3/delete.js.map +7 -0
  9. package/dist/modules/storage_s3/api/get/storage-providers/s3/download.js +79 -0
  10. package/dist/modules/storage_s3/api/get/storage-providers/s3/download.js.map +7 -0
  11. package/dist/modules/storage_s3/api/get/storage-providers/s3/list.js +87 -0
  12. package/dist/modules/storage_s3/api/get/storage-providers/s3/list.js.map +7 -0
  13. package/dist/modules/storage_s3/api/post/storage-providers/s3/signed-url.js +77 -0
  14. package/dist/modules/storage_s3/api/post/storage-providers/s3/signed-url.js.map +7 -0
  15. package/dist/modules/storage_s3/api/post/storage-providers/s3/upload.js +99 -0
  16. package/dist/modules/storage_s3/api/post/storage-providers/s3/upload.js.map +7 -0
  17. package/dist/modules/storage_s3/di.js +75 -0
  18. package/dist/modules/storage_s3/di.js.map +7 -0
  19. package/dist/modules/storage_s3/index.js +9 -0
  20. package/dist/modules/storage_s3/index.js.map +7 -0
  21. package/dist/modules/storage_s3/integration.js +96 -0
  22. package/dist/modules/storage_s3/integration.js.map +7 -0
  23. package/dist/modules/storage_s3/lib/health.js +84 -0
  24. package/dist/modules/storage_s3/lib/health.js.map +7 -0
  25. package/dist/modules/storage_s3/lib/preset.js +84 -0
  26. package/dist/modules/storage_s3/lib/preset.js.map +7 -0
  27. package/dist/modules/storage_s3/lib/s3-driver.js +170 -0
  28. package/dist/modules/storage_s3/lib/s3-driver.js.map +7 -0
  29. package/dist/modules/storage_s3/lib/storage-service.js +53 -0
  30. package/dist/modules/storage_s3/lib/storage-service.js.map +7 -0
  31. package/dist/modules/storage_s3/setup.js +27 -0
  32. package/dist/modules/storage_s3/setup.js.map +7 -0
  33. package/dist/test-helpers/s3Fixtures.js +69 -0
  34. package/dist/test-helpers/s3Fixtures.js.map +7 -0
  35. package/jest.config.cjs +24 -0
  36. package/package.json +94 -0
  37. package/src/index.ts +1 -0
  38. package/src/modules/storage_s3/__tests__/s3Driver.test.ts +241 -0
  39. package/src/modules/storage_s3/acl.ts +9 -0
  40. package/src/modules/storage_s3/api/delete/storage-providers/s3/delete.ts +75 -0
  41. package/src/modules/storage_s3/api/get/storage-providers/s3/download.ts +86 -0
  42. package/src/modules/storage_s3/api/get/storage-providers/s3/list.ts +99 -0
  43. package/src/modules/storage_s3/api/post/storage-providers/s3/signed-url.ts +86 -0
  44. package/src/modules/storage_s3/api/post/storage-providers/s3/upload.ts +116 -0
  45. package/src/modules/storage_s3/di.ts +89 -0
  46. package/src/modules/storage_s3/index.ts +5 -0
  47. package/src/modules/storage_s3/integration.ts +97 -0
  48. package/src/modules/storage_s3/lib/health.ts +99 -0
  49. package/src/modules/storage_s3/lib/preset.ts +127 -0
  50. package/src/modules/storage_s3/lib/s3-driver.ts +233 -0
  51. package/src/modules/storage_s3/lib/storage-service.ts +124 -0
  52. package/src/modules/storage_s3/setup.ts +26 -0
  53. package/src/test-helpers/s3Fixtures.ts +71 -0
  54. package/tsconfig.json +9 -0
  55. package/watch.mjs +7 -0
@@ -0,0 +1,2 @@
1
+ Found 16 entry points
2
+ storage-s3 built successfully
package/build.mjs ADDED
@@ -0,0 +1,70 @@
1
+ import * as esbuild from 'esbuild'
2
+ import { glob } from 'glob'
3
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+
9
+ const entryPoints = await glob('src/**/*.{ts,tsx}', {
10
+ cwd: __dirname,
11
+ ignore: ['**/__tests__/**', '**/*.test.ts', '**/*.test.tsx'],
12
+ absolute: true,
13
+ })
14
+
15
+ if (entryPoints.length === 0) {
16
+ console.error('No entry points found!')
17
+ process.exit(1)
18
+ }
19
+
20
+ console.log(`Found ${entryPoints.length} entry points`)
21
+
22
+ const addJsExtension = {
23
+ name: 'add-js-extension',
24
+ setup(build) {
25
+ build.onEnd(async (result) => {
26
+ if (result.errors.length > 0) return
27
+ const outputFiles = await glob('dist/**/*.js', { cwd: __dirname, absolute: true })
28
+ for (const file of outputFiles) {
29
+ const fileDir = dirname(file)
30
+ let content = readFileSync(file, 'utf-8')
31
+ content = content.replace(
32
+ /from\s+["'](\.[^"']+)["']/g,
33
+ (match, path) => {
34
+ if (path.endsWith('.js') || path.endsWith('.json')) return match
35
+ const resolvedPath = join(fileDir, path)
36
+ if (existsSync(resolvedPath) && existsSync(join(resolvedPath, 'index.js'))) {
37
+ return `from "${path}/index.js"`
38
+ }
39
+ return `from "${path}.js"`
40
+ }
41
+ )
42
+ content = content.replace(
43
+ /import\s*\(\s*["'](\.[^"']+)["']\s*\)/g,
44
+ (match, path) => {
45
+ if (path.endsWith('.js') || path.endsWith('.json')) return match
46
+ const resolvedPath = join(fileDir, path)
47
+ if (existsSync(resolvedPath) && existsSync(join(resolvedPath, 'index.js'))) {
48
+ return `import("${path}/index.js")`
49
+ }
50
+ return `import("${path}.js")`
51
+ }
52
+ )
53
+ writeFileSync(file, content)
54
+ }
55
+ })
56
+ }
57
+ }
58
+
59
+ await esbuild.build({
60
+ entryPoints,
61
+ outdir: 'dist',
62
+ format: 'esm',
63
+ platform: 'node',
64
+ target: 'node18',
65
+ sourcemap: true,
66
+ jsx: 'automatic',
67
+ plugins: [addJsExtension],
68
+ })
69
+
70
+ console.log('storage-s3 built successfully')
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./modules/storage_s3/integration.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["export * from './modules/storage_s3/integration'\n"],
5
+ "mappings": "AAAA,cAAc;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ const features = [
2
+ {
3
+ id: "storage_providers.manage",
4
+ title: "Manage storage providers",
5
+ module: "storage_s3"
6
+ }
7
+ ];
8
+ var acl_default = features;
9
+ export {
10
+ acl_default as default,
11
+ features
12
+ };
13
+ //# sourceMappingURL=acl.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/storage_s3/acl.ts"],
4
+ "sourcesContent": ["export const features = [\n {\n id: 'storage_providers.manage',\n title: 'Manage storage providers',\n module: 'storage_s3',\n },\n]\n\nexport default features\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AACF;AAEA,IAAO,cAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,68 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { S3StorageDriver } from "../../../../lib/s3-driver.js";
6
+ const metadata = {
7
+ path: "/storage-providers/s3/delete",
8
+ DELETE: { requireAuth: true, requireFeatures: ["storage_providers.manage"] }
9
+ };
10
+ const requestSchema = z.object({
11
+ key: z.string().min(1)
12
+ });
13
+ async function resolveDriver(tenantId, orgId) {
14
+ const { resolve } = await createRequestContainer();
15
+ const credentialsService = resolve("integrationCredentialsService");
16
+ const creds = await credentialsService.resolve("storage_s3", { tenantId, organizationId: orgId });
17
+ if (!creds) return null;
18
+ return new S3StorageDriver(creds);
19
+ }
20
+ function isKeyScoped(key, orgId, tenantId) {
21
+ const parts = key.split("/");
22
+ return parts.length >= 3 && parts[1] === `org_${orgId}` && parts[2] === `tenant_${tenantId}`;
23
+ }
24
+ async function DELETE(req) {
25
+ const auth = await getAuthFromRequest(req);
26
+ if (!auth?.tenantId || !auth.orgId) {
27
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
28
+ }
29
+ const json = await req.json().catch(() => null);
30
+ const parsed = requestSchema.safeParse(json);
31
+ if (!parsed.success) {
32
+ return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
33
+ }
34
+ if (!isKeyScoped(parsed.data.key, auth.orgId, auth.tenantId)) {
35
+ return NextResponse.json({ error: "Access denied: key is not scoped to this tenant." }, { status: 403 });
36
+ }
37
+ const driver = await resolveDriver(auth.tenantId, auth.orgId);
38
+ if (!driver) {
39
+ return NextResponse.json({ error: "S3 integration is not configured." }, { status: 400 });
40
+ }
41
+ await driver.delete("", parsed.data.key);
42
+ return new NextResponse(null, { status: 204 });
43
+ }
44
+ var delete_default = DELETE;
45
+ const openApi = {
46
+ tag: "Storage",
47
+ summary: "Delete file from S3",
48
+ methods: {
49
+ DELETE: {
50
+ summary: "Delete a file from S3 by key",
51
+ description: "Permanently removes the object at the given key from the configured S3 bucket.",
52
+ requestBody: { contentType: "application/json", schema: requestSchema },
53
+ responses: [{ status: 204, description: "File deleted", schema: z.null() }],
54
+ errors: [
55
+ { status: 400, description: "Invalid payload or S3 not configured", schema: z.object({ error: z.string() }) },
56
+ { status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
57
+ { status: 403, description: "Key not scoped to this tenant", schema: z.object({ error: z.string() }) }
58
+ ]
59
+ }
60
+ }
61
+ };
62
+ export {
63
+ DELETE,
64
+ delete_default as default,
65
+ metadata,
66
+ openApi
67
+ };
68
+ //# sourceMappingURL=delete.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/storage_s3/api/delete/storage-providers/s3/delete.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { S3StorageDriver } from '../../../../lib/s3-driver'\n\nexport const metadata = {\n path: '/storage-providers/s3/delete',\n DELETE: { requireAuth: true, requireFeatures: ['storage_providers.manage'] },\n}\n\nconst requestSchema = z.object({\n key: z.string().min(1),\n})\n\nasync function resolveDriver(tenantId: string, orgId: string): Promise<S3StorageDriver | null> {\n const { resolve } = await createRequestContainer()\n const credentialsService = resolve('integrationCredentialsService') as {\n resolve(integrationId: string, scope: { tenantId: string; organizationId: string }): Promise<Record<string, unknown> | null>\n }\n const creds = await credentialsService.resolve('storage_s3', { tenantId, organizationId: orgId })\n if (!creds) return null\n return new S3StorageDriver(creds)\n}\n\nfunction isKeyScoped(key: string, orgId: string, tenantId: string): boolean {\n const parts = key.split('/')\n return parts.length >= 3 && parts[1] === `org_${orgId}` && parts[2] === `tenant_${tenantId}`\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const json = await req.json().catch(() => null)\n const parsed = requestSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n\n if (!isKeyScoped(parsed.data.key, auth.orgId, auth.tenantId)) {\n return NextResponse.json({ error: 'Access denied: key is not scoped to this tenant.' }, { status: 403 })\n }\n\n const driver = await resolveDriver(auth.tenantId, auth.orgId)\n if (!driver) {\n return NextResponse.json({ error: 'S3 integration is not configured.' }, { status: 400 })\n }\n\n await driver.delete('', parsed.data.key)\n return new NextResponse(null, { status: 204 })\n}\n\nexport default DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Storage',\n summary: 'Delete file from S3',\n methods: {\n DELETE: {\n summary: 'Delete a file from S3 by key',\n description: 'Permanently removes the object at the given key from the configured S3 bucket.',\n requestBody: { contentType: 'application/json', schema: requestSchema },\n responses: [{ status: 204, description: 'File deleted', schema: z.null() }],\n errors: [\n { status: 400, description: 'Invalid payload or S3 not configured', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Key not scoped to this tenant', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAEzB,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC7E;AAEA,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC;AACvB,CAAC;AAED,eAAe,cAAc,UAAkB,OAAgD;AAC7F,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,QAAM,qBAAqB,QAAQ,+BAA+B;AAGlE,QAAM,QAAQ,MAAM,mBAAmB,QAAQ,cAAc,EAAE,UAAU,gBAAgB,MAAM,CAAC;AAChG,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,IAAI,gBAAgB,KAAK;AAClC;AAEA,SAAS,YAAY,KAAa,OAAe,UAA2B;AAC1E,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,SAAO,MAAM,UAAU,KAAK,MAAM,CAAC,MAAM,OAAO,KAAK,MAAM,MAAM,CAAC,MAAM,UAAU,QAAQ;AAC5F;AAEA,eAAsB,OAAO,KAAc;AACzC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAC9C,QAAM,SAAS,cAAc,UAAU,IAAI;AAC3C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxE;AAEA,MAAI,CAAC,YAAY,OAAO,KAAK,KAAK,KAAK,OAAO,KAAK,QAAQ,GAAG;AAC5D,WAAO,aAAa,KAAK,EAAE,OAAO,mDAAmD,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzG;AAEA,QAAM,SAAS,MAAM,cAAc,KAAK,UAAU,KAAK,KAAK;AAC5D,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,OAAO,oCAAoC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1F;AAEA,QAAM,OAAO,OAAO,IAAI,OAAO,KAAK,GAAG;AACvC,SAAO,IAAI,aAAa,MAAM,EAAE,QAAQ,IAAI,CAAC;AAC/C;AAEA,IAAO,iBAAQ;AAER,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa,EAAE,aAAa,oBAAoB,QAAQ,cAAc;AAAA,MACtE,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,KAAK,EAAE,CAAC;AAAA,MAC1E,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,wCAAwC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QAC5G,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACpF,EAAE,QAAQ,KAAK,aAAa,iCAAiC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,MACvG;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,79 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { S3StorageDriver } from "../../../../lib/s3-driver.js";
6
+ const metadata = {
7
+ path: "/storage-providers/s3/download",
8
+ GET: { requireAuth: true, requireFeatures: ["storage_providers.manage"] }
9
+ };
10
+ async function resolveDriver(tenantId, orgId) {
11
+ const { resolve } = await createRequestContainer();
12
+ const credentialsService = resolve("integrationCredentialsService");
13
+ const creds = await credentialsService.resolve("storage_s3", { tenantId, organizationId: orgId });
14
+ if (!creds) return null;
15
+ return new S3StorageDriver(creds);
16
+ }
17
+ function isKeyScoped(key, orgId, tenantId) {
18
+ const parts = key.split("/");
19
+ return parts.length >= 3 && parts[1] === `org_${orgId}` && parts[2] === `tenant_${tenantId}`;
20
+ }
21
+ async function GET(req) {
22
+ const auth = await getAuthFromRequest(req);
23
+ if (!auth?.tenantId || !auth.orgId) {
24
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
25
+ }
26
+ const key = new URL(req.url).searchParams.get("key");
27
+ if (!key) {
28
+ return NextResponse.json({ error: "key query param is required" }, { status: 400 });
29
+ }
30
+ if (!isKeyScoped(key, auth.orgId, auth.tenantId)) {
31
+ return NextResponse.json({ error: "Access denied: key is not scoped to this tenant." }, { status: 403 });
32
+ }
33
+ const driver = await resolveDriver(auth.tenantId, auth.orgId);
34
+ if (!driver) {
35
+ return NextResponse.json({ error: "S3 integration is not configured." }, { status: 400 });
36
+ }
37
+ let buffer;
38
+ let contentType;
39
+ try {
40
+ const result = await driver.read("", key);
41
+ buffer = result.buffer;
42
+ contentType = result.contentType;
43
+ } catch {
44
+ return NextResponse.json({ error: "File not found" }, { status: 404 });
45
+ }
46
+ return new NextResponse(new Uint8Array(buffer), {
47
+ status: 200,
48
+ headers: {
49
+ "Content-Type": contentType ?? "application/octet-stream",
50
+ "Content-Length": String(buffer.length)
51
+ }
52
+ });
53
+ }
54
+ var download_default = GET;
55
+ const openApi = {
56
+ tag: "Storage",
57
+ summary: "Download file from S3",
58
+ methods: {
59
+ GET: {
60
+ summary: "Download a file from S3 by key",
61
+ description: "Streams the file content for the given S3 key. Requires storage_providers.manage feature.",
62
+ query: z.object({ key: z.string().describe("S3 object key") }),
63
+ responses: [{ status: 200, description: "File content stream", schema: z.any() }],
64
+ errors: [
65
+ { status: 400, description: "Missing key or S3 not configured", schema: z.object({ error: z.string() }) },
66
+ { status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
67
+ { status: 403, description: "Key not scoped to this tenant", schema: z.object({ error: z.string() }) },
68
+ { status: 404, description: "File not found", schema: z.object({ error: z.string() }) }
69
+ ]
70
+ }
71
+ }
72
+ };
73
+ export {
74
+ GET,
75
+ download_default as default,
76
+ metadata,
77
+ openApi
78
+ };
79
+ //# sourceMappingURL=download.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/storage_s3/api/get/storage-providers/s3/download.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { S3StorageDriver } from '../../../../lib/s3-driver'\n\nexport const metadata = {\n path: '/storage-providers/s3/download',\n GET: { requireAuth: true, requireFeatures: ['storage_providers.manage'] },\n}\n\nasync function resolveDriver(tenantId: string, orgId: string): Promise<S3StorageDriver | null> {\n const { resolve } = await createRequestContainer()\n const credentialsService = resolve('integrationCredentialsService') as {\n resolve(integrationId: string, scope: { tenantId: string; organizationId: string }): Promise<Record<string, unknown> | null>\n }\n const creds = await credentialsService.resolve('storage_s3', { tenantId, organizationId: orgId })\n if (!creds) return null\n return new S3StorageDriver(creds)\n}\n\nfunction isKeyScoped(key: string, orgId: string, tenantId: string): boolean {\n const parts = key.split('/')\n return parts.length >= 3 && parts[1] === `org_${orgId}` && parts[2] === `tenant_${tenantId}`\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const key = new URL(req.url).searchParams.get('key')\n if (!key) {\n return NextResponse.json({ error: 'key query param is required' }, { status: 400 })\n }\n\n if (!isKeyScoped(key, auth.orgId, auth.tenantId)) {\n return NextResponse.json({ error: 'Access denied: key is not scoped to this tenant.' }, { status: 403 })\n }\n\n const driver = await resolveDriver(auth.tenantId, auth.orgId)\n if (!driver) {\n return NextResponse.json({ error: 'S3 integration is not configured.' }, { status: 400 })\n }\n\n let buffer: Buffer\n let contentType: string | undefined\n try {\n const result = await driver.read('', key)\n buffer = result.buffer\n contentType = result.contentType\n } catch {\n return NextResponse.json({ error: 'File not found' }, { status: 404 })\n }\n\n return new NextResponse(new Uint8Array(buffer), {\n status: 200,\n headers: {\n 'Content-Type': contentType ?? 'application/octet-stream',\n 'Content-Length': String(buffer.length),\n },\n })\n}\n\nexport default GET\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Storage',\n summary: 'Download file from S3',\n methods: {\n GET: {\n summary: 'Download a file from S3 by key',\n description: 'Streams the file content for the given S3 key. Requires storage_providers.manage feature.',\n query: z.object({ key: z.string().describe('S3 object key') }),\n responses: [{ status: 200, description: 'File content stream', schema: z.any() }],\n errors: [\n { status: 400, description: 'Missing key or S3 not configured', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Key not scoped to this tenant', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'File not found', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,87 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { S3StorageDriver } from "../../../../lib/s3-driver.js";
6
+ const metadata = {
7
+ path: "/storage-providers/s3/list",
8
+ GET: { requireAuth: true, requireFeatures: ["storage_providers.manage"] }
9
+ };
10
+ const querySchema = z.object({
11
+ prefix: z.string().optional().default(""),
12
+ maxKeys: z.coerce.number().int().min(1).max(1e3).optional().default(100),
13
+ continuationToken: z.string().optional()
14
+ });
15
+ const fileSchema = z.object({
16
+ key: z.string(),
17
+ size: z.number().int(),
18
+ lastModified: z.string()
19
+ });
20
+ const responseSchema = z.object({
21
+ files: z.array(fileSchema),
22
+ truncated: z.boolean(),
23
+ nextContinuationToken: z.string().optional()
24
+ });
25
+ async function resolveDriver(tenantId, orgId) {
26
+ const { resolve } = await createRequestContainer();
27
+ const credentialsService = resolve("integrationCredentialsService");
28
+ const creds = await credentialsService.resolve("storage_s3", { tenantId, organizationId: orgId });
29
+ if (!creds) return null;
30
+ return new S3StorageDriver(creds);
31
+ }
32
+ async function GET(req) {
33
+ const auth = await getAuthFromRequest(req);
34
+ if (!auth?.tenantId || !auth.orgId) {
35
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
36
+ }
37
+ const params = Object.fromEntries(new URL(req.url).searchParams.entries());
38
+ const parsed = querySchema.safeParse(params);
39
+ if (!parsed.success) {
40
+ return NextResponse.json({ error: "Invalid query parameters" }, { status: 400 });
41
+ }
42
+ const tenantPrefix = `org_${auth.orgId}/tenant_${auth.tenantId}/`;
43
+ const userPrefix = parsed.data.prefix;
44
+ const effectivePrefix = userPrefix.includes(`org_${auth.orgId}/tenant_${auth.tenantId}`) ? userPrefix : tenantPrefix + userPrefix.replace(/^\//, "");
45
+ const driver = await resolveDriver(auth.tenantId, auth.orgId);
46
+ if (!driver) {
47
+ return NextResponse.json({ error: "S3 integration is not configured." }, { status: 400 });
48
+ }
49
+ const result = await driver.listObjects(
50
+ effectivePrefix,
51
+ parsed.data.maxKeys,
52
+ parsed.data.continuationToken
53
+ );
54
+ return NextResponse.json({
55
+ files: result.files.map((f) => ({
56
+ key: f.key,
57
+ size: f.size,
58
+ lastModified: f.lastModified.toISOString()
59
+ })),
60
+ truncated: result.truncated,
61
+ nextContinuationToken: result.nextContinuationToken
62
+ });
63
+ }
64
+ var list_default = GET;
65
+ const openApi = {
66
+ tag: "Storage",
67
+ summary: "List S3 objects",
68
+ methods: {
69
+ GET: {
70
+ summary: "List files in S3 by prefix",
71
+ description: "Returns a paginated list of S3 objects scoped to the authenticated tenant namespace.",
72
+ query: querySchema,
73
+ responses: [{ status: 200, description: "File listing", schema: responseSchema }],
74
+ errors: [
75
+ { status: 400, description: "Invalid params or S3 not configured", schema: z.object({ error: z.string() }) },
76
+ { status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) }
77
+ ]
78
+ }
79
+ }
80
+ };
81
+ export {
82
+ GET,
83
+ list_default as default,
84
+ metadata,
85
+ openApi
86
+ };
87
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/storage_s3/api/get/storage-providers/s3/list.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { S3StorageDriver } from '../../../../lib/s3-driver'\n\nexport const metadata = {\n path: '/storage-providers/s3/list',\n GET: { requireAuth: true, requireFeatures: ['storage_providers.manage'] },\n}\n\nconst querySchema = z.object({\n prefix: z.string().optional().default(''),\n maxKeys: z.coerce.number().int().min(1).max(1000).optional().default(100),\n continuationToken: z.string().optional(),\n})\n\nconst fileSchema = z.object({\n key: z.string(),\n size: z.number().int(),\n lastModified: z.string(),\n})\n\nconst responseSchema = z.object({\n files: z.array(fileSchema),\n truncated: z.boolean(),\n nextContinuationToken: z.string().optional(),\n})\n\nasync function resolveDriver(tenantId: string, orgId: string): Promise<S3StorageDriver | null> {\n const { resolve } = await createRequestContainer()\n const credentialsService = resolve('integrationCredentialsService') as {\n resolve(integrationId: string, scope: { tenantId: string; organizationId: string }): Promise<Record<string, unknown> | null>\n }\n const creds = await credentialsService.resolve('storage_s3', { tenantId, organizationId: orgId })\n if (!creds) return null\n return new S3StorageDriver(creds)\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const params = Object.fromEntries(new URL(req.url).searchParams.entries())\n const parsed = querySchema.safeParse(params)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 })\n }\n\n // Always scope list operations to the tenant namespace to prevent cross-tenant enumeration.\n const tenantPrefix = `org_${auth.orgId}/tenant_${auth.tenantId}/`\n const userPrefix = parsed.data.prefix\n const effectivePrefix = userPrefix.includes(`org_${auth.orgId}/tenant_${auth.tenantId}`)\n ? userPrefix\n : tenantPrefix + userPrefix.replace(/^\\//, '')\n\n const driver = await resolveDriver(auth.tenantId, auth.orgId)\n if (!driver) {\n return NextResponse.json({ error: 'S3 integration is not configured.' }, { status: 400 })\n }\n\n const result = await driver.listObjects(\n effectivePrefix,\n parsed.data.maxKeys,\n parsed.data.continuationToken,\n )\n\n return NextResponse.json({\n files: result.files.map((f) => ({\n key: f.key,\n size: f.size,\n lastModified: f.lastModified.toISOString(),\n })),\n truncated: result.truncated,\n nextContinuationToken: result.nextContinuationToken,\n })\n}\n\nexport default GET\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Storage',\n summary: 'List S3 objects',\n methods: {\n GET: {\n summary: 'List files in S3 by prefix',\n description: 'Returns a paginated list of S3 objects scoped to the authenticated tenant namespace.',\n query: querySchema,\n responses: [{ status: 200, description: 'File listing', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid params or S3 not configured', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB;AAEzB,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC1E;AAEA,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE;AAAA,EACxC,SAAS,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI,EAAE,SAAS,EAAE,QAAQ,GAAG;AAAA,EACxE,mBAAmB,EAAE,OAAO,EAAE,SAAS;AACzC,CAAC;AAED,MAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,KAAK,EAAE,OAAO;AAAA,EACd,MAAM,EAAE,OAAO,EAAE,IAAI;AAAA,EACrB,cAAc,EAAE,OAAO;AACzB,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,OAAO,EAAE,MAAM,UAAU;AAAA,EACzB,WAAW,EAAE,QAAQ;AAAA,EACrB,uBAAuB,EAAE,OAAO,EAAE,SAAS;AAC7C,CAAC;AAED,eAAe,cAAc,UAAkB,OAAgD;AAC7F,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,QAAM,qBAAqB,QAAQ,+BAA+B;AAGlE,QAAM,QAAQ,MAAM,mBAAmB,QAAQ,cAAc,EAAE,UAAU,gBAAgB,MAAM,CAAC;AAChG,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,IAAI,gBAAgB,KAAK;AAClC;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,SAAS,OAAO,YAAY,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,QAAQ,CAAC;AACzE,QAAM,SAAS,YAAY,UAAU,MAAM;AAC3C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjF;AAGA,QAAM,eAAe,OAAO,KAAK,KAAK,WAAW,KAAK,QAAQ;AAC9D,QAAM,aAAa,OAAO,KAAK;AAC/B,QAAM,kBAAkB,WAAW,SAAS,OAAO,KAAK,KAAK,WAAW,KAAK,QAAQ,EAAE,IACnF,aACA,eAAe,WAAW,QAAQ,OAAO,EAAE;AAE/C,QAAM,SAAS,MAAM,cAAc,KAAK,UAAU,KAAK,KAAK;AAC5D,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,OAAO,oCAAoC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1F;AAEA,QAAM,SAAS,MAAM,OAAO;AAAA,IAC1B;AAAA,IACA,OAAO,KAAK;AAAA,IACZ,OAAO,KAAK;AAAA,EACd;AAEA,SAAO,aAAa,KAAK;AAAA,IACvB,OAAO,OAAO,MAAM,IAAI,CAAC,OAAO;AAAA,MAC9B,KAAK,EAAE;AAAA,MACP,MAAM,EAAE;AAAA,MACR,cAAc,EAAE,aAAa,YAAY;AAAA,IAC3C,EAAE;AAAA,IACF,WAAW,OAAO;AAAA,IAClB,uBAAuB,OAAO;AAAA,EAChC,CAAC;AACH;AAEA,IAAO,eAAQ;AAER,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,OAAO;AAAA,MACP,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,eAAe,CAAC;AAAA,MAChF,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,uCAAuC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QAC3G,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,MACtF;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,77 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { S3StorageDriver } from "../../../../lib/s3-driver.js";
6
+ const metadata = {
7
+ path: "/storage-providers/s3/signed-url",
8
+ POST: { requireAuth: true, requireFeatures: ["storage_providers.manage"] }
9
+ };
10
+ const requestSchema = z.object({
11
+ key: z.string().min(1),
12
+ operation: z.enum(["upload", "download"]),
13
+ expiresIn: z.number().int().min(60).max(604800).optional().default(3600),
14
+ contentType: z.string().optional()
15
+ });
16
+ const responseSchema = z.object({
17
+ url: z.string(),
18
+ expiresAt: z.string()
19
+ });
20
+ async function resolveDriver(tenantId, orgId) {
21
+ const { resolve } = await createRequestContainer();
22
+ const credentialsService = resolve("integrationCredentialsService");
23
+ const creds = await credentialsService.resolve("storage_s3", { tenantId, organizationId: orgId });
24
+ if (!creds) return null;
25
+ return new S3StorageDriver(creds);
26
+ }
27
+ function isKeyScoped(key, orgId, tenantId) {
28
+ const parts = key.split("/");
29
+ return parts.length >= 3 && parts[1] === `org_${orgId}` && parts[2] === `tenant_${tenantId}`;
30
+ }
31
+ async function POST(req) {
32
+ const auth = await getAuthFromRequest(req);
33
+ if (!auth?.tenantId || !auth.orgId) {
34
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
35
+ }
36
+ const json = await req.json().catch(() => null);
37
+ const parsed = requestSchema.safeParse(json);
38
+ if (!parsed.success) {
39
+ return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
40
+ }
41
+ if (!isKeyScoped(parsed.data.key, auth.orgId, auth.tenantId)) {
42
+ return NextResponse.json({ error: "Access denied: key is not scoped to this tenant." }, { status: 403 });
43
+ }
44
+ const driver = await resolveDriver(auth.tenantId, auth.orgId);
45
+ if (!driver) {
46
+ return NextResponse.json({ error: "S3 integration is not configured." }, { status: 400 });
47
+ }
48
+ const { key, operation, expiresIn, contentType } = parsed.data;
49
+ const url = await driver.getSignedUrl(key, operation, expiresIn, contentType);
50
+ const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
51
+ return NextResponse.json({ url, expiresAt });
52
+ }
53
+ var signed_url_default = POST;
54
+ const openApi = {
55
+ tag: "Storage",
56
+ summary: "Generate S3 pre-signed URL",
57
+ methods: {
58
+ POST: {
59
+ summary: "Generate a pre-signed URL for direct browser upload or download",
60
+ description: "Returns a time-limited URL that allows a browser to directly upload or download a file from S3.",
61
+ requestBody: { contentType: "application/json", schema: requestSchema },
62
+ responses: [{ status: 200, description: "Pre-signed URL and expiry", schema: responseSchema }],
63
+ errors: [
64
+ { status: 400, description: "Invalid payload or S3 not configured", schema: z.object({ error: z.string() }) },
65
+ { status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
66
+ { status: 403, description: "Key not scoped to this tenant", schema: z.object({ error: z.string() }) }
67
+ ]
68
+ }
69
+ }
70
+ };
71
+ export {
72
+ POST,
73
+ signed_url_default as default,
74
+ metadata,
75
+ openApi
76
+ };
77
+ //# sourceMappingURL=signed-url.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/storage_s3/api/post/storage-providers/s3/signed-url.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { S3StorageDriver } from '../../../../lib/s3-driver'\n\nexport const metadata = {\n path: '/storage-providers/s3/signed-url',\n POST: { requireAuth: true, requireFeatures: ['storage_providers.manage'] },\n}\n\nconst requestSchema = z.object({\n key: z.string().min(1),\n operation: z.enum(['upload', 'download']),\n expiresIn: z.number().int().min(60).max(604800).optional().default(3600),\n contentType: z.string().optional(),\n})\n\nconst responseSchema = z.object({\n url: z.string(),\n expiresAt: z.string(),\n})\n\nasync function resolveDriver(tenantId: string, orgId: string): Promise<S3StorageDriver | null> {\n const { resolve } = await createRequestContainer()\n const credentialsService = resolve('integrationCredentialsService') as {\n resolve(integrationId: string, scope: { tenantId: string; organizationId: string }): Promise<Record<string, unknown> | null>\n }\n const creds = await credentialsService.resolve('storage_s3', { tenantId, organizationId: orgId })\n if (!creds) return null\n return new S3StorageDriver(creds)\n}\n\nfunction isKeyScoped(key: string, orgId: string, tenantId: string): boolean {\n const parts = key.split('/')\n return parts.length >= 3 && parts[1] === `org_${orgId}` && parts[2] === `tenant_${tenantId}`\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const json = await req.json().catch(() => null)\n const parsed = requestSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n\n if (!isKeyScoped(parsed.data.key, auth.orgId, auth.tenantId)) {\n return NextResponse.json({ error: 'Access denied: key is not scoped to this tenant.' }, { status: 403 })\n }\n\n const driver = await resolveDriver(auth.tenantId, auth.orgId)\n if (!driver) {\n return NextResponse.json({ error: 'S3 integration is not configured.' }, { status: 400 })\n }\n\n const { key, operation, expiresIn, contentType } = parsed.data\n const url = await driver.getSignedUrl(key, operation, expiresIn, contentType)\n const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString()\n\n return NextResponse.json({ url, expiresAt })\n}\n\nexport default POST\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Storage',\n summary: 'Generate S3 pre-signed URL',\n methods: {\n POST: {\n summary: 'Generate a pre-signed URL for direct browser upload or download',\n description: 'Returns a time-limited URL that allows a browser to directly upload or download a file from S3.',\n requestBody: { contentType: 'application/json', schema: requestSchema },\n responses: [{ status: 200, description: 'Pre-signed URL and expiry', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload or S3 not configured', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Key not scoped to this tenant', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,99 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { S3StorageDriver } from "../../../../lib/s3-driver.js";
6
+ import { randomUUID } from "crypto";
7
+ const metadata = {
8
+ path: "/storage-providers/s3/upload",
9
+ POST: { requireAuth: true, requireFeatures: ["storage_providers.manage"] }
10
+ };
11
+ const responseSchema = z.object({
12
+ key: z.string(),
13
+ bucket: z.string(),
14
+ size: z.number().int(),
15
+ contentType: z.string().optional()
16
+ });
17
+ function sanitizeFileName(name) {
18
+ return name.replace(/[^a-zA-Z0-9._-]/g, "_") || "upload";
19
+ }
20
+ function isKeyScoped(key, orgId, tenantId) {
21
+ const parts = key.split("/");
22
+ return parts.length >= 3 && parts[1] === `org_${orgId}` && parts[2] === `tenant_${tenantId}`;
23
+ }
24
+ async function resolveDriver(tenantId, orgId) {
25
+ const { resolve } = await createRequestContainer();
26
+ const credentialsService = resolve("integrationCredentialsService");
27
+ const creds = await credentialsService.resolve("storage_s3", { tenantId, organizationId: orgId });
28
+ if (!creds) return null;
29
+ return new S3StorageDriver(creds);
30
+ }
31
+ async function POST(req) {
32
+ const auth = await getAuthFromRequest(req);
33
+ if (!auth?.tenantId || !auth.orgId) {
34
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
35
+ }
36
+ const contentType = req.headers.get("content-type") ?? "";
37
+ if (!contentType.includes("multipart/form-data")) {
38
+ return NextResponse.json({ error: "Expected multipart/form-data" }, { status: 400 });
39
+ }
40
+ const form = await req.formData();
41
+ const file = form.get("file");
42
+ if (!file) {
43
+ return NextResponse.json({ error: "file field is required" }, { status: 400 });
44
+ }
45
+ const keyOverride = form.get("key") ? String(form.get("key")) : null;
46
+ const contentTypeHeader = form.get("contentType") ? String(form.get("contentType")) : file.type || void 0;
47
+ if (keyOverride !== null && !isKeyScoped(keyOverride, auth.orgId, auth.tenantId)) {
48
+ return NextResponse.json(
49
+ { error: "Access denied: key override is not scoped to this tenant." },
50
+ { status: 403 }
51
+ );
52
+ }
53
+ const driver = await resolveDriver(auth.tenantId, auth.orgId);
54
+ if (!driver) {
55
+ return NextResponse.json({ error: "S3 integration is not configured." }, { status: 400 });
56
+ }
57
+ const buffer = Buffer.from(await file.arrayBuffer());
58
+ const safeName = sanitizeFileName(file.name);
59
+ const key = keyOverride ?? `uploads/org_${auth.orgId}/tenant_${auth.tenantId}/${Date.now()}_${randomUUID().slice(0, 8)}_${safeName}`;
60
+ await driver.putObject(key, buffer, contentTypeHeader);
61
+ return NextResponse.json({
62
+ key,
63
+ bucket: driver.getBucket(),
64
+ size: buffer.length,
65
+ contentType: contentTypeHeader
66
+ });
67
+ }
68
+ var upload_default = POST;
69
+ const openApi = {
70
+ tag: "Storage",
71
+ summary: "Upload file to S3",
72
+ methods: {
73
+ POST: {
74
+ summary: "Upload a file directly to S3",
75
+ description: "Uploads a file to the configured S3 bucket. Requires storage_providers.manage feature.",
76
+ requestBody: {
77
+ contentType: "multipart/form-data",
78
+ schema: z.object({
79
+ file: z.any().describe("File to upload"),
80
+ key: z.string().optional().describe("Optional S3 key override (must be scoped to org/tenant)"),
81
+ contentType: z.string().optional().describe("Optional content-type override")
82
+ })
83
+ },
84
+ responses: [{ status: 200, description: "Upload result", schema: responseSchema }],
85
+ errors: [
86
+ { status: 400, description: "Missing file or S3 not configured", schema: z.object({ error: z.string() }) },
87
+ { status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
88
+ { status: 403, description: "Key override not scoped to this tenant", schema: z.object({ error: z.string() }) }
89
+ ]
90
+ }
91
+ }
92
+ };
93
+ export {
94
+ POST,
95
+ upload_default as default,
96
+ metadata,
97
+ openApi
98
+ };
99
+ //# sourceMappingURL=upload.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/storage_s3/api/post/storage-providers/s3/upload.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { S3StorageDriver } from '../../../../lib/s3-driver'\nimport { randomUUID } from 'crypto'\n\nexport const metadata = {\n path: '/storage-providers/s3/upload',\n POST: { requireAuth: true, requireFeatures: ['storage_providers.manage'] },\n}\n\nconst responseSchema = z.object({\n key: z.string(),\n bucket: z.string(),\n size: z.number().int(),\n contentType: z.string().optional(),\n})\n\nfunction sanitizeFileName(name: string): string {\n return name.replace(/[^a-zA-Z0-9._-]/g, '_') || 'upload'\n}\n\nfunction isKeyScoped(key: string, orgId: string, tenantId: string): boolean {\n const parts = key.split('/')\n return parts.length >= 3 && parts[1] === `org_${orgId}` && parts[2] === `tenant_${tenantId}`\n}\n\nasync function resolveDriver(\n tenantId: string,\n orgId: string,\n): Promise<S3StorageDriver | null> {\n const { resolve } = await createRequestContainer()\n const credentialsService = resolve('integrationCredentialsService') as {\n resolve(integrationId: string, scope: { tenantId: string; organizationId: string }): Promise<Record<string, unknown> | null>\n }\n const creds = await credentialsService.resolve('storage_s3', { tenantId, organizationId: orgId })\n if (!creds) return null\n return new S3StorageDriver(creds)\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const contentType = req.headers.get('content-type') ?? ''\n if (!contentType.includes('multipart/form-data')) {\n return NextResponse.json({ error: 'Expected multipart/form-data' }, { status: 400 })\n }\n\n const form = await req.formData()\n const file = form.get('file') as File | null\n if (!file) {\n return NextResponse.json({ error: 'file field is required' }, { status: 400 })\n }\n\n const keyOverride = form.get('key') ? String(form.get('key')) : null\n const contentTypeHeader = form.get('contentType') ? String(form.get('contentType')) : file.type || undefined\n\n if (keyOverride !== null && !isKeyScoped(keyOverride, auth.orgId, auth.tenantId)) {\n return NextResponse.json(\n { error: 'Access denied: key override is not scoped to this tenant.' },\n { status: 403 },\n )\n }\n\n const driver = await resolveDriver(auth.tenantId, auth.orgId)\n if (!driver) {\n return NextResponse.json({ error: 'S3 integration is not configured.' }, { status: 400 })\n }\n\n const buffer = Buffer.from(await file.arrayBuffer())\n const safeName = sanitizeFileName(file.name)\n const key =\n keyOverride ??\n `uploads/org_${auth.orgId}/tenant_${auth.tenantId}/${Date.now()}_${randomUUID().slice(0, 8)}_${safeName}`\n\n await driver.putObject(key, buffer, contentTypeHeader)\n\n return NextResponse.json({\n key,\n bucket: driver.getBucket(),\n size: buffer.length,\n contentType: contentTypeHeader,\n })\n}\n\nexport default POST\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Storage',\n summary: 'Upload file to S3',\n methods: {\n POST: {\n summary: 'Upload a file directly to S3',\n description: 'Uploads a file to the configured S3 bucket. Requires storage_providers.manage feature.',\n requestBody: {\n contentType: 'multipart/form-data',\n schema: z.object({\n file: z.any().describe('File to upload'),\n key: z.string().optional().describe('Optional S3 key override (must be scoped to org/tenant)'),\n contentType: z.string().optional().describe('Optional content-type override'),\n }),\n },\n responses: [{ status: 200, description: 'Upload result', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Missing file or S3 not configured', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Key override not scoped to this tenant', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }