diff --git a/README.md b/README.md index ff8585c..38033bc 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Our goal is to bring the TypeScript ecosystem the observability tools it’s bee ## Supported integrations +- [`@kubiks/otel-autumn`](./packages/otel-autumn/README.md) - [`@kubiks/otel-better-auth`](./packages/otel-better-auth/README.md) - [`@kubiks/otel-drizzle`](./packages/otel-drizzle/README.md) - [`@kubiks/otel-resend`](./packages/otel-resend/README.md) @@ -23,7 +24,6 @@ Our goal is to bring the TypeScript ecosystem the observability tools it’s bee - [Stripe](https://stripe.com/) - [Polar.sh](https://polar.sh/) -- [Autumn](https://useautumn.com/) - [ClickHouse](https://clickhouse.com/) - [AI SDK](https://ai-sdk.dev/) - [Mastra](https://mastra.ai/) diff --git a/images/otel-autumn-trace.png b/images/otel-autumn-trace.png new file mode 100644 index 0000000..4ca537b Binary files /dev/null and b/images/otel-autumn-trace.png differ diff --git a/packages/otel-autumn/.npmignore b/packages/otel-autumn/.npmignore new file mode 100644 index 0000000..18682a1 --- /dev/null +++ b/packages/otel-autumn/.npmignore @@ -0,0 +1,5 @@ +src/ +tsconfig.json +*.test.ts +.gitignore +node_modules/ diff --git a/packages/otel-autumn/CHANGELOG.md b/packages/otel-autumn/CHANGELOG.md new file mode 100644 index 0000000..cc75fd1 --- /dev/null +++ b/packages/otel-autumn/CHANGELOG.md @@ -0,0 +1,13 @@ +# @kubiks/otel-autumn + +## 1.0.0 + +### Major Changes + +- Initial release of OpenTelemetry instrumentation for Autumn billing SDK +- Instrument core billing operations: check, track, checkout, attach, cancel +- Comprehensive span attributes for billing observability +- Support for feature access control monitoring +- Usage tracking instrumentation +- Checkout flow tracing with Stripe integration details +- Product lifecycle management (attach/cancel) tracing diff --git a/packages/otel-autumn/LICENSE b/packages/otel-autumn/LICENSE new file mode 100644 index 0000000..55f20b0 --- /dev/null +++ b/packages/otel-autumn/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Kubiks + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/otel-autumn/README.md b/packages/otel-autumn/README.md new file mode 100644 index 0000000..858b417 --- /dev/null +++ b/packages/otel-autumn/README.md @@ -0,0 +1,207 @@ +# @kubiks/otel-autumn + +OpenTelemetry instrumentation for the [Autumn](https://useautumn.com) billing SDK. +Capture spans for every billing operation including feature checks, usage tracking, checkout flows, product attachments, and cancellations with detailed metadata. + +![Autumn Trace Visualization](https://github.com/kubiks-inc/otel/blob/main/images/otel-autumn-trace.png) + +_Visualize your billing operations with detailed span information including operation type, customer IDs, feature IDs, and billing metadata._ + +## Installation + +```bash +npm install @kubiks/otel-autumn +# or +pnpm add @kubiks/otel-autumn +``` + +**Peer Dependencies:** `@opentelemetry/api` >= 1.9.0, `autumn-js` >= 0.1.0 + +## Quick Start + +```ts +import { Autumn } from "autumn-js"; +import { instrumentAutumn } from "@kubiks/otel-autumn"; + +const autumn = instrumentAutumn( + new Autumn({ + secretKey: process.env.AUTUMN_SECRET_KEY!, + }), +); + +// All operations are now automatically traced +const checkResult = await autumn.check({ + customer_id: "user_123", + feature_id: "messages", +}); + +await autumn.track({ + customer_id: "user_123", + feature_id: "messages", + value: 1, +}); +``` + +`instrumentAutumn` wraps your Autumn client instance — no configuration changes needed. Every SDK call creates a client span with detailed billing attributes. + +## What Gets Traced + +This instrumentation wraps the core Autumn billing methods: + +- **`check`** - Feature access and product status checks +- **`track`** - Usage event tracking +- **`checkout`** - Checkout session creation +- **`attach`** - Product attachment to customers +- **`cancel`** - Product cancellation + +Each operation creates a dedicated span with operation-specific attributes. + +## Span Attributes + +### Common Attributes (All Operations) + +| Attribute | Description | Example | +| -------------------- | ------------------------- | ---------------------- | +| `billing.system` | Constant value `autumn` | `autumn` | +| `billing.operation` | Operation type | `check`, `track` | +| `autumn.resource` | Resource being accessed | `features`, `products` | +| `autumn.target` | Full operation target | `features.check` | +| `autumn.customer_id` | Customer ID | `user_123` | +| `autumn.entity_id` | Entity ID (if applicable) | `org_456` | + +### Check Operation + +| Attribute | Description | Example | +| ------------------------- | ------------------------------ | ---------- | +| `autumn.feature_id` | Feature being checked | `messages` | +| `autumn.product_id` | Product being checked | `pro` | +| `autumn.allowed` | Whether access is allowed | `true` | +| `autumn.balance` | Current balance/remaining uses | `42` | +| `autumn.usage` | Current usage | `8` | +| `autumn.included_usage` | Included usage in plan | `50` | +| `autumn.unlimited` | Whether usage is unlimited | `false` | +| `autumn.required_balance` | Required balance for operation | `1` | + +### Track Operation + +| Attribute | Description | Example | +| ------------------------ | ------------------------- | -------------- | +| `autumn.feature_id` | Feature being tracked | `messages` | +| `autumn.event_name` | Custom event name | `message_sent` | +| `autumn.value` | Usage value tracked | `1` | +| `autumn.event_id` | Generated event ID | `evt_123` | +| `autumn.idempotency_key` | Idempotency key for dedup | `msg_456` | + +### Checkout Operation + +| Attribute | Description | Example | +| ----------------------- | ----------------------------------- | --------------------------------- | +| `autumn.product_id` | Product being purchased | `pro` | +| `autumn.product_ids` | Multiple products (comma-separated) | `pro, addon_analytics` | +| `autumn.checkout_url` | Stripe checkout URL | `https://checkout.stripe.com/...` | +| `autumn.has_prorations` | Whether prorations apply | `true` | +| `autumn.total_amount` | Total checkout amount | `2000` (cents) | +| `autumn.currency` | Currency code | `usd` | +| `autumn.force_checkout` | Whether to force Stripe checkout | `false` | +| `autumn.invoice` | Whether to create invoice | `true` | + +### Attach Operation + +| Attribute | Description | Example | +| --------------------- | ------------------------------ | --------------------------------- | +| `autumn.product_id` | Product being attached | `pro` | +| `autumn.success` | Whether attachment succeeded | `true` | +| `autumn.checkout_url` | Checkout URL if payment needed | `https://checkout.stripe.com/...` | + +### Cancel Operation + +| Attribute | Description | Example | +| ------------------- | ------------------------------ | ------- | +| `autumn.product_id` | Product being cancelled | `pro` | +| `autumn.success` | Whether cancellation succeeded | `true` | + +## Configuration + +You can optionally configure the instrumentation: + +```ts +import { instrumentAutumn } from "@kubiks/otel-autumn"; + +const autumn = instrumentAutumn(client, { + // Capture customer data in spans (default: false) + captureCustomerData: true, + + // Capture product options/configuration (default: false) + captureOptions: true, +}); +``` + +## Usage Examples + +### Feature Access Control + +```ts +const autumn = instrumentAutumn( + new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY! }), +); + +// Check if user can access a feature +const result = await autumn.check({ + customer_id: "user_123", + feature_id: "messages", + required_balance: 1, +}); + +if (result.data?.allowed) { + // User has access + console.log(`Remaining: ${result.data.balance}`); +} +``` + +### Usage Tracking + +```ts +// Track feature usage +await autumn.track({ + customer_id: "user_123", + feature_id: "messages", + value: 1, + idempotency_key: `msg_${messageId}`, // Prevent double-counting +}); +``` + +### Checkout Flow + +```ts +// Create a checkout session for a product +const result = await autumn.checkout({ + customer_id: "user_123", + product_id: "pro", + force_checkout: false, // Use billing portal if payment method exists +}); + +if (result.data?.url) { + // Redirect to Stripe checkout + console.log(`Checkout URL: ${result.data.url}`); +} +``` + +### Product Management + +```ts +// Attach a free product +const attachResult = await autumn.attach({ + customer_id: "user_123", + product_id: "free", +}); + +// Cancel a subscription +const cancelResult = await autumn.cancel({ + customer_id: "user_123", + product_id: "pro", +}); +``` + +## License + +MIT diff --git a/packages/otel-autumn/package.json b/packages/otel-autumn/package.json new file mode 100644 index 0000000..423e954 --- /dev/null +++ b/packages/otel-autumn/package.json @@ -0,0 +1,53 @@ +{ + "name": "@kubiks/otel-autumn", + "version": "1.0.0", + "private": false, + "publishConfig": { + "access": "public" + }, + "description": "OpenTelemetry instrumentation for the Autumn billing SDK", + "author": "Kubiks", + "license": "MIT", + "repository": "kubiks-inc/otel", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "scripts": { + "build": "pnpm clean && tsc", + "clean": "rimraf dist", + "prepublishOnly": "pnpm build", + "type-check": "tsc --noEmit", + "unit-test": "vitest --run", + "unit-test-watch": "vitest" + }, + "dependencies": {}, + "devDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-trace-base": "^2.1.0", + "@types/node": "18.15.11", + "autumn-js": "^0.1.40", + "rimraf": "3.0.2", + "typescript": "^5", + "vitest": "0.33.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <2.0.0", + "autumn-js": ">=0.1.0" + } +} diff --git a/packages/otel-autumn/src/index.test.ts b/packages/otel-autumn/src/index.test.ts new file mode 100644 index 0000000..49aa557 --- /dev/null +++ b/packages/otel-autumn/src/index.test.ts @@ -0,0 +1,204 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { trace, type Tracer, type Span } from "@opentelemetry/api"; +import { instrumentAutumn } from "./index"; +import type { Autumn } from "autumn-js"; + +describe("instrumentAutumn", () => { + let mockClient: Autumn; + let mockTracer: Tracer; + let mockSpan: Span; + + beforeEach(() => { + // Mock span + mockSpan = { + setAttributes: vi.fn(), + setAttribute: vi.fn(), + setStatus: vi.fn(), + end: vi.fn(), + recordException: vi.fn(), + } as any; + + // Mock tracer + mockTracer = { + startSpan: vi.fn().mockReturnValue(mockSpan), + } as any; + + // Mock trace.getTracer + vi.spyOn(trace, "getTracer").mockReturnValue(mockTracer); + + // Mock Autumn client + mockClient = { + check: vi.fn().mockResolvedValue({ data: { allowed: true, balance: 5 } }), + track: vi.fn().mockResolvedValue({ data: { id: "evt_123" } }), + checkout: vi.fn().mockResolvedValue({ data: { url: "https://checkout.stripe.com" } }), + attach: vi.fn().mockResolvedValue({ data: { success: true } }), + cancel: vi.fn().mockResolvedValue({ data: { success: true } }), + } as any; + }); + + it("should not instrument the same client twice", () => { + const instrumented1 = instrumentAutumn(mockClient); + const instrumented2 = instrumentAutumn(instrumented1); + + expect(instrumented1).toBe(instrumented2); + }); + + describe("check method", () => { + it("should create a span for check operations", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.check({ + customer_id: "user_123", + feature_id: "messages", + }); + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + "autumn.check", + expect.objectContaining({ kind: expect.any(Number) }) + ); + }); + + it("should set span attributes for check", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.check({ + customer_id: "user_123", + feature_id: "messages", + required_balance: 1, + }); + + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.customer_id", "user_123"); + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.feature_id", "messages"); + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.required_balance", 1); + }); + + it("should handle check errors", async () => { + mockClient.check = vi.fn().mockRejectedValue(new Error("API Error")); + const instrumented = instrumentAutumn(mockClient); + + await expect( + instrumented.check({ customer_id: "user_123", feature_id: "messages" }) + ).rejects.toThrow("API Error"); + + expect(mockSpan.recordException).toHaveBeenCalled(); + }); + }); + + describe("track method", () => { + it("should create a span for track operations", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.track({ + customer_id: "user_123", + feature_id: "messages", + value: 1, + }); + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + "autumn.track", + expect.objectContaining({ kind: expect.any(Number) }) + ); + }); + + it("should set span attributes for track", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.track({ + customer_id: "user_123", + feature_id: "messages", + value: 1, + idempotency_key: "msg_456", + }); + + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.customer_id", "user_123"); + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.feature_id", "messages"); + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.value", 1); + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.idempotency_key", "msg_456"); + }); + }); + + describe("checkout method", () => { + it("should create a span for checkout operations", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.checkout({ + customer_id: "user_123", + product_id: "pro", + }); + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + "autumn.checkout", + expect.objectContaining({ kind: expect.any(Number) }) + ); + }); + + it("should set span attributes for checkout", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.checkout({ + customer_id: "user_123", + product_id: "pro", + force_checkout: true, + }); + + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.customer_id", "user_123"); + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.product_id", "pro"); + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.force_checkout", true); + }); + + it("should handle multiple product IDs", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.checkout({ + customer_id: "user_123", + product_ids: ["pro", "addon_analytics"], + }); + + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.product_ids", "pro, addon_analytics"); + }); + }); + + describe("attach method", () => { + it("should create a span for attach operations", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.attach({ + customer_id: "user_123", + product_id: "free", + }); + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + "autumn.attach", + expect.objectContaining({ kind: expect.any(Number) }) + ); + }); + }); + + describe("cancel method", () => { + it("should create a span for cancel operations", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.cancel({ + customer_id: "user_123", + product_id: "pro", + }); + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + "autumn.cancel", + expect.objectContaining({ kind: expect.any(Number) }) + ); + }); + + it("should set span attributes for cancel", async () => { + const instrumented = instrumentAutumn(mockClient); + + await instrumented.cancel({ + customer_id: "user_123", + product_id: "pro", + }); + + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.customer_id", "user_123"); + expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.product_id", "pro"); + }); + }); +}); diff --git a/packages/otel-autumn/src/index.ts b/packages/otel-autumn/src/index.ts new file mode 100644 index 0000000..c1012b8 --- /dev/null +++ b/packages/otel-autumn/src/index.ts @@ -0,0 +1,529 @@ +import { + context, + SpanKind, + SpanStatusCode, + trace, + type Span, +} from "@opentelemetry/api"; +import type { Autumn } from "autumn-js"; + +const DEFAULT_TRACER_NAME = "@kubiks/otel-autumn"; +const INSTRUMENTED_FLAG = Symbol("kubiksOtelAutumnInstrumented"); + +// Semantic attribute constants +export const SEMATTRS_BILLING_SYSTEM = "billing.system" as const; +export const SEMATTRS_BILLING_OPERATION = "billing.operation" as const; +export const SEMATTRS_AUTUMN_RESOURCE = "autumn.resource" as const; +export const SEMATTRS_AUTUMN_TARGET = "autumn.target" as const; + +// Customer attributes +export const SEMATTRS_AUTUMN_CUSTOMER_ID = "autumn.customer_id" as const; +export const SEMATTRS_AUTUMN_ENTITY_ID = "autumn.entity_id" as const; + +// Product attributes +export const SEMATTRS_AUTUMN_PRODUCT_ID = "autumn.product_id" as const; +export const SEMATTRS_AUTUMN_PRODUCT_IDS = "autumn.product_ids" as const; +export const SEMATTRS_AUTUMN_PRODUCT_NAME = "autumn.product_name" as const; +export const SEMATTRS_AUTUMN_PRODUCT_SCENARIO = "autumn.product_scenario" as const; + +// Feature attributes +export const SEMATTRS_AUTUMN_FEATURE_ID = "autumn.feature_id" as const; +export const SEMATTRS_AUTUMN_FEATURE_NAME = "autumn.feature_name" as const; +export const SEMATTRS_AUTUMN_ALLOWED = "autumn.allowed" as const; +export const SEMATTRS_AUTUMN_BALANCE = "autumn.balance" as const; +export const SEMATTRS_AUTUMN_USAGE = "autumn.usage" as const; +export const SEMATTRS_AUTUMN_INCLUDED_USAGE = "autumn.included_usage" as const; +export const SEMATTRS_AUTUMN_UNLIMITED = "autumn.unlimited" as const; +export const SEMATTRS_AUTUMN_REQUIRED_BALANCE = "autumn.required_balance" as const; + +// Checkout attributes +export const SEMATTRS_AUTUMN_CHECKOUT_URL = "autumn.checkout_url" as const; +export const SEMATTRS_AUTUMN_HAS_PRORATIONS = "autumn.has_prorations" as const; +export const SEMATTRS_AUTUMN_TOTAL_AMOUNT = "autumn.total_amount" as const; +export const SEMATTRS_AUTUMN_CURRENCY = "autumn.currency" as const; +export const SEMATTRS_AUTUMN_FORCE_CHECKOUT = "autumn.force_checkout" as const; +export const SEMATTRS_AUTUMN_INVOICE = "autumn.invoice" as const; + +// Track attributes +export const SEMATTRS_AUTUMN_EVENT_NAME = "autumn.event_name" as const; +export const SEMATTRS_AUTUMN_VALUE = "autumn.value" as const; +export const SEMATTRS_AUTUMN_EVENT_ID = "autumn.event_id" as const; +export const SEMATTRS_AUTUMN_IDEMPOTENCY_KEY = "autumn.idempotency_key" as const; + +// Attach/Cancel attributes +export const SEMATTRS_AUTUMN_SUCCESS = "autumn.success" as const; + +export interface InstrumentationConfig { + /** + * Whether to capture customer data in spans. + * @default false + */ + captureCustomerData?: boolean; + + /** + * Whether to capture product options/configuration in spans. + * @default false + */ + captureOptions?: boolean; +} + +interface InstrumentedAutumn extends Autumn { + [INSTRUMENTED_FLAG]?: true; +} + +function finalizeSpan(span: Span, error?: unknown): void { + if (error) { + if (error instanceof Error) { + span.recordException(error); + } else { + span.recordException(new Error(String(error))); + } + span.setStatus({ code: SpanStatusCode.ERROR }); + } else { + span.setStatus({ code: SpanStatusCode.OK }); + } + span.end(); +} + +function annotateCheckSpan( + span: Span, + params: { + customer_id: string; + feature_id?: string; + product_id?: string; + entity_id?: string; + required_balance?: number; + }, +): void { + span.setAttributes({ + [SEMATTRS_BILLING_SYSTEM]: "autumn", + [SEMATTRS_BILLING_OPERATION]: "check", + [SEMATTRS_AUTUMN_RESOURCE]: params.feature_id ? "features" : "products", + [SEMATTRS_AUTUMN_TARGET]: params.feature_id + ? "features.check" + : "products.check", + }); + + span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id); + + if (params.feature_id) { + span.setAttribute(SEMATTRS_AUTUMN_FEATURE_ID, params.feature_id); + } + + if (params.product_id) { + span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_ID, params.product_id); + } + + if (params.entity_id) { + span.setAttribute(SEMATTRS_AUTUMN_ENTITY_ID, params.entity_id); + } + + if (typeof params.required_balance === "number") { + span.setAttribute(SEMATTRS_AUTUMN_REQUIRED_BALANCE, params.required_balance); + } +} + +function annotateCheckResponse(span: Span, response: Record): void { + if (typeof response.allowed === "boolean") { + span.setAttribute(SEMATTRS_AUTUMN_ALLOWED, response.allowed); + } + + if (typeof response.balance === "number") { + span.setAttribute(SEMATTRS_AUTUMN_BALANCE, response.balance); + } + + if (typeof response.usage === "number") { + span.setAttribute(SEMATTRS_AUTUMN_USAGE, response.usage); + } + + if (typeof response.included_usage === "number") { + span.setAttribute(SEMATTRS_AUTUMN_INCLUDED_USAGE, response.included_usage); + } + + if (typeof response.unlimited === "boolean") { + span.setAttribute(SEMATTRS_AUTUMN_UNLIMITED, response.unlimited); + } + + if (typeof response.status === "string") { + span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_SCENARIO, response.status); + } +} + +function annotateTrackSpan( + span: Span, + params: { + customer_id: string; + feature_id?: string; + event_name?: string; + value?: number; + entity_id?: string; + idempotency_key?: string; + }, +): void { + span.setAttributes({ + [SEMATTRS_BILLING_SYSTEM]: "autumn", + [SEMATTRS_BILLING_OPERATION]: "track", + [SEMATTRS_AUTUMN_RESOURCE]: "events", + [SEMATTRS_AUTUMN_TARGET]: "events.track", + }); + + span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id); + + if (params.feature_id) { + span.setAttribute(SEMATTRS_AUTUMN_FEATURE_ID, params.feature_id); + } + + if (params.event_name) { + span.setAttribute(SEMATTRS_AUTUMN_EVENT_NAME, params.event_name); + } + + if (typeof params.value === "number") { + span.setAttribute(SEMATTRS_AUTUMN_VALUE, params.value); + } + + if (params.entity_id) { + span.setAttribute(SEMATTRS_AUTUMN_ENTITY_ID, params.entity_id); + } + + if (params.idempotency_key) { + span.setAttribute(SEMATTRS_AUTUMN_IDEMPOTENCY_KEY, params.idempotency_key); + } +} + +function annotateTrackResponse(span: Span, response: { id?: string }): void { + if (response.id) { + span.setAttribute(SEMATTRS_AUTUMN_EVENT_ID, response.id); + } +} + +function annotateCheckoutSpan( + span: Span, + params: { + customer_id: string; + product_id?: string; + product_ids?: string[]; + entity_id?: string; + force_checkout?: boolean; + invoice?: boolean; + }, + config?: InstrumentationConfig, +): void { + span.setAttributes({ + [SEMATTRS_BILLING_SYSTEM]: "autumn", + [SEMATTRS_BILLING_OPERATION]: "checkout", + [SEMATTRS_AUTUMN_RESOURCE]: "checkout", + [SEMATTRS_AUTUMN_TARGET]: "checkout.create", + }); + + span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id); + + if (params.product_id) { + span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_ID, params.product_id); + } + + if (params.product_ids && params.product_ids.length > 0) { + span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_IDS, params.product_ids.join(", ")); + } + + if (params.entity_id) { + span.setAttribute(SEMATTRS_AUTUMN_ENTITY_ID, params.entity_id); + } + + if (typeof params.force_checkout === "boolean") { + span.setAttribute(SEMATTRS_AUTUMN_FORCE_CHECKOUT, params.force_checkout); + } + + if (typeof params.invoice === "boolean") { + span.setAttribute(SEMATTRS_AUTUMN_INVOICE, params.invoice); + } +} + +function annotateCheckoutResponse( + span: Span, + response: { + url?: string; + has_prorations?: boolean; + total?: number; + currency?: string; + }, +): void { + if (response.url) { + span.setAttribute(SEMATTRS_AUTUMN_CHECKOUT_URL, response.url); + } + + if (typeof response.has_prorations === "boolean") { + span.setAttribute(SEMATTRS_AUTUMN_HAS_PRORATIONS, response.has_prorations); + } + + if (typeof response.total === "number") { + span.setAttribute(SEMATTRS_AUTUMN_TOTAL_AMOUNT, response.total); + } + + if (response.currency) { + span.setAttribute(SEMATTRS_AUTUMN_CURRENCY, response.currency); + } +} + +function annotateAttachSpan( + span: Span, + params: { + customer_id: string; + product_id: string; + entity_id?: string; + }, +): void { + span.setAttributes({ + [SEMATTRS_BILLING_SYSTEM]: "autumn", + [SEMATTRS_BILLING_OPERATION]: "attach", + [SEMATTRS_AUTUMN_RESOURCE]: "products", + [SEMATTRS_AUTUMN_TARGET]: "products.attach", + }); + + span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id); + span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_ID, params.product_id); + + if (params.entity_id) { + span.setAttribute(SEMATTRS_AUTUMN_ENTITY_ID, params.entity_id); + } +} + +function annotateAttachResponse( + span: Span, + response: { + success?: boolean; + checkout_url?: string; + }, +): void { + if (typeof response.success === "boolean") { + span.setAttribute(SEMATTRS_AUTUMN_SUCCESS, response.success); + } + + if (response.checkout_url) { + span.setAttribute(SEMATTRS_AUTUMN_CHECKOUT_URL, response.checkout_url); + } +} + +function annotateCancelSpan( + span: Span, + params: { + customer_id: string; + product_id: string; + }, +): void { + span.setAttributes({ + [SEMATTRS_BILLING_SYSTEM]: "autumn", + [SEMATTRS_BILLING_OPERATION]: "cancel", + [SEMATTRS_AUTUMN_RESOURCE]: "products", + [SEMATTRS_AUTUMN_TARGET]: "products.cancel", + }); + + span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id); + span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_ID, params.product_id); +} + +function annotateCancelResponse(span: Span, response: { success?: boolean }): void { + if (typeof response.success === "boolean") { + span.setAttribute(SEMATTRS_AUTUMN_SUCCESS, response.success); + } +} + +export function instrumentAutumn( + client: Autumn, + config?: InstrumentationConfig, +): Autumn { + // Check if already instrumented + if ((client as InstrumentedAutumn)[INSTRUMENTED_FLAG]) { + return client; + } + + const tracer = trace.getTracer(DEFAULT_TRACER_NAME); + + // Instrument check method + const originalCheck = client.check.bind(client); + const instrumentedCheck = async function instrumentedCheck( + params: Parameters[0], + ): Promise> { + const span = tracer.startSpan("autumn.check", { + kind: SpanKind.CLIENT, + }); + + annotateCheckSpan(span, params as { + customer_id: string; + feature_id?: string; + product_id?: string; + entity_id?: string; + required_balance?: number; + }); + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalCheck(params), + ); + + if (result.data) { + annotateCheckResponse(span, result.data as Record); + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + + // Instrument track method + const originalTrack = client.track.bind(client); + const instrumentedTrack = async function instrumentedTrack( + params: Parameters[0], + ): Promise> { + const span = tracer.startSpan("autumn.track", { + kind: SpanKind.CLIENT, + }); + + annotateTrackSpan(span, params as { + customer_id: string; + feature_id?: string; + event_name?: string; + value?: number; + entity_id?: string; + idempotency_key?: string; + }); + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalTrack(params), + ); + + if (result.data) { + annotateTrackResponse(span, result.data); + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + + // Instrument checkout method + const originalCheckout = client.checkout.bind(client); + const instrumentedCheckout = async function instrumentedCheckout( + params: Parameters[0], + ): Promise> { + const span = tracer.startSpan("autumn.checkout", { + kind: SpanKind.CLIENT, + }); + + annotateCheckoutSpan(span, params as { + customer_id: string; + product_id?: string; + product_ids?: string[]; + entity_id?: string; + force_checkout?: boolean; + invoice?: boolean; + }, config); + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalCheckout(params), + ); + + if (result.data) { + annotateCheckoutResponse(span, result.data); + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + + // Instrument attach method + const originalAttach = client.attach.bind(client); + const instrumentedAttach = async function instrumentedAttach( + params: Parameters[0], + ): Promise> { + const span = tracer.startSpan("autumn.attach", { + kind: SpanKind.CLIENT, + }); + + annotateAttachSpan(span, params as { + customer_id: string; + product_id: string; + entity_id?: string; + }); + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalAttach(params), + ); + + if (result.data) { + annotateAttachResponse(span, result.data); + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + + // Instrument cancel method + const originalCancel = client.cancel.bind(client); + const instrumentedCancel = async function instrumentedCancel( + params: Parameters[0], + ): Promise> { + const span = tracer.startSpan("autumn.cancel", { + kind: SpanKind.CLIENT, + }); + + annotateCancelSpan(span, params as { + customer_id: string; + product_id: string; + }); + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalCancel(params), + ); + + if (result.data) { + annotateCancelResponse(span, result.data); + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + + // Replace methods with instrumented versions + client.check = instrumentedCheck; + client.track = instrumentedTrack; + client.checkout = instrumentedCheckout; + client.attach = instrumentedAttach; + client.cancel = instrumentedCancel; + + // Mark as instrumented + (client as InstrumentedAutumn)[INSTRUMENTED_FLAG] = true; + + return client; +} diff --git a/packages/otel-autumn/tsconfig.json b/packages/otel-autumn/tsconfig.json new file mode 100644 index 0000000..47fac92 --- /dev/null +++ b/packages/otel-autumn/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declarationDir": "dist/types", + "stripInternal": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f029d39..c57fe6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,30 @@ importers: specifier: ^1.11.3 version: 1.11.3 + packages/otel-autumn: + devDependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/sdk-trace-base': + specifier: ^2.1.0 + version: 2.1.0(@opentelemetry/api@1.9.0) + '@types/node': + specifier: 18.15.11 + version: 18.15.11 + autumn-js: + specifier: ^0.1.40 + version: 0.1.40(better-auth@1.3.25(react@18.2.0))(better-call@1.0.19)(convex@1.27.4(react@18.2.0))(react@18.2.0) + rimraf: + specifier: 3.0.2 + version: 3.0.2 + typescript: + specifier: ^5 + version: 5.3.3 + vitest: + specifier: 0.33.0 + version: 0.33.0(less@4.2.0)(sass@1.69.7)(stylus@0.59.0) + packages/otel-better-auth: devDependencies: '@opentelemetry/api': @@ -37,7 +61,7 @@ importers: version: 18.15.11 better-auth: specifier: ^1.0.0 - version: 1.3.25 + version: 1.3.25(react@18.2.0) rimraf: specifier: 3.0.2 version: 3.0.2 @@ -217,138 +241,288 @@ packages: '@changesets/write@0.3.0': resolution: {integrity: sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==} + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -652,6 +826,21 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + autumn-js@0.1.40: + resolution: {integrity: sha512-nAmyFJLOQqKosb8MHv09rB2pma8LyOHWsuYtrjXND+2LM51vToco1mweLIYIs/aX33iLAVUxfpXEEt8P3UYoxw==} + peerDependencies: + better-auth: ^1.3.17 + better-call: ^1.0.12 + convex: ^1.25.4 + react: '*' + peerDependenciesMeta: + better-auth: + optional: true + better-call: + optional: true + react: + optional: true + available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -787,6 +976,22 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + convex@1.27.4: + resolution: {integrity: sha512-aPP3uxOF5v+K4uftXxRh8GAYepsjsFgU+S9IpAyLVNaFU3Z72WB1rIhaSzPAo4Q0TJWsOKANFGU903IU92QDTA==} + engines: {node: '>=18.0.0', npm: '>=7.0.0'} + hasBin: true + peerDependencies: + '@auth0/auth0-react': ^2.0.1 + '@clerk/clerk-react': ^4.12.8 || ^5.0.0 + react: ^18.0.0 || ^19.0.0-0 || ^19.0.0 + peerDependenciesMeta: + '@auth0/auth0-react': + optional: true + '@clerk/clerk-react': + optional: true + react: + optional: true + copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} @@ -836,6 +1041,10 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} @@ -858,6 +1067,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1028,6 +1241,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -1062,6 +1280,10 @@ packages: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1713,6 +1935,10 @@ packages: resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} engines: {node: '>=6.0.0'} + query-string@9.3.1: + resolution: {integrity: sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==} + engines: {node: '>=18'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1801,6 +2027,9 @@ packages: rou3@0.5.1: resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + rou3@0.6.3: + resolution: {integrity: sha512-1HSG1ENTj7Kkm5muMnXuzzfdDOf7CFnbSYFA+H3Fp/rB9lOCxCPgy1jlZxTKyFoC5jJay8Mmc+VbPLYRjzYLrA==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1919,6 +2148,10 @@ packages: spdx-license-ids@3.0.16: resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} + split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -1984,6 +2217,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swr@2.3.6: + resolution: {integrity: sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -2112,6 +2350,11 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -2471,72 +2714,147 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@esbuild/aix-ppc64@0.25.4': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true + '@esbuild/android-arm64@0.25.4': + optional: true + '@esbuild/android-arm@0.18.20': optional: true + '@esbuild/android-arm@0.25.4': + optional: true + '@esbuild/android-x64@0.18.20': optional: true + '@esbuild/android-x64@0.25.4': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true + '@esbuild/darwin-arm64@0.25.4': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true + '@esbuild/darwin-x64@0.25.4': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true + '@esbuild/freebsd-arm64@0.25.4': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true + '@esbuild/freebsd-x64@0.25.4': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true + '@esbuild/linux-arm64@0.25.4': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true + '@esbuild/linux-arm@0.25.4': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true + '@esbuild/linux-ia32@0.25.4': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true + '@esbuild/linux-loong64@0.25.4': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true + '@esbuild/linux-mips64el@0.25.4': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true + '@esbuild/linux-ppc64@0.25.4': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true + '@esbuild/linux-riscv64@0.25.4': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true + '@esbuild/linux-s390x@0.25.4': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true + '@esbuild/linux-x64@0.25.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.4': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true + '@esbuild/netbsd-x64@0.25.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.4': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true + '@esbuild/openbsd-x64@0.25.4': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true + '@esbuild/sunos-x64@0.25.4': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true + '@esbuild/win32-arm64@0.25.4': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true + '@esbuild/win32-ia32@0.25.4': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true + '@esbuild/win32-x64@0.25.4': + optional: true + '@hexagon/base64@1.1.28': {} '@isaacs/cliui@8.0.2': @@ -2920,11 +3238,23 @@ snapshots: assertion-error@1.1.0: {} + autumn-js@0.1.40(better-auth@1.3.25(react@18.2.0))(better-call@1.0.19)(convex@1.27.4(react@18.2.0))(react@18.2.0): + dependencies: + convex: 1.27.4(react@18.2.0) + query-string: 9.3.1 + rou3: 0.6.3 + swr: 2.3.6(react@18.2.0) + zod: 4.1.11 + optionalDependencies: + better-auth: 1.3.25(react@18.2.0) + better-call: 1.0.19 + react: 18.2.0 + available-typed-arrays@1.0.5: {} balanced-match@1.0.2: {} - better-auth@1.3.25: + better-auth@1.3.25(react@18.2.0): dependencies: '@better-auth/core': 1.3.25 '@better-auth/utils': 0.3.0 @@ -2939,6 +3269,8 @@ snapshots: kysely: 0.28.7 nanostores: 1.0.1 zod: 4.1.11 + optionalDependencies: + react: 18.2.0 better-call@1.0.19: dependencies: @@ -3065,6 +3397,13 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + convex@1.27.4(react@18.2.0): + dependencies: + esbuild: 0.25.4 + prettier: 3.1.1 + optionalDependencies: + react: 18.2.0 + copy-anything@2.0.6: dependencies: is-what: 3.14.1 @@ -3113,6 +3452,8 @@ snapshots: decamelize@1.2.0: {} + decode-uri-component@0.4.1: {} + deep-eql@4.1.3: dependencies: type-detect: 4.0.8 @@ -3137,6 +3478,8 @@ snapshots: defu@6.1.4: {} + dequal@2.0.3: {} + detect-indent@6.1.0: {} diff-sequences@29.6.3: {} @@ -3286,6 +3629,34 @@ snapshots: '@esbuild/win32-ia32': 0.18.20 '@esbuild/win32-x64': 0.18.20 + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + escalade@3.1.1: {} escape-string-regexp@1.0.5: {} @@ -3318,6 +3689,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + filter-obj@5.1.0: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -3954,6 +4327,12 @@ snapshots: pvutils@1.1.3: {} + query-string@9.3.1: + dependencies: + decode-uri-component: 0.4.1 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + queue-microtask@1.2.3: {} quick-lru@4.0.1: {} @@ -4045,6 +4424,8 @@ snapshots: rou3@0.5.1: {} + rou3@0.6.3: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4170,6 +4551,8 @@ snapshots: spdx-license-ids@3.0.16: {} + split-on-first@3.0.0: {} + sprintf-js@1.0.3: {} stackback@0.0.2: {} @@ -4249,6 +4632,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swr@2.3.6(react@18.2.0): + dependencies: + dequal: 2.0.3 + react: 18.2.0 + use-sync-external-store: 1.6.0(react@18.2.0) + term-size@2.2.1: {} tinybench@2.5.1: {} @@ -4364,6 +4753,10 @@ snapshots: universalify@0.1.2: {} + use-sync-external-store@1.6.0(react@18.2.0): + dependencies: + react: 18.2.0 + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0