runline 0.28.1 → 0.30.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.
Files changed (73) hide show
  1. package/dist/auth/errors.d.ts +22 -0
  2. package/dist/auth/errors.js +25 -0
  3. package/dist/auth/oauth2.d.ts +21 -0
  4. package/dist/auth/oauth2.js +154 -0
  5. package/dist/auth/token.d.ts +11 -0
  6. package/dist/auth/token.js +231 -0
  7. package/dist/auth/types.d.ts +93 -0
  8. package/dist/auth/types.js +1 -0
  9. package/dist/commands/auth.js +26 -13
  10. package/dist/commands/connection.js +3 -6
  11. package/dist/commands/exec.js +7 -2
  12. package/dist/commands/init.js +9 -2
  13. package/dist/config/loader.d.ts +7 -13
  14. package/dist/config/loader.js +58 -88
  15. package/dist/config/store.d.ts +17 -0
  16. package/dist/config/store.js +85 -0
  17. package/dist/connections/file.d.ts +13 -0
  18. package/dist/connections/file.js +53 -0
  19. package/dist/connections/memory.d.ts +15 -0
  20. package/dist/connections/memory.js +70 -0
  21. package/dist/connections/types.d.ts +47 -0
  22. package/dist/connections/types.js +10 -0
  23. package/dist/core/engine.d.ts +5 -2
  24. package/dist/core/engine.js +48 -55
  25. package/dist/core/oauth.d.ts +20 -15
  26. package/dist/core/oauth.js +195 -98
  27. package/dist/credentials/http.d.ts +19 -0
  28. package/dist/credentials/http.js +109 -0
  29. package/dist/credentials/policy.d.ts +5 -0
  30. package/dist/credentials/policy.js +92 -0
  31. package/dist/credentials/registry.d.ts +31 -0
  32. package/dist/credentials/registry.js +171 -0
  33. package/dist/credentials/transport.d.ts +38 -0
  34. package/dist/credentials/transport.js +283 -0
  35. package/dist/credentials/types.d.ts +80 -0
  36. package/dist/credentials/types.js +1 -0
  37. package/dist/index.d.ts +12 -0
  38. package/dist/index.js +8 -0
  39. package/dist/plugin/types.d.ts +30 -17
  40. package/dist/plugins/_shared/credentialAdapter.js +88 -0
  41. package/dist/plugins/_shared/googleAuth.js +89 -103
  42. package/dist/plugins/_shared/googleCredentials.js +160 -0
  43. package/dist/plugins/_shared/microsoftAuth.js +81 -74
  44. package/dist/plugins/_shared/microsoftCredentials.js +90 -0
  45. package/dist/plugins/_shared/tokenRefresh.js +39 -0
  46. package/dist/plugins/bitwarden/src/index.js +13 -31
  47. package/dist/plugins/gmail/src/index.js +8 -44
  48. package/dist/plugins/googleAppsScript/src/index.js +3 -25
  49. package/dist/plugins/googleCalendar/src/index.js +3 -41
  50. package/dist/plugins/googleContacts/src/index.js +3 -36
  51. package/dist/plugins/googleDocs/src/index.js +5 -0
  52. package/dist/plugins/googleDocs/src/shared.js +2 -30
  53. package/dist/plugins/googleDrive/src/index.js +52 -98
  54. package/dist/plugins/googleSheets/src/index.js +3 -40
  55. package/dist/plugins/googleSlides/src/index.js +5 -39
  56. package/dist/plugins/googleTasks/src/index.js +3 -30
  57. package/dist/plugins/halopsa/src/index.js +16 -31
  58. package/dist/plugins/linear/src/comments.js +2 -2
  59. package/dist/plugins/linear/src/index.js +1 -1
  60. package/dist/plugins/linear/src/issues.js +1 -17
  61. package/dist/plugins/microsoftCalendar/src/index.js +2 -1
  62. package/dist/plugins/microsoftFiles/src/index.js +10 -24
  63. package/dist/plugins/microsoftMail/src/index.js +2 -1
  64. package/dist/plugins/openai/src/index.js +13 -8
  65. package/dist/plugins/paypal/src/index.js +14 -29
  66. package/dist/plugins/plaud/src/index.js +210 -0
  67. package/dist/plugins/plaud/src/shared.js +221 -0
  68. package/dist/plugins/salesforce/src/shared.js +17 -32
  69. package/dist/sdk.d.ts +13 -5
  70. package/dist/sdk.js +35 -29
  71. package/dist/utils/observer.d.ts +2 -0
  72. package/dist/utils/observer.js +11 -0
  73. package/package.json +1 -1
@@ -55,7 +55,7 @@
55
55
  */
56
56
  import { createReadStream, readFileSync, statSync, writeFileSync } from "node:fs";
57
57
  import * as t from "typebox";
58
- import { googleAccessToken } from "../../_shared/googleAuth.js";
58
+ import { googleJsonRequest, googleResponse } from "../../_shared/googleAuth.js";
59
59
  import { Id, NonEmptyString, RawGoogleObject, StringMap, StringOrStringArray, stringEnum, } from "../../_shared/googleSchemas.js";
60
60
  const PermissionRole = stringEnum([
61
61
  "owner",
@@ -105,46 +105,23 @@ const DRIVE = {
105
105
  UNKNOWN: "application/vnd.google-apps.unknown",
106
106
  };
107
107
  // ─── Auth ────────────────────────────────────────────────────────
108
- async function accessToken(ctx) {
109
- return googleAccessToken(ctx, "googleDrive", SCOPES);
108
+ function driveResponse(ctx, url, init = {}) {
109
+ return googleResponse(ctx, "googleDrive", SCOPES, url, init);
110
110
  }
111
111
  // ─── Request ─────────────────────────────────────────────────────
112
112
  const API_BASE = "https://www.googleapis.com";
113
+ function filePathId(id) {
114
+ if (!/^[a-zA-Z0-9_-]+$/.test(id))
115
+ throw new Error("googleDrive: invalid file ID");
116
+ return id;
117
+ }
118
+ function uploadSession(value) {
119
+ if (!value || !/^https:\/\/www\.googleapis\.com\/upload\/drive\/v3\/files(?:\/[a-zA-Z0-9_-]+)?\?/.test(value))
120
+ throw new Error("googleDrive: invalid resumable session Location");
121
+ return value;
122
+ }
113
123
  async function driveRequest(ctx, method, path, body, qs) {
114
- const token = await accessToken(ctx);
115
- const url = new URL(`${API_BASE}${path}`);
116
- if (qs) {
117
- for (const [k, v] of Object.entries(qs)) {
118
- if (v === undefined || v === null)
119
- continue;
120
- if (Array.isArray(v)) {
121
- for (const entry of v)
122
- url.searchParams.append(k, String(entry));
123
- }
124
- else {
125
- url.searchParams.set(k, String(v));
126
- }
127
- }
128
- }
129
- const init = {
130
- method,
131
- headers: {
132
- Authorization: `Bearer ${token}`,
133
- Accept: "application/json",
134
- },
135
- };
136
- if (body && Object.keys(body).length > 0) {
137
- init.headers["Content-Type"] = "application/json";
138
- init.body = JSON.stringify(body);
139
- }
140
- const res = await fetch(url.toString(), init);
141
- if (res.status === 204)
142
- return { success: true };
143
- const text = await res.text();
144
- if (!res.ok) {
145
- throw new Error(`googleDrive: ${method} ${path} → ${res.status} ${text}`);
146
- }
147
- return text ? JSON.parse(text) : { success: true };
124
+ return googleJsonRequest(ctx, "googleDrive", SCOPES, method, `${API_BASE}${path}`, body, qs);
148
125
  }
149
126
  /**
150
127
  * Paginate any Drive list endpoint by repeatedly following
@@ -233,7 +210,6 @@ function buildMultipart(metadata, content, mimeType) {
233
210
  */
234
211
  async function uploadBytes(ctx, metadata, c, extraQs = {}) {
235
212
  const mimeType = c.mimeType ?? "application/octet-stream";
236
- const token = await accessToken(ctx);
237
213
  if (c.buffer) {
238
214
  const body = buildMultipart(metadata, c.buffer, mimeType);
239
215
  const url = new URL(`${API_BASE}/upload/drive/v3/files`);
@@ -243,19 +219,17 @@ async function uploadBytes(ctx, metadata, c, extraQs = {}) {
243
219
  if (v !== undefined)
244
220
  url.searchParams.set(k, String(v));
245
221
  }
246
- const res = await fetch(url.toString(), {
222
+ const res = await driveResponse(ctx, url.toString(), {
247
223
  method: "POST",
248
224
  headers: {
249
- Authorization: `Bearer ${token}`,
250
225
  "Content-Type": `multipart/related; boundary=${MULTIPART_BOUNDARY}`,
251
- "Content-Length": String(body.byteLength),
252
226
  },
253
227
  // Buffer is a Uint8Array so it's a valid BodyInit.
254
228
  body: new Uint8Array(body),
255
229
  });
256
230
  const text = await res.text();
257
231
  if (!res.ok) {
258
- throw new Error(`googleDrive: upload failed (${res.status}): ${text}`);
232
+ throw new Error(`googleDrive: upload failed (HTTP ${res.status})`);
259
233
  }
260
234
  return JSON.parse(text);
261
235
  }
@@ -267,10 +241,9 @@ async function uploadBytes(ctx, metadata, c, extraQs = {}) {
267
241
  if (v !== undefined)
268
242
  initUrl.searchParams.set(k, String(v));
269
243
  }
270
- const initRes = await fetch(initUrl.toString(), {
244
+ const initRes = await driveResponse(ctx, initUrl.toString(), {
271
245
  method: "POST",
272
246
  headers: {
273
- Authorization: `Bearer ${token}`,
274
247
  "Content-Type": "application/json; charset=UTF-8",
275
248
  "X-Upload-Content-Type": mimeType,
276
249
  "X-Upload-Content-Length": String(c.size),
@@ -278,12 +251,9 @@ async function uploadBytes(ctx, metadata, c, extraQs = {}) {
278
251
  body: JSON.stringify(metadata),
279
252
  });
280
253
  if (!initRes.ok) {
281
- const t = await initRes.text();
282
- throw new Error(`googleDrive: resumable init failed (${initRes.status}): ${t}`);
254
+ throw new Error(`googleDrive: resumable init failed (HTTP ${initRes.status})`);
283
255
  }
284
- const uploadUrl = initRes.headers.get("location");
285
- if (!uploadUrl)
286
- throw new Error("googleDrive: resumable session missing Location header");
256
+ const uploadUrl = uploadSession(initRes.headers.get("location"));
287
257
  // Stream in 2 MiB chunks. Must be a multiple of 256 KiB per Drive docs.
288
258
  const CHUNK_SIZE = 2 * 1024 * 1024;
289
259
  const stream = createReadStream(c.path, { highWaterMark: CHUNK_SIZE });
@@ -294,10 +264,9 @@ async function uploadBytes(ctx, metadata, c, extraQs = {}) {
294
264
  const flushChunk = async (chunk, isLast) => {
295
265
  const start = offset;
296
266
  const end = offset + chunk.byteLength - 1;
297
- const res = await fetch(uploadUrl, {
267
+ const res = await driveResponse(ctx, uploadUrl, {
298
268
  method: "PUT",
299
269
  headers: {
300
- "Content-Length": String(chunk.byteLength),
301
270
  "Content-Range": `bytes ${start}-${end}/${c.size}`,
302
271
  },
303
272
  body: new Uint8Array(chunk),
@@ -313,8 +282,7 @@ async function uploadBytes(ctx, metadata, c, extraQs = {}) {
313
282
  await res.text();
314
283
  }
315
284
  else {
316
- const t = await res.text();
317
- throw new Error(`googleDrive: resumable chunk failed (${res.status}): ${t}`);
285
+ throw new Error(`googleDrive: resumable chunk failed (HTTP ${res.status})`);
318
286
  }
319
287
  void isLast;
320
288
  };
@@ -418,6 +386,7 @@ export default function googleDrive(rl) {
418
386
  ],
419
387
  });
420
388
  rl.setConnectionSchema({
389
+ authMethod: { type: "string", required: false, description: "delegated or serviceAccount (legacy configs infer the method)" },
421
390
  clientId: {
422
391
  type: "string",
423
392
  required: false,
@@ -554,11 +523,9 @@ export default function googleDrive(rl) {
554
523
  const doc = (await driveRequest(ctx, "POST", "/drive/v3/files", metadata, {
555
524
  supportsAllDrives: true,
556
525
  }));
557
- const token = await accessToken(ctx);
558
- const res = await fetch(`https://docs.googleapis.com/v1/documents/${doc.id}:batchUpdate`, {
526
+ const res = await driveResponse(ctx, `https://docs.googleapis.com/v1/documents/${encodeURIComponent(doc.id)}:batchUpdate`, {
559
527
  method: "POST",
560
528
  headers: {
561
- Authorization: `Bearer ${token}`,
562
529
  "Content-Type": "application/json",
563
530
  },
564
531
  body: JSON.stringify({
@@ -573,7 +540,7 @@ export default function googleDrive(rl) {
573
540
  }),
574
541
  });
575
542
  if (!res.ok) {
576
- throw new Error(`googleDrive: docs.batchUpdate failed (${res.status}): ${await res.text()}`);
543
+ throw new Error(`googleDrive: docs.batchUpdate failed (HTTP ${res.status})`);
577
544
  }
578
545
  return { id: doc.id };
579
546
  }
@@ -608,7 +575,6 @@ export default function googleDrive(rl) {
608
575
  const fileId = p.fileId;
609
576
  const meta = (await driveRequest(ctx, "GET", `/drive/v3/files/${fileId}`, undefined, { fields: "mimeType,name", supportsAllDrives: true }));
610
577
  const isGoogleNative = meta.mimeType?.includes("vnd.google-apps");
611
- const token = await accessToken(ctx);
612
578
  let url;
613
579
  let contentType = meta.mimeType;
614
580
  if (isGoogleNative) {
@@ -621,22 +587,20 @@ export default function googleDrive(rl) {
621
587
  };
622
588
  const mime = p.googleDocFormat ?? defaults[type] ?? "application/pdf";
623
589
  contentType = mime;
624
- const u = new URL(`${API_BASE}/drive/v3/files/${fileId}/export`);
590
+ const u = new URL(`${API_BASE}/drive/v3/files/${filePathId(fileId)}/export`);
625
591
  u.searchParams.set("mimeType", mime);
626
592
  u.searchParams.set("supportsAllDrives", "true");
627
593
  url = u.toString();
628
594
  }
629
595
  else {
630
- const u = new URL(`${API_BASE}/drive/v3/files/${fileId}`);
596
+ const u = new URL(`${API_BASE}/drive/v3/files/${filePathId(fileId)}`);
631
597
  u.searchParams.set("alt", "media");
632
598
  u.searchParams.set("supportsAllDrives", "true");
633
599
  url = u.toString();
634
600
  }
635
- const res = await fetch(url, {
636
- headers: { Authorization: `Bearer ${token}` },
637
- });
601
+ const res = await driveResponse(ctx, url);
638
602
  if (!res.ok) {
639
- throw new Error(`googleDrive: download failed (${res.status}): ${await res.text()}`);
603
+ throw new Error(`googleDrive: download failed (HTTP ${res.status})`);
640
604
  }
641
605
  const bytes = Buffer.from(await res.arrayBuffer());
642
606
  const fileName = meta.name;
@@ -754,9 +718,8 @@ export default function googleDrive(rl) {
754
718
  if (hasBytes) {
755
719
  const c = resolveContent(p);
756
720
  const mimeType = c.mimeType ?? p.mimeType ?? "application/octet-stream";
757
- const token = await accessToken(ctx);
758
721
  if (c.buffer) {
759
- const url = new URL(`${API_BASE}/upload/drive/v3/files/${fileId}`);
722
+ const url = new URL(`${API_BASE}/upload/drive/v3/files/${filePathId(fileId)}`);
760
723
  url.searchParams.set("uploadType", "media");
761
724
  url.searchParams.set("supportsAllDrives", "true");
762
725
  for (const key of [
@@ -768,22 +731,20 @@ export default function googleDrive(rl) {
768
731
  url.searchParams.set(key, String(p[key]));
769
732
  }
770
733
  }
771
- const res = await fetch(url.toString(), {
734
+ const res = await driveResponse(ctx, url.toString(), {
772
735
  method: "PATCH",
773
736
  headers: {
774
- Authorization: `Bearer ${token}`,
775
737
  "Content-Type": mimeType,
776
- "Content-Length": String(c.buffer.byteLength),
777
738
  },
778
739
  body: new Uint8Array(c.buffer),
779
740
  });
780
741
  if (!res.ok) {
781
- throw new Error(`googleDrive: content update failed (${res.status}): ${await res.text()}`);
742
+ throw new Error(`googleDrive: content update failed (HTTP ${res.status})`);
782
743
  }
783
744
  }
784
745
  else if (c.path) {
785
746
  // Resumable PATCH
786
- const initUrl = new URL(`${API_BASE}/upload/drive/v3/files/${fileId}`);
747
+ const initUrl = new URL(`${API_BASE}/upload/drive/v3/files/${filePathId(fileId)}`);
787
748
  initUrl.searchParams.set("uploadType", "resumable");
788
749
  initUrl.searchParams.set("supportsAllDrives", "true");
789
750
  for (const key of [
@@ -795,36 +756,34 @@ export default function googleDrive(rl) {
795
756
  initUrl.searchParams.set(key, String(p[key]));
796
757
  }
797
758
  }
798
- const initRes = await fetch(initUrl.toString(), {
759
+ const initRes = await driveResponse(ctx, initUrl.toString(), {
799
760
  method: "PATCH",
800
761
  headers: {
801
- Authorization: `Bearer ${token}`,
802
762
  "X-Upload-Content-Type": mimeType,
803
763
  "X-Upload-Content-Length": String(c.size),
804
764
  },
805
765
  });
806
766
  if (!initRes.ok) {
807
- throw new Error(`googleDrive: resumable update init failed (${initRes.status}): ${await initRes.text()}`);
767
+ throw new Error(`googleDrive: resumable update init failed (HTTP ${initRes.status})`);
808
768
  }
809
- const uploadUrl = initRes.headers.get("location");
810
- if (!uploadUrl)
811
- throw new Error("googleDrive: missing Location on resumable init");
769
+ const uploadUrl = uploadSession(initRes.headers.get("location"));
812
770
  const CHUNK = 2 * 1024 * 1024;
813
771
  const stream = createReadStream(c.path, { highWaterMark: CHUNK });
814
772
  let offset = 0;
815
773
  let pending = Buffer.alloc(0);
774
+ let lastStatus = 0;
816
775
  const flush = async (chunk) => {
817
- const res = await fetch(uploadUrl, {
776
+ const res = await driveResponse(ctx, uploadUrl, {
818
777
  method: "PUT",
819
778
  headers: {
820
- "Content-Length": String(chunk.byteLength),
821
779
  "Content-Range": `bytes ${offset}-${offset + chunk.byteLength - 1}/${c.size}`,
822
780
  },
823
781
  body: new Uint8Array(chunk),
824
782
  });
825
783
  offset += chunk.byteLength;
784
+ lastStatus = res.status;
826
785
  if (res.status !== 200 && res.status !== 201 && res.status !== 308) {
827
- throw new Error(`googleDrive: resumable chunk failed (${res.status}): ${await res.text()}`);
786
+ throw new Error(`googleDrive: resumable chunk failed (HTTP ${res.status})`);
828
787
  }
829
788
  await res.text();
830
789
  };
@@ -837,6 +796,8 @@ export default function googleDrive(rl) {
837
796
  }
838
797
  if (pending.byteLength > 0)
839
798
  await flush(pending);
799
+ if (offset !== c.size || (lastStatus !== 200 && lastStatus !== 201))
800
+ throw new Error("googleDrive: resumable update did not complete");
840
801
  }
841
802
  }
842
803
  // Step 2: metadata patch
@@ -1484,14 +1445,11 @@ export default function googleDrive(rl) {
1484
1445
  const p = (input ?? {});
1485
1446
  const fileId = p.fileId;
1486
1447
  const revisionId = p.revisionId;
1487
- const token = await accessToken(ctx);
1488
- const url = new URL(`${API_BASE}/drive/v3/files/${fileId}/revisions/${revisionId}`);
1448
+ const url = new URL(`${API_BASE}/drive/v3/files/${filePathId(fileId)}/revisions/${filePathId(revisionId)}`);
1489
1449
  url.searchParams.set("alt", "media");
1490
- const res = await fetch(url.toString(), {
1491
- headers: { Authorization: `Bearer ${token}` },
1492
- });
1450
+ const res = await driveResponse(ctx, url.toString());
1493
1451
  if (!res.ok) {
1494
- throw new Error(`googleDrive: revision download failed (${res.status}): ${await res.text()}`);
1452
+ throw new Error(`googleDrive: revision download failed (HTTP ${res.status})`);
1495
1453
  }
1496
1454
  const bytes = Buffer.from(await res.arrayBuffer());
1497
1455
  const mimeType = res.headers.get("content-type") ?? "application/octet-stream";
@@ -1563,33 +1521,30 @@ export default function googleDrive(rl) {
1563
1521
  const p = (input ?? {});
1564
1522
  const fileId = p.fileId;
1565
1523
  const revisionId = p.revisionId;
1566
- const token = await accessToken(ctx);
1567
1524
  // 1. Pull the chosen revision's bytes.
1568
- const dl = await fetch(`${API_BASE}/drive/v3/files/${fileId}/revisions/${revisionId}?alt=media`, { headers: { Authorization: `Bearer ${token}` } });
1525
+ const dl = await driveResponse(ctx, `${API_BASE}/drive/v3/files/${encodeURIComponent(fileId)}/revisions/${encodeURIComponent(revisionId)}?alt=media`);
1569
1526
  if (!dl.ok) {
1570
- throw new Error(`googleDrive: revision restore download failed (${dl.status}): ${await dl.text()}`);
1527
+ throw new Error(`googleDrive: revision restore download failed (HTTP ${dl.status})`);
1571
1528
  }
1572
1529
  const bytes = Buffer.from(await dl.arrayBuffer());
1573
1530
  const mime = p.mimeType ??
1574
1531
  dl.headers.get("content-type") ??
1575
1532
  "application/octet-stream";
1576
1533
  // 2. Multipart-PATCH them as the new head of the same file.
1577
- const url = new URL(`${API_BASE}/upload/drive/v3/files/${fileId}`);
1534
+ const url = new URL(`${API_BASE}/upload/drive/v3/files/${filePathId(fileId)}`);
1578
1535
  url.searchParams.set("uploadType", "multipart");
1579
1536
  url.searchParams.set("supportsAllDrives", "true");
1580
1537
  url.searchParams.set("fields", "id,name,mimeType,modifiedTime,size,headRevisionId,webViewLink");
1581
1538
  const body = buildMultipart({ mimeType: mime }, bytes, mime);
1582
- const res = await fetch(url.toString(), {
1539
+ const res = await driveResponse(ctx, url.toString(), {
1583
1540
  method: "PATCH",
1584
1541
  headers: {
1585
- Authorization: `Bearer ${token}`,
1586
1542
  "Content-Type": `multipart/related; boundary=${MULTIPART_BOUNDARY}`,
1587
- "Content-Length": String(body.byteLength),
1588
1543
  },
1589
1544
  body: new Uint8Array(body),
1590
1545
  });
1591
1546
  if (!res.ok) {
1592
- throw new Error(`googleDrive: revision restore upload failed (${res.status}): ${await res.text()}`);
1547
+ throw new Error(`googleDrive: revision restore upload failed (HTTP ${res.status})`);
1593
1548
  }
1594
1549
  const head = (await res.json());
1595
1550
  return { fileId, restoredFromRevisionId: revisionId, head };
@@ -1816,12 +1771,11 @@ export default function googleDrive(rl) {
1816
1771
  }, { additionalProperties: false }),
1817
1772
  async execute(input, ctx) {
1818
1773
  const p = (input ?? {});
1819
- const token = await accessToken(ctx);
1820
- const u = new URL(`${API_BASE}/drive/v3/files/${p.fileId}/export`);
1774
+ const u = new URL(`${API_BASE}/drive/v3/files/${filePathId(String(p.fileId))}/export`);
1821
1775
  u.searchParams.set("mimeType", p.mimeType);
1822
- const res = await fetch(u.toString(), { headers: { Authorization: `Bearer ${token}` } });
1776
+ const res = await driveResponse(ctx, u.toString());
1823
1777
  if (!res.ok) {
1824
- throw new Error(`googleDrive: export failed (${res.status}): ${await res.text()}`);
1778
+ throw new Error(`googleDrive: export failed (HTTP ${res.status})`);
1825
1779
  }
1826
1780
  const bytes = Buffer.from(await res.arrayBuffer());
1827
1781
  const contentType = res.headers.get("content-type") ?? p.mimeType;
@@ -35,7 +35,7 @@
35
35
  * raw rows / objects and callers post-filter.
36
36
  */
37
37
  import * as t from "typebox";
38
- import { googleAccessToken } from "../../_shared/googleAuth.js";
38
+ import { googleJsonRequest } from "../../_shared/googleAuth.js";
39
39
  import { Id, JsonValue, NonEmptyString, NonNegativeInteger, PositiveInteger, RawGoogleObject, StringArray, stringEnum, } from "../../_shared/googleSchemas.js";
40
40
  const ROW_NUMBER = "row_number";
41
41
  const sheetReferenceSchema = t.Union([NonEmptyString, NonNegativeInteger]);
@@ -73,48 +73,10 @@ const initialSheetSchema = t.Object({
73
73
  columnGroupControlAfter: t.Optional(t.Boolean()),
74
74
  }, { additionalProperties: false })),
75
75
  }, { additionalProperties: false });
76
- // ─── Auth ────────────────────────────────────────────────────────
77
- async function accessToken(ctx) {
78
- return googleAccessToken(ctx, "googleSheets", SCOPES);
79
- }
80
76
  // ─── Request ─────────────────────────────────────────────────────
81
77
  const SHEETS_BASE = "https://sheets.googleapis.com";
82
78
  async function sheetsRequest(ctx, method, path, body, qs, baseOverride) {
83
- const token = await accessToken(ctx);
84
- const url = new URL(`${baseOverride ?? SHEETS_BASE}${path}`);
85
- if (qs) {
86
- for (const [k, v] of Object.entries(qs)) {
87
- if (v === undefined || v === null)
88
- continue;
89
- if (Array.isArray(v)) {
90
- for (const entry of v)
91
- url.searchParams.append(k, String(entry));
92
- }
93
- else {
94
- url.searchParams.set(k, String(v));
95
- }
96
- }
97
- }
98
- const init = {
99
- method,
100
- headers: {
101
- Authorization: `Bearer ${token}`,
102
- Accept: "application/json",
103
- },
104
- };
105
- if (body && Object.keys(body).length > 0) {
106
- init.headers["Content-Type"] =
107
- "application/json";
108
- init.body = JSON.stringify(body);
109
- }
110
- const res = await fetch(url.toString(), init);
111
- if (res.status === 204)
112
- return { success: true };
113
- const text = await res.text();
114
- if (!res.ok) {
115
- throw new Error(`googleSheets: ${method} ${path} → ${res.status} ${text}`);
116
- }
117
- return text ? JSON.parse(text) : { success: true };
79
+ return googleJsonRequest(ctx, "googleSheets", SCOPES, method, `${baseOverride ?? SHEETS_BASE}${path}`, body, qs);
118
80
  }
119
81
  // ─── A1 helpers ─────────────────────────────────────────────────
120
82
  /**
@@ -411,6 +373,7 @@ export default function googleSheets(rl) {
411
373
  ],
412
374
  });
413
375
  rl.setConnectionSchema({
376
+ authMethod: { type: "string", required: false, description: "delegated or serviceAccount (legacy configs infer the method)" },
414
377
  clientId: {
415
378
  type: "string",
416
379
  required: false,
@@ -21,47 +21,12 @@
21
21
  */
22
22
  import { writeFileSync } from "node:fs";
23
23
  import * as t from "typebox";
24
- import { googleAccessToken } from "../../_shared/googleAuth.js";
24
+ import { googleJsonRequest, googleDownload } from "../../_shared/googleAuth.js";
25
25
  import { Id, NonEmptyString, PositiveInteger, RawGoogleObject, StringArray, stringEnum, } from "../../_shared/googleSchemas.js";
26
- // ─── Auth ────────────────────────────────────────────────────────
27
- async function accessToken(ctx) {
28
- return googleAccessToken(ctx, "googleSlides", SCOPES);
29
- }
30
26
  // ─── Request ─────────────────────────────────────────────────────
31
27
  const API_BASE = "https://slides.googleapis.com/v1";
32
28
  async function slidesRequest(ctx, method, path, body, qs) {
33
- const token = await accessToken(ctx);
34
- const url = new URL(`${API_BASE}${path}`);
35
- if (qs) {
36
- for (const [k, v] of Object.entries(qs)) {
37
- if (v === undefined || v === null)
38
- continue;
39
- if (Array.isArray(v)) {
40
- for (const entry of v)
41
- url.searchParams.append(k, String(entry));
42
- }
43
- else {
44
- url.searchParams.set(k, String(v));
45
- }
46
- }
47
- }
48
- const init = {
49
- method,
50
- headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
51
- };
52
- if (body && Object.keys(body).length > 0) {
53
- init.headers["Content-Type"] =
54
- "application/json";
55
- init.body = JSON.stringify(body);
56
- }
57
- const res = await fetch(url.toString(), init);
58
- if (res.status === 204)
59
- return { success: true };
60
- const text = await res.text();
61
- if (!res.ok) {
62
- throw new Error(`googleSlides: ${method} ${path} → ${res.status} ${text}`);
63
- }
64
- return text ? JSON.parse(text) : { success: true };
29
+ return googleJsonRequest(ctx, "googleSlides", SCOPES, method, `${API_BASE}${path}`, body, qs);
65
30
  }
66
31
  // ─── Helpers ────────────────────────────────────────────────────
67
32
  const PRES_URL_REGEX = /https:\/\/docs\.google\.com\/presentation\/d\/([a-zA-Z0-9-_]+)/;
@@ -112,6 +77,7 @@ export default function googleSlides(rl) {
112
77
  ],
113
78
  });
114
79
  rl.setConnectionSchema({
80
+ authMethod: { type: "string", required: false, description: "delegated or serviceAccount (legacy configs infer the method)" },
115
81
  clientId: {
116
82
  type: "string",
117
83
  required: false,
@@ -289,9 +255,9 @@ export default function googleSlides(rl) {
289
255
  if (!p.savePath && !p.download)
290
256
  return res;
291
257
  // contentUrl is a signed Google URL — fetch without auth headers.
292
- const imgRes = await fetch(res.contentUrl);
258
+ const imgRes = await googleDownload(res.contentUrl);
293
259
  if (!imgRes.ok) {
294
- throw new Error(`googleSlides: thumbnail fetch failed (${imgRes.status}): ${await imgRes.text()}`);
260
+ throw new Error(`googleSlides: thumbnail fetch failed (HTTP ${imgRes.status})`);
295
261
  }
296
262
  const bytes = Buffer.from(await imgRes.arrayBuffer());
297
263
  if (typeof p.savePath === "string") {
@@ -20,40 +20,12 @@
20
20
  * normalize ISO input via `Date.toISOString()` when detected.
21
21
  */
22
22
  import * as t from "typebox";
23
- import { googleAccessToken } from "../../_shared/googleAuth.js";
23
+ import { googleJsonRequest } from "../../_shared/googleAuth.js";
24
24
  import { GoogleTimestamp, Id, stringEnum, } from "../../_shared/googleSchemas.js";
25
- // ─── Auth ────────────────────────────────────────────────────────
26
- async function accessToken(ctx) {
27
- return googleAccessToken(ctx, "googleTasks", SCOPES);
28
- }
29
25
  // ─── Request ─────────────────────────────────────────────────────
30
26
  const API_BASE = "https://tasks.googleapis.com/tasks/v1";
31
27
  async function tasksRequest(ctx, method, path, body, qs) {
32
- const token = await accessToken(ctx);
33
- const url = new URL(`${API_BASE}${path}`);
34
- if (qs) {
35
- for (const [k, v] of Object.entries(qs)) {
36
- if (v === undefined || v === null)
37
- continue;
38
- url.searchParams.set(k, String(v));
39
- }
40
- }
41
- const init = {
42
- method,
43
- headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
44
- };
45
- if (body && Object.keys(body).length > 0) {
46
- init.headers["Content-Type"] = "application/json";
47
- init.body = JSON.stringify(body);
48
- }
49
- const res = await fetch(url.toString(), init);
50
- if (res.status === 204)
51
- return { success: true };
52
- const text = await res.text();
53
- if (!res.ok) {
54
- throw new Error(`googleTasks: ${method} ${path} → ${res.status} ${text}`);
55
- }
56
- return text ? JSON.parse(text) : { success: true };
28
+ return googleJsonRequest(ctx, "googleTasks", SCOPES, method, `${API_BASE}${path}`, body, qs);
57
29
  }
58
30
  async function paginateAll(ctx, path, qs) {
59
31
  const out = [];
@@ -129,6 +101,7 @@ export default function googleTasks(rl) {
129
101
  ],
130
102
  });
131
103
  rl.setConnectionSchema({
104
+ authMethod: { type: "string", required: false, description: "delegated or serviceAccount (legacy configs infer the method)" },
132
105
  clientId: { type: "string", required: false, env: "GOOGLE_TASKS_CLIENT_ID" },
133
106
  clientSecret: { type: "string", required: false, env: "GOOGLE_TASKS_CLIENT_SECRET" },
134
107
  refreshToken: { type: "string", required: false, env: "GOOGLE_TASKS_REFRESH_TOKEN" },
@@ -1,28 +1,4 @@
1
- let cachedToken = null;
2
- async function getToken(authUrl, clientId, clientSecret, scope, tenant) {
3
- if (cachedToken && Date.now() < cachedToken.expiresAt)
4
- return cachedToken.token;
5
- const url = tenant ? `${authUrl}?tenant=${tenant}` : authUrl;
6
- const body = new URLSearchParams({
7
- grant_type: "client_credentials",
8
- client_id: clientId,
9
- client_secret: clientSecret,
10
- scope,
11
- });
12
- const res = await fetch(url, {
13
- method: "POST",
14
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
15
- body: body.toString(),
16
- });
17
- if (!res.ok)
18
- throw new Error(`HaloPSA auth error ${res.status}: ${await res.text()}`);
19
- const data = (await res.json());
20
- cachedToken = {
21
- token: data.access_token,
22
- expiresAt: Date.now() + (data.expires_in - 60) * 1000,
23
- };
24
- return cachedToken.token;
25
- }
1
+ import { coordinatedAccessToken, requestToken, } from "../../_shared/tokenRefresh.js";
26
2
  async function apiRequest(apiUrl, token, method, endpoint, body, qs) {
27
3
  const url = new URL(`${apiUrl}${endpoint}`);
28
4
  if (qs) {
@@ -51,12 +27,21 @@ async function apiRequest(apiUrl, token, method, endpoint, body, qs) {
51
27
  return res.json();
52
28
  }
53
29
  async function req(ctx, method, endpoint, body, qs) {
54
- const cfg = ctx.connection.config;
55
- const authUrl = cfg.hostingType === "on-premise"
56
- ? `${cfg.appUrl}/auth/token`
57
- : `${cfg.authUrl}/token`;
58
- const token = await getToken(authUrl, cfg.clientId, cfg.clientSecret, cfg.scope ?? "all", cfg.tenant);
59
- return apiRequest(cfg.resourceApiUrl.replace(/\/$/, ""), token, method, endpoint, body, qs);
30
+ const token = await coordinatedAccessToken(ctx, (cfg) => {
31
+ const url = new URL(cfg.hostingType === "on-premise"
32
+ ? `${cfg.appUrl}/auth/token`
33
+ : `${cfg.authUrl}/token`);
34
+ if (cfg.tenant)
35
+ url.searchParams.set("tenant", cfg.tenant);
36
+ return requestToken({ url: url.toString(), clientAuthentication: "client_secret_post" }, {
37
+ grant_type: "client_credentials",
38
+ scope: cfg.scope ?? "all",
39
+ }, {
40
+ clientId: cfg.clientId,
41
+ clientSecret: cfg.clientSecret,
42
+ });
43
+ });
44
+ return apiRequest(ctx.connection.config.resourceApiUrl.replace(/\/$/, ""), token, method, endpoint, body, qs);
60
45
  }
61
46
  function registerCrud(rl, resource, apiPath) {
62
47
  rl.registerAction(`${resource}.create`, {
@@ -1,9 +1,9 @@
1
1
  import * as t from "typebox";
2
2
  import { assertCommentInScope, assertIssueInScope, COMMENT_FIELDS, gql, key, requireUnscoped, } from "./shared.js";
3
3
  export function registerCommentActions(rl) {
4
- rl.registerAction("issue.addComment", {
4
+ rl.registerAction("comment.create", {
5
5
  access: "write",
6
- description: "Add a comment to an issue. Pass parentId to nest as a reply.",
6
+ description: "Create a comment on an issue. Pass parentId to nest as a reply.",
7
7
  inputSchema: t.Object({
8
8
  issueId: t.String({
9
9
  description: "The issue to associate the comment with. UUID or issue identifier (e.g., 'LIN-123')",
@@ -14,7 +14,7 @@ import { registerViewActions } from "./views.js";
14
14
  import { registerWebhookActions } from "./webhooks.js";
15
15
  export default function linear(rl) {
16
16
  rl.setName("linear");
17
- rl.setVersion("0.4.0");
17
+ rl.setVersion("0.5.0");
18
18
  rl.setConnectionSchema(t.Object({
19
19
  apiKey: t.String({
20
20
  description: "Linear API key (https://linear.app/settings/account/security)",