ossput 0.0.1
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/.cursor/skills/ossput/SKILL.md +87 -0
- package/.cursor/skills/ossput/examples.md +59 -0
- package/.cursor/skills/ossput/reference.md +78 -0
- package/.ossput.json.example +3 -0
- package/AGENTS.md +16 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/config.index.example.json +8 -0
- package/dist/batch-upload.d.ts +28 -0
- package/dist/batch-upload.js +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +253 -0
- package/dist/config-profiles.d.ts +45 -0
- package/dist/config-profiles.js +282 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +81 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/delete-object.d.ts +6 -0
- package/dist/delete-object.js +26 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +128 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +21 -0
- package/dist/key-builder.d.ts +1 -0
- package/dist/key-builder.js +13 -0
- package/dist/list-directories.d.ts +11 -0
- package/dist/list-directories.js +64 -0
- package/dist/list-format.d.ts +6 -0
- package/dist/list-format.js +57 -0
- package/dist/list-objects.d.ts +7 -0
- package/dist/list-objects.js +47 -0
- package/dist/mcp-annotations.d.ts +16 -0
- package/dist/mcp-annotations.js +16 -0
- package/dist/mcp-result.d.ts +14 -0
- package/dist/mcp-result.js +57 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +244 -0
- package/dist/object-key.d.ts +10 -0
- package/dist/object-key.js +46 -0
- package/dist/oss-client.d.ts +4 -0
- package/dist/oss-client.js +24 -0
- package/dist/profile-cli.d.ts +16 -0
- package/dist/profile-cli.js +191 -0
- package/dist/setup/connectivity.d.ts +2 -0
- package/dist/setup/connectivity.js +5 -0
- package/dist/setup/logo.d.ts +1 -0
- package/dist/setup/logo.js +30 -0
- package/dist/setup/mcp-registry.d.ts +17 -0
- package/dist/setup/mcp-registry.js +128 -0
- package/dist/setup/prompts.d.ts +24 -0
- package/dist/setup/prompts.js +410 -0
- package/dist/setup/run-setup.d.ts +9 -0
- package/dist/setup/run-setup.js +156 -0
- package/dist/setup/skill-install.d.ts +19 -0
- package/dist/setup/skill-install.js +85 -0
- package/dist/setup/ui.d.ts +49 -0
- package/dist/setup/ui.js +164 -0
- package/dist/setup/write-config.d.ts +5 -0
- package/dist/setup/write-config.js +5 -0
- package/dist/skill-cli.d.ts +6 -0
- package/dist/skill-cli.js +34 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +1 -0
- package/dist/upload-pipeline.d.ts +7 -0
- package/dist/upload-pipeline.js +96 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +77 -0
- package/docs/ram-policy.example.json +31 -0
- package/package.json +59 -0
- package/profiles.example/default.json +21 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { MAX_FILE_BYTES } from "./types.js";
|
|
3
|
+
import { createOssClient, publicObjectUrl } from "./oss-client.js";
|
|
4
|
+
import { buildObjectKey } from "./key-builder.js";
|
|
5
|
+
import { inferContentType, validateExtension, validateLocalFile, validateSubdir, } from "./validators.js";
|
|
6
|
+
const UPLOAD_TIMEOUT_MS = 600_000;
|
|
7
|
+
async function signPutUrl(config, objectKey, contentType) {
|
|
8
|
+
const client = createOssClient(config);
|
|
9
|
+
return client.asyncSignatureUrl(objectKey, {
|
|
10
|
+
method: "PUT",
|
|
11
|
+
expires: config.presignExpiresSec,
|
|
12
|
+
"Content-Type": contentType,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function prepareUpload(config, filename, contentType, subdir, overwrite = false) {
|
|
16
|
+
validateExtension(filename, config);
|
|
17
|
+
const normalizedSubdir = validateSubdir(subdir);
|
|
18
|
+
const objectKey = buildObjectKey(config.prefix, normalizedSubdir, filename, overwrite);
|
|
19
|
+
const uploadUrl = await signPutUrl(config, objectKey, contentType);
|
|
20
|
+
const expiresAt = new Date(Date.now() + config.presignExpiresSec * 1000).toISOString();
|
|
21
|
+
return {
|
|
22
|
+
bucket: config.bucket,
|
|
23
|
+
objectKey,
|
|
24
|
+
uploadUrl,
|
|
25
|
+
method: "PUT",
|
|
26
|
+
headers: { "Content-Type": contentType },
|
|
27
|
+
expiresAt,
|
|
28
|
+
maxSizeBytes: MAX_FILE_BYTES,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function formatUploadHttpError(status, body) {
|
|
32
|
+
const snippet = body.trim().slice(0, 300);
|
|
33
|
+
const detail = snippet ? `: ${snippet}` : "";
|
|
34
|
+
return `OSS upload failed (HTTP ${status})${detail}`;
|
|
35
|
+
}
|
|
36
|
+
/** Presigned PUT via Node fetch (no external curl). */
|
|
37
|
+
export async function putPresignedFile(uploadUrl, filePath, contentType) {
|
|
38
|
+
if (typeof globalThis.fetch !== "function") {
|
|
39
|
+
throw new Error("当前 Node 运行时无 fetch,请使用 Node.js 18 或更高版本");
|
|
40
|
+
}
|
|
41
|
+
const body = await readFile(filePath);
|
|
42
|
+
const response = await fetch(uploadUrl, {
|
|
43
|
+
method: "PUT",
|
|
44
|
+
headers: { "Content-Type": contentType },
|
|
45
|
+
body,
|
|
46
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const text = await response.text().catch(() => "");
|
|
50
|
+
throw new Error(formatUploadHttpError(response.status, text));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function confirmUpload(config, objectKey, expectedSizeBytes) {
|
|
54
|
+
const client = createOssClient(config);
|
|
55
|
+
try {
|
|
56
|
+
const head = await client.head(objectKey);
|
|
57
|
+
const headers = head.res.headers;
|
|
58
|
+
const size = Number(headers["content-length"] ?? 0);
|
|
59
|
+
if (expectedSizeBytes != null && size !== expectedSizeBytes) {
|
|
60
|
+
throw new Error(`size mismatch: expected ${expectedSizeBytes}, got ${size}`);
|
|
61
|
+
}
|
|
62
|
+
const etag = headers.etag != null ? String(headers.etag) : undefined;
|
|
63
|
+
const lastModified = headers["last-modified"] != null
|
|
64
|
+
? String(headers["last-modified"])
|
|
65
|
+
: undefined;
|
|
66
|
+
return {
|
|
67
|
+
exists: true,
|
|
68
|
+
size,
|
|
69
|
+
etag,
|
|
70
|
+
objectUrl: publicObjectUrl(config, objectKey),
|
|
71
|
+
lastModified,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const e = err;
|
|
76
|
+
if (e.code === "NoSuchKey" || e.status === 404) {
|
|
77
|
+
return {
|
|
78
|
+
exists: false,
|
|
79
|
+
size: 0,
|
|
80
|
+
objectUrl: publicObjectUrl(config, objectKey),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function uploadFile(config, localPath, subdir, contentType) {
|
|
87
|
+
const { absolutePath, size, filename } = await validateLocalFile(localPath);
|
|
88
|
+
const ct = contentType?.trim() || inferContentType(filename);
|
|
89
|
+
const prepared = await prepareUpload(config, filename, ct, subdir, false);
|
|
90
|
+
await putPresignedFile(prepared.uploadUrl, absolutePath, ct);
|
|
91
|
+
const confirmed = await confirmUpload(config, prepared.objectKey, size);
|
|
92
|
+
if (!confirmed.exists) {
|
|
93
|
+
throw new Error("upload completed but object not found on OSS");
|
|
94
|
+
}
|
|
95
|
+
return { objectKey: prepared.objectKey, ...confirmed };
|
|
96
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AppConfig } from "./types.js";
|
|
2
|
+
export declare function getExtension(filename: string): string;
|
|
3
|
+
export declare function inferContentType(filename: string): string;
|
|
4
|
+
export declare function validateSubdir(subdir?: string): string;
|
|
5
|
+
export declare function validateExtension(filename: string, config: AppConfig): void;
|
|
6
|
+
export declare function validateLocalFile(localPath: string): Promise<{
|
|
7
|
+
absolutePath: string;
|
|
8
|
+
size: number;
|
|
9
|
+
filename: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { basename, resolve, normalize } from "node:path";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { MAX_FILE_BYTES } from "./types.js";
|
|
4
|
+
const DENIED_SUBDIRS = ["release", "prod", "production"];
|
|
5
|
+
export function getExtension(filename) {
|
|
6
|
+
const base = basename(filename);
|
|
7
|
+
const dot = base.lastIndexOf(".");
|
|
8
|
+
if (dot <= 0)
|
|
9
|
+
return "";
|
|
10
|
+
return base.slice(dot + 1).toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
export function inferContentType(filename) {
|
|
13
|
+
const ext = getExtension(filename);
|
|
14
|
+
const map = {
|
|
15
|
+
png: "image/png",
|
|
16
|
+
jpg: "image/jpeg",
|
|
17
|
+
jpeg: "image/jpeg",
|
|
18
|
+
gif: "image/gif",
|
|
19
|
+
webp: "image/webp",
|
|
20
|
+
pdf: "application/pdf",
|
|
21
|
+
doc: "application/msword",
|
|
22
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
23
|
+
xls: "application/vnd.ms-excel",
|
|
24
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
25
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
26
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
27
|
+
mp4: "video/mp4",
|
|
28
|
+
mov: "video/quicktime",
|
|
29
|
+
zip: "application/zip",
|
|
30
|
+
};
|
|
31
|
+
return map[ext] ?? "application/octet-stream";
|
|
32
|
+
}
|
|
33
|
+
export function validateSubdir(subdir) {
|
|
34
|
+
if (!subdir?.trim())
|
|
35
|
+
return "";
|
|
36
|
+
const s = subdir.trim().replace(/^\/+|\/+$/g, "");
|
|
37
|
+
if (!s)
|
|
38
|
+
return "";
|
|
39
|
+
if (s.includes(".."))
|
|
40
|
+
throw new Error("subdir must not contain '..'");
|
|
41
|
+
if (!/^[a-zA-Z0-9_\-/]+$/.test(s)) {
|
|
42
|
+
throw new Error("subdir may only contain letters, numbers, _, -, and /");
|
|
43
|
+
}
|
|
44
|
+
for (const denied of DENIED_SUBDIRS) {
|
|
45
|
+
if (s === denied || s.startsWith(`${denied}/`)) {
|
|
46
|
+
throw new Error(`subdir '${denied}' is not allowed`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return s.endsWith("/") ? s : `${s}/`;
|
|
50
|
+
}
|
|
51
|
+
export function validateExtension(filename, config) {
|
|
52
|
+
const ext = getExtension(filename);
|
|
53
|
+
if (!ext)
|
|
54
|
+
throw new Error("file must have an extension");
|
|
55
|
+
const allowed = config.allowedExtensions.map((e) => e.toLowerCase());
|
|
56
|
+
if (!allowed.includes(ext)) {
|
|
57
|
+
throw new Error(`extension '.${ext}' not allowed. Allowed: ${allowed.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function validateLocalFile(localPath) {
|
|
61
|
+
const absolutePath = resolve(normalize(localPath));
|
|
62
|
+
const st = await stat(absolutePath);
|
|
63
|
+
if (!st.isFile())
|
|
64
|
+
throw new Error(`not a file: ${absolutePath}`);
|
|
65
|
+
if (st.size > MAX_FILE_BYTES) {
|
|
66
|
+
throw new Error(`file exceeds ${MAX_FILE_BYTES} bytes (100MB limit)`);
|
|
67
|
+
}
|
|
68
|
+
const filename = basename(absolutePath);
|
|
69
|
+
const lower = filename.toLowerCase();
|
|
70
|
+
if (lower === ".env" ||
|
|
71
|
+
lower.endsWith(".pem") ||
|
|
72
|
+
lower.endsWith(".key") ||
|
|
73
|
+
lower.includes("id_rsa")) {
|
|
74
|
+
throw new Error("refusing to upload sensitive-looking files");
|
|
75
|
+
}
|
|
76
|
+
return { absolutePath, size: st.size, filename };
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Version": "1",
|
|
3
|
+
"Statement": [
|
|
4
|
+
{
|
|
5
|
+
"Sid": "OssputListAndRead",
|
|
6
|
+
"Effect": "Allow",
|
|
7
|
+
"Action": [
|
|
8
|
+
"oss:ListObjects",
|
|
9
|
+
"oss:GetObject",
|
|
10
|
+
"oss:HeadObject"
|
|
11
|
+
],
|
|
12
|
+
"Resource": [
|
|
13
|
+
"acs:oss:*:*:your-bucket",
|
|
14
|
+
"acs:oss:*:*:your-bucket/your-prefix/*"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"Sid": "OssputWrite",
|
|
19
|
+
"Effect": "Allow",
|
|
20
|
+
"Action": ["oss:PutObject"],
|
|
21
|
+
"Resource": ["acs:oss:*:*:your-bucket/your-prefix/*"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"Sid": "OssputDeleteOptional",
|
|
25
|
+
"Effect": "Allow",
|
|
26
|
+
"Action": ["oss:DeleteObject"],
|
|
27
|
+
"Resource": ["acs:oss:*:*:your-bucket/your-prefix/*"],
|
|
28
|
+
"Comment": "仅当 profile 开启 allowDelete 且确需 delete_object 时授予"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ossput",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MCP server for Aliyun OSS presigned direct upload (Node fetch) — setup via CLI, upload via Agent or `ossput put`",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/devoink/ossput.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/devoink/ossput/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/devoink/ossput#readme",
|
|
15
|
+
"bin": {
|
|
16
|
+
"ossput": "dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"AGENTS.md",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
".cursor/skills/ossput",
|
|
23
|
+
"config.index.example.json",
|
|
24
|
+
"profiles.example",
|
|
25
|
+
".ossput.json.example",
|
|
26
|
+
"docs"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"test": "npm run build && node scripts/run-tests.mjs",
|
|
31
|
+
"prepublishOnly": "npm run build && npm test",
|
|
32
|
+
"start": "node dist/index.js",
|
|
33
|
+
"dev": "tsc --watch"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"cursor",
|
|
38
|
+
"aliyun",
|
|
39
|
+
"oss",
|
|
40
|
+
"upload",
|
|
41
|
+
"ossput",
|
|
42
|
+
"model-context-protocol"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@inquirer/prompts": "^7.4.0",
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
51
|
+
"ali-oss": "^6.22.0",
|
|
52
|
+
"zod": "^3.24.2"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/ali-oss": "^6.16.11",
|
|
56
|
+
"@types/node": "^22.13.10",
|
|
57
|
+
"typescript": "^5.8.2"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"region": "oss-cn-hangzhou",
|
|
3
|
+
"bucket": "your-bucket",
|
|
4
|
+
"prefix": "/",
|
|
5
|
+
"accessKeyId": "LTAI...",
|
|
6
|
+
"accessKeySecret": "...",
|
|
7
|
+
"presignExpiresSec": 900,
|
|
8
|
+
"allowedExtensions": [
|
|
9
|
+
"png",
|
|
10
|
+
"jpg",
|
|
11
|
+
"jpeg",
|
|
12
|
+
"gif",
|
|
13
|
+
"webp",
|
|
14
|
+
"pdf",
|
|
15
|
+
"mp4",
|
|
16
|
+
"zip"
|
|
17
|
+
],
|
|
18
|
+
"endpoint": null,
|
|
19
|
+
"publicBaseUrl": "https://cdn.example.com",
|
|
20
|
+
"allowDelete": false
|
|
21
|
+
}
|