@onesub/server 0.18.3 → 0.20.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.
Files changed (38) hide show
  1. package/README.md +4 -0
  2. package/dist/__tests__/apple-jws-cache.test.d.ts +16 -0
  3. package/dist/__tests__/apple-jws-cache.test.d.ts.map +1 -0
  4. package/dist/__tests__/metrics-aggregate.test.d.ts +10 -0
  5. package/dist/__tests__/metrics-aggregate.test.d.ts.map +1 -0
  6. package/dist/__tests__/metrics-cache.test.d.ts +11 -0
  7. package/dist/__tests__/metrics-cache.test.d.ts.map +1 -0
  8. package/dist/__tests__/test-overrides.test.d.ts +15 -0
  9. package/dist/__tests__/test-overrides.test.d.ts.map +1 -0
  10. package/dist/__tests__/verified-key-cache.test.d.ts +18 -0
  11. package/dist/__tests__/verified-key-cache.test.d.ts.map +1 -0
  12. package/dist/http.d.ts.map +1 -1
  13. package/dist/index.cjs +397 -191
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +398 -193
  18. package/dist/index.js.map +1 -1
  19. package/dist/metrics-aggregate.d.ts +82 -0
  20. package/dist/metrics-aggregate.d.ts.map +1 -0
  21. package/dist/metrics-cache.d.ts +20 -0
  22. package/dist/metrics-cache.d.ts.map +1 -0
  23. package/dist/openapi.d.ts.map +1 -1
  24. package/dist/providers/apple.d.ts +2 -0
  25. package/dist/providers/apple.d.ts.map +1 -1
  26. package/dist/providers/verified-key-cache.d.ts +75 -0
  27. package/dist/providers/verified-key-cache.d.ts.map +1 -0
  28. package/dist/routes/admin.d.ts.map +1 -1
  29. package/dist/routes/entitlements.d.ts +18 -1
  30. package/dist/routes/entitlements.d.ts.map +1 -1
  31. package/dist/routes/metrics.d.ts +8 -3
  32. package/dist/routes/metrics.d.ts.map +1 -1
  33. package/dist/routes/validate.d.ts.map +1 -1
  34. package/dist/store.d.ts +3 -2
  35. package/dist/store.d.ts.map +1 -1
  36. package/dist/test-overrides.d.ts +36 -0
  37. package/dist/test-overrides.d.ts.map +1 -0
  38. package/package.json +2 -2
@@ -0,0 +1,82 @@
1
+ import type { MetricsActiveResponse, MetricsBucket, MetricsGroupBy, PurchaseInfo, SubscriptionInfo } from '@onesub/shared';
2
+ /**
3
+ * In-memory reduction of store records into the metrics responses.
4
+ *
5
+ * Split out of the route handlers for two reasons. It was duplicated four times
6
+ * — each range endpoint re-implemented the same window filter, the same
7
+ * product/platform tallies, and the same zero-filled daily bucketing — and none
8
+ * of it was reachable by a test without standing up an HTTP server. Both
9
+ * problems go away when the reduction is a pure function of the records.
10
+ *
11
+ * This is deliberately store-agnostic. Aggregating in the application process
12
+ * still costs one full read per query, which is the remaining scaling limit for
13
+ * large deployments; pushing the aggregation down into SQL requires per-store
14
+ * support and is separate work. What lives here is the semantics every store
15
+ * must agree on, so a pushdown implementation has something to be tested
16
+ * against.
17
+ */
18
+ /** Minimum shape the tallies need. Both `SubscriptionInfo` and `PurchaseInfo` satisfy it. */
19
+ interface Countable {
20
+ productId: string;
21
+ platform: string;
22
+ }
23
+ /** The count-shaped part of a metrics response; the route adds `from`/`to`. */
24
+ export interface RangeAggregate {
25
+ total: number;
26
+ byProduct: Record<string, number>;
27
+ byPlatform: Record<string, number>;
28
+ /** Present only when `groupBy: 'day'` was requested. */
29
+ buckets?: MetricsBucket[];
30
+ }
31
+ export interface RangeAggregateOptions<T> {
32
+ /** Inclusive window bounds, ms-since-epoch. */
33
+ fromMs: number;
34
+ toMs: number;
35
+ groupBy: MetricsGroupBy;
36
+ /**
37
+ * ISO timestamp that places a record in the window and its daily bucket —
38
+ * `purchasedAt` for "started", `expiresAt` for "expired".
39
+ */
40
+ anchor: (record: T) => string;
41
+ /** Records this rejects are ignored entirely (status / type gates). */
42
+ include?: (record: T) => boolean;
43
+ }
44
+ /**
45
+ * UTC `YYYY-MM-DD` for an epoch-ms instant.
46
+ *
47
+ * UTC keeps bucket assignment deterministic regardless of server timezone:
48
+ * Postgres `updated_at` is UTC and `SubscriptionInfo` dates carry explicit zone
49
+ * offsets, so a local-time key would silently shift every boundary when the
50
+ * process moved between regions.
51
+ */
52
+ export declare function utcDateKey(ms: number): string;
53
+ /**
54
+ * Zero-filled daily series across `[fromMs, toMs]`, inclusive of both boundary
55
+ * days. Zero-filling is what lets a chart render a flat stretch as flat rather
56
+ * than as a gap.
57
+ */
58
+ export declare function emptyDailyBuckets(fromMs: number, toMs: number): MetricsBucket[];
59
+ /**
60
+ * Tally records whose anchor timestamp falls inside the window.
61
+ *
62
+ * Records outside the window, and records rejected by `include`, contribute
63
+ * nothing — not to `total`, not to the distributions, not to a bucket.
64
+ */
65
+ export declare function aggregateRange<T extends Countable>(records: readonly T[], opts: RangeAggregateOptions<T>): RangeAggregate;
66
+ /** A subscription counts as currently entitled: allowed status AND not yet expired. */
67
+ export declare function isActiveSubscription(sub: SubscriptionInfo, nowMs: number): boolean;
68
+ /** A subscription counts as ended: the store recorded it expired or canceled. */
69
+ export declare function isEndedSubscription(sub: SubscriptionInfo): boolean;
70
+ /** Non-consumables grant an ongoing right; consumables are a spent resource. */
71
+ export declare function isNonConsumable(purchase: PurchaseInfo): boolean;
72
+ /**
73
+ * Point-in-time entitlement snapshot.
74
+ *
75
+ * `byProduct` is subscriptions-only and `byProductPurchases` is
76
+ * non-consumables-only, so a dashboard can show "subscription mix" and
77
+ * "lifetime mix" as separate panels. `byPlatform` deliberately spans both —
78
+ * it answers "where are my paying users", which is not a per-kind question.
79
+ */
80
+ export declare function aggregateActive(subs: readonly SubscriptionInfo[], purchases: readonly PurchaseInfo[], nowMs: number): MetricsActiveResponse;
81
+ export {};
82
+ //# sourceMappingURL=metrics-aggregate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics-aggregate.d.ts","sourceRoot":"","sources":["../src/metrics-aggregate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,YAAY,EACZ,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAGxB;;;;;;;;;;;;;;;GAeG;AAEH,6FAA6F;AAC7F,UAAU,SAAS;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,wDAAwD;IACxD,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,cAAc,CAAC;IACxB;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IAC9B,uEAAuE;IACvE,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;CAClC;AAQD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAW/E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,SAAS,EAChD,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC7B,cAAc,CA0BhB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIlF;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAIlE;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAE/D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,SAAS,gBAAgB,EAAE,EACjC,SAAS,EAAE,SAAS,YAAY,EAAE,EAClC,KAAK,EAAE,MAAM,GACZ,qBAAqB,CAgCvB"}
@@ -0,0 +1,20 @@
1
+ import type { CacheAdapter } from './cache.js';
2
+ export interface MetricsCache {
3
+ /**
4
+ * Return the value for `keyParts`, computing it via `produce` on a miss.
5
+ *
6
+ * `keyParts` must not contain a raw request timestamp — see
7
+ * `quantizeToWindow`.
8
+ */
9
+ resolve<T>(keyParts: readonly (string | number)[], produce: () => Promise<T>): Promise<T>;
10
+ /** Snap a millisecond timestamp onto the TTL grid, for use in a cache key. */
11
+ quantizeToWindow(ms: number): number;
12
+ }
13
+ /**
14
+ * @param ttlSeconds Freshness bound. `0` (or less) disables the cache.
15
+ * @param storage Backing store. Defaults to storage private to this
16
+ * instance — see the note above on why this is not the shared
17
+ * adapter. Injectable for tests.
18
+ */
19
+ export declare function createMetricsCache(ttlSeconds: number, storage?: CacheAdapter): MetricsCache;
20
+ //# sourceMappingURL=metrics-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics-cache.d.ts","sourceRoot":"","sources":["../src/metrics-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAkC/C,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1F,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,YAAyC,GACjD,YAAY,CAgDd"}
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,EAAE;QACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC1C,CAAC;CACH;AA6BD,eAAO,MAAM,cAAc,EAAE,UAigB5B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,KACpB,MAAM,OAAO,SAAS,EAAE,OAAO,EAAE,KAAK,OAAO,SAAS,EAAE,QAAQ,UAGzE"}
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,EAAE;QACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC1C,CAAC;CACH;AA6BD,eAAO,MAAM,cAAc,EAAE,UAojB5B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,KACpB,MAAM,OAAO,SAAS,EAAE,OAAO,EAAE,KAAK,OAAO,SAAS,EAAE,QAAQ,UAGzE"}
@@ -20,6 +20,8 @@ type AppleConfig = NonNullable<OneSubServerConfig['apple']>;
20
20
  * Exported for unit testing.
21
21
  */
22
22
  export declare function assertIssuerCanSign(cert: Pick<X509Certificate, 'ca'>, index: number): void;
23
+ /** Test/diagnostic hook — drop every memoised chain verification. */
24
+ export declare function clearAppleCertCache(): void;
23
25
  /**
24
26
  * Decode and verify a StoreKit 2 signed transaction JWS.
25
27
  *
@@ -1 +1 @@
1
- {"version":3,"file":"apple.d.ts","sourceRoot":"","sources":["../../src/providers/apple.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAA0B,MAAM,aAAa,CAAC;AACtE,OAAO,KAAK,EACV,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,gBAAgB,CAAC;AAWxB,KAAK,WAAW,GAAG,WAAW,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;AAsD5D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAM1F;AAqDD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,UAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAuBpF;AAsBD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA8DlC;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,8BAA8B,CAClD,iBAAiB,EAAE,MAAM,EACzB,MAAM,EAAE,WAAW,EACnB,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA4FpC;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,wBAAwB,EACjC,mBAAmB,UAAQ,GAC1B,OAAO,CAAC;IACT,qBAAqB,EAAE,MAAM,CAAC;IAC9B,gGAAgG;IAChG,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oGAAoG;IACpG,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,2EAA2E;IAC3E,WAAW,EAAE,YAAY,GAAG,SAAS,CAAC;IACtC,MAAM,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC,GAAG,IAAI,CAAC,CA6CR;AAmED,0DAA0D;AAC1D,iBAAS,0BAA0B,IAAI,IAAI,CAO1C;AAED,eAAO,MAAM,SAAS;;CAAiC,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,wBAAsB,4BAA4B,CAChD,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9B,OAAO,CAAC,IAAI,CAAC,CAgCf;AAgDD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,4BAA4B,CAChD,qBAAqB,EAAE,MAAM,EAC7B,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9B,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA8ElC;AAeD;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAUD;;;;;;;;GAQG;AACH,wBAAsB,4BAA4B,CAChD,qBAAqB,EAAE,MAAM,EAC7B,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9B,OAAO,CAAC,sBAAsB,EAAE,GAAG,IAAI,CAAC,CA+E1C;AAMD;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,yBAAyB,CAC7C,KAAK,EAAE,wBAAwB,EAC/B,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,yBAAyB,CAAC,CAoCpC"}
1
+ {"version":3,"file":"apple.d.ts","sourceRoot":"","sources":["../../src/providers/apple.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Memo for "this certificate chain was verified, and here is the key it
3
+ * authorises" — the result of an expensive, purely local verification.
4
+ *
5
+ * Why this exists: Apple's StoreKit JWS verification re-parses the x5c chain,
6
+ * re-checks every issuer signature against a bundled Apple root, and re-imports
7
+ * the leaf public key on EVERY receipt and EVERY webhook. All of it is
8
+ * synchronous CPU work on the event loop, and Apple's leaf certificates are
9
+ * stable for weeks — so the same chain is verified over and over with the same
10
+ * outcome.
11
+ *
12
+ * Why this is NOT the pluggable `CacheAdapter`: that adapter exists to share
13
+ * *rate-limited network* results (Apple API JWTs, Google OAuth tokens) between
14
+ * cluster nodes, and it serialises values as JSON. An imported key is not
15
+ * JSON-serialisable — a Redis-backed adapter would silently round-trip it to
16
+ * `{}` — and what is being avoided here is local CPU, not a network call or a
17
+ * quota. A Redis round-trip would plausibly cost more than the verification it
18
+ * replaces. So this cache is deliberately process-local.
19
+ *
20
+ * Security invariants, all enforced below and covered by tests:
21
+ *
22
+ * - The entry key covers EVERY input to the verification, including the
23
+ * algorithm the key was imported for. A different chain, or the same chain
24
+ * claiming a different `alg`, is a different entry — it can never reuse a
25
+ * key that was authorised for something else.
26
+ * - A failed verification is never stored. A rejected `produce` propagates and
27
+ * leaves the cache untouched, so a bad chain is re-verified (and re-rejected)
28
+ * every time rather than being remembered either way.
29
+ * - No entry outlives the chain that produced it. `notAfter` (the earliest
30
+ * expiry in the verified chain) is a hard ceiling on the entry's lifetime,
31
+ * so an expired certificate can never be served from cache.
32
+ */
33
+ /** What a successful verification yields: the key, and when it stops being valid. */
34
+ export interface VerifiedKey<K> {
35
+ key: K;
36
+ /**
37
+ * Earliest `notAfter` across the verified chain, as ms-since-epoch. The cache
38
+ * entry is dropped at or before this instant, never after it.
39
+ */
40
+ notAfter: number;
41
+ }
42
+ export interface VerifiedKeyCacheOptions {
43
+ /**
44
+ * Upper bound on entries. Only successfully verified chains are stored, so in
45
+ * practice this holds a handful (Apple production + sandbox leaves, across a
46
+ * rotation). The cap is defence in depth against unbounded growth.
47
+ */
48
+ maxEntries?: number;
49
+ /** Upper bound on an entry's lifetime, independent of certificate expiry. */
50
+ maxTtlMs?: number;
51
+ /** Injectable clock. Tests drive expiry with it; production uses `Date.now`. */
52
+ now?: () => number;
53
+ }
54
+ export declare class VerifiedKeyCache<K> {
55
+ private readonly entries;
56
+ private readonly maxEntries;
57
+ private readonly maxTtlMs;
58
+ private readonly now;
59
+ constructor(opts?: VerifiedKeyCacheOptions);
60
+ /**
61
+ * Return the key for `parts`, verifying via `produce` only on a miss.
62
+ *
63
+ * `parts` must contain every input the verification depends on — for Apple
64
+ * that is the JWS `alg` plus the full x5c chain. Anything left out would let
65
+ * one input's result be served for a different input.
66
+ */
67
+ resolve(parts: readonly string[], produce: () => Promise<VerifiedKey<K>>): Promise<K>;
68
+ /** Entries currently held, including any not yet pruned. Test/diagnostic use. */
69
+ get size(): number;
70
+ /** Drop everything. Test/diagnostic use. */
71
+ clear(): void;
72
+ /** Make room: expired entries first, then the oldest insertions. */
73
+ private prune;
74
+ }
75
+ //# sourceMappingURL=verified-key-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verified-key-cache.d.ts","sourceRoot":"","sources":["../../src/providers/verified-key-cache.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,qFAAqF;AACrF,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,GAAG,EAAE,CAAC,CAAC;IACP;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAWD,qBAAa,gBAAgB,CAAC,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,GAAE,uBAA4B;IAM9C;;;;;;OAMG;IACG,OAAO,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA0B3F,iFAAiF;IACjF,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,4CAA4C;IAC5C,KAAK,IAAI,IAAI;IAIb,oEAAoE;IACpE,OAAO,CAAC,KAAK;CAWd"}
@@ -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;AAMxD;;;;;;;;;;;;;;;;;;;;;;;;;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,CAiTf"}
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;AAQxD;;;;;;;;;;;;;;;;;;;;;;;;;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,CAmWf"}
@@ -2,7 +2,12 @@ import { Router } from 'express';
2
2
  import type { Entitlement, EntitlementStatus, OneSubServerConfig, PurchaseInfo, SubscriptionInfo } from '@onesub/shared';
3
3
  import type { PurchaseStore, SubscriptionStore } from '../store.js';
4
4
  /**
5
- * Evaluate a single entitlement for a user.
5
+ * Evaluate one entitlement against records the caller already holds.
6
+ *
7
+ * This is the whole decision, with no store access — so a caller evaluating N
8
+ * entitlements for one user reads that user's records ONCE and calls this N
9
+ * times, instead of paying 2N store round-trips. `evaluateEntitlement` below
10
+ * is the convenience wrapper for the single-entitlement case.
6
11
  *
7
12
  * A user is entitled when EITHER condition holds for any productId in
8
13
  * `entitlement.productIds`:
@@ -16,6 +21,18 @@ import type { PurchaseStore, SubscriptionStore } from '../store.js';
16
21
  * Subscription is preferred over purchase when both match (subs carry an
17
22
  * expiry; non-consumables are forever) so the source field skews toward the
18
23
  * more "interesting" signal for ops/analytics.
24
+ *
25
+ * `now` is injected so a batch evaluation scores every entitlement against one
26
+ * consistent instant rather than drifting across awaits.
27
+ */
28
+ export declare function evaluateEntitlementFrom(subs: readonly SubscriptionInfo[], purchases: readonly PurchaseInfo[], entitlement: Entitlement, now?: number): EntitlementStatus;
29
+ /**
30
+ * Evaluate a single entitlement for a user, reading the records it needs.
31
+ *
32
+ * Thin wrapper over `evaluateEntitlementFrom` — see there for the entitlement
33
+ * rules. Evaluating several entitlements for the same user through this
34
+ * function costs 2 store reads each; prefer reading once and calling
35
+ * `evaluateEntitlementFrom` directly in that case.
19
36
  */
20
37
  export declare function evaluateEntitlement(userId: string, entitlement: Entitlement, store: SubscriptionStore, purchaseStore: PurchaseStore): Promise<EntitlementStatus>;
21
38
  export declare function createEntitlementRouter(config: OneSubServerConfig, store: SubscriptionStore, purchaseStore: PurchaseStore): Router | null;
@@ -1 +1 @@
1
- {"version":3,"file":"entitlements.d.ts","sourceRoot":"","sources":["../../src/routes/entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EACV,WAAW,EAEX,iBAAiB,EAGjB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIpE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,iBAAiB,EACxB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CAkC5B;AAKD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,iBAAiB,EACxB,aAAa,EAAE,aAAa,GAC3B,MAAM,GAAG,IAAI,CA0Ef;AAID,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"entitlements.d.ts","sourceRoot":"","sources":["../../src/routes/entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EACV,WAAW,EAEX,iBAAiB,EAGjB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIpE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,SAAS,gBAAgB,EAAE,EACjC,SAAS,EAAE,SAAS,YAAY,EAAE,EAClC,WAAW,EAAE,WAAW,EACxB,GAAG,GAAE,MAAmB,GACvB,iBAAiB,CA+BnB;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,iBAAiB,EACxB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CAW5B;AAKD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,iBAAiB,EACxB,aAAa,EAAE,aAAa,GAC3B,MAAM,GAAG,IAAI,CAgFf;AAID,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
@@ -1,6 +1,8 @@
1
1
  import { Router } from 'express';
2
2
  import type { OneSubServerConfig, PurchaseInfo, SubscriptionInfo } from '@onesub/shared';
3
3
  import type { PurchaseStore, SubscriptionStore } from '../store.js';
4
+ /** Freshness bound when the host does not configure one. */
5
+ export declare const DEFAULT_METRICS_CACHE_TTL_SECONDS = 30;
4
6
  /**
5
7
  * Read-only aggregate metrics endpoints — gated behind `config.adminSecret`.
6
8
  * Returns count-based metrics only (active count, started count, expired count).
@@ -9,9 +11,12 @@ import type { PurchaseStore, SubscriptionStore } from '../store.js';
9
11
  * server doesn't currently track; deferred to a follow-up release that adds
10
12
  * `config.products: { 'pro_monthly': { price: 9.99, currency: 'USD' } }`.
11
13
  *
12
- * Aggregation strategy: pulls every record via `store.listAll()` and reduces
13
- * in memory. Fine up to ~100k records; large deployments should optimise the
14
- * Postgres path with SQL aggregates (separate PR).
14
+ * Aggregation strategy: reads every record via `store.listAll()` and reduces in
15
+ * memory (see `metrics-aggregate.ts` for the reduction, which is where the
16
+ * semantics are tested). That is one full read per computed response, so
17
+ * responses are cached for `config.metricsCacheTtlSeconds` to bound how often
18
+ * it happens — see `metrics-cache.ts`. Removing the per-response scan entirely
19
+ * needs SQL-side aggregation, which is per-store work and still pending.
15
20
  */
16
21
  export declare function createMetricsRouter(config: OneSubServerConfig, store: SubscriptionStore, purchaseStore: PurchaseStore): Router | null;
17
22
  export type { PurchaseInfo, SubscriptionInfo };
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/routes/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EAIV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAOxB,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOpE;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,iBAAiB,EACxB,aAAa,EAAE,aAAa,GAC3B,MAAM,GAAG,IAAI,CA0Sf;AAED,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/routes/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EAIV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAepE,4DAA4D;AAC5D,eAAO,MAAM,iCAAiC,KAAK,CAAC;AAEpD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,iBAAiB,EACxB,aAAa,EAAE,aAAa,GAC3B,MAAM,GAAG,IAAI,CAuKf;AAED,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/routes/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EAA2B,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAElF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAkBrD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAgGR"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/routes/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EAA2B,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAElF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAmBrD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,iBAAiB,GACvB,MAAM,CA6GR"}
package/dist/store.d.ts CHANGED
@@ -54,8 +54,9 @@ export interface SubscriptionStore {
54
54
  * see all of a user's active subscriptions to decide whether any of them
55
55
  * grants the requested entitlement.
56
56
  *
57
- * Implementations: in-memory currently coalesces by userId so this returns
58
- * at most one row; Postgres returns the full history.
57
+ * All three built-in implementations return the user's full set, one record
58
+ * per `originalTransactionId`. "Most-recent-first" is last-written-first for
59
+ * the in-memory store and `updated_at DESC` for Postgres/Redis.
59
60
  */
60
61
  getAllByUserId(userId: string): Promise<SubscriptionInfo[]>;
61
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Sandbox-only entitlement overrides for manual testing.
3
+ *
4
+ * Why this exists: Apple provides no way to cancel a sandbox subscription that
5
+ * was bought with a real Apple Account through TestFlight. "Clear Purchase
6
+ * History" in App Store Connect only covers sandbox *tester* accounts, and the
7
+ * store expires an accelerated subscription on its own schedule. A developer
8
+ * who subscribes once to check the paywall is therefore stuck as an entitled
9
+ * user, and cannot re-run the purchase flow — the exact situation that hid a
10
+ * paywall entry point from App Review (PenguinRun 2.0.6, Guideline 2.1(b)).
11
+ *
12
+ * Deleting server records does not help: `/onesub/validate` re-derives
13
+ * entitlement from Apple on every call, so any local edit is overwritten the
14
+ * next time the app launches. The override has to sit in front of that verdict.
15
+ *
16
+ * Safety model — an override can never affect a paying customer:
17
+ * - it is keyed by userId, so it touches exactly one account;
18
+ * - it is honoured ONLY when the receipt Apple just validated came from
19
+ * Sandbox (`SubscriptionInfo.sandbox`), so a Production receipt ignores it
20
+ * even if an override for that userId exists;
21
+ * - it lives behind the admin secret, like every other admin route;
22
+ * - it is process-local and non-persistent, so it cannot outlive a deploy or
23
+ * silently linger in a database. Multi-instance deployments must set it on
24
+ * the instance under test (or run one instance) — deliberate, so this stays
25
+ * a debugging aid rather than a product feature.
26
+ */
27
+ export declare function setTestOverride(userId: string, entitled: boolean): void;
28
+ export declare function clearTestOverride(userId: string): boolean;
29
+ export declare function getTestOverride(userId: string): boolean | undefined;
30
+ export declare function listTestOverrides(): Array<{
31
+ userId: string;
32
+ entitled: boolean;
33
+ }>;
34
+ /** Test seam — resets state between suites. */
35
+ export declare function clearAllTestOverrides(): void;
36
+ //# sourceMappingURL=test-overrides.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-overrides.d.ts","sourceRoot":"","sources":["../src/test-overrides.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAEvE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAEnE;AAED,wBAAgB,iBAAiB,IAAI,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAEhF;AAED,+CAA+C;AAC/C,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onesub/server",
3
- "version": "0.18.3",
3
+ "version": "0.20.0",
4
4
  "description": "Server-side receipt validation middleware for react-native-iap. Apple StoreKit 2 + Google Play Billing. One line.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "license": "MIT",
57
57
  "dependencies": {
58
- "@onesub/shared": "0.13.0",
58
+ "@onesub/shared": "0.15.0",
59
59
  "jose": "^6.2.3",
60
60
  "zod": "^4.4.3"
61
61
  },