@bobbykim/manguito-cms-api 0.1.0
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/codegen/index.d.mts +7 -0
- package/dist/codegen/index.d.ts +7 -0
- package/dist/codegen/index.js +701 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/index.mjs +672 -0
- package/dist/codegen/index.mjs.map +1 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +3228 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3189 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime/index.d.mts +29 -0
- package/dist/runtime/index.d.ts +29 -0
- package/dist/runtime/index.js +118 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/index.mjs +86 -0
- package/dist/runtime/index.mjs.map +1 -0
- package/dist/storage/index.d.mts +25 -0
- package/dist/storage/index.d.ts +25 -0
- package/dist/storage/index.js +287 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +253 -0
- package/dist/storage/index.mjs.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/storage/index.ts","../../src/storage/adapters/local.ts","../../src/storage/adapters/s3.ts","../../src/storage/adapters/cloudinary.ts"],"sourcesContent":["export { createLocalAdapter } from './adapters/local'\nexport type { LocalAdapterOptions } from './adapters/local'\n\nexport { createS3Adapter } from './adapters/s3'\nexport type { S3AdapterOptions } from './adapters/s3'\n\nexport { createCloudinaryAdapter } from './adapters/cloudinary'\nexport type { CloudinaryAdapterOptions } from './adapters/cloudinary'\n","import { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport crypto from 'node:crypto'\nimport type {\n StorageAdapter,\n PresignedOptions,\n PresignedResult,\n} from '@bobbykim/manguito-cms-core'\n\nexport type LocalAdapterOptions = {\n upload_dir?: string\n}\n\ntype PendingUpload = {\n key: string\n expires_at: number\n folder: string\n}\n\nexport function createLocalAdapter(\n options: LocalAdapterOptions = {}\n): StorageAdapter {\n if (process.env['NODE_ENV'] === 'production') {\n console.warn(\n '⚠ Warning: Local storage adapter is not recommended for production.\\n' +\n ' Files will not persist across container restarts or serverless invocations.\\n' +\n ' Consider createS3Adapter() or createCloudinaryAdapter() for production.'\n )\n }\n\n const upload_dir = options.upload_dir ?? './uploads'\n const pending = new Map<string, PendingUpload>()\n\n return {\n type: 'local',\n\n async getPresignedUploadUrl(opts: PresignedOptions): Promise<PresignedResult> {\n const raw_ext = path.extname(opts.filename)\n const ext = raw_ext.startsWith('.') ? raw_ext.slice(1) : raw_ext\n const uuid = crypto.randomUUID()\n const key = ext ? `${opts.folder}/${uuid}.${ext}` : `${opts.folder}/${uuid}`\n const token = crypto.randomUUID()\n const expires_in = opts.expires_in ?? 3600\n const expires_at = Math.floor(Date.now() / 1000) + expires_in\n\n pending.set(token, { key, expires_at, folder: opts.folder })\n\n return {\n upload_url: `http://localhost:3000/_local_upload/${key}?token=${token}`,\n key,\n expires_at,\n }\n },\n\n getUrl(key: string): string {\n return `http://localhost:3000/uploads/${key}`\n },\n\n async upload(key: string, data: Uint8Array): Promise<void> {\n const filepath = path.join(upload_dir, key)\n await fs.mkdir(path.dirname(filepath), { recursive: true })\n await fs.writeFile(filepath, data)\n },\n\n async delete(key: string): Promise<void> {\n // Key may come in as 'uploads/image/uuid.jpg' when derived from URL pathname\n const normalizedKey = key.startsWith('uploads/') ? key.slice('uploads/'.length) : key\n const filepath = path.join(upload_dir, normalizedKey)\n try {\n await fs.unlink(filepath)\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err\n }\n }\n },\n\n async stat(key: string): Promise<{ size: number; content_type?: string } | null> {\n const filepath = path.join(upload_dir, key.startsWith('uploads/') ? key.slice('uploads/'.length) : key)\n try {\n const s = await fs.stat(filepath)\n return { size: s.size }\n } catch {\n return null\n }\n },\n }\n}\n","import path from 'node:path'\nimport crypto from 'node:crypto'\nimport {\n S3Client,\n PutObjectCommand,\n DeleteObjectCommand,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner'\nimport type {\n StorageAdapter,\n PresignedOptions,\n PresignedResult,\n} from '@bobbykim/manguito-cms-core'\n\nexport type S3AdapterOptions = {\n bucket: string\n region: string\n prefix?: string\n access_key_id?: string\n secret_access_key?: string\n}\n\nexport function createS3Adapter(options: S3AdapterOptions): StorageAdapter {\n const { bucket, region, prefix, access_key_id, secret_access_key } = options\n\n const client = new S3Client({\n region,\n // AWS SDK v3 (>= ~3.729) adds a CRC32 checksum to PutObject by default. For\n // presigned URLs that bakes the *empty-body* checksum into the signed URL,\n // so the browser's real upload fails the check with 403. Only checksum when\n // an operation actually requires it, which excludes presigned PUTs.\n requestChecksumCalculation: 'WHEN_REQUIRED',\n // Only override credentials when explicitly configured. Otherwise fall back\n // to the SDK's default provider chain, which resolves the Lambda/Fargate\n // execution role *including the AWS_SESSION_TOKEN that temporary credentials\n // require*. Hand-reading only AWS_ACCESS_KEY_ID/SECRET dropped the session\n // token, so presigned URLs 403'd on Lambda (works on Fargate, whose role\n // credentials don't arrive via those env vars).\n ...(access_key_id && secret_access_key\n ? { credentials: { accessKeyId: access_key_id, secretAccessKey: secret_access_key } }\n : {}),\n })\n\n return {\n type: 's3',\n\n async getPresignedUploadUrl(opts: PresignedOptions): Promise<PresignedResult> {\n const raw_ext = path.extname(opts.filename)\n const ext = raw_ext.startsWith('.') ? raw_ext.slice(1) : raw_ext\n const uuid = crypto.randomUUID()\n const segment = ext ? `${uuid}.${ext}` : uuid\n const parts = [prefix, opts.folder, segment].filter(Boolean) as string[]\n const key = parts.join('/')\n const expires_in = opts.expires_in ?? 3600\n const expires_at = Math.floor(Date.now() / 1000) + expires_in\n\n const command = new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n ContentType: opts.mime_type,\n })\n\n const upload_url = await getSignedUrl(client, command, { expiresIn: expires_in })\n\n return { upload_url, key, expires_at }\n },\n\n getUrl(key: string): string {\n return `https://${bucket}.s3.${region}.amazonaws.com/${key}`\n },\n\n async upload(key: string, data: Uint8Array, mimeType: string): Promise<void> {\n const command = new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: data,\n ContentType: mimeType,\n })\n await client.send(command)\n },\n\n async delete(key: string): Promise<void> {\n const command = new DeleteObjectCommand({ Bucket: bucket, Key: key })\n await client.send(command)\n },\n\n async stat(key: string): Promise<{ size: number; content_type?: string } | null> {\n try {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))\n return {\n size: head.ContentLength ?? 0,\n ...(head.ContentType !== undefined && { content_type: head.ContentType }),\n }\n } catch {\n return null\n }\n },\n\n getUploadOrigins(): string[] {\n return [`https://${bucket}.s3.${region}.amazonaws.com`]\n },\n }\n}\n","import crypto from 'node:crypto'\nimport type {\n StorageAdapter,\n PresignedOptions,\n PresignedResult,\n} from '@bobbykim/manguito-cms-core'\n\nexport type CloudinaryAdapterOptions = {\n cloud_name?: string\n folder?: string\n access_key_id?: string\n secret_access_key?: string\n}\n\nfunction toCloudinaryResourceType(folder: string): string {\n if (folder === 'video') return 'video'\n if (folder === 'file') return 'raw'\n return 'image'\n}\n\nfunction resourceTypeFromKey(key: string): string {\n for (const part of key.split('/')) {\n if (part === 'video') return 'video'\n if (part === 'file') return 'raw'\n }\n return 'image'\n}\n\nfunction signParams(params: Record<string, string | number>, secret: string): string {\n const to_sign = Object.keys(params)\n .sort()\n .map((k) => `${k}=${params[k]}`)\n .join('&')\n // Cloudinary's default signature algorithm is SHA-1 (accounts can opt into\n // SHA-256). Match the default so signed uploads/deletes verify out of the box.\n return crypto.createHash('sha1').update(to_sign + secret).digest('hex')\n}\n\nexport function createCloudinaryAdapter(\n options: CloudinaryAdapterOptions = {}\n): StorageAdapter {\n const cloud_name = options.cloud_name ?? process.env['CLOUDINARY_CLOUD_NAME'] ?? ''\n const folder_prefix = options.folder\n const access_key_id = options.access_key_id ?? process.env['CLOUDINARY_API_KEY'] ?? ''\n const secret_access_key =\n options.secret_access_key ?? process.env['CLOUDINARY_API_SECRET'] ?? ''\n\n return {\n type: 'cloudinary',\n\n async getPresignedUploadUrl(opts: PresignedOptions): Promise<PresignedResult> {\n const uuid = crypto.randomUUID()\n const public_id_parts = [folder_prefix, opts.folder, uuid].filter(Boolean) as string[]\n const public_id = public_id_parts.join('/')\n const key = public_id\n const expires_in = opts.expires_in ?? 3600\n const timestamp = Math.floor(Date.now() / 1000)\n const expires_at = timestamp + expires_in\n\n const params_to_sign: Record<string, string | number> = {\n public_id,\n timestamp,\n }\n\n const signature = signParams(params_to_sign, secret_access_key)\n const resource_type = toCloudinaryResourceType(opts.folder)\n const upload_url = `https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/upload`\n\n // Cloudinary uploads are a signed multipart POST — the client posts these\n // fields plus the file directly to Cloudinary (never through the server).\n return {\n upload_url,\n key,\n expires_at,\n method: 'POST',\n fields: {\n public_id,\n timestamp: String(timestamp),\n signature,\n api_key: access_key_id,\n },\n }\n },\n\n getUrl(key: string): string {\n const resource_type = resourceTypeFromKey(key)\n return `https://res.cloudinary.com/${cloud_name}/${resource_type}/upload/${key}`\n },\n\n async upload(key: string, data: Uint8Array, mimeType: string): Promise<void> {\n const timestamp = Math.floor(Date.now() / 1000)\n const params_to_sign: Record<string, string | number> = { public_id: key, timestamp }\n const signature = signParams(params_to_sign, secret_access_key)\n const resource_type = resourceTypeFromKey(key)\n\n const form = new FormData()\n form.append('file', new Blob([new Uint8Array(data)], { type: mimeType }), 'upload')\n form.append('public_id', key)\n form.append('timestamp', String(timestamp))\n form.append('api_key', access_key_id)\n form.append('signature', signature)\n\n const res = await fetch(\n `https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/upload`,\n { method: 'POST', body: form }\n )\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`Cloudinary upload failed: ${res.status} ${res.statusText} — ${text}`)\n }\n },\n\n async delete(key: string): Promise<void> {\n const timestamp = Math.floor(Date.now() / 1000)\n const params_to_sign: Record<string, string | number> = {\n public_id: key,\n timestamp,\n }\n const signature = signParams(params_to_sign, secret_access_key)\n\n const resource_type = resourceTypeFromKey(key)\n const endpoint = `https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/destroy`\n\n const body = new URLSearchParams({\n public_id: key,\n signature,\n timestamp: String(timestamp),\n api_key: access_key_id,\n })\n\n const res = await fetch(endpoint, {\n method: 'POST',\n body,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n })\n\n const json = await res.json() as { result?: string; error?: { message: string } }\n\n if (json.result === 'ok' || json.result === 'not found') return\n\n throw new Error(\n `Cloudinary delete failed: ${json.error?.message ?? json.result ?? res.statusText}`\n )\n },\n\n getUploadOrigins(): string[] {\n // Uploads POST to api.cloudinary.com (cloud_name is in the path, not the host).\n return ['https://api.cloudinary.com']\n },\n }\n}\n"],"mappings":"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","names":["path","crypto","fs","import_node_path","import_node_crypto","path","crypto","import_node_crypto","crypto"]}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// src/storage/adapters/local.ts
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
function createLocalAdapter(options = {}) {
|
|
6
|
+
if (process.env["NODE_ENV"] === "production") {
|
|
7
|
+
console.warn(
|
|
8
|
+
"\u26A0 Warning: Local storage adapter is not recommended for production.\n Files will not persist across container restarts or serverless invocations.\n Consider createS3Adapter() or createCloudinaryAdapter() for production."
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
const upload_dir = options.upload_dir ?? "./uploads";
|
|
12
|
+
const pending = /* @__PURE__ */ new Map();
|
|
13
|
+
return {
|
|
14
|
+
type: "local",
|
|
15
|
+
async getPresignedUploadUrl(opts) {
|
|
16
|
+
const raw_ext = path.extname(opts.filename);
|
|
17
|
+
const ext = raw_ext.startsWith(".") ? raw_ext.slice(1) : raw_ext;
|
|
18
|
+
const uuid = crypto.randomUUID();
|
|
19
|
+
const key = ext ? `${opts.folder}/${uuid}.${ext}` : `${opts.folder}/${uuid}`;
|
|
20
|
+
const token = crypto.randomUUID();
|
|
21
|
+
const expires_in = opts.expires_in ?? 3600;
|
|
22
|
+
const expires_at = Math.floor(Date.now() / 1e3) + expires_in;
|
|
23
|
+
pending.set(token, { key, expires_at, folder: opts.folder });
|
|
24
|
+
return {
|
|
25
|
+
upload_url: `http://localhost:3000/_local_upload/${key}?token=${token}`,
|
|
26
|
+
key,
|
|
27
|
+
expires_at
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
getUrl(key) {
|
|
31
|
+
return `http://localhost:3000/uploads/${key}`;
|
|
32
|
+
},
|
|
33
|
+
async upload(key, data) {
|
|
34
|
+
const filepath = path.join(upload_dir, key);
|
|
35
|
+
await fs.mkdir(path.dirname(filepath), { recursive: true });
|
|
36
|
+
await fs.writeFile(filepath, data);
|
|
37
|
+
},
|
|
38
|
+
async delete(key) {
|
|
39
|
+
const normalizedKey = key.startsWith("uploads/") ? key.slice("uploads/".length) : key;
|
|
40
|
+
const filepath = path.join(upload_dir, normalizedKey);
|
|
41
|
+
try {
|
|
42
|
+
await fs.unlink(filepath);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.code !== "ENOENT") {
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
async stat(key) {
|
|
50
|
+
const filepath = path.join(upload_dir, key.startsWith("uploads/") ? key.slice("uploads/".length) : key);
|
|
51
|
+
try {
|
|
52
|
+
const s = await fs.stat(filepath);
|
|
53
|
+
return { size: s.size };
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/storage/adapters/s3.ts
|
|
62
|
+
import path2 from "path";
|
|
63
|
+
import crypto2 from "crypto";
|
|
64
|
+
import {
|
|
65
|
+
S3Client,
|
|
66
|
+
PutObjectCommand,
|
|
67
|
+
DeleteObjectCommand,
|
|
68
|
+
HeadObjectCommand
|
|
69
|
+
} from "@aws-sdk/client-s3";
|
|
70
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
71
|
+
function createS3Adapter(options) {
|
|
72
|
+
const { bucket, region, prefix, access_key_id, secret_access_key } = options;
|
|
73
|
+
const client = new S3Client({
|
|
74
|
+
region,
|
|
75
|
+
// AWS SDK v3 (>= ~3.729) adds a CRC32 checksum to PutObject by default. For
|
|
76
|
+
// presigned URLs that bakes the *empty-body* checksum into the signed URL,
|
|
77
|
+
// so the browser's real upload fails the check with 403. Only checksum when
|
|
78
|
+
// an operation actually requires it, which excludes presigned PUTs.
|
|
79
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
80
|
+
// Only override credentials when explicitly configured. Otherwise fall back
|
|
81
|
+
// to the SDK's default provider chain, which resolves the Lambda/Fargate
|
|
82
|
+
// execution role *including the AWS_SESSION_TOKEN that temporary credentials
|
|
83
|
+
// require*. Hand-reading only AWS_ACCESS_KEY_ID/SECRET dropped the session
|
|
84
|
+
// token, so presigned URLs 403'd on Lambda (works on Fargate, whose role
|
|
85
|
+
// credentials don't arrive via those env vars).
|
|
86
|
+
...access_key_id && secret_access_key ? { credentials: { accessKeyId: access_key_id, secretAccessKey: secret_access_key } } : {}
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
type: "s3",
|
|
90
|
+
async getPresignedUploadUrl(opts) {
|
|
91
|
+
const raw_ext = path2.extname(opts.filename);
|
|
92
|
+
const ext = raw_ext.startsWith(".") ? raw_ext.slice(1) : raw_ext;
|
|
93
|
+
const uuid = crypto2.randomUUID();
|
|
94
|
+
const segment = ext ? `${uuid}.${ext}` : uuid;
|
|
95
|
+
const parts = [prefix, opts.folder, segment].filter(Boolean);
|
|
96
|
+
const key = parts.join("/");
|
|
97
|
+
const expires_in = opts.expires_in ?? 3600;
|
|
98
|
+
const expires_at = Math.floor(Date.now() / 1e3) + expires_in;
|
|
99
|
+
const command = new PutObjectCommand({
|
|
100
|
+
Bucket: bucket,
|
|
101
|
+
Key: key,
|
|
102
|
+
ContentType: opts.mime_type
|
|
103
|
+
});
|
|
104
|
+
const upload_url = await getSignedUrl(client, command, { expiresIn: expires_in });
|
|
105
|
+
return { upload_url, key, expires_at };
|
|
106
|
+
},
|
|
107
|
+
getUrl(key) {
|
|
108
|
+
return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
109
|
+
},
|
|
110
|
+
async upload(key, data, mimeType) {
|
|
111
|
+
const command = new PutObjectCommand({
|
|
112
|
+
Bucket: bucket,
|
|
113
|
+
Key: key,
|
|
114
|
+
Body: data,
|
|
115
|
+
ContentType: mimeType
|
|
116
|
+
});
|
|
117
|
+
await client.send(command);
|
|
118
|
+
},
|
|
119
|
+
async delete(key) {
|
|
120
|
+
const command = new DeleteObjectCommand({ Bucket: bucket, Key: key });
|
|
121
|
+
await client.send(command);
|
|
122
|
+
},
|
|
123
|
+
async stat(key) {
|
|
124
|
+
try {
|
|
125
|
+
const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
126
|
+
return {
|
|
127
|
+
size: head.ContentLength ?? 0,
|
|
128
|
+
...head.ContentType !== void 0 && { content_type: head.ContentType }
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
getUploadOrigins() {
|
|
135
|
+
return [`https://${bucket}.s3.${region}.amazonaws.com`];
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/storage/adapters/cloudinary.ts
|
|
141
|
+
import crypto3 from "crypto";
|
|
142
|
+
function toCloudinaryResourceType(folder) {
|
|
143
|
+
if (folder === "video") return "video";
|
|
144
|
+
if (folder === "file") return "raw";
|
|
145
|
+
return "image";
|
|
146
|
+
}
|
|
147
|
+
function resourceTypeFromKey(key) {
|
|
148
|
+
for (const part of key.split("/")) {
|
|
149
|
+
if (part === "video") return "video";
|
|
150
|
+
if (part === "file") return "raw";
|
|
151
|
+
}
|
|
152
|
+
return "image";
|
|
153
|
+
}
|
|
154
|
+
function signParams(params, secret) {
|
|
155
|
+
const to_sign = Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
156
|
+
return crypto3.createHash("sha1").update(to_sign + secret).digest("hex");
|
|
157
|
+
}
|
|
158
|
+
function createCloudinaryAdapter(options = {}) {
|
|
159
|
+
const cloud_name = options.cloud_name ?? process.env["CLOUDINARY_CLOUD_NAME"] ?? "";
|
|
160
|
+
const folder_prefix = options.folder;
|
|
161
|
+
const access_key_id = options.access_key_id ?? process.env["CLOUDINARY_API_KEY"] ?? "";
|
|
162
|
+
const secret_access_key = options.secret_access_key ?? process.env["CLOUDINARY_API_SECRET"] ?? "";
|
|
163
|
+
return {
|
|
164
|
+
type: "cloudinary",
|
|
165
|
+
async getPresignedUploadUrl(opts) {
|
|
166
|
+
const uuid = crypto3.randomUUID();
|
|
167
|
+
const public_id_parts = [folder_prefix, opts.folder, uuid].filter(Boolean);
|
|
168
|
+
const public_id = public_id_parts.join("/");
|
|
169
|
+
const key = public_id;
|
|
170
|
+
const expires_in = opts.expires_in ?? 3600;
|
|
171
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
172
|
+
const expires_at = timestamp + expires_in;
|
|
173
|
+
const params_to_sign = {
|
|
174
|
+
public_id,
|
|
175
|
+
timestamp
|
|
176
|
+
};
|
|
177
|
+
const signature = signParams(params_to_sign, secret_access_key);
|
|
178
|
+
const resource_type = toCloudinaryResourceType(opts.folder);
|
|
179
|
+
const upload_url = `https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/upload`;
|
|
180
|
+
return {
|
|
181
|
+
upload_url,
|
|
182
|
+
key,
|
|
183
|
+
expires_at,
|
|
184
|
+
method: "POST",
|
|
185
|
+
fields: {
|
|
186
|
+
public_id,
|
|
187
|
+
timestamp: String(timestamp),
|
|
188
|
+
signature,
|
|
189
|
+
api_key: access_key_id
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
getUrl(key) {
|
|
194
|
+
const resource_type = resourceTypeFromKey(key);
|
|
195
|
+
return `https://res.cloudinary.com/${cloud_name}/${resource_type}/upload/${key}`;
|
|
196
|
+
},
|
|
197
|
+
async upload(key, data, mimeType) {
|
|
198
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
199
|
+
const params_to_sign = { public_id: key, timestamp };
|
|
200
|
+
const signature = signParams(params_to_sign, secret_access_key);
|
|
201
|
+
const resource_type = resourceTypeFromKey(key);
|
|
202
|
+
const form = new FormData();
|
|
203
|
+
form.append("file", new Blob([new Uint8Array(data)], { type: mimeType }), "upload");
|
|
204
|
+
form.append("public_id", key);
|
|
205
|
+
form.append("timestamp", String(timestamp));
|
|
206
|
+
form.append("api_key", access_key_id);
|
|
207
|
+
form.append("signature", signature);
|
|
208
|
+
const res = await fetch(
|
|
209
|
+
`https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/upload`,
|
|
210
|
+
{ method: "POST", body: form }
|
|
211
|
+
);
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
const text = await res.text();
|
|
214
|
+
throw new Error(`Cloudinary upload failed: ${res.status} ${res.statusText} \u2014 ${text}`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
async delete(key) {
|
|
218
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
219
|
+
const params_to_sign = {
|
|
220
|
+
public_id: key,
|
|
221
|
+
timestamp
|
|
222
|
+
};
|
|
223
|
+
const signature = signParams(params_to_sign, secret_access_key);
|
|
224
|
+
const resource_type = resourceTypeFromKey(key);
|
|
225
|
+
const endpoint = `https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/destroy`;
|
|
226
|
+
const body = new URLSearchParams({
|
|
227
|
+
public_id: key,
|
|
228
|
+
signature,
|
|
229
|
+
timestamp: String(timestamp),
|
|
230
|
+
api_key: access_key_id
|
|
231
|
+
});
|
|
232
|
+
const res = await fetch(endpoint, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
body,
|
|
235
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" }
|
|
236
|
+
});
|
|
237
|
+
const json = await res.json();
|
|
238
|
+
if (json.result === "ok" || json.result === "not found") return;
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Cloudinary delete failed: ${json.error?.message ?? json.result ?? res.statusText}`
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
getUploadOrigins() {
|
|
244
|
+
return ["https://api.cloudinary.com"];
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
export {
|
|
249
|
+
createCloudinaryAdapter,
|
|
250
|
+
createLocalAdapter,
|
|
251
|
+
createS3Adapter
|
|
252
|
+
};
|
|
253
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/storage/adapters/local.ts","../../src/storage/adapters/s3.ts","../../src/storage/adapters/cloudinary.ts"],"sourcesContent":["import { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport crypto from 'node:crypto'\nimport type {\n StorageAdapter,\n PresignedOptions,\n PresignedResult,\n} from '@bobbykim/manguito-cms-core'\n\nexport type LocalAdapterOptions = {\n upload_dir?: string\n}\n\ntype PendingUpload = {\n key: string\n expires_at: number\n folder: string\n}\n\nexport function createLocalAdapter(\n options: LocalAdapterOptions = {}\n): StorageAdapter {\n if (process.env['NODE_ENV'] === 'production') {\n console.warn(\n '⚠ Warning: Local storage adapter is not recommended for production.\\n' +\n ' Files will not persist across container restarts or serverless invocations.\\n' +\n ' Consider createS3Adapter() or createCloudinaryAdapter() for production.'\n )\n }\n\n const upload_dir = options.upload_dir ?? './uploads'\n const pending = new Map<string, PendingUpload>()\n\n return {\n type: 'local',\n\n async getPresignedUploadUrl(opts: PresignedOptions): Promise<PresignedResult> {\n const raw_ext = path.extname(opts.filename)\n const ext = raw_ext.startsWith('.') ? raw_ext.slice(1) : raw_ext\n const uuid = crypto.randomUUID()\n const key = ext ? `${opts.folder}/${uuid}.${ext}` : `${opts.folder}/${uuid}`\n const token = crypto.randomUUID()\n const expires_in = opts.expires_in ?? 3600\n const expires_at = Math.floor(Date.now() / 1000) + expires_in\n\n pending.set(token, { key, expires_at, folder: opts.folder })\n\n return {\n upload_url: `http://localhost:3000/_local_upload/${key}?token=${token}`,\n key,\n expires_at,\n }\n },\n\n getUrl(key: string): string {\n return `http://localhost:3000/uploads/${key}`\n },\n\n async upload(key: string, data: Uint8Array): Promise<void> {\n const filepath = path.join(upload_dir, key)\n await fs.mkdir(path.dirname(filepath), { recursive: true })\n await fs.writeFile(filepath, data)\n },\n\n async delete(key: string): Promise<void> {\n // Key may come in as 'uploads/image/uuid.jpg' when derived from URL pathname\n const normalizedKey = key.startsWith('uploads/') ? key.slice('uploads/'.length) : key\n const filepath = path.join(upload_dir, normalizedKey)\n try {\n await fs.unlink(filepath)\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err\n }\n }\n },\n\n async stat(key: string): Promise<{ size: number; content_type?: string } | null> {\n const filepath = path.join(upload_dir, key.startsWith('uploads/') ? key.slice('uploads/'.length) : key)\n try {\n const s = await fs.stat(filepath)\n return { size: s.size }\n } catch {\n return null\n }\n },\n }\n}\n","import path from 'node:path'\nimport crypto from 'node:crypto'\nimport {\n S3Client,\n PutObjectCommand,\n DeleteObjectCommand,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner'\nimport type {\n StorageAdapter,\n PresignedOptions,\n PresignedResult,\n} from '@bobbykim/manguito-cms-core'\n\nexport type S3AdapterOptions = {\n bucket: string\n region: string\n prefix?: string\n access_key_id?: string\n secret_access_key?: string\n}\n\nexport function createS3Adapter(options: S3AdapterOptions): StorageAdapter {\n const { bucket, region, prefix, access_key_id, secret_access_key } = options\n\n const client = new S3Client({\n region,\n // AWS SDK v3 (>= ~3.729) adds a CRC32 checksum to PutObject by default. For\n // presigned URLs that bakes the *empty-body* checksum into the signed URL,\n // so the browser's real upload fails the check with 403. Only checksum when\n // an operation actually requires it, which excludes presigned PUTs.\n requestChecksumCalculation: 'WHEN_REQUIRED',\n // Only override credentials when explicitly configured. Otherwise fall back\n // to the SDK's default provider chain, which resolves the Lambda/Fargate\n // execution role *including the AWS_SESSION_TOKEN that temporary credentials\n // require*. Hand-reading only AWS_ACCESS_KEY_ID/SECRET dropped the session\n // token, so presigned URLs 403'd on Lambda (works on Fargate, whose role\n // credentials don't arrive via those env vars).\n ...(access_key_id && secret_access_key\n ? { credentials: { accessKeyId: access_key_id, secretAccessKey: secret_access_key } }\n : {}),\n })\n\n return {\n type: 's3',\n\n async getPresignedUploadUrl(opts: PresignedOptions): Promise<PresignedResult> {\n const raw_ext = path.extname(opts.filename)\n const ext = raw_ext.startsWith('.') ? raw_ext.slice(1) : raw_ext\n const uuid = crypto.randomUUID()\n const segment = ext ? `${uuid}.${ext}` : uuid\n const parts = [prefix, opts.folder, segment].filter(Boolean) as string[]\n const key = parts.join('/')\n const expires_in = opts.expires_in ?? 3600\n const expires_at = Math.floor(Date.now() / 1000) + expires_in\n\n const command = new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n ContentType: opts.mime_type,\n })\n\n const upload_url = await getSignedUrl(client, command, { expiresIn: expires_in })\n\n return { upload_url, key, expires_at }\n },\n\n getUrl(key: string): string {\n return `https://${bucket}.s3.${region}.amazonaws.com/${key}`\n },\n\n async upload(key: string, data: Uint8Array, mimeType: string): Promise<void> {\n const command = new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: data,\n ContentType: mimeType,\n })\n await client.send(command)\n },\n\n async delete(key: string): Promise<void> {\n const command = new DeleteObjectCommand({ Bucket: bucket, Key: key })\n await client.send(command)\n },\n\n async stat(key: string): Promise<{ size: number; content_type?: string } | null> {\n try {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))\n return {\n size: head.ContentLength ?? 0,\n ...(head.ContentType !== undefined && { content_type: head.ContentType }),\n }\n } catch {\n return null\n }\n },\n\n getUploadOrigins(): string[] {\n return [`https://${bucket}.s3.${region}.amazonaws.com`]\n },\n }\n}\n","import crypto from 'node:crypto'\nimport type {\n StorageAdapter,\n PresignedOptions,\n PresignedResult,\n} from '@bobbykim/manguito-cms-core'\n\nexport type CloudinaryAdapterOptions = {\n cloud_name?: string\n folder?: string\n access_key_id?: string\n secret_access_key?: string\n}\n\nfunction toCloudinaryResourceType(folder: string): string {\n if (folder === 'video') return 'video'\n if (folder === 'file') return 'raw'\n return 'image'\n}\n\nfunction resourceTypeFromKey(key: string): string {\n for (const part of key.split('/')) {\n if (part === 'video') return 'video'\n if (part === 'file') return 'raw'\n }\n return 'image'\n}\n\nfunction signParams(params: Record<string, string | number>, secret: string): string {\n const to_sign = Object.keys(params)\n .sort()\n .map((k) => `${k}=${params[k]}`)\n .join('&')\n // Cloudinary's default signature algorithm is SHA-1 (accounts can opt into\n // SHA-256). Match the default so signed uploads/deletes verify out of the box.\n return crypto.createHash('sha1').update(to_sign + secret).digest('hex')\n}\n\nexport function createCloudinaryAdapter(\n options: CloudinaryAdapterOptions = {}\n): StorageAdapter {\n const cloud_name = options.cloud_name ?? process.env['CLOUDINARY_CLOUD_NAME'] ?? ''\n const folder_prefix = options.folder\n const access_key_id = options.access_key_id ?? process.env['CLOUDINARY_API_KEY'] ?? ''\n const secret_access_key =\n options.secret_access_key ?? process.env['CLOUDINARY_API_SECRET'] ?? ''\n\n return {\n type: 'cloudinary',\n\n async getPresignedUploadUrl(opts: PresignedOptions): Promise<PresignedResult> {\n const uuid = crypto.randomUUID()\n const public_id_parts = [folder_prefix, opts.folder, uuid].filter(Boolean) as string[]\n const public_id = public_id_parts.join('/')\n const key = public_id\n const expires_in = opts.expires_in ?? 3600\n const timestamp = Math.floor(Date.now() / 1000)\n const expires_at = timestamp + expires_in\n\n const params_to_sign: Record<string, string | number> = {\n public_id,\n timestamp,\n }\n\n const signature = signParams(params_to_sign, secret_access_key)\n const resource_type = toCloudinaryResourceType(opts.folder)\n const upload_url = `https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/upload`\n\n // Cloudinary uploads are a signed multipart POST — the client posts these\n // fields plus the file directly to Cloudinary (never through the server).\n return {\n upload_url,\n key,\n expires_at,\n method: 'POST',\n fields: {\n public_id,\n timestamp: String(timestamp),\n signature,\n api_key: access_key_id,\n },\n }\n },\n\n getUrl(key: string): string {\n const resource_type = resourceTypeFromKey(key)\n return `https://res.cloudinary.com/${cloud_name}/${resource_type}/upload/${key}`\n },\n\n async upload(key: string, data: Uint8Array, mimeType: string): Promise<void> {\n const timestamp = Math.floor(Date.now() / 1000)\n const params_to_sign: Record<string, string | number> = { public_id: key, timestamp }\n const signature = signParams(params_to_sign, secret_access_key)\n const resource_type = resourceTypeFromKey(key)\n\n const form = new FormData()\n form.append('file', new Blob([new Uint8Array(data)], { type: mimeType }), 'upload')\n form.append('public_id', key)\n form.append('timestamp', String(timestamp))\n form.append('api_key', access_key_id)\n form.append('signature', signature)\n\n const res = await fetch(\n `https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/upload`,\n { method: 'POST', body: form }\n )\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`Cloudinary upload failed: ${res.status} ${res.statusText} — ${text}`)\n }\n },\n\n async delete(key: string): Promise<void> {\n const timestamp = Math.floor(Date.now() / 1000)\n const params_to_sign: Record<string, string | number> = {\n public_id: key,\n timestamp,\n }\n const signature = signParams(params_to_sign, secret_access_key)\n\n const resource_type = resourceTypeFromKey(key)\n const endpoint = `https://api.cloudinary.com/v1_1/${cloud_name}/${resource_type}/destroy`\n\n const body = new URLSearchParams({\n public_id: key,\n signature,\n timestamp: String(timestamp),\n api_key: access_key_id,\n })\n\n const res = await fetch(endpoint, {\n method: 'POST',\n body,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n })\n\n const json = await res.json() as { result?: string; error?: { message: string } }\n\n if (json.result === 'ok' || json.result === 'not found') return\n\n throw new Error(\n `Cloudinary delete failed: ${json.error?.message ?? json.result ?? res.statusText}`\n )\n },\n\n getUploadOrigins(): string[] {\n // Uploads POST to api.cloudinary.com (cloud_name is in the path, not the host).\n return ['https://api.cloudinary.com']\n },\n }\n}\n"],"mappings":";AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,OAAO,YAAY;AAiBZ,SAAS,mBACd,UAA+B,CAAC,GAChB;AAChB,MAAI,QAAQ,IAAI,UAAU,MAAM,cAAc;AAC5C,YAAQ;AAAA,MACN;AAAA,IAGF;AAAA,EACF;AAEA,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,UAAU,oBAAI,IAA2B;AAE/C,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,MAAM,sBAAsB,MAAkD;AAC5E,YAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ;AAC1C,YAAM,MAAM,QAAQ,WAAW,GAAG,IAAI,QAAQ,MAAM,CAAC,IAAI;AACzD,YAAM,OAAO,OAAO,WAAW;AAC/B,YAAM,MAAM,MAAM,GAAG,KAAK,MAAM,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,KAAK,MAAM,IAAI,IAAI;AAC1E,YAAM,QAAQ,OAAO,WAAW;AAChC,YAAM,aAAa,KAAK,cAAc;AACtC,YAAM,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI;AAEnD,cAAQ,IAAI,OAAO,EAAE,KAAK,YAAY,QAAQ,KAAK,OAAO,CAAC;AAE3D,aAAO;AAAA,QACL,YAAY,uCAAuC,GAAG,UAAU,KAAK;AAAA,QACrE;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IAEA,OAAO,KAAqB;AAC1B,aAAO,iCAAiC,GAAG;AAAA,IAC7C;AAAA,IAEA,MAAM,OAAO,KAAa,MAAiC;AACzD,YAAM,WAAW,KAAK,KAAK,YAAY,GAAG;AAC1C,YAAM,GAAG,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,YAAM,GAAG,UAAU,UAAU,IAAI;AAAA,IACnC;AAAA,IAEA,MAAM,OAAO,KAA4B;AAEvC,YAAM,gBAAgB,IAAI,WAAW,UAAU,IAAI,IAAI,MAAM,WAAW,MAAM,IAAI;AAClF,YAAM,WAAW,KAAK,KAAK,YAAY,aAAa;AACpD,UAAI;AACF,cAAM,GAAG,OAAO,QAAQ;AAAA,MAC1B,SAAS,KAAc;AACrB,YAAK,IAA8B,SAAS,UAAU;AACpD,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,KAAK,KAAsE;AAC/E,YAAM,WAAW,KAAK,KAAK,YAAY,IAAI,WAAW,UAAU,IAAI,IAAI,MAAM,WAAW,MAAM,IAAI,GAAG;AACtG,UAAI;AACF,cAAM,IAAI,MAAM,GAAG,KAAK,QAAQ;AAChC,eAAO,EAAE,MAAM,EAAE,KAAK;AAAA,MACxB,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;;;ACvFA,OAAOA,WAAU;AACjB,OAAOC,aAAY;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAetB,SAAS,gBAAgB,SAA2C;AACzE,QAAM,EAAE,QAAQ,QAAQ,QAAQ,eAAe,kBAAkB,IAAI;AAErE,QAAM,SAAS,IAAI,SAAS;AAAA,IAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,4BAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAO5B,GAAI,iBAAiB,oBACjB,EAAE,aAAa,EAAE,aAAa,eAAe,iBAAiB,kBAAkB,EAAE,IAClF,CAAC;AAAA,EACP,CAAC;AAED,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,MAAM,sBAAsB,MAAkD;AAC5E,YAAM,UAAUD,MAAK,QAAQ,KAAK,QAAQ;AAC1C,YAAM,MAAM,QAAQ,WAAW,GAAG,IAAI,QAAQ,MAAM,CAAC,IAAI;AACzD,YAAM,OAAOC,QAAO,WAAW;AAC/B,YAAM,UAAU,MAAM,GAAG,IAAI,IAAI,GAAG,KAAK;AACzC,YAAM,QAAQ,CAAC,QAAQ,KAAK,QAAQ,OAAO,EAAE,OAAO,OAAO;AAC3D,YAAM,MAAM,MAAM,KAAK,GAAG;AAC1B,YAAM,aAAa,KAAK,cAAc;AACtC,YAAM,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI;AAEnD,YAAM,UAAU,IAAI,iBAAiB;AAAA,QACnC,QAAQ;AAAA,QACR,KAAK;AAAA,QACL,aAAa,KAAK;AAAA,MACpB,CAAC;AAED,YAAM,aAAa,MAAM,aAAa,QAAQ,SAAS,EAAE,WAAW,WAAW,CAAC;AAEhF,aAAO,EAAE,YAAY,KAAK,WAAW;AAAA,IACvC;AAAA,IAEA,OAAO,KAAqB;AAC1B,aAAO,WAAW,MAAM,OAAO,MAAM,kBAAkB,GAAG;AAAA,IAC5D;AAAA,IAEA,MAAM,OAAO,KAAa,MAAkB,UAAiC;AAC3E,YAAM,UAAU,IAAI,iBAAiB;AAAA,QACnC,QAAQ;AAAA,QACR,KAAK;AAAA,QACL,MAAM;AAAA,QACN,aAAa;AAAA,MACf,CAAC;AACD,YAAM,OAAO,KAAK,OAAO;AAAA,IAC3B;AAAA,IAEA,MAAM,OAAO,KAA4B;AACvC,YAAM,UAAU,IAAI,oBAAoB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC;AACpE,YAAM,OAAO,KAAK,OAAO;AAAA,IAC3B;AAAA,IAEA,MAAM,KAAK,KAAsE;AAC/E,UAAI;AACF,cAAM,OAAO,MAAM,OAAO,KAAK,IAAI,kBAAkB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AAClF,eAAO;AAAA,UACL,MAAM,KAAK,iBAAiB;AAAA,UAC5B,GAAI,KAAK,gBAAgB,UAAa,EAAE,cAAc,KAAK,YAAY;AAAA,QACzE;AAAA,MACF,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,mBAA6B;AAC3B,aAAO,CAAC,WAAW,MAAM,OAAO,MAAM,gBAAgB;AAAA,IACxD;AAAA,EACF;AACF;;;ACvGA,OAAOC,aAAY;AAcnB,SAAS,yBAAyB,QAAwB;AACxD,MAAI,WAAW,QAAS,QAAO;AAC/B,MAAI,WAAW,OAAQ,QAAO;AAC9B,SAAO;AACT;AAEA,SAAS,oBAAoB,KAAqB;AAChD,aAAW,QAAQ,IAAI,MAAM,GAAG,GAAG;AACjC,QAAI,SAAS,QAAS,QAAO;AAC7B,QAAI,SAAS,OAAQ,QAAO;AAAA,EAC9B;AACA,SAAO;AACT;AAEA,SAAS,WAAW,QAAyC,QAAwB;AACnF,QAAM,UAAU,OAAO,KAAK,MAAM,EAC/B,KAAK,EACL,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,EAC9B,KAAK,GAAG;AAGX,SAAOA,QAAO,WAAW,MAAM,EAAE,OAAO,UAAU,MAAM,EAAE,OAAO,KAAK;AACxE;AAEO,SAAS,wBACd,UAAoC,CAAC,GACrB;AAChB,QAAM,aAAa,QAAQ,cAAc,QAAQ,IAAI,uBAAuB,KAAK;AACjF,QAAM,gBAAgB,QAAQ;AAC9B,QAAM,gBAAgB,QAAQ,iBAAiB,QAAQ,IAAI,oBAAoB,KAAK;AACpF,QAAM,oBACJ,QAAQ,qBAAqB,QAAQ,IAAI,uBAAuB,KAAK;AAEvE,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,MAAM,sBAAsB,MAAkD;AAC5E,YAAM,OAAOA,QAAO,WAAW;AAC/B,YAAM,kBAAkB,CAAC,eAAe,KAAK,QAAQ,IAAI,EAAE,OAAO,OAAO;AACzE,YAAM,YAAY,gBAAgB,KAAK,GAAG;AAC1C,YAAM,MAAM;AACZ,YAAM,aAAa,KAAK,cAAc;AACtC,YAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC9C,YAAM,aAAa,YAAY;AAE/B,YAAM,iBAAkD;AAAA,QACtD;AAAA,QACA;AAAA,MACF;AAEA,YAAM,YAAY,WAAW,gBAAgB,iBAAiB;AAC9D,YAAM,gBAAgB,yBAAyB,KAAK,MAAM;AAC1D,YAAM,aAAa,mCAAmC,UAAU,IAAI,aAAa;AAIjF,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ;AAAA,UACN;AAAA,UACA,WAAW,OAAO,SAAS;AAAA,UAC3B;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,IAEA,OAAO,KAAqB;AAC1B,YAAM,gBAAgB,oBAAoB,GAAG;AAC7C,aAAO,8BAA8B,UAAU,IAAI,aAAa,WAAW,GAAG;AAAA,IAChF;AAAA,IAEA,MAAM,OAAO,KAAa,MAAkB,UAAiC;AAC3E,YAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC9C,YAAM,iBAAkD,EAAE,WAAW,KAAK,UAAU;AACpF,YAAM,YAAY,WAAW,gBAAgB,iBAAiB;AAC9D,YAAM,gBAAgB,oBAAoB,GAAG;AAE7C,YAAM,OAAO,IAAI,SAAS;AAC1B,WAAK,OAAO,QAAQ,IAAI,KAAK,CAAC,IAAI,WAAW,IAAI,CAAC,GAAG,EAAE,MAAM,SAAS,CAAC,GAAG,QAAQ;AAClF,WAAK,OAAO,aAAa,GAAG;AAC5B,WAAK,OAAO,aAAa,OAAO,SAAS,CAAC;AAC1C,WAAK,OAAO,WAAW,aAAa;AACpC,WAAK,OAAO,aAAa,SAAS;AAElC,YAAM,MAAM,MAAM;AAAA,QAChB,mCAAmC,UAAU,IAAI,aAAa;AAAA,QAC9D,EAAE,QAAQ,QAAQ,MAAM,KAAK;AAAA,MAC/B;AAEA,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,cAAM,IAAI,MAAM,6BAA6B,IAAI,MAAM,IAAI,IAAI,UAAU,WAAM,IAAI,EAAE;AAAA,MACvF;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAA4B;AACvC,YAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC9C,YAAM,iBAAkD;AAAA,QACtD,WAAW;AAAA,QACX;AAAA,MACF;AACA,YAAM,YAAY,WAAW,gBAAgB,iBAAiB;AAE9D,YAAM,gBAAgB,oBAAoB,GAAG;AAC7C,YAAM,WAAW,mCAAmC,UAAU,IAAI,aAAa;AAE/E,YAAM,OAAO,IAAI,gBAAgB;AAAA,QAC/B,WAAW;AAAA,QACX;AAAA,QACA,WAAW,OAAO,SAAS;AAAA,QAC3B,SAAS;AAAA,MACX,CAAC;AAED,YAAM,MAAM,MAAM,MAAM,UAAU;AAAA,QAChC,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MACjE,CAAC;AAED,YAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,UAAI,KAAK,WAAW,QAAQ,KAAK,WAAW,YAAa;AAEzD,YAAM,IAAI;AAAA,QACR,6BAA6B,KAAK,OAAO,WAAW,KAAK,UAAU,IAAI,UAAU;AAAA,MACnF;AAAA,IACF;AAAA,IAEA,mBAA6B;AAE3B,aAAO,CAAC,4BAA4B;AAAA,IACtC;AAAA,EACF;AACF;","names":["path","crypto","crypto"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bobbykim/manguito-cms-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=22.0.0"
|
|
6
|
+
},
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./storage": {
|
|
14
|
+
"types": "./dist/storage/index.d.ts",
|
|
15
|
+
"import": "./dist/storage/index.mjs",
|
|
16
|
+
"require": "./dist/storage/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./runtime": {
|
|
19
|
+
"types": "./dist/runtime/index.d.ts",
|
|
20
|
+
"import": "./dist/runtime/index.mjs",
|
|
21
|
+
"require": "./dist/runtime/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./codegen": {
|
|
24
|
+
"types": "./dist/codegen/index.d.ts",
|
|
25
|
+
"import": "./dist/codegen/index.mjs",
|
|
26
|
+
"require": "./dist/codegen/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"module": "./dist/index.mjs",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@aws-sdk/client-s3": "^3.1081.0",
|
|
37
|
+
"@aws-sdk/s3-request-presigner": "^3.1081.0",
|
|
38
|
+
"@hono/node-server": "^1.19.14",
|
|
39
|
+
"@hono/zod-openapi": "^1.4.0",
|
|
40
|
+
"bcryptjs": "^2.4.3",
|
|
41
|
+
"drizzle-orm": "^0.45.2",
|
|
42
|
+
"hono": "^4.12.28",
|
|
43
|
+
"zod": "^4.4.3",
|
|
44
|
+
"@bobbykim/manguito-cms-core": "0.1.0",
|
|
45
|
+
"@bobbykim/manguito-cms-db": "0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/bcryptjs": "^2.4.6",
|
|
49
|
+
"@types/node": "^25.9.5",
|
|
50
|
+
"dotenv-cli": "latest",
|
|
51
|
+
"tsup": "^8.5.1",
|
|
52
|
+
"typescript": "^6.0.3",
|
|
53
|
+
"vitest": "^4.1.10",
|
|
54
|
+
"@bobbykim/manguito-cms-test-utils": "0.0.1"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"dev": "tsup --watch",
|
|
59
|
+
"test": "dotenv -e .env.test -- vitest run",
|
|
60
|
+
"lint": "eslint src"
|
|
61
|
+
}
|
|
62
|
+
}
|