@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.
- package/README.md +119 -2
- package/dist/__tests__/cache.test.d.ts +2 -0
- package/dist/__tests__/cache.test.d.ts.map +1 -0
- package/dist/__tests__/openapi.test.d.ts +2 -0
- package/dist/__tests__/openapi.test.d.ts.map +1 -0
- package/dist/__tests__/redis-store.test.d.ts +2 -0
- package/dist/__tests__/redis-store.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-apple.test.d.ts +12 -0
- package/dist/__tests__/webhook-apple.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-events.test.d.ts +2 -0
- package/dist/__tests__/webhook-events.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-google.test.d.ts +13 -0
- package/dist/__tests__/webhook-google.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-queue.test.d.ts +2 -0
- package/dist/__tests__/webhook-queue.test.d.ts.map +1 -0
- package/dist/cache.d.ts +32 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/index.cjs +3084 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +34 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3045 -118
- package/dist/index.js.map +1 -1
- package/dist/openapi.d.ts +41 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/providers/apple.d.ts +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/routes/admin.d.ts +2 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/webhook-apple.d.ts +6 -0
- package/dist/routes/webhook-apple.d.ts.map +1 -0
- package/dist/routes/webhook-google.d.ts +7 -0
- package/dist/routes/webhook-google.d.ts.map +1 -0
- package/dist/routes/webhook.d.ts +6 -18
- package/dist/routes/webhook.d.ts.map +1 -1
- package/dist/stores/redis.d.ts +60 -0
- package/dist/stores/redis.d.ts.map +1 -0
- package/dist/stores/schema.d.ts.map +1 -1
- package/dist/tracing.d.ts +32 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/webhook-events.d.ts +58 -0
- package/dist/webhook-events.d.ts.map +1 -0
- package/dist/webhook-queue.d.ts +92 -0
- package/dist/webhook-queue.d.ts.map +1 -0
- package/package.json +33 -4
- package/sql/schema.sql +10 -0
- package/dist/__tests__/admin-customer-detail.test.js +0 -146
- package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
- package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
- package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
- package/dist/__tests__/admin-subscription-detail.test.js +0 -112
- package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
- package/dist/__tests__/apple-root-ca.test.js +0 -41
- package/dist/__tests__/apple-root-ca.test.js.map +0 -1
- package/dist/__tests__/apple-status-api.test.js +0 -345
- package/dist/__tests__/apple-status-api.test.js.map +0 -1
- package/dist/__tests__/entitlements.test.js +0 -268
- package/dist/__tests__/entitlements.test.js.map +0 -1
- package/dist/__tests__/error-codes.test.js +0 -168
- package/dist/__tests__/error-codes.test.js.map +0 -1
- package/dist/__tests__/fetch-hardening.test.js +0 -192
- package/dist/__tests__/fetch-hardening.test.js.map +0 -1
- package/dist/__tests__/google-paused.test.js +0 -126
- package/dist/__tests__/google-paused.test.js.map +0 -1
- package/dist/__tests__/google-price-change.test.js +0 -186
- package/dist/__tests__/google-price-change.test.js.map +0 -1
- package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
- package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
- package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
- package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
- package/dist/__tests__/lifecycle-states.test.js +0 -423
- package/dist/__tests__/lifecycle-states.test.js.map +0 -1
- package/dist/__tests__/linked-purchase-token.test.js +0 -227
- package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
- package/dist/__tests__/metrics.test.js +0 -340
- package/dist/__tests__/metrics.test.js.map +0 -1
- package/dist/__tests__/mock-provider.test.js +0 -188
- package/dist/__tests__/mock-provider.test.js.map +0 -1
- package/dist/__tests__/paused-auto-resume.test.js +0 -162
- package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
- package/dist/__tests__/providers.test.js +0 -321
- package/dist/__tests__/providers.test.js.map +0 -1
- package/dist/__tests__/refund-policy.test.js +0 -317
- package/dist/__tests__/refund-policy.test.js.map +0 -1
- package/dist/__tests__/schema.test.js +0 -46
- package/dist/__tests__/schema.test.js.map +0 -1
- package/dist/__tests__/store.test.js +0 -172
- package/dist/__tests__/store.test.js.map +0 -1
- package/dist/__tests__/test-utils.js +0 -30
- package/dist/__tests__/test-utils.js.map +0 -1
- package/dist/__tests__/webhook-refund.test.js +0 -313
- package/dist/__tests__/webhook-refund.test.js.map +0 -1
- package/dist/errors.js +0 -19
- package/dist/errors.js.map +0 -1
- package/dist/http.js +0 -36
- package/dist/http.js.map +0 -1
- package/dist/logger.js +0 -20
- package/dist/logger.js.map +0 -1
- package/dist/providers/apple-root-ca.js +0 -38
- package/dist/providers/apple-root-ca.js.map +0 -1
- package/dist/providers/apple.js +0 -499
- package/dist/providers/apple.js.map +0 -1
- package/dist/providers/google.js +0 -493
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/mock.js +0 -93
- package/dist/providers/mock.js.map +0 -1
- package/dist/routes/admin.js +0 -251
- package/dist/routes/admin.js.map +0 -1
- package/dist/routes/entitlements.js +0 -130
- package/dist/routes/entitlements.js.map +0 -1
- package/dist/routes/metrics.js +0 -281
- package/dist/routes/metrics.js.map +0 -1
- package/dist/routes/purchase.js +0 -187
- package/dist/routes/purchase.js.map +0 -1
- package/dist/routes/status.js +0 -55
- package/dist/routes/status.js.map +0 -1
- package/dist/routes/validate.js +0 -70
- package/dist/routes/validate.js.map +0 -1
- package/dist/routes/webhook.js +0 -441
- package/dist/routes/webhook.js.map +0 -1
- package/dist/store.js +0 -145
- package/dist/store.js.map +0 -1
- package/dist/stores/postgres.js +0 -329
- package/dist/stores/postgres.js.map +0 -1
- package/dist/stores/schema.js +0 -57
- 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
|
|
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
|
-
-
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cache.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/openapi.test.ts"],"names":[],"mappings":""}
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-queue.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-queue.test.ts"],"names":[],"mappings":""}
|
package/dist/cache.d.ts
ADDED
|
@@ -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"}
|