teamclaws 1.0.4 → 2.0.1
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 +92 -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 +24 -8
- package/skills/teamclaws-onboard/SKILL.md +89 -19
- package/skills/teamclaws-onboard/references/workspace-uses.md +164 -0
- package/skills/teamclaws-pair/SKILL.md +88 -19
- package/skills/teamclaws-project/SKILL.md +37 -3
package/dist/pairing.js
CHANGED
|
@@ -4,6 +4,7 @@ import { PAIRING_STATUS_TIMEOUT_MS } from "./constants.js";
|
|
|
4
4
|
import { errorFromResponse, requestJson, } from "./http.js";
|
|
5
5
|
import { directTargetUrl, isObject, parsePairingCredential, parseVerifiedDevice, parseVerifiedWorkspace, requireInteger, } from "./validation.js";
|
|
6
6
|
import { openExternalBrowser } from "./browser.js";
|
|
7
|
+
import { TargetAuthorization, requestTargetJson, } from "./target-authorization.js";
|
|
7
8
|
export class PairingClient {
|
|
8
9
|
fetcher;
|
|
9
10
|
store;
|
|
@@ -11,6 +12,7 @@ export class PairingClient {
|
|
|
11
12
|
openBrowser;
|
|
12
13
|
now;
|
|
13
14
|
wait;
|
|
15
|
+
authorization;
|
|
14
16
|
constructor(options) {
|
|
15
17
|
this.fetcher = options.fetcher ?? fetch;
|
|
16
18
|
this.store = options.store;
|
|
@@ -18,13 +20,18 @@ export class PairingClient {
|
|
|
18
20
|
this.openBrowser = options.openBrowser ?? openExternalBrowser;
|
|
19
21
|
this.now = options.now ?? Date.now;
|
|
20
22
|
this.wait = options.wait ?? waitFor;
|
|
23
|
+
this.authorization =
|
|
24
|
+
options.authorization ??
|
|
25
|
+
new TargetAuthorization(this.store, this.fetcher);
|
|
21
26
|
}
|
|
22
27
|
async status(target, signal) {
|
|
23
28
|
let credential;
|
|
24
29
|
try {
|
|
25
30
|
credential = await this.store.findWorkspace(target);
|
|
26
31
|
}
|
|
27
|
-
catch {
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof TeamClawsError && error.code === "re_pair_required")
|
|
34
|
+
throw error;
|
|
28
35
|
this.output.pairingStatus("unreachable");
|
|
29
36
|
return "unreachable";
|
|
30
37
|
}
|
|
@@ -38,10 +45,8 @@ export class PairingClient {
|
|
|
38
45
|
display_name: credential.display_name,
|
|
39
46
|
};
|
|
40
47
|
try {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
Authorization: `Bearer ${credential.device_id}:${credential.device_token}`,
|
|
44
|
-
},
|
|
48
|
+
const url = `${credential.origin}/teamclaws-admin/api/device-pairings/me/`;
|
|
49
|
+
const response = await requestTargetJson(this.fetcher, this.authorization, url, credential, {
|
|
45
50
|
timeoutMs: PAIRING_STATUS_TIMEOUT_MS,
|
|
46
51
|
...(signal ? { signal } : {}),
|
|
47
52
|
});
|
|
@@ -63,6 +68,9 @@ export class PairingClient {
|
|
|
63
68
|
catch (error) {
|
|
64
69
|
if (signal?.aborted)
|
|
65
70
|
throw error;
|
|
71
|
+
if (error instanceof TeamClawsError &&
|
|
72
|
+
!["network_error", "network_timeout"].includes(error.code))
|
|
73
|
+
throw error;
|
|
66
74
|
this.output.pairingStatus("unreachable", workspace);
|
|
67
75
|
return "unreachable";
|
|
68
76
|
}
|
|
@@ -74,50 +82,52 @@ export class PairingClient {
|
|
|
74
82
|
let credential;
|
|
75
83
|
try {
|
|
76
84
|
credential = await this.poll(pending, signal);
|
|
85
|
+
const grant = await this.authorization.requireGrant();
|
|
86
|
+
if (credential.pairing_id !== credential.device_id ||
|
|
87
|
+
credential.server_id !== pending.server_id ||
|
|
88
|
+
credential.origin !== pending.target.origin ||
|
|
89
|
+
credential.requester_sub !== grant.sub ||
|
|
90
|
+
credential.authorization_id !== grant.authorization_id ||
|
|
91
|
+
credential.machine_jkt !== grant.machine_key.jkt)
|
|
92
|
+
throw verificationError();
|
|
77
93
|
const initial = {
|
|
78
|
-
version:
|
|
94
|
+
version: 2,
|
|
79
95
|
server_id: pending.server_id,
|
|
80
96
|
origin: pending.target.origin,
|
|
81
97
|
display_name: deviceName,
|
|
82
98
|
device_id: credential.device_id,
|
|
99
|
+
pairing_id: credential.pairing_id,
|
|
83
100
|
device_token: credential.device_token,
|
|
101
|
+
requester_sub: credential.requester_sub,
|
|
102
|
+
authorization_id: credential.authorization_id,
|
|
103
|
+
machine_jkt: credential.machine_jkt,
|
|
84
104
|
created_at: new Date(this.now()).toISOString(),
|
|
85
105
|
};
|
|
86
|
-
await this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (typeof timestamp === "string" &&
|
|
105
|
-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|\+00:00)$/.test(timestamp) &&
|
|
106
|
-
Number.isFinite(Date.parse(timestamp))) {
|
|
107
|
-
snapshot = parseCapabilityInventory(credential.external_chat_capabilities);
|
|
108
|
-
generatedAt = timestamp;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// Optional inventory must not invalidate a successfully verified credential.
|
|
106
|
+
const verification = await this.verify(pending, initial, signal);
|
|
107
|
+
const verified = {
|
|
108
|
+
...initial,
|
|
109
|
+
display_name: verification.display_name,
|
|
110
|
+
};
|
|
111
|
+
await this.store.writeWorkspace(verified);
|
|
112
|
+
let snapshot;
|
|
113
|
+
let generatedAt;
|
|
114
|
+
if (Object.hasOwn(credential, "external_chat_capabilities")) {
|
|
115
|
+
snapshot = null;
|
|
116
|
+
generatedAt = null;
|
|
117
|
+
try {
|
|
118
|
+
const timestamp = credential.external_chat_capabilities_generated_at;
|
|
119
|
+
if (typeof timestamp === "string" &&
|
|
120
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|\+00:00)$/.test(timestamp) &&
|
|
121
|
+
Number.isFinite(Date.parse(timestamp))) {
|
|
122
|
+
snapshot = parseCapabilityInventory(credential.external_chat_capabilities);
|
|
123
|
+
generatedAt = timestamp;
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
await this.store.removeWorkspace(pending.server_id);
|
|
119
|
-
throw error;
|
|
126
|
+
catch {
|
|
127
|
+
// Optional inventory must not invalidate a successfully verified credential.
|
|
128
|
+
}
|
|
120
129
|
}
|
|
130
|
+
this.output.pairingComplete(pending.server_id, pending.target.origin, verification.display_name, verification.external_chat_handoff, snapshot, generatedAt);
|
|
121
131
|
}
|
|
122
132
|
catch (error) {
|
|
123
133
|
if (signal?.aborted) {
|
|
@@ -173,10 +183,8 @@ export class PairingClient {
|
|
|
173
183
|
throw errorFromResponse(response, "cancel_failed");
|
|
174
184
|
}
|
|
175
185
|
async verify(pending, credential, signal) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
Authorization: `Bearer ${credential.device_id}:${credential.device_token}`,
|
|
179
|
-
},
|
|
186
|
+
const url = directTargetUrl(pending.target, pending.target.credential_verification_path);
|
|
187
|
+
const response = await requestTargetJson(this.fetcher, this.authorization, url, credential, {
|
|
180
188
|
...(signal ? { signal } : {}),
|
|
181
189
|
});
|
|
182
190
|
if (response.status < 200 || response.status >= 300)
|
package/dist/shell.js
CHANGED
|
@@ -2,18 +2,17 @@ import { request as httpRequest } from "node:http";
|
|
|
2
2
|
import { request as httpsRequest } from "node:https";
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
4
|
import { TeamClawsError } from "./errors.js";
|
|
5
|
-
import { errorFromResponse
|
|
5
|
+
import { errorFromResponse } from "./http.js";
|
|
6
|
+
import { requestTargetJson } from "./target-authorization.js";
|
|
6
7
|
import { isObject } from "./validation.js";
|
|
7
|
-
export async function runShell(store, fetcher, target, command, signal) {
|
|
8
|
+
export async function runShell(store, fetcher, target, command, authorization, signal) {
|
|
8
9
|
const credential = await store.findWorkspace(target);
|
|
9
10
|
if (!credential) {
|
|
10
11
|
throw new TeamClawsError("re_pair_required", "No saved pairing was found for this workspace. Pair it first.", { exitCode: 3 });
|
|
11
12
|
}
|
|
12
|
-
const
|
|
13
|
+
const url = `${credential.origin}/teamclaws-admin/api/shell/`;
|
|
14
|
+
const response = await requestTargetJson(fetcher, authorization, url, credential, {
|
|
13
15
|
method: "POST",
|
|
14
|
-
headers: {
|
|
15
|
-
Authorization: `Bearer ${credential.device_id}:${credential.device_token}`,
|
|
16
|
-
},
|
|
17
16
|
textBody: command,
|
|
18
17
|
timeoutMs: null,
|
|
19
18
|
responseLimitBytes: null,
|
package/dist/storage.js
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { TeamClawsError } from "./errors.js";
|
|
8
|
+
import { parseDurableAuthorization, } from "./target-authorization.js";
|
|
8
9
|
import { isObject, requireHttpsUrl, requireInteger, normalizeUuid, parseLoginCredential, parseStatusWorkspaceCredential, parseWorkspaceCredential, } from "./validation.js";
|
|
9
10
|
const execFile = promisify(execFileCallback);
|
|
10
11
|
const SYSTEM_SID = "S-1-5-18";
|
|
@@ -13,11 +14,13 @@ const UUID_CREDENTIAL_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0
|
|
|
13
14
|
export class CredentialStore {
|
|
14
15
|
root;
|
|
15
16
|
loginPath;
|
|
17
|
+
authorizationPath;
|
|
16
18
|
workspaceDirectory;
|
|
17
19
|
platform;
|
|
18
20
|
constructor(options = {}) {
|
|
19
21
|
this.root = options.root ?? defaultRoot();
|
|
20
22
|
this.loginPath = join(this.root, "cli-auth.json");
|
|
23
|
+
this.authorizationPath = join(this.root, "cli-authorization.json");
|
|
21
24
|
this.workspaceDirectory = join(this.root, "clawgate-devices");
|
|
22
25
|
this.platform = options.platform ?? process.platform;
|
|
23
26
|
}
|
|
@@ -34,6 +37,19 @@ export class CredentialStore {
|
|
|
34
37
|
throw storageError();
|
|
35
38
|
});
|
|
36
39
|
}
|
|
40
|
+
async readAuthorization() {
|
|
41
|
+
const value = await this.readJsonIfPresent(this.authorizationPath);
|
|
42
|
+
return value === undefined ? undefined : parseDurableAuthorization(value);
|
|
43
|
+
}
|
|
44
|
+
async writeAuthorization(grant) {
|
|
45
|
+
await this.writePrivateJson(this.authorizationPath, parseDurableAuthorization(grant));
|
|
46
|
+
}
|
|
47
|
+
async removeAuthorization() {
|
|
48
|
+
await unlink(this.authorizationPath).catch((error) => {
|
|
49
|
+
if (error.code !== "ENOENT")
|
|
50
|
+
throw storageError();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
37
53
|
workspacePath(serverId) {
|
|
38
54
|
return join(this.workspaceDirectory, `${normalizeUuid(serverId)}.json`);
|
|
39
55
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { API_V2_BASE, OAUTH_TOKEN_URL } from "./constants.js";
|
|
3
|
+
import { TeamClawsError } from "./errors.js";
|
|
4
|
+
import { errorFromResponse, requestJson, } from "./http.js";
|
|
5
|
+
import { OsMachineKeyProvider, jwkThumbprint, } from "./machine-key.js";
|
|
6
|
+
import { isObject } from "./validation.js";
|
|
7
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
8
|
+
export class TargetAuthorization {
|
|
9
|
+
fetcher;
|
|
10
|
+
store;
|
|
11
|
+
keys;
|
|
12
|
+
now;
|
|
13
|
+
constructor(store, fetcher = fetch, keys = new OsMachineKeyProvider(store.root), now = Date.now) {
|
|
14
|
+
this.store = store;
|
|
15
|
+
this.fetcher = fetcher;
|
|
16
|
+
this.keys = keys;
|
|
17
|
+
this.now = now;
|
|
18
|
+
}
|
|
19
|
+
async requireGrant() {
|
|
20
|
+
const grant = await this.store.readAuthorization();
|
|
21
|
+
if (!grant)
|
|
22
|
+
throw new TeamClawsError("login_required", "Approve this CLI at TeamClaws with `teamclaws login` before using a workspace.", { exitCode: 3 });
|
|
23
|
+
return grant;
|
|
24
|
+
}
|
|
25
|
+
async accountHeadersFor(method, url) {
|
|
26
|
+
return this.accountHeadersForGrant(await this.requireGrant(), method, url);
|
|
27
|
+
}
|
|
28
|
+
async accountHeadersForGrant(grant, method, url) {
|
|
29
|
+
if (new URL(url).origin !== new URL(API_V2_BASE).origin)
|
|
30
|
+
throw new TeamClawsError("invalid_target", "A TeamClaws issuer URL is required.", { exitCode: 9 });
|
|
31
|
+
return {
|
|
32
|
+
"X-TeamClaws-Authorization-Id": grant.authorization_id,
|
|
33
|
+
DPoP: await this.dpop(grant.machine_key, method, url),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
assertPairing(grant, credential) {
|
|
37
|
+
if (credential.version !== 2 ||
|
|
38
|
+
credential.requester_sub !== grant.sub ||
|
|
39
|
+
credential.authorization_id !== grant.authorization_id ||
|
|
40
|
+
credential.machine_jkt !== grant.machine_key.jkt ||
|
|
41
|
+
credential.pairing_id !== credential.device_id) {
|
|
42
|
+
throw new TeamClawsError("re_pair_required", "This pairing is not bound to the current TeamClaws authorization. Pair this workspace again.", { exitCode: 3 });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async headersFor(method, url, credential, signal) {
|
|
46
|
+
const grant = await this.requireGrant();
|
|
47
|
+
this.assertPairing(grant, credential);
|
|
48
|
+
if (new URL(url).origin !== credential.origin)
|
|
49
|
+
throw new TeamClawsError("target_mismatch", "The request target does not match the selected pairing.", { exitCode: 9 });
|
|
50
|
+
const pairingAuthorization = `Bearer ${credential.device_id}:${credential.device_token}`;
|
|
51
|
+
const exchangeUrl = `${API_V2_BASE}authorizations/${grant.authorization_id}/proofs/`;
|
|
52
|
+
const exchange = await requestJson(this.fetcher, exchangeUrl, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: {
|
|
55
|
+
DPoP: await this.dpop(grant.machine_key, "POST", exchangeUrl),
|
|
56
|
+
},
|
|
57
|
+
body: {
|
|
58
|
+
server_id: credential.server_id,
|
|
59
|
+
pairing_id: credential.pairing_id,
|
|
60
|
+
scope: "paired:invoke",
|
|
61
|
+
},
|
|
62
|
+
...(signal ? { signal } : {}),
|
|
63
|
+
});
|
|
64
|
+
if (exchange.status < 200 || exchange.status >= 300)
|
|
65
|
+
throw errorFromResponse(exchange, "target_proof_denied");
|
|
66
|
+
if (!isObject(exchange.body) ||
|
|
67
|
+
exchange.body.token_type !== "DPoP" ||
|
|
68
|
+
exchange.body.scope !== "paired:invoke" ||
|
|
69
|
+
typeof exchange.body.access_token !== "string" ||
|
|
70
|
+
exchange.body.access_token.length < 16 ||
|
|
71
|
+
exchange.body.access_token.length > 8192 ||
|
|
72
|
+
!Number.isInteger(exchange.body.expires_in) ||
|
|
73
|
+
exchange.body.expires_in < 1 ||
|
|
74
|
+
exchange.body.expires_in > 300)
|
|
75
|
+
throw new TeamClawsError("invalid_response", "TeamClaws returned an invalid target proof.", { exitCode: 9 });
|
|
76
|
+
const proof = exchange.body.access_token;
|
|
77
|
+
return {
|
|
78
|
+
Authorization: pairingAuthorization,
|
|
79
|
+
"X-TeamClaws-Target-Proof": proof,
|
|
80
|
+
DPoP: await this.dpop(grant.machine_key, method, url, proof, pairingAuthorization),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async dpop(key, method, url, targetProof, pairingAuthorization) {
|
|
84
|
+
const parsed = new URL(url);
|
|
85
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password)
|
|
86
|
+
throw new TeamClawsError("invalid_target", "A canonical HTTPS target is required.", { exitCode: 9 });
|
|
87
|
+
const header = { typ: "dpop+jwt", alg: "ES256", jwk: key.public_jwk };
|
|
88
|
+
const payload = {
|
|
89
|
+
htm: method.toUpperCase(),
|
|
90
|
+
htu: `${parsed.origin}${parsed.pathname}`,
|
|
91
|
+
iat: Math.floor(this.now() / 1000),
|
|
92
|
+
jti: randomUUID(),
|
|
93
|
+
};
|
|
94
|
+
if (targetProof !== undefined)
|
|
95
|
+
payload.ath = hashAscii(targetProof);
|
|
96
|
+
if (pairingAuthorization !== undefined)
|
|
97
|
+
payload.pairing_ath = hashAscii(pairingAuthorization);
|
|
98
|
+
const signingInput = `${base64Json(header)}.${base64Json(payload)}`;
|
|
99
|
+
return `${signingInput}.${await this.keys.sign(key, signingInput)}`;
|
|
100
|
+
}
|
|
101
|
+
async tokenDpop(key) {
|
|
102
|
+
return this.dpop(key, "POST", OAUTH_TOKEN_URL);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function requestTargetJson(fetcher, authorization, url, credential, options = {}) {
|
|
106
|
+
const method = options.method ?? "GET";
|
|
107
|
+
const attempt = async () => requestJson(fetcher, url, {
|
|
108
|
+
...options,
|
|
109
|
+
headers: {
|
|
110
|
+
...options.headers,
|
|
111
|
+
...(await authorization.headersFor(method, url, credential, options.signal)),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const first = await attempt();
|
|
115
|
+
if (first.status === 401 &&
|
|
116
|
+
isObject(first.body) &&
|
|
117
|
+
first.body.error === "target_proof_expired")
|
|
118
|
+
return attempt();
|
|
119
|
+
return first;
|
|
120
|
+
}
|
|
121
|
+
export function parseDurableAuthorization(value) {
|
|
122
|
+
if (!isObject(value) ||
|
|
123
|
+
value.version !== 2 ||
|
|
124
|
+
typeof value.authorization_id !== "string" ||
|
|
125
|
+
!uuid.test(value.authorization_id) ||
|
|
126
|
+
typeof value.sub !== "string" ||
|
|
127
|
+
value.sub.length < 1 ||
|
|
128
|
+
value.sub.length > 255 ||
|
|
129
|
+
!isObject(value.machine_key) ||
|
|
130
|
+
!["windows-cng", "posix-file"].includes(String(value.machine_key.provider)) ||
|
|
131
|
+
typeof value.machine_key.name !== "string" ||
|
|
132
|
+
!/^teamclaws-cli-[0-9a-f-]{36}$/u.test(value.machine_key.name) ||
|
|
133
|
+
!isObject(value.machine_key.public_jwk) ||
|
|
134
|
+
value.machine_key.public_jwk.kty !== "EC" ||
|
|
135
|
+
value.machine_key.public_jwk.crv !== "P-256" ||
|
|
136
|
+
typeof value.machine_key.public_jwk.x !== "string" ||
|
|
137
|
+
typeof value.machine_key.public_jwk.y !== "string" ||
|
|
138
|
+
typeof value.machine_key.jkt !== "string")
|
|
139
|
+
throw new TeamClawsError("invalid_credential_file", "The saved CLI authorization is invalid.", { exitCode: 9 });
|
|
140
|
+
const key = value.machine_key;
|
|
141
|
+
if (key.jkt !== jwkThumbprint(key.public_jwk))
|
|
142
|
+
throw new TeamClawsError("invalid_credential_file", "The saved CLI authorization key is invalid.", { exitCode: 9 });
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
function hashAscii(value) {
|
|
146
|
+
return createHash("sha256").update(value, "ascii").digest("base64url");
|
|
147
|
+
}
|
|
148
|
+
function base64Json(value) {
|
|
149
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
150
|
+
}
|
package/dist/validation.js
CHANGED
|
@@ -227,6 +227,12 @@ export function parsePairingCredential(value) {
|
|
|
227
227
|
return {
|
|
228
228
|
device_id: normalizeUuid(value.device_id, "device identifier"),
|
|
229
229
|
device_token: requireString(value.device_token, "device token", 8_192),
|
|
230
|
+
pairing_id: normalizeUuid(value.pairing_id, "pairing identifier"),
|
|
231
|
+
server_id: normalizeUuid(value.server_id, "server identifier"),
|
|
232
|
+
origin: canonicalWorkspaceOrigin(value.origin),
|
|
233
|
+
requester_sub: requireString(value.requester_sub, "requester subject", 255),
|
|
234
|
+
authorization_id: normalizeUuid(value.authorization_id, "authorization identifier"),
|
|
235
|
+
machine_jkt: requireThumbprint(value.machine_jkt),
|
|
230
236
|
...(Object.hasOwn(value, "external_chat_capabilities")
|
|
231
237
|
? {
|
|
232
238
|
external_chat_capabilities: value.external_chat_capabilities,
|
|
@@ -317,13 +323,13 @@ export function parseSafeUserResponse(value) {
|
|
|
317
323
|
exitCode: 9,
|
|
318
324
|
});
|
|
319
325
|
}
|
|
320
|
-
assertExactKeys(value, ["id", "email", "display_name", "
|
|
321
|
-
if (value.
|
|
326
|
+
assertExactKeys(value, ["id", "email", "display_name", "authorization_id", "authorization_status"], [], "account response");
|
|
327
|
+
if (value.authorization_status !== "active") {
|
|
322
328
|
throw new TeamClawsError("invalid_response", "TeamClaws returned an invalid account response.", {
|
|
323
329
|
exitCode: 9,
|
|
324
330
|
});
|
|
325
331
|
}
|
|
326
|
-
|
|
332
|
+
normalizeUuid(value.authorization_id, "authorization identifier");
|
|
327
333
|
return {
|
|
328
334
|
id: normalizeUuid(value.id, "user identifier"),
|
|
329
335
|
display_name: requireString(value.display_name, "display name", 200),
|
|
@@ -405,44 +411,70 @@ export function parseLoginCredential(value) {
|
|
|
405
411
|
export function parseWorkspaceCredential(value) {
|
|
406
412
|
if (!isObject(value))
|
|
407
413
|
throw invalidCredentialFile();
|
|
414
|
+
if (value.version === 1)
|
|
415
|
+
throw legacyPairingError();
|
|
408
416
|
assertExactKeys(value, [
|
|
409
417
|
"version",
|
|
410
418
|
"server_id",
|
|
411
419
|
"origin",
|
|
412
420
|
"display_name",
|
|
413
421
|
"device_id",
|
|
422
|
+
"pairing_id",
|
|
414
423
|
"device_token",
|
|
424
|
+
"requester_sub",
|
|
425
|
+
"authorization_id",
|
|
426
|
+
"machine_jkt",
|
|
415
427
|
"created_at",
|
|
416
428
|
], [], "workspace credential file");
|
|
417
|
-
if (value.version !==
|
|
429
|
+
if (value.version !== 2)
|
|
418
430
|
throw invalidCredentialFile();
|
|
419
431
|
return {
|
|
420
|
-
version:
|
|
432
|
+
version: 2,
|
|
421
433
|
server_id: normalizeUuid(value.server_id),
|
|
422
434
|
origin: canonicalWorkspaceOrigin(value.origin),
|
|
423
435
|
display_name: requireString(value.display_name, "display name", 120),
|
|
424
436
|
device_id: normalizeUuid(value.device_id, "device identifier"),
|
|
437
|
+
pairing_id: normalizeUuid(value.pairing_id, "pairing identifier"),
|
|
425
438
|
device_token: requireSecret(value.device_token),
|
|
439
|
+
requester_sub: requireString(value.requester_sub, "requester subject", 255),
|
|
440
|
+
authorization_id: normalizeUuid(value.authorization_id, "authorization identifier"),
|
|
441
|
+
machine_jkt: requireThumbprint(value.machine_jkt),
|
|
426
442
|
created_at: requireIsoDate(value.created_at, "credential creation date"),
|
|
427
443
|
};
|
|
428
444
|
}
|
|
429
445
|
export function parseStatusWorkspaceCredential(value, expectedServerId) {
|
|
430
|
-
if (!isObject(value)
|
|
446
|
+
if (!isObject(value))
|
|
447
|
+
throw invalidCredentialFile();
|
|
448
|
+
if (value.version === 1)
|
|
449
|
+
throw legacyPairingError();
|
|
450
|
+
if (value.version !== 2)
|
|
431
451
|
throw invalidCredentialFile();
|
|
432
452
|
const serverId = normalizeUuid(value.server_id);
|
|
433
453
|
if (serverId !== normalizeUuid(expectedServerId))
|
|
434
454
|
throw invalidCredentialFile();
|
|
435
455
|
return {
|
|
436
|
-
version:
|
|
456
|
+
version: 2,
|
|
437
457
|
server_id: serverId,
|
|
438
458
|
origin: canonicalWorkspaceOrigin(value.origin),
|
|
439
459
|
display_name: typeof value.display_name === "string" && value.display_name.length > 0
|
|
440
460
|
? requireString(value.display_name, "display name", 120)
|
|
441
461
|
: "Paired device",
|
|
442
462
|
device_id: normalizeUuid(value.device_id, "device identifier"),
|
|
463
|
+
pairing_id: normalizeUuid(value.pairing_id, "pairing identifier"),
|
|
443
464
|
device_token: requireSecret(value.device_token),
|
|
465
|
+
requester_sub: requireString(value.requester_sub, "requester subject", 255),
|
|
466
|
+
authorization_id: normalizeUuid(value.authorization_id, "authorization identifier"),
|
|
467
|
+
machine_jkt: requireThumbprint(value.machine_jkt),
|
|
444
468
|
};
|
|
445
469
|
}
|
|
470
|
+
function requireThumbprint(value) {
|
|
471
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/u.test(value))
|
|
472
|
+
throw invalidCredentialFile();
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
function legacyPairingError() {
|
|
476
|
+
return new TeamClawsError("re_pair_required", "This saved pairing predates bound CLI authorization. Pair this workspace again.", { exitCode: 3 });
|
|
477
|
+
}
|
|
446
478
|
function requireSecret(value) {
|
|
447
479
|
if (typeof value !== "string" || value.length < 8 || value.length > 8_192) {
|
|
448
480
|
throw invalidCredentialFile();
|
package/package.json
CHANGED
|
@@ -9,6 +9,13 @@ For install-and-connect requests, follow
|
|
|
9
9
|
[teamclaws-onboard](../teamclaws-onboard/SKILL.md) for the conversation sequence.
|
|
10
10
|
A status-only request runs only `teamclaws auth status --json` and stops.
|
|
11
11
|
|
|
12
|
+
Before starting browser login, explain that the user will sign this computer's
|
|
13
|
+
TeamClaws command-line client into their TeamClaws account through the browser.
|
|
14
|
+
This connects the client to their account; workspace selection and browser
|
|
15
|
+
pairing approval come next when they are connecting a workspace. If onboarding
|
|
16
|
+
already explained the process, use a short transition into this step. Give the
|
|
17
|
+
approval link with that context, never as an unexplained authorization request.
|
|
18
|
+
|
|
12
19
|
Use the official executable:
|
|
13
20
|
|
|
14
21
|
```text
|
|
@@ -16,22 +23,31 @@ npx teamclaws login --json
|
|
|
16
23
|
npx teamclaws login owner@example.com --json
|
|
17
24
|
npx teamclaws auth status --json
|
|
18
25
|
npx teamclaws logout --json
|
|
26
|
+
npx teamclaws logout --revoke --json
|
|
19
27
|
```
|
|
20
28
|
|
|
21
29
|
Login uses OAuth 2.0 Device Authorization Grant against www.teamclaws.com. The
|
|
22
30
|
next owner-facing message must link `login_browser_required.url` with the label
|
|
23
31
|
**Sign in to TeamClaws** and include `account_guidance`. Keep the command running
|
|
24
|
-
while the user signs in; do not restart it while awaiting approval.
|
|
25
|
-
|
|
32
|
+
while the user signs in; do not restart it while awaiting approval. Keep your
|
|
33
|
+
turn active and use the execution tool's wait/read operation on the same process
|
|
34
|
+
handle. Do not require an "I approved" or "done" message. If the execution
|
|
35
|
+
environment requires yielding, use a supported automatic completion callback or
|
|
36
|
+
wakeup that preserves the process; report a limitation if neither is possible.
|
|
37
|
+
Confirm the returned account after `login_complete` and exit 0, then immediately
|
|
38
|
+
resume the authorized onboarding or pairing task. Denial, expiry, cancellation,
|
|
39
|
+
and failure end the attempt; report the result without automatically restarting.
|
|
26
40
|
Start without an email unless the user supplies one. Optional email is a bind
|
|
27
41
|
sent to TeamClaws, not identity asserted by this process. Never transfer a
|
|
28
|
-
model or harness token into this process. The
|
|
29
|
-
protected
|
|
42
|
+
model or harness token into this process. The client stores an authorization ID
|
|
43
|
+
and protected local signing key; temporary OAuth tokens are discarded.
|
|
30
44
|
|
|
31
45
|
Only treat the fixed structured events as safe. Never expose access tokens,
|
|
32
46
|
refresh tokens, device codes, cookies, or authorization headers.
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
Ordinary logout removes the local authorization reference. Use `--revoke` only
|
|
49
|
+
when the user asks to revoke this CLI authorization at TeamClaws; the CLI waits
|
|
50
|
+
for confirmation before removing its local reference. If remote revocation is
|
|
51
|
+
uncertain, report that uncertainty and retain the local reference. Completed
|
|
52
|
+
Clawgate workspace credentials are not deleted by logout, but old pairings
|
|
53
|
+
without authorization binding require re-pairing.
|