chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import type { CreatePurchaseParams, CreatePurchaseResponse } from '@/tools/revenuecat/types'
|
||||
import {
|
||||
extractCustomer,
|
||||
extractSubscriber,
|
||||
SUBSCRIBER_OUTPUT,
|
||||
shapeSubscriber,
|
||||
throwIfRevenueCatError,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatCreatePurchaseTool: ToolConfig<
|
||||
CreatePurchaseParams,
|
||||
CreatePurchaseResponse
|
||||
> = {
|
||||
id: 'revenuecat_create_purchase',
|
||||
name: 'RevenueCat Create Purchase',
|
||||
description: 'Record a purchase (receipt) for a subscriber via the REST API',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat API key (public or secret)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber',
|
||||
},
|
||||
fetchToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'For iOS, the base64-encoded receipt (or JWSTransaction for StoreKit2); for Android the purchase token; for Amazon the receipt; for Stripe the subscription ID or Checkout Session ID; for Roku the transaction ID; for Paddle the subscription ID or transaction ID',
|
||||
},
|
||||
productId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Apple, Google, Amazon, Roku, or Paddle product identifier or SKU. Required for Google.',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Price of the product. Required if you provide a currency.',
|
||||
},
|
||||
currency: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 4217 currency code (e.g., USD, EUR). Required if you provide a price.',
|
||||
},
|
||||
isRestore: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deprecated. Triggers configured restore behavior for shared fetch tokens.',
|
||||
},
|
||||
presentedOfferingIdentifier: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Identifier of the offering presented to the customer at the time of purchase. Attached to new transactions in this fetch token and exposed in ETL exports and webhooks.',
|
||||
},
|
||||
paymentMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Payment mode for the introductory period. One of: pay_as_you_go, pay_up_front, free_trial. Defaults to free_trial when an introductory period is detected and no value is provided.',
|
||||
},
|
||||
introductoryPrice: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Introductory price paid (if any).',
|
||||
},
|
||||
attributes: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON object of subscriber attributes to set alongside the purchase. Each key maps to {"value": string, "updated_at_ms": number}.',
|
||||
},
|
||||
updatedAtMs: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'UNIX epoch in milliseconds used to resolve attribute conflicts at the request level.',
|
||||
},
|
||||
platform: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Platform of the purchase. One of: ios, android, amazon, macos, uikitformac, stripe, roku, paddle. Sent as the X-Platform header (required by RevenueCat).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.revenuecat.com/v1/receipts',
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (params.platform) {
|
||||
headers['X-Platform'] = params.platform
|
||||
}
|
||||
return headers
|
||||
},
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
app_user_id: params.appUserId,
|
||||
fetch_token: params.fetchToken,
|
||||
}
|
||||
if (params.productId) body.product_id = params.productId
|
||||
if (params.price !== undefined) body.price = params.price
|
||||
if (params.currency) body.currency = params.currency
|
||||
if (params.isRestore !== undefined) body.is_restore = params.isRestore
|
||||
if (params.presentedOfferingIdentifier) {
|
||||
body.presented_offering_identifier = params.presentedOfferingIdentifier
|
||||
}
|
||||
if (params.paymentMode) body.payment_mode = params.paymentMode
|
||||
if (params.introductoryPrice !== undefined) {
|
||||
body.introductory_price = params.introductoryPrice
|
||||
}
|
||||
if (params.attributes !== undefined && params.attributes !== '') {
|
||||
if (typeof params.attributes === 'string') {
|
||||
try {
|
||||
body.attributes = JSON.parse(params.attributes)
|
||||
} catch {
|
||||
throw new Error('attributes must be a valid JSON object')
|
||||
}
|
||||
} else {
|
||||
body.attributes = params.attributes
|
||||
}
|
||||
}
|
||||
if (params.updatedAtMs !== undefined) body.updated_at_ms = params.updatedAtMs
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
customer: extractCustomer(data),
|
||||
subscriber: shapeSubscriber(extractSubscriber(data)),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
customer: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Customer object returned at the top level of POST /v1/receipts (first_seen, last_seen, original_app_user_id, original_application_version, original_sdk_version, management_url, entitlements, original_purchase_date, request_date). Null when the response uses the `value`-wrapped envelope.',
|
||||
optional: true,
|
||||
},
|
||||
subscriber: {
|
||||
...SUBSCRIBER_OUTPUT,
|
||||
description: 'The updated subscriber object after recording the purchase',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type {
|
||||
DeferGoogleSubscriptionParams,
|
||||
DeferGoogleSubscriptionResponse,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import {
|
||||
extractSubscriber,
|
||||
SUBSCRIBER_OUTPUT,
|
||||
shapeSubscriber,
|
||||
throwIfRevenueCatError,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatDeferGoogleSubscriptionTool: ToolConfig<
|
||||
DeferGoogleSubscriptionParams,
|
||||
DeferGoogleSubscriptionResponse
|
||||
> = {
|
||||
id: 'revenuecat_defer_google_subscription',
|
||||
name: 'RevenueCat Defer Google Subscription',
|
||||
description:
|
||||
'Defer a Google Play subscription by extending its billing date by a number of days (Google Play only)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat secret API key (sk_...)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber',
|
||||
},
|
||||
productId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The Google Play product identifier of the subscription to defer (use the part before the colon for products set up after Feb 2023)',
|
||||
},
|
||||
extendByDays: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Number of days to extend the subscription by (1-365). Provide either extendByDays or expiryTimeMs.',
|
||||
},
|
||||
expiryTimeMs: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Absolute new expiry time in milliseconds since Unix epoch. Use instead of extendByDays to set an exact expiry.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/subscriptions/${encodeURIComponent(params.productId.trim())}/defer`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const hasExtend = params.extendByDays !== undefined && (params.extendByDays as unknown) !== ''
|
||||
const hasExpiry = params.expiryTimeMs !== undefined && (params.expiryTimeMs as unknown) !== ''
|
||||
if (!hasExtend && !hasExpiry) {
|
||||
throw new Error('Provide either extendByDays or expiryTimeMs to defer a subscription')
|
||||
}
|
||||
if (hasExtend && hasExpiry) {
|
||||
throw new Error(
|
||||
'Provide only one of extendByDays or expiryTimeMs — they cannot be used together'
|
||||
)
|
||||
}
|
||||
const body: Record<string, unknown> = {}
|
||||
if (hasExpiry) body.expiry_time_ms = params.expiryTimeMs
|
||||
else if (hasExtend) {
|
||||
const days = params.extendByDays as number
|
||||
if (!Number.isInteger(days) || days < 1 || days > 365) {
|
||||
throw new Error('extendByDays must be an integer between 1 and 365')
|
||||
}
|
||||
body.extend_by_days = days
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
subscriber: shapeSubscriber(extractSubscriber(data)),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
subscriber: {
|
||||
...SUBSCRIBER_OUTPUT,
|
||||
description: 'The updated subscriber object after deferring the Google subscription',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { DeleteCustomerParams, DeleteCustomerResponse } from '@/tools/revenuecat/types'
|
||||
import { DELETE_OUTPUT_PROPERTIES, throwIfRevenueCatError } from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatDeleteCustomerTool: ToolConfig<
|
||||
DeleteCustomerParams,
|
||||
DeleteCustomerResponse
|
||||
> = {
|
||||
id: 'revenuecat_delete_customer',
|
||||
name: 'RevenueCat Delete Customer',
|
||||
description: 'Permanently delete a subscriber and all associated data',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat secret API key (sk_...)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
let body: Record<string, unknown> = {}
|
||||
try {
|
||||
body = await response.json()
|
||||
} catch {
|
||||
// Some delete responses have empty bodies — treat as success
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: typeof body.deleted === 'boolean' ? body.deleted : true,
|
||||
app_user_id:
|
||||
typeof body.app_user_id === 'string' ? body.app_user_id : (params?.appUserId ?? ''),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: DELETE_OUTPUT_PROPERTIES.deleted,
|
||||
app_user_id: DELETE_OUTPUT_PROPERTIES.app_user_id,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { CustomerResponse, GetCustomerParams } from '@/tools/revenuecat/types'
|
||||
import {
|
||||
extractSubscriber,
|
||||
METADATA_OUTPUT_PROPERTIES,
|
||||
SUBSCRIBER_OUTPUT,
|
||||
shapeSubscriber,
|
||||
throwIfRevenueCatError,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatGetCustomerTool: ToolConfig<GetCustomerParams, CustomerResponse> = {
|
||||
id: 'revenuecat_get_customer',
|
||||
name: 'RevenueCat Get Customer',
|
||||
description: 'Retrieve subscriber information by app user ID',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat secret API key (sk_...)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
const data = await response.json()
|
||||
const subscriberRaw = extractSubscriber(data)
|
||||
const subscriber = shapeSubscriber(subscriberRaw)
|
||||
const requestDate = (data?.value?.request_date ?? data?.request_date) as string | undefined
|
||||
const parsed = requestDate ? new Date(requestDate).getTime() : Number.NaN
|
||||
const now = Number.isFinite(parsed) ? parsed : Date.now()
|
||||
|
||||
const isActiveByDates = (
|
||||
expires: string | null | undefined,
|
||||
grace: string | null | undefined,
|
||||
refundedAt?: string | null | undefined
|
||||
) => {
|
||||
if (refundedAt) return false
|
||||
if (!expires) return true
|
||||
if (new Date(expires).getTime() > now) return true
|
||||
if (grace && new Date(grace).getTime() > now) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const activeEntitlements = Object.values(subscriber.entitlements).filter((e) => {
|
||||
const ent = e as Record<string, unknown>
|
||||
return isActiveByDates(
|
||||
ent.expires_date as string | null | undefined,
|
||||
ent.grace_period_expires_date as string | null | undefined
|
||||
)
|
||||
}).length
|
||||
|
||||
const activeSubscriptions = Object.values(subscriber.subscriptions).filter((s) => {
|
||||
const sub = s as Record<string, unknown>
|
||||
return isActiveByDates(
|
||||
sub.expires_date as string | null | undefined,
|
||||
sub.grace_period_expires_date as string | null | undefined,
|
||||
sub.refunded_at as string | null | undefined
|
||||
)
|
||||
}).length
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
subscriber,
|
||||
metadata: {
|
||||
app_user_id: subscriber.original_app_user_id,
|
||||
first_seen: subscriber.first_seen,
|
||||
active_entitlements: activeEntitlements,
|
||||
active_subscriptions: activeSubscriptions,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
subscriber: {
|
||||
...SUBSCRIBER_OUTPUT,
|
||||
description: 'The subscriber object with subscriptions and entitlements',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Subscriber summary metadata',
|
||||
properties: METADATA_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { GrantEntitlementParams, GrantEntitlementResponse } from '@/tools/revenuecat/types'
|
||||
import {
|
||||
extractSubscriber,
|
||||
SUBSCRIBER_OUTPUT,
|
||||
shapeSubscriber,
|
||||
throwIfRevenueCatError,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatGrantEntitlementTool: ToolConfig<
|
||||
GrantEntitlementParams,
|
||||
GrantEntitlementResponse
|
||||
> = {
|
||||
id: 'revenuecat_grant_entitlement',
|
||||
name: 'RevenueCat Grant Entitlement',
|
||||
description: 'Grant a promotional entitlement to a subscriber',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat secret API key (sk_...)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber',
|
||||
},
|
||||
entitlementIdentifier: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The entitlement identifier to grant',
|
||||
},
|
||||
duration: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Deprecated. Duration of the entitlement. Provide either duration or endTimeMs (endTimeMs preferred). One of: daily, three_day, weekly, two_week, monthly, two_month, three_month, six_month, yearly, lifetime',
|
||||
},
|
||||
endTimeMs: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Absolute end time in milliseconds since Unix epoch. Use instead of duration to grant the entitlement until a specific timestamp.',
|
||||
},
|
||||
startTimeMs: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Deprecated. Optional start time in milliseconds since Unix epoch, used with duration to determine expiration. Regardless of value, the entitlement is always granted immediately.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/entitlements/${encodeURIComponent(params.entitlementIdentifier.trim())}/promotional`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const hasEnd = params.endTimeMs !== undefined && (params.endTimeMs as unknown) !== ''
|
||||
const hasDuration = Boolean(params.duration)
|
||||
if (!hasDuration && !hasEnd) {
|
||||
throw new Error('Provide either duration or endTimeMs to grant a promotional entitlement')
|
||||
}
|
||||
if (hasDuration && hasEnd) {
|
||||
throw new Error('Provide only one of duration or endTimeMs — they cannot be used together')
|
||||
}
|
||||
const body: Record<string, unknown> = {}
|
||||
if (hasEnd) body.end_time_ms = params.endTimeMs
|
||||
else if (hasDuration) body.duration = params.duration
|
||||
if (params.startTimeMs !== undefined && (params.startTimeMs as unknown) !== '') {
|
||||
body.start_time_ms = params.startTimeMs
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
subscriber: shapeSubscriber(extractSubscriber(data)),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
subscriber: {
|
||||
...SUBSCRIBER_OUTPUT,
|
||||
description: 'The updated subscriber object after granting the entitlement',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export { revenuecatCreatePurchaseTool } from './create_purchase'
|
||||
export { revenuecatDeferGoogleSubscriptionTool } from './defer_google_subscription'
|
||||
export { revenuecatDeleteCustomerTool } from './delete_customer'
|
||||
export { revenuecatGetCustomerTool } from './get_customer'
|
||||
export { revenuecatGrantEntitlementTool } from './grant_entitlement'
|
||||
export { revenuecatListOfferingsTool } from './list_offerings'
|
||||
export { revenuecatRefundGoogleSubscriptionTool } from './refund_google_subscription'
|
||||
export { revenuecatRevokeEntitlementTool } from './revoke_entitlement'
|
||||
export { revenuecatRevokeGoogleSubscriptionTool } from './revoke_google_subscription'
|
||||
export * from './types'
|
||||
export { revenuecatUpdateSubscriberAttributesTool } from './update_subscriber_attributes'
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { ListOfferingsParams, ListOfferingsResponse } from '@/tools/revenuecat/types'
|
||||
import {
|
||||
OFFERING_OUTPUT_PROPERTIES,
|
||||
OFFERINGS_METADATA_OUTPUT_PROPERTIES,
|
||||
throwIfRevenueCatError,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatListOfferingsTool: ToolConfig<ListOfferingsParams, ListOfferingsResponse> = {
|
||||
id: 'revenuecat_list_offerings',
|
||||
name: 'RevenueCat List Offerings',
|
||||
description: 'List all offerings configured for the project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat API key',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'An app user ID to retrieve offerings for',
|
||||
},
|
||||
platform: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'X-Platform header value. One of: ios, android, amazon, stripe, roku, paddle. Required when using a legacy public API key; ignored with app-specific API keys.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/offerings`,
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (params.platform) {
|
||||
headers['X-Platform'] = params.platform
|
||||
}
|
||||
return headers
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
const raw = await response.json()
|
||||
/**
|
||||
* RevenueCat's offerings endpoint may return the payload wrapped in `{ value: { ... } }`
|
||||
* or unwrapped. Normalize to a single shape.
|
||||
*/
|
||||
const data =
|
||||
raw && typeof raw === 'object' && 'value' in raw && raw.value && typeof raw.value === 'object'
|
||||
? (raw.value as Record<string, unknown>)
|
||||
: (raw as Record<string, unknown>)
|
||||
const offerings = (data.offerings as Array<Record<string, unknown>>) ?? []
|
||||
const currentOfferingId = (data.current_offering_id as string | null) ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
current_offering_id: currentOfferingId,
|
||||
offerings: offerings.map((offering: Record<string, unknown>) => ({
|
||||
identifier: (offering.identifier as string) ?? '',
|
||||
description: (offering.description as string) ?? null,
|
||||
packages: ((offering.packages as Array<Record<string, unknown>>) ?? []).map(
|
||||
(pkg: Record<string, unknown>) => ({
|
||||
identifier: (pkg.identifier as string) ?? '',
|
||||
platform_product_identifier: (pkg.platform_product_identifier as string) ?? null,
|
||||
})
|
||||
),
|
||||
})),
|
||||
metadata: {
|
||||
count: offerings.length,
|
||||
current_offering_id: currentOfferingId,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
current_offering_id: {
|
||||
type: 'string',
|
||||
description: 'The identifier of the current offering',
|
||||
optional: true,
|
||||
},
|
||||
offerings: {
|
||||
type: 'array',
|
||||
description: 'List of offerings',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: OFFERING_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Offerings metadata',
|
||||
properties: OFFERINGS_METADATA_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type {
|
||||
RefundGoogleSubscriptionParams,
|
||||
RefundGoogleSubscriptionResponse,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import {
|
||||
extractSubscriber,
|
||||
SUBSCRIBER_OUTPUT,
|
||||
shapeSubscriber,
|
||||
throwIfRevenueCatError,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatRefundGoogleSubscriptionTool: ToolConfig<
|
||||
RefundGoogleSubscriptionParams,
|
||||
RefundGoogleSubscriptionResponse
|
||||
> = {
|
||||
id: 'revenuecat_refund_google_subscription',
|
||||
name: 'RevenueCat Refund Google Subscription',
|
||||
description:
|
||||
'Refund a specific store transaction by its store transaction identifier and revoke access (subscription or non-subscription, last 365 days)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat secret API key (sk_...)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber',
|
||||
},
|
||||
storeTransactionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The store transaction identifier of the purchase to refund (e.g., GPA.3309-9122-6177-45730 for Google Play)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/transactions/${encodeURIComponent(params.storeTransactionId.trim())}/refund`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
subscriber: shapeSubscriber(extractSubscriber(data)),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
subscriber: {
|
||||
...SUBSCRIBER_OUTPUT,
|
||||
description: 'The updated subscriber object after refunding the Google subscription',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { RevokeEntitlementParams, RevokeEntitlementResponse } from '@/tools/revenuecat/types'
|
||||
import {
|
||||
extractSubscriber,
|
||||
SUBSCRIBER_OUTPUT,
|
||||
shapeSubscriber,
|
||||
throwIfRevenueCatError,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatRevokeEntitlementTool: ToolConfig<
|
||||
RevokeEntitlementParams,
|
||||
RevokeEntitlementResponse
|
||||
> = {
|
||||
id: 'revenuecat_revoke_entitlement',
|
||||
name: 'RevenueCat Revoke Entitlement',
|
||||
description: 'Revoke all promotional entitlements for a specific entitlement identifier',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat secret API key (sk_...)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber',
|
||||
},
|
||||
entitlementIdentifier: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The entitlement identifier to revoke',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/entitlements/${encodeURIComponent(params.entitlementIdentifier.trim())}/revoke_promotionals`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
subscriber: shapeSubscriber(extractSubscriber(data)),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
subscriber: {
|
||||
...SUBSCRIBER_OUTPUT,
|
||||
description: 'The updated subscriber object after revoking the entitlement',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type {
|
||||
RevokeGoogleSubscriptionParams,
|
||||
RevokeGoogleSubscriptionResponse,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import {
|
||||
extractSubscriber,
|
||||
SUBSCRIBER_OUTPUT,
|
||||
shapeSubscriber,
|
||||
throwIfRevenueCatError,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatRevokeGoogleSubscriptionTool: ToolConfig<
|
||||
RevokeGoogleSubscriptionParams,
|
||||
RevokeGoogleSubscriptionResponse
|
||||
> = {
|
||||
id: 'revenuecat_revoke_google_subscription',
|
||||
name: 'RevenueCat Revoke Google Subscription',
|
||||
description:
|
||||
'Immediately revoke access to a Google Play subscription and issue a refund (Google Play only)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat secret API key (sk_...)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber',
|
||||
},
|
||||
productId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Google Play product identifier of the subscription to revoke',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/subscriptions/${encodeURIComponent(params.productId.trim())}/revoke`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
subscriber: shapeSubscriber(extractSubscriber(data)),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
subscriber: {
|
||||
...SUBSCRIBER_OUTPUT,
|
||||
description: 'The updated subscriber object after revoking the Google subscription',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Shared output property definitions for RevenueCat API responses.
|
||||
* Based on official RevenueCat API v1 documentation.
|
||||
*/
|
||||
|
||||
export const SUBSCRIPTION_OUTPUT_PROPERTIES = {
|
||||
store_transaction_id: {
|
||||
type: 'string',
|
||||
description: 'Store transaction identifier',
|
||||
optional: true,
|
||||
},
|
||||
original_transaction_id: {
|
||||
type: 'string',
|
||||
description: 'Original transaction identifier',
|
||||
optional: true,
|
||||
},
|
||||
purchase_date: { type: 'string', description: 'ISO 8601 purchase date', optional: true },
|
||||
original_purchase_date: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date of the original purchase',
|
||||
optional: true,
|
||||
},
|
||||
expires_date: { type: 'string', description: 'ISO 8601 expiration date', optional: true },
|
||||
is_sandbox: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this is a sandbox purchase',
|
||||
optional: true,
|
||||
},
|
||||
unsubscribe_detected_at: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when unsubscribe was detected',
|
||||
optional: true,
|
||||
},
|
||||
billing_issues_detected_at: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when billing issues were detected',
|
||||
optional: true,
|
||||
},
|
||||
grace_period_expires_date: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 grace period expiration date',
|
||||
optional: true,
|
||||
},
|
||||
ownership_type: {
|
||||
type: 'string',
|
||||
description: 'Ownership type (purchased, family_shared)',
|
||||
optional: true,
|
||||
},
|
||||
period_type: {
|
||||
type: 'string',
|
||||
description: 'Period type (normal, trial, intro, promotional, prepaid)',
|
||||
optional: true,
|
||||
},
|
||||
store: {
|
||||
type: 'string',
|
||||
description: 'Store the subscription was purchased from (app_store, play_store, stripe, etc.)',
|
||||
optional: true,
|
||||
},
|
||||
refunded_at: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when subscription was refunded',
|
||||
optional: true,
|
||||
},
|
||||
auto_resume_date: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when a paused subscription will auto-resume',
|
||||
optional: true,
|
||||
},
|
||||
product_plan_identifier: {
|
||||
type: 'string',
|
||||
description: 'Google Play base plan identifier (for products set up after Feb 2023)',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const ENTITLEMENT_OUTPUT_PROPERTIES = {
|
||||
expires_date: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 expiration date (null for non-expiring entitlements)',
|
||||
optional: true,
|
||||
},
|
||||
grace_period_expires_date: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 grace period expiration date',
|
||||
optional: true,
|
||||
},
|
||||
product_identifier: { type: 'string', description: 'Product identifier', optional: true },
|
||||
purchase_date: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date of the latest purchase or renewal',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const SUBSCRIBER_OUTPUT_PROPERTIES = {
|
||||
first_seen: { type: 'string', description: 'ISO 8601 date when subscriber was first seen' },
|
||||
last_seen: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when subscriber was last seen',
|
||||
optional: true,
|
||||
},
|
||||
original_app_user_id: { type: 'string', description: 'Original app user ID' },
|
||||
original_application_version: {
|
||||
type: 'string',
|
||||
description: 'iOS only. First App Store version of your app the customer installed',
|
||||
optional: true,
|
||||
},
|
||||
original_purchase_date: {
|
||||
type: 'string',
|
||||
description: 'iOS only. Date the app was first purchased/downloaded',
|
||||
optional: true,
|
||||
},
|
||||
management_url: {
|
||||
type: 'string',
|
||||
description: 'URL for managing the subscriber subscriptions',
|
||||
optional: true,
|
||||
},
|
||||
subscriptions: {
|
||||
type: 'object',
|
||||
description: 'Map of product identifiers to subscription objects',
|
||||
properties: SUBSCRIPTION_OUTPUT_PROPERTIES,
|
||||
},
|
||||
entitlements: {
|
||||
type: 'object',
|
||||
description: 'Map of entitlement identifiers to entitlement objects',
|
||||
properties: ENTITLEMENT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
non_subscriptions: {
|
||||
type: 'object',
|
||||
description: 'Map of non-subscription product identifiers to arrays of purchase objects',
|
||||
optional: true,
|
||||
},
|
||||
other_purchases: {
|
||||
type: 'object',
|
||||
description: 'Other purchases attached to the subscriber',
|
||||
optional: true,
|
||||
},
|
||||
subscriber_attributes: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Custom attributes set on the subscriber. Only returned when using a secret API key',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const SUBSCRIBER_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'RevenueCat subscriber object',
|
||||
properties: SUBSCRIBER_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
export const OFFERING_PACKAGE_OUTPUT_PROPERTIES = {
|
||||
identifier: { type: 'string', description: 'Package identifier' },
|
||||
platform_product_identifier: {
|
||||
type: 'string',
|
||||
description: 'Platform-specific product identifier',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const OFFERING_OUTPUT_PROPERTIES = {
|
||||
identifier: { type: 'string', description: 'Offering identifier' },
|
||||
description: { type: 'string', description: 'Offering description', optional: true },
|
||||
packages: {
|
||||
type: 'array',
|
||||
description: 'List of packages in the offering',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: OFFERING_PACKAGE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const DELETE_OUTPUT_PROPERTIES = {
|
||||
deleted: { type: 'boolean', description: 'Whether the subscriber was deleted' },
|
||||
app_user_id: { type: 'string', description: 'The deleted app user ID' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const METADATA_OUTPUT_PROPERTIES = {
|
||||
app_user_id: { type: 'string', description: 'The app user ID' },
|
||||
first_seen: { type: 'string', description: 'ISO 8601 date when the subscriber was first seen' },
|
||||
active_entitlements: { type: 'number', description: 'Number of active entitlements' },
|
||||
active_subscriptions: { type: 'number', description: 'Number of active subscriptions' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
export const OFFERINGS_METADATA_OUTPUT_PROPERTIES = {
|
||||
count: { type: 'number', description: 'Number of offerings returned' },
|
||||
current_offering_id: {
|
||||
type: 'string',
|
||||
description: 'Current offering identifier',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Several RevenueCat v1 endpoints (post receipts, update attributes, revoke promotionals,
|
||||
* defer/refund/revoke Google subscriptions) wrap responses in `{ value: { request_date, subscriber } }`.
|
||||
* GET customer info returns the same payload unwrapped. This helper handles both shapes.
|
||||
*/
|
||||
export function extractSubscriber(data: unknown): Record<string, unknown> {
|
||||
if (!data || typeof data !== 'object') return {}
|
||||
const root = data as Record<string, unknown>
|
||||
const wrapped = root.value as Record<string, unknown> | undefined
|
||||
const subscriber = (wrapped?.subscriber ?? root.subscriber) as Record<string, unknown> | undefined
|
||||
return subscriber ?? {}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/receipts may return a top-level `customer` object alongside `subscriber`.
|
||||
* Returns null when not present (e.g., wrapped envelope responses).
|
||||
*/
|
||||
export function extractCustomer(data: unknown): Record<string, unknown> | null {
|
||||
if (!data || typeof data !== 'object') return null
|
||||
const customer = (data as Record<string, unknown>).customer
|
||||
return customer && typeof customer === 'object' ? (customer as Record<string, unknown>) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a RevenueCat REST API error response into a meaningful Error.
|
||||
* RevenueCat returns `{ code, message }` on 4xx/5xx.
|
||||
*/
|
||||
export async function throwIfRevenueCatError(response: Response): Promise<void> {
|
||||
if (response.ok) return
|
||||
let message = `RevenueCat API error (${response.status})`
|
||||
try {
|
||||
const body = await response.clone().json()
|
||||
if (body && typeof body === 'object') {
|
||||
const m = (body as Record<string, unknown>).message
|
||||
const c = (body as Record<string, unknown>).code
|
||||
if (typeof m === 'string' && m.length > 0) {
|
||||
message = c ? `${m} (code ${c})` : m
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Body not JSON — fall back to status-only message
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base params interface for RevenueCat API calls
|
||||
*/
|
||||
interface RevenueCatBaseParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface GetCustomerParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
}
|
||||
|
||||
export interface DeleteCustomerParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
}
|
||||
|
||||
export interface GrantEntitlementParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
entitlementIdentifier: string
|
||||
duration?: string
|
||||
endTimeMs?: number
|
||||
startTimeMs?: number
|
||||
}
|
||||
|
||||
export interface RevokeEntitlementParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
entitlementIdentifier: string
|
||||
}
|
||||
|
||||
export interface ListOfferingsParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
platform?: string
|
||||
}
|
||||
|
||||
export interface CreatePurchaseParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
fetchToken: string
|
||||
productId?: string
|
||||
price?: number
|
||||
currency?: string
|
||||
isRestore?: boolean
|
||||
presentedOfferingIdentifier?: string
|
||||
paymentMode?: string
|
||||
introductoryPrice?: number
|
||||
attributes?: string
|
||||
updatedAtMs?: number
|
||||
platform: string
|
||||
}
|
||||
|
||||
export interface UpdateSubscriberAttributesParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
attributes: string
|
||||
}
|
||||
|
||||
export interface DeferGoogleSubscriptionParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
productId: string
|
||||
extendByDays?: number
|
||||
expiryTimeMs?: number
|
||||
}
|
||||
|
||||
export interface RefundGoogleSubscriptionParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
storeTransactionId: string
|
||||
}
|
||||
|
||||
export interface RevokeGoogleSubscriptionParams extends RevenueCatBaseParams {
|
||||
appUserId: string
|
||||
productId: string
|
||||
}
|
||||
|
||||
export interface RevenueCatSubscriber {
|
||||
first_seen: string
|
||||
last_seen: string | null
|
||||
original_app_user_id: string
|
||||
original_application_version: string | null
|
||||
original_purchase_date: string | null
|
||||
management_url: string | null
|
||||
subscriptions: Record<string, unknown>
|
||||
entitlements: Record<string, unknown>
|
||||
non_subscriptions: Record<string, unknown>
|
||||
other_purchases: Record<string, unknown>
|
||||
subscriber_attributes: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export function shapeSubscriber(raw: Record<string, unknown>): RevenueCatSubscriber {
|
||||
return {
|
||||
first_seen: (raw.first_seen as string) ?? '',
|
||||
last_seen: (raw.last_seen as string | null) ?? null,
|
||||
original_app_user_id: (raw.original_app_user_id as string) ?? '',
|
||||
original_application_version: (raw.original_application_version as string | null) ?? null,
|
||||
original_purchase_date: (raw.original_purchase_date as string | null) ?? null,
|
||||
management_url: (raw.management_url as string | null) ?? null,
|
||||
subscriptions: (raw.subscriptions as Record<string, unknown>) ?? {},
|
||||
entitlements: (raw.entitlements as Record<string, unknown>) ?? {},
|
||||
non_subscriptions: (raw.non_subscriptions as Record<string, unknown>) ?? {},
|
||||
other_purchases: (raw.other_purchases as Record<string, unknown>) ?? {},
|
||||
subscriber_attributes: (raw.subscriber_attributes as Record<string, unknown> | null) ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export interface CustomerResponse extends ToolResponse {
|
||||
output: {
|
||||
subscriber: RevenueCatSubscriber
|
||||
metadata: {
|
||||
app_user_id: string
|
||||
first_seen: string
|
||||
active_entitlements: number
|
||||
active_subscriptions: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeleteCustomerResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
app_user_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface GrantEntitlementResponse extends ToolResponse {
|
||||
output: {
|
||||
subscriber: RevenueCatSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
export interface RevokeEntitlementResponse extends ToolResponse {
|
||||
output: {
|
||||
subscriber: RevenueCatSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
export interface ListOfferingsResponse extends ToolResponse {
|
||||
output: {
|
||||
current_offering_id: string | null
|
||||
offerings: Array<{
|
||||
identifier: string
|
||||
description: string | null
|
||||
packages: Array<{
|
||||
identifier: string
|
||||
platform_product_identifier: string | null
|
||||
}>
|
||||
}>
|
||||
metadata: {
|
||||
count: number
|
||||
current_offering_id: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CreatePurchaseResponse extends ToolResponse {
|
||||
output: {
|
||||
customer: Record<string, unknown> | null
|
||||
subscriber: RevenueCatSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpdateSubscriberAttributesResponse extends ToolResponse {
|
||||
output: {
|
||||
updated: boolean
|
||||
app_user_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeferGoogleSubscriptionResponse extends ToolResponse {
|
||||
output: {
|
||||
subscriber: RevenueCatSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
export interface RefundGoogleSubscriptionResponse extends ToolResponse {
|
||||
output: {
|
||||
subscriber: RevenueCatSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
export interface RevokeGoogleSubscriptionResponse extends ToolResponse {
|
||||
output: {
|
||||
subscriber: RevenueCatSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
export type RevenueCatResponse =
|
||||
| CustomerResponse
|
||||
| DeleteCustomerResponse
|
||||
| GrantEntitlementResponse
|
||||
| RevokeEntitlementResponse
|
||||
| ListOfferingsResponse
|
||||
| CreatePurchaseResponse
|
||||
| UpdateSubscriberAttributesResponse
|
||||
| DeferGoogleSubscriptionResponse
|
||||
| RefundGoogleSubscriptionResponse
|
||||
| RevokeGoogleSubscriptionResponse
|
||||
@@ -0,0 +1,84 @@
|
||||
import type {
|
||||
UpdateSubscriberAttributesParams,
|
||||
UpdateSubscriberAttributesResponse,
|
||||
} from '@/tools/revenuecat/types'
|
||||
import { throwIfRevenueCatError } from '@/tools/revenuecat/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const revenuecatUpdateSubscriberAttributesTool: ToolConfig<
|
||||
UpdateSubscriberAttributesParams,
|
||||
UpdateSubscriberAttributesResponse
|
||||
> = {
|
||||
id: 'revenuecat_update_subscriber_attributes',
|
||||
name: 'RevenueCat Update Subscriber Attributes',
|
||||
description:
|
||||
'Update custom subscriber attributes (e.g., $email, $displayName, or custom key-value pairs)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'RevenueCat secret API key (sk_...)',
|
||||
},
|
||||
appUserId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The app user ID of the subscriber',
|
||||
},
|
||||
attributes: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON object of attributes to set. Each key maps to an object with "value" (string; null or empty deletes the attribute) and "updated_at_ms" (Unix epoch ms used for conflict resolution — required). Example: {"$email": {"value": "user@example.com", "updated_at_ms": 1709195668093}}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/attributes`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let attributes: unknown
|
||||
if (typeof params.attributes === 'string') {
|
||||
try {
|
||||
attributes = JSON.parse(params.attributes)
|
||||
} catch {
|
||||
throw new Error('attributes must be a valid JSON object')
|
||||
}
|
||||
} else {
|
||||
attributes = params.attributes
|
||||
}
|
||||
return { attributes }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params) => {
|
||||
await throwIfRevenueCatError(response)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updated: true,
|
||||
app_user_id: params?.appUserId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updated: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the subscriber attributes were successfully updated',
|
||||
},
|
||||
app_user_id: {
|
||||
type: 'string',
|
||||
description: 'The app user ID of the updated subscriber',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user