google

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package google bootstraps the Google-side credential RevenueCat needs for a Google Play app: it drives a local (loopback + PKCE) Google OAuth sign-in and then uses that human authority to create a narrowly scoped service account, its key, and the Play Console grants. The human's OAuth token stays in memory and is never sent to RevenueCat.

This package is service-only — no CLI concepts — mirroring internal/appleconnect.

Index

Constants

View Source
const (
	ScopeAndroidPublisher = "https://www.googleapis.com/auth/androidpublisher"
	ScopeCloudPlatform    = "https://www.googleapis.com/auth/cloud-platform"
)

OAuth scopes. Request the minimum: androidpublisher for Play users/grants, cloud-platform for the GCP bootstrap, and openid/email so we can show who signed in. All are "sensitive" (not "restricted") to Google — verification is required for >100 users but not the CASA security assessment.

View Source
const (
	// SAAccountID is the account-id portion of the service account RevenueCat
	// uses; the full email is {SAAccountID}@{projectID}.iam.gserviceaccount.com.
	SAAccountID   = "revenuecat-service-account"
	SADisplayName = "RevenueCat"
)

Variables

View Source
var (
	DefaultClientID     = ""
	DefaultClientSecret = ""
)

DefaultClientID / DefaultClientSecret are the RevenueCat-owned Desktop OAuth client baked into the binary. They are intentionally empty until RevenueCat provisions the official client; RC_GOOGLE_CLIENT_ID / RC_GOOGLE_CLIENT_SECRET override them (and let developers test against their own Desktop client under Google's 100-user unverified cap in the meantime). A Desktop client secret is not confidential — PKCE is the real protection — so shipping it is expected.

View Source
var DefaultScopes = []string{ScopeAndroidPublisher, ScopeCloudPlatform, scopeOpenID, scopeEmail}

DefaultScopes covers the whole setup flow.

View Source
var ErrNoClientID = errors.New(
	"no Google OAuth client configured: set RC_GOOGLE_CLIENT_ID (and RC_GOOGLE_CLIENT_SECRET) " +
		"to a Google Cloud \"Desktop app\" OAuth client. Create one at " +
		"https://console.cloud.google.com/apis/credentials → Create credentials → OAuth client ID → Desktop app")

ErrNoClientID is returned when neither a baked-in nor an env-provided client ID is available. The message tells the human how to create one.

View Source
var PlayAppPermissions = []string{
	"CAN_VIEW_NON_FINANCIAL_DATA",
	"CAN_VIEW_FINANCIAL_DATA",
	"CAN_MANAGE_ORDERS",
}

PlayAppPermissions are the per-app (package-scoped) grant permissions RevenueCat needs. All three exist at the app level, so we never grant account-wide access. Maps to the Play Console labels:

  • CAN_VIEW_NON_FINANCIAL_DATA → "View app information"
  • CAN_VIEW_FINANCIAL_DATA → "View financial data, orders, and surveys"
  • CAN_MANAGE_ORDERS → "Manage orders and subscriptions"
View Source
var ProjectRoles = []string{"roles/pubsub.editor", "roles/monitoring.viewer"}

ProjectRoles are the Google Cloud IAM roles the service account needs. Matches RevenueCat's current requirements.

View Source
var RequiredAPIs = []string{
	"cloudresourcemanager.googleapis.com",
	"iam.googleapis.com",
	"androidpublisher.googleapis.com",
	"playdeveloperreporting.googleapis.com",
	"pubsub.googleapis.com",
}

RequiredAPIs are the Google APIs RevenueCat's Play integration depends on. This matches RevenueCat's current Cloud Shell setup script.

Functions

func CreateKey

func CreateKey(ctx context.Context, ts oauth2.TokenSource, projectID, saEmail string) ([]byte, string, error)

CreateKey creates a JSON service-account key and returns the decoded key file bytes and the key's resource name. The private key material is returned inline by Google (only on create) so it never touches disk here — the caller keeps it in memory. The name lets the caller prune older keys without deleting this one.

func CreateProject

func CreateProject(ctx context.Context, ts oauth2.TokenSource, projectID, displayName string) error

CreateProject creates a new Google Cloud project and waits for it to become active. Project creation is a long-running operation, so this polls until it completes. The signed-in user needs permission to create projects (personal accounts can, subject to quota; org accounts may require a folder/org parent their admin controls).

func EnableAPIs

func EnableAPIs(ctx context.Context, ts oauth2.TokenSource, projectID string) error

EnableAPIs enables all RequiredAPIs on the project in a single batch call. batchEnable is idempotent — already-enabled services are a no-op — so re-runs are safe. (Google caps batchEnable at 20 service IDs; we have 5.)

func EnsureServiceAccount

func EnsureServiceAccount(ctx context.Context, ts oauth2.TokenSource, projectID string) (created bool, email string, err error)

EnsureServiceAccount returns the RevenueCat service account, creating it if it doesn't exist. Idempotent: an existing account is returned without error. The bool reports whether it was newly created (for user-facing messaging).

func GrantProjectRoles

func GrantProjectRoles(ctx context.Context, ts oauth2.TokenSource, projectID, saEmail string) ([]string, error)

GrantProjectRoles ensures the service account holds ProjectRoles on the project. Read-modify-write on the project IAM policy; idempotent (roles the member already has are skipped). Returns the roles it actually added.

func ParseDeveloperID

func ParseDeveloperID(input string) (string, error)

ParseDeveloperID normalizes a pasted Play Console URL or raw account ID into the numeric developer ID. Google exposes no API to discover this, so the human must supply it once.

func ProjectIDFromName

func ProjectIDFromName(name string) string

ProjectIDFromName derives a valid, likely-unique Google Cloud project ID from a display name. Project IDs must be 6–30 chars, start with a lowercase letter, and contain only lowercase letters, digits, and hyphens. A short random suffix keeps it from colliding with an existing global ID.

func PruneUserKeys

func PruneUserKeys(ctx context.Context, ts oauth2.TokenSource, projectID, saEmail, keepKeyName string) (int, error)

PruneUserKeys deletes the user-managed keys on the RevenueCat service account, skipping keepKeyName, and reports how many it removed. This keeps re-runs from piling keys up toward Google's ~10-per-account limit. It only ever runs against the rc-owned service account, and only touches USER_MANAGED keys — Google's SYSTEM_MANAGED signing keys are never listed or deleted. Pass the newly created key's name as keepKeyName so pruning never deletes the credential just issued.

func ServiceAccountEmail

func ServiceAccountEmail(projectID string) string

ServiceAccountEmail is the deterministic email for the given project.

Types

type BrowserFunc

type BrowserFunc func(url string) error

BrowserFunc opens a URL in the user's browser. Injected so the CLI can reuse its own openBrowser and tests can stub it.

type Credentials

type Credentials struct {
	TokenSource oauth2.TokenSource
	Email       string
}

Credentials is the result of a successful sign-in: a refreshing TokenSource to authorize every subsequent API client, and the signed-in email for display.

func Authenticate

func Authenticate(ctx context.Context, clientID, clientSecret string, scopes []string, open BrowserFunc) (*Credentials, error)

Authenticate runs the Desktop-app OAuth flow: it starts a loopback listener on 127.0.0.1, opens the browser to Google's consent screen with a PKCE challenge, captures the redirect, and exchanges the code for tokens. The returned TokenSource auto-refreshes.

type OrgPolicyError

type OrgPolicyError struct {
	Constraint string // e.g. "iam.disableServiceAccountKeyCreation"
	// contains filtered or unexported fields
}

OrgPolicyError signals that a Google Cloud organization policy blocked the operation, so the CLI can print actionable guidance instead of a raw 400.

func (*OrgPolicyError) Error

func (e *OrgPolicyError) Error() string

func (*OrgPolicyError) Unwrap

func (e *OrgPolicyError) Unwrap() error

type PlayResult

type PlayResult struct {
	UserCreated  bool
	GrantCreated bool
	GrantUpdated bool
}

PlayResult reports what AddServiceAccountToPlay did, for idempotent messaging.

func AddServiceAccountToPlay

func AddServiceAccountToPlay(ctx context.Context, ts oauth2.TokenSource, developerID, saEmail, packageName, quotaProject string) (*PlayResult, error)

AddServiceAccountToPlay invites the service account to the Play developer account (if not already a user) and grants it the package-scoped permissions (creating or updating the grant as needed). Idempotent.

Requires the signed-in human to be a Play Console Owner/Admin.

type Project

type Project struct {
	ID          string // e.g. "my-app-production"
	DisplayName string // human name, may be empty
}

Project is a Google Cloud project the signed-in user can access.

func ListProjects

func ListProjects(ctx context.Context, ts oauth2.TokenSource) ([]Project, error)

ListProjects returns the active Cloud projects accessible to the token. It uses projects.search, which needs no parent and returns everything the caller can see, so the developer never has to copy a project ID by hand.

type TosError

type TosError struct {
	URL string // the terms-acceptance page to visit
	// contains filtered or unexported fields
}

TosError signals that the signed-in account hasn't accepted a required Google API terms of service (e.g. Android Publisher), which blocks enabling those APIs until the human accepts them once in the Console.

func (*TosError) Error

func (e *TosError) Error() string

func (*TosError) Unwrap

func (e *TosError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL