dsh-plugin-onenote 0.0.0-stage → 0.2.2
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/LICENSE +21 -0
- package/README.md +150 -2
- package/README.zh.md +135 -0
- package/client.js +518 -0
- package/cordis.patch.yml +9 -0
- package/icon.svg +16 -0
- package/index.js +87 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +73 -4
- package/src/auth.js +291 -0
- package/src/config.js +53 -0
- package/src/graph-client.js +274 -0
- package/src/parser.js +393 -0
- package/src/permissions.js +108 -0
- package/src/routes.js +166 -0
- package/src/service.js +393 -0
- package/src/state.js +150 -0
- package/src/tools.js +295 -0
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-onenote",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Read the Microsoft OneNote notebooks you allow, from DeepSeek Harness. Device-code sign-in and a per-notebook allow-list you control in Settings. · 让 DSH 读取你勾选的 OneNote 笔记本:设备码登录,笔记本白名单在设置页勾选。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "hestudio-community",
|
|
9
|
+
"homepage": "https://github.com/hestudio-community/dsh-plugin-onenote#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/hestudio-community/dsh-plugin-onenote.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/hestudio-community/dsh-plugin-onenote/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"dsh",
|
|
19
|
+
"dsh-plugin",
|
|
20
|
+
"deepseek-harness",
|
|
21
|
+
"onenote",
|
|
22
|
+
"microsoft-graph",
|
|
23
|
+
"notes"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22",
|
|
27
|
+
"dsh": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.3.0-0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --import ./test/support/stub-tools.mjs --test \"test/*.test.js\"",
|
|
31
|
+
"test:watch": "node --import ./test/support/stub-tools.mjs --test --watch \"test/*.test.js\"",
|
|
32
|
+
"pack:check": "npm pack --dry-run"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.3.0-0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@deepseek-ai/dsh-tools": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"exports": {
|
|
43
|
+
".": "./index.js",
|
|
44
|
+
"./client": "./client.js",
|
|
45
|
+
"./locale/*.json": "./locale/*.json",
|
|
46
|
+
"./icon": "./icon.svg",
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"index.js",
|
|
51
|
+
"client.js",
|
|
52
|
+
"src/*.js",
|
|
53
|
+
"cordis.patch.yml",
|
|
54
|
+
"locale/*.json",
|
|
55
|
+
"icon.svg",
|
|
56
|
+
"README.md",
|
|
57
|
+
"README.zh.md",
|
|
58
|
+
"LICENSE"
|
|
59
|
+
],
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"dsh": {
|
|
64
|
+
"bundle": {
|
|
65
|
+
"patch": "./cordis.patch.yml"
|
|
66
|
+
},
|
|
67
|
+
"client": {
|
|
68
|
+
"platform": "web",
|
|
69
|
+
"inject": [
|
|
70
|
+
"@deepseek-ai/dsh-client-locale",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft identity: OAuth 2.0 device-code sign-in plus refresh-token reuse.
|
|
3
|
+
*
|
|
4
|
+
* Device code is the only flow that needs no listener, no browser automation,
|
|
5
|
+
* and no client secret, so it works the same on macOS, Windows, and a remote
|
|
6
|
+
* shell. One pending attempt is kept on disk so the settings card can continue
|
|
7
|
+
* the SAME code across reloads instead of invalidating what the user is typing.
|
|
8
|
+
*/
|
|
9
|
+
import { SCOPES, clearPendingSignIn, clearTokens, readPendingSignIn, readTokens, writePendingSignIn, writeTokens } from './state.js';
|
|
10
|
+
|
|
11
|
+
const GRAPH_ORIGIN = 'https://graph.microsoft.com';
|
|
12
|
+
const DEVICE_CODE_PATH = '/oauth2/v2.0/devicecode';
|
|
13
|
+
const TOKEN_PATH = '/oauth2/v2.0/token';
|
|
14
|
+
|
|
15
|
+
/** Treat a token as expired this early so a request never races the deadline. */
|
|
16
|
+
const EXPIRY_SKEW_MS = 60_000;
|
|
17
|
+
/** How long one interactive sign-in call waits before reporting `pending`. */
|
|
18
|
+
const POLL_BUDGET_MS = 30_000;
|
|
19
|
+
const HTTP_TIMEOUT_MS = 20_000;
|
|
20
|
+
|
|
21
|
+
/** A failure whose message is safe and useful to show the model. */
|
|
22
|
+
export class OneNoteAuthError extends Error {
|
|
23
|
+
constructor(message, code) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'OneNoteAuthError';
|
|
26
|
+
this.code = code;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function authorityUrl(tenant, path) {
|
|
31
|
+
return `https://login.microsoftonline.com/${encodeURIComponent(tenant)}${path}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function describeHttpFailure(status, payload) {
|
|
35
|
+
const error = typeof payload?.error === 'string' ? payload.error : '';
|
|
36
|
+
const description = typeof payload?.error_description === 'string' ? payload.error_description : '';
|
|
37
|
+
const detail = description.length > 0 ? description.split('\n')[0] : `HTTP ${status}`;
|
|
38
|
+
return { error, detail };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readJson(response) {
|
|
42
|
+
try {
|
|
43
|
+
return await response.json();
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Start a device-code attempt for this client.
|
|
51
|
+
* @param {{ clientId: string, tenant: string }} config - resolved plugin config.
|
|
52
|
+
* @returns {Promise<object>} the pending attempt to persist.
|
|
53
|
+
*/
|
|
54
|
+
export async function startDeviceCode(config) {
|
|
55
|
+
const body = new URLSearchParams({ client_id: config.clientId, scope: SCOPES.join(' ') });
|
|
56
|
+
let response;
|
|
57
|
+
try {
|
|
58
|
+
response = await fetch(authorityUrl(config.tenant, DEVICE_CODE_PATH), {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
61
|
+
body,
|
|
62
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new OneNoteAuthError(`Could not reach Microsoft sign-in (${error instanceof Error ? error.message : String(error)}). Check the network and try again.`, 'NETWORK');
|
|
66
|
+
}
|
|
67
|
+
const payload = await readJson(response);
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const { error: code, detail } = describeHttpFailure(response.status, payload);
|
|
70
|
+
const hint = code === 'invalid_client'
|
|
71
|
+
? ' The Azure app registration does not match this client ID, or public client flows are disabled for it.'
|
|
72
|
+
: code === 'unauthorized_client'
|
|
73
|
+
? ' Enable "Allow public client flows" for the app registration.'
|
|
74
|
+
: '';
|
|
75
|
+
throw new OneNoteAuthError(`Microsoft refused the device-code request: ${detail}.${hint}`, code || 'DEVICE_CODE_FAILED');
|
|
76
|
+
}
|
|
77
|
+
const deviceCode = typeof payload?.device_code === 'string' ? payload.device_code : '';
|
|
78
|
+
const userCode = typeof payload?.user_code === 'string' ? payload.user_code : '';
|
|
79
|
+
if (deviceCode.length === 0 || userCode.length === 0) throw new OneNoteAuthError('Microsoft returned an incomplete device-code response. Try again.', 'DEVICE_CODE_INCOMPLETE');
|
|
80
|
+
const expiresIn = typeof payload.expires_in === 'number' ? payload.expires_in : 900;
|
|
81
|
+
const interval = typeof payload.interval === 'number' ? payload.interval : 5;
|
|
82
|
+
return {
|
|
83
|
+
deviceCode,
|
|
84
|
+
userCode,
|
|
85
|
+
verificationUri: typeof payload.verification_uri === 'string' ? payload.verification_uri : 'https://microsoft.com/devicelogin',
|
|
86
|
+
message: typeof payload.message === 'string' ? payload.message : undefined,
|
|
87
|
+
intervalMs: Math.max(1000, interval * 1000),
|
|
88
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
89
|
+
clientId: config.clientId,
|
|
90
|
+
tenant: config.tenant,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Poll one attempt until it resolves or the per-call budget runs out.
|
|
96
|
+
*
|
|
97
|
+
* `authorization_pending` and `slow_down` are expected while the user types;
|
|
98
|
+
* every other error ends the attempt so the next call can start a fresh code.
|
|
99
|
+
* @param {object} pending - attempt from {@link startDeviceCode}.
|
|
100
|
+
* @param {number} [budgetMs] - how long this call may wait.
|
|
101
|
+
* @returns {Promise<{ status: 'signed-in', tokens: object } | { status: 'pending', pending: object }>}
|
|
102
|
+
*/
|
|
103
|
+
export async function pollDeviceCode(pending, budgetMs = POLL_BUDGET_MS) {
|
|
104
|
+
const deadline = Math.min(Date.now() + budgetMs, pending.expiresAt);
|
|
105
|
+
let intervalMs = pending.intervalMs;
|
|
106
|
+
let attempt = { ...pending };
|
|
107
|
+
|
|
108
|
+
for (;;) {
|
|
109
|
+
const remaining = deadline - Date.now();
|
|
110
|
+
if (remaining <= 0) return { status: 'pending', pending: attempt };
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remaining)));
|
|
112
|
+
if (Date.now() >= pending.expiresAt) {
|
|
113
|
+
clearPendingSignIn();
|
|
114
|
+
throw new OneNoteAuthError('The sign-in code expired before it was used. Start sign-in again for a fresh code.', 'DEVICE_CODE_EXPIRED');
|
|
115
|
+
}
|
|
116
|
+
const outcome = await pollDeviceCodeOnce(attempt);
|
|
117
|
+
if (outcome.status === 'signed-in') return outcome;
|
|
118
|
+
if (outcome.status === 'pending') continue;
|
|
119
|
+
// `slow_down` widens the interval for both this loop and later calls.
|
|
120
|
+
attempt = outcome.pending;
|
|
121
|
+
intervalMs = attempt.intervalMs;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Ask Microsoft once whether the pending attempt has been approved.
|
|
127
|
+
*
|
|
128
|
+
* A settings card polls this on a timer, so one call must never block: it
|
|
129
|
+
* performs exactly one token request and reports what it found.
|
|
130
|
+
* @param {object} pending - attempt from {@link startDeviceCode}.
|
|
131
|
+
* @returns {Promise<{ status: 'signed-in', tokens: object } | { status: 'pending', pending: object }>}
|
|
132
|
+
*/
|
|
133
|
+
export async function pollDeviceCodeOnce(pending) {
|
|
134
|
+
if (Date.now() >= pending.expiresAt) {
|
|
135
|
+
clearPendingSignIn();
|
|
136
|
+
throw new OneNoteAuthError('The sign-in code expired before it was used. Start sign-in again for a fresh code.', 'DEVICE_CODE_EXPIRED');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const body = new URLSearchParams({
|
|
140
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
141
|
+
device_code: pending.deviceCode,
|
|
142
|
+
client_id: pending.clientId ?? '',
|
|
143
|
+
});
|
|
144
|
+
let response;
|
|
145
|
+
try {
|
|
146
|
+
response = await fetch(authorityUrl(pending.tenant ?? 'common', TOKEN_PATH), {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
149
|
+
body,
|
|
150
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new OneNoteAuthError(`Could not reach Microsoft sign-in (${error instanceof Error ? error.message : String(error)}). Try again.`, 'NETWORK');
|
|
154
|
+
}
|
|
155
|
+
const payload = await readJson(response);
|
|
156
|
+
if (response.ok && typeof payload?.access_token === 'string') {
|
|
157
|
+
const tokens = toStoredTokens(payload, pending);
|
|
158
|
+
writeTokens(tokens);
|
|
159
|
+
clearPendingSignIn();
|
|
160
|
+
return { status: 'signed-in', tokens };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { error: code, detail } = describeHttpFailure(response.status, payload);
|
|
164
|
+
if (code === 'authorization_pending') return { status: 'pending', pending };
|
|
165
|
+
if (code === 'slow_down') {
|
|
166
|
+
const widened = { ...pending, intervalMs: pending.intervalMs + 5000 };
|
|
167
|
+
writePendingSignIn(widened);
|
|
168
|
+
return { status: 'pending', pending: widened };
|
|
169
|
+
}
|
|
170
|
+
clearPendingSignIn();
|
|
171
|
+
if (code === 'authorization_declined') throw new OneNoteAuthError('Sign-in was declined in the browser. Start sign-in again if this was a mistake.', code);
|
|
172
|
+
if (code === 'expired_token') throw new OneNoteAuthError('The sign-in code expired. Start sign-in again for a fresh code.', code);
|
|
173
|
+
if (code === 'bad_verification_code') throw new OneNoteAuthError('The stored sign-in attempt is no longer valid. Start sign-in again.', code);
|
|
174
|
+
throw new OneNoteAuthError(`Sign-in failed: ${detail}.`, code || 'SIGN_IN_FAILED');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function toStoredTokens(payload, pending) {
|
|
178
|
+
const expiresIn = typeof payload.expires_in === 'number' ? payload.expires_in : 3600;
|
|
179
|
+
return {
|
|
180
|
+
accessToken: payload.access_token,
|
|
181
|
+
refreshToken: typeof payload.refresh_token === 'string' ? payload.refresh_token : undefined,
|
|
182
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
183
|
+
scope: typeof payload.scope === 'string' ? payload.scope : SCOPES.join(' '),
|
|
184
|
+
account: undefined,
|
|
185
|
+
clientId: pending.clientId,
|
|
186
|
+
tenant: pending.tenant,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Exchange a stored refresh token for a new access token.
|
|
192
|
+
* @param {object} tokens - stored tokens.
|
|
193
|
+
* @param {{ clientId: string, tenant: string }} config - resolved plugin config.
|
|
194
|
+
* @returns {Promise<object>} the refreshed token set.
|
|
195
|
+
*/
|
|
196
|
+
async function refreshTokens(tokens, config) {
|
|
197
|
+
if (tokens.refreshToken === undefined) {
|
|
198
|
+
clearTokens();
|
|
199
|
+
throw new OneNoteAuthError('The OneNote session expired and cannot be renewed. Open Settings → Plugins → OneNote and sign in again.', 'SIGN_IN_REQUIRED');
|
|
200
|
+
}
|
|
201
|
+
const body = new URLSearchParams({
|
|
202
|
+
grant_type: 'refresh_token',
|
|
203
|
+
refresh_token: tokens.refreshToken,
|
|
204
|
+
client_id: config.clientId,
|
|
205
|
+
scope: SCOPES.join(' '),
|
|
206
|
+
});
|
|
207
|
+
let response;
|
|
208
|
+
try {
|
|
209
|
+
response = await fetch(authorityUrl(config.tenant, TOKEN_PATH), {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
212
|
+
body,
|
|
213
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new OneNoteAuthError(`Could not reach Microsoft sign-in (${error instanceof Error ? error.message : String(error)}). Try again.`, 'NETWORK');
|
|
217
|
+
}
|
|
218
|
+
const payload = await readJson(response);
|
|
219
|
+
if (!response.ok || typeof payload?.access_token !== 'string') {
|
|
220
|
+
const { error: code, detail } = describeHttpFailure(response.status, payload);
|
|
221
|
+
clearTokens();
|
|
222
|
+
const hint = code === 'invalid_grant' ? ' The stored sign-in is no longer valid (it may have been revoked or expired); sign in again from Settings → Plugins → OneNote.' : '';
|
|
223
|
+
throw new OneNoteAuthError(`Could not renew the OneNote session: ${detail}.${hint}`, code || 'REFRESH_FAILED');
|
|
224
|
+
}
|
|
225
|
+
const refreshed = {
|
|
226
|
+
accessToken: payload.access_token,
|
|
227
|
+
refreshToken: typeof payload.refresh_token === 'string' ? payload.refresh_token : tokens.refreshToken,
|
|
228
|
+
expiresAt: Date.now() + (typeof payload.expires_in === 'number' ? payload.expires_in : 3600) * 1000,
|
|
229
|
+
scope: typeof payload.scope === 'string' ? payload.scope : tokens.scope,
|
|
230
|
+
account: tokens.account,
|
|
231
|
+
clientId: config.clientId,
|
|
232
|
+
tenant: config.tenant,
|
|
233
|
+
};
|
|
234
|
+
writeTokens(refreshed);
|
|
235
|
+
return refreshed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Return a usable access token, refreshing or failing with a clear next step.
|
|
240
|
+
* @param {{ clientId: string, tenant: string }} config - resolved plugin config.
|
|
241
|
+
* @returns {Promise<string>} the access token.
|
|
242
|
+
*/
|
|
243
|
+
export async function requireAccessToken(config) {
|
|
244
|
+
const tokens = readTokens();
|
|
245
|
+
if (tokens === undefined) throw new OneNoteAuthError('Not signed in to OneNote yet. Open Settings → Plugins → OneNote and sign in.', 'SIGN_IN_REQUIRED');
|
|
246
|
+
if (tokens.expiresAt - EXPIRY_SKEW_MS > Date.now()) return tokens.accessToken;
|
|
247
|
+
const refreshed = await refreshTokens(tokens, config);
|
|
248
|
+
return refreshed.accessToken;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Describe the signed-in session without exposing the token. */
|
|
252
|
+
export function signInSummary() {
|
|
253
|
+
const tokens = readTokens();
|
|
254
|
+
if (tokens === undefined) return { signedIn: false };
|
|
255
|
+
// A lapsed access token is not a signed-out session while a refresh token can
|
|
256
|
+
// still renew it: reporting `signedIn: false` there hid the notebook list in
|
|
257
|
+
// the card and made `onenote_status` claim the plugin was not connected.
|
|
258
|
+
const usable = tokens.expiresAt > Date.now() || tokens.refreshToken !== undefined;
|
|
259
|
+
return {
|
|
260
|
+
signedIn: usable,
|
|
261
|
+
renewsAutomatically: tokens.refreshToken !== undefined,
|
|
262
|
+
expiresAt: new Date(tokens.expiresAt).toISOString(),
|
|
263
|
+
account: tokens.account,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Record who is signed in, so later calls can name the account. */
|
|
268
|
+
export function rememberAccount(account) {
|
|
269
|
+
const tokens = readTokens();
|
|
270
|
+
if (tokens === undefined) return;
|
|
271
|
+
writeTokens({ ...tokens, account });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Load a persisted device-code attempt, if one is still waiting. */
|
|
275
|
+
export function pendingSignIn() {
|
|
276
|
+
return readPendingSignIn();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Persist a started device-code attempt. */
|
|
280
|
+
export function rememberPendingSignIn(pending) {
|
|
281
|
+
writePendingSignIn(pending);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Forget the stored session. */
|
|
285
|
+
export function signOut() {
|
|
286
|
+
clearTokens();
|
|
287
|
+
clearPendingSignIn();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Graph origin used by the client; exported for tests and diagnostics. */
|
|
291
|
+
export const GRAPH_BASE = GRAPH_ORIGIN;
|
package/src/config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration resolution.
|
|
3
|
+
*
|
|
4
|
+
* The bundle's patch supplies `clientId` and `tenant`; a user patch layer may
|
|
5
|
+
* restate them. Invalid values never take the whole plugin down: each falls
|
|
6
|
+
* back to the documented default and a single warning names the offending
|
|
7
|
+
* field, so the tools stay usable and the fix stays discoverable.
|
|
8
|
+
*/
|
|
9
|
+
import { DEFAULT_CLIENT_ID } from './state.js';
|
|
10
|
+
|
|
11
|
+
/** Azure's multi-tenant + personal-account endpoint. */
|
|
12
|
+
const DEFAULT_TENANT = 'common';
|
|
13
|
+
const TENANT_PATTERN = /^[A-Za-z0-9.-]+$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} raw - the row's `config` value.
|
|
17
|
+
* @param {{ warn?: (message: string) => void }} [sink] - warning receiver.
|
|
18
|
+
* @returns {{ clientId: string, tenant: string, maxPagesPerList: number, maxPageChars: number }}
|
|
19
|
+
*/
|
|
20
|
+
export function resolveConfig(raw, sink = {}) {
|
|
21
|
+
const warn = typeof sink.warn === 'function' ? sink.warn : () => {};
|
|
22
|
+
const config = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
23
|
+
|
|
24
|
+
let clientId = DEFAULT_CLIENT_ID;
|
|
25
|
+
if (config.clientId !== undefined) {
|
|
26
|
+
if (typeof config.clientId === 'string' && /^[0-9a-fA-F-]{36}$/.test(config.clientId.trim())) {
|
|
27
|
+
clientId = config.clientId.trim();
|
|
28
|
+
} else {
|
|
29
|
+
warn('onenote: config.clientId must be an Azure application (client) ID; using the built-in default.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let tenant = DEFAULT_TENANT;
|
|
34
|
+
if (config.tenant !== undefined) {
|
|
35
|
+
if (typeof config.tenant === 'string' && TENANT_PATTERN.test(config.tenant.trim())) {
|
|
36
|
+
tenant = config.tenant.trim();
|
|
37
|
+
} else {
|
|
38
|
+
warn('onenote: config.tenant must be a tenant id or domain name (for example "common" or "contoso.onmicrosoft.com"); using "common".');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
clientId,
|
|
44
|
+
tenant,
|
|
45
|
+
maxPagesPerList: clampInt(config.maxPagesPerList, 25, 200, 100),
|
|
46
|
+
maxPageChars: clampInt(config.maxPageChars, 4000, 200000, 60000),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clampInt(value, min, max, fallback) {
|
|
51
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
|
|
52
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
53
|
+
}
|