@onesub/server 0.11.3 → 0.12.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.
- package/README.md +119 -2
- package/dist/__tests__/admin-customer-detail.test.d.ts +10 -0
- package/dist/__tests__/admin-customer-detail.test.d.ts.map +1 -0
- package/dist/__tests__/admin-customer-detail.test.js +146 -0
- package/dist/__tests__/admin-customer-detail.test.js.map +1 -0
- package/dist/__tests__/cache.test.d.ts +2 -0
- package/dist/__tests__/cache.test.d.ts.map +1 -0
- package/dist/__tests__/cache.test.js +34 -0
- package/dist/__tests__/cache.test.js.map +1 -0
- package/dist/__tests__/metrics.test.js +57 -0
- package/dist/__tests__/metrics.test.js.map +1 -1
- package/dist/__tests__/openapi.test.d.ts +2 -0
- package/dist/__tests__/openapi.test.d.ts.map +1 -0
- package/dist/__tests__/openapi.test.js +32 -0
- package/dist/__tests__/openapi.test.js.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__/redis-store.test.js +117 -0
- package/dist/__tests__/redis-store.test.js.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-events.test.js +36 -0
- package/dist/__tests__/webhook-events.test.js.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/__tests__/webhook-queue.test.js +26 -0
- package/dist/__tests__/webhook-queue.test.js.map +1 -0
- package/dist/cache.d.ts +32 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +43 -0
- package/dist/cache.js.map +1 -0
- package/dist/index.cjs +3114 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3072 -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/openapi.js +196 -0
- package/dist/openapi.js.map +1 -0
- package/dist/providers/apple.d.ts +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/apple.js +45 -34
- package/dist/providers/apple.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +49 -20
- package/dist/providers/google.js.map +1 -1
- package/dist/routes/admin.d.ts +6 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/admin.js +87 -1
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/metrics.d.ts.map +1 -1
- package/dist/routes/metrics.js +57 -1
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/webhook.d.ts +2 -1
- package/dist/routes/webhook.d.ts.map +1 -1
- package/dist/routes/webhook.js +21 -1
- package/dist/routes/webhook.js.map +1 -1
- package/dist/stores/redis.d.ts +60 -0
- package/dist/stores/redis.d.ts.map +1 -0
- package/dist/stores/redis.js +255 -0
- package/dist/stores/redis.js.map +1 -0
- package/dist/stores/schema.d.ts.map +1 -1
- package/dist/stores/schema.js +10 -0
- package/dist/stores/schema.js.map +1 -1
- package/dist/tracing.d.ts +32 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +74 -0
- package/dist/tracing.js.map +1 -0
- package/dist/webhook-events.d.ts +58 -0
- package/dist/webhook-events.d.ts.map +1 -0
- package/dist/webhook-events.js +57 -0
- package/dist/webhook-events.js.map +1 -0
- package/dist/webhook-queue.d.ts +92 -0
- package/dist/webhook-queue.d.ts.map +1 -0
- package/dist/webhook-queue.js +108 -0
- package/dist/webhook-queue.js.map +1 -0
- package/package.json +33 -4
- package/sql/schema.sql +10 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
|
+
import IORedisMock from 'ioredis-mock';
|
|
3
|
+
import { RedisSubscriptionStore, RedisPurchaseStore, RedisCacheAdapter } from '../stores/redis.js';
|
|
4
|
+
import { SUBSCRIPTION_STATUS } from '@onesub/shared';
|
|
5
|
+
function makeRedis() {
|
|
6
|
+
// ioredis-mock's surface matches ioredis enough for our store's methods
|
|
7
|
+
// (set/get/zadd/zrevrange/sadd/smembers/multi). Cast to keep TS happy.
|
|
8
|
+
return new IORedisMock();
|
|
9
|
+
}
|
|
10
|
+
const baseSub = {
|
|
11
|
+
originalTransactionId: 'tx-1',
|
|
12
|
+
userId: 'alice',
|
|
13
|
+
productId: 'pro_monthly',
|
|
14
|
+
platform: 'apple',
|
|
15
|
+
status: SUBSCRIPTION_STATUS.ACTIVE,
|
|
16
|
+
expiresAt: '2030-01-01T00:00:00.000Z',
|
|
17
|
+
purchasedAt: '2025-01-01T00:00:00.000Z',
|
|
18
|
+
willRenew: true,
|
|
19
|
+
};
|
|
20
|
+
describe('RedisSubscriptionStore', () => {
|
|
21
|
+
let redis;
|
|
22
|
+
let store;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
redis = makeRedis();
|
|
25
|
+
store = new RedisSubscriptionStore(redis);
|
|
26
|
+
});
|
|
27
|
+
it('save + getByTransactionId round-trip', async () => {
|
|
28
|
+
await store.save(baseSub);
|
|
29
|
+
const fetched = await store.getByTransactionId('tx-1');
|
|
30
|
+
expect(fetched).toEqual(baseSub);
|
|
31
|
+
});
|
|
32
|
+
it('getByUserId returns the most-recent record', async () => {
|
|
33
|
+
await store.save({ ...baseSub, originalTransactionId: 'tx-1', expiresAt: '2026-01-01T00:00:00.000Z' });
|
|
34
|
+
// Force a 1ms gap so the second save lands with a higher zadd score.
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
36
|
+
await store.save({ ...baseSub, originalTransactionId: 'tx-2', expiresAt: '2027-01-01T00:00:00.000Z' });
|
|
37
|
+
const latest = await store.getByUserId('alice');
|
|
38
|
+
expect(latest?.originalTransactionId).toBe('tx-2');
|
|
39
|
+
});
|
|
40
|
+
it('getAllByUserId returns every tx newest-first', async () => {
|
|
41
|
+
await store.save({ ...baseSub, originalTransactionId: 'tx-1' });
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
43
|
+
await store.save({ ...baseSub, originalTransactionId: 'tx-2' });
|
|
44
|
+
const all = await store.getAllByUserId('alice');
|
|
45
|
+
expect(all.map((s) => s.originalTransactionId)).toEqual(['tx-2', 'tx-1']);
|
|
46
|
+
});
|
|
47
|
+
it('listAll surfaces subs across users', async () => {
|
|
48
|
+
await store.save({ ...baseSub, originalTransactionId: 'tx-a', userId: 'alice' });
|
|
49
|
+
await store.save({ ...baseSub, originalTransactionId: 'tx-b', userId: 'bob' });
|
|
50
|
+
const all = await store.listAll();
|
|
51
|
+
expect(all).toHaveLength(2);
|
|
52
|
+
});
|
|
53
|
+
it('listFiltered respects userId + status filters', async () => {
|
|
54
|
+
await store.save({ ...baseSub, originalTransactionId: 'tx-a', userId: 'alice' });
|
|
55
|
+
await store.save({ ...baseSub, originalTransactionId: 'tx-b', userId: 'alice', status: SUBSCRIPTION_STATUS.EXPIRED });
|
|
56
|
+
const result = await store.listFiltered({ userId: 'alice', status: SUBSCRIPTION_STATUS.ACTIVE });
|
|
57
|
+
expect(result.total).toBe(1);
|
|
58
|
+
expect(result.items[0].originalTransactionId).toBe('tx-a');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
const basePurchase = {
|
|
62
|
+
transactionId: 'p-1',
|
|
63
|
+
userId: 'alice',
|
|
64
|
+
productId: 'remove_ads',
|
|
65
|
+
platform: 'apple',
|
|
66
|
+
type: 'non_consumable',
|
|
67
|
+
quantity: 1,
|
|
68
|
+
purchasedAt: '2025-01-01T00:00:00.000Z',
|
|
69
|
+
};
|
|
70
|
+
describe('RedisPurchaseStore', () => {
|
|
71
|
+
let redis;
|
|
72
|
+
let store;
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
redis = makeRedis();
|
|
75
|
+
store = new RedisPurchaseStore(redis);
|
|
76
|
+
});
|
|
77
|
+
it('savePurchase + getPurchaseByTransactionId round-trip', async () => {
|
|
78
|
+
await store.savePurchase(basePurchase);
|
|
79
|
+
const fetched = await store.getPurchaseByTransactionId('p-1');
|
|
80
|
+
expect(fetched).toEqual(basePurchase);
|
|
81
|
+
});
|
|
82
|
+
it('refuses cross-user reuse of the same transactionId', async () => {
|
|
83
|
+
await store.savePurchase(basePurchase);
|
|
84
|
+
await expect(store.savePurchase({ ...basePurchase, userId: 'mallory' })).rejects.toThrow(/TRANSACTION_BELONGS_TO_OTHER_USER/);
|
|
85
|
+
});
|
|
86
|
+
it('hasPurchased detects a non-consumable buy', async () => {
|
|
87
|
+
await store.savePurchase(basePurchase);
|
|
88
|
+
expect(await store.hasPurchased('alice', 'remove_ads')).toBe(true);
|
|
89
|
+
expect(await store.hasPurchased('alice', 'other_product')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it('reassignPurchase moves ownership without losing the row', async () => {
|
|
92
|
+
await store.savePurchase(basePurchase);
|
|
93
|
+
expect(await store.reassignPurchase('p-1', 'bob')).toBe(true);
|
|
94
|
+
const fetched = await store.getPurchaseByTransactionId('p-1');
|
|
95
|
+
expect(fetched?.userId).toBe('bob');
|
|
96
|
+
expect(await store.hasPurchased('alice', 'remove_ads')).toBe(false);
|
|
97
|
+
expect(await store.hasPurchased('bob', 'remove_ads')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('deletePurchaseByTransactionId removes everywhere', async () => {
|
|
100
|
+
await store.savePurchase(basePurchase);
|
|
101
|
+
expect(await store.deletePurchaseByTransactionId('p-1')).toBe(true);
|
|
102
|
+
expect(await store.getPurchaseByTransactionId('p-1')).toBeNull();
|
|
103
|
+
expect(await store.hasPurchased('alice', 'remove_ads')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('RedisCacheAdapter', () => {
|
|
107
|
+
it('round-trips with TTL', async () => {
|
|
108
|
+
const cache = new RedisCacheAdapter(makeRedis());
|
|
109
|
+
await cache.set('k', { token: 'abc' }, 60);
|
|
110
|
+
expect(await cache.get('k')).toEqual({ token: 'abc' });
|
|
111
|
+
});
|
|
112
|
+
it('returns null for missing keys', async () => {
|
|
113
|
+
const cache = new RedisCacheAdapter(makeRedis());
|
|
114
|
+
expect(await cache.get('absent')).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=redis-store.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.test.js","sourceRoot":"","sources":["../../src/__tests__/redis-store.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CacheWebhookEventStore, InMemoryWebhookEventStore } from '../webhook-events.js';
|
|
3
|
+
import { InMemoryCacheAdapter } from '../cache.js';
|
|
4
|
+
describe('InMemoryWebhookEventStore', () => {
|
|
5
|
+
it('returns true the first time, false on retry', async () => {
|
|
6
|
+
const store = new InMemoryWebhookEventStore();
|
|
7
|
+
expect(await store.markIfNew('apple', 'uuid-1')).toBe(true);
|
|
8
|
+
expect(await store.markIfNew('apple', 'uuid-1')).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('keys by provider so apple/google ids never collide', async () => {
|
|
11
|
+
const store = new InMemoryWebhookEventStore();
|
|
12
|
+
expect(await store.markIfNew('apple', 'shared')).toBe(true);
|
|
13
|
+
expect(await store.markIfNew('google', 'shared')).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('expires entries after the TTL', async () => {
|
|
16
|
+
const store = new InMemoryWebhookEventStore(1); // 1-second TTL
|
|
17
|
+
expect(await store.markIfNew('apple', 'uuid-1')).toBe(true);
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
19
|
+
// Re-eligible after expiry — Apple/Google retries beyond the window are
|
|
20
|
+
// effectively new events, which is the intended behavior.
|
|
21
|
+
expect(await store.markIfNew('apple', 'uuid-1')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('CacheWebhookEventStore', () => {
|
|
25
|
+
it('uses any CacheAdapter for cluster-shared dedupe', async () => {
|
|
26
|
+
const cache = new InMemoryCacheAdapter();
|
|
27
|
+
const store = new CacheWebhookEventStore(cache);
|
|
28
|
+
expect(await store.markIfNew('google', 'msg-1')).toBe(true);
|
|
29
|
+
expect(await store.markIfNew('google', 'msg-1')).toBe(false);
|
|
30
|
+
// A second store backed by the same cache sees the prior write — this is
|
|
31
|
+
// the multi-instance idempotency guarantee we ship.
|
|
32
|
+
const store2 = new CacheWebhookEventStore(cache);
|
|
33
|
+
expect(await store2.markIfNew('google', 'msg-1')).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=webhook-events.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-events.test.js","sourceRoot":"","sources":["../../src/__tests__/webhook-events.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,KAAK,GAAG,IAAI,yBAAyB,EAAE,CAAC;QAC9C,MAAM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,KAAK,GAAG,IAAI,yBAAyB,EAAE,CAAC;QAC9C,MAAM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC7C,MAAM,KAAK,GAAG,IAAI,yBAAyB,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe;QAC/D,MAAM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAC1D,wEAAwE;QACxE,0DAA0D;QAC1D,MAAM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,KAAK,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,sBAAsB,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE7D,yEAAyE;QACzE,oDAAoD;QACpD,MAAM,MAAM,GAAG,IAAI,sBAAsB,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-queue.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-queue.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { InProcessWebhookQueue } from '../webhook-queue.js';
|
|
3
|
+
describe('InProcessWebhookQueue', () => {
|
|
4
|
+
it('runs the handler synchronously inside enqueue', async () => {
|
|
5
|
+
const queue = new InProcessWebhookQueue();
|
|
6
|
+
const seen = [];
|
|
7
|
+
queue.setHandler(async (job) => {
|
|
8
|
+
seen.push(job);
|
|
9
|
+
});
|
|
10
|
+
await queue.enqueue({ provider: 'apple', eventId: 'uuid-1', payload: { foo: 'bar' } });
|
|
11
|
+
expect(seen).toHaveLength(1);
|
|
12
|
+
expect(seen[0].eventId).toBe('uuid-1');
|
|
13
|
+
});
|
|
14
|
+
it('throws if no handler has been registered', async () => {
|
|
15
|
+
const queue = new InProcessWebhookQueue();
|
|
16
|
+
await expect(queue.enqueue({ provider: 'apple', eventId: 'uuid-1', payload: {} })).rejects.toThrow(/no handler/);
|
|
17
|
+
});
|
|
18
|
+
it('propagates handler errors to enqueue', async () => {
|
|
19
|
+
const queue = new InProcessWebhookQueue();
|
|
20
|
+
queue.setHandler(async () => {
|
|
21
|
+
throw new Error('downstream failed');
|
|
22
|
+
});
|
|
23
|
+
await expect(queue.enqueue({ provider: 'apple', eventId: 'uuid-1', payload: {} })).rejects.toThrow(/downstream failed/);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=webhook-queue.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-queue.test.js","sourceRoot":"","sources":["../../src/__tests__/webhook-queue.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,KAAK,GAAG,IAAI,qBAAqB,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAiB,EAAE,CAAC;QAC9B,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC,CAAC,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACvF,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,KAAK,GAAG,IAAI,qBAAqB,EAAE,CAAC;QAC1C,MAAM,MAAM,CACV,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CACrE,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,KAAK,GAAG,IAAI,qBAAqB,EAAE,CAAC;QAC1C,KAAK,CAAC,UAAU,CAAC,KAAK,IAAI,EAAE;YAC1B,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,CACV,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CACrE,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
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"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local in-memory cache. Default for single-instance deployments.
|
|
3
|
+
*/
|
|
4
|
+
export class InMemoryCacheAdapter {
|
|
5
|
+
entries = new Map();
|
|
6
|
+
async get(key) {
|
|
7
|
+
const entry = this.entries.get(key);
|
|
8
|
+
if (!entry)
|
|
9
|
+
return null;
|
|
10
|
+
if (entry.expiresAt !== 0 && entry.expiresAt < Date.now()) {
|
|
11
|
+
this.entries.delete(key);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return entry.value;
|
|
15
|
+
}
|
|
16
|
+
async set(key, value, ttlSeconds) {
|
|
17
|
+
const expiresAt = ttlSeconds && ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : 0;
|
|
18
|
+
this.entries.set(key, { value, expiresAt });
|
|
19
|
+
}
|
|
20
|
+
async del(key) {
|
|
21
|
+
this.entries.delete(key);
|
|
22
|
+
}
|
|
23
|
+
/** Test helper — clear everything. Not part of the public CacheAdapter contract. */
|
|
24
|
+
clear() {
|
|
25
|
+
this.entries.clear();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Module-level singleton used by providers when no per-call adapter is
|
|
30
|
+
* supplied. Keeps the existing single-instance behavior with zero config.
|
|
31
|
+
*/
|
|
32
|
+
let defaultAdapter = new InMemoryCacheAdapter();
|
|
33
|
+
export function getDefaultCache() {
|
|
34
|
+
return defaultAdapter;
|
|
35
|
+
}
|
|
36
|
+
export function setDefaultCache(adapter) {
|
|
37
|
+
defaultAdapter = adapter;
|
|
38
|
+
}
|
|
39
|
+
/** Test helper — reset the default adapter to a fresh in-memory one. */
|
|
40
|
+
export function __resetDefaultCacheForTests() {
|
|
41
|
+
defaultAdapter = new InMemoryCacheAdapter();
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAyBA;;GAEG;AACH,MAAM,OAAO,oBAAoB;IACd,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAC;IAEpD,KAAK,CAAC,GAAG,CAAI,GAAW;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,KAAK,CAAC,SAAS,KAAK,CAAC,IAAI,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC1D,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC,KAAU,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,GAAG,CAAI,GAAW,EAAE,KAAQ,EAAE,UAAmB;QACrD,MAAM,SAAS,GAAG,UAAU,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACpF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED,oFAAoF;IACpF,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACF;AAED;;;GAGG;AACH,IAAI,cAAc,GAAiB,IAAI,oBAAoB,EAAE,CAAC;AAE9D,MAAM,UAAU,eAAe;IAC7B,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAqB;IACnD,cAAc,GAAG,OAAO,CAAC;AAC3B,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,2BAA2B;IACzC,cAAc,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC9C,CAAC"}
|