@mpgd/adapter-devvit 0.9.19 → 0.10.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/commerce-contract.d.ts +85 -0
- package/dist/commerce-contract.js +145 -0
- package/dist/commerce-provider.d.ts +93 -0
- package/dist/commerce-provider.js +472 -0
- package/dist/index.d.ts +41 -2
- package/dist/index.js +154 -3
- package/dist/payments.d.ts +17 -0
- package/dist/payments.js +3 -2
- package/dist/server/commerce-bridge.d.ts +111 -0
- package/dist/server/commerce-bridge.js +402 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/package.json +5 -4
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { PlatformEvidenceEnvelope, PlatformProviderAvailability } from '@mpgd/platform';
|
|
2
|
+
import { type CommerceCapabilities, type StoreId, type StorePurchase } from '@mpgd/platform/commerce';
|
|
3
|
+
/**
|
|
4
|
+
* Commerce v2 contract pieces shared by the Devvit webview provider and the
|
|
5
|
+
* Devvit server bridge handler: the store id, the RFC profile, the
|
|
6
|
+
* `devvit.order.v1` evidence envelope, and the order-to-purchase mapping.
|
|
7
|
+
* This module imports no Devvit SDK.
|
|
8
|
+
*/
|
|
9
|
+
/** The open store id of the Devvit provider (RFC "Capabilities"). */
|
|
10
|
+
export declare const devvitCommerceStoreId: 'devvit';
|
|
11
|
+
/**
|
|
12
|
+
* Evidence schema of a Devvit order reported by the client (a checkout result
|
|
13
|
+
* or the server's paid-order list). It names the order; the backend's
|
|
14
|
+
* server-push lookup decides from the ledger the fulfillment handler wrote
|
|
15
|
+
* (`devvit.payment-order.v1`), never from this envelope alone.
|
|
16
|
+
*/
|
|
17
|
+
export declare const devvitOrderEvidenceSchema: 'devvit.order.v1';
|
|
18
|
+
/** Bounds what one order list can turn into available purchases. */
|
|
19
|
+
export declare const maximumDevvitAvailableOrders = 64;
|
|
20
|
+
export type DevvitOrderEvidenceSource = 'checkout' | 'order-list';
|
|
21
|
+
export type DevvitOrderEnvironment = 'sandbox' | 'production';
|
|
22
|
+
/** The Reddit order status an order-list envelope reports: paid and awaiting delivery, or delivered. */
|
|
23
|
+
export type DevvitOrderEvidenceStatus = 'PAID' | 'DELIVERED';
|
|
24
|
+
export interface DevvitOrderEvidencePayload {
|
|
25
|
+
readonly orderId: string;
|
|
26
|
+
readonly sku: string;
|
|
27
|
+
readonly source: DevvitOrderEvidenceSource;
|
|
28
|
+
readonly environment?: DevvitOrderEnvironment;
|
|
29
|
+
readonly status?: DevvitOrderEvidenceStatus;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A paid Devvit order as the order list reports it: `delivered` says whether
|
|
33
|
+
* Reddit already acknowledged its delivery (`DELIVERED`), which it does as
|
|
34
|
+
* soon as the fulfillment handler answered; absent, the order is `PAID` and
|
|
35
|
+
* the handler has not settled it yet.
|
|
36
|
+
*/
|
|
37
|
+
export interface DevvitPaidOrder {
|
|
38
|
+
readonly orderId: string;
|
|
39
|
+
readonly sku: string;
|
|
40
|
+
/** ISO timestamp of the payment (`Order.updatedAt`, else `createdAt`). */
|
|
41
|
+
readonly paidAt?: string;
|
|
42
|
+
readonly environment?: DevvitOrderEnvironment;
|
|
43
|
+
readonly delivered?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The RFC profile row for `devvit`: consumable and durable one-time products,
|
|
47
|
+
* no subscription, no quantity, `lifecycle.finish: 'none'` (the fulfillment
|
|
48
|
+
* handler settles on the server; the client has nothing to finish),
|
|
49
|
+
* `authority.evidence: 'server-push'`, and `accountBinding: 'host'` (the
|
|
50
|
+
* purchasing Reddit account comes from the server request context).
|
|
51
|
+
* `lifecycle.pending` covers the server-side pending of a server-push store:
|
|
52
|
+
* verify answers `pending` until the fulfillment push wrote the ledger; Devvit
|
|
53
|
+
* reports no store-side pending purchase.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createDevvitCommerceCapabilities(availability: PlatformProviderAvailability): CommerceCapabilities;
|
|
56
|
+
export declare function createDevvitOrderEvidence(payload: DevvitOrderEvidencePayload): PlatformEvidenceEnvelope;
|
|
57
|
+
/**
|
|
58
|
+
* Validates a `devvit.order.v1` envelope the way a server registry entry must
|
|
59
|
+
* before it looks the order up: the schema, a bounded order id and SKU, a
|
|
60
|
+
* known source, and optional environment and status. Throws `TypeError`
|
|
61
|
+
* otherwise.
|
|
62
|
+
*/
|
|
63
|
+
export declare function assertDevvitOrderEvidence(envelope: PlatformEvidenceEnvelope): DevvitOrderEvidencePayload;
|
|
64
|
+
/** A bounded, trimmed, control-character-free identifier, or nothing. */
|
|
65
|
+
export declare function readDevvitIdentifier(value: unknown): string | undefined;
|
|
66
|
+
/** An ISO timestamp for a Devvit order time, or nothing when it is absent or unparseable. */
|
|
67
|
+
export declare function readDevvitOrderTime(value: unknown): string | undefined;
|
|
68
|
+
export interface DevvitStorePurchaseInput {
|
|
69
|
+
readonly store: StoreId;
|
|
70
|
+
readonly orderId: string;
|
|
71
|
+
readonly sku: string;
|
|
72
|
+
readonly logicalId: string;
|
|
73
|
+
readonly source: DevvitOrderEvidenceSource;
|
|
74
|
+
readonly paidAt?: string | undefined;
|
|
75
|
+
readonly environment?: DevvitOrderEnvironment | undefined;
|
|
76
|
+
/** Known from the order list only; a checkout result carries no status. */
|
|
77
|
+
readonly delivered?: boolean | undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A paid Devvit order as a `purchased` store purchase: the order id is both
|
|
81
|
+
* `transactionId` and `token` (RFC "Client contract": host order id), the
|
|
82
|
+
* evidence is the `devvit.order.v1` envelope, and the account binding is the
|
|
83
|
+
* host identity. An order the contract rejects throws `INVALID_RESPONSE`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function createDevvitStorePurchase(input: DevvitStorePurchaseInput): StorePurchase;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { assertStorePurchase, CommerceError, } from '@mpgd/platform/commerce';
|
|
2
|
+
/**
|
|
3
|
+
* Commerce v2 contract pieces shared by the Devvit webview provider and the
|
|
4
|
+
* Devvit server bridge handler: the store id, the RFC profile, the
|
|
5
|
+
* `devvit.order.v1` evidence envelope, and the order-to-purchase mapping.
|
|
6
|
+
* This module imports no Devvit SDK.
|
|
7
|
+
*/
|
|
8
|
+
/** The open store id of the Devvit provider (RFC "Capabilities"). */
|
|
9
|
+
export const devvitCommerceStoreId = 'devvit';
|
|
10
|
+
/**
|
|
11
|
+
* Evidence schema of a Devvit order reported by the client (a checkout result
|
|
12
|
+
* or the server's paid-order list). It names the order; the backend's
|
|
13
|
+
* server-push lookup decides from the ledger the fulfillment handler wrote
|
|
14
|
+
* (`devvit.payment-order.v1`), never from this envelope alone.
|
|
15
|
+
*/
|
|
16
|
+
export const devvitOrderEvidenceSchema = 'devvit.order.v1';
|
|
17
|
+
/** Bounds what one order list can turn into available purchases. */
|
|
18
|
+
export const maximumDevvitAvailableOrders = 64;
|
|
19
|
+
/**
|
|
20
|
+
* The RFC profile row for `devvit`: consumable and durable one-time products,
|
|
21
|
+
* no subscription, no quantity, `lifecycle.finish: 'none'` (the fulfillment
|
|
22
|
+
* handler settles on the server; the client has nothing to finish),
|
|
23
|
+
* `authority.evidence: 'server-push'`, and `accountBinding: 'host'` (the
|
|
24
|
+
* purchasing Reddit account comes from the server request context).
|
|
25
|
+
* `lifecycle.pending` covers the server-side pending of a server-push store:
|
|
26
|
+
* verify answers `pending` until the fulfillment push wrote the ledger; Devvit
|
|
27
|
+
* reports no store-side pending purchase.
|
|
28
|
+
*/
|
|
29
|
+
export function createDevvitCommerceCapabilities(availability) {
|
|
30
|
+
return {
|
|
31
|
+
purchase: { consumable: true, durable: true, subscription: false, quantity: false },
|
|
32
|
+
catalog: { products: true, offers: false, localizedPrice: false },
|
|
33
|
+
lifecycle: {
|
|
34
|
+
availablePurchases: true,
|
|
35
|
+
pending: true,
|
|
36
|
+
restore: true,
|
|
37
|
+
events: true,
|
|
38
|
+
finish: 'none',
|
|
39
|
+
},
|
|
40
|
+
authority: { evidence: 'server-push', accountBinding: 'host' },
|
|
41
|
+
availability,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function createDevvitOrderEvidence(payload) {
|
|
45
|
+
return {
|
|
46
|
+
schema: devvitOrderEvidenceSchema,
|
|
47
|
+
payload: {
|
|
48
|
+
orderId: payload.orderId,
|
|
49
|
+
sku: payload.sku,
|
|
50
|
+
source: payload.source,
|
|
51
|
+
...(payload.environment === undefined ? {} : { environment: payload.environment }),
|
|
52
|
+
...(payload.status === undefined ? {} : { status: payload.status }),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validates a `devvit.order.v1` envelope the way a server registry entry must
|
|
58
|
+
* before it looks the order up: the schema, a bounded order id and SKU, a
|
|
59
|
+
* known source, and optional environment and status. Throws `TypeError`
|
|
60
|
+
* otherwise.
|
|
61
|
+
*/
|
|
62
|
+
export function assertDevvitOrderEvidence(envelope) {
|
|
63
|
+
if (envelope.schema !== devvitOrderEvidenceSchema) {
|
|
64
|
+
throw new TypeError(`Devvit order evidence schema must be ${devvitOrderEvidenceSchema}.`);
|
|
65
|
+
}
|
|
66
|
+
const { payload } = envelope;
|
|
67
|
+
const orderId = readDevvitIdentifier(payload.orderId);
|
|
68
|
+
const sku = readDevvitIdentifier(payload.sku);
|
|
69
|
+
if (orderId === undefined) {
|
|
70
|
+
throw new TypeError('Devvit order evidence must carry an orderId.');
|
|
71
|
+
}
|
|
72
|
+
if (sku === undefined) {
|
|
73
|
+
throw new TypeError('Devvit order evidence must carry a sku.');
|
|
74
|
+
}
|
|
75
|
+
const { source, environment, status } = payload;
|
|
76
|
+
if (source !== 'checkout' && source !== 'order-list') {
|
|
77
|
+
throw new TypeError('Devvit order evidence source must be checkout or order-list.');
|
|
78
|
+
}
|
|
79
|
+
if (environment !== undefined && environment !== 'sandbox' && environment !== 'production') {
|
|
80
|
+
throw new TypeError('Devvit order evidence environment must be sandbox or production.');
|
|
81
|
+
}
|
|
82
|
+
if (status !== undefined && status !== 'PAID' && status !== 'DELIVERED') {
|
|
83
|
+
throw new TypeError('Devvit order evidence status must be PAID or DELIVERED.');
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
orderId,
|
|
87
|
+
sku,
|
|
88
|
+
source,
|
|
89
|
+
...(environment === undefined ? {} : { environment }),
|
|
90
|
+
...(status === undefined ? {} : { status }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** A bounded, trimmed, control-character-free identifier, or nothing. */
|
|
94
|
+
export function readDevvitIdentifier(value) {
|
|
95
|
+
if (typeof value !== 'string'
|
|
96
|
+
|| value.length === 0
|
|
97
|
+
|| value.length > 256
|
|
98
|
+
|| value.trim() !== value
|
|
99
|
+
|| /[\p{Cc}\p{Cf}]/u.test(value)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
/** An ISO timestamp for a Devvit order time, or nothing when it is absent or unparseable. */
|
|
105
|
+
export function readDevvitOrderTime(value) {
|
|
106
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 64) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const parsed = Date.parse(value);
|
|
110
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : undefined;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A paid Devvit order as a `purchased` store purchase: the order id is both
|
|
114
|
+
* `transactionId` and `token` (RFC "Client contract": host order id), the
|
|
115
|
+
* evidence is the `devvit.order.v1` envelope, and the account binding is the
|
|
116
|
+
* host identity. An order the contract rejects throws `INVALID_RESPONSE`.
|
|
117
|
+
*/
|
|
118
|
+
export function createDevvitStorePurchase(input) {
|
|
119
|
+
try {
|
|
120
|
+
return assertStorePurchase({
|
|
121
|
+
store: input.store,
|
|
122
|
+
transactionId: input.orderId,
|
|
123
|
+
storeSku: input.sku,
|
|
124
|
+
logicalId: input.logicalId,
|
|
125
|
+
quantity: 1,
|
|
126
|
+
state: 'purchased',
|
|
127
|
+
...(input.paidAt === undefined ? {} : { purchasedAt: input.paidAt }),
|
|
128
|
+
token: input.orderId,
|
|
129
|
+
evidence: createDevvitOrderEvidence({
|
|
130
|
+
orderId: input.orderId,
|
|
131
|
+
sku: input.sku,
|
|
132
|
+
source: input.source,
|
|
133
|
+
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
134
|
+
...(input.delivered === undefined ? {} : { status: input.delivered ? 'DELIVERED' : 'PAID' }),
|
|
135
|
+
}),
|
|
136
|
+
accountBinding: { kind: 'host-identity' },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
throw new CommerceError('INVALID_RESPONSE', {
|
|
141
|
+
message: 'Devvit reported an invalid order.',
|
|
142
|
+
cause: error,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { PlatformProviderAvailability } from '@mpgd/platform';
|
|
2
|
+
import { type CommerceProvider, type StoreId } from '@mpgd/platform/commerce';
|
|
3
|
+
import { type DevvitPaidOrder } from './commerce-contract.js';
|
|
4
|
+
import { type DevvitCommerceProduct, type DevvitPaymentsClient } from './payments.js';
|
|
5
|
+
/**
|
|
6
|
+
* Refreshes an order reported ahead of the list (by a checkout result or a
|
|
7
|
+
* signal) survives without a listing showing it; it is forgotten on the next
|
|
8
|
+
* one, so a re-listed order is reported again.
|
|
9
|
+
*/
|
|
10
|
+
export declare const unlistedOrderGraceRefreshes = 7;
|
|
11
|
+
/** Store codes the provider attaches to failures of its own. */
|
|
12
|
+
export declare const devvitCommerceStoreCodes: Readonly<{
|
|
13
|
+
readonly productNotConfigured: 'DEVVIT_PRODUCT_NOT_CONFIGURED';
|
|
14
|
+
readonly checkoutFailed: 'DEVVIT_CHECKOUT_FAILED';
|
|
15
|
+
readonly checkoutCancelled: 'DEVVIT_CHECKOUT_CANCELLED';
|
|
16
|
+
readonly ordersUnavailable: 'DEVVIT_ORDERS_UNAVAILABLE';
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* The server-push lookup of the Devvit provider: the user's recent paid
|
|
20
|
+
* orders, each marked `delivered` once Reddit acknowledged its delivery
|
|
21
|
+
* (which happens as soon as the fulfillment handler answered). The order
|
|
22
|
+
* list is server-only in Devvit (`payments.getOrders`), so the webview reads
|
|
23
|
+
* it through the server bridge (`commerce.v2.getAvailablePurchases`); the
|
|
24
|
+
* conformance harness reads it from its fake payments plugin.
|
|
25
|
+
*/
|
|
26
|
+
export interface DevvitOrderSource {
|
|
27
|
+
listPaidOrders(): Promise<readonly DevvitPaidOrder[]>;
|
|
28
|
+
/** Whether the server side serves commerce v2; defaults to `available`. */
|
|
29
|
+
getAvailability?(): Promise<PlatformProviderAvailability>;
|
|
30
|
+
}
|
|
31
|
+
export interface DevvitOrdersChange {
|
|
32
|
+
/**
|
|
33
|
+
* Paid orders the signaller already knows, for example the order a
|
|
34
|
+
* realtime message named. The provider reports them at once and still
|
|
35
|
+
* re-reads the order source; an order for an unconfigured SKU is ignored.
|
|
36
|
+
*/
|
|
37
|
+
readonly orders?: readonly DevvitPaidOrder[];
|
|
38
|
+
}
|
|
39
|
+
export type DevvitOrdersChangeListener = (change?: DevvitOrdersChange) => void;
|
|
40
|
+
export interface CreateDevvitCommerceProviderInput {
|
|
41
|
+
readonly products: readonly DevvitCommerceProduct[];
|
|
42
|
+
/** Opens the Reddit checkout (`purchase()` from `@devvit/payments/client`). */
|
|
43
|
+
readonly client: Pick<DevvitPaymentsClient, 'purchase'>;
|
|
44
|
+
readonly orders: DevvitOrderSource;
|
|
45
|
+
/** Defaults to `devvit`; conformance vectors name their fixture store. */
|
|
46
|
+
readonly store?: StoreId;
|
|
47
|
+
/**
|
|
48
|
+
* Signals that the paid-order list may have changed, for example because the
|
|
49
|
+
* webview became visible again. The provider reports the orders the signal
|
|
50
|
+
* carries at once, then re-reads the list and emits `purchaseUpdated` for
|
|
51
|
+
* paid orders it has not reported yet. Observed only while a listener
|
|
52
|
+
* exists. Absent, orders are read on demand only.
|
|
53
|
+
*/
|
|
54
|
+
readonly onOrdersChanged?: (listener: DevvitOrdersChangeListener) => () => void;
|
|
55
|
+
readonly onCheckoutError?: (error: unknown) => void;
|
|
56
|
+
}
|
|
57
|
+
export interface DevvitCommerceProvider extends CommerceProvider {
|
|
58
|
+
restorePurchases(): Promise<void>;
|
|
59
|
+
/** Stops observing the order signal; the provider answers no further events. */
|
|
60
|
+
dispose(): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Commerce v2 provider for Devvit (RFC profile `devvit`, see
|
|
64
|
+
* `createDevvitCommerceCapabilities`). Reddit's fulfillment handler settles a
|
|
65
|
+
* purchase on the server: it writes the backend ledger, and the client learns
|
|
66
|
+
* of the grant through the backend's server-push lookup verifier, which
|
|
67
|
+
* answers `pending` until that entry exists. The provider therefore never
|
|
68
|
+
* grants and has nothing to finish.
|
|
69
|
+
*
|
|
70
|
+
* - `requestPurchase` opens the Reddit checkout. A checkout that already
|
|
71
|
+
* carries the created order id answers `purchased` with the order id as
|
|
72
|
+
* `transactionId` and `token` and `devvit.order.v1` evidence; the backend
|
|
73
|
+
* verify is what grants it. A checkout without an order id, or one that
|
|
74
|
+
* ended `pending`, is `launched`: an order may exist, and the reconciler
|
|
75
|
+
* resolves it from `getAvailablePurchases`. A dismissed checkout is
|
|
76
|
+
* `cancelled`; a failed one is `failed` with the platform code as
|
|
77
|
+
* `storeCode` (`OUTCOME_UNKNOWN` and `ALREADY_OWNED` classes stay open as
|
|
78
|
+
* `launched`, as the RFC requires, and so does a `purchase()` call that
|
|
79
|
+
* rejects, since a missing response does not prove a missing order).
|
|
80
|
+
* - `getAvailablePurchases` lists the paid orders whose delivery the server
|
|
81
|
+
* has not acknowledged (the order source without its delivered entries),
|
|
82
|
+
* bounded by `maximumDevvitAvailableOrders`; `includePending` adds nothing
|
|
83
|
+
* because Devvit has no store-side pending order.
|
|
84
|
+
* - `restorePurchases` re-reads the order source and emits `purchaseUpdated`
|
|
85
|
+
* once per order not reported before, delivered orders included: Reddit
|
|
86
|
+
* marks an order delivered as soon as the fulfillment handler answered, so
|
|
87
|
+
* a session that lost its checkout result finds the order this way and the
|
|
88
|
+
* backend lookup answers `granted` for it. The first listener triggers the
|
|
89
|
+
* same read. Events before the first `subscribe` are buffered.
|
|
90
|
+
* - The provider emits no `purchaseError`: every checkout outcome is a
|
|
91
|
+
* synchronous result of the purchase call.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createDevvitCommerceProvider(input: CreateDevvitCommerceProviderInput): DevvitCommerceProvider;
|