@onesub/server 0.12.1 → 0.13.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.
Files changed (118) hide show
  1. package/dist/__tests__/admin-customer-detail.test.js +146 -0
  2. package/dist/__tests__/admin-customer-detail.test.js.map +1 -0
  3. package/dist/__tests__/admin-list-subscriptions.test.js +192 -0
  4. package/dist/__tests__/admin-list-subscriptions.test.js.map +1 -0
  5. package/dist/__tests__/admin-subscription-detail.test.js +112 -0
  6. package/dist/__tests__/admin-subscription-detail.test.js.map +1 -0
  7. package/dist/__tests__/apple-root-ca.test.js +41 -0
  8. package/dist/__tests__/apple-root-ca.test.js.map +1 -0
  9. package/dist/__tests__/apple-status-api.test.js +345 -0
  10. package/dist/__tests__/apple-status-api.test.js.map +1 -0
  11. package/dist/__tests__/cache.test.js +34 -0
  12. package/dist/__tests__/cache.test.js.map +1 -0
  13. package/dist/__tests__/entitlements.test.js +268 -0
  14. package/dist/__tests__/entitlements.test.js.map +1 -0
  15. package/dist/__tests__/error-codes.test.js +168 -0
  16. package/dist/__tests__/error-codes.test.js.map +1 -0
  17. package/dist/__tests__/fetch-hardening.test.js +192 -0
  18. package/dist/__tests__/fetch-hardening.test.js.map +1 -0
  19. package/dist/__tests__/google-paused.test.js +126 -0
  20. package/dist/__tests__/google-paused.test.js.map +1 -0
  21. package/dist/__tests__/google-price-change.test.js +186 -0
  22. package/dist/__tests__/google-price-change.test.js.map +1 -0
  23. package/dist/__tests__/google-subscriptions-v2.test.js +206 -0
  24. package/dist/__tests__/google-subscriptions-v2.test.js.map +1 -0
  25. package/dist/__tests__/lifecycle-scenarios.test.js +579 -0
  26. package/dist/__tests__/lifecycle-scenarios.test.js.map +1 -0
  27. package/dist/__tests__/lifecycle-states.test.js +423 -0
  28. package/dist/__tests__/lifecycle-states.test.js.map +1 -0
  29. package/dist/__tests__/linked-purchase-token.test.js +227 -0
  30. package/dist/__tests__/linked-purchase-token.test.js.map +1 -0
  31. package/dist/__tests__/metrics.test.js +340 -0
  32. package/dist/__tests__/metrics.test.js.map +1 -0
  33. package/dist/__tests__/mock-provider.test.js +188 -0
  34. package/dist/__tests__/mock-provider.test.js.map +1 -0
  35. package/dist/__tests__/openapi.test.js +32 -0
  36. package/dist/__tests__/openapi.test.js.map +1 -0
  37. package/dist/__tests__/paused-auto-resume.test.js +162 -0
  38. package/dist/__tests__/paused-auto-resume.test.js.map +1 -0
  39. package/dist/__tests__/providers.test.js +321 -0
  40. package/dist/__tests__/providers.test.js.map +1 -0
  41. package/dist/__tests__/redis-store.test.js +117 -0
  42. package/dist/__tests__/redis-store.test.js.map +1 -0
  43. package/dist/__tests__/refund-policy.test.js +317 -0
  44. package/dist/__tests__/refund-policy.test.js.map +1 -0
  45. package/dist/__tests__/schema.test.js +46 -0
  46. package/dist/__tests__/schema.test.js.map +1 -0
  47. package/dist/__tests__/store.test.js +172 -0
  48. package/dist/__tests__/store.test.js.map +1 -0
  49. package/dist/__tests__/test-utils.js +30 -0
  50. package/dist/__tests__/test-utils.js.map +1 -0
  51. package/dist/__tests__/webhook-events.test.js +36 -0
  52. package/dist/__tests__/webhook-events.test.js.map +1 -0
  53. package/dist/__tests__/webhook-queue.test.js +26 -0
  54. package/dist/__tests__/webhook-queue.test.js.map +1 -0
  55. package/dist/__tests__/webhook-refund.test.js +313 -0
  56. package/dist/__tests__/webhook-refund.test.js.map +1 -0
  57. package/dist/cache.js +43 -0
  58. package/dist/cache.js.map +1 -0
  59. package/dist/errors.js +19 -0
  60. package/dist/errors.js.map +1 -0
  61. package/dist/http.js +36 -0
  62. package/dist/http.js.map +1 -0
  63. package/dist/index.cjs +231 -6
  64. package/dist/index.cjs.map +1 -1
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +231 -8
  68. package/dist/index.js.map +1 -1
  69. package/dist/logger.js +20 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/openapi.js +196 -0
  72. package/dist/openapi.js.map +1 -0
  73. package/dist/providers/apple-root-ca.js +38 -0
  74. package/dist/providers/apple-root-ca.js.map +1 -0
  75. package/dist/providers/apple.d.ts +76 -0
  76. package/dist/providers/apple.d.ts.map +1 -1
  77. package/dist/providers/apple.js +510 -0
  78. package/dist/providers/apple.js.map +1 -0
  79. package/dist/providers/google.d.ts +25 -0
  80. package/dist/providers/google.d.ts.map +1 -1
  81. package/dist/providers/google.js +522 -0
  82. package/dist/providers/google.js.map +1 -0
  83. package/dist/providers/mock.js +93 -0
  84. package/dist/providers/mock.js.map +1 -0
  85. package/dist/routes/admin.d.ts.map +1 -1
  86. package/dist/routes/admin.js +287 -0
  87. package/dist/routes/admin.js.map +1 -0
  88. package/dist/routes/apple-offer.d.ts +21 -0
  89. package/dist/routes/apple-offer.d.ts.map +1 -0
  90. package/dist/routes/entitlements.js +130 -0
  91. package/dist/routes/entitlements.js.map +1 -0
  92. package/dist/routes/metrics.js +281 -0
  93. package/dist/routes/metrics.js.map +1 -0
  94. package/dist/routes/purchase.js +187 -0
  95. package/dist/routes/purchase.js.map +1 -0
  96. package/dist/routes/status.js +55 -0
  97. package/dist/routes/status.js.map +1 -0
  98. package/dist/routes/validate.js +70 -0
  99. package/dist/routes/validate.js.map +1 -0
  100. package/dist/routes/webhook-apple.d.ts.map +1 -1
  101. package/dist/routes/webhook-google.d.ts.map +1 -1
  102. package/dist/routes/webhook.js +461 -0
  103. package/dist/routes/webhook.js.map +1 -0
  104. package/dist/store.js +145 -0
  105. package/dist/store.js.map +1 -0
  106. package/dist/stores/postgres.js +329 -0
  107. package/dist/stores/postgres.js.map +1 -0
  108. package/dist/stores/redis.js +255 -0
  109. package/dist/stores/redis.js.map +1 -0
  110. package/dist/stores/schema.js +67 -0
  111. package/dist/stores/schema.js.map +1 -0
  112. package/dist/tracing.js +74 -0
  113. package/dist/tracing.js.map +1 -0
  114. package/dist/webhook-events.js +57 -0
  115. package/dist/webhook-events.js.map +1 -0
  116. package/dist/webhook-queue.js +108 -0
  117. package/dist/webhook-queue.js.map +1 -0
  118. package/package.json +2 -2
@@ -0,0 +1,461 @@
1
+ import { Router } from 'express';
2
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
3
+ import { ROUTES, SUBSCRIPTION_STATUS, ONESUB_ERROR_CODE } from '@onesub/shared';
4
+ import { decodeAppleNotification, decodeJws, sendAppleConsumptionResponse, fetchAppleSubscriptionStatus, } from '../providers/apple.js';
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
+ }
96
+ /**
97
+ * Retry / durability semantics.
98
+ *
99
+ * Both Apple (App Store Server Notifications V2) and Google (Pub/Sub RTDN)
100
+ * retry on any non-2xx response:
101
+ * - Apple: up to 5 retries over ~3 days, exponential backoff.
102
+ * - Google: per-Pub/Sub-subscription retry policy (default: retries until
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()`.
116
+ */
117
+ export function createWebhookRouter(config, store, purchaseStore, webhookEventStore) {
118
+ const router = Router();
119
+ /**
120
+ * POST /onesub/webhook/apple
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
+ });
459
+ return router;
460
+ }
461
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAOrD,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEhF,OAAO,EACL,uBAAuB,EACvB,SAAS,EACT,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,wBAAwB,EACxB,8BAA8B,EAC9B,qBAAqB,EACrB,0BAA0B,EAC1B,4BAA4B,EAC5B,2BAA2B,EAC3B,+BAA+B,EAC/B,0BAA0B,EAC1B,0BAA0B,EAC1B,wCAAwC,GACzC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGzC;;GAEG;AACH,MAAM,eAAe,GAAG,4CAA4C,CAAC;AAErE,iFAAiF;AACjF,kCAAkC;AAClC,IAAI,UAAU,GAAiD,IAAI,CAAC;AAEpE,SAAS,aAAa;IACpB,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,UAAU,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,qBAAqB,CAAC,GAAY,EAAE,gBAAwB;IACzE,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAChD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACxE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACxE,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,YAAY;IACZ,WAAW;IACX,aAAa;IACb,gBAAgB;CACjB,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC;IACnC,QAAQ;IACR,QAAQ;IACR,qBAAqB;CACtB,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,SAAS;CACV,CAAC,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,SAAS,0BAA0B,CACjC,gBAAwB,EACxB,OAA2B;IAE3B,IAAI,gBAAgB,KAAK,mBAAmB,EAAE,CAAC;QAC7C,OAAO,OAAO,KAAK,cAAc;YAC/B,CAAC,CAAC,mBAAmB,CAAC,YAAY;YAClC,CAAC,CAAC,mBAAmB,CAAC,OAAO,CAAC;IAClC,CAAC;IACD,IAAI,gBAAgB,KAAK,sBAAsB;QAAE,OAAO,mBAAmB,CAAC,OAAO,CAAC;IACpF,IAAI,oBAAoB,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAAE,OAAO,mBAAmB,CAAC,QAAQ,CAAC;IACpF,IAAI,mBAAmB,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAAE,OAAO,mBAAmB,CAAC,OAAO,CAAC;IAClF,IAAI,kBAAkB,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAAE,OAAO,mBAAmB,CAAC,MAAM,CAAC;IAChF,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAA0B,EAC1B,KAAwB,EACxB,aAA4B,EAC5B,iBAAqC;IAErC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC;IAExB;;;;;OAKG;IACH,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;QACtE,8DAA8D;QAC9D,0EAA0E;QAC1E,gFAAgF;QAChF,MAAM,IAAI,GAAG,GAAG,CAAC,IAAkC,CAAC;QAEpD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,sBAAsB,EAAE,uBAAuB,CAAC,CAAC;YACvF,OAAO;QACT,CAAC;QAED,IAAI,OAAiC,CAAC;QAEtC,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,SAAS,CACvB,IAAI,CAAC,aAAa,EAClB,MAAM,CAAC,KAAK,EAAE,mBAAmB,CAClC,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,KAAK,CAAC,wDAAwD,EAAE,GAAG,CAAC,CAAC;YACzE,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,sBAAsB,EAAE,uBAAuB,CAAC,CAAC;YACvF,OAAO;QACT,CAAC;QAED,kEAAkE;QAClE,0DAA0D;QAC1D,IAAI,iBAAiB,IAAI,OAAO,OAAO,CAAC,gBAAgB,KAAK,QAAQ,EAAE,CAAC;YACtE,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;YACnF,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,GAAG,CAAC,IAAI,CAAC,kDAAkD,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;gBACvF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;gBACxD,OAAO;YACT,CAAC;QACH,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;QAC1F,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,oEAAoE;YACpE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACzC,OAAO;QACT,CAAC;QAED,MAAM,EACJ,qBAAqB,EACrB,aAAa,EACb,IAAI,EACJ,SAAS,EACT,QAAQ,EACR,WAAW,EACX,MAAM,EACN,SAAS,EACT,SAAS,GACV,GAAG,OAAO,CAAC;QACZ,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;QAClD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAEhC,0EAA0E;QAC1E,kEAAkE;QAClE,MAAM,MAAM,GAAG,0BAA0B,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACrE,MAAM,WAAW,GAA+B,MAAM,IAAI,MAAM,CAAC;QAEjE,sEAAsE;QACtE,0EAA0E;QAC1E,iEAAiE;QACjE,IACE,gBAAgB,KAAK,qBAAqB;YAC1C,MAAM,CAAC,KAAK,EAAE,uBAAuB;YACrC,aAAa;YACb,SAAS,EACT,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC;YACtD,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC;YACjC,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC;wBAC1B,aAAa;wBACb,qBAAqB;wBACrB,SAAS;wBACT,QAAQ,EAAE,QAAQ,IAAI,WAAW,CAAC,QAAQ;wBAC1C,WAAW;qBACZ,CAAC,CAAC;oBACH,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,4BAA4B,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,EAAE;4BACnE,OAAO,EAAE,WAAW,KAAK,SAAS;yBACnC,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,GAAG,CAAC,IAAI,CAAC,wDAAwD,EAAE,GAAG,CAAC,CAAC;gBAC1E,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;QACP,CAAC;QAED,4EAA4E;QAC5E,wEAAwE;QACxE,4EAA4E;QAC5E,2CAA2C;QAC3C,MAAM,iBAAiB,GAAG,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,gBAAgB,CAAC;QAC7E,MAAM,gBAAgB,GAAG,oBAAoB,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAEpE,IAAI,CAAC;YACH,IAAI,iBAAiB,IAAI,gBAAgB,EAAE,CAAC;gBAC1C,sEAAsE;gBACtE,0EAA0E;gBAC1E,mBAAmB;gBACnB,MAAM,QAAQ,GAAG,aAAa,IAAI,qBAAqB,CAAC;gBACxD,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,6BAA6B,CAAC,QAAQ,CAAC,CAAC;gBAC5E,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,GAAG,CAAC,IAAI,CACN,4DAA4D,EAC5D,QAAQ,CACT,CAAC;gBACJ,CAAC;gBACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;gBACzC,OAAO;YACT,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;YACvE,IAAI,QAAQ,EAAE,CAAC;gBACb,sEAAsE;gBACtE,qEAAqE;gBACrE,qEAAqE;gBACrE,kEAAkE;gBAClE,MAAM,oBAAoB,GACxB,gBAAgB,IAAI,CAAC,iBAAiB,IAAI,gBAAgB,KAAK,qBAAqB,CAAC;gBACvF,MAAM,eAAe,GAAG,oBAAoB,IAAI,MAAM,CAAC,YAAY,KAAK,cAAc,CAAC;gBAEvF,MAAM,OAAO,GAAqB,eAAe;oBAC/C,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE;oBACnC,CAAC,CAAC;wBACE,GAAG,QAAQ;wBACX,MAAM,EAAE,WAAW;wBACnB,SAAS;wBACT,kEAAkE;wBAClE,mEAAmE;wBACnE,SAAS,EAAE,SAAS,IAAI,QAAQ,CAAC,SAAS;qBAC3C,CAAC;gBACN,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC5B,CAAC;iBAAM,IAAI,MAAM,CAAC,KAAK,EAAE,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,KAAK,IAAI,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,CAAC;gBACrF,qEAAqE;gBACrE,kEAAkE;gBAClE,8DAA8D;gBAC9D,sDAAsD;gBACtD,wEAAwE;gBACxE,uEAAuE;gBACvE,MAAM,KAAK,GAAG,MAAM,4BAA4B,CAAC,qBAAqB,EAAE,MAAM,CAAC,KAAK,EAAE;oBACpF,OAAO,EAAE,WAAW,KAAK,SAAS;iBACnC,CAAC,CAAC;gBACH,IAAI,KAAK,EAAE,CAAC;oBACV,KAAK,CAAC,MAAM,GAAG,qBAAqB,CAAC;oBACrC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC1B,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,IAAI,CACN,+EAA+E,EAC/E,qBAAqB,CACtB,CAAC;gBACJ,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,oEAAoE;gBACpE,GAAG,CAAC,IAAI,CACN,iGAAiG,EACjG,qBAAqB,CACtB,CAAC;YACJ,CAAC;YAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,KAAK,CAAC,4CAA4C,EAAE,GAAG,CAAC,CAAC;YAC7D,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,yBAAyB,EAAE,+BAA+B,CAAC,CAAC;QACpG,CAAC;IACH,CAAC,CAAC,CAAC;IAEH;;;;;OAKG;IACH,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;QACvE,4DAA4D;QAC5D,oFAAoF;QACpF,IAAI,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC;YAChC,MAAM,aAAa,GAAG,MAAM,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;YACnF,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;gBACpE,OAAO;YACT,CAAC;QACH,CAAC;QAED,MAAM,IAAI,GAAG,GAAG,CAAC,IAA0C,CAAC;QAE5D,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;YACxB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,oBAAoB,EAAE,sBAAsB,CAAC,CAAC;YACpF,OAAO;QACT,CAAC;QAED,4EAA4E;QAC5E,iEAAiE;QACjE,IAAI,iBAAiB,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YACpE,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAClF,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,GAAG,CAAC,IAAI,CAAC,mDAAmD,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBACtF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;gBACxD,OAAO;YACT,CAAC;QACH,CAAC;QAED,0EAA0E;QAC1E,sEAAsE;QACtE,6EAA6E;QAC7E,MAAM,MAAM,GAAG,8BAA8B,CAAC,IAAiC,CAAC,CAAC;QACjF,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,IAAI,MAAM,CAAC,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;gBACnF,GAAG,CAAC,IAAI,CACN,uDAAuD,EACvD,MAAM,CAAC,WAAW,EAClB,KAAK,EACL,MAAM,CAAC,MAAM,CAAC,WAAW,CAC1B,CAAC;gBACF,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,qBAAqB,EAAE,uBAAuB,CAAC,CAAC;gBACtF,OAAO;YACT,CAAC;YAED,IAAI,CAAC;gBACH,IAAI,MAAM,CAAC,WAAW,KAAK,CAAC,EAAE,CAAC;oBAC7B,0EAA0E;oBAC1E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;oBACtE,IAAI,QAAQ,EAAE,CAAC;wBACb,6DAA6D;wBAC7D,mEAAmE;wBACnE,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,KAAK,cAAc;4BACpD,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE;4BACnC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,mBAAmB,CAAC,QAAQ,EAAE,CAAC;wBAC1D,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBAC5B,CAAC;yBAAM,CAAC;wBACN,GAAG,CAAC,IAAI,CACN,wEAAwE,EACxE,MAAM,CAAC,aAAa,CACrB,CAAC;oBACJ,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,gEAAgE;oBAChE,qEAAqE;oBACrE,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,6BAA6B,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;oBAClF,IAAI,CAAC,OAAO,EAAE,CAAC;wBACb,GAAG,CAAC,IAAI,CACN,yDAAyD,EACzD,MAAM,CAAC,OAAO,CACf,CAAC;oBACJ,CAAC;gBACH,CAAC;gBACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,KAAK,CAAC,oDAAoD,EAAE,GAAG,CAAC,CAAC;gBACrE,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,yBAAyB,EAAE,uCAAuC,CAAC,CAAC;YAC5G,CAAC;YACD,OAAO;QACT,CAAC;QAED,MAAM,YAAY,GAAG,wBAAwB,CAAC,IAAiC,CAAC,CAAC;QAEjF,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,kDAAkD;YAClD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACzC,OAAO;QACT,CAAC;QAED,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,YAAY,CAAC;QAEtF,+CAA+C;QAC/C,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,IAAI,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAC5E,GAAG,CAAC,IAAI,CACN,gDAAgD,EAChD,WAAW,EACX,KAAK,EACL,MAAM,CAAC,MAAM,CAAC,WAAW,CAC1B,CAAC;YACF,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,qBAAqB,EAAE,uBAAuB,CAAC,CAAC;YACtF,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,oFAAoF;YACpF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;YAE/D,IAAI,WAAuC,CAAC;YAC5C,IAAI,0BAA0B,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACjD,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC;YAC3C,CAAC;iBAAM,IAAI,+BAA+B,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBAC7D,WAAW,GAAG,mBAAmB,CAAC,YAAY,CAAC;YACjD,CAAC;iBAAM,IAAI,0BAA0B,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACxD,WAAW,GAAG,mBAAmB,CAAC,OAAO,CAAC;YAC5C,CAAC;iBAAM,IAAI,0BAA0B,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACxD,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC;YAC3C,CAAC;iBAAM,IAAI,wCAAwC,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACtE,uEAAuE;gBACvE,oEAAoE;gBACpE,8CAA8C;gBAC9C,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC;YAC3C,CAAC;iBAAM,IAAI,4BAA4B,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBAC1D,WAAW,GAAG,mBAAmB,CAAC,QAAQ,CAAC;YAC7C,CAAC;iBAAM,IAAI,2BAA2B,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACzD,WAAW,GAAG,mBAAmB,CAAC,OAAO,CAAC;YAC5C,CAAC;iBAAM,CAAC;gBACN,yEAAyE;gBACzE,0EAA0E;gBAC1E,4CAA4C;gBAC5C,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC,6CAA6C;YACzF,CAAC;YAED,yEAAyE;YACzE,kEAAkE;YAClE,oEAAoE;YACpE,IACE,wCAAwC,CAAC,gBAAgB,CAAC;gBAC1D,MAAM,CAAC,MAAM,EAAE,sBAAsB,EACrC,CAAC;gBACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC;gBAClD,KAAK,OAAO,CAAC,OAAO,EAAE;qBACnB,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;qBAChE,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,6DAA6D,EAAE,GAAG,CAAC,CAAC,CAAC;YAClG,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,6DAA6D;gBAC7D,IAAI,OAAO,GAAqB,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;gBAErE,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC;oBACrC,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,aAAa,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;oBACxF,IAAI,KAAK,EAAE,CAAC;wBACV,oEAAoE;wBACpE,kEAAkE;wBAClE,gEAAgE;wBAChE,+DAA+D;wBAC/D,MAAM,0BAA0B,GAC9B,WAAW,KAAK,mBAAmB,CAAC,YAAY;4BAChD,WAAW,KAAK,mBAAmB,CAAC,OAAO,CAAC;wBAC9C,OAAO,GAAG;4BACR,GAAG,QAAQ;4BACX,MAAM,EAAE,0BAA0B,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM;4BAC/D,SAAS,EAAE,KAAK,CAAC,SAAS;4BAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;4BAC1B,gEAAgE;4BAChE,iEAAiE;4BACjE,8DAA8D;4BAC9D,qEAAqE;4BACrE,+DAA+D;4BAC/D,cAAc,EAAE,KAAK,CAAC,cAAc;4BACpC,mBAAmB,EAAE,KAAK,CAAC,mBAAmB,IAAI,QAAQ,CAAC,mBAAmB;yBAC/E,CAAC;oBACJ,CAAC;gBACH,CAAC;gBAED,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC5B,CAAC;iBAAM,CAAC;gBACN,mEAAmE;gBACnE,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC;oBACrC,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,aAAa,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;oBACxF,IAAI,KAAK,EAAE,CAAC;wBACV,6DAA6D;wBAC7D,kEAAkE;wBAClE,+DAA+D;wBAC/D,yDAAyD;wBACzD,IAAI,KAAK,CAAC,mBAAmB,EAAE,CAAC;4BAC9B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,kBAAkB,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;4BAC3E,IAAI,QAAQ,EAAE,CAAC;gCACb,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;gCAC/B,GAAG,CAAC,IAAI,CACN,4CAA4C,QAAQ,CAAC,MAAM,6BAA6B,KAAK,CAAC,mBAAmB,gBAAgB,aAAa,EAAE,CACjJ,CAAC;4BACJ,CAAC;iCAAM,CAAC;gCACN,KAAK,CAAC,MAAM,GAAG,aAAa,CAAC;4BAC/B,CAAC;wBACH,CAAC;6BAAM,CAAC;4BACN,uEAAuE;4BACvE,KAAK,CAAC,MAAM,GAAG,aAAa,CAAC;wBAC/B,CAAC;wBACD,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBAC1B,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,IAAI,CACN,sFAAsF,EACtF,aAAa,CACd,CAAC;gBACJ,CAAC;YACH,CAAC;YAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,KAAK,CAAC,sDAAsD,EAAE,GAAG,CAAC,CAAC;YACvE,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAAiB,CAAC,yBAAyB,EAAE,gCAAgC,CAAC,CAAC;QACrG,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/store.js ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * In-memory implementation — suitable for development and testing.
3
+ * Data is lost on process restart.
4
+ */
5
+ export class InMemorySubscriptionStore {
6
+ // Multiple records per user (different originalTransactionIds) — required
7
+ // for entitlement evaluation across multiple productIds. Ordering is
8
+ // last-written-first so getByUserId returns "most recent" naturally.
9
+ byUserId = new Map();
10
+ byTransactionId = new Map();
11
+ async save(sub) {
12
+ this.byTransactionId.set(sub.originalTransactionId, sub);
13
+ const existing = this.byUserId.get(sub.userId) ?? [];
14
+ // Replace any prior record with the same originalTransactionId, then
15
+ // unshift to the front so the latest write is index 0 (= getByUserId result).
16
+ const filtered = existing.filter((s) => s.originalTransactionId !== sub.originalTransactionId);
17
+ filtered.unshift(sub);
18
+ this.byUserId.set(sub.userId, filtered);
19
+ }
20
+ async getByUserId(userId) {
21
+ const list = this.byUserId.get(userId);
22
+ return list?.[0] ?? null;
23
+ }
24
+ async getAllByUserId(userId) {
25
+ return [...(this.byUserId.get(userId) ?? [])];
26
+ }
27
+ async getByTransactionId(txId) {
28
+ return this.byTransactionId.get(txId) ?? null;
29
+ }
30
+ async listAll() {
31
+ return [...this.byTransactionId.values()];
32
+ }
33
+ async listFiltered(opts) {
34
+ const limit = opts.limit ?? 50;
35
+ const offset = opts.offset ?? 0;
36
+ // Iterate via byUserId so insertion-order is "newest first" within each
37
+ // user (matches PostgresStore's `updated_at DESC` semantic). Across users
38
+ // we collect in Map iteration order — stable for a given run.
39
+ const all = [];
40
+ for (const list of this.byUserId.values()) {
41
+ for (const s of list)
42
+ all.push(s);
43
+ }
44
+ const filtered = all.filter((s) => {
45
+ if (opts.userId && s.userId !== opts.userId)
46
+ return false;
47
+ if (opts.status && s.status !== opts.status)
48
+ return false;
49
+ if (opts.productId && s.productId !== opts.productId)
50
+ return false;
51
+ if (opts.platform && s.platform !== opts.platform)
52
+ return false;
53
+ return true;
54
+ });
55
+ return {
56
+ items: filtered.slice(offset, offset + limit),
57
+ total: filtered.length,
58
+ limit,
59
+ offset,
60
+ };
61
+ }
62
+ }
63
+ /**
64
+ * In-memory implementation of PurchaseStore — suitable for development and testing.
65
+ * Data is lost on process restart.
66
+ */
67
+ export class InMemoryPurchaseStore {
68
+ byTransactionId = new Map();
69
+ byUserId = new Map();
70
+ async savePurchase(purchase) {
71
+ const existing = this.byTransactionId.get(purchase.transactionId);
72
+ if (existing) {
73
+ if (existing.userId !== purchase.userId) {
74
+ const err = new Error('TRANSACTION_BELONGS_TO_OTHER_USER');
75
+ err.code = 'TRANSACTION_BELONGS_TO_OTHER_USER';
76
+ throw err;
77
+ }
78
+ return; // same user — idempotent
79
+ }
80
+ this.byTransactionId.set(purchase.transactionId, purchase);
81
+ const list = this.byUserId.get(purchase.userId) ?? [];
82
+ list.push(purchase);
83
+ this.byUserId.set(purchase.userId, list);
84
+ }
85
+ async getPurchasesByUserId(userId) {
86
+ return this.byUserId.get(userId) ?? [];
87
+ }
88
+ async getPurchaseByTransactionId(txId) {
89
+ return this.byTransactionId.get(txId) ?? null;
90
+ }
91
+ async hasPurchased(userId, productId) {
92
+ const purchases = this.byUserId.get(userId);
93
+ if (!purchases)
94
+ return false;
95
+ return purchases.some((p) => p.productId === productId);
96
+ }
97
+ async reassignPurchase(transactionId, newUserId) {
98
+ const existing = this.byTransactionId.get(transactionId);
99
+ if (!existing)
100
+ return false;
101
+ const oldUserId = existing.userId;
102
+ if (oldUserId === newUserId)
103
+ return true;
104
+ const updated = { ...existing, userId: newUserId };
105
+ this.byTransactionId.set(transactionId, updated);
106
+ // remove from old userId index
107
+ const oldList = (this.byUserId.get(oldUserId) ?? []).filter((p) => p.transactionId !== transactionId);
108
+ if (oldList.length)
109
+ this.byUserId.set(oldUserId, oldList);
110
+ else
111
+ this.byUserId.delete(oldUserId);
112
+ // add to new userId index
113
+ const newList = this.byUserId.get(newUserId) ?? [];
114
+ newList.push(updated);
115
+ this.byUserId.set(newUserId, newList);
116
+ return true;
117
+ }
118
+ async deletePurchases(userId, productId) {
119
+ const list = this.byUserId.get(userId) ?? [];
120
+ const kept = list.filter((p) => p.productId !== productId);
121
+ const deleted = list.length - kept.length;
122
+ this.byUserId.set(userId, kept);
123
+ for (const p of list) {
124
+ if (p.productId === productId)
125
+ this.byTransactionId.delete(p.transactionId);
126
+ }
127
+ return deleted;
128
+ }
129
+ async listAll() {
130
+ return [...this.byTransactionId.values()];
131
+ }
132
+ async deletePurchaseByTransactionId(transactionId) {
133
+ const existing = this.byTransactionId.get(transactionId);
134
+ if (!existing)
135
+ return false;
136
+ this.byTransactionId.delete(transactionId);
137
+ const list = (this.byUserId.get(existing.userId) ?? []).filter((p) => p.transactionId !== transactionId);
138
+ if (list.length)
139
+ this.byUserId.set(existing.userId, list);
140
+ else
141
+ this.byUserId.delete(existing.userId);
142
+ return true;
143
+ }
144
+ }
145
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}