diff --git a/.gitignore b/.gitignore
index a543d68..42b2a8e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,7 @@ nbproject
# Folders to ignore
node_modules
-package*.json
+package-lock.json
vendor
.sass-cache
.jekyll-metadata
diff --git a/functions/[[catchall]].ts b/functions/[[catchall]].ts
index 3a0557a..1b9ee77 100644
--- a/functions/[[catchall]].ts
+++ b/functions/[[catchall]].ts
@@ -29,7 +29,7 @@ async function getPrices(request: Request, env: Env, waitUntil: (promise: Promis
const productUrl = new URL(`https://api.gumroad.com/v2/products/${ProductId}`);
productUrl.searchParams.append('access_token', env.GUMROAD_ACCESS_TOKEN);
- const productResponse = await fetch(productUrl, { method: 'GET' });
+ const productResponse = await fetch(productUrl, { method: 'GET', headers: [[ 'user-agent', navigator.userAgent ]] });
if (!productResponse.ok) {
console.error('Product response not ok', productResponse.status);
return null;
diff --git a/functions/api/ping/[[catchall]].ts b/functions/api/ping/[[catchall]].ts
new file mode 100644
index 0000000..4f8d7ef
--- /dev/null
+++ b/functions/api/ping/[[catchall]].ts
@@ -0,0 +1,75 @@
+///
+
+import qs from 'qs';
+
+const ShortProductId = 'nuOluY';
+
+const Org = 'hydecorp';
+const TeamSlug = 'pro-customers';
+const GitHubApiVersion = '2022-11-28';
+
+interface Env {
+ GUMROAD_ACCESS_TOKEN: string
+ GITHUB_ADMIN_PAT: string
+ PING_SECRET: string
+ SELLER_ID: string
+ KV?: KVNamespace
+}
+
+export const onRequestPost: PagesFunction = async (context) => {
+ const { env, params } = context;
+ const request = context.request as Request;
+
+ const secret = params.catchall?.[0];
+ if (secret !== env.PING_SECRET) {
+ console.error("Invalid secret:", secret);
+ return new Response(null, { status: 401 });
+ }
+
+ console.debug("content-type", request.headers.get('content-type'));
+ if (!request.headers.get('content-type')?.includes('x-www-form-urlencoded')) {
+ console.error("Invalid content-type", request.headers.get('content-type'));
+ return new Response(null, { status: 400 });
+ }
+ const payload = qs.parse(await request.text())
+ console.debug("payload", payload);
+
+ if (payload.seller_id !== env.SELLER_ID) {
+ console.error("Invalid seller_id, this should never happen");
+ return new Response(null, { status: 400 });
+ }
+ if (payload.short_product_id !== ShortProductId) {
+ console.warn("Unsupported product_permalink");
+ return new Response(); // ok
+ }
+ if (!payload.custom_fields) {
+ console.error("No custom_fields");
+ return new Response(null, { status: 400 });
+ }
+ console.debug("payload.custom_fields", payload.custom_fields);
+
+ const customFields = payload.custom_fields as Record|null;
+ const githubHandle = customFields && Object.entries(customFields).find(([k]) => k.trim().toLowerCase().startsWith('github'))?.[1];
+ console.debug("githubHandle", githubHandle);
+
+ if (!githubHandle) {
+ console.warn("No GitHub handle provided");
+ return new Response(); // ok
+ }
+
+ const url = `https://api.github.com/orgs/${Org}/teams/${TeamSlug}/memberships/${githubHandle}`;
+ const ghResponse = await fetch(url, {
+ method: 'PUT',
+ headers: {
+ 'accept': 'application/vnd.github+json',
+ 'authorization': `Bearer ${env.GITHUB_ADMIN_PAT}`,
+ 'x-github-api-version': GitHubApiVersion,
+ 'content-type': 'application/json',
+ 'user-agent': navigator.userAgent
+ },
+ body: JSON.stringify({ role: 'member' })
+ });
+ console.debug("ghResponse/status", ghResponse.status);
+ console.debug("ghResponse/body", await ghResponse.text());
+ return new Response(null, { status: ghResponse.status });
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bbaf773
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "dependencies": {
+ "qs": "^6.13.0",
+ "re-template-tag": "^2.0.1"
+ },
+ "devDependencies": {
+ "@cloudflare/workers-types": "^4.20240903.0",
+ "@types/qs": "^6.9.15"
+ }
+}