@michaelschnyder/teams-cli 0.1.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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/SECURITY.md +21 -0
- package/dist/auth.js +337 -0
- package/dist/cli.js +647 -0
- package/dist/commands/skills.js +70 -0
- package/dist/commands/version.js +16 -0
- package/dist/config.js +106 -0
- package/dist/constants.js +8 -0
- package/dist/data.js +56 -0
- package/dist/diagnostics.js +60 -0
- package/dist/jwt.js +42 -0
- package/dist/oauth.js +146 -0
- package/dist/policy.js +328 -0
- package/dist/skills/teams-authentication/SKILL.md +30 -0
- package/dist/skills/teams-cli/SKILL.md +34 -0
- package/dist/skills/teams-messaging-policies/SKILL.md +26 -0
- package/dist/skills/teams-reading/SKILL.md +29 -0
- package/dist/skills.js +156 -0
- package/dist/storage.js +147 -0
- package/dist/teams-auth.js +54 -0
- package/dist/teams-client.js +604 -0
- package/dist/update.js +137 -0
- package/dist/upgrade.js +45 -0
- package/dist/version.js +7 -0
- package/dist/yaml.js +33 -0
- package/docs/releasing.md +48 -0
- package/docs/use/authentication.md +25 -0
- package/docs/use/policies.md +136 -0
- package/docs/use/profiles.md +57 -0
- package/package.json +64 -0
package/dist/storage.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export function identityKey(identity) {
|
|
6
|
+
return createHash("sha256")
|
|
7
|
+
.update(identity.tenantId)
|
|
8
|
+
.update("\0")
|
|
9
|
+
.update(identity.userId)
|
|
10
|
+
.digest("hex");
|
|
11
|
+
}
|
|
12
|
+
export function storagePaths(root = join(homedir(), ".teams-cli")) {
|
|
13
|
+
const authDirectory = join(root, "auth");
|
|
14
|
+
const browserProfilesDirectory = join(root, "browser-profiles");
|
|
15
|
+
const browserStagingDirectory = join(browserProfilesDirectory, ".staging");
|
|
16
|
+
return {
|
|
17
|
+
root,
|
|
18
|
+
configFile: join(root, "config.yaml"),
|
|
19
|
+
authDirectory,
|
|
20
|
+
browserProfilesDirectory,
|
|
21
|
+
browserStagingDirectory,
|
|
22
|
+
policiesDirectory: join(root, "policies"),
|
|
23
|
+
sessionFile: (identity) => join(authDirectory, `${identityKey(identity)}.json`),
|
|
24
|
+
browserProfile: (identity, browser) => join(browserProfilesDirectory, identityKey(identity), browser),
|
|
25
|
+
browserStagingProfile: (identifier, browser) => join(browserStagingDirectory, identifier, browser),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function preparePrivateDirectory(path) {
|
|
29
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
30
|
+
await chmod(path, 0o700);
|
|
31
|
+
}
|
|
32
|
+
export async function prepareBrowserProfile(paths, identity, browser) {
|
|
33
|
+
await preparePrivateDirectory(paths.root);
|
|
34
|
+
await preparePrivateDirectory(paths.browserProfilesDirectory);
|
|
35
|
+
const profile = paths.browserProfile(identity, browser);
|
|
36
|
+
await preparePrivateDirectory(profile);
|
|
37
|
+
return profile;
|
|
38
|
+
}
|
|
39
|
+
export async function prepareStagingBrowserProfile(paths, browser) {
|
|
40
|
+
await preparePrivateDirectory(paths.root);
|
|
41
|
+
await preparePrivateDirectory(paths.browserProfilesDirectory);
|
|
42
|
+
await preparePrivateDirectory(paths.browserStagingDirectory);
|
|
43
|
+
const identifier = randomUUID();
|
|
44
|
+
const directory = paths.browserStagingProfile(identifier, browser);
|
|
45
|
+
await preparePrivateDirectory(directory);
|
|
46
|
+
return { identifier, directory };
|
|
47
|
+
}
|
|
48
|
+
export async function promoteStagingBrowserProfile(paths, identifier, identity, browser) {
|
|
49
|
+
const sourceRoot = join(paths.browserStagingDirectory, identifier);
|
|
50
|
+
const source = paths.browserStagingProfile(identifier, browser);
|
|
51
|
+
const destination = paths.browserProfile(identity, browser);
|
|
52
|
+
const backup = `${destination}.backup-${randomUUID()}`;
|
|
53
|
+
await preparePrivateDirectory(join(paths.browserProfilesDirectory, identityKey(identity)));
|
|
54
|
+
let backedUp = false;
|
|
55
|
+
try {
|
|
56
|
+
try {
|
|
57
|
+
await rename(destination, backup);
|
|
58
|
+
backedUp = true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== "ENOENT")
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
await rename(source, destination);
|
|
65
|
+
await chmod(destination, 0o700);
|
|
66
|
+
if (backedUp)
|
|
67
|
+
await rm(backup, { recursive: true, force: true });
|
|
68
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (backedUp) {
|
|
72
|
+
await rm(destination, { recursive: true, force: true });
|
|
73
|
+
await rename(backup, destination);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function discardStagingBrowserProfile(paths, identifier) {
|
|
79
|
+
await rm(join(paths.browserStagingDirectory, identifier), { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
export async function saveSession(paths, session) {
|
|
82
|
+
await preparePrivateDirectory(paths.root);
|
|
83
|
+
await preparePrivateDirectory(paths.authDirectory);
|
|
84
|
+
const file = paths.sessionFile(session);
|
|
85
|
+
const temporary = join(paths.authDirectory, `.session-${randomUUID()}.tmp`);
|
|
86
|
+
await writeFile(temporary, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
|
|
87
|
+
await rename(temporary, file);
|
|
88
|
+
await chmod(file, 0o600);
|
|
89
|
+
}
|
|
90
|
+
function isStoredToken(value) {
|
|
91
|
+
if (!value || typeof value !== "object")
|
|
92
|
+
return false;
|
|
93
|
+
const token = value;
|
|
94
|
+
return typeof token.value === "string" && typeof token.expiresAt === "string";
|
|
95
|
+
}
|
|
96
|
+
function isAnyStoredSession(value) {
|
|
97
|
+
if (!value || typeof value !== "object")
|
|
98
|
+
return false;
|
|
99
|
+
const session = value;
|
|
100
|
+
const common = (session.browser === "edge" || session.browser === "chrome") &&
|
|
101
|
+
typeof session.tenantId === "string" &&
|
|
102
|
+
typeof session.savedAt === "string" &&
|
|
103
|
+
isStoredToken(session.accessToken) &&
|
|
104
|
+
isStoredToken(session.skypeToken);
|
|
105
|
+
if (!common)
|
|
106
|
+
return false;
|
|
107
|
+
if (session.version === 1 || session.version === 2)
|
|
108
|
+
return true;
|
|
109
|
+
if (session.version !== 3)
|
|
110
|
+
return false;
|
|
111
|
+
const current = session;
|
|
112
|
+
return typeof current.userId === "string" && current.userId.length > 0 &&
|
|
113
|
+
typeof current.region === "string" &&
|
|
114
|
+
isStoredToken(current.chatToken) &&
|
|
115
|
+
isStoredToken(current.searchToken) &&
|
|
116
|
+
typeof current.endpoints?.chatService === "string";
|
|
117
|
+
}
|
|
118
|
+
export async function loadSession(paths, identity) {
|
|
119
|
+
const file = paths.sessionFile(identity);
|
|
120
|
+
let raw;
|
|
121
|
+
try {
|
|
122
|
+
raw = await readFile(file, "utf8");
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code === "ENOENT") {
|
|
126
|
+
throw new Error("Not logged in for the selected tenant and user. Run `teams-cli auth login`.");
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
if (!isAnyStoredSession(parsed))
|
|
132
|
+
throw new Error("Stored Teams session is invalid. Log in again.");
|
|
133
|
+
if (parsed.tenantId !== identity.tenantId || parsed.version !== 3 || parsed.userId !== identity.userId) {
|
|
134
|
+
throw new Error("Stored Teams session belongs to a different identity or is outdated. Log in again.");
|
|
135
|
+
}
|
|
136
|
+
return parsed;
|
|
137
|
+
}
|
|
138
|
+
export function requireCurrentSession(session) {
|
|
139
|
+
if (session.version !== 3) {
|
|
140
|
+
throw new Error("Stored Teams session is outdated. Run `teams-cli auth login` again.");
|
|
141
|
+
}
|
|
142
|
+
return session;
|
|
143
|
+
}
|
|
144
|
+
export async function clearAuthentication(paths, identity) {
|
|
145
|
+
await rm(paths.sessionFile(identity), { force: true });
|
|
146
|
+
await rm(join(paths.browserProfilesDirectory, identityKey(identity)), { recursive: true, force: true });
|
|
147
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { TEAMS_AUTHZ_URL } from "./constants.js";
|
|
2
|
+
import { observedFetch } from "./diagnostics.js";
|
|
3
|
+
export class TeamsAuthError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
constructor(status, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.name = "TeamsAuthError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export async function exchangeInitialToken(initialToken) {
|
|
12
|
+
const response = await observedFetch(fetch, TEAMS_AUTHZ_URL, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
authorization: `Bearer ${initialToken}`,
|
|
16
|
+
"ms-teams-authz-type": "TokenRefresh",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const raw = await response.text();
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
let message = raw.slice(0, 300);
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
message = parsed.errorCode ?? parsed.message ?? message;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Preserve the bounded text response for diagnostics.
|
|
28
|
+
}
|
|
29
|
+
throw new TeamsAuthError(response.status, `Teams auth exchange failed (${response.status}): ${message}`);
|
|
30
|
+
}
|
|
31
|
+
const payload = JSON.parse(raw);
|
|
32
|
+
const skypeToken = payload.tokens?.skypeToken;
|
|
33
|
+
if (!skypeToken)
|
|
34
|
+
throw new Error("Teams auth exchange returned no Skype token");
|
|
35
|
+
return {
|
|
36
|
+
skypeToken,
|
|
37
|
+
...(payload.tokens?.expiresIn !== undefined
|
|
38
|
+
? { expiresIn: payload.tokens.expiresIn }
|
|
39
|
+
: {}),
|
|
40
|
+
...(payload.region ? { region: payload.region } : {}),
|
|
41
|
+
...(payload.partition ? { partition: payload.partition } : {}),
|
|
42
|
+
endpoints: {
|
|
43
|
+
...(payload.regionGtms?.chatService
|
|
44
|
+
? { chatService: payload.regionGtms.chatService }
|
|
45
|
+
: {}),
|
|
46
|
+
...(payload.regionGtms?.chatServiceAggregator
|
|
47
|
+
? { chatServiceAggregator: payload.regionGtms.chatServiceAggregator }
|
|
48
|
+
: {}),
|
|
49
|
+
...(payload.regionGtms?.middleTier
|
|
50
|
+
? { middleTier: payload.regionGtms.middleTier }
|
|
51
|
+
: {}),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|