@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
package/dist/platform-order.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createServerPushLedgerGrant, devvitCommerceStoreId, findPurchaseGrantByPlatformEvidence, } from './commerce-evidence-registry.js';
|
|
2
|
+
import { revokeEntitlementGrant } from './commerce-ledger.js';
|
|
1
3
|
export function createPlatformOrderIdempotencyKey(input) {
|
|
2
4
|
assertPlatformOrderTarget(input.target);
|
|
3
5
|
assertIdentifier(input.orderId, 'orderId');
|
|
@@ -67,3 +69,87 @@ function assertRecord(input, label) {
|
|
|
67
69
|
throw new TypeError(`${label} must be an object.`);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The ledger grant a Devvit fulfillment handler writes for a paid order
|
|
74
|
+
* (`normalizeDevvitFulfillmentOrder`), under the platform order key
|
|
75
|
+
* `reddit:<orderId>` and in the shape the `devvit.order.v1` lookup verifier
|
|
76
|
+
* reads (target `reddit`, store `devvit`, the order id as the platform
|
|
77
|
+
* transaction id). Its evidence identity equals the one
|
|
78
|
+
* `createDevvitPaymentOrderEvidenceVerifier` issues, so a fulfillment that
|
|
79
|
+
* went through `verifyPurchase` and one that wrote the ledger directly
|
|
80
|
+
* dedupe on the same order.
|
|
81
|
+
*/
|
|
82
|
+
export function createPlatformOrderLedgerGrant(input) {
|
|
83
|
+
const order = assertFulfillPlatformOrderInput(input.order);
|
|
84
|
+
return createServerPushLedgerGrant({
|
|
85
|
+
store: devvitCommerceStoreId,
|
|
86
|
+
target: order.target,
|
|
87
|
+
...(input.deploymentTarget === undefined ? {} : { deploymentTarget: input.deploymentTarget }),
|
|
88
|
+
playerId: order.playerId,
|
|
89
|
+
product: input.product,
|
|
90
|
+
platformProductId: order.platformSku,
|
|
91
|
+
transactionId: order.orderId,
|
|
92
|
+
purchasedAt: order.paidAt,
|
|
93
|
+
grantedAt: input.grantedAt,
|
|
94
|
+
evidence: order.evidence,
|
|
95
|
+
idempotencyKey: createPlatformOrderIdempotencyKey(order),
|
|
96
|
+
evidenceVerificationId: createPlatformOrderEvidenceVerificationId(order),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The verify request a fulfillment handler sends when the backend verifies
|
|
101
|
+
* the order through the `devvit.payment-order.v1` registry entry instead of
|
|
102
|
+
* writing the ledger directly: the platform order key, the order id as the
|
|
103
|
+
* platform transaction id, and the handler's own envelope as evidence.
|
|
104
|
+
*/
|
|
105
|
+
export function createPlatformOrderVerifyRequest(input) {
|
|
106
|
+
const order = assertFulfillPlatformOrderInput(input.order);
|
|
107
|
+
return {
|
|
108
|
+
target: order.target,
|
|
109
|
+
store: devvitCommerceStoreId,
|
|
110
|
+
...(input.deploymentTarget === undefined ? {} : { deploymentTarget: input.deploymentTarget }),
|
|
111
|
+
playerId: order.playerId,
|
|
112
|
+
productId: input.productId,
|
|
113
|
+
platformTransactionId: order.orderId,
|
|
114
|
+
idempotencyKey: createPlatformOrderIdempotencyKey(order),
|
|
115
|
+
purchasedAt: order.paidAt,
|
|
116
|
+
evidence: order.evidence,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Writes the `revoke` entry for a refunded order (`normalizeDevvitRefundOrder`):
|
|
121
|
+
* the grant written for the same order id under the order target is
|
|
122
|
+
* reversed once, under the key `reddit:<orderId>:refund`. Answers
|
|
123
|
+
* `grant_not_found` when no grant for that order exists (the refund arrived
|
|
124
|
+
* before, or without, a fulfillment) and `grant_mismatch` when the grant
|
|
125
|
+
* belongs to another player.
|
|
126
|
+
*/
|
|
127
|
+
export async function revokePlatformOrder(input) {
|
|
128
|
+
const order = assertRefundPlatformOrderInput(input.order);
|
|
129
|
+
const grant = await findPurchaseGrantByPlatformEvidence(input.store, {
|
|
130
|
+
target: order.target,
|
|
131
|
+
platformEvidenceId: order.orderId,
|
|
132
|
+
});
|
|
133
|
+
if (grant === undefined) {
|
|
134
|
+
return { status: 'grant_not_found' };
|
|
135
|
+
}
|
|
136
|
+
return revokeEntitlementGrant(input.store, {
|
|
137
|
+
playerId: order.playerId,
|
|
138
|
+
revokesLedgerEntryId: grant.ledgerEntryId,
|
|
139
|
+
idempotencyKey: `${createPlatformOrderIdempotencyKey(order)}:refund`,
|
|
140
|
+
reason: 'refund',
|
|
141
|
+
revokedAt: input.revokedAt ?? order.refundedAt,
|
|
142
|
+
evidenceVerificationId: `${createPlatformOrderEvidenceVerificationId(order)}:refund`,
|
|
143
|
+
payload: {
|
|
144
|
+
target: order.target,
|
|
145
|
+
store: devvitCommerceStoreId,
|
|
146
|
+
platformTransactionId: order.orderId,
|
|
147
|
+
platformProductId: order.platformSku,
|
|
148
|
+
refundedAt: order.refundedAt,
|
|
149
|
+
evidenceSchema: order.evidence.schema,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function createPlatformOrderEvidenceVerificationId(order) {
|
|
154
|
+
return `devvit:payment-order:${order.orderId}`;
|
|
155
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { PlatformGateway, PlatformTarget } from '@mpgd/platform';
|
|
|
3
3
|
import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesClient, type GameServicesHeaderResolver } from './client.js';
|
|
4
4
|
import type { GameServicesLedgerTarget } from './types.js';
|
|
5
5
|
import type { ClientRewardEvidenceRegistry } from './client-reward-evidence.js';
|
|
6
|
+
import type { CommerceReconcilerProduct } from './commerce-reconciler.js';
|
|
6
7
|
import { type MonetizationOperationStore, type RecoverableMonetizationClient } from './monetization-recovery.js';
|
|
7
8
|
export type GameServicesAuthorityMode = 'production' | 'non-production';
|
|
8
9
|
export type GameServicesRuntimeMode = 'disabled' | 'local' | 'http' | 'orpc';
|
|
@@ -34,6 +35,12 @@ interface CreateGameServicesRuntimeCommonInput {
|
|
|
34
35
|
/** Durable, compare-and-swap journal for purchase and rewarded-ad recovery. */
|
|
35
36
|
readonly operationStore?: MonetizationOperationStore;
|
|
36
37
|
readonly rewardEvidenceRegistry?: ClientRewardEvidenceRegistry;
|
|
38
|
+
/**
|
|
39
|
+
* Catalog kinds and store SKUs the commerce v2 reconciler resolves without a
|
|
40
|
+
* store read (`CommerceReconcilerProduct`), for example derived from the
|
|
41
|
+
* product catalog's `platformProductIds` for the gateway target.
|
|
42
|
+
*/
|
|
43
|
+
readonly commerceProducts?: readonly CommerceReconcilerProduct[];
|
|
37
44
|
}
|
|
38
45
|
export type CreateGameServicesRuntimeInput = CreateGameServicesRuntimeCommonInput & ({
|
|
39
46
|
readonly transport?: 'http';
|
package/dist/runtime.js
CHANGED
|
@@ -62,17 +62,29 @@ export function createGameServicesRuntime(input) {
|
|
|
62
62
|
: { analyticsSessionId: input.analyticsSessionId }),
|
|
63
63
|
...(input.now === undefined ? {} : { now: input.now }),
|
|
64
64
|
...(input.rewardEvidenceRegistry === undefined ? {} : { rewardEvidenceRegistry: input.rewardEvidenceRegistry }),
|
|
65
|
+
...(input.commerceProducts === undefined ? {} : { commerceProducts: input.commerceProducts }),
|
|
65
66
|
};
|
|
66
|
-
const client = createGameServicesClient(clientInput);
|
|
67
67
|
const monetizationRecovery = input.operationStore === undefined
|
|
68
68
|
? undefined
|
|
69
69
|
: createRecoverableMonetizationClient({ ...clientInput, operationStore: input.operationStore });
|
|
70
|
+
// The recovery client owns the reconciler over the durable journal; the
|
|
71
|
+
// plain client shares it instead of journaling in memory.
|
|
72
|
+
const client = createGameServicesClient({
|
|
73
|
+
...clientInput,
|
|
74
|
+
...(monetizationRecovery?.commerceReconciler === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { commerceReconciler: monetizationRecovery.commerceReconciler }),
|
|
77
|
+
});
|
|
70
78
|
let routedClient = client;
|
|
71
79
|
if (monetizationRecovery !== undefined) {
|
|
72
80
|
routedClient = {
|
|
73
81
|
...client,
|
|
74
82
|
purchase: monetizationRecovery.purchase,
|
|
75
83
|
claimRewardedAd: monetizationRecovery.claimRewardedAd,
|
|
84
|
+
// The recovery client owns the reconciler over the durable journal.
|
|
85
|
+
...(monetizationRecovery.commerceReconciler === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: { commerceReconciler: monetizationRecovery.commerceReconciler }),
|
|
76
88
|
};
|
|
77
89
|
}
|
|
78
90
|
return {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type AnalyticsSink } from '@mpgd/analytics';
|
|
2
2
|
import { type AdPlacements, type ProductCatalog } from '@mpgd/catalog';
|
|
3
3
|
import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesBackendTransportRequest, type GameServicesBackendTransportResponse } from './client.js';
|
|
4
|
+
import { type CommerceEvidenceRegistry } from './commerce-evidence-registry.js';
|
|
4
5
|
import { type GameServicesHealthResponse } from './contract.js';
|
|
5
6
|
import { type GameServicesEvidenceVerifier, type GameServicesPurchaseGrantFinalizer } from './evidence-verification.js';
|
|
6
|
-
import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerResult, type GameServicesAdRewardTarget, type GameServicesStoreTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types.js';
|
|
7
|
+
import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerResult, type EntitlementLedgerRevocation, type EntitlementRevocationTransaction, type GameServicesAdRewardTarget, type GameServicesStoreTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type PurchaseLedgerStatusRequest, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types.js';
|
|
7
8
|
export { gameServicesRequestLimits, GameServicesRequestValidationError } from './validation.js';
|
|
9
|
+
export { listActiveEntitlementTransactions, readPurchaseLedgerStatuses, revokeEntitlementGrant, RevocationKeyConflictError, type RevokeEntitlementGrantResult, } from './commerce-ledger.js';
|
|
8
10
|
type CorsHeaders = Record<string, string>;
|
|
9
11
|
export interface CreateGameServicesBackendInput {
|
|
10
12
|
readonly catalog: ProductCatalog;
|
|
@@ -21,6 +23,16 @@ export interface CreateGameServicesBackendInput {
|
|
|
21
23
|
readonly purchaseGrantFinalizationTimeoutMs?: number;
|
|
22
24
|
readonly purchaseGrantFinalizer?: GameServicesPurchaseGrantFinalizer;
|
|
23
25
|
/**
|
|
26
|
+
* The commerce v2 evidence registry (RFC "Evidence registry"). With it,
|
|
27
|
+
* purchase evidence is routed by `evidence.schema` to the registered
|
|
28
|
+
* verifier and finalizer, `purchaseGrantFinalizer` is not consulted for
|
|
29
|
+
* purchases, `evidenceVerifier` serves rewarded ads only, and the backend
|
|
30
|
+
* publishes `getCommerceCapabilities`. Without it the backend keeps the v1
|
|
31
|
+
* behaviour: one `evidenceVerifier` and one `purchaseGrantFinalizer` for
|
|
32
|
+
* every purchase, and no capabilities endpoint.
|
|
33
|
+
*/
|
|
34
|
+
readonly commerceEvidenceRegistry?: CommerceEvidenceRegistry;
|
|
35
|
+
/**
|
|
24
36
|
* Receives unexpected (non-validation) failures such as store errors before the
|
|
25
37
|
* transport answers with the generic `INTERNAL_ERROR` code. Defaults to `console.error`.
|
|
26
38
|
* The hook is diagnostics only: if it throws or returns a rejecting promise, the failure
|
|
@@ -55,6 +67,26 @@ export interface GameServicesStore {
|
|
|
55
67
|
findEntitlementTransactionByPlatformEvidence?(input: EntitlementPlatformEvidenceIdentity): Promise<ProductGrantTransaction | undefined>;
|
|
56
68
|
getEntitlementTransaction(ledgerEntryId: string): Promise<ProductGrantTransaction | undefined>;
|
|
57
69
|
listEntitlementTransactions(): Promise<readonly ProductGrantTransaction[]>;
|
|
70
|
+
/** The grants with the given ids (at most `ledgerReadChunkSize` per call), any order. */
|
|
71
|
+
getEntitlementTransactions?(ledgerEntryIds: readonly string[]): Promise<readonly ProductGrantTransaction[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Appends a `revoke` entry (RFC "Revocation and expiry entries"). A grant is
|
|
74
|
+
* reversed once: a second revocation of the same grant, under any key,
|
|
75
|
+
* answers the first entry with `alreadyProcessed: true`, as does a replay of
|
|
76
|
+
* the same `(playerId, idempotencyKey)`; a key reused for another grant
|
|
77
|
+
* throws `RevocationKeyConflictError`, a reused `evidenceVerificationId`
|
|
78
|
+
* throws `EvidenceAlreadyProcessedError`, and an unknown grant throws.
|
|
79
|
+
* Optional: a store without it answers every ledger status read as
|
|
80
|
+
* `active` and refuses `revokeEntitlementGrant`.
|
|
81
|
+
*/
|
|
82
|
+
recordEntitlementRevocation?(input: EntitlementLedgerRevocation): Promise<EntitlementLedgerResult>;
|
|
83
|
+
/**
|
|
84
|
+
* The revocations that reverse any of the given grant entries (at most
|
|
85
|
+
* `ledgerReadChunkSize` per call), oldest first.
|
|
86
|
+
*/
|
|
87
|
+
findEntitlementRevocations?(input: {
|
|
88
|
+
readonly ledgerEntryIds: readonly string[];
|
|
89
|
+
}): Promise<readonly EntitlementRevocationTransaction[]>;
|
|
58
90
|
recordLeaderboardScore(input: RecordLeaderboardScoreRequest, options?: {
|
|
59
91
|
readonly recordedAt?: string;
|
|
60
92
|
}): Promise<RecordLeaderboardScoreResponse>;
|
|
@@ -87,6 +119,9 @@ export declare class InMemoryGameServicesStore implements GameServicesStore {
|
|
|
87
119
|
private readonly entitlementTransactionsByEvidence;
|
|
88
120
|
private readonly entitlementTransactionsByPlatformEvidence;
|
|
89
121
|
private readonly entitlementTransactionsById;
|
|
122
|
+
private readonly revocationsByKey;
|
|
123
|
+
private readonly revocationsByGrant;
|
|
124
|
+
private readonly revocationsByEvidence;
|
|
90
125
|
private readonly leaderboardTransactionsByRun;
|
|
91
126
|
private readonly leaderboardTransactionsById;
|
|
92
127
|
recordEntitlementGrant(input: EntitlementLedgerGrant): Promise<EntitlementLedgerResult>;
|
|
@@ -102,6 +137,11 @@ export declare class InMemoryGameServicesStore implements GameServicesStore {
|
|
|
102
137
|
findEntitlementTransactionByPlatformEvidence(input: EntitlementPlatformEvidenceIdentity): Promise<ProductGrantTransaction | undefined>;
|
|
103
138
|
getEntitlementTransaction(ledgerEntryId: string): Promise<ProductGrantTransaction | undefined>;
|
|
104
139
|
listEntitlementTransactions(): Promise<readonly ProductGrantTransaction[]>;
|
|
140
|
+
getEntitlementTransactions(ledgerEntryIds: readonly string[]): Promise<readonly ProductGrantTransaction[]>;
|
|
141
|
+
recordEntitlementRevocation(input: EntitlementLedgerRevocation): Promise<EntitlementLedgerResult>;
|
|
142
|
+
findEntitlementRevocations(input: {
|
|
143
|
+
readonly ledgerEntryIds: readonly string[];
|
|
144
|
+
}): Promise<readonly EntitlementRevocationTransaction[]>;
|
|
105
145
|
recordLeaderboardScore(input: RecordLeaderboardScoreRequest, options?: {
|
|
106
146
|
readonly recordedAt?: string;
|
|
107
147
|
}): Promise<RecordLeaderboardScoreResponse>;
|
|
@@ -121,6 +161,8 @@ export declare function createGameServicesRouter(backend: GameServicesBackendApi
|
|
|
121
161
|
health: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<void, void>, import("@orpc/contract").Schema<GameServicesHealthResponse, GameServicesHealthResponse>, object>;
|
|
122
162
|
commerce: {
|
|
123
163
|
verifyPurchase: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<VerifyPurchaseRequest, VerifyPurchaseRequest>, import("@orpc/contract").Schema<VerifyPurchaseResponse, VerifyPurchaseResponse>, object>;
|
|
164
|
+
getPurchaseLedgerStatus: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<PurchaseLedgerStatusRequest, PurchaseLedgerStatusRequest>, import("@orpc/contract").Schema<import("./operation-results.js").PurchaseLedgerStatusResponse, import("./operation-results.js").PurchaseLedgerStatusResponse>, object>;
|
|
165
|
+
getCapabilities: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<void, void>, import("@orpc/contract").Schema<import("@mpgd/platform/commerce").CommerceServiceCapabilities, import("@mpgd/platform/commerce").CommerceServiceCapabilities>, object>;
|
|
124
166
|
};
|
|
125
167
|
ads: {
|
|
126
168
|
claimReward: import("@orpc/server").ImplementedProcedure<GameServicesOrpcContext & object, object, import("@orpc/contract").Schema<ClaimAdRewardRequest, ClaimAdRewardRequest>, import("@orpc/contract").Schema<ClaimAdRewardResponse, ClaimAdRewardResponse>, object>;
|