@allbaseai/openclaw-allbase 0.1.3 → 0.1.4
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 +219 -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
|
+
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) }],
|
|
@@ -417,6 +503,138 @@ const plugin = {
|
|
|
417
503
|
{ optional: true },
|
|
418
504
|
);
|
|
419
505
|
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
api.registerCommand({
|
|
509
|
+
name: "allbase-auth-start",
|
|
510
|
+
description: "Start OAuth browser flow for Allbase and print authorize URL",
|
|
511
|
+
acceptsArgs: true,
|
|
512
|
+
requireAuth: true,
|
|
513
|
+
handler: async (cmdCtx) => {
|
|
514
|
+
try {
|
|
515
|
+
const args = (cmdCtx.args || "").trim();
|
|
516
|
+
const profileName = args || "default";
|
|
517
|
+
const cfg = getConfig(api);
|
|
518
|
+
const profile = (cfg.profiles || {})[profileName] || {};
|
|
519
|
+
const baseUrl = resolveBaseUrl(cfg, profile);
|
|
520
|
+
const clientId = (profile.oauthClientId || process.env.ALLBASE_OAUTH_CLIENT_ID || "").trim();
|
|
521
|
+
if (!clientId) {
|
|
522
|
+
return { text: "Missing oauthClientId. Set it in plugins.entries.openclaw-allbase.config.profiles.<profile>.oauthClientId or ALLBASE_OAUTH_CLIENT_ID." };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const tokenEndpoint = (profile.oauthTokenEndpoint || `${baseUrl}/oauth/token`).trim();
|
|
526
|
+
const authorizeEndpoint = `${baseUrl}/oauth/authorize`;
|
|
527
|
+
const redirectUri = (process.env.ALLBASE_OAUTH_REDIRECT_URI || "urn:ietf:wg:oauth:2.0:oob").trim();
|
|
528
|
+
const scope = (profile.oauthScope || process.env.ALLBASE_OAUTH_SCOPE || "offline_access openid profile").trim();
|
|
529
|
+
const state = base64Url(randomBytes(24));
|
|
530
|
+
const codeVerifier = base64Url(randomBytes(48));
|
|
531
|
+
const codeChallenge = base64Url(createHash('sha256').update(codeVerifier).digest());
|
|
532
|
+
|
|
533
|
+
const authUrl = new URL(authorizeEndpoint);
|
|
534
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
535
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
536
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
537
|
+
authUrl.searchParams.set('scope', scope);
|
|
538
|
+
authUrl.searchParams.set('state', state);
|
|
539
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
540
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
541
|
+
|
|
542
|
+
const store = readOAuthStore();
|
|
543
|
+
store.pending = store.pending || {};
|
|
544
|
+
store.pending[profileName] = {
|
|
545
|
+
profile: profileName,
|
|
546
|
+
baseUrl,
|
|
547
|
+
clientId,
|
|
548
|
+
tokenEndpoint,
|
|
549
|
+
authorizeEndpoint,
|
|
550
|
+
redirectUri,
|
|
551
|
+
scope,
|
|
552
|
+
codeVerifier,
|
|
553
|
+
state,
|
|
554
|
+
createdAt: Date.now(),
|
|
555
|
+
};
|
|
556
|
+
writeOAuthStore(store);
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
text:
|
|
560
|
+
`Allbase OAuth started for profile: ${profileName}\n\n` +
|
|
561
|
+
`1) Open this URL in your browser:\n${authUrl.toString()}\n\n` +
|
|
562
|
+
`2) Complete login and agent selection\n` +
|
|
563
|
+
`3) Copy the authorization code and run:\n/allbase-auth-complete <CODE> ${profileName}\n\n` +
|
|
564
|
+
`Tip: You can do this on any laptop/device.`
|
|
565
|
+
};
|
|
566
|
+
} catch (err) {
|
|
567
|
+
return { text: `Failed to start OAuth flow: ${err instanceof Error ? err.message : String(err)}` };
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
api.registerCommand({
|
|
573
|
+
name: "allbase-auth-complete",
|
|
574
|
+
description: "Complete OAuth flow using authorization code",
|
|
575
|
+
acceptsArgs: true,
|
|
576
|
+
requireAuth: true,
|
|
577
|
+
handler: async (cmdCtx) => {
|
|
578
|
+
try {
|
|
579
|
+
const raw = (cmdCtx.args || "").trim();
|
|
580
|
+
const [code, profileArg] = raw.split(/\s+/);
|
|
581
|
+
if (!code) return { text: "Usage: /allbase-auth-complete <CODE> [profile]" };
|
|
582
|
+
const profileName = profileArg || "default";
|
|
583
|
+
const store = readOAuthStore();
|
|
584
|
+
const pending = store.pending?.[profileName];
|
|
585
|
+
if (!pending) return { text: `No pending OAuth flow for profile '${profileName}'. Run /allbase-auth-start ${profileName} first.` };
|
|
586
|
+
|
|
587
|
+
const body = new URLSearchParams({
|
|
588
|
+
grant_type: 'authorization_code',
|
|
589
|
+
code,
|
|
590
|
+
client_id: pending.clientId,
|
|
591
|
+
redirect_uri: pending.redirectUri,
|
|
592
|
+
code_verifier: pending.codeVerifier,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const resp = await fetch(pending.tokenEndpoint, {
|
|
596
|
+
method: 'POST',
|
|
597
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
598
|
+
body: body.toString(),
|
|
599
|
+
});
|
|
600
|
+
const text = await resp.text();
|
|
601
|
+
let data: any = {};
|
|
602
|
+
try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
|
|
603
|
+
if (!resp.ok || !data.access_token) {
|
|
604
|
+
return { text: `OAuth token exchange failed (${resp.status}): ${typeof data === 'string' ? data : JSON.stringify(data)}` };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
writeOAuthProfileToConfig(profileName, {
|
|
608
|
+
oauthAccessToken: data.access_token,
|
|
609
|
+
oauthRefreshToken: data.refresh_token,
|
|
610
|
+
oauthClientId: pending.clientId,
|
|
611
|
+
oauthTokenEndpoint: pending.tokenEndpoint,
|
|
612
|
+
baseUrl: pending.baseUrl,
|
|
613
|
+
}, cmdCtx.config.defaultAgentId);
|
|
614
|
+
|
|
615
|
+
if (store.pending) delete store.pending[profileName];
|
|
616
|
+
writeOAuthStore(store);
|
|
617
|
+
|
|
618
|
+
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.` };
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return { text: `Failed to complete OAuth flow: ${err instanceof Error ? err.message : String(err)}` };
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
api.registerCommand({
|
|
626
|
+
name: "allbase-auth-status",
|
|
627
|
+
description: "Show pending OAuth profiles for Allbase",
|
|
628
|
+
acceptsArgs: false,
|
|
629
|
+
requireAuth: true,
|
|
630
|
+
handler: async () => {
|
|
631
|
+
const store = readOAuthStore();
|
|
632
|
+
const keys = Object.keys(store.pending || {});
|
|
633
|
+
if (!keys.length) return { text: "No pending Allbase OAuth flows." };
|
|
634
|
+
return { text: `Pending Allbase OAuth profiles: ${keys.join(', ')}` };
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
420
638
|
api.registerCommand({
|
|
421
639
|
name: "allbase-status",
|
|
422
640
|
description: "Show resolved Allbase profile for this agent",
|