@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.
@@ -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>;