a2a-exposed 0.7.0 → 0.7.2
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 +20 -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,17 @@ 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
|
+
and the control Worker logs oauth_callback_failed with that code
|
|
175
|
+
|
|
164
176
|
Presets: grok-bot | claude-code | openclaw-wake | openclaw-agent | hermes | generic
|
|
165
177
|
Config: ${C.CONFIG_FILE} (override dir with --config-dir DIR or A2A_CONFIG_DIR; env vars override file values)
|
|
166
178
|
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 +196,8 @@ const SPEC = {
|
|
|
184
196
|
export: { sql: B },
|
|
185
197
|
upstream: { json: B },
|
|
186
198
|
connect: { alias: S, name: S, json: B, "no-wait": B, replace: B, force: B, "card-url": S },
|
|
199
|
+
signup: { "control-url": S, json: B },
|
|
200
|
+
login: { "control-url": S, json: B },
|
|
187
201
|
pair: { json: B, web: B, ttl: S, issuer: S, "client-id": S, subjects: S, methods: S, "secret-stdin": B },
|
|
188
202
|
};
|
|
189
203
|
const STATES = ["completed", "input-required", "failed", "rejected", "working"];
|
|
@@ -250,6 +264,12 @@ export async function main(argv) {
|
|
|
250
264
|
case "export": return cmd.exportInbox(o);
|
|
251
265
|
case "token": return cmd.token(need(p[0], "token issue|list|revoke|rotate [label]"), p[1], o);
|
|
252
266
|
case "peers": return cmd.peers(p[0] || "list", p.slice(1), o);
|
|
267
|
+
case "signup":
|
|
268
|
+
case "login": {
|
|
269
|
+
const result = await account.deviceLogin({ controlUrl: o["control-url"] || C.get("CONTROL_URL"), intent: name });
|
|
270
|
+
if (o.json) console.log(JSON.stringify({ control_url: result.controlUrl, email: result.email }));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
253
273
|
case "connect": return pair.connect(need(p[0], "connect <base-or-card-url> [--alias A] [--no-wait] [--json]"), o);
|
|
254
274
|
case "pair": {
|
|
255
275
|
const sub = need(p[0], "pair set-password|set-oidc|list|approve <code>|deny <code>");
|
package/package.json
CHANGED