onenote-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/src/auth.ts ADDED
@@ -0,0 +1,179 @@
1
+ import {
2
+ PublicClientApplication,
3
+ type DeviceCodeRequest,
4
+ type AuthenticationResult,
5
+ type AccountInfo,
6
+ } from "@azure/msal-node";
7
+ import { readFile, writeFile } from "node:fs/promises";
8
+ import { readFileSync, existsSync } from "node:fs";
9
+ import { join, dirname } from "node:path";
10
+ import { homedir } from "node:os";
11
+
12
+ // Auto-load .env.local from the package root (one level up from src/) when
13
+ // running from a different working directory.
14
+ function autoLoadEnv() {
15
+ const packageRoot = dirname(import.meta.dir);
16
+ for (const name of [".env.local", ".env"]) {
17
+ const path = join(packageRoot, name);
18
+ if (!existsSync(path)) continue;
19
+ try {
20
+ const content = readFileSync(path, "utf-8");
21
+ for (const line of content.split(/\r?\n/)) {
22
+ const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)\s*$/);
23
+ if (!m) continue;
24
+ const key = m[1];
25
+ let val = m[2];
26
+ // Strip optional surrounding quotes
27
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
28
+ val = val.slice(1, -1);
29
+ }
30
+ // Don't override env vars set by the shell
31
+ if (process.env[key] === undefined) process.env[key] = val;
32
+ }
33
+ } catch {}
34
+ }
35
+ }
36
+ autoLoadEnv();
37
+
38
+ const CACHE_PATH = join(homedir(), ".onenote-cli", "msal-cache.json");
39
+ const CONFIG_PATH = join(homedir(), ".onenote-cli", "config.json");
40
+
41
+ const SCOPES = [
42
+ "Notes.Read", "Notes.ReadWrite", "Notes.Read.All", "Notes.ReadWrite.All",
43
+ "Files.Read", "Files.Read.All",
44
+ "Sites.Read.All",
45
+ ];
46
+
47
+ interface AppConfig {
48
+ clientId: string;
49
+ authority: string;
50
+ }
51
+
52
+ const DEFAULT_CONFIG: AppConfig = {
53
+ clientId: "YOUR_CLIENT_ID",
54
+ authority: "https://login.microsoftonline.com/common",
55
+ };
56
+
57
+ async function ensureDir(path: string) {
58
+ const dir = path.substring(0, path.lastIndexOf("/"));
59
+ await Bun.write(join(dir, ".keep"), "");
60
+ }
61
+
62
+ async function loadConfig(): Promise<AppConfig> {
63
+ // Env vars take priority over config file
64
+ const envClientId = process.env.ONENOTE_CLIENT_ID;
65
+ const envAuthority = process.env.ONENOTE_AUTHORITY;
66
+ if (envClientId && envClientId !== "YOUR_CLIENT_ID") {
67
+ return {
68
+ clientId: envClientId,
69
+ authority: envAuthority || DEFAULT_CONFIG.authority,
70
+ };
71
+ }
72
+
73
+ try {
74
+ const raw = await readFile(CONFIG_PATH, "utf-8");
75
+ return JSON.parse(raw) as AppConfig;
76
+ } catch {
77
+ await ensureDir(CONFIG_PATH);
78
+ await writeFile(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
79
+ return DEFAULT_CONFIG;
80
+ }
81
+ }
82
+
83
+ async function createPca(): Promise<PublicClientApplication> {
84
+ const config = await loadConfig();
85
+ if (config.clientId === "YOUR_CLIENT_ID") {
86
+ console.error(
87
+ `Please configure your Azure AD app credentials in:\n ${CONFIG_PATH}\n\n` +
88
+ "To register an app:\n" +
89
+ " 1. Go to https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps\n" +
90
+ " 2. New registration -> Name: onenote-cli, Supported account types: Personal + Org\n" +
91
+ " 3. Set platform to 'Mobile and desktop applications'\n" +
92
+ " 4. Copy the Application (client) ID into config.json\n"
93
+ );
94
+ process.exit(1);
95
+ }
96
+
97
+ const pca = new PublicClientApplication({
98
+ auth: {
99
+ clientId: config.clientId,
100
+ authority: config.authority,
101
+ },
102
+ cache: {
103
+ cachePlugin: {
104
+ beforeCacheAccess: async (ctx) => {
105
+ try {
106
+ const data = await readFile(CACHE_PATH, "utf-8");
107
+ ctx.tokenCache.deserialize(data);
108
+ } catch {
109
+ // no cache yet
110
+ }
111
+ },
112
+ afterCacheAccess: async (ctx) => {
113
+ if (ctx.cacheHasChanged) {
114
+ await ensureDir(CACHE_PATH);
115
+ await writeFile(CACHE_PATH, ctx.tokenCache.serialize());
116
+ }
117
+ },
118
+ },
119
+ },
120
+ });
121
+
122
+ return pca;
123
+ }
124
+
125
+ export async function getAccessToken(): Promise<string> {
126
+ const pca = await createPca();
127
+
128
+ // Try silent auth first
129
+ const accounts = await pca.getTokenCache().getAllAccounts();
130
+ if (accounts.length > 0) {
131
+ try {
132
+ const result = await pca.acquireTokenSilent({
133
+ account: accounts[0] as AccountInfo,
134
+ scopes: SCOPES,
135
+ });
136
+ return result.accessToken;
137
+ } catch {
138
+ // fall through to device code
139
+ }
140
+ }
141
+
142
+ // Device code flow
143
+ const request: DeviceCodeRequest = {
144
+ scopes: SCOPES,
145
+ deviceCodeCallback: (response) => {
146
+ console.error(response.message);
147
+ },
148
+ };
149
+
150
+ const result: AuthenticationResult = await pca.acquireTokenByDeviceCode(request);
151
+ if (!result) {
152
+ throw new Error("Authentication failed");
153
+ }
154
+ return result.accessToken;
155
+ }
156
+
157
+ export async function logout(): Promise<void> {
158
+ try {
159
+ const { unlink } = await import("node:fs/promises");
160
+ await unlink(CACHE_PATH);
161
+ console.log("Logged out successfully. Token cache removed.");
162
+ } catch {
163
+ console.log("Already logged out (no cached tokens).");
164
+ }
165
+ }
166
+
167
+ export async function whoami(): Promise<void> {
168
+ const pca = await createPca();
169
+ const accounts = await pca.getTokenCache().getAllAccounts();
170
+ if (accounts.length === 0) {
171
+ console.log("Not logged in. Run `onenote auth login` to authenticate.");
172
+ return;
173
+ }
174
+ const account = accounts[0] as AccountInfo;
175
+ console.log("Username:", account.username || "(unknown)");
176
+ console.log("Name:", account.name || "(unknown)");
177
+ console.log("Tenant:", account.tenantId || "(unknown)");
178
+ console.log("Environment:", account.environment || "(unknown)");
179
+ }