@mpgd/platform 0.17.0 → 0.19.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/capability-conformance.js +103 -1
- package/dist/commerce-capabilities.d.ts +9 -0
- package/dist/commerce-capabilities.js +24 -0
- package/dist/commerce-conformance.d.ts +386 -0
- package/dist/commerce-conformance.js +1701 -0
- package/dist/commerce-vectors.d.ts +269 -0
- package/dist/commerce-vectors.js +3084 -0
- package/dist/commerce.d.ts +445 -0
- package/dist/commerce.js +5623 -0
- package/dist/index.d.ts +60 -0
- package/package.json +18 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deriveLegacyCommerceCapabilities } from './commerce-capabilities.js';
|
|
1
2
|
export const platformCapabilityKeys = Object.freeze([
|
|
2
3
|
'nativeIap',
|
|
3
4
|
'nativeAds',
|
|
@@ -22,6 +23,7 @@ const allowedPlatformCapabilityKeys = Object.freeze([
|
|
|
22
23
|
const allowedPlatformCapabilityKeySet = new Set([
|
|
23
24
|
...allowedPlatformCapabilityKeys,
|
|
24
25
|
'providerAvailability',
|
|
26
|
+
'commerce',
|
|
25
27
|
]);
|
|
26
28
|
const optionalPlatformCapabilityKeySet = new Set(optionalPlatformCapabilityKeys);
|
|
27
29
|
const providerFeatureSet = new Set([
|
|
@@ -41,6 +43,19 @@ const providerAvailabilitySet = new Set([
|
|
|
41
43
|
'temporarily-unavailable',
|
|
42
44
|
'available',
|
|
43
45
|
]);
|
|
46
|
+
// The optional commerce v2 block follows the same rules as the boolean
|
|
47
|
+
// snapshot: exact keys, typed values, and a fresh object on every read.
|
|
48
|
+
const commerceCapabilityBlockKeys = Object.freeze({
|
|
49
|
+
purchase: ['consumable', 'durable', 'subscription', 'quantity'],
|
|
50
|
+
catalog: ['products', 'offers', 'localizedPrice'],
|
|
51
|
+
lifecycle: ['availablePurchases', 'pending', 'restore', 'events', 'finish'],
|
|
52
|
+
authority: ['evidence', 'accountBinding'],
|
|
53
|
+
});
|
|
54
|
+
const commerceCapabilityEnums = Object.freeze({
|
|
55
|
+
'lifecycle.finish': ['client', 'server', 'host', 'none'],
|
|
56
|
+
'authority.evidence': ['client-token', 'host-callback', 'adapter-settled', 'server-push'],
|
|
57
|
+
'authority.accountBinding': ['required', 'optional', 'host'],
|
|
58
|
+
});
|
|
44
59
|
/**
|
|
45
60
|
* Runs provider-neutral capability checks against real adapter gateways or
|
|
46
61
|
* target-configured wrappers. Every read must return a complete boolean
|
|
@@ -116,6 +131,55 @@ function assertCapabilitySnapshot(actual, expected) {
|
|
|
116
131
|
}
|
|
117
132
|
assertProviderAvailabilityShape(actual.providerAvailability);
|
|
118
133
|
assertEqual(normalizedAvailability(actual.providerAvailability), normalizedAvailability(expected.providerAvailability), 'provider availability must match the expected provider state');
|
|
134
|
+
assertCommerceCapabilityBlock(actual, expected.commerce);
|
|
135
|
+
}
|
|
136
|
+
function assertCommerceCapabilityBlock(snapshot, expected) {
|
|
137
|
+
const actual = snapshot.commerce;
|
|
138
|
+
if (actual === undefined) {
|
|
139
|
+
assert(expected === undefined, 'commerce capabilities must be present when the fixture expects them');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
assert(expected !== undefined, 'commerce capabilities must be absent when the fixture omits them');
|
|
143
|
+
assert(typeof actual === 'object' && actual !== null && !Array.isArray(actual), 'commerce capabilities must be a non-array record');
|
|
144
|
+
const block = actual;
|
|
145
|
+
const blockKeys = Object.keys(block).sort();
|
|
146
|
+
assertEqual(JSON.stringify(blockKeys), JSON.stringify([...Object.keys(commerceCapabilityBlockKeys), 'availability'].sort()), 'commerce capabilities must contain exactly purchase, catalog, lifecycle, authority, and availability');
|
|
147
|
+
for (const [group, keys] of Object.entries(commerceCapabilityBlockKeys)) {
|
|
148
|
+
const nested = block[group];
|
|
149
|
+
assert(typeof nested === 'object' && nested !== null && !Array.isArray(nested), `commerce ${group} capabilities must be a non-array record`);
|
|
150
|
+
const record = nested;
|
|
151
|
+
assertEqual(JSON.stringify(Object.keys(record).sort()), JSON.stringify([...keys].sort()), `commerce ${group} capabilities must contain exactly ${keys.join(', ')}`);
|
|
152
|
+
for (const key of keys) {
|
|
153
|
+
const path = `${group}.${key}`;
|
|
154
|
+
const allowed = commerceCapabilityEnums[path];
|
|
155
|
+
if (allowed === undefined) {
|
|
156
|
+
assertEqual(typeof record[key], 'boolean', `commerce capability ${path} must be a boolean`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
assert(allowed.includes(record[key]), `commerce capability ${path} must be one of ${allowed.join(', ')}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
assert(providerAvailabilitySet.has(block.availability), 'commerce availability must be a known readiness state');
|
|
164
|
+
assertEqual(normalizedCommerce(block), normalizedCommerce(expected), 'commerce capabilities must match the expected provider state');
|
|
165
|
+
const legacy = deriveLegacyCommerceCapabilities(block);
|
|
166
|
+
assertEqual(snapshot.nativeIap, legacy.nativeIap, 'nativeIap must equal the value derived from the commerce capabilities');
|
|
167
|
+
assertEqual(snapshot.subscriptionIap ?? false, legacy.subscriptionIap, 'subscriptionIap must equal the value derived from the commerce capabilities');
|
|
168
|
+
}
|
|
169
|
+
function normalizedCommerce(value) {
|
|
170
|
+
if (value === undefined) {
|
|
171
|
+
return 'undefined';
|
|
172
|
+
}
|
|
173
|
+
return JSON.stringify([
|
|
174
|
+
['purchase', sortedEntries(value.purchase)],
|
|
175
|
+
['catalog', sortedEntries(value.catalog)],
|
|
176
|
+
['lifecycle', sortedEntries(value.lifecycle)],
|
|
177
|
+
['authority', sortedEntries(value.authority)],
|
|
178
|
+
['availability', value.availability],
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
function sortedEntries(value) {
|
|
182
|
+
return Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
119
183
|
}
|
|
120
184
|
function assertProviderAvailabilityShape(value) {
|
|
121
185
|
if (value === undefined) {
|
|
@@ -133,6 +197,17 @@ function cloneExpectedCapabilities(value) {
|
|
|
133
197
|
...(value.providerAvailability === undefined
|
|
134
198
|
? {}
|
|
135
199
|
: { providerAvailability: { ...value.providerAvailability } }),
|
|
200
|
+
...(value.commerce === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: {
|
|
203
|
+
commerce: {
|
|
204
|
+
purchase: { ...value.commerce.purchase },
|
|
205
|
+
catalog: { ...value.commerce.catalog },
|
|
206
|
+
lifecycle: { ...value.commerce.lifecycle },
|
|
207
|
+
authority: { ...value.commerce.authority },
|
|
208
|
+
availability: value.commerce.availability,
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
136
211
|
};
|
|
137
212
|
}
|
|
138
213
|
function normalizedAvailability(value) {
|
|
@@ -156,12 +231,39 @@ function tryMutateSnapshot(snapshot) {
|
|
|
156
231
|
// A frozen nested readiness record already satisfies isolation.
|
|
157
232
|
}
|
|
158
233
|
}
|
|
234
|
+
if (snapshot.commerce !== undefined) {
|
|
235
|
+
tryMutateCommerceSnapshot(snapshot.commerce);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Every nested block is probed: a gateway may rebuild the outer object while
|
|
239
|
+
// reusing one mutable nested record across reads.
|
|
240
|
+
function tryMutateCommerceSnapshot(commerce) {
|
|
241
|
+
const probes = [
|
|
242
|
+
[commerce.purchase, 'consumable', !commerce.purchase.consumable],
|
|
243
|
+
[commerce.catalog, 'products', !commerce.catalog.products],
|
|
244
|
+
[commerce.lifecycle, 'events', !commerce.lifecycle.events],
|
|
245
|
+
[
|
|
246
|
+
commerce.authority,
|
|
247
|
+
'accountBinding',
|
|
248
|
+
commerce.authority.accountBinding === 'host' ? 'optional' : 'host',
|
|
249
|
+
],
|
|
250
|
+
[commerce, 'availability', commerce.availability === 'available' ? 'unsupported' : 'available'],
|
|
251
|
+
];
|
|
252
|
+
for (const [target, key, next] of probes) {
|
|
253
|
+
try {
|
|
254
|
+
Reflect.set(target, key, next);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// A frozen nested commerce block already satisfies isolation.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
159
260
|
}
|
|
160
261
|
function hasCapabilityDifference(first, second) {
|
|
161
262
|
return allowedPlatformCapabilityKeys
|
|
162
263
|
.some((key) => (first[key] ?? false) !== (second[key] ?? false))
|
|
163
264
|
|| normalizedAvailability(first.providerAvailability)
|
|
164
|
-
!== normalizedAvailability(second.providerAvailability)
|
|
265
|
+
!== normalizedAvailability(second.providerAvailability)
|
|
266
|
+
|| normalizedCommerce(first.commerce) !== normalizedCommerce(second.commerce);
|
|
165
267
|
}
|
|
166
268
|
function assert(condition, message) {
|
|
167
269
|
if (!condition) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CommerceCapabilities } from './index.js';
|
|
2
|
+
export interface LegacyCommerceCapabilities {
|
|
3
|
+
readonly nativeIap: boolean;
|
|
4
|
+
readonly subscriptionIap: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Transition rule for the boolean `PlatformCapabilities` flags. */
|
|
7
|
+
export declare function deriveLegacyCommerceCapabilities(capabilities: CommerceCapabilities): LegacyCommerceCapabilities;
|
|
8
|
+
/** A fresh all-false snapshot; callers must not share or mutate a returned object. */
|
|
9
|
+
export declare function createUnsupportedCommerceCapabilities(): CommerceCapabilities;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Transition rule for the boolean `PlatformCapabilities` flags. */
|
|
2
|
+
export function deriveLegacyCommerceCapabilities(capabilities) {
|
|
3
|
+
return {
|
|
4
|
+
nativeIap: capabilities.availability === 'available'
|
|
5
|
+
&& (capabilities.purchase.consumable || capabilities.purchase.durable),
|
|
6
|
+
subscriptionIap: capabilities.purchase.subscription,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** A fresh all-false snapshot; callers must not share or mutate a returned object. */
|
|
10
|
+
export function createUnsupportedCommerceCapabilities() {
|
|
11
|
+
return {
|
|
12
|
+
purchase: { consumable: false, durable: false, subscription: false, quantity: false },
|
|
13
|
+
catalog: { products: false, offers: false, localizedPrice: false },
|
|
14
|
+
lifecycle: {
|
|
15
|
+
availablePurchases: false,
|
|
16
|
+
pending: false,
|
|
17
|
+
restore: false,
|
|
18
|
+
events: false,
|
|
19
|
+
finish: 'none',
|
|
20
|
+
},
|
|
21
|
+
authority: { evidence: 'client-token', accountBinding: 'optional' },
|
|
22
|
+
availability: 'unsupported',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { type ActiveSubscription, type CommerceBillingAuthority, type CommerceCapabilities, type CommerceErrorCode, type CommerceErrorInfo, type CommerceEvent, type CommerceLogicalProductId, type CommerceProvider, type CommercePurchaseState, type CreditBalance, type CreditDebit, type CreditEntitlement, type CreditLot, type CreditLotSpendScope, type CreditMembershipTerms, type CreditOffer, type CreditSpendInput, type CreditSpendResult, type CreditWalletClient, type FetchProductsInput, type PurchaseKind, type RequestPurchaseInput, type RequestPurchaseResult, type StoreId, type StoreProduct, type StorePurchase } from './commerce.js';
|
|
2
|
+
import type { CommerceConformanceCatalogEntry, CommerceConformanceErrorFixture, CommerceConformanceFinishResultFixture, CommerceConformanceLotFixture, CommerceConformanceProfile, CommerceConformancePurchaseFixture, CommerceConformanceRequestResultFixture, CommerceConformanceSubscriptionEventType, CommerceConformanceVector, CommerceConformanceVerifyDecision } from './commerce-vectors.js';
|
|
3
|
+
import type { PlatformEvidenceEnvelope } from './index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Commerce v2 conformance runner (RFC "Conformance vectors") with the in-memory
|
|
6
|
+
* reference pieces every vector needs: a fake clock, a fake store with its
|
|
7
|
+
* provider, a fake backend and ledger, an in-memory journal, the reference
|
|
8
|
+
* reconciler runtime, and a reference credit wallet with its provider. Pure
|
|
9
|
+
* in-process code: no Phaser, DOM, network, or SDK imports. Nothing here
|
|
10
|
+
* grants outside the fake ledger, and the fake ledger only writes what a
|
|
11
|
+
* vector scripts through `queueVerify`, `push`, or `revoke`.
|
|
12
|
+
*
|
|
13
|
+
* Store (B-phase) providers run the same vectors by supplying
|
|
14
|
+
* `createProvider` and translating the store actor steps into their own fake
|
|
15
|
+
* SDK; the reconciler, journal, backend, and matcher stay shared.
|
|
16
|
+
*/
|
|
17
|
+
export declare const commerceConformanceDefaultPlayerId = "player-1";
|
|
18
|
+
export declare const commerceConformanceDefaultTitleId = "title-a";
|
|
19
|
+
export declare const commerceConformanceDefaultClockStart = "2026-10-01T00:00:00Z";
|
|
20
|
+
/** An operation the vector did not script; it fails the vector with its message. */
|
|
21
|
+
export declare class CommerceConformanceScriptError extends Error {
|
|
22
|
+
readonly name = "CommerceConformanceScriptError";
|
|
23
|
+
}
|
|
24
|
+
export interface CommerceConformanceClock {
|
|
25
|
+
/** ISO-8601 timestamp of the fake "now". */
|
|
26
|
+
now(): string;
|
|
27
|
+
set(at: string): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function createCommerceConformanceClock(initial?: string): CommerceConformanceClock;
|
|
30
|
+
/** Canonical `YYYY-MM-DDTHH:mm:ssZ`; milliseconds are kept only when non-zero. */
|
|
31
|
+
export declare function normalizeIsoTimestamp(input: string): string;
|
|
32
|
+
/** Adds an ISO-8601 duration (`P30D`, `P1M`, `PT12H`, ...) to a timestamp, in UTC calendar units. */
|
|
33
|
+
export declare function addIsoDuration(at: string, duration: string): string;
|
|
34
|
+
/** Capabilities of a vector profile: omitted blocks default to all supported and `available`. */
|
|
35
|
+
export declare function createCommerceConformanceCapabilities(profile: CommerceConformanceProfile): CommerceCapabilities;
|
|
36
|
+
export interface CommerceConformanceFixtureContext {
|
|
37
|
+
readonly profile: CommerceConformanceProfile;
|
|
38
|
+
readonly catalog: readonly CommerceConformanceCatalogEntry[];
|
|
39
|
+
readonly playerId: string;
|
|
40
|
+
}
|
|
41
|
+
/** Fake evidence for a fixture purchase: `fixture.<store>.v1` carrying the token. */
|
|
42
|
+
export declare function createCommerceConformanceEvidence(store: StoreId, token: string): PlatformEvidenceEnvelope;
|
|
43
|
+
/**
|
|
44
|
+
* Fills vector purchase shorthand into a typed `StorePurchase`: the profile's
|
|
45
|
+
* store, quantity 1, the catalog logical id of the SKU, fake evidence, and the
|
|
46
|
+
* account binding the profile's authority block implies.
|
|
47
|
+
*/
|
|
48
|
+
export declare function fillCommerceConformancePurchase(fixture: CommerceConformancePurchaseFixture, context: CommerceConformanceFixtureContext): StorePurchase;
|
|
49
|
+
export declare function commerceErrorInfoOfFixture(fixture: CommerceConformanceErrorFixture): CommerceErrorInfo;
|
|
50
|
+
export interface CommerceConformanceFinishCall {
|
|
51
|
+
readonly by: 'client' | 'host';
|
|
52
|
+
readonly transactionId: string;
|
|
53
|
+
readonly ledgerEntryId: string;
|
|
54
|
+
readonly isConsumable: boolean;
|
|
55
|
+
readonly result: 'finished' | 'unfinished' | CommerceErrorCode;
|
|
56
|
+
}
|
|
57
|
+
/** The store actor of a vector: scripted answers, store-side events, and store observations. */
|
|
58
|
+
export interface CommerceConformanceStoreActor {
|
|
59
|
+
/** Calls that reached the store's purchase UI. */
|
|
60
|
+
readonly purchaseUiOpened: number;
|
|
61
|
+
readonly finishCalls: readonly CommerceConformanceFinishCall[];
|
|
62
|
+
complete(purchase: CommerceConformancePurchaseFixture): void;
|
|
63
|
+
update(purchase: CommerceConformancePurchaseFixture): void;
|
|
64
|
+
error(input: {
|
|
65
|
+
readonly logicalId?: CommerceLogicalProductId;
|
|
66
|
+
readonly error: CommerceConformanceErrorFixture;
|
|
67
|
+
}): void;
|
|
68
|
+
queueRequest(result: CommerceConformanceRequestResultFixture): void;
|
|
69
|
+
queueFinish(result: CommerceConformanceFinishResultFixture): void;
|
|
70
|
+
/** The backend's finalizer consumed the purchase (`lifecycle.finish: 'server'`). */
|
|
71
|
+
finalize?(input: {
|
|
72
|
+
readonly transactionId: string;
|
|
73
|
+
readonly ledgerEntryId: string;
|
|
74
|
+
}): void | Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
export interface CommerceConformanceStore extends CommerceConformanceStoreActor {
|
|
77
|
+
/** The `CommerceProvider` over this fake store. */
|
|
78
|
+
readonly provider: CommerceProvider;
|
|
79
|
+
/** Purchases the store still reports as unfinished. */
|
|
80
|
+
readonly purchases: readonly StorePurchase[];
|
|
81
|
+
readonly restoreCalls: number;
|
|
82
|
+
/** Removes a purchase from the unfinished list. */
|
|
83
|
+
markFinished(transactionId: string): void;
|
|
84
|
+
}
|
|
85
|
+
export type CreateCommerceConformanceStoreInput = CommerceConformanceFixtureContext;
|
|
86
|
+
/**
|
|
87
|
+
* Fake store: answers `launched` unless a `queueRequest` entry is queued,
|
|
88
|
+
* `{ finished: true }` unless a `queueFinish` entry is queued, lists reported
|
|
89
|
+
* purchases until they are finished, and buffers events emitted before the
|
|
90
|
+
* first listener with `createCommerceEventBuffer`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function createCommerceConformanceStore(input: CreateCommerceConformanceStoreInput): CommerceConformanceStore;
|
|
93
|
+
export interface CommerceVerifyRequest {
|
|
94
|
+
readonly idempotencyKey: string;
|
|
95
|
+
readonly playerId: string;
|
|
96
|
+
readonly store: StoreId;
|
|
97
|
+
readonly transactionId: string;
|
|
98
|
+
readonly token: string;
|
|
99
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
100
|
+
readonly quantity: number;
|
|
101
|
+
readonly evidence: PlatformEvidenceEnvelope;
|
|
102
|
+
}
|
|
103
|
+
export interface CommerceVerifyResponse {
|
|
104
|
+
readonly status: 'granted' | 'pending' | 'rejected';
|
|
105
|
+
readonly ledgerEntryId?: string;
|
|
106
|
+
/** Reported for `lifecycle.finish: 'server'` stores. */
|
|
107
|
+
readonly finalization?: 'pending' | 'completed';
|
|
108
|
+
readonly alreadyProcessed?: boolean;
|
|
109
|
+
}
|
|
110
|
+
export type CommerceLedgerEntry = {
|
|
111
|
+
readonly entryId: string;
|
|
112
|
+
readonly type: 'grant';
|
|
113
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
114
|
+
readonly store: StoreId;
|
|
115
|
+
readonly transactionId: string;
|
|
116
|
+
readonly idempotencyKey?: string;
|
|
117
|
+
/** Written by the host's fulfillment path rather than a verify call. */
|
|
118
|
+
readonly pushed?: boolean;
|
|
119
|
+
} | {
|
|
120
|
+
readonly entryId: string;
|
|
121
|
+
readonly type: 'revoke';
|
|
122
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
123
|
+
readonly reverses: string;
|
|
124
|
+
readonly reason: string;
|
|
125
|
+
};
|
|
126
|
+
export interface CommerceLedgerStatus {
|
|
127
|
+
readonly ledgerEntryId: string;
|
|
128
|
+
readonly status: 'active' | 'reversed' | 'unknown';
|
|
129
|
+
readonly reversedBy?: string;
|
|
130
|
+
}
|
|
131
|
+
/** What the reference runtime needs from a backend. */
|
|
132
|
+
export interface CommerceConformanceBackendClient {
|
|
133
|
+
verifyPurchase(request: CommerceVerifyRequest): Promise<CommerceVerifyResponse>;
|
|
134
|
+
getLedgerStatus(ledgerEntryIds: readonly string[]): Promise<readonly CommerceLedgerStatus[]>;
|
|
135
|
+
}
|
|
136
|
+
export interface CommerceConformanceBackend extends CommerceConformanceBackendClient {
|
|
137
|
+
readonly verifyRequests: readonly CommerceVerifyRequest[];
|
|
138
|
+
readonly ledger: readonly CommerceLedgerEntry[];
|
|
139
|
+
queueVerify(decision: CommerceConformanceVerifyDecision): void;
|
|
140
|
+
/** Writes a server-push grant keyed by transaction id; a retry for the same transaction replays the original entry. */
|
|
141
|
+
push(input: {
|
|
142
|
+
readonly transactionId: string;
|
|
143
|
+
readonly entryId: string;
|
|
144
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
145
|
+
readonly store?: StoreId;
|
|
146
|
+
}): void;
|
|
147
|
+
revoke(input: {
|
|
148
|
+
readonly ledgerEntryId: string;
|
|
149
|
+
readonly entryId: string;
|
|
150
|
+
readonly reason: string;
|
|
151
|
+
}): void;
|
|
152
|
+
/** Grants the ledger still holds active. */
|
|
153
|
+
entitlements(): readonly {
|
|
154
|
+
readonly id: string;
|
|
155
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
156
|
+
}[];
|
|
157
|
+
}
|
|
158
|
+
export interface CreateCommerceConformanceBackendInput {
|
|
159
|
+
/** The server-side finalizer, invoked when a decision reports `finalization: 'completed'`. */
|
|
160
|
+
readonly finalize?: (input: {
|
|
161
|
+
readonly store: StoreId;
|
|
162
|
+
readonly transactionId: string;
|
|
163
|
+
readonly ledgerEntryId: string;
|
|
164
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
165
|
+
}) => void | Promise<void>;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Fake backend: every verify call consumes the next `queueVerify` decision in
|
|
169
|
+
* order and is logged; an unqueued verify is an unscripted operation. A
|
|
170
|
+
* `granted` decision makes sure the ledger holds the entry, so a replayed
|
|
171
|
+
* grant and a host-pushed grant never create a second entry.
|
|
172
|
+
*/
|
|
173
|
+
export declare function createCommerceConformanceBackend(input?: CreateCommerceConformanceBackendInput): CommerceConformanceBackend;
|
|
174
|
+
export interface CommerceJournalRecord {
|
|
175
|
+
/** The purchase idempotency key: the game's key or a derived key. */
|
|
176
|
+
readonly key: string;
|
|
177
|
+
readonly revision: number;
|
|
178
|
+
readonly playerId: string;
|
|
179
|
+
readonly store: StoreId;
|
|
180
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
181
|
+
readonly kind: PurchaseKind;
|
|
182
|
+
readonly origin: 'game' | 'discovered';
|
|
183
|
+
readonly state: CommercePurchaseState;
|
|
184
|
+
readonly updatedAt: string;
|
|
185
|
+
readonly purchase?: StorePurchase;
|
|
186
|
+
/** Saved before the first send and resent unchanged on later passes. */
|
|
187
|
+
readonly request?: CommerceVerifyRequest;
|
|
188
|
+
readonly ledgerEntryId?: string;
|
|
189
|
+
readonly error?: CommerceErrorInfo;
|
|
190
|
+
readonly revokedBy?: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Purchase journal with the compare-and-replace semantics of
|
|
194
|
+
* `MonetizationOperationStore`. A game supplies a durable, atomic
|
|
195
|
+
* implementation; the in-memory one below exists for conformance runs.
|
|
196
|
+
*/
|
|
197
|
+
export interface CommerceJournalStore {
|
|
198
|
+
/** Atomically insert or return the existing record for this key. */
|
|
199
|
+
reserve(record: CommerceJournalRecord): Promise<{
|
|
200
|
+
readonly created: boolean;
|
|
201
|
+
readonly record: CommerceJournalRecord;
|
|
202
|
+
}>;
|
|
203
|
+
read(key: string): Promise<CommerceJournalRecord | undefined>;
|
|
204
|
+
/** Compare-and-swap on `expectedRevision`; the stored record carries the next revision. */
|
|
205
|
+
replace(expectedRevision: number, record: CommerceJournalRecord): Promise<CommerceJournalRecord>;
|
|
206
|
+
/** Non-terminal records; terminal records stay addressable by `read`. */
|
|
207
|
+
listRecoverable(playerId: string): Promise<readonly CommerceJournalRecord[]>;
|
|
208
|
+
/** Records in `granted`, `finishing`, or `finished` kept for revocation reporting. */
|
|
209
|
+
listGranted(playerId: string): Promise<readonly CommerceJournalRecord[]>;
|
|
210
|
+
}
|
|
211
|
+
export interface CommerceConformanceJournal extends CommerceJournalStore {
|
|
212
|
+
readonly keys: readonly string[];
|
|
213
|
+
/** Every state revision of a key, in write order. */
|
|
214
|
+
history(key: string): readonly CommercePurchaseState[];
|
|
215
|
+
/** `history` for every key, in creation order. */
|
|
216
|
+
snapshot(): Readonly<Record<string, readonly CommercePurchaseState[]>>;
|
|
217
|
+
}
|
|
218
|
+
/** In-memory journal that keeps every revision so `expect.journal` can read the full sequence. */
|
|
219
|
+
export declare function createCommerceConformanceJournal(): CommerceConformanceJournal;
|
|
220
|
+
export interface CommerceConformanceRuntime {
|
|
221
|
+
readonly started: boolean;
|
|
222
|
+
/** Subscribe, read available purchases when declared, then reconcile until idle. */
|
|
223
|
+
start(): Promise<void>;
|
|
224
|
+
/** Drop in-memory state; the journal, store, ledger, and wallet are kept. */
|
|
225
|
+
stop(): Promise<void>;
|
|
226
|
+
/** One reconcile pass, then any passes that events arriving meanwhile trigger. */
|
|
227
|
+
reconcile(): Promise<void>;
|
|
228
|
+
/** Reconcile while a trigger is pending; a no-op when stopped. */
|
|
229
|
+
runUntilIdle(): Promise<void>;
|
|
230
|
+
requestPurchase(input: RequestPurchaseInput): Promise<RequestPurchaseResult>;
|
|
231
|
+
fetchProducts(input?: FetchProductsInput): Promise<readonly StoreProduct[]>;
|
|
232
|
+
}
|
|
233
|
+
export interface CreateCommerceConformanceRuntimeInput {
|
|
234
|
+
readonly provider: CommerceProvider;
|
|
235
|
+
readonly backend: CommerceConformanceBackendClient;
|
|
236
|
+
readonly journal: CommerceJournalStore;
|
|
237
|
+
readonly clock: CommerceConformanceClock;
|
|
238
|
+
readonly catalog: readonly CommerceConformanceCatalogEntry[];
|
|
239
|
+
readonly playerId: string;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Reference reconciler. Every transition is its own journal revision written
|
|
243
|
+
* through compare-and-replace; the backend is in-process, so there is no
|
|
244
|
+
* transport, but a thrown finish or verify keeps the state for the next pass
|
|
245
|
+
* as a transport failure would. Only a ledger entry moves a record to
|
|
246
|
+
* `granted`, and only a ledger revocation moves it to `revoked`.
|
|
247
|
+
*/
|
|
248
|
+
export declare function createCommerceConformanceRuntime(input: CreateCommerceConformanceRuntimeInput): CommerceConformanceRuntime;
|
|
249
|
+
export interface CommerceScheduledDebit extends CreditDebit {
|
|
250
|
+
readonly idempotencyKey: string;
|
|
251
|
+
}
|
|
252
|
+
export interface CommerceConformanceSubscriptionEvent {
|
|
253
|
+
/** Stable id `<subscriptionId>:<periodIndex>:<type>`, so redelivery is idempotent. */
|
|
254
|
+
readonly eventId: string;
|
|
255
|
+
readonly type: CommerceConformanceSubscriptionEventType;
|
|
256
|
+
readonly subscriptionId: string;
|
|
257
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
258
|
+
readonly periodIndex: number;
|
|
259
|
+
readonly expiresAt: string;
|
|
260
|
+
readonly reason?: CommerceErrorCode;
|
|
261
|
+
}
|
|
262
|
+
export interface CommerceConformanceMembershipStart {
|
|
263
|
+
readonly subscriptionId: string;
|
|
264
|
+
readonly titleId: string;
|
|
265
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
266
|
+
readonly terms: CreditMembershipTerms;
|
|
267
|
+
}
|
|
268
|
+
export type CommerceConformanceMembershipResult = {
|
|
269
|
+
readonly status: 'started';
|
|
270
|
+
readonly subscription: ActiveSubscription;
|
|
271
|
+
readonly entitlementId: string;
|
|
272
|
+
} | {
|
|
273
|
+
readonly status: 'rejected';
|
|
274
|
+
readonly error: CommerceErrorInfo;
|
|
275
|
+
};
|
|
276
|
+
/** The wallet actor of a vector: wallet steps plus wallet observations. */
|
|
277
|
+
export interface CommerceConformanceWalletActor {
|
|
278
|
+
readonly lots: readonly CreditLot[];
|
|
279
|
+
/** Every debit of every processed spend, in order; replays add nothing. */
|
|
280
|
+
readonly debits: readonly CommerceScheduledDebit[];
|
|
281
|
+
readonly subscriptionEvents: readonly CommerceConformanceSubscriptionEvent[];
|
|
282
|
+
creditFromGrant(lot: CommerceConformanceLotFixture, options?: {
|
|
283
|
+
readonly spendScope?: CreditLotSpendScope;
|
|
284
|
+
}): CreditLot;
|
|
285
|
+
spend(input: CreditSpendInput): Promise<CreditSpendResult>;
|
|
286
|
+
/** Runs the renewal scheduler for memberships whose paid period has ended. */
|
|
287
|
+
renewDue(): Promise<void>;
|
|
288
|
+
getBalance(input: {
|
|
289
|
+
readonly titleId: string;
|
|
290
|
+
}): Promise<CreditBalance>;
|
|
291
|
+
getActiveSubscriptions(): Promise<readonly ActiveSubscription[]>;
|
|
292
|
+
/** The backend's finalizer redeemed the entitlement (`lifecycle.finish: 'server'`). */
|
|
293
|
+
finalize?(input: {
|
|
294
|
+
readonly transactionId: string;
|
|
295
|
+
readonly ledgerEntryId: string;
|
|
296
|
+
}): void | Promise<void>;
|
|
297
|
+
}
|
|
298
|
+
export interface CommerceConformanceWallet extends CreditWalletClient, CommerceConformanceWalletActor {
|
|
299
|
+
readonly entitlements: readonly CreditEntitlement[];
|
|
300
|
+
startMembership(input: CommerceConformanceMembershipStart): Promise<CommerceConformanceMembershipResult>;
|
|
301
|
+
subscribe(listener: (event: CommerceEvent) => void): () => void;
|
|
302
|
+
}
|
|
303
|
+
export interface CreateCommerceConformanceWalletInput {
|
|
304
|
+
readonly store: StoreId;
|
|
305
|
+
readonly clock: CommerceConformanceClock;
|
|
306
|
+
readonly currencies: readonly string[];
|
|
307
|
+
/** Offers served by `listOffers`; defaults to none. */
|
|
308
|
+
readonly offers?: readonly CreditOffer[];
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Reference in-memory wallet implementing the RFC "Credit lots" and "Billing
|
|
312
|
+
* authority and memberships" rules: lot ids equal ledger entry ids, spend
|
|
313
|
+
* scope derives from the lot origin, spends consume eligible lots in the
|
|
314
|
+
* fixed order, are idempotent by key, and debit nothing on
|
|
315
|
+
* `INSUFFICIENT_BALANCE`; memberships debit `membership:<id>:<period>` per
|
|
316
|
+
* paid period and lapse at period end when the eligible balance runs out.
|
|
317
|
+
*/
|
|
318
|
+
export declare function createCommerceConformanceWallet(input: CreateCommerceConformanceWalletInput): CommerceConformanceWallet;
|
|
319
|
+
/** Store lots are title-scoped, off-store lots cross-title, promotion lots as issued (title by default). */
|
|
320
|
+
export declare function deriveCreditLotSpendScope(origin: CreditLot['origin'], explicit?: CreditLotSpendScope): CreditLotSpendScope;
|
|
321
|
+
/** RFC spend order: earliest `expiresAt` (none last), `title` before `cross-title`, oldest `createdAt`, then `lotId`. */
|
|
322
|
+
export declare function compareCreditLotSpendOrder(left: CreditLot, right: CreditLot): number;
|
|
323
|
+
export interface CreateCommerceConformanceWalletProviderInput {
|
|
324
|
+
readonly wallet: CommerceConformanceWallet;
|
|
325
|
+
readonly profile: CommerceConformanceProfile;
|
|
326
|
+
readonly catalog: readonly CommerceConformanceCatalogEntry[];
|
|
327
|
+
readonly currencies: readonly string[];
|
|
328
|
+
readonly billingAuthority: CommerceBillingAuthority;
|
|
329
|
+
readonly titleId: string;
|
|
330
|
+
readonly clock: CommerceConformanceClock;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* `CommerceProvider` over the reference wallet: `fetchProducts` lists the
|
|
334
|
+
* wallet's offers (catalog-derived offers only when the wallet lists none),
|
|
335
|
+
* `requestPurchase` spends the offer price times the requested quantity
|
|
336
|
+
* (outcome `purchased`, `settlement` set, evidence adapter-settled),
|
|
337
|
+
* `getAvailablePurchases` lists unredeemed entitlements, and finishing is the
|
|
338
|
+
* backend's `redeem`. Memberships exist only where the billing authority is
|
|
339
|
+
* `host`.
|
|
340
|
+
*/
|
|
341
|
+
export declare function createCommerceConformanceWalletProvider(input: CreateCommerceConformanceWalletProviderInput): CommerceProvider;
|
|
342
|
+
export interface CommerceConformanceHarness {
|
|
343
|
+
readonly provider: CommerceProvider;
|
|
344
|
+
/** Handles `store` steps; required by store vectors. */
|
|
345
|
+
readonly store?: CommerceConformanceStoreActor;
|
|
346
|
+
/** Handles `wallet` steps; required by wallet vectors. */
|
|
347
|
+
readonly wallet?: CommerceConformanceWalletActor;
|
|
348
|
+
}
|
|
349
|
+
export interface CommerceConformanceContext extends CommerceConformanceFixtureContext {
|
|
350
|
+
readonly vector: CommerceConformanceVector;
|
|
351
|
+
readonly titleId: string;
|
|
352
|
+
readonly clock: CommerceConformanceClock;
|
|
353
|
+
readonly backend: CommerceConformanceBackend;
|
|
354
|
+
readonly journal: CommerceConformanceJournal;
|
|
355
|
+
}
|
|
356
|
+
export interface CommerceConformanceRunOptions {
|
|
357
|
+
/** Supplies a provider over the caller's fake SDK; defaults to the fake store or the reference wallet. */
|
|
358
|
+
readonly createProvider?: (context: CommerceConformanceContext) => CommerceConformanceHarness | Promise<CommerceConformanceHarness>;
|
|
359
|
+
readonly playerId?: string;
|
|
360
|
+
readonly titleId?: string;
|
|
361
|
+
}
|
|
362
|
+
export interface CommerceConformanceFailure {
|
|
363
|
+
readonly path: string;
|
|
364
|
+
readonly expected: unknown;
|
|
365
|
+
readonly actual: unknown;
|
|
366
|
+
}
|
|
367
|
+
export interface CommerceConformanceVectorResult {
|
|
368
|
+
readonly vector: string;
|
|
369
|
+
readonly passed: boolean;
|
|
370
|
+
readonly failures: readonly CommerceConformanceFailure[];
|
|
371
|
+
}
|
|
372
|
+
export declare function createCommerceConformanceHarness(context: CommerceConformanceContext): CommerceConformanceHarness;
|
|
373
|
+
/**
|
|
374
|
+
* Runs one vector: executes every step, lets the runtime run until idle after
|
|
375
|
+
* each, then matches `expect`. Matching compares only the listed fields; arrays
|
|
376
|
+
* compare in order and by length; a listed `journal` is the whole journal.
|
|
377
|
+
* Any unscripted operation fails the vector at the step that needed it.
|
|
378
|
+
*/
|
|
379
|
+
export declare function runCommerceConformanceVector(vector: CommerceConformanceVector, options?: CommerceConformanceRunOptions): Promise<CommerceConformanceVectorResult>;
|
|
380
|
+
export declare function runCommerceConformanceVectors(vectors: readonly CommerceConformanceVector[], options?: CommerceConformanceRunOptions): Promise<readonly CommerceConformanceVectorResult[]>;
|
|
381
|
+
/**
|
|
382
|
+
* Partial match: an expected object asserts only its listed keys, an expected
|
|
383
|
+
* array asserts its length and each element in order, and primitives compare
|
|
384
|
+
* strictly. Failures name the deepest differing path.
|
|
385
|
+
*/
|
|
386
|
+
export declare function matchPartial(expected: unknown, actual: unknown, path: string, failures: CommerceConformanceFailure[]): void;
|