@lizardbyte/contribkit 2025.315.185528

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.
@@ -0,0 +1,1453 @@
1
+ import { loadConfig as loadConfig$1 } from 'unconfig';
2
+ import process from 'node:process';
3
+ import dotenv from 'dotenv';
4
+ import { Buffer } from 'node:buffer';
5
+ import { consola } from 'consola';
6
+ import { $fetch, ofetch } from 'ofetch';
7
+ import sharp from 'sharp';
8
+ import { createHash } from 'node:crypto';
9
+
10
+ const none = {
11
+ avatar: {
12
+ size: 0
13
+ },
14
+ boxWidth: 0,
15
+ boxHeight: 0,
16
+ container: {
17
+ sidePadding: 0
18
+ }
19
+ };
20
+ const base = {
21
+ avatar: {
22
+ size: 40
23
+ },
24
+ boxWidth: 48,
25
+ boxHeight: 48,
26
+ container: {
27
+ sidePadding: 30
28
+ }
29
+ };
30
+ const xs = {
31
+ avatar: {
32
+ size: 25
33
+ },
34
+ boxWidth: 30,
35
+ boxHeight: 30,
36
+ container: {
37
+ sidePadding: 30
38
+ }
39
+ };
40
+ const small = {
41
+ avatar: {
42
+ size: 35
43
+ },
44
+ boxWidth: 38,
45
+ boxHeight: 38,
46
+ container: {
47
+ sidePadding: 30
48
+ }
49
+ };
50
+ const medium = {
51
+ avatar: {
52
+ size: 50
53
+ },
54
+ boxWidth: 80,
55
+ boxHeight: 90,
56
+ container: {
57
+ sidePadding: 20
58
+ },
59
+ name: {
60
+ maxLength: 10
61
+ }
62
+ };
63
+ const large = {
64
+ avatar: {
65
+ size: 70
66
+ },
67
+ boxWidth: 95,
68
+ boxHeight: 115,
69
+ container: {
70
+ sidePadding: 20
71
+ },
72
+ name: {
73
+ maxLength: 16
74
+ }
75
+ };
76
+ const xl = {
77
+ avatar: {
78
+ size: 90
79
+ },
80
+ boxWidth: 120,
81
+ boxHeight: 130,
82
+ container: {
83
+ sidePadding: 20
84
+ },
85
+ name: {
86
+ maxLength: 20
87
+ }
88
+ };
89
+ const tierPresets = {
90
+ none,
91
+ xs,
92
+ small,
93
+ base,
94
+ medium,
95
+ large,
96
+ xl
97
+ };
98
+ const presets = tierPresets;
99
+
100
+ const defaultTiers = [
101
+ {
102
+ title: "Past Sponsors",
103
+ monthlyDollars: -1,
104
+ preset: tierPresets.xs
105
+ },
106
+ {
107
+ title: "Backers",
108
+ preset: tierPresets.base
109
+ },
110
+ {
111
+ title: "Sponsors",
112
+ monthlyDollars: 10,
113
+ preset: tierPresets.medium
114
+ },
115
+ {
116
+ title: "Silver Sponsors",
117
+ monthlyDollars: 50,
118
+ preset: tierPresets.large
119
+ },
120
+ {
121
+ title: "Gold Sponsors",
122
+ monthlyDollars: 100,
123
+ preset: tierPresets.xl
124
+ }
125
+ ];
126
+ const defaultInlineCSS = `
127
+ text {
128
+ font-weight: 300;
129
+ font-size: 14px;
130
+ fill: #777777;
131
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
132
+ }
133
+ .contribkit-link {
134
+ cursor: pointer;
135
+ }
136
+ .contribkit-tier-title {
137
+ font-weight: 500;
138
+ font-size: 20px;
139
+ }
140
+ `;
141
+ const defaultConfig = {
142
+ width: 800,
143
+ outputDir: "./contribkit",
144
+ cacheFile: ".cache.json",
145
+ formats: ["json", "svg", "png"],
146
+ tiers: defaultTiers,
147
+ name: "sponsors",
148
+ includePrivate: false,
149
+ svgInlineCSS: defaultInlineCSS
150
+ };
151
+
152
+ function loadEnv() {
153
+ dotenv.config();
154
+ const config = {
155
+ github: {
156
+ login: process.env.CONTRIBKIT_GITHUB_LOGIN || process.env.GITHUB_LOGIN,
157
+ token: process.env.CONTRIBKIT_GITHUB_TOKEN || process.env.GITHUB_TOKEN,
158
+ type: process.env.CONTRIBKIT_GITHUB_TYPE || process.env.GITHUB_TYPE
159
+ },
160
+ patreon: {
161
+ token: process.env.CONTRIBKIT_PATREON_TOKEN || process.env.PATREON_TOKEN
162
+ },
163
+ opencollective: {
164
+ key: process.env.CONTRIBKIT_OPENCOLLECTIVE_KEY || process.env.OPENCOLLECTIVE_KEY,
165
+ id: process.env.CONTRIBKIT_OPENCOLLECTIVE_ID || process.env.OPENCOLLECTIVE_ID,
166
+ slug: process.env.CONTRIBKIT_OPENCOLLECTIVE_SLUG || process.env.OPENCOLLECTIVE_SLUG,
167
+ githubHandle: process.env.CONTRIBKIT_OPENCOLLECTIVE_GH_HANDLE || process.env.OPENCOLLECTIVE_GH_HANDLE,
168
+ type: process.env.CONTRIBKIT_OPENCOLLECTIVE_TYPE || process.env.OPENCOLLECTIVE_TYPE
169
+ },
170
+ afdian: {
171
+ userId: process.env.CONTRIBKIT_AFDIAN_USER_ID || process.env.AFDIAN_USER_ID,
172
+ token: process.env.CONTRIBKIT_AFDIAN_TOKEN || process.env.AFDIAN_TOKEN,
173
+ exechangeRate: Number.parseFloat(process.env.CONTRIBKIT_AFDIAN_EXECHANGERATE || process.env.AFDIAN_EXECHANGERATE || "0") || void 0
174
+ },
175
+ polar: {
176
+ token: process.env.CONTRIBKIT_POLAR_TOKEN || process.env.POLAR_TOKEN,
177
+ organization: process.env.CONTRIBKIT_POLAR_ORGANIZATION || process.env.POLAR_ORGANIZATION
178
+ },
179
+ liberapay: {
180
+ login: process.env.CONTRIBKIT_LIBERAPAY_LOGIN || process.env.LIBERAPAY_LOGIN
181
+ },
182
+ outputDir: process.env.CONTRIBKIT_DIR
183
+ };
184
+ return JSON.parse(JSON.stringify(config));
185
+ }
186
+
187
+ const version = "2025.315.185528";
188
+
189
+ async function fetchImage(url) {
190
+ const arrayBuffer = await $fetch(url, {
191
+ responseType: "arrayBuffer",
192
+ headers: {
193
+ "User-Agent": `Mozilla/5.0 Chrome/124.0.0.0 Safari/537.36 Contribkit/${version}`
194
+ }
195
+ });
196
+ return Buffer.from(arrayBuffer);
197
+ }
198
+ async function resolveAvatars(ships, getFallbackAvatar, t = consola) {
199
+ const fallbackAvatar = await (() => {
200
+ if (typeof getFallbackAvatar === "string") {
201
+ return fetchImage(getFallbackAvatar);
202
+ }
203
+ if (getFallbackAvatar)
204
+ return getFallbackAvatar;
205
+ })();
206
+ const pLimit = await import('../chunks/index.mjs').then((r) => r.default);
207
+ const limit = pLimit(15);
208
+ return Promise.all(ships.map((ship) => limit(async () => {
209
+ if (ship.privacyLevel === "PRIVATE" || !ship.sponsor.avatarUrl) {
210
+ ship.sponsor.avatarBuffer = fallbackAvatar;
211
+ return;
212
+ }
213
+ const pngBuffer = await fetchImage(ship.sponsor.avatarUrl).catch((e) => {
214
+ if (ship.provider === "liberapay" && e.toString().includes("404 Not Found") && fallbackAvatar)
215
+ return fallbackAvatar;
216
+ t.error(`Failed to fetch avatar for ${ship.sponsor.login || ship.sponsor.name} [${ship.sponsor.avatarUrl}]`);
217
+ t.error(e);
218
+ if (fallbackAvatar)
219
+ return fallbackAvatar;
220
+ throw e;
221
+ });
222
+ if (pngBuffer) {
223
+ ship.sponsor.avatarBuffer = await resizeImage(pngBuffer, 120, "webp");
224
+ }
225
+ })));
226
+ }
227
+ const cache = /* @__PURE__ */ new Map();
228
+ async function resizeImage(image, size = 100, format) {
229
+ const cacheKey = `${size}:${format}`;
230
+ if (cache.has(image)) {
231
+ const cacheHit = cache.get(image).get(cacheKey);
232
+ if (cacheHit) {
233
+ return cacheHit;
234
+ }
235
+ }
236
+ let processing = sharp(image).resize(size, size, { fit: sharp.fit.cover });
237
+ processing = format === "webp" ? processing.webp() : processing.png({ quality: 80, compressionLevel: 8 });
238
+ const result = await processing.toBuffer();
239
+ if (!cache.has(image)) {
240
+ cache.set(image, /* @__PURE__ */ new Map());
241
+ }
242
+ cache.get(image).set(cacheKey, result);
243
+ return result;
244
+ }
245
+ function svgToPng(svg) {
246
+ return sharp(Buffer.from(svg), { density: 150 }).png({ quality: 90 }).toBuffer();
247
+ }
248
+ function svgToWebp(svg) {
249
+ return sharp(Buffer.from(svg), { density: 150 }).webp().toBuffer();
250
+ }
251
+
252
+ const fallback = `
253
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 135.47 135.47">
254
+ <path fill="#2d333b" stroke="#000" stroke-linejoin="round" stroke-width=".32" d="M.16.16h135.15v135.15H.16z" paint-order="stroke markers fill"/>
255
+ <path fill="#636e7b" fill-rule="evenodd" d="M81.85 53.56a14.13 14.13 0 1 1-28.25 0 14.13 14.13 0 0 1 28.25 0zm.35 17.36a22.6 22.6 0 1 0-28.95 0 33.92 33.92 0 0 0-19.38 29.05 4.24 4.24 0 0 0 8.46.4 25.43 25.43 0 0 1 50.8 0 4.24 4.24 0 1 0 8.46-.4 33.93 33.93 0 0 0-19.4-29.05z"/>
256
+ </svg>
257
+ `;
258
+ const FALLBACK_AVATAR = svgToPng(fallback);
259
+
260
+ function defineConfig(config) {
261
+ return config;
262
+ }
263
+ async function loadConfig(inlineConfig = {}) {
264
+ const env = loadEnv();
265
+ const { config = {} } = await loadConfig$1({
266
+ sources: [
267
+ {
268
+ files: "sponsorkit.config"
269
+ },
270
+ {
271
+ files: "contribkit.config"
272
+ }
273
+ ],
274
+ merge: true
275
+ });
276
+ const hasNegativeTier = !!config.tiers?.find((tier) => tier && tier.monthlyDollars <= 0);
277
+ const resolved = {
278
+ fallbackAvatar: FALLBACK_AVATAR,
279
+ includePastSponsors: hasNegativeTier,
280
+ ...defaultConfig,
281
+ ...env,
282
+ ...config,
283
+ ...inlineConfig,
284
+ github: {
285
+ ...env.github,
286
+ ...config.github,
287
+ ...inlineConfig.github
288
+ },
289
+ patreon: {
290
+ ...env.patreon,
291
+ ...config.patreon,
292
+ ...inlineConfig.patreon
293
+ },
294
+ opencollective: {
295
+ ...env.opencollective,
296
+ ...config.opencollective,
297
+ ...inlineConfig.opencollective
298
+ },
299
+ afdian: {
300
+ ...env.afdian,
301
+ ...config.afdian,
302
+ ...inlineConfig.afdian
303
+ }
304
+ };
305
+ return resolved;
306
+ }
307
+ function partitionTiers(sponsors, tiers, includePastSponsors) {
308
+ const tierMappings = tiers.map((tier) => ({
309
+ monthlyDollars: tier.monthlyDollars ?? 0,
310
+ tier,
311
+ sponsors: []
312
+ }));
313
+ tierMappings.sort((a, b) => b.monthlyDollars - a.monthlyDollars);
314
+ const finalSponsors = tierMappings.filter((i) => i.monthlyDollars === 0);
315
+ if (finalSponsors.length !== 1)
316
+ throw new Error(`There should be exactly one tier with no \`monthlyDollars\`, but got ${finalSponsors.length}`);
317
+ sponsors.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt)).filter((s) => s.monthlyDollars > 0 || includePastSponsors).forEach((sponsor) => {
318
+ const tier = tierMappings.find((t) => sponsor.monthlyDollars >= t.monthlyDollars) ?? tierMappings[0];
319
+ tier.sponsors.push(sponsor);
320
+ });
321
+ return tierMappings;
322
+ }
323
+
324
+ let id = 0;
325
+ function genSvgImage(x, y, size, radius, base64Image, imageFormat) {
326
+ const cropId = `c${id++}`;
327
+ return `
328
+ <clipPath id="${cropId}">
329
+ <rect x="${x}" y="${y}" width="${size}" height="${size}" rx="${size * radius}" ry="${size * radius}" />
330
+ </clipPath>
331
+ <image x="${x}" y="${y}" width="${size}" height="${size}" href="data:image/${imageFormat};base64,${base64Image}" clip-path="url(#${cropId})"/>`;
332
+ }
333
+ async function generateBadge(x, y, sponsor, preset, radius, imageFormat) {
334
+ const { login } = sponsor;
335
+ let name = (sponsor.name || sponsor.login).trim();
336
+ const url = sponsor.websiteUrl || sponsor.linkUrl;
337
+ if (preset.name && preset.name.maxLength && name.length > preset.name.maxLength) {
338
+ if (name.includes(" "))
339
+ name = name.split(" ")[0];
340
+ else
341
+ name = `${name.slice(0, preset.name.maxLength - 3)}...`;
342
+ }
343
+ const { size } = preset.avatar;
344
+ let avatar = sponsor.avatarBuffer;
345
+ if (size < 50) {
346
+ avatar = await resizeImage(avatar, 50, imageFormat);
347
+ } else if (size < 80) {
348
+ avatar = await resizeImage(avatar, 80, imageFormat);
349
+ } else if (imageFormat === "png") {
350
+ avatar = await resizeImage(avatar, 120, imageFormat);
351
+ }
352
+ const avatarBase64 = avatar.toString("base64");
353
+ return `<a ${url ? `href="${url}" ` : ""}class="${preset.classes || "contribkit-link"}" target="_blank" id="${login}">
354
+ ${preset.name ? `<text x="${x + size / 2}" y="${y + size + 18}" text-anchor="middle" class="${preset.name.classes || "contribkit-name"}" fill="${preset.name.color || "currentColor"}">${encodeHtmlEntities(name)}</text>
355
+ ` : ""}${genSvgImage(x, y, size, radius, avatarBase64, imageFormat)}
356
+ </a>`.trim();
357
+ }
358
+ class SvgComposer {
359
+ constructor(config) {
360
+ this.config = config;
361
+ }
362
+ height = 0;
363
+ body = "";
364
+ addSpan(height = 0) {
365
+ this.height += height;
366
+ return this;
367
+ }
368
+ addTitle(text, classes = "contribkit-tier-title") {
369
+ return this.addText(text, classes);
370
+ }
371
+ addText(text, classes = "text") {
372
+ this.body += `<text x="${this.config.width / 2}" y="${this.height}" text-anchor="middle" class="${classes}">${text}</text>`;
373
+ this.height += 20;
374
+ return this;
375
+ }
376
+ addRaw(svg) {
377
+ this.body += svg;
378
+ return this;
379
+ }
380
+ async addSponsorLine(sponsors, preset) {
381
+ const offsetX = (this.config.width - sponsors.length * preset.boxWidth) / 2 + (preset.boxWidth - preset.avatar.size) / 2;
382
+ const sponsorLine = await Promise.all(sponsors.map(async (s, i) => {
383
+ const x = offsetX + preset.boxWidth * i;
384
+ const y = this.height;
385
+ const radius = s.sponsor.type === "Organization" ? 0.1 : 0.5;
386
+ return await generateBadge(x, y, s.sponsor, preset, radius, this.config.imageFormat);
387
+ }));
388
+ this.body += sponsorLine.join("\n");
389
+ this.height += preset.boxHeight;
390
+ }
391
+ async addSponsorGrid(sponsors, preset) {
392
+ const perLine = Math.floor((this.config.width - (preset.container?.sidePadding || 0) * 2) / preset.boxWidth);
393
+ for (let i = 0; i < Math.ceil(sponsors.length / perLine); i++) {
394
+ await this.addSponsorLine(sponsors.slice(i * perLine, (i + 1) * perLine), preset);
395
+ }
396
+ return this;
397
+ }
398
+ generateSvg() {
399
+ return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${this.config.width} ${this.height}" width="${this.config.width}" height="${this.height}">
400
+ <!-- Generated by https://github.com/antfu/sponsorskit -->
401
+ <style>${this.config.svgInlineCSS}</style>
402
+ ${this.body}
403
+ </svg>
404
+ `;
405
+ }
406
+ }
407
+ function encodeHtmlEntities(str) {
408
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
409
+ }
410
+
411
+ const AfdianProvider = {
412
+ name: "afdian",
413
+ fetchSponsors(config) {
414
+ return fetchAfdianSponsors(config.afdian);
415
+ }
416
+ };
417
+ async function fetchAfdianSponsors(options = {}) {
418
+ const {
419
+ userId,
420
+ token,
421
+ exechangeRate = 6.5,
422
+ includePurchases = true,
423
+ purchaseEffectivity = 30
424
+ } = options;
425
+ if (!userId || !token)
426
+ throw new Error("Afdian id and token are required");
427
+ const sponsors = [];
428
+ const sponsorshipApi = "https://afdian.com/api/open/query-sponsor";
429
+ let page = 1;
430
+ let pages = 1;
431
+ do {
432
+ const params = JSON.stringify({ page });
433
+ const ts = Math.round(+/* @__PURE__ */ new Date() / 1e3);
434
+ const sign = md5(token, params, ts, userId);
435
+ const sponsorshipData = await $fetch(sponsorshipApi, {
436
+ method: "POST",
437
+ headers: {
438
+ "Content-Type": "application/json"
439
+ },
440
+ responseType: "json",
441
+ body: {
442
+ user_id: userId,
443
+ params,
444
+ ts,
445
+ sign
446
+ }
447
+ });
448
+ page += 1;
449
+ if (sponsorshipData?.ec !== 200)
450
+ break;
451
+ pages = sponsorshipData.data.total_page;
452
+ if (!includePurchases) {
453
+ sponsorshipData.data.list = sponsorshipData.data.list.filter((sponsor) => {
454
+ const current = sponsor.current_plan;
455
+ if (!current || current.product_type === 0)
456
+ return true;
457
+ return false;
458
+ });
459
+ }
460
+ if (purchaseEffectivity > 0) {
461
+ sponsorshipData.data.list = sponsorshipData.data.list.map((sponsor) => {
462
+ const current = sponsor.current_plan;
463
+ if (!current || current.product_type === 0)
464
+ return sponsor;
465
+ const expireTime = current.update_time + purchaseEffectivity * 24 * 3600;
466
+ sponsor.current_plan.expire_time = expireTime;
467
+ return sponsor;
468
+ });
469
+ }
470
+ sponsors.push(...sponsorshipData.data.list);
471
+ } while (page <= pages);
472
+ const processed = sponsors.map((raw) => {
473
+ const current = raw.current_plan;
474
+ const expireTime = current?.expire_time;
475
+ const isExpired = expireTime ? expireTime < Date.now() / 1e3 : true;
476
+ let name = raw.user.name;
477
+ if (name.startsWith("\u7231\u53D1\u7535\u7528\u6237_"))
478
+ name = raw.user.user_id.slice(0, 5);
479
+ const avatarUrl = raw.user.avatar;
480
+ return {
481
+ sponsor: {
482
+ type: "User",
483
+ login: raw.user.user_id,
484
+ name,
485
+ avatarUrl,
486
+ linkUrl: `https://afdian.com/u/${raw.user.user_id}`
487
+ },
488
+ // all_sum_amount is based on cny
489
+ monthlyDollars: isExpired ? -1 : Number.parseFloat(raw.all_sum_amount) / exechangeRate,
490
+ privacyLevel: "PUBLIC",
491
+ tierName: "Afdian",
492
+ createdAt: new Date(raw.first_pay_time * 1e3).toISOString(),
493
+ expireAt: expireTime ? new Date(expireTime * 1e3).toISOString() : void 0,
494
+ // empty string means no plan, consider as one time sponsor
495
+ isOneTime: Boolean(raw.current_plan?.name),
496
+ provider: "afdian",
497
+ raw
498
+ };
499
+ });
500
+ return processed;
501
+ }
502
+ function md5(token, params, ts, userId) {
503
+ return createHash("md5").update(`${token}params${params}ts${ts}user_id${userId}`).digest("hex");
504
+ }
505
+
506
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
507
+ const DATA_URL_DEFAULT_MIME_TYPE = 'text/plain';
508
+ const DATA_URL_DEFAULT_CHARSET = 'us-ascii';
509
+
510
+ const testParameter = (name, filters) => filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name);
511
+
512
+ const supportedProtocols = new Set([
513
+ 'https:',
514
+ 'http:',
515
+ 'file:',
516
+ ]);
517
+
518
+ const hasCustomProtocol = urlString => {
519
+ try {
520
+ const {protocol} = new URL(urlString);
521
+
522
+ return protocol.endsWith(':')
523
+ && !protocol.includes('.')
524
+ && !supportedProtocols.has(protocol);
525
+ } catch {
526
+ return false;
527
+ }
528
+ };
529
+
530
+ const normalizeDataURL = (urlString, {stripHash}) => {
531
+ const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString);
532
+
533
+ if (!match) {
534
+ throw new Error(`Invalid URL: ${urlString}`);
535
+ }
536
+
537
+ let {type, data, hash} = match.groups;
538
+ const mediaType = type.split(';');
539
+ hash = stripHash ? '' : hash;
540
+
541
+ let isBase64 = false;
542
+ if (mediaType[mediaType.length - 1] === 'base64') {
543
+ mediaType.pop();
544
+ isBase64 = true;
545
+ }
546
+
547
+ // Lowercase MIME type
548
+ const mimeType = mediaType.shift()?.toLowerCase() ?? '';
549
+ const attributes = mediaType
550
+ .map(attribute => {
551
+ let [key, value = ''] = attribute.split('=').map(string => string.trim());
552
+
553
+ // Lowercase `charset`
554
+ if (key === 'charset') {
555
+ value = value.toLowerCase();
556
+
557
+ if (value === DATA_URL_DEFAULT_CHARSET) {
558
+ return '';
559
+ }
560
+ }
561
+
562
+ return `${key}${value ? `=${value}` : ''}`;
563
+ })
564
+ .filter(Boolean);
565
+
566
+ const normalizedMediaType = [
567
+ ...attributes,
568
+ ];
569
+
570
+ if (isBase64) {
571
+ normalizedMediaType.push('base64');
572
+ }
573
+
574
+ if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) {
575
+ normalizedMediaType.unshift(mimeType);
576
+ }
577
+
578
+ return `data:${normalizedMediaType.join(';')},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : ''}`;
579
+ };
580
+
581
+ function normalizeUrl$1(urlString, options) {
582
+ options = {
583
+ defaultProtocol: 'http',
584
+ normalizeProtocol: true,
585
+ forceHttp: false,
586
+ forceHttps: false,
587
+ stripAuthentication: true,
588
+ stripHash: false,
589
+ stripTextFragment: true,
590
+ stripWWW: true,
591
+ removeQueryParameters: [/^utm_\w+/i],
592
+ removeTrailingSlash: true,
593
+ removeSingleSlash: true,
594
+ removeDirectoryIndex: false,
595
+ removeExplicitPort: false,
596
+ sortQueryParameters: true,
597
+ ...options,
598
+ };
599
+
600
+ // Legacy: Append `:` to the protocol if missing.
601
+ if (typeof options.defaultProtocol === 'string' && !options.defaultProtocol.endsWith(':')) {
602
+ options.defaultProtocol = `${options.defaultProtocol}:`;
603
+ }
604
+
605
+ urlString = urlString.trim();
606
+
607
+ // Data URL
608
+ if (/^data:/i.test(urlString)) {
609
+ return normalizeDataURL(urlString, options);
610
+ }
611
+
612
+ if (hasCustomProtocol(urlString)) {
613
+ return urlString;
614
+ }
615
+
616
+ const hasRelativeProtocol = urlString.startsWith('//');
617
+ const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
618
+
619
+ // Prepend protocol
620
+ if (!isRelativeUrl) {
621
+ urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
622
+ }
623
+
624
+ const urlObject = new URL(urlString);
625
+
626
+ if (options.forceHttp && options.forceHttps) {
627
+ throw new Error('The `forceHttp` and `forceHttps` options cannot be used together');
628
+ }
629
+
630
+ if (options.forceHttp && urlObject.protocol === 'https:') {
631
+ urlObject.protocol = 'http:';
632
+ }
633
+
634
+ if (options.forceHttps && urlObject.protocol === 'http:') {
635
+ urlObject.protocol = 'https:';
636
+ }
637
+
638
+ // Remove auth
639
+ if (options.stripAuthentication) {
640
+ urlObject.username = '';
641
+ urlObject.password = '';
642
+ }
643
+
644
+ // Remove hash
645
+ if (options.stripHash) {
646
+ urlObject.hash = '';
647
+ } else if (options.stripTextFragment) {
648
+ urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, '');
649
+ }
650
+
651
+ // Remove duplicate slashes if not preceded by a protocol
652
+ // NOTE: This could be implemented using a single negative lookbehind
653
+ // regex, but we avoid that to maintain compatibility with older js engines
654
+ // which do not have support for that feature.
655
+ if (urlObject.pathname) {
656
+ // TODO: Replace everything below with `urlObject.pathname = urlObject.pathname.replace(/(?<!\b[a-z][a-z\d+\-.]{1,50}:)\/{2,}/g, '/');` when Safari supports negative lookbehind.
657
+
658
+ // Split the string by occurrences of this protocol regex, and perform
659
+ // duplicate-slash replacement on the strings between those occurrences
660
+ // (if any).
661
+ const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g;
662
+
663
+ let lastIndex = 0;
664
+ let result = '';
665
+ for (;;) {
666
+ const match = protocolRegex.exec(urlObject.pathname);
667
+ if (!match) {
668
+ break;
669
+ }
670
+
671
+ const protocol = match[0];
672
+ const protocolAtIndex = match.index;
673
+ const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex);
674
+
675
+ result += intermediate.replace(/\/{2,}/g, '/');
676
+ result += protocol;
677
+ lastIndex = protocolAtIndex + protocol.length;
678
+ }
679
+
680
+ const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length);
681
+ result += remnant.replace(/\/{2,}/g, '/');
682
+
683
+ urlObject.pathname = result;
684
+ }
685
+
686
+ // Decode URI octets
687
+ if (urlObject.pathname) {
688
+ try {
689
+ urlObject.pathname = decodeURI(urlObject.pathname);
690
+ } catch {}
691
+ }
692
+
693
+ // Remove directory index
694
+ if (options.removeDirectoryIndex === true) {
695
+ options.removeDirectoryIndex = [/^index\.[a-z]+$/];
696
+ }
697
+
698
+ if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
699
+ let pathComponents = urlObject.pathname.split('/');
700
+ const lastComponent = pathComponents[pathComponents.length - 1];
701
+
702
+ if (testParameter(lastComponent, options.removeDirectoryIndex)) {
703
+ pathComponents = pathComponents.slice(0, -1);
704
+ urlObject.pathname = pathComponents.slice(1).join('/') + '/';
705
+ }
706
+ }
707
+
708
+ if (urlObject.hostname) {
709
+ // Remove trailing dot
710
+ urlObject.hostname = urlObject.hostname.replace(/\.$/, '');
711
+
712
+ // Remove `www.`
713
+ if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) {
714
+ // Each label should be max 63 at length (min: 1).
715
+ // Source: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
716
+ // Each TLD should be up to 63 characters long (min: 2).
717
+ // It is technically possible to have a single character TLD, but none currently exist.
718
+ urlObject.hostname = urlObject.hostname.replace(/^www\./, '');
719
+ }
720
+ }
721
+
722
+ // Remove query unwanted parameters
723
+ if (Array.isArray(options.removeQueryParameters)) {
724
+ // eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
725
+ for (const key of [...urlObject.searchParams.keys()]) {
726
+ if (testParameter(key, options.removeQueryParameters)) {
727
+ urlObject.searchParams.delete(key);
728
+ }
729
+ }
730
+ }
731
+
732
+ if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {
733
+ urlObject.search = '';
734
+ }
735
+
736
+ // Keep wanted query parameters
737
+ if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
738
+ // eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
739
+ for (const key of [...urlObject.searchParams.keys()]) {
740
+ if (!testParameter(key, options.keepQueryParameters)) {
741
+ urlObject.searchParams.delete(key);
742
+ }
743
+ }
744
+ }
745
+
746
+ // Sort query parameters
747
+ if (options.sortQueryParameters) {
748
+ urlObject.searchParams.sort();
749
+
750
+ // Calling `.sort()` encodes the search parameters, so we need to decode them again.
751
+ try {
752
+ urlObject.search = decodeURIComponent(urlObject.search);
753
+ } catch {}
754
+ }
755
+
756
+ if (options.removeTrailingSlash) {
757
+ urlObject.pathname = urlObject.pathname.replace(/\/$/, '');
758
+ }
759
+
760
+ // Remove an explicit port number, excluding a default port number, if applicable
761
+ if (options.removeExplicitPort && urlObject.port) {
762
+ urlObject.port = '';
763
+ }
764
+
765
+ const oldUrlString = urlString;
766
+
767
+ // Take advantage of many of the Node `url` normalizations
768
+ urlString = urlObject.toString();
769
+
770
+ if (!options.removeSingleSlash && urlObject.pathname === '/' && !oldUrlString.endsWith('/') && urlObject.hash === '') {
771
+ urlString = urlString.replace(/\/$/, '');
772
+ }
773
+
774
+ // Remove ending `/` unless removeSingleSlash is false
775
+ if ((options.removeTrailingSlash || urlObject.pathname === '/') && urlObject.hash === '' && options.removeSingleSlash) {
776
+ urlString = urlString.replace(/\/$/, '');
777
+ }
778
+
779
+ // Restore relative protocol, if applicable
780
+ if (hasRelativeProtocol && !options.normalizeProtocol) {
781
+ urlString = urlString.replace(/^http:\/\//, '//');
782
+ }
783
+
784
+ // Remove http/https
785
+ if (options.stripProtocol) {
786
+ urlString = urlString.replace(/^(?:https?:)?\/\//, '');
787
+ }
788
+
789
+ return urlString;
790
+ }
791
+
792
+ function normalizeUrl(url) {
793
+ if (!url)
794
+ return void 0;
795
+ try {
796
+ return normalizeUrl$1(url, {
797
+ defaultProtocol: "https"
798
+ });
799
+ } catch {
800
+ return url;
801
+ }
802
+ }
803
+
804
+ function getMonthDifference(startDate, endDate) {
805
+ return (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth());
806
+ }
807
+ function getCurrentMonthTier(dateNow, sponsorDate, tiers, monthlyDollars) {
808
+ let currentMonths = 0;
809
+ for (const tier of tiers) {
810
+ const monthsAtTier = Math.floor(monthlyDollars / tier.monthlyDollars);
811
+ if (monthsAtTier === 0) {
812
+ continue;
813
+ }
814
+ if (currentMonths + monthsAtTier > getMonthDifference(sponsorDate, dateNow)) {
815
+ return tier.monthlyDollars;
816
+ }
817
+ monthlyDollars -= monthsAtTier * tier.monthlyDollars;
818
+ currentMonths += monthsAtTier;
819
+ }
820
+ return -1;
821
+ }
822
+ const API$1 = "https://api.github.com/graphql";
823
+ const graphql$1 = String.raw;
824
+ const GitHubProvider = {
825
+ name: "github",
826
+ fetchSponsors(config) {
827
+ return fetchGitHubSponsors(
828
+ config.github?.token || config.token,
829
+ config.github?.login || config.login,
830
+ config.github?.type || "user",
831
+ config
832
+ );
833
+ }
834
+ };
835
+ async function fetchGitHubSponsors(token, login, type, config) {
836
+ if (!token)
837
+ throw new Error("GitHub token is required");
838
+ if (!login)
839
+ throw new Error("GitHub login is required");
840
+ if (!["user", "organization"].includes(type))
841
+ throw new Error("GitHub type must be either `user` or `organization`");
842
+ const sponsors = [];
843
+ let cursor;
844
+ const tiers = config.tiers?.filter((tier) => tier.monthlyDollars && tier.monthlyDollars > 0).sort((a, b) => b.monthlyDollars - a.monthlyDollars);
845
+ do {
846
+ const query = makeQuery(login, type, !config.includePastSponsors, cursor);
847
+ const data = await $fetch(API$1, {
848
+ method: "POST",
849
+ body: { query },
850
+ headers: {
851
+ "Authorization": `bearer ${token}`,
852
+ "Content-Type": "application/json"
853
+ }
854
+ });
855
+ if (!data)
856
+ throw new Error(`Get no response on requesting ${API$1}`);
857
+ else if (data.errors?.[0]?.type === "INSUFFICIENT_SCOPES")
858
+ throw new Error("Token is missing the `read:user` and/or `read:org` scopes");
859
+ else if (data.errors?.length)
860
+ throw new Error(`GitHub API error:
861
+ ${JSON.stringify(data.errors, null, 2)}`);
862
+ sponsors.push(
863
+ ...data.data[type].sponsorshipsAsMaintainer.nodes || []
864
+ );
865
+ if (data.data[type].sponsorshipsAsMaintainer.pageInfo.hasNextPage)
866
+ cursor = data.data[type].sponsorshipsAsMaintainer.pageInfo.endCursor;
867
+ else
868
+ cursor = void 0;
869
+ } while (cursor);
870
+ const dateNow = /* @__PURE__ */ new Date();
871
+ const processed = sponsors.filter((raw) => !!raw.tier).map((raw) => {
872
+ let monthlyDollars = raw.tier.monthlyPriceInDollars;
873
+ if (!raw.isActive) {
874
+ if (tiers && raw.tier.isOneTime && config.prorateOnetime) {
875
+ monthlyDollars = getCurrentMonthTier(
876
+ dateNow,
877
+ new Date(raw.createdAt),
878
+ tiers,
879
+ monthlyDollars
880
+ );
881
+ } else {
882
+ monthlyDollars = -1;
883
+ }
884
+ }
885
+ return {
886
+ sponsor: {
887
+ ...raw.sponsorEntity,
888
+ websiteUrl: normalizeUrl(raw.sponsorEntity.websiteUrl),
889
+ linkUrl: `https://github.com/${raw.sponsorEntity.login}`,
890
+ __typename: void 0,
891
+ type: raw.sponsorEntity.__typename
892
+ },
893
+ isOneTime: raw.tier.isOneTime,
894
+ monthlyDollars,
895
+ privacyLevel: raw.privacyLevel,
896
+ tierName: raw.tier.name,
897
+ createdAt: raw.createdAt
898
+ };
899
+ });
900
+ return processed;
901
+ }
902
+ function makeQuery(login, type, activeOnly = true, cursor) {
903
+ return graphql$1`{
904
+ ${type}(login: "${login}") {
905
+ sponsorshipsAsMaintainer(activeOnly: ${Boolean(activeOnly)}, first: 100${cursor ? ` after: "${cursor}"` : ""}) {
906
+ totalCount
907
+ pageInfo {
908
+ endCursor
909
+ hasNextPage
910
+ }
911
+ nodes {
912
+ createdAt
913
+ privacyLevel
914
+ isActive
915
+ tier {
916
+ name
917
+ isOneTime
918
+ monthlyPriceInCents
919
+ monthlyPriceInDollars
920
+ }
921
+ sponsorEntity {
922
+ __typename
923
+ ...on Organization {
924
+ login
925
+ name
926
+ avatarUrl
927
+ websiteUrl
928
+ }
929
+ ...on User {
930
+ login
931
+ name
932
+ avatarUrl
933
+ websiteUrl
934
+ }
935
+ }
936
+ }
937
+ }
938
+ }
939
+ }`;
940
+ }
941
+
942
+ const LiberapayProvider = {
943
+ name: "liberapay",
944
+ fetchSponsors(config) {
945
+ return fetchLiberapaySponsors(config.liberapay?.login);
946
+ }
947
+ };
948
+ async function fetchLiberapaySponsors(login) {
949
+ if (!login)
950
+ throw new Error("Liberapay login is required");
951
+ const csvUrl = `https://liberapay.com/${login}/patrons/public.csv`;
952
+ const csvResponse = await $fetch(csvUrl);
953
+ const rows = [];
954
+ const { parseString } = await import('../chunks/index3.mjs').then(function (n) { return n.i; });
955
+ await new Promise((resolve) => {
956
+ parseString(csvResponse, {
957
+ headers: true,
958
+ ignoreEmpty: true,
959
+ trim: true
960
+ }).on("data", (row) => rows.push(row)).on("end", resolve);
961
+ });
962
+ const exchangeRates = rows.some((r) => r.donation_currency !== "USD") ? await $fetch("https://www.floatrates.com/daily/usd.json") : {};
963
+ return rows.map((row) => ({
964
+ sponsor: {
965
+ type: "User",
966
+ login: row.patron_username,
967
+ name: row.patron_public_name || row.patron_username,
968
+ avatarUrl: row.patron_avatar_url,
969
+ linkUrl: `https://liberapay.com/${row.patron_username}`
970
+ },
971
+ monthlyDollars: getMonthlyDollarAmount(Number.parseFloat(row.weekly_amount), row.donation_currency, exchangeRates),
972
+ privacyLevel: "PUBLIC",
973
+ createdAt: new Date(row.pledge_date).toISOString(),
974
+ provider: "liberapay"
975
+ }));
976
+ }
977
+ function getMonthlyDollarAmount(weeklyAmount, currency, exchangeRates) {
978
+ const weeksPerMonth = 4.345;
979
+ const monthlyAmount = weeklyAmount * weeksPerMonth;
980
+ if (currency === "USD")
981
+ return monthlyAmount;
982
+ const currencyLower = currency.toLowerCase();
983
+ const inverseRate = exchangeRates[currencyLower]?.inverseRate ?? 1;
984
+ return monthlyAmount * inverseRate;
985
+ }
986
+
987
+ const OpenCollectiveProvider = {
988
+ name: "opencollective",
989
+ fetchSponsors(config) {
990
+ return fetchOpenCollectiveSponsors(
991
+ config.opencollective?.key,
992
+ config.opencollective?.id,
993
+ config.opencollective?.slug,
994
+ config.opencollective?.githubHandle,
995
+ config.includePastSponsors
996
+ );
997
+ }
998
+ };
999
+ const API = "https://api.opencollective.com/graphql/v2/";
1000
+ const graphql = String.raw;
1001
+ async function fetchOpenCollectiveSponsors(key, id, slug, githubHandle, includePastSponsors) {
1002
+ if (!key)
1003
+ throw new Error("OpenCollective api key is required");
1004
+ if (!slug && !id && !githubHandle)
1005
+ throw new Error("OpenCollective collective id or slug or GitHub handle is required");
1006
+ const sponsors = [];
1007
+ const monthlyTransactions = [];
1008
+ let offset;
1009
+ offset = 0;
1010
+ do {
1011
+ const query = makeSubscriptionsQuery(id, slug, githubHandle, offset, !includePastSponsors);
1012
+ const data = await $fetch(API, {
1013
+ method: "POST",
1014
+ body: { query },
1015
+ headers: {
1016
+ "Api-Key": `${key}`,
1017
+ "Content-Type": "application/json"
1018
+ }
1019
+ });
1020
+ const nodes = data.data.account.orders.nodes;
1021
+ const totalCount = data.data.account.orders.totalCount;
1022
+ sponsors.push(...nodes || []);
1023
+ if (nodes.length !== 0) {
1024
+ if (totalCount > offset + nodes.length)
1025
+ offset += nodes.length;
1026
+ else
1027
+ offset = void 0;
1028
+ } else {
1029
+ offset = void 0;
1030
+ }
1031
+ } while (offset);
1032
+ offset = 0;
1033
+ do {
1034
+ const now = /* @__PURE__ */ new Date();
1035
+ const dateFrom = includePastSponsors ? void 0 : new Date(now.getFullYear(), now.getMonth(), 1);
1036
+ const query = makeTransactionsQuery(id, slug, githubHandle, offset, dateFrom);
1037
+ const data = await $fetch(API, {
1038
+ method: "POST",
1039
+ body: { query },
1040
+ headers: {
1041
+ "Api-Key": `${key}`,
1042
+ "Content-Type": "application/json"
1043
+ }
1044
+ });
1045
+ const nodes = data.data.account.transactions.nodes;
1046
+ const totalCount = data.data.account.transactions.totalCount;
1047
+ monthlyTransactions.push(...nodes || []);
1048
+ if (nodes.length !== 0) {
1049
+ if (totalCount > offset + nodes.length)
1050
+ offset += nodes.length;
1051
+ else
1052
+ offset = void 0;
1053
+ } else {
1054
+ offset = void 0;
1055
+ }
1056
+ } while (offset);
1057
+ const sponsorships = sponsors.map(createSponsorFromOrder).filter((sponsorship) => sponsorship !== null);
1058
+ const monthlySponsorships = monthlyTransactions.map((t) => createSponsorFromTransaction(t, sponsorships.map((i) => i[1].raw.id))).filter((sponsorship) => sponsorship !== null && sponsorship !== void 0);
1059
+ const transactionsBySponsorId = monthlySponsorships.reduce((map, [id2, sponsor]) => {
1060
+ const existingSponsor = map.get(id2);
1061
+ if (existingSponsor) {
1062
+ const createdAt = new Date(sponsor.createdAt);
1063
+ const existingSponsorCreatedAt = new Date(existingSponsor.createdAt);
1064
+ if (createdAt >= existingSponsorCreatedAt)
1065
+ map.set(id2, sponsor);
1066
+ else if (new Date(existingSponsorCreatedAt.getFullYear(), existingSponsorCreatedAt.getMonth(), 1) === new Date(createdAt.getFullYear(), createdAt.getMonth(), 1))
1067
+ existingSponsor.monthlyDollars += sponsor.monthlyDollars;
1068
+ } else {
1069
+ map.set(id2, sponsor);
1070
+ }
1071
+ return map;
1072
+ }, /* @__PURE__ */ new Map());
1073
+ const processed = sponsorships.reduce((map, [id2, sponsor]) => {
1074
+ const existingSponsor = map.get(id2);
1075
+ if (existingSponsor) {
1076
+ const createdAt = new Date(sponsor.createdAt);
1077
+ const existingSponsorCreatedAt = new Date(existingSponsor.createdAt);
1078
+ if (createdAt >= existingSponsorCreatedAt)
1079
+ map.set(id2, sponsor);
1080
+ } else {
1081
+ map.set(id2, sponsor);
1082
+ }
1083
+ return map;
1084
+ }, /* @__PURE__ */ new Map());
1085
+ const result = Array.from(processed.values()).concat(Array.from(transactionsBySponsorId.values()));
1086
+ return result;
1087
+ }
1088
+ function createSponsorFromOrder(order) {
1089
+ const slug = order.fromAccount.slug;
1090
+ if (slug === "github-sponsors")
1091
+ return void 0;
1092
+ let monthlyDollars = order.amount.value;
1093
+ if (order.status !== "ACTIVE")
1094
+ monthlyDollars = -1;
1095
+ else if (order.frequency === "MONTHLY")
1096
+ monthlyDollars = order.amount.value;
1097
+ else if (order.frequency === "YEARLY")
1098
+ monthlyDollars = order.amount.value / 12;
1099
+ else if (order.frequency === "ONETIME")
1100
+ monthlyDollars = order.amount.value;
1101
+ const sponsor = {
1102
+ sponsor: {
1103
+ name: order.fromAccount.name,
1104
+ type: getAccountType(order.fromAccount.type),
1105
+ login: slug,
1106
+ avatarUrl: order.fromAccount.imageUrl,
1107
+ websiteUrl: normalizeUrl(getBestUrl(order.fromAccount.socialLinks)),
1108
+ linkUrl: `https://opencollective.com/${slug}`,
1109
+ socialLogins: getSocialLogins(order.fromAccount.socialLinks, slug)
1110
+ },
1111
+ isOneTime: order.frequency === "ONETIME",
1112
+ monthlyDollars,
1113
+ privacyLevel: order.fromAccount.isIncognito ? "PRIVATE" : "PUBLIC",
1114
+ tierName: order.tier?.name,
1115
+ createdAt: order.frequency === "ONETIME" ? order.createdAt : order.order?.createdAt,
1116
+ raw: order
1117
+ };
1118
+ return [order.fromAccount.id, sponsor];
1119
+ }
1120
+ function createSponsorFromTransaction(transaction, excludeOrders) {
1121
+ const slug = transaction.fromAccount.slug;
1122
+ if (slug === "github-sponsors")
1123
+ return void 0;
1124
+ if (excludeOrders.includes(transaction.order?.id))
1125
+ return void 0;
1126
+ let monthlyDollars = transaction.amount.value;
1127
+ if (transaction.order?.status !== "ACTIVE") {
1128
+ const firstDayOfCurrentMonth = new Date((/* @__PURE__ */ new Date()).getUTCFullYear(), (/* @__PURE__ */ new Date()).getUTCMonth(), 1);
1129
+ if (new Date(transaction.createdAt) < firstDayOfCurrentMonth)
1130
+ monthlyDollars = -1;
1131
+ } else if (transaction.order?.frequency === "MONTHLY") {
1132
+ monthlyDollars = transaction.order?.amount.value;
1133
+ } else if (transaction.order?.frequency === "YEARLY") {
1134
+ monthlyDollars = transaction.order?.amount.value / 12;
1135
+ }
1136
+ const sponsor = {
1137
+ sponsor: {
1138
+ name: transaction.fromAccount.name,
1139
+ type: getAccountType(transaction.fromAccount.type),
1140
+ login: slug,
1141
+ avatarUrl: transaction.fromAccount.imageUrl,
1142
+ websiteUrl: normalizeUrl(getBestUrl(transaction.fromAccount.socialLinks)),
1143
+ linkUrl: `https://opencollective.com/${slug}`,
1144
+ socialLogins: getSocialLogins(transaction.fromAccount.socialLinks, slug)
1145
+ },
1146
+ isOneTime: transaction.order?.frequency === "ONETIME",
1147
+ monthlyDollars,
1148
+ privacyLevel: transaction.fromAccount.isIncognito ? "PRIVATE" : "PUBLIC",
1149
+ tierName: transaction.tier?.name,
1150
+ createdAt: transaction.order?.frequency === "ONETIME" ? transaction.createdAt : transaction.order?.createdAt,
1151
+ raw: transaction
1152
+ };
1153
+ return [transaction.fromAccount.id, sponsor];
1154
+ }
1155
+ function makeAccountQueryPartial(id, slug, githubHandle) {
1156
+ if (id)
1157
+ return `id: "${id}"`;
1158
+ else if (slug)
1159
+ return `slug: "${slug}"`;
1160
+ else if (githubHandle)
1161
+ return `githubHandle: "${githubHandle}"`;
1162
+ else
1163
+ throw new Error("OpenCollective collective id or slug or GitHub handle is required");
1164
+ }
1165
+ function makeTransactionsQuery(id, slug, githubHandle, offset, dateFrom, dateTo) {
1166
+ const accountQueryPartial = makeAccountQueryPartial(id, slug, githubHandle);
1167
+ const dateFromParam = dateFrom ? `, dateFrom: "${dateFrom.toISOString()}"` : "";
1168
+ const dateToParam = "";
1169
+ return graphql`{
1170
+ account(${accountQueryPartial}) {
1171
+ transactions(limit: 1000, offset:${offset}, type: CREDIT ${dateFromParam} ${dateToParam}) {
1172
+ offset
1173
+ limit
1174
+ totalCount
1175
+ nodes {
1176
+ type
1177
+ kind
1178
+ id
1179
+ order {
1180
+ id
1181
+ status
1182
+ frequency
1183
+ tier {
1184
+ name
1185
+ }
1186
+ amount {
1187
+ value
1188
+ }
1189
+ }
1190
+ createdAt
1191
+ amount {
1192
+ value
1193
+ }
1194
+ fromAccount {
1195
+ name
1196
+ id
1197
+ slug
1198
+ type
1199
+ githubHandle
1200
+ socialLinks {
1201
+ url
1202
+ type
1203
+ }
1204
+ isIncognito
1205
+ imageUrl(height: 460, format: png)
1206
+ }
1207
+ }
1208
+ }
1209
+ }
1210
+ }`;
1211
+ }
1212
+ function makeSubscriptionsQuery(id, slug, githubHandle, offset, activeOnly) {
1213
+ const activeOrNot = activeOnly ? "onlyActiveSubscriptions: true" : "onlySubscriptions: true";
1214
+ return graphql`{
1215
+ account(${makeAccountQueryPartial(id, slug, githubHandle)}) {
1216
+ orders(limit: 1000, offset:${offset}, ${activeOrNot}, filter: INCOMING) {
1217
+ nodes {
1218
+ id
1219
+ createdAt
1220
+ frequency
1221
+ status
1222
+ tier {
1223
+ name
1224
+ }
1225
+ amount {
1226
+ value
1227
+ }
1228
+ totalDonations {
1229
+ value
1230
+ }
1231
+ createdAt
1232
+ fromAccount {
1233
+ name
1234
+ id
1235
+ slug
1236
+ type
1237
+ socialLinks {
1238
+ url
1239
+ type
1240
+ }
1241
+ isIncognito
1242
+ imageUrl(height: 460, format: png)
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1247
+ }`;
1248
+ }
1249
+ function getAccountType(type) {
1250
+ switch (type) {
1251
+ case "INDIVIDUAL":
1252
+ return "User";
1253
+ case "ORGANIZATION":
1254
+ case "COLLECTIVE":
1255
+ case "FUND":
1256
+ case "PROJECT":
1257
+ case "EVENT":
1258
+ case "VENDOR":
1259
+ case "BOT":
1260
+ return "Organization";
1261
+ default:
1262
+ throw new Error(`Unknown account type: ${type}`);
1263
+ }
1264
+ }
1265
+ function getBestUrl(socialLinks) {
1266
+ const urls = socialLinks.filter((i) => i.type === "WEBSITE" || i.type === "GITHUB" || i.type === "GITLAB" || i.type === "TWITTER" || i.type === "FACEBOOK" || i.type === "YOUTUBE" || i.type === "INSTAGRAM" || i.type === "LINKEDIN" || i.type === "DISCORD" || i.type === "TUMBLR").map((i) => i.url);
1267
+ return urls[0];
1268
+ }
1269
+ function getSocialLogins(socialLinks = [], opencollectiveLogin) {
1270
+ const socialLogins = {};
1271
+ for (const link of socialLinks) {
1272
+ if (link.type === "GITHUB") {
1273
+ const login = link.url.match(/github\.com\/([^/]*)/)?.[1];
1274
+ if (login)
1275
+ socialLogins.github = login;
1276
+ }
1277
+ }
1278
+ if (opencollectiveLogin)
1279
+ socialLogins.opencollective = opencollectiveLogin;
1280
+ return socialLogins;
1281
+ }
1282
+
1283
+ const PatreonProvider = {
1284
+ name: "patreon",
1285
+ fetchSponsors(config) {
1286
+ return fetchPatreonSponsors(config.patreon?.token || config.token);
1287
+ }
1288
+ };
1289
+ async function fetchPatreonSponsors(token) {
1290
+ if (!token)
1291
+ throw new Error("Patreon token is required");
1292
+ const userData = await $fetch(
1293
+ "https://www.patreon.com/api/oauth2/api/current_user/campaigns?include=null",
1294
+ {
1295
+ method: "GET",
1296
+ headers: {
1297
+ "Authorization": `bearer ${token}`,
1298
+ "Content-Type": "application/json"
1299
+ },
1300
+ responseType: "json"
1301
+ }
1302
+ );
1303
+ const userCampaignId = userData.data[0].id;
1304
+ const sponsors = [];
1305
+ let sponsorshipApi = `https://www.patreon.com/api/oauth2/v2/campaigns/${userCampaignId}/members?include=user&fields%5Bmember%5D=currently_entitled_amount_cents,patron_status,pledge_relationship_start,lifetime_support_cents&fields%5Buser%5D=image_url,url,first_name,full_name&page%5Bcount%5D=100`;
1306
+ do {
1307
+ const sponsorshipData = await $fetch(sponsorshipApi, {
1308
+ method: "GET",
1309
+ headers: {
1310
+ "Authorization": `bearer ${token}`,
1311
+ "Content-Type": "application/json"
1312
+ },
1313
+ responseType: "json"
1314
+ });
1315
+ sponsors.push(
1316
+ ...sponsorshipData.data.filter((membership) => membership.attributes.patron_status !== null).map((membership) => ({
1317
+ membership,
1318
+ patron: sponsorshipData.included.find(
1319
+ (v) => v.id === membership.relationships.user.data.id
1320
+ )
1321
+ }))
1322
+ );
1323
+ sponsorshipApi = sponsorshipData.links?.next;
1324
+ } while (sponsorshipApi);
1325
+ const processed = sponsors.map(
1326
+ (raw) => ({
1327
+ sponsor: {
1328
+ avatarUrl: raw.patron.attributes.image_url,
1329
+ login: raw.patron.attributes.first_name,
1330
+ name: raw.patron.attributes.full_name,
1331
+ type: "User",
1332
+ // Patreon only support user
1333
+ linkUrl: raw.patron.attributes.url
1334
+ },
1335
+ isOneTime: false,
1336
+ // One-time pledges not supported
1337
+ // The "former_patron" and "declined_patron" both is past sponsors
1338
+ monthlyDollars: ["former_patron", "declined_patron"].includes(raw.membership.attributes.patron_status) ? -1 : Math.floor(raw.membership.attributes.currently_entitled_amount_cents / 100),
1339
+ privacyLevel: "PUBLIC",
1340
+ // Patreon is all public
1341
+ tierName: "Patreon",
1342
+ createdAt: raw.membership.attributes.pledge_relationship_start
1343
+ })
1344
+ );
1345
+ return processed;
1346
+ }
1347
+
1348
+ const PolarProvider = {
1349
+ name: "polar",
1350
+ fetchSponsors(config) {
1351
+ return fetchPolarSponsors(config.polar?.token || config.token, config.polar?.organization);
1352
+ }
1353
+ };
1354
+ async function fetchPolarSponsors(token, organization) {
1355
+ if (!token)
1356
+ throw new Error("Polar token is required");
1357
+ if (!organization)
1358
+ throw new Error("Polar organization is required");
1359
+ const apiFetch = ofetch.create({
1360
+ baseURL: "https://api.polar.sh/v1",
1361
+ headers: { Authorization: `Bearer ${token}` }
1362
+ });
1363
+ const org = await apiFetch("/organizations", {
1364
+ params: {
1365
+ slug: organization
1366
+ }
1367
+ });
1368
+ const orgId = org.items?.[0]?.id;
1369
+ if (!orgId)
1370
+ throw new Error(`Polar organization "${organization}" not found`);
1371
+ let page = 1;
1372
+ let pages = 1;
1373
+ const subscriptions = [];
1374
+ do {
1375
+ const params = {
1376
+ organization_id: orgId,
1377
+ page
1378
+ };
1379
+ const subs = await apiFetch("/subscriptions", { params });
1380
+ subscriptions.push(...subs.items);
1381
+ pages = subs.pagination.max_page;
1382
+ page += 1;
1383
+ } while (page <= pages);
1384
+ return subscriptions.filter((sub) => !!sub.price).map((sub) => {
1385
+ const isActive = sub.status === "active";
1386
+ return {
1387
+ sponsor: {
1388
+ name: sub.user.public_name,
1389
+ avatarUrl: sub.user.avatar_url,
1390
+ login: sub.user.github_username,
1391
+ type: sub.product.type === "individual" ? "User" : "Organization",
1392
+ socialLogins: {
1393
+ github: sub.user.github_username
1394
+ }
1395
+ },
1396
+ isOneTime: false,
1397
+ provider: "polar",
1398
+ privacyLevel: "PUBLIC",
1399
+ createdAt: new Date(sub.created_at).toISOString(),
1400
+ tierName: isActive ? sub.product.name : void 0,
1401
+ monthlyDollars: isActive ? sub.price.price_amount / 100 : -1
1402
+ };
1403
+ });
1404
+ }
1405
+
1406
+ const ProvidersMap = {
1407
+ github: GitHubProvider,
1408
+ patreon: PatreonProvider,
1409
+ opencollective: OpenCollectiveProvider,
1410
+ afdian: AfdianProvider,
1411
+ polar: PolarProvider,
1412
+ liberapay: LiberapayProvider
1413
+ };
1414
+ function guessProviders(config) {
1415
+ const items = [];
1416
+ if (config.github && config.github.login)
1417
+ items.push("github");
1418
+ if (config.patreon && config.patreon.token)
1419
+ items.push("patreon");
1420
+ if (config.opencollective && (config.opencollective.id || config.opencollective.slug || config.opencollective.githubHandle))
1421
+ items.push("opencollective");
1422
+ if (config.afdian && config.afdian.userId && config.afdian.token)
1423
+ items.push("afdian");
1424
+ if (config.polar && config.polar.token)
1425
+ items.push("polar");
1426
+ if (config.liberapay && config.liberapay.login)
1427
+ items.push("liberapay");
1428
+ if (!items.length)
1429
+ items.push("github");
1430
+ return items;
1431
+ }
1432
+ function resolveProviders(names) {
1433
+ return Array.from(new Set(names)).map((i) => {
1434
+ if (typeof i === "string") {
1435
+ const provider = ProvidersMap[i];
1436
+ if (!provider)
1437
+ throw new Error(`Unknown provider: ${i}`);
1438
+ return provider;
1439
+ }
1440
+ return i;
1441
+ });
1442
+ }
1443
+ async function fetchSponsors(config) {
1444
+ const providers = resolveProviders(guessProviders(config));
1445
+ const sponsorships = await Promise.all(
1446
+ providers.map((provider) => provider.fetchSponsors(config))
1447
+ );
1448
+ return sponsorships.flat(1);
1449
+ }
1450
+
1451
+ const outputFormats = ["svg", "png", "webp", "json"];
1452
+
1453
+ export { FALLBACK_AVATAR as F, GitHubProvider as G, ProvidersMap as P, SvgComposer as S, defaultTiers as a, defaultInlineCSS as b, defaultConfig as c, defineConfig as d, presets as e, resizeImage as f, svgToWebp as g, genSvgImage as h, generateBadge as i, guessProviders as j, resolveProviders as k, loadConfig as l, fetchSponsors as m, fetchGitHubSponsors as n, makeQuery as o, partitionTiers as p, outputFormats as q, resolveAvatars as r, svgToPng as s, tierPresets as t, version as v };