@onesub/server 0.17.1 → 0.18.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=apps.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apps.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/apps.test.ts"],"names":[],"mappings":""}
package/dist/apps.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { OneSubAppConfig, OneSubServerConfig } from '@onesub/shared';
2
+ /**
3
+ * Resolves which app an incoming receipt belongs to, so one onesub instance can
4
+ * serve N bundles.
5
+ *
6
+ * A single-app deployment configures `apple`/`google` at the top level and never
7
+ * touches `apps` — that config stays the default and nothing here changes its
8
+ * behaviour.
9
+ */
10
+ export interface AppRegistry {
11
+ readonly apps: OneSubAppConfig[];
12
+ readonly defaultApp: OneSubAppConfig | undefined;
13
+ /**
14
+ * Returns the server config with `apple`/`google` swapped to the matched app's
15
+ * credentials, so downstream providers keep reading `config.apple` /
16
+ * `config.google` without knowing about multi-app.
17
+ */
18
+ configFor(hint: AppHint): OneSubServerConfig;
19
+ }
20
+ export interface AppHint {
21
+ /** `appId` from the request body, when the client sends one. */
22
+ appId?: string | null | undefined;
23
+ /** Bundle id read out of an Apple receipt — the receipt names its own app. */
24
+ bundleId?: string | null | undefined;
25
+ }
26
+ /**
27
+ * Registry for this config, built once. Routes and webhook handlers all resolve
28
+ * through the same instance so the multi-app summary is logged a single time.
29
+ */
30
+ export declare function getAppRegistry(config: OneSubServerConfig): AppRegistry;
31
+ export declare function buildAppRegistry(config: OneSubServerConfig): AppRegistry;
32
+ /**
33
+ * Reads the bundleId out of an Apple JWS **without verifying its signature**,
34
+ * only to decide which app's credentials to validate it with.
35
+ *
36
+ * Trusting this to pick the app is safe: the chosen app's validator then verifies
37
+ * the signature against Apple's roots and re-checks the bundleId, so a forged
38
+ * payload claiming another app's bundle fails there.
39
+ */
40
+ export declare function peekAppleBundleId(jws: string): string | undefined;
41
+ //# sourceMappingURL=apps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAG1E;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,eAAe,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC;IACjD;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,kBAAkB,CAAC;CAC9C;AAED,MAAM,WAAW,OAAO;IACtB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACtC;AAQD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,CAMtE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,CA8DxE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUjE"}
package/dist/index.cjs CHANGED
@@ -1173,23 +1173,95 @@ function sendZodError(res, err2, extra = {}) {
1173
1173
  );
1174
1174
  }
1175
1175
 
1176
+ // src/apps.ts
1177
+ function appName(app) {
1178
+ return app.id || app.apple?.bundleId || app.google?.packageName || "default";
1179
+ }
1180
+ var registryCache = /* @__PURE__ */ new WeakMap();
1181
+ function getAppRegistry(config) {
1182
+ const cached = registryCache.get(config);
1183
+ if (cached) return cached;
1184
+ const registry = buildAppRegistry(config);
1185
+ registryCache.set(config, registry);
1186
+ return registry;
1187
+ }
1188
+ function buildAppRegistry(config) {
1189
+ const apps = [...config.apps ?? []];
1190
+ if (config.apple || config.google) {
1191
+ const id = config.defaultAppId || config.apple?.bundleId || config.google?.packageName || "default";
1192
+ if (!apps.some((a) => a.id === id)) {
1193
+ apps.unshift({ id, apple: config.apple, google: config.google });
1194
+ }
1195
+ }
1196
+ const defaultApp = apps.find((a) => a.id === config.defaultAppId) ?? // No explicit default: fall back to the top-level config, which unshifted to
1197
+ // the front above. Never silently pick an arbitrary app when several exist —
1198
+ // that would validate one app's receipt against another's credentials.
1199
+ (config.apple || config.google || apps.length === 1 ? apps[0] : void 0);
1200
+ if (apps.length > 1) {
1201
+ log.info("[onesub] Multi-app mode:", apps.map(appName).join(", "), "| default:", defaultApp ? appName(defaultApp) : "(none)");
1202
+ }
1203
+ function match(hint) {
1204
+ if (hint.appId) {
1205
+ const byId = apps.find(
1206
+ (a) => a.id === hint.appId || a.apple?.bundleId === hint.appId || a.google?.packageName === hint.appId
1207
+ );
1208
+ if (byId) return byId;
1209
+ log.warn("[onesub] Unknown appId:", hint.appId);
1210
+ return void 0;
1211
+ }
1212
+ if (hint.bundleId) {
1213
+ const byBundle = apps.find((a) => a.apple?.bundleId === hint.bundleId);
1214
+ if (byBundle) return byBundle;
1215
+ log.warn("[onesub] No app configured for bundleId:", hint.bundleId);
1216
+ return void 0;
1217
+ }
1218
+ return defaultApp;
1219
+ }
1220
+ return {
1221
+ apps,
1222
+ defaultApp,
1223
+ configFor(hint) {
1224
+ const app = match(hint);
1225
+ if (!app) {
1226
+ return { ...config, apple: void 0, google: void 0 };
1227
+ }
1228
+ return { ...config, apple: app.apple, google: app.google };
1229
+ }
1230
+ };
1231
+ }
1232
+ function peekAppleBundleId(jws) {
1233
+ try {
1234
+ const payload = jws.split(".")[1];
1235
+ if (!payload) return void 0;
1236
+ const json = Buffer.from(payload, "base64url").toString("utf-8");
1237
+ const claims = JSON.parse(json);
1238
+ return typeof claims.bundleId === "string" ? claims.bundleId : void 0;
1239
+ } catch {
1240
+ return void 0;
1241
+ }
1242
+ }
1243
+
1176
1244
  // src/routes/validate.ts
1177
1245
  var NO_SUB = { valid: false, subscription: null };
1178
1246
  var validateSchema = zod.z.object({
1179
1247
  platform: zod.z.enum(["apple", "google"]),
1180
1248
  receipt: zod.z.string().min(1).max(1e4),
1181
1249
  userId: zod.z.string().min(1).max(256),
1182
- productId: zod.z.string().min(1).max(256)
1250
+ productId: zod.z.string().min(1).max(256),
1251
+ /** Which app this receipt belongs to. Optional — see OneSubServerConfig.apps. */
1252
+ appId: zod.z.string().min(1).max(256).optional()
1183
1253
  });
1184
1254
  function createValidateRouter(config, store) {
1185
1255
  const router = express.Router();
1256
+ const registry = getAppRegistry(config);
1186
1257
  router.post(shared.ROUTES.VALIDATE, async (req, res) => {
1187
1258
  let platform;
1188
1259
  let receipt;
1189
1260
  let userId;
1190
1261
  let productId;
1262
+ let appId;
1191
1263
  try {
1192
- ({ platform, receipt, userId, productId } = validateSchema.parse(req.body));
1264
+ ({ platform, receipt, userId, productId, appId } = validateSchema.parse(req.body));
1193
1265
  } catch (err2) {
1194
1266
  if (err2 instanceof zod.z.ZodError) {
1195
1267
  sendZodError(res, err2, NO_SUB);
@@ -1199,18 +1271,22 @@ function createValidateRouter(config, store) {
1199
1271
  }
1200
1272
  try {
1201
1273
  let sub = null;
1274
+ const appConfig = registry.configFor({
1275
+ appId,
1276
+ bundleId: platform === "apple" ? peekAppleBundleId(receipt) : void 0
1277
+ });
1202
1278
  if (platform === "apple") {
1203
- if (!config.apple) {
1279
+ if (!appConfig.apple) {
1204
1280
  sendError(res, 500, shared.ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple configuration not provided", NO_SUB);
1205
1281
  return;
1206
1282
  }
1207
- sub = await validateAppleReceipt(receipt, config.apple);
1283
+ sub = await validateAppleReceipt(receipt, appConfig.apple);
1208
1284
  } else {
1209
- if (!config.google) {
1285
+ if (!appConfig.google) {
1210
1286
  sendError(res, 500, shared.ONESUB_ERROR_CODE.GOOGLE_CONFIG_MISSING, "Google configuration not provided", NO_SUB);
1211
1287
  return;
1212
1288
  }
1213
- sub = await validateGoogleReceipt(receipt, productId, config.google);
1289
+ sub = await validateGoogleReceipt(receipt, productId, appConfig.google);
1214
1290
  }
1215
1291
  if (!sub) {
1216
1292
  sendError(res, 422, shared.ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED, "Receipt validation failed", NO_SUB);
@@ -1234,8 +1310,8 @@ function createValidateRouter(config, store) {
1234
1310
  }
1235
1311
  sub.userId = userId;
1236
1312
  await store.save(sub);
1237
- if (platform === "google" && config.google) {
1238
- void acknowledgeGoogleSubscription(receipt, productId, config.google);
1313
+ if (platform === "google" && appConfig.google) {
1314
+ void acknowledgeGoogleSubscription(receipt, productId, appConfig.google);
1239
1315
  }
1240
1316
  const response = { valid: true, subscription: sub };
1241
1317
  res.status(200).json(response);
@@ -1359,11 +1435,12 @@ async function processAppleNotification(work, config, store, purchaseStore) {
1359
1435
  appAccountToken,
1360
1436
  inAppOwnershipType
1361
1437
  } = work.decoded;
1438
+ const appleConfigForApp = getAppRegistry(config).configFor({ bundleId }).apple;
1362
1439
  const mapped = mapAppleNotificationStatus(notificationType, subtype);
1363
1440
  const finalStatus = mapped ?? status;
1364
- if (notificationType === "CONSUMPTION_REQUEST" && config.apple?.consumptionInfoProvider && transactionId && productId) {
1365
- const provider = config.apple.consumptionInfoProvider;
1366
- const appleConfig = config.apple;
1441
+ if (notificationType === "CONSUMPTION_REQUEST" && appleConfigForApp?.consumptionInfoProvider && transactionId && productId) {
1442
+ const provider = appleConfigForApp.consumptionInfoProvider;
1443
+ const appleConfig = appleConfigForApp;
1367
1444
  void (async () => {
1368
1445
  try {
1369
1446
  const info = await provider({
@@ -1412,8 +1489,8 @@ async function processAppleNotification(work, config, store, purchaseStore) {
1412
1489
  expiresAt: expiresAt ?? existing.expiresAt
1413
1490
  };
1414
1491
  await store.save(updated);
1415
- } else if (config.apple?.issuerId && config.apple?.keyId && config.apple?.privateKey) {
1416
- const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, {
1492
+ } else if (appleConfigForApp?.issuerId && appleConfigForApp?.keyId && appleConfigForApp?.privateKey) {
1493
+ const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, appleConfigForApp, {
1417
1494
  sandbox: environment === "Sandbox"
1418
1495
  });
1419
1496
  if (fresh) {
@@ -1468,8 +1545,9 @@ async function handleAppleWebhook(req, res, config, store, purchaseStore, webhoo
1468
1545
  res.status(200).json({ received: true });
1469
1546
  return;
1470
1547
  }
1471
- if (config.apple?.bundleId && decoded.bundleId && decoded.bundleId !== config.apple.bundleId) {
1472
- log.warn("[onesub/webhook/apple] Bundle ID mismatch:", decoded.bundleId, "!==", config.apple.bundleId);
1548
+ const notifiedApp = getAppRegistry(config).configFor({ bundleId: decoded.bundleId });
1549
+ if (decoded.bundleId && !notifiedApp.apple) {
1550
+ log.warn("[onesub/webhook/apple] No app configured for bundleId:", decoded.bundleId);
1473
1551
  sendError(res, 400, shared.ONESUB_ERROR_CODE.BUNDLE_ID_MISMATCH, "Bundle ID mismatch");
1474
1552
  return;
1475
1553
  }
@@ -1546,7 +1624,17 @@ var GOOGLE_FAILURE_MESSAGES = {
1546
1624
  message: "Failed to process notification"
1547
1625
  }
1548
1626
  };
1627
+ function googleResolver(config) {
1628
+ const registry = getAppRegistry(config);
1629
+ const restricted = registry.apps.some((app) => !!app.google?.packageName);
1630
+ return {
1631
+ registry,
1632
+ serves: (packageName) => !restricted || !!registry.configFor({ appId: packageName }).google,
1633
+ googleFor: (packageName) => restricted ? registry.configFor({ appId: packageName }).google : registry.defaultApp?.google ?? config.google
1634
+ };
1635
+ }
1549
1636
  async function processGoogleNotification(work, config, store, purchaseStore) {
1637
+ const { googleFor } = googleResolver(config);
1550
1638
  if (work.kind === "voided") {
1551
1639
  const { voided } = work;
1552
1640
  if (voided.productType === 1) {
@@ -1569,8 +1657,9 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
1569
1657
  const { notificationType: notificationType2, purchaseToken: purchaseToken2, sku } = work.oneTimeProduct;
1570
1658
  if (notificationType2 === 1) {
1571
1659
  log.info("[onesub/webhook/google] oneTimeProduct PURCHASED:", sku);
1572
- if (config.google?.serviceAccountKey && config.google.packageName) {
1573
- void acknowledgeGoogleProduct(purchaseToken2, sku, config.google).catch(
1660
+ const googleCfg = googleFor(work.oneTimeProduct.packageName);
1661
+ if (googleCfg?.serviceAccountKey && googleCfg.packageName) {
1662
+ void acknowledgeGoogleProduct(purchaseToken2, sku, googleCfg).catch(
1574
1663
  (err2) => log.warn(`[onesub/webhook/google] oneTimeProduct ack failed for SKU ${sku} \u2014 3-day auto-refund risk:`, err2)
1575
1664
  );
1576
1665
  }
@@ -1599,14 +1688,15 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
1599
1688
  } else {
1600
1689
  finalStatus = existing?.status ?? shared.SUBSCRIPTION_STATUS.ACTIVE;
1601
1690
  }
1602
- if (isGooglePriceChangeConfirmedNotification(notificationType) && config.google?.onPriceChangeConfirmed) {
1603
- const hook = config.google.onPriceChangeConfirmed;
1691
+ const subGoogleCfg = googleFor(packageName);
1692
+ if (isGooglePriceChangeConfirmedNotification(notificationType) && subGoogleCfg?.onPriceChangeConfirmed) {
1693
+ const hook = subGoogleCfg.onPriceChangeConfirmed;
1604
1694
  void Promise.resolve().then(() => hook({ purchaseToken, subscriptionId, packageName })).catch((err2) => log.warn("[onesub/webhook/google] onPriceChangeConfirmed hook failed:", err2));
1605
1695
  }
1606
1696
  if (existing) {
1607
1697
  let updated = { ...existing, status: finalStatus };
1608
- if (config.google?.serviceAccountKey) {
1609
- const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
1698
+ if (subGoogleCfg?.serviceAccountKey) {
1699
+ const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, subGoogleCfg);
1610
1700
  if (fresh) {
1611
1701
  const preserveNotificationStatus = isGoogleGracePeriodNotification(notificationType) || isGoogleOnHoldNotification(notificationType);
1612
1702
  updated = {
@@ -1621,8 +1711,8 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
1621
1711
  }
1622
1712
  await store.save(updated);
1623
1713
  } else {
1624
- if (config.google?.serviceAccountKey) {
1625
- const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, config.google);
1714
+ if (subGoogleCfg?.serviceAccountKey) {
1715
+ const fresh = await validateGoogleReceipt(purchaseToken, subscriptionId, subGoogleCfg);
1626
1716
  if (fresh) {
1627
1717
  const boundAccountId = fresh.boundAccountId;
1628
1718
  delete fresh.boundAccountId;
@@ -1643,12 +1733,15 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
1643
1733
  }
1644
1734
  }
1645
1735
  async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
1646
- if (config.google?.pushAudience) {
1647
- const authenticated = await verifyGooglePushToken(
1648
- req,
1649
- config.google.pushAudience,
1650
- config.google.pushServiceAccountEmail
1651
- );
1736
+ const pushIdentities = getAppRegistry(config).apps.map((app) => app.google).filter((g) => !!g?.pushAudience);
1737
+ if (pushIdentities.length > 0) {
1738
+ let authenticated = false;
1739
+ for (const google of pushIdentities) {
1740
+ if (await verifyGooglePushToken(req, google.pushAudience, google.pushServiceAccountEmail)) {
1741
+ authenticated = true;
1742
+ break;
1743
+ }
1744
+ }
1652
1745
  if (!authenticated) {
1653
1746
  sendError(res, 401, shared.ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
1654
1747
  return;
@@ -1670,10 +1763,11 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1670
1763
  markedEventId = body.message.messageId;
1671
1764
  }
1672
1765
  let work;
1766
+ const { serves: servesPackage } = googleResolver(config);
1673
1767
  const voided = decodeGoogleVoidedNotification(body);
1674
1768
  if (voided) {
1675
- if (config.google?.packageName && voided.packageName !== config.google.packageName) {
1676
- log.warn("[onesub/webhook/google] voided package name mismatch:", voided.packageName, "!==", config.google.packageName);
1769
+ if (!servesPackage(voided.packageName)) {
1770
+ log.warn("[onesub/webhook/google] voided package name not served:", voided.packageName);
1677
1771
  sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
1678
1772
  return;
1679
1773
  }
@@ -1681,8 +1775,8 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1681
1775
  } else {
1682
1776
  const oneTimeProduct = decodeGoogleOneTimeProductNotification(body);
1683
1777
  if (oneTimeProduct) {
1684
- if (config.google?.packageName && oneTimeProduct.packageName !== config.google.packageName) {
1685
- log.warn("[onesub/webhook/google] oneTimeProduct package name mismatch:", oneTimeProduct.packageName, "!==", config.google.packageName);
1778
+ if (!servesPackage(oneTimeProduct.packageName)) {
1779
+ log.warn("[onesub/webhook/google] oneTimeProduct package name not served:", oneTimeProduct.packageName);
1686
1780
  sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
1687
1781
  return;
1688
1782
  }
@@ -1693,8 +1787,8 @@ async function handleGoogleWebhook(req, res, config, store, purchaseStore, webho
1693
1787
  res.status(200).json({ received: true });
1694
1788
  return;
1695
1789
  }
1696
- if (config.google?.packageName && notification.packageName !== config.google.packageName) {
1697
- log.warn("[onesub/webhook/google] Package name mismatch:", notification.packageName, "!==", config.google.packageName);
1790
+ if (!servesPackage(notification.packageName)) {
1791
+ log.warn("[onesub/webhook/google] Package name not served:", notification.packageName);
1698
1792
  sendError(res, 400, shared.ONESUB_ERROR_CODE.PACKAGE_NAME_MISMATCH, "Package name mismatch");
1699
1793
  return;
1700
1794
  }
@@ -1757,7 +1851,9 @@ var validatePurchaseSchema = zod.z.object({
1757
1851
  receipt: zod.z.string().min(1).max(1e4),
1758
1852
  userId: zod.z.string().min(1).max(256),
1759
1853
  productId: zod.z.string().min(1).max(256),
1760
- type: zod.z.enum([shared.PURCHASE_TYPE.CONSUMABLE, shared.PURCHASE_TYPE.NON_CONSUMABLE])
1854
+ type: zod.z.enum([shared.PURCHASE_TYPE.CONSUMABLE, shared.PURCHASE_TYPE.NON_CONSUMABLE]),
1855
+ /** Which app this receipt belongs to. Optional — see OneSubServerConfig.apps. */
1856
+ appId: zod.z.string().min(1).max(256).optional()
1761
1857
  });
1762
1858
  var purchaseStatusQuerySchema = zod.z.object({
1763
1859
  userId: zod.z.string().min(1).max(256),
@@ -1765,6 +1861,7 @@ var purchaseStatusQuerySchema = zod.z.object({
1765
1861
  });
1766
1862
  function createPurchaseRouter(config, purchaseStore) {
1767
1863
  const router = express.Router();
1864
+ const registry = getAppRegistry(config);
1768
1865
  router.post(shared.ROUTES.VALIDATE_PURCHASE, async (req, res) => {
1769
1866
  let body;
1770
1867
  try {
@@ -1776,7 +1873,11 @@ function createPurchaseRouter(config, purchaseStore) {
1776
1873
  }
1777
1874
  throw err2;
1778
1875
  }
1779
- const { platform, receipt, userId, productId, type } = body;
1876
+ const { platform, receipt, userId, productId, type, appId } = body;
1877
+ const appConfig = registry.configFor({
1878
+ appId,
1879
+ bundleId: platform === "apple" ? peekAppleBundleId(receipt) : void 0
1880
+ });
1780
1881
  try {
1781
1882
  if (type === shared.PURCHASE_TYPE.NON_CONSUMABLE) {
1782
1883
  const owned = (await purchaseStore.getPurchasesByUserId(userId)).find(
@@ -1796,25 +1897,25 @@ function createPurchaseRouter(config, purchaseStore) {
1796
1897
  let purchasedAt = (/* @__PURE__ */ new Date()).toISOString();
1797
1898
  let boundAccountId = null;
1798
1899
  if (platform === "apple") {
1799
- if (!config.apple) {
1900
+ if (!appConfig.apple) {
1800
1901
  sendError(res, 500, shared.ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, "Apple configuration not provided", NO_PURCHASE);
1801
1902
  return;
1802
1903
  }
1803
- const result = await validateAppleConsumableReceipt(receipt, config.apple, productId);
1904
+ const result = await validateAppleConsumableReceipt(receipt, appConfig.apple, productId);
1804
1905
  if (result) {
1805
1906
  transactionId = result.transactionId;
1806
1907
  purchasedAt = result.purchasedAt;
1807
1908
  boundAccountId = result.appAccountToken ?? null;
1808
1909
  }
1809
1910
  } else {
1810
- if (!config.google) {
1911
+ if (!appConfig.google) {
1811
1912
  sendError(res, 500, shared.ONESUB_ERROR_CODE.GOOGLE_CONFIG_MISSING, "Google configuration not provided", NO_PURCHASE);
1812
1913
  return;
1813
1914
  }
1814
1915
  const result = await validateGoogleProductReceipt(
1815
1916
  receipt,
1816
1917
  productId,
1817
- config.google,
1918
+ appConfig.google,
1818
1919
  type === shared.PURCHASE_TYPE.CONSUMABLE ? "consumable" : "non_consumable"
1819
1920
  );
1820
1921
  if (result) {
@@ -1880,11 +1981,11 @@ function createPurchaseRouter(config, purchaseStore) {
1880
1981
  quantity: 1
1881
1982
  };
1882
1983
  await purchaseStore.savePurchase(purchase);
1883
- if (platform === "google" && config.google) {
1984
+ if (platform === "google" && appConfig.google) {
1884
1985
  if (type === shared.PURCHASE_TYPE.CONSUMABLE) {
1885
- void consumeGoogleProductReceipt(receipt, productId, config.google);
1986
+ void consumeGoogleProductReceipt(receipt, productId, appConfig.google);
1886
1987
  } else {
1887
- void acknowledgeGoogleProduct(receipt, productId, config.google);
1988
+ void acknowledgeGoogleProduct(receipt, productId, appConfig.google);
1888
1989
  }
1889
1990
  }
1890
1991
  const response = {