@churchapps/content-providers 0.5.0 → 0.6.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 CHANGED
@@ -60,8 +60,14 @@ import { BaseProvider, registerProvider } from '@churchapps/content-providers';
60
60
 
61
61
  class MyProvider extends BaseProvider {
62
62
  // Declare id/name/logos/config/requiresAuth/authTypes/capabilities and implement browse().
63
- // Optionally implement getPlaylist()/getInstructions(); the protected apiRequest() helper
64
- // performs authenticated JSON fetches against config.apiBase.
63
+ // Every declared authType/capability must be backed by its IProvider methods (enforced by
64
+ // tests/contentProviders.test.ts):
65
+ // oauth_pkce -> generateCodeVerifier / buildAuthUrl / buildAuthUrlFromChallenge / exchangeCodeForTokens
66
+ // (OAuthHelper implements the standard flow; delegate to it like SignPresenterProvider)
67
+ // device_flow -> initiateDeviceFlow / pollDeviceFlowToken (see DeviceFlowHelper)
68
+ // form_login -> performLogin
69
+ // playlist / instructions / mediaLicensing -> getPlaylist / getInstructions / checkMediaLicense
70
+ // The protected apiRequest() helper performs authenticated JSON fetches against config.apiBase.
65
71
  }
66
72
 
67
73
  registerProvider(new MyProvider());
package/dist/index.cjs CHANGED
@@ -143,6 +143,9 @@ var OAuthHelper = class {
143
143
  }
144
144
  async buildAuthUrl(config, codeVerifier, redirectUri, state) {
145
145
  const codeChallenge = await generateCodeChallenge(codeVerifier);
146
+ return { url: this.buildAuthUrlFromChallenge(config, codeChallenge, redirectUri, state || ""), challengeMethod: "S256" };
147
+ }
148
+ buildAuthUrlFromChallenge(config, codeChallenge, redirectUri, state) {
146
149
  const params = new URLSearchParams({
147
150
  response_type: "code",
148
151
  client_id: config.clientId,
@@ -150,9 +153,9 @@ var OAuthHelper = class {
150
153
  scope: config.scopes.join(" "),
151
154
  code_challenge: codeChallenge,
152
155
  code_challenge_method: "S256",
153
- state: state || ""
156
+ state
154
157
  });
155
- return { url: `${config.oauthBase}/authorize?${params.toString()}`, challengeMethod: "S256" };
158
+ return `${config.oauthBase}/authorize?${params.toString()}`;
156
159
  }
157
160
  async exchangeCodeForTokens(config, _providerId, code, codeVerifier, redirectUri) {
158
161
  try {
@@ -334,12 +337,15 @@ function filesToInstructions(name, files, section) {
334
337
  const items = section ? [{ id: section.id, itemType: "section", label: section.label ?? name, children: actions }] : actions;
335
338
  return { name, items };
336
339
  }
340
+ function instructionItemMediaType(item) {
341
+ return detectMediaType(`${item.downloadUrl || ""} ${item.label || ""}`, item.mediaType);
342
+ }
337
343
  function instructionsToPlaylist(instructions) {
338
344
  const files = [];
339
345
  function extractFiles(items) {
340
346
  for (const item of items) {
341
347
  if (item.downloadUrl && (item.itemType === "file" || !item.children?.length)) {
342
- files.push({ type: "file", id: item.id || item.relatedId || generateId(), title: item.label || "Untitled", mediaType: detectMediaType(item.downloadUrl), url: item.downloadUrl, downloadUrl: item.downloadUrl, seconds: item.seconds, thumbnail: item.thumbnail });
348
+ files.push({ type: "file", id: item.id || item.relatedId || generateId(), title: item.label || "Untitled", mediaType: instructionItemMediaType(item), url: item.downloadUrl, downloadUrl: item.downloadUrl, seconds: item.seconds, thumbnail: item.thumbnail });
343
349
  }
344
350
  if (item.children) extractFiles(item.children);
345
351
  }
@@ -449,6 +455,7 @@ function extractArray(response, ...keys) {
449
455
  var APlayProvider = class extends BaseProvider {
450
456
  constructor() {
451
457
  super(...arguments);
458
+ this.oauthHelper = new OAuthHelper();
452
459
  this.id = "aplay";
453
460
  this.name = "APlay";
454
461
  this.logos = { light: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg", dark: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg" };
@@ -518,6 +525,18 @@ var APlayProvider = class extends BaseProvider {
518
525
  async checkMediaLicense(mediaId, auth) {
519
526
  return checkMediaLicense(mediaId, auth);
520
527
  }
528
+ generateCodeVerifier() {
529
+ return this.oauthHelper.generateCodeVerifier();
530
+ }
531
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
532
+ return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
533
+ }
534
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
535
+ return this.oauthHelper.buildAuthUrlFromChallenge(this.config, codeChallenge, redirectUri, state);
536
+ }
537
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
538
+ return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
539
+ }
521
540
  };
522
541
 
523
542
  // src/providers/signPresenter/SignPresenterProvider.ts
@@ -596,13 +615,15 @@ var SignPresenterProvider = class extends BaseProvider {
596
615
  const instructions = filesToInstructions(title, files, { id: playlistId + "-section", label: title });
597
616
  return { name: title, items: [{ id: playlistId + "-header", itemType: "header", label: title, children: instructions.items }] };
598
617
  }
599
- // Auth methods
600
618
  generateCodeVerifier() {
601
619
  return this.oauthHelper.generateCodeVerifier();
602
620
  }
603
621
  async buildAuthUrl(codeVerifier, redirectUri, state) {
604
622
  return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
605
623
  }
624
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
625
+ return this.oauthHelper.buildAuthUrlFromChallenge(this.config, codeChallenge, redirectUri, state);
626
+ }
606
627
  async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
607
628
  return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
608
629
  }
@@ -975,8 +996,7 @@ function registerProvider(provider) {
975
996
  }
976
997
 
977
998
  // src/providers/b1Church/B1ChurchAuth.ts
978
- async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state) {
979
- const codeChallenge = await generateCodeChallenge(codeVerifier);
999
+ function buildB1AuthUrlFromChallenge(config, appBase, redirectUri, codeChallenge, state) {
980
1000
  const oauthParams = new URLSearchParams({
981
1001
  response_type: "code",
982
1002
  client_id: config.clientId,
@@ -984,10 +1004,13 @@ async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state)
984
1004
  scope: config.scopes.join(" "),
985
1005
  code_challenge: codeChallenge,
986
1006
  code_challenge_method: "S256",
987
- state: state || ""
1007
+ state
988
1008
  });
989
- const url = `${appBase}/oauth?${oauthParams.toString()}`;
990
- return { url, challengeMethod: "S256" };
1009
+ return `${appBase}/oauth?${oauthParams.toString()}`;
1010
+ }
1011
+ async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state) {
1012
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1013
+ return { url: buildB1AuthUrlFromChallenge(config, appBase, redirectUri, codeChallenge, state || ""), challengeMethod: "S256" };
991
1014
  }
992
1015
  async function exchangeCodeForTokensWithPKCE(config, code, redirectUri, codeVerifier) {
993
1016
  try {
@@ -1219,6 +1242,7 @@ function collectFilesFromNode(node) {
1219
1242
  id: node.id,
1220
1243
  name: node.label || "",
1221
1244
  url: node.downloadUrl,
1245
+ fileType: instructionItemMediaType(node),
1222
1246
  seconds: node.seconds || 10
1223
1247
  });
1224
1248
  }
@@ -1284,6 +1308,9 @@ function findFirstThumbnail(items) {
1284
1308
  }
1285
1309
  return void 0;
1286
1310
  }
1311
+ function isLessonsChurchContent(plan) {
1312
+ return !plan.providerId || plan.providerId === "lessonschurch";
1313
+ }
1287
1314
  function isExternalProviderItem(item) {
1288
1315
  if (!item.providerId || item.providerId === "b1church") return false;
1289
1316
  if (item.providerPath) return true;
@@ -1310,10 +1337,16 @@ var B1ChurchProvider = class extends BaseProvider {
1310
1337
  this.capabilities = { browse: true, playlist: true, instructions: true, mediaLicensing: false };
1311
1338
  this.planTypeId = null;
1312
1339
  }
1340
+ generateCodeVerifier() {
1341
+ return generateCodeVerifier();
1342
+ }
1313
1343
  async buildAuthUrl(codeVerifier, redirectUri, state) {
1314
1344
  return buildB1AuthUrl(this.config, this.appBase, redirectUri, codeVerifier, state);
1315
1345
  }
1316
- async exchangeCodeForTokensWithPKCE(code, redirectUri, codeVerifier) {
1346
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
1347
+ return buildB1AuthUrlFromChallenge(this.config, this.appBase, redirectUri, codeChallenge, state);
1348
+ }
1349
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
1317
1350
  return exchangeCodeForTokensWithPKCE(this.config, code, redirectUri, codeVerifier);
1318
1351
  }
1319
1352
  async exchangeCodeForTokensWithSecret(code, redirectUri, clientSecret) {
@@ -1449,7 +1482,7 @@ var B1ChurchProvider = class extends BaseProvider {
1449
1482
  const planFolder = plans.find((p) => p.id === planId);
1450
1483
  if (!planFolder) return null;
1451
1484
  const churchId = planFolder.churchId;
1452
- const venueId = planFolder.contentId;
1485
+ const venueId = isLessonsChurchContent(planFolder) ? planFolder.contentId : void 0;
1453
1486
  const planTitle = planFolder.name || "Plan";
1454
1487
  if (!churchId) {
1455
1488
  console.warn("[B1Church getInstructions] planFolder missing churchId:", planFolder.id);
@@ -1558,7 +1591,7 @@ var B1ChurchProvider = class extends BaseProvider {
1558
1591
  return null;
1559
1592
  }
1560
1593
  const churchId = planFolder.churchId;
1561
- const venueId = planFolder.contentId;
1594
+ const venueId = isLessonsChurchContent(planFolder) ? planFolder.contentId : void 0;
1562
1595
  if (!churchId) {
1563
1596
  console.warn("[B1Church] getPlaylist: planFolder missing churchId:", planFolder.id);
1564
1597
  return null;
@@ -1582,7 +1615,7 @@ var B1ChurchProvider = class extends BaseProvider {
1582
1615
  const venueFeed = venueId ? await fetchVenueFeed(venueId) : null;
1583
1616
  const files = [];
1584
1617
  for (const sectionItem of planItems) {
1585
- for (const child of sectionItem.children || []) {
1618
+ for (const child of sectionItem.children?.length ? sectionItem.children : [sectionItem]) {
1586
1619
  const childItemType = child.itemType;
1587
1620
  const isSectionType = childItemType === "section" || childItemType === "lessonSection" || childItemType === "providerSection";
1588
1621
  const canExpandLocally = isSectionType && venueFeed && child.relatedId;
@@ -1713,7 +1746,6 @@ var DropboxProvider = class extends BaseProvider {
1713
1746
  mediaLicensing: false
1714
1747
  };
1715
1748
  }
1716
- // -- Pagination helper --
1717
1749
  dropboxPost(endpoint, body, auth) {
1718
1750
  if (!auth) {
1719
1751
  console.error("[Dropbox] No auth token provided");
@@ -1747,7 +1779,6 @@ var DropboxProvider = class extends BaseProvider {
1747
1779
  }
1748
1780
  return allEntries;
1749
1781
  }
1750
- // -- File link helpers --
1751
1782
  async getSharedLink(filePath, auth) {
1752
1783
  if (!auth) return null;
1753
1784
  try {
@@ -1803,7 +1834,6 @@ var DropboxProvider = class extends BaseProvider {
1803
1834
  );
1804
1835
  return results.filter((r) => r.viewUrl || r.downloadUrl).map((r) => fileEntryToContentFile(r.entry, r.viewUrl || r.downloadUrl, r.downloadUrl));
1805
1836
  }
1806
- // -- Leaf detection --
1807
1837
  async checkIsLeaf(folderPath, auth) {
1808
1838
  const response = await this.dropboxPost(
1809
1839
  "/2/files/list_folder",
@@ -1813,7 +1843,6 @@ var DropboxProvider = class extends BaseProvider {
1813
1843
  if (!response) return false;
1814
1844
  return !response.entries.some((e) => e[".tag"] === "folder");
1815
1845
  }
1816
- // -- IProvider methods --
1817
1846
  async browse(path, auth) {
1818
1847
  const dropboxPath = path && path !== "/" ? path.startsWith("/") ? path : "/" + path : "";
1819
1848
  const allEntries = await this.listAllEntries(dropboxPath, auth);
@@ -1825,7 +1854,6 @@ var DropboxProvider = class extends BaseProvider {
1825
1854
  const fileItems = await this.resolveFileLinks(mediaFiles, auth);
1826
1855
  return [...folderItems, ...fileItems];
1827
1856
  }
1828
- /** Resolve a folder path to playable files (list + media filter + link resolution). */
1829
1857
  async resolveMediaFiles(path, auth) {
1830
1858
  if (!path || path === "/") return null;
1831
1859
  const dropboxPath = path.startsWith("/") ? path : "/" + path;
@@ -1847,7 +1875,6 @@ var DropboxProvider = class extends BaseProvider {
1847
1875
  const folderName = segments[segments.length - 1] || "Dropbox";
1848
1876
  return filesToInstructions(folderName, resolved.files, { id: "dropbox-section", label: folderName });
1849
1877
  }
1850
- // -- Auth methods --
1851
1878
  generateCodeVerifier() {
1852
1879
  return this.oauthHelper.generateCodeVerifier();
1853
1880
  }
@@ -1904,7 +1931,8 @@ var PlanningCenterProvider = class extends BaseProvider {
1904
1931
  this.logos = { light: "https://www.planningcenter.com/icons/icon-512x512.png", dark: "https://www.planningcenter.com/icons/icon-512x512.png" };
1905
1932
  this.config = { id: "planningcenter", name: "Planning Center", apiBase: "https://api.planningcenteronline.com", oauthBase: "https://api.planningcenteronline.com/oauth", clientId: "", scopes: ["services"] };
1906
1933
  this.requiresAuth = true;
1907
- this.authTypes = ["oauth_pkce"];
1934
+ // No client-side auth flow: clientId is unset and no PKCE methods exist. Auth tokens must be supplied externally (server-side).
1935
+ this.authTypes = [];
1908
1936
  // Plan items come back without media URLs, so playlist/instructions are not actually supported yet.
1909
1937
  this.capabilities = { browse: true, playlist: false, instructions: false, mediaLicensing: false };
1910
1938
  }
@@ -16367,7 +16395,7 @@ function getAvailableProviders(ids) {
16367
16395
  }
16368
16396
 
16369
16397
  // src/index.ts
16370
- var VERSION = true ? "0.5.0" : "dev";
16398
+ var VERSION = true ? "0.6.0" : "dev";
16371
16399
  // Annotate the CommonJS export names for ESM import in node:
16372
16400
  0 && (module.exports = {
16373
16401
  APlayProvider,