@mpgd/adapter-capacitor 0.4.11 → 0.5.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/index.js CHANGED
@@ -1,10 +1,122 @@
1
- import { decodeBridgeStorageLoadData, } from '@mpgd/bridge';
1
+ import { decodeBridgeCredentialLoadData, decodeBridgeStorageLoadData, } from '@mpgd/bridge';
2
2
  import { CapacitorGameServices } from '@mpgd/capacitor-game-services';
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
+ ]);
18
+ function isRecord(value) {
19
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
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
+ }
3
44
  export function createCapacitorPlatformGateway(input) {
4
45
  const bridge = input.bridge ?? CapacitorGameServices;
5
- async function request(method, payload) {
6
- const response = (await bridge.request({
7
- id: crypto.randomUUID(),
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) {
117
+ const id = crypto.randomUUID();
118
+ const response = await (provider?.bridge ?? bridge).request({
119
+ id,
8
120
  method,
9
121
  payload,
10
122
  meta: {
@@ -13,16 +125,55 @@ export function createCapacitorPlatformGateway(input) {
13
125
  buildId: input.buildId,
14
126
  sentAt: new Date().toISOString(),
15
127
  },
16
- }));
17
- if (!response.ok) {
18
- throw new Error(response.error.message);
128
+ });
129
+ if (!isRecord(response) || response.id !== id || typeof response.ok !== 'boolean') {
130
+ throw new PlatformOperationError({
131
+ code: 'NATIVE_BRIDGE_INVALID_RESPONSE',
132
+ message: `Native bridge returned an invalid response for ${method}.`,
133
+ });
134
+ }
135
+ if (response.ok) {
136
+ if (!Object.hasOwn(response, 'data')) {
137
+ throw new PlatformOperationError({
138
+ code: 'NATIVE_BRIDGE_INVALID_RESPONSE',
139
+ message: `Native bridge omitted response data for ${method}.`,
140
+ });
141
+ }
142
+ if (provider !== undefined) {
143
+ assertProviderResponseData(method, response.data);
144
+ }
145
+ return response.data;
19
146
  }
20
- return response.data;
147
+ if (!isRecord(response.error)
148
+ || typeof response.error.code !== 'string'
149
+ || typeof response.error.message !== 'string'
150
+ || typeof response.error.retryable !== 'boolean') {
151
+ throw new PlatformOperationError({
152
+ code: 'NATIVE_BRIDGE_INVALID_RESPONSE',
153
+ message: `Native bridge returned an invalid error for ${method}.`,
154
+ });
155
+ }
156
+ throw new PlatformOperationError(response.error);
21
157
  }
22
158
  return {
23
159
  target: input.target,
24
160
  async getCapabilities() {
25
- return { ...await request('runtime.getCapabilities', {}) };
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
+ };
26
177
  },
27
178
  identity: {
28
179
  getPlayer: () => request('identity.getPlayer', {}),
@@ -51,24 +202,48 @@ export function createCapacitorPlatformGateway(input) {
51
202
  preload: (payload) => request('ads.preload', payload),
52
203
  showRewarded: (payload) => request('ads.showRewarded', payload),
53
204
  showInterstitial: (payload) => request('ads.showInterstitial', payload),
205
+ mountBanner: (payload) => request('ads.mountBanner', payload),
206
+ unmountBanner: (payload) => request('ads.unmountBanner', payload),
54
207
  },
55
208
  leaderboard: {
56
- submitScore: (payload) => request('leaderboard.submitScore', payload),
57
- open: (payload) => request('leaderboard.open', payload ?? {}),
58
- },
59
- lifecycle: {
60
- onPause() {
61
- return () => { };
62
- },
63
- onResume() {
64
- 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);
65
213
  },
66
214
  },
215
+ lifecycle: gatewayLifecycle,
216
+ viewport,
67
217
  storage: {
68
218
  async load(payload) {
69
219
  return decodeBridgeStorageLoadData(await request('storage.load', payload));
70
220
  },
71
221
  save: (payload) => request('storage.save', payload),
72
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
+ },
73
248
  };
74
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;