@saptools/sharepoint-excel 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/README.md +352 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +1365 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +1179 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var DEFAULT_PROFILE_NAME = "default";
|
|
5
|
+
var DEFAULT_AUTH_BASE = "https://login.microsoftonline.com";
|
|
6
|
+
var DEFAULT_GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
7
|
+
var ENV_TENANT = "SHAREPOINT_EXCEL_TENANT_ID";
|
|
8
|
+
var ENV_CLIENT_ID = "SHAREPOINT_EXCEL_CLIENT_ID";
|
|
9
|
+
var ENV_CLIENT_SECRET = "SHAREPOINT_EXCEL_CLIENT_SECRET";
|
|
10
|
+
var ENV_SITE = "SHAREPOINT_EXCEL_SITE";
|
|
11
|
+
var ENV_DRIVE = "SHAREPOINT_EXCEL_DRIVE";
|
|
12
|
+
var ENV_PROFILE = "SHAREPOINT_EXCEL_PROFILE";
|
|
13
|
+
var ENV_AUTH_BASE = "SHAREPOINT_EXCEL_AUTH_BASE";
|
|
14
|
+
var ENV_GRAPH_BASE = "SHAREPOINT_EXCEL_GRAPH_BASE";
|
|
15
|
+
var ENV_HOME = "SAPTOOLS_SHAREPOINT_EXCEL_HOME";
|
|
16
|
+
var ENV_ALLOW_PLAINTEXT = "SAPTOOLS_SHAREPOINT_EXCEL_ALLOW_PLAINTEXT";
|
|
17
|
+
var FALLBACK_ENV_TENANT = "SHAREPOINT_TENANT_ID";
|
|
18
|
+
var FALLBACK_ENV_CLIENT_ID = "SHAREPOINT_CLIENT_ID";
|
|
19
|
+
var FALLBACK_ENV_CLIENT_SECRET = "SHAREPOINT_CLIENT_SECRET";
|
|
20
|
+
var FALLBACK_ENV_SITE = "SHAREPOINT_SITE";
|
|
21
|
+
var FALLBACK_ENV_DRIVE = "SHAREPOINT_DRIVE";
|
|
22
|
+
|
|
23
|
+
// src/auth/token.ts
|
|
24
|
+
import process from "process";
|
|
25
|
+
var DEFAULT_SCOPE = "https://graph.microsoft.com/.default";
|
|
26
|
+
function resolveAuthBase(options) {
|
|
27
|
+
if (options.authBase !== void 0 && options.authBase.length > 0) {
|
|
28
|
+
return options.authBase.replace(/\/+$/, "");
|
|
29
|
+
}
|
|
30
|
+
const fromEnv = (options.env ?? process.env)[ENV_AUTH_BASE];
|
|
31
|
+
return fromEnv === void 0 || fromEnv.length === 0 ? DEFAULT_AUTH_BASE : fromEnv.replace(/\/+$/, "");
|
|
32
|
+
}
|
|
33
|
+
function assertString(value, field) {
|
|
34
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
35
|
+
throw new Error(`Token response missing field: ${field}`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function assertNumber(value, field) {
|
|
40
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
41
|
+
throw new Error(`Token response missing numeric field: ${field}`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function parseTokenResponse(text, status) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new Error(`Failed to parse token response (HTTP ${status.toString()})`, { cause: err });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function throwIfTokenError(response, parsed) {
|
|
53
|
+
if (response.ok && typeof parsed.error !== "string") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const code = typeof parsed.error === "string" ? parsed.error : "unknown_error";
|
|
57
|
+
const description = typeof parsed.error_description === "string" && parsed.error_description.length > 0 ? parsed.error_description : response.statusText;
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Azure AD token request failed (HTTP ${response.status.toString()} ${code}): ${description}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
async function acquireAppToken(credentials, options = {}) {
|
|
63
|
+
if (credentials.tenantId.length === 0) {
|
|
64
|
+
throw new Error("tenantId is required");
|
|
65
|
+
}
|
|
66
|
+
if (credentials.clientId.length === 0) {
|
|
67
|
+
throw new Error("clientId is required");
|
|
68
|
+
}
|
|
69
|
+
if (credentials.clientSecret.length === 0) {
|
|
70
|
+
throw new Error("clientSecret is required");
|
|
71
|
+
}
|
|
72
|
+
const authBase = resolveAuthBase(options);
|
|
73
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
74
|
+
const url = `${authBase}/${encodeURIComponent(credentials.tenantId)}/oauth2/v2.0/token`;
|
|
75
|
+
const form = new URLSearchParams({
|
|
76
|
+
grant_type: "client_credentials",
|
|
77
|
+
client_id: credentials.clientId,
|
|
78
|
+
client_secret: credentials.clientSecret,
|
|
79
|
+
scope: options.scope ?? DEFAULT_SCOPE
|
|
80
|
+
});
|
|
81
|
+
const response = await fetchFn(url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
84
|
+
body: form.toString()
|
|
85
|
+
});
|
|
86
|
+
const parsed = parseTokenResponse(await response.text(), response.status);
|
|
87
|
+
throwIfTokenError(response, parsed);
|
|
88
|
+
const scopeValue = typeof parsed.scope === "string" ? parsed.scope : void 0;
|
|
89
|
+
const base = {
|
|
90
|
+
accessToken: assertString(parsed.access_token, "access_token"),
|
|
91
|
+
tokenType: assertString(parsed.token_type, "token_type"),
|
|
92
|
+
expiresOn: Math.floor(Date.now() / 1e3) + assertNumber(parsed.expires_in, "expires_in")
|
|
93
|
+
};
|
|
94
|
+
return scopeValue === void 0 ? base : { ...base, scope: scopeValue };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/config/paths.ts
|
|
98
|
+
import { homedir } from "os";
|
|
99
|
+
import { join } from "path";
|
|
100
|
+
import process2 from "process";
|
|
101
|
+
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
102
|
+
var PACKAGE_DIR_NAME = "sharepoint-excel";
|
|
103
|
+
var PROFILES_FILENAME = "profiles.json";
|
|
104
|
+
var FILE_SECRETS_FILENAME = "secrets.json";
|
|
105
|
+
function packageDataDir(env = process2.env) {
|
|
106
|
+
const override = env[ENV_HOME];
|
|
107
|
+
if (override !== void 0 && override.length > 0) {
|
|
108
|
+
return override;
|
|
109
|
+
}
|
|
110
|
+
return join(homedir(), SAPTOOLS_DIR_NAME, PACKAGE_DIR_NAME);
|
|
111
|
+
}
|
|
112
|
+
function profilesPath(env = process2.env) {
|
|
113
|
+
return join(packageDataDir(env), PROFILES_FILENAME);
|
|
114
|
+
}
|
|
115
|
+
function fileSecretsPath(env = process2.env) {
|
|
116
|
+
return join(packageDataDir(env), FILE_SECRETS_FILENAME);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/config/resolve.ts
|
|
120
|
+
import process4 from "process";
|
|
121
|
+
|
|
122
|
+
// src/credentials/profile-store.ts
|
|
123
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
124
|
+
import { dirname } from "path";
|
|
125
|
+
var PROFILE_FILE_MODE = 384;
|
|
126
|
+
var EMPTY_PROFILE_FILE = { version: 1, profiles: [] };
|
|
127
|
+
function isMissingFileError(err) {
|
|
128
|
+
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
129
|
+
}
|
|
130
|
+
function isSecretStoreKind(value) {
|
|
131
|
+
return value === "keyring" || value === "file";
|
|
132
|
+
}
|
|
133
|
+
function toStoredProfile(value) {
|
|
134
|
+
if (typeof value !== "object" || value === null) {
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
const raw = value;
|
|
138
|
+
const name = raw["name"];
|
|
139
|
+
const tenantId = raw["tenantId"];
|
|
140
|
+
const clientId = raw["clientId"];
|
|
141
|
+
const site = raw["site"];
|
|
142
|
+
const drive = raw["drive"];
|
|
143
|
+
const secretStore = raw["secretStore"];
|
|
144
|
+
const updatedAt = raw["updatedAt"];
|
|
145
|
+
if (typeof name !== "string" || typeof tenantId !== "string" || typeof clientId !== "string" || typeof site !== "string" || typeof updatedAt !== "string" || !isSecretStoreKind(secretStore)) {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
name,
|
|
150
|
+
tenantId,
|
|
151
|
+
clientId,
|
|
152
|
+
site,
|
|
153
|
+
...typeof drive === "string" ? { drive } : {},
|
|
154
|
+
secretStore,
|
|
155
|
+
updatedAt
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function isStoredProfile(value) {
|
|
159
|
+
return value !== void 0;
|
|
160
|
+
}
|
|
161
|
+
async function readProfileFile(path) {
|
|
162
|
+
try {
|
|
163
|
+
const raw = await readFile(path, "utf8");
|
|
164
|
+
const parsed = JSON.parse(raw);
|
|
165
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.profiles)) {
|
|
166
|
+
return EMPTY_PROFILE_FILE;
|
|
167
|
+
}
|
|
168
|
+
return { version: 1, profiles: parsed.profiles.map(toStoredProfile).filter(isStoredProfile) };
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (isMissingFileError(err)) {
|
|
171
|
+
return EMPTY_PROFILE_FILE;
|
|
172
|
+
}
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function writeProfileFile(path, value) {
|
|
177
|
+
await mkdir(dirname(path), { recursive: true });
|
|
178
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
179
|
+
`, {
|
|
180
|
+
encoding: "utf8",
|
|
181
|
+
mode: PROFILE_FILE_MODE
|
|
182
|
+
});
|
|
183
|
+
await chmod(path, PROFILE_FILE_MODE);
|
|
184
|
+
}
|
|
185
|
+
function createProfileStore(path = profilesPath()) {
|
|
186
|
+
return {
|
|
187
|
+
async readProfiles() {
|
|
188
|
+
return (await readProfileFile(path)).profiles;
|
|
189
|
+
},
|
|
190
|
+
async writeProfiles(profiles) {
|
|
191
|
+
await writeProfileFile(path, { version: 1, profiles });
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function findProfile(profiles, name = DEFAULT_PROFILE_NAME) {
|
|
196
|
+
return profiles.find((profile) => profile.name === name);
|
|
197
|
+
}
|
|
198
|
+
async function upsertProfile(store, vault, input) {
|
|
199
|
+
const name = input.name ?? DEFAULT_PROFILE_NAME;
|
|
200
|
+
const stored = {
|
|
201
|
+
name,
|
|
202
|
+
tenantId: input.tenantId,
|
|
203
|
+
clientId: input.clientId,
|
|
204
|
+
site: input.site,
|
|
205
|
+
...input.drive === void 0 || input.drive.length === 0 ? {} : { drive: input.drive },
|
|
206
|
+
secretStore: input.secretStore,
|
|
207
|
+
updatedAt: (input.updatedAt ?? /* @__PURE__ */ new Date()).toISOString()
|
|
208
|
+
};
|
|
209
|
+
const profiles = await store.readProfiles();
|
|
210
|
+
const nextProfiles = profiles.some((profile) => profile.name === name) ? profiles.map((profile) => profile.name === name ? stored : profile) : [...profiles, stored];
|
|
211
|
+
await vault.setSecret(name, input.clientSecret);
|
|
212
|
+
await store.writeProfiles(nextProfiles);
|
|
213
|
+
return stored;
|
|
214
|
+
}
|
|
215
|
+
async function removeProfile(store, vault, name = DEFAULT_PROFILE_NAME) {
|
|
216
|
+
const profiles = await store.readProfiles();
|
|
217
|
+
const nextProfiles = profiles.filter((profile) => profile.name !== name);
|
|
218
|
+
await vault.deleteSecret(name);
|
|
219
|
+
if (nextProfiles.length === profiles.length) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
await store.writeProfiles(nextProfiles);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
async function redactProfile(profile, vault) {
|
|
226
|
+
const secret = await vault.getSecret(profile.name);
|
|
227
|
+
return {
|
|
228
|
+
...profile,
|
|
229
|
+
clientId: `${profile.clientId.slice(0, 4)}...${profile.clientId.slice(-4)}`,
|
|
230
|
+
hasClientSecret: secret !== void 0
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/credentials/secret-vault.ts
|
|
235
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
236
|
+
import { dirname as dirname2 } from "path";
|
|
237
|
+
import { Entry } from "@napi-rs/keyring";
|
|
238
|
+
var SERVICE_NAME = "saptools-sharepoint-excel";
|
|
239
|
+
var SECRET_FILE_MODE = 384;
|
|
240
|
+
var EMPTY_SECRET_FILE = { version: 1, entries: {} };
|
|
241
|
+
function isMissingFileError2(err) {
|
|
242
|
+
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
243
|
+
}
|
|
244
|
+
async function readSecretFile(path) {
|
|
245
|
+
try {
|
|
246
|
+
const raw = await readFile2(path, "utf8");
|
|
247
|
+
const parsed = JSON.parse(raw);
|
|
248
|
+
if (parsed.version !== 1 || typeof parsed.entries !== "object" || parsed.entries === null) {
|
|
249
|
+
return EMPTY_SECRET_FILE;
|
|
250
|
+
}
|
|
251
|
+
const entries = {};
|
|
252
|
+
for (const [key, value] of Object.entries(parsed.entries)) {
|
|
253
|
+
if (typeof value === "string") {
|
|
254
|
+
entries[key] = value;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { version: 1, entries };
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (isMissingFileError2(err)) {
|
|
260
|
+
return EMPTY_SECRET_FILE;
|
|
261
|
+
}
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function writeSecretFile(path, value) {
|
|
266
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
267
|
+
await writeFile2(path, `${JSON.stringify(value, null, 2)}
|
|
268
|
+
`, {
|
|
269
|
+
encoding: "utf8",
|
|
270
|
+
mode: SECRET_FILE_MODE
|
|
271
|
+
});
|
|
272
|
+
await chmod2(path, SECRET_FILE_MODE);
|
|
273
|
+
}
|
|
274
|
+
function createKeyringSecretVault(serviceName = SERVICE_NAME) {
|
|
275
|
+
return {
|
|
276
|
+
getSecret(profileName) {
|
|
277
|
+
const password = new Entry(serviceName, profileName).getPassword();
|
|
278
|
+
return Promise.resolve(password === null || password.length === 0 ? void 0 : password);
|
|
279
|
+
},
|
|
280
|
+
setSecret(profileName, secret) {
|
|
281
|
+
new Entry(serviceName, profileName).setPassword(secret);
|
|
282
|
+
return Promise.resolve();
|
|
283
|
+
},
|
|
284
|
+
deleteSecret(profileName) {
|
|
285
|
+
try {
|
|
286
|
+
new Entry(serviceName, profileName).deletePassword();
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err instanceof Error && /not found|no entry|missing/i.test(err.message)) {
|
|
289
|
+
return Promise.resolve();
|
|
290
|
+
}
|
|
291
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
292
|
+
}
|
|
293
|
+
return Promise.resolve();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function createFileSecretVault(path = fileSecretsPath()) {
|
|
298
|
+
return {
|
|
299
|
+
async getSecret(profileName) {
|
|
300
|
+
const file = await readSecretFile(path);
|
|
301
|
+
return file.entries[profileName];
|
|
302
|
+
},
|
|
303
|
+
async setSecret(profileName, secret) {
|
|
304
|
+
const file = await readSecretFile(path);
|
|
305
|
+
await writeSecretFile(path, {
|
|
306
|
+
version: 1,
|
|
307
|
+
entries: { ...file.entries, [profileName]: secret }
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
async deleteSecret(profileName) {
|
|
311
|
+
const file = await readSecretFile(path);
|
|
312
|
+
const remaining = Object.fromEntries(
|
|
313
|
+
Object.entries(file.entries).filter(([entryName]) => entryName !== profileName)
|
|
314
|
+
);
|
|
315
|
+
await writeSecretFile(path, { version: 1, entries: remaining });
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/graph/client.ts
|
|
321
|
+
import process3 from "process";
|
|
322
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);
|
|
323
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
324
|
+
var DEFAULT_BASE_DELAY_MS = 500;
|
|
325
|
+
function defaultSleep(ms) {
|
|
326
|
+
return new Promise((resolve) => {
|
|
327
|
+
setTimeout(resolve, ms);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function parseRetryAfter(header) {
|
|
331
|
+
if (header === null || header.length === 0) {
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
const seconds = Number.parseFloat(header);
|
|
335
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
336
|
+
return Math.ceil(seconds * 1e3);
|
|
337
|
+
}
|
|
338
|
+
const dateMs = Date.parse(header);
|
|
339
|
+
return Number.isNaN(dateMs) ? void 0 : Math.max(0, dateMs - Date.now());
|
|
340
|
+
}
|
|
341
|
+
function resolveBaseUrl(options) {
|
|
342
|
+
if (options.baseUrl !== void 0 && options.baseUrl.length > 0) {
|
|
343
|
+
return options.baseUrl.replace(/\/+$/, "");
|
|
344
|
+
}
|
|
345
|
+
const fromEnv = (options.env ?? process3.env)[ENV_GRAPH_BASE];
|
|
346
|
+
return fromEnv === void 0 || fromEnv.length === 0 ? DEFAULT_GRAPH_BASE : fromEnv.replace(/\/+$/, "");
|
|
347
|
+
}
|
|
348
|
+
function resolveUrl(base, path) {
|
|
349
|
+
if (/^https?:\/\//i.test(path)) {
|
|
350
|
+
return path;
|
|
351
|
+
}
|
|
352
|
+
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
|
353
|
+
}
|
|
354
|
+
var GraphHttpError = class extends Error {
|
|
355
|
+
status;
|
|
356
|
+
code;
|
|
357
|
+
detail;
|
|
358
|
+
constructor(status, code, detail) {
|
|
359
|
+
super(
|
|
360
|
+
`Microsoft Graph request failed (${status.toString()}${code === void 0 ? "" : ` ${code}`}): ${detail}`
|
|
361
|
+
);
|
|
362
|
+
this.name = "GraphHttpError";
|
|
363
|
+
this.status = status;
|
|
364
|
+
this.code = code;
|
|
365
|
+
this.detail = detail;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
async function extractErrorDetail(response) {
|
|
369
|
+
const text = await response.text().catch(() => "");
|
|
370
|
+
if (text.length === 0) {
|
|
371
|
+
return { code: void 0, detail: response.statusText };
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const body = JSON.parse(text);
|
|
375
|
+
const code = typeof body.error?.code === "string" ? body.error.code : void 0;
|
|
376
|
+
const detail = typeof body.error?.message === "string" && body.error.message.length > 0 ? body.error.message : response.statusText;
|
|
377
|
+
return { code, detail };
|
|
378
|
+
} catch {
|
|
379
|
+
return { code: void 0, detail: text };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function buildInit(accessToken, options) {
|
|
383
|
+
const headers = {
|
|
384
|
+
Authorization: `Bearer ${accessToken}`,
|
|
385
|
+
Accept: "application/json",
|
|
386
|
+
...options.headers
|
|
387
|
+
};
|
|
388
|
+
const init = { method: options.method ?? "GET", headers };
|
|
389
|
+
if (options.rawBody !== void 0) {
|
|
390
|
+
init.body = rawBodyToBodyInit(options.rawBody);
|
|
391
|
+
return init;
|
|
392
|
+
}
|
|
393
|
+
if (options.body !== void 0) {
|
|
394
|
+
headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
|
|
395
|
+
init.body = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
|
|
396
|
+
}
|
|
397
|
+
return init;
|
|
398
|
+
}
|
|
399
|
+
function rawBodyToBodyInit(rawBody) {
|
|
400
|
+
if (typeof rawBody === "string") {
|
|
401
|
+
return rawBody;
|
|
402
|
+
}
|
|
403
|
+
const copy = new ArrayBuffer(rawBody.byteLength);
|
|
404
|
+
new Uint8Array(copy).set(rawBody);
|
|
405
|
+
return copy;
|
|
406
|
+
}
|
|
407
|
+
async function discardBody(response) {
|
|
408
|
+
if (response.body === null) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await response.body.cancel().catch(() => {
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function createGraphClient(options) {
|
|
415
|
+
const baseUrl = resolveBaseUrl(options);
|
|
416
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
417
|
+
const maxRetries = options.retry?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
418
|
+
const baseDelayMs = options.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
419
|
+
const sleepFn = options.retry?.sleepFn ?? defaultSleep;
|
|
420
|
+
async function execute(path, requestOptions) {
|
|
421
|
+
const url = resolveUrl(baseUrl, path);
|
|
422
|
+
const init = buildInit(options.accessToken, requestOptions);
|
|
423
|
+
let attempt = 0;
|
|
424
|
+
let response = await fetchFn(url, init);
|
|
425
|
+
while (!response.ok && RETRYABLE_STATUSES.has(response.status) && attempt < maxRetries) {
|
|
426
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after")) ?? baseDelayMs * 2 ** attempt;
|
|
427
|
+
await discardBody(response);
|
|
428
|
+
await sleepFn(retryAfterMs);
|
|
429
|
+
attempt += 1;
|
|
430
|
+
response = await fetchFn(url, init);
|
|
431
|
+
}
|
|
432
|
+
if (!response.ok) {
|
|
433
|
+
const { code, detail } = await extractErrorDetail(response);
|
|
434
|
+
throw new GraphHttpError(response.status, code, detail);
|
|
435
|
+
}
|
|
436
|
+
return response;
|
|
437
|
+
}
|
|
438
|
+
async function requestJson(path, requestOptions = {}) {
|
|
439
|
+
const response = await execute(path, requestOptions);
|
|
440
|
+
if (response.status === 204) {
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
444
|
+
if (!contentType.includes("application/json")) {
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
return await response.json();
|
|
448
|
+
}
|
|
449
|
+
async function requestBytes(path, requestOptions = {}) {
|
|
450
|
+
const response = await execute(path, requestOptions);
|
|
451
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
452
|
+
}
|
|
453
|
+
async function requestNoContent(path, requestOptions = {}) {
|
|
454
|
+
await execute(path, requestOptions);
|
|
455
|
+
}
|
|
456
|
+
return { baseUrl, requestJson, requestBytes, requestNoContent };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/graph/site.ts
|
|
460
|
+
function asString(value, fallback = "") {
|
|
461
|
+
return typeof value === "string" ? value : fallback;
|
|
462
|
+
}
|
|
463
|
+
function stripQueryAndHash(value) {
|
|
464
|
+
const markerIndex = value.search(/[?#]/);
|
|
465
|
+
return markerIndex === -1 ? value : value.slice(0, markerIndex);
|
|
466
|
+
}
|
|
467
|
+
function decodeSitePath(sitePath, input) {
|
|
468
|
+
return sitePath.split("/").map((segment) => {
|
|
469
|
+
try {
|
|
470
|
+
return decodeURIComponent(segment);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
throw new Error(`Invalid site reference "${input}". Site path has invalid encoding`, {
|
|
473
|
+
cause: err
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}).join("/");
|
|
477
|
+
}
|
|
478
|
+
function parseSiteInput(trimmed) {
|
|
479
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
480
|
+
const url = new URL(trimmed);
|
|
481
|
+
return { hostname: url.hostname, rawPath: url.pathname };
|
|
482
|
+
}
|
|
483
|
+
const withoutQuery = stripQueryAndHash(trimmed);
|
|
484
|
+
const firstSlash = withoutQuery.indexOf("/");
|
|
485
|
+
if (firstSlash === -1) {
|
|
486
|
+
throw new Error(`Invalid site reference "${trimmed}". Expected host/sites/<name> or full URL`);
|
|
487
|
+
}
|
|
488
|
+
return { hostname: withoutQuery.slice(0, firstSlash), rawPath: withoutQuery.slice(firstSlash + 1) };
|
|
489
|
+
}
|
|
490
|
+
function parseSiteRef(input) {
|
|
491
|
+
const trimmed = input.trim();
|
|
492
|
+
if (trimmed.length === 0) {
|
|
493
|
+
throw new Error("Site reference is empty");
|
|
494
|
+
}
|
|
495
|
+
const { hostname, rawPath } = parseSiteInput(trimmed);
|
|
496
|
+
const sitePath = decodeSitePath(rawPath.replace(/^\/+|\/+$/g, ""), trimmed);
|
|
497
|
+
if (hostname.length === 0 || sitePath.length === 0) {
|
|
498
|
+
throw new Error(`Invalid site reference "${trimmed}". Missing hostname or site path`);
|
|
499
|
+
}
|
|
500
|
+
return { hostname, sitePath };
|
|
501
|
+
}
|
|
502
|
+
function encodeSitePath(sitePath) {
|
|
503
|
+
return sitePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
504
|
+
}
|
|
505
|
+
async function resolveSite(client, ref) {
|
|
506
|
+
const path = `/sites/${encodeURIComponent(ref.hostname)}:/${encodeSitePath(ref.sitePath)}`;
|
|
507
|
+
let raw;
|
|
508
|
+
try {
|
|
509
|
+
raw = await client.requestJson(path);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
if (err instanceof GraphHttpError && err.status === 404) {
|
|
512
|
+
throw new Error(`SharePoint site not found at ${ref.hostname}/${ref.sitePath}`, {
|
|
513
|
+
cause: err
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
throw err;
|
|
517
|
+
}
|
|
518
|
+
const id = asString(raw.id);
|
|
519
|
+
if (id.length === 0) {
|
|
520
|
+
throw new Error(`Site response missing id for ${ref.hostname}/${ref.sitePath}`);
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
id,
|
|
524
|
+
name: asString(raw.name, ref.sitePath),
|
|
525
|
+
displayName: asString(raw.displayName, asString(raw.name, ref.sitePath)),
|
|
526
|
+
webUrl: asString(raw.webUrl)
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/config/resolve.ts
|
|
531
|
+
function pickEnv(env, primary, fallback) {
|
|
532
|
+
const value = env[primary] ?? env[fallback];
|
|
533
|
+
return value === void 0 || value.length === 0 ? void 0 : value;
|
|
534
|
+
}
|
|
535
|
+
function pickValue(override, envValue, profileValue) {
|
|
536
|
+
if (override !== void 0 && override.length > 0) {
|
|
537
|
+
return override;
|
|
538
|
+
}
|
|
539
|
+
return envValue ?? profileValue;
|
|
540
|
+
}
|
|
541
|
+
function vaultForProfile(profile, options) {
|
|
542
|
+
if (profile?.secretStore === "file") {
|
|
543
|
+
return options.fileVault ?? createFileSecretVault();
|
|
544
|
+
}
|
|
545
|
+
return options.keyringVault ?? createKeyringSecretVault();
|
|
546
|
+
}
|
|
547
|
+
async function resolveClientSecret(override, envValue, profile, options) {
|
|
548
|
+
if (override !== void 0 && override.length > 0) {
|
|
549
|
+
return override;
|
|
550
|
+
}
|
|
551
|
+
if (envValue !== void 0) {
|
|
552
|
+
return envValue;
|
|
553
|
+
}
|
|
554
|
+
return profile === void 0 ? void 0 : await vaultForProfile(profile, options).getSecret(profile.name);
|
|
555
|
+
}
|
|
556
|
+
function requireValue(value, humanName) {
|
|
557
|
+
if (value === void 0 || value.length === 0) {
|
|
558
|
+
throw new Error(`${humanName} is required (pass a flag, set env, or run config set)`);
|
|
559
|
+
}
|
|
560
|
+
return value;
|
|
561
|
+
}
|
|
562
|
+
async function resolveRuntime(options = {}) {
|
|
563
|
+
const env = options.env ?? process4.env;
|
|
564
|
+
const overrides = options.overrides ?? {};
|
|
565
|
+
const profileName = overrides.profile ?? env[ENV_PROFILE] ?? DEFAULT_PROFILE_NAME;
|
|
566
|
+
const store = options.profileStore ?? createProfileStore();
|
|
567
|
+
const profile = findProfile(await store.readProfiles(), profileName);
|
|
568
|
+
const tenantId = pickValue(
|
|
569
|
+
overrides.tenant,
|
|
570
|
+
pickEnv(env, ENV_TENANT, FALLBACK_ENV_TENANT),
|
|
571
|
+
profile?.tenantId
|
|
572
|
+
);
|
|
573
|
+
const clientId = pickValue(
|
|
574
|
+
overrides.clientId,
|
|
575
|
+
pickEnv(env, ENV_CLIENT_ID, FALLBACK_ENV_CLIENT_ID),
|
|
576
|
+
profile?.clientId
|
|
577
|
+
);
|
|
578
|
+
const site = pickValue(overrides.site, pickEnv(env, ENV_SITE, FALLBACK_ENV_SITE), profile?.site);
|
|
579
|
+
const drive = pickValue(overrides.drive, pickEnv(env, ENV_DRIVE, FALLBACK_ENV_DRIVE), profile?.drive);
|
|
580
|
+
const clientSecret = await resolveClientSecret(
|
|
581
|
+
overrides.clientSecret,
|
|
582
|
+
pickEnv(env, ENV_CLIENT_SECRET, FALLBACK_ENV_CLIENT_SECRET),
|
|
583
|
+
profile,
|
|
584
|
+
options
|
|
585
|
+
);
|
|
586
|
+
return {
|
|
587
|
+
target: {
|
|
588
|
+
credentials: {
|
|
589
|
+
tenantId: requireValue(tenantId, "Tenant ID"),
|
|
590
|
+
clientId: requireValue(clientId, "Client ID"),
|
|
591
|
+
clientSecret: requireValue(clientSecret, "Client secret")
|
|
592
|
+
},
|
|
593
|
+
site: parseSiteRef(requireValue(site, "SharePoint site"))
|
|
594
|
+
},
|
|
595
|
+
...drive === void 0 ? {} : { drive },
|
|
596
|
+
profileName,
|
|
597
|
+
source: profile === void 0 ? "env" : "profile"
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function parseSecretStoreKind(value) {
|
|
601
|
+
if (value === void 0 || value === "keyring") {
|
|
602
|
+
return "keyring";
|
|
603
|
+
}
|
|
604
|
+
if (value === "file") {
|
|
605
|
+
return "file";
|
|
606
|
+
}
|
|
607
|
+
throw new Error(`Invalid secret store "${value}". Expected keyring or file`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/graph/drive.ts
|
|
611
|
+
var XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
612
|
+
function asString2(value, fallback = "") {
|
|
613
|
+
return typeof value === "string" ? value : fallback;
|
|
614
|
+
}
|
|
615
|
+
function asNumber(value, fallback = 0) {
|
|
616
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
617
|
+
}
|
|
618
|
+
function encodeDrivePath(relativePath) {
|
|
619
|
+
return relativePath.split("/").filter((segment) => segment.length > 0).map((segment) => encodeURIComponent(segment)).join("/");
|
|
620
|
+
}
|
|
621
|
+
function toDrive(raw) {
|
|
622
|
+
return {
|
|
623
|
+
id: asString2(raw.id),
|
|
624
|
+
name: asString2(raw.name),
|
|
625
|
+
driveType: asString2(raw.driveType, "documentLibrary"),
|
|
626
|
+
webUrl: asString2(raw.webUrl)
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function toDriveItem(raw) {
|
|
630
|
+
const base = {
|
|
631
|
+
id: asString2(raw.id),
|
|
632
|
+
name: asString2(raw.name),
|
|
633
|
+
isFolder: raw.folder !== void 0 && raw.folder !== null,
|
|
634
|
+
size: asNumber(raw.size)
|
|
635
|
+
};
|
|
636
|
+
return {
|
|
637
|
+
...base,
|
|
638
|
+
...typeof raw.eTag === "string" ? { eTag: raw.eTag } : {},
|
|
639
|
+
...typeof raw.cTag === "string" ? { cTag: raw.cTag } : {},
|
|
640
|
+
...typeof raw.webUrl === "string" ? { webUrl: raw.webUrl } : {}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async function listDrives(client, siteId) {
|
|
644
|
+
const response = await client.requestJson(`/sites/${encodeURIComponent(siteId)}/drives`);
|
|
645
|
+
if (!Array.isArray(response.value)) {
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
return response.value.filter((entry) => typeof entry === "object" && entry !== null).map(toDrive);
|
|
649
|
+
}
|
|
650
|
+
function selectDrive(drives, driveHint) {
|
|
651
|
+
if (drives.length === 0) {
|
|
652
|
+
throw new Error("SharePoint site has no drives (document libraries)");
|
|
653
|
+
}
|
|
654
|
+
if (driveHint === void 0 || driveHint.length === 0) {
|
|
655
|
+
const first = drives[0];
|
|
656
|
+
if (first === void 0) {
|
|
657
|
+
throw new Error("No drives available");
|
|
658
|
+
}
|
|
659
|
+
return first;
|
|
660
|
+
}
|
|
661
|
+
const match = drives.find((drive) => drive.id === driveHint || drive.name === driveHint);
|
|
662
|
+
if (match === void 0) {
|
|
663
|
+
throw new Error(`Drive "${driveHint}" not found. Available: ${drives.map((d) => d.name).join(", ")}`);
|
|
664
|
+
}
|
|
665
|
+
return match;
|
|
666
|
+
}
|
|
667
|
+
async function getDriveItemByPath(client, driveId, relativePath) {
|
|
668
|
+
const normalized = relativePath.replace(/^\/+|\/+$/g, "");
|
|
669
|
+
const path = normalized.length === 0 ? `/drives/${encodeURIComponent(driveId)}/root` : `/drives/${encodeURIComponent(driveId)}/root:/${encodeDrivePath(normalized)}`;
|
|
670
|
+
try {
|
|
671
|
+
return toDriveItem(await client.requestJson(path));
|
|
672
|
+
} catch (err) {
|
|
673
|
+
if (err instanceof GraphHttpError && err.status === 404) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
throw err;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function downloadDriveFile(client, driveId, relativePath) {
|
|
680
|
+
const encodedPath = encodeDrivePath(relativePath.replace(/^\/+|\/+$/g, ""));
|
|
681
|
+
return await client.requestBytes(`/drives/${encodeURIComponent(driveId)}/root:/${encodedPath}:/content`, {
|
|
682
|
+
headers: { Accept: XLSX_CONTENT_TYPE }
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
async function createUploadSession(client, driveId, relativePath) {
|
|
686
|
+
const encodedPath = encodeDrivePath(relativePath.replace(/^\/+|\/+$/g, ""));
|
|
687
|
+
const response = await client.requestJson(
|
|
688
|
+
`/drives/${encodeURIComponent(driveId)}/root:/${encodedPath}:/createUploadSession`,
|
|
689
|
+
{
|
|
690
|
+
method: "POST",
|
|
691
|
+
body: { item: { "@microsoft.graph.conflictBehavior": "fail" } }
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
if (typeof response.uploadUrl !== "string" || response.uploadUrl.length === 0) {
|
|
695
|
+
throw new Error("Graph upload session response missing uploadUrl");
|
|
696
|
+
}
|
|
697
|
+
return response.uploadUrl;
|
|
698
|
+
}
|
|
699
|
+
async function uploadNewDriveFile(client, driveId, relativePath, bytes) {
|
|
700
|
+
const existing = await getDriveItemByPath(client, driveId, relativePath);
|
|
701
|
+
if (existing !== null) {
|
|
702
|
+
throw new Error(`Refusing to overwrite existing SharePoint file: ${relativePath}`);
|
|
703
|
+
}
|
|
704
|
+
const uploadUrl = await createUploadSession(client, driveId, relativePath);
|
|
705
|
+
const lastByte = bytes.byteLength - 1;
|
|
706
|
+
const raw = await client.requestJson(uploadUrl, {
|
|
707
|
+
method: "PUT",
|
|
708
|
+
rawBody: bytes,
|
|
709
|
+
headers: {
|
|
710
|
+
"Content-Length": bytes.byteLength.toString(),
|
|
711
|
+
"Content-Range": `bytes 0-${lastByte.toString()}/${bytes.byteLength.toString()}`,
|
|
712
|
+
"Content-Type": XLSX_CONTENT_TYPE
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
return toDriveItem(raw);
|
|
716
|
+
}
|
|
717
|
+
async function replaceDriveFile(client, driveId, relativePath, eTag, bytes) {
|
|
718
|
+
const encodedPath = encodeDrivePath(relativePath.replace(/^\/+|\/+$/g, ""));
|
|
719
|
+
const raw = await client.requestJson(
|
|
720
|
+
`/drives/${encodeURIComponent(driveId)}/root:/${encodedPath}:/content`,
|
|
721
|
+
{
|
|
722
|
+
method: "PUT",
|
|
723
|
+
rawBody: bytes,
|
|
724
|
+
headers: {
|
|
725
|
+
"Content-Type": XLSX_CONTENT_TYPE,
|
|
726
|
+
"If-Match": eTag
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
);
|
|
730
|
+
return toDriveItem(raw);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/session.ts
|
|
734
|
+
async function openSession(target, options = {}) {
|
|
735
|
+
const tokenOptions = {
|
|
736
|
+
...options.authBase === void 0 ? {} : { authBase: options.authBase },
|
|
737
|
+
...options.fetchFn === void 0 ? {} : { fetchFn: options.fetchFn }
|
|
738
|
+
};
|
|
739
|
+
const token = await acquireAppToken(target.credentials, tokenOptions);
|
|
740
|
+
const client = createGraphClient({
|
|
741
|
+
accessToken: token.accessToken,
|
|
742
|
+
...options.graphBase === void 0 ? {} : { baseUrl: options.graphBase },
|
|
743
|
+
...options.fetchFn === void 0 ? {} : { fetchFn: options.fetchFn },
|
|
744
|
+
...options.retry === void 0 ? {} : { retry: options.retry }
|
|
745
|
+
});
|
|
746
|
+
const site = await resolveSite(client, target.site);
|
|
747
|
+
const drives = await listDrives(client, site.id);
|
|
748
|
+
return { token, client, site, drives };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/workbook/a1.ts
|
|
752
|
+
var CELL_REF_PATTERN = /^([A-Za-z]+)([1-9]\d*)$/;
|
|
753
|
+
function columnNameToNumber(columnName) {
|
|
754
|
+
const normalized = columnName.trim().toUpperCase();
|
|
755
|
+
if (!/^[A-Z]+$/.test(normalized)) {
|
|
756
|
+
throw new Error(`Invalid column name "${columnName}"`);
|
|
757
|
+
}
|
|
758
|
+
let total = 0;
|
|
759
|
+
for (const char of normalized) {
|
|
760
|
+
total = total * 26 + char.charCodeAt(0) - 64;
|
|
761
|
+
}
|
|
762
|
+
return total;
|
|
763
|
+
}
|
|
764
|
+
function parseA1Cell(input) {
|
|
765
|
+
const trimmed = input.trim();
|
|
766
|
+
const match = CELL_REF_PATTERN.exec(trimmed);
|
|
767
|
+
if (match === null) {
|
|
768
|
+
throw new Error(`Invalid A1 cell reference "${input}"`);
|
|
769
|
+
}
|
|
770
|
+
const columnName = match[1];
|
|
771
|
+
const rowValue = match[2];
|
|
772
|
+
if (columnName === void 0 || rowValue === void 0) {
|
|
773
|
+
throw new Error(`Invalid A1 cell reference "${input}"`);
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
row: Number.parseInt(rowValue, 10),
|
|
777
|
+
column: columnNameToNumber(columnName)
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function orderRange(start, end) {
|
|
781
|
+
return {
|
|
782
|
+
start: {
|
|
783
|
+
row: Math.min(start.row, end.row),
|
|
784
|
+
column: Math.min(start.column, end.column)
|
|
785
|
+
},
|
|
786
|
+
end: {
|
|
787
|
+
row: Math.max(start.row, end.row),
|
|
788
|
+
column: Math.max(start.column, end.column)
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function parseA1Range(input) {
|
|
793
|
+
const parts = input.split(":").map((part) => part.trim());
|
|
794
|
+
if (parts.length === 1) {
|
|
795
|
+
const cell = parseA1Cell(parts[0] ?? "");
|
|
796
|
+
return { start: cell, end: cell };
|
|
797
|
+
}
|
|
798
|
+
if (parts.length !== 2) {
|
|
799
|
+
throw new Error(`Invalid A1 range "${input}"`);
|
|
800
|
+
}
|
|
801
|
+
return orderRange(parseA1Cell(parts[0] ?? ""), parseA1Cell(parts[1] ?? ""));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/workbook/excel.ts
|
|
805
|
+
import ExcelJS from "exceljs";
|
|
806
|
+
var INVALID_SHEET_CHARS = /[\][:*?/\\]/;
|
|
807
|
+
function validateSheetName(sheetName) {
|
|
808
|
+
const trimmed = sheetName.trim();
|
|
809
|
+
if (trimmed.length === 0 || trimmed.length > 31 || INVALID_SHEET_CHARS.test(trimmed)) {
|
|
810
|
+
throw new Error(`Invalid Excel sheet name "${sheetName}"`);
|
|
811
|
+
}
|
|
812
|
+
return trimmed;
|
|
813
|
+
}
|
|
814
|
+
function isRecord(row) {
|
|
815
|
+
return !Array.isArray(row);
|
|
816
|
+
}
|
|
817
|
+
function deriveHeaders(headers, rows) {
|
|
818
|
+
if (headers.length > 0) {
|
|
819
|
+
return headers;
|
|
820
|
+
}
|
|
821
|
+
const firstRecord = rows.find(isRecord);
|
|
822
|
+
return firstRecord === void 0 ? [] : Object.keys(firstRecord);
|
|
823
|
+
}
|
|
824
|
+
function rowToValues(row, headers) {
|
|
825
|
+
if (!isRecord(row)) {
|
|
826
|
+
return [...row];
|
|
827
|
+
}
|
|
828
|
+
const record = row;
|
|
829
|
+
if (headers.length === 0) {
|
|
830
|
+
return Object.keys(record).map((key) => record[key] ?? null);
|
|
831
|
+
}
|
|
832
|
+
return headers.map((header) => record[header] ?? null);
|
|
833
|
+
}
|
|
834
|
+
function addRows(sheet, headers, rows) {
|
|
835
|
+
if (headers.length > 0) {
|
|
836
|
+
sheet.addRow([...headers]);
|
|
837
|
+
}
|
|
838
|
+
for (const row of rows) {
|
|
839
|
+
sheet.addRow([...rowToValues(row, headers)]);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function addTable(sheet, tableName, headers, rows) {
|
|
843
|
+
sheet.addTable({
|
|
844
|
+
name: tableName,
|
|
845
|
+
ref: "A1",
|
|
846
|
+
headerRow: true,
|
|
847
|
+
totalsRow: false,
|
|
848
|
+
style: { theme: "TableStyleMedium2", showRowStripes: true },
|
|
849
|
+
columns: headers.map((name) => ({ name })),
|
|
850
|
+
rows: rows.map((row) => [...rowToValues(row, headers)])
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
async function workbookToBytes(workbook) {
|
|
854
|
+
const buffer = await workbook.xlsx.writeBuffer();
|
|
855
|
+
return new Uint8Array(buffer);
|
|
856
|
+
}
|
|
857
|
+
async function loadWorkbook(bytes) {
|
|
858
|
+
const workbook = new ExcelJS.Workbook();
|
|
859
|
+
await workbook.xlsx.load(uint8ArrayToArrayBuffer(bytes));
|
|
860
|
+
return workbook;
|
|
861
|
+
}
|
|
862
|
+
function uint8ArrayToArrayBuffer(bytes) {
|
|
863
|
+
const copy = new ArrayBuffer(bytes.byteLength);
|
|
864
|
+
new Uint8Array(copy).set(bytes);
|
|
865
|
+
return copy;
|
|
866
|
+
}
|
|
867
|
+
function getWorksheet(workbook, sheetName) {
|
|
868
|
+
const sheet = workbook.getWorksheet(sheetName);
|
|
869
|
+
if (sheet === void 0) {
|
|
870
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
|
871
|
+
}
|
|
872
|
+
return sheet;
|
|
873
|
+
}
|
|
874
|
+
function serializeCellValue(value) {
|
|
875
|
+
if (value === null || value === void 0) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
879
|
+
return value;
|
|
880
|
+
}
|
|
881
|
+
if (value instanceof Date) {
|
|
882
|
+
return value.toISOString();
|
|
883
|
+
}
|
|
884
|
+
if (typeof value === "object" && "result" in value) {
|
|
885
|
+
return serializeCellValue(value.result);
|
|
886
|
+
}
|
|
887
|
+
return JSON.stringify(value);
|
|
888
|
+
}
|
|
889
|
+
function readRow(sheet, rowNumber, startColumn, endColumn) {
|
|
890
|
+
const row = sheet.getRow(rowNumber);
|
|
891
|
+
const columnCount = endColumn - startColumn + 1;
|
|
892
|
+
return Array.from(
|
|
893
|
+
{ length: columnCount },
|
|
894
|
+
(_value, index) => serializeCellValue(row.getCell(startColumn + index).value)
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
function readHeaderRow(sheet) {
|
|
898
|
+
if (sheet.actualRowCount === 0) {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
return readRow(sheet, 1, 1, Math.max(1, sheet.actualColumnCount)).map((value) => String(value ?? ""));
|
|
902
|
+
}
|
|
903
|
+
function readSheet(sheet, options) {
|
|
904
|
+
const range = options.range === void 0 ? void 0 : parseA1Range(options.range);
|
|
905
|
+
const startRow = range?.start.row ?? 1;
|
|
906
|
+
const endRow = range?.end.row ?? Math.max(1, sheet.actualRowCount);
|
|
907
|
+
const startColumn = range?.start.column ?? 1;
|
|
908
|
+
const endColumn = range?.end.column ?? Math.max(1, sheet.actualColumnCount);
|
|
909
|
+
const rows = [];
|
|
910
|
+
for (let row = startRow; row <= endRow; row += 1) {
|
|
911
|
+
rows.push(readRow(sheet, row, startColumn, endColumn));
|
|
912
|
+
}
|
|
913
|
+
return { name: sheet.name, rowCount: rows.length, columnCount: endColumn - startColumn + 1, rows };
|
|
914
|
+
}
|
|
915
|
+
async function createWorkbookBytes(input) {
|
|
916
|
+
const workbook = new ExcelJS.Workbook();
|
|
917
|
+
const sheet = workbook.addWorksheet(validateSheetName(input.sheetName));
|
|
918
|
+
const headers = deriveHeaders(input.headers, input.rows);
|
|
919
|
+
if (input.tableName !== void 0 && input.tableName.length > 0 && headers.length > 0) {
|
|
920
|
+
addTable(sheet, input.tableName, headers, input.rows);
|
|
921
|
+
} else {
|
|
922
|
+
addRows(sheet, headers, input.rows);
|
|
923
|
+
}
|
|
924
|
+
return await workbookToBytes(workbook);
|
|
925
|
+
}
|
|
926
|
+
async function readWorkbookBytes(bytes, options = {}) {
|
|
927
|
+
const workbook = await loadWorkbook(bytes);
|
|
928
|
+
const sheets = options.sheetName === void 0 ? workbook.worksheets : [getWorksheet(workbook, validateSheetName(options.sheetName))];
|
|
929
|
+
return { sheets: sheets.map((sheet) => readSheet(sheet, options)) };
|
|
930
|
+
}
|
|
931
|
+
async function appendWorkbookRows(bytes, sheetName, rows, matchHeader) {
|
|
932
|
+
const workbook = await loadWorkbook(bytes);
|
|
933
|
+
const sheet = getWorksheet(workbook, validateSheetName(sheetName));
|
|
934
|
+
const headers = matchHeader ? readHeaderRow(sheet) : deriveHeaders([], rows);
|
|
935
|
+
for (const row of rows) {
|
|
936
|
+
sheet.addRow([...rowToValues(row, headers)]);
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
bytes: await workbookToBytes(workbook),
|
|
940
|
+
sheetName: sheet.name,
|
|
941
|
+
rowCount: sheet.actualRowCount,
|
|
942
|
+
columnCount: sheet.actualColumnCount
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
async function updateWorkbookCell(bytes, sheetName, cellRef, value) {
|
|
946
|
+
const workbook = await loadWorkbook(bytes);
|
|
947
|
+
const sheet = getWorksheet(workbook, validateSheetName(sheetName));
|
|
948
|
+
const cell = parseA1Cell(cellRef);
|
|
949
|
+
sheet.getCell(cell.row, cell.column).value = value;
|
|
950
|
+
return {
|
|
951
|
+
bytes: await workbookToBytes(workbook),
|
|
952
|
+
sheetName: sheet.name,
|
|
953
|
+
rowCount: sheet.actualRowCount,
|
|
954
|
+
columnCount: sheet.actualColumnCount
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
async function addWorkbookSheet(bytes, sheetName, headers) {
|
|
958
|
+
const workbook = await loadWorkbook(bytes);
|
|
959
|
+
const normalized = validateSheetName(sheetName);
|
|
960
|
+
if (workbook.getWorksheet(normalized) !== void 0) {
|
|
961
|
+
throw new Error(`Sheet "${normalized}" already exists`);
|
|
962
|
+
}
|
|
963
|
+
const sheet = workbook.addWorksheet(normalized);
|
|
964
|
+
if (headers.length > 0) {
|
|
965
|
+
sheet.addRow([...headers]);
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
bytes: await workbookToBytes(workbook),
|
|
969
|
+
sheetName: sheet.name,
|
|
970
|
+
rowCount: sheet.actualRowCount,
|
|
971
|
+
columnCount: sheet.actualColumnCount
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/workbook/json.ts
|
|
976
|
+
import { z } from "zod";
|
|
977
|
+
var JsonCellValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
978
|
+
var JsonRowSchema = z.array(JsonCellValueSchema);
|
|
979
|
+
var JsonRecordSchema = z.record(z.string(), JsonCellValueSchema);
|
|
980
|
+
function parseHeaders(input) {
|
|
981
|
+
if (input === void 0 || input.trim().length === 0) {
|
|
982
|
+
return [];
|
|
983
|
+
}
|
|
984
|
+
return input.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
985
|
+
}
|
|
986
|
+
function parseJson(input, label) {
|
|
987
|
+
try {
|
|
988
|
+
return JSON.parse(input);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
throw new Error(`Invalid JSON for ${label}`, { cause: err });
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function parseCellValue(input) {
|
|
994
|
+
const trimmed = input.trim();
|
|
995
|
+
if (trimmed.length === 0) {
|
|
996
|
+
return "";
|
|
997
|
+
}
|
|
998
|
+
let parsed;
|
|
999
|
+
try {
|
|
1000
|
+
parsed = parseJson(trimmed, "cell value");
|
|
1001
|
+
} catch {
|
|
1002
|
+
return input;
|
|
1003
|
+
}
|
|
1004
|
+
const result = JsonCellValueSchema.safeParse(parsed);
|
|
1005
|
+
if (!result.success) {
|
|
1006
|
+
return input;
|
|
1007
|
+
}
|
|
1008
|
+
return result.data;
|
|
1009
|
+
}
|
|
1010
|
+
function parseWorkbookRows(input) {
|
|
1011
|
+
if (input === void 0 || input.trim().length === 0) {
|
|
1012
|
+
return [];
|
|
1013
|
+
}
|
|
1014
|
+
const parsed = parseJson(input, "rows");
|
|
1015
|
+
const rowResult = JsonRowSchema.safeParse(parsed);
|
|
1016
|
+
if (rowResult.success) {
|
|
1017
|
+
return [rowResult.data];
|
|
1018
|
+
}
|
|
1019
|
+
const recordResult = JsonRecordSchema.safeParse(parsed);
|
|
1020
|
+
if (recordResult.success) {
|
|
1021
|
+
return [recordResult.data];
|
|
1022
|
+
}
|
|
1023
|
+
if (Array.isArray(parsed)) {
|
|
1024
|
+
return parsed.map(parseWorkbookRow);
|
|
1025
|
+
}
|
|
1026
|
+
throw new Error(`Rows JSON must be an object, row array, or array of rows/objects`);
|
|
1027
|
+
}
|
|
1028
|
+
function parseWorkbookRow(value) {
|
|
1029
|
+
const rowResult = JsonRowSchema.safeParse(value);
|
|
1030
|
+
if (rowResult.success) {
|
|
1031
|
+
return rowResult.data;
|
|
1032
|
+
}
|
|
1033
|
+
const recordResult = JsonRecordSchema.safeParse(value);
|
|
1034
|
+
if (recordResult.success) {
|
|
1035
|
+
return recordResult.data;
|
|
1036
|
+
}
|
|
1037
|
+
throw new Error(`Rows JSON must be an object, row array, or array of rows/objects`);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/workbook/service.ts
|
|
1041
|
+
function normalizeWorkbookPath(path) {
|
|
1042
|
+
const normalized = path.trim().replace(/^\/+|\/+$/g, "");
|
|
1043
|
+
if (normalized.length === 0) {
|
|
1044
|
+
throw new Error("Workbook path is required");
|
|
1045
|
+
}
|
|
1046
|
+
if (!normalized.toLowerCase().endsWith(".xlsx")) {
|
|
1047
|
+
throw new Error(`Workbook path must end with .xlsx: ${path}`);
|
|
1048
|
+
}
|
|
1049
|
+
return normalized;
|
|
1050
|
+
}
|
|
1051
|
+
function requireEtag(item, path) {
|
|
1052
|
+
if (item.eTag === void 0 || item.eTag.length === 0) {
|
|
1053
|
+
throw new Error(`Cannot update ${path}: SharePoint item is missing an ETag`);
|
|
1054
|
+
}
|
|
1055
|
+
return item.eTag;
|
|
1056
|
+
}
|
|
1057
|
+
async function downloadExistingWorkbook(target, path) {
|
|
1058
|
+
const drive = selectDrive(target.session.drives, target.driveHint);
|
|
1059
|
+
const item = await getDriveItemByPath(target.session.client, drive.id, path);
|
|
1060
|
+
if (item === null || item.isFolder) {
|
|
1061
|
+
throw new Error(`Workbook not found: ${path}`);
|
|
1062
|
+
}
|
|
1063
|
+
const bytes = await downloadDriveFile(target.session.client, drive.id, path);
|
|
1064
|
+
return { bytes, item, driveId: drive.id, driveName: drive.name };
|
|
1065
|
+
}
|
|
1066
|
+
async function createRemoteWorkbook(target, path, input) {
|
|
1067
|
+
const normalizedPath = normalizeWorkbookPath(path);
|
|
1068
|
+
const drive = selectDrive(target.session.drives, target.driveHint);
|
|
1069
|
+
const bytes = await createWorkbookBytes(input);
|
|
1070
|
+
const item = await uploadNewDriveFile(target.session.client, drive.id, normalizedPath, bytes);
|
|
1071
|
+
return { driveId: drive.id, driveName: drive.name, path: normalizedPath, item };
|
|
1072
|
+
}
|
|
1073
|
+
async function readRemoteWorkbook(target, path, options = {}) {
|
|
1074
|
+
const normalizedPath = normalizeWorkbookPath(path);
|
|
1075
|
+
const downloaded = await downloadExistingWorkbook(target, normalizedPath);
|
|
1076
|
+
const workbook = await readWorkbookBytes(downloaded.bytes, options);
|
|
1077
|
+
return { ...downloaded, path: normalizedPath, workbook };
|
|
1078
|
+
}
|
|
1079
|
+
async function appendRemoteWorkbookRows(target, path, sheetName, rows, matchHeader) {
|
|
1080
|
+
const normalizedPath = normalizeWorkbookPath(path);
|
|
1081
|
+
const downloaded = await downloadExistingWorkbook(target, normalizedPath);
|
|
1082
|
+
const mutation = await appendWorkbookRows(downloaded.bytes, sheetName, rows, matchHeader);
|
|
1083
|
+
const item = await replaceDriveFile(
|
|
1084
|
+
target.session.client,
|
|
1085
|
+
downloaded.driveId,
|
|
1086
|
+
normalizedPath,
|
|
1087
|
+
requireEtag(downloaded.item, normalizedPath),
|
|
1088
|
+
mutation.bytes
|
|
1089
|
+
);
|
|
1090
|
+
return { ...downloaded, item, path: normalizedPath, mutation };
|
|
1091
|
+
}
|
|
1092
|
+
async function updateRemoteWorkbookCell(target, path, sheetName, cellRef, value) {
|
|
1093
|
+
const normalizedPath = normalizeWorkbookPath(path);
|
|
1094
|
+
const downloaded = await downloadExistingWorkbook(target, normalizedPath);
|
|
1095
|
+
const mutation = await updateWorkbookCell(downloaded.bytes, sheetName, cellRef, value);
|
|
1096
|
+
const item = await replaceDriveFile(
|
|
1097
|
+
target.session.client,
|
|
1098
|
+
downloaded.driveId,
|
|
1099
|
+
normalizedPath,
|
|
1100
|
+
requireEtag(downloaded.item, normalizedPath),
|
|
1101
|
+
mutation.bytes
|
|
1102
|
+
);
|
|
1103
|
+
return { ...downloaded, item, path: normalizedPath, mutation };
|
|
1104
|
+
}
|
|
1105
|
+
async function addRemoteWorkbookSheet(target, path, sheetName, headers) {
|
|
1106
|
+
const normalizedPath = normalizeWorkbookPath(path);
|
|
1107
|
+
const downloaded = await downloadExistingWorkbook(target, normalizedPath);
|
|
1108
|
+
const mutation = await addWorkbookSheet(downloaded.bytes, sheetName, headers);
|
|
1109
|
+
const item = await replaceDriveFile(
|
|
1110
|
+
target.session.client,
|
|
1111
|
+
downloaded.driveId,
|
|
1112
|
+
normalizedPath,
|
|
1113
|
+
requireEtag(downloaded.item, normalizedPath),
|
|
1114
|
+
mutation.bytes
|
|
1115
|
+
);
|
|
1116
|
+
return { ...downloaded, item, path: normalizedPath, mutation };
|
|
1117
|
+
}
|
|
1118
|
+
export {
|
|
1119
|
+
DEFAULT_AUTH_BASE,
|
|
1120
|
+
DEFAULT_GRAPH_BASE,
|
|
1121
|
+
DEFAULT_PROFILE_NAME,
|
|
1122
|
+
ENV_ALLOW_PLAINTEXT,
|
|
1123
|
+
ENV_AUTH_BASE,
|
|
1124
|
+
ENV_CLIENT_ID,
|
|
1125
|
+
ENV_CLIENT_SECRET,
|
|
1126
|
+
ENV_DRIVE,
|
|
1127
|
+
ENV_GRAPH_BASE,
|
|
1128
|
+
ENV_HOME,
|
|
1129
|
+
ENV_PROFILE,
|
|
1130
|
+
ENV_SITE,
|
|
1131
|
+
ENV_TENANT,
|
|
1132
|
+
FALLBACK_ENV_CLIENT_ID,
|
|
1133
|
+
FALLBACK_ENV_CLIENT_SECRET,
|
|
1134
|
+
FALLBACK_ENV_DRIVE,
|
|
1135
|
+
FALLBACK_ENV_SITE,
|
|
1136
|
+
FALLBACK_ENV_TENANT,
|
|
1137
|
+
GraphHttpError,
|
|
1138
|
+
acquireAppToken,
|
|
1139
|
+
addRemoteWorkbookSheet,
|
|
1140
|
+
addWorkbookSheet,
|
|
1141
|
+
appendRemoteWorkbookRows,
|
|
1142
|
+
appendWorkbookRows,
|
|
1143
|
+
columnNameToNumber,
|
|
1144
|
+
createFileSecretVault,
|
|
1145
|
+
createGraphClient,
|
|
1146
|
+
createKeyringSecretVault,
|
|
1147
|
+
createProfileStore,
|
|
1148
|
+
createRemoteWorkbook,
|
|
1149
|
+
createWorkbookBytes,
|
|
1150
|
+
downloadDriveFile,
|
|
1151
|
+
encodeDrivePath,
|
|
1152
|
+
fileSecretsPath,
|
|
1153
|
+
findProfile,
|
|
1154
|
+
getDriveItemByPath,
|
|
1155
|
+
listDrives,
|
|
1156
|
+
openSession,
|
|
1157
|
+
packageDataDir,
|
|
1158
|
+
parseA1Cell,
|
|
1159
|
+
parseA1Range,
|
|
1160
|
+
parseCellValue,
|
|
1161
|
+
parseHeaders,
|
|
1162
|
+
parseSecretStoreKind,
|
|
1163
|
+
parseSiteRef,
|
|
1164
|
+
parseWorkbookRows,
|
|
1165
|
+
profilesPath,
|
|
1166
|
+
readRemoteWorkbook,
|
|
1167
|
+
readWorkbookBytes,
|
|
1168
|
+
redactProfile,
|
|
1169
|
+
removeProfile,
|
|
1170
|
+
replaceDriveFile,
|
|
1171
|
+
resolveRuntime,
|
|
1172
|
+
resolveSite,
|
|
1173
|
+
selectDrive,
|
|
1174
|
+
updateRemoteWorkbookCell,
|
|
1175
|
+
updateWorkbookCell,
|
|
1176
|
+
uploadNewDriveFile,
|
|
1177
|
+
upsertProfile
|
|
1178
|
+
};
|
|
1179
|
+
//# sourceMappingURL=index.js.map
|