@mpgd/adapter-capacitor 0.7.4 → 0.8.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/README.md +13 -9
- package/dist/app-store-server.d.ts +12 -12
- package/dist/app-store-server.js +10 -119
- package/dist/providers.d.ts +6 -1
- package/dist/providers.js +29 -2
- package/package.json +7 -12
package/README.md
CHANGED
|
@@ -66,15 +66,19 @@ SDK works on a physical device or that a store/ad setup is release-ready.
|
|
|
66
66
|
|
|
67
67
|
## App Store server-side transaction signatures
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
`@mpgd/
|
|
73
|
-
|
|
74
|
-
the
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
The server-side verifier moved to `@mpgd/game-services/app-store-server`
|
|
70
|
+
(see the [game-services README](../../packages/game-services/README.md)):
|
|
71
|
+
install the optional `@apple/app-store-server-library@3.1.0` peer next to
|
|
72
|
+
`@mpgd/game-services` and import `createAppleSignedTransactionVerifier` from
|
|
73
|
+
there. `@mpgd/adapter-capacitor/app-store-server` is deprecated: it re-exports
|
|
74
|
+
the same function and options type for one release and is removed in the next
|
|
75
|
+
major version. Supply DER-encoded Apple root certificates from the
|
|
76
|
+
[Apple PKI](https://www.apple.com/certificateauthority/), the game's bundle
|
|
77
|
+
ID, environment, and production App Apple ID. Use the result as
|
|
78
|
+
`signedTransactionVerifier` in `createAppStoreGameServicesEvidenceVerifier`.
|
|
79
|
+
Keep App Store API credentials, trust roots, and this verifier on the backend,
|
|
80
|
+
not in the Capacitor game; the adapter itself no longer peers on Apple's
|
|
81
|
+
library.
|
|
78
82
|
|
|
79
83
|
The server fetches the transaction from Apple's API, verifies its JWS, checks
|
|
80
84
|
the product, bundle, environment, account token and transaction ID, then
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
export
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Moved to `@mpgd/game-services/app-store-server` in A4 of the
|
|
3
|
+
* commerce v2 plan: server-side App Store verification does not belong in the
|
|
4
|
+
* client adapter. This subpath re-exports the same symbols for one release and
|
|
5
|
+
* will be removed in the next major version. Install
|
|
6
|
+
* `@apple/app-store-server-library` next to `@mpgd/game-services`.
|
|
7
|
+
*/
|
|
8
|
+
import { createAppleSignedTransactionVerifier as createAppleSignedTransactionVerifierFromGameServices, type CreateAppleSignedTransactionVerifierOptions as GameServicesAppleVerifierOptions } from '@mpgd/game-services/app-store-server';
|
|
9
|
+
/** @deprecated Import `createAppleSignedTransactionVerifier` from `@mpgd/game-services/app-store-server`. */
|
|
10
|
+
export declare const createAppleSignedTransactionVerifier: typeof createAppleSignedTransactionVerifierFromGameServices;
|
|
11
|
+
/** @deprecated Import `CreateAppleSignedTransactionVerifierOptions` from `@mpgd/game-services/app-store-server`. */
|
|
12
|
+
export type CreateAppleSignedTransactionVerifierOptions = GameServicesAppleVerifierOptions;
|
package/dist/app-store-server.js
CHANGED
|
@@ -1,119 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (options.environment !== 'Production' && options.environment !== 'Sandbox') {
|
|
12
|
-
throw new TypeError('App Store environment must be Production or Sandbox.');
|
|
13
|
-
}
|
|
14
|
-
if (options.bundleId.trim() === '') {
|
|
15
|
-
throw new TypeError('App Store bundle ID is required.');
|
|
16
|
-
}
|
|
17
|
-
if (options.environment === 'Production'
|
|
18
|
-
&& (!Number.isSafeInteger(options.appAppleId) || (options.appAppleId ?? 0) <= 0)) {
|
|
19
|
-
throw new TypeError('Production App Store verification requires appAppleId.');
|
|
20
|
-
}
|
|
21
|
-
if (options.environment === 'Production' && options.enableOnlineChecks === false) {
|
|
22
|
-
throw new TypeError('Production App Store verification requires online certificate checks.');
|
|
23
|
-
}
|
|
24
|
-
const verifier = new SignedDataVerifier(options.rootCertificates.map((certificate) => Buffer.from(certificate)), options.enableOnlineChecks ?? true, options.environment === 'Production' ? Environment.PRODUCTION : Environment.SANDBOX, options.bundleId, options.appAppleId);
|
|
25
|
-
return {
|
|
26
|
-
async verifyAndDecode(input) {
|
|
27
|
-
if (input.environment !== options.environment || input.bundleId !== options.bundleId) {
|
|
28
|
-
return { status: 'rejected', reason: 'APP_STORE_VERIFIER_SCOPE_MISMATCH' };
|
|
29
|
-
}
|
|
30
|
-
if (typeof input.signedTransaction !== 'string'
|
|
31
|
-
|| input.signedTransaction.length === 0
|
|
32
|
-
|| input.signedTransaction.length > 512 * 1024) {
|
|
33
|
-
return { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_INVALID' };
|
|
34
|
-
}
|
|
35
|
-
input.signal.throwIfAborted();
|
|
36
|
-
let decoded;
|
|
37
|
-
try {
|
|
38
|
-
decoded = await waitForVerification(verifier.verifyAndDecodeTransaction(input.signedTransaction), input.signal);
|
|
39
|
-
input.signal.throwIfAborted();
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (input.signal.aborted) {
|
|
43
|
-
throw input.signal.reason;
|
|
44
|
-
}
|
|
45
|
-
if (error instanceof VerificationException) {
|
|
46
|
-
if (error.status === VerificationStatus.RETRYABLE_VERIFICATION_FAILURE) {
|
|
47
|
-
throw new AppStoreDependencyUnavailableError('Apple signature checks are temporarily unavailable.', error);
|
|
48
|
-
}
|
|
49
|
-
return { status: 'rejected', reason: 'APP_STORE_SIGNATURE_INVALID' };
|
|
50
|
-
}
|
|
51
|
-
// Only Apple's explicit retryable status is a transient dependency
|
|
52
|
-
// failure. Unexpected adapter/library faults must reach the caller.
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
const payload = decodeOneTimeTransaction(decoded);
|
|
56
|
-
return payload === undefined
|
|
57
|
-
? { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_INVALID' }
|
|
58
|
-
: { status: 'verified', payload };
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
async function waitForVerification(operation, signal) {
|
|
63
|
-
signal.throwIfAborted();
|
|
64
|
-
let onAbort;
|
|
65
|
-
const aborted = new Promise((_resolve, reject) => {
|
|
66
|
-
onAbort = () => reject(signal.reason ?? new Error('Verification was aborted.'));
|
|
67
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
68
|
-
if (signal.aborted) {
|
|
69
|
-
onAbort();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
try {
|
|
73
|
-
return await Promise.race([operation, aborted]);
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
if (onAbort !== undefined) {
|
|
77
|
-
signal.removeEventListener('abort', onAbort);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function decodeOneTimeTransaction(decoded) {
|
|
82
|
-
if (!nonEmpty(decoded.transactionId)
|
|
83
|
-
|| !nonEmpty(decoded.originalTransactionId)
|
|
84
|
-
|| !nonEmpty(decoded.bundleId)
|
|
85
|
-
|| !nonEmpty(decoded.productId)
|
|
86
|
-
|| !validTimestamp(decoded.purchaseDate)
|
|
87
|
-
|| !validTimestamp(decoded.signedDate)
|
|
88
|
-
|| (decoded.environment !== 'Production' && decoded.environment !== 'Sandbox')
|
|
89
|
-
|| (decoded.type !== 'Consumable' && decoded.type !== 'Non-Consumable')
|
|
90
|
-
|| (decoded.quantity !== undefined
|
|
91
|
-
&& (!Number.isSafeInteger(decoded.quantity) || decoded.quantity <= 0))
|
|
92
|
-
|| (decoded.appAccountToken !== undefined && !nonEmpty(decoded.appAccountToken))
|
|
93
|
-
|| (decoded.revocationDate !== undefined && !validTimestamp(decoded.revocationDate))
|
|
94
|
-
|| (decoded.expiresDate !== undefined && !validTimestamp(decoded.expiresDate))
|
|
95
|
-
|| (decoded.isUpgraded !== undefined && typeof decoded.isUpgraded !== 'boolean')) {
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
transactionId: decoded.transactionId,
|
|
100
|
-
originalTransactionId: decoded.originalTransactionId,
|
|
101
|
-
bundleId: decoded.bundleId,
|
|
102
|
-
productId: decoded.productId,
|
|
103
|
-
purchaseDate: decoded.purchaseDate,
|
|
104
|
-
signedDate: decoded.signedDate,
|
|
105
|
-
environment: decoded.environment,
|
|
106
|
-
type: decoded.type,
|
|
107
|
-
...(decoded.quantity === undefined ? {} : { quantity: decoded.quantity }),
|
|
108
|
-
...(decoded.appAccountToken === undefined ? {} : { appAccountToken: decoded.appAccountToken }),
|
|
109
|
-
...(decoded.revocationDate === undefined ? {} : { revocationDate: decoded.revocationDate }),
|
|
110
|
-
...(decoded.expiresDate === undefined ? {} : { expiresDate: decoded.expiresDate }),
|
|
111
|
-
...(decoded.isUpgraded === undefined ? {} : { isUpgraded: decoded.isUpgraded }),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
function nonEmpty(value) {
|
|
115
|
-
return typeof value === 'string' && value.trim() !== '';
|
|
116
|
-
}
|
|
117
|
-
function validTimestamp(value) {
|
|
118
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
119
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Moved to `@mpgd/game-services/app-store-server` in A4 of the
|
|
3
|
+
* commerce v2 plan: server-side App Store verification does not belong in the
|
|
4
|
+
* client adapter. This subpath re-exports the same symbols for one release and
|
|
5
|
+
* will be removed in the next major version. Install
|
|
6
|
+
* `@apple/app-store-server-library` next to `@mpgd/game-services`.
|
|
7
|
+
*/
|
|
8
|
+
import { createAppleSignedTransactionVerifier as createAppleSignedTransactionVerifierFromGameServices, } from '@mpgd/game-services/app-store-server';
|
|
9
|
+
/** @deprecated Import `createAppleSignedTransactionVerifier` from `@mpgd/game-services/app-store-server`. */
|
|
10
|
+
export const createAppleSignedTransactionVerifier = createAppleSignedTransactionVerifierFromGameServices;
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BridgeEvent, type BridgeMethod, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
|
|
2
2
|
import type { AdProvider } from '@mpgd/platform/ads';
|
|
3
3
|
import { type PlatformProviderAvailability, type PlatformProviderFeature, type ProductType } from '@mpgd/platform';
|
|
4
4
|
export interface NativeBridge {
|
|
5
5
|
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
6
|
+
/**
|
|
7
|
+
* Optional commerce v2 event delivery (`{ protocol: 'mpgd.commerce.v2', sequence, event }`).
|
|
8
|
+
* B1 wires it for store providers; the base gateway works without it.
|
|
9
|
+
*/
|
|
10
|
+
subscribe?(listener: (event: BridgeEvent) => void): () => void;
|
|
6
11
|
}
|
|
7
12
|
/**
|
|
8
13
|
* An optional, separately installed provider handles only its declared bridge
|
package/dist/providers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBridgeCommerceV2Method, } from '@mpgd/bridge';
|
|
1
2
|
import { isAdMobClientRewardEvidence } from '@mpgd/game-services/admob-client-reward';
|
|
2
3
|
import { PlatformOperationError, } from '@mpgd/platform';
|
|
3
4
|
const featureMethods = {
|
|
@@ -53,6 +54,12 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
53
54
|
throw new Error(`Capacitor provider ${provider.id} repeats a bridge method.`);
|
|
54
55
|
}
|
|
55
56
|
for (const method of methods) {
|
|
57
|
+
if (isBridgeCommerceV2Method(method)) {
|
|
58
|
+
// v1 providers own the v1 commerce methods only; a v2 provider arrives
|
|
59
|
+
// with B1 and is registered through its own surface.
|
|
60
|
+
throw new Error(`Capacitor provider ${provider.id} cannot declare commerce v2 method ${method}; `
|
|
61
|
+
+ 'commerce v2 is unavailable through the provider registry.');
|
|
62
|
+
}
|
|
56
63
|
if (!providerMethodSet.has(method)) {
|
|
57
64
|
throw new Error(`Capacitor provider ${provider.id} cannot replace base method ${method}.`);
|
|
58
65
|
}
|
|
@@ -233,7 +240,8 @@ function isValidProviderData(method, value) {
|
|
|
233
240
|
&& value.authoritativeGrant.ledgerEntryId.length > 0
|
|
234
241
|
&& (value.authoritativeGrant.alreadyProcessed === undefined
|
|
235
242
|
|| typeof value.authoritativeGrant.alreadyProcessed === 'boolean')))
|
|
236
|
-
&& (value.evidence === undefined || isEvidence(value.evidence))
|
|
243
|
+
&& (value.evidence === undefined || isEvidence(value.evidence))
|
|
244
|
+
&& (value.diagnostic === undefined || isCommerceDiagnostic(value.diagnostic));
|
|
237
245
|
case 'commerce.restore':
|
|
238
246
|
return isRecord(value)
|
|
239
247
|
&& Array.isArray(value.restoredEntitlements)
|
|
@@ -245,7 +253,10 @@ function isValidProviderData(method, value) {
|
|
|
245
253
|
&& ['granted', 'refunded'].includes(settlement.status)
|
|
246
254
|
&& (settlement.ledgerEntryId === undefined || isString(settlement.ledgerEntryId))
|
|
247
255
|
&& (settlement.alreadyProcessed === undefined
|
|
248
|
-
|| typeof settlement.alreadyProcessed === 'boolean')
|
|
256
|
+
|| typeof settlement.alreadyProcessed === 'boolean')
|
|
257
|
+
&& (settlement.idempotencyKey === undefined
|
|
258
|
+
|| isIdempotencyKey(settlement.idempotencyKey)))))
|
|
259
|
+
&& (value.diagnostic === undefined || isCommerceDiagnostic(value.diagnostic));
|
|
249
260
|
case 'commerce.getEntitlements':
|
|
250
261
|
return Array.isArray(value) && value.every(isEntitlement);
|
|
251
262
|
case 'ads.preload':
|
|
@@ -304,6 +315,22 @@ function isEntitlement(value) {
|
|
|
304
315
|
&& isString(value.grantedAt)
|
|
305
316
|
&& (value.expiresAt === undefined || isString(value.expiresAt));
|
|
306
317
|
}
|
|
318
|
+
/** Mirrors the stable, non-sensitive code format of `PlatformOperationError`. */
|
|
319
|
+
function isDiagnosticCode(value) {
|
|
320
|
+
return isString(value) && value.length <= 128 && /^[A-Z][A-Z0-9_:-]*$/u.test(value);
|
|
321
|
+
}
|
|
322
|
+
function isCommerceDiagnostic(value) {
|
|
323
|
+
return isRecord(value)
|
|
324
|
+
&& isDiagnosticCode(value.code)
|
|
325
|
+
&& typeof value.retryable === 'boolean'
|
|
326
|
+
&& (value.providerCode === undefined || isDiagnosticCode(value.providerCode));
|
|
327
|
+
}
|
|
328
|
+
function isIdempotencyKey(value) {
|
|
329
|
+
return isString(value)
|
|
330
|
+
&& value.length > 0
|
|
331
|
+
&& value.length <= 256
|
|
332
|
+
&& !/[\p{Cc}\p{Cf}]/u.test(value);
|
|
333
|
+
}
|
|
307
334
|
function isEvidence(value) {
|
|
308
335
|
return isRecord(value)
|
|
309
336
|
&& isString(value.schema)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-capacitor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Capacitor platform adapter for mpgd Android and iOS shells.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,33 +43,28 @@
|
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@capacitor/app": "8.1.1",
|
|
46
|
-
"@mpgd/
|
|
47
|
-
"@mpgd/
|
|
48
|
-
"@mpgd/
|
|
49
|
-
"@mpgd/
|
|
50
|
-
"@mpgd/
|
|
46
|
+
"@mpgd/capacitor-game-services": "0.6.2",
|
|
47
|
+
"@mpgd/bridge": "0.12.0",
|
|
48
|
+
"@mpgd/platform": "0.19.0",
|
|
49
|
+
"@mpgd/game-services": "0.20.0",
|
|
50
|
+
"@mpgd/target-config": "0.21.2"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@apple/app-store-server-library": "^3.1.0",
|
|
54
53
|
"@capacitor-community/admob": "^8.1.0",
|
|
55
54
|
"@capacitor/core": "^8.5.1"
|
|
56
55
|
},
|
|
57
56
|
"peerDependenciesMeta": {
|
|
58
|
-
"@apple/app-store-server-library": {
|
|
59
|
-
"optional": true
|
|
60
|
-
},
|
|
61
57
|
"@capacitor-community/admob": {
|
|
62
58
|
"optional": true
|
|
63
59
|
}
|
|
64
60
|
},
|
|
65
61
|
"devDependencies": {
|
|
66
|
-
"@apple/app-store-server-library": "3.1.0",
|
|
67
62
|
"@capacitor-community/admob": "8.1.0",
|
|
68
63
|
"@capacitor/core": "8.5.2",
|
|
69
64
|
"ttsc": "0.30.4",
|
|
70
65
|
"typescript": "7.0.2",
|
|
71
66
|
"vitest": "^4.0.0",
|
|
72
|
-
"@mpgd/game-runtime": "0.6.
|
|
67
|
+
"@mpgd/game-runtime": "0.6.2"
|
|
73
68
|
},
|
|
74
69
|
"main": "./dist/index.js",
|
|
75
70
|
"types": "./dist/index.d.ts",
|