@vee3/upload 0.1.1 → 0.2.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/package.json +31 -24
- package/src/api.js +34 -34
- package/src/index.js +99 -96
- package/src/systemCertificates.js +61 -0
- package/src/uploadFile.js +25 -25
package/package.json
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@vee3/upload",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Upload local files to Vee3.",
|
|
5
|
-
"homepage": "https://vee3.io",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"vee3-upload": "./src/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@vee3/upload",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Upload local files to Vee3.",
|
|
5
|
+
"homepage": "https://vee3.io",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vee3-upload": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/vee3io/Vee3.git",
|
|
22
|
+
"directory": "packages/upload"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"vee3",
|
|
26
|
+
"upload",
|
|
27
|
+
"cli",
|
|
28
|
+
"mcp"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT"
|
|
31
|
+
}
|
package/src/api.js
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
export const DEFAULT_API_BASE_URL = "https://api.vee3.io";
|
|
2
|
-
|
|
3
|
-
async function formatApiError(response) {
|
|
4
|
-
const responseText = await response.text();
|
|
5
|
-
try {
|
|
6
|
-
const payload = JSON.parse(responseText);
|
|
7
|
-
if (payload && typeof payload.error === "object") {
|
|
8
|
-
const requestId = payload.error.request_id ? ` (request_id=${payload.error.request_id})` : "";
|
|
9
|
-
return `${payload.error.code}: ${payload.error.message}${requestId}`;
|
|
10
|
-
}
|
|
11
|
-
} catch {
|
|
12
|
-
// Fall through to raw response text.
|
|
13
|
-
}
|
|
14
|
-
return responseText || `HTTP ${response.status}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function resolveUpload({ apiBaseUrl, uploadCode, contentType }) {
|
|
18
|
-
const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}/v1/agent-uploads/resolve`, {
|
|
19
|
-
method: "POST",
|
|
20
|
-
headers: {
|
|
21
|
-
"Content-Type": "application/json"
|
|
22
|
-
},
|
|
23
|
-
body: JSON.stringify({
|
|
24
|
-
upload_code: uploadCode,
|
|
25
|
-
content_type: contentType
|
|
26
|
-
})
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (!response.ok) {
|
|
30
|
-
throw new Error(await formatApiError(response));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return await response.json();
|
|
34
|
-
}
|
|
1
|
+
export const DEFAULT_API_BASE_URL = "https://api.vee3.io";
|
|
2
|
+
|
|
3
|
+
async function formatApiError(response) {
|
|
4
|
+
const responseText = await response.text();
|
|
5
|
+
try {
|
|
6
|
+
const payload = JSON.parse(responseText);
|
|
7
|
+
if (payload && typeof payload.error === "object") {
|
|
8
|
+
const requestId = payload.error.request_id ? ` (request_id=${payload.error.request_id})` : "";
|
|
9
|
+
return `${payload.error.code}: ${payload.error.message}${requestId}`;
|
|
10
|
+
}
|
|
11
|
+
} catch {
|
|
12
|
+
// Fall through to raw response text.
|
|
13
|
+
}
|
|
14
|
+
return responseText || `HTTP ${response.status}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function resolveUpload({ apiBaseUrl, uploadCode, contentType }) {
|
|
18
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/$/, "")}/v1/agent-uploads/resolve`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json"
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
upload_code: uploadCode,
|
|
25
|
+
content_type: contentType
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(await formatApiError(response));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return await response.json();
|
|
34
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,96 +1,99 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { stat } from "node:fs/promises";
|
|
4
|
-
import { DEFAULT_API_BASE_URL, resolveUpload } from "./api.js";
|
|
5
|
-
import { detectContentType } from "./detectContentType.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import { DEFAULT_API_BASE_URL, resolveUpload } from "./api.js";
|
|
5
|
+
import { detectContentType } from "./detectContentType.js";
|
|
6
|
+
import { ensureSystemCertificateTrust } from "./systemCertificates.js";
|
|
7
|
+
import { uploadFileToSignedUrl } from "./uploadFile.js";
|
|
8
|
+
|
|
9
|
+
ensureSystemCertificateTrust();
|
|
10
|
+
|
|
11
|
+
function printUsage() {
|
|
12
|
+
console.error(
|
|
13
|
+
"Usage: vee3-upload <upload_code> <file_path> [--api-base-url https://api.vee3.io] [--json]"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseArguments(argumentsList) {
|
|
18
|
+
const positionalArguments = [];
|
|
19
|
+
let apiBaseUrl = DEFAULT_API_BASE_URL;
|
|
20
|
+
let outputJson = false;
|
|
21
|
+
|
|
22
|
+
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
23
|
+
const argument = argumentsList[index];
|
|
24
|
+
if (argument === "--api-base-url") {
|
|
25
|
+
const nextArgument = argumentsList[index + 1];
|
|
26
|
+
if (!nextArgument) {
|
|
27
|
+
throw new Error("--api-base-url requires a value");
|
|
28
|
+
}
|
|
29
|
+
apiBaseUrl = nextArgument;
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (argument === "--json") {
|
|
34
|
+
outputJson = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
positionalArguments.push(argument);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (positionalArguments.length !== 2) {
|
|
41
|
+
throw new Error("Expected upload_code and file_path");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
uploadCode: positionalArguments[0],
|
|
46
|
+
filePath: positionalArguments[1],
|
|
47
|
+
apiBaseUrl,
|
|
48
|
+
outputJson
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const { uploadCode, filePath, apiBaseUrl, outputJson } = parseArguments(
|
|
54
|
+
process.argv.slice(2)
|
|
55
|
+
);
|
|
56
|
+
const fileStat = await stat(filePath);
|
|
57
|
+
if (!fileStat.isFile()) {
|
|
58
|
+
throw new Error(`File path is not a file: ${filePath}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const contentType = await detectContentType(filePath);
|
|
62
|
+
const resolvedUpload = await resolveUpload({
|
|
63
|
+
apiBaseUrl,
|
|
64
|
+
uploadCode,
|
|
65
|
+
contentType
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (fileStat.size > resolvedUpload.max_bytes) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`File is too large: ${fileStat.size} bytes exceeds ${resolvedUpload.max_bytes} bytes`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await uploadFileToSignedUrl({
|
|
75
|
+
uploadUrl: resolvedUpload.upload_url,
|
|
76
|
+
filePath,
|
|
77
|
+
fileSizeBytes: fileStat.size,
|
|
78
|
+
requiredHeaders: resolvedUpload.required_headers
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (outputJson) {
|
|
82
|
+
console.log(
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
upload_id: resolvedUpload.upload_id,
|
|
85
|
+
content_type: contentType,
|
|
86
|
+
size_bytes: fileStat.size
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(resolvedUpload.upload_id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
main().catch((error) => {
|
|
96
|
+
printUsage();
|
|
97
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const SYSTEM_CA_FLAG = "--use-system-ca";
|
|
4
|
+
const REEXEC_GUARD_VARIABLE = "VEE3_UPLOAD_SYSTEM_CA_REEXEC";
|
|
5
|
+
|
|
6
|
+
function systemCaFlagAlreadyActive() {
|
|
7
|
+
if (process.execArgv.includes(SYSTEM_CA_FLAG)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
const nodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
11
|
+
return nodeOptions.includes(SYSTEM_CA_FLAG);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function systemCaFlagSupported() {
|
|
15
|
+
try {
|
|
16
|
+
return process.allowedNodeEnvironmentFlags.has(SYSTEM_CA_FLAG);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Networks that perform TLS inspection (corporate proxies, some antivirus
|
|
24
|
+
* products) present certificates signed by a private root CA that Node does not
|
|
25
|
+
* trust by default, so HTTPS requests fail with UNABLE_TO_VERIFY_LEAF_SIGNATURE.
|
|
26
|
+
* Re-executing with --use-system-ca makes Node trust the same roots the
|
|
27
|
+
* operating system already trusts, which is what browsers and OS HTTP clients
|
|
28
|
+
* use. On older Node versions without the flag we fall back to whatever
|
|
29
|
+
* NODE_EXTRA_CA_CERTS provides.
|
|
30
|
+
*/
|
|
31
|
+
export function ensureSystemCertificateTrust() {
|
|
32
|
+
if (process.env[REEXEC_GUARD_VARIABLE] === "1") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (systemCaFlagAlreadyActive()) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!systemCaFlagSupported()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entryScript = process.argv[1];
|
|
43
|
+
if (!entryScript) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = spawnSync(
|
|
48
|
+
process.execPath,
|
|
49
|
+
[SYSTEM_CA_FLAG, entryScript, ...process.argv.slice(2)],
|
|
50
|
+
{
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
env: { ...process.env, [REEXEC_GUARD_VARIABLE]: "1" }
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (result.error) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
process.exit(result.status ?? 0);
|
|
61
|
+
}
|
package/src/uploadFile.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
2
|
-
|
|
3
|
-
export async function uploadFileToSignedUrl({
|
|
4
|
-
uploadUrl,
|
|
5
|
-
filePath,
|
|
6
|
-
fileSizeBytes,
|
|
7
|
-
requiredHeaders
|
|
8
|
-
}) {
|
|
9
|
-
const headers = {
|
|
10
|
-
...requiredHeaders,
|
|
11
|
-
"Content-Length": String(fileSizeBytes)
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const response = await fetch(uploadUrl, {
|
|
15
|
-
method: "PUT",
|
|
16
|
-
headers,
|
|
17
|
-
body: createReadStream(filePath),
|
|
18
|
-
duplex: "half"
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
if (!response.ok) {
|
|
22
|
-
const responseText = await response.text();
|
|
23
|
-
throw new Error(responseText || `Upload failed with HTTP ${response.status}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export async function uploadFileToSignedUrl({
|
|
4
|
+
uploadUrl,
|
|
5
|
+
filePath,
|
|
6
|
+
fileSizeBytes,
|
|
7
|
+
requiredHeaders
|
|
8
|
+
}) {
|
|
9
|
+
const headers = {
|
|
10
|
+
...requiredHeaders,
|
|
11
|
+
"Content-Length": String(fileSizeBytes)
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const response = await fetch(uploadUrl, {
|
|
15
|
+
method: "PUT",
|
|
16
|
+
headers,
|
|
17
|
+
body: createReadStream(filePath),
|
|
18
|
+
duplex: "half"
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const responseText = await response.text();
|
|
23
|
+
throw new Error(responseText || `Upload failed with HTTP ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
}
|