@saptools/sharepoint-excel 0.1.1 → 0.1.3

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 CHANGED
@@ -11,7 +11,7 @@ Create `.xlsx` files, read workbook content, append records, update cells, and a
11
11
  [![install size](https://packagephobia.com/badge?p=@saptools/sharepoint-excel)](https://packagephobia.com/result?p=@saptools/sharepoint-excel)
12
12
  [![types](https://img.shields.io/npm/types/@saptools/sharepoint-excel.svg?style=flat&color=3178C6&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
13
13
 
14
- [Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [Security](#-credential-security) • [API](#-programmatic-usage)
14
+ [Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [Security](#-credential-security)
15
15
 
16
16
  </div>
17
17
 
@@ -29,24 +29,13 @@ Create `.xlsx` files, read workbook content, append records, update cells, and a
29
29
  - 🧪 **Fake-backed e2e tests**: package tests do not call real Microsoft Graph or SharePoint
30
30
  - 🧰 **CLI and typed API**: every CLI action is backed by exported TypeScript functions
31
31
 
32
- > [!NOTE]
33
- > Microsoft Graph's direct Excel workbook APIs are excellent for delegated user flows, but several workbook mutation endpoints do not support application permissions. This package intentionally treats SharePoint as file storage, edits the workbook locally, and uploads the changed `.xlsx` with conflict protection.
34
-
35
- ---
36
-
37
32
  ## 📦 Install
38
33
 
39
34
  ```bash
40
- # Global CLI
41
35
  npm install -g @saptools/sharepoint-excel
42
-
43
- # Or as a dependency
44
- npm install @saptools/sharepoint-excel
45
- # pnpm add @saptools/sharepoint-excel
46
- # yarn add @saptools/sharepoint-excel
47
36
  ```
48
37
 
49
- Requires **Node.js >= 20**. The CLI binary is `saptools-sharepoint-excel`.
38
+ Requires **Node.js >= 20**. The CLI binary is `sharepoint-excel`.
50
39
 
51
40
  ---
52
41
 
@@ -54,7 +43,7 @@ Requires **Node.js >= 20**. The CLI binary is `saptools-sharepoint-excel`.
54
43
 
55
44
  ```bash
56
45
  # 1. Store an app-only SharePoint profile
57
- saptools-sharepoint-excel config set \
46
+ sharepoint-excel config set \
58
47
  --tenant "11111111-1111-1111-1111-111111111111" \
59
48
  --client-id "22222222-2222-2222-2222-222222222222" \
60
49
  --client-secret "<your-client-secret>" \
@@ -62,30 +51,30 @@ saptools-sharepoint-excel config set \
62
51
  --drive "Documents"
63
52
 
64
53
  # 2. Prove auth and target resolution
65
- saptools-sharepoint-excel test
54
+ sharepoint-excel test
66
55
 
67
56
  # 3. Create a workbook without overwriting an existing file
68
- saptools-sharepoint-excel create \
57
+ sharepoint-excel create \
69
58
  --path "Reports/orders.xlsx" \
70
59
  --sheet "Orders" \
71
60
  --headers "Name,Amount,Status" \
72
61
  --rows '[{"Name":"Coffee","Amount":3,"Status":"open"}]'
73
62
 
74
63
  # 4. Append one object by matching row 1 headers
75
- saptools-sharepoint-excel append \
64
+ sharepoint-excel append \
76
65
  --path "Reports/orders.xlsx" \
77
66
  --sheet "Orders" \
78
67
  --record '{"Name":"Tea","Amount":8,"Status":"open"}'
79
68
 
80
69
  # 5. Update one cell
81
- saptools-sharepoint-excel update-cell \
70
+ sharepoint-excel update-cell \
82
71
  --path "Reports/orders.xlsx" \
83
72
  --sheet "Orders" \
84
73
  --cell "C2" \
85
74
  --value '"closed"'
86
75
 
87
76
  # 6. Read workbook JSON
88
- saptools-sharepoint-excel read --path "Reports/orders.xlsx" --json
77
+ sharepoint-excel read --path "Reports/orders.xlsx" --json
89
78
  ```
90
79
 
91
80
  For CI, every command can also read credentials from environment variables:
@@ -121,7 +110,7 @@ The CLI also accepts the shorter `SHAREPOINT_TENANT_ID`, `SHAREPOINT_CLIENT_ID`,
121
110
  Store a reusable local profile.
122
111
 
123
112
  ```bash
124
- saptools-sharepoint-excel config set \
113
+ sharepoint-excel config set \
125
114
  --profile finance \
126
115
  --tenant "$SHAREPOINT_EXCEL_TENANT_ID" \
127
116
  --client-id "$SHAREPOINT_EXCEL_CLIENT_ID" \
@@ -140,7 +129,7 @@ For headless CI containers where an OS keyring is unavailable, an explicit plain
140
129
 
141
130
  ```bash
142
131
  SAPTOOLS_SHAREPOINT_EXCEL_ALLOW_PLAINTEXT=1 \
143
- saptools-sharepoint-excel config set --store file --allow-plaintext-secret ...
132
+ sharepoint-excel config set --store file --allow-plaintext-secret ...
144
133
  ```
145
134
 
146
135
  Use that only in controlled CI environments. The file is written with `0600` permissions under `~/.saptools/sharepoint-excel/secrets.json`.
@@ -148,8 +137,8 @@ Use that only in controlled CI environments. The file is written with `0600` per
148
137
  ### 👀 `config get`
149
138
 
150
139
  ```bash
151
- saptools-sharepoint-excel config get --profile finance
152
- saptools-sharepoint-excel config get --profile finance --json
140
+ sharepoint-excel config get --profile finance
141
+ sharepoint-excel config get --profile finance --json
153
142
  ```
154
143
 
155
144
  Secrets are never printed.
@@ -157,7 +146,7 @@ Secrets are never printed.
157
146
  ### 🧹 `config remove`
158
147
 
159
148
  ```bash
160
- saptools-sharepoint-excel config remove --profile finance
149
+ sharepoint-excel config remove --profile finance
161
150
  ```
162
151
 
163
152
  Removes both profile metadata and the stored secret.
@@ -167,15 +156,15 @@ Removes both profile metadata and the stored secret.
167
156
  Authenticate, resolve the site, and list document libraries.
168
157
 
169
158
  ```bash
170
- saptools-sharepoint-excel test
171
- saptools-sharepoint-excel test --json
159
+ sharepoint-excel test
160
+ sharepoint-excel test --json
172
161
  ```
173
162
 
174
163
  ### 🗂️ `drives`
175
164
 
176
165
  ```bash
177
- saptools-sharepoint-excel drives
178
- saptools-sharepoint-excel drives --json
166
+ sharepoint-excel drives
167
+ sharepoint-excel drives --json
179
168
  ```
180
169
 
181
170
  Use this when you are unsure whether the document library is named `Documents`, `Shared Documents`, or something custom.
@@ -183,7 +172,7 @@ Use this when you are unsure whether the document library is named `Documents`,
183
172
  ### 🆕 `create`
184
173
 
185
174
  ```bash
186
- saptools-sharepoint-excel create \
175
+ sharepoint-excel create \
187
176
  --path "Reports/orders.xlsx" \
188
177
  --sheet "Orders" \
189
178
  --headers "Name,Amount,Status" \
@@ -196,14 +185,14 @@ saptools-sharepoint-excel create \
196
185
  ### 📖 `read`
197
186
 
198
187
  ```bash
199
- saptools-sharepoint-excel read --path "Reports/orders.xlsx"
200
- saptools-sharepoint-excel read --path "Reports/orders.xlsx" --sheet "Orders" --range "A1:C10" --json
188
+ sharepoint-excel read --path "Reports/orders.xlsx"
189
+ sharepoint-excel read --path "Reports/orders.xlsx" --sheet "Orders" --range "A1:C10" --json
201
190
  ```
202
191
 
203
192
  ### ➕ `append`
204
193
 
205
194
  ```bash
206
- saptools-sharepoint-excel append \
195
+ sharepoint-excel append \
207
196
  --path "Reports/orders.xlsx" \
208
197
  --sheet "Orders" \
209
198
  --record '{"Name":"Tea","Amount":8,"Status":"open"}'
@@ -214,7 +203,7 @@ Objects are mapped by the first row's headers by default. Use `--no-match-header
214
203
  ### 🎯 `update-cell`
215
204
 
216
205
  ```bash
217
- saptools-sharepoint-excel update-cell \
206
+ sharepoint-excel update-cell \
218
207
  --path "Reports/orders.xlsx" \
219
208
  --sheet "Orders" \
220
209
  --cell "B2" \
@@ -226,7 +215,7 @@ saptools-sharepoint-excel update-cell \
226
215
  ### 📄 `add-sheet`
227
216
 
228
217
  ```bash
229
- saptools-sharepoint-excel add-sheet \
218
+ sharepoint-excel add-sheet \
230
219
  --path "Reports/orders.xlsx" \
231
220
  --sheet "Audit" \
232
221
  --headers "At,Action,Actor"
@@ -250,71 +239,6 @@ Required Graph application permissions depend on your tenant model. Typical setu
250
239
 
251
240
  ---
252
241
 
253
- ## 🧑‍💻 Programmatic Usage
254
-
255
- ```ts
256
- import {
257
- appendRemoteWorkbookRows,
258
- createRemoteWorkbook,
259
- openSession,
260
- parseSiteRef,
261
- } from "@saptools/sharepoint-excel";
262
-
263
- const session = await openSession({
264
- credentials: {
265
- tenantId: process.env.SHAREPOINT_EXCEL_TENANT_ID ?? "",
266
- clientId: process.env.SHAREPOINT_EXCEL_CLIENT_ID ?? "",
267
- clientSecret: process.env.SHAREPOINT_EXCEL_CLIENT_SECRET ?? "",
268
- },
269
- site: parseSiteRef("contoso.sharepoint.com/sites/demo"),
270
- });
271
-
272
- await createRemoteWorkbook(
273
- { session, driveHint: "Documents" },
274
- "Reports/orders.xlsx",
275
- {
276
- sheetName: "Orders",
277
- headers: ["Name", "Amount"],
278
- rows: [{ Name: "Coffee", Amount: 3 }],
279
- },
280
- );
281
-
282
- await appendRemoteWorkbookRows(
283
- { session, driveHint: "Documents" },
284
- "Reports/orders.xlsx",
285
- "Orders",
286
- [{ Name: "Tea", Amount: 8 }],
287
- true,
288
- );
289
- ```
290
-
291
- <details>
292
- <summary><b>📚 Main exports</b></summary>
293
-
294
- | Export | Description |
295
- | --- | --- |
296
- | `resolveRuntime()` | Resolve flags/env/profile into a SharePoint target |
297
- | `createProfileStore()` | Read/write redacted local profile metadata |
298
- | `createKeyringSecretVault()` | OS credential vault adapter |
299
- | `acquireAppToken()` | Request an app-only Graph token |
300
- | `createGraphClient()` | Minimal Graph JSON/binary client |
301
- | `parseSiteRef()` / `resolveSite()` | Parse and resolve SharePoint site references |
302
- | `listDrives()` / `selectDrive()` | Discover and select document libraries |
303
- | `createWorkbookBytes()` | Build a local `.xlsx` workbook |
304
- | `readWorkbookBytes()` | Read sheet rows from workbook bytes |
305
- | `appendWorkbookRows()` | Append rows in workbook bytes |
306
- | `updateWorkbookCell()` | Update an A1 cell in workbook bytes |
307
- | `addWorkbookSheet()` | Add a new worksheet |
308
- | `createRemoteWorkbook()` | Create SharePoint workbook without overwriting |
309
- | `readRemoteWorkbook()` | Download and read SharePoint workbook |
310
- | `appendRemoteWorkbookRows()` | Append rows and upload with ETag protection |
311
- | `updateRemoteWorkbookCell()` | Update one cell and upload with ETag protection |
312
- | `addRemoteWorkbookSheet()` | Add a worksheet and upload with ETag protection |
313
-
314
- </details>
315
-
316
- ---
317
-
318
242
  ## 🛠️ Development
319
243
 
320
244
  From the monorepo root:
package/dist/cli.js CHANGED
@@ -172,7 +172,8 @@ async function redactProfile(profile, vault) {
172
172
  import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
173
173
  import { dirname as dirname2 } from "path";
174
174
  import { Entry } from "@napi-rs/keyring";
175
- var SERVICE_NAME = "saptools-sharepoint-excel";
175
+ var SERVICE_NAME = "sharepoint-excel";
176
+ var LEGACY_SERVICE_NAME = `saptools-${SERVICE_NAME}`;
176
177
  var SECRET_FILE_MODE = 384;
177
178
  var EMPTY_SECRET_FILE = { version: 1, entries: {} };
178
179
  function isMissingFileError2(err) {
@@ -208,11 +209,29 @@ async function writeSecretFile(path, value) {
208
209
  });
209
210
  await chmod2(path, SECRET_FILE_MODE);
210
211
  }
212
+ function getKeyringPassword(serviceName, profileName) {
213
+ const password = new Entry(serviceName, profileName).getPassword();
214
+ return password === null || password.length === 0 ? void 0 : password;
215
+ }
216
+ function deleteKeyringPassword(serviceName, profileName) {
217
+ try {
218
+ new Entry(serviceName, profileName).deletePassword();
219
+ } catch (err) {
220
+ if (err instanceof Error && /not found|no entry|missing/i.test(err.message)) {
221
+ return;
222
+ }
223
+ throw err instanceof Error ? err : new Error(String(err));
224
+ }
225
+ }
211
226
  function createKeyringSecretVault(serviceName = SERVICE_NAME) {
227
+ const legacyServiceName = serviceName === SERVICE_NAME ? LEGACY_SERVICE_NAME : void 0;
212
228
  return {
213
229
  getSecret(profileName) {
214
- const password = new Entry(serviceName, profileName).getPassword();
215
- return Promise.resolve(password === null || password.length === 0 ? void 0 : password);
230
+ const password = getKeyringPassword(serviceName, profileName);
231
+ if (password !== void 0 || legacyServiceName === void 0) {
232
+ return Promise.resolve(password);
233
+ }
234
+ return Promise.resolve(getKeyringPassword(legacyServiceName, profileName));
216
235
  },
217
236
  setSecret(profileName, secret) {
218
237
  new Entry(serviceName, profileName).setPassword(secret);
@@ -220,11 +239,11 @@ function createKeyringSecretVault(serviceName = SERVICE_NAME) {
220
239
  },
221
240
  deleteSecret(profileName) {
222
241
  try {
223
- new Entry(serviceName, profileName).deletePassword();
224
- } catch (err) {
225
- if (err instanceof Error && /not found|no entry|missing/i.test(err.message)) {
226
- return Promise.resolve();
242
+ deleteKeyringPassword(serviceName, profileName);
243
+ if (legacyServiceName !== void 0) {
244
+ deleteKeyringPassword(legacyServiceName, profileName);
227
245
  }
246
+ } catch (err) {
228
247
  return Promise.reject(err instanceof Error ? err : new Error(String(err)));
229
248
  }
230
249
  return Promise.resolve();
@@ -282,8 +301,11 @@ function resolveBaseUrl(options) {
282
301
  const fromEnv = (options.env ?? process2.env)[ENV_GRAPH_BASE];
283
302
  return fromEnv === void 0 || fromEnv.length === 0 ? DEFAULT_GRAPH_BASE : fromEnv.replace(/\/+$/, "");
284
303
  }
285
- function resolveUrl(base, path) {
304
+ function resolveUrl(base, path, includeAuthorization) {
286
305
  if (/^https?:\/\//i.test(path)) {
306
+ if (includeAuthorization && new URL(path).origin !== new URL(base).origin) {
307
+ throw new Error("Refusing to send a Graph bearer token to a different origin");
308
+ }
287
309
  return path;
288
310
  }
289
311
  return `${base}${path.startsWith("/") ? path : `/${path}`}`;
@@ -318,8 +340,8 @@ async function extractErrorDetail(response) {
318
340
  }
319
341
  function buildInit(accessToken, options) {
320
342
  const headers = {
321
- Authorization: `Bearer ${accessToken}`,
322
343
  Accept: "application/json",
344
+ ...options.includeAuthorization === false ? {} : { Authorization: `Bearer ${accessToken}` },
323
345
  ...options.headers
324
346
  };
325
347
  const init = { method: options.method ?? "GET", headers };
@@ -355,7 +377,8 @@ function createGraphClient(options) {
355
377
  const baseDelayMs = options.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
356
378
  const sleepFn = options.retry?.sleepFn ?? defaultSleep;
357
379
  async function execute(path, requestOptions) {
358
- const url = resolveUrl(baseUrl, path);
380
+ const includeAuthorization = requestOptions.includeAuthorization !== false;
381
+ const url = resolveUrl(baseUrl, path, includeAuthorization);
359
382
  const init = buildInit(options.accessToken, requestOptions);
360
383
  let attempt = 0;
361
384
  let response = await fetchFn(url, init);
@@ -755,6 +778,7 @@ async function uploadNewDriveFile(client, driveId, relativePath, bytes) {
755
778
  const raw = await client.requestJson(uploadUrl, {
756
779
  method: "PUT",
757
780
  rawBody: bytes,
781
+ includeAuthorization: false,
758
782
  headers: {
759
783
  "Content-Length": bytes.byteLength.toString(),
760
784
  "Content-Range": `bytes 0-${lastByte.toString()}/${bytes.byteLength.toString()}`,
@@ -1347,7 +1371,7 @@ function registerCommands(program) {
1347
1371
  // src/cli/index.ts
1348
1372
  async function main(argv) {
1349
1373
  const program = new Command();
1350
- program.name("saptools-sharepoint-excel").description("Create, read, and update SharePoint-hosted Excel workbooks");
1374
+ program.name("sharepoint-excel").description("Create, read, and update SharePoint-hosted Excel workbooks");
1351
1375
  registerCommands(program);
1352
1376
  await program.parseAsync([...argv]);
1353
1377
  }