@mpgd/game-services 0.21.1 → 0.23.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 +30 -0
- package/dist/admob-ssv-conformance.js +3 -1
- package/dist/app-store-recovery.js +5 -0
- package/dist/client.d.ts +39 -1
- package/dist/client.js +198 -0
- package/dist/commerce-evidence-registry.d.ts +406 -0
- package/dist/commerce-evidence-registry.js +773 -0
- package/dist/commerce-ledger.d.ts +64 -0
- package/dist/commerce-ledger.js +146 -0
- package/dist/commerce-reconciler.d.ts +191 -0
- package/dist/commerce-reconciler.js +1251 -0
- package/dist/contract.d.ts +4 -1
- package/dist/contract.js +8 -0
- package/dist/evidence-verification.d.ts +14 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/monetization-operation-store.d.ts +20 -0
- package/dist/monetization-operation-store.js +80 -0
- package/dist/monetization-recovery.d.ts +57 -2
- package/dist/monetization-recovery.js +102 -8
- package/dist/notification-delivery.d.ts +9 -1
- package/dist/notification-delivery.js +45 -5
- package/dist/operation-results.d.ts +14 -0
- package/dist/operations.d.ts +2 -1
- package/dist/platform-order.d.ts +47 -0
- package/dist/platform-order.js +86 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +13 -1
- package/dist/server.d.ts +43 -1
- package/dist/server.js +305 -7
- package/dist/types.d.ts +79 -3
- package/dist/types.js +97 -6
- package/package.json +18 -6
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { commerceProtocolVersion, } from '@mpgd/platform/commerce';
|
|
2
|
+
import { assertCommerceStoreId, } from './types.js';
|
|
3
|
+
const defaultPurchaseCapabilities = Object.freeze({
|
|
4
|
+
consumable: true,
|
|
5
|
+
durable: true,
|
|
6
|
+
subscription: false,
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* The v1 request mapping (RFC "Open store ids"): a request without `store`
|
|
10
|
+
* names its store through its deployment target. `android` is `google-play`,
|
|
11
|
+
* `ios` is `app-store`, `ait` is `apps-in-toss`, `reddit` is `devvit`, and
|
|
12
|
+
* every other target is its own store id (`microsoft-store`, `verse8`,
|
|
13
|
+
* `oneplay`, or a project store that reuses its target name).
|
|
14
|
+
*/
|
|
15
|
+
export function resolveLegacyCommerceStoreId(target) {
|
|
16
|
+
switch (target) {
|
|
17
|
+
case 'android':
|
|
18
|
+
return 'google-play';
|
|
19
|
+
case 'ios':
|
|
20
|
+
return 'app-store';
|
|
21
|
+
case 'ait':
|
|
22
|
+
return 'apps-in-toss';
|
|
23
|
+
case 'reddit':
|
|
24
|
+
return 'devvit';
|
|
25
|
+
default:
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The store a verify request names: its `store`, or the v1 mapping of its `target`. */
|
|
30
|
+
export function resolveCommerceStoreId(request) {
|
|
31
|
+
return request.store ?? resolveLegacyCommerceStoreId(request.target);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds the registry. Construction fails when two entries claim one schema,
|
|
35
|
+
* when a `finish: 'server'` entry has no finalizer, when two entries of one
|
|
36
|
+
* store disagree on `finish`, or when more than one entry of a store accepts
|
|
37
|
+
* a missing envelope.
|
|
38
|
+
*/
|
|
39
|
+
export function createCommerceEvidenceRegistry(entries, options = {}) {
|
|
40
|
+
const bySchema = new Map();
|
|
41
|
+
const byStore = new Map();
|
|
42
|
+
const missingEvidenceByStore = new Map();
|
|
43
|
+
const frozenEntries = [];
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
assertRegistryEntry(entry);
|
|
46
|
+
if (bySchema.has(entry.schema)) {
|
|
47
|
+
throw new TypeError(`Commerce evidence schema ${entry.schema} is claimed by two registry entries.`);
|
|
48
|
+
}
|
|
49
|
+
const siblings = byStore.get(entry.store) ?? [];
|
|
50
|
+
const sibling = siblings[0];
|
|
51
|
+
if (sibling !== undefined && sibling.finish !== entry.finish) {
|
|
52
|
+
throw new TypeError(`Commerce store ${entry.store} declares finish ${sibling.finish} for ${sibling.schema} `
|
|
53
|
+
+ `and ${entry.finish} for ${entry.schema}; one store has one finish owner.`);
|
|
54
|
+
}
|
|
55
|
+
if (sibling !== undefined && !samePurchaseCapabilities(sibling.purchase, entry.purchase)) {
|
|
56
|
+
// What a store sells is one profile; two entries of one store must
|
|
57
|
+
// declare it identically (or both leave the default), never be merged.
|
|
58
|
+
throw new TypeError(`Commerce store ${entry.store} declares different purchase kinds for ${sibling.schema} `
|
|
59
|
+
+ `and ${entry.schema}; every entry of a store declares the same purchase profile.`);
|
|
60
|
+
}
|
|
61
|
+
if (entry.acceptsMissingEvidence === true) {
|
|
62
|
+
if (missingEvidenceByStore.has(entry.store)) {
|
|
63
|
+
throw new TypeError(`Commerce store ${entry.store} has two entries that accept a missing evidence envelope.`);
|
|
64
|
+
}
|
|
65
|
+
missingEvidenceByStore.set(entry.store, entry);
|
|
66
|
+
}
|
|
67
|
+
const frozen = Object.freeze({ ...entry });
|
|
68
|
+
bySchema.set(entry.schema, frozen);
|
|
69
|
+
byStore.set(entry.store, [...siblings, frozen]);
|
|
70
|
+
frozenEntries.push(frozen);
|
|
71
|
+
}
|
|
72
|
+
const fallback = options.unregistered;
|
|
73
|
+
const fallbackStores = new Map();
|
|
74
|
+
for (const declared of fallback?.stores ?? []) {
|
|
75
|
+
assertCommerceStoreId(declared.store, 'unregistered.stores[].store');
|
|
76
|
+
if (byStore.has(declared.store)) {
|
|
77
|
+
throw new TypeError(`Commerce store ${declared.store} has registry entries and is also declared for the `
|
|
78
|
+
+ 'unregistered fallback; one or the other serves it.');
|
|
79
|
+
}
|
|
80
|
+
if (fallbackStores.has(declared.store)) {
|
|
81
|
+
throw new TypeError(`Commerce store ${declared.store} is declared twice for the fallback.`);
|
|
82
|
+
}
|
|
83
|
+
if (declared.finish === 'server' && fallback?.finalizer === undefined) {
|
|
84
|
+
throw new TypeError(`Commerce store ${declared.store} is declared for the fallback with finish: server and `
|
|
85
|
+
+ 'needs unregistered.finalizer; without one its grants would never be acknowledged.');
|
|
86
|
+
}
|
|
87
|
+
fallbackStores.set(declared.store, declared);
|
|
88
|
+
}
|
|
89
|
+
const unclaimedByStore = new Map();
|
|
90
|
+
for (const entry of frozenEntries) {
|
|
91
|
+
if (entry.acceptsUnclaimedSchemas === true) {
|
|
92
|
+
if (unclaimedByStore.has(entry.store)) {
|
|
93
|
+
throw new TypeError(`Commerce store ${entry.store} has two entries that accept unclaimed schemas.`);
|
|
94
|
+
}
|
|
95
|
+
unclaimedByStore.set(entry.store, entry);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const servesUndeclaredStores = fallback !== undefined
|
|
99
|
+
&& (fallback.servesUndeclaredStores ?? fallback.stores === undefined);
|
|
100
|
+
const stores = new Set([...byStore.keys(), ...fallbackStores.keys()]);
|
|
101
|
+
const allowedTargets = (store) => {
|
|
102
|
+
const storeEntries = byStore.get(store);
|
|
103
|
+
if (storeEntries !== undefined) {
|
|
104
|
+
if (storeEntries.some((entry) => entry.targets === undefined)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return new Set(storeEntries.flatMap((entry) => entry.targets ?? []));
|
|
108
|
+
}
|
|
109
|
+
const declared = fallbackStores.get(store);
|
|
110
|
+
return declared?.targets === undefined ? undefined : new Set(declared.targets);
|
|
111
|
+
};
|
|
112
|
+
const resolveStore = (request) => resolveCommerceStoreId(request);
|
|
113
|
+
const targetAllowed = (allowed, target) => allowed === undefined || allowed.includes(target);
|
|
114
|
+
const routeToEntry = (store, target, entry) => {
|
|
115
|
+
if (entry.store !== store) {
|
|
116
|
+
return { status: 'rejected', store, reason: 'EVIDENCE_SCHEMA_STORE_MISMATCH' };
|
|
117
|
+
}
|
|
118
|
+
if (!targetAllowed(entry.targets, target)) {
|
|
119
|
+
return { status: 'rejected', store, reason: 'EVIDENCE_SCHEMA_TARGET_MISMATCH' };
|
|
120
|
+
}
|
|
121
|
+
return { status: 'routed', store, entry };
|
|
122
|
+
};
|
|
123
|
+
const route = (request) => {
|
|
124
|
+
const store = resolveStore(request);
|
|
125
|
+
const schema = request.evidence?.schema;
|
|
126
|
+
const claimed = schema === undefined ? undefined : bySchema.get(schema);
|
|
127
|
+
if (claimed !== undefined) {
|
|
128
|
+
return routeToEntry(store, request.target, claimed);
|
|
129
|
+
}
|
|
130
|
+
if (byStore.has(store)) {
|
|
131
|
+
// A store with entries is never served by the fallback: an unclaimed
|
|
132
|
+
// schema or a missing envelope reaches only the entry that opted in.
|
|
133
|
+
const owner = schema === undefined
|
|
134
|
+
? missingEvidenceByStore.get(store)
|
|
135
|
+
: unclaimedByStore.get(store);
|
|
136
|
+
return owner === undefined
|
|
137
|
+
? { status: 'rejected', store, reason: 'EVIDENCE_SCHEMA_UNSUPPORTED' }
|
|
138
|
+
: routeToEntry(store, request.target, owner);
|
|
139
|
+
}
|
|
140
|
+
const declared = fallbackStores.get(store);
|
|
141
|
+
if (declared !== undefined) {
|
|
142
|
+
return targetAllowed(declared.targets, request.target)
|
|
143
|
+
? { status: 'unregistered', store }
|
|
144
|
+
: { status: 'rejected', store, reason: 'EVIDENCE_SCHEMA_TARGET_MISMATCH' };
|
|
145
|
+
}
|
|
146
|
+
if (!servesUndeclaredStores) {
|
|
147
|
+
return { status: 'rejected', store, reason: 'EVIDENCE_SCHEMA_UNSUPPORTED' };
|
|
148
|
+
}
|
|
149
|
+
if (dodgesServedStore(request)) {
|
|
150
|
+
return { status: 'rejected', store, reason: 'EVIDENCE_SCHEMA_STORE_MISMATCH' };
|
|
151
|
+
}
|
|
152
|
+
return { status: 'unregistered', store };
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* An undeclared `store` label on a target whose v1 store is served by an
|
|
156
|
+
* entry or a declared fallback store (`{ target: 'android', store: 'x' }`)
|
|
157
|
+
* would route the target's purchase around that entry; it is refused.
|
|
158
|
+
*/
|
|
159
|
+
const dodgesServedStore = (request) => {
|
|
160
|
+
const legacy = resolveLegacyCommerceStoreId(request.target);
|
|
161
|
+
return request.store !== undefined && request.store !== legacy && stores.has(legacy);
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* The entry that owns a grant being finalized: the schema and store the
|
|
165
|
+
* grant's ledger payload recorded come first (a retry may carry another
|
|
166
|
+
* envelope), then the request's schema, then the single finalizing entry
|
|
167
|
+
* of the grant's store (a grant written before the payload carried
|
|
168
|
+
* `evidenceSchema`).
|
|
169
|
+
*/
|
|
170
|
+
const routeFinalization = (input) => {
|
|
171
|
+
const payloadStore = input.evidencePayload?.store;
|
|
172
|
+
const store = typeof payloadStore === 'string' && payloadStore.length > 0
|
|
173
|
+
? payloadStore
|
|
174
|
+
: resolveStore(input.request);
|
|
175
|
+
const payloadSchema = input.evidencePayload?.evidenceSchema;
|
|
176
|
+
const schema = typeof payloadSchema === 'string' && payloadSchema.length > 0
|
|
177
|
+
? payloadSchema
|
|
178
|
+
: input.request.evidence?.schema;
|
|
179
|
+
if (schema !== undefined) {
|
|
180
|
+
const entry = bySchema.get(schema);
|
|
181
|
+
if (entry !== undefined) {
|
|
182
|
+
return entry.store === store ? { kind: 'entry', entry } : { kind: 'mismatch', entry };
|
|
183
|
+
}
|
|
184
|
+
const unclaimed = unclaimedByStore.get(store);
|
|
185
|
+
if (unclaimed !== undefined) {
|
|
186
|
+
return { kind: 'entry', entry: unclaimed };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const finalizing = (byStore.get(store) ?? []).filter((entry) => entry.finalizer !== undefined);
|
|
190
|
+
const single = finalizing[0];
|
|
191
|
+
if (finalizing.length === 1 && single !== undefined) {
|
|
192
|
+
return { kind: 'entry', entry: single };
|
|
193
|
+
}
|
|
194
|
+
if (!byStore.has(store) && (fallbackStores.has(store) || servesUndeclaredStores)) {
|
|
195
|
+
return { kind: 'fallback' };
|
|
196
|
+
}
|
|
197
|
+
return { kind: 'none' };
|
|
198
|
+
};
|
|
199
|
+
const finalizer = {
|
|
200
|
+
supportsPurchaseGrant(input) {
|
|
201
|
+
const owner = routeFinalization(input);
|
|
202
|
+
switch (owner.kind) {
|
|
203
|
+
case 'entry':
|
|
204
|
+
return owner.entry.finalizer !== undefined
|
|
205
|
+
&& (owner.entry.finalizer.supportsPurchaseGrant?.(input) ?? true);
|
|
206
|
+
case 'fallback':
|
|
207
|
+
return fallback?.finalizer !== undefined
|
|
208
|
+
&& (fallback.finalizer.supportsPurchaseGrant?.(input) ?? true);
|
|
209
|
+
case 'mismatch':
|
|
210
|
+
// The owning entry has a finalizer that must not run for another
|
|
211
|
+
// store's grant: the grant stays pending rather than reported done.
|
|
212
|
+
return owner.entry.finalizer !== undefined;
|
|
213
|
+
case 'none':
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
async finalizePurchaseGrant(input) {
|
|
218
|
+
const owner = routeFinalization(input);
|
|
219
|
+
const selected = owner.kind === 'entry'
|
|
220
|
+
? owner.entry.finalizer
|
|
221
|
+
: owner.kind === 'fallback'
|
|
222
|
+
? fallback?.finalizer
|
|
223
|
+
: undefined;
|
|
224
|
+
if (selected === undefined) {
|
|
225
|
+
return {
|
|
226
|
+
status: 'pending',
|
|
227
|
+
alreadyCompleted: false,
|
|
228
|
+
reason: owner.kind === 'mismatch'
|
|
229
|
+
? 'PURCHASE_FINALIZER_STORE_MISMATCH'
|
|
230
|
+
: 'PURCHASE_FINALIZER_UNREGISTERED',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return selected.finalizePurchaseGrant(input);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
const purchaseProfileOf = (declared) => ({ ...(declared ?? defaultPurchaseCapabilities) });
|
|
237
|
+
const describeStore = (store, storeEntries) => {
|
|
238
|
+
const first = storeEntries[0];
|
|
239
|
+
if (first === undefined) {
|
|
240
|
+
throw new Error(`Commerce store ${store} has no registry entries.`);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
store,
|
|
244
|
+
schemas: storeEntries.map((entry) => entry.schema),
|
|
245
|
+
finish: first.finish,
|
|
246
|
+
purchase: purchaseProfileOf(first.purchase),
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
const getCapabilities = () => {
|
|
250
|
+
const published = [];
|
|
251
|
+
for (const [store, storeEntries] of byStore) {
|
|
252
|
+
published.push(describeStore(store, storeEntries));
|
|
253
|
+
}
|
|
254
|
+
for (const declared of fallbackStores.values()) {
|
|
255
|
+
published.push({
|
|
256
|
+
store: declared.store,
|
|
257
|
+
schemas: [],
|
|
258
|
+
finish: declared.finish,
|
|
259
|
+
purchase: purchaseProfileOf(declared.purchase),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return { commerceProtocolVersion, stores: published };
|
|
263
|
+
};
|
|
264
|
+
const serves = (request) => {
|
|
265
|
+
const store = resolveStore(request);
|
|
266
|
+
if (stores.has(store)) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
return servesUndeclaredStores && !dodgesServedStore(request);
|
|
270
|
+
};
|
|
271
|
+
const registry = {
|
|
272
|
+
entries: Object.freeze(frozenEntries),
|
|
273
|
+
stores,
|
|
274
|
+
serves,
|
|
275
|
+
servesUndeclaredStores,
|
|
276
|
+
allowedTargets,
|
|
277
|
+
resolveStore,
|
|
278
|
+
route,
|
|
279
|
+
async verifyPurchase(input) {
|
|
280
|
+
const routed = route(input.request);
|
|
281
|
+
if (routed.status === 'rejected') {
|
|
282
|
+
return { status: 'rejected', reason: routed.reason };
|
|
283
|
+
}
|
|
284
|
+
if (routed.status === 'unregistered') {
|
|
285
|
+
if (fallback === undefined) {
|
|
286
|
+
return { status: 'rejected', reason: 'EVIDENCE_SCHEMA_UNSUPPORTED' };
|
|
287
|
+
}
|
|
288
|
+
return fallback.verifier.verifyPurchase(input);
|
|
289
|
+
}
|
|
290
|
+
return routed.entry.verifier.verifyPurchase(input);
|
|
291
|
+
},
|
|
292
|
+
finalizer,
|
|
293
|
+
getCapabilities,
|
|
294
|
+
};
|
|
295
|
+
return Object.freeze(registry);
|
|
296
|
+
}
|
|
297
|
+
function samePurchaseCapabilities(left, right) {
|
|
298
|
+
const a = left ?? defaultPurchaseCapabilities;
|
|
299
|
+
const b = right ?? defaultPurchaseCapabilities;
|
|
300
|
+
return a.consumable === b.consumable
|
|
301
|
+
&& a.durable === b.durable
|
|
302
|
+
&& a.subscription === b.subscription;
|
|
303
|
+
}
|
|
304
|
+
function assertRegistryEntry(entry) {
|
|
305
|
+
if (typeof entry.schema !== 'string'
|
|
306
|
+
|| entry.schema.trim() === ''
|
|
307
|
+
|| entry.schema.length > 512
|
|
308
|
+
|| entry.schema.trim() !== entry.schema) {
|
|
309
|
+
throw new TypeError('A commerce evidence registry entry needs a bounded schema identifier.');
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
assertCommerceStoreId(entry.store, 'store');
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema}: ${error instanceof Error ? error.message : String(error)}`);
|
|
316
|
+
}
|
|
317
|
+
if (typeof entry.verifier?.verifyPurchase !== 'function') {
|
|
318
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema} needs a verifier with verifyPurchase.`);
|
|
319
|
+
}
|
|
320
|
+
if (entry.finish !== 'client'
|
|
321
|
+
&& entry.finish !== 'server'
|
|
322
|
+
&& entry.finish !== 'host'
|
|
323
|
+
&& entry.finish !== 'none') {
|
|
324
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema} declares an unknown finish owner.`);
|
|
325
|
+
}
|
|
326
|
+
if (entry.finalizer !== undefined && typeof entry.finalizer.finalizePurchaseGrant !== 'function') {
|
|
327
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema} has a finalizer without finalizePurchaseGrant.`);
|
|
328
|
+
}
|
|
329
|
+
if (entry.finish === 'server' && entry.finalizer === undefined) {
|
|
330
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema} (store ${entry.store}) declares `
|
|
331
|
+
+ 'finish: server and needs a finalizer; a server-finish store without one would leave '
|
|
332
|
+
+ 'every grant unacknowledged.');
|
|
333
|
+
}
|
|
334
|
+
if (entry.creditScope !== undefined
|
|
335
|
+
&& entry.creditScope !== 'title'
|
|
336
|
+
&& entry.creditScope !== 'cross-title') {
|
|
337
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema} declares an unknown credit scope.`);
|
|
338
|
+
}
|
|
339
|
+
if (entry.acceptsMissingEvidence !== undefined && typeof entry.acceptsMissingEvidence !== 'boolean') {
|
|
340
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema}: acceptsMissingEvidence must be a boolean.`);
|
|
341
|
+
}
|
|
342
|
+
if (entry.acceptsUnclaimedSchemas !== undefined && typeof entry.acceptsUnclaimedSchemas !== 'boolean') {
|
|
343
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema}: acceptsUnclaimedSchemas must be a boolean.`);
|
|
344
|
+
}
|
|
345
|
+
if (entry.targets !== undefined
|
|
346
|
+
&& (!Array.isArray(entry.targets) || entry.targets.length === 0
|
|
347
|
+
|| entry.targets.some((target) => typeof target !== 'string' || target.length === 0))) {
|
|
348
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema}: targets must list ledger target names.`);
|
|
349
|
+
}
|
|
350
|
+
if (entry.purchase !== undefined) {
|
|
351
|
+
const { consumable, durable, subscription } = entry.purchase;
|
|
352
|
+
if (typeof consumable !== 'boolean'
|
|
353
|
+
|| typeof durable !== 'boolean'
|
|
354
|
+
|| typeof subscription !== 'boolean') {
|
|
355
|
+
throw new TypeError(`Commerce evidence registry entry ${entry.schema}: purchase flags must be booleans.`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Reference entries. Each wraps a verifier (and finalizer) the kit already
|
|
360
|
+
// ships with the RFC "Capabilities" profile row of its store.
|
|
361
|
+
/** The ledger target each reference store's verify requests name (RFC "Open store ids"). */
|
|
362
|
+
export const referenceCommerceLedgerTargets = Object.freeze({
|
|
363
|
+
'google-play': Object.freeze(['android']),
|
|
364
|
+
'app-store': Object.freeze(['ios']),
|
|
365
|
+
'microsoft-store': Object.freeze(['microsoft-store']),
|
|
366
|
+
'apps-in-toss': Object.freeze(['ait']),
|
|
367
|
+
devvit: Object.freeze(['reddit']),
|
|
368
|
+
verse8: Object.freeze(['verse8']),
|
|
369
|
+
});
|
|
370
|
+
/** The RFC profile purchase flags of the reference stores. */
|
|
371
|
+
export const referenceCommercePurchaseCapabilities = Object.freeze({
|
|
372
|
+
'google-play': Object.freeze({ consumable: true, durable: true, subscription: false }),
|
|
373
|
+
'app-store': Object.freeze({ consumable: true, durable: true, subscription: false }),
|
|
374
|
+
'microsoft-store': Object.freeze({ consumable: true, durable: false, subscription: false }),
|
|
375
|
+
'apps-in-toss': Object.freeze({ consumable: true, durable: true, subscription: false }),
|
|
376
|
+
devvit: Object.freeze({ consumable: true, durable: true, subscription: false }),
|
|
377
|
+
verse8: Object.freeze({ consumable: true, durable: true, subscription: false }),
|
|
378
|
+
});
|
|
379
|
+
export const googlePlayCommerceStoreId = 'google-play';
|
|
380
|
+
export const appStoreCommerceStoreId = 'app-store';
|
|
381
|
+
export const microsoftStoreCommerceStoreId = 'microsoft-store';
|
|
382
|
+
export const appsInTossCommerceStoreId = 'apps-in-toss';
|
|
383
|
+
export const devvitCommerceStoreId = 'devvit';
|
|
384
|
+
export const verse8CommerceStoreId = 'verse8';
|
|
385
|
+
export const googlePlayCommerceEvidenceSchema = 'google-play.product-purchase.v2';
|
|
386
|
+
export const appStoreCommerceEvidenceSchema = 'app-store.signed-transaction.v1';
|
|
387
|
+
export const microsoftStoreCommerceEvidenceSchema = 'mpgd.microsoft-store.digital-goods.v1';
|
|
388
|
+
export const appsInTossCommerceEvidenceSchema = 'apps-in-toss.iap.callback.v1';
|
|
389
|
+
export const devvitPaymentOrderEvidenceSchema = 'devvit.payment-order.v1';
|
|
390
|
+
export const devvitOrderEvidenceSchema = 'devvit.order.v1';
|
|
391
|
+
export const verse8Agent8EventEvidenceSchema = 'verse8.agent8-event.v1';
|
|
392
|
+
/**
|
|
393
|
+
* `google-play.product-purchase.v2`: the Play boundary verifies the purchase
|
|
394
|
+
* token and, after the ledger grant, acknowledges or consumes it
|
|
395
|
+
* (`finish: 'server'`). Pass `createGooglePlayProductPurchaseBoundary(...)`,
|
|
396
|
+
* or a verifier binding plus a separate finalizer.
|
|
397
|
+
*/
|
|
398
|
+
export function createGooglePlayCommerceEvidenceEntry(input) {
|
|
399
|
+
const finish = input.finish ?? 'server';
|
|
400
|
+
return {
|
|
401
|
+
schema: googlePlayCommerceEvidenceSchema,
|
|
402
|
+
store: googlePlayCommerceStoreId,
|
|
403
|
+
verifier: input.verifier,
|
|
404
|
+
...(finish === 'server' ? { finalizer: input.finalizer } : {}),
|
|
405
|
+
finish,
|
|
406
|
+
creditScope: 'title',
|
|
407
|
+
...optionalRouting(input),
|
|
408
|
+
targets: input.targets ?? referenceCommerceLedgerTargets['google-play'],
|
|
409
|
+
purchase: referenceCommercePurchaseCapabilities['google-play'],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function optionalRouting(input) {
|
|
413
|
+
return {
|
|
414
|
+
...(input.acceptsMissingEvidence === undefined
|
|
415
|
+
? {}
|
|
416
|
+
: { acceptsMissingEvidence: input.acceptsMissingEvidence }),
|
|
417
|
+
...(input.acceptsUnclaimedSchemas === undefined
|
|
418
|
+
? {}
|
|
419
|
+
: { acceptsUnclaimedSchemas: input.acceptsUnclaimedSchemas }),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* `app-store.signed-transaction.v1`: the App Store verifier checks the
|
|
424
|
+
* StoreKit JWS offline and decides on the Server API status; a request
|
|
425
|
+
* without the envelope keeps the `Get Transaction Info` lookup, so the entry
|
|
426
|
+
* accepts a missing envelope. The client finishes after the ledger grant.
|
|
427
|
+
*/
|
|
428
|
+
export function createAppStoreCommerceEvidenceEntry(input) {
|
|
429
|
+
return {
|
|
430
|
+
schema: appStoreCommerceEvidenceSchema,
|
|
431
|
+
store: appStoreCommerceStoreId,
|
|
432
|
+
verifier: input.verifier,
|
|
433
|
+
finish: 'client',
|
|
434
|
+
creditScope: 'title',
|
|
435
|
+
...optionalRouting(input),
|
|
436
|
+
acceptsMissingEvidence: input.acceptsMissingEvidence ?? true,
|
|
437
|
+
targets: input.targets ?? referenceCommerceLedgerTargets['app-store'],
|
|
438
|
+
purchase: referenceCommercePurchaseCapabilities['app-store'],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* `mpgd.microsoft-store.digital-goods.v1`: the Collections verifier before
|
|
443
|
+
* the grant and the consume finalizer after it (`finish: 'server'`), always
|
|
444
|
+
* configured together.
|
|
445
|
+
*/
|
|
446
|
+
export function createMicrosoftStoreCommerceEvidenceEntry(input) {
|
|
447
|
+
return {
|
|
448
|
+
schema: microsoftStoreCommerceEvidenceSchema,
|
|
449
|
+
store: microsoftStoreCommerceStoreId,
|
|
450
|
+
verifier: input.verifier,
|
|
451
|
+
finalizer: input.finalizer,
|
|
452
|
+
finish: 'server',
|
|
453
|
+
...optionalRouting(input),
|
|
454
|
+
targets: input.targets ?? referenceCommerceLedgerTargets['microsoft-store'],
|
|
455
|
+
purchase: referenceCommercePurchaseCapabilities['microsoft-store'],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* `apps-in-toss.iap.callback.v1`: the production evidence verifier binds the
|
|
460
|
+
* order through the partner API; the host finishes (`completeProductGrant`)
|
|
461
|
+
* after the ledger grant.
|
|
462
|
+
*/
|
|
463
|
+
export function createAppsInTossCommerceEvidenceEntry(input) {
|
|
464
|
+
return {
|
|
465
|
+
schema: appsInTossCommerceEvidenceSchema,
|
|
466
|
+
store: appsInTossCommerceStoreId,
|
|
467
|
+
verifier: input.verifier,
|
|
468
|
+
finish: 'host',
|
|
469
|
+
...optionalRouting(input),
|
|
470
|
+
targets: input.targets ?? referenceCommerceLedgerTargets['apps-in-toss'],
|
|
471
|
+
purchase: referenceCommercePurchaseCapabilities['apps-in-toss'],
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* The `devvit.payment-order.v1` verifier: the envelope
|
|
476
|
+
* `normalizeDevvitFulfillmentOrder` produced names the order, and the grant
|
|
477
|
+
* is decided by the deployment's payments authority (`PAID` or `DELIVERED`
|
|
478
|
+
* for the requesting player and SKU). Replay identity is the order id, so a
|
|
479
|
+
* second fulfillment of one order meets the first grant.
|
|
480
|
+
*/
|
|
481
|
+
export function createDevvitPaymentOrderEvidenceVerifier(input) {
|
|
482
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
483
|
+
return {
|
|
484
|
+
async verifyPurchase({ request, platformProductId, signal }) {
|
|
485
|
+
const envelope = request.evidence;
|
|
486
|
+
if (envelope?.schema !== devvitPaymentOrderEvidenceSchema) {
|
|
487
|
+
return { status: 'rejected', reason: 'EVIDENCE_SCHEMA_UNSUPPORTED' };
|
|
488
|
+
}
|
|
489
|
+
const orderId = readBoundedIdentifier(envelope.payload.orderId);
|
|
490
|
+
if (orderId === undefined || orderId !== request.platformTransactionId) {
|
|
491
|
+
return { status: 'rejected', reason: 'DEVVIT_ORDER_ID_MISMATCH' };
|
|
492
|
+
}
|
|
493
|
+
if (envelope.payload.status !== 'PAID') {
|
|
494
|
+
return { status: 'rejected', reason: 'DEVVIT_ORDER_NOT_PAID' };
|
|
495
|
+
}
|
|
496
|
+
let result;
|
|
497
|
+
try {
|
|
498
|
+
result = await input.authority.getOrder({
|
|
499
|
+
orderId,
|
|
500
|
+
playerId: request.playerId,
|
|
501
|
+
platformSku: platformProductId,
|
|
502
|
+
signal,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (signal.aborted) {
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
return { status: 'pending', reason: 'DEVVIT_ORDER_AUTHORITY_UNAVAILABLE' };
|
|
510
|
+
}
|
|
511
|
+
if (result.decision === 'pending') {
|
|
512
|
+
return { status: 'pending', ...(result.reason === undefined ? {} : { reason: result.reason }) };
|
|
513
|
+
}
|
|
514
|
+
if (result.decision === 'rejected') {
|
|
515
|
+
return { status: 'rejected', reason: result.reason };
|
|
516
|
+
}
|
|
517
|
+
const order = result.order;
|
|
518
|
+
if (order.orderId !== orderId) {
|
|
519
|
+
return { status: 'rejected', reason: 'DEVVIT_ORDER_ID_MISMATCH' };
|
|
520
|
+
}
|
|
521
|
+
if (order.playerId !== request.playerId) {
|
|
522
|
+
return { status: 'rejected', reason: 'DEVVIT_ORDER_PLAYER_MISMATCH' };
|
|
523
|
+
}
|
|
524
|
+
if (order.sku !== platformProductId) {
|
|
525
|
+
return { status: 'rejected', reason: 'DEVVIT_ORDER_SKU_MISMATCH' };
|
|
526
|
+
}
|
|
527
|
+
if (order.status === 'REVERTED') {
|
|
528
|
+
return { status: 'rejected', reason: 'DEVVIT_ORDER_REVERTED' };
|
|
529
|
+
}
|
|
530
|
+
if (order.status !== 'PAID' && order.status !== 'DELIVERED') {
|
|
531
|
+
return { status: 'pending', reason: 'DEVVIT_ORDER_NOT_PAID' };
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
status: 'verified',
|
|
535
|
+
verificationId: `devvit:payment-order:${orderId}`,
|
|
536
|
+
verifiedAt: now(),
|
|
537
|
+
platformEvidenceId: orderId,
|
|
538
|
+
payload: {
|
|
539
|
+
devvitOrderId: orderId,
|
|
540
|
+
devvitOrderStatus: order.status,
|
|
541
|
+
...(order.occurredAt === undefined ? {} : { devvitOrderOccurredAt: order.occurredAt }),
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/** `devvit.payment-order.v1`: the fulfillment handler's own record, bound through the payments authority. */
|
|
548
|
+
export function createDevvitPaymentOrderCommerceEvidenceEntry(input) {
|
|
549
|
+
return {
|
|
550
|
+
schema: devvitPaymentOrderEvidenceSchema,
|
|
551
|
+
store: devvitCommerceStoreId,
|
|
552
|
+
verifier: createDevvitPaymentOrderEvidenceVerifier(input),
|
|
553
|
+
finish: 'none',
|
|
554
|
+
targets: input.targets ?? referenceCommerceLedgerTargets.devvit,
|
|
555
|
+
purchase: referenceCommercePurchaseCapabilities.devvit,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* A server-push lookup verifier (RFC "Evidence registry"): it never grants
|
|
560
|
+
* from the client envelope. It answers `settled` with the ledger entry the
|
|
561
|
+
* host's fulfillment path already wrote for the same store and transaction
|
|
562
|
+
* id, when that entry belongs to the requesting player and product and is
|
|
563
|
+
* not reversed; `pending` while no such entry exists; and `rejected` when the
|
|
564
|
+
* envelope names another transaction or product than the request, or the
|
|
565
|
+
* entry belongs to someone else.
|
|
566
|
+
*/
|
|
567
|
+
export function createCommerceLedgerLookupVerifier(input) {
|
|
568
|
+
return {
|
|
569
|
+
async verifyPurchase({ request, platformProductId }) {
|
|
570
|
+
const envelope = request.evidence;
|
|
571
|
+
if (envelope?.schema !== input.schema) {
|
|
572
|
+
return { status: 'rejected', reason: 'EVIDENCE_SCHEMA_UNSUPPORTED' };
|
|
573
|
+
}
|
|
574
|
+
const transactionId = input.readTransactionId(envelope.payload);
|
|
575
|
+
if (transactionId === undefined) {
|
|
576
|
+
return { status: 'rejected', reason: 'EVIDENCE_PAYLOAD_INVALID' };
|
|
577
|
+
}
|
|
578
|
+
if (transactionId !== request.platformTransactionId) {
|
|
579
|
+
return { status: 'rejected', reason: 'PLATFORM_TRANSACTION_ID_MISMATCH' };
|
|
580
|
+
}
|
|
581
|
+
const storeSku = input.readStoreSku?.(envelope.payload);
|
|
582
|
+
if (storeSku !== undefined && storeSku !== platformProductId) {
|
|
583
|
+
return { status: 'rejected', reason: 'PLATFORM_PRODUCT_MISMATCH' };
|
|
584
|
+
}
|
|
585
|
+
const grant = await findPurchaseGrantByPlatformEvidence(input.ledger, {
|
|
586
|
+
target: input.ledgerTarget ?? request.target,
|
|
587
|
+
platformEvidenceId: transactionId,
|
|
588
|
+
});
|
|
589
|
+
if (grant === undefined) {
|
|
590
|
+
return { status: 'pending', reason: 'LEDGER_GRANT_PENDING' };
|
|
591
|
+
}
|
|
592
|
+
if (grant.playerId !== request.playerId) {
|
|
593
|
+
return { status: 'rejected', reason: 'LEDGER_GRANT_PLAYER_MISMATCH' };
|
|
594
|
+
}
|
|
595
|
+
if (grant.grantId !== request.productId) {
|
|
596
|
+
return { status: 'rejected', reason: 'LEDGER_GRANT_PRODUCT_MISMATCH' };
|
|
597
|
+
}
|
|
598
|
+
const grantStore = grant.payload.store;
|
|
599
|
+
if (typeof grantStore === 'string' && grantStore.length > 0 && grantStore !== input.store) {
|
|
600
|
+
return { status: 'rejected', reason: 'EVIDENCE_SCHEMA_STORE_MISMATCH' };
|
|
601
|
+
}
|
|
602
|
+
if (input.ledger.findEntitlementRevocations !== undefined) {
|
|
603
|
+
const revocations = await input.ledger.findEntitlementRevocations({
|
|
604
|
+
ledgerEntryIds: [grant.ledgerEntryId],
|
|
605
|
+
});
|
|
606
|
+
if (revocations.length > 0) {
|
|
607
|
+
return { status: 'rejected', reason: 'LEDGER_GRANT_REVERSED' };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
status: 'settled',
|
|
612
|
+
ledgerEntryId: grant.ledgerEntryId,
|
|
613
|
+
...(input.ledgerTarget === undefined ? {} : { ledgerTarget: input.ledgerTarget }),
|
|
614
|
+
};
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
/** The purchase grant written under a ledger target and store transaction id, through the store's index or a list scan. */
|
|
619
|
+
export async function findPurchaseGrantByPlatformEvidence(ledger, identity) {
|
|
620
|
+
if (ledger.findEntitlementTransactionByPlatformEvidence !== undefined) {
|
|
621
|
+
return ledger.findEntitlementTransactionByPlatformEvidence({
|
|
622
|
+
source: 'purchase',
|
|
623
|
+
target: identity.target,
|
|
624
|
+
platformEvidenceId: identity.platformEvidenceId,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const transactions = await ledger.listEntitlementTransactions();
|
|
628
|
+
return transactions.find((transaction) => transaction.source === 'purchase'
|
|
629
|
+
&& transaction.payload.target === identity.target
|
|
630
|
+
&& transaction.payload.platformTransactionId === identity.platformEvidenceId);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* `devvit.order.v1`: the client's order envelope (a checkout result or the
|
|
634
|
+
* paid-order list) looked up against the ledger the fulfillment handler
|
|
635
|
+
* wrote under target `reddit` (`createPlatformOrderLedgerGrant`).
|
|
636
|
+
*/
|
|
637
|
+
export function createDevvitOrderLookupCommerceEvidenceEntry(input) {
|
|
638
|
+
return {
|
|
639
|
+
schema: devvitOrderEvidenceSchema,
|
|
640
|
+
store: devvitCommerceStoreId,
|
|
641
|
+
verifier: createCommerceLedgerLookupVerifier({
|
|
642
|
+
schema: devvitOrderEvidenceSchema,
|
|
643
|
+
store: devvitCommerceStoreId,
|
|
644
|
+
ledger: input.ledger,
|
|
645
|
+
readTransactionId: (payload) => readBoundedIdentifier(payload.orderId),
|
|
646
|
+
readStoreSku: (payload) => readBoundedIdentifier(payload.sku),
|
|
647
|
+
...(input.ledgerTarget === undefined ? {} : { ledgerTarget: input.ledgerTarget }),
|
|
648
|
+
}),
|
|
649
|
+
finish: 'none',
|
|
650
|
+
targets: input.targets ?? referenceCommerceLedgerTargets.devvit,
|
|
651
|
+
purchase: referenceCommercePurchaseCapabilities.devvit,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* `verse8.agent8-event.v1`: the client's Agent8 purchase envelope looked up
|
|
656
|
+
* against the ledger the `$onItemPurchased` event wrote under target `verse8`
|
|
657
|
+
* (`createServerPushLedgerGrant`; the Agent8 commerce service writes it when
|
|
658
|
+
* given a ledger).
|
|
659
|
+
*/
|
|
660
|
+
export function createVerse8Agent8EventLookupCommerceEvidenceEntry(input) {
|
|
661
|
+
return {
|
|
662
|
+
schema: verse8Agent8EventEvidenceSchema,
|
|
663
|
+
store: verse8CommerceStoreId,
|
|
664
|
+
verifier: createCommerceLedgerLookupVerifier({
|
|
665
|
+
schema: verse8Agent8EventEvidenceSchema,
|
|
666
|
+
store: verse8CommerceStoreId,
|
|
667
|
+
ledger: input.ledger,
|
|
668
|
+
readTransactionId: (payload) => readBoundedIdentifier(payload.purchaseId),
|
|
669
|
+
readStoreSku: (payload) => readBoundedIdentifier(payload.productId),
|
|
670
|
+
...(input.ledgerTarget === undefined ? {} : { ledgerTarget: input.ledgerTarget }),
|
|
671
|
+
}),
|
|
672
|
+
finish: 'none',
|
|
673
|
+
targets: input.targets ?? referenceCommerceLedgerTargets.verse8,
|
|
674
|
+
purchase: referenceCommercePurchaseCapabilities.verse8,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* The ledger grant a host's fulfillment path writes for a server-push
|
|
679
|
+
* purchase, in the shape the lookup verifiers read: `payload.target`,
|
|
680
|
+
* `payload.store`, and `payload.platformTransactionId` identify it, and the
|
|
681
|
+
* platform-evidence unique index keeps a second write of the same transaction
|
|
682
|
+
* out. Write it with `GameServicesStore.recordEntitlementGrant`.
|
|
683
|
+
*/
|
|
684
|
+
export function createServerPushLedgerGrant(input) {
|
|
685
|
+
assertCommerceStoreId(input.store, 'store');
|
|
686
|
+
const transactionId = readBoundedIdentifier(input.transactionId);
|
|
687
|
+
if (transactionId === undefined) {
|
|
688
|
+
throw new TypeError('A server-push ledger grant needs a bounded transaction id.');
|
|
689
|
+
}
|
|
690
|
+
const evidenceVerificationId = input.evidenceVerificationId
|
|
691
|
+
?? `${input.store}:${input.evidence.schema}:${transactionId}`;
|
|
692
|
+
const quantity = input.quantity ?? 1;
|
|
693
|
+
if (!Number.isSafeInteger(quantity) || quantity <= 0) {
|
|
694
|
+
throw new TypeError('A server-push ledger grant quantity must be a positive integer.');
|
|
695
|
+
}
|
|
696
|
+
const grant = input.product.grant;
|
|
697
|
+
const scaledGrant = quantity === 1 || grant.type === 'entitlement'
|
|
698
|
+
? grant
|
|
699
|
+
: { ...grant, amount: grant.amount * quantity };
|
|
700
|
+
return {
|
|
701
|
+
playerId: input.playerId,
|
|
702
|
+
grantId: input.product.id,
|
|
703
|
+
source: 'purchase',
|
|
704
|
+
idempotencyKey: input.idempotencyKey
|
|
705
|
+
?? `store:${encodeURIComponent(input.store)}:${encodeURIComponent(transactionId)}`,
|
|
706
|
+
grantedAt: input.grantedAt,
|
|
707
|
+
grant: scaledGrant,
|
|
708
|
+
evidenceVerificationId,
|
|
709
|
+
payload: {
|
|
710
|
+
target: input.target,
|
|
711
|
+
store: input.store,
|
|
712
|
+
...(input.deploymentTarget === undefined ? {} : { deploymentTarget: input.deploymentTarget }),
|
|
713
|
+
productId: input.product.id,
|
|
714
|
+
productType: input.product.type,
|
|
715
|
+
platformProductId: input.platformProductId,
|
|
716
|
+
platformTransactionId: transactionId,
|
|
717
|
+
quantity,
|
|
718
|
+
purchasedAt: input.purchasedAt,
|
|
719
|
+
evidenceSchema: input.evidence.schema,
|
|
720
|
+
evidenceVerificationId,
|
|
721
|
+
evidenceVerifiedAt: input.grantedAt,
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Compares a provider's capability block with the backend's published
|
|
727
|
+
* capabilities (RFC "Provider capabilities endpoint"): a store the backend
|
|
728
|
+
* does not accept, a finish owner that disagrees, or another protocol major
|
|
729
|
+
* version sets the provider's availability to `configuration-required`
|
|
730
|
+
* before any purchase UI opens. The reconciler applies it at start.
|
|
731
|
+
*/
|
|
732
|
+
export function compareCommerceServiceCapabilities(input) {
|
|
733
|
+
const { store } = input;
|
|
734
|
+
if (majorVersionOf(input.service.commerceProtocolVersion) !== majorVersionOf(commerceProtocolVersion)) {
|
|
735
|
+
return {
|
|
736
|
+
status: 'configuration-required',
|
|
737
|
+
store,
|
|
738
|
+
reason: 'PROTOCOL_VERSION_MISMATCH',
|
|
739
|
+
message: `The backend speaks commerce protocol ${input.service.commerceProtocolVersion}; this client speaks ${commerceProtocolVersion}.`,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
const accepted = input.service.stores.find((candidate) => candidate.store === store);
|
|
743
|
+
if (accepted === undefined) {
|
|
744
|
+
return {
|
|
745
|
+
status: 'configuration-required',
|
|
746
|
+
store,
|
|
747
|
+
reason: 'STORE_NOT_ACCEPTED',
|
|
748
|
+
message: `The backend accepts no evidence for store ${store}.`,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (accepted.finish !== input.provider.lifecycle.finish) {
|
|
752
|
+
return {
|
|
753
|
+
status: 'configuration-required',
|
|
754
|
+
store,
|
|
755
|
+
reason: 'FINISH_OWNER_MISMATCH',
|
|
756
|
+
message: `The backend expects ${accepted.finish} to finish ${store} purchases; the provider declares ${input.provider.lifecycle.finish}.`,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
return { status: 'aligned', store };
|
|
760
|
+
}
|
|
761
|
+
function majorVersionOf(version) {
|
|
762
|
+
return version.split('.')[0] ?? version;
|
|
763
|
+
}
|
|
764
|
+
function readBoundedIdentifier(value) {
|
|
765
|
+
if (typeof value !== 'string'
|
|
766
|
+
|| value.length === 0
|
|
767
|
+
|| value.length > 256
|
|
768
|
+
|| value.trim() !== value
|
|
769
|
+
|| /[\p{Cc}\p{Cf}]/u.test(value)) {
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
return value;
|
|
773
|
+
}
|