@mpgd/adapter-capacitor 0.4.12 → 0.6.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 +318 -0
- package/dist/admob.d.ts +26 -0
- package/dist/admob.js +288 -0
- package/dist/app-events.d.ts +23 -0
- package/dist/app-events.js +332 -0
- package/dist/app-store-server.d.ts +12 -0
- package/dist/app-store-server.js +119 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +162 -13
- package/dist/native-http.d.ts +50 -0
- package/dist/native-http.js +259 -0
- package/dist/providers.d.ts +25 -0
- package/dist/providers.js +312 -0
- package/dist/viewport.d.ts +25 -0
- package/dist/viewport.js +221 -0
- package/package.json +29 -5
package/dist/index.js
CHANGED
|
@@ -1,14 +1,121 @@
|
|
|
1
|
-
import { decodeBridgeStorageLoadData, } from '@mpgd/bridge';
|
|
1
|
+
import { decodeBridgeCredentialLoadData, decodeBridgeStorageLoadData, } from '@mpgd/bridge';
|
|
2
2
|
import { CapacitorGameServices } from '@mpgd/capacitor-game-services';
|
|
3
3
|
import { PlatformOperationError, } from '@mpgd/platform';
|
|
4
|
+
import { createCapacitorAppEvents, } from './app-events.js';
|
|
5
|
+
import { assertProviderResponseData, createCapacitorProviderRegistry, } from './providers.js';
|
|
6
|
+
import { createCapacitorViewport } from './viewport.js';
|
|
7
|
+
export { createCapacitorProviderRegistry, } from './providers.js';
|
|
8
|
+
export { createCapacitorAppEvents, } from './app-events.js';
|
|
9
|
+
export { CapacitorNativeHttpError, createCapacitorNativeJsonTransport, } from './native-http.js';
|
|
10
|
+
export { createCapacitorViewport, } from './viewport.js';
|
|
11
|
+
const externalActivityMethods = new Set([
|
|
12
|
+
'commerce.purchase',
|
|
13
|
+
'ads.showRewarded',
|
|
14
|
+
'ads.showInterstitial',
|
|
15
|
+
'identity.requestUpgrade',
|
|
16
|
+
'notifications.requestSubscription',
|
|
17
|
+
]);
|
|
4
18
|
function isRecord(value) {
|
|
5
19
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
20
|
}
|
|
21
|
+
const safeFallbackMethods = new Set([
|
|
22
|
+
'identity.getPlayer',
|
|
23
|
+
'identity.getSession',
|
|
24
|
+
'notifications.getStatus',
|
|
25
|
+
]);
|
|
26
|
+
const credentialKeyPattern = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
27
|
+
const maximumCredentialBytes = 16 * 1024;
|
|
28
|
+
function assertCredentialKey(key) {
|
|
29
|
+
if (typeof key !== 'string' || !credentialKeyPattern.test(key)) {
|
|
30
|
+
throw new PlatformOperationError({ code: 'NATIVE_CREDENTIAL_INVALID_KEY' });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function assertCredentialValue(value) {
|
|
34
|
+
if (typeof value !== 'string' || value.length === 0
|
|
35
|
+
|| new TextEncoder().encode(value).byteLength > maximumCredentialBytes) {
|
|
36
|
+
throw new PlatformOperationError({ code: 'NATIVE_CREDENTIAL_INVALID_VALUE' });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function assertCredentialMutationResponse(input, field) {
|
|
40
|
+
if (!isRecord(input) || input[field] !== true) {
|
|
41
|
+
throw new PlatformOperationError({ code: 'NATIVE_CREDENTIAL_INVALID_RESPONSE' });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
7
44
|
export function createCapacitorPlatformGateway(input) {
|
|
8
45
|
const bridge = input.bridge ?? CapacitorGameServices;
|
|
9
|
-
|
|
46
|
+
const providers = createCapacitorProviderRegistry(input.providers ?? []);
|
|
47
|
+
const lifecycle = createCapacitorAppEvents({
|
|
48
|
+
target: input.target,
|
|
49
|
+
...(input.app === undefined ? {} : { app: input.app }),
|
|
50
|
+
...(input.visibility === undefined ? {} : { visibility: input.visibility }),
|
|
51
|
+
...(input.classifyIncomingUrl === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { classifyIncomingUrl: input.classifyIncomingUrl }),
|
|
54
|
+
...(input.historyBack === undefined ? {} : { historyBack: input.historyBack }),
|
|
55
|
+
...(input.onAppEventError === undefined ? {} : { onError: input.onAppEventError }),
|
|
56
|
+
});
|
|
57
|
+
const viewport = input.viewport ?? createCapacitorViewport({
|
|
58
|
+
...(input.onAppEventError === undefined ? {} : { onError: input.onAppEventError }),
|
|
59
|
+
});
|
|
60
|
+
const gatewayLifecycle = {
|
|
61
|
+
...lifecycle,
|
|
62
|
+
async dispose() {
|
|
63
|
+
try {
|
|
64
|
+
await lifecycle.dispose?.();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
if (input.viewport === undefined) {
|
|
68
|
+
viewport.dispose?.();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
async function request(method, payload, leaderboardRoute) {
|
|
74
|
+
const releaseExternalActivity = externalActivityMethods.has(method)
|
|
75
|
+
? lifecycle.beginExternalActivity?.()
|
|
76
|
+
: undefined;
|
|
77
|
+
try {
|
|
78
|
+
const provider = leaderboardRoute === 'remote'
|
|
79
|
+
&& (method === 'leaderboard.submitScore' || method === 'leaderboard.open')
|
|
80
|
+
? undefined
|
|
81
|
+
: providers.byMethod.get(method);
|
|
82
|
+
if (provider !== undefined) {
|
|
83
|
+
let productType;
|
|
84
|
+
if (method === 'commerce.purchase'
|
|
85
|
+
&& provider.features.includes('nativeIap')
|
|
86
|
+
&& provider.features.includes('subscriptionIap')) {
|
|
87
|
+
// The caller supplies only an ID. Resolve its provider-owned type before
|
|
88
|
+
// selecting a store route; a forged or unknown ID must not bypass it.
|
|
89
|
+
const products = await request('commerce.getProducts', {});
|
|
90
|
+
const productId = isRecord(payload) ? payload.productId : undefined;
|
|
91
|
+
const matches = products.filter((product) => product.id === productId);
|
|
92
|
+
if (matches.length !== 1) {
|
|
93
|
+
throw new PlatformOperationError({ code: 'NATIVE_PROVIDER_PRODUCT_UNKNOWN' });
|
|
94
|
+
}
|
|
95
|
+
productType = matches[0]?.type;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await providers.assertMethodReady(method, payload, productType);
|
|
99
|
+
return await sendRequest(method, payload, provider);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// Only non-mutating guest/status queries may degrade. A stale readiness
|
|
103
|
+
// snapshot, SDK rejection, or malformed provider response is no safer
|
|
104
|
+
// than initialization failure for these queries.
|
|
105
|
+
if (!safeFallbackMethods.has(method)) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return await sendRequest(method, payload, undefined);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
releaseExternalActivity?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function sendRequest(method, payload, provider) {
|
|
10
117
|
const id = crypto.randomUUID();
|
|
11
|
-
const response = await bridge.request({
|
|
118
|
+
const response = await (provider?.bridge ?? bridge).request({
|
|
12
119
|
id,
|
|
13
120
|
method,
|
|
14
121
|
payload,
|
|
@@ -32,6 +139,9 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
32
139
|
message: `Native bridge omitted response data for ${method}.`,
|
|
33
140
|
});
|
|
34
141
|
}
|
|
142
|
+
if (provider !== undefined) {
|
|
143
|
+
assertProviderResponseData(method, response.data);
|
|
144
|
+
}
|
|
35
145
|
return response.data;
|
|
36
146
|
}
|
|
37
147
|
if (!isRecord(response.error)
|
|
@@ -48,7 +158,22 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
48
158
|
return {
|
|
49
159
|
target: input.target,
|
|
50
160
|
async getCapabilities() {
|
|
51
|
-
|
|
161
|
+
const base = await request('runtime.getCapabilities', {});
|
|
162
|
+
const states = await providers.getAvailability();
|
|
163
|
+
const rewardedAds = states.rewardedAds === 'available';
|
|
164
|
+
const interstitialAds = states.interstitialAds === 'available';
|
|
165
|
+
const bannerAds = states.bannerAds === 'available';
|
|
166
|
+
return {
|
|
167
|
+
...base,
|
|
168
|
+
nativeIap: states.nativeIap === 'available',
|
|
169
|
+
subscriptionIap: states.subscriptionIap === 'available',
|
|
170
|
+
nativeAds: rewardedAds || interstitialAds || bannerAds,
|
|
171
|
+
rewardedAds,
|
|
172
|
+
interstitialAds,
|
|
173
|
+
bannerAds,
|
|
174
|
+
nativeLeaderboard: states.nativeLeaderboard === 'available',
|
|
175
|
+
providerAvailability: states,
|
|
176
|
+
};
|
|
52
177
|
},
|
|
53
178
|
identity: {
|
|
54
179
|
getPlayer: () => request('identity.getPlayer', {}),
|
|
@@ -77,24 +202,48 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
77
202
|
preload: (payload) => request('ads.preload', payload),
|
|
78
203
|
showRewarded: (payload) => request('ads.showRewarded', payload),
|
|
79
204
|
showInterstitial: (payload) => request('ads.showInterstitial', payload),
|
|
205
|
+
mountBanner: (payload) => request('ads.mountBanner', payload),
|
|
206
|
+
unmountBanner: (payload) => request('ads.unmountBanner', payload),
|
|
80
207
|
},
|
|
81
208
|
leaderboard: {
|
|
82
|
-
submitScore: (payload) => request('leaderboard.submitScore', payload),
|
|
83
|
-
open: (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
onPause() {
|
|
87
|
-
return () => { };
|
|
88
|
-
},
|
|
89
|
-
onResume() {
|
|
90
|
-
return () => { };
|
|
209
|
+
submitScore: ({ route, ...payload }) => request('leaderboard.submitScore', payload, route),
|
|
210
|
+
open: (input) => {
|
|
211
|
+
const { route, ...payload } = input ?? {};
|
|
212
|
+
return request('leaderboard.open', payload, route);
|
|
91
213
|
},
|
|
92
214
|
},
|
|
215
|
+
lifecycle: gatewayLifecycle,
|
|
216
|
+
viewport,
|
|
93
217
|
storage: {
|
|
94
218
|
async load(payload) {
|
|
95
219
|
return decodeBridgeStorageLoadData(await request('storage.load', payload));
|
|
96
220
|
},
|
|
97
221
|
save: (payload) => request('storage.save', payload),
|
|
98
222
|
},
|
|
223
|
+
secureCredentials: {
|
|
224
|
+
async load({ key }) {
|
|
225
|
+
assertCredentialKey(key);
|
|
226
|
+
const data = await request('credentials.load', { key });
|
|
227
|
+
try {
|
|
228
|
+
const value = decodeBridgeCredentialLoadData(data);
|
|
229
|
+
if (value !== null) {
|
|
230
|
+
assertCredentialValue(value);
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
throw new PlatformOperationError({ code: 'NATIVE_CREDENTIAL_INVALID_RESPONSE' });
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
async save({ key, value }) {
|
|
239
|
+
assertCredentialKey(key);
|
|
240
|
+
assertCredentialValue(value);
|
|
241
|
+
assertCredentialMutationResponse(await request('credentials.save', { key, value }), 'saved');
|
|
242
|
+
},
|
|
243
|
+
async remove({ key }) {
|
|
244
|
+
assertCredentialKey(key);
|
|
245
|
+
assertCredentialMutationResponse(await request('credentials.remove', { key }), 'removed');
|
|
246
|
+
},
|
|
247
|
+
},
|
|
99
248
|
};
|
|
100
249
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CapacitorHttp } from '@capacitor/core';
|
|
2
|
+
export type CapacitorNativeHttpErrorCode = 'NATIVE_HTTP_CONFIGURATION' | 'NATIVE_HTTP_UNSUPPORTED' | 'NATIVE_HTTP_INVALID_REQUEST' | 'NATIVE_HTTP_REQUEST_TOO_LARGE' | 'NATIVE_HTTP_RESPONSE_TOO_LARGE' | 'NATIVE_HTTP_INVALID_RESPONSE' | 'NATIVE_HTTP_INVALID_JSON' | 'NATIVE_HTTP_REDIRECT_BLOCKED' | 'NATIVE_HTTP_CROSS_ORIGIN' | 'NATIVE_HTTP_TIMEOUT' | 'NATIVE_HTTP_ABORTED' | 'NATIVE_HTTP_FAILED';
|
|
3
|
+
/** Deliberately omits URLs, headers, and native SDK exception details. */
|
|
4
|
+
export declare class CapacitorNativeHttpError extends Error {
|
|
5
|
+
readonly code: CapacitorNativeHttpErrorCode;
|
|
6
|
+
constructor(code: CapacitorNativeHttpErrorCode);
|
|
7
|
+
}
|
|
8
|
+
export interface CapacitorJsonRequest {
|
|
9
|
+
readonly method: 'GET' | 'POST';
|
|
10
|
+
/** Relative API path beginning with one slash; a query string is allowed. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly body?: unknown;
|
|
13
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
14
|
+
/** Stops waiting in JS; it does not cancel an in-flight native request. */
|
|
15
|
+
readonly signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
export interface CapacitorJsonResponse {
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly body: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A narrow JSON API that structurally satisfies GameServicesBackendTransport
|
|
23
|
+
* without importing game-services or pretending to implement Fetch.
|
|
24
|
+
*/
|
|
25
|
+
export interface CapacitorNativeJsonTransport {
|
|
26
|
+
request(input: CapacitorJsonRequest): Promise<CapacitorJsonResponse>;
|
|
27
|
+
send(input: {
|
|
28
|
+
readonly method: 'POST';
|
|
29
|
+
readonly endpoint: string;
|
|
30
|
+
readonly body: unknown;
|
|
31
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
32
|
+
}): Promise<CapacitorJsonResponse>;
|
|
33
|
+
}
|
|
34
|
+
export interface CreateCapacitorNativeJsonTransportInput {
|
|
35
|
+
readonly target: 'android' | 'ios';
|
|
36
|
+
readonly baseUrl: string;
|
|
37
|
+
/** Exact HTTPS origin of the API; initial requests never use another origin. */
|
|
38
|
+
readonly allowedOrigin: string;
|
|
39
|
+
readonly connectTimeoutMs?: number;
|
|
40
|
+
readonly readTimeoutMs?: number;
|
|
41
|
+
readonly overallTimeoutMs?: number;
|
|
42
|
+
readonly maxRequestBytes?: number;
|
|
43
|
+
/** Checked after the native bridge has already returned data to JS. */
|
|
44
|
+
readonly maxResponseBytes?: number;
|
|
45
|
+
/** Inject only for tests or a host-owned native Http implementation. */
|
|
46
|
+
readonly http?: Pick<typeof CapacitorHttp, 'request'>;
|
|
47
|
+
/** Inject only for deterministic tests. */
|
|
48
|
+
readonly getPlatform?: () => string;
|
|
49
|
+
}
|
|
50
|
+
export declare function createCapacitorNativeJsonTransport(input: CreateCapacitorNativeJsonTransportInput): CapacitorNativeJsonTransport;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { Capacitor, CapacitorHttp } from '@capacitor/core';
|
|
2
|
+
/** Deliberately omits URLs, headers, and native SDK exception details. */
|
|
3
|
+
export class CapacitorNativeHttpError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code) {
|
|
6
|
+
super(`Capacitor native JSON request failed: ${code}.`);
|
|
7
|
+
this.name = 'CapacitorNativeHttpError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const defaultMaxRequestBytes = 256 * 1024;
|
|
12
|
+
const defaultMaxResponseBytes = 1024 * 1024;
|
|
13
|
+
const defaultConnectTimeoutMs = 10_000;
|
|
14
|
+
const defaultReadTimeoutMs = 15_000;
|
|
15
|
+
const defaultOverallTimeoutMs = 25_000;
|
|
16
|
+
const headerNamePattern = /^[!#$%&'*+.^_`|~0-9a-z-]+$/u;
|
|
17
|
+
export function createCapacitorNativeJsonTransport(input) {
|
|
18
|
+
const base = parseHttpsUrl(input.baseUrl);
|
|
19
|
+
const allowedOrigin = parseHttpsUrl(input.allowedOrigin);
|
|
20
|
+
if (allowedOrigin.href !== `${allowedOrigin.origin}/` || allowedOrigin.origin !== base.origin
|
|
21
|
+
|| base.username !== '' || base.password !== ''
|
|
22
|
+
|| base.search !== '' || base.hash !== '') {
|
|
23
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_CONFIGURATION');
|
|
24
|
+
}
|
|
25
|
+
const basePath = base.pathname.replace(/\/+$/u, '');
|
|
26
|
+
const maxRequestBytes = positiveInteger(input.maxRequestBytes, defaultMaxRequestBytes);
|
|
27
|
+
const maxResponseBytes = positiveInteger(input.maxResponseBytes, defaultMaxResponseBytes);
|
|
28
|
+
const connectTimeoutMs = positiveInteger(input.connectTimeoutMs, defaultConnectTimeoutMs);
|
|
29
|
+
const readTimeoutMs = positiveInteger(input.readTimeoutMs, defaultReadTimeoutMs);
|
|
30
|
+
const overallTimeoutMs = positiveInteger(input.overallTimeoutMs, defaultOverallTimeoutMs);
|
|
31
|
+
const http = input.http ?? CapacitorHttp;
|
|
32
|
+
const getPlatform = input.getPlatform ?? (() => Capacitor.getPlatform());
|
|
33
|
+
const encoder = new TextEncoder();
|
|
34
|
+
async function request(inputRequest) {
|
|
35
|
+
if (getPlatform() !== input.target) {
|
|
36
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_UNSUPPORTED');
|
|
37
|
+
}
|
|
38
|
+
if (inputRequest.signal?.aborted === true) {
|
|
39
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_ABORTED');
|
|
40
|
+
}
|
|
41
|
+
const url = resolvePath(inputRequest.path, base, basePath);
|
|
42
|
+
const headers = normalizeHeaders(inputRequest.headers);
|
|
43
|
+
let data;
|
|
44
|
+
let serializedData;
|
|
45
|
+
if (inputRequest.method === 'GET') {
|
|
46
|
+
if (inputRequest.body !== undefined) {
|
|
47
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (inputRequest.method === 'POST') {
|
|
51
|
+
try {
|
|
52
|
+
assertJsonValue(inputRequest.body, new WeakSet(), 0);
|
|
53
|
+
serializedData = JSON.stringify(inputRequest.body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
57
|
+
}
|
|
58
|
+
if (serializedData === undefined) {
|
|
59
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
60
|
+
}
|
|
61
|
+
if (encoder.encode(serializedData).byteLength > maxRequestBytes) {
|
|
62
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_REQUEST_TOO_LARGE');
|
|
63
|
+
}
|
|
64
|
+
// Native serializers must receive a JSON value, never its serialized
|
|
65
|
+
// string. Parse the size-checked snapshot so caller mutation cannot
|
|
66
|
+
// change the native request after validation.
|
|
67
|
+
data = JSON.parse(serializedData);
|
|
68
|
+
headers['content-type'] = 'application/json';
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
72
|
+
}
|
|
73
|
+
headers.accept = 'application/json';
|
|
74
|
+
const options = {
|
|
75
|
+
url: url.href,
|
|
76
|
+
method: inputRequest.method,
|
|
77
|
+
headers,
|
|
78
|
+
responseType: 'json',
|
|
79
|
+
disableRedirects: true,
|
|
80
|
+
connectTimeout: connectTimeoutMs,
|
|
81
|
+
readTimeout: readTimeoutMs,
|
|
82
|
+
...(inputRequest.method === 'POST' ? { data } : {}),
|
|
83
|
+
};
|
|
84
|
+
const response = await waitForNativeResponse(http, options, inputRequest.signal, overallTimeoutMs);
|
|
85
|
+
if (!isRecord(response) || !Number.isInteger(response.status)
|
|
86
|
+
|| response.status < 100 || response.status > 599
|
|
87
|
+
|| typeof response.url !== 'string') {
|
|
88
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_RESPONSE');
|
|
89
|
+
}
|
|
90
|
+
if (response.status >= 300 && response.status < 400) {
|
|
91
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_REDIRECT_BLOCKED');
|
|
92
|
+
}
|
|
93
|
+
let responseUrl;
|
|
94
|
+
try {
|
|
95
|
+
responseUrl = new URL(response.url);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_RESPONSE');
|
|
99
|
+
}
|
|
100
|
+
if (responseUrl.origin !== url.origin) {
|
|
101
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_CROSS_ORIGIN');
|
|
102
|
+
}
|
|
103
|
+
if (responseUrl.username !== '' || responseUrl.password !== '' || responseUrl.hash !== ''
|
|
104
|
+
|| normalizeNativePath(responseUrl.pathname) !== normalizeNativePath(url.pathname)
|
|
105
|
+
|| new URLSearchParams(responseUrl.search).toString()
|
|
106
|
+
!== new URLSearchParams(url.search).toString()) {
|
|
107
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_REDIRECT_BLOCKED');
|
|
108
|
+
}
|
|
109
|
+
const raw = response.data;
|
|
110
|
+
let serialized;
|
|
111
|
+
try {
|
|
112
|
+
serialized = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_JSON');
|
|
116
|
+
}
|
|
117
|
+
if (encoder.encode(serialized ?? '').byteLength > maxResponseBytes) {
|
|
118
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_RESPONSE_TOO_LARGE');
|
|
119
|
+
}
|
|
120
|
+
if (raw === null || raw === undefined || (typeof raw === 'string' && raw.trim() === '')) {
|
|
121
|
+
return { status: response.status, body: null };
|
|
122
|
+
}
|
|
123
|
+
let body;
|
|
124
|
+
try {
|
|
125
|
+
body = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
126
|
+
assertJsonValue(body, new WeakSet(), 0);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_JSON');
|
|
130
|
+
}
|
|
131
|
+
return { status: response.status, body };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
request,
|
|
135
|
+
send(inputRequest) {
|
|
136
|
+
return request({
|
|
137
|
+
method: 'POST',
|
|
138
|
+
path: inputRequest.endpoint,
|
|
139
|
+
body: inputRequest.body,
|
|
140
|
+
...(inputRequest.headers === undefined ? {} : { headers: inputRequest.headers }),
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parseHttpsUrl(value) {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = new URL(value);
|
|
148
|
+
if (parsed.protocol === 'https:' && parsed.username === '' && parsed.password === '') {
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Normalized to a stable, non-sensitive code below.
|
|
154
|
+
}
|
|
155
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_CONFIGURATION');
|
|
156
|
+
}
|
|
157
|
+
function positiveInteger(value, fallback) {
|
|
158
|
+
const selected = value ?? fallback;
|
|
159
|
+
if (!Number.isSafeInteger(selected) || selected <= 0) {
|
|
160
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_CONFIGURATION');
|
|
161
|
+
}
|
|
162
|
+
return selected;
|
|
163
|
+
}
|
|
164
|
+
function resolvePath(path, base, basePath) {
|
|
165
|
+
if (!path.startsWith('/') || path.startsWith('//') || path.includes('\\') || path.includes('#')) {
|
|
166
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
167
|
+
}
|
|
168
|
+
let resolved;
|
|
169
|
+
try {
|
|
170
|
+
resolved = new URL(`${base.origin}${basePath}${path}`);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
174
|
+
}
|
|
175
|
+
if (resolved.origin !== base.origin
|
|
176
|
+
|| (basePath !== '' && !resolved.pathname.startsWith(`${basePath}/`))) {
|
|
177
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_CROSS_ORIGIN');
|
|
178
|
+
}
|
|
179
|
+
return resolved;
|
|
180
|
+
}
|
|
181
|
+
/** Native URL stacks may decode harmless path escapes while preserving the route. */
|
|
182
|
+
function normalizeNativePath(path) {
|
|
183
|
+
return path.replace(/%([0-9a-f]{2})/giu, (_match, hex) => {
|
|
184
|
+
const character = String.fromCharCode(Number.parseInt(hex, 16));
|
|
185
|
+
return /^[A-Za-z0-9._~,*-]$/u.test(character) ? character : `%${hex.toUpperCase()}`;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function normalizeHeaders(input) {
|
|
189
|
+
const headers = {};
|
|
190
|
+
for (const [rawName, value] of Object.entries(input ?? {})) {
|
|
191
|
+
const name = rawName.toLowerCase();
|
|
192
|
+
if (!headerNamePattern.test(name) || typeof value !== 'string'
|
|
193
|
+
|| /[\r\n]/u.test(value) || name === 'host' || name === 'content-length'
|
|
194
|
+
|| name === 'accept' || name === 'content-type') {
|
|
195
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
196
|
+
}
|
|
197
|
+
headers[name] = value;
|
|
198
|
+
}
|
|
199
|
+
return headers;
|
|
200
|
+
}
|
|
201
|
+
function isRecord(value) {
|
|
202
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
203
|
+
}
|
|
204
|
+
function assertJsonValue(value, seen, depth) {
|
|
205
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean'
|
|
206
|
+
|| (typeof value === 'number' && Number.isFinite(value))) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (typeof value !== 'object' || depth >= 32 || seen.has(value)) {
|
|
210
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
211
|
+
}
|
|
212
|
+
const prototype = Object.getPrototypeOf(value);
|
|
213
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
214
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
215
|
+
}
|
|
216
|
+
if (Reflect.ownKeys(value).some((key) => typeof key === 'symbol')) {
|
|
217
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_INVALID_REQUEST');
|
|
218
|
+
}
|
|
219
|
+
seen.add(value);
|
|
220
|
+
for (const entry of Array.isArray(value) ? value : Object.values(value)) {
|
|
221
|
+
assertJsonValue(entry, seen, depth + 1);
|
|
222
|
+
}
|
|
223
|
+
seen.delete(value);
|
|
224
|
+
}
|
|
225
|
+
async function waitForNativeResponse(http, options, signal, timeoutMs) {
|
|
226
|
+
let timer;
|
|
227
|
+
let onAbort;
|
|
228
|
+
const pending = Promise.resolve().then(() => http.request(options));
|
|
229
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
230
|
+
timer = setTimeout(() => reject(new CapacitorNativeHttpError('NATIVE_HTTP_TIMEOUT')), timeoutMs);
|
|
231
|
+
});
|
|
232
|
+
const abort = new Promise((_resolve, reject) => {
|
|
233
|
+
if (signal === undefined) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
onAbort = () => reject(new CapacitorNativeHttpError('NATIVE_HTTP_ABORTED'));
|
|
237
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
238
|
+
if (signal.aborted) {
|
|
239
|
+
onAbort();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
try {
|
|
243
|
+
return await Promise.race([pending, timeout, abort]);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (error instanceof CapacitorNativeHttpError) {
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
throw new CapacitorNativeHttpError('NATIVE_HTTP_FAILED');
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
if (timer !== undefined) {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
}
|
|
255
|
+
if (signal !== undefined && onAbort !== undefined) {
|
|
256
|
+
signal.removeEventListener('abort', onAbort);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BridgeMethod, BridgeRequest, BridgeResponse } from '@mpgd/bridge';
|
|
2
|
+
import { type PlatformProviderAvailability, type PlatformProviderFeature, type ProductType } from '@mpgd/platform';
|
|
3
|
+
export interface NativeBridge {
|
|
4
|
+
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* An optional, separately installed provider handles only its declared bridge
|
|
8
|
+
* methods. The base game-services plugin remains responsible for local storage.
|
|
9
|
+
* A provider may be backed by a native plugin or by a server-backed adapter;
|
|
10
|
+
* neither dependency is imported by the base Capacitor adapter.
|
|
11
|
+
*/
|
|
12
|
+
export interface CapacitorServiceProvider {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly bridge: NativeBridge;
|
|
15
|
+
readonly features: readonly PlatformProviderFeature[];
|
|
16
|
+
readonly methods: readonly BridgeMethod[];
|
|
17
|
+
getAvailability(): Promise<Readonly<Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>>>;
|
|
18
|
+
}
|
|
19
|
+
export interface CapacitorProviderRegistry {
|
|
20
|
+
readonly byMethod: ReadonlyMap<BridgeMethod, CapacitorServiceProvider>;
|
|
21
|
+
assertMethodReady(method: BridgeMethod, payload?: unknown, productType?: ProductType): Promise<void>;
|
|
22
|
+
getAvailability(): Promise<Readonly<Record<PlatformProviderFeature, PlatformProviderAvailability>>>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createCapacitorProviderRegistry(input: readonly CapacitorServiceProvider[]): CapacitorProviderRegistry;
|
|
25
|
+
export declare function assertProviderResponseData(method: BridgeMethod, data: unknown): void;
|