@mpgd/adapter-verse8 0.1.0 → 0.2.1
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/ads-contract.d.ts +1 -0
- package/dist/ads-contract.js +1 -0
- package/dist/agent8-services.d.ts +83 -0
- package/dist/agent8-services.js +928 -0
- package/dist/agent8.d.ts +37 -0
- package/dist/agent8.js +276 -0
- package/dist/index.d.ts +68 -1
- package/dist/index.js +273 -32
- package/dist/server.d.ts +40 -0
- package/dist/server.js +258 -0
- package/package.json +14 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Verse8Ads, } from '@verse8/ads';
|
|
2
|
+
import { Verse8, VXShop } from '@verse8/platform/vanilla';
|
|
2
3
|
import { createUnsupportedCapabilities, } from '@mpgd/platform';
|
|
4
|
+
import { verse8AdsRewardEvidenceSchema } from './ads-contract.js';
|
|
5
|
+
export { verse8AdsRewardEvidenceSchema } from './ads-contract.js';
|
|
6
|
+
export function createVerse8CommerceProducts(catalog) {
|
|
7
|
+
return normalizeCommerceProducts(catalog.products.flatMap((product) => {
|
|
8
|
+
const platformProductId = product.platformProductIds.verse8;
|
|
9
|
+
return platformProductId === undefined || product.grant.type === 'resource'
|
|
10
|
+
? []
|
|
11
|
+
: [
|
|
12
|
+
{
|
|
13
|
+
id: product.id,
|
|
14
|
+
type: product.type,
|
|
15
|
+
platformProductId,
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
3
20
|
export function createVerse8PlatformGateway(options = {}) {
|
|
4
21
|
const authClient = options.authClient ?? createDefaultAuthClient();
|
|
22
|
+
const adsClient = options.adsClient ?? Verse8Ads;
|
|
23
|
+
const adsAvailable = options.resolveAdPlacementId !== undefined;
|
|
24
|
+
const commerce = createVerse8Commerce(options.vxShop);
|
|
5
25
|
const pauseListeners = new Set();
|
|
6
26
|
const resumeListeners = new Set();
|
|
7
27
|
const visibility = options.visibility ?? resolveVisibilitySource();
|
|
@@ -17,6 +37,11 @@ export function createVerse8PlatformGateway(options = {}) {
|
|
|
17
37
|
async getCapabilities() {
|
|
18
38
|
return {
|
|
19
39
|
...createUnsupportedCapabilities(),
|
|
40
|
+
nativeIap: commerce !== undefined,
|
|
41
|
+
nativeAds: adsAvailable,
|
|
42
|
+
rewardedAds: adsAvailable,
|
|
43
|
+
interstitialAds: adsAvailable,
|
|
44
|
+
cloudSave: options.agent8Storage !== undefined,
|
|
20
45
|
localizedContent: true,
|
|
21
46
|
};
|
|
22
47
|
},
|
|
@@ -28,32 +53,84 @@ export function createVerse8PlatformGateway(options = {}) {
|
|
|
28
53
|
return resolveVerse8Identity(authClient).session;
|
|
29
54
|
},
|
|
30
55
|
},
|
|
31
|
-
commerce:
|
|
32
|
-
async getProducts() {
|
|
33
|
-
return [];
|
|
34
|
-
},
|
|
35
|
-
async purchase() {
|
|
36
|
-
return {
|
|
37
|
-
status: 'failed',
|
|
38
|
-
entitlementIds: [],
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
async getEntitlements() {
|
|
42
|
-
return [];
|
|
43
|
-
},
|
|
44
|
-
},
|
|
56
|
+
commerce: commerce ?? createUnavailableVerse8Commerce(),
|
|
45
57
|
ads: {
|
|
46
58
|
async preload() { },
|
|
47
|
-
async showRewarded() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
59
|
+
async showRewarded(input) {
|
|
60
|
+
const placementId = options.resolveAdPlacementId?.(input.placementId);
|
|
61
|
+
if (placementId === undefined) {
|
|
62
|
+
return unavailableReward();
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const result = await adsClient.showRewarded({
|
|
66
|
+
placementId,
|
|
67
|
+
...(options.adsTimeoutMs === undefined
|
|
68
|
+
? {}
|
|
69
|
+
: { timeoutMs: options.adsTimeoutMs }),
|
|
70
|
+
meta: {
|
|
71
|
+
logicalPlacementId: input.placementId,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (result.status === 'rewarded') {
|
|
75
|
+
return {
|
|
76
|
+
status: 'completed',
|
|
77
|
+
rewardGranted: true,
|
|
78
|
+
ledgerEntryId: result.requestId,
|
|
79
|
+
evidence: {
|
|
80
|
+
schema: verse8AdsRewardEvidenceSchema,
|
|
81
|
+
payload: {
|
|
82
|
+
requestId: result.requestId,
|
|
83
|
+
placementId,
|
|
84
|
+
...(result.platform === undefined ? {} : { platform: result.platform }),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (result.status === 'dismissed') {
|
|
90
|
+
return {
|
|
91
|
+
status: 'skipped',
|
|
92
|
+
rewardGranted: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return result.error.code === 'unsupported_env'
|
|
96
|
+
? unavailableReward()
|
|
97
|
+
: {
|
|
98
|
+
status: 'failed',
|
|
99
|
+
rewardGranted: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return {
|
|
104
|
+
status: 'failed',
|
|
105
|
+
rewardGranted: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
52
108
|
},
|
|
53
|
-
async showInterstitial() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
109
|
+
async showInterstitial(input) {
|
|
110
|
+
const placementId = options.resolveAdPlacementId?.(input.placementId);
|
|
111
|
+
if (placementId === undefined) {
|
|
112
|
+
return { status: 'unavailable' };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const result = await adsClient.showInterstitial({
|
|
116
|
+
placementId,
|
|
117
|
+
...(options.adsTimeoutMs === undefined
|
|
118
|
+
? {}
|
|
119
|
+
: { timeoutMs: options.adsTimeoutMs }),
|
|
120
|
+
meta: {
|
|
121
|
+
logicalPlacementId: input.placementId,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
if (result.status === 'dismissed') {
|
|
125
|
+
return { status: 'shown' };
|
|
126
|
+
}
|
|
127
|
+
return result.error.code === 'unsupported_env'
|
|
128
|
+
? { status: 'unavailable' }
|
|
129
|
+
: { status: 'skipped' };
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return { status: 'skipped' };
|
|
133
|
+
}
|
|
57
134
|
},
|
|
58
135
|
},
|
|
59
136
|
leaderboard: {
|
|
@@ -76,15 +153,21 @@ export function createVerse8PlatformGateway(options = {}) {
|
|
|
76
153
|
},
|
|
77
154
|
storage: {
|
|
78
155
|
async load(input) {
|
|
79
|
-
|
|
80
|
-
|
|
156
|
+
if (options.agent8Storage !== undefined) {
|
|
157
|
+
return options.agent8Storage.load(input);
|
|
158
|
+
}
|
|
159
|
+
const storage = resolveStorage(options.storage);
|
|
160
|
+
const value = storage.getItem(storageKey(authClient, input.key));
|
|
81
161
|
return value === undefined || value === null
|
|
82
162
|
? null
|
|
83
163
|
: { value: JSON.parse(value) };
|
|
84
164
|
},
|
|
85
165
|
async save(input) {
|
|
86
|
-
|
|
87
|
-
|
|
166
|
+
if (options.agent8Storage !== undefined) {
|
|
167
|
+
await options.agent8Storage.save(input);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
resolveStorage(options.storage).setItem(storageKey(authClient, input.key), serializeStorageValue(input.value));
|
|
88
171
|
},
|
|
89
172
|
},
|
|
90
173
|
presentation: {
|
|
@@ -99,6 +182,144 @@ export function createVerse8PlatformGateway(options = {}) {
|
|
|
99
182
|
},
|
|
100
183
|
};
|
|
101
184
|
}
|
|
185
|
+
function createVerse8Commerce(options) {
|
|
186
|
+
if (options === undefined) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
if (options.purchaseEventAuthority !== 'agent8-server' || options.products.length === 0) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const products = normalizeCommerceProducts(options.products);
|
|
193
|
+
const client = options.client ?? VXShop;
|
|
194
|
+
let initialized = false;
|
|
195
|
+
const ensureInitialized = () => {
|
|
196
|
+
if (initialized) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (options.init === undefined) {
|
|
200
|
+
client.init({ autoRefresh: false });
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
client.init({ ...options.init, autoRefresh: false });
|
|
204
|
+
}
|
|
205
|
+
initialized = true;
|
|
206
|
+
};
|
|
207
|
+
const loadEntitlements = async () => {
|
|
208
|
+
try {
|
|
209
|
+
return [...await options.loadEntitlements()];
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
async getProducts() {
|
|
217
|
+
try {
|
|
218
|
+
ensureInitialized();
|
|
219
|
+
await client.refresh();
|
|
220
|
+
const shopItems = new Map(client.getItems().map((item) => [item.productId, item]));
|
|
221
|
+
return products.flatMap((product) => {
|
|
222
|
+
const item = shopItems.get(product.platformProductId);
|
|
223
|
+
return item === undefined
|
|
224
|
+
? []
|
|
225
|
+
: [{
|
|
226
|
+
id: product.id,
|
|
227
|
+
type: product.type,
|
|
228
|
+
title: item.name,
|
|
229
|
+
description: item.description,
|
|
230
|
+
price: {
|
|
231
|
+
formatted: `${item.price} VX`,
|
|
232
|
+
currencyCode: 'VX',
|
|
233
|
+
},
|
|
234
|
+
}];
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
async purchase(input) {
|
|
242
|
+
const product = products.find((candidate) => candidate.id === input.productId);
|
|
243
|
+
if (product === undefined) {
|
|
244
|
+
return failedPurchase();
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
if (!(options.canOpenShop ?? canOpenDefaultVerse8Shop)()) {
|
|
248
|
+
return failedPurchase();
|
|
249
|
+
}
|
|
250
|
+
ensureInitialized();
|
|
251
|
+
await client.refresh();
|
|
252
|
+
const item = client.getItem(product.platformProductId);
|
|
253
|
+
if (item === undefined
|
|
254
|
+
|| item.purchasable !== true
|
|
255
|
+
|| item.purchaseLimitReached === true) {
|
|
256
|
+
return failedPurchase();
|
|
257
|
+
}
|
|
258
|
+
client.buyItem(product.platformProductId);
|
|
259
|
+
return {
|
|
260
|
+
status: 'pending',
|
|
261
|
+
entitlementIds: [],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return failedPurchase();
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
async restore() {
|
|
269
|
+
return {
|
|
270
|
+
restoredEntitlements: await loadEntitlements(),
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
getEntitlements: loadEntitlements,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function normalizeCommerceProducts(products) {
|
|
277
|
+
const logicalIds = new Set();
|
|
278
|
+
const platformIds = new Set();
|
|
279
|
+
return products.map((product) => {
|
|
280
|
+
const platformProductId = product.platformProductId.trim();
|
|
281
|
+
if (product.id.trim().length === 0 || platformProductId.length === 0) {
|
|
282
|
+
throw new Error('Verse8 commerce product IDs must be non-empty.');
|
|
283
|
+
}
|
|
284
|
+
if (logicalIds.has(product.id) || platformIds.has(platformProductId)) {
|
|
285
|
+
throw new Error('Verse8 commerce product IDs must be unique.');
|
|
286
|
+
}
|
|
287
|
+
logicalIds.add(product.id);
|
|
288
|
+
platformIds.add(platformProductId);
|
|
289
|
+
return {
|
|
290
|
+
...product,
|
|
291
|
+
platformProductId,
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function createUnavailableVerse8Commerce() {
|
|
296
|
+
return {
|
|
297
|
+
async getProducts() {
|
|
298
|
+
return [];
|
|
299
|
+
},
|
|
300
|
+
async purchase() {
|
|
301
|
+
return failedPurchase();
|
|
302
|
+
},
|
|
303
|
+
async getEntitlements() {
|
|
304
|
+
return [];
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function failedPurchase() {
|
|
309
|
+
return {
|
|
310
|
+
status: 'failed',
|
|
311
|
+
entitlementIds: [],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function canOpenDefaultVerse8Shop() {
|
|
315
|
+
return typeof window !== 'undefined' && window.parent !== window;
|
|
316
|
+
}
|
|
317
|
+
function unavailableReward() {
|
|
318
|
+
return {
|
|
319
|
+
status: 'unavailable',
|
|
320
|
+
rewardGranted: false,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
102
323
|
function createDefaultAuthClient() {
|
|
103
324
|
return {
|
|
104
325
|
getUser(options) {
|
|
@@ -139,11 +360,31 @@ function createResolvedIdentity(credential, identityLevel, trustLevel) {
|
|
|
139
360
|
},
|
|
140
361
|
};
|
|
141
362
|
}
|
|
142
|
-
function storageKey(key) {
|
|
143
|
-
|
|
363
|
+
function storageKey(authClient, key) {
|
|
364
|
+
const playerId = resolveVerse8Identity(authClient).player?.playerId;
|
|
365
|
+
const identityNamespace = playerId === undefined ? 'guest' : playerId.toLowerCase();
|
|
366
|
+
return `mpgd:verse8:${identityNamespace}:${key}`;
|
|
144
367
|
}
|
|
145
|
-
function resolveStorage() {
|
|
146
|
-
|
|
368
|
+
function resolveStorage(override) {
|
|
369
|
+
if (override !== undefined) {
|
|
370
|
+
return override;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
if (globalThis.localStorage !== undefined) {
|
|
374
|
+
return globalThis.localStorage;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// Access to browser storage can be denied by the embedding environment.
|
|
379
|
+
}
|
|
380
|
+
throw new Error('Verse8 local storage is unavailable.');
|
|
381
|
+
}
|
|
382
|
+
function serializeStorageValue(value) {
|
|
383
|
+
const serialized = JSON.stringify(value);
|
|
384
|
+
if (typeof serialized !== 'string') {
|
|
385
|
+
throw new Error('Verse8 local storage values must be JSON serializable.');
|
|
386
|
+
}
|
|
387
|
+
return serialized;
|
|
147
388
|
}
|
|
148
389
|
function resolveVisibilitySource() {
|
|
149
390
|
return typeof document === 'undefined' ? undefined : document;
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { GameServicesEvidenceVerifier } from '@mpgd/game-services';
|
|
2
|
+
export declare const defaultVerse8AdsVerifierBaseUrl = "https://ads-verifier.verse8.io";
|
|
3
|
+
export interface Verse8AdsVerifierReward {
|
|
4
|
+
readonly amount: number;
|
|
5
|
+
readonly type: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Verse8AdsVerificationRecord {
|
|
8
|
+
readonly verified: boolean;
|
|
9
|
+
readonly status?: 'verified' | 'consumed' | 'pending';
|
|
10
|
+
readonly reward?: Verse8AdsVerifierReward;
|
|
11
|
+
readonly requestId?: string;
|
|
12
|
+
readonly placementId?: string;
|
|
13
|
+
readonly userId?: string;
|
|
14
|
+
readonly adNetwork?: 'google';
|
|
15
|
+
readonly verifiedAt?: string;
|
|
16
|
+
readonly consumedAt?: string;
|
|
17
|
+
readonly error?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Verse8AdsVerifierClient {
|
|
20
|
+
consume(input: {
|
|
21
|
+
readonly requestId: string;
|
|
22
|
+
readonly signal: AbortSignal;
|
|
23
|
+
}): Promise<Verse8AdsVerificationRecord>;
|
|
24
|
+
}
|
|
25
|
+
export type Verse8AdsVerifierAuthorization = string | (() => string | Promise<string>);
|
|
26
|
+
export interface CreateVerse8AdsVerifierHttpClientInput {
|
|
27
|
+
readonly authorization: Verse8AdsVerifierAuthorization;
|
|
28
|
+
readonly baseUrl?: string;
|
|
29
|
+
readonly fetch?: typeof fetch;
|
|
30
|
+
}
|
|
31
|
+
export interface CreateVerse8AdsEvidenceVerifierInput {
|
|
32
|
+
readonly client: Verse8AdsVerifierClient;
|
|
33
|
+
}
|
|
34
|
+
export declare class Verse8AdsVerifierHttpError extends Error {
|
|
35
|
+
readonly status: number;
|
|
36
|
+
readonly code: string | undefined;
|
|
37
|
+
constructor(status: number, code?: string);
|
|
38
|
+
}
|
|
39
|
+
export declare function createVerse8AdsVerifierHttpClient(input: CreateVerse8AdsVerifierHttpClientInput): Verse8AdsVerifierClient;
|
|
40
|
+
export declare function createVerse8AdsEvidenceVerifier(input: CreateVerse8AdsEvidenceVerifierInput): GameServicesEvidenceVerifier;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { verse8AdsRewardEvidenceSchema } from './ads-contract.js';
|
|
2
|
+
export const defaultVerse8AdsVerifierBaseUrl = 'https://ads-verifier.verse8.io';
|
|
3
|
+
const maximumVerifierResponseLength = 65536;
|
|
4
|
+
export class Verse8AdsVerifierHttpError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
code;
|
|
7
|
+
constructor(status, code) {
|
|
8
|
+
super(`Verse8 Ads verifier request failed with HTTP ${status}.`);
|
|
9
|
+
this.name = 'Verse8AdsVerifierHttpError';
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function createVerse8AdsVerifierHttpClient(input) {
|
|
15
|
+
const endpoint = verifierEndpoint(input.baseUrl ?? defaultVerse8AdsVerifierBaseUrl);
|
|
16
|
+
const fetcher = input.fetch ?? readGlobalFetch();
|
|
17
|
+
return {
|
|
18
|
+
async consume({ requestId, signal }) {
|
|
19
|
+
requireNonEmptyString(requestId, 'requestId');
|
|
20
|
+
const authorization = await resolveAuthorization(input.authorization);
|
|
21
|
+
const response = await fetcher(endpoint, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
Accept: 'application/json',
|
|
25
|
+
Authorization: authorization,
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({ requestId }),
|
|
29
|
+
signal,
|
|
30
|
+
});
|
|
31
|
+
const body = await readBoundedJson(response);
|
|
32
|
+
if (!response.ok && !isVerse8AdsVerificationRecord(body)) {
|
|
33
|
+
throw new Verse8AdsVerifierHttpError(response.status, readErrorCode(body));
|
|
34
|
+
}
|
|
35
|
+
const record = assertVerse8AdsVerificationRecord(body);
|
|
36
|
+
if (!response.ok && record.status !== 'consumed') {
|
|
37
|
+
throw new Verse8AdsVerifierHttpError(response.status, record.error);
|
|
38
|
+
}
|
|
39
|
+
return record;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function createVerse8AdsEvidenceVerifier(input) {
|
|
44
|
+
return {
|
|
45
|
+
async verifyPurchase() {
|
|
46
|
+
return rejected('VERSE8_PURCHASE_EVIDENCE_UNSUPPORTED');
|
|
47
|
+
},
|
|
48
|
+
async verifyAdReward(verificationInput) {
|
|
49
|
+
return verifyVerse8AdReward(input.client, verificationInput);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function verifyVerse8AdReward(client, input) {
|
|
54
|
+
const { request, platformPlacementId } = input;
|
|
55
|
+
if (request.target !== 'verse8') {
|
|
56
|
+
return rejected('VERSE8_AD_TARGET_REQUIRED');
|
|
57
|
+
}
|
|
58
|
+
if (platformPlacementId === undefined || platformPlacementId.length === 0) {
|
|
59
|
+
return rejected('VERSE8_AD_PLACEMENT_UNCONFIGURED');
|
|
60
|
+
}
|
|
61
|
+
const requestId = request.platformImpressionId;
|
|
62
|
+
if (requestId === undefined) {
|
|
63
|
+
return rejected('VERSE8_AD_REQUEST_ID_REQUIRED');
|
|
64
|
+
}
|
|
65
|
+
if (request.evidence?.schema !== verse8AdsRewardEvidenceSchema) {
|
|
66
|
+
return rejected('VERSE8_AD_EVIDENCE_SCHEMA_INVALID');
|
|
67
|
+
}
|
|
68
|
+
const evidenceRequestId = readEvidenceString(request.evidence.payload, 'requestId');
|
|
69
|
+
const evidencePlacementId = readEvidenceString(request.evidence.payload, 'placementId');
|
|
70
|
+
if (evidenceRequestId !== requestId) {
|
|
71
|
+
return rejected('VERSE8_AD_REQUEST_ID_MISMATCH');
|
|
72
|
+
}
|
|
73
|
+
if (evidencePlacementId !== platformPlacementId) {
|
|
74
|
+
return rejected('VERSE8_AD_PLACEMENT_MISMATCH');
|
|
75
|
+
}
|
|
76
|
+
const record = await client.consume({
|
|
77
|
+
requestId,
|
|
78
|
+
signal: input.signal,
|
|
79
|
+
});
|
|
80
|
+
if (record.status === 'pending') {
|
|
81
|
+
return {
|
|
82
|
+
status: 'pending',
|
|
83
|
+
reason: 'VERSE8_AD_EVIDENCE_PENDING',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (record.status === 'consumed') {
|
|
87
|
+
return rejected('VERSE8_AD_EVIDENCE_CONSUMED');
|
|
88
|
+
}
|
|
89
|
+
if (record.status === 'verified' && !record.verified) {
|
|
90
|
+
return rejected('VERSE8_AD_VERIFIER_RESPONSE_INCONSISTENT');
|
|
91
|
+
}
|
|
92
|
+
if (!record.verified) {
|
|
93
|
+
return {
|
|
94
|
+
status: 'pending',
|
|
95
|
+
reason: 'VERSE8_AD_EVIDENCE_PENDING',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (record.requestId !== requestId) {
|
|
99
|
+
return rejected('VERSE8_AD_VERIFIED_REQUEST_MISMATCH');
|
|
100
|
+
}
|
|
101
|
+
if (record.placementId !== platformPlacementId) {
|
|
102
|
+
return rejected('VERSE8_AD_VERIFIED_PLACEMENT_MISMATCH');
|
|
103
|
+
}
|
|
104
|
+
if (record.userId === undefined || !sameVerse8User(record.userId, request.playerId)) {
|
|
105
|
+
return rejected('VERSE8_AD_VERIFIED_USER_MISMATCH');
|
|
106
|
+
}
|
|
107
|
+
const verifiedAt = normalizeTimestamp(record.verifiedAt);
|
|
108
|
+
if (verifiedAt === undefined) {
|
|
109
|
+
return rejected('VERSE8_AD_VERIFIED_AT_INVALID');
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
status: 'verified',
|
|
113
|
+
verificationId: `verse8:ad-reward:${requestId}`,
|
|
114
|
+
verifiedAt,
|
|
115
|
+
payload: {
|
|
116
|
+
verse8RequestId: requestId,
|
|
117
|
+
verse8PlacementId: platformPlacementId,
|
|
118
|
+
...(record.adNetwork === undefined ? {} : { verse8AdNetwork: record.adNetwork }),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function verifierEndpoint(baseUrl) {
|
|
123
|
+
const url = new URL(baseUrl);
|
|
124
|
+
if (url.protocol !== 'https:') {
|
|
125
|
+
throw new Error('Verse8 Ads verifier baseUrl must use HTTPS.');
|
|
126
|
+
}
|
|
127
|
+
url.hash = '';
|
|
128
|
+
url.search = '';
|
|
129
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/ads/verify`;
|
|
130
|
+
return url.toString();
|
|
131
|
+
}
|
|
132
|
+
function readGlobalFetch() {
|
|
133
|
+
const fetcher = globalThis.fetch;
|
|
134
|
+
if (fetcher === undefined) {
|
|
135
|
+
throw new Error('globalThis.fetch is unavailable. Provide a Verse8 verifier fetch implementation.');
|
|
136
|
+
}
|
|
137
|
+
return fetcher.bind(globalThis);
|
|
138
|
+
}
|
|
139
|
+
async function resolveAuthorization(authorization) {
|
|
140
|
+
const resolved = typeof authorization === 'function' ? await authorization() : authorization;
|
|
141
|
+
requireNonEmptyString(resolved, 'authorization');
|
|
142
|
+
return resolved.trim();
|
|
143
|
+
}
|
|
144
|
+
async function readBoundedJson(response) {
|
|
145
|
+
const body = await readBoundedBody(response);
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(body);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new Error('Verse8 Ads verifier response must be valid JSON.');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function readBoundedBody(response) {
|
|
154
|
+
const declaredLength = response.headers.get('Content-Length');
|
|
155
|
+
if (declaredLength !== null && Number(declaredLength) > maximumVerifierResponseLength) {
|
|
156
|
+
throw new Error('Verse8 Ads verifier response exceeds the maximum size.');
|
|
157
|
+
}
|
|
158
|
+
if (response.body === null) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
const reader = response.body.getReader();
|
|
162
|
+
const decoder = new TextDecoder();
|
|
163
|
+
let byteLength = 0;
|
|
164
|
+
let body = '';
|
|
165
|
+
try {
|
|
166
|
+
while (true) {
|
|
167
|
+
const chunk = await reader.read();
|
|
168
|
+
if (chunk.done) {
|
|
169
|
+
return body + decoder.decode();
|
|
170
|
+
}
|
|
171
|
+
byteLength += chunk.value.byteLength;
|
|
172
|
+
if (byteLength > maximumVerifierResponseLength) {
|
|
173
|
+
await reader.cancel().catch(() => undefined);
|
|
174
|
+
throw new Error('Verse8 Ads verifier response exceeds the maximum size.');
|
|
175
|
+
}
|
|
176
|
+
body += decoder.decode(chunk.value, { stream: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
reader.releaseLock();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function assertVerse8AdsVerificationRecord(input) {
|
|
184
|
+
if (!isVerse8AdsVerificationRecord(input)) {
|
|
185
|
+
throw new Error('Verse8 Ads verifier response has an invalid shape.');
|
|
186
|
+
}
|
|
187
|
+
return input;
|
|
188
|
+
}
|
|
189
|
+
function isVerse8AdsVerificationRecord(input) {
|
|
190
|
+
if (!isRecord(input) || typeof input.verified !== 'boolean') {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (input.status !== undefined
|
|
194
|
+
&& input.status !== 'verified'
|
|
195
|
+
&& input.status !== 'consumed'
|
|
196
|
+
&& input.status !== 'pending') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (!isOptionalString(input.requestId)
|
|
200
|
+
|| !isOptionalString(input.placementId)
|
|
201
|
+
|| !isOptionalString(input.userId)
|
|
202
|
+
|| !isOptionalString(input.verifiedAt)
|
|
203
|
+
|| !isOptionalString(input.consumedAt)
|
|
204
|
+
|| !isOptionalString(input.error)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (input.adNetwork !== undefined && input.adNetwork !== 'google') {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (input.reward !== undefined) {
|
|
211
|
+
if (!isRecord(input.reward)
|
|
212
|
+
|| typeof input.reward.amount !== 'number'
|
|
213
|
+
|| !Number.isFinite(input.reward.amount)
|
|
214
|
+
|| typeof input.reward.type !== 'string') {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
function readErrorCode(input) {
|
|
221
|
+
return isRecord(input) && typeof input.error === 'string' ? input.error : undefined;
|
|
222
|
+
}
|
|
223
|
+
function readEvidenceString(payload, key) {
|
|
224
|
+
const value = payload[key];
|
|
225
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
226
|
+
}
|
|
227
|
+
function normalizeTimestamp(value) {
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const timestamp = Date.parse(value);
|
|
232
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
|
|
233
|
+
}
|
|
234
|
+
function sameVerse8User(left, right) {
|
|
235
|
+
return isHexAccount(left) && isHexAccount(right)
|
|
236
|
+
? left.toLowerCase() === right.toLowerCase()
|
|
237
|
+
: left === right;
|
|
238
|
+
}
|
|
239
|
+
function isHexAccount(value) {
|
|
240
|
+
return /^0x[0-9a-fA-F]+$/.test(value);
|
|
241
|
+
}
|
|
242
|
+
function rejected(reason) {
|
|
243
|
+
return {
|
|
244
|
+
status: 'rejected',
|
|
245
|
+
reason,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function isRecord(input) {
|
|
249
|
+
return typeof input === 'object' && input !== null && !Array.isArray(input);
|
|
250
|
+
}
|
|
251
|
+
function isOptionalString(input) {
|
|
252
|
+
return input === undefined || typeof input === 'string';
|
|
253
|
+
}
|
|
254
|
+
function requireNonEmptyString(input, label) {
|
|
255
|
+
if (input.trim().length === 0) {
|
|
256
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-verse8",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Verse8 iframe platform adapter for mpgd games.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,14 +27,25 @@
|
|
|
27
27
|
".": {
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./server": {
|
|
32
|
+
"types": "./dist/server.d.ts",
|
|
33
|
+
"default": "./dist/server.js"
|
|
34
|
+
},
|
|
35
|
+
"./agent8": {
|
|
36
|
+
"types": "./dist/agent8.d.ts",
|
|
37
|
+
"default": "./dist/agent8.js"
|
|
30
38
|
}
|
|
31
39
|
},
|
|
32
40
|
"files": [
|
|
33
41
|
"dist"
|
|
34
42
|
],
|
|
35
43
|
"dependencies": {
|
|
44
|
+
"@verse8/ads": "0.4.0",
|
|
36
45
|
"@verse8/platform": "2.1.0",
|
|
37
|
-
"@mpgd/
|
|
46
|
+
"@mpgd/catalog": "0.5.0",
|
|
47
|
+
"@mpgd/platform": "0.7.0",
|
|
48
|
+
"@mpgd/game-services": "0.10.0"
|
|
38
49
|
},
|
|
39
50
|
"devDependencies": {
|
|
40
51
|
"ttsc": "0.18.4",
|
|
@@ -51,6 +62,6 @@
|
|
|
51
62
|
"lint": "ttsc --noEmit",
|
|
52
63
|
"format": "ttsc format",
|
|
53
64
|
"fix": "ttsc fix",
|
|
54
|
-
"test": "vitest run"
|
|
65
|
+
"test": "vitest run && node test/server-dist-import.mjs && node test/agent8-dist-import.mjs"
|
|
55
66
|
}
|
|
56
67
|
}
|