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 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 valid TeamClaws CLI account login. Run `teamclaws login` first. The CLI
118
- refreshes and verifies that login before contacting the workspace or requesting
119
- pairing approval. Missing or invalid authentication exits with code 3 in both
120
- human and JSON modes. Help, version, login, auth status, and logout remain
121
- available. Saved workspace credentials are preserved when login is required.
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
- After account authentication, `pair status` reads the matching protected local
133
- workspace credential, performs at most one direct workspace verification with a
134
- five-second timeout, reports the result, and stops. It never starts pairing or
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
- Login credentials are stored in the private TeamClaws user directory. Completed
168
- workspace credentials are stored by stable server identifier beneath
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
- const token = await auth.requireAccessToken(parsed.action === "pair"
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(token, signal);
123
- await offerServerMetadata(inventory.servers, broker, token, parsed.json, output, dependencies.io, signal);
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(token, parsed.serverId, parsed.updates, signal);
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, token, parsed.json, output, dependencies.io, signal);
136
- const pending = await broker.startPairing(target, token, signal);
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" && args.length === 1)
228
- return { action: "logout", json };
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, token, json, output, io, signal) {
386
- const inventory = await broker.servers(token, signal);
387
- await offerServerMetadata(inventory.servers, broker, token, json, output, io, signal);
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, token, json, output, io, signal) {
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(token, server.server_id, signal);
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(token, server.server_id, updates, signal);
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 { ACCESS_TOKEN_MAX_LIFETIME_SECONDS, API_BASE, API_RESOURCE, OAUTH_CLIENT_ID, OAUTH_DEVICE_AUTHORIZATION_URL, OAUTH_LOGIN_TIMEOUT_MS, OAUTH_REVOKE_URL, OAUTH_TOKEN_URL, TEAMCLAWS_ORIGIN, } from "./constants.js";
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 pending = await this.startDevice(boundEmail, signal);
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 credential = await this.poll(pending, signal);
30
- const user = await this.fetchIdentity(credential.access_token, signal);
31
- await this.store.writeLogin(credential);
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 credential = await this.store.readLogin();
37
- if (!credential) {
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 active = await this.ensureActive(credential, signal);
43
- const user = await this.fetchIdentity(active.access_token, signal);
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 credential = await this.store.readLogin();
59
- let remoteConfirmed = true;
60
- if (credential) {
61
- try {
62
- await this.revoke(credential.refresh_token, signal);
63
- }
64
- catch {
65
- remoteConfirmed = false;
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.output.logoutComplete(remoteConfirmed);
70
- return remoteConfirmed;
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
- return parseTokenResponse(response.body, this.now());
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 refresh(refreshToken, signal) {
152
- try {
153
- const response = await requestJson(this.fetcher, OAUTH_TOKEN_URL, {
154
- method: "POST",
155
- form: new URLSearchParams({
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 login is no longer valid. Run `teamclaws login` to authenticate.", {
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
- return parseSafeUserResponse(response.body);
190
- }
191
- async revoke(refreshToken, signal) {
192
- const timeout = AbortSignal.timeout(10_000);
193
- const combined = signal ? AbortSignal.any([timeout, signal]) : timeout;
194
- const response = await this.fetcher(OAUTH_REVOKE_URL, {
195
- method: "POST",
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 parseTokenResponse(body, now) {
214
+ function validateBootstrapTokens(body) {
243
215
  if (!isObject(body))
244
216
  throw invalidTokenResponse();
245
- const accessToken = requireToken(body.access_token);
246
- const refreshToken = requireToken(body.refresh_token);
217
+ requireToken(body.access_token);
218
+ requireToken(body.refresh_token);
247
219
  if (body.token_type !== "Bearer")
248
220
  throw invalidTokenResponse();
249
- const expiresIn = Math.min(requireInteger(body.expires_in, "access-token lifetime", 1, 86_400), ACCESS_TOKEN_MAX_LIFETIME_SECONDS);
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(accessToken, signal) {
18
- const response = await requestJson(this.fetcher, `${API_BASE}servers/?include_inventory=1&include_description=1&include_editable_metadata=1`, {
19
- headers: requestHeaders(accessToken),
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 login is no longer valid. Run `teamclaws login` to authenticate.", {
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(accessToken, serverId, updates, signal) {
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 response = await requestJson(this.fetcher, `${API_BASE}servers/${id}/metadata/`, {
43
+ const url = `${API_BASE}servers/${id}/metadata/`;
44
+ const response = await requestJson(this.fetcher, url, {
41
45
  method: "POST",
42
- headers: requestHeaders(accessToken),
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(accessToken, serverId, signal) {
57
+ async claimMetadataPrompt(serverId, signal) {
54
58
  const id = normalizeUuid(serverId);
55
- const response = await requestJson(this.fetcher, `${API_BASE}servers/${id}/metadata-prompt/claim/`, {
59
+ const url = `${API_BASE}servers/${id}/metadata-prompt/claim/`;
60
+ const response = await requestJson(this.fetcher, url, {
56
61
  method: "POST",
57
- headers: requestHeaders(accessToken),
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, accessToken, signal) {
74
- const request = pairingStartRequest(target, this.currentDeviceName());
75
- const response = await requestJson(this.fetcher, `${API_BASE}pairing/start/`, {
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: requestHeaders(accessToken),
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(accessToken) {
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 = "1.0.3";
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/`;