@mpgd/platform 0.20.0 → 0.22.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.
@@ -0,0 +1,324 @@
1
+ import { type tags } from 'typia';
2
+ import { type ActiveSubscription, type CommerceAuthorityCapabilities, type CommerceBillingAuthority, type CommerceCapabilities, type CommerceCatalogCapabilities, type CommerceErrorCode, type CommerceErrorInfo, type CommerceEvent, type CommerceLifecycleCapabilities, type CommerceLogicalProductId, type CommerceProvider, type CommercePurchaseCapabilities, type CreditDebit, type CreditEntitlement, type CreditLot, type CreditLotOrigin, type CreditLotSpendScope, type CreditMembershipScope, type CreditMembershipTerms, type CreditOffer, type CreditSpendResult, type CreditWalletClient, type CreditWalletSubscribeInput, type StoreId } from './commerce.js';
3
+ import type { IsoTimestamp, NonEmptyString, NonNegativeInteger } from './commerce-contract.js';
4
+ import type { PlatformEvidenceEnvelope, PlatformProviderAvailability } from './index.js';
5
+ /**
6
+ * Commerce v2 credit wallet contract (RFC "Credit wallet", "Credit lots",
7
+ * "Billing authority and memberships", "Subscriptions"). The consuming
8
+ * project implements `CreditWalletClient` against its own wallet service;
9
+ * this module supplies the rules both sides share, the provider that turns a
10
+ * client into a `CommerceProvider`, and a reference in-memory wallet that the
11
+ * conformance runner and consumer tests can run the wallet vectors against.
12
+ * Pure in-process code: no Phaser, DOM, network, or SDK imports. Nothing here
13
+ * grants: a spend debits the wallet's own lots, the entitlement it creates is
14
+ * a pointer the reconciler still verifies with the ledger, and a lot is
15
+ * credited only from a ledger grant keyed by its entry id.
16
+ */
17
+ export type { CommerceBillingAuthority, CreditBalance, CreditDebit, CreditEntitlement, CreditLot, CreditLotChannel, CreditLotOrigin, CreditLotSpendScope, CreditMembershipScope, CreditMembershipTerms, CreditOffer, CreditPrice, CreditSpendInput, CreditSpendResult, CreditWalletClient, CreditWalletSubscribeInput, } from './commerce.js';
18
+ export { assertCreditBalance, assertCreditEntitlement, assertCreditLot, assertCreditOffer, assertCreditSpendInput, assertCreditSpendResult, } from './commerce.js';
19
+ /** Canonical `YYYY-MM-DDTHH:mm:ssZ`; milliseconds are kept only when non-zero. */
20
+ export declare function normalizeIsoTimestamp(input: string): string;
21
+ /** The integer components of an ISO-8601 duration (`P1Y2M3W4DT5H6M7S`). */
22
+ export interface IsoDurationParts {
23
+ readonly years: number;
24
+ readonly months: number;
25
+ readonly weeks: number;
26
+ readonly days: number;
27
+ readonly hours: number;
28
+ readonly minutes: number;
29
+ readonly seconds: number;
30
+ }
31
+ /**
32
+ * Parses a positive ISO-8601 duration a membership period accepts (`P30D`,
33
+ * `P1M`, `PT12H`, ...). Zero-length durations (`P0D`, `PT0S`), a bare `P` or
34
+ * `PT`, and a `T` with no time components are rejected, so a period can
35
+ * never renew without time passing.
36
+ */
37
+ export declare function parseIsoDuration(duration: string): IsoDurationParts | undefined;
38
+ export declare function isIsoDuration(duration: string): boolean;
39
+ /**
40
+ * Adds `times` ISO-8601 durations to a timestamp in UTC. Years and months are
41
+ * calendar months with the day clamped to the target month's length
42
+ * (`2026-01-31` + `P1M` is `2026-02-28`), so a membership's periods are
43
+ * anchored on its start date: period `n` ends at `startedAt + (n + 1) × period`
44
+ * rather than drifting from the previous period's end.
45
+ */
46
+ export declare function addIsoDuration(at: string, duration: string, times?: number): string;
47
+ /** Profiles whose distribution requires store billing for digital goods; their authority is always `store`. */
48
+ export declare const storeBillingAuthorityProfiles: readonly ["google-play", "app-store"];
49
+ /** Profiles whose authority defaults to `host`; every other profile defaults to `store`. */
50
+ export declare const hostBillingAuthorityProfiles: readonly ["web", "web-preview"];
51
+ export declare function isCommerceBillingAuthority(value: unknown): value is CommerceBillingAuthority;
52
+ /** The RFC default for a release profile before any project override. */
53
+ export declare function defaultCommerceBillingAuthority(profile: string): CommerceBillingAuthority;
54
+ /**
55
+ * Resolves a target's billing authority from its release profile. `google-play`
56
+ * and `app-store` are always `store` and reject a `host` override (D-02: both
57
+ * stores require their own billing for digital entitlements). `web` and
58
+ * `web-preview` default to `host`, every other profile to `store`, and a
59
+ * project may override those after its own policy review.
60
+ */
61
+ export declare function resolveCommerceBillingAuthority(profile: string, override?: CommerceBillingAuthority): CommerceBillingAuthority;
62
+ export type CreditScope = 'title' | 'cross-title';
63
+ /**
64
+ * `creditScope` per store id: which stores count as off-store, so their lots
65
+ * are spendable across titles. Absent entries are `title`; the kit's reference
66
+ * stores keep that default.
67
+ */
68
+ export type CreditScopeRegistry = Readonly<Record<string, CreditScope>>;
69
+ /** Stores whose lots are always title-scoped; neither a registry nor a promotion can make them `cross-title`. */
70
+ export declare const titleScopedCreditStores: readonly ["google-play", "app-store"];
71
+ export declare function isTitleScopedCreditStore(store: string): boolean;
72
+ /** Rejects a registry that marks Google Play or the App Store `cross-title`, or holds an unknown scope. */
73
+ export declare function assertCreditScopeRegistry(registry: CreditScopeRegistry): CreditScopeRegistry;
74
+ export declare const assertCreditLotSpendScope: (input: unknown) => CreditLotSpendScope;
75
+ export interface DeriveCreditLotSpendScopeOptions {
76
+ /** Scope a promotion lot was issued with; rejected for store and off-store lots, whose scope is derived. */
77
+ readonly explicit?: CreditLotSpendScope;
78
+ readonly creditScope?: CreditScopeRegistry;
79
+ }
80
+ /**
81
+ * Derives a lot's `spendScope` from its origin (RFC "Credit lots"): a lot
82
+ * bought through a store is `title`-scoped to its purchasing title, an
83
+ * off-store lot is `cross-title`, and a promotion lot carries the scope it was
84
+ * issued with (`title` by default). A registry entry overrides the channel for
85
+ * store and off-store lots. A lot attributed to Google Play or the App Store is
86
+ * always `title`, whatever the channel, registry, or issued scope says.
87
+ */
88
+ export declare function deriveCreditLotSpendScope(origin: CreditLotOrigin, options?: DeriveCreditLotSpendScopeOptions): CreditLotSpendScope;
89
+ /** RFC spend order: earliest `expiresAt` (none last), `title` before `cross-title`, oldest `createdAt`, then `lotId`. */
90
+ export declare function compareCreditLotSpendOrder(left: CreditLot, right: CreditLot): number;
91
+ export interface CreditLotEligibility {
92
+ readonly titleId: string;
93
+ readonly currency: string;
94
+ /**
95
+ * `title` (the default) spends that title's lots and `cross-title` lots;
96
+ * `platform` spends `cross-title` lots only, as a platform-scoped membership
97
+ * does.
98
+ */
99
+ readonly scope?: CreditMembershipScope;
100
+ /** ISO-8601 timestamp or epoch milliseconds; a lot whose `expiresAt` is not after it is excluded. */
101
+ readonly now: string | number;
102
+ /**
103
+ * When set, a lot created after this instant is excluded. A catch-up
104
+ * renewal passes the boundary of the period it pays for, so credits granted
105
+ * after that period ended cannot fund it retroactively.
106
+ */
107
+ readonly createdBy?: string | number;
108
+ }
109
+ /** Whether a lot may fund a spend: same currency, credit left, not expired, and in scope. */
110
+ export declare function isCreditLotEligible(lot: CreditLot, criteria: CreditLotEligibility): boolean;
111
+ /** The eligible lots of a spend in the order they are consumed; the input is not mutated. */
112
+ export declare function selectEligibleCreditLots(lots: readonly CreditLot[], criteria: CreditLotEligibility): readonly CreditLot[];
113
+ export declare function sumCreditLotRemaining(lots: readonly CreditLot[]): number;
114
+ export declare function sumCreditDebits(debits: readonly CreditDebit[]): number;
115
+ export declare function sameCreditAmount(left: number, right: number): boolean;
116
+ /** The whole number of `unit` prices in `amount`, or `undefined` when `amount` is not a positive multiple. */
117
+ export declare function creditPriceMultiple(amount: number, unit: number): number | undefined;
118
+ export type CreditDebitPlan = {
119
+ readonly status: 'debited';
120
+ readonly debits: readonly CreditDebit[];
121
+ } | {
122
+ readonly status: 'rejected';
123
+ readonly error: CommerceErrorInfo;
124
+ };
125
+ /**
126
+ * Plans the debits of `amount` across eligible lots in spend order without
127
+ * mutating them. A spend the lots cannot cover is rejected with
128
+ * `INSUFFICIENT_BALANCE` and plans nothing.
129
+ */
130
+ export declare function planCreditDebits(eligible: readonly CreditLot[], amount: number): CreditDebitPlan;
131
+ /** Prefix of every scheduled membership debit key; a client spend cannot use it. */
132
+ export declare const membershipDebitKeyPrefix = "membership:";
133
+ /** `membership:<subscriptionId>:<periodIndex>`, so a repeated scheduler tick cannot charge a period twice. */
134
+ export declare function createMembershipRenewalKey(subscriptionId: string, periodIndex: number): string;
135
+ export type CreditSubscriptionEventType = 'subscriptionStarted' | 'subscriptionRenewed' | 'subscriptionLapsed' | 'subscriptionRevoked';
136
+ export declare const creditSubscriptionEventTypes: readonly ["subscriptionStarted", "subscriptionRenewed", "subscriptionLapsed", "subscriptionRevoked"];
137
+ /** `<subscriptionId>:<periodIndex>:<type>`, so redelivery of a lifecycle event is idempotent. */
138
+ export declare function createCreditSubscriptionEventId(subscriptionId: string, periodIndex: number, type: CreditSubscriptionEventType): string;
139
+ /** A membership lifecycle event as the wallet service records and pushes it. */
140
+ export interface CreditSubscriptionEvent {
141
+ readonly eventId: NonEmptyString;
142
+ readonly type: CreditSubscriptionEventType;
143
+ readonly subscriptionId: NonEmptyString;
144
+ readonly logicalId: CommerceLogicalProductId;
145
+ readonly periodIndex: NonNegativeInteger;
146
+ readonly expiresAt: IsoTimestamp;
147
+ /** Why a membership lapsed, for example INSUFFICIENT_BALANCE. */
148
+ readonly reason?: CommerceErrorCode;
149
+ }
150
+ /** A ledger grant the wallet owner credits as a lot; `ledgerEntryId` becomes the `lotId`. */
151
+ export interface CreditLotGrant {
152
+ readonly ledgerEntryId: NonEmptyString;
153
+ readonly currency: NonEmptyString;
154
+ readonly amount: number & tags.ExclusiveMinimum<0>;
155
+ readonly origin: CreditLotOrigin;
156
+ readonly createdAt: IsoTimestamp;
157
+ readonly expiresAt?: IsoTimestamp;
158
+ }
159
+ /** A debit with the idempotency key of the spend that made it. */
160
+ export interface CreditScheduledDebit extends CreditDebit {
161
+ readonly idempotencyKey: NonEmptyString;
162
+ }
163
+ export interface CreditRedeemInput {
164
+ readonly entitlementId: NonEmptyString;
165
+ /** The ledger entry that granted the entitlement; a redeem without one is refused. */
166
+ readonly ledgerEntryId: NonEmptyString;
167
+ }
168
+ export interface CreditMembershipStartInput {
169
+ readonly subscriptionId: NonEmptyString;
170
+ readonly titleId: NonEmptyString;
171
+ readonly logicalId: CommerceLogicalProductId;
172
+ readonly terms: CreditMembershipTerms;
173
+ }
174
+ export type CreditMembershipStartResult = {
175
+ readonly status: 'started';
176
+ readonly subscription: ActiveSubscription;
177
+ /** The period-0 entitlement, `membership:<subscriptionId>:0`. */
178
+ readonly entitlementId: NonEmptyString;
179
+ readonly debits: readonly CreditDebit[];
180
+ readonly createdAt: IsoTimestamp;
181
+ /** The id was already started with these terms; the original period-0 debits are returned. */
182
+ readonly alreadyProcessed?: boolean;
183
+ } | {
184
+ readonly status: 'rejected';
185
+ readonly error: CommerceErrorInfo;
186
+ };
187
+ /** The `CreditSpendResult` a membership start answers a `spend` with. */
188
+ export declare function creditSpendResultOfMembershipStart(result: CreditMembershipStartResult): CreditSpendResult;
189
+ export declare const assertCreditLotGrant: (input: unknown) => CreditLotGrant;
190
+ export declare const assertCreditMembershipTerms: (input: unknown) => CreditMembershipTerms;
191
+ export declare const assertCreditRedeemInput: (input: unknown) => CreditRedeemInput;
192
+ export declare const assertCreditSubscriptionEvent: (input: unknown) => CreditSubscriptionEvent;
193
+ export declare const assertCreditMembershipStartInput: (input: unknown) => CreditMembershipStartInput;
194
+ /**
195
+ * `assertCreditOffer` plus the wallet rules an offer must satisfy: membership
196
+ * terms only on a `subscription` offer, every `subscription` offer with terms,
197
+ * a valid positive period, and a membership priced as its terms say.
198
+ */
199
+ export declare function assertCreditWalletOffer(input: unknown): CreditOffer;
200
+ export declare const creditWalletSpendEvidenceSchema: 'credit-wallet.spend.v1';
201
+ export interface CreditWalletSpendEvidence {
202
+ readonly store: StoreId;
203
+ readonly titleId: string;
204
+ /** The wallet's own id for the settled spend (its entitlement id); not a backend ledger entry. */
205
+ readonly entitlementId: string;
206
+ readonly amount: number;
207
+ readonly debits: readonly CreditDebit[];
208
+ }
209
+ /** The `adapter-settled` evidence of a wallet spend; `debits` is JSON because envelope payloads are flat. */
210
+ export declare function createCreditWalletSpendEvidence(evidence: CreditWalletSpendEvidence): PlatformEvidenceEnvelope;
211
+ /** Reads a `credit-wallet.spend.v1` envelope; any other schema or a malformed payload is `undefined`. */
212
+ export declare function readCreditWalletSpendEvidence(envelope: PlatformEvidenceEnvelope): CreditWalletSpendEvidence | undefined;
213
+ export interface CreditCommerceCapabilityOverrides {
214
+ readonly purchase?: Partial<CommercePurchaseCapabilities>;
215
+ readonly catalog?: Partial<CommerceCatalogCapabilities>;
216
+ readonly lifecycle?: Partial<CommerceLifecycleCapabilities>;
217
+ readonly authority?: Partial<CommerceAuthorityCapabilities>;
218
+ readonly availability?: PlatformProviderAvailability;
219
+ }
220
+ export interface CreateCreditCommerceProviderOptions {
221
+ readonly titleId: string;
222
+ readonly billingAuthority: CommerceBillingAuthority;
223
+ /**
224
+ * The catalog's declared currencies. An offer priced in another currency is
225
+ * a configuration error (RFC "Catalog currencies"); omitted, no check runs.
226
+ */
227
+ readonly currencies?: readonly string[];
228
+ /**
229
+ * Overrides on the RFC wallet profile. `purchase.subscription` is forced
230
+ * `false` where the billing authority is `store` or the client cannot
231
+ * report active subscriptions, whatever the override.
232
+ */
233
+ readonly capabilities?: CreditCommerceCapabilityOverrides;
234
+ /** Clock for `purchasedAt` when the wallet reports no settlement time; defaults to the system clock. */
235
+ readonly now?: () => string;
236
+ }
237
+ /** A `CommerceProvider` over a wallet client that also releases its client subscription. */
238
+ export interface CreditCommerceProvider extends CommerceProvider {
239
+ /** Unsubscribes from the client; events after this are no longer forwarded. */
240
+ dispose(): void;
241
+ }
242
+ /**
243
+ * The RFC profile row of a project-owned wallet, before overrides.
244
+ * `purchase.subscription` is true only where the billing authority is `host`
245
+ * and the caller states that the client reports active subscriptions
246
+ * (`client.subscriptions: true`); with no client description it is `false`.
247
+ */
248
+ export declare function createCreditCommerceCapabilities(billingAuthority: CommerceBillingAuthority, overrides?: CreditCommerceCapabilityOverrides, client?: {
249
+ readonly subscriptions?: boolean;
250
+ }): CommerceCapabilities;
251
+ /**
252
+ * `CommerceProvider` over a project-owned `CreditWalletClient` (RFC "Credit
253
+ * wallet"): `fetchProducts` lists the wallet's offers (memberships omitted
254
+ * where the billing authority is `store`), `requestPurchase` spends the offer
255
+ * price times the quantity under the game's idempotency key and answers
256
+ * `purchased` with `adapter-settled` evidence (`credit-wallet.spend.v1`)
257
+ * once the wallet's debits add up to that price, without a `settlement`
258
+ * pointer: the backend ledger entry exists only after the reconciler verifies
259
+ * that evidence, so there is nothing to point at yet,
260
+ * `getAvailablePurchases` lists the title's unredeemed entitlements,
261
+ * `finishTransaction` redeems one with the ledger entry id of its grant and
262
+ * refuses to run without it, `restorePurchases` re-reads the unredeemed
263
+ * entitlements as `purchaseUpdated` events, and the client's own events pass
264
+ * through the standard pre-listener buffer. Every client answer is
265
+ * validated; a malformed one is `INVALID_RESPONSE`. The provider never
266
+ * grants and never credits: lots are credited by the wallet owner from
267
+ * ledger grants, and memberships renew, lapse, and expire on the wallet
268
+ * service alone.
269
+ */
270
+ export declare function createCreditCommerceProvider(client: CreditWalletClient, options: CreateCreditCommerceProviderOptions): CreditCommerceProvider;
271
+ /** Periods one `renewDue` tick charges per membership before it yields to the next tick. */
272
+ export declare const defaultMaxRenewalsPerTick = 12;
273
+ export interface CreateInMemoryCreditWalletInput {
274
+ readonly store: StoreId;
275
+ /** Currencies the wallet keeps balances in; a lot or spend in another currency is refused. */
276
+ readonly currencies: readonly string[];
277
+ /** Clock for lot eligibility, entitlements, and periods; defaults to the system clock. */
278
+ readonly now?: () => string;
279
+ /** Offers the wallet prices spends against; a `spend` of a membership offer starts the membership. */
280
+ readonly offers?: readonly CreditOffer[];
281
+ readonly creditScope?: CreditScopeRegistry;
282
+ /** Cap on the periods a single `renewDue` tick charges per membership; defaults to 12. */
283
+ readonly maxRenewalsPerTick?: number;
284
+ }
285
+ /**
286
+ * The reference wallet: the client surface plus the owner-side operations a
287
+ * wallet service runs (credit a lot from a ledger grant, register offers,
288
+ * start a membership, run the renewal scheduler) and the observations the
289
+ * conformance vectors assert. The owner-side operations are not on
290
+ * `CreditWalletClient`; a game client gets only that interface.
291
+ */
292
+ export interface InMemoryCreditWallet extends CreditWalletClient {
293
+ readonly lots: readonly CreditLot[];
294
+ /** Every debit of every processed spend, in order; replays add nothing. */
295
+ readonly debits: readonly CreditScheduledDebit[];
296
+ readonly entitlements: readonly CreditEntitlement[];
297
+ readonly subscriptionEvents: readonly CreditSubscriptionEvent[];
298
+ readonly offers: readonly CreditOffer[];
299
+ /** Registers or replaces offers by logical id. */
300
+ addOffers(offers: readonly CreditOffer[]): void;
301
+ /** Credits a lot whose id is the ledger entry id; a replayed grant returns the existing lot. */
302
+ creditFromGrant(grant: CreditLotGrant, options?: DeriveCreditLotSpendScopeOptions): CreditLot;
303
+ /** Starts a membership by debiting period 0; an id already started with the same terms answers with its original debits. */
304
+ startMembership(input: CreditMembershipStartInput): Promise<CreditMembershipStartResult>;
305
+ /** Runs the renewal scheduler for memberships whose paid period has ended at `now` (default: the wallet clock). */
306
+ renewDue(now?: string): Promise<void>;
307
+ getActiveSubscriptions(input?: {
308
+ readonly titleId?: string;
309
+ }): Promise<readonly ActiveSubscription[]>;
310
+ /** With `input.titleId`, a title-scoped membership's events reach only that title's listeners. */
311
+ subscribe(listener: (event: CommerceEvent) => void, input?: CreditWalletSubscribeInput): () => void;
312
+ }
313
+ /**
314
+ * Reference in-memory wallet implementing the RFC "Credit lots" and "Billing
315
+ * authority and memberships" rules on the pure rule functions above: lot ids
316
+ * equal ledger entry ids, spend scope derives from the lot origin, spends
317
+ * consume eligible lots in the fixed order, are idempotent by key, are priced
318
+ * by the wallet's own offers, and debit nothing on `INSUFFICIENT_BALANCE`;
319
+ * memberships debit `membership:<subscriptionId>:<periodIndex>` per paid
320
+ * period with lot eligibility judged at the period boundary, a
321
+ * platform-scoped membership debits `cross-title` lots only, and a membership
322
+ * lapses at period end when the eligible balance runs out.
323
+ */
324
+ export declare function createInMemoryCreditWallet(input: CreateInMemoryCreditWalletInput): InMemoryCreditWallet;