@mpgd/game-services 0.4.0 → 0.6.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 +3 -0
- package/dist/index.js +3 -0
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.js +198 -0
- package/dist/verified-leaderboard-conformance.d.ts +29 -0
- package/dist/verified-leaderboard-conformance.js +630 -0
- package/dist/verified-leaderboard-transport.d.ts +24 -0
- package/dist/verified-leaderboard-transport.js +169 -0
- package/dist/verified-leaderboard.d.ts +112 -0
- package/dist/verified-leaderboard.js +539 -0
- package/package.json +23 -7
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,8 @@ export * from './client';
|
|
|
2
2
|
export * from './contract';
|
|
3
3
|
export * from './notification-delivery';
|
|
4
4
|
export * from './progress-link';
|
|
5
|
+
export * from './runtime.js';
|
|
5
6
|
export * from './server';
|
|
6
7
|
export * from './types';
|
|
8
|
+
export * from './verified-leaderboard';
|
|
9
|
+
export * from './verified-leaderboard-transport';
|
package/dist/index.js
CHANGED
|
@@ -2,5 +2,8 @@ export * from './client';
|
|
|
2
2
|
export * from './contract';
|
|
3
3
|
export * from './notification-delivery';
|
|
4
4
|
export * from './progress-link';
|
|
5
|
+
export * from './runtime.js';
|
|
5
6
|
export * from './server';
|
|
6
7
|
export * from './types';
|
|
8
|
+
export * from './verified-leaderboard';
|
|
9
|
+
export * from './verified-leaderboard-transport';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AnalyticsSink } from '@mpgd/analytics';
|
|
2
|
+
import type { PlatformGateway, PlatformTarget } from '@mpgd/platform';
|
|
3
|
+
import { type GameServicesBackendApi, type GameServicesClient } from './client.js';
|
|
4
|
+
import type { GameServicesLedgerTarget } from './types.js';
|
|
5
|
+
export type GameServicesAuthorityMode = 'production' | 'non-production';
|
|
6
|
+
export type GameServicesRuntimeMode = 'disabled' | 'local' | 'http' | 'orpc';
|
|
7
|
+
export type GameServicesDisabledReason = 'unsupported_target' | 'missing_authoritative_backend' | 'invalid_authoritative_backend' | 'local_backend_not_allowed' | 'local_backend_unavailable';
|
|
8
|
+
export interface GameServicesRuntime {
|
|
9
|
+
readonly mode: GameServicesRuntimeMode;
|
|
10
|
+
readonly reason?: GameServicesDisabledReason;
|
|
11
|
+
readonly baseUrl?: string;
|
|
12
|
+
readonly target?: GameServicesLedgerTarget;
|
|
13
|
+
readonly client?: GameServicesClient;
|
|
14
|
+
}
|
|
15
|
+
export interface CreateGameServicesRuntimeInput {
|
|
16
|
+
readonly gateway: PlatformGateway;
|
|
17
|
+
readonly playerId: string;
|
|
18
|
+
readonly authorityMode: GameServicesAuthorityMode;
|
|
19
|
+
readonly target?: PlatformTarget | string;
|
|
20
|
+
readonly baseUrl?: string;
|
|
21
|
+
readonly transport?: 'http' | 'orpc';
|
|
22
|
+
readonly allowLocalBackend?: boolean;
|
|
23
|
+
readonly localBackend?: GameServicesBackendApi;
|
|
24
|
+
readonly analytics?: AnalyticsSink;
|
|
25
|
+
readonly analyticsSessionId?: string;
|
|
26
|
+
readonly now?: () => string;
|
|
27
|
+
}
|
|
28
|
+
export declare function createGameServicesRuntime(input: CreateGameServicesRuntimeInput): GameServicesRuntime;
|
|
29
|
+
export declare function resolveGameServicesLedgerTarget(target: PlatformTarget | string): GameServicesLedgerTarget | null;
|
|
30
|
+
export declare function resolveGameServicesAuthorityMode(profile: string): GameServicesAuthorityMode;
|
|
31
|
+
export declare function resolveGameServicesTransport(transport: string | undefined): 'http' | 'orpc';
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createGameServicesClient, createGameServicesFetchBackendTransport, createGameServicesHttpBackendApi, createGameServicesOrpcBackendApi, createGameServicesOrpcClient, } from './client.js';
|
|
2
|
+
export function createGameServicesRuntime(input) {
|
|
3
|
+
assertAuthorityMode(input.authorityMode);
|
|
4
|
+
assertTransport(input.transport);
|
|
5
|
+
const target = resolveGameServicesLedgerTarget(input.target ?? input.gateway.target);
|
|
6
|
+
if (target === null) {
|
|
7
|
+
return disabledRuntime('unsupported_target');
|
|
8
|
+
}
|
|
9
|
+
const baseUrl = normalizeBaseUrl(input.baseUrl);
|
|
10
|
+
let backend;
|
|
11
|
+
let mode;
|
|
12
|
+
if (baseUrl !== undefined) {
|
|
13
|
+
const url = parseAbsoluteUrl(baseUrl);
|
|
14
|
+
if (input.authorityMode === 'production' && !isPublicHttpsUrl(url)) {
|
|
15
|
+
return disabledRuntime('invalid_authoritative_backend', target);
|
|
16
|
+
}
|
|
17
|
+
if (url === undefined) {
|
|
18
|
+
throw new Error('Game Services baseUrl must be a valid absolute URL.');
|
|
19
|
+
}
|
|
20
|
+
mode = input.transport === 'orpc' ? 'orpc' : 'http';
|
|
21
|
+
backend = mode === 'orpc'
|
|
22
|
+
? createGameServicesOrpcBackendApi(createGameServicesOrpcClient({ url: baseUrl }))
|
|
23
|
+
: createGameServicesHttpBackendApi({
|
|
24
|
+
transport: createGameServicesFetchBackendTransport({ baseUrl }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (input.authorityMode === 'production') {
|
|
29
|
+
return disabledRuntime('missing_authoritative_backend', target);
|
|
30
|
+
}
|
|
31
|
+
if (input.allowLocalBackend !== true) {
|
|
32
|
+
return disabledRuntime('local_backend_not_allowed', target);
|
|
33
|
+
}
|
|
34
|
+
if (input.localBackend === undefined) {
|
|
35
|
+
return disabledRuntime('local_backend_unavailable', target);
|
|
36
|
+
}
|
|
37
|
+
mode = 'local';
|
|
38
|
+
backend = input.localBackend;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
mode,
|
|
42
|
+
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
43
|
+
target,
|
|
44
|
+
client: createGameServicesClient({
|
|
45
|
+
gateway: input.gateway,
|
|
46
|
+
backend,
|
|
47
|
+
playerId: input.playerId,
|
|
48
|
+
target,
|
|
49
|
+
...(input.analytics === undefined ? {} : { analytics: input.analytics }),
|
|
50
|
+
...(input.analyticsSessionId === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { analyticsSessionId: input.analyticsSessionId }),
|
|
53
|
+
...(input.now === undefined ? {} : { now: input.now }),
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function resolveGameServicesLedgerTarget(target) {
|
|
58
|
+
if (target === 'browser'
|
|
59
|
+
|| target === 'android'
|
|
60
|
+
|| target === 'ios'
|
|
61
|
+
|| target === 'ait'
|
|
62
|
+
|| target === 'reddit') {
|
|
63
|
+
return target;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
export function resolveGameServicesAuthorityMode(profile) {
|
|
68
|
+
if (profile.length === 0 || profile.trim() !== profile) {
|
|
69
|
+
throw new Error('Game Services profile must be non-empty without surrounding whitespace.');
|
|
70
|
+
}
|
|
71
|
+
return profile === 'production' ? 'production' : 'non-production';
|
|
72
|
+
}
|
|
73
|
+
export function resolveGameServicesTransport(transport) {
|
|
74
|
+
if (transport === undefined || transport.length === 0 || transport === 'http') {
|
|
75
|
+
return 'http';
|
|
76
|
+
}
|
|
77
|
+
if (transport === 'orpc') {
|
|
78
|
+
return 'orpc';
|
|
79
|
+
}
|
|
80
|
+
throw new Error('Game Services transport must be http or orpc.');
|
|
81
|
+
}
|
|
82
|
+
function disabledRuntime(reason, target) {
|
|
83
|
+
return {
|
|
84
|
+
mode: 'disabled',
|
|
85
|
+
reason,
|
|
86
|
+
...(target === undefined ? {} : { target }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function normalizeBaseUrl(baseUrl) {
|
|
90
|
+
const normalized = baseUrl?.trim();
|
|
91
|
+
return normalized === undefined || normalized.length === 0 ? undefined : normalized;
|
|
92
|
+
}
|
|
93
|
+
function isPublicHttpsUrl(url) {
|
|
94
|
+
return url !== undefined
|
|
95
|
+
&& url.protocol === 'https:'
|
|
96
|
+
&& url.username.length === 0
|
|
97
|
+
&& url.password.length === 0
|
|
98
|
+
&& !isNonPublicHostname(url.hostname);
|
|
99
|
+
}
|
|
100
|
+
function parseAbsoluteUrl(value) {
|
|
101
|
+
try {
|
|
102
|
+
return new URL(value);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function isNonPublicHostname(hostname) {
|
|
109
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/u, '');
|
|
110
|
+
if (normalized === 'localhost'
|
|
111
|
+
|| normalized.endsWith('.localhost')
|
|
112
|
+
|| normalized.endsWith('.local')) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (/^(?:\d{1,3}\.){3}\d{1,3}$/u.test(normalized)) {
|
|
116
|
+
return isNonPublicIpv4(normalized);
|
|
117
|
+
}
|
|
118
|
+
return normalized.includes(':') && isNonPublicIpv6(normalized);
|
|
119
|
+
}
|
|
120
|
+
function isNonPublicIpv4(address) {
|
|
121
|
+
const [first = 0, second = 0, third = 0] = address.split('.').map(Number);
|
|
122
|
+
return first === 0
|
|
123
|
+
|| first === 10
|
|
124
|
+
|| first === 127
|
|
125
|
+
|| (first === 100 && second >= 64 && second <= 127)
|
|
126
|
+
|| (first === 169 && second === 254)
|
|
127
|
+
|| (first === 172 && second >= 16 && second <= 31)
|
|
128
|
+
|| (first === 192 && second === 0 && third === 0)
|
|
129
|
+
|| (first === 192 && second === 0 && third === 2)
|
|
130
|
+
|| (first === 192 && second === 168)
|
|
131
|
+
|| (first === 198 && (second === 18 || second === 19))
|
|
132
|
+
|| (first === 198 && second === 51 && third === 100)
|
|
133
|
+
|| (first === 203 && second === 0 && third === 113)
|
|
134
|
+
|| first >= 224;
|
|
135
|
+
}
|
|
136
|
+
function isNonPublicIpv6(address) {
|
|
137
|
+
const words = expandIpv6(address);
|
|
138
|
+
if (words === undefined) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (words.slice(0, 7).every((word) => word === 0)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (words.slice(0, 5).every((word) => word === 0) && words[5] === 0xffff) {
|
|
145
|
+
return isNonPublicIpv4(wordsToIpv4(words));
|
|
146
|
+
}
|
|
147
|
+
if (words.slice(0, 6).every((word) => word === 0)) {
|
|
148
|
+
return isNonPublicIpv4(wordsToIpv4(words));
|
|
149
|
+
}
|
|
150
|
+
const first = words[0] ?? 0;
|
|
151
|
+
return (first & 0xfe00) === 0xfc00
|
|
152
|
+
|| (first & 0xffc0) === 0xfe80
|
|
153
|
+
|| (first & 0xff00) === 0xff00
|
|
154
|
+
|| (first === 0x2001 && words[1] === 0x0db8);
|
|
155
|
+
}
|
|
156
|
+
function wordsToIpv4(words) {
|
|
157
|
+
return `${(words[6] ?? 0) >> 8}.${(words[6] ?? 0) & 0xff}.`
|
|
158
|
+
+ `${(words[7] ?? 0) >> 8}.${(words[7] ?? 0) & 0xff}`;
|
|
159
|
+
}
|
|
160
|
+
function expandIpv6(address) {
|
|
161
|
+
const sections = address.split('::');
|
|
162
|
+
if (sections.length > 2) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const head = ipv6Words(sections[0] ?? '');
|
|
166
|
+
const tail = ipv6Words(sections[1] ?? '');
|
|
167
|
+
if (head === undefined || tail === undefined) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const omitted = 8 - head.length - tail.length;
|
|
171
|
+
if ((sections.length === 1 && omitted !== 0) || (sections.length === 2 && omitted < 1)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
return [...head, ...Array.from({ length: omitted }, () => 0), ...tail];
|
|
175
|
+
}
|
|
176
|
+
function ipv6Words(section) {
|
|
177
|
+
if (section.length === 0) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const words = [];
|
|
181
|
+
for (const segment of section.split(':')) {
|
|
182
|
+
if (!/^[0-9a-f]{1,4}$/u.test(segment)) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
words.push(Number.parseInt(segment, 16));
|
|
186
|
+
}
|
|
187
|
+
return words;
|
|
188
|
+
}
|
|
189
|
+
function assertAuthorityMode(authorityMode) {
|
|
190
|
+
if (authorityMode !== 'production' && authorityMode !== 'non-production') {
|
|
191
|
+
throw new Error('authorityMode must be production or non-production.');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function assertTransport(transport) {
|
|
195
|
+
if (transport !== undefined && transport !== 'http' && transport !== 'orpc') {
|
|
196
|
+
throw new Error('Game Services transport must be http or orpc.');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type VerifiedLeaderboardService } from './verified-leaderboard';
|
|
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
|
+
export type VerifiedLeaderboardConformanceScenario = (typeof verifiedLeaderboardConformanceScenarios)[number];
|
|
4
|
+
export interface CreateVerifiedLeaderboardConformanceFixtureInput {
|
|
5
|
+
readonly scenario: VerifiedLeaderboardConformanceScenario;
|
|
6
|
+
readonly now: string;
|
|
7
|
+
}
|
|
8
|
+
export interface VerifiedLeaderboardConformanceFixture {
|
|
9
|
+
readonly service: VerifiedLeaderboardService;
|
|
10
|
+
readonly dispose?: () => Promise<void> | void;
|
|
11
|
+
}
|
|
12
|
+
export type CreateVerifiedLeaderboardConformanceFixture = (input: CreateVerifiedLeaderboardConformanceFixtureInput) => Promise<VerifiedLeaderboardConformanceFixture> | VerifiedLeaderboardConformanceFixture;
|
|
13
|
+
export interface RunVerifiedLeaderboardConformanceInput {
|
|
14
|
+
/**
|
|
15
|
+
* Return an isolated provider fixture for each scenario. Durable adapters may
|
|
16
|
+
* instead namespace every fixture in a shared test database.
|
|
17
|
+
*/
|
|
18
|
+
readonly createFixture: CreateVerifiedLeaderboardConformanceFixture;
|
|
19
|
+
readonly now?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface VerifiedLeaderboardConformanceReport {
|
|
22
|
+
readonly passedScenarios: readonly VerifiedLeaderboardConformanceScenario[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runs the provider-neutral verified leaderboard contract against an adapter.
|
|
26
|
+
* The helper is test-framework independent and throws with the failed scenario
|
|
27
|
+
* name when a provider diverges from the public service semantics.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runVerifiedLeaderboardConformance(input: RunVerifiedLeaderboardConformanceInput): Promise<VerifiedLeaderboardConformanceReport>;
|