@onesub/server 0.13.0 → 0.13.1
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/__tests__/error-codes.test.js +8 -3
- package/dist/__tests__/error-codes.test.js.map +1 -1
- package/dist/__tests__/redis-store.test.js +58 -9
- package/dist/__tests__/redis-store.test.js.map +1 -1
- package/dist/__tests__/webhook-apple.test.js +247 -0
- package/dist/__tests__/webhook-apple.test.js.map +1 -0
- package/dist/__tests__/webhook-google.test.js +255 -0
- package/dist/__tests__/webhook-google.test.js.map +1 -0
- package/dist/cache.js +0 -4
- package/dist/cache.js.map +1 -1
- package/dist/index.cjs +10 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/providers/apple.js +121 -1
- package/dist/providers/apple.js.map +1 -1
- package/dist/providers/google.js +42 -0
- package/dist/providers/google.js.map +1 -1
- package/dist/routes/admin.js +40 -0
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/apple-offer.js +72 -0
- package/dist/routes/apple-offer.js.map +1 -0
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/routes/purchase.js +21 -4
- package/dist/routes/purchase.js.map +1 -1
- package/dist/routes/webhook-apple.js +145 -0
- package/dist/routes/webhook-apple.js.map +1 -0
- package/dist/routes/webhook-google.js +204 -0
- package/dist/routes/webhook-google.js.map +1 -0
- package/dist/routes/webhook.js +9 -451
- package/dist/routes/webhook.js.map +1 -1
- package/dist/stores/redis.js +65 -23
- package/dist/stores/redis.js.map +1 -1
- package/dist/webhook-events.js +9 -8
- package/dist/webhook-events.js.map +1 -1
- package/package.json +3 -3
package/dist/routes/webhook.js
CHANGED
|
@@ -1,461 +1,19 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { decodeGoogleNotification, decodeGoogleVoidedNotification, validateGoogleReceipt, isGoogleActiveNotification, isGoogleCanceledNotification, isGoogleExpiredNotification, isGoogleGracePeriodNotification, isGoogleOnHoldNotification, isGooglePausedNotification, isGooglePriceChangeConfirmedNotification, } from '../providers/google.js';
|
|
6
|
-
import { log } from '../logger.js';
|
|
7
|
-
import { sendError } from '../errors.js';
|
|
8
|
-
/**
|
|
9
|
-
* Google's public JWKS endpoint used to verify Pub/Sub push JWT tokens.
|
|
10
|
-
*/
|
|
11
|
-
const GOOGLE_JWKS_URL = 'https://www.googleapis.com/oauth2/v3/certs';
|
|
12
|
-
// Lazily initialised — the JWKS fetch only occurs when the endpoint is first hit
|
|
13
|
-
// and pushAudience is configured.
|
|
14
|
-
let googleJwks = null;
|
|
15
|
-
function getGoogleJwks() {
|
|
16
|
-
if (!googleJwks) {
|
|
17
|
-
googleJwks = createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
|
|
18
|
-
}
|
|
19
|
-
return googleJwks;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Verifies the `Authorization: Bearer <token>` header as a Google-signed JWT
|
|
23
|
-
* and checks that the `aud` claim matches `expectedAudience`.
|
|
24
|
-
*
|
|
25
|
-
* Returns `true` when verification succeeds, `false` otherwise.
|
|
26
|
-
*/
|
|
27
|
-
async function verifyGooglePushToken(req, expectedAudience) {
|
|
28
|
-
const authHeader = req.headers['authorization'];
|
|
29
|
-
if (typeof authHeader !== 'string' || !authHeader.startsWith('Bearer ')) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
const token = authHeader.slice('Bearer '.length).trim();
|
|
33
|
-
try {
|
|
34
|
-
await jwtVerify(token, getGoogleJwks(), { audience: expectedAudience });
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Apple V2 notification types that indicate the subscription is now active.
|
|
43
|
-
*/
|
|
44
|
-
const APPLE_ACTIVE_TYPES = new Set([
|
|
45
|
-
'SUBSCRIBED',
|
|
46
|
-
'DID_RENEW',
|
|
47
|
-
'DID_RECOVER',
|
|
48
|
-
'OFFER_REDEEMED',
|
|
49
|
-
]);
|
|
50
|
-
/**
|
|
51
|
-
* Apple V2 notification types that indicate cancellation.
|
|
52
|
-
*/
|
|
53
|
-
const APPLE_CANCELED_TYPES = new Set([
|
|
54
|
-
'REVOKE',
|
|
55
|
-
'REFUND',
|
|
56
|
-
'CONSUMPTION_REQUEST',
|
|
57
|
-
]);
|
|
58
|
-
/**
|
|
59
|
-
* Apple V2 notification types that indicate expiry.
|
|
60
|
-
* Pure EXPIRED only — GRACE_PERIOD_EXPIRED is handled separately as on_hold
|
|
61
|
-
* because billing retry continues after the grace window ends.
|
|
62
|
-
*/
|
|
63
|
-
const APPLE_EXPIRED_TYPES = new Set([
|
|
64
|
-
'EXPIRED',
|
|
65
|
-
]);
|
|
66
|
-
/**
|
|
67
|
-
* Map an Apple notification (notificationType + optional subtype) to a
|
|
68
|
-
* lifecycle state. Returns null if the notification doesn't carry an explicit
|
|
69
|
-
* lifecycle signal (in which case the caller falls back to the JWS-derived status).
|
|
70
|
-
*
|
|
71
|
-
* Apple references:
|
|
72
|
-
* DID_FAIL_TO_RENEW + subtype GRACE_PERIOD → GRACE_PERIOD
|
|
73
|
-
* DID_FAIL_TO_RENEW (no subtype) → ON_HOLD (billing retry, no grace)
|
|
74
|
-
* GRACE_PERIOD_EXPIRED → ON_HOLD (grace ended, retry continues)
|
|
75
|
-
* EXPIRED → EXPIRED (terminal, no further retries)
|
|
76
|
-
* SUBSCRIBED / DID_RENEW / DID_RECOVER /
|
|
77
|
-
* OFFER_REDEEMED → ACTIVE
|
|
78
|
-
* REVOKE / REFUND / CONSUMPTION_REQUEST → CANCELED
|
|
79
|
-
*/
|
|
80
|
-
function mapAppleNotificationStatus(notificationType, subtype) {
|
|
81
|
-
if (notificationType === 'DID_FAIL_TO_RENEW') {
|
|
82
|
-
return subtype === 'GRACE_PERIOD'
|
|
83
|
-
? SUBSCRIPTION_STATUS.GRACE_PERIOD
|
|
84
|
-
: SUBSCRIPTION_STATUS.ON_HOLD;
|
|
85
|
-
}
|
|
86
|
-
if (notificationType === 'GRACE_PERIOD_EXPIRED')
|
|
87
|
-
return SUBSCRIPTION_STATUS.ON_HOLD;
|
|
88
|
-
if (APPLE_CANCELED_TYPES.has(notificationType))
|
|
89
|
-
return SUBSCRIPTION_STATUS.CANCELED;
|
|
90
|
-
if (APPLE_EXPIRED_TYPES.has(notificationType))
|
|
91
|
-
return SUBSCRIPTION_STATUS.EXPIRED;
|
|
92
|
-
if (APPLE_ACTIVE_TYPES.has(notificationType))
|
|
93
|
-
return SUBSCRIPTION_STATUS.ACTIVE;
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
2
|
+
import { ROUTES } from '@onesub/shared';
|
|
3
|
+
import { handleAppleWebhook } from './webhook-apple.js';
|
|
4
|
+
import { handleGoogleWebhook } from './webhook-google.js';
|
|
96
5
|
/**
|
|
97
6
|
* Retry / durability semantics.
|
|
98
7
|
*
|
|
99
|
-
* Both Apple
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* the message is ack'd or the retention window expires, up to 7 days).
|
|
104
|
-
*
|
|
105
|
-
* This router uses that built-in retry instead of a local dead-letter queue:
|
|
106
|
-
*
|
|
107
|
-
* 4xx — the payload is unusable (missing signedPayload, bad signature,
|
|
108
|
-
* package mismatch). Return 4xx so the sender does NOT retry, since
|
|
109
|
-
* the same request would fail again.
|
|
110
|
-
* 5xx — a transient failure on our side (DB down, network to Play API).
|
|
111
|
-
* Return 5xx so Apple / Google retry the notification for us.
|
|
112
|
-
* 2xx — processed, or intentionally ignored (e.g. test notification).
|
|
113
|
-
*
|
|
114
|
-
* If you need an explicit DLQ, wrap `store.save()` with your own error
|
|
115
|
-
* handler before passing the store to `createOneSubMiddleware()`.
|
|
8
|
+
* Both Apple and Google retry on any non-2xx response.
|
|
9
|
+
* 4xx — payload is unusable; do NOT retry.
|
|
10
|
+
* 5xx — transient failure; source will retry.
|
|
11
|
+
* 2xx — processed or intentionally ignored.
|
|
116
12
|
*/
|
|
117
13
|
export function createWebhookRouter(config, store, purchaseStore, webhookEventStore) {
|
|
118
14
|
const router = Router();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
*
|
|
122
|
-
* Receives Apple App Store Server Notifications V2.
|
|
123
|
-
* Apple sends a JWS-signed payload with a `signedPayload` field.
|
|
124
|
-
*/
|
|
125
|
-
router.post(ROUTES.WEBHOOK_APPLE, async (req, res) => {
|
|
126
|
-
// Apple sends the notification as { signedPayload: "<JWS>" }.
|
|
127
|
-
// Only the JWS-signed path is accepted. Pre-decoded payloads are rejected
|
|
128
|
-
// because they bypass signature verification and allow arbitrary state changes.
|
|
129
|
-
const body = req.body;
|
|
130
|
-
if (!body.signedPayload) {
|
|
131
|
-
sendError(res, 400, ONESUB_ERROR_CODE.MISSING_SIGNED_PAYLOAD, 'Missing signedPayload');
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
let payload;
|
|
135
|
-
try {
|
|
136
|
-
payload = await decodeJws(body.signedPayload, config.apple?.skipJwsVerification);
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
log.error('[onesub/webhook/apple] Failed to decode signedPayload:', err);
|
|
140
|
-
sendError(res, 400, ONESUB_ERROR_CODE.INVALID_SIGNED_PAYLOAD, 'Invalid signedPayload');
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// Idempotency — Apple's notificationUUID uniquely identifies each
|
|
144
|
-
// notification across retries. Skip if already processed.
|
|
145
|
-
if (webhookEventStore && typeof payload.notificationUUID === 'string') {
|
|
146
|
-
const fresh = await webhookEventStore.markIfNew('apple', payload.notificationUUID);
|
|
147
|
-
if (!fresh) {
|
|
148
|
-
log.info('[onesub/webhook/apple] dedupe: already processed', payload.notificationUUID);
|
|
149
|
-
res.status(200).json({ received: true, deduped: true });
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const decoded = await decodeAppleNotification(payload, config.apple?.skipJwsVerification);
|
|
154
|
-
if (!decoded) {
|
|
155
|
-
// Could be a test notification or unsupported type — acknowledge it
|
|
156
|
-
res.status(200).json({ received: true });
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const { originalTransactionId, transactionId, type, productId, bundleId, environment, status, willRenew, expiresAt, } = decoded;
|
|
160
|
-
const notificationType = payload.notificationType;
|
|
161
|
-
const subtype = payload.subtype;
|
|
162
|
-
// Derive final status from the notification type + subtype (overrides the
|
|
163
|
-
// JWS-derived status when there is an explicit lifecycle signal).
|
|
164
|
-
const mapped = mapAppleNotificationStatus(notificationType, subtype);
|
|
165
|
-
const finalStatus = mapped ?? status;
|
|
166
|
-
// CONSUMPTION_REQUEST — Apple is asking whether to grant a consumable
|
|
167
|
-
// refund. If the host app provided a consumptionInfoProvider, call it and
|
|
168
|
-
// PUT the response. Failures are logged; the webhook still 200s.
|
|
169
|
-
if (notificationType === 'CONSUMPTION_REQUEST' &&
|
|
170
|
-
config.apple?.consumptionInfoProvider &&
|
|
171
|
-
transactionId &&
|
|
172
|
-
productId) {
|
|
173
|
-
const provider = config.apple.consumptionInfoProvider;
|
|
174
|
-
const appleConfig = config.apple;
|
|
175
|
-
void (async () => {
|
|
176
|
-
try {
|
|
177
|
-
const info = await provider({
|
|
178
|
-
transactionId,
|
|
179
|
-
originalTransactionId,
|
|
180
|
-
productId,
|
|
181
|
-
bundleId: bundleId ?? appleConfig.bundleId,
|
|
182
|
-
environment,
|
|
183
|
-
});
|
|
184
|
-
if (info) {
|
|
185
|
-
await sendAppleConsumptionResponse(transactionId, info, appleConfig, {
|
|
186
|
-
sandbox: environment === 'Sandbox',
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
catch (err) {
|
|
191
|
-
log.warn('[onesub/webhook/apple] consumptionInfoProvider failed:', err);
|
|
192
|
-
}
|
|
193
|
-
})();
|
|
194
|
-
}
|
|
195
|
-
// IAP refund / revoke for one-time purchases (consumable / non-consumable).
|
|
196
|
-
// Apple sends REFUND notifications for both subscriptions and IAP — the
|
|
197
|
-
// transaction `type` field disambiguates. Subscription refunds keep flowing
|
|
198
|
-
// into the SubscriptionStore branch below.
|
|
199
|
-
const isOneTimePurchase = type === 'Consumable' || type === 'Non-Consumable';
|
|
200
|
-
const isRefundOrRevoke = APPLE_CANCELED_TYPES.has(notificationType);
|
|
201
|
-
try {
|
|
202
|
-
if (isOneTimePurchase && isRefundOrRevoke) {
|
|
203
|
-
// For consumables, the refunded transactionId is unique per purchase.
|
|
204
|
-
// For non-consumables, transactionId === originalTransactionId, so either
|
|
205
|
-
// lookup succeeds.
|
|
206
|
-
const lookupId = transactionId ?? originalTransactionId;
|
|
207
|
-
const removed = await purchaseStore.deletePurchaseByTransactionId(lookupId);
|
|
208
|
-
if (!removed) {
|
|
209
|
-
log.warn('[onesub/webhook/apple] IAP refund for unknown transaction:', lookupId);
|
|
210
|
-
}
|
|
211
|
-
res.status(200).json({ received: true });
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const existing = await store.getByTransactionId(originalTransactionId);
|
|
215
|
-
if (existing) {
|
|
216
|
-
// refundPolicy='until_expiry' for subscription refunds: keep status +
|
|
217
|
-
// expiresAt untouched, only flip willRenew=false. The status route's
|
|
218
|
-
// stale-record check will drop the user automatically once expiresAt
|
|
219
|
-
// passes. Refer to OneSubServerConfig.refundPolicy for rationale.
|
|
220
|
-
const isSubscriptionRefund = isRefundOrRevoke && !isOneTimePurchase && notificationType !== 'CONSUMPTION_REQUEST';
|
|
221
|
-
const keepEntitlement = isSubscriptionRefund && config.refundPolicy === 'until_expiry';
|
|
222
|
-
const updated = keepEntitlement
|
|
223
|
-
? { ...existing, willRenew: false }
|
|
224
|
-
: {
|
|
225
|
-
...existing,
|
|
226
|
-
status: finalStatus,
|
|
227
|
-
willRenew,
|
|
228
|
-
// expiresAt may be absent on non-subscription payloads — keep the
|
|
229
|
-
// previously-stored value rather than overwriting with null/empty.
|
|
230
|
-
expiresAt: expiresAt ?? existing.expiresAt,
|
|
231
|
-
};
|
|
232
|
-
await store.save(updated);
|
|
233
|
-
}
|
|
234
|
-
else if (config.apple?.issuerId && config.apple?.keyId && config.apple?.privateKey) {
|
|
235
|
-
// No local record but App Store Server API credentials are present —
|
|
236
|
-
// fetch the canonical state from Apple. This recovers from missed
|
|
237
|
-
// webhooks (server downtime, queue truncation) and bootstraps
|
|
238
|
-
// subscriptions purchased before this server existed.
|
|
239
|
-
// userId is unknown at webhook time — store under originalTransactionId
|
|
240
|
-
// as placeholder so a later /onesub/validate call can claim ownership.
|
|
241
|
-
const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, {
|
|
242
|
-
sandbox: environment === 'Sandbox',
|
|
243
|
-
});
|
|
244
|
-
if (fresh) {
|
|
245
|
-
fresh.userId = originalTransactionId;
|
|
246
|
-
await store.save(fresh);
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
log.warn('[onesub/webhook/apple] Unknown transaction and Status API returned no record:', originalTransactionId);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
// No local record and no API credentials to re-fetch — log and ack.
|
|
254
|
-
log.warn('[onesub/webhook/apple] Received notification for unknown transaction (no API creds to recover):', originalTransactionId);
|
|
255
|
-
}
|
|
256
|
-
res.status(200).json({ received: true });
|
|
257
|
-
}
|
|
258
|
-
catch (err) {
|
|
259
|
-
log.error('[onesub/webhook/apple] Store update error:', err);
|
|
260
|
-
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, 'Failed to update subscription');
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
/**
|
|
264
|
-
* POST /onesub/webhook/google
|
|
265
|
-
*
|
|
266
|
-
* Receives Google Play Real-Time Developer Notifications (RTDN) via Pub/Sub push.
|
|
267
|
-
* The body is a standard Pub/Sub push message with base64-encoded data.
|
|
268
|
-
*/
|
|
269
|
-
router.post(ROUTES.WEBHOOK_GOOGLE, async (req, res) => {
|
|
270
|
-
// Verify Google-signed JWT when pushAudience is configured.
|
|
271
|
-
// If pushAudience is not set, authentication is skipped for backward compatibility.
|
|
272
|
-
if (config.google?.pushAudience) {
|
|
273
|
-
const authenticated = await verifyGooglePushToken(req, config.google.pushAudience);
|
|
274
|
-
if (!authenticated) {
|
|
275
|
-
sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, 'Unauthorized');
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
const body = req.body;
|
|
280
|
-
if (!body.message?.data) {
|
|
281
|
-
sendError(res, 400, ONESUB_ERROR_CODE.MISSING_MESSAGE_DATA, 'Missing message.data');
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
// Idempotency — Pub/Sub guarantees `messageId` uniqueness per notification.
|
|
285
|
-
// Skip if already processed (Pub/Sub redelivers on any non-2xx).
|
|
286
|
-
if (webhookEventStore && typeof body.message.messageId === 'string') {
|
|
287
|
-
const fresh = await webhookEventStore.markIfNew('google', body.message.messageId);
|
|
288
|
-
if (!fresh) {
|
|
289
|
-
log.info('[onesub/webhook/google] dedupe: already processed', body.message.messageId);
|
|
290
|
-
res.status(200).json({ received: true, deduped: true });
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// voidedPurchaseNotification — Google's refund/chargeback signal for both
|
|
295
|
-
// subscriptions and one-time products. Routed here before the regular
|
|
296
|
-
// subscriptionNotification decoder so it doesn't get swallowed as "unknown".
|
|
297
|
-
const voided = decodeGoogleVoidedNotification(body);
|
|
298
|
-
if (voided) {
|
|
299
|
-
if (config.google?.packageName && voided.packageName !== config.google.packageName) {
|
|
300
|
-
log.warn('[onesub/webhook/google] voided package name mismatch:', voided.packageName, '!==', config.google.packageName);
|
|
301
|
-
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, 'Package name mismatch');
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
try {
|
|
305
|
-
if (voided.productType === 1) {
|
|
306
|
-
// Subscription refund — purchaseToken is stored as originalTransactionId.
|
|
307
|
-
const existing = await store.getByTransactionId(voided.purchaseToken);
|
|
308
|
-
if (existing) {
|
|
309
|
-
// refundPolicy='until_expiry' for subscription refunds: keep
|
|
310
|
-
// status + expiresAt, only flip willRenew. See OneSubServerConfig.
|
|
311
|
-
const updated = config.refundPolicy === 'until_expiry'
|
|
312
|
-
? { ...existing, willRenew: false }
|
|
313
|
-
: { ...existing, status: SUBSCRIPTION_STATUS.CANCELED };
|
|
314
|
-
await store.save(updated);
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
log.warn('[onesub/webhook/google] voided subscription for unknown purchaseToken:', voided.purchaseToken);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
// One-time product refund — orderId is stored as transactionId.
|
|
322
|
-
// Always immediate (refundPolicy doesn't apply — IAP has no expiry).
|
|
323
|
-
const removed = await purchaseStore.deletePurchaseByTransactionId(voided.orderId);
|
|
324
|
-
if (!removed) {
|
|
325
|
-
log.warn('[onesub/webhook/google] voided IAP for unknown orderId:', voided.orderId);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
res.status(200).json({ received: true });
|
|
329
|
-
}
|
|
330
|
-
catch (err) {
|
|
331
|
-
log.error('[onesub/webhook/google] voided notification error:', err);
|
|
332
|
-
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, 'Failed to process voided notification');
|
|
333
|
-
}
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
const notification = decodeGoogleNotification(body);
|
|
337
|
-
if (!notification) {
|
|
338
|
-
// Test notification or unknown type — acknowledge
|
|
339
|
-
res.status(200).json({ received: true });
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const { notificationType, purchaseToken, subscriptionId, packageName } = notification;
|
|
343
|
-
// Validate the package name matches our config
|
|
344
|
-
if (config.google?.packageName && packageName !== config.google.packageName) {
|
|
345
|
-
log.warn('[onesub/webhook/google] Package name mismatch:', packageName, '!==', config.google.packageName);
|
|
346
|
-
sendError(res, 400, ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, 'Package name mismatch');
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
try {
|
|
350
|
-
// Try to find the existing record by purchase token (used as originalTransactionId)
|
|
351
|
-
const existing = await store.getByTransactionId(purchaseToken);
|
|
352
|
-
let finalStatus;
|
|
353
|
-
if (isGoogleActiveNotification(notificationType)) {
|
|
354
|
-
finalStatus = SUBSCRIPTION_STATUS.ACTIVE;
|
|
355
|
-
}
|
|
356
|
-
else if (isGoogleGracePeriodNotification(notificationType)) {
|
|
357
|
-
finalStatus = SUBSCRIPTION_STATUS.GRACE_PERIOD;
|
|
358
|
-
}
|
|
359
|
-
else if (isGoogleOnHoldNotification(notificationType)) {
|
|
360
|
-
finalStatus = SUBSCRIPTION_STATUS.ON_HOLD;
|
|
361
|
-
}
|
|
362
|
-
else if (isGooglePausedNotification(notificationType)) {
|
|
363
|
-
finalStatus = SUBSCRIPTION_STATUS.PAUSED;
|
|
364
|
-
}
|
|
365
|
-
else if (isGooglePriceChangeConfirmedNotification(notificationType)) {
|
|
366
|
-
// User accepted a developer-initiated price change. Subscription stays
|
|
367
|
-
// active; the new price kicks in at the next renewal. Host can hook
|
|
368
|
-
// onPriceChangeConfirmed below to log/notify.
|
|
369
|
-
finalStatus = SUBSCRIPTION_STATUS.ACTIVE;
|
|
370
|
-
}
|
|
371
|
-
else if (isGoogleCanceledNotification(notificationType)) {
|
|
372
|
-
finalStatus = SUBSCRIPTION_STATUS.CANCELED;
|
|
373
|
-
}
|
|
374
|
-
else if (isGoogleExpiredNotification(notificationType)) {
|
|
375
|
-
finalStatus = SUBSCRIPTION_STATUS.EXPIRED;
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
// Unhandled notification type (PAUSE_SCHEDULE_CHANGED, DEFERRED, etc.) —
|
|
379
|
-
// re-fetch from Play API will correct the status (subscriptionsv2 returns
|
|
380
|
-
// SUBSCRIPTION_STATE_PAUSED etc. directly).
|
|
381
|
-
finalStatus = SUBSCRIPTION_STATUS.ACTIVE; // optimistic; re-fetch below will correct it
|
|
382
|
-
}
|
|
383
|
-
// Fire-and-forget hook for PRICE_CHANGE_CONFIRMED — happens before store
|
|
384
|
-
// save so the hook still runs even if save errors out (the host's
|
|
385
|
-
// analytics/notification side effect shouldn't block on DB issues).
|
|
386
|
-
if (isGooglePriceChangeConfirmedNotification(notificationType) &&
|
|
387
|
-
config.google?.onPriceChangeConfirmed) {
|
|
388
|
-
const hook = config.google.onPriceChangeConfirmed;
|
|
389
|
-
void Promise.resolve()
|
|
390
|
-
.then(() => hook({ purchaseToken, subscriptionId, packageName }))
|
|
391
|
-
.catch((err) => log.warn('[onesub/webhook/google] onPriceChangeConfirmed hook failed:', err));
|
|
392
|
-
}
|
|
393
|
-
if (existing) {
|
|
394
|
-
// Re-validate against Play API to get the latest expiry date
|
|
395
|
-
let updated = { ...existing, status: finalStatus };
|
|
396
|
-
if (config.google?.serviceAccountKey) {
|
|
397
|
-
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
398
|
-
if (fresh) {
|
|
399
|
-
// Notification-derived grace_period/on_hold are authoritative — the
|
|
400
|
-
// expiry-based deriveStatus() in the provider can't observe these
|
|
401
|
-
// states (paymentState heuristics are unreliable), so trust the
|
|
402
|
-
// RTDN signal over the API-derived status for those two cases.
|
|
403
|
-
const preserveNotificationStatus = finalStatus === SUBSCRIPTION_STATUS.GRACE_PERIOD ||
|
|
404
|
-
finalStatus === SUBSCRIPTION_STATUS.ON_HOLD;
|
|
405
|
-
updated = {
|
|
406
|
-
...existing,
|
|
407
|
-
status: preserveNotificationStatus ? finalStatus : fresh.status,
|
|
408
|
-
expiresAt: fresh.expiresAt,
|
|
409
|
-
willRenew: fresh.willRenew,
|
|
410
|
-
// Propagate v2-only fields that fresh may have updated. Without
|
|
411
|
-
// these, paused→active transitions left autoResumeTime stale and
|
|
412
|
-
// upgrade chains lost their linkedPurchaseToken on subsequent
|
|
413
|
-
// notifications. Use ?? undefined so cleared values become undefined
|
|
414
|
-
// (e.g. resume from paused → autoResumeTime should disappear).
|
|
415
|
-
autoResumeTime: fresh.autoResumeTime,
|
|
416
|
-
linkedPurchaseToken: fresh.linkedPurchaseToken ?? existing.linkedPurchaseToken,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
await store.save(updated);
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
// Unknown purchase token — attempt to create a record via Play API
|
|
424
|
-
if (config.google?.serviceAccountKey) {
|
|
425
|
-
const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
|
|
426
|
-
if (fresh) {
|
|
427
|
-
// Upgrade/downgrade continuity: if the v2 response carries a
|
|
428
|
-
// linkedPurchaseToken pointing at a previous subscription we know
|
|
429
|
-
// about, inherit the userId from it so the same person doesn't
|
|
430
|
-
// appear as a brand-new placeholder after a plan change.
|
|
431
|
-
if (fresh.linkedPurchaseToken) {
|
|
432
|
-
const previous = await store.getByTransactionId(fresh.linkedPurchaseToken);
|
|
433
|
-
if (previous) {
|
|
434
|
-
fresh.userId = previous.userId;
|
|
435
|
-
log.info(`[onesub/webhook/google] inherited userId ${previous.userId} from linkedPurchaseToken ${fresh.linkedPurchaseToken} → new token ${purchaseToken}`);
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
fresh.userId = purchaseToken;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
// userId is unknown at webhook time — use purchaseToken as placeholder
|
|
443
|
-
fresh.userId = purchaseToken;
|
|
444
|
-
}
|
|
445
|
-
await store.save(fresh);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
log.warn('[onesub/webhook/google] Unknown purchase token and no serviceAccountKey to re-fetch:', purchaseToken);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
res.status(200).json({ received: true });
|
|
453
|
-
}
|
|
454
|
-
catch (err) {
|
|
455
|
-
log.error('[onesub/webhook/google] Error handling notification:', err);
|
|
456
|
-
sendError(res, 500, ONESUB_ERROR_CODE.WEBHOOK_PROCESSING_FAILED, 'Failed to process notification');
|
|
457
|
-
}
|
|
458
|
-
});
|
|
15
|
+
router.post(ROUTES.WEBHOOK_APPLE, (req, res) => handleAppleWebhook(req, res, config, store, purchaseStore, webhookEventStore));
|
|
16
|
+
router.post(ROUTES.WEBHOOK_GOOGLE, (req, res) => handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore));
|
|
459
17
|
return router;
|
|
460
18
|
}
|
|
461
19
|
//# sourceMappingURL=webhook.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAA0B,EAC1B,KAAwB,EACxB,aAA4B,EAC5B,iBAAqC;IAErC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC;IAExB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE,CAChE,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,iBAAiB,CAAC,CAC9E,CAAC;IAEF,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE,CACjE,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,iBAAiB,CAAC,CAC/E,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/stores/redis.js
CHANGED
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
const SUB_TX_PREFIX = 'onesub:sub:tx:';
|
|
36
36
|
const SUB_USER_PREFIX = 'onesub:sub:user:';
|
|
37
37
|
const SUB_ALL = 'onesub:sub:all';
|
|
38
|
+
// Global sorted set (score = save timestamp ms) — enables ordered listAll and
|
|
39
|
+
// O(log n + limit) fast-path pagination when no secondary filters are applied.
|
|
40
|
+
const SUB_ALL_SORTED = 'onesub:sub:all:sorted';
|
|
38
41
|
const PUR_TX_PREFIX = 'onesub:purchase:tx:';
|
|
39
42
|
const PUR_USER_PREFIX = 'onesub:purchase:user:';
|
|
40
43
|
const PUR_USER_PRODUCT_PREFIX = 'onesub:purchase:user_product:';
|
|
@@ -48,14 +51,11 @@ export class RedisSubscriptionStore {
|
|
|
48
51
|
const score = Date.now();
|
|
49
52
|
const txKey = SUB_TX_PREFIX + sub.originalTransactionId;
|
|
50
53
|
const userKey = SUB_USER_PREFIX + sub.userId;
|
|
51
|
-
// Multi-key write — use a pipeline so the three commands are at least
|
|
52
|
-
// sent in one round-trip. Atomicity is best-effort; the only consistency
|
|
53
|
-
// hazard is a crash between commands, which leaves the user index without
|
|
54
|
-
// the new tx (recoverable: subsequent writes for the same tx fix it).
|
|
55
54
|
const pipeline = this.redis.multi();
|
|
56
55
|
pipeline.set(txKey, JSON.stringify(sub));
|
|
57
56
|
pipeline.zadd(userKey, score, sub.originalTransactionId);
|
|
58
57
|
pipeline.sadd(SUB_ALL, sub.originalTransactionId);
|
|
58
|
+
pipeline.zadd(SUB_ALL_SORTED, score, sub.originalTransactionId);
|
|
59
59
|
await pipeline.exec();
|
|
60
60
|
}
|
|
61
61
|
async getByUserId(userId) {
|
|
@@ -80,30 +80,51 @@ export class RedisSubscriptionStore {
|
|
|
80
80
|
return raw ? JSON.parse(raw) : null;
|
|
81
81
|
}
|
|
82
82
|
async listAll() {
|
|
83
|
-
|
|
83
|
+
// ZREVRANGE on the sorted set gives newest-first order (vs random SMEMBERS).
|
|
84
|
+
const ids = await this.redis.zrevrange(SUB_ALL_SORTED, 0, -1);
|
|
84
85
|
if (ids.length === 0)
|
|
85
86
|
return [];
|
|
86
87
|
const raws = await this.redis.mget(...ids.map((id) => SUB_TX_PREFIX + id));
|
|
87
88
|
return raws.filter((r) => r != null).map((r) => JSON.parse(r));
|
|
88
89
|
}
|
|
89
90
|
async listFiltered(opts) {
|
|
90
|
-
// Redis has no secondary indexes — for listFiltered we materialise the
|
|
91
|
-
// candidate set then apply filters in-process. Fine for small/medium data;
|
|
92
|
-
// for >100k rows pair this with Postgres or use Redis Stack with RediSearch.
|
|
93
91
|
const limit = opts.limit ?? 50;
|
|
94
92
|
const offset = opts.offset ?? 0;
|
|
95
|
-
|
|
93
|
+
const hasSecondaryFilters = !!(opts.status || opts.productId || opts.platform);
|
|
96
94
|
if (opts.userId) {
|
|
97
|
-
|
|
95
|
+
// Per-user path — already O(log n) via per-user sorted set.
|
|
96
|
+
const candidates = await this.getAllByUserId(opts.userId);
|
|
97
|
+
const filtered = candidates.filter((s) => {
|
|
98
|
+
if (opts.status && s.status !== opts.status)
|
|
99
|
+
return false;
|
|
100
|
+
if (opts.productId && s.productId !== opts.productId)
|
|
101
|
+
return false;
|
|
102
|
+
if (opts.platform && s.platform !== opts.platform)
|
|
103
|
+
return false;
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
return { items: filtered.slice(offset, offset + limit), total: filtered.length, limit, offset };
|
|
98
107
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
if (!hasSecondaryFilters) {
|
|
109
|
+
// Fast path: pure pagination with no filter — O(log n + limit) via sorted set.
|
|
110
|
+
const [ids, total] = await Promise.all([
|
|
111
|
+
this.redis.zrevrange(SUB_ALL_SORTED, offset, offset + limit - 1),
|
|
112
|
+
this.redis.zcard(SUB_ALL_SORTED),
|
|
113
|
+
]);
|
|
114
|
+
if (ids.length === 0)
|
|
115
|
+
return { items: [], total, limit, offset };
|
|
116
|
+
const raws = await this.redis.mget(...ids.map((id) => SUB_TX_PREFIX + id));
|
|
117
|
+
const items = raws.filter((r) => r != null).map((r) => JSON.parse(r));
|
|
118
|
+
return { items, total, limit, offset };
|
|
105
119
|
}
|
|
106
|
-
|
|
120
|
+
// Slow path: secondary filters require full scan. Results come in
|
|
121
|
+
// newest-first order. For >100k rows pair with Postgres or Redis Stack.
|
|
122
|
+
const allIds = await this.redis.zrevrange(SUB_ALL_SORTED, 0, -1);
|
|
123
|
+
if (allIds.length === 0)
|
|
124
|
+
return { items: [], total: 0, limit, offset };
|
|
125
|
+
const raws = await this.redis.mget(...allIds.map((id) => SUB_TX_PREFIX + id));
|
|
126
|
+
const all = raws.filter((r) => r != null).map((r) => JSON.parse(r));
|
|
127
|
+
const filtered = all.filter((s) => {
|
|
107
128
|
if (opts.status && s.status !== opts.status)
|
|
108
129
|
return false;
|
|
109
130
|
if (opts.productId && s.productId !== opts.productId)
|
|
@@ -112,12 +133,7 @@ export class RedisSubscriptionStore {
|
|
|
112
133
|
return false;
|
|
113
134
|
return true;
|
|
114
135
|
});
|
|
115
|
-
return {
|
|
116
|
-
items: filtered.slice(offset, offset + limit),
|
|
117
|
-
total: filtered.length,
|
|
118
|
-
limit,
|
|
119
|
-
offset,
|
|
120
|
-
};
|
|
136
|
+
return { items: filtered.slice(offset, offset + limit), total: filtered.length, limit, offset };
|
|
121
137
|
}
|
|
122
138
|
}
|
|
123
139
|
export class RedisPurchaseStore {
|
|
@@ -252,4 +268,30 @@ export class RedisCacheAdapter {
|
|
|
252
268
|
await this.redis.del(this.prefix + key);
|
|
253
269
|
}
|
|
254
270
|
}
|
|
271
|
+
const WEBHOOK_EVENT_PREFIX = 'onesub:webhook:event:';
|
|
272
|
+
const DEFAULT_WEBHOOK_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days — covers Apple's 3-day retry window
|
|
273
|
+
/**
|
|
274
|
+
* Redis-backed webhook idempotency store.
|
|
275
|
+
*
|
|
276
|
+
* Uses `SET key '1' EX ttl NX` — a single atomic command that sets the key
|
|
277
|
+
* only if it does not already exist. Returns 'OK' (new event) or null
|
|
278
|
+
* (already seen), with no race between a GET and a subsequent SET.
|
|
279
|
+
*
|
|
280
|
+
* Prefer this over `CacheWebhookEventStore(new RedisCacheAdapter(...))` for
|
|
281
|
+
* production deployments; the cache-based variant is non-atomic under
|
|
282
|
+
* concurrent retries.
|
|
283
|
+
*/
|
|
284
|
+
export class RedisWebhookEventStore {
|
|
285
|
+
redis;
|
|
286
|
+
ttlSeconds;
|
|
287
|
+
constructor(redis, ttlSeconds = DEFAULT_WEBHOOK_TTL_SECONDS) {
|
|
288
|
+
this.redis = redis;
|
|
289
|
+
this.ttlSeconds = ttlSeconds;
|
|
290
|
+
}
|
|
291
|
+
async markIfNew(provider, eventId) {
|
|
292
|
+
const key = WEBHOOK_EVENT_PREFIX + provider + ':' + eventId;
|
|
293
|
+
const result = await this.redis.set(key, '1', 'EX', this.ttlSeconds, 'NX');
|
|
294
|
+
return result === 'OK';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
255
297
|
//# sourceMappingURL=redis.js.map
|