@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/dist/index.d.cts CHANGED
@@ -16,6 +16,8 @@ interface ContentProviderConfig {
16
16
  apiBase: string;
17
17
  oauthBase: string;
18
18
  clientId: string;
19
+ /** For confidential OAuth clients (e.g. WP OAuth Server); sent on token exchange and refresh. */
20
+ clientSecret?: string;
19
21
  scopes: string[];
20
22
  supportsDeviceFlow?: boolean;
21
23
  deviceAuthEndpoint?: string;
@@ -223,6 +225,8 @@ interface IProvider {
223
225
  url: string;
224
226
  challengeMethod: string;
225
227
  }>;
228
+ /** For environments without Web Crypto (e.g. React Native): caller computes the S256 challenge. */
229
+ buildAuthUrlFromChallenge?(codeChallenge: string, redirectUri: string, state: string): string;
226
230
  exchangeCodeForTokens?(code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
227
231
  initiateDeviceFlow?(): Promise<DeviceAuthorizationResponse | null>;
228
232
  pollDeviceFlowToken?(deviceCode: string): Promise<DeviceFlowPollResult>;
@@ -245,6 +249,7 @@ declare class OAuthHelper {
245
249
  url: string;
246
250
  challengeMethod: string;
247
251
  }>;
252
+ buildAuthUrlFromChallenge(config: ContentProviderConfig, codeChallenge: string, redirectUri: string, state: string): string;
248
253
  exchangeCodeForTokens(config: ContentProviderConfig, _providerId: string, code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
249
254
  }
250
255
 
@@ -283,6 +288,7 @@ declare abstract class BaseProvider implements IProvider {
283
288
 
284
289
  /** APlay Provider: media curriculum library with OAuth 2.0 auth. */
285
290
  declare class APlayProvider extends BaseProvider {
291
+ private readonly oauthHelper;
286
292
  readonly id = "aplay";
287
293
  readonly name = "APlay";
288
294
  readonly logos: ProviderLogos;
@@ -298,6 +304,13 @@ declare class APlayProvider extends BaseProvider {
298
304
  getPlaylist(path: string, auth?: ContentProviderAuthData | null, _resolution?: number): Promise<ContentFile[] | null>;
299
305
  getInstructions(path: string, auth?: ContentProviderAuthData | null): Promise<Instructions | null>;
300
306
  checkMediaLicense(mediaId: string, auth?: ContentProviderAuthData | null): Promise<MediaLicenseResult | null>;
307
+ generateCodeVerifier(): string;
308
+ buildAuthUrl(codeVerifier: string, redirectUri: string, state?: string): Promise<{
309
+ url: string;
310
+ challengeMethod: string;
311
+ }>;
312
+ buildAuthUrlFromChallenge(codeChallenge: string, redirectUri: string, state: string): string;
313
+ exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
301
314
  }
302
315
 
303
316
  /** Browses SignPresenter playlists → messages (files). */
@@ -322,6 +335,7 @@ declare class SignPresenterProvider extends BaseProvider {
322
335
  url: string;
323
336
  challengeMethod: string;
324
337
  }>;
338
+ buildAuthUrlFromChallenge(codeChallenge: string, redirectUri: string, state: string): string;
325
339
  exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
326
340
  initiateDeviceFlow(): Promise<DeviceAuthorizationResponse | null>;
327
341
  pollDeviceFlowToken(deviceCode: string): Promise<DeviceFlowPollResult>;
@@ -366,11 +380,13 @@ declare class B1ChurchProvider extends BaseProvider {
366
380
  readonly requiresAuth = true;
367
381
  readonly authTypes: AuthType[];
368
382
  readonly capabilities: ProviderCapabilities;
383
+ generateCodeVerifier(): string;
369
384
  buildAuthUrl(codeVerifier: string, redirectUri: string, state?: string): Promise<{
370
385
  url: string;
371
386
  challengeMethod: string;
372
387
  }>;
373
- exchangeCodeForTokensWithPKCE(code: string, redirectUri: string, codeVerifier: string): Promise<ContentProviderAuthData | null>;
388
+ buildAuthUrlFromChallenge(codeChallenge: string, redirectUri: string, state: string): string;
389
+ exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
374
390
  exchangeCodeForTokensWithSecret(code: string, redirectUri: string, clientSecret: string): Promise<ContentProviderAuthData | null>;
375
391
  refreshTokenWithSecret(authData: ContentProviderAuthData, clientSecret: string): Promise<ContentProviderAuthData | null>;
376
392
  initiateDeviceFlow(): Promise<DeviceAuthorizationResponse | null>;
@@ -379,7 +395,7 @@ declare class B1ChurchProvider extends BaseProvider {
379
395
  private fetchPlanImages;
380
396
  private planTypeId;
381
397
  setPairingData(data: unknown): void;
382
- getCurrentPlan(_authData?: ContentProviderAuthData | null): Promise<CurrentPlan | null>;
398
+ getCurrentPlan(authData?: ContentProviderAuthData | null): Promise<CurrentPlan | null>;
383
399
  getInstructions(path: string, authData?: ContentProviderAuthData | null): Promise<Instructions | null>;
384
400
  private processInstructionItems;
385
401
  private findItemByPath;
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@ interface ContentProviderConfig {
16
16
  apiBase: string;
17
17
  oauthBase: string;
18
18
  clientId: string;
19
+ /** For confidential OAuth clients (e.g. WP OAuth Server); sent on token exchange and refresh. */
20
+ clientSecret?: string;
19
21
  scopes: string[];
20
22
  supportsDeviceFlow?: boolean;
21
23
  deviceAuthEndpoint?: string;
@@ -223,6 +225,8 @@ interface IProvider {
223
225
  url: string;
224
226
  challengeMethod: string;
225
227
  }>;
228
+ /** For environments without Web Crypto (e.g. React Native): caller computes the S256 challenge. */
229
+ buildAuthUrlFromChallenge?(codeChallenge: string, redirectUri: string, state: string): string;
226
230
  exchangeCodeForTokens?(code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
227
231
  initiateDeviceFlow?(): Promise<DeviceAuthorizationResponse | null>;
228
232
  pollDeviceFlowToken?(deviceCode: string): Promise<DeviceFlowPollResult>;
@@ -245,6 +249,7 @@ declare class OAuthHelper {
245
249
  url: string;
246
250
  challengeMethod: string;
247
251
  }>;
252
+ buildAuthUrlFromChallenge(config: ContentProviderConfig, codeChallenge: string, redirectUri: string, state: string): string;
248
253
  exchangeCodeForTokens(config: ContentProviderConfig, _providerId: string, code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
249
254
  }
250
255
 
@@ -283,6 +288,7 @@ declare abstract class BaseProvider implements IProvider {
283
288
 
284
289
  /** APlay Provider: media curriculum library with OAuth 2.0 auth. */
285
290
  declare class APlayProvider extends BaseProvider {
291
+ private readonly oauthHelper;
286
292
  readonly id = "aplay";
287
293
  readonly name = "APlay";
288
294
  readonly logos: ProviderLogos;
@@ -298,6 +304,13 @@ declare class APlayProvider extends BaseProvider {
298
304
  getPlaylist(path: string, auth?: ContentProviderAuthData | null, _resolution?: number): Promise<ContentFile[] | null>;
299
305
  getInstructions(path: string, auth?: ContentProviderAuthData | null): Promise<Instructions | null>;
300
306
  checkMediaLicense(mediaId: string, auth?: ContentProviderAuthData | null): Promise<MediaLicenseResult | null>;
307
+ generateCodeVerifier(): string;
308
+ buildAuthUrl(codeVerifier: string, redirectUri: string, state?: string): Promise<{
309
+ url: string;
310
+ challengeMethod: string;
311
+ }>;
312
+ buildAuthUrlFromChallenge(codeChallenge: string, redirectUri: string, state: string): string;
313
+ exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
301
314
  }
302
315
 
303
316
  /** Browses SignPresenter playlists → messages (files). */
@@ -322,6 +335,7 @@ declare class SignPresenterProvider extends BaseProvider {
322
335
  url: string;
323
336
  challengeMethod: string;
324
337
  }>;
338
+ buildAuthUrlFromChallenge(codeChallenge: string, redirectUri: string, state: string): string;
325
339
  exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
326
340
  initiateDeviceFlow(): Promise<DeviceAuthorizationResponse | null>;
327
341
  pollDeviceFlowToken(deviceCode: string): Promise<DeviceFlowPollResult>;
@@ -366,11 +380,13 @@ declare class B1ChurchProvider extends BaseProvider {
366
380
  readonly requiresAuth = true;
367
381
  readonly authTypes: AuthType[];
368
382
  readonly capabilities: ProviderCapabilities;
383
+ generateCodeVerifier(): string;
369
384
  buildAuthUrl(codeVerifier: string, redirectUri: string, state?: string): Promise<{
370
385
  url: string;
371
386
  challengeMethod: string;
372
387
  }>;
373
- exchangeCodeForTokensWithPKCE(code: string, redirectUri: string, codeVerifier: string): Promise<ContentProviderAuthData | null>;
388
+ buildAuthUrlFromChallenge(codeChallenge: string, redirectUri: string, state: string): string;
389
+ exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri: string): Promise<ContentProviderAuthData | null>;
374
390
  exchangeCodeForTokensWithSecret(code: string, redirectUri: string, clientSecret: string): Promise<ContentProviderAuthData | null>;
375
391
  refreshTokenWithSecret(authData: ContentProviderAuthData, clientSecret: string): Promise<ContentProviderAuthData | null>;
376
392
  initiateDeviceFlow(): Promise<DeviceAuthorizationResponse | null>;
@@ -379,7 +395,7 @@ declare class B1ChurchProvider extends BaseProvider {
379
395
  private fetchPlanImages;
380
396
  private planTypeId;
381
397
  setPairingData(data: unknown): void;
382
- getCurrentPlan(_authData?: ContentProviderAuthData | null): Promise<CurrentPlan | null>;
398
+ getCurrentPlan(authData?: ContentProviderAuthData | null): Promise<CurrentPlan | null>;
383
399
  getInstructions(path: string, authData?: ContentProviderAuthData | null): Promise<Instructions | null>;
384
400
  private processInstructionItems;
385
401
  private findItemByPath;
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ var TokenHelper = class {
47
47
  refresh_token: auth.refresh_token,
48
48
  client_id: config.clientId
49
49
  });
50
+ if (config.clientSecret) params.set("client_secret", config.clientSecret);
50
51
  const response = await fetch(`${config.oauthBase}/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
51
52
  if (!response.ok) {
52
53
  console.warn(`[TokenHelper] Token refresh failed: ${response.status} ${response.statusText}`);
@@ -93,6 +94,9 @@ var OAuthHelper = class {
93
94
  }
94
95
  async buildAuthUrl(config, codeVerifier, redirectUri, state) {
95
96
  const codeChallenge = await generateCodeChallenge(codeVerifier);
97
+ return { url: this.buildAuthUrlFromChallenge(config, codeChallenge, redirectUri, state || ""), challengeMethod: "S256" };
98
+ }
99
+ buildAuthUrlFromChallenge(config, codeChallenge, redirectUri, state) {
96
100
  const params = new URLSearchParams({
97
101
  response_type: "code",
98
102
  client_id: config.clientId,
@@ -100,9 +104,9 @@ var OAuthHelper = class {
100
104
  scope: config.scopes.join(" "),
101
105
  code_challenge: codeChallenge,
102
106
  code_challenge_method: "S256",
103
- state: state || ""
107
+ state
104
108
  });
105
- return { url: `${config.oauthBase}/authorize?${params.toString()}`, challengeMethod: "S256" };
109
+ return `${config.oauthBase}/authorize?${params.toString()}`;
106
110
  }
107
111
  async exchangeCodeForTokens(config, _providerId, code, codeVerifier, redirectUri) {
108
112
  try {
@@ -113,6 +117,7 @@ var OAuthHelper = class {
113
117
  client_id: config.clientId,
114
118
  code_verifier: codeVerifier
115
119
  });
120
+ if (config.clientSecret) params.set("client_secret", config.clientSecret);
116
121
  const tokenUrl = `${config.oauthBase}/token`;
117
122
  const response = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() });
118
123
  if (!response.ok) {
@@ -284,12 +289,15 @@ function filesToInstructions(name, files, section) {
284
289
  const items = section ? [{ id: section.id, itemType: "section", label: section.label ?? name, children: actions }] : actions;
285
290
  return { name, items };
286
291
  }
292
+ function instructionItemMediaType(item) {
293
+ return detectMediaType(`${item.downloadUrl || ""} ${item.label || ""}`, item.mediaType);
294
+ }
287
295
  function instructionsToPlaylist(instructions) {
288
296
  const files = [];
289
297
  function extractFiles(items) {
290
298
  for (const item of items) {
291
299
  if (item.downloadUrl && (item.itemType === "file" || !item.children?.length)) {
292
- 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 });
300
+ 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 });
293
301
  }
294
302
  if (item.children) extractFiles(item.children);
295
303
  }
@@ -399,6 +407,7 @@ function extractArray(response, ...keys) {
399
407
  var APlayProvider = class extends BaseProvider {
400
408
  constructor() {
401
409
  super(...arguments);
410
+ this.oauthHelper = new OAuthHelper();
402
411
  this.id = "aplay";
403
412
  this.name = "APlay";
404
413
  this.logos = { light: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg", dark: "https://www.joinamazing.com/_assets/v11/3ba846c5afd7e73d27bc4d87b63d423e7ae2dc73.svg" };
@@ -468,6 +477,18 @@ var APlayProvider = class extends BaseProvider {
468
477
  async checkMediaLicense(mediaId, auth) {
469
478
  return checkMediaLicense(mediaId, auth);
470
479
  }
480
+ generateCodeVerifier() {
481
+ return this.oauthHelper.generateCodeVerifier();
482
+ }
483
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
484
+ return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
485
+ }
486
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
487
+ return this.oauthHelper.buildAuthUrlFromChallenge(this.config, codeChallenge, redirectUri, state);
488
+ }
489
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
490
+ return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
491
+ }
471
492
  };
472
493
 
473
494
  // src/providers/signPresenter/SignPresenterProvider.ts
@@ -552,6 +573,9 @@ var SignPresenterProvider = class extends BaseProvider {
552
573
  async buildAuthUrl(codeVerifier, redirectUri, state) {
553
574
  return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
554
575
  }
576
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
577
+ return this.oauthHelper.buildAuthUrlFromChallenge(this.config, codeChallenge, redirectUri, state);
578
+ }
555
579
  async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
556
580
  return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
557
581
  }
@@ -911,21 +935,8 @@ var LessonsChurchProvider = class extends BaseProvider {
911
935
  }
912
936
  };
913
937
 
914
- // src/providers/registry.ts
915
- var providerRegistry = /* @__PURE__ */ new Map();
916
- function getProvider(providerId) {
917
- return providerRegistry.get(providerId) || null;
918
- }
919
- function getAllProviders() {
920
- return Array.from(providerRegistry.values());
921
- }
922
- function registerProvider(provider) {
923
- providerRegistry.set(provider.id, provider);
924
- }
925
-
926
938
  // src/providers/b1Church/B1ChurchAuth.ts
927
- async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state) {
928
- const codeChallenge = await generateCodeChallenge(codeVerifier);
939
+ function buildB1AuthUrlFromChallenge(config, appBase, redirectUri, codeChallenge, state) {
929
940
  const oauthParams = new URLSearchParams({
930
941
  response_type: "code",
931
942
  client_id: config.clientId,
@@ -933,10 +944,13 @@ async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state)
933
944
  scope: config.scopes.join(" "),
934
945
  code_challenge: codeChallenge,
935
946
  code_challenge_method: "S256",
936
- state: state || ""
947
+ state
937
948
  });
938
- const url = `${appBase}/oauth?${oauthParams.toString()}`;
939
- return { url, challengeMethod: "S256" };
949
+ return `${appBase}/oauth?${oauthParams.toString()}`;
950
+ }
951
+ async function buildB1AuthUrl(config, appBase, redirectUri, codeVerifier, state) {
952
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
953
+ return { url: buildB1AuthUrlFromChallenge(config, appBase, redirectUri, codeChallenge, state || ""), challengeMethod: "S256" };
940
954
  }
941
955
  async function exchangeCodeForTokensWithPKCE(config, code, redirectUri, codeVerifier) {
942
956
  try {
@@ -1168,6 +1182,7 @@ function collectFilesFromNode(node) {
1168
1182
  id: node.id,
1169
1183
  name: node.label || "",
1170
1184
  url: node.downloadUrl,
1185
+ fileType: instructionItemMediaType(node),
1171
1186
  seconds: node.seconds || 10
1172
1187
  });
1173
1188
  }
@@ -1233,6 +1248,9 @@ function findFirstThumbnail(items) {
1233
1248
  }
1234
1249
  return void 0;
1235
1250
  }
1251
+ function isLessonsChurchContent(plan) {
1252
+ return !plan.providerId || plan.providerId === "lessonschurch";
1253
+ }
1236
1254
  function isExternalProviderItem(item) {
1237
1255
  if (!item.providerId || item.providerId === "b1church") return false;
1238
1256
  if (item.providerPath) return true;
@@ -1259,10 +1277,16 @@ var B1ChurchProvider = class extends BaseProvider {
1259
1277
  this.capabilities = { browse: true, playlist: true, instructions: true, mediaLicensing: false };
1260
1278
  this.planTypeId = null;
1261
1279
  }
1280
+ generateCodeVerifier() {
1281
+ return generateCodeVerifier();
1282
+ }
1262
1283
  async buildAuthUrl(codeVerifier, redirectUri, state) {
1263
1284
  return buildB1AuthUrl(this.config, this.appBase, redirectUri, codeVerifier, state);
1264
1285
  }
1265
- async exchangeCodeForTokensWithPKCE(code, redirectUri, codeVerifier) {
1286
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
1287
+ return buildB1AuthUrlFromChallenge(this.config, this.appBase, redirectUri, codeChallenge, state);
1288
+ }
1289
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
1266
1290
  return exchangeCodeForTokensWithPKCE(this.config, code, redirectUri, codeVerifier);
1267
1291
  }
1268
1292
  async exchangeCodeForTokensWithSecret(code, redirectUri, clientSecret) {
@@ -1369,14 +1393,12 @@ var B1ChurchProvider = class extends BaseProvider {
1369
1393
  this.planTypeId = null;
1370
1394
  }
1371
1395
  }
1372
- async getCurrentPlan(_authData) {
1396
+ async getCurrentPlan(authData) {
1373
1397
  if (!this.planTypeId) return null;
1374
1398
  const plan = await fetchCurrentPlanByType(this.planTypeId);
1375
- if (!plan?.providerId || !plan.providerPlanId) return null;
1399
+ if (!plan?.providerId || !plan.providerPlanId || !plan.ministryId) return null;
1376
1400
  const items = plan.churchId && plan.id ? await fetchPlanItems(plan.churchId, plan.id) : [];
1377
- const innerProvider = getProvider(plan.providerId);
1378
- if (!innerProvider?.getInstructions) return null;
1379
- const instructions = await innerProvider.getInstructions(plan.providerPlanId, null);
1401
+ const instructions = await fetchFromProviderProxy("getInstructions", plan.ministryId, plan.providerId, plan.providerPlanId, authData);
1380
1402
  if (!instructions) return null;
1381
1403
  const files = getOrderedFiles(instructions, items);
1382
1404
  const thumbnail = findFirstThumbnail(instructions.items);
@@ -1398,7 +1420,7 @@ var B1ChurchProvider = class extends BaseProvider {
1398
1420
  const planFolder = plans.find((p) => p.id === planId);
1399
1421
  if (!planFolder) return null;
1400
1422
  const churchId = planFolder.churchId;
1401
- const venueId = planFolder.contentId;
1423
+ const venueId = isLessonsChurchContent(planFolder) ? planFolder.contentId : void 0;
1402
1424
  const planTitle = planFolder.name || "Plan";
1403
1425
  if (!churchId) {
1404
1426
  console.warn("[B1Church getInstructions] planFolder missing churchId:", planFolder.id);
@@ -1507,7 +1529,7 @@ var B1ChurchProvider = class extends BaseProvider {
1507
1529
  return null;
1508
1530
  }
1509
1531
  const churchId = planFolder.churchId;
1510
- const venueId = planFolder.contentId;
1532
+ const venueId = isLessonsChurchContent(planFolder) ? planFolder.contentId : void 0;
1511
1533
  if (!churchId) {
1512
1534
  console.warn("[B1Church] getPlaylist: planFolder missing churchId:", planFolder.id);
1513
1535
  return null;
@@ -1531,7 +1553,7 @@ var B1ChurchProvider = class extends BaseProvider {
1531
1553
  const venueFeed = venueId ? await fetchVenueFeed(venueId) : null;
1532
1554
  const files = [];
1533
1555
  for (const sectionItem of planItems) {
1534
- for (const child of sectionItem.children || []) {
1556
+ for (const child of sectionItem.children?.length ? sectionItem.children : [sectionItem]) {
1535
1557
  const childItemType = child.itemType;
1536
1558
  const isSectionType = childItemType === "section" || childItemType === "lessonSection" || childItemType === "providerSection";
1537
1559
  const canExpandLocally = isSectionType && venueFeed && child.relatedId;
@@ -1847,7 +1869,8 @@ var PlanningCenterProvider = class extends BaseProvider {
1847
1869
  this.logos = { light: "https://www.planningcenter.com/icons/icon-512x512.png", dark: "https://www.planningcenter.com/icons/icon-512x512.png" };
1848
1870
  this.config = { id: "planningcenter", name: "Planning Center", apiBase: "https://api.planningcenteronline.com", oauthBase: "https://api.planningcenteronline.com/oauth", clientId: "", scopes: ["services"] };
1849
1871
  this.requiresAuth = true;
1850
- this.authTypes = ["oauth_pkce"];
1872
+ // No client-side auth flow: clientId is unset and no PKCE methods exist. Auth tokens must be supplied externally (server-side).
1873
+ this.authTypes = [];
1851
1874
  // Plan items come back without media URLs, so playlist/instructions are not actually supported yet.
1852
1875
  this.capabilities = { browse: true, playlist: false, instructions: false, mediaLicensing: false };
1853
1876
  }
@@ -16218,6 +16241,110 @@ var LifeChurchProvider = class extends BaseProvider {
16218
16241
  }
16219
16242
  };
16220
16243
 
16244
+ // src/providers/registry.ts
16245
+ var providerRegistry = /* @__PURE__ */ new Map();
16246
+ function getProvider(providerId) {
16247
+ return providerRegistry.get(providerId) || null;
16248
+ }
16249
+ function getAllProviders() {
16250
+ return Array.from(providerRegistry.values());
16251
+ }
16252
+ function registerProvider(provider) {
16253
+ providerRegistry.set(provider.id, provider);
16254
+ }
16255
+
16256
+ // src/providers/goCurriculum/data.json
16257
+ var data_default4 = {
16258
+ collections: []
16259
+ };
16260
+
16261
+ // src/providers/goCurriculum/GoCurriculumProvider.ts
16262
+ var GoCurriculumProvider = class extends BaseProvider {
16263
+ constructor() {
16264
+ super(...arguments);
16265
+ this.oauthHelper = new OAuthHelper();
16266
+ this.verifiedTokens = /* @__PURE__ */ new Set();
16267
+ this.id = "gocurriculum";
16268
+ this.name = "Go Curriculum";
16269
+ this.logos = {
16270
+ light: "https://gocurriculum.com/wp-content/uploads/go-logo-curriculum-v2.png",
16271
+ dark: "https://gocurriculum.com/wp-content/uploads/go-logo-curriculum-v2.png"
16272
+ };
16273
+ this.config = {
16274
+ id: "gocurriculum",
16275
+ name: "Go Curriculum",
16276
+ apiBase: "https://gocurriculum.com",
16277
+ oauthBase: "https://gocurriculum.com/oauth",
16278
+ clientId: "Rdrvz9jz9nTNf8rCsbpuAgVLdoWHZmcjdmmrP0Bw",
16279
+ clientSecret: "yfAbd2NERjzwVZP38g1AALdIvPPXwhFayCguiTCS",
16280
+ scopes: ["basic"]
16281
+ };
16282
+ this.data = data_default4;
16283
+ this.requiresAuth = true;
16284
+ this.authTypes = ["oauth_pkce"];
16285
+ this.capabilities = { browse: true, playlist: true, instructions: true, mediaLicensing: false };
16286
+ }
16287
+ // ponytail: any valid login unlocks everything for the pilot — per-tier (MemberPress) filtering waits on Go's membership endpoint
16288
+ async verifyAuth(auth) {
16289
+ const token = auth?.access_token;
16290
+ if (!token) return false;
16291
+ if (this.verifiedTokens.has(token)) return true;
16292
+ try {
16293
+ 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() });
16294
+ if (!response.ok) return false;
16295
+ this.verifiedTokens.add(token);
16296
+ return true;
16297
+ } catch {
16298
+ return false;
16299
+ }
16300
+ }
16301
+ async browse(path, auth) {
16302
+ if (!await this.verifyAuth(auth)) return [];
16303
+ const { segments, depth } = parsePath(path);
16304
+ if (depth === 0) return this.data.collections.map((c) => createFolder(c.id, c.name, `/${c.id}`, c.image));
16305
+ const collection = this.data.collections.find((c) => c.id === segments[0]);
16306
+ if (!collection) return [];
16307
+ if (depth === 1) return collection.lessons.map((l) => createFolder(l.id, l.name, `/${collection.id}/${l.id}`, l.image || collection.image, true));
16308
+ if (depth === 2) return this.getLessonFiles(collection, segments[1]);
16309
+ return [];
16310
+ }
16311
+ getLessonFiles(collection, lessonId) {
16312
+ const lesson = collection.lessons.find((l) => l.id === lessonId);
16313
+ if (!lesson) return [];
16314
+ 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 }));
16315
+ }
16316
+ async getPlaylist(path, auth, _resolution) {
16317
+ if (!await this.verifyAuth(auth)) return null;
16318
+ const { segments, depth } = parsePath(path);
16319
+ if (depth !== 2) return null;
16320
+ const collection = this.data.collections.find((c) => c.id === segments[0]);
16321
+ if (!collection) return null;
16322
+ const files = this.getLessonFiles(collection, segments[1]);
16323
+ return files.length > 0 ? files : null;
16324
+ }
16325
+ async getInstructions(path, auth) {
16326
+ const { segments, depth } = parsePath(path);
16327
+ if (depth !== 2) return null;
16328
+ const files = await this.getPlaylist(path, auth);
16329
+ if (!files) return null;
16330
+ const collection = this.data.collections.find((c) => c.id === segments[0]);
16331
+ const lesson = collection?.lessons.find((l) => l.id === segments[1]);
16332
+ return filesToInstructions(lesson?.name || "Lesson", files);
16333
+ }
16334
+ generateCodeVerifier() {
16335
+ return this.oauthHelper.generateCodeVerifier();
16336
+ }
16337
+ async buildAuthUrl(codeVerifier, redirectUri, state) {
16338
+ return this.oauthHelper.buildAuthUrl(this.config, codeVerifier, redirectUri, state || this.id);
16339
+ }
16340
+ buildAuthUrlFromChallenge(codeChallenge, redirectUri, state) {
16341
+ return this.oauthHelper.buildAuthUrlFromChallenge(this.config, codeChallenge, redirectUri, state);
16342
+ }
16343
+ async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
16344
+ return this.oauthHelper.exchangeCodeForTokens(this.config, this.id, code, codeVerifier, redirectUri);
16345
+ }
16346
+ };
16347
+
16221
16348
  // src/providers/index.ts
16222
16349
  var unimplementedProviders = [
16223
16350
  {
@@ -16236,14 +16363,6 @@ var unimplementedProviders = [
16236
16363
  dark: "https://freeshow.app/images/favicon.png"
16237
16364
  }
16238
16365
  },
16239
- {
16240
- id: "gocurriculum",
16241
- name: "Go Curriculum",
16242
- logos: {
16243
- light: "https://gocurriculum.com/wp-content/uploads/go-logo-curriculum-v2.png",
16244
- dark: "https://gocurriculum.com/wp-content/uploads/go-logo-curriculum-v2.png"
16245
- }
16246
- },
16247
16366
  {
16248
16367
  id: "iteachchurch",
16249
16368
  name: "iTeachChurch",
@@ -16266,6 +16385,7 @@ function initializeProviders() {
16266
16385
  new APlayProvider(),
16267
16386
  new B1ChurchProvider(),
16268
16387
  new DropboxProvider(),
16388
+ new GoCurriculumProvider(),
16269
16389
  new BibleProjectProvider(),
16270
16390
  new HighVoltageKidsProvider(),
16271
16391
  new JesusFilmProvider(),
@@ -16310,7 +16430,7 @@ function getAvailableProviders(ids) {
16310
16430
  }
16311
16431
 
16312
16432
  // src/index.ts
16313
- var VERSION = true ? "0.5.1" : "dev";
16433
+ var VERSION = true ? "0.6.1" : "dev";
16314
16434
  export {
16315
16435
  APlayProvider,
16316
16436
  B1ChurchProvider,