@onesub/server 0.12.1 → 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.
Files changed (129) 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 +173 -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 +166 -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-apple.test.js +247 -0
  52. package/dist/__tests__/webhook-apple.test.js.map +1 -0
  53. package/dist/__tests__/webhook-events.test.js +36 -0
  54. package/dist/__tests__/webhook-events.test.js.map +1 -0
  55. package/dist/__tests__/webhook-google.test.js +255 -0
  56. package/dist/__tests__/webhook-google.test.js.map +1 -0
  57. package/dist/__tests__/webhook-queue.test.js +26 -0
  58. package/dist/__tests__/webhook-queue.test.js.map +1 -0
  59. package/dist/__tests__/webhook-refund.test.js +313 -0
  60. package/dist/__tests__/webhook-refund.test.js.map +1 -0
  61. package/dist/cache.js +39 -0
  62. package/dist/cache.js.map +1 -0
  63. package/dist/errors.js +19 -0
  64. package/dist/errors.js.map +1 -0
  65. package/dist/http.js +36 -0
  66. package/dist/http.js.map +1 -0
  67. package/dist/index.cjs +241 -9
  68. package/dist/index.cjs.map +1 -1
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +241 -11
  72. package/dist/index.js.map +1 -1
  73. package/dist/logger.js +20 -0
  74. package/dist/logger.js.map +1 -0
  75. package/dist/openapi.js +196 -0
  76. package/dist/openapi.js.map +1 -0
  77. package/dist/providers/apple-root-ca.js +38 -0
  78. package/dist/providers/apple-root-ca.js.map +1 -0
  79. package/dist/providers/apple.d.ts +76 -0
  80. package/dist/providers/apple.d.ts.map +1 -1
  81. package/dist/providers/apple.js +630 -0
  82. package/dist/providers/apple.js.map +1 -0
  83. package/dist/providers/google.d.ts +25 -0
  84. package/dist/providers/google.d.ts.map +1 -1
  85. package/dist/providers/google.js +564 -0
  86. package/dist/providers/google.js.map +1 -0
  87. package/dist/providers/mock.js +93 -0
  88. package/dist/providers/mock.js.map +1 -0
  89. package/dist/routes/admin.d.ts.map +1 -1
  90. package/dist/routes/admin.js +327 -0
  91. package/dist/routes/admin.js.map +1 -0
  92. package/dist/routes/apple-offer.d.ts +21 -0
  93. package/dist/routes/apple-offer.d.ts.map +1 -0
  94. package/dist/routes/apple-offer.js +72 -0
  95. package/dist/routes/apple-offer.js.map +1 -0
  96. package/dist/routes/entitlements.js +130 -0
  97. package/dist/routes/entitlements.js.map +1 -0
  98. package/dist/routes/metrics.js +281 -0
  99. package/dist/routes/metrics.js.map +1 -0
  100. package/dist/routes/purchase.d.ts.map +1 -1
  101. package/dist/routes/purchase.js +204 -0
  102. package/dist/routes/purchase.js.map +1 -0
  103. package/dist/routes/status.js +55 -0
  104. package/dist/routes/status.js.map +1 -0
  105. package/dist/routes/validate.js +70 -0
  106. package/dist/routes/validate.js.map +1 -0
  107. package/dist/routes/webhook-apple.d.ts.map +1 -1
  108. package/dist/routes/webhook-apple.js +145 -0
  109. package/dist/routes/webhook-apple.js.map +1 -0
  110. package/dist/routes/webhook-google.d.ts.map +1 -1
  111. package/dist/routes/webhook-google.js +204 -0
  112. package/dist/routes/webhook-google.js.map +1 -0
  113. package/dist/routes/webhook.js +19 -0
  114. package/dist/routes/webhook.js.map +1 -0
  115. package/dist/store.js +145 -0
  116. package/dist/store.js.map +1 -0
  117. package/dist/stores/postgres.js +329 -0
  118. package/dist/stores/postgres.js.map +1 -0
  119. package/dist/stores/redis.js +297 -0
  120. package/dist/stores/redis.js.map +1 -0
  121. package/dist/stores/schema.js +67 -0
  122. package/dist/stores/schema.js.map +1 -0
  123. package/dist/tracing.js +74 -0
  124. package/dist/tracing.js.map +1 -0
  125. package/dist/webhook-events.js +58 -0
  126. package/dist/webhook-events.js.map +1 -0
  127. package/dist/webhook-queue.js +108 -0
  128. package/dist/webhook-queue.js.map +1 -0
  129. package/package.json +4 -4
@@ -0,0 +1,93 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { SUBSCRIPTION_STATUS, MOCK_RECEIPT_PREFIX } from '@onesub/shared';
3
+ import { log } from '../logger.js';
4
+ export function classifyMockReceipt(receipt) {
5
+ if (receipt.startsWith(MOCK_RECEIPT_PREFIX.NETWORK_ERROR))
6
+ return { kind: 'network-error' };
7
+ if (receipt.startsWith(MOCK_RECEIPT_PREFIX.REVOKED))
8
+ return { kind: 'revoked' };
9
+ if (receipt.startsWith(MOCK_RECEIPT_PREFIX.EXPIRED))
10
+ return { kind: 'expired' };
11
+ if (receipt.startsWith(MOCK_RECEIPT_PREFIX.INVALID) ||
12
+ receipt.startsWith(MOCK_RECEIPT_PREFIX.BAD_SIG)) {
13
+ return { kind: 'invalid' };
14
+ }
15
+ if (receipt.startsWith(MOCK_RECEIPT_PREFIX.SANDBOX))
16
+ return { kind: 'sandbox' };
17
+ return { kind: 'valid' };
18
+ }
19
+ /**
20
+ * Gate the outcome against `valid` / `sandbox`. Throws for network-error,
21
+ * logs + returns false for rejection cases, returns true for pass-through.
22
+ * Every mock validator uses this so their rejection behavior stays in sync.
23
+ */
24
+ function outcomePasses(outcome, tag) {
25
+ if (outcome.kind === 'network-error') {
26
+ throw new Error(`[onesub/mock/${tag}] simulated upstream network error`);
27
+ }
28
+ if (outcome.kind === 'valid' || outcome.kind === 'sandbox')
29
+ return true;
30
+ log.warn(`[onesub/mock/${tag}] receipt rejected`, { outcome: outcome.kind });
31
+ return false;
32
+ }
33
+ function deterministicTransactionId(prefix, receipt) {
34
+ const digest = createHash('sha256').update(receipt).digest('hex').slice(0, 24);
35
+ return `${prefix}_${digest}`;
36
+ }
37
+ const HOURS = 60 * 60 * 1000;
38
+ const DAYS = 24 * HOURS;
39
+ export function mockValidateAppleSubscription(receipt) {
40
+ const outcome = classifyMockReceipt(receipt);
41
+ if (!outcomePasses(outcome, 'apple'))
42
+ return null;
43
+ const now = Date.now();
44
+ const expiresAt = outcome.kind === 'sandbox' ? now + 1 * HOURS : now + 30 * DAYS;
45
+ return {
46
+ userId: '',
47
+ productId: 'mock_subscription',
48
+ platform: 'apple',
49
+ status: SUBSCRIPTION_STATUS.ACTIVE,
50
+ expiresAt: new Date(expiresAt).toISOString(),
51
+ originalTransactionId: deterministicTransactionId('mock_apple_orig', receipt),
52
+ purchasedAt: new Date(now).toISOString(),
53
+ willRenew: true,
54
+ };
55
+ }
56
+ export function mockValidateAppleProduct(receipt, expectedProductId) {
57
+ const outcome = classifyMockReceipt(receipt);
58
+ if (!outcomePasses(outcome, 'apple'))
59
+ return null;
60
+ const productId = expectedProductId ?? 'mock_product';
61
+ return {
62
+ transactionId: deterministicTransactionId(`mock_apple_${productId}`, receipt),
63
+ productId,
64
+ purchasedAt: new Date().toISOString(),
65
+ };
66
+ }
67
+ export function mockValidateGoogleSubscription(receipt, productId) {
68
+ const outcome = classifyMockReceipt(receipt);
69
+ if (!outcomePasses(outcome, 'google'))
70
+ return null;
71
+ const now = Date.now();
72
+ const expiresAt = outcome.kind === 'sandbox' ? now + 1 * HOURS : now + 30 * DAYS;
73
+ return {
74
+ userId: '',
75
+ productId,
76
+ platform: 'google',
77
+ status: SUBSCRIPTION_STATUS.ACTIVE,
78
+ expiresAt: new Date(expiresAt).toISOString(),
79
+ originalTransactionId: deterministicTransactionId('mock_google_orig', receipt),
80
+ purchasedAt: new Date(now).toISOString(),
81
+ willRenew: true,
82
+ };
83
+ }
84
+ export function mockValidateGoogleProduct(receipt, productId) {
85
+ const outcome = classifyMockReceipt(receipt);
86
+ if (!outcomePasses(outcome, 'google'))
87
+ return null;
88
+ return {
89
+ transactionId: deterministicTransactionId(`mock_google_${productId}`, receipt),
90
+ purchasedAt: new Date().toISOString(),
91
+ };
92
+ }
93
+ //# sourceMappingURL=mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock.js","sourceRoot":"","sources":["../../src/providers/mock.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EAGV,kBAAkB,EAKnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAIxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,aAAa,EAAE,aAAa,EAC5B,KAAK,EAAE,iBAAiB,EACxB,YAAY,CAAC,EAAE,YAAY,GAC1B,MAAM,GAAG,IAAI,CAiQf"}
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EAGV,kBAAkB,EAKnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAKxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,aAAa,EAAE,aAAa,EAC5B,KAAK,EAAE,iBAAiB,EACxB,YAAY,CAAC,EAAE,YAAY,GAC1B,MAAM,GAAG,IAAI,CA2Sf"}
@@ -0,0 +1,327 @@
1
+ import { Router } from 'express';
2
+ import { z } from 'zod';
3
+ import { PURCHASE_TYPE, ONESUB_ERROR_CODE, ROUTES, SUBSCRIPTION_STATUS } from '@onesub/shared';
4
+ import { evaluateEntitlement } from './entitlements.js';
5
+ import { sendError, sendZodError } from '../errors.js';
6
+ import { fetchAppleSubscriptionStatus } from '../providers/apple.js';
7
+ const ADMIN_SECRET_HEADER = 'x-admin-secret';
8
+ /**
9
+ * Admin routes for testing / operational tasks. Require `X-Admin-Secret`
10
+ * header matching config.adminSecret. If config.adminSecret is not set, the
11
+ * entire admin router is not mounted (returns 404 from the parent router).
12
+ *
13
+ * Endpoints:
14
+ * DELETE /onesub/purchase/admin/:userId/:productId
15
+ * → reset a non-consumable so the user can test the purchase flow again
16
+ *
17
+ * POST /onesub/purchase/admin/grant
18
+ * → manually insert a purchase record (skips store verification)
19
+ * body: { userId, productId, platform, type?, transactionId? }
20
+ *
21
+ * POST /onesub/purchase/admin/transfer
22
+ * → reassign a transactionId to a new userId (device migration)
23
+ *
24
+ * GET /onesub/admin/subscriptions?userId=&status=&productId=&platform=&limit=&offset=
25
+ * → filtered/paginated subscription list (used by dashboard + scripts)
26
+ *
27
+ * GET /onesub/admin/subscriptions/:transactionId
28
+ * → single subscription record by originalTransactionId (dashboard detail page)
29
+ *
30
+ * GET /onesub/admin/customers/:userId
31
+ * → bundle of every record onesub knows for one user (subs + purchases +
32
+ * entitlements when configured). Backs the dashboard customer detail page.
33
+ */
34
+ export function createAdminRouter(config, purchaseStore, store, webhookQueue) {
35
+ if (!config.adminSecret)
36
+ return null;
37
+ const router = Router();
38
+ const adminSecret = config.adminSecret;
39
+ // Auth middleware — applied to both admin scopes. Without this guard, a
40
+ // misconfigured mount on the parent root would expose admin endpoints
41
+ // alongside host-app routes (e.g. /health).
42
+ const adminAuth = (req, res, next) => {
43
+ const provided = req.headers[ADMIN_SECRET_HEADER];
44
+ if (typeof provided !== 'string' || provided !== adminSecret) {
45
+ sendError(res, 401, ONESUB_ERROR_CODE.INVALID_ADMIN_SECRET, 'INVALID_ADMIN_SECRET');
46
+ return;
47
+ }
48
+ next();
49
+ };
50
+ router.use('/onesub/purchase/admin', adminAuth);
51
+ router.use('/onesub/admin', adminAuth);
52
+ // DELETE /onesub/purchase/admin/:userId/:productId
53
+ // Express 5 types route params as `string | string[]` — narrow via zod.
54
+ const resetParamsSchema = z.object({
55
+ userId: z.string().min(1).max(256),
56
+ productId: z.string().min(1).max(256),
57
+ });
58
+ router.delete('/onesub/purchase/admin/:userId/:productId', async (req, res) => {
59
+ let params;
60
+ try {
61
+ params = resetParamsSchema.parse(req.params);
62
+ }
63
+ catch {
64
+ sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, 'userId and productId required');
65
+ return;
66
+ }
67
+ const deleted = await purchaseStore.deletePurchases(params.userId, params.productId);
68
+ res.json({ ok: true, deleted });
69
+ });
70
+ // POST /onesub/purchase/admin/transfer — reassign transactionId to a new userId
71
+ // (legitimate device/account migration)
72
+ const transferSchema = z.object({
73
+ transactionId: z.string().min(1).max(256),
74
+ newUserId: z.string().min(1).max(256),
75
+ });
76
+ router.post('/onesub/purchase/admin/transfer', async (req, res) => {
77
+ let body;
78
+ try {
79
+ body = transferSchema.parse(req.body);
80
+ }
81
+ catch (err) {
82
+ if (err instanceof z.ZodError) {
83
+ sendZodError(res, err);
84
+ return;
85
+ }
86
+ throw err;
87
+ }
88
+ const existing = await purchaseStore.getPurchaseByTransactionId(body.transactionId);
89
+ if (!existing) {
90
+ sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, 'TRANSACTION_NOT_FOUND');
91
+ return;
92
+ }
93
+ // Delete the old row, then save under new userId
94
+ await purchaseStore.deletePurchases(existing.userId, existing.productId);
95
+ const migrated = { ...existing, userId: body.newUserId };
96
+ await purchaseStore.savePurchase(migrated);
97
+ res.json({ ok: true, purchase: migrated });
98
+ });
99
+ // POST /onesub/purchase/admin/grant
100
+ const grantSchema = z.object({
101
+ userId: z.string().min(1).max(256),
102
+ productId: z.string().min(1).max(256),
103
+ platform: z.enum(['apple', 'google']),
104
+ type: z.enum([PURCHASE_TYPE.CONSUMABLE, PURCHASE_TYPE.NON_CONSUMABLE]).default(PURCHASE_TYPE.NON_CONSUMABLE),
105
+ transactionId: z.string().min(1).max(256).optional(),
106
+ });
107
+ router.post('/onesub/purchase/admin/grant', async (req, res) => {
108
+ let body;
109
+ try {
110
+ body = grantSchema.parse(req.body);
111
+ }
112
+ catch (err) {
113
+ if (err instanceof z.ZodError) {
114
+ sendZodError(res, err);
115
+ return;
116
+ }
117
+ throw err;
118
+ }
119
+ const transactionId = body.transactionId ?? `admin_grant_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
120
+ const purchase = {
121
+ transactionId,
122
+ userId: body.userId,
123
+ productId: body.productId,
124
+ platform: body.platform,
125
+ type: body.type,
126
+ quantity: 1,
127
+ purchasedAt: new Date().toISOString(),
128
+ };
129
+ await purchaseStore.savePurchase(purchase);
130
+ res.json({ ok: true, purchase });
131
+ });
132
+ // GET /onesub/admin/subscriptions?userId=&status=&productId=&platform=&limit=&offset=
133
+ // Filtered + paginated subscription list. Backs the dashboard's
134
+ // subscriptions page and ad-hoc operational scripts.
135
+ const listQuerySchema = z.object({
136
+ userId: z.string().min(1).max(256).optional(),
137
+ status: z.enum([
138
+ SUBSCRIPTION_STATUS.ACTIVE,
139
+ SUBSCRIPTION_STATUS.GRACE_PERIOD,
140
+ SUBSCRIPTION_STATUS.ON_HOLD,
141
+ SUBSCRIPTION_STATUS.PAUSED,
142
+ SUBSCRIPTION_STATUS.EXPIRED,
143
+ SUBSCRIPTION_STATUS.CANCELED,
144
+ SUBSCRIPTION_STATUS.NONE,
145
+ ]).optional(),
146
+ productId: z.string().min(1).max(256).optional(),
147
+ platform: z.enum(['apple', 'google']).optional(),
148
+ // Cap at 200 server-side so a runaway dashboard query can't tip the DB.
149
+ limit: z.coerce.number().int().positive().max(200).optional(),
150
+ offset: z.coerce.number().int().nonnegative().optional(),
151
+ });
152
+ router.get(ROUTES.ADMIN_SUBSCRIPTIONS, async (req, res) => {
153
+ let query;
154
+ try {
155
+ query = listQuerySchema.parse(req.query);
156
+ }
157
+ catch (err) {
158
+ if (err instanceof z.ZodError) {
159
+ sendZodError(res, err);
160
+ return;
161
+ }
162
+ throw err;
163
+ }
164
+ try {
165
+ const result = await store.listFiltered(query);
166
+ const response = {
167
+ items: result.items,
168
+ total: result.total,
169
+ limit: result.limit,
170
+ offset: result.offset,
171
+ };
172
+ res.status(200).json(response);
173
+ }
174
+ catch (err) {
175
+ // Bubble the message up but not the stack — admin clients log status code
176
+ sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? 'list error');
177
+ }
178
+ });
179
+ // GET /onesub/admin/subscriptions/:transactionId — single record by
180
+ // originalTransactionId. Backs the dashboard's subscription detail page.
181
+ const detailParamsSchema = z.object({
182
+ transactionId: z.string().min(1).max(256),
183
+ });
184
+ router.get('/onesub/admin/subscriptions/:transactionId', async (req, res) => {
185
+ let params;
186
+ try {
187
+ params = detailParamsSchema.parse(req.params);
188
+ }
189
+ catch {
190
+ sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, 'transactionId required');
191
+ return;
192
+ }
193
+ try {
194
+ const sub = await store.getByTransactionId(params.transactionId);
195
+ if (!sub) {
196
+ sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, 'TRANSACTION_NOT_FOUND');
197
+ return;
198
+ }
199
+ res.status(200).json(sub);
200
+ }
201
+ catch (err) {
202
+ sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? 'detail error');
203
+ }
204
+ });
205
+ // GET /onesub/admin/customers/:userId — full per-user profile. Combines the
206
+ // store calls a CS dashboard makes for "show me everything about this user"
207
+ // into a single round-trip.
208
+ //
209
+ // Always returns 200 (even for unknown userIds) — empty arrays + no
210
+ // entitlements means "we have no record of this user", which is a valid
211
+ // signal for the dashboard to render rather than an error condition.
212
+ const customerParamsSchema = z.object({
213
+ userId: z.string().min(1).max(256),
214
+ });
215
+ router.get('/onesub/admin/customers/:userId', async (req, res) => {
216
+ let params;
217
+ try {
218
+ params = customerParamsSchema.parse(req.params);
219
+ }
220
+ catch {
221
+ sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, 'userId required');
222
+ return;
223
+ }
224
+ try {
225
+ const [subscriptions, purchases] = await Promise.all([
226
+ store.getAllByUserId(params.userId),
227
+ purchaseStore.getPurchasesByUserId(params.userId),
228
+ ]);
229
+ // Evaluate entitlements only when configured. Hosts that don't use the
230
+ // entitlement abstraction get the field omitted (dashboard hides panel).
231
+ let entitlements;
232
+ if (config.entitlements && Object.keys(config.entitlements).length > 0) {
233
+ entitlements = {};
234
+ for (const [id, def] of Object.entries(config.entitlements)) {
235
+ entitlements[id] = await evaluateEntitlement(params.userId, def, store, purchaseStore);
236
+ }
237
+ }
238
+ const response = {
239
+ userId: params.userId,
240
+ subscriptions,
241
+ purchases,
242
+ ...(entitlements ? { entitlements } : {}),
243
+ };
244
+ res.status(200).json(response);
245
+ }
246
+ catch (err) {
247
+ sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? 'customer error');
248
+ }
249
+ });
250
+ // POST /onesub/admin/sync-apple/:originalTransactionId
251
+ // Fetch the current subscription state directly from Apple's Status API and
252
+ // upsert into the local store. Useful for reconciliation / missed-webhook recovery.
253
+ const syncAppleParamsSchema = z.object({
254
+ originalTransactionId: z.string().min(1).max(256),
255
+ });
256
+ router.post(ROUTES.ADMIN_SYNC_APPLE, async (req, res) => {
257
+ let params;
258
+ try {
259
+ params = syncAppleParamsSchema.parse(req.params);
260
+ }
261
+ catch {
262
+ sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, 'originalTransactionId required');
263
+ return;
264
+ }
265
+ if (!config.apple?.issuerId || !config.apple?.keyId || !config.apple?.privateKey) {
266
+ sendError(res, 400, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, 'Apple API credentials not configured');
267
+ return;
268
+ }
269
+ try {
270
+ const { originalTransactionId } = params;
271
+ // Allow callers to force sandbox mode; otherwise infer from the stored record.
272
+ const existingForEnv = await store.getByTransactionId(originalTransactionId);
273
+ const isSandbox = req.query['sandbox'] === 'true' ||
274
+ existingForEnv?.environment === 'Sandbox';
275
+ const fresh = await fetchAppleSubscriptionStatus(originalTransactionId, config.apple, { sandbox: isSandbox });
276
+ if (!fresh) {
277
+ sendError(res, 404, ONESUB_ERROR_CODE.TRANSACTION_NOT_FOUND, 'Apple Status API returned no record');
278
+ return;
279
+ }
280
+ const existing = existingForEnv;
281
+ fresh.userId = existing?.userId ?? originalTransactionId;
282
+ await store.save(fresh);
283
+ res.status(200).json({ ok: true, subscription: fresh });
284
+ }
285
+ catch (err) {
286
+ sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? 'sync error');
287
+ }
288
+ });
289
+ // GET /onesub/admin/webhook-deadletters
290
+ // POST /onesub/admin/webhook-replay/:id
291
+ // Only mounted when the configured queue supports dead-letter inspection
292
+ // (the default in-process queue does not — Apple/Google retries are the
293
+ // durability layer for that mode).
294
+ if (webhookQueue?.listDeadLetters) {
295
+ router.get('/onesub/admin/webhook-deadletters', async (_req, res) => {
296
+ try {
297
+ const items = await webhookQueue.listDeadLetters();
298
+ res.status(200).json({ items });
299
+ }
300
+ catch (err) {
301
+ sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? 'list error');
302
+ }
303
+ });
304
+ }
305
+ if (webhookQueue?.replayDeadLetter) {
306
+ const replayParamsSchema = z.object({ id: z.string().min(1).max(256) });
307
+ router.post('/onesub/admin/webhook-replay/:id', async (req, res) => {
308
+ let params;
309
+ try {
310
+ params = replayParamsSchema.parse(req.params);
311
+ }
312
+ catch {
313
+ sendError(res, 400, ONESUB_ERROR_CODE.INVALID_INPUT, 'id required');
314
+ return;
315
+ }
316
+ try {
317
+ await webhookQueue.replayDeadLetter(params.id);
318
+ res.status(200).json({ ok: true });
319
+ }
320
+ catch (err) {
321
+ sendError(res, 500, ONESUB_ERROR_CODE.STORE_ERROR, err.message ?? 'replay error');
322
+ }
323
+ });
324
+ }
325
+ return router;
326
+ }
327
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.js","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { Router } from 'express';
2
+ import type { OneSubServerConfig } from '@onesub/shared';
3
+ /**
4
+ * POST /onesub/apple/offer-signature
5
+ *
6
+ * Sign an Apple Promotional Offer payload server-side.
7
+ * Requires config.apple.offerKeyId and config.apple.offerPrivateKey.
8
+ *
9
+ * Authentication: if config.adminSecret is set, the request must carry
10
+ * `X-Onesub-Offer-Secret: <adminSecret>` (same value). Hosts that mount
11
+ * onesub without adminSecret are responsible for securing this endpoint
12
+ * themselves (e.g. behind their own auth middleware).
13
+ *
14
+ * Body: { productId, offerId, applicationUsername }
15
+ * Response: { keyId, nonce, timestamp, signature }
16
+ *
17
+ * The client passes these four values to StoreKit's
18
+ * Product.SubscriptionOffer.Signature to redeem the offer.
19
+ */
20
+ export declare function createAppleOfferRouter(config: OneSubServerConfig): Router | null;
21
+ //# sourceMappingURL=apple-offer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-offer.d.ts","sourceRoot":"","sources":["../../src/routes/apple-offer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAazD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CA+ChF"}
@@ -0,0 +1,72 @@
1
+ import { Router } from 'express';
2
+ import { z } from 'zod';
3
+ import { ONESUB_ERROR_CODE } from '@onesub/shared';
4
+ import { signApplePromotionalOffer } from '../providers/apple.js';
5
+ import { sendError, sendZodError } from '../errors.js';
6
+ const OFFER_SECRET_HEADER = 'x-onesub-offer-secret';
7
+ const offerBodySchema = z.object({
8
+ productId: z.string().min(1).max(256),
9
+ offerId: z.string().min(1).max(256),
10
+ applicationUsername: z.string().min(1).max(256),
11
+ });
12
+ /**
13
+ * POST /onesub/apple/offer-signature
14
+ *
15
+ * Sign an Apple Promotional Offer payload server-side.
16
+ * Requires config.apple.offerKeyId and config.apple.offerPrivateKey.
17
+ *
18
+ * Authentication: if config.adminSecret is set, the request must carry
19
+ * `X-Onesub-Offer-Secret: <adminSecret>` (same value). Hosts that mount
20
+ * onesub without adminSecret are responsible for securing this endpoint
21
+ * themselves (e.g. behind their own auth middleware).
22
+ *
23
+ * Body: { productId, offerId, applicationUsername }
24
+ * Response: { keyId, nonce, timestamp, signature }
25
+ *
26
+ * The client passes these four values to StoreKit's
27
+ * Product.SubscriptionOffer.Signature to redeem the offer.
28
+ */
29
+ export function createAppleOfferRouter(config) {
30
+ const apple = config.apple;
31
+ if (!apple?.offerKeyId || !apple?.offerPrivateKey)
32
+ return null;
33
+ const router = Router();
34
+ router.post('/onesub/apple/offer-signature', async (req, res) => {
35
+ if (config.adminSecret) {
36
+ const provided = req.headers[OFFER_SECRET_HEADER];
37
+ if (typeof provided !== 'string' || provided !== config.adminSecret) {
38
+ sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, 'Unauthorized');
39
+ return;
40
+ }
41
+ }
42
+ let body;
43
+ try {
44
+ body = offerBodySchema.parse(req.body);
45
+ }
46
+ catch (err) {
47
+ if (err instanceof z.ZodError) {
48
+ sendZodError(res, err);
49
+ return;
50
+ }
51
+ throw err;
52
+ }
53
+ if (!apple.bundleId) {
54
+ sendError(res, 400, ONESUB_ERROR_CODE.APPLE_CONFIG_MISSING, 'config.apple.bundleId is required for offer signing');
55
+ return;
56
+ }
57
+ try {
58
+ const result = await signApplePromotionalOffer({
59
+ bundleId: apple.bundleId,
60
+ productId: body.productId,
61
+ offerId: body.offerId,
62
+ applicationUsername: body.applicationUsername,
63
+ }, apple);
64
+ res.status(200).json(result);
65
+ }
66
+ catch (err) {
67
+ sendError(res, 500, ONESUB_ERROR_CODE.INTERNAL_ERROR, err.message ?? 'Offer signing failed');
68
+ }
69
+ });
70
+ return router;
71
+ }
72
+ //# sourceMappingURL=apple-offer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-offer.js","sourceRoot":"","sources":["../../src/routes/apple-offer.ts"],"names":[],"mappings":"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"}