@onesub/server 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,15 @@ import { createOneSubMiddleware, PostgresSubscriptionStore, PostgresPurchaseStor
24
24
  const app = express();
25
25
 
26
26
  app.use(createOneSubMiddleware({
27
- apple: { bundleId: 'com.yourapp.id', sharedSecret: process.env.APPLE_SHARED_SECRET },
27
+ apple: {
28
+ bundleId: 'com.yourapp.id',
29
+ sharedSecret: process.env.APPLE_SHARED_SECRET,
30
+ // Optional — required only for the App Store Server API features below
31
+ // (status fetch fallback, consumption response).
32
+ keyId: process.env.APPLE_KEY_ID,
33
+ issuerId: process.env.APPLE_ISSUER_ID,
34
+ privateKey: process.env.APPLE_PRIVATE_KEY,
35
+ },
28
36
  google: { packageName: 'com.yourapp.id', serviceAccountKey: process.env.GOOGLE_SERVICE_ACCOUNT_KEY },
29
37
  database: { url: process.env.DATABASE_URL },
30
38
  store: new PostgresSubscriptionStore(process.env.DATABASE_URL),
@@ -32,6 +40,7 @@ app.use(createOneSubMiddleware({
32
40
  // Optional:
33
41
  adminSecret: process.env.ADMIN_SECRET, // enables /onesub/purchase/admin/*
34
42
  logger: require('pino')(), // any { info, warn, error } logger
43
+ refundPolicy: 'immediate', // 'immediate' (default) | 'until_expiry'
35
44
  }));
36
45
 
37
46
  app.listen(4100);
@@ -51,10 +60,86 @@ app.listen(4100);
51
60
  | `POST /onesub/purchase/admin/grant` | Manually grant a purchase (requires `adminSecret`) |
52
61
  | `POST /onesub/purchase/admin/transfer` | Reassign a `transactionId` to a new `userId` (requires `adminSecret`) |
53
62
 
63
+ ## Lifecycle states (`0.4.0+`)
64
+
65
+ `SubscriptionInfo.status` carries the full lifecycle. The status route's `active: boolean` is computed as `(active || grace_period) && expiresAt > now` — but the raw status lets you render accurate UX:
66
+
67
+ | Status | active | UX hint |
68
+ |--------|--------|---------|
69
+ | `active` | ✅ | normal |
70
+ | `grace_period` | ✅ | "결제 정보 확인 필요 (계속 사용 가능)" |
71
+ | `on_hold` | ❌ | "결제 정보를 업데이트하세요" |
72
+ | `paused` | ❌ | "재개 예정: \{autoResumeTime\}" |
73
+ | `expired` / `canceled` | ❌ | re-purchase or restore |
74
+
75
+ See [`@onesub/shared` README](../shared/README.md) for the full mapping.
76
+
77
+ ## Refund policy (`0.8.0+`)
78
+
79
+ ```ts
80
+ refundPolicy: 'immediate' | 'until_expiry' // default 'immediate'
81
+ ```
82
+
83
+ - `'immediate'` — subscription refunds (Apple `REFUND`/`REVOKE`, Google voided productType=1) flip status to `canceled` right away. Strict, fraud-resistant.
84
+ - `'until_expiry'` — keep `status`/`expiresAt` untouched, only flip `willRenew = false`. User keeps entitlement until the original expiry. Better UX for goodwill refunds.
85
+
86
+ IAP refunds (consumable / non-consumable) are **always immediate** regardless of policy — they have no expiry concept.
87
+
88
+ ## Optional Apple App Store Server API features (`0.8.0+`)
89
+
90
+ Set `apple.keyId` / `apple.issuerId` / `apple.privateKey` (PKCS8 ES256 from App Store Connect → Users and Access → Keys) to unlock:
91
+
92
+ ### Status API fallback (automatic)
93
+
94
+ If a webhook arrives for an `originalTransactionId` the store doesn't know (server downtime, queue truncation, fresh install), the webhook handler calls `GET /inApps/v1/subscriptions/{originalTransactionId}` to fetch canonical state from Apple and saves a record under a placeholder `userId`. Subsequent `/onesub/validate` from the host can claim ownership.
95
+
96
+ You can also call it directly:
97
+
98
+ ```ts
99
+ import { fetchAppleSubscriptionStatus } from '@onesub/server';
100
+
101
+ const sub = await fetchAppleSubscriptionStatus(originalTxId, config.apple, { sandbox: false });
102
+ // sub: SubscriptionInfo | null — null on missing creds / 404 / network failure
103
+ ```
104
+
105
+ ### CONSUMPTION_REQUEST response hook
106
+
107
+ When Apple sends a `CONSUMPTION_REQUEST` notification (consumable refund review), without a hook Apple has no usage signal and tends to grant the refund. Provide a hook to PUT consumption info back to `/inApps/v1/transactions/consumption/{txId}`:
108
+
109
+ ```ts
110
+ apple: {
111
+ // ...
112
+ consumptionInfoProvider: async (ctx) => ({
113
+ customerConsented: true, // required; false makes Apple ignore the response
114
+ consumptionStatus: 3, // 0=undeclared, 1=not consumed, 2=partial, 3=full
115
+ deliveryStatus: 1, // 0=undeclared, 1=delivered & working, 2=quality issue, ...
116
+ refundPreference: 2, // 0=undeclared, 1=grant, 2=decline, 3=no preference
117
+ // see AppleConsumptionRequest for the full set of optional fields
118
+ }),
119
+ }
120
+ ```
121
+
122
+ Fire-and-forget; failures are logged but don't block the webhook 200.
123
+
124
+ ## Optional Google hooks (`0.8.0+`)
125
+
126
+ ```ts
127
+ google: {
128
+ // ...
129
+ // Called when SUBSCRIPTION_PRICE_CHANGE_CONFIRMED (8) arrives — user agreed
130
+ // to a price change; new price applies on next renewal. Useful for analytics.
131
+ onPriceChangeConfirmed: async (ctx) => {
132
+ await analytics.track('price_change_confirmed', ctx);
133
+ },
134
+ }
135
+ ```
136
+
54
137
  ## Schema
55
138
 
56
139
  Canonical Postgres DDL shipped at [`sql/schema.sql`](./sql/schema.sql). Apply with `psql -f` or let `store.initSchema()` run it for you on startup.
57
140
 
141
+ `store.initSchema()` is **safe to call on every boot** — all DDL is `IF NOT EXISTS`. New columns added in later releases (e.g. `linked_purchase_token`, `auto_resume_time`) ship with `ALTER TABLE IF NOT EXISTS` so existing installs auto-backfill on the next startup.
142
+
58
143
  ## Security
59
144
 
60
145
  - Apple JWS signature verified end-to-end against **Apple Root CA G3** (as of `0.6.0`)
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Entitlements abstraction tests.
3
+ *
4
+ * Covers:
5
+ * - evaluateEntitlement helper (subscription / purchase / mixed / none /
6
+ * expired sub / wrong status / consumable excluded)
7
+ * - GET /onesub/entitlement single-check route (404 unknown id, 400 bad input)
8
+ * - GET /onesub/entitlements bulk route
9
+ * - Router not mounted when config.entitlements is absent (404)
10
+ * - getAllByUserId returns multi-product subscriptions (regression for the
11
+ * InMemoryStore change that backs entitlements)
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=entitlements.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitlements.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/entitlements.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Entitlements abstraction tests.
3
+ *
4
+ * Covers:
5
+ * - evaluateEntitlement helper (subscription / purchase / mixed / none /
6
+ * expired sub / wrong status / consumable excluded)
7
+ * - GET /onesub/entitlement single-check route (404 unknown id, 400 bad input)
8
+ * - GET /onesub/entitlements bulk route
9
+ * - Router not mounted when config.entitlements is absent (404)
10
+ * - getAllByUserId returns multi-product subscriptions (regression for the
11
+ * InMemoryStore change that backs entitlements)
12
+ */
13
+ import { describe, it, expect, beforeEach } from 'vitest';
14
+ import express from 'express';
15
+ import { SUBSCRIPTION_STATUS, PURCHASE_TYPE } from '@onesub/shared';
16
+ import { createOneSubMiddleware } from '../index.js';
17
+ import { evaluateEntitlement } from '../routes/entitlements.js';
18
+ import { InMemorySubscriptionStore, InMemoryPurchaseStore } from '../store.js';
19
+ // ── helpers ─────────────────────────────────────────────────────────────────
20
+ const futureExpiry = '2099-01-01T00:00:00.000Z';
21
+ const pastExpiry = '2024-01-01T00:00:00.000Z';
22
+ function sub(overrides) {
23
+ return {
24
+ userId: 'u',
25
+ productId: 'pro_monthly',
26
+ platform: 'apple',
27
+ status: 'active',
28
+ expiresAt: futureExpiry,
29
+ originalTransactionId: `orig_${Math.random()}`,
30
+ purchasedAt: '2026-01-01T00:00:00.000Z',
31
+ willRenew: true,
32
+ ...overrides,
33
+ };
34
+ }
35
+ function purchase(overrides) {
36
+ return {
37
+ userId: 'u',
38
+ productId: 'lifetime_pass',
39
+ platform: 'apple',
40
+ type: 'non_consumable',
41
+ transactionId: `tx_${Math.random()}`,
42
+ purchasedAt: '2026-01-01T00:00:00.000Z',
43
+ quantity: 1,
44
+ ...overrides,
45
+ };
46
+ }
47
+ function spinUp(handler) {
48
+ return {
49
+ async get(path) {
50
+ const httpServer = handler.listen(0);
51
+ const port = httpServer.address().port;
52
+ try {
53
+ const resp = await fetch(`http://127.0.0.1:${port}${path}`);
54
+ const text = await resp.text();
55
+ let parsed = text;
56
+ try {
57
+ parsed = JSON.parse(text);
58
+ }
59
+ catch { /* keep as text */ }
60
+ return { status: resp.status, body: parsed };
61
+ }
62
+ finally {
63
+ await new Promise((r) => httpServer.close(() => r()));
64
+ }
65
+ },
66
+ };
67
+ }
68
+ function buildServer(opts) {
69
+ const store = new InMemorySubscriptionStore();
70
+ const purchaseStore = new InMemoryPurchaseStore();
71
+ const config = {
72
+ apple: { bundleId: 'com.example.app', skipJwsVerification: true },
73
+ database: { url: '' },
74
+ entitlements: opts.entitlements,
75
+ };
76
+ const app = express();
77
+ // createOneSubMiddleware mounts express.json itself, but the entitlement
78
+ // routes are GET-only so it's fine either way.
79
+ const middleware = createOneSubMiddleware({ ...config, store, purchaseStore });
80
+ app.use(middleware);
81
+ return { store, purchaseStore, server: spinUp(app) };
82
+ }
83
+ const PREMIUM = {
84
+ premium: { productIds: ['pro_monthly', 'pro_yearly', 'lifetime_pass'] },
85
+ };
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+ // evaluateEntitlement (unit, no HTTP)
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+ describe('evaluateEntitlement', () => {
90
+ let store;
91
+ let purchaseStore;
92
+ beforeEach(() => {
93
+ store = new InMemorySubscriptionStore();
94
+ purchaseStore = new InMemoryPurchaseStore();
95
+ });
96
+ it('active subscription matching one of the productIds → entitled', async () => {
97
+ await store.save(sub({ userId: 'u1', productId: 'pro_yearly' }));
98
+ const result = await evaluateEntitlement('u1', PREMIUM.premium, store, purchaseStore);
99
+ expect(result.active).toBe(true);
100
+ expect(result.source).toBe('subscription');
101
+ expect(result.productId).toBe('pro_yearly');
102
+ expect(result.expiresAt).toBe(futureExpiry);
103
+ });
104
+ it('non-consumable purchase matching one of the productIds → entitled', async () => {
105
+ await purchaseStore.savePurchase(purchase({ userId: 'u2', productId: 'lifetime_pass' }));
106
+ const result = await evaluateEntitlement('u2', PREMIUM.premium, store, purchaseStore);
107
+ expect(result.active).toBe(true);
108
+ expect(result.source).toBe('purchase');
109
+ expect(result.productId).toBe('lifetime_pass');
110
+ expect(result.expiresAt).toBeUndefined();
111
+ });
112
+ it('no matching record → not entitled', async () => {
113
+ const result = await evaluateEntitlement('u_none', PREMIUM.premium, store, purchaseStore);
114
+ expect(result).toEqual({ active: false, source: null });
115
+ });
116
+ it('subscription preferred over purchase when both match', async () => {
117
+ await store.save(sub({ userId: 'u3', productId: 'pro_monthly' }));
118
+ await purchaseStore.savePurchase(purchase({ userId: 'u3', productId: 'lifetime_pass' }));
119
+ const result = await evaluateEntitlement('u3', PREMIUM.premium, store, purchaseStore);
120
+ expect(result.source).toBe('subscription');
121
+ expect(result.productId).toBe('pro_monthly');
122
+ });
123
+ it('expired subscription (expiresAt < now) → not entitled even if status=active', async () => {
124
+ await store.save(sub({ userId: 'u4', productId: 'pro_monthly', expiresAt: pastExpiry }));
125
+ const result = await evaluateEntitlement('u4', PREMIUM.premium, store, purchaseStore);
126
+ expect(result.active).toBe(false);
127
+ });
128
+ it('grace_period subscription → entitled (still has valid expiresAt)', async () => {
129
+ await store.save(sub({ userId: 'u5', productId: 'pro_monthly', status: 'grace_period' }));
130
+ const result = await evaluateEntitlement('u5', PREMIUM.premium, store, purchaseStore);
131
+ expect(result.active).toBe(true);
132
+ expect(result.source).toBe('subscription');
133
+ });
134
+ it('on_hold / paused / canceled / expired status → not entitled', async () => {
135
+ for (const status of ['on_hold', 'paused', 'canceled', 'expired']) {
136
+ const localStore = new InMemorySubscriptionStore();
137
+ await localStore.save(sub({ userId: 'u', productId: 'pro_monthly', status }));
138
+ const result = await evaluateEntitlement('u', PREMIUM.premium, localStore, purchaseStore);
139
+ expect(result.active).toBe(false);
140
+ }
141
+ });
142
+ it('consumable purchase is NOT considered for entitlement', async () => {
143
+ await purchaseStore.savePurchase(purchase({
144
+ userId: 'u6',
145
+ productId: 'pro_monthly', // even matching productId
146
+ type: 'consumable',
147
+ }));
148
+ const result = await evaluateEntitlement('u6', PREMIUM.premium, store, purchaseStore);
149
+ expect(result.active).toBe(false);
150
+ });
151
+ it('multi-product subscriptions: returns the first matching active sub', async () => {
152
+ // User has two subs — one for pro_yearly (matches), one for off-premium product (no match)
153
+ await store.save(sub({ userId: 'u7', productId: 'unrelated_product' }));
154
+ await store.save(sub({ userId: 'u7', productId: 'pro_yearly' }));
155
+ const result = await evaluateEntitlement('u7', PREMIUM.premium, store, purchaseStore);
156
+ expect(result.active).toBe(true);
157
+ expect(result.productId).toBe('pro_yearly');
158
+ });
159
+ });
160
+ // ─────────────────────────────────────────────────────────────────────────────
161
+ // HTTP routes
162
+ // ─────────────────────────────────────────────────────────────────────────────
163
+ describe('GET /onesub/entitlement', () => {
164
+ it('returns active=true when user has matching active sub', async () => {
165
+ const { store, server } = buildServer({ entitlements: PREMIUM });
166
+ await store.save(sub({ userId: 'u_yes', productId: 'pro_monthly' }));
167
+ const resp = await server.get('/onesub/entitlement?userId=u_yes&id=premium');
168
+ expect(resp.status).toBe(200);
169
+ expect(resp.body.id).toBe('premium');
170
+ expect(resp.body.active).toBe(true);
171
+ expect(resp.body.source).toBe('subscription');
172
+ });
173
+ it('returns active=false when no matching record', async () => {
174
+ const { server } = buildServer({ entitlements: PREMIUM });
175
+ const resp = await server.get('/onesub/entitlement?userId=u_none&id=premium');
176
+ expect(resp.status).toBe(200);
177
+ expect(resp.body.active).toBe(false);
178
+ expect(resp.body.source).toBeNull();
179
+ });
180
+ it('404 + ENTITLEMENT_NOT_FOUND for unknown id', async () => {
181
+ const { server } = buildServer({ entitlements: PREMIUM });
182
+ const resp = await server.get('/onesub/entitlement?userId=u&id=enterprise');
183
+ expect(resp.status).toBe(404);
184
+ expect(resp.body.errorCode).toBe('ENTITLEMENT_NOT_FOUND');
185
+ });
186
+ it('400 INVALID_INPUT when userId or id is missing', async () => {
187
+ const { server } = buildServer({ entitlements: PREMIUM });
188
+ const resp = await server.get('/onesub/entitlement?userId=u');
189
+ expect(resp.status).toBe(400);
190
+ expect(resp.body.errorCode).toBe('INVALID_INPUT');
191
+ });
192
+ });
193
+ describe('GET /onesub/entitlements (bulk)', () => {
194
+ it('evaluates every configured entitlement in one round-trip', async () => {
195
+ const config = {
196
+ premium: { productIds: ['pro_monthly', 'pro_yearly'] },
197
+ promode: { productIds: ['dev_tools_addon'] },
198
+ };
199
+ const { store, server } = buildServer({ entitlements: config });
200
+ await store.save(sub({ userId: 'u_bulk', productId: 'pro_yearly' }));
201
+ const resp = await server.get('/onesub/entitlements?userId=u_bulk');
202
+ expect(resp.status).toBe(200);
203
+ expect(resp.body.entitlements.premium.active).toBe(true);
204
+ expect(resp.body.entitlements.premium.productId).toBe('pro_yearly');
205
+ expect(resp.body.entitlements.promode.active).toBe(false);
206
+ });
207
+ it('returns empty entitlement statuses for a user with nothing', async () => {
208
+ const config = {
209
+ premium: { productIds: ['pro_monthly'] },
210
+ addon: { productIds: ['some_addon'] },
211
+ };
212
+ const { server } = buildServer({ entitlements: config });
213
+ const resp = await server.get('/onesub/entitlements?userId=u_empty');
214
+ expect(resp.body.entitlements.premium.active).toBe(false);
215
+ expect(resp.body.entitlements.addon.active).toBe(false);
216
+ });
217
+ });
218
+ describe('Entitlement routes are NOT mounted without config', () => {
219
+ it('returns 404 when config.entitlements is absent', async () => {
220
+ const { server } = buildServer({}); // no entitlements
221
+ const resp = await server.get('/onesub/entitlement?userId=u&id=premium');
222
+ expect(resp.status).toBe(404);
223
+ const respBulk = await server.get('/onesub/entitlements?userId=u');
224
+ expect(respBulk.status).toBe(404);
225
+ });
226
+ it('returns 404 when config.entitlements is empty {}', async () => {
227
+ const { server } = buildServer({ entitlements: {} });
228
+ const resp = await server.get('/onesub/entitlement?userId=u&id=premium');
229
+ expect(resp.status).toBe(404);
230
+ });
231
+ });
232
+ // ─────────────────────────────────────────────────────────────────────────────
233
+ // Store regression: getAllByUserId returns multi-product subs
234
+ // ─────────────────────────────────────────────────────────────────────────────
235
+ describe('InMemorySubscriptionStore — multi-sub per user (entitlement support)', () => {
236
+ it('getAllByUserId returns all subs across productIds, latest first', async () => {
237
+ const store = new InMemorySubscriptionStore();
238
+ await store.save(sub({ userId: 'u', productId: 'pro_monthly', originalTransactionId: 'orig_a' }));
239
+ await store.save(sub({ userId: 'u', productId: 'pro_yearly', originalTransactionId: 'orig_b' }));
240
+ await store.save(sub({ userId: 'u', productId: 'addon', originalTransactionId: 'orig_c' }));
241
+ const all = await store.getAllByUserId('u');
242
+ expect(all.map((s) => s.productId)).toEqual(['addon', 'pro_yearly', 'pro_monthly']);
243
+ });
244
+ it('getByUserId still returns the most recent (legacy contract preserved)', async () => {
245
+ const store = new InMemorySubscriptionStore();
246
+ await store.save(sub({ userId: 'u', productId: 'pro_monthly', originalTransactionId: 'orig_a' }));
247
+ await store.save(sub({ userId: 'u', productId: 'pro_yearly', originalTransactionId: 'orig_b' }));
248
+ const latest = await store.getByUserId('u');
249
+ expect(latest?.productId).toBe('pro_yearly');
250
+ });
251
+ it('save() with same originalTransactionId replaces prior record (no duplicates)', async () => {
252
+ const store = new InMemorySubscriptionStore();
253
+ await store.save(sub({
254
+ userId: 'u', productId: 'pro_monthly', originalTransactionId: 'orig_x',
255
+ status: 'active',
256
+ }));
257
+ await store.save(sub({
258
+ userId: 'u', productId: 'pro_monthly', originalTransactionId: 'orig_x',
259
+ status: 'canceled',
260
+ }));
261
+ const all = await store.getAllByUserId('u');
262
+ expect(all).toHaveLength(1);
263
+ expect(all[0].status).toBe(SUBSCRIPTION_STATUS.CANCELED);
264
+ });
265
+ });
266
+ // keep the import honest
267
+ PURCHASE_TYPE;
268
+ //# sourceMappingURL=entitlements.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitlements.test.js","sourceRoot":"","sources":["../../src/__tests__/entitlements.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * End-to-end lifecycle scenarios — exercise multi-notification sequences against
3
+ * the real webhook router + status route + stores, the way Apple/Google would
4
+ * deliver them in production.
5
+ *
6
+ * Single-notification correctness is covered by the per-feature unit tests; this
7
+ * file catches issues that only show up when notifications arrive in a sequence:
8
+ * stale state bleeding across transitions, idempotency under replay, recovery
9
+ * paths after a missed webhook, etc.
10
+ *
11
+ * Why scenarios instead of more unit tests:
12
+ * - The webhook handler has many branches (per-notification mapping, fresh
13
+ * re-fetch, refundPolicy gating, hooks). Each is unit-tested in isolation,
14
+ * but a transition like SUBSCRIBED → DID_FAIL_TO_RENEW(GRACE) → DID_RENEW
15
+ * touches three of those branches in turn — a regression in any one only
16
+ * surfaces when run together.
17
+ * - These scenarios mirror the runbook for sandbox verification, so the
18
+ * mock results here can be cross-checked against real sandbox behaviour.
19
+ */
20
+ export {};
21
+ //# sourceMappingURL=lifecycle-scenarios.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-scenarios.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/lifecycle-scenarios.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}