@mpgd/game-services 0.11.1 → 0.12.1

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.
@@ -27,12 +27,61 @@ export interface AppsInTossFunctionalMessageResult {
27
27
  readonly sentFriendtalkCount: number;
28
28
  readonly contentIds: readonly string[];
29
29
  }
30
+ /** The source environment returned by the Apps in Toss login SDK. */
31
+ export type AppsInTossLoginReferrer = 'DEFAULT' | 'SANDBOX';
32
+ /**
33
+ * Short-lived OAuth token material returned by the partner API.
34
+ *
35
+ * Callers should use this only to immediately resolve a login identity or
36
+ * securely persist it in a dedicated credential store. It must never be sent
37
+ * back to a game client.
38
+ */
39
+ export interface AppsInTossLoginToken {
40
+ readonly tokenType: 'bearer';
41
+ readonly accessToken: string;
42
+ readonly refreshToken: string;
43
+ readonly expiresInSeconds: number;
44
+ readonly scope: string;
45
+ }
46
+ /** App-scoped user identity returned by the Apps in Toss login API. */
47
+ export interface AppsInTossLoginUser {
48
+ /** Serialized because the documented API returns a numeric user key. */
49
+ readonly userKey: string;
50
+ readonly scope: string;
51
+ }
52
+ export type AppsInTossIapOrderStatus = 'PURCHASED' | 'PAYMENT_COMPLETED' | 'FAILED' | 'REFUNDED' | 'ORDER_IN_PROGRESS' | 'NOT_FOUND' | 'MINIAPP_MISMATCH' | 'ERROR';
53
+ /** Authoritative order status returned from the mTLS Apps in Toss partner API. */
54
+ export interface AppsInTossIapOrderStatusResult {
55
+ readonly orderId: string;
56
+ readonly sku: string;
57
+ readonly statusDeterminedAt: string;
58
+ readonly status: AppsInTossIapOrderStatus;
59
+ readonly reason?: string;
60
+ }
30
61
  export interface AppsInTossPartnerApiClient {
31
62
  verifyAnonymousKey(input: {
32
63
  readonly anonymousKey: string;
33
64
  readonly signal?: AbortSignal;
34
65
  }): Promise<boolean>;
35
66
  sendFunctionalMessage(input: SendAppsInTossFunctionalMessageInput): Promise<AppsInTossFunctionalMessageResult>;
67
+ exchangeLoginAuthorizationCode(input: {
68
+ readonly authorizationCode: string;
69
+ readonly referrer: AppsInTossLoginReferrer;
70
+ readonly signal?: AbortSignal;
71
+ }): Promise<AppsInTossLoginToken>;
72
+ getLoginUser(input: {
73
+ readonly accessToken: string;
74
+ readonly signal?: AbortSignal;
75
+ }): Promise<AppsInTossLoginUser>;
76
+ getIapOrderStatus(input: {
77
+ readonly orderId: string;
78
+ /**
79
+ * A user key resolved through Apps in Toss login. Supplying it binds the
80
+ * lookup to the same authenticated user that initiated the purchase.
81
+ */
82
+ readonly tossUserKey: string;
83
+ readonly signal?: AbortSignal;
84
+ }): Promise<AppsInTossIapOrderStatusResult>;
36
85
  }
37
86
  export interface CreateAppsInTossPartnerApiClientInput {
38
87
  readonly mtls: AppsInTossMutualTlsFetcher;
@@ -2,6 +2,9 @@ export const appsInTossPartnerApiBaseUrl = 'https://apps-in-toss-api.toss.im';
2
2
  export const defaultAppsInTossPartnerApiTimeoutMs = 10000;
3
3
  const verifyAnonymousKeyPath = 'api-partner/v1/apps-in-toss/users/anon-key/verify';
4
4
  const sendFunctionalMessagePath = 'api-partner/v1/apps-in-toss/messenger/send-message';
5
+ const exchangeLoginAuthorizationCodePath = 'api-partner/v1/apps-in-toss/user/oauth2/generate-token';
6
+ const getLoginUserPath = 'api-partner/v1/apps-in-toss/user/oauth2/login-me';
7
+ const getIapOrderStatusPath = 'api-partner/v1/apps-in-toss/order/get-order-status';
5
8
  const maximumResponseBodyBytes = 256 * 1024;
6
9
  export class AppsInTossPartnerApiError extends Error {
7
10
  status;
@@ -19,8 +22,9 @@ export function createAppsInTossPartnerApiClient(input) {
19
22
  return {
20
23
  async verifyAnonymousKey(request) {
21
24
  const anonymousKey = normalizeIdentifier(request.anonymousKey, 'anonymousKey');
22
- const response = await postJson({
25
+ const response = await requestJson({
23
26
  mtls: input.mtls,
27
+ method: 'POST',
24
28
  url: new URL(verifyAnonymousKeyPath, baseUrl).href,
25
29
  headers: { 'x-anon-key': anonymousKey },
26
30
  body: undefined,
@@ -39,8 +43,9 @@ export function createAppsInTossPartnerApiClient(input) {
39
43
  async sendFunctionalMessage(request) {
40
44
  const recipient = normalizeRecipient(request.recipient);
41
45
  const templateSetCode = normalizeIdentifier(request.templateSetCode, 'templateSetCode');
42
- const response = await postJson({
46
+ const response = await requestJson({
43
47
  mtls: input.mtls,
48
+ method: 'POST',
44
49
  url: new URL(sendFunctionalMessagePath, baseUrl).href,
45
50
  headers: recipient.type === 'anonymous'
46
51
  ? { 'x-anon-key': recipient.key }
@@ -55,26 +60,83 @@ export function createAppsInTossPartnerApiClient(input) {
55
60
  const envelope = requireSuccessEnvelope(response);
56
61
  return parseFunctionalMessageResult(envelope.success, response.status);
57
62
  },
63
+ async exchangeLoginAuthorizationCode(request) {
64
+ const authorizationCode = normalizeIdentifier(request.authorizationCode, 'authorizationCode');
65
+ const referrer = normalizeLoginReferrer(request.referrer);
66
+ const response = await requestJson({
67
+ mtls: input.mtls,
68
+ method: 'POST',
69
+ url: new URL(exchangeLoginAuthorizationCodePath, baseUrl).href,
70
+ headers: {},
71
+ body: { authorizationCode, referrer },
72
+ timeoutMs,
73
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
74
+ });
75
+ const envelope = requireSuccessEnvelope(response);
76
+ return parseLoginToken(envelope.success, response.status);
77
+ },
78
+ async getLoginUser(request) {
79
+ const accessToken = normalizeOpaqueValue(request.accessToken, 'accessToken', 16384);
80
+ const response = await requestJson({
81
+ mtls: input.mtls,
82
+ method: 'GET',
83
+ url: new URL(getLoginUserPath, baseUrl).href,
84
+ headers: { authorization: `Bearer ${accessToken}` },
85
+ body: undefined,
86
+ timeoutMs,
87
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
88
+ });
89
+ const envelope = requireSuccessEnvelope(response);
90
+ return parseLoginUser(envelope.success, response.status);
91
+ },
92
+ async getIapOrderStatus(request) {
93
+ const orderId = normalizeIdentifier(request.orderId, 'orderId');
94
+ const tossUserKey = normalizeTossUserKey(request.tossUserKey, 'tossUserKey');
95
+ const response = await requestJson({
96
+ mtls: input.mtls,
97
+ method: 'POST',
98
+ url: new URL(getIapOrderStatusPath, baseUrl).href,
99
+ headers: { 'x-toss-user-key': tossUserKey },
100
+ body: { orderId },
101
+ timeoutMs,
102
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
103
+ });
104
+ const envelope = requireSuccessEnvelope(response);
105
+ const orderStatus = parseIapOrderStatus(envelope.success, response.status);
106
+ if (orderStatus.orderId !== orderId) {
107
+ throw new AppsInTossPartnerApiError('Apps in Toss returned a mismatched order id.', response.status);
108
+ }
109
+ return orderStatus;
110
+ },
58
111
  };
59
112
  }
60
- async function postJson(input) {
113
+ async function requestJson(input) {
61
114
  const timeout = createTimeoutSignal(input.signal, input.timeoutMs);
62
115
  try {
63
116
  // The documented anonymous-key verification request uses an empty body
64
- // while still declaring application/json.
65
- const requestBody = input.body === undefined ? '' : JSON.stringify(input.body);
117
+ // while still declaring application/json. GET requests must omit a body.
118
+ let requestBody;
119
+ if (input.method === 'GET') {
120
+ requestBody = undefined;
121
+ }
122
+ else if (input.body === undefined) {
123
+ requestBody = '';
124
+ }
125
+ else {
126
+ requestBody = JSON.stringify(input.body);
127
+ }
66
128
  const headers = new Headers({
67
129
  accept: 'application/json',
68
- 'content-type': 'application/json',
130
+ ...(input.method === 'GET' ? {} : { 'content-type': 'application/json' }),
69
131
  ...input.headers,
70
132
  });
71
133
  let response;
72
134
  let text;
73
135
  try {
74
136
  response = await input.mtls.fetch(input.url, {
75
- method: 'POST',
137
+ method: input.method,
76
138
  headers,
77
- body: requestBody,
139
+ ...(requestBody === undefined ? {} : { body: requestBody }),
78
140
  signal: timeout.signal,
79
141
  });
80
142
  text = await readBoundedResponseText(response);
@@ -181,6 +243,60 @@ function parseFunctionalMessageResult(input, status) {
181
243
  contentIds: readContentIds(input.detail),
182
244
  };
183
245
  }
246
+ function parseLoginToken(input, status) {
247
+ if (!isRecord(input)) {
248
+ throw new AppsInTossPartnerApiError('Apps in Toss returned an invalid login token.', status);
249
+ }
250
+ const tokenType = requireOpaqueValue(input.tokenType, 'tokenType', status, 64).toLowerCase();
251
+ if (tokenType !== 'bearer') {
252
+ throw new AppsInTossPartnerApiError('Apps in Toss returned an unsupported token type.', status);
253
+ }
254
+ return {
255
+ tokenType,
256
+ accessToken: requireOpaqueValue(input.accessToken, 'accessToken', status, 16384),
257
+ refreshToken: requireOpaqueValue(input.refreshToken, 'refreshToken', status, 16384),
258
+ expiresInSeconds: requirePositiveInteger(input.expiresIn, 'expiresIn', status),
259
+ scope: requireOpaqueValue(input.scope, 'scope', status, 4096),
260
+ };
261
+ }
262
+ function parseLoginUser(input, status) {
263
+ if (!isRecord(input)) {
264
+ throw new AppsInTossPartnerApiError('Apps in Toss returned an invalid login user.', status);
265
+ }
266
+ return {
267
+ userKey: requireTossUserKey(input.userKey, 'userKey', status),
268
+ scope: requireOpaqueValue(input.scope, 'scope', status, 4096),
269
+ };
270
+ }
271
+ function parseIapOrderStatus(input, status) {
272
+ if (!isRecord(input)) {
273
+ throw new AppsInTossPartnerApiError('Apps in Toss returned an invalid IAP order status.', status);
274
+ }
275
+ const orderStatus = requireOpaqueValue(input.status, 'status', status, 64);
276
+ if (!appsInTossIapOrderStatuses.has(orderStatus)) {
277
+ throw new AppsInTossPartnerApiError('Apps in Toss returned an unknown IAP order status.', status);
278
+ }
279
+ const reason = input.reason;
280
+ return {
281
+ orderId: requireOpaqueValue(input.orderId, 'orderId', status, 2048),
282
+ sku: requireOpaqueValue(input.sku, 'sku', status, 2048),
283
+ statusDeterminedAt: requireOpaqueValue(input.statusDeterminedAt, 'statusDeterminedAt', status, 2048),
284
+ status: orderStatus,
285
+ ...(reason === undefined
286
+ ? {}
287
+ : { reason: requireOpaqueValue(reason, 'reason', status, 4096) }),
288
+ };
289
+ }
290
+ const appsInTossIapOrderStatuses = new Set([
291
+ 'PURCHASED',
292
+ 'PAYMENT_COMPLETED',
293
+ 'FAILED',
294
+ 'REFUNDED',
295
+ 'ORDER_IN_PROGRESS',
296
+ 'NOT_FOUND',
297
+ 'MINIAPP_MISMATCH',
298
+ 'ERROR',
299
+ ]);
184
300
  function readContentIds(detail) {
185
301
  if (!isRecord(detail)) {
186
302
  return [];
@@ -240,6 +356,72 @@ function normalizeIdentifier(value, field) {
240
356
  }
241
357
  return normalized;
242
358
  }
359
+ function normalizeLoginReferrer(value) {
360
+ if (value !== 'DEFAULT' && value !== 'SANDBOX') {
361
+ throw new TypeError('referrer must be DEFAULT or SANDBOX.');
362
+ }
363
+ return value;
364
+ }
365
+ function normalizeOpaqueValue(value, field, maximumLength) {
366
+ if (value.length === 0
367
+ || value.length > maximumLength
368
+ || value.trim() !== value
369
+ || /[\p{Cc}\p{Cf}]/u.test(value)) {
370
+ throw new TypeError(`${field} must contain 1 to ${maximumLength} characters without surrounding whitespace, control, or format characters.`);
371
+ }
372
+ return value;
373
+ }
374
+ function requireOpaqueValue(value, field, status, maximumLength) {
375
+ if (typeof value !== 'string') {
376
+ throw new AppsInTossPartnerApiError(`Apps in Toss returned an invalid ${field}.`, status);
377
+ }
378
+ try {
379
+ return normalizeOpaqueValue(value, field, maximumLength);
380
+ }
381
+ catch {
382
+ throw new AppsInTossPartnerApiError(`Apps in Toss returned an invalid ${field}.`, status);
383
+ }
384
+ }
385
+ function requirePositiveInteger(value, field, status) {
386
+ let parsed;
387
+ if (typeof value === 'number') {
388
+ parsed = value;
389
+ }
390
+ else if (typeof value === 'string' && /^\d+$/u.test(value)) {
391
+ parsed = Number(value);
392
+ }
393
+ else {
394
+ parsed = Number.NaN;
395
+ }
396
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
397
+ throw new AppsInTossPartnerApiError(`Apps in Toss returned an invalid ${field}.`, status);
398
+ }
399
+ return parsed;
400
+ }
401
+ function parseTossUserKey(value) {
402
+ let normalized;
403
+ if (typeof value === 'number') {
404
+ normalized = Number.isSafeInteger(value) && value > 0 ? String(value) : undefined;
405
+ }
406
+ else if (typeof value === 'string' && /^[1-9]\d*$/u.test(value)) {
407
+ normalized = value;
408
+ }
409
+ return normalized;
410
+ }
411
+ function normalizeTossUserKey(value, field) {
412
+ const normalized = parseTossUserKey(value);
413
+ if (normalized === undefined) {
414
+ throw new TypeError(`${field} must be a positive Apps in Toss user key.`);
415
+ }
416
+ return normalized;
417
+ }
418
+ function requireTossUserKey(value, field, status) {
419
+ const normalized = parseTossUserKey(value);
420
+ if (normalized === undefined) {
421
+ throw new AppsInTossPartnerApiError(`Apps in Toss returned an invalid ${field}.`, status);
422
+ }
423
+ return normalized;
424
+ }
243
425
  function normalizeBaseUrl(input) {
244
426
  const url = new URL(input);
245
427
  if (url.protocol !== 'https:'
package/dist/client.js CHANGED
@@ -67,13 +67,30 @@ export function createGameServicesClient(input) {
67
67
  purchase,
68
68
  };
69
69
  }
70
+ if (isAuthoritativeMicrosoftStoreCompletion(target, purchase)) {
71
+ const result = {
72
+ status: 'granted',
73
+ purchase,
74
+ ledgerEntryId: purchase.transactionId,
75
+ };
76
+ await analytics.track({
77
+ name: 'purchase_granted',
78
+ properties: {
79
+ productId: purchaseInput.productId,
80
+ status: result.status,
81
+ ledgerEntryId: purchase.transactionId,
82
+ alreadyProcessed: purchase.authoritativeGrant?.alreadyProcessed,
83
+ },
84
+ });
85
+ return result;
86
+ }
70
87
  if (purchase.status !== 'completed' || purchase.transactionId === undefined) {
71
88
  await analytics.track({
72
89
  name: 'purchase_rejected',
73
90
  properties: {
74
91
  productId: purchaseInput.productId,
75
92
  status: purchase.status,
76
- reason: purchase.transactionId === undefined ? 'missing_transaction_id' : undefined,
93
+ reason: purchaseRejectionReason(purchase),
77
94
  },
78
95
  });
79
96
  return {
@@ -323,14 +340,34 @@ export function createGameServicesIdempotencyKey(input) {
323
340
  function normalizeSegment(value) {
324
341
  return value.replaceAll(/[^a-zA-Z0-9_-]+/g, '-').replaceAll(/^-|-$/g, '').slice(0, 64);
325
342
  }
343
+ function purchaseRejectionReason(purchase) {
344
+ if (purchase.status === 'completed' && purchase.transactionId === undefined) {
345
+ return 'missing_transaction_id';
346
+ }
347
+ if (purchase.status === 'pending') {
348
+ return 'purchase_pending';
349
+ }
350
+ return undefined;
351
+ }
352
+ function isAuthoritativeMicrosoftStoreCompletion(target, purchase) {
353
+ return target === 'microsoft-store'
354
+ && purchase.status === 'completed'
355
+ && purchase.transactionId !== undefined
356
+ && purchase.authoritativeGrant?.ledgerEntryId === purchase.transactionId;
357
+ }
326
358
  function isGameServicesCommerceTarget(target) {
327
- return target === 'android' || target === 'ios' || target === 'ait' || target === 'verse8';
359
+ return target === 'microsoft-store'
360
+ || target === 'android'
361
+ || target === 'ios'
362
+ || target === 'ait'
363
+ || target === 'verse8';
328
364
  }
329
365
  function isGameServicesAdRewardTarget(target) {
330
366
  return target === 'android' || target === 'ios' || target === 'ait' || target === 'verse8';
331
367
  }
332
368
  function isGameServicesLeaderboardTarget(target) {
333
369
  return target === 'browser'
370
+ || target === 'microsoft-store'
334
371
  || target === 'android'
335
372
  || target === 'ios'
336
373
  || target === 'ait'
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from './client';
6
6
  export * from './contract';
7
7
  export * from './evidence-verification';
8
8
  export * from './google-play-purchase';
9
+ export * from './microsoft-store-purchase';
9
10
  export * from './notification-delivery';
10
11
  export * from './platform-order';
11
12
  export * from './progress-link';
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export * from './client';
6
6
  export * from './contract';
7
7
  export * from './evidence-verification';
8
8
  export * from './google-play-purchase';
9
+ export * from './microsoft-store-purchase';
9
10
  export * from './notification-delivery';
10
11
  export * from './platform-order';
11
12
  export * from './progress-link';
@@ -0,0 +1,131 @@
1
+ import type { GameServicesEvidenceVerifier, GameServicesPurchaseGrantFinalizer, VerifyPurchaseEvidenceInput } from './evidence-verification';
2
+ export declare const microsoftStoreDigitalGoodsEvidenceSchema: 'mpgd.microsoft-store.digital-goods.v1';
3
+ export declare const microsoftStoreCollectionsQueryUrl: 'https://collections.mp.microsoft.com/v9.0/collections/publisherQuery';
4
+ export declare const microsoftStoreCollectionsConsumeUrl: 'https://collections.mp.microsoft.com/v8.0/collections/consume';
5
+ export interface MicrosoftStoreCollectionsCredentials {
6
+ /** Runtime-generated Microsoft Entra service access token. */
7
+ readonly accessToken: string;
8
+ /** Server-resolved User Store ID. Never accept this value from purchase evidence. */
9
+ readonly userStoreId: string;
10
+ /**
11
+ * Stable opaque ID for the trusted player-to-Store-account link. It must survive User Store ID
12
+ * renewal, resolve to the same value whenever the same Store account appears, and change only
13
+ * when the player links a different Store account. Never use the User Store ID, a player-pair
14
+ * identifier, or another renewable credential as this value.
15
+ */
16
+ readonly accountBindingId: string;
17
+ /** Omit for RETAIL. Non-RETAIL developer-managed consume requires delegated XSTS. */
18
+ readonly sandbox?: string;
19
+ }
20
+ export interface MicrosoftStoreCollectionsClient {
21
+ queryProduct(input: {
22
+ readonly credentials: MicrosoftStoreCollectionsCredentials;
23
+ readonly storeId: string;
24
+ readonly signal: AbortSignal;
25
+ }): Promise<unknown>;
26
+ consumeProduct(input: {
27
+ readonly credentials: MicrosoftStoreCollectionsCredentials;
28
+ readonly storeId: string;
29
+ readonly trackingId: string;
30
+ readonly signal: AbortSignal;
31
+ }): Promise<unknown>;
32
+ }
33
+ export interface MicrosoftStoreFetchResponse {
34
+ readonly status: number;
35
+ readonly body: ReadableStream<Uint8Array> | null;
36
+ }
37
+ export type MicrosoftStoreFetch = (url: string, init: {
38
+ readonly method: 'POST';
39
+ readonly headers: Readonly<Record<string, string>>;
40
+ readonly body: string;
41
+ readonly signal: AbortSignal;
42
+ }) => Promise<MicrosoftStoreFetchResponse>;
43
+ export interface CreateMicrosoftStoreCollectionsClientInput {
44
+ readonly fetch?: MicrosoftStoreFetch;
45
+ readonly userAgent?: string;
46
+ readonly maxResponseBytes?: number;
47
+ }
48
+ export interface CreateMicrosoftStorePurchaseBoundaryInput {
49
+ readonly client: MicrosoftStoreCollectionsClient;
50
+ /** Trusted logical-product to current Digital Goods Product ID mapping. */
51
+ readonly inAppOfferTokens: Readonly<Record<string, string>>;
52
+ readonly storeIds: Readonly<Record<string, string>>;
53
+ /** Keep old Digital Goods token/Collections ID pairs until pending purchases are settled. */
54
+ readonly historicalProductMappings?: Readonly<Record<string, readonly MicrosoftStoreHistoricalProductMapping[]>>;
55
+ readonly resolveCredentials: (playerId: string, signal: AbortSignal) => Promise<MicrosoftStoreCollectionsCredentials> | MicrosoftStoreCollectionsCredentials;
56
+ /**
57
+ * Durable, atomic ownership registry. Browser storage is never an authorization source.
58
+ * Production implementations must share this state across every game-service instance.
59
+ */
60
+ readonly recoveryOwnershipStore: MicrosoftStoreRecoveryOwnershipStore;
61
+ readonly now?: () => string;
62
+ }
63
+ export interface MicrosoftStoreHistoricalProductMapping {
64
+ readonly inAppOfferToken: string;
65
+ readonly storeId: string;
66
+ }
67
+ export interface MicrosoftStoreRecoveryOwnershipInput {
68
+ readonly playerId: string;
69
+ readonly productId: string;
70
+ readonly inAppOfferToken: string;
71
+ readonly purchaseToken: string;
72
+ /** Stable checkout identity used as an opaque ownership generation. */
73
+ readonly idempotencyKey?: string;
74
+ readonly evidence: {
75
+ readonly schema: string;
76
+ readonly payload: Readonly<Record<string, unknown>>;
77
+ };
78
+ readonly signal: AbortSignal;
79
+ }
80
+ export interface MicrosoftStoreRecoveryOwnershipRecord {
81
+ readonly accountBindingHash: string;
82
+ readonly storeId: string;
83
+ readonly playerId: string;
84
+ readonly generation: string;
85
+ /** Stable identity derived from the exact Collections item being granted. */
86
+ readonly providerPurchaseId: string;
87
+ }
88
+ export type MicrosoftStoreRecoveryOwnershipLookup = Omit<MicrosoftStoreRecoveryOwnershipRecord, 'generation' | 'providerPurchaseId'> & {
89
+ readonly generation?: string;
90
+ readonly providerPurchaseId?: string;
91
+ };
92
+ export type MicrosoftStoreRecoveryOwnershipResult = {
93
+ readonly status: 'granted';
94
+ readonly idempotencyKey: string;
95
+ } | {
96
+ readonly status: 'denied';
97
+ } | {
98
+ readonly status: 'unavailable';
99
+ };
100
+ export interface MicrosoftStoreRecoveryOwnershipStore {
101
+ /**
102
+ * Atomically claims an unconsumed Store product for one authenticated game player. An
103
+ * existing claim for the same provider purchase remains unchanged so a retry cannot replace
104
+ * its generation. A different provider purchase may atomically replace the same player's
105
+ * consumed purchase claim. Return the effective record, or undefined when another player owns
106
+ * the Store product binding.
107
+ */
108
+ claim(input: MicrosoftStoreRecoveryOwnershipRecord): Promise<MicrosoftStoreRecoveryOwnershipRecord | undefined>;
109
+ get(input: MicrosoftStoreRecoveryOwnershipLookup): Promise<MicrosoftStoreRecoveryOwnershipRecord | undefined>;
110
+ /** Atomically releases only the exact player, generation, and provider purchase supplied. */
111
+ release(input: MicrosoftStoreRecoveryOwnershipRecord): Promise<void>;
112
+ }
113
+ export interface MicrosoftStorePurchaseBoundary extends GameServicesPurchaseGrantFinalizer {
114
+ claimRecoveryOwnership(input: MicrosoftStoreRecoveryOwnershipInput): Promise<MicrosoftStoreRecoveryOwnershipResult>;
115
+ hasRecoveryOwnership(input: MicrosoftStoreRecoveryOwnershipInput): Promise<MicrosoftStoreRecoveryOwnershipResult>;
116
+ verifyPurchase(input: VerifyPurchaseEvidenceInput): ReturnType<GameServicesEvidenceVerifier['verifyPurchase']>;
117
+ }
118
+ /** Process-local test/development store. Production requires durable atomic shared storage. */
119
+ export declare class InMemoryMicrosoftStoreRecoveryOwnershipStore implements MicrosoftStoreRecoveryOwnershipStore {
120
+ private readonly owners;
121
+ claim(input: MicrosoftStoreRecoveryOwnershipRecord): Promise<MicrosoftStoreRecoveryOwnershipRecord | undefined>;
122
+ get(input: MicrosoftStoreRecoveryOwnershipLookup): Promise<MicrosoftStoreRecoveryOwnershipRecord | undefined>;
123
+ release(input: MicrosoftStoreRecoveryOwnershipRecord): Promise<void>;
124
+ }
125
+ export declare function createInMemoryMicrosoftStoreRecoveryOwnershipStore(): MicrosoftStoreRecoveryOwnershipStore;
126
+ export declare class MicrosoftStoreCollectionsUnavailableError extends Error {
127
+ readonly cause: unknown;
128
+ constructor(message: string, cause?: unknown);
129
+ }
130
+ export declare function createMicrosoftStoreCollectionsClient(input?: CreateMicrosoftStoreCollectionsClientInput): MicrosoftStoreCollectionsClient;
131
+ export declare function createMicrosoftStorePurchaseBoundary(input: CreateMicrosoftStorePurchaseBoundaryInput): MicrosoftStorePurchaseBoundary;