@mpgd/game-services 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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-identity';
9
10
  export * from './microsoft-store-purchase';
10
11
  export * from './notification-delivery';
11
12
  export * from './platform-order';
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-identity';
9
10
  export * from './microsoft-store-purchase';
10
11
  export * from './notification-delivery';
11
12
  export * from './platform-order';
@@ -0,0 +1,39 @@
1
+ import type { MicrosoftStoreCollectionsCredentials } from './microsoft-store-purchase';
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
+ }
@@ -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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-services",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -73,6 +73,10 @@
73
73
  "types": "./dist/google-play-purchase.d.ts",
74
74
  "default": "./dist/google-play-purchase.js"
75
75
  },
76
+ "./microsoft-store-identity": {
77
+ "types": "./dist/microsoft-store-identity.d.ts",
78
+ "default": "./dist/microsoft-store-identity.js"
79
+ },
76
80
  "./microsoft-store-purchase": {
77
81
  "types": "./dist/microsoft-store-purchase.d.ts",
78
82
  "default": "./dist/microsoft-store-purchase.js"
@@ -130,9 +134,9 @@
130
134
  "@orpc/client": "2.0.0-beta.14",
131
135
  "@orpc/contract": "2.0.0-beta.14",
132
136
  "@orpc/server": "2.0.0-beta.14",
133
- "@mpgd/analytics": "0.3.11",
134
- "@mpgd/catalog": "0.7.1",
135
- "@mpgd/platform": "0.11.0"
137
+ "@mpgd/analytics": "0.3.12",
138
+ "@mpgd/platform": "0.12.0",
139
+ "@mpgd/catalog": "0.7.2"
136
140
  },
137
141
  "devDependencies": {
138
142
  "ttsc": "0.18.4",
@@ -148,6 +152,6 @@
148
152
  "lint": "ttsc --noEmit",
149
153
  "format": "ttsc format",
150
154
  "fix": "ttsc fix",
151
- "test": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/types.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/platform-order.test.ts && node tools/run-ttsx.mjs packages/game-services/src/progress-link.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notification-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/admob-ssv.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-verifier.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-evidence-verification-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-partner-api.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-durability-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-transport.test.ts"
155
+ "test": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/types.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-identity.test.ts && node tools/run-ttsx.mjs packages/game-services/src/platform-order.test.ts && node tools/run-ttsx.mjs packages/game-services/src/progress-link.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notification-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/admob-ssv.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-verifier.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-evidence-verification-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-partner-api.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-durability-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-transport.test.ts"
152
156
  }
153
157
  }