@onesub/server 0.8.0 → 0.9.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/dist/__tests__/fetch-hardening.test.d.ts +7 -0
- package/dist/__tests__/fetch-hardening.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-hardening.test.js +192 -0
- package/dist/__tests__/fetch-hardening.test.js.map +1 -0
- package/dist/__tests__/lifecycle-scenarios.test.d.ts +21 -0
- package/dist/__tests__/lifecycle-scenarios.test.d.ts.map +1 -0
- package/dist/__tests__/lifecycle-scenarios.test.js +579 -0
- package/dist/__tests__/lifecycle-scenarios.test.js.map +1 -0
- package/dist/__tests__/paused-auto-resume.test.d.ts +7 -0
- package/dist/__tests__/paused-auto-resume.test.d.ts.map +1 -0
- package/dist/__tests__/paused-auto-resume.test.js +162 -0
- package/dist/__tests__/paused-auto-resume.test.js.map +1 -0
- package/dist/__tests__/schema.test.js +4 -0
- package/dist/__tests__/schema.test.js.map +1 -1
- package/dist/http.d.ts +17 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +36 -0
- package/dist/http.js.map +1 -0
- package/dist/providers/apple.d.ts +5 -0
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/apple.js +48 -10
- package/dist/providers/apple.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +14 -6
- package/dist/providers/google.js.map +1 -1
- package/dist/routes/webhook.d.ts.map +1 -1
- package/dist/routes/webhook.js +7 -0
- package/dist/routes/webhook.js.map +1 -1
- package/dist/stores/postgres.d.ts.map +1 -1
- package/dist/stores/postgres.js +6 -2
- package/dist/stores/postgres.js.map +1 -1
- package/dist/stores/schema.d.ts.map +1 -1
- package/dist/stores/schema.js +3 -1
- package/dist/stores/schema.js.map +1 -1
- package/package.json +2 -2
- package/sql/schema.sql +3 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for outbound fetch hardening:
|
|
3
|
+
* - Apple Server API JWT cache (re-use within TTL, mint dedup under concurrent calls)
|
|
4
|
+
* - fetchWithTimeout: AbortController fires when upstream hangs longer than the budget
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=fetch-hardening.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-hardening.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fetch-hardening.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for outbound fetch hardening:
|
|
3
|
+
* - Apple Server API JWT cache (re-use within TTL, mint dedup under concurrent calls)
|
|
4
|
+
* - fetchWithTimeout: AbortController fires when upstream hangs longer than the budget
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
|
|
7
|
+
import { generateKeyPairSync } from 'crypto';
|
|
8
|
+
import { fetchAppleSubscriptionStatus, __testing as appleTesting } from '../providers/apple.js';
|
|
9
|
+
import { fetchWithTimeout } from '../http.js';
|
|
10
|
+
import { isLocalhostUrl } from './test-utils.js';
|
|
11
|
+
let testEcKey;
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
14
|
+
testEcKey = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
15
|
+
});
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
appleTesting.clearAppleJwtCacheForTests();
|
|
19
|
+
});
|
|
20
|
+
function appleConfig() {
|
|
21
|
+
return {
|
|
22
|
+
bundleId: 'com.example.app',
|
|
23
|
+
skipJwsVerification: true,
|
|
24
|
+
keyId: 'KEY1',
|
|
25
|
+
issuerId: 'iss-uuid',
|
|
26
|
+
privateKey: testEcKey,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function makeJws(payload) {
|
|
30
|
+
const header = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
|
|
31
|
+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
32
|
+
return `${header}.${body}.fakesig`;
|
|
33
|
+
}
|
|
34
|
+
function statusResponse(orig) {
|
|
35
|
+
return {
|
|
36
|
+
bundleId: 'com.example.app',
|
|
37
|
+
environment: 'Production',
|
|
38
|
+
data: [{
|
|
39
|
+
lastTransactions: [{
|
|
40
|
+
originalTransactionId: orig,
|
|
41
|
+
status: 1,
|
|
42
|
+
signedTransactionInfo: makeJws({
|
|
43
|
+
bundleId: 'com.example.app',
|
|
44
|
+
productId: 'pro_monthly',
|
|
45
|
+
transactionId: 'tx',
|
|
46
|
+
originalTransactionId: orig,
|
|
47
|
+
purchaseDate: Date.now(),
|
|
48
|
+
originalPurchaseDate: Date.now(),
|
|
49
|
+
expiresDate: Date.now() + 86400000,
|
|
50
|
+
}),
|
|
51
|
+
signedRenewalInfo: makeJws({ autoRenewStatus: 1 }),
|
|
52
|
+
}],
|
|
53
|
+
}],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ── Apple JWT cache ─────────────────────────────────────────────────────────
|
|
57
|
+
describe('makeAppleApiJwt cache', () => {
|
|
58
|
+
it('mints a JWT once per cache window when called sequentially', async () => {
|
|
59
|
+
const originalFetch = global.fetch;
|
|
60
|
+
const calls = [];
|
|
61
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
62
|
+
if (isLocalhostUrl(url))
|
|
63
|
+
return originalFetch(url, init);
|
|
64
|
+
const headers = init?.headers;
|
|
65
|
+
calls.push({ url: String(url), auth: headers?.Authorization });
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
status: 200,
|
|
69
|
+
json: async () => statusResponse('orig_a'),
|
|
70
|
+
text: async () => '',
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
await fetchAppleSubscriptionStatus('orig_a', appleConfig());
|
|
74
|
+
await fetchAppleSubscriptionStatus('orig_a', appleConfig());
|
|
75
|
+
await fetchAppleSubscriptionStatus('orig_a', appleConfig());
|
|
76
|
+
// 3 outbound API calls, but the Authorization header (Bearer <jwt>) should
|
|
77
|
+
// be the same JWT in all of them — cache hit on calls 2 and 3.
|
|
78
|
+
expect(calls).toHaveLength(3);
|
|
79
|
+
expect(calls[0].auth).toBeDefined();
|
|
80
|
+
expect(calls[1].auth).toBe(calls[0].auth);
|
|
81
|
+
expect(calls[2].auth).toBe(calls[0].auth);
|
|
82
|
+
});
|
|
83
|
+
it('dedups concurrent JWT mints (single in-flight Promise under burst)', async () => {
|
|
84
|
+
// Use a real key pair sign spy through jose? Indirect — just count fetch
|
|
85
|
+
// Authorization headers across many concurrent calls. With dedup, all
|
|
86
|
+
// share the same JWT.
|
|
87
|
+
const originalFetch = global.fetch;
|
|
88
|
+
const auths = [];
|
|
89
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
90
|
+
if (isLocalhostUrl(url))
|
|
91
|
+
return originalFetch(url, init);
|
|
92
|
+
const headers = init?.headers;
|
|
93
|
+
auths.push(headers?.Authorization ?? '');
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
status: 200,
|
|
97
|
+
json: async () => statusResponse('orig_burst'),
|
|
98
|
+
text: async () => '',
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
await Promise.all(Array.from({ length: 10 }, () => fetchAppleSubscriptionStatus('orig_burst', appleConfig())));
|
|
102
|
+
expect(auths).toHaveLength(10);
|
|
103
|
+
const unique = new Set(auths);
|
|
104
|
+
expect(unique.size).toBe(1); // all 10 used the same JWT
|
|
105
|
+
});
|
|
106
|
+
it('mints a fresh JWT after the cache is cleared (e.g. credential rotation)', async () => {
|
|
107
|
+
const originalFetch = global.fetch;
|
|
108
|
+
const auths = [];
|
|
109
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
110
|
+
if (isLocalhostUrl(url))
|
|
111
|
+
return originalFetch(url, init);
|
|
112
|
+
const headers = init?.headers;
|
|
113
|
+
auths.push(headers?.Authorization ?? '');
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
status: 200,
|
|
117
|
+
json: async () => statusResponse('orig_clear'),
|
|
118
|
+
text: async () => '',
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
await fetchAppleSubscriptionStatus('orig_clear', appleConfig());
|
|
122
|
+
appleTesting.clearAppleJwtCacheForTests();
|
|
123
|
+
await fetchAppleSubscriptionStatus('orig_clear', appleConfig());
|
|
124
|
+
expect(auths).toHaveLength(2);
|
|
125
|
+
expect(auths[0]).not.toBe(auths[1]); // cache miss → fresh JWT
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ── fetchWithTimeout ────────────────────────────────────────────────────────
|
|
129
|
+
describe('fetchWithTimeout', () => {
|
|
130
|
+
it('returns the response when upstream replies before the timeout', async () => {
|
|
131
|
+
vi.spyOn(global, 'fetch').mockImplementation(async () => {
|
|
132
|
+
return { ok: true, status: 200, text: async () => 'hi' };
|
|
133
|
+
});
|
|
134
|
+
const resp = await fetchWithTimeout('https://example.com', undefined, 1000);
|
|
135
|
+
expect(resp.ok).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
it('aborts when upstream hangs longer than the timeout', async () => {
|
|
138
|
+
vi.spyOn(global, 'fetch').mockImplementation((_input, init) => {
|
|
139
|
+
// Simulate a hung server — never resolves until the AbortController fires
|
|
140
|
+
return new Promise((_resolve, reject) => {
|
|
141
|
+
const signal = init?.signal;
|
|
142
|
+
if (signal) {
|
|
143
|
+
signal.addEventListener('abort', () => {
|
|
144
|
+
const reason = signal.reason;
|
|
145
|
+
reject(reason instanceof Error ? reason : new Error('aborted'));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
const start = Date.now();
|
|
151
|
+
await expect(fetchWithTimeout('https://hung.example', undefined, 100)).rejects.toThrow(/timed out|aborted/i);
|
|
152
|
+
const elapsed = Date.now() - start;
|
|
153
|
+
expect(elapsed).toBeGreaterThanOrEqual(90);
|
|
154
|
+
expect(elapsed).toBeLessThan(500);
|
|
155
|
+
});
|
|
156
|
+
it('respects a caller-provided AbortSignal (composes with timeout)', async () => {
|
|
157
|
+
vi.spyOn(global, 'fetch').mockImplementation((_input, init) => {
|
|
158
|
+
return new Promise((_resolve, reject) => {
|
|
159
|
+
init?.signal?.addEventListener('abort', () => reject(new Error('aborted')));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
const ac = new AbortController();
|
|
163
|
+
setTimeout(() => ac.abort(new Error('caller cancelled')), 30);
|
|
164
|
+
await expect(fetchWithTimeout('https://hung.example', { signal: ac.signal }, 5000)).rejects.toThrow();
|
|
165
|
+
});
|
|
166
|
+
it('clears its timer on success (no leaked handles)', async () => {
|
|
167
|
+
vi.spyOn(global, 'fetch').mockImplementation(async () => {
|
|
168
|
+
return { ok: true, status: 200, text: async () => 'ok' };
|
|
169
|
+
});
|
|
170
|
+
// 100s timeout, but resolves immediately. If the timer leaked, vitest
|
|
171
|
+
// would hold the process open after the test — vitest's `--detectLeaks`
|
|
172
|
+
// would catch that. Smoke test here is just that the call returns and the
|
|
173
|
+
// process doesn't hang the test runner.
|
|
174
|
+
await fetchWithTimeout('https://example.com', undefined, 100_000);
|
|
175
|
+
// Reaching this line means the timer either fired (it shouldn't) or was
|
|
176
|
+
// cleared. We can't easily inspect `setTimeout` internals, but the suite
|
|
177
|
+
// completing in normal time is the implicit assertion.
|
|
178
|
+
expect(true).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
it('uses the default 10s budget when no timeoutMs is passed', async () => {
|
|
181
|
+
let observedTimeout;
|
|
182
|
+
vi.spyOn(global, 'fetch').mockImplementation((_input, init) => {
|
|
183
|
+
// Snapshot any abort behaviour: we can't read the timeout directly,
|
|
184
|
+
// but we can verify a signal is attached.
|
|
185
|
+
observedTimeout = init?.signal ? 1 : 0;
|
|
186
|
+
return Promise.resolve({ ok: true, status: 200, text: async () => '' });
|
|
187
|
+
});
|
|
188
|
+
await fetchWithTimeout('https://example.com');
|
|
189
|
+
expect(observedTimeout).toBe(1); // signal attached → timeout active
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
//# sourceMappingURL=fetch-hardening.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-hardening.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-hardening.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end lifecycle scenarios — exercise multi-notification sequences against
|
|
3
|
+
* the real webhook router + status route + stores, the way Apple/Google would
|
|
4
|
+
* deliver them in production.
|
|
5
|
+
*
|
|
6
|
+
* Single-notification correctness is covered by the per-feature unit tests; this
|
|
7
|
+
* file catches issues that only show up when notifications arrive in a sequence:
|
|
8
|
+
* stale state bleeding across transitions, idempotency under replay, recovery
|
|
9
|
+
* paths after a missed webhook, etc.
|
|
10
|
+
*
|
|
11
|
+
* Why scenarios instead of more unit tests:
|
|
12
|
+
* - The webhook handler has many branches (per-notification mapping, fresh
|
|
13
|
+
* re-fetch, refundPolicy gating, hooks). Each is unit-tested in isolation,
|
|
14
|
+
* but a transition like SUBSCRIBED → DID_FAIL_TO_RENEW(GRACE) → DID_RENEW
|
|
15
|
+
* touches three of those branches in turn — a regression in any one only
|
|
16
|
+
* surfaces when run together.
|
|
17
|
+
* - These scenarios mirror the runbook for sandbox verification, so the
|
|
18
|
+
* mock results here can be cross-checked against real sandbox behaviour.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=lifecycle-scenarios.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle-scenarios.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/lifecycle-scenarios.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
|