UNPKG

@xmcp-dev/polar

Version:

Polar integration for xmcp

338 lines 15.8 kB
export class PolarProvider { constructor(config) { this.config = config; this.customerData = null; this.event = null; this.config.type = this.config.type ?? "production"; this.endpointUrl = this.config.type === "sandbox" ? "https://sandbox-api.polar.sh" : "https://api.polar.sh"; } static getInstance(config) { if (!PolarProvider.instance) { PolarProvider.instance = new PolarProvider(config); } return PolarProvider.instance; } async getMeterIdFromProduct() { const endpoint = this.endpointUrl + "/v1/products/" + this.config.productId; const response = await fetch(endpoint, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.config.token}`, }, }); const data = (await response.json()); // search in data.benefits (array) for type = meter credit and get the id from that object const meterCreditBenefit = data.benefits.find((benefit) => benefit.type === "meter_credit"); if (!meterCreditBenefit) { throw new Error("No meter credit benefit found in product"); } // the benefit ID might not be the same as the meter ID // We might need to use the meter_id property if it exists const meterId = meterCreditBenefit.meter_id || meterCreditBenefit.id; return meterId; } async hasUsageLeft() { console.log("[PolarProvider] Checking usage left..."); console.log("[PolarProvider] Current meterId:", PolarProvider.meterId); console.log("[PolarProvider] Current event:", this.event); console.log("[PolarProvider] Current customerData:", this.customerData); if (!PolarProvider.meterId && this.event?.name) { console.log("[PolarProvider] No meter ID set, attempting to get from product..."); try { PolarProvider.meterId = await this.getMeterIdFromProduct(); console.log("[PolarProvider] Retrieved meter ID:", PolarProvider.meterId); } catch (error) { console.error("[PolarProvider] Failed to get meter ID:", error); return { hasUsage: false, message: "Failed to get meter ID" }; } } if (!PolarProvider.meterId || !this.customerData?.id) { console.log("[PolarProvider] Missing meter ID or customer ID"); return { hasUsage: false, message: "No meter tracking configured" }; } try { const endpoint = `${this.endpointUrl}/v1/customers/${this.customerData.id}/state`; console.log("[PolarProvider] Fetching customer state from:", endpoint); const response = await fetch(endpoint, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.config.token}`, }, }); console.log("[PolarProvider] Customer state response status:", response.status); if (!response.ok) { const errorText = await response.text(); console.error("[PolarProvider] Customer state API failed:", response.status, errorText); return { hasUsage: false, message: "Customer state API failed" }; } const data = (await response.json()); console.log("[PolarProvider] Customer state data:", JSON.stringify(data, null, 2)); // check if customer has any meter credit benefits const meterCreditBenefits = data.granted_benefits?.filter((benefit) => benefit.benefit_type === "meter_credit") || []; console.log("[PolarProvider] Found meter credit benefits:", meterCreditBenefits.length); if (meterCreditBenefits.length === 0) { // ONLY case for unlimited usage: no meter credit benefits set up at all console.log("[PolarProvider] No meter credit benefits found - unlimited usage"); return { hasUsage: true }; } // automatically find the meter that corresponds to this event let targetMeter = null; // verify customer has the granted meter credit benefit for this product const grantedMeterBenefit = data.granted_benefits?.find((benefit) => benefit.benefit_id === PolarProvider.meterId && benefit.benefit_type === "meter_credit"); console.log("[PolarProvider] Granted meter benefit for this meter:", grantedMeterBenefit); if (!grantedMeterBenefit) { console.log("[PolarProvider] No granted meter benefit found for meter ID:", PolarProvider.meterId); return { hasUsage: false, message: "No granted meter benefit found" }; } // since we're using a single event name, we expect a single meter to be associated with it const activeMeters = data.active_meters || []; console.log("[PolarProvider] Active meters:", activeMeters); if (activeMeters.length === 0) { console.log("[PolarProvider] No active meters found"); return { hasUsage: false, message: "No active meters found" }; } // use the first meter that has been credited (indicating it's active for this event) const creditedMeter = activeMeters.find((meter) => meter.credited_units > 0); console.log("[PolarProvider] Credited meter:", creditedMeter); if (creditedMeter) { targetMeter = creditedMeter; } else { // if no meters have been credited, meter credits exist but no credits allocated yet console.log("[PolarProvider] No credited meters found"); return { hasUsage: false, message: "No credited meters found - purchase credits to continue", }; } if (!targetMeter) { console.log("[PolarProvider] No target meter identified"); return { hasUsage: false, message: "No target meter found" }; } const { consumed_units, credited_units, balance } = targetMeter; console.log("[PolarProvider] Meter usage - consumed:", consumed_units, "credited:", credited_units, "balance:", balance); const hasUsage = balance > 0; if (!hasUsage) { console.log("[PolarProvider] No usage credits remaining"); return { hasUsage: false, message: `Usage meter credit exhausted. You have consumed ${consumed_units} credits out of ${credited_units}.`, }; } console.log("[PolarProvider] Usage credits available"); return { hasUsage: true }; } catch (error) { console.error("[PolarProvider] Error checking customer meter usage:", error); return { hasUsage: false, message: "Error checking customer meter usage", }; } } async evaluate(licenseKey) { const endpoint = this.endpointUrl + "/v1/license-keys/validate"; const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.config.token}`, }, body: JSON.stringify({ key: licenseKey, organization_id: this.config.organizationId, }), }); const data = (await response.json()); // save the customer Id for usage in ingestEvents in case of enabled this.customerData = { id: data.customer.id, external_id: data.customer.external_id, }; return data; } async validate(object) { let checkoutUrl = ""; try { checkoutUrl = await this.getCheckoutUrl(); } catch (error) { checkoutUrl = ""; } // status is granted if (object.status !== "granted") { return { valid: false, code: "license_key_not_granted", message: `License key access denied. Purchase a valid license at: ${checkoutUrl}`, }; } // usage limits if (object.limit_usage !== null && object.usage >= object.limit_usage) { return { valid: false, code: "license_key_usage_limit_reached", message: `License key usage limit reached (${object.usage}/${object.limit_usage}). Purchase a new license at: ${checkoutUrl}`, }; } // expiration date if (object.expires_at !== null) { const expirationDate = new Date(object.expires_at); const currentDate = new Date(); if (currentDate >= expirationDate) { return { valid: false, code: "license_key_expired", message: `License key expired on ${expirationDate.toLocaleDateString()}. Purchase a new license at: ${checkoutUrl}`, }; } } return { valid: true, code: "license_key_valid", message: "License key is valid and active", }; } async validateLicenseKey(licenseKey, event) { console.log("[PolarProvider] validateLicenseKey called"); console.log("[PolarProvider] License key provided:", !!licenseKey); console.log("[PolarProvider] Event provided:", event); // Store event for use in event ingestion this.event = event; console.log("[PolarProvider] Event stored for tracking:", this.event); if (!licenseKey || licenseKey.trim() === "") { let checkoutUrl = ""; try { checkoutUrl = await this.getCheckoutUrl(); } catch (error) { checkoutUrl = ""; } return { valid: false, code: "license_key_missing", message: `No license key provided. Please provide a valid license key in the 'license-key' header. Purchase a valid license at: ${checkoutUrl}`, }; } try { const response = await this.evaluate(licenseKey); const validateResponse = await this.validate(response); // only check usage if we have valid license and meter tracking is configured if (validateResponse.valid && this.event?.name) { console.log("[PolarProvider] License is valid and event name provided, checking usage..."); console.log("[PolarProvider] Event to track:", this.event); const usageResult = await this.hasUsageLeft(); console.log("[PolarProvider] Usage check result:", usageResult); if (!usageResult.hasUsage) { console.log("[PolarProvider] No usage left, returning error"); let checkoutUrl = ""; try { checkoutUrl = await this.getCheckoutUrl(); } catch (error) { checkoutUrl = ""; } return { valid: false, code: "meter_credit_exhausted", message: `${usageResult.message} Purchase additional credits at: ${checkoutUrl}`, }; } // if we reach here, usage is available console.log("[PolarProvider] Usage available, proceeding with event ingestion..."); const ingestResult = await this.ingestEvents(); console.log("[PolarProvider] Event ingestion result:", ingestResult); } else { console.log("[PolarProvider] Skipping event ingestion - license valid:", validateResponse.valid, "event name:", this.event?.name); } return validateResponse; } catch (error) { let checkoutUrl; try { checkoutUrl = await this.getCheckoutUrl(); } catch (checkoutError) { checkoutUrl = null; } return { valid: false, code: "license_key_error", message: `An error occurred while validating the license key. Please provide a valid license key in the 'license-key' header. Purchase a valid license at: ${checkoutUrl}`, }; } } async getCheckoutUrl() { const endpoint = this.endpointUrl + "/v1/checkouts"; const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.config.token}`, }, body: JSON.stringify({ products: [this.config.productId], }), }); const data = (await response.json()); return data.url; } async ingestEvents() { console.log("[PolarProvider] Starting event ingestion"); const endpoint = this.endpointUrl + "/v1/events/ingest"; console.log("[PolarProvider] Ingestion endpoint:", endpoint); // Log the current state console.log("[PolarProvider] Current event:", this.event); console.log("[PolarProvider] Current customer data:", this.customerData); console.log("[PolarProvider] Meter ID:", PolarProvider.meterId); const eventPayload = { events: [ { name: this.event?.name, customer_id: this.customerData?.id, metadata: this.event?.metadata, }, ], }; console.log("[PolarProvider] Event payload to be sent:", JSON.stringify(eventPayload, null, 2)); try { console.log("[PolarProvider] Sending event ingestion request..."); const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.config.token}`, }, body: JSON.stringify(eventPayload), }); console.log("[PolarProvider] Event ingestion response status:", response.status); console.log("[PolarProvider] Event ingestion response headers:", Object.fromEntries(response.headers.entries())); if (!response.ok) { const errorText = await response.text(); console.error("[PolarProvider] Event ingestion failed with status:", response.status); console.error("[PolarProvider] Error response body:", errorText); return { error: `Event ingestion failed: ${response.status} - ${errorText}`, }; } const data = (await response.json()); console.log("[PolarProvider] Event ingestion successful, response:", JSON.stringify(data, null, 2)); return data; } catch (error) { console.error("[PolarProvider] Event ingestion error:", error); return { error: `Failed to ingest event: ${error}` }; } } } PolarProvider.instance = null; PolarProvider.meterId = null; // get once //# sourceMappingURL=index.js.map