a2a-exposed 0.7.1 → 0.7.3
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/lib/account.mjs +94 -0
- package/lib/main.mjs +21 -0
- package/package.json +1 -1
package/lib/account.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Control-plane sign-in (RFC 8628). The CLI is a public client (`a2a-cli`).
|
|
2
|
+
// The session token is written to config.env and is never printed.
|
|
3
|
+
import * as C from "./config.mjs";
|
|
4
|
+
import { CLI, die } from "./a2a.mjs";
|
|
5
|
+
|
|
6
|
+
export const CLI_CLIENT_ID = "a2a-cli";
|
|
7
|
+
export const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
8
|
+
|
|
9
|
+
/** https origin with no userinfo, or "" when the value cannot be a control plane. */
|
|
10
|
+
export function controlOrigin(value) {
|
|
11
|
+
const raw = String(value || "").trim();
|
|
12
|
+
if (!raw) return "";
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(raw);
|
|
15
|
+
if (url.protocol !== "https:" || url.username || url.password) return "";
|
|
16
|
+
if (url.pathname !== "/" && url.pathname !== "") return "";
|
|
17
|
+
if (url.search || url.hash) return "";
|
|
18
|
+
return url.origin;
|
|
19
|
+
} catch {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function scale() {
|
|
25
|
+
const n = Number(process.env.A2A_POLL_SCALE);
|
|
26
|
+
return Number.isFinite(n) && n > 0 && n <= 1 ? n : 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readJson(response) {
|
|
30
|
+
const text = await response.text();
|
|
31
|
+
try { return text ? JSON.parse(text) : {}; } catch { return { error: "invalid_response" }; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run the device flow and store CONTROL_URL plus CONTROL_TOKEN.
|
|
36
|
+
* `fetchImpl` and `sleep` are for tests. The token is not included in the result.
|
|
37
|
+
*/
|
|
38
|
+
export async function deviceLogin({
|
|
39
|
+
controlUrl,
|
|
40
|
+
intent = "login",
|
|
41
|
+
fetchImpl = fetch,
|
|
42
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
43
|
+
write = (line) => console.error(line),
|
|
44
|
+
save = (updates) => C.saveConfig(updates),
|
|
45
|
+
} = {}) {
|
|
46
|
+
const origin = controlOrigin(controlUrl);
|
|
47
|
+
if (!origin) die(`--control-url must be an https origin with no path (example: --control-url https://control.example)`);
|
|
48
|
+
const codeResponse = await fetchImpl(`${origin}/api/auth/device/code`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
51
|
+
body: JSON.stringify({ client_id: CLI_CLIENT_ID }),
|
|
52
|
+
});
|
|
53
|
+
const started = await readJson(codeResponse);
|
|
54
|
+
if (!codeResponse.ok || !started.device_code || !started.user_code || !started.verification_uri_complete) {
|
|
55
|
+
const reason = typeof started.error === "string" ? started.error : `HTTP ${codeResponse.status}`;
|
|
56
|
+
die(reason === "not_found" || codeResponse.status === 404
|
|
57
|
+
? `login is not turned on at ${origin}`
|
|
58
|
+
: `could not start sign-in at ${origin} (${reason})`);
|
|
59
|
+
}
|
|
60
|
+
const verb = intent === "signup" ? "Create an account and approve" : "Sign in and approve";
|
|
61
|
+
write(`${verb} this code:\n${started.user_code}\n${started.verification_uri_complete}`);
|
|
62
|
+
const interval = Math.max(1, Number(started.interval) || 5);
|
|
63
|
+
const deadline = Date.now() + Math.max(1, Number(started.expires_in) || 600) * 1000;
|
|
64
|
+
let wait = interval;
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
await sleep(wait * 1000 * scale());
|
|
67
|
+
const tokenResponse = await fetchImpl(`${origin}/api/auth/device/token`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
70
|
+
body: JSON.stringify({ grant_type: DEVICE_GRANT, device_code: started.device_code, client_id: CLI_CLIENT_ID }),
|
|
71
|
+
});
|
|
72
|
+
const body = await readJson(tokenResponse);
|
|
73
|
+
if (tokenResponse.ok && typeof body.access_token === "string" && body.access_token) {
|
|
74
|
+
save({ CONTROL_URL: origin, CONTROL_TOKEN: body.access_token });
|
|
75
|
+
const sessionResponse = await fetchImpl(`${origin}/api/auth/get-session`, {
|
|
76
|
+
headers: { accept: "application/json", authorization: `Bearer ${body.access_token}` },
|
|
77
|
+
});
|
|
78
|
+
const session = await readJson(sessionResponse);
|
|
79
|
+
const email = typeof session?.user?.email === "string" ? session.user.email : "";
|
|
80
|
+
write(email ? `Signed in as ${email}.` : "Signed in.");
|
|
81
|
+
return { controlUrl: origin, email };
|
|
82
|
+
}
|
|
83
|
+
const error = typeof body.error === "string" ? body.error : "";
|
|
84
|
+
if (error === "authorization_pending") continue;
|
|
85
|
+
if (error === "slow_down") {
|
|
86
|
+
wait += 5;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (error === "access_denied") die("sign-in was denied");
|
|
90
|
+
if (error === "expired_token") die("that code expired; run the command again");
|
|
91
|
+
die(`sign-in failed (${error || `HTTP ${tokenResponse.status}`})`);
|
|
92
|
+
}
|
|
93
|
+
die("that code expired; run the command again");
|
|
94
|
+
}
|
package/lib/main.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import * as dep from "./deploy.mjs";
|
|
|
7
7
|
import * as tun from "./tunnel.mjs";
|
|
8
8
|
import * as st from "./status.mjs";
|
|
9
9
|
import * as pair from "./pair.mjs";
|
|
10
|
+
import * as account from "./account.mjs";
|
|
10
11
|
import * as upd from "./update.mjs";
|
|
11
12
|
|
|
12
13
|
const VERSION = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
@@ -161,6 +162,18 @@ Outbound (agents you call)
|
|
|
161
162
|
poll --to <alias|url> <taskId> [--proto 0.3|1.0]
|
|
162
163
|
outbound <taskId> stored state of a task you sent (incl. pushed updates)
|
|
163
164
|
|
|
165
|
+
Control plane account (only when that deployment's login is on)
|
|
166
|
+
signup --control-url https://control.example
|
|
167
|
+
login --control-url https://control.example
|
|
168
|
+
device-flow sign-in. Prints a code and a page. Type that code
|
|
169
|
+
on the page; the link alone does not approve. The session is saved
|
|
170
|
+
as CONTROL_TOKEN in config.env and is not printed. signup and login
|
|
171
|
+
are the same flow; signup is how the first account is created.
|
|
172
|
+
--json prints the control origin and email, not the token.
|
|
173
|
+
A browser sign-in that does not complete returns to /app?error=<code>.
|
|
174
|
+
The control Worker logs social_sign_in_failed before the provider
|
|
175
|
+
page, and oauth_callback_failed when the provider sends the browser back
|
|
176
|
+
|
|
164
177
|
Presets: grok-bot | claude-code | openclaw-wake | openclaw-agent | hermes | generic
|
|
165
178
|
Config: ${C.CONFIG_FILE} (override dir with --config-dir DIR or A2A_CONFIG_DIR; env vars override file values)
|
|
166
179
|
Update notice: at most a daily background check of the npm registry; off with A2A_NO_UPDATE_CHECK=1 (or DO_NOT_TRACK=1)`;
|
|
@@ -184,6 +197,8 @@ const SPEC = {
|
|
|
184
197
|
export: { sql: B },
|
|
185
198
|
upstream: { json: B },
|
|
186
199
|
connect: { alias: S, name: S, json: B, "no-wait": B, replace: B, force: B, "card-url": S },
|
|
200
|
+
signup: { "control-url": S, json: B },
|
|
201
|
+
login: { "control-url": S, json: B },
|
|
187
202
|
pair: { json: B, web: B, ttl: S, issuer: S, "client-id": S, subjects: S, methods: S, "secret-stdin": B },
|
|
188
203
|
};
|
|
189
204
|
const STATES = ["completed", "input-required", "failed", "rejected", "working"];
|
|
@@ -250,6 +265,12 @@ export async function main(argv) {
|
|
|
250
265
|
case "export": return cmd.exportInbox(o);
|
|
251
266
|
case "token": return cmd.token(need(p[0], "token issue|list|revoke|rotate [label]"), p[1], o);
|
|
252
267
|
case "peers": return cmd.peers(p[0] || "list", p.slice(1), o);
|
|
268
|
+
case "signup":
|
|
269
|
+
case "login": {
|
|
270
|
+
const result = await account.deviceLogin({ controlUrl: o["control-url"] || C.get("CONTROL_URL"), intent: name });
|
|
271
|
+
if (o.json) console.log(JSON.stringify({ control_url: result.controlUrl, email: result.email }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
253
274
|
case "connect": return pair.connect(need(p[0], "connect <base-or-card-url> [--alias A] [--no-wait] [--json]"), o);
|
|
254
275
|
case "pair": {
|
|
255
276
|
const sub = need(p[0], "pair set-password|set-oidc|list|approve <code>|deny <code>");
|
package/package.json
CHANGED