@mpgd/game-services 0.9.0 → 0.11.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-conformance.d.ts +19 -0
- package/dist/admob-ssv-conformance.js +258 -0
- package/dist/admob-ssv.d.ts +54 -0
- package/dist/admob-ssv.js +566 -0
- package/dist/app-store-verifier-conformance.d.ts +25 -0
- package/dist/app-store-verifier-conformance.js +28 -0
- package/dist/app-store-verifier.d.ts +87 -0
- package/dist/app-store-verifier.js +439 -0
- package/dist/apps-in-toss-evidence-verification-conformance.d.ts +26 -0
- package/dist/apps-in-toss-evidence-verification-conformance.js +613 -0
- package/dist/apps-in-toss-evidence-verification.d.ts +162 -0
- package/dist/apps-in-toss-evidence-verification.js +458 -0
- package/dist/apps-in-toss-partner-api.d.ts +48 -0
- package/dist/apps-in-toss-partner-api.js +282 -0
- package/dist/evidence-verification.d.ts +21 -1
- package/dist/google-play-purchase-conformance.d.ts +26 -0
- package/dist/google-play-purchase-conformance.js +39 -0
- package/dist/google-play-purchase.d.ts +40 -0
- package/dist/google-play-purchase.js +559 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.js +199 -14
- package/dist/types.d.ts +10 -0
- package/dist/types.js +32 -0
- package/package.json +42 -5
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { GameServicesEvidenceVerifier } from './evidence-verification';
|
|
2
|
+
export type AppStoreEnvironment = 'Production' | 'Sandbox';
|
|
3
|
+
export declare const appStoreServerApiBaseUrls: Readonly<{
|
|
4
|
+
readonly Production: 'https://api.storekit.apple.com';
|
|
5
|
+
readonly Sandbox: 'https://api.storekit-sandbox.apple.com';
|
|
6
|
+
}>;
|
|
7
|
+
export declare class AppStoreDependencyUnavailableError extends Error {
|
|
8
|
+
readonly cause: unknown;
|
|
9
|
+
constructor(message: string, cause?: unknown);
|
|
10
|
+
}
|
|
11
|
+
export type AppStoreServerApiTransactionResult = {
|
|
12
|
+
readonly status: 'found';
|
|
13
|
+
readonly signedTransactionInfo: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly status: 'pending' | 'rejected';
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
};
|
|
18
|
+
export interface AppStoreServerApiTransactionInput {
|
|
19
|
+
readonly transactionId: string;
|
|
20
|
+
readonly environment: AppStoreEnvironment;
|
|
21
|
+
readonly signal: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
export interface AppStoreServerApiClient {
|
|
24
|
+
getTransactionInfo(input: AppStoreServerApiTransactionInput): Promise<AppStoreServerApiTransactionResult>;
|
|
25
|
+
}
|
|
26
|
+
export type AppStoreInAppPurchaseType = 'Auto-Renewable Subscription' | 'Consumable' | 'Non-Consumable' | 'Non-Renewing Subscription';
|
|
27
|
+
export interface AppStoreTransactionPayload {
|
|
28
|
+
readonly transactionId: string;
|
|
29
|
+
readonly originalTransactionId: string;
|
|
30
|
+
readonly bundleId: string;
|
|
31
|
+
readonly productId: string;
|
|
32
|
+
readonly purchaseDate: number;
|
|
33
|
+
readonly signedDate: number;
|
|
34
|
+
readonly environment: AppStoreEnvironment;
|
|
35
|
+
readonly type: AppStoreInAppPurchaseType;
|
|
36
|
+
readonly quantity?: number;
|
|
37
|
+
readonly appAccountToken?: string;
|
|
38
|
+
readonly expiresDate?: number;
|
|
39
|
+
readonly revocationDate?: number;
|
|
40
|
+
readonly isUpgraded?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export type AppStoreSignedTransactionVerificationResult = {
|
|
43
|
+
readonly status: 'verified';
|
|
44
|
+
readonly payload: AppStoreTransactionPayload;
|
|
45
|
+
} | {
|
|
46
|
+
readonly status: 'rejected';
|
|
47
|
+
readonly reason: string;
|
|
48
|
+
};
|
|
49
|
+
export interface AppStoreSignedTransactionVerifier {
|
|
50
|
+
verifyAndDecode(input: {
|
|
51
|
+
readonly signedTransaction: string;
|
|
52
|
+
readonly environment: AppStoreEnvironment;
|
|
53
|
+
readonly bundleId: string;
|
|
54
|
+
readonly signal: AbortSignal;
|
|
55
|
+
}): Promise<AppStoreSignedTransactionVerificationResult>;
|
|
56
|
+
}
|
|
57
|
+
export interface CreateAppStoreServerApiClientOptions {
|
|
58
|
+
readonly getBearerToken: (input: {
|
|
59
|
+
readonly signal: AbortSignal;
|
|
60
|
+
}) => string | Promise<string>;
|
|
61
|
+
readonly fetch?: AppStoreFetch;
|
|
62
|
+
readonly maxResponseBytes?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface CreateAppStoreEvidenceVerifierOptions {
|
|
65
|
+
readonly bundleId: string;
|
|
66
|
+
readonly environment: AppStoreEnvironment;
|
|
67
|
+
readonly serverApi: AppStoreServerApiClient;
|
|
68
|
+
readonly signedTransactionVerifier: AppStoreSignedTransactionVerifier;
|
|
69
|
+
readonly resolveAppAccountToken: (input: {
|
|
70
|
+
readonly playerId: string;
|
|
71
|
+
readonly signal: AbortSignal;
|
|
72
|
+
}) => string | Promise<string>;
|
|
73
|
+
readonly now?: () => string;
|
|
74
|
+
readonly clockSkewMs?: number;
|
|
75
|
+
}
|
|
76
|
+
export interface AppStoreFetchResponse {
|
|
77
|
+
readonly status: number;
|
|
78
|
+
readonly body: ReadableStream<Uint8Array> | null;
|
|
79
|
+
}
|
|
80
|
+
export type AppStoreFetch = (url: string, init: {
|
|
81
|
+
readonly method: 'GET';
|
|
82
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
83
|
+
readonly signal: AbortSignal;
|
|
84
|
+
}) => Promise<AppStoreFetchResponse>;
|
|
85
|
+
export declare function createAppStoreServerApiClient(options: CreateAppStoreServerApiClientOptions): AppStoreServerApiClient;
|
|
86
|
+
export declare function createAppStoreGameServicesEvidenceVerifier(options: CreateAppStoreEvidenceVerifierOptions): GameServicesEvidenceVerifier;
|
|
87
|
+
export declare function assertAppStoreTransactionPayload(input: unknown): AppStoreTransactionPayload;
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
export const appStoreServerApiBaseUrls = Object.freeze({
|
|
2
|
+
Production: 'https://api.storekit.apple.com',
|
|
3
|
+
Sandbox: 'https://api.storekit-sandbox.apple.com',
|
|
4
|
+
});
|
|
5
|
+
export class AppStoreDependencyUnavailableError extends Error {
|
|
6
|
+
cause;
|
|
7
|
+
constructor(message, cause) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'AppStoreDependencyUnavailableError';
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
class AppStoreResponseTooLargeError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('App Store Server API response exceeded maxResponseBytes.');
|
|
16
|
+
this.name = 'AppStoreResponseTooLargeError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function createAppStoreServerApiClient(options) {
|
|
20
|
+
const fetch = options.fetch ?? readGlobalFetch();
|
|
21
|
+
const maxResponseBytes = readPositiveSafeInteger(options.maxResponseBytes ?? 128 * 1024, 'maxResponseBytes');
|
|
22
|
+
return {
|
|
23
|
+
async getTransactionInfo(input) {
|
|
24
|
+
assertNonEmptyString(input.transactionId, 'transactionId');
|
|
25
|
+
assertAppStoreEnvironment(input.environment);
|
|
26
|
+
const token = await options.getBearerToken({ signal: input.signal });
|
|
27
|
+
assertAuthorizationValue(token, 'App Store bearer token');
|
|
28
|
+
const url = new URL(`/inApps/v1/transactions/${encodeURIComponent(input.transactionId)}`, appStoreServerApiBaseUrls[input.environment]);
|
|
29
|
+
let response;
|
|
30
|
+
try {
|
|
31
|
+
response = await fetch(url.toString(), {
|
|
32
|
+
method: 'GET',
|
|
33
|
+
headers: {
|
|
34
|
+
accept: 'application/json',
|
|
35
|
+
authorization: `Bearer ${token}`,
|
|
36
|
+
},
|
|
37
|
+
signal: input.signal,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (input.signal.aborted) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
throw new AppStoreDependencyUnavailableError('App Store Server API transport is unavailable.', error);
|
|
45
|
+
}
|
|
46
|
+
if (response.status === 200) {
|
|
47
|
+
let body;
|
|
48
|
+
try {
|
|
49
|
+
body = await readBoundedJson(response, maxResponseBytes, input.signal);
|
|
50
|
+
assertRecord(body, 'App Store transaction response');
|
|
51
|
+
assertNonEmptyString(body.signedTransactionInfo, 'signedTransactionInfo');
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (input.signal.aborted
|
|
55
|
+
|| error instanceof AppStoreDependencyUnavailableError) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
return { status: 'pending', reason: 'APP_STORE_SERVER_API_INVALID_RESPONSE' };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
status: 'found',
|
|
62
|
+
signedTransactionInfo: body.signedTransactionInfo,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
await cancelResponseBody(response);
|
|
66
|
+
if (response.status === 400) {
|
|
67
|
+
return { status: 'rejected', reason: 'APP_STORE_INVALID_TRANSACTION_ID' };
|
|
68
|
+
}
|
|
69
|
+
if (response.status === 404) {
|
|
70
|
+
return { status: 'pending', reason: 'APP_STORE_TRANSACTION_NOT_FOUND' };
|
|
71
|
+
}
|
|
72
|
+
if (response.status === 401 || response.status === 403) {
|
|
73
|
+
return { status: 'pending', reason: 'APP_STORE_AUTHORIZATION_FAILED' };
|
|
74
|
+
}
|
|
75
|
+
if (response.status === 429 || response.status >= 500) {
|
|
76
|
+
return { status: 'pending', reason: 'APP_STORE_SERVER_API_UNAVAILABLE' };
|
|
77
|
+
}
|
|
78
|
+
return { status: 'rejected', reason: 'APP_STORE_SERVER_API_REJECTED' };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function createAppStoreGameServicesEvidenceVerifier(options) {
|
|
83
|
+
assertNonEmptyString(options.bundleId, 'bundleId');
|
|
84
|
+
assertAppStoreEnvironment(options.environment);
|
|
85
|
+
const clockSkewMs = readNonNegativeSafeInteger(options.clockSkewMs ?? 5 * 60 * 1000, 'clockSkewMs');
|
|
86
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
87
|
+
return {
|
|
88
|
+
async verifyPurchase(input) {
|
|
89
|
+
return verifyAppStorePurchase(input, options, now, clockSkewMs);
|
|
90
|
+
},
|
|
91
|
+
async verifyAdReward() {
|
|
92
|
+
return {
|
|
93
|
+
status: 'rejected',
|
|
94
|
+
reason: 'APP_STORE_AD_REWARD_UNSUPPORTED',
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function verifyAppStorePurchase(input, options, now, clockSkewMs) {
|
|
100
|
+
if (input.request.target !== 'ios') {
|
|
101
|
+
return { status: 'rejected', reason: 'APP_STORE_TARGET_MISMATCH' };
|
|
102
|
+
}
|
|
103
|
+
if (input.product.type === 'subscription') {
|
|
104
|
+
return { status: 'rejected', reason: 'APP_STORE_SUBSCRIPTION_UNSUPPORTED' };
|
|
105
|
+
}
|
|
106
|
+
let transactionResult;
|
|
107
|
+
try {
|
|
108
|
+
transactionResult = await options.serverApi.getTransactionInfo({
|
|
109
|
+
transactionId: input.request.platformTransactionId,
|
|
110
|
+
environment: options.environment,
|
|
111
|
+
signal: input.signal,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (!(error instanceof AppStoreDependencyUnavailableError)) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
return { status: 'pending', reason: 'APP_STORE_SERVER_API_UNAVAILABLE' };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
assertAppStoreServerApiTransactionResult(transactionResult);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return { status: 'pending', reason: 'APP_STORE_SERVER_API_INVALID_RESPONSE' };
|
|
125
|
+
}
|
|
126
|
+
if (transactionResult.status !== 'found') {
|
|
127
|
+
return { status: transactionResult.status, reason: transactionResult.reason };
|
|
128
|
+
}
|
|
129
|
+
let signatureResult;
|
|
130
|
+
try {
|
|
131
|
+
signatureResult = await options.signedTransactionVerifier.verifyAndDecode({
|
|
132
|
+
signedTransaction: transactionResult.signedTransactionInfo,
|
|
133
|
+
environment: options.environment,
|
|
134
|
+
bundleId: options.bundleId,
|
|
135
|
+
signal: input.signal,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (!(error instanceof AppStoreDependencyUnavailableError)) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
return { status: 'pending', reason: 'APP_STORE_SIGNATURE_VERIFIER_UNAVAILABLE' };
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
assertAppStoreSignedTransactionVerificationResult(signatureResult);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return { status: 'rejected', reason: 'APP_STORE_SIGNATURE_RESULT_INVALID' };
|
|
149
|
+
}
|
|
150
|
+
if (signatureResult.status === 'rejected') {
|
|
151
|
+
return { status: 'rejected', reason: signatureResult.reason };
|
|
152
|
+
}
|
|
153
|
+
const transaction = signatureResult.payload;
|
|
154
|
+
const expectedAccountToken = await resolveExpectedAppAccountToken(input, options);
|
|
155
|
+
if (expectedAccountToken.status !== 'verified') {
|
|
156
|
+
return expectedAccountToken;
|
|
157
|
+
}
|
|
158
|
+
const nowIso = now();
|
|
159
|
+
const nowMs = Date.parse(nowIso);
|
|
160
|
+
if (!Number.isFinite(nowMs)) {
|
|
161
|
+
return { status: 'pending', reason: 'APP_STORE_VERIFIER_CLOCK_INVALID' };
|
|
162
|
+
}
|
|
163
|
+
const rejection = validateTransaction(input, transaction, options, expectedAccountToken.value, nowMs, clockSkewMs);
|
|
164
|
+
if (rejection !== undefined) {
|
|
165
|
+
return { status: 'rejected', reason: rejection };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
status: 'verified',
|
|
169
|
+
verificationId: createVerificationId(transaction),
|
|
170
|
+
verifiedAt: new Date(nowMs).toISOString(),
|
|
171
|
+
payload: {
|
|
172
|
+
appStoreBundleId: transaction.bundleId,
|
|
173
|
+
appStoreEnvironment: transaction.environment,
|
|
174
|
+
appStoreOriginalTransactionId: transaction.originalTransactionId,
|
|
175
|
+
appStorePurchaseDate: transaction.purchaseDate,
|
|
176
|
+
appStoreQuantity: transaction.quantity ?? 1,
|
|
177
|
+
appStoreSignedDate: transaction.signedDate,
|
|
178
|
+
appStoreTransactionType: transaction.type,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function resolveExpectedAppAccountToken(input, options) {
|
|
183
|
+
try {
|
|
184
|
+
const value = await options.resolveAppAccountToken({
|
|
185
|
+
playerId: input.request.playerId,
|
|
186
|
+
signal: input.signal,
|
|
187
|
+
});
|
|
188
|
+
return { status: 'verified', value: normalizeAppAccountToken(value) };
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (!(error instanceof AppStoreDependencyUnavailableError)) {
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
return { status: 'pending', reason: 'APP_STORE_ACCOUNT_BINDING_UNAVAILABLE' };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function validateTransaction(input, transaction, options, expectedAccountToken, nowMs, clockSkewMs) {
|
|
198
|
+
if (transaction.transactionId !== input.request.platformTransactionId) {
|
|
199
|
+
return 'APP_STORE_TRANSACTION_ID_MISMATCH';
|
|
200
|
+
}
|
|
201
|
+
if (transaction.bundleId !== options.bundleId) {
|
|
202
|
+
return 'APP_STORE_BUNDLE_ID_MISMATCH';
|
|
203
|
+
}
|
|
204
|
+
if (transaction.productId !== input.platformProductId) {
|
|
205
|
+
return 'APP_STORE_PRODUCT_ID_MISMATCH';
|
|
206
|
+
}
|
|
207
|
+
if (transaction.environment !== options.environment) {
|
|
208
|
+
return 'APP_STORE_ENVIRONMENT_MISMATCH';
|
|
209
|
+
}
|
|
210
|
+
if (transaction.appAccountToken === undefined
|
|
211
|
+
|| normalizeAppAccountToken(transaction.appAccountToken) !== expectedAccountToken) {
|
|
212
|
+
return 'APP_STORE_ACCOUNT_TOKEN_MISMATCH';
|
|
213
|
+
}
|
|
214
|
+
if (toCatalogProductType(transaction.type) !== input.product.type) {
|
|
215
|
+
return 'APP_STORE_PRODUCT_TYPE_MISMATCH';
|
|
216
|
+
}
|
|
217
|
+
const clientPurchaseObservedAt = Date.parse(input.request.purchasedAt);
|
|
218
|
+
if (!Number.isFinite(clientPurchaseObservedAt)) {
|
|
219
|
+
return 'APP_STORE_CLIENT_PURCHASE_TIME_INVALID';
|
|
220
|
+
}
|
|
221
|
+
if (clientPurchaseObservedAt > nowMs + clockSkewMs) {
|
|
222
|
+
return 'APP_STORE_CLIENT_PURCHASE_TIME_IN_FUTURE';
|
|
223
|
+
}
|
|
224
|
+
if (transaction.quantity !== undefined && transaction.quantity !== 1) {
|
|
225
|
+
return 'APP_STORE_QUANTITY_UNSUPPORTED';
|
|
226
|
+
}
|
|
227
|
+
if (transaction.purchaseDate > nowMs + clockSkewMs) {
|
|
228
|
+
return 'APP_STORE_PURCHASE_DATE_IN_FUTURE';
|
|
229
|
+
}
|
|
230
|
+
if (transaction.signedDate < transaction.purchaseDate) {
|
|
231
|
+
return 'APP_STORE_SIGNED_DATE_INVALID';
|
|
232
|
+
}
|
|
233
|
+
if (transaction.signedDate > nowMs + clockSkewMs) {
|
|
234
|
+
return 'APP_STORE_SIGNED_DATE_IN_FUTURE';
|
|
235
|
+
}
|
|
236
|
+
if (transaction.revocationDate !== undefined) {
|
|
237
|
+
return 'APP_STORE_TRANSACTION_REVOKED';
|
|
238
|
+
}
|
|
239
|
+
if (transaction.isUpgraded === true) {
|
|
240
|
+
return 'APP_STORE_TRANSACTION_UPGRADED';
|
|
241
|
+
}
|
|
242
|
+
if (transaction.expiresDate !== undefined && transaction.expiresDate <= nowMs) {
|
|
243
|
+
return 'APP_STORE_TRANSACTION_EXPIRED';
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
function createVerificationId(transaction) {
|
|
248
|
+
const grantTransactionId = transaction.type === 'Non-Consumable'
|
|
249
|
+
? transaction.originalTransactionId
|
|
250
|
+
: transaction.transactionId;
|
|
251
|
+
return [
|
|
252
|
+
'app-store',
|
|
253
|
+
transaction.environment,
|
|
254
|
+
transaction.bundleId,
|
|
255
|
+
grantTransactionId,
|
|
256
|
+
].map(encodeVerificationIdSegment).join(':');
|
|
257
|
+
}
|
|
258
|
+
function encodeVerificationIdSegment(value) {
|
|
259
|
+
return `${value.length}:${encodeURIComponent(value)}`;
|
|
260
|
+
}
|
|
261
|
+
function toCatalogProductType(type) {
|
|
262
|
+
switch (type) {
|
|
263
|
+
case 'Consumable':
|
|
264
|
+
return 'consumable';
|
|
265
|
+
case 'Non-Consumable':
|
|
266
|
+
return 'non_consumable';
|
|
267
|
+
case 'Auto-Renewable Subscription':
|
|
268
|
+
case 'Non-Renewing Subscription':
|
|
269
|
+
return 'subscription';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function assertAppStoreServerApiTransactionResult(input) {
|
|
273
|
+
assertRecord(input, 'AppStoreServerApiTransactionResult');
|
|
274
|
+
if (input.status === 'found') {
|
|
275
|
+
assertNonEmptyString(input.signedTransactionInfo, 'signedTransactionInfo');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (input.status === 'pending' || input.status === 'rejected') {
|
|
279
|
+
assertNonEmptyString(input.reason, 'reason');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
throw new Error('AppStoreServerApiTransactionResult.status is invalid.');
|
|
283
|
+
}
|
|
284
|
+
function assertAppStoreSignedTransactionVerificationResult(input) {
|
|
285
|
+
assertRecord(input, 'AppStoreSignedTransactionVerificationResult');
|
|
286
|
+
if (input.status === 'verified') {
|
|
287
|
+
assertAppStoreTransactionPayload(input.payload);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (input.status === 'rejected') {
|
|
291
|
+
assertNonEmptyString(input.reason, 'reason');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
throw new Error('AppStoreSignedTransactionVerificationResult.status is invalid.');
|
|
295
|
+
}
|
|
296
|
+
export function assertAppStoreTransactionPayload(input) {
|
|
297
|
+
assertRecord(input, 'AppStoreTransactionPayload');
|
|
298
|
+
assertNonEmptyString(input.transactionId, 'transactionId');
|
|
299
|
+
assertNonEmptyString(input.originalTransactionId, 'originalTransactionId');
|
|
300
|
+
assertNonEmptyString(input.bundleId, 'bundleId');
|
|
301
|
+
assertNonEmptyString(input.productId, 'productId');
|
|
302
|
+
assertNonNegativeSafeInteger(input.purchaseDate, 'purchaseDate');
|
|
303
|
+
assertNonNegativeSafeInteger(input.signedDate, 'signedDate');
|
|
304
|
+
assertAppStoreEnvironment(input.environment);
|
|
305
|
+
assertAppStoreInAppPurchaseType(input.type);
|
|
306
|
+
assertOptionalPositiveSafeInteger(input.quantity, 'quantity');
|
|
307
|
+
assertOptionalAppAccountToken(input.appAccountToken);
|
|
308
|
+
assertOptionalNonNegativeSafeInteger(input.expiresDate, 'expiresDate');
|
|
309
|
+
assertOptionalNonNegativeSafeInteger(input.revocationDate, 'revocationDate');
|
|
310
|
+
assertOptionalBoolean(input.isUpgraded, 'isUpgraded');
|
|
311
|
+
return input;
|
|
312
|
+
}
|
|
313
|
+
async function readBoundedJson(response, maxResponseBytes, signal) {
|
|
314
|
+
if (response.body === null) {
|
|
315
|
+
throw new Error('App Store Server API response body is missing.');
|
|
316
|
+
}
|
|
317
|
+
const reader = response.body.getReader();
|
|
318
|
+
const decoder = new TextDecoder();
|
|
319
|
+
let byteLength = 0;
|
|
320
|
+
let text = '';
|
|
321
|
+
try {
|
|
322
|
+
for (;;) {
|
|
323
|
+
let chunk;
|
|
324
|
+
try {
|
|
325
|
+
chunk = await reader.read();
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
if (signal.aborted) {
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
throw new AppStoreDependencyUnavailableError('App Store Server API response stream is unavailable.', error);
|
|
332
|
+
}
|
|
333
|
+
if (chunk.done) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (!(chunk.value instanceof Uint8Array)) {
|
|
337
|
+
throw new Error('App Store Server API response body yielded an invalid chunk.');
|
|
338
|
+
}
|
|
339
|
+
byteLength += chunk.value.byteLength;
|
|
340
|
+
if (byteLength > maxResponseBytes) {
|
|
341
|
+
throw new AppStoreResponseTooLargeError();
|
|
342
|
+
}
|
|
343
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
344
|
+
}
|
|
345
|
+
text += decoder.decode();
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
await reader.cancel(error).catch(() => undefined);
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
reader.releaseLock();
|
|
353
|
+
}
|
|
354
|
+
return JSON.parse(text);
|
|
355
|
+
}
|
|
356
|
+
async function cancelResponseBody(response) {
|
|
357
|
+
await response.body?.cancel().catch(() => undefined);
|
|
358
|
+
}
|
|
359
|
+
function readGlobalFetch() {
|
|
360
|
+
if (typeof globalThis.fetch !== 'function') {
|
|
361
|
+
throw new Error('A fetch implementation is required for App Store Server API requests.');
|
|
362
|
+
}
|
|
363
|
+
return globalThis.fetch;
|
|
364
|
+
}
|
|
365
|
+
function assertAuthorizationValue(input, label) {
|
|
366
|
+
assertNonEmptyString(input, label);
|
|
367
|
+
if (/\s/u.test(input)) {
|
|
368
|
+
throw new Error(`${label} must not contain whitespace or header control characters.`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function normalizeAppAccountToken(input) {
|
|
372
|
+
assertNonEmptyString(input, 'appAccountToken');
|
|
373
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/iu.test(input)) {
|
|
374
|
+
throw new Error('appAccountToken must be a UUID.');
|
|
375
|
+
}
|
|
376
|
+
const normalized = input.toLowerCase();
|
|
377
|
+
if (normalized === '00000000-0000-0000-0000-000000000000') {
|
|
378
|
+
throw new Error('appAccountToken must not be the nil UUID.');
|
|
379
|
+
}
|
|
380
|
+
return normalized;
|
|
381
|
+
}
|
|
382
|
+
function assertOptionalAppAccountToken(input) {
|
|
383
|
+
if (input !== undefined) {
|
|
384
|
+
normalizeAppAccountToken(input);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function assertAppStoreEnvironment(input) {
|
|
388
|
+
if (input !== 'Production' && input !== 'Sandbox') {
|
|
389
|
+
throw new Error('environment must be Production or Sandbox.');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function assertAppStoreInAppPurchaseType(input) {
|
|
393
|
+
if (input !== 'Auto-Renewable Subscription'
|
|
394
|
+
&& input !== 'Consumable'
|
|
395
|
+
&& input !== 'Non-Consumable'
|
|
396
|
+
&& input !== 'Non-Renewing Subscription') {
|
|
397
|
+
throw new Error('type must be a supported App Store in-app purchase type.');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function assertRecord(input, label) {
|
|
401
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
402
|
+
throw new Error(`${label} must be an object.`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function assertNonEmptyString(input, label) {
|
|
406
|
+
if (typeof input !== 'string' || input.length === 0) {
|
|
407
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function assertNonNegativeSafeInteger(input, label) {
|
|
411
|
+
if (!Number.isSafeInteger(input) || input < 0) {
|
|
412
|
+
throw new Error(`${label} must be a non-negative safe integer.`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function assertOptionalNonNegativeSafeInteger(input, label) {
|
|
416
|
+
if (input !== undefined) {
|
|
417
|
+
assertNonNegativeSafeInteger(input, label);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function assertOptionalPositiveSafeInteger(input, label) {
|
|
421
|
+
if (input !== undefined && (!Number.isSafeInteger(input) || input <= 0)) {
|
|
422
|
+
throw new Error(`${label} must be a positive safe integer when provided.`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function assertOptionalBoolean(input, label) {
|
|
426
|
+
if (input !== undefined && typeof input !== 'boolean') {
|
|
427
|
+
throw new Error(`${label} must be a boolean when provided.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function readPositiveSafeInteger(input, label) {
|
|
431
|
+
if (!Number.isSafeInteger(input) || input <= 0) {
|
|
432
|
+
throw new Error(`${label} must be a positive safe integer.`);
|
|
433
|
+
}
|
|
434
|
+
return input;
|
|
435
|
+
}
|
|
436
|
+
function readNonNegativeSafeInteger(input, label) {
|
|
437
|
+
assertNonNegativeSafeInteger(input, label);
|
|
438
|
+
return input;
|
|
439
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type AppsInTossPurchaseAuthority, type AppsInTossPurchaseAuthorityInput, type AppsInTossPurchaseAuthorityResult, type AppsInTossRewardAuthority, type AppsInTossRewardAuthorityInput, type AppsInTossRewardAuthorityResult, type CreateAppsInTossProductionEvidenceVerifierInput } from './apps-in-toss-evidence-verification';
|
|
2
|
+
import type { GameServicesEvidenceVerifier } from './evidence-verification';
|
|
3
|
+
export declare const appsInTossProductionEvidenceConformanceScenarios: readonly ['callback-only-fail-closed', 'purchase-product-grant-callback', 'purchase-authority-retry', 'purchase-pending-order-restore', 'purchase-authority-matching', 'purchase-timestamp-normalization', 'reward-authority-retry-and-replay', 'reward-timestamp-validation', 'authority-errors-and-reward-matching'];
|
|
4
|
+
export type AppsInTossProductionEvidenceConformanceScenario = (typeof appsInTossProductionEvidenceConformanceScenarios)[number];
|
|
5
|
+
export type CreateAppsInTossProductionEvidenceVerifier = (input?: CreateAppsInTossProductionEvidenceVerifierInput) => GameServicesEvidenceVerifier;
|
|
6
|
+
export interface RunAppsInTossProductionEvidenceConformanceInput {
|
|
7
|
+
readonly createVerifier?: CreateAppsInTossProductionEvidenceVerifier;
|
|
8
|
+
readonly now?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AppsInTossProductionEvidenceConformanceReport {
|
|
11
|
+
readonly passedScenarios: readonly AppsInTossProductionEvidenceConformanceScenario[];
|
|
12
|
+
}
|
|
13
|
+
export interface AppsInTossProductionEvidenceAuthorityFixture {
|
|
14
|
+
readonly purchaseAuthority: AppsInTossPurchaseAuthority;
|
|
15
|
+
readonly rewardAuthority: AppsInTossRewardAuthority;
|
|
16
|
+
enqueuePurchaseResult(result: AppsInTossPurchaseAuthorityResult | Error): void;
|
|
17
|
+
enqueueRewardResult(result: AppsInTossRewardAuthorityResult | Error): void;
|
|
18
|
+
readonly purchaseInputs: readonly AppsInTossPurchaseAuthorityInput[];
|
|
19
|
+
readonly rewardInputs: readonly AppsInTossRewardAuthorityInput[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Runs deterministic client-callback, authority, and ledger scenarios without
|
|
23
|
+
* requiring Apps in Toss credentials or platform SDK imports.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runAppsInTossProductionEvidenceConformance(input?: RunAppsInTossProductionEvidenceConformanceInput): Promise<AppsInTossProductionEvidenceConformanceReport>;
|
|
26
|
+
export declare function createAppsInTossProductionEvidenceAuthorityFixture(): AppsInTossProductionEvidenceAuthorityFixture;
|