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