@epure-sh/cli 0.1.8 → 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/README.md +46 -68
- package/bin/epure-cli.js +2 -516
- package/lib/api.js +40 -16
- package/lib/auth.js +153 -447
- package/lib/cli.js +146 -0
- package/lib/mcp.js +87 -0
- package/lib/operations.js +83 -0
- package/package.json +2 -2
package/lib/api.js
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const headers={Authorization:`Bearer ${token}`};let payload;
|
|
10
|
-
if(files){payload=new FormData();for(const [key,value] of Object.entries(body||{}))payload.append(key,String(value));for(const f of files)payload.append(f.field||'file',new Blob([Buffer.from(f.base64,'base64')],{type:f.contentType||'application/octet-stream'}),f.filename);}
|
|
11
|
-
else if(body!==undefined){headers['Content-Type']='application/json';payload=JSON.stringify(body);}
|
|
12
|
-
const response=await fetch(url,{method,headers,body:payload,redirect:'error',signal:AbortSignal.timeout(30000)});
|
|
13
|
-
const text=await response.text();let data;try{data=JSON.parse(text)}catch{data=text||null}
|
|
14
|
-
if(!response.ok)throw new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`);
|
|
15
|
-
return {status:response.status,data};
|
|
1
|
+
'use strict';
|
|
2
|
+
const { ClientError, endpoint, getActiveCredentials, ensureValidToken, authRequired } = require('./auth');
|
|
3
|
+
const { catalog } = require('./operations');
|
|
4
|
+
function safePath(method, path) {
|
|
5
|
+
if (typeof path !== 'string' || !path.startsWith('/api/v1/') || /[\\?#]|%2f|%5c|%2e|\.{2}/i.test(path)) throw new ClientError('INVALID_PATH', 'Use an API path from epure api-catalog, without a query string.', 2);
|
|
6
|
+
const allowed = catalog.some(o => o.method === method && new RegExp(`^${o.path.replace(/\{[^}]+\}/g, '[^/]+')}$`).test(path));
|
|
7
|
+
// Auth identity is the OAuth Agent API, never browser login or PAT management.
|
|
8
|
+
if (!allowed) throw new ClientError('UNSUPPORTED_OPERATION', 'Unknown or excluded API operation. Run epure api-catalog.', 2);
|
|
16
9
|
}
|
|
17
|
-
|
|
10
|
+
async function apiRequest(instanceUrl, request = {}, signal) {
|
|
11
|
+
const url = endpoint(instanceUrl); const method = (request.method || 'GET').toUpperCase(); safePath(method, request.path);
|
|
12
|
+
const target = new URL(request.path, url);
|
|
13
|
+
if (target.origin !== url) throw new ClientError('INVALID_PATH', 'Cross-origin requests are forbidden.', 2);
|
|
14
|
+
for (const [k, v] of Object.entries(request.query || {})) if (v != null) target.searchParams.set(k, String(v));
|
|
15
|
+
const session = getActiveCredentials(url); if (!session?.refresh_token) throw authRequired();
|
|
16
|
+
let credential = await ensureValidToken(session);
|
|
17
|
+
async function send() {
|
|
18
|
+
const headers = { Authorization: `Bearer ${credential}`, Accept: 'application/json', 'User-Agent': 'epure-clients/0.2.0' }; let body;
|
|
19
|
+
if (request.files) {
|
|
20
|
+
body = new FormData(); for (const [k, v] of Object.entries(request.body || {})) body.append(k, String(v));
|
|
21
|
+
for (const f of request.files) body.append(f.field || 'file', new Blob([Buffer.from(f.base64, 'base64')], { type: f.contentType || 'application/octet-stream' }), f.filename);
|
|
22
|
+
} else if (request.body !== undefined && !['GET', 'HEAD'].includes(method)) { headers['Content-Type'] = 'application/json'; body = JSON.stringify(request.body); }
|
|
23
|
+
const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 30000);
|
|
24
|
+
const abort = () => controller.abort(); signal?.addEventListener('abort', abort, { once: true }); if (signal?.aborted) abort();
|
|
25
|
+
try { const response = await fetch(target, { method, headers, body, redirect: 'error', signal: controller.signal }); return { response, text: await response.text() }; }
|
|
26
|
+
catch { throw new ClientError(signal?.aborted ? 'CANCELLED' : 'NETWORK_ERROR', signal?.aborted ? 'Request cancelled.' : 'Request failed or timed out. Check the connection before retrying a write.', signal?.aborted ? 2 : 1); }
|
|
27
|
+
finally { clearTimeout(timer); signal?.removeEventListener('abort', abort); }
|
|
28
|
+
}
|
|
29
|
+
let reply = await send();
|
|
30
|
+
if (reply.response.status === 401) {
|
|
31
|
+
credential = await ensureValidToken({ ...session, access_token: credential }, true); reply = await send();
|
|
32
|
+
}
|
|
33
|
+
const { response, text } = reply; let data; try { data = JSON.parse(text); } catch { data = text || null; }
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
if (response.status === 401) throw authRequired();
|
|
36
|
+
const hint = response.status === 403 ? 'Your approved scopes or workspace role do not allow this action. Reauthorize with the required scope, or ask an administrator.' : response.status === 429 ? 'Rate limited. Retry after the server limit resets.' : 'Check the resource ID, arguments and instance.';
|
|
37
|
+
const e = new ClientError(response.status === 403 ? 'PERMISSION_DENIED' : `HTTP_${response.status}`, `HTTP ${response.status}. ${hint}`, response.status === 403 ? 5 : 1); e.status = response.status; throw e;
|
|
38
|
+
}
|
|
39
|
+
return { status: response.status, data };
|
|
40
|
+
}
|
|
41
|
+
module.exports = { apiRequest, safePath };
|