@mpgd/game-services 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/dist/admob-ssv-conformance.d.ts +2 -2
- package/dist/admob-ssv.d.ts +2 -2
- package/dist/app-store-verifier-conformance.d.ts +1 -1
- package/dist/app-store-verifier.d.ts +1 -1
- package/dist/apps-in-toss-evidence-verification-conformance.d.ts +2 -2
- package/dist/apps-in-toss-evidence-verification-conformance.js +2 -2
- package/dist/apps-in-toss-evidence-verification.d.ts +2 -2
- package/dist/apps-in-toss-partner-api.d.ts +1 -1
- package/dist/client.d.ts +7 -27
- package/dist/client.js +162 -147
- package/dist/contract.d.ts +1 -1
- package/dist/evidence-verification.d.ts +1 -1
- package/dist/google-play-purchase-conformance.d.ts +1 -1
- package/dist/google-play-purchase-conformance.js +1 -1
- package/dist/google-play-purchase.d.ts +1 -1
- package/dist/index.d.ts +16 -15
- package/dist/index.js +16 -15
- package/dist/microsoft-store-identity.d.ts +39 -0
- package/dist/microsoft-store-identity.js +304 -0
- package/dist/microsoft-store-purchase.d.ts +1 -1
- package/dist/notification-delivery.js +3 -1
- package/dist/operation-progress.d.ts +47 -0
- package/dist/operation-progress.js +87 -0
- package/dist/operations.d.ts +30 -0
- package/dist/operations.js +1 -0
- package/dist/progress-link.js +1 -1
- package/dist/server.d.ts +6 -6
- package/dist/server.js +9 -6
- package/dist/verified-leaderboard-conformance.d.ts +1 -1
- package/dist/verified-leaderboard-conformance.js +1 -1
- package/dist/verified-leaderboard-durability-conformance.d.ts +1 -1
- package/dist/verified-leaderboard-durability-conformance.js +1 -1
- package/package.json +13 -5
package/dist/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
export * from './admob-ssv.js';
|
|
2
|
-
export * from './app-store-verifier';
|
|
3
|
-
export * from './apps-in-toss-evidence-verification';
|
|
4
|
-
export * from './apps-in-toss-partner-api';
|
|
5
|
-
export * from './client';
|
|
6
|
-
export * from './contract';
|
|
7
|
-
export * from './evidence-verification';
|
|
8
|
-
export * from './google-play-purchase';
|
|
9
|
-
export * from './microsoft-store-
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './
|
|
12
|
-
export * from './
|
|
2
|
+
export * from './app-store-verifier.js';
|
|
3
|
+
export * from './apps-in-toss-evidence-verification.js';
|
|
4
|
+
export * from './apps-in-toss-partner-api.js';
|
|
5
|
+
export * from './client.js';
|
|
6
|
+
export * from './contract.js';
|
|
7
|
+
export * from './evidence-verification.js';
|
|
8
|
+
export * from './google-play-purchase.js';
|
|
9
|
+
export * from './microsoft-store-identity.js';
|
|
10
|
+
export * from './microsoft-store-purchase.js';
|
|
11
|
+
export * from './notification-delivery.js';
|
|
12
|
+
export * from './platform-order.js';
|
|
13
|
+
export * from './progress-link.js';
|
|
13
14
|
export * from './runtime.js';
|
|
14
|
-
export * from './server';
|
|
15
|
-
export * from './types';
|
|
16
|
-
export * from './verified-leaderboard';
|
|
17
|
-
export * from './verified-leaderboard-transport';
|
|
15
|
+
export * from './server.js';
|
|
16
|
+
export * from './types.js';
|
|
17
|
+
export * from './verified-leaderboard.js';
|
|
18
|
+
export * from './verified-leaderboard-transport.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MicrosoftStoreCollectionsCredentials } from './microsoft-store-purchase.js';
|
|
2
|
+
export declare const microsoftStoreIdentityCredentialsSchema: 'mpgd.microsoft-store.identity-credentials.v1';
|
|
3
|
+
export declare const microsoftStoreIdentityCredentialsRequestUrl: 'https://microsoft-store-authority.internal/v1/credentials';
|
|
4
|
+
export interface MicrosoftStoreIdentityCredentialsRequest {
|
|
5
|
+
readonly schema: typeof microsoftStoreIdentityCredentialsSchema;
|
|
6
|
+
readonly gameId: string;
|
|
7
|
+
readonly playerId: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MicrosoftStoreIdentityCredentialsResponse extends MicrosoftStoreCollectionsCredentials {
|
|
10
|
+
readonly schema: typeof microsoftStoreIdentityCredentialsSchema;
|
|
11
|
+
readonly gameId: string;
|
|
12
|
+
readonly playerId: string;
|
|
13
|
+
}
|
|
14
|
+
export interface MicrosoftStoreIdentityAuthorityFetcher {
|
|
15
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
16
|
+
}
|
|
17
|
+
export declare class MicrosoftStoreIdentityAuthorityError extends Error {
|
|
18
|
+
readonly code: 'MICROSOFT_STORE_ACCOUNT_LINK_REQUIRED' | 'MICROSOFT_STORE_IDENTITY_RESPONSE_INVALID' | 'MICROSOFT_STORE_IDENTITY_UNAVAILABLE';
|
|
19
|
+
readonly name = "MicrosoftStoreIdentityAuthorityError";
|
|
20
|
+
constructor(code: 'MICROSOFT_STORE_ACCOUNT_LINK_REQUIRED' | 'MICROSOFT_STORE_IDENTITY_RESPONSE_INVALID' | 'MICROSOFT_STORE_IDENTITY_UNAVAILABLE', options?: ErrorOptions);
|
|
21
|
+
}
|
|
22
|
+
export declare function parseMicrosoftStoreIdentityCredentialsRequest(value: unknown): MicrosoftStoreIdentityCredentialsRequest;
|
|
23
|
+
export declare function createMicrosoftStoreIdentityCredentialsResponse(input: {
|
|
24
|
+
readonly request: MicrosoftStoreIdentityCredentialsRequest;
|
|
25
|
+
readonly credentials: MicrosoftStoreCollectionsCredentials;
|
|
26
|
+
}): MicrosoftStoreIdentityCredentialsResponse;
|
|
27
|
+
export declare function parseMicrosoftStoreIdentityCredentialsResponse(value: unknown, expectedRequest?: MicrosoftStoreIdentityCredentialsRequest): MicrosoftStoreIdentityCredentialsResponse;
|
|
28
|
+
/**
|
|
29
|
+
* Resolves short-lived Store credentials over a private service binding. The authority request is
|
|
30
|
+
* scoped by both game and player so a shared provider Worker cannot accidentally reuse one game's
|
|
31
|
+
* account link for another game. The returned credentials must never be exposed to a browser.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveMicrosoftStoreIdentityCredentials(input: {
|
|
34
|
+
readonly authority: MicrosoftStoreIdentityAuthorityFetcher;
|
|
35
|
+
readonly gameId: string;
|
|
36
|
+
readonly playerId: string;
|
|
37
|
+
readonly signal?: AbortSignal;
|
|
38
|
+
readonly timeoutMs?: number;
|
|
39
|
+
}): Promise<MicrosoftStoreIdentityCredentialsResponse>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
export const microsoftStoreIdentityCredentialsSchema = 'mpgd.microsoft-store.identity-credentials.v1';
|
|
2
|
+
export const microsoftStoreIdentityCredentialsRequestUrl = 'https://microsoft-store-authority.internal/v1/credentials';
|
|
3
|
+
const defaultTimeoutMs = 8000;
|
|
4
|
+
const maximumResponseBytes = 16 * 1024;
|
|
5
|
+
const gameIdPattern = /^[a-z0-9][a-z0-9-]{0,63}$/u;
|
|
6
|
+
export class MicrosoftStoreIdentityAuthorityError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
name = 'MicrosoftStoreIdentityAuthorityError';
|
|
9
|
+
constructor(code, options) {
|
|
10
|
+
super(code, options);
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function parseMicrosoftStoreIdentityCredentialsRequest(value) {
|
|
15
|
+
const record = requireExactRecord(value, ['schema', 'gameId', 'playerId']);
|
|
16
|
+
if (record.schema !== microsoftStoreIdentityCredentialsSchema) {
|
|
17
|
+
throw new TypeError('Microsoft Store identity request schema is invalid.');
|
|
18
|
+
}
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
schema: microsoftStoreIdentityCredentialsSchema,
|
|
21
|
+
gameId: requireGameId(record.gameId),
|
|
22
|
+
playerId: requireText(record.playerId, 'playerId', 256),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function createMicrosoftStoreIdentityCredentialsResponse(input) {
|
|
26
|
+
const request = parseMicrosoftStoreIdentityCredentialsRequest(input.request);
|
|
27
|
+
return parseMicrosoftStoreIdentityCredentialsResponse({
|
|
28
|
+
schema: microsoftStoreIdentityCredentialsSchema,
|
|
29
|
+
gameId: request.gameId,
|
|
30
|
+
playerId: request.playerId,
|
|
31
|
+
accessToken: input.credentials.accessToken,
|
|
32
|
+
userStoreId: input.credentials.userStoreId,
|
|
33
|
+
accountBindingId: input.credentials.accountBindingId,
|
|
34
|
+
...(input.credentials.sandbox === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { sandbox: input.credentials.sandbox }),
|
|
37
|
+
}, request);
|
|
38
|
+
}
|
|
39
|
+
export function parseMicrosoftStoreIdentityCredentialsResponse(value, expectedRequest) {
|
|
40
|
+
const record = requireExactRecord(value, ['schema', 'gameId', 'playerId', 'accessToken', 'userStoreId', 'accountBindingId'], ['sandbox']);
|
|
41
|
+
if (record.schema !== microsoftStoreIdentityCredentialsSchema) {
|
|
42
|
+
throw new TypeError('Microsoft Store identity response schema is invalid.');
|
|
43
|
+
}
|
|
44
|
+
const response = Object.freeze({
|
|
45
|
+
schema: microsoftStoreIdentityCredentialsSchema,
|
|
46
|
+
gameId: requireGameId(record.gameId),
|
|
47
|
+
playerId: requireText(record.playerId, 'response playerId', 256),
|
|
48
|
+
accessToken: requireToken(record.accessToken, 'Microsoft Store access token', 4096),
|
|
49
|
+
userStoreId: requireToken(record.userStoreId, 'Microsoft Store User Store ID', 4096),
|
|
50
|
+
accountBindingId: requireText(record.accountBindingId, 'Microsoft Store account binding ID', 512),
|
|
51
|
+
...(record.sandbox === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { sandbox: requireText(record.sandbox, 'Microsoft Store sandbox', 256) }),
|
|
54
|
+
});
|
|
55
|
+
if (expectedRequest !== undefined
|
|
56
|
+
&& (response.gameId !== expectedRequest.gameId
|
|
57
|
+
|| response.playerId !== expectedRequest.playerId)) {
|
|
58
|
+
throw new TypeError('Microsoft Store identity response scope does not match the request.');
|
|
59
|
+
}
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolves short-lived Store credentials over a private service binding. The authority request is
|
|
64
|
+
* scoped by both game and player so a shared provider Worker cannot accidentally reuse one game's
|
|
65
|
+
* account link for another game. The returned credentials must never be exposed to a browser.
|
|
66
|
+
*/
|
|
67
|
+
export async function resolveMicrosoftStoreIdentityCredentials(input) {
|
|
68
|
+
const request = parseMicrosoftStoreIdentityCredentialsRequest({
|
|
69
|
+
schema: microsoftStoreIdentityCredentialsSchema,
|
|
70
|
+
gameId: input.gameId,
|
|
71
|
+
playerId: input.playerId,
|
|
72
|
+
});
|
|
73
|
+
const timeoutMs = input.timeoutMs ?? defaultTimeoutMs;
|
|
74
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 25000) {
|
|
75
|
+
throw new TypeError('Microsoft Store identity timeout is invalid.');
|
|
76
|
+
}
|
|
77
|
+
const abort = createAbortScope(input.signal, timeoutMs);
|
|
78
|
+
try {
|
|
79
|
+
let response;
|
|
80
|
+
try {
|
|
81
|
+
response = await raceWithAbort(input.authority.fetch(microsoftStoreIdentityCredentialsRequestUrl, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
Accept: 'application/json',
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(request),
|
|
88
|
+
signal: abort.signal,
|
|
89
|
+
}), abort.signal);
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
if (isAbortCause(abort.signal, cause)) {
|
|
93
|
+
throw abort.signal.reason;
|
|
94
|
+
}
|
|
95
|
+
throw unavailable(cause);
|
|
96
|
+
}
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
await discardResponseBody(response, abort.signal);
|
|
99
|
+
const accountLinkRequired = response.status === 404 || response.status === 409;
|
|
100
|
+
const code = accountLinkRequired
|
|
101
|
+
? 'MICROSOFT_STORE_ACCOUNT_LINK_REQUIRED'
|
|
102
|
+
: 'MICROSOFT_STORE_IDENTITY_UNAVAILABLE';
|
|
103
|
+
const options = accountLinkRequired
|
|
104
|
+
? undefined
|
|
105
|
+
: { cause: authorityHttpStatusError(response.status) };
|
|
106
|
+
throw new MicrosoftStoreIdentityAuthorityError(code, options);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return parseMicrosoftStoreIdentityCredentialsResponse(await readBoundedJson(response, maximumResponseBytes, abort.signal), request);
|
|
110
|
+
}
|
|
111
|
+
catch (cause) {
|
|
112
|
+
if (cause instanceof MicrosoftStoreIdentityAuthorityError) {
|
|
113
|
+
throw cause;
|
|
114
|
+
}
|
|
115
|
+
if (isAbortCause(abort.signal, cause)) {
|
|
116
|
+
throw abort.signal.reason;
|
|
117
|
+
}
|
|
118
|
+
throw invalidResponse(cause);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
abort.cleanup();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function requireExactRecord(value, requiredKeys, optionalKeys = []) {
|
|
126
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
127
|
+
throw new TypeError('Microsoft Store identity payload must be an object.');
|
|
128
|
+
}
|
|
129
|
+
const record = value;
|
|
130
|
+
const allowed = new Set([...requiredKeys, ...optionalKeys]);
|
|
131
|
+
if (requiredKeys.some((key) => !Object.hasOwn(record, key))
|
|
132
|
+
|| Object.keys(record).some((key) => !allowed.has(key))) {
|
|
133
|
+
throw new TypeError('Microsoft Store identity payload shape is invalid.');
|
|
134
|
+
}
|
|
135
|
+
return record;
|
|
136
|
+
}
|
|
137
|
+
function requireGameId(value) {
|
|
138
|
+
if (typeof value !== 'string' || !gameIdPattern.test(value)) {
|
|
139
|
+
throw new TypeError('Microsoft Store identity gameId is invalid.');
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
function requireText(value, label, maximumLength) {
|
|
144
|
+
if (typeof value !== 'string'
|
|
145
|
+
|| value.length === 0
|
|
146
|
+
|| value.length > maximumLength
|
|
147
|
+
|| value.trim() !== value
|
|
148
|
+
|| /[\p{Cc}\p{Cf}]/u.test(value)
|
|
149
|
+
|| !isWellFormedUnicode(value)) {
|
|
150
|
+
throw new TypeError(`${label} is invalid.`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function requireToken(value, label, maximumLength) {
|
|
155
|
+
const token = requireText(value, label, maximumLength);
|
|
156
|
+
if (/\s/u.test(token)) {
|
|
157
|
+
throw new TypeError(`${label} is invalid.`);
|
|
158
|
+
}
|
|
159
|
+
return token;
|
|
160
|
+
}
|
|
161
|
+
function isWellFormedUnicode(input) {
|
|
162
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
163
|
+
const codeUnit = input.charCodeAt(index);
|
|
164
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
165
|
+
const nextCodeUnit = input.charCodeAt(index + 1);
|
|
166
|
+
if (index + 1 >= input.length
|
|
167
|
+
|| nextCodeUnit < 0xdc00
|
|
168
|
+
|| nextCodeUnit > 0xdfff) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
index += 1;
|
|
172
|
+
}
|
|
173
|
+
else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
function createAbortScope(upstream, timeoutMs) {
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const onAbort = () => controller.abort(upstream?.reason);
|
|
182
|
+
if (upstream?.aborted === true) {
|
|
183
|
+
onAbort();
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
upstream?.addEventListener('abort', onAbort, { once: true });
|
|
187
|
+
}
|
|
188
|
+
const timeout = setTimeout(() => controller.abort(new Error('Microsoft Store identity request timed out.')), timeoutMs);
|
|
189
|
+
return Object.freeze({
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
cleanup() {
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
upstream?.removeEventListener('abort', onAbort);
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function readBoundedJson(response, maximumBytes, signal) {
|
|
198
|
+
const contentLengthHeader = response.headers.get('content-length');
|
|
199
|
+
if (contentLengthHeader !== null) {
|
|
200
|
+
if (!/^\d+$/u.test(contentLengthHeader)) {
|
|
201
|
+
await discardResponseBody(response, signal);
|
|
202
|
+
throw new TypeError('Microsoft Store identity response content length is invalid.');
|
|
203
|
+
}
|
|
204
|
+
const contentLength = Number(contentLengthHeader);
|
|
205
|
+
if (!Number.isSafeInteger(contentLength)) {
|
|
206
|
+
await discardResponseBody(response, signal);
|
|
207
|
+
throw new TypeError('Microsoft Store identity response content length is invalid.');
|
|
208
|
+
}
|
|
209
|
+
if (contentLength > maximumBytes) {
|
|
210
|
+
await discardResponseBody(response, signal);
|
|
211
|
+
throw new TypeError('Microsoft Store identity response is too large.');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (response.body === null) {
|
|
215
|
+
if (signal.aborted) {
|
|
216
|
+
throw signal.reason;
|
|
217
|
+
}
|
|
218
|
+
throw new TypeError('Microsoft Store identity response body is missing.');
|
|
219
|
+
}
|
|
220
|
+
const reader = response.body.getReader();
|
|
221
|
+
const chunks = [];
|
|
222
|
+
let total = 0;
|
|
223
|
+
try {
|
|
224
|
+
while (true) {
|
|
225
|
+
if (signal.aborted) {
|
|
226
|
+
throw signal.reason;
|
|
227
|
+
}
|
|
228
|
+
const result = await raceWithAbort(reader.read(), signal);
|
|
229
|
+
if (result.done) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (!(result.value instanceof Uint8Array)) {
|
|
233
|
+
throw new TypeError('Microsoft Store identity response chunk is invalid.');
|
|
234
|
+
}
|
|
235
|
+
total += result.value.byteLength;
|
|
236
|
+
if (total > maximumBytes) {
|
|
237
|
+
throw new TypeError('Microsoft Store identity response is too large.');
|
|
238
|
+
}
|
|
239
|
+
chunks.push(result.value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
await raceWithAbort(reader.cancel(), signal).catch(() => undefined);
|
|
244
|
+
try {
|
|
245
|
+
reader.releaseLock();
|
|
246
|
+
}
|
|
247
|
+
catch (cause) {
|
|
248
|
+
if (!signal.aborted) {
|
|
249
|
+
throw cause;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (signal.aborted) {
|
|
253
|
+
throw signal.reason;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const bytes = new Uint8Array(total);
|
|
257
|
+
let offset = 0;
|
|
258
|
+
for (const chunk of chunks) {
|
|
259
|
+
bytes.set(chunk, offset);
|
|
260
|
+
offset += chunk.byteLength;
|
|
261
|
+
}
|
|
262
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
263
|
+
}
|
|
264
|
+
function unavailable(cause) {
|
|
265
|
+
return new MicrosoftStoreIdentityAuthorityError('MICROSOFT_STORE_IDENTITY_UNAVAILABLE', cause === undefined ? undefined : { cause });
|
|
266
|
+
}
|
|
267
|
+
function invalidResponse(cause) {
|
|
268
|
+
return new MicrosoftStoreIdentityAuthorityError('MICROSOFT_STORE_IDENTITY_RESPONSE_INVALID', cause === undefined ? undefined : { cause });
|
|
269
|
+
}
|
|
270
|
+
function authorityHttpStatusError(status) {
|
|
271
|
+
return new Error(`Microsoft Store identity authority returned HTTP ${String(status)}.`);
|
|
272
|
+
}
|
|
273
|
+
async function discardResponseBody(response, signal) {
|
|
274
|
+
if (response.body !== null) {
|
|
275
|
+
await raceWithAbort(response.body.cancel(), signal).catch(() => undefined);
|
|
276
|
+
}
|
|
277
|
+
if (signal.aborted) {
|
|
278
|
+
throw signal.reason;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function raceWithAbort(operation, signal) {
|
|
282
|
+
if (signal.aborted) {
|
|
283
|
+
return Promise.race([operation, Promise.reject(signal.reason)]);
|
|
284
|
+
}
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
const onAbort = () => {
|
|
287
|
+
signal.removeEventListener('abort', onAbort);
|
|
288
|
+
reject(signal.reason);
|
|
289
|
+
};
|
|
290
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
291
|
+
operation.then((value) => {
|
|
292
|
+
signal.removeEventListener('abort', onAbort);
|
|
293
|
+
resolve(value);
|
|
294
|
+
}, (cause) => {
|
|
295
|
+
signal.removeEventListener('abort', onAbort);
|
|
296
|
+
reject(cause);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function isAbortCause(signal, cause) {
|
|
301
|
+
return signal.aborted
|
|
302
|
+
&& (cause === signal.reason
|
|
303
|
+
|| (cause instanceof Error && cause.name === 'AbortError'));
|
|
304
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GameServicesEvidenceVerifier, GameServicesPurchaseGrantFinalizer, VerifyPurchaseEvidenceInput } from './evidence-verification';
|
|
1
|
+
import type { GameServicesEvidenceVerifier, GameServicesPurchaseGrantFinalizer, VerifyPurchaseEvidenceInput } from './evidence-verification.js';
|
|
2
2
|
export declare const microsoftStoreDigitalGoodsEvidenceSchema: 'mpgd.microsoft-store.digital-goods.v1';
|
|
3
3
|
export declare const microsoftStoreCollectionsQueryUrl: 'https://collections.mp.microsoft.com/v9.0/collections/publisherQuery';
|
|
4
4
|
export declare const microsoftStoreCollectionsConsumeUrl: 'https://collections.mp.microsoft.com/v8.0/collections/consume';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertOwnEnumerablePropertyLimit } from './validation';
|
|
1
|
+
import { assertOwnEnumerablePropertyLimit } from './validation.js';
|
|
2
2
|
const platformTargets = new Set([
|
|
3
3
|
'browser',
|
|
4
4
|
'android',
|
|
@@ -9,6 +9,8 @@ const platformTargets = new Set([
|
|
|
9
9
|
'telegram',
|
|
10
10
|
'tauri',
|
|
11
11
|
'microsoft-store',
|
|
12
|
+
'wechat',
|
|
13
|
+
'tiktok',
|
|
12
14
|
]);
|
|
13
15
|
const notificationTopics = new Set([
|
|
14
16
|
'daily-ready',
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PurchaseResult, RewardedAdResult } from '@mpgd/platform';
|
|
2
|
+
import type { GameServicesPurchaseResult, GameServicesRewardedAdResult } from './operations.js';
|
|
3
|
+
export type GameServicesOperationKind = 'purchase' | 'rewarded-ad';
|
|
4
|
+
export type GameServicesOperationLocation = 'local' | 'platform' | 'server';
|
|
5
|
+
type PlatformStatus<K extends GameServicesOperationKind> = K extends 'purchase' ? PurchaseResult['status'] : RewardedAdResult['status'];
|
|
6
|
+
type OperationResult<K extends GameServicesOperationKind> = K extends 'purchase' ? GameServicesPurchaseResult : GameServicesRewardedAdResult;
|
|
7
|
+
type ProgressStage<K extends GameServicesOperationKind> = {
|
|
8
|
+
readonly phase: 'platform-requested';
|
|
9
|
+
} | {
|
|
10
|
+
readonly phase: 'platform-result';
|
|
11
|
+
readonly status: PlatformStatus<K>;
|
|
12
|
+
} | {
|
|
13
|
+
readonly phase: 'server-requested';
|
|
14
|
+
} | {
|
|
15
|
+
readonly phase: 'server-result';
|
|
16
|
+
readonly accepted: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
readonly phase: 'completed';
|
|
19
|
+
readonly status: OperationResult<K>['status'];
|
|
20
|
+
} | {
|
|
21
|
+
readonly phase: 'exception';
|
|
22
|
+
readonly at: GameServicesOperationLocation;
|
|
23
|
+
};
|
|
24
|
+
type OperationProgress<K extends GameServicesOperationKind> = ProgressStage<K> & {
|
|
25
|
+
readonly kind: K;
|
|
26
|
+
/** Invocation-local event order, starting at one. */
|
|
27
|
+
readonly sequence: number;
|
|
28
|
+
/** Optional UI/diagnostic correlation, never an idempotency key or grant proof. */
|
|
29
|
+
readonly correlationId?: string;
|
|
30
|
+
};
|
|
31
|
+
export type GameServicesPurchaseProgress = OperationProgress<'purchase'>;
|
|
32
|
+
export type GameServicesRewardedAdProgress = OperationProgress<'rewarded-ad'>;
|
|
33
|
+
export type GameServicesOperationProgress = GameServicesPurchaseProgress | GameServicesRewardedAdProgress;
|
|
34
|
+
export interface GameServicesOperationOptions<TProgress = GameServicesOperationProgress> {
|
|
35
|
+
readonly onProgress?: (progress: TProgress) => void | Promise<void>;
|
|
36
|
+
readonly onObserverError?: (error: unknown) => void | Promise<void>;
|
|
37
|
+
readonly correlationId?: string;
|
|
38
|
+
}
|
|
39
|
+
interface OperationReporter<K extends GameServicesOperationKind> {
|
|
40
|
+
platformRequested(): void;
|
|
41
|
+
platformResult(status: PlatformStatus<K>): void;
|
|
42
|
+
serverRequested(): void;
|
|
43
|
+
serverResult(accepted: boolean): void;
|
|
44
|
+
}
|
|
45
|
+
/** Internal observation wrapper; it does not retry, cancel, verify, or grant anything. */
|
|
46
|
+
export declare function observeGameServicesOperation<K extends GameServicesOperationKind>(kind: K, options: GameServicesOperationOptions<OperationProgress<K>> | undefined, operation: (reporter: OperationReporter<K>) => Promise<OperationResult<K>>): Promise<OperationResult<K>>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Internal observation wrapper; it does not retry, cancel, verify, or grant anything. */
|
|
2
|
+
export async function observeGameServicesOperation(kind, options, operation) {
|
|
3
|
+
let onProgress;
|
|
4
|
+
let onObserverError;
|
|
5
|
+
let correlationId;
|
|
6
|
+
let sequence = 0;
|
|
7
|
+
let location = 'local';
|
|
8
|
+
function report(error) {
|
|
9
|
+
try {
|
|
10
|
+
if (typeof onObserverError === 'function') {
|
|
11
|
+
const result = onObserverError(error);
|
|
12
|
+
if (result !== undefined) {
|
|
13
|
+
void Promise.resolve(result).catch(() => { });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Diagnostic callbacks have no authority to change business results.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Capture observation options once. Even accessor failures stay outside the business flow.
|
|
22
|
+
try {
|
|
23
|
+
onObserverError = options?.onObserverError;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// There is no safe error observer to invoke if retrieving it failed.
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
onProgress = options?.onProgress;
|
|
30
|
+
const suppliedId = options?.correlationId;
|
|
31
|
+
if (typeof suppliedId === 'string') {
|
|
32
|
+
correlationId = suppliedId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
report(error);
|
|
37
|
+
}
|
|
38
|
+
function emit(stage) {
|
|
39
|
+
sequence += 1;
|
|
40
|
+
if (typeof onProgress !== 'function') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const observer = onProgress;
|
|
44
|
+
const progress = Object.freeze({
|
|
45
|
+
...stage,
|
|
46
|
+
kind,
|
|
47
|
+
sequence,
|
|
48
|
+
...(correlationId === undefined ? {} : { correlationId }),
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const result = observer(progress);
|
|
52
|
+
if (result !== undefined) {
|
|
53
|
+
void Promise.resolve(result).catch(report);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
report(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const reporter = {
|
|
61
|
+
platformRequested() {
|
|
62
|
+
location = 'platform';
|
|
63
|
+
emit({ phase: 'platform-requested' });
|
|
64
|
+
},
|
|
65
|
+
platformResult(status) {
|
|
66
|
+
location = 'local';
|
|
67
|
+
emit({ phase: 'platform-result', status });
|
|
68
|
+
},
|
|
69
|
+
serverRequested() {
|
|
70
|
+
location = 'server';
|
|
71
|
+
emit({ phase: 'server-requested' });
|
|
72
|
+
},
|
|
73
|
+
serverResult(accepted) {
|
|
74
|
+
location = 'local';
|
|
75
|
+
emit({ phase: 'server-result', accepted });
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
const result = await operation(reporter);
|
|
80
|
+
emit({ phase: 'completed', status: result.status });
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
emit({ phase: 'exception', at: location });
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { LogicalAdPlacementId, LogicalProductId, PurchaseResult, RewardedAdResult } from '@mpgd/platform';
|
|
2
|
+
import type { GameServicesOperationOptions, GameServicesPurchaseProgress, GameServicesRewardedAdProgress } from './operation-progress.js';
|
|
3
|
+
import type { ClaimAdRewardResponse, VerifyPurchaseResponse } from './types.js';
|
|
4
|
+
export type { GameServicesOperationKind, GameServicesOperationLocation, GameServicesOperationOptions, GameServicesOperationProgress, GameServicesPurchaseProgress, GameServicesRewardedAdProgress, } from './operation-progress.js';
|
|
5
|
+
/** DOM-free public operation port, implemented by GameServicesClient. */
|
|
6
|
+
export interface GameServicesOperationClient {
|
|
7
|
+
purchase(input: GameServicesPurchaseInput, options?: GameServicesOperationOptions<GameServicesPurchaseProgress>): Promise<GameServicesPurchaseResult>;
|
|
8
|
+
claimRewardedAd(input: GameServicesRewardedAdInput, options?: GameServicesOperationOptions<GameServicesRewardedAdProgress>): Promise<GameServicesRewardedAdResult>;
|
|
9
|
+
}
|
|
10
|
+
export interface GameServicesPurchaseInput {
|
|
11
|
+
readonly productId: LogicalProductId;
|
|
12
|
+
readonly source: 'shop' | 'stage_fail' | 'result' | 'event';
|
|
13
|
+
readonly idempotencyKey: string;
|
|
14
|
+
}
|
|
15
|
+
export interface GameServicesPurchaseResult {
|
|
16
|
+
readonly status: 'granted' | 'cancelled' | 'pending' | 'failed' | 'rejected';
|
|
17
|
+
readonly purchase: PurchaseResult;
|
|
18
|
+
readonly verification?: VerifyPurchaseResponse;
|
|
19
|
+
readonly ledgerEntryId?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface GameServicesRewardedAdInput {
|
|
22
|
+
readonly placementId: LogicalAdPlacementId;
|
|
23
|
+
readonly idempotencyKey: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GameServicesRewardedAdResult {
|
|
26
|
+
readonly status: 'granted' | 'skipped' | 'unavailable' | 'failed' | 'rejected';
|
|
27
|
+
readonly reward: RewardedAdResult;
|
|
28
|
+
readonly claim?: ClaimAdRewardResponse;
|
|
29
|
+
readonly ledgerEntryId?: string;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/progress-link.js
CHANGED
package/dist/server.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type AnalyticsSink } from '@mpgd/analytics';
|
|
2
2
|
import { type AdPlacements, type ProductCatalog } from '@mpgd/catalog';
|
|
3
|
-
import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesBackendTransportRequest, type GameServicesBackendTransportResponse } from './client';
|
|
4
|
-
import { type GameServicesHealthResponse } from './contract';
|
|
5
|
-
import { type GameServicesEvidenceVerifier, type GameServicesPurchaseGrantFinalizer } from './evidence-verification';
|
|
6
|
-
import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerResult, type GameServicesAdRewardTarget, type GameServicesStoreTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types';
|
|
3
|
+
import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesBackendTransportRequest, type GameServicesBackendTransportResponse } from './client.js';
|
|
4
|
+
import { type GameServicesHealthResponse } from './contract.js';
|
|
5
|
+
import { type GameServicesEvidenceVerifier, type GameServicesPurchaseGrantFinalizer } from './evidence-verification.js';
|
|
6
|
+
import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerResult, type GameServicesAdRewardTarget, type GameServicesStoreTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types.js';
|
|
7
7
|
type CorsHeaders = Record<string, string>;
|
|
8
8
|
export interface CreateGameServicesBackendInput {
|
|
9
9
|
readonly catalog: ProductCatalog;
|
|
@@ -113,5 +113,5 @@ export declare function createGameServicesRouter(backend: GameServicesBackendApi
|
|
|
113
113
|
};
|
|
114
114
|
export declare function createGameServicesRpcFetchHandler(router: ReturnType<typeof createGameServicesRouter>, options?: CreateGameServicesRpcFetchHandlerOptions): (request: Request) => Promise<Response>;
|
|
115
115
|
export declare function createGameServicesHttpFetchHandler(handler: GameServicesBackendApiHandler, options?: CreateGameServicesFetchHandlerOptions): (request: Request) => Promise<Response>;
|
|
116
|
-
export * from './notification-delivery';
|
|
117
|
-
export * from './progress-link';
|
|
116
|
+
export * from './notification-delivery.js';
|
|
117
|
+
export * from './progress-link.js';
|
package/dist/server.js
CHANGED
|
@@ -2,10 +2,10 @@ import { implement } from '@orpc/server';
|
|
|
2
2
|
import { RPCHandler } from '@orpc/server/fetch';
|
|
3
3
|
import { createAnalyticsReporter } from '@mpgd/analytics';
|
|
4
4
|
import { resolveAdPlacementPlatformId, resolveProductPlatformId, } from '@mpgd/catalog';
|
|
5
|
-
import { gameServicesBackendEndpoints, } from './client';
|
|
6
|
-
import { gameServicesContract } from './contract';
|
|
7
|
-
import { createRejectingGameServicesEvidenceVerifier, } from './evidence-verification';
|
|
8
|
-
import { assertClaimAdRewardRequest, assertClaimAdRewardResponse, assertEntitlementLedgerGrant, assertEntitlementLedgerResult, assertGameServicesDeploymentTarget, assertLeaderboardScoreTransaction, assertProductGrantTransaction, assertPurchaseGrantFinalization, assertRecordLeaderboardScoreRequest, assertRecordLeaderboardScoreResponse, assertVerifyPurchaseRequest, assertVerifyPurchaseResponse, } from './types';
|
|
5
|
+
import { gameServicesBackendEndpoints, } from './client.js';
|
|
6
|
+
import { gameServicesContract } from './contract.js';
|
|
7
|
+
import { createRejectingGameServicesEvidenceVerifier, } from './evidence-verification.js';
|
|
8
|
+
import { assertClaimAdRewardRequest, assertClaimAdRewardResponse, assertEntitlementLedgerGrant, assertEntitlementLedgerResult, assertGameServicesDeploymentTarget, assertLeaderboardScoreTransaction, assertProductGrantTransaction, assertPurchaseGrantFinalization, assertRecordLeaderboardScoreRequest, assertRecordLeaderboardScoreResponse, assertVerifyPurchaseRequest, assertVerifyPurchaseResponse, } from './types.js';
|
|
9
9
|
export class InMemoryGameServicesStore {
|
|
10
10
|
entitlementTransactionsByKey = new Map();
|
|
11
11
|
entitlementTransactionsByEvidence = new Map();
|
|
@@ -622,6 +622,9 @@ function createStoredPurchaseRequest(request, transaction) {
|
|
|
622
622
|
const purchasedAt = transaction.payload.purchasedAt;
|
|
623
623
|
return {
|
|
624
624
|
...request,
|
|
625
|
+
// Evidence recovery can arrive with another request key. Consume/release must use the
|
|
626
|
+
// original durable grant generation, never the retry's newly supplied key.
|
|
627
|
+
idempotencyKey: transaction.idempotencyKey,
|
|
625
628
|
...(typeof platformTransactionId === 'string' && platformTransactionId.length > 0
|
|
626
629
|
? { platformTransactionId }
|
|
627
630
|
: {}),
|
|
@@ -1157,5 +1160,5 @@ function applyCorsHeaders(headers, corsHeaders) {
|
|
|
1157
1160
|
headers.set(key, value);
|
|
1158
1161
|
}
|
|
1159
1162
|
}
|
|
1160
|
-
export * from './notification-delivery';
|
|
1161
|
-
export * from './progress-link';
|
|
1163
|
+
export * from './notification-delivery.js';
|
|
1164
|
+
export * from './progress-link.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type VerifiedLeaderboardService } from './verified-leaderboard';
|
|
1
|
+
import { type VerifiedLeaderboardService } from './verified-leaderboard.js';
|
|
2
2
|
export declare const verifiedLeaderboardConformanceScenarios: readonly ['first-selection-and-snapshot', 'best-selection-and-retry', 'deterministic-ties', 'identity-and-definition-conflicts', 'concurrent-idempotency', 'mutation-isolation', 'runtime-validation'];
|
|
3
3
|
export type VerifiedLeaderboardConformanceScenario = (typeof verifiedLeaderboardConformanceScenarios)[number];
|
|
4
4
|
export interface CreateVerifiedLeaderboardConformanceFixtureInput {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertRecordVerifiedLeaderboardAttemptResponse, assertVerifiedLeaderboardSnapshot, } from './verified-leaderboard';
|
|
1
|
+
import { assertRecordVerifiedLeaderboardAttemptResponse, assertVerifiedLeaderboardSnapshot, } from './verified-leaderboard.js';
|
|
2
2
|
export const verifiedLeaderboardConformanceScenarios = [
|
|
3
3
|
'first-selection-and-snapshot',
|
|
4
4
|
'best-selection-and-retry',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type VerifiedLeaderboardService } from './verified-leaderboard';
|
|
1
|
+
import { type VerifiedLeaderboardService } from './verified-leaderboard.js';
|
|
2
2
|
export declare const verifiedLeaderboardDurabilityConformanceScenarios: readonly ['replacement-after-interrupted-retained-write', 'concurrent-retry-after-interrupted-retained-write', 'snapshot-after-interrupted-retained-write'];
|
|
3
3
|
export type VerifiedLeaderboardDurabilityConformanceScenario = (typeof verifiedLeaderboardDurabilityConformanceScenarios)[number];
|
|
4
4
|
export interface CreateVerifiedLeaderboardDurabilityConformanceFixtureInput {
|