@mpgd/game-services 0.4.0 → 0.5.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 +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.js +198 -0
- package/package.json +9 -5
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/game-services",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"types": "./dist/progress-link.d.ts",
|
|
46
46
|
"default": "./dist/progress-link.js"
|
|
47
47
|
},
|
|
48
|
+
"./runtime": {
|
|
49
|
+
"types": "./dist/runtime.d.ts",
|
|
50
|
+
"default": "./dist/runtime.js"
|
|
51
|
+
},
|
|
48
52
|
"./server": {
|
|
49
53
|
"types": "./dist/server.d.ts",
|
|
50
54
|
"default": "./dist/server.js"
|
|
@@ -61,9 +65,9 @@
|
|
|
61
65
|
"@orpc/client": "2.0.0-beta.14",
|
|
62
66
|
"@orpc/contract": "2.0.0-beta.14",
|
|
63
67
|
"@orpc/server": "2.0.0-beta.14",
|
|
64
|
-
"@mpgd/
|
|
65
|
-
"@mpgd/
|
|
66
|
-
"@mpgd/platform": "0.
|
|
68
|
+
"@mpgd/catalog": "0.3.4",
|
|
69
|
+
"@mpgd/analytics": "0.3.4",
|
|
70
|
+
"@mpgd/platform": "0.5.0"
|
|
67
71
|
},
|
|
68
72
|
"devDependencies": {
|
|
69
73
|
"ttsc": "0.16.9",
|
|
@@ -79,6 +83,6 @@
|
|
|
79
83
|
"lint": "ttsc --noEmit",
|
|
80
84
|
"format": "ttsc format",
|
|
81
85
|
"fix": "ttsc fix",
|
|
82
|
-
"test": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.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"
|
|
86
|
+
"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/server.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"
|
|
83
87
|
}
|
|
84
88
|
}
|