@mpgd/game-services 0.33.1 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admob-ssv.js +6 -26
- package/dist/app-store-verifier-conformance.d.ts +37 -1
- package/dist/app-store-verifier-conformance.js +199 -1
- package/dist/app-store-verifier.d.ts +57 -4
- package/dist/app-store-verifier.js +106 -4
- package/dist/base64.d.ts +10 -0
- package/dist/base64.js +35 -0
- package/dist/google-play-publisher-client.js +24 -1
- package/dist/google-play-rtdn.d.ts +248 -0
- package/dist/google-play-rtdn.js +594 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/notifications/client.d.ts +19 -1
- package/dist/notifications/client.js +24 -2
- package/dist/notifications/text.d.ts +5 -1
- package/dist/notifications/text.js +11 -2
- package/package.json +9 -5
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { type GooglePlayVoidedPurchasesClient } from './google-play-store-server.js';
|
|
2
|
+
import type { GameServicesStore } from './server.js';
|
|
3
|
+
/**
|
|
4
|
+
* Google Play Real-time developer notifications (RTDN) delivered by a Cloud
|
|
5
|
+
* Pub/Sub push subscription: the fast path for revoking a refunded or voided
|
|
6
|
+
* one-time purchase. The Voided Purchases API sweep
|
|
7
|
+
* (`sweepGooglePlayVoidedPurchases`) stays required next to it, because a
|
|
8
|
+
* push can be lost (a misconfigured subscription, a message that outlives
|
|
9
|
+
* its retention); both revoke through `revokeGooglePlayVoidedPurchase`, keyed
|
|
10
|
+
* by the purchase token, so whichever comes second is a no-op.
|
|
11
|
+
*
|
|
12
|
+
* - RTDN reference: https://developer.android.com/google/play/billing/rtdn-reference
|
|
13
|
+
* - Push authentication: https://cloud.google.com/pubsub/docs/authenticate-push-subscriptions
|
|
14
|
+
* - Push acknowledgement (102, 200, 201, 202, 204 ack; anything else is
|
|
15
|
+
* redelivered): https://cloud.google.com/pubsub/docs/push
|
|
16
|
+
* - Payload unwrapping: https://cloud.google.com/pubsub/docs/payload-unwrapping
|
|
17
|
+
* - Voided Purchases API (6,000 queries a day, 30 in any 30 seconds):
|
|
18
|
+
* https://developers.google.com/android-publisher/voided-purchases
|
|
19
|
+
*/
|
|
20
|
+
/** Issuers of Google-signed ID tokens (Google's OpenID Connect guide). */
|
|
21
|
+
export declare const googleIdTokenIssuers: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* Google's published ID token signing keys (the `jwks_uri` of
|
|
24
|
+
* https://accounts.google.com/.well-known/openid-configuration).
|
|
25
|
+
*/
|
|
26
|
+
export declare const googleIdTokenJwksUrl = "https://www.googleapis.com/oauth2/v3/certs";
|
|
27
|
+
/** Google's keys could not be read: answer non-2xx so Pub/Sub redelivers. */
|
|
28
|
+
export declare class GoogleIdTokenKeysUnavailableError extends Error {
|
|
29
|
+
readonly name = "GoogleIdTokenKeysUnavailableError";
|
|
30
|
+
}
|
|
31
|
+
/** The source of Google's RS256 ID token signing keys; inject a fake in tests. */
|
|
32
|
+
export interface GoogleIdTokenSigningKeys {
|
|
33
|
+
/**
|
|
34
|
+
* The published public key with this key id, or undefined when Google does
|
|
35
|
+
* not publish one. Throws `GoogleIdTokenKeysUnavailableError` when the keys
|
|
36
|
+
* cannot be read.
|
|
37
|
+
*/
|
|
38
|
+
getSigningKey(input: {
|
|
39
|
+
readonly kid: string;
|
|
40
|
+
readonly signal: AbortSignal;
|
|
41
|
+
}): Promise<JsonWebKey | undefined>;
|
|
42
|
+
}
|
|
43
|
+
export interface GoogleIdTokenSigningKeysOptions {
|
|
44
|
+
readonly fetch?: typeof fetch;
|
|
45
|
+
readonly jwksUrl?: string;
|
|
46
|
+
/** Milliseconds since the epoch; for the cache. */
|
|
47
|
+
readonly now?: () => number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* How long past its cache expiry a held key still verifies while Google's
|
|
51
|
+
* key endpoint cannot be read (an outage, or the refresh throttle). Six
|
|
52
|
+
* hours rides out a long endpoint outage without dropping pushes, and is the
|
|
53
|
+
* longest a push is retried (`maxUnmatchedVoidRetryMs`); past it a key Google
|
|
54
|
+
* may have withdrawn is no longer trusted and pushes answer 503 until the
|
|
55
|
+
* keys can be read again.
|
|
56
|
+
*/
|
|
57
|
+
export declare const googleIdTokenKeyStaleGraceMs: number;
|
|
58
|
+
/**
|
|
59
|
+
* Reads Google's ID token keys from `googleIdTokenJwksUrl`, cached for the
|
|
60
|
+
* response's `Cache-Control: max-age` (one minute to a day; an hour without
|
|
61
|
+
* one) and refreshed early for a key id it does not hold: Google rotates its
|
|
62
|
+
* keys. Fetches, failed ones included, are at least a minute apart. While
|
|
63
|
+
* the keys cannot be read (an outage, or between throttled fetches), a held
|
|
64
|
+
* key answers for `googleIdTokenKeyStaleGraceMs` past its cache expiry; past
|
|
65
|
+
* that, an outage is `GoogleIdTokenKeysUnavailableError` and a throttled
|
|
66
|
+
* lookup finds no key. One instance per isolate shares the cache.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createGoogleIdTokenSigningKeys(options?: GoogleIdTokenSigningKeysOptions): GoogleIdTokenSigningKeys;
|
|
69
|
+
export interface GooglePubSubPushAuthentication {
|
|
70
|
+
/** The audience set on the push subscription (`--push-auth-token-audience`). */
|
|
71
|
+
readonly audience: string;
|
|
72
|
+
/** The service account the push subscription signs as (`--push-auth-service-account`). */
|
|
73
|
+
readonly serviceAccountEmail: string;
|
|
74
|
+
readonly keys: GoogleIdTokenSigningKeys;
|
|
75
|
+
/** Clock skew allowed on `exp` and `iat`, in seconds; 60 by default. */
|
|
76
|
+
readonly clockSkewSeconds?: number;
|
|
77
|
+
}
|
|
78
|
+
export type GooglePubSubPushTokenVerification = {
|
|
79
|
+
readonly status: 'verified';
|
|
80
|
+
} | {
|
|
81
|
+
readonly status: 'rejected';
|
|
82
|
+
readonly reason: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Verifies the OIDC token a Pub/Sub push subscription with authentication
|
|
86
|
+
* sends as `Authorization: Bearer <JWT>`: an RS256 signature by one of
|
|
87
|
+
* Google's published keys, a Google issuer, the subscription's audience, its
|
|
88
|
+
* service account (`email`, `email_verified`), and `exp`/`iat`. Throws
|
|
89
|
+
* `GoogleIdTokenKeysUnavailableError` when Google's keys cannot be read.
|
|
90
|
+
*/
|
|
91
|
+
export declare function verifyGooglePubSubPushToken(input: GooglePubSubPushAuthentication & {
|
|
92
|
+
readonly authorization: string | null | undefined;
|
|
93
|
+
readonly signal: AbortSignal;
|
|
94
|
+
/** Milliseconds since the epoch. */
|
|
95
|
+
readonly now?: () => number;
|
|
96
|
+
}): Promise<GooglePubSubPushTokenVerification>;
|
|
97
|
+
/** `voidedPurchaseNotification.productType` (RTDN reference). */
|
|
98
|
+
export declare const googlePlayRtdnProductType: Readonly<{
|
|
99
|
+
readonly subscription: 1;
|
|
100
|
+
readonly oneTime: 2;
|
|
101
|
+
}>;
|
|
102
|
+
/** `voidedPurchaseNotification.refundType` (RTDN reference). */
|
|
103
|
+
export declare const googlePlayRtdnRefundType: Readonly<{
|
|
104
|
+
readonly fullRefund: 1;
|
|
105
|
+
readonly quantityBasedPartialRefund: 2;
|
|
106
|
+
}>;
|
|
107
|
+
/** The decoded `message.data`: one notification kind besides `version`, `packageName`, `eventTimeMillis`. */
|
|
108
|
+
export interface GooglePlayDeveloperNotification {
|
|
109
|
+
readonly version?: string;
|
|
110
|
+
readonly packageName: string;
|
|
111
|
+
/** Milliseconds since the epoch (an int64 string or a number in the JSON). */
|
|
112
|
+
readonly eventTimeMillis?: number;
|
|
113
|
+
readonly voidedPurchaseNotification?: {
|
|
114
|
+
readonly purchaseToken?: unknown;
|
|
115
|
+
readonly orderId?: unknown;
|
|
116
|
+
readonly productType?: unknown;
|
|
117
|
+
readonly refundType?: unknown;
|
|
118
|
+
};
|
|
119
|
+
readonly oneTimeProductNotification?: Readonly<Record<string, unknown>>;
|
|
120
|
+
readonly subscriptionNotification?: Readonly<Record<string, unknown>>;
|
|
121
|
+
readonly testNotification?: Readonly<Record<string, unknown>>;
|
|
122
|
+
readonly [kind: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
export type GooglePlayDeveloperNotificationPush = {
|
|
125
|
+
readonly status: 'decoded';
|
|
126
|
+
readonly messageId?: string;
|
|
127
|
+
readonly notification: GooglePlayDeveloperNotification;
|
|
128
|
+
} | {
|
|
129
|
+
readonly status: 'rejected';
|
|
130
|
+
readonly reason: string;
|
|
131
|
+
readonly messageId?: string;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Decodes a Pub/Sub push body into the `DeveloperNotification` it carries.
|
|
135
|
+
* Both push formats are accepted: the default wrapped body (`{ message:
|
|
136
|
+
* { data, messageId }, subscription }`, the notification base64 in `data`)
|
|
137
|
+
* and an unwrapped one (payload unwrapping on: the body is the notification
|
|
138
|
+
* JSON itself, with a top-level `packageName` and no `message`). An
|
|
139
|
+
* unwrapped push carries its message id only in the
|
|
140
|
+
* `x-goog-pubsub-message-id` header when "write metadata" is on; pass it as
|
|
141
|
+
* `messageId`.
|
|
142
|
+
*/
|
|
143
|
+
export declare function decodeGooglePlayDeveloperNotificationPush(body: unknown, options?: {
|
|
144
|
+
readonly messageId?: string | null;
|
|
145
|
+
}): GooglePlayDeveloperNotificationPush;
|
|
146
|
+
export interface HandleGooglePlayDeveloperNotificationInput {
|
|
147
|
+
readonly store: GameServicesStore;
|
|
148
|
+
/** The app's package; a notification for another package is acknowledged and ignored. */
|
|
149
|
+
readonly packageName: string;
|
|
150
|
+
/**
|
|
151
|
+
* The Voided Purchases API (the sweep's client). A voided notification is
|
|
152
|
+
* only a trigger: the void is revoked once this lists it, with the listed
|
|
153
|
+
* entry, so the ledger records what the sweep would.
|
|
154
|
+
*/
|
|
155
|
+
readonly voidedPurchases: GooglePlayVoidedPurchasesClient;
|
|
156
|
+
/** The push request body parsed from JSON. Authenticate the push before calling this. */
|
|
157
|
+
readonly body: unknown;
|
|
158
|
+
/** An unwrapped push's `x-goog-pubsub-message-id` header, for logs. */
|
|
159
|
+
readonly messageId?: string | null;
|
|
160
|
+
/** Bounds the Voided Purchases API lookup. */
|
|
161
|
+
readonly signal: AbortSignal;
|
|
162
|
+
readonly now?: () => string;
|
|
163
|
+
/**
|
|
164
|
+
* How long after the notification's `eventTimeMillis` a void that the
|
|
165
|
+
* Voided Purchases API does not list yet, or whose grant this ledger does
|
|
166
|
+
* not hold, is answered `retry` instead of `ignored`; 1 hour by default, at
|
|
167
|
+
* most `maxUnmatchedVoidRetryMs` (6 hours), `0` acknowledges at once. It
|
|
168
|
+
* covers Google's lag in listing the void and a purchase whose grant is
|
|
169
|
+
* still being written; past it the message is acknowledged and the sweep
|
|
170
|
+
* reads the void again.
|
|
171
|
+
*/
|
|
172
|
+
readonly unmatchedVoidRetryMs?: number;
|
|
173
|
+
}
|
|
174
|
+
/** Default of `unmatchedVoidRetryMs`. */
|
|
175
|
+
export declare const defaultUnmatchedVoidRetryMs: number;
|
|
176
|
+
/** Upper bound of `unmatchedVoidRetryMs`. */
|
|
177
|
+
export declare const maxUnmatchedVoidRetryMs: number;
|
|
178
|
+
/**
|
|
179
|
+
* The lookup window around the notification's event time: this much before
|
|
180
|
+
* it (Google's own clocks), and up to `maxUnmatchedVoidRetryMs` after it
|
|
181
|
+
* (Google lists a void a little after it records it), never past now.
|
|
182
|
+
*/
|
|
183
|
+
export declare const googlePlayRtdnLookupLeadMs: number;
|
|
184
|
+
/** Voided Purchases API pages one notification reads at most (a page holds up to 1,000 voids). */
|
|
185
|
+
export declare const googlePlayRtdnLookupMaxPages = 2;
|
|
186
|
+
/**
|
|
187
|
+
* Every outcome's `orderId` is the order id of the matched Voided Purchases
|
|
188
|
+
* API entry, never the push body's: it is absent until Google lists the void.
|
|
189
|
+
*/
|
|
190
|
+
export type GooglePlayDeveloperNotificationOutcome =
|
|
191
|
+
/** The grant was reversed (or already had been: a redelivery, or the sweep came first). */
|
|
192
|
+
{
|
|
193
|
+
readonly status: 'revoked';
|
|
194
|
+
readonly messageId?: string;
|
|
195
|
+
readonly ledgerEntryId: string;
|
|
196
|
+
readonly alreadyProcessed: boolean;
|
|
197
|
+
readonly orderId?: string;
|
|
198
|
+
}
|
|
199
|
+
/** Nothing to do (a test, a subscription, a partial refund, another package): acknowledge. */
|
|
200
|
+
| {
|
|
201
|
+
readonly status: 'ignored';
|
|
202
|
+
readonly reason: string;
|
|
203
|
+
readonly messageId?: string;
|
|
204
|
+
readonly orderId?: string;
|
|
205
|
+
}
|
|
206
|
+
/** The ledger refused the revocation; a redelivery cannot fix it, so acknowledge and alert. */
|
|
207
|
+
| {
|
|
208
|
+
readonly status: 'conflict';
|
|
209
|
+
readonly reason: string;
|
|
210
|
+
readonly messageId?: string;
|
|
211
|
+
readonly orderId?: string;
|
|
212
|
+
}
|
|
213
|
+
/** Malformed: acknowledged so Pub/Sub does not redeliver it until retention runs out. */
|
|
214
|
+
| {
|
|
215
|
+
readonly status: 'rejected';
|
|
216
|
+
readonly reason: string;
|
|
217
|
+
readonly messageId?: string;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Answer non-2xx so Pub/Sub redelivers: the Voided Purchases API failed or
|
|
221
|
+
* does not list the void yet, or this ledger may still be writing its grant.
|
|
222
|
+
*/
|
|
223
|
+
| {
|
|
224
|
+
readonly status: 'retry';
|
|
225
|
+
readonly reason: string;
|
|
226
|
+
readonly messageId?: string;
|
|
227
|
+
readonly orderId?: string;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Applies one RTDN push to the ledger. A `voidedPurchaseNotification` of a
|
|
231
|
+
* one-time product with a full refund is confirmed with the Voided Purchases
|
|
232
|
+
* API (one window around the event time, matched on the purchase token) and
|
|
233
|
+
* the listed entry is revoked through `revokeGooglePlayVoidedPurchase`: the
|
|
234
|
+
* sweep's path, idempotency key, reason, source, and void time. A push alone
|
|
235
|
+
* never revokes, so a message published to the topic by anyone but Google
|
|
236
|
+
* cannot reverse a grant Google did not void. A void not listed yet, or
|
|
237
|
+
* whose grant this ledger does not hold, is `retry` while recent
|
|
238
|
+
* (`unmatchedVoidRetryMs`), `ignored` after; a lookup failure is `retry`. A
|
|
239
|
+
* quantity-based partial refund is ignored without a lookup, as the sweep
|
|
240
|
+
* (which lists full refunds only) ignores it. Test, subscription, one-time
|
|
241
|
+
* product purchase, and every other kind are `ignored`: the kit sells no
|
|
242
|
+
* Play subscriptions, and a purchase is granted only when the game verifies
|
|
243
|
+
* it. A thrown error (a store outage) is not an outcome: answer 5xx so
|
|
244
|
+
* Pub/Sub redelivers.
|
|
245
|
+
*/
|
|
246
|
+
export declare function handleGooglePlayDeveloperNotification(input: HandleGooglePlayDeveloperNotificationInput): Promise<GooglePlayDeveloperNotificationOutcome>;
|
|
247
|
+
/** The HTTP status to answer Pub/Sub with: 200 acknowledges, 503 asks for a redelivery. */
|
|
248
|
+
export declare function googlePlayDeveloperNotificationHttpStatus(outcome: GooglePlayDeveloperNotificationOutcome): 200 | 503;
|