@zegazone_mcp/mcp 2.0.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 +123 -0
- package/dist/api.js +159 -0
- package/dist/config.js +47 -0
- package/dist/credentials-types.js +1 -0
- package/dist/index.js +44 -0
- package/dist/oauth-client.js +67 -0
- package/dist/ops.js +100 -0
- package/dist/server.js +159 -0
- package/dist/token-provider.js +93 -0
- package/dist/token-store.js +48 -0
- package/dist/tool-metadata.js +128 -0
- package/dist/tool-schemas.js +146 -0
- package/package.json +37 -0
- package/scripts/oauth-pair.mjs +226 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @zegazone_mcp/mcp
|
|
2
|
+
|
|
3
|
+
MCP wrapper over `POST /functions/v1/thirdparty-v1`.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @zegazone_mcp/mcp --pair
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
One-time OAuth pairing stores credentials in `~/.zegazone-mcp/credentials.json`. Access tokens refresh automatically — you never handle tokens manually.
|
|
12
|
+
|
|
13
|
+
## MCP client config
|
|
14
|
+
|
|
15
|
+
**Claude Desktop / Cursor:**
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"zegazone": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["@zegazone_mcp/mcp"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Set `ZEGA_API_BASE=https://api.zegaphone.com` in env if needed (defaults apply for production).
|
|
29
|
+
|
|
30
|
+
## What this server does
|
|
31
|
+
|
|
32
|
+
- Exposes stable MCP tools (`collections_delete`, `media_move`, etc.) mapped to API `op` values.
|
|
33
|
+
- Keeps API as source of truth.
|
|
34
|
+
- Applies safety defaults for destructive operations (`dry_run` when neither `confirm` nor `dry_run` is provided).
|
|
35
|
+
- Adds `idempotency_key` automatically to mutating operations when absent.
|
|
36
|
+
|
|
37
|
+
## Tool mapping
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
|
|
41
|
+
- `operations_list` -> `operations.list`
|
|
42
|
+
- `collections_delete` -> `collections.delete`
|
|
43
|
+
- `media_delete` -> `media.delete`
|
|
44
|
+
- `media_move` -> `media.move`
|
|
45
|
+
- `media_reorder` -> `media.reorder`
|
|
46
|
+
|
|
47
|
+
Also includes generic `thirdparty_call` for forward compatibility.
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Copy `.env.example` to `.env.local`. You need `ZEGA_API_BASE` plus **either** a one-time OAuth pairing **or** a static access token.
|
|
52
|
+
|
|
53
|
+
**OAuth (recommended):** stores refresh + access metadata in `~/.zegazone-mcp/credentials.json`. The server refreshes access tokens automatically.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
export ZEGA_API_BASE=https://api.zegaphone.com
|
|
57
|
+
npx @zegazone_mcp/mcp --pair
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Legacy:** set `ZEGA_ACCESS_TOKEN` to a short-lived JWT (see `supabase/docs/OPENCLAW_ZEGAZONE.md`).
|
|
61
|
+
|
|
62
|
+
Optional: `ZEGA_APP_BASE` (defaults to `https://www.zegazone.com`), `ZEGA_OAUTH_CLIENT_ID`, `ZEGA_CREDENTIALS_PATH`, `ZEGA_DEFAULT_DRY_RUN`, `ZEGA_TIMEOUT_MS`.
|
|
63
|
+
|
|
64
|
+
## Local development
|
|
65
|
+
|
|
66
|
+
Clone the repo and work from `supabase/mcp-zegazone-thirdparty/`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install
|
|
70
|
+
npm run dev
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
PowerShell helper:
|
|
74
|
+
|
|
75
|
+
```powershell
|
|
76
|
+
./scripts/run-local.ps1
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Build / run
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run build
|
|
83
|
+
npm start
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Check version:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx @zegazone_mcp/mcp --version
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Smoke tests
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm run smoke
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Smoke tests call the backing API directly and validate read + destructive dry-run paths. Use `ZEGA_ACCESS_TOKEN` or a credential file whose `access_token` is still valid (fresh pairing).
|
|
99
|
+
|
|
100
|
+
## Hosted deployment (container)
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
docker build -t zegazone-mcp .
|
|
104
|
+
docker run --rm \
|
|
105
|
+
-e ZEGA_API_BASE=https://api.zegaphone.com \
|
|
106
|
+
-e ZEGA_ACCESS_TOKEN=<token> \
|
|
107
|
+
-e ZEGA_DEFAULT_DRY_RUN=true \
|
|
108
|
+
zegazone-mcp
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Mount a credential file instead of `ZEGA_ACCESS_TOKEN` if you use pairing on the host:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
docker run --rm \
|
|
115
|
+
-v "$HOME/.zegazone-mcp:/root/.zegazone-mcp:ro" \
|
|
116
|
+
-e ZEGA_API_BASE=https://api.zegaphone.com \
|
|
117
|
+
-e ZEGA_DEFAULT_DRY_RUN=true \
|
|
118
|
+
zegazone-mcp
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## npm
|
|
122
|
+
|
|
123
|
+
Published as [`@zegazone_mcp/mcp`](https://www.npmjs.com/package/@zegazone_mcp/mcp).
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { STATUS_CODES } from "node:http";
|
|
2
|
+
import { DESTRUCTIVE_OPS, MUTATING_OPS } from "./ops.js";
|
|
3
|
+
function jsonReplacerForApiErrors(_key, value) {
|
|
4
|
+
if (value instanceof Error) {
|
|
5
|
+
return { name: value.name, message: value.message };
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
/** API / gateway `error` fields may be strings or structured objects — never rely on String(obj). */
|
|
10
|
+
function coerceApiErrorField(value) {
|
|
11
|
+
if (typeof value === "string")
|
|
12
|
+
return value;
|
|
13
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
14
|
+
return String(value);
|
|
15
|
+
if (value == null)
|
|
16
|
+
return "api_error";
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "api_error";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Thrown on non-2xx API responses so MCP hosts see a real `Error#message` (plain objects become "[object Object]").
|
|
26
|
+
*/
|
|
27
|
+
export class ThirdPartyRequestError extends Error {
|
|
28
|
+
payload;
|
|
29
|
+
constructor(payload) {
|
|
30
|
+
super(ThirdPartyRequestError.formatMessage(payload));
|
|
31
|
+
this.name = "ThirdPartyRequestError";
|
|
32
|
+
this.payload = payload;
|
|
33
|
+
}
|
|
34
|
+
static formatMessage(p) {
|
|
35
|
+
const phrase = STATUS_CODES[p.status] ?? "";
|
|
36
|
+
const headline = phrase ? `HTTP ${p.status} ${phrase}` : `HTTP ${p.status}`;
|
|
37
|
+
const summary = {
|
|
38
|
+
status: p.status,
|
|
39
|
+
error: p.error,
|
|
40
|
+
};
|
|
41
|
+
if (p.detail !== undefined)
|
|
42
|
+
summary.detail = p.detail;
|
|
43
|
+
if (p.field !== undefined)
|
|
44
|
+
summary.field = p.field;
|
|
45
|
+
if (p.need !== undefined)
|
|
46
|
+
summary.need = p.need;
|
|
47
|
+
if (p.raw !== undefined)
|
|
48
|
+
summary.raw = p.raw;
|
|
49
|
+
let body;
|
|
50
|
+
try {
|
|
51
|
+
body = JSON.stringify(summary, jsonReplacerForApiErrors, 2);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
body = JSON.stringify({ status: p.status, error: p.error }, jsonReplacerForApiErrors, 2);
|
|
55
|
+
}
|
|
56
|
+
return `${headline}\n\n${body}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function ensureDestructiveSafety(op, payload, defaultDryRun) {
|
|
60
|
+
if (!DESTRUCTIVE_OPS.has(op))
|
|
61
|
+
return payload;
|
|
62
|
+
const hasConfirm = payload.confirm === true;
|
|
63
|
+
const hasDryRun = payload.dry_run === true;
|
|
64
|
+
if (hasConfirm || hasDryRun)
|
|
65
|
+
return payload;
|
|
66
|
+
return {
|
|
67
|
+
...payload,
|
|
68
|
+
dry_run: defaultDryRun,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function ensureIdempotency(op, payload) {
|
|
72
|
+
if (!MUTATING_OPS.has(op))
|
|
73
|
+
return payload;
|
|
74
|
+
if (typeof payload.idempotency_key === "string" && payload.idempotency_key.trim()) {
|
|
75
|
+
return payload;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
...payload,
|
|
79
|
+
idempotency_key: `mcp-${op}-${Date.now()}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function callThirdPartyOnce(config, accessToken, op, args) {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
85
|
+
try {
|
|
86
|
+
const body = JSON.stringify({
|
|
87
|
+
op,
|
|
88
|
+
...args,
|
|
89
|
+
});
|
|
90
|
+
const res = await fetch(`${config.apiBase}/functions/v1/thirdparty-v1`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
Authorization: `Bearer ${accessToken}`,
|
|
95
|
+
},
|
|
96
|
+
body,
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
const parsed = (await res.json().catch(() => ({})));
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const detail = typeof parsed.detail === "string"
|
|
102
|
+
? parsed.detail
|
|
103
|
+
: typeof parsed.message === "string"
|
|
104
|
+
? parsed.message
|
|
105
|
+
: undefined;
|
|
106
|
+
const errorObj = {
|
|
107
|
+
error: coerceApiErrorField(parsed.error ?? "api_error"),
|
|
108
|
+
detail,
|
|
109
|
+
field: typeof parsed.field === "string" ? parsed.field : undefined,
|
|
110
|
+
need: parsed.need ?? undefined,
|
|
111
|
+
status: res.status,
|
|
112
|
+
raw: parsed,
|
|
113
|
+
};
|
|
114
|
+
return { ok: false, error: errorObj };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, data: parsed };
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
120
|
+
throw new ThirdPartyRequestError({
|
|
121
|
+
error: "transport_error",
|
|
122
|
+
detail: message,
|
|
123
|
+
status: 502,
|
|
124
|
+
raw: e,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function callThirdParty(config, op, args) {
|
|
132
|
+
let token = await config.getAccessToken();
|
|
133
|
+
let attempt = await callThirdPartyOnce(config, token, op, args);
|
|
134
|
+
if (!attempt.ok && attempt.error.status === 401) {
|
|
135
|
+
// The server rejected our token. The cached access_token may be structurally unexpired
|
|
136
|
+
// but signature-invalid (e.g. server-side JWT_SECRET rotated). Bypass every cache and
|
|
137
|
+
// run the OAuth refresh grant unconditionally.
|
|
138
|
+
let refreshed = null;
|
|
139
|
+
try {
|
|
140
|
+
refreshed = await config.forceRefreshAccessToken();
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
// If we cannot refresh (legacy ZEGA_ACCESS_TOKEN mode, or refresh_token revoked),
|
|
144
|
+
// surface the original 401 with the refresh failure attached as context.
|
|
145
|
+
const refreshMsg = e instanceof Error ? e.message : String(e);
|
|
146
|
+
throw new ThirdPartyRequestError({
|
|
147
|
+
...attempt.error,
|
|
148
|
+
detail: attempt.error.detail
|
|
149
|
+
? `${attempt.error.detail} (token refresh also failed: ${refreshMsg})`
|
|
150
|
+
: `token refresh failed: ${refreshMsg}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
attempt = await callThirdPartyOnce(config, refreshed, op, args);
|
|
154
|
+
}
|
|
155
|
+
if (!attempt.ok) {
|
|
156
|
+
throw new ThirdPartyRequestError(attempt.error);
|
|
157
|
+
}
|
|
158
|
+
return attempt.data;
|
|
159
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createAccessTokenHandle } from "./token-provider.js";
|
|
3
|
+
import { defaultCredentialsPath } from "./token-store.js";
|
|
4
|
+
function readBool(input, fallback) {
|
|
5
|
+
if (!input)
|
|
6
|
+
return fallback;
|
|
7
|
+
const v = input.trim().toLowerCase();
|
|
8
|
+
return v === "1" || v === "true" || v === "yes";
|
|
9
|
+
}
|
|
10
|
+
function hasRefreshInFileSync(credentialsPath) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = fs.readFileSync(credentialsPath, "utf8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
return typeof parsed.refresh_token === "string" && parsed.refresh_token.length > 0;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function loadConfig() {
|
|
21
|
+
const apiBase = (process.env.ZEGA_API_BASE ?? "").trim().replace(/\/+$/, "");
|
|
22
|
+
if (!apiBase) {
|
|
23
|
+
throw new Error("Missing ZEGA_API_BASE");
|
|
24
|
+
}
|
|
25
|
+
const credentialsPath = defaultCredentialsPath();
|
|
26
|
+
const legacyAccessToken = (process.env.ZEGA_ACCESS_TOKEN ?? "").trim() || null;
|
|
27
|
+
const timeoutMsRaw = Number(process.env.ZEGA_TIMEOUT_MS ?? "30000");
|
|
28
|
+
const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 30000;
|
|
29
|
+
const fileHasRefresh = hasRefreshInFileSync(credentialsPath);
|
|
30
|
+
if (!fileHasRefresh && !legacyAccessToken) {
|
|
31
|
+
throw new Error(`Missing credentials. Run: npm run oauth-pair (saves ${credentialsPath}) or set ZEGA_ACCESS_TOKEN.`);
|
|
32
|
+
}
|
|
33
|
+
const handle = createAccessTokenHandle({
|
|
34
|
+
apiBase,
|
|
35
|
+
credentialsPath,
|
|
36
|
+
legacyAccessToken,
|
|
37
|
+
timeoutMs,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
apiBase,
|
|
41
|
+
defaultDryRun: readBool(process.env.ZEGA_DEFAULT_DRY_RUN, true),
|
|
42
|
+
timeoutMs,
|
|
43
|
+
getAccessToken: handle.getAccessToken,
|
|
44
|
+
forceRefreshAccessToken: handle.forceRefreshAccessToken,
|
|
45
|
+
invalidateAccessTokenCache: handle.invalidateMemory,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { loadConfig } from "./config.js";
|
|
8
|
+
import { createServer } from "./server.js";
|
|
9
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const packageRoot = path.resolve(moduleDir, "..");
|
|
11
|
+
function readVersion() {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
13
|
+
return pkg.version ?? "0.0.0";
|
|
14
|
+
}
|
|
15
|
+
async function runPair() {
|
|
16
|
+
const script = path.join(packageRoot, "scripts", "oauth-pair.mjs");
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const child = spawn(process.execPath, [script], {
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
env: process.env,
|
|
21
|
+
});
|
|
22
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
23
|
+
child.on("error", () => resolve(1));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function main() {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
if (args.includes("--version")) {
|
|
29
|
+
console.log(readVersion());
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
if (args.includes("--pair")) {
|
|
33
|
+
process.exit(await runPair());
|
|
34
|
+
}
|
|
35
|
+
const config = await loadConfig();
|
|
36
|
+
const server = createServer(config);
|
|
37
|
+
const transport = new StdioServerTransport();
|
|
38
|
+
await server.connect(transport);
|
|
39
|
+
}
|
|
40
|
+
main().catch((err) => {
|
|
41
|
+
const message = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
42
|
+
console.error(`[zegazone-mcp] startup failed: ${message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export async function exchangeAuthorizationCode(params) {
|
|
2
|
+
const body = JSON.stringify({
|
|
3
|
+
grant_type: "authorization_code",
|
|
4
|
+
client_id: params.clientId,
|
|
5
|
+
code: params.code,
|
|
6
|
+
redirect_uri: params.redirectUri,
|
|
7
|
+
code_verifier: params.codeVerifier,
|
|
8
|
+
});
|
|
9
|
+
return postToken(params.apiBase, body, params.timeoutMs);
|
|
10
|
+
}
|
|
11
|
+
export async function refreshWithStoredRefresh(params) {
|
|
12
|
+
const body = JSON.stringify({
|
|
13
|
+
grant_type: "refresh_token",
|
|
14
|
+
client_id: params.clientId,
|
|
15
|
+
refresh_token: params.refreshToken,
|
|
16
|
+
});
|
|
17
|
+
return postToken(params.apiBase, body, params.timeoutMs);
|
|
18
|
+
}
|
|
19
|
+
function toStoredCredentials(clientId, tokens) {
|
|
20
|
+
const access_expires_at_ms = Date.now() + Math.max(0, tokens.expires_in) * 1000;
|
|
21
|
+
return {
|
|
22
|
+
version: 1,
|
|
23
|
+
client_id: clientId,
|
|
24
|
+
refresh_token: tokens.refresh_token,
|
|
25
|
+
access_token: tokens.access_token,
|
|
26
|
+
access_expires_at_ms,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function refreshAndPersist(params) {
|
|
30
|
+
const tokens = await refreshWithStoredRefresh({
|
|
31
|
+
apiBase: params.apiBase,
|
|
32
|
+
clientId: params.previous.client_id,
|
|
33
|
+
refreshToken: params.previous.refresh_token,
|
|
34
|
+
timeoutMs: params.timeoutMs,
|
|
35
|
+
});
|
|
36
|
+
const next = toStoredCredentials(params.previous.client_id, tokens);
|
|
37
|
+
await params.writeStoredCredentials(params.credentialsPath, next);
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
async function postToken(apiBase, body, timeoutMs) {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`${apiBase}/functions/v1/thirdparty-oauth-token`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body,
|
|
48
|
+
signal: controller.signal,
|
|
49
|
+
});
|
|
50
|
+
const parsed = (await res.json().catch(() => ({})));
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const err = String(parsed.error ?? "token_error");
|
|
53
|
+
const detail = typeof parsed.detail === "string" ? parsed.detail : JSON.stringify(parsed);
|
|
54
|
+
throw new Error(`OAuth token endpoint failed (${res.status}): ${err} — ${detail}`);
|
|
55
|
+
}
|
|
56
|
+
const access_token = typeof parsed.access_token === "string" ? parsed.access_token : "";
|
|
57
|
+
const refresh_token = typeof parsed.refresh_token === "string" ? parsed.refresh_token : "";
|
|
58
|
+
const expires_in = typeof parsed.expires_in === "number" ? parsed.expires_in : 3600;
|
|
59
|
+
if (!access_token || !refresh_token) {
|
|
60
|
+
throw new Error("OAuth token response missing access_token or refresh_token");
|
|
61
|
+
}
|
|
62
|
+
return { access_token, refresh_token, expires_in };
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/ops.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const OP_TOOL_MAP = {
|
|
2
|
+
ping: "ping",
|
|
3
|
+
schema_get: "schema.get",
|
|
4
|
+
operations_list: "operations.list",
|
|
5
|
+
operation_describe: "operation.describe",
|
|
6
|
+
ui_state_get: "ui.state.get",
|
|
7
|
+
ui_state_set: "ui.state.set",
|
|
8
|
+
collections_list: "collections.list",
|
|
9
|
+
collections_batch_get: "collections.batch.get",
|
|
10
|
+
collections_export: "collections.export",
|
|
11
|
+
collections_create: "collections.create",
|
|
12
|
+
collections_update: "collections.update",
|
|
13
|
+
collections_delete: "collections.delete",
|
|
14
|
+
collections_restore: "collections.restore",
|
|
15
|
+
collections_reorder: "collections.reorder",
|
|
16
|
+
collections_archive: "collections.archive",
|
|
17
|
+
collections_unarchive: "collections.unarchive",
|
|
18
|
+
collections_like: "collections.like",
|
|
19
|
+
collections_unlike: "collections.unlike",
|
|
20
|
+
collections_liked_list: "collections.liked.list",
|
|
21
|
+
collections_search: "collections.search",
|
|
22
|
+
collections_browse: "collections.browse",
|
|
23
|
+
collections_stats_get: "collections.stats.get",
|
|
24
|
+
collections_share_url: "collections.share_url",
|
|
25
|
+
collections_publish: "collections.publish",
|
|
26
|
+
collections_unpublish: "collections.unpublish",
|
|
27
|
+
aliases_list: "aliases.list",
|
|
28
|
+
aliases_follow: "aliases.follow",
|
|
29
|
+
aliases_unfollow: "aliases.unfollow",
|
|
30
|
+
aliases_following_list: "aliases.following.list",
|
|
31
|
+
profile_get: "profile.get",
|
|
32
|
+
profile_update: "profile.update",
|
|
33
|
+
collaborators_list: "collaborators.list",
|
|
34
|
+
collaborators_invite: "collaborators.invite",
|
|
35
|
+
collaborators_revoke: "collaborators.revoke",
|
|
36
|
+
collaborators_invites_list: "collaborators.invites.list",
|
|
37
|
+
collaborators_invites_received: "collaborators.invites.received",
|
|
38
|
+
collaborators_invite_accept: "collaborators.invite.accept",
|
|
39
|
+
collaborators_invite_decline: "collaborators.invite.decline",
|
|
40
|
+
media_list: "media.list",
|
|
41
|
+
media_search: "media.search",
|
|
42
|
+
media_batch_list: "media.batch.list",
|
|
43
|
+
media_download: "media.download",
|
|
44
|
+
media_create: "media.create",
|
|
45
|
+
media_update: "media.update",
|
|
46
|
+
media_describe: "media.describe",
|
|
47
|
+
media_replace: "media.replace",
|
|
48
|
+
media_delete: "media.delete",
|
|
49
|
+
media_restore: "media.restore",
|
|
50
|
+
media_move: "media.move",
|
|
51
|
+
media_copy: "media.copy",
|
|
52
|
+
media_reorder: "media.reorder",
|
|
53
|
+
media_archive: "media.archive",
|
|
54
|
+
media_unarchive: "media.unarchive",
|
|
55
|
+
media_text_note_add: "media.text_note.add",
|
|
56
|
+
media_text_note_get: "media.text_note.get",
|
|
57
|
+
media_text_note_update: "media.text_note.update",
|
|
58
|
+
media_text_note_delete: "media.text_note.delete",
|
|
59
|
+
};
|
|
60
|
+
export const DESTRUCTIVE_OPS = new Set([
|
|
61
|
+
"collections.delete",
|
|
62
|
+
"collections.restore",
|
|
63
|
+
"media.delete",
|
|
64
|
+
"media.restore",
|
|
65
|
+
"media.text_note.delete",
|
|
66
|
+
]);
|
|
67
|
+
export const MUTATING_OPS = new Set([
|
|
68
|
+
"ui.state.set",
|
|
69
|
+
"collections.create",
|
|
70
|
+
"collections.update",
|
|
71
|
+
"collections.delete",
|
|
72
|
+
"collections.restore",
|
|
73
|
+
"collections.reorder",
|
|
74
|
+
"collections.archive",
|
|
75
|
+
"collections.unarchive",
|
|
76
|
+
"collections.like",
|
|
77
|
+
"collections.unlike",
|
|
78
|
+
"collections.publish",
|
|
79
|
+
"collections.unpublish",
|
|
80
|
+
"aliases.follow",
|
|
81
|
+
"aliases.unfollow",
|
|
82
|
+
"profile.update",
|
|
83
|
+
"collaborators.invite",
|
|
84
|
+
"collaborators.revoke",
|
|
85
|
+
"collaborators.invite.accept",
|
|
86
|
+
"collaborators.invite.decline",
|
|
87
|
+
"media.create",
|
|
88
|
+
"media.update",
|
|
89
|
+
"media.replace",
|
|
90
|
+
"media.delete",
|
|
91
|
+
"media.restore",
|
|
92
|
+
"media.move",
|
|
93
|
+
"media.copy",
|
|
94
|
+
"media.reorder",
|
|
95
|
+
"media.archive",
|
|
96
|
+
"media.unarchive",
|
|
97
|
+
"media.text_note.add",
|
|
98
|
+
"media.text_note.update",
|
|
99
|
+
"media.text_note.delete",
|
|
100
|
+
]);
|