@onesub/server 0.1.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/store.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { InMemorySubscriptionStore } from '../store.js';
3
+ const makeSub = (overrides) => ({
4
+ userId: 'user_123',
5
+ productId: 'com.example.pro_monthly',
6
+ platform: 'apple',
7
+ status: 'active',
8
+ expiresAt: '2025-12-31T00:00:00.000Z',
9
+ originalTransactionId: 'txn_abc123',
10
+ purchasedAt: '2025-01-01T00:00:00.000Z',
11
+ willRenew: true,
12
+ ...overrides,
13
+ });
14
+ describe('InMemorySubscriptionStore', () => {
15
+ let store;
16
+ beforeEach(() => {
17
+ store = new InMemorySubscriptionStore();
18
+ });
19
+ it('save() and getByUserId() returns the correct subscription', async () => {
20
+ const sub = makeSub();
21
+ await store.save(sub);
22
+ const result = await store.getByUserId('user_123');
23
+ expect(result).toEqual(sub);
24
+ });
25
+ it('save() and getByTransactionId() returns the correct subscription', async () => {
26
+ const sub = makeSub();
27
+ await store.save(sub);
28
+ const result = await store.getByTransactionId('txn_abc123');
29
+ expect(result).toEqual(sub);
30
+ });
31
+ it('getByUserId() returns null for an unknown user', async () => {
32
+ const result = await store.getByUserId('nonexistent_user');
33
+ expect(result).toBeNull();
34
+ });
35
+ it('getByTransactionId() returns null for an unknown transaction', async () => {
36
+ const result = await store.getByTransactionId('nonexistent_txn');
37
+ expect(result).toBeNull();
38
+ });
39
+ it('save() overwrites an existing subscription for the same userId', async () => {
40
+ const original = makeSub({ status: 'active', expiresAt: '2025-06-01T00:00:00.000Z' });
41
+ await store.save(original);
42
+ const updated = makeSub({
43
+ status: 'expired',
44
+ expiresAt: '2025-01-15T00:00:00.000Z',
45
+ originalTransactionId: 'txn_abc123',
46
+ });
47
+ await store.save(updated);
48
+ const result = await store.getByUserId('user_123');
49
+ expect(result?.status).toBe('expired');
50
+ expect(result?.expiresAt).toBe('2025-01-15T00:00:00.000Z');
51
+ });
52
+ it('save() overwrites an existing subscription indexed by the same transactionId', async () => {
53
+ const original = makeSub({ willRenew: true });
54
+ await store.save(original);
55
+ const updated = makeSub({ willRenew: false, originalTransactionId: 'txn_abc123' });
56
+ await store.save(updated);
57
+ const result = await store.getByTransactionId('txn_abc123');
58
+ expect(result?.willRenew).toBe(false);
59
+ });
60
+ it('stores multiple subscriptions independently', async () => {
61
+ const sub1 = makeSub({ userId: 'user_1', originalTransactionId: 'txn_1' });
62
+ const sub2 = makeSub({ userId: 'user_2', originalTransactionId: 'txn_2', platform: 'google' });
63
+ await store.save(sub1);
64
+ await store.save(sub2);
65
+ expect(await store.getByUserId('user_1')).toEqual(sub1);
66
+ expect(await store.getByUserId('user_2')).toEqual(sub2);
67
+ expect(await store.getByTransactionId('txn_1')).toEqual(sub1);
68
+ expect(await store.getByTransactionId('txn_2')).toEqual(sub2);
69
+ });
70
+ });
71
+ //# sourceMappingURL=store.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.test.js","sourceRoot":"","sources":["../../src/__tests__/store.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import express, { Router } from 'express';
2
+ import type { OneSubServerConfig } from '@onesub/shared';
3
+ import type { SubscriptionStore } from './store.js';
4
+ /**
5
+ * Extended config with optional pluggable store.
6
+ */
7
+ export interface OneSubMiddlewareConfig extends OneSubServerConfig {
8
+ /**
9
+ * Custom subscription store. Defaults to InMemorySubscriptionStore.
10
+ * For production, provide a PostgreSQL or Redis backed implementation.
11
+ */
12
+ store?: SubscriptionStore;
13
+ }
14
+ /**
15
+ * Create an Express Router with all OneSub routes mounted.
16
+ *
17
+ * Mount this in your existing Express app:
18
+ * ```ts
19
+ * import { createOneSubMiddleware } from '@onesub/server';
20
+ * app.use(createOneSubMiddleware(config));
21
+ * ```
22
+ *
23
+ * The routes registered are (all prefixed with nothing — mount at root or use
24
+ * a prefix via `app.use('/prefix', createOneSubMiddleware(config))`):
25
+ *
26
+ * POST /onesub/validate
27
+ * GET /onesub/status
28
+ * POST /onesub/webhook/apple
29
+ * POST /onesub/webhook/google
30
+ */
31
+ export declare function createOneSubMiddleware(config: OneSubMiddlewareConfig): Router;
32
+ /**
33
+ * Create a standalone Express application with all OneSub routes.
34
+ *
35
+ * ```ts
36
+ * import { createOneSubServer } from '@onesub/server';
37
+ * createOneSubServer(config).listen(4100);
38
+ * ```
39
+ */
40
+ export declare function createOneSubServer(config: OneSubMiddlewareConfig): ReturnType<typeof express>;
41
+ export { InMemorySubscriptionStore } from './store.js';
42
+ export type { SubscriptionStore } from './store.js';
43
+ export { PostgresSubscriptionStore } from './stores/postgres.js';
44
+ export { validateAppleReceipt } from './providers/apple.js';
45
+ export { validateGoogleReceipt } from './providers/google.js';
46
+ export default createOneSubMiddleware;
47
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKpD;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE;;;OAGG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAa7E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,sBAAsB,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAW7F;AAGD,OAAO,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAGjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAG9D,eAAe,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,91 @@
1
+ import express, { Router } from 'express';
2
+ import { DEFAULT_PORT } from '@onesub/shared';
3
+ import { InMemorySubscriptionStore } from './store.js';
4
+ import { createValidateRouter } from './routes/validate.js';
5
+ import { createStatusRouter } from './routes/status.js';
6
+ import { createWebhookRouter } from './routes/webhook.js';
7
+ /**
8
+ * Create an Express Router with all OneSub routes mounted.
9
+ *
10
+ * Mount this in your existing Express app:
11
+ * ```ts
12
+ * import { createOneSubMiddleware } from '@onesub/server';
13
+ * app.use(createOneSubMiddleware(config));
14
+ * ```
15
+ *
16
+ * The routes registered are (all prefixed with nothing — mount at root or use
17
+ * a prefix via `app.use('/prefix', createOneSubMiddleware(config))`):
18
+ *
19
+ * POST /onesub/validate
20
+ * GET /onesub/status
21
+ * POST /onesub/webhook/apple
22
+ * POST /onesub/webhook/google
23
+ */
24
+ export function createOneSubMiddleware(config) {
25
+ const store = config.store ?? new InMemorySubscriptionStore();
26
+ const router = Router();
27
+ // Parse JSON bodies for all OneSub routes (50 kb cap to prevent payload flooding)
28
+ router.use(express.json({ limit: '50kb' }));
29
+ router.use(createValidateRouter(config, store));
30
+ router.use(createStatusRouter(store));
31
+ router.use(createWebhookRouter(config, store));
32
+ return router;
33
+ }
34
+ /**
35
+ * Create a standalone Express application with all OneSub routes.
36
+ *
37
+ * ```ts
38
+ * import { createOneSubServer } from '@onesub/server';
39
+ * createOneSubServer(config).listen(4100);
40
+ * ```
41
+ */
42
+ export function createOneSubServer(config) {
43
+ const app = express();
44
+ // Health check
45
+ app.get('/health', (_req, res) => {
46
+ res.json({ ok: true, service: 'onesub' });
47
+ });
48
+ app.use(createOneSubMiddleware(config));
49
+ return app;
50
+ }
51
+ // Named re-exports for consumers who want to bring their own store
52
+ export { InMemorySubscriptionStore } from './store.js';
53
+ export { PostgresSubscriptionStore } from './stores/postgres.js';
54
+ // Provider functions for direct (non-HTTP) usage
55
+ export { validateAppleReceipt } from './providers/apple.js';
56
+ export { validateGoogleReceipt } from './providers/google.js';
57
+ // Default export: the middleware factory
58
+ export default createOneSubMiddleware;
59
+ // Allow running directly: node dist/index.js
60
+ // Reads config from environment variables for quick local testing.
61
+ const isMain = typeof process !== 'undefined' &&
62
+ process.argv[1] != null &&
63
+ new URL(import.meta.url).pathname.endsWith(process.argv[1].replace(/\\/g, '/'));
64
+ if (isMain) {
65
+ const config = {
66
+ apple: process.env['APPLE_BUNDLE_ID']
67
+ ? {
68
+ bundleId: process.env['APPLE_BUNDLE_ID'],
69
+ sharedSecret: process.env['APPLE_SHARED_SECRET'],
70
+ keyId: process.env['APPLE_KEY_ID'],
71
+ issuerId: process.env['APPLE_ISSUER_ID'],
72
+ privateKey: process.env['APPLE_PRIVATE_KEY'],
73
+ }
74
+ : undefined,
75
+ google: process.env['GOOGLE_PACKAGE_NAME']
76
+ ? {
77
+ packageName: process.env['GOOGLE_PACKAGE_NAME'],
78
+ serviceAccountKey: process.env['GOOGLE_SERVICE_ACCOUNT_KEY'],
79
+ }
80
+ : undefined,
81
+ database: {
82
+ url: process.env['DATABASE_URL'] ?? '',
83
+ },
84
+ webhookSecret: process.env['WEBHOOK_SECRET'],
85
+ };
86
+ const port = process.env['PORT'] ? parseInt(process.env['PORT'], 10) : DEFAULT_PORT;
87
+ createOneSubServer(config).listen(port, () => {
88
+ console.log(`[onesub] Server listening on port ${port}`);
89
+ });
90
+ }
91
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type { SubscriptionInfo, AppleNotificationPayload, OneSubServerConfig } from '@onesub/shared';
2
+ type AppleConfig = NonNullable<OneSubServerConfig['apple']>;
3
+ /**
4
+ * Decode and verify a JWS compact serialisation using Apple's JWKS.
5
+ * When skipVerification is true, falls back to decodeJwt() without signature
6
+ * verification (useful for dev/testing environments).
7
+ */
8
+ export declare function decodeJws<T>(jws: string, skipVerification?: boolean): Promise<T>;
9
+ /**
10
+ * Validate an Apple receipt string.
11
+ *
12
+ * StoreKit 2 receipts are JWS-encoded signed transactions (the `signedTransaction`
13
+ * field from the client). For legacy base64 receipts from StoreKit 1, you would
14
+ * POST to verifyReceipt — that path is left as a stub here because Apple is
15
+ * deprecating it.
16
+ */
17
+ export declare function validateAppleReceipt(receipt: string, config: AppleConfig): Promise<SubscriptionInfo | null>;
18
+ /**
19
+ * Decode an Apple Server Notification V2 payload.
20
+ * Returns the derived subscription update to be merged into the store.
21
+ */
22
+ export declare function decodeAppleNotification(payload: AppleNotificationPayload, skipJwsVerification?: boolean): Promise<{
23
+ originalTransactionId: string;
24
+ status: SubscriptionInfo['status'];
25
+ willRenew: boolean;
26
+ expiresAt: string;
27
+ } | null>;
28
+ export {};
29
+ //# sourceMappingURL=apple.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple.d.ts","sourceRoot":"","sources":["../../src/providers/apple.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGrG,KAAK,WAAW,GAAG,WAAW,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;AAwC5D;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,UAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAepF;AAsBD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAkClC;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,wBAAwB,EACjC,mBAAmB,UAAQ,GAC1B,OAAO,CAAC;IAAE,qBAAqB,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA6B9H"}
@@ -0,0 +1,111 @@
1
+ import { decodeJwt, createRemoteJWKSet, jwtVerify } from 'jose';
2
+ import { SUBSCRIPTION_STATUS } from '@onesub/shared';
3
+ /**
4
+ * Module-level JWKS fetcher for Apple's public keys.
5
+ * Cached automatically by jose's createRemoteJWKSet.
6
+ */
7
+ const appleJWKS = createRemoteJWKSet(new URL('https://appleid.apple.com/auth/keys'));
8
+ /**
9
+ * Decode and verify a JWS compact serialisation using Apple's JWKS.
10
+ * When skipVerification is true, falls back to decodeJwt() without signature
11
+ * verification (useful for dev/testing environments).
12
+ */
13
+ export async function decodeJws(jws, skipVerification = false) {
14
+ if (skipVerification) {
15
+ if (process.env['NODE_ENV'] === 'production') {
16
+ console.warn('[onesub/apple] WARNING: skipJwsVerification is enabled in production. ' +
17
+ 'JWS signatures are NOT being verified. This is a security risk. ' +
18
+ 'Disable skipJwsVerification before going live.');
19
+ }
20
+ // Dev/test path: decode payload only, no signature check
21
+ return decodeJwt(jws);
22
+ }
23
+ const { payload } = await jwtVerify(jws, appleJWKS);
24
+ return payload;
25
+ }
26
+ /**
27
+ * Derive a SubscriptionStatus from the transaction + renewal payloads.
28
+ */
29
+ function deriveStatus(tx, renewal) {
30
+ if (tx.revocationDate)
31
+ return SUBSCRIPTION_STATUS.CANCELED;
32
+ const now = Date.now();
33
+ const expires = tx.expiresDate ?? 0;
34
+ if (expires > now)
35
+ return SUBSCRIPTION_STATUS.ACTIVE;
36
+ // Expired — check if it was voluntarily canceled
37
+ if (renewal?.autoRenewStatus === 0)
38
+ return SUBSCRIPTION_STATUS.CANCELED;
39
+ return SUBSCRIPTION_STATUS.EXPIRED;
40
+ }
41
+ /**
42
+ * Validate an Apple receipt string.
43
+ *
44
+ * StoreKit 2 receipts are JWS-encoded signed transactions (the `signedTransaction`
45
+ * field from the client). For legacy base64 receipts from StoreKit 1, you would
46
+ * POST to verifyReceipt — that path is left as a stub here because Apple is
47
+ * deprecating it.
48
+ */
49
+ export async function validateAppleReceipt(receipt, config) {
50
+ let tx;
51
+ try {
52
+ // StoreKit 2: receipt is a signed JWS transaction
53
+ tx = await decodeJws(receipt, config.skipJwsVerification);
54
+ }
55
+ catch {
56
+ console.warn('[onesub/apple] Failed to decode receipt as JWS. Falling back to null.');
57
+ return null;
58
+ }
59
+ if (!tx.originalTransactionId || !tx.productId || !tx.expiresDate) {
60
+ return null;
61
+ }
62
+ // Validate bundle ID if provided
63
+ if (tx.bundleId && tx.bundleId !== config.bundleId) {
64
+ console.warn('[onesub/apple] Bundle ID mismatch:', tx.bundleId, '!==', config.bundleId);
65
+ return null;
66
+ }
67
+ const status = deriveStatus(tx, null);
68
+ const purchasedAt = tx.originalPurchaseDate ?? tx.purchaseDate ?? Date.now();
69
+ return {
70
+ userId: '', // caller fills this in from the request body
71
+ productId: tx.productId,
72
+ platform: 'apple',
73
+ status,
74
+ expiresAt: new Date(tx.expiresDate).toISOString(),
75
+ originalTransactionId: tx.originalTransactionId,
76
+ purchasedAt: new Date(purchasedAt).toISOString(),
77
+ willRenew: status === SUBSCRIPTION_STATUS.ACTIVE, // refined by renewal info in webhook
78
+ };
79
+ }
80
+ /**
81
+ * Decode an Apple Server Notification V2 payload.
82
+ * Returns the derived subscription update to be merged into the store.
83
+ */
84
+ export async function decodeAppleNotification(payload, skipJwsVerification = false) {
85
+ const { signedTransactionInfo, signedRenewalInfo } = payload.data;
86
+ let tx;
87
+ let renewal = null;
88
+ try {
89
+ tx = await decodeJws(signedTransactionInfo, skipJwsVerification);
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ try {
95
+ renewal = await decodeJws(signedRenewalInfo, skipJwsVerification);
96
+ }
97
+ catch {
98
+ // renewal info is optional for some notification types
99
+ }
100
+ if (!tx.originalTransactionId || !tx.expiresDate)
101
+ return null;
102
+ const status = deriveStatus(tx, renewal);
103
+ const willRenew = renewal?.autoRenewStatus === 1;
104
+ return {
105
+ originalTransactionId: tx.originalTransactionId,
106
+ status,
107
+ willRenew,
108
+ expiresAt: new Date(tx.expiresDate).toISOString(),
109
+ };
110
+ }
111
+ //# sourceMappingURL=apple.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple.js","sourceRoot":"","sources":["../../src/providers/apple.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEhE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAkCrD;;;GAGG;AACH,MAAM,SAAS,GAAG,kBAAkB,CAClC,IAAI,GAAG,CAAC,qCAAqC,CAAC,CAC/C,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,GAAW,EAAE,gBAAgB,GAAG,KAAK;IACtE,IAAI,gBAAgB,EAAE,CAAC;QACrB,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC7C,OAAO,CAAC,IAAI,CACV,wEAAwE;gBACtE,kEAAkE;gBAClE,gDAAgD,CACnD,CAAC;QACJ,CAAC;QACD,yDAAyD;QACzD,OAAO,SAAS,CAAC,GAAG,CAAM,CAAC;IAC7B,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACpD,OAAO,OAAY,CAAC;AACtB,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CACnB,EAA2B,EAC3B,OAAmC;IAEnC,IAAI,EAAE,CAAC,cAAc;QAAE,OAAO,mBAAmB,CAAC,QAAQ,CAAC;IAE3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,IAAI,CAAC,CAAC;IAEpC,IAAI,OAAO,GAAG,GAAG;QAAE,OAAO,mBAAmB,CAAC,MAAM,CAAC;IAErD,iDAAiD;IACjD,IAAI,OAAO,EAAE,eAAe,KAAK,CAAC;QAAE,OAAO,mBAAmB,CAAC,QAAQ,CAAC;IAExE,OAAO,mBAAmB,CAAC,OAAO,CAAC;AACrC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,MAAmB;IAEnB,IAAI,EAA2B,CAAC;IAEhC,IAAI,CAAC;QACH,kDAAkD;QAClD,EAAE,GAAG,MAAM,SAAS,CAA0B,OAAO,EAAE,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACrF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;QACtF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,qBAAqB,IAAI,CAAC,EAAE,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,iCAAiC;IACjC,IAAI,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,oCAAoC,EAAE,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACtC,MAAM,WAAW,GAAG,EAAE,CAAC,oBAAoB,IAAI,EAAE,CAAC,YAAY,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAE7E,OAAO;QACL,MAAM,EAAE,EAAE,EAAG,6CAA6C;QAC1D,SAAS,EAAE,EAAE,CAAC,SAAS;QACvB,QAAQ,EAAE,OAAO;QACjB,MAAM;QACN,SAAS,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE;QACjD,qBAAqB,EAAE,EAAE,CAAC,qBAAqB;QAC/C,WAAW,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE;QAChD,SAAS,EAAE,MAAM,KAAK,mBAAmB,CAAC,MAAM,EAAE,qCAAqC;KACxF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,OAAiC,EACjC,mBAAmB,GAAG,KAAK;IAE3B,MAAM,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAElE,IAAI,EAA2B,CAAC;IAChC,IAAI,OAAO,GAA+B,IAAI,CAAC;IAE/C,IAAI,CAAC;QACH,EAAE,GAAG,MAAM,SAAS,CAA0B,qBAAqB,EAAE,mBAAmB,CAAC,CAAC;IAC5F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,SAAS,CAAsB,iBAAiB,EAAE,mBAAmB,CAAC,CAAC;IACzF,CAAC;IAAC,MAAM,CAAC;QACP,uDAAuD;IACzD,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,qBAAqB,IAAI,CAAC,EAAE,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAE9D,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,OAAO,EAAE,eAAe,KAAK,CAAC,CAAC;IAEjD,OAAO;QACL,qBAAqB,EAAE,EAAE,CAAC,qBAAqB;QAC/C,MAAM;QACN,SAAS;QACT,SAAS,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE;KAClD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,48 @@
1
+ import type { SubscriptionInfo, GoogleNotificationPayload, OneSubServerConfig } from '@onesub/shared';
2
+ type GoogleConfig = NonNullable<OneSubServerConfig['google']>;
3
+ /**
4
+ * Google RTDN notification types.
5
+ * https://developer.android.com/google/play/billing/rtdn-reference
6
+ */
7
+ declare const GOOGLE_NOTIFICATION_TYPE: {
8
+ readonly SUBSCRIPTION_RECOVERED: 1;
9
+ readonly SUBSCRIPTION_RENEWED: 2;
10
+ readonly SUBSCRIPTION_CANCELED: 3;
11
+ readonly SUBSCRIPTION_PURCHASED: 4;
12
+ readonly SUBSCRIPTION_ON_HOLD: 5;
13
+ readonly SUBSCRIPTION_IN_GRACE_PERIOD: 6;
14
+ readonly SUBSCRIPTION_RESTARTED: 7;
15
+ readonly SUBSCRIPTION_PRICE_CHANGE_CONFIRMED: 8;
16
+ readonly SUBSCRIPTION_DEFERRED: 9;
17
+ readonly SUBSCRIPTION_PAUSED: 10;
18
+ readonly SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED: 11;
19
+ readonly SUBSCRIPTION_REVOKED: 12;
20
+ readonly SUBSCRIPTION_EXPIRED: 13;
21
+ };
22
+ type GoogleNotificationType = (typeof GOOGLE_NOTIFICATION_TYPE)[keyof typeof GOOGLE_NOTIFICATION_TYPE];
23
+ /**
24
+ * Validate a Google Play purchase token.
25
+ *
26
+ * @param receipt - The purchaseToken from the client (Google Play Billing)
27
+ * @param productId - The subscription product ID
28
+ * @param config - Google config with packageName and optional serviceAccountKey
29
+ */
30
+ export declare function validateGoogleReceipt(receipt: string, productId: string, config: GoogleConfig): Promise<SubscriptionInfo | null>;
31
+ /**
32
+ * Decode a Google RTDN Pub/Sub notification.
33
+ * Returns the extracted subscription notification data, or null for test pings.
34
+ */
35
+ export declare function decodeGoogleNotification(payload: GoogleNotificationPayload): {
36
+ notificationType: GoogleNotificationType;
37
+ purchaseToken: string;
38
+ subscriptionId: string;
39
+ packageName: string;
40
+ } | null;
41
+ /**
42
+ * Determine if a Google RTDN notification type represents an active subscription.
43
+ */
44
+ export declare function isGoogleActiveNotification(notificationType: GoogleNotificationType): boolean;
45
+ export declare function isGoogleCanceledNotification(notificationType: GoogleNotificationType): boolean;
46
+ export declare function isGoogleExpiredNotification(notificationType: GoogleNotificationType): boolean;
47
+ export {};
48
+ //# sourceMappingURL=google.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../src/providers/google.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGtG,KAAK,YAAY,GAAG,WAAW,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAC;AAuB9D;;;GAGG;AACH,QAAA,MAAM,wBAAwB;;;;;;;;;;;;;;CAcpB,CAAC;AAEX,KAAK,sBAAsB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAyJvG;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA6BlC;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG;IAC5E,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CAuBP;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,gBAAgB,EAAE,sBAAsB,GAAG,OAAO,CAQ5F;AAED,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,sBAAsB,GAAG,OAAO,CAK9F;AAED,wBAAgB,2BAA2B,CAAC,gBAAgB,EAAE,sBAAsB,GAAG,OAAO,CAE7F"}
@@ -0,0 +1,203 @@
1
+ import { SUBSCRIPTION_STATUS } from '@onesub/shared';
2
+ /**
3
+ * Google RTDN notification types.
4
+ * https://developer.android.com/google/play/billing/rtdn-reference
5
+ */
6
+ const GOOGLE_NOTIFICATION_TYPE = {
7
+ SUBSCRIPTION_RECOVERED: 1,
8
+ SUBSCRIPTION_RENEWED: 2,
9
+ SUBSCRIPTION_CANCELED: 3,
10
+ SUBSCRIPTION_PURCHASED: 4,
11
+ SUBSCRIPTION_ON_HOLD: 5,
12
+ SUBSCRIPTION_IN_GRACE_PERIOD: 6,
13
+ SUBSCRIPTION_RESTARTED: 7,
14
+ SUBSCRIPTION_PRICE_CHANGE_CONFIRMED: 8,
15
+ SUBSCRIPTION_DEFERRED: 9,
16
+ SUBSCRIPTION_PAUSED: 10,
17
+ SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED: 11,
18
+ SUBSCRIPTION_REVOKED: 12,
19
+ SUBSCRIPTION_EXPIRED: 13,
20
+ };
21
+ /**
22
+ * Module-level token cache. Keyed by the raw serviceAccountKey string so that
23
+ * different service accounts (rare but possible) are cached independently.
24
+ */
25
+ let cachedToken = null;
26
+ let refreshPromise = null;
27
+ /**
28
+ * Obtain a Google OAuth2 access token, returning a cached token if it has more
29
+ * than 60 seconds of remaining validity. Google tokens are valid for 3600 seconds,
30
+ * so this avoids a network round-trip on every API call.
31
+ *
32
+ * Promise deduplication prevents a thundering herd: concurrent callers that
33
+ * arrive while the token is being refreshed all await the same in-flight
34
+ * request instead of each issuing their own.
35
+ */
36
+ async function getCachedAccessToken(serviceAccountKey) {
37
+ const now = Date.now();
38
+ if (cachedToken && cachedToken.key === serviceAccountKey && cachedToken.expiresAt - now > 60_000) {
39
+ return cachedToken.token;
40
+ }
41
+ if (!refreshPromise) {
42
+ refreshPromise = getAccessToken(serviceAccountKey)
43
+ .then((token) => {
44
+ cachedToken = { token, expiresAt: Date.now() + 3_600_000, key: serviceAccountKey };
45
+ return token;
46
+ })
47
+ .finally(() => { refreshPromise = null; });
48
+ }
49
+ return refreshPromise;
50
+ }
51
+ /**
52
+ * Obtain a Google OAuth2 access token using a service account key (JWT assertion flow).
53
+ * The serviceAccountKey is expected to be a JSON string of the service account credentials.
54
+ */
55
+ async function getAccessToken(serviceAccountKey) {
56
+ let key;
57
+ try {
58
+ key = JSON.parse(serviceAccountKey);
59
+ }
60
+ catch {
61
+ throw new Error('[onesub/google] Invalid serviceAccountKey JSON');
62
+ }
63
+ const tokenUri = key.token_uri ?? 'https://oauth2.googleapis.com/token';
64
+ const scope = 'https://www.googleapis.com/auth/androidpublisher';
65
+ const now = Math.floor(Date.now() / 1000);
66
+ // Build a JWT assertion
67
+ const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
68
+ const payload = Buffer.from(JSON.stringify({
69
+ iss: key.client_email,
70
+ scope,
71
+ aud: tokenUri,
72
+ iat: now,
73
+ exp: now + 3600,
74
+ })).toString('base64url');
75
+ const signingInput = `${header}.${payload}`;
76
+ // Sign with the private key using the native crypto module
77
+ const { createSign } = await import('crypto');
78
+ const sign = createSign('RSA-SHA256');
79
+ sign.update(signingInput);
80
+ const signature = sign.sign(key.private_key, 'base64url');
81
+ const assertion = `${signingInput}.${signature}`;
82
+ const resp = await fetch(tokenUri, {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
85
+ body: new URLSearchParams({
86
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
87
+ assertion,
88
+ }),
89
+ });
90
+ if (!resp.ok) {
91
+ throw new Error(`[onesub/google] Token request failed: ${resp.status}`);
92
+ }
93
+ const data = (await resp.json());
94
+ if (!data.access_token)
95
+ throw new Error('[onesub/google] No access_token in response');
96
+ return data.access_token;
97
+ }
98
+ /**
99
+ * Fetch a subscription purchase from the Google Play Developer API.
100
+ */
101
+ async function fetchSubscriptionPurchase(packageName, productId, purchaseToken, accessToken) {
102
+ const url = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${encodeURIComponent(packageName)}/purchases/subscriptions/${encodeURIComponent(productId)}/tokens/${encodeURIComponent(purchaseToken)}`;
103
+ const resp = await fetch(url, {
104
+ headers: { Authorization: `Bearer ${accessToken}` },
105
+ });
106
+ if (!resp.ok) {
107
+ const body = await resp.text();
108
+ throw new Error(`[onesub/google] Play API error ${resp.status}: ${body}`);
109
+ }
110
+ return resp.json();
111
+ }
112
+ /**
113
+ * Derive a SubscriptionStatus from a Google Play purchase resource.
114
+ */
115
+ function deriveStatus(purchase) {
116
+ const now = Date.now();
117
+ const expiryMs = purchase.expiryTimeMillis ? parseInt(purchase.expiryTimeMillis, 10) : 0;
118
+ if (expiryMs > now) {
119
+ // paymentState 0 = pending (grace period treated as active)
120
+ return SUBSCRIPTION_STATUS.ACTIVE;
121
+ }
122
+ if (purchase.cancelReason !== undefined)
123
+ return SUBSCRIPTION_STATUS.CANCELED;
124
+ return SUBSCRIPTION_STATUS.EXPIRED;
125
+ }
126
+ /**
127
+ * Validate a Google Play purchase token.
128
+ *
129
+ * @param receipt - The purchaseToken from the client (Google Play Billing)
130
+ * @param productId - The subscription product ID
131
+ * @param config - Google config with packageName and optional serviceAccountKey
132
+ */
133
+ export async function validateGoogleReceipt(receipt, productId, config) {
134
+ if (!config.serviceAccountKey) {
135
+ console.warn('[onesub/google] No serviceAccountKey provided — cannot call Play API');
136
+ return null;
137
+ }
138
+ let purchase;
139
+ try {
140
+ const token = await getCachedAccessToken(config.serviceAccountKey);
141
+ purchase = await fetchSubscriptionPurchase(config.packageName, productId, receipt, token);
142
+ }
143
+ catch (err) {
144
+ console.error('[onesub/google] Receipt validation failed:', err);
145
+ return null;
146
+ }
147
+ const status = deriveStatus(purchase);
148
+ const expiryMs = purchase.expiryTimeMillis ? parseInt(purchase.expiryTimeMillis, 10) : Date.now();
149
+ const startMs = purchase.startTimeMillis ? parseInt(purchase.startTimeMillis, 10) : Date.now();
150
+ return {
151
+ userId: '', // caller fills this in
152
+ productId,
153
+ platform: 'google',
154
+ status,
155
+ expiresAt: new Date(expiryMs).toISOString(),
156
+ originalTransactionId: purchase.orderId ?? receipt.slice(0, 64),
157
+ purchasedAt: new Date(startMs).toISOString(),
158
+ willRenew: purchase.autoRenewing ?? false,
159
+ };
160
+ }
161
+ /**
162
+ * Decode a Google RTDN Pub/Sub notification.
163
+ * Returns the extracted subscription notification data, or null for test pings.
164
+ */
165
+ export function decodeGoogleNotification(payload) {
166
+ let notification;
167
+ try {
168
+ const json = Buffer.from(payload.message.data, 'base64').toString('utf-8');
169
+ notification = JSON.parse(json);
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ if (!notification.subscriptionNotification) {
175
+ // test notification or unsupported type
176
+ return null;
177
+ }
178
+ const { notificationType, purchaseToken, subscriptionId } = notification.subscriptionNotification;
179
+ return {
180
+ notificationType,
181
+ purchaseToken,
182
+ subscriptionId,
183
+ packageName: notification.packageName,
184
+ };
185
+ }
186
+ /**
187
+ * Determine if a Google RTDN notification type represents an active subscription.
188
+ */
189
+ export function isGoogleActiveNotification(notificationType) {
190
+ return (notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_PURCHASED ||
191
+ notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RENEWED ||
192
+ notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RECOVERED ||
193
+ notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_RESTARTED ||
194
+ notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_IN_GRACE_PERIOD);
195
+ }
196
+ export function isGoogleCanceledNotification(notificationType) {
197
+ return (notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_CANCELED ||
198
+ notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_REVOKED);
199
+ }
200
+ export function isGoogleExpiredNotification(notificationType) {
201
+ return notificationType === GOOGLE_NOTIFICATION_TYPE.SUBSCRIPTION_EXPIRED;
202
+ }
203
+ //# sourceMappingURL=google.js.map