@churchapps/content-providers 0.5.1 → 0.6.1

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
@@ -97,6 +97,7 @@ var TokenHelper = class {
97
97
  refresh_token: auth.refresh_token,
98
98
  client_id: config.clientId
99
99
  });
100
+ if (config.clientSecret) params.set("client_secret", config.clientSecret);
100
101
  const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
101
102
  if (!response.ok) {
102
103
  console.warn(`[TokenHelper] Token refresh failed: ${response.status} ${response.statusText}`);
@@ -143,6 +144,9 @@ var OAuthHelper = class {
143
144
  }
144
145
  async buildAuthUrl(config, codeVerifier, redirectUri, state) {
145
146
  const codeChallenge = await generateCodeChallenge(codeVerifier);
147
+ return { url: this.buildAuthUrlFromChallenge(config, codeChallenge, redirectUri, state || ""), challengeMethod: "S256" };
148
+ }
149
+ buildAuthUrlFromChallenge(config, codeChallenge, redirectUri, state) {
146
150
  const params = new URLSearchParams({
147
151
  response_type: "code",
148
152
  client_id: config.clientId,
@@ -150,9 +154,9 @@ var OAuthHelper = class {
150
154
  scope: config.scopes.join(" "),
151
155
  code_challenge: codeChallenge,
152
156
  code_challenge_method: "S256",
153
- state: state || ""
157
+ state
154
158
  });
155
- return { url: `${config.oauthBase}/authorize?${params.toString()}`, challengeMethod: "S256" };
159
+ return `${config.oauthBase}/authorize?${params.toString()}`;
156
160
  }
157
161
  async exchangeCodeForTokens(config, _providerId, code, codeVerifier, redirectUri) {
158
162
  try {
@@ -163,6 +167,7 @@ var OAuthHelper = class {
163
167
  client_id: config.clientId,
164
168
  code_verifier: codeVerifier
165
169
  });
170
+ if (config.clientSecret) params.set("client_secret", config.clientSecret);
166
171
  const tokenUrl = `${config.oauthBase}/token`;
167
172
  const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
168
173
  if (!response.ok) {
@@ -334,12 +339,15 @@ function filesToInstructions(name, files, section) {
334
339
  const items = section ? [{ id: section.id, itemType: "section", label: section.label ?? name, children: actions }] : actions;
335
340
  return { name, items };
336
341
  }
342
+ function instructionItemMediaType(item) {
343
+ return detectMediaType(`${item.downloadUrl || ""} ${item.label || ""}`, item.mediaType);
344
+ }
337
345
  function instructionsToPlaylist(instructions) {
338
346
  const files = [];
339
347
  function extractFiles(items) {
340
348
  for (const item of items) {
341
349
  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 });
350
+ 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
351
  }
344
352
  if (item.children) extractFiles(item.children);
345
353
  }
@@ -449,6 +457,7 @@ function extractArray(response, ...keys) {
449
457
  var APlayProvider = class extends BaseProvider {
450
458
  constructor() {
451
459
  super(...arguments);
460
+ this.oauthHelper = new OAuthHelper();
452
461
  this.id = "aplay";
453
462
  this.name = "APlay";
454
463
  this.logos = { light: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg", dark: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg" };
@@ -518,6 +527,18 @@ var APlayProvider = class extends BaseProvider {
518
527
  async checkMediaLicense(mediaId, auth) {
519
528
  return checkMediaLicense(mediaId, auth);
520
529
  }
530
+ generateCodeVerifier() {
531
+ return this.oauthHelper.generateCodeVerifier();
532
+ }
533
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
534
+ return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
535
+ }
536
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
537
+ return this.oauthHelper.buildAuthUrlFromChallenge(this.config, codeChallenge, redirectUri, state);
538
+ }
539
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
540
+ return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
541
+ }
521
542
  };
522
543
 
523
544
  // src/providers/signPresenter/SignPresenterProvider.ts
@@ -602,6 +623,9 @@ var SignPresenterProvider = class extends BaseProvider {
602
623
  async buildAuthUrl(codeVerifier, redirectUri, state) {
603
624
  return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
604
625
  }
626
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
627
+ return this.oauthHelper.buildAuthUrlFromChallenge(this.config, codeChallenge, redirectUri, state);
628
+ }
605
629
  async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
606
630
  return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
607
631
  }
@@ -961,21 +985,8 @@ var LessonsChurchProvider = class extends BaseProvider {
961
985
  }
962
986
  };
963
987
 
964
- // src/providers/registry.ts
965
- var providerRegistry = /* @__PURE__ */ new Map();
966
- function getProvider(providerId) {
967
- return providerRegistry.get(providerId) || null;
968
- }
969
- function getAllProviders() {
970
- return Array.from(providerRegistry.values());
971
- }
972
- function registerProvider(provider) {
973
- providerRegistry.set(provider.id, provider);
974
- }
975
-
976
988
  // src/providers/b1Church/B1ChurchAuth.ts
977
- async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state) {
978
- const codeChallenge = await generateCodeChallenge(codeVerifier);
989
+ function buildB1AuthUrlFromChallenge(config, appBase, redirectUri, codeChallenge, state) {
979
990
  const oauthParams = new URLSearchParams({
980
991
  response_type: "code",
981
992
  client_id: config.clientId,
@@ -983,10 +994,13 @@ async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state)
983
994
  scope: config.scopes.join(" "),
984
995
  code_challenge: codeChallenge,
985
996
  code_challenge_method: "S256",
986
- state: state || ""
997
+ state
987
998
  });
988
- const url = `${appBase}/oauth?${oauthParams.toString()}`;
989
- return { url, challengeMethod: "S256" };
999
+ return `${appBase}/oauth?${oauthParams.toString()}`;
1000
+ }
1001
+ async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state) {
1002
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1003
+ return { url: buildB1AuthUrlFromChallenge(config, appBase, redirectUri, codeChallenge, state || ""), challengeMethod: "S256" };
990
1004
  }
991
1005
  async function exchangeCodeForTokensWithPKCE(config, code, redirectUri, codeVerifier) {
992
1006
  try {
@@ -1218,6 +1232,7 @@ function collectFilesFromNode(node) {
1218
1232
  id: node.id,
1219
1233
  name: node.label || "",
1220
1234
  url: node.downloadUrl,
1235
+ fileType: instructionItemMediaType(node),
1221
1236
  seconds: node.seconds || 10
1222
1237
  });
1223
1238
  }
@@ -1283,6 +1298,9 @@ function findFirstThumbnail(items) {
1283
1298
  }
1284
1299
  return void 0;
1285
1300
  }
1301
+ function isLessonsChurchContent(plan) {
1302
+ return !plan.providerId || plan.providerId === "lessonschurch";
1303
+ }
1286
1304
  function isExternalProviderItem(item) {
1287
1305
  if (!item.providerId || item.providerId === "b1church") return false;
1288
1306
  if (item.providerPath) return true;
@@ -1309,10 +1327,16 @@ var B1ChurchProvider = class extends BaseProvider {
1309
1327
  this.capabilities = { browse: true, playlist: true, instructions: true, mediaLicensing: false };
1310
1328
  this.planTypeId = null;
1311
1329
  }
1330
+ generateCodeVerifier() {
1331
+ return generateCodeVerifier();
1332
+ }
1312
1333
  async buildAuthUrl(codeVerifier, redirectUri, state) {
1313
1334
  return buildB1AuthUrl(this.config, this.appBase, redirectUri, codeVerifier, state);
1314
1335
  }
1315
- async exchangeCodeForTokensWithPKCE(code, redirectUri, codeVerifier) {
1336
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
1337
+ return buildB1AuthUrlFromChallenge(this.config, this.appBase, redirectUri, codeChallenge, state);
1338
+ }
1339
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
1316
1340
  return exchangeCodeForTokensWithPKCE(this.config, code, redirectUri, codeVerifier);
1317
1341
  }
1318
1342
  async exchangeCodeForTokensWithSecret(code, redirectUri, clientSecret) {
@@ -1419,14 +1443,12 @@ var B1ChurchProvider = class extends BaseProvider {
1419
1443
  this.planTypeId = null;
1420
1444
  }
1421
1445
  }
1422
- async getCurrentPlan(_authData) {
1446
+ async getCurrentPlan(authData) {
1423
1447
  if (!this.planTypeId) return null;
1424
1448
  const plan = await fetchCurrentPlanByType(this.planTypeId);
1425
- if (!plan?.providerId || !plan.providerPlanId) return null;
1449
+ if (!plan?.providerId || !plan.providerPlanId || !plan.ministryId) return null;
1426
1450
  const items = plan.churchId && plan.id ? await fetchPlanItems(plan.churchId, plan.id) : [];
1427
- const innerProvider = getProvider(plan.providerId);
1428
- if (!innerProvider?.getInstructions) return null;
1429
- const instructions = await innerProvider.getInstructions(plan.providerPlanId, null);
1451
+ const instructions = await fetchFromProviderProxy("getInstructions", plan.ministryId, plan.providerId, plan.providerPlanId, authData);
1430
1452
  if (!instructions) return null;
1431
1453
  const files = getOrderedFiles(instructions, items);
1432
1454
  const thumbnail = findFirstThumbnail(instructions.items);
@@ -1448,7 +1470,7 @@ var B1ChurchProvider = class extends BaseProvider {
1448
1470
  const planFolder = plans.find((p) => p.id === planId);
1449
1471
  if (!planFolder) return null;
1450
1472
  const churchId = planFolder.churchId;
1451
- const venueId = planFolder.contentId;
1473
+ const venueId = isLessonsChurchContent(planFolder) ? planFolder.contentId : void 0;
1452
1474
  const planTitle = planFolder.name || "Plan";
1453
1475
  if (!churchId) {
1454
1476
  console.warn("[B1Church getInstructions] planFolder missing churchId:", planFolder.id);
@@ -1557,7 +1579,7 @@ var B1ChurchProvider = class extends BaseProvider {
1557
1579
  return null;
1558
1580
  }
1559
1581
  const churchId = planFolder.churchId;
1560
- const venueId = planFolder.contentId;
1582
+ const venueId = isLessonsChurchContent(planFolder) ? planFolder.contentId : void 0;
1561
1583
  if (!churchId) {
1562
1584
  console.warn("[B1Church] getPlaylist: planFolder missing churchId:", planFolder.id);
1563
1585
  return null;
@@ -1581,7 +1603,7 @@ var B1ChurchProvider = class extends BaseProvider {
1581
1603
  const venueFeed = venueId ? await fetchVenueFeed(venueId) : null;
1582
1604
  const files = [];
1583
1605
  for (const sectionItem of planItems) {
1584
- for (const child of sectionItem.children || []) {
1606
+ for (const child of sectionItem.children?.length ? sectionItem.children : [sectionItem]) {
1585
1607
  const childItemType = child.itemType;
1586
1608
  const isSectionType = childItemType === "section" || childItemType === "lessonSection" || childItemType === "providerSection";
1587
1609
  const canExpandLocally = isSectionType && venueFeed && child.relatedId;
@@ -1897,7 +1919,8 @@ var PlanningCenterProvider = class extends BaseProvider {
1897
1919
  this.logos = { light: "https://www.planningcenter.com/icons/icon-512x512.png", dark: "https://www.planningcenter.com/icons/icon-512x512.png" };
1898
1920
  this.config = { id: "planningcenter", name: "Planning Center", apiBase: "https://api.planningcenteronline.com", oauthBase: "https://api.planningcenteronline.com/oauth", clientId: "", scopes: ["services"] };
1899
1921
  this.requiresAuth = true;
1900
- this.authTypes = ["oauth_pkce"];
1922
+ // No client-side auth flow: clientId is unset and no PKCE methods exist. Auth tokens must be supplied externally (server-side).
1923
+ this.authTypes = [];
1901
1924
  // Plan items come back without media URLs, so playlist/instructions are not actually supported yet.
1902
1925
  this.capabilities = { browse: true, playlist: false, instructions: false, mediaLicensing: false };
1903
1926
  }
@@ -16268,6 +16291,110 @@ var LifeChurchProvider = class extends BaseProvider {
16268
16291
  }
16269
16292
  };
16270
16293
 
16294
+ // src/providers/registry.ts
16295
+ var providerRegistry = /* @__PURE__ */ new Map();
16296
+ function getProvider(providerId) {
16297
+ return providerRegistry.get(providerId) || null;
16298
+ }
16299
+ function getAllProviders() {
16300
+ return Array.from(providerRegistry.values());
16301
+ }
16302
+ function registerProvider(provider) {
16303
+ providerRegistry.set(provider.id, provider);
16304
+ }
16305
+
16306
+ // src/providers/goCurriculum/data.json
16307
+ var data_default4 = {
16308
+ collections: []
16309
+ };
16310
+
16311
+ // src/providers/goCurriculum/GoCurriculumProvider.ts
16312
+ var GoCurriculumProvider = class extends BaseProvider {
16313
+ constructor() {
16314
+ super(...arguments);
16315
+ this.oauthHelper = new OAuthHelper();
16316
+ this.verifiedTokens = /* @__PURE__ */ new Set();
16317
+ this.id = "gocurriculum";
16318
+ this.name = "Go Curriculum";
16319
+ this.logos = {
16320
+ light: "https://gocurriculum.com/wp-content/uploads/go-logo-curriculum-v2.png",
16321
+ dark: "https://gocurriculum.com/wp-content/uploads/go-logo-curriculum-v2.png"
16322
+ };
16323
+ this.config = {
16324
+ id: "gocurriculum",
16325
+ name: "Go Curriculum",
16326
+ apiBase: "https://gocurriculum.com",
16327
+ oauthBase: "https://gocurriculum.com/oauth",
16328
+ clientId: "Rdrvz9jz9nTNf8rCsbpuAgVLdoWHZmcjdmmrP0Bw",
16329
+ clientSecret: "yfAbd2NERjzwVZP38g1AALdIvPPXwhFayCguiTCS",
16330
+ scopes: ["basic"]
16331
+ };
16332
+ this.data = data_default4;
16333
+ this.requiresAuth = true;
16334
+ this.authTypes = ["oauth_pkce"];
16335
+ this.capabilities = { browse: true, playlist: true, instructions: true, mediaLicensing: false };
16336
+ }
16337
+ // ponytail: any valid login unlocks everything for the pilot — per-tier (MemberPress) filtering waits on Go's membership endpoint
16338
+ async verifyAuth(auth) {
16339
+ const token = auth?.access_token;
16340
+ if (!token) return false;
16341
+ if (this.verifiedTokens.has(token)) return true;
16342
+ try {
16343
+ const response = await fetch(`${this.config.oauthBase}/me`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ access_token: token }).toString() });
16344
+ if (!response.ok) return false;
16345
+ this.verifiedTokens.add(token);
16346
+ return true;
16347
+ } catch {
16348
+ return false;
16349
+ }
16350
+ }
16351
+ async browse(path, auth) {
16352
+ if (!await this.verifyAuth(auth)) return [];
16353
+ const { segments, depth } = parsePath(path);
16354
+ if (depth === 0) return this.data.collections.map((c) => createFolder(c.id, c.name, `/${c.id}`, c.image));
16355
+ const collection = this.data.collections.find((c) => c.id === segments[0]);
16356
+ if (!collection) return [];
16357
+ if (depth === 1) return collection.lessons.map((l) => createFolder(l.id, l.name, `/${collection.id}/${l.id}`, l.image || collection.image, true));
16358
+ if (depth === 2) return this.getLessonFiles(collection, segments[1]);
16359
+ return [];
16360
+ }
16361
+ getLessonFiles(collection, lessonId) {
16362
+ const lesson = collection.lessons.find((l) => l.id === lessonId);
16363
+ if (!lesson) return [];
16364
+ return lesson.files.map((f) => createFile(f.id, f.name, f.url, { mediaType: detectMediaType(`${f.url} ${f.name}`, f.fileType), thumbnail: f.thumbnail || lesson.image || collection.image, seconds: f.seconds, loop: f.loop, loopVideo: f.loopVideo }));
16365
+ }
16366
+ async getPlaylist(path, auth, _resolution) {
16367
+ if (!await this.verifyAuth(auth)) return null;
16368
+ const { segments, depth } = parsePath(path);
16369
+ if (depth !== 2) return null;
16370
+ const collection = this.data.collections.find((c) => c.id === segments[0]);
16371
+ if (!collection) return null;
16372
+ const files = this.getLessonFiles(collection, segments[1]);
16373
+ return files.length > 0 ? files : null;
16374
+ }
16375
+ async getInstructions(path, auth) {
16376
+ const { segments, depth } = parsePath(path);
16377
+ if (depth !== 2) return null;
16378
+ const files = await this.getPlaylist(path, auth);
16379
+ if (!files) return null;
16380
+ const collection = this.data.collections.find((c) => c.id === segments[0]);
16381
+ const lesson = collection?.lessons.find((l) => l.id === segments[1]);
16382
+ return filesToInstructions(lesson?.name || "Lesson", files);
16383
+ }
16384
+ generateCodeVerifier() {
16385
+ return this.oauthHelper.generateCodeVerifier();
16386
+ }
16387
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
16388
+ return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
16389
+ }
16390
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
16391
+ return this.oauthHelper.buildAuthUrlFromChallenge(this.config, codeChallenge, redirectUri, state);
16392
+ }
16393
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
16394
+ return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
16395
+ }
16396
+ };
16397
+
16271
16398
  // src/providers/index.ts
16272
16399
  var unimplementedProviders = [
16273
16400
  {
@@ -16286,14 +16413,6 @@ var unimplementedProviders = [
16286
16413
  dark: "https://freeshow.app/images/favicon.png"
16287
16414
  }
16288
16415
  },
16289
- {
16290
- id: "gocurriculum",
16291
- name: "Go Curriculum",
16292
- logos: {
16293
- light: "https://gocurriculum.com/wp-content/uploads/go-logo-curriculum-v2.png",
16294
- dark: "https://gocurriculum.com/wp-content/uploads/go-logo-curriculum-v2.png"
16295
- }
16296
- },
16297
16416
  {
16298
16417
  id: "iteachchurch",
16299
16418
  name: "iTeachChurch",
@@ -16316,6 +16435,7 @@ function initializeProviders() {
16316
16435
  new APlayProvider(),
16317
16436
  new B1ChurchProvider(),
16318
16437
  new DropboxProvider(),
16438
+ new GoCurriculumProvider(),
16319
16439
  new BibleProjectProvider(),
16320
16440
  new HighVoltageKidsProvider(),
16321
16441
  new JesusFilmProvider(),
@@ -16360,7 +16480,7 @@ function getAvailableProviders(ids) {
16360
16480
  }
16361
16481
 
16362
16482
  // src/index.ts
16363
- var VERSION = true ? "0.5.1" : "dev";
16483
+ var VERSION = true ? "0.6.1" : "dev";
16364
16484
  // Annotate the CommonJS export names for ESM import in node:
16365
16485
  0 && (module.exports = {
16366
16486
  APlayProvider,