@mpgd/game-services 0.30.5 → 0.32.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/runtime.js CHANGED
@@ -1,13 +1,25 @@
1
- import { createAccountDeletionClient } from './account-deletion.js';
2
- import { createGameServicesClient, createGameServicesFetchBackendTransport, createGameServicesHttpBackendApi, createGameServicesOrpcBackendApi, createGameServicesOrpcClient, resolveGameServicesRequestHeaders, } from './client.js';
1
+ import { AccountDeletionError, createAccountDeletionClient, } from './account-deletion.js';
2
+ import { CommerceError } from '@mpgd/platform/commerce';
3
+ import { isHostedGameServicesPath } from '@mpgd/platform/routes';
4
+ import { assertCommerceStaleLaunchedAfterMs, createGameServicesClient, createGameServicesFetchBackendTransport, createGameServicesHttpBackendApi, createGameServicesOrpcBackendApi, createGameServicesOrpcClient, resolveGameServicesRequestHeaders, } from './client.js';
5
+ import { gameServicesPlayerChangedCode, GameServicesRuntimeClosedError, gameServicesRuntimeDisposedCode, gameServicesSessionRequiredCode, } from './request-headers.js';
3
6
  import { createRecoverableMonetizationClient, } from './monetization-recovery.js';
7
+ /**
8
+ * How long `dispose()` waits for a purchase whose store sheet is open to
9
+ * journal its answer, and then for the reconciler to stop: 3 minutes, past
10
+ * Google Play's own wait for a purchase result.
11
+ */
12
+ export const gameServicesDisposeWaitMs = 3 * 60 * 1000;
13
+ export { gameServicesPlayerChangedCode, gameServicesRuntimeDisposedCode, GameServicesRuntimeClosedError, gameServicesSessionRequiredCode, } from './request-headers.js';
4
14
  export function createGameServicesRuntime(input) {
5
15
  assertAuthorityMode(input.authorityMode);
6
16
  assertTransport(input.transport);
17
+ assertCommerceStaleLaunchedAfterMs(input.commerceStaleLaunchedAfterMs);
7
18
  if (input.transport === 'orpc' && input.httpTransport !== undefined) {
8
19
  throw new Error('Game Services httpTransport cannot be used with oRPC.');
9
20
  }
10
21
  const target = resolveGameServicesLedgerTarget(input.target ?? input.gateway.target);
22
+ const gate = createRuntimeGate(input.playerId, input.sessionPlayerId);
11
23
  if (target === null) {
12
24
  return disabledRuntime('unsupported_target');
13
25
  }
@@ -30,10 +42,10 @@ export function createGameServicesRuntime(input) {
30
42
  ? createGameServicesOrpcBackendApi(createGameServicesOrpcClient({
31
43
  url: baseUrl,
32
44
  ...(input.headers === undefined ? {} : { headers: input.headers }),
33
- ...(input.getHeaders === undefined ? {} : { getHeaders: input.getHeaders }),
45
+ getHeaders: guardHeaders(input.getHeaders, gate),
34
46
  }))
35
47
  : createGameServicesHttpBackendApi({
36
- transport: withRuntimeHeaders(input.httpTransport ?? createGameServicesFetchBackendTransport({ baseUrl }), input),
48
+ transport: withRuntimeHeaders(input.httpTransport ?? createGameServicesFetchBackendTransport({ baseUrl }), input, gate),
37
49
  });
38
50
  }
39
51
  else {
@@ -49,8 +61,11 @@ export function createGameServicesRuntime(input) {
49
61
  mode = 'local';
50
62
  backend = input.localBackend;
51
63
  }
64
+ // Every backend call is refused before it starts once the runtime is closed.
65
+ backend = guardBackend(backend, gate);
52
66
  const clientInput = {
53
- gateway: input.gateway,
67
+ // A rewarded ad queued before the runtime closed is not shown after it.
68
+ gateway: guardRewardedAds(input.gateway, gate),
54
69
  backend,
55
70
  playerId: input.playerId,
56
71
  target,
@@ -64,6 +79,23 @@ export function createGameServicesRuntime(input) {
64
79
  ...(input.now === undefined ? {} : { now: input.now }),
65
80
  ...(input.rewardEvidenceRegistry === undefined ? {} : { rewardEvidenceRegistry: input.rewardEvidenceRegistry }),
66
81
  ...(input.commerceProducts === undefined ? {} : { commerceProducts: input.commerceProducts }),
82
+ ...(input.commerceAccountBinding === undefined ? {} : { commerceAccountBinding: input.commerceAccountBinding }),
83
+ commerceSessionCheck: () => {
84
+ const refusal = gate.check();
85
+ return refusal === undefined
86
+ ? undefined
87
+ : new CommerceError('ACCOUNT_BINDING_REQUIRED', {
88
+ retryable: true,
89
+ storeCode: refusal.code,
90
+ message: refusal.message,
91
+ });
92
+ },
93
+ // Closed for good, the reconciler starts nothing: a recovery pass that
94
+ // reaches `start()` after `dispose()` stopped it subscribes nothing.
95
+ commerceClosed: () => gate.closed,
96
+ ...(input.commerceStaleLaunchedAfterMs === undefined
97
+ ? {}
98
+ : { commerceStaleLaunchedAfterMs: input.commerceStaleLaunchedAfterMs }),
67
99
  };
68
100
  const monetizationRecovery = input.operationStore === undefined
69
101
  ? undefined
@@ -88,34 +120,331 @@ export function createGameServicesRuntime(input) {
88
120
  : { commerceReconciler: monetizationRecovery.commerceReconciler }),
89
121
  };
90
122
  }
123
+ const reconciler = routedClient.commerceReconciler;
124
+ /**
125
+ * Purchases, rewarded-ad claims, and recovery passes running now:
126
+ * `dispose()` lets them journal first.
127
+ */
128
+ const purchases = new Set();
129
+ const track = (running) => {
130
+ purchases.add(running);
131
+ void running.finally(() => purchases.delete(running)).catch(() => undefined);
132
+ return running;
133
+ };
134
+ const exposedReconciler = reconciler === undefined
135
+ ? undefined
136
+ : guardReconciler(reconciler, track);
137
+ gate.onClose(async () => {
138
+ // A purchase whose store sheet is open journals the store answer first,
139
+ // an open rewarded ad its platform result (both stay this player's to
140
+ // recover), and a recovery pass finishes its start, then the reconciler
141
+ // stops.
142
+ await settleWithin(Promise.allSettled([...purchases]), gameServicesDisposeWaitMs);
143
+ await settleWithin(reconciler?.stop() ?? Promise.resolve(), gameServicesDisposeWaitMs);
144
+ });
91
145
  return {
92
146
  mode,
93
147
  ...(baseUrl === undefined ? {} : { baseUrl }),
94
148
  target,
95
- client: routedClient,
96
- ...(monetizationRecovery === undefined ? {} : { monetizationRecovery }),
149
+ client: guardClient(routedClient, gate, track, exposedReconciler),
150
+ ...(monetizationRecovery === undefined
151
+ ? {}
152
+ : { monetizationRecovery: guardRecovery(monetizationRecovery, gate, exposedReconciler, track) }),
97
153
  ...(baseUrl === undefined
98
154
  ? {}
99
155
  : {
100
- accountDeletion: createAccountDeletionClient({
156
+ accountDeletion: guardAccountDeletion(createAccountDeletionClient({
101
157
  // The deletion route is plain HTTP next to the oRPC handler: an
102
158
  // oRPC URL names the handler's `/rpc` prefix, which the route
103
159
  // does not sit under.
104
160
  baseUrl: input.transport === 'orpc' ? baseUrl.replace(/\/rpc\/?$/u, '') : baseUrl,
105
161
  ...(input.headers === undefined ? {} : { headers: input.headers }),
106
- ...(input.getHeaders === undefined ? {} : { getHeaders: input.getHeaders }),
162
+ getHeaders: guardHeaders(input.getHeaders, gate),
163
+ }), gate),
164
+ }),
165
+ dispose: () => gate.close(gameServicesRuntimeDisposedCode),
166
+ };
167
+ }
168
+ function createRuntimeGate(playerId, sessionPlayerId) {
169
+ let closed;
170
+ let closing;
171
+ const teardowns = [];
172
+ const gate = {
173
+ check() {
174
+ if (closed !== undefined) {
175
+ return closed;
176
+ }
177
+ if (sessionPlayerId === undefined) {
178
+ return undefined;
179
+ }
180
+ let current;
181
+ try {
182
+ current = sessionPlayerId()?.trim();
183
+ }
184
+ catch {
185
+ current = undefined;
186
+ }
187
+ if (current === undefined || current === '') {
188
+ return new GameServicesRuntimeClosedError(gameServicesSessionRequiredCode);
189
+ }
190
+ if (current !== playerId.trim()) {
191
+ void gate.close(gameServicesPlayerChangedCode);
192
+ return closed;
193
+ }
194
+ return undefined;
195
+ },
196
+ get closed() {
197
+ return closed !== undefined;
198
+ },
199
+ assert() {
200
+ const refusal = gate.check();
201
+ if (refusal !== undefined) {
202
+ throw refusal;
203
+ }
204
+ },
205
+ close(code) {
206
+ closed ??= new GameServicesRuntimeClosedError(code);
207
+ closing ??= (async () => {
208
+ for (const teardown of teardowns) {
209
+ try {
210
+ await teardown();
211
+ }
212
+ catch {
213
+ // Closed regardless: every operation is refused from now on.
214
+ }
215
+ }
216
+ })();
217
+ return closing;
218
+ },
219
+ onClose(teardown) {
220
+ teardowns.push(teardown);
221
+ },
222
+ };
223
+ return gate;
224
+ }
225
+ /**
226
+ * Account deletion refused before anything resolves or is sent: signed out
227
+ * answers `UNAUTHORIZED` (no session on this device), a closed runtime the
228
+ * retryable `ACCOUNT_DELETION_UNREACHABLE`. Static headers never stand in
229
+ * for a session the runtime refuses.
230
+ */
231
+ function guardAccountDeletion(deletion, gate) {
232
+ return {
233
+ ...deletion,
234
+ async deleteAccount(request) {
235
+ const refusal = gate.check();
236
+ if (refusal !== undefined) {
237
+ throw new AccountDeletionError(refusal.code === gameServicesSessionRequiredCode
238
+ ? 'UNAUTHORIZED'
239
+ : 'ACCOUNT_DELETION_UNREACHABLE', { requestSent: false, cause: refusal });
240
+ }
241
+ return deletion.deleteAccount(request);
242
+ },
243
+ };
244
+ }
245
+ function guardHeaders(getHeaders, gate) {
246
+ return async () => {
247
+ gate.assert();
248
+ const headers = (await getHeaders?.()) ?? {};
249
+ // A session that changed player while its headers were resolved: they
250
+ // may be the new player's, so they are never sent for this one.
251
+ gate.assert();
252
+ return headers;
253
+ };
254
+ }
255
+ function guardBackend(backend, gate) {
256
+ const { purchases, adRewards, leaderboard } = backend;
257
+ const ledgerStatus = purchases.getPurchaseLedgerStatus;
258
+ const capabilities = purchases.getCommerceCapabilities;
259
+ return {
260
+ ...(backend.version === undefined ? {} : { version: backend.version }),
261
+ purchases: {
262
+ async verifyPurchase(request) {
263
+ gate.assert();
264
+ return purchases.verifyPurchase(request);
265
+ },
266
+ ...(ledgerStatus === undefined
267
+ ? {}
268
+ : {
269
+ async getPurchaseLedgerStatus(request) {
270
+ gate.assert();
271
+ return ledgerStatus.call(purchases, request);
272
+ },
273
+ }),
274
+ ...(capabilities === undefined
275
+ ? {}
276
+ : {
277
+ async getCommerceCapabilities() {
278
+ gate.assert();
279
+ return capabilities.call(purchases);
280
+ },
107
281
  }),
282
+ },
283
+ adRewards: {
284
+ async claimAdReward(request) {
285
+ gate.assert();
286
+ return adRewards.claimAdReward(request);
287
+ },
288
+ },
289
+ leaderboard: {
290
+ async recordScore(request) {
291
+ gate.assert();
292
+ return leaderboard.recordScore(request);
293
+ },
294
+ },
295
+ };
296
+ }
297
+ /** Resolves when `work` settles or after `ms`, whichever is first; never rejects. */
298
+ async function settleWithin(work, ms) {
299
+ let timer;
300
+ try {
301
+ await Promise.race([
302
+ work.then(() => undefined, () => undefined),
303
+ new Promise((resolve) => {
304
+ timer = setTimeout(resolve, ms);
108
305
  }),
306
+ ]);
307
+ }
308
+ finally {
309
+ clearTimeout(timer);
310
+ }
311
+ }
312
+ /**
313
+ * The gateway with a rewarded-ad dispatch that throws the refusal once the
314
+ * runtime refuses, so no ad opens. It is not an ad outcome: a journaled
315
+ * operation stays unstarted (not ended), and `claimRewardedAd` answers
316
+ * `unavailable`.
317
+ */
318
+ function guardRewardedAds(gateway, gate) {
319
+ const ads = gateway.ads;
320
+ return {
321
+ ...gateway,
322
+ ads: {
323
+ ...ads,
324
+ async showRewarded(request) {
325
+ gate.assert();
326
+ return ads.showRewarded(request);
327
+ },
328
+ },
109
329
  };
110
330
  }
111
- function withRuntimeHeaders(transport, input) {
331
+ /** A reward claim through an open runtime; a dispatch the runtime refused answers `unavailable`. */
332
+ async function guardedReward(gate, track, run) {
333
+ if (gate.check() !== undefined) {
334
+ return refusedReward();
335
+ }
336
+ try {
337
+ return await track(run());
338
+ }
339
+ catch (error) {
340
+ if (error instanceof GameServicesRuntimeClosedError) {
341
+ return refusedReward();
342
+ }
343
+ throw error;
344
+ }
345
+ }
346
+ function refusedPurchase(refusal) {
347
+ return {
348
+ status: 'failed',
349
+ purchase: {
350
+ status: 'failed',
351
+ entitlementIds: [],
352
+ diagnostic: { code: refusal.code, retryable: true },
353
+ },
354
+ };
355
+ }
356
+ function refusedReward() {
357
+ return { status: 'unavailable', reward: { status: 'unavailable', rewardGranted: false } };
358
+ }
359
+ /**
360
+ * A purchase through an open runtime. A refusal the reconciler gave because
361
+ * the runtime closed meanwhile answers the runtime's diagnostic, as a
362
+ * refusal at the entry does.
363
+ */
364
+ async function guardedPurchase(gate, track, run) {
365
+ const refusal = gate.check();
366
+ if (refusal !== undefined) {
367
+ return refusedPurchase(refusal);
368
+ }
369
+ const result = await track(run());
370
+ const late = result.status === 'failed'
371
+ && result.purchase.diagnostic?.code === 'ACCOUNT_BINDING_REQUIRED'
372
+ ? gate.check()
373
+ : undefined;
374
+ return late === undefined ? result : refusedPurchase(late);
375
+ }
376
+ /**
377
+ * The reconciler a runtime exposes. Once the runtime closed for good its
378
+ * `start()` subscribes nothing (the reconciler's `closed`); while only signed
379
+ * out it starts as usual and its work waits for the session (its
380
+ * `sessionCheck`). Its purchases are tracked like the client's, so
381
+ * `dispose()` waits for an open store sheet's answer.
382
+ */
383
+ function guardReconciler(reconciler, track) {
384
+ // The reconciler's getters and methods are closures: the prototype chain serves them.
385
+ return Object.create(reconciler, {
386
+ requestPurchase: {
387
+ value: (request) => track(reconciler.requestPurchase(request)),
388
+ },
389
+ });
390
+ }
391
+ /** The client of an open runtime; refusals answer before any store UI or ad opens. */
392
+ function guardClient(client, gate, track, reconciler) {
393
+ return {
394
+ ...client,
395
+ ...(reconciler === undefined ? {} : { commerceReconciler: reconciler }),
396
+ purchase: (purchase, options) => guardedPurchase(gate, track, () => client.purchase(purchase, options)),
397
+ claimRewardedAd: (reward, options) => guardedReward(gate, track, () => client.claimRewardedAd(reward, options)),
398
+ async submitLeaderboardScore(score) {
399
+ gate.assert();
400
+ return client.submitLeaderboardScore(score);
401
+ },
402
+ };
403
+ }
404
+ function guardRecovery(recovery, gate, reconciler, track) {
405
+ return {
406
+ ...recovery,
407
+ ...(reconciler === undefined ? {} : { commerceReconciler: reconciler }),
408
+ purchase: (purchase, options) => guardedPurchase(gate, track, () => recovery.purchase(purchase, options)),
409
+ claimRewardedAd: (reward, options) => guardedReward(gate, track, () => recovery.claimRewardedAd(reward, options)),
410
+ // A late ad callback is journaled; its claim is refused until a runtime
411
+ // for the player resends it, so it stays pending, never ended.
412
+ recoverRewardResult: (idempotencyKey, platform) => recovery.recoverRewardResult(idempotencyKey, platform),
413
+ async reconcile() {
414
+ // Startup and resume recovery wait for a runtime the session permits.
415
+ // `dispose()` waits for a pass under way, so the reconciler it starts
416
+ // is stopped after it, not before.
417
+ return gate.check() === undefined ? track(recovery.reconcile()) : [];
418
+ },
419
+ async restorePurchases() {
420
+ gate.assert();
421
+ let restored;
422
+ try {
423
+ restored = await recovery.restorePurchases();
424
+ }
425
+ catch (error) {
426
+ // Closed during the restore: the runtime's refusal says why.
427
+ gate.assert();
428
+ throw error;
429
+ }
430
+ // Closed during the restore: its passes stopped early for this player.
431
+ gate.assert();
432
+ return restored;
433
+ },
434
+ };
435
+ }
436
+ function withRuntimeHeaders(transport, input, gate) {
112
437
  return {
113
438
  async send(request) {
439
+ gate.assert();
114
440
  const headers = await resolveGameServicesRequestHeaders({
115
441
  headers: input.headers,
116
442
  getHeaders: input.getHeaders,
117
443
  requestHeaders: request.headers,
118
444
  });
445
+ // A session that changed player while its headers were resolved: they
446
+ // may be the new player's, so they are never sent for this one.
447
+ gate.assert();
119
448
  return transport.send({
120
449
  ...request,
121
450
  headers,
@@ -156,12 +485,36 @@ function disabledRuntime(reason, target) {
156
485
  mode: 'disabled',
157
486
  reason,
158
487
  ...(target === undefined ? {} : { target }),
488
+ // Nothing to close: a disabled runtime holds no client.
489
+ dispose: async () => undefined,
159
490
  };
160
491
  }
161
492
  function normalizeBaseUrl(baseUrl) {
162
493
  const normalized = baseUrl?.trim();
163
494
  return normalized === undefined || normalized.length === 0 ? undefined : normalized;
164
495
  }
496
+ export { isHostedGameServicesPath };
497
+ /**
498
+ * Resolve a hosted browser's clean /api ingress before constructing absolute-URL
499
+ * clients. No ambient DOM access: composition roots supply the actual page origin
500
+ * and gateway target. Unresolved/invalid inputs remain invalid for the runtime's
501
+ * fail-closed authority check; this helper never supplies a fallback backend.
502
+ */
503
+ export function resolveGameServicesBaseUrl(baseUrl, context) {
504
+ const value = normalizeBaseUrl(baseUrl);
505
+ if (value === undefined || (context.target !== 'browser' && context.target !== 'microsoft-store')
506
+ || value !== baseUrl || !isHostedGameServicesPath(value)) {
507
+ return value;
508
+ }
509
+ const origin = context.pageOrigin === undefined
510
+ ? undefined
511
+ : parseAbsoluteUrl(context.pageOrigin);
512
+ if (origin === undefined || origin.origin !== context.pageOrigin
513
+ || (origin.protocol !== 'https:' && origin.protocol !== 'http:')) {
514
+ return value;
515
+ }
516
+ return new URL(value, origin).href;
517
+ }
165
518
  /**
166
519
  * Whether a game-services URL may carry a session's credentials in a
167
520
  * production build: an absolute https URL with no credentials and a public
package/dist/server.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type AdPlacements, type ProductCatalog } from '@mpgd/catalog';
3
3
  import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesBackendTransportRequest, type GameServicesBackendTransportResponse } from './client.js';
4
4
  import { type CommerceEvidenceRegistry } from './commerce-evidence-registry.js';
5
5
  import { type PlayerDataErasureInput, type PlayerDataErasureResult } from './account-deletion.js';
6
+ import { purchaseBoundToAnotherAccountCode } from './another-account.js';
6
7
  import { type GameServicesHealthResponse } from './contract.js';
7
8
  import { type GameServicesEvidenceVerifier, type GameServicesPurchaseGrantFinalizer } from './evidence-verification.js';
8
9
  import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerNote, type EntitlementLedgerResult, type EntitlementLedgerRevocation, type EntitlementRevocationTransaction, type GameServicesAdRewardTarget, type GameServicesStoreTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type PurchaseLedgerStatusRequest, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types.js';
@@ -58,11 +59,15 @@ export interface GameServicesBackendApiHandler {
58
59
  handle(request: GameServicesBackendTransportRequest): Promise<GameServicesBackendTransportResponse>;
59
60
  }
60
61
  /** Stable error codes emitted by the backend API handler and the HTTP fetch handler. */
61
- export type GameServicesBackendErrorCode = 'INVALID_REQUEST' | 'INVALID_JSON' | 'PAYLOAD_TOO_LARGE' | 'METHOD_NOT_ALLOWED' | 'UNKNOWN_ENDPOINT' | 'INTERNAL_ERROR';
62
+ export type GameServicesBackendErrorCode = 'INVALID_REQUEST' | 'INVALID_JSON' | 'PAYLOAD_TOO_LARGE' | 'METHOD_NOT_ALLOWED' | 'UNKNOWN_ENDPOINT' | 'INTERNAL_ERROR'
63
+ /** 409: the store binds the purchase to another account (RFC "Another account's purchase"). */
64
+ | typeof purchaseBoundToAnotherAccountCode;
62
65
  export interface GameServicesBackendErrorResponse {
63
66
  readonly error: GameServicesBackendErrorCode;
64
67
  /** Present only for `INVALID_REQUEST`; never carries internal failure details. */
65
68
  readonly message?: string;
69
+ /** Present only for `PURCHASE_BOUND_TO_ANOTHER_ACCOUNT`: the store's binding mismatch. */
70
+ readonly reason?: string;
66
71
  }
67
72
  export interface GameServicesStore {
68
73
  recordEntitlementGrant(input: EntitlementLedgerGrant): Promise<EntitlementLedgerResult>;
@@ -152,6 +157,15 @@ export interface CreateGameServicesRouterOptions {
152
157
  }
153
158
  export interface CreateGameServicesRpcFetchHandlerOptions extends CreateGameServicesFetchHandlerOptions {
154
159
  readonly prefix?: string;
160
+ /**
161
+ * Writes the pre-beta.34 `inferable: false` flag into oRPC error bodies so game clients
162
+ * from `@mpgd/game-services` 0.30.5 or older (oRPC 2.0.0-beta.14) still decode error
163
+ * codes. A plain oRPC 2.0.0-beta.34+ client rejects that flag: such clients must wrap their
164
+ * fetch with `withLegacyOrpcErrorCompat`, as `createGameServicesOrpcClient` from 0.31.0 or
165
+ * later does. Set this to false once every supported game client comes from 0.31.0 or
166
+ * later. Defaults to true.
167
+ */
168
+ readonly legacyOrpcErrorFlag?: boolean;
155
169
  }
156
170
  export interface GameServicesOrpcContext {
157
171
  readonly request?: Request;
@@ -217,19 +231,29 @@ export declare function createGameServicesBackend(input: CreateGameServicesBacke
217
231
  export declare function createGameServicesBackendApiHandler(input: CreateGameServicesBackendInput): GameServicesBackendApiHandler;
218
232
  export declare function createInProcessGameServicesBackendTransport(handler: GameServicesBackendApiHandler): GameServicesBackendTransport;
219
233
  export declare function createGameServicesRouter(backend: GameServicesBackendApi, options?: CreateGameServicesRouterOptions): {
220
- health: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<void, void>, import("@orpc/contract").Schema<GameServicesHealthResponse, GameServicesHealthResponse>, object>;
234
+ health: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<void, void>, import("@orpc/contract").Schema<GameServicesHealthResponse, GameServicesHealthResponse>, Record<never, never>>;
221
235
  commerce: {
222
- verifyPurchase: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<VerifyPurchaseRequest, VerifyPurchaseRequest>, import("@orpc/contract").Schema<VerifyPurchaseResponse, VerifyPurchaseResponse>, object>;
223
- getPurchaseLedgerStatus: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<PurchaseLedgerStatusRequest, PurchaseLedgerStatusRequest>, import("@orpc/contract").Schema<import("./operation-results.js").PurchaseLedgerStatusResponse, import("./operation-results.js").PurchaseLedgerStatusResponse>, object>;
224
- getCapabilities: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<void, void>, import("@orpc/contract").Schema<import("@mpgd/platform/commerce").CommerceServiceCapabilities, import("@mpgd/platform/commerce").CommerceServiceCapabilities>, object>;
236
+ verifyPurchase: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<VerifyPurchaseRequest, VerifyPurchaseRequest>, import("@orpc/contract").Schema<VerifyPurchaseResponse, VerifyPurchaseResponse>, Record<never, never>>;
237
+ getPurchaseLedgerStatus: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<PurchaseLedgerStatusRequest, PurchaseLedgerStatusRequest>, import("@orpc/contract").Schema<import("./operation-results.js").PurchaseLedgerStatusResponse, import("./operation-results.js").PurchaseLedgerStatusResponse>, Record<never, never>>;
238
+ getCapabilities: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<void, void>, import("@orpc/contract").Schema<import("@mpgd/platform/commerce").CommerceServiceCapabilities, import("@mpgd/platform/commerce").CommerceServiceCapabilities>, Record<never, never>>;
225
239
  };
226
240
  ads: {
227
- claimReward: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<ClaimAdRewardRequest, ClaimAdRewardRequest>, import("@orpc/contract").Schema<ClaimAdRewardResponse, ClaimAdRewardResponse>, object>;
241
+ claimReward: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<ClaimAdRewardRequest, ClaimAdRewardRequest>, import("@orpc/contract").Schema<ClaimAdRewardResponse, ClaimAdRewardResponse>, Record<never, never>>;
228
242
  };
229
243
  leaderboard: {
230
- recordScore: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<RecordLeaderboardScoreRequest, RecordLeaderboardScoreRequest>, import("@orpc/contract").Schema<RecordLeaderboardScoreResponse, RecordLeaderboardScoreResponse>, object>;
244
+ recordScore: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<RecordLeaderboardScoreRequest, RecordLeaderboardScoreRequest>, import("@orpc/contract").Schema<RecordLeaderboardScoreResponse, RecordLeaderboardScoreResponse>, Record<never, never>>;
231
245
  };
232
246
  };
247
+ /**
248
+ * The RPC handler's error status map: oRPC's common codes and
249
+ * `PURCHASE_BOUND_TO_ANOTHER_ACCOUNT` at 409. oRPC 2.0.0-beta.46 derives the
250
+ * HTTP status of an error from its code through this map (an `ORPCError`
251
+ * carries no status of its own), so a host that mounts
252
+ * `createGameServicesRouter` in its own `RPCHandler` passes it as
253
+ * `errorStatusMap` to answer 409 too; without it the code still reaches the
254
+ * client, with status 500.
255
+ */
256
+ export declare const gameServicesRpcErrorStatusMap: Readonly<Record<string, number>>;
233
257
  export declare function createGameServicesRpcFetchHandler(router: ReturnType<typeof createGameServicesRouter>, options?: CreateGameServicesRpcFetchHandlerOptions): (request: Request) => Promise<Response>;
234
258
  export declare function createGameServicesHttpFetchHandler(handler: GameServicesBackendApiHandler, options?: CreateGameServicesFetchHandlerOptions): (request: Request) => Promise<Response>;
235
259
  export * from './notification-delivery.js';