@mpgd/game-services 0.11.1 → 0.12.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/apps-in-toss-partner-api.d.ts +49 -0
- package/dist/apps-in-toss-partner-api.js +190 -8
- package/dist/client.js +39 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/microsoft-store-purchase.d.ts +131 -0
- package/dist/microsoft-store-purchase.js +835 -0
- package/dist/notification-delivery.js +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +9 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +37 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.js +7 -3
- package/package.json +9 -5
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 === '
|
|
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;
|