@allbaseai/openclaw-allbase 0.1.3 → 0.1.5
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 +31 -0
- package/index.ts +286 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,3 +103,34 @@ Optional `profile` parameter overrides automatic mapping for one call.
|
|
|
103
103
|
- Plugin id is `openclaw-allbase` (use this id in `plugins.entries.*`).
|
|
104
104
|
- Keep tokens/API keys private.
|
|
105
105
|
- `upload_document` requires local file path available to the gateway host.
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
## Interactive OAuth CLI flow (new)
|
|
109
|
+
|
|
110
|
+
You can now run a browser-based OAuth flow directly from OpenClaw commands:
|
|
111
|
+
|
|
112
|
+
1. Start flow (prints a URL):
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
/allbase-auth-start <profile>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
2. Open URL on any device, approve, copy code.
|
|
119
|
+
|
|
120
|
+
3. Complete flow (stores tokens in `~/.openclaw/openclaw.json`):
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
/allbase-auth-complete <CODE> <profile>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
4. Check pending states:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
/allbase-auth-status
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
After completion, restart gateway if needed:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
openclaw gateway restart
|
|
136
|
+
```
|
package/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { AnyAgentTool, OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
3
3
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename } from "node:path";
|
|
6
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
+
import { homedir } from "node:os";
|
|
6
8
|
|
|
7
9
|
type AgentResult = { content: Array<{ type: string; text: string }>; details?: unknown };
|
|
8
10
|
|
|
@@ -58,6 +60,90 @@ const AllbaseToolSchema = Type.Object(
|
|
|
58
60
|
|
|
59
61
|
const tokenCache = new Map<string, { token: string; exp: number }>();
|
|
60
62
|
|
|
63
|
+
|
|
64
|
+
type OAuthPending = {
|
|
65
|
+
profile: string;
|
|
66
|
+
baseUrl: string;
|
|
67
|
+
clientId: string;
|
|
68
|
+
tokenEndpoint: string;
|
|
69
|
+
authorizeEndpoint: string;
|
|
70
|
+
redirectUri: string;
|
|
71
|
+
scope: string;
|
|
72
|
+
codeVerifier: string;
|
|
73
|
+
state: string;
|
|
74
|
+
createdAt: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type OAuthStore = {
|
|
78
|
+
pending?: Record<string, OAuthPending>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function oauthStorePath(): string {
|
|
82
|
+
return `${homedir()}/.openclaw/openclaw-allbase-oauth-state.json`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readOAuthStore(): OAuthStore {
|
|
86
|
+
const path = oauthStorePath();
|
|
87
|
+
try {
|
|
88
|
+
if (!existsSync(path)) return {};
|
|
89
|
+
return JSON.parse(readFileSync(path, "utf8")) as OAuthStore;
|
|
90
|
+
} catch {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeOAuthStore(store: OAuthStore): void {
|
|
96
|
+
const path = oauthStorePath();
|
|
97
|
+
const dir = path.split('/').slice(0, -1).join('/');
|
|
98
|
+
if (dir) mkdirSync(dir, { recursive: true });
|
|
99
|
+
writeFileSync(path, JSON.stringify(store, null, 2));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function base64Url(buf: Buffer): string {
|
|
103
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getOpenclawConfigPath(): string {
|
|
107
|
+
return `${homedir()}/.openclaw/openclaw.json`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeOAuthProfileToConfig(
|
|
111
|
+
profileName: string,
|
|
112
|
+
values: {
|
|
113
|
+
oauthAccessToken: string;
|
|
114
|
+
oauthRefreshToken?: string;
|
|
115
|
+
oauthClientId: string;
|
|
116
|
+
oauthTokenEndpoint: string;
|
|
117
|
+
baseUrl: string;
|
|
118
|
+
},
|
|
119
|
+
defaultAgentId?: string,
|
|
120
|
+
): void {
|
|
121
|
+
const cfgPath = getOpenclawConfigPath();
|
|
122
|
+
const raw = existsSync(cfgPath) ? readFileSync(cfgPath, 'utf8') : '{}';
|
|
123
|
+
const obj = JSON.parse(raw) as any;
|
|
124
|
+
obj.plugins = obj.plugins || {};
|
|
125
|
+
obj.plugins.entries = obj.plugins.entries || {};
|
|
126
|
+
obj.plugins.entries['openclaw-allbase'] = obj.plugins.entries['openclaw-allbase'] || { enabled: true, config: {} };
|
|
127
|
+
const cfg = obj.plugins.entries['openclaw-allbase'].config || {};
|
|
128
|
+
cfg.baseUrl = cfg.baseUrl || values.baseUrl;
|
|
129
|
+
cfg.defaultProfile = cfg.defaultProfile || profileName;
|
|
130
|
+
cfg.profiles = cfg.profiles || {};
|
|
131
|
+
cfg.profiles[profileName] = {
|
|
132
|
+
...(cfg.profiles[profileName] || {}),
|
|
133
|
+
...(values.oauthAccessToken ? { oauthAccessToken: values.oauthAccessToken } : {}),
|
|
134
|
+
...(values.oauthRefreshToken ? { oauthRefreshToken: values.oauthRefreshToken } : {}),
|
|
135
|
+
oauthClientId: values.oauthClientId,
|
|
136
|
+
oauthTokenEndpoint: values.oauthTokenEndpoint,
|
|
137
|
+
};
|
|
138
|
+
cfg.mapByOpenclawAgent = cfg.mapByOpenclawAgent || {};
|
|
139
|
+
if (defaultAgentId) cfg.mapByOpenclawAgent[defaultAgentId] = profileName;
|
|
140
|
+
obj.plugins.entries['openclaw-allbase'].enabled = true;
|
|
141
|
+
obj.plugins.entries['openclaw-allbase'].config = cfg;
|
|
142
|
+
obj.plugins.allow = Array.isArray(obj.plugins.allow) ? obj.plugins.allow : [];
|
|
143
|
+
if (!obj.plugins.allow.includes('openclaw-allbase')) obj.plugins.allow.push('openclaw-allbase');
|
|
144
|
+
writeFileSync(cfgPath, JSON.stringify(obj, null, 2) + '\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
61
147
|
function json(payload: unknown): AgentResult {
|
|
62
148
|
return {
|
|
63
149
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
@@ -147,6 +233,67 @@ function jwtExpMs(token: string): number | null {
|
|
|
147
233
|
return null;
|
|
148
234
|
}
|
|
149
235
|
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async function ensureOAuthClientId(
|
|
239
|
+
profileName: string,
|
|
240
|
+
baseUrl: string,
|
|
241
|
+
profile: AllbaseProfile,
|
|
242
|
+
redirectUri: string,
|
|
243
|
+
): Promise<{ clientId: string; tokenEndpoint: string; registrationEndpoint: string }> {
|
|
244
|
+
const existingClientId = (profile.oauthClientId || process.env.ALLBASE_OAUTH_CLIENT_ID || '').trim();
|
|
245
|
+
const tokenEndpoint = (profile.oauthTokenEndpoint || process.env.ALLBASE_OAUTH_TOKEN_ENDPOINT || `${baseUrl}/oauth/token`).trim();
|
|
246
|
+
|
|
247
|
+
// discover registration endpoint
|
|
248
|
+
let registrationEndpoint = `${baseUrl}/oauth/register`;
|
|
249
|
+
try {
|
|
250
|
+
const d = await fetch(`${baseUrl}/.well-known/oauth-authorization-server`);
|
|
251
|
+
if (d.ok) {
|
|
252
|
+
const doc = await d.json() as any;
|
|
253
|
+
if (typeof doc?.registration_endpoint === 'string' && doc.registration_endpoint.trim()) {
|
|
254
|
+
registrationEndpoint = doc.registration_endpoint.trim();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// use default
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (existingClientId) {
|
|
262
|
+
return { clientId: existingClientId, tokenEndpoint, registrationEndpoint };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const resp = await fetch(registrationEndpoint, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
client_name: 'OpenClaw Allbase Plugin',
|
|
270
|
+
redirect_uris: [redirectUri],
|
|
271
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
272
|
+
response_types: ['code'],
|
|
273
|
+
scope: 'allbase:read allbase:write',
|
|
274
|
+
token_endpoint_auth_method: 'none',
|
|
275
|
+
client_uri: 'https://docs.openclaw.ai',
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const text = await resp.text();
|
|
280
|
+
let body: any = {};
|
|
281
|
+
try { body = text ? JSON.parse(text) : {}; } catch { body = { raw: text }; }
|
|
282
|
+
if (!resp.ok || !body?.client_id) {
|
|
283
|
+
throw new Error(`OAuth client registration failed (${resp.status}): ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const clientId = String(body.client_id);
|
|
287
|
+
writeOAuthProfileToConfig(profileName, {
|
|
288
|
+
oauthAccessToken: profile.oauthAccessToken || '',
|
|
289
|
+
oauthRefreshToken: profile.oauthRefreshToken,
|
|
290
|
+
oauthClientId: clientId,
|
|
291
|
+
oauthTokenEndpoint: tokenEndpoint,
|
|
292
|
+
baseUrl,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return { clientId, tokenEndpoint, registrationEndpoint };
|
|
296
|
+
}
|
|
150
297
|
async function getOAuthToken(baseUrl: string, profile: AllbaseProfile): Promise<{ token: string; exp: number }> {
|
|
151
298
|
const directToken = profile.oauthAccessToken?.trim();
|
|
152
299
|
if (directToken) {
|
|
@@ -417,6 +564,144 @@ const plugin = {
|
|
|
417
564
|
{ optional: true },
|
|
418
565
|
);
|
|
419
566
|
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
api.registerCommand({
|
|
570
|
+
name: "allbase-auth-start",
|
|
571
|
+
description: "Start OAuth browser flow for Allbase and print authorize URL",
|
|
572
|
+
acceptsArgs: true,
|
|
573
|
+
requireAuth: true,
|
|
574
|
+
handler: async (cmdCtx) => {
|
|
575
|
+
try {
|
|
576
|
+
const args = (cmdCtx.args || "").trim();
|
|
577
|
+
const profileName = args || "default";
|
|
578
|
+
const cfg = getConfig(api);
|
|
579
|
+
const profile = (cfg.profiles || {})[profileName] || {};
|
|
580
|
+
const baseUrl = resolveBaseUrl(cfg, profile);
|
|
581
|
+
const redirectUri = (process.env.ALLBASE_OAUTH_REDIRECT_URI || "urn:ietf:wg:oauth:2.0:oob").trim();
|
|
582
|
+
const scope = (profile.oauthScope || process.env.ALLBASE_OAUTH_SCOPE || "allbase:read allbase:write").trim();
|
|
583
|
+
const { clientId, tokenEndpoint } = await ensureOAuthClientId(profileName, baseUrl, profile, redirectUri);
|
|
584
|
+
const authorizeEndpoint = `${baseUrl}/oauth/authorize`;
|
|
585
|
+
const state = base64Url(randomBytes(24));
|
|
586
|
+
const codeVerifier = base64Url(randomBytes(48));
|
|
587
|
+
const codeChallenge = base64Url(createHash('sha256').update(codeVerifier).digest());
|
|
588
|
+
|
|
589
|
+
const authUrl = new URL(authorizeEndpoint);
|
|
590
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
591
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
592
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
593
|
+
authUrl.searchParams.set('scope', scope);
|
|
594
|
+
authUrl.searchParams.set('state', state);
|
|
595
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
596
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
597
|
+
|
|
598
|
+
const store = readOAuthStore();
|
|
599
|
+
store.pending = store.pending || {};
|
|
600
|
+
store.pending[profileName] = {
|
|
601
|
+
profile: profileName,
|
|
602
|
+
baseUrl,
|
|
603
|
+
clientId,
|
|
604
|
+
tokenEndpoint,
|
|
605
|
+
authorizeEndpoint,
|
|
606
|
+
redirectUri,
|
|
607
|
+
scope,
|
|
608
|
+
codeVerifier,
|
|
609
|
+
state,
|
|
610
|
+
createdAt: Date.now(),
|
|
611
|
+
};
|
|
612
|
+
writeOAuthStore(store);
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
text:
|
|
616
|
+
`Allbase OAuth started for profile: ${profileName}\n\n` +
|
|
617
|
+
`1) Open this URL in your browser:\n${authUrl.toString()}\n\n` +
|
|
618
|
+
`2) Complete login and agent selection\n` +
|
|
619
|
+
`3) Copy the authorization code and run:\n/allbase-auth-complete <CODE> ${profileName}\n\n` +
|
|
620
|
+
`Tip: You can do this on any laptop/device.`
|
|
621
|
+
};
|
|
622
|
+
} catch (err) {
|
|
623
|
+
return { text: `Failed to start OAuth flow: ${err instanceof Error ? err.message : String(err)}` };
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
api.registerCommand({
|
|
629
|
+
name: "allbase-auth-complete",
|
|
630
|
+
description: "Complete OAuth flow using authorization code",
|
|
631
|
+
acceptsArgs: true,
|
|
632
|
+
requireAuth: true,
|
|
633
|
+
handler: async (cmdCtx) => {
|
|
634
|
+
try {
|
|
635
|
+
const raw = (cmdCtx.args || "").trim();
|
|
636
|
+
const [codeOrUrl, profileArg] = raw.split(/\s+/);
|
|
637
|
+
if (!codeOrUrl) return { text: "Usage: /allbase-auth-complete <CODE|CALLBACK_URL> [profile]" };
|
|
638
|
+
const profileName = profileArg || "default";
|
|
639
|
+
let code = codeOrUrl;
|
|
640
|
+
if (codeOrUrl.includes("code=") || codeOrUrl.startsWith("http")) {
|
|
641
|
+
try {
|
|
642
|
+
const u = new URL(codeOrUrl);
|
|
643
|
+
const c = u.searchParams.get("code");
|
|
644
|
+
if (c) code = c;
|
|
645
|
+
} catch {
|
|
646
|
+
const m = codeOrUrl.match(/[?&]code=([^&]+)/);
|
|
647
|
+
if (m?.[1]) code = decodeURIComponent(m[1]);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const store = readOAuthStore();
|
|
651
|
+
const pending = store.pending?.[profileName];
|
|
652
|
+
if (!pending) return { text: `No pending OAuth flow for profile '${profileName}'. Run /allbase-auth-start ${profileName} first.` };
|
|
653
|
+
|
|
654
|
+
const body = new URLSearchParams({
|
|
655
|
+
grant_type: 'authorization_code',
|
|
656
|
+
code,
|
|
657
|
+
client_id: pending.clientId,
|
|
658
|
+
redirect_uri: pending.redirectUri,
|
|
659
|
+
code_verifier: pending.codeVerifier,
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const resp = await fetch(pending.tokenEndpoint, {
|
|
663
|
+
method: 'POST',
|
|
664
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
665
|
+
body: body.toString(),
|
|
666
|
+
});
|
|
667
|
+
const text = await resp.text();
|
|
668
|
+
let data: any = {};
|
|
669
|
+
try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
|
|
670
|
+
if (!resp.ok || !data.access_token) {
|
|
671
|
+
return { text: `OAuth token exchange failed (${resp.status}): ${typeof data === 'string' ? data : JSON.stringify(data)}` };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
writeOAuthProfileToConfig(profileName, {
|
|
675
|
+
oauthAccessToken: data.access_token,
|
|
676
|
+
oauthRefreshToken: data.refresh_token,
|
|
677
|
+
oauthClientId: pending.clientId,
|
|
678
|
+
oauthTokenEndpoint: pending.tokenEndpoint,
|
|
679
|
+
baseUrl: pending.baseUrl,
|
|
680
|
+
}, cmdCtx.config.defaultAgentId);
|
|
681
|
+
|
|
682
|
+
if (store.pending) delete store.pending[profileName];
|
|
683
|
+
writeOAuthStore(store);
|
|
684
|
+
|
|
685
|
+
return { text: `✅ Allbase OAuth connected for profile '${profileName}'.\nMapped current agent (${cmdCtx.config.defaultAgentId || 'default'}) -> ${profileName}.\nRestart gateway to pick up updated config if needed.` };
|
|
686
|
+
} catch (err) {
|
|
687
|
+
return { text: `Failed to complete OAuth flow: ${err instanceof Error ? err.message : String(err)}` };
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
api.registerCommand({
|
|
693
|
+
name: "allbase-auth-status",
|
|
694
|
+
description: "Show pending OAuth profiles for Allbase",
|
|
695
|
+
acceptsArgs: false,
|
|
696
|
+
requireAuth: true,
|
|
697
|
+
handler: async () => {
|
|
698
|
+
const store = readOAuthStore();
|
|
699
|
+
const keys = Object.keys(store.pending || {});
|
|
700
|
+
if (!keys.length) return { text: "No pending Allbase OAuth flows." };
|
|
701
|
+
return { text: `Pending Allbase OAuth profiles: ${keys.join(', ')}` };
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
|
|
420
705
|
api.registerCommand({
|
|
421
706
|
name: "allbase-status",
|
|
422
707
|
description: "Show resolved Allbase profile for this agent",
|