@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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/index.ts +286 -1
  3. 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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allbaseai/openclaw-allbase",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin to map OpenClaw agents to Allbase agent/workspace profiles.",
6
6
  "license": "MIT",