@onesub/server 0.11.4 → 0.12.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 (127) hide show
  1. package/README.md +119 -2
  2. package/dist/__tests__/cache.test.d.ts +2 -0
  3. package/dist/__tests__/cache.test.d.ts.map +1 -0
  4. package/dist/__tests__/openapi.test.d.ts +2 -0
  5. package/dist/__tests__/openapi.test.d.ts.map +1 -0
  6. package/dist/__tests__/redis-store.test.d.ts +2 -0
  7. package/dist/__tests__/redis-store.test.d.ts.map +1 -0
  8. package/dist/__tests__/webhook-apple.test.d.ts +12 -0
  9. package/dist/__tests__/webhook-apple.test.d.ts.map +1 -0
  10. package/dist/__tests__/webhook-events.test.d.ts +2 -0
  11. package/dist/__tests__/webhook-events.test.d.ts.map +1 -0
  12. package/dist/__tests__/webhook-google.test.d.ts +13 -0
  13. package/dist/__tests__/webhook-google.test.d.ts.map +1 -0
  14. package/dist/__tests__/webhook-queue.test.d.ts +2 -0
  15. package/dist/__tests__/webhook-queue.test.d.ts.map +1 -0
  16. package/dist/cache.d.ts +32 -0
  17. package/dist/cache.d.ts.map +1 -0
  18. package/dist/index.cjs +3084 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +34 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3045 -118
  23. package/dist/index.js.map +1 -1
  24. package/dist/openapi.d.ts +41 -0
  25. package/dist/openapi.d.ts.map +1 -0
  26. package/dist/providers/apple.d.ts +1 -1
  27. package/dist/providers/apple.d.ts.map +1 -1
  28. package/dist/providers/google.d.ts.map +1 -1
  29. package/dist/routes/admin.d.ts +2 -1
  30. package/dist/routes/admin.d.ts.map +1 -1
  31. package/dist/routes/webhook-apple.d.ts +6 -0
  32. package/dist/routes/webhook-apple.d.ts.map +1 -0
  33. package/dist/routes/webhook-google.d.ts +7 -0
  34. package/dist/routes/webhook-google.d.ts.map +1 -0
  35. package/dist/routes/webhook.d.ts +6 -18
  36. package/dist/routes/webhook.d.ts.map +1 -1
  37. package/dist/stores/redis.d.ts +60 -0
  38. package/dist/stores/redis.d.ts.map +1 -0
  39. package/dist/stores/schema.d.ts.map +1 -1
  40. package/dist/tracing.d.ts +32 -0
  41. package/dist/tracing.d.ts.map +1 -0
  42. package/dist/webhook-events.d.ts +58 -0
  43. package/dist/webhook-events.d.ts.map +1 -0
  44. package/dist/webhook-queue.d.ts +92 -0
  45. package/dist/webhook-queue.d.ts.map +1 -0
  46. package/package.json +33 -4
  47. package/sql/schema.sql +10 -0
  48. package/dist/__tests__/admin-customer-detail.test.js +0 -146
  49. package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
  50. package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
  51. package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
  52. package/dist/__tests__/admin-subscription-detail.test.js +0 -112
  53. package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
  54. package/dist/__tests__/apple-root-ca.test.js +0 -41
  55. package/dist/__tests__/apple-root-ca.test.js.map +0 -1
  56. package/dist/__tests__/apple-status-api.test.js +0 -345
  57. package/dist/__tests__/apple-status-api.test.js.map +0 -1
  58. package/dist/__tests__/entitlements.test.js +0 -268
  59. package/dist/__tests__/entitlements.test.js.map +0 -1
  60. package/dist/__tests__/error-codes.test.js +0 -168
  61. package/dist/__tests__/error-codes.test.js.map +0 -1
  62. package/dist/__tests__/fetch-hardening.test.js +0 -192
  63. package/dist/__tests__/fetch-hardening.test.js.map +0 -1
  64. package/dist/__tests__/google-paused.test.js +0 -126
  65. package/dist/__tests__/google-paused.test.js.map +0 -1
  66. package/dist/__tests__/google-price-change.test.js +0 -186
  67. package/dist/__tests__/google-price-change.test.js.map +0 -1
  68. package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
  69. package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
  70. package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
  71. package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
  72. package/dist/__tests__/lifecycle-states.test.js +0 -423
  73. package/dist/__tests__/lifecycle-states.test.js.map +0 -1
  74. package/dist/__tests__/linked-purchase-token.test.js +0 -227
  75. package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
  76. package/dist/__tests__/metrics.test.js +0 -340
  77. package/dist/__tests__/metrics.test.js.map +0 -1
  78. package/dist/__tests__/mock-provider.test.js +0 -188
  79. package/dist/__tests__/mock-provider.test.js.map +0 -1
  80. package/dist/__tests__/paused-auto-resume.test.js +0 -162
  81. package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
  82. package/dist/__tests__/providers.test.js +0 -321
  83. package/dist/__tests__/providers.test.js.map +0 -1
  84. package/dist/__tests__/refund-policy.test.js +0 -317
  85. package/dist/__tests__/refund-policy.test.js.map +0 -1
  86. package/dist/__tests__/schema.test.js +0 -46
  87. package/dist/__tests__/schema.test.js.map +0 -1
  88. package/dist/__tests__/store.test.js +0 -172
  89. package/dist/__tests__/store.test.js.map +0 -1
  90. package/dist/__tests__/test-utils.js +0 -30
  91. package/dist/__tests__/test-utils.js.map +0 -1
  92. package/dist/__tests__/webhook-refund.test.js +0 -313
  93. package/dist/__tests__/webhook-refund.test.js.map +0 -1
  94. package/dist/errors.js +0 -19
  95. package/dist/errors.js.map +0 -1
  96. package/dist/http.js +0 -36
  97. package/dist/http.js.map +0 -1
  98. package/dist/logger.js +0 -20
  99. package/dist/logger.js.map +0 -1
  100. package/dist/providers/apple-root-ca.js +0 -38
  101. package/dist/providers/apple-root-ca.js.map +0 -1
  102. package/dist/providers/apple.js +0 -499
  103. package/dist/providers/apple.js.map +0 -1
  104. package/dist/providers/google.js +0 -493
  105. package/dist/providers/google.js.map +0 -1
  106. package/dist/providers/mock.js +0 -93
  107. package/dist/providers/mock.js.map +0 -1
  108. package/dist/routes/admin.js +0 -251
  109. package/dist/routes/admin.js.map +0 -1
  110. package/dist/routes/entitlements.js +0 -130
  111. package/dist/routes/entitlements.js.map +0 -1
  112. package/dist/routes/metrics.js +0 -281
  113. package/dist/routes/metrics.js.map +0 -1
  114. package/dist/routes/purchase.js +0 -187
  115. package/dist/routes/purchase.js.map +0 -1
  116. package/dist/routes/status.js +0 -55
  117. package/dist/routes/status.js.map +0 -1
  118. package/dist/routes/validate.js +0 -70
  119. package/dist/routes/validate.js.map +0 -1
  120. package/dist/routes/webhook.js +0 -441
  121. package/dist/routes/webhook.js.map +0 -1
  122. package/dist/store.js +0 -145
  123. package/dist/store.js.map +0 -1
  124. package/dist/stores/postgres.js +0 -329
  125. package/dist/stores/postgres.js.map +0 -1
  126. package/dist/stores/schema.js +0 -57
  127. package/dist/stores/schema.js.map +0 -1
package/README.md CHANGED
@@ -9,11 +9,17 @@ npm install @onesub/server
9
9
  ## Requirements
10
10
 
11
11
  - Node.js **>= 20**
12
- - **Express** as a peer dependency — `^4.17.0 || ^5.0.0`. Install in your app:
12
+ - **Express** as a peer dependency — `^4.17.0 || ^5.0.0`:
13
13
  ```bash
14
14
  npm install @onesub/server express
15
15
  ```
16
- - PostgreSQL **12+** (optional, for production stores)
16
+ - Optional peer dependencies (install only what you use):
17
+ ```bash
18
+ npm install ioredis # Redis stores, cache, webhook idempotency
19
+ npm install bullmq # Durable webhook queue with dead-letter list
20
+ npm install pg # Postgres stores
21
+ npm install @opentelemetry/api # Distributed tracing
22
+ ```
17
23
 
18
24
  ## Quick start
19
25
 
@@ -59,6 +65,9 @@ app.listen(4100);
59
65
  | `DELETE /onesub/purchase/admin/:userId/:productId` | Wipe a non-consumable (requires `adminSecret`) |
60
66
  | `POST /onesub/purchase/admin/grant` | Manually grant a purchase (requires `adminSecret`) |
61
67
  | `POST /onesub/purchase/admin/transfer` | Reassign a `transactionId` to a new `userId` (requires `adminSecret`) |
68
+ | `GET /onesub/admin/webhook-deadletters` | List failed webhook jobs in DLQ (requires `adminSecret` + BullMQ) |
69
+ | `POST /onesub/admin/webhook-replay/:id` | Replay a dead-letter job (requires `adminSecret` + BullMQ) |
70
+ | `GET /openapi.json` | OpenAPI 3.1 spec (opt-in via `openapiHandler()`) |
62
71
 
63
72
  ## Lifecycle states (`0.4.0+`)
64
73
 
@@ -134,6 +143,114 @@ google: {
134
143
  }
135
144
  ```
136
145
 
146
+ ## Multi-instance deployments (`0.12.0+`)
147
+
148
+ By default every option uses in-process memory — zero config, zero infra. For multi-node clusters, swap in the Redis-backed equivalents:
149
+
150
+ ```ts
151
+ import Redis from 'ioredis';
152
+ import {
153
+ createOneSubMiddleware,
154
+ RedisSubscriptionStore,
155
+ RedisPurchaseStore,
156
+ RedisCacheAdapter,
157
+ RedisWebhookEventStore,
158
+ } from '@onesub/server';
159
+
160
+ const redis = new Redis(process.env.REDIS_URL!);
161
+
162
+ app.use(createOneSubMiddleware({
163
+ // ...apple / google / database config...
164
+ store: new RedisSubscriptionStore(redis),
165
+ purchaseStore: new RedisPurchaseStore(redis),
166
+ cache: new RedisCacheAdapter(redis), // share Apple JWT + Google OAuth tokens across nodes
167
+ webhookEventStore: new RedisWebhookEventStore(redis), // atomic SET NX dedup — one node handles each event
168
+ }));
169
+ ```
170
+
171
+ | Class | Replaces | What it does |
172
+ |-------|----------|--------------|
173
+ | `RedisSubscriptionStore` | `InMemorySubscriptionStore` | Sorted-set index, newest-first pagination |
174
+ | `RedisPurchaseStore` | `InMemoryPurchaseStore` | Cross-node purchase ownership |
175
+ | `RedisCacheAdapter` | `InMemoryCacheAdapter` | Shared Apple JWT / Google OAuth token cache |
176
+ | `RedisWebhookEventStore` | `InMemoryWebhookEventStore` | Atomic `SET NX` dedup (no get→set race) |
177
+
178
+ ### Webhook queue with durable retries
179
+
180
+ Replace the default in-process queue with BullMQ for decoupled retries and a dead-letter list:
181
+
182
+ ```ts
183
+ import { BullMQWebhookQueue } from '@onesub/server';
184
+ import Redis from 'ioredis';
185
+
186
+ const connection = new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null });
187
+ const webhookQueue = new BullMQWebhookQueue({ connection });
188
+
189
+ app.use(createOneSubMiddleware({
190
+ // ...
191
+ webhookQueue,
192
+ adminSecret: process.env.ADMIN_SECRET, // required for DLQ endpoints
193
+ }));
194
+ ```
195
+
196
+ The webhook route 200s immediately once the job is enqueued. The BullMQ worker retries up to 5× (exponential backoff, 1 s base) before moving the job to the dead-letter list. Inspect and replay via the admin endpoints above.
197
+
198
+ ## Cache adapter (`0.12.0+`)
199
+
200
+ The `cache` option controls how Apple JWT assertions and Google OAuth access tokens are cached between requests. Default is `InMemoryCacheAdapter` (process-local). Shared Redis cache prevents every cluster node from minting its own token:
201
+
202
+ ```ts
203
+ import { InMemoryCacheAdapter, RedisCacheAdapter } from '@onesub/server';
204
+
205
+ // single instance (default — no config needed)
206
+ cache: new InMemoryCacheAdapter()
207
+
208
+ // multi-instance
209
+ cache: new RedisCacheAdapter(redis)
210
+ cache: new RedisCacheAdapter(redis, 'myapp:cache:') // custom key prefix
211
+ ```
212
+
213
+ You can also pass any object implementing `CacheAdapter`:
214
+
215
+ ```ts
216
+ export interface CacheAdapter {
217
+ get<T>(key: string): Promise<T | null>;
218
+ set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
219
+ del(key: string): Promise<void>;
220
+ }
221
+ ```
222
+
223
+ ## OpenAPI (`0.12.0+`)
224
+
225
+ The full OpenAPI 3.1 document is exported as `ONESUB_OPENAPI`. Self-host it on your existing Express app:
226
+
227
+ ```ts
228
+ import { openapiHandler } from '@onesub/server';
229
+
230
+ app.get('/openapi.json', openapiHandler());
231
+ ```
232
+
233
+ Or import the spec directly for client generation:
234
+
235
+ ```ts
236
+ import { ONESUB_OPENAPI } from '@onesub/server';
237
+ // use with openapi-typescript, swagger-ui-express, etc.
238
+ ```
239
+
240
+ ## OpenTelemetry tracing (`0.12.0+`)
241
+
242
+ Install `@opentelemetry/api` alongside an OTel SDK and spans appear automatically for receipt validation and webhook processing. The helper is zero-overhead when the package is absent — no `require` error, no performance hit.
243
+
244
+ You can wrap your own code in the same tracer:
245
+
246
+ ```ts
247
+ import { withSpan } from '@onesub/server';
248
+
249
+ const result = await withSpan('my-operation', { 'user.id': userId }, async () => {
250
+ return await doWork();
251
+ });
252
+ ```
253
+
137
254
  ## Schema
138
255
 
139
256
  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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cache.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cache.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=openapi.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/openapi.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=redis-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-store.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/redis-store.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Unit tests for webhook-apple.ts handler behaviors.
3
+ *
4
+ * Covers gaps not reached by lifecycle-scenarios or webhook-refund:
5
+ * - Input validation (missing / malformed signedPayload)
6
+ * - Webhook event deduplication via WebhookEventStore
7
+ * - DID_FAIL_TO_RENEW without GRACE_PERIOD subtype → on_hold
8
+ * - Notification type mappings: DID_RECOVER, OFFER_REDEEMED → active; EXPIRED → expired
9
+ * - Unknown notification type falls through to JWS transaction status
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=webhook-apple.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-apple.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-apple.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webhook-events.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-events.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-events.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Unit tests for webhook-google.ts handler behaviors.
3
+ *
4
+ * Covers gaps not reached by google-paused, google-price-change, or webhook-refund:
5
+ * - Input validation (missing message.data)
6
+ * - Webhook event deduplication via WebhookEventStore
7
+ * - pushAudience JWT authentication (missing / invalid token → 401)
8
+ * - Package name mismatch for subscription notifications
9
+ * - Notification type mappings: CANCELED, REVOKED, EXPIRED, IN_GRACE_PERIOD, unknown fallback
10
+ * - verifyGooglePushToken unit (no network required for invalid tokens)
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=webhook-google.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-google.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-google.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webhook-queue.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-queue.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-queue.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Pluggable cache adapter.
3
+ *
4
+ * Used internally by the Apple JWT minter, Google OAuth token minter, and any
5
+ * future per-process cache. The default `InMemoryCacheAdapter` matches the
6
+ * pre-adapter behavior — module-level state, lost on restart, not shared
7
+ * between cluster nodes.
8
+ *
9
+ * For multi-instance deployments, pass a `RedisCacheAdapter` in
10
+ * `OneSubMiddlewareConfig.cache` so all nodes share the same JWKS / token
11
+ * cache and avoid every node re-minting on its own schedule.
12
+ */
13
+ export interface CacheAdapter {
14
+ get<T = unknown>(key: string): Promise<T | null>;
15
+ /** TTL in seconds. Pass 0 / undefined for no expiry. */
16
+ set<T = unknown>(key: string, value: T, ttlSeconds?: number): Promise<void>;
17
+ del(key: string): Promise<void>;
18
+ }
19
+ /**
20
+ * Process-local in-memory cache. Default for single-instance deployments.
21
+ */
22
+ export declare class InMemoryCacheAdapter implements CacheAdapter {
23
+ private readonly entries;
24
+ get<T>(key: string): Promise<T | null>;
25
+ set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
26
+ del(key: string): Promise<void>;
27
+ /** Test helper — clear everything. Not part of the public CacheAdapter contract. */
28
+ clear(): void;
29
+ }
30
+ export declare function getDefaultCache(): CacheAdapter;
31
+ export declare function setDefaultCache(adapter: CacheAdapter): void;
32
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACjD,wDAAwD;IACxD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAQD;;GAEG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IAE9C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAUtC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrC,oFAAoF;IACpF,KAAK,IAAI,IAAI;CAGd;AAQD,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE3D"}