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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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,
},
}
+105
View File
@@ -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',
},
},
}
+11
View File
@@ -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'
+109
View File
@@ -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',
},
},
}
+433
View File
@@ -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',
},
},
}