teamclaws 1.0.3 → 2.0.0
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 +20 -12
- package/dist/app.js +23 -19
- package/dist/auth.js +74 -109
- package/dist/broker.js +37 -18
- package/dist/constants.js +2 -1
- package/dist/external-chat.js +20 -19
- package/dist/http.js +2 -0
- package/dist/machine-key.js +245 -0
- package/dist/pairing.js +50 -42
- package/dist/shell.js +5 -6
- package/dist/storage.js +16 -0
- package/dist/target-authorization.js +150 -0
- package/dist/validation.js +39 -7
- package/package.json +1 -1
- package/skills/teamclaws-login/SKILL.md +9 -6
- package/skills/teamclaws-onboard/SKILL.md +9 -6
- package/skills/teamclaws-pair/SKILL.md +7 -4
package/README.md
CHANGED
|
@@ -100,6 +100,7 @@ npx teamclaws login
|
|
|
100
100
|
npx teamclaws login <email>
|
|
101
101
|
npx teamclaws auth status
|
|
102
102
|
npx teamclaws logout
|
|
103
|
+
npx teamclaws logout --revoke
|
|
103
104
|
npx teamclaws servers list
|
|
104
105
|
npx teamclaws pair status <server-identifier-or-canonical-workspace-address>
|
|
105
106
|
npx teamclaws pair
|
|
@@ -114,11 +115,14 @@ Human-readable output is the default. Add `--json` for the strict, versioned,
|
|
|
114
115
|
newline-delimited JSON event stream intended for automation.
|
|
115
116
|
|
|
116
117
|
Pairing (including explicit targets), server listing, pair status, chat, and shell
|
|
117
|
-
require a
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
require a saved TeamClaws CLI authorization and its local machine key. Run
|
|
119
|
+
`teamclaws login` first. Login uses the browser approval to bind a new key to
|
|
120
|
+
your TeamClaws account; the CLI discards the temporary OAuth tokens. Later
|
|
121
|
+
commands sign fresh proofs without repeating browser approval. Missing or
|
|
122
|
+
revoked authorization exits with code 3 in both human and JSON modes. Help,
|
|
123
|
+
version, login, auth status, and logout remain available. Saved workspace
|
|
124
|
+
credentials are preserved when login is required, but pairings made before this
|
|
125
|
+
authorization protocol must be paired again.
|
|
122
126
|
|
|
123
127
|
Pairing is broad privileged delegation to the approved workspace. The client
|
|
124
128
|
does not request or select scopes, profiles, capability bundles, roles, user
|
|
@@ -129,10 +133,10 @@ The client never accepts a model or harness access token. An agent that already
|
|
|
129
133
|
discovered a workspace passes only the selected stable server identifier to the
|
|
130
134
|
`pair` command.
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
five-second timeout, reports the result, and
|
|
135
|
-
loads external-chat resources.
|
|
136
|
+
`pair status` reads the matching protected local workspace credential, obtains
|
|
137
|
+
a short-lived target proof for that bound pairing, performs at most one direct
|
|
138
|
+
workspace verification with a five-second timeout, reports the result, and
|
|
139
|
+
stops. It never starts pairing or loads external-chat resources.
|
|
136
140
|
|
|
137
141
|
After pairing, `chat start`, `chat send`, and `chat watch` use the protected
|
|
138
142
|
workspace credential directly against that exact workspace. They verify the
|
|
@@ -164,12 +168,16 @@ execution outcome unknown; inspect the host before deciding to run it again.
|
|
|
164
168
|
|
|
165
169
|
## Local storage
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
The CLI stores the approved authorization ID, account ID, and public key
|
|
172
|
+
reference in its private user directory. The private signing key remains in
|
|
173
|
+
Windows CNG or, on Linux and macOS, a separate protected PKCS#8 key file.
|
|
174
|
+
Completed workspace credentials are stored by stable server identifier beneath
|
|
169
175
|
`~/.teamclaws/clawgate-devices/<server-uuid>.json`. Writes are atomic. Unix
|
|
170
176
|
directories use mode `0700` and files use mode `0600`; Windows uses
|
|
171
177
|
inheritance-disabled access-control entries limited to the current user and the
|
|
172
|
-
Windows system account.
|
|
178
|
+
Windows system account. Ordinary `logout` removes local authorization access;
|
|
179
|
+
`logout --revoke` waits for TeamClaws to confirm server-side revocation before
|
|
180
|
+
removing the local reference. Neither command deletes saved pairings.
|
|
173
181
|
|
|
174
182
|
The package is licensed under the PolyForm Shield License 1.0.0. Paid service
|
|
175
183
|
access is enforced by TeamClaws servers, not by the client license.
|
package/dist/app.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Output } from "./output.js";
|
|
|
8
8
|
import { PairingClient } from "./pairing.js";
|
|
9
9
|
import { runShell, shellFetch } from "./shell.js";
|
|
10
10
|
import { CredentialStore } from "./storage.js";
|
|
11
|
+
import { TargetAuthorization } from "./target-authorization.js";
|
|
11
12
|
import { installRelease } from "./update.js";
|
|
12
13
|
import { normalizeUuid, parsePairingTarget } from "./validation.js";
|
|
13
14
|
export async function run(arguments_, dependencies = {}, signal) {
|
|
@@ -68,6 +69,7 @@ export async function run(arguments_, dependencies = {}, signal) {
|
|
|
68
69
|
return code;
|
|
69
70
|
}
|
|
70
71
|
const store = dependencies.store ?? new CredentialStore();
|
|
72
|
+
const targetAuthorization = new TargetAuthorization(store, dependencies.fetcher);
|
|
71
73
|
const auth = new AuthService({
|
|
72
74
|
store,
|
|
73
75
|
output,
|
|
@@ -77,9 +79,11 @@ export async function run(arguments_, dependencies = {}, signal) {
|
|
|
77
79
|
: {}),
|
|
78
80
|
});
|
|
79
81
|
const broker = new BrokerClient({
|
|
82
|
+
authorization: targetAuthorization,
|
|
80
83
|
...(dependencies.fetcher ? { fetcher: dependencies.fetcher } : {}),
|
|
81
84
|
});
|
|
82
85
|
const pairing = new PairingClient({
|
|
86
|
+
authorization: targetAuthorization,
|
|
83
87
|
store,
|
|
84
88
|
output,
|
|
85
89
|
...(dependencies.fetcher ? { fetcher: dependencies.fetcher } : {}),
|
|
@@ -88,6 +92,7 @@ export async function run(arguments_, dependencies = {}, signal) {
|
|
|
88
92
|
: {}),
|
|
89
93
|
});
|
|
90
94
|
const externalChat = new ExternalChatClient({
|
|
95
|
+
authorization: targetAuthorization,
|
|
91
96
|
store,
|
|
92
97
|
output,
|
|
93
98
|
...(dependencies.fetcher ? { fetcher: dependencies.fetcher } : {}),
|
|
@@ -101,15 +106,13 @@ export async function run(arguments_, dependencies = {}, signal) {
|
|
|
101
106
|
await auth.status(signal);
|
|
102
107
|
return 0;
|
|
103
108
|
case "logout":
|
|
104
|
-
await auth.logout(signal);
|
|
109
|
+
await auth.logout(signal, parsed.revoke);
|
|
105
110
|
return 0;
|
|
106
111
|
}
|
|
107
|
-
|
|
108
|
-
? "Authenticate in the TeamClaws CLI with `teamclaws login` before making a pairing request."
|
|
109
|
-
: "Authenticate in the TeamClaws CLI with `teamclaws login` before running this command.", signal);
|
|
112
|
+
await targetAuthorization.requireGrant();
|
|
110
113
|
switch (parsed.action) {
|
|
111
114
|
case "shell": {
|
|
112
|
-
const result = await runShell(store, dependencies.fetcher ?? shellFetch, parsePairingTarget(parsed.target), parsed.command, signal);
|
|
115
|
+
const result = await runShell(store, dependencies.fetcher ?? shellFetch, parsePairingTarget(parsed.target), parsed.command, targetAuthorization, signal);
|
|
113
116
|
if (parsed.json)
|
|
114
117
|
stdout.write(`${JSON.stringify(result)}\n`);
|
|
115
118
|
else {
|
|
@@ -119,21 +122,21 @@ export async function run(arguments_, dependencies = {}, signal) {
|
|
|
119
122
|
return result.exit_code;
|
|
120
123
|
}
|
|
121
124
|
case "servers_list": {
|
|
122
|
-
const inventory = await broker.servers(
|
|
123
|
-
await offerServerMetadata(inventory.servers, broker,
|
|
125
|
+
const inventory = await broker.servers(signal);
|
|
126
|
+
await offerServerMetadata(inventory.servers, broker, parsed.json, output, dependencies.io, signal);
|
|
124
127
|
output.servers(inventory.servers, inventory.workspace_count);
|
|
125
128
|
return 0;
|
|
126
129
|
}
|
|
127
130
|
case "servers_update": {
|
|
128
|
-
const server = await broker.updateServerMetadata(
|
|
131
|
+
const server = await broker.updateServerMetadata(parsed.serverId, parsed.updates, signal);
|
|
129
132
|
output.serverMetadataUpdated(server);
|
|
130
133
|
return 0;
|
|
131
134
|
}
|
|
132
135
|
case "pair": {
|
|
133
136
|
const target = parsed.target
|
|
134
137
|
? parsePairingTarget(parsed.target)
|
|
135
|
-
: await discoverTarget(broker,
|
|
136
|
-
const pending = await broker.startPairing(target,
|
|
138
|
+
: await discoverTarget(broker, parsed.json, output, dependencies.io, signal);
|
|
139
|
+
const pending = await broker.startPairing(target, signal);
|
|
137
140
|
await pairing.complete(pending, broker.currentDeviceName(), parsed.noOpen, signal);
|
|
138
141
|
return 0;
|
|
139
142
|
}
|
|
@@ -224,8 +227,9 @@ export function parseArguments(arguments_) {
|
|
|
224
227
|
}
|
|
225
228
|
throw usageError();
|
|
226
229
|
}
|
|
227
|
-
if (args[0] === "logout" &&
|
|
228
|
-
|
|
230
|
+
if (args[0] === "logout" &&
|
|
231
|
+
(args.length === 1 || (args.length === 2 && args[1] === "--revoke")))
|
|
232
|
+
return { action: "logout", json, revoke: args.length === 2 };
|
|
229
233
|
if (args[0] === "auth" && args[1] === "status" && args.length === 2) {
|
|
230
234
|
return { action: "auth_status", json };
|
|
231
235
|
}
|
|
@@ -382,9 +386,9 @@ function nonNegativeInteger(value) {
|
|
|
382
386
|
throw usageError();
|
|
383
387
|
return parsed;
|
|
384
388
|
}
|
|
385
|
-
async function discoverTarget(broker,
|
|
386
|
-
const inventory = await broker.servers(
|
|
387
|
-
await offerServerMetadata(inventory.servers, broker,
|
|
389
|
+
async function discoverTarget(broker, json, output, io, signal) {
|
|
390
|
+
const inventory = await broker.servers(signal);
|
|
391
|
+
await offerServerMetadata(inventory.servers, broker, json, output, io, signal);
|
|
388
392
|
const servers = inventory.servers.filter((server) => server.pairing_ready);
|
|
389
393
|
if (servers.length === 0) {
|
|
390
394
|
throw new TeamClawsError("no_pairing_ready_workspaces", inventory.workspace_count === 0
|
|
@@ -399,7 +403,7 @@ async function discoverTarget(broker, token, json, output, io, signal) {
|
|
|
399
403
|
const selected = await promptForServer(servers, io?.input ?? process.stdin, io?.stdout ?? process.stdout);
|
|
400
404
|
return { server_id: selected.server_id };
|
|
401
405
|
}
|
|
402
|
-
async function offerServerMetadata(servers, broker,
|
|
406
|
+
async function offerServerMetadata(servers, broker, json, output, io, signal) {
|
|
403
407
|
const interactive = io?.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
|
|
404
408
|
if (!json && !interactive)
|
|
405
409
|
return;
|
|
@@ -409,7 +413,7 @@ async function offerServerMetadata(servers, broker, token, json, output, io, sig
|
|
|
409
413
|
server.description === undefined ||
|
|
410
414
|
(server.custom_alias.trim() !== "" && server.description.trim() !== ""))
|
|
411
415
|
continue;
|
|
412
|
-
const claimed = await broker.claimMetadataPrompt(
|
|
416
|
+
const claimed = await broker.claimMetadataPrompt(server.server_id, signal);
|
|
413
417
|
Object.assign(server, claimed.server);
|
|
414
418
|
if (!claimed.should_prompt)
|
|
415
419
|
continue;
|
|
@@ -432,7 +436,7 @@ async function offerServerMetadata(servers, broker, token, json, output, io, sig
|
|
|
432
436
|
if (description.trim())
|
|
433
437
|
updates.description = description;
|
|
434
438
|
if (Object.keys(updates).length > 0) {
|
|
435
|
-
const updated = await broker.updateServerMetadata(
|
|
439
|
+
const updated = await broker.updateServerMetadata(server.server_id, updates, signal);
|
|
436
440
|
Object.assign(server, updated);
|
|
437
441
|
output.serverMetadataUpdated(updated);
|
|
438
442
|
}
|
|
@@ -467,5 +471,5 @@ function usageError() {
|
|
|
467
471
|
});
|
|
468
472
|
}
|
|
469
473
|
export function helpText() {
|
|
470
|
-
return `TeamClaws ${PACKAGE_VERSION}\n\nUsage:\n teamclaws update [--json]\n teamclaws shell <target> --command <text> [--json]\n teamclaws login [email] [--json]\n teamclaws auth status [--json]\n teamclaws logout [--json]\n teamclaws servers list [--json]\n teamclaws servers update <server-id> [--name <text>] [--description <text>] [--json]\n teamclaws pair status <server-identifier-or-canonical-workspace-address> [--json]\n teamclaws pair [server-identifier-or-canonical-workspace-address] [--no-open] [--json]\n teamclaws chat list <target> [--json]\n teamclaws chat capabilities <target> [--json]\n teamclaws chat start <target> --message <text> [selection options] [--json]\n teamclaws chat send <target> --session-id <uuid> --message <text> [--json]\n teamclaws chat watch <target> --session-id <uuid> --request-id <uuid> [resume cursors] [--json]\n\nPair status performs one bounded direct verification and stops. Pairing grants the standard host-issued credential. Chat commands use that credential directly against the selected workspace.\n`;
|
|
474
|
+
return `TeamClaws ${PACKAGE_VERSION}\n\nUsage:\n teamclaws update [--json]\n teamclaws shell <target> --command <text> [--json]\n teamclaws login [email] [--json]\n teamclaws auth status [--json]\n teamclaws logout [--revoke] [--json]\n teamclaws servers list [--json]\n teamclaws servers update <server-id> [--name <text>] [--description <text>] [--json]\n teamclaws pair status <server-identifier-or-canonical-workspace-address> [--json]\n teamclaws pair [server-identifier-or-canonical-workspace-address] [--no-open] [--json]\n teamclaws chat list <target> [--json]\n teamclaws chat capabilities <target> [--json]\n teamclaws chat start <target> --message <text> [selection options] [--json]\n teamclaws chat send <target> --session-id <uuid> --message <text> [--json]\n teamclaws chat watch <target> --session-id <uuid> --request-id <uuid> [resume cursors] [--json]\n\nPair status performs one bounded direct verification and stops. Pairing grants the standard host-issued credential. Chat commands use that credential directly against the selected workspace.\n`;
|
|
471
475
|
}
|
package/dist/auth.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { API_BASE, API_V2_BASE, API_RESOURCE, OAUTH_CLIENT_ID, OAUTH_DEVICE_AUTHORIZATION_URL, OAUTH_LOGIN_TIMEOUT_MS, OAUTH_TOKEN_URL, TEAMCLAWS_ORIGIN, } from "./constants.js";
|
|
2
2
|
import { TeamClawsError } from "./errors.js";
|
|
3
3
|
import { errorFromResponse, requestJson } from "./http.js";
|
|
4
4
|
import { assertExactKeys, isObject, parseSafeUserResponse, requireHttpsUrl, requireInteger, requireString, } from "./validation.js";
|
|
5
5
|
import { openExternalBrowser } from "./browser.js";
|
|
6
|
+
import { OsMachineKeyProvider, } from "./machine-key.js";
|
|
7
|
+
import { TargetAuthorization, } from "./target-authorization.js";
|
|
6
8
|
const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
7
9
|
export class AuthService {
|
|
8
10
|
fetcher;
|
|
@@ -12,6 +14,7 @@ export class AuthService {
|
|
|
12
14
|
now;
|
|
13
15
|
wait;
|
|
14
16
|
loginTimeoutMs;
|
|
17
|
+
machineKeys;
|
|
15
18
|
constructor(options) {
|
|
16
19
|
this.fetcher = options.fetcher ?? fetch;
|
|
17
20
|
this.store = options.store;
|
|
@@ -20,33 +23,47 @@ export class AuthService {
|
|
|
20
23
|
this.now = options.now ?? Date.now;
|
|
21
24
|
this.wait = options.wait ?? waitFor;
|
|
22
25
|
this.loginTimeoutMs = options.loginTimeoutMs ?? OAUTH_LOGIN_TIMEOUT_MS;
|
|
26
|
+
this.machineKeys =
|
|
27
|
+
options.machineKeys ?? new OsMachineKeyProvider(this.store.root);
|
|
23
28
|
}
|
|
24
29
|
async login(email, signal) {
|
|
25
30
|
const boundEmail = email === undefined ? undefined : parseLoginEmail(email);
|
|
26
|
-
const
|
|
31
|
+
const machineKey = await this.machineKeys.create();
|
|
32
|
+
const pending = await this.startDevice(boundEmail, machineKey, signal);
|
|
27
33
|
this.output.loginBrowser(pending.verification_uri_complete);
|
|
28
34
|
await this.openBrowser(pending.verification_uri_complete).catch(() => false);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const { authorization_id, sub, machine_jkt } = await this.poll(pending, machineKey, signal);
|
|
36
|
+
if (machine_jkt !== machineKey.jkt)
|
|
37
|
+
throw invalidTokenResponse();
|
|
38
|
+
const grant = {
|
|
39
|
+
version: 2,
|
|
40
|
+
authorization_id,
|
|
41
|
+
sub,
|
|
42
|
+
machine_key: machineKey,
|
|
43
|
+
};
|
|
44
|
+
const user = await this.fetchIdentity(grant, signal);
|
|
45
|
+
if (user.id !== sub)
|
|
46
|
+
throw invalidTokenResponse();
|
|
47
|
+
await this.store.writeAuthorization(grant);
|
|
48
|
+
await this.store.removeLogin();
|
|
32
49
|
this.output.loginComplete(user);
|
|
33
50
|
return user;
|
|
34
51
|
}
|
|
35
52
|
async status(signal) {
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
53
|
+
const grant = await this.store.readAuthorization();
|
|
54
|
+
if (!grant) {
|
|
38
55
|
this.output.authStatus("signed_out");
|
|
39
56
|
return undefined;
|
|
40
57
|
}
|
|
41
58
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
this.output.authStatus("signed_in", user, active.expires_at);
|
|
59
|
+
const user = await this.fetchIdentity(grant, signal);
|
|
60
|
+
this.output.authStatus("signed_in", user);
|
|
45
61
|
return user;
|
|
46
62
|
}
|
|
47
63
|
catch (error) {
|
|
48
64
|
if (error instanceof TeamClawsError &&
|
|
49
65
|
error.code === "invalid_authentication") {
|
|
66
|
+
await this.store.removeAuthorization();
|
|
50
67
|
await this.store.removeLogin();
|
|
51
68
|
this.output.authStatus("signed_out");
|
|
52
69
|
return undefined;
|
|
@@ -54,48 +71,30 @@ export class AuthService {
|
|
|
54
71
|
throw error;
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
|
-
async logout(signal) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
74
|
+
async logout(signal, revokeGrant = false) {
|
|
75
|
+
const grant = await this.store.readAuthorization();
|
|
76
|
+
if (revokeGrant && grant) {
|
|
77
|
+
const url = `${API_V2_BASE}authorizations/${grant.authorization_id}/revoke/`;
|
|
78
|
+
const signer = new TargetAuthorization(this.store, this.fetcher, this.machineKeys, this.now);
|
|
79
|
+
const response = await requestJson(this.fetcher, url, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: await signer.accountHeadersForGrant(grant, "POST", url),
|
|
82
|
+
...(signal ? { signal } : {}),
|
|
83
|
+
});
|
|
84
|
+
if (response.status !== 204)
|
|
85
|
+
throw errorFromResponse(response, "remote_logout_unconfirmed");
|
|
67
86
|
}
|
|
68
87
|
await this.store.removeLogin();
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
async accessToken(signal) {
|
|
73
|
-
const credential = await this.store.readLogin();
|
|
74
|
-
if (!credential)
|
|
75
|
-
return undefined;
|
|
76
|
-
const active = await this.ensureActive(credential, signal);
|
|
77
|
-
return active.access_token;
|
|
78
|
-
}
|
|
79
|
-
async requireAccessToken(message, signal) {
|
|
80
|
-
const token = await this.accessToken(signal);
|
|
81
|
-
if (!token) {
|
|
82
|
-
throw new TeamClawsError("login_required", message, { exitCode: 3 });
|
|
83
|
-
}
|
|
84
|
-
await this.fetchIdentity(token, signal);
|
|
85
|
-
return token;
|
|
86
|
-
}
|
|
87
|
-
async ensureActive(credential, signal) {
|
|
88
|
-
const expiresAt = Date.parse(credential.expires_at);
|
|
89
|
-
if (expiresAt - this.now() > 30_000)
|
|
90
|
-
return credential;
|
|
91
|
-
const refreshed = await this.refresh(credential.refresh_token, signal);
|
|
92
|
-
await this.store.writeLogin(refreshed);
|
|
93
|
-
return refreshed;
|
|
88
|
+
await this.store.removeAuthorization();
|
|
89
|
+
this.output.logoutComplete(true);
|
|
90
|
+
return true;
|
|
94
91
|
}
|
|
95
|
-
async startDevice(email, signal) {
|
|
92
|
+
async startDevice(email, machineKey, signal) {
|
|
96
93
|
const form = new URLSearchParams({
|
|
97
94
|
client_id: OAUTH_CLIENT_ID,
|
|
98
95
|
resource: API_RESOURCE,
|
|
96
|
+
scope: "basic",
|
|
97
|
+
machine_jwk: JSON.stringify(machineKey.public_jwk),
|
|
99
98
|
});
|
|
100
99
|
if (email)
|
|
101
100
|
form.set("email", email);
|
|
@@ -109,7 +108,7 @@ export class AuthService {
|
|
|
109
108
|
}
|
|
110
109
|
return parseDeviceAuthorization(response.body);
|
|
111
110
|
}
|
|
112
|
-
async poll(pending, signal) {
|
|
111
|
+
async poll(pending, machineKey, signal) {
|
|
113
112
|
const startedAt = this.now();
|
|
114
113
|
const deadline = Math.min(startedAt + pending.expires_in * 1_000, startedAt + this.loginTimeoutMs);
|
|
115
114
|
let intervalSeconds = pending.interval;
|
|
@@ -119,6 +118,9 @@ export class AuthService {
|
|
|
119
118
|
break;
|
|
120
119
|
const response = await requestJson(this.fetcher, OAUTH_TOKEN_URL, {
|
|
121
120
|
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
DPoP: await new TargetAuthorization(this.store, this.fetcher, this.machineKeys, this.now).tokenDpop(machineKey),
|
|
123
|
+
},
|
|
122
124
|
form: new URLSearchParams({
|
|
123
125
|
grant_type: DEVICE_CODE_GRANT_TYPE,
|
|
124
126
|
device_code: pending.device_code,
|
|
@@ -128,7 +130,14 @@ export class AuthService {
|
|
|
128
130
|
...(signal ? { signal } : {}),
|
|
129
131
|
});
|
|
130
132
|
if (response.status >= 200 && response.status < 300) {
|
|
131
|
-
|
|
133
|
+
if (!isObject(response.body))
|
|
134
|
+
throw invalidTokenResponse();
|
|
135
|
+
validateBootstrapTokens(response.body);
|
|
136
|
+
return {
|
|
137
|
+
authorization_id: requireString(response.body.authorization_id, "authorization ID", 36),
|
|
138
|
+
sub: requireString(response.body.sub, "user subject", 255),
|
|
139
|
+
machine_jkt: requireString(response.body.machine_jkt, "machine key thumbprint", 43),
|
|
140
|
+
};
|
|
132
141
|
}
|
|
133
142
|
const code = oauthErrorCode(response.body);
|
|
134
143
|
if (code === "authorization_pending") {
|
|
@@ -148,65 +157,28 @@ export class AuthService {
|
|
|
148
157
|
}
|
|
149
158
|
throw new TeamClawsError("login_timeout", "CLI sign-in timed out after 10 minutes.", { exitCode: 3 });
|
|
150
159
|
}
|
|
151
|
-
async
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
grant_type: "refresh_token",
|
|
157
|
-
refresh_token: refreshToken,
|
|
158
|
-
client_id: OAUTH_CLIENT_ID,
|
|
159
|
-
resource: API_RESOURCE,
|
|
160
|
-
}),
|
|
161
|
-
...(signal ? { signal } : {}),
|
|
162
|
-
});
|
|
163
|
-
if (response.status < 200 || response.status >= 300) {
|
|
164
|
-
throw oauthError(response.status, response.body);
|
|
165
|
-
}
|
|
166
|
-
return parseTokenResponse(response.body, this.now());
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
if (error instanceof TeamClawsError &&
|
|
170
|
-
error.code === "invalid_authentication") {
|
|
171
|
-
await this.store.removeLogin();
|
|
172
|
-
}
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
async fetchIdentity(accessToken, signal) {
|
|
177
|
-
const response = await requestJson(this.fetcher, `${API_BASE}me/`, {
|
|
178
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
160
|
+
async fetchIdentity(grant, signal) {
|
|
161
|
+
const url = `${API_BASE}me/`;
|
|
162
|
+
const signer = new TargetAuthorization(this.store, this.fetcher, this.machineKeys, this.now);
|
|
163
|
+
const response = await requestJson(this.fetcher, url, {
|
|
164
|
+
headers: await signer.accountHeadersForGrant(grant, "GET", url),
|
|
179
165
|
...(signal ? { signal } : {}),
|
|
180
166
|
});
|
|
181
167
|
if (response.status === 401) {
|
|
182
|
-
throw new TeamClawsError("invalid_authentication", "The TeamClaws
|
|
168
|
+
throw new TeamClawsError("invalid_authentication", "The TeamClaws CLI authorization is inactive. Run `teamclaws login` to authorize this computer again.", {
|
|
183
169
|
exitCode: 3,
|
|
184
170
|
});
|
|
185
171
|
}
|
|
186
172
|
if (response.status < 200 || response.status >= 300) {
|
|
187
173
|
throw errorFromResponse(response, "identity_unavailable");
|
|
188
174
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
headers: {
|
|
197
|
-
Accept: "application/json",
|
|
198
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
199
|
-
},
|
|
200
|
-
body: new URLSearchParams({
|
|
201
|
-
token: refreshToken,
|
|
202
|
-
client_id: OAUTH_CLIENT_ID,
|
|
203
|
-
}),
|
|
204
|
-
redirect: "error",
|
|
205
|
-
signal: combined,
|
|
206
|
-
});
|
|
207
|
-
if (response.status < 200 || response.status >= 300) {
|
|
208
|
-
throw new TeamClawsError("remote_logout_unconfirmed", "Remote logout was not confirmed.");
|
|
209
|
-
}
|
|
175
|
+
if (!isObject(response.body) ||
|
|
176
|
+
response.body.authorization_id !== grant.authorization_id)
|
|
177
|
+
throw invalidTokenResponse();
|
|
178
|
+
const user = parseSafeUserResponse(response.body);
|
|
179
|
+
if (user.id !== grant.sub)
|
|
180
|
+
throw invalidTokenResponse();
|
|
181
|
+
return user;
|
|
210
182
|
}
|
|
211
183
|
}
|
|
212
184
|
function parseDeviceAuthorization(body) {
|
|
@@ -239,21 +211,14 @@ function parseDeviceAuthorization(body) {
|
|
|
239
211
|
interval: requireInteger(body.interval, "interval", 1, 60),
|
|
240
212
|
};
|
|
241
213
|
}
|
|
242
|
-
function
|
|
214
|
+
function validateBootstrapTokens(body) {
|
|
243
215
|
if (!isObject(body))
|
|
244
216
|
throw invalidTokenResponse();
|
|
245
|
-
|
|
246
|
-
|
|
217
|
+
requireToken(body.access_token);
|
|
218
|
+
requireToken(body.refresh_token);
|
|
247
219
|
if (body.token_type !== "Bearer")
|
|
248
220
|
throw invalidTokenResponse();
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
version: 1,
|
|
252
|
-
token_type: "Bearer",
|
|
253
|
-
access_token: accessToken,
|
|
254
|
-
refresh_token: refreshToken,
|
|
255
|
-
expires_at: new Date(now + expiresIn * 1_000).toISOString(),
|
|
256
|
-
};
|
|
221
|
+
requireInteger(body.expires_in, "access-token lifetime", 1, 86_400);
|
|
257
222
|
}
|
|
258
223
|
function parseLoginEmail(value) {
|
|
259
224
|
const email = value.trim();
|
package/dist/broker.js
CHANGED
|
@@ -7,20 +7,23 @@ import { assertExactKeys, isObject, normalizeUuid, parsePendingPairing, parseWor
|
|
|
7
7
|
export class BrokerClient {
|
|
8
8
|
fetcher;
|
|
9
9
|
deviceName;
|
|
10
|
+
authorization;
|
|
10
11
|
constructor(options = {}) {
|
|
11
12
|
this.fetcher = options.fetcher ?? fetch;
|
|
12
13
|
this.deviceName = options.deviceName ?? hostname;
|
|
14
|
+
this.authorization = options.authorization;
|
|
13
15
|
}
|
|
14
16
|
currentDeviceName() {
|
|
15
17
|
return normalizedDeviceName(this.deviceName());
|
|
16
18
|
}
|
|
17
|
-
async servers(
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
async servers(signal) {
|
|
20
|
+
const url = `${API_BASE}servers/?include_inventory=1&include_description=1&include_editable_metadata=1`;
|
|
21
|
+
const response = await requestJson(this.fetcher, url, {
|
|
22
|
+
headers: await this.issuerHeaders("GET", url),
|
|
20
23
|
...(signal ? { signal } : {}),
|
|
21
24
|
});
|
|
22
25
|
if (response.status === 401) {
|
|
23
|
-
throw new TeamClawsError("invalid_authentication", "The TeamClaws
|
|
26
|
+
throw new TeamClawsError("invalid_authentication", "The TeamClaws CLI authorization is inactive. Run `teamclaws login` to authorize this computer again.", {
|
|
24
27
|
exitCode: 3,
|
|
25
28
|
});
|
|
26
29
|
}
|
|
@@ -29,7 +32,7 @@ export class BrokerClient {
|
|
|
29
32
|
}
|
|
30
33
|
return parseWorkspaceInventory(response.body);
|
|
31
34
|
}
|
|
32
|
-
async updateServerMetadata(
|
|
35
|
+
async updateServerMetadata(serverId, updates, signal) {
|
|
33
36
|
if (Object.keys(updates).length === 0 ||
|
|
34
37
|
(updates.alias !== undefined && [...updates.alias.trim()].length > 100) ||
|
|
35
38
|
(updates.description !== undefined &&
|
|
@@ -37,9 +40,10 @@ export class BrokerClient {
|
|
|
37
40
|
throw new TeamClawsError("invalid_server_metadata", "Provide a name of 100 characters or fewer and/or a description of 2000 characters or fewer.", { exitCode: 2 });
|
|
38
41
|
}
|
|
39
42
|
const id = normalizeUuid(serverId);
|
|
40
|
-
const
|
|
43
|
+
const url = `${API_BASE}servers/${id}/metadata/`;
|
|
44
|
+
const response = await requestJson(this.fetcher, url, {
|
|
41
45
|
method: "POST",
|
|
42
|
-
headers:
|
|
46
|
+
headers: await this.issuerHeaders("POST", url),
|
|
43
47
|
body: updates,
|
|
44
48
|
...(signal ? { signal } : {}),
|
|
45
49
|
});
|
|
@@ -50,11 +54,12 @@ export class BrokerClient {
|
|
|
50
54
|
assertExactKeys(response.body, ["server"], [], "server metadata response");
|
|
51
55
|
return editableServer(response.body.server, id);
|
|
52
56
|
}
|
|
53
|
-
async claimMetadataPrompt(
|
|
57
|
+
async claimMetadataPrompt(serverId, signal) {
|
|
54
58
|
const id = normalizeUuid(serverId);
|
|
55
|
-
const
|
|
59
|
+
const url = `${API_BASE}servers/${id}/metadata-prompt/claim/`;
|
|
60
|
+
const response = await requestJson(this.fetcher, url, {
|
|
56
61
|
method: "POST",
|
|
57
|
-
headers:
|
|
62
|
+
headers: await this.issuerHeaders("POST", url),
|
|
58
63
|
body: {},
|
|
59
64
|
...(signal ? { signal } : {}),
|
|
60
65
|
});
|
|
@@ -70,11 +75,18 @@ export class BrokerClient {
|
|
|
70
75
|
server: editableServer(response.body.server, id),
|
|
71
76
|
};
|
|
72
77
|
}
|
|
73
|
-
async startPairing(target,
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
async startPairing(target, signal) {
|
|
79
|
+
if (!this.authorization)
|
|
80
|
+
throw new TeamClawsError("login_required", "CLI authorization is required for pairing.", { exitCode: 3 });
|
|
81
|
+
const grant = await this.authorization.requireGrant();
|
|
82
|
+
const request = pairingStartRequest(target, this.currentDeviceName(), grant);
|
|
83
|
+
const url = `${API_BASE}pairing/start/`;
|
|
84
|
+
const response = await requestJson(this.fetcher, url, {
|
|
76
85
|
method: "POST",
|
|
77
|
-
headers:
|
|
86
|
+
headers: {
|
|
87
|
+
...requestHeaders(),
|
|
88
|
+
DPoP: await this.authorization.dpop(grant.machine_key, "POST", url),
|
|
89
|
+
},
|
|
78
90
|
body: request,
|
|
79
91
|
...(signal ? { signal } : {}),
|
|
80
92
|
});
|
|
@@ -91,6 +103,14 @@ export class BrokerClient {
|
|
|
91
103
|
}
|
|
92
104
|
return pending;
|
|
93
105
|
}
|
|
106
|
+
async issuerHeaders(method, url) {
|
|
107
|
+
if (!this.authorization)
|
|
108
|
+
throw new TeamClawsError("login_required", "CLI authorization is required.", { exitCode: 3 });
|
|
109
|
+
return {
|
|
110
|
+
...requestHeaders(),
|
|
111
|
+
...(await this.authorization.accountHeadersFor(method, url)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
94
114
|
}
|
|
95
115
|
function invalidMetadataResponse() {
|
|
96
116
|
return new TeamClawsError("invalid_response", "TeamClaws returned invalid server metadata.", { exitCode: 9 });
|
|
@@ -114,7 +134,7 @@ function editableServer(value, expectedId) {
|
|
|
114
134
|
can_edit_metadata: true,
|
|
115
135
|
};
|
|
116
136
|
}
|
|
117
|
-
export function pairingStartRequest(target, deviceName) {
|
|
137
|
+
export function pairingStartRequest(target, deviceName, grant) {
|
|
118
138
|
return {
|
|
119
139
|
contract_version: CONTRACT_VERSION,
|
|
120
140
|
target,
|
|
@@ -122,6 +142,7 @@ export function pairingStartRequest(target, deviceName) {
|
|
|
122
142
|
device_name: deviceName,
|
|
123
143
|
client_name: CLIENT_NAME,
|
|
124
144
|
client_version: PACKAGE_VERSION,
|
|
145
|
+
authorization_id: grant.authorization_id,
|
|
125
146
|
};
|
|
126
147
|
}
|
|
127
148
|
export function normalizedDeviceName(value) {
|
|
@@ -131,11 +152,9 @@ export function normalizedDeviceName(value) {
|
|
|
131
152
|
.slice(0, MAX_DEVICE_NAME_LENGTH);
|
|
132
153
|
return normalized || "Unnamed computer";
|
|
133
154
|
}
|
|
134
|
-
function requestHeaders(
|
|
155
|
+
function requestHeaders() {
|
|
135
156
|
const headers = {
|
|
136
157
|
"User-Agent": `${CLIENT_NAME}/${PACKAGE_VERSION}`.slice(0, MAX_CLIENT_LABEL_LENGTH),
|
|
137
158
|
};
|
|
138
|
-
if (accessToken)
|
|
139
|
-
headers.Authorization = `Bearer ${accessToken}`;
|
|
140
159
|
return headers;
|
|
141
160
|
}
|
package/dist/constants.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "teamclaws";
|
|
2
|
-
export const PACKAGE_VERSION = "
|
|
2
|
+
export const PACKAGE_VERSION = "2.0.0";
|
|
3
3
|
export const CLIENT_NAME = "teamclaws-cli";
|
|
4
4
|
export const CONTRACT_VERSION = 1;
|
|
5
5
|
export const OUTPUT_SCHEMA_VERSION = 1;
|
|
6
6
|
export const TEAMCLAWS_ORIGIN = "https://www.teamclaws.com";
|
|
7
7
|
export const API_RESOURCE = `${TEAMCLAWS_ORIGIN}/api/cli/v1`;
|
|
8
8
|
export const API_BASE = `${API_RESOURCE}/`;
|
|
9
|
+
export const API_V2_BASE = `${TEAMCLAWS_ORIGIN}/api/cli/v2/`;
|
|
9
10
|
export const OAUTH_CLIENT_ID = "teamclaws-cli";
|
|
10
11
|
export const OAUTH_DEVICE_AUTHORIZATION_URL = `${TEAMCLAWS_ORIGIN}/o/device-authorization/`;
|
|
11
12
|
export const OAUTH_TOKEN_URL = `${TEAMCLAWS_ORIGIN}/o/token/`;
|