@antasphere/chassis-server 1.3.0 → 1.5.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/dist/api/create-api.d.ts.map +1 -1
- package/dist/api/create-api.js +17 -0
- package/dist/api/create-api.js.map +1 -1
- package/dist/apps/index.d.ts +11 -0
- package/dist/apps/index.d.ts.map +1 -0
- package/dist/apps/index.js +11 -0
- package/dist/apps/index.js.map +1 -0
- package/dist/apps/routes.d.ts +14 -0
- package/dist/apps/routes.d.ts.map +1 -0
- package/dist/apps/routes.js +23 -0
- package/dist/apps/routes.js.map +1 -0
- package/dist/apps/select.d.ts +64 -0
- package/dist/apps/select.d.ts.map +1 -0
- package/dist/apps/select.js +168 -0
- package/dist/apps/select.js.map +1 -0
- package/dist/apps/service.d.ts +74 -0
- package/dist/apps/service.d.ts.map +1 -0
- package/dist/apps/service.js +165 -0
- package/dist/apps/service.js.map +1 -0
- package/dist/boot.d.ts.map +1 -1
- package/dist/boot.js +5 -0
- package/dist/boot.js.map +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +2 -0
- package/dist/env.js.map +1 -1
- package/dist/identity/hub-grant.d.ts +70 -0
- package/dist/identity/hub-grant.d.ts.map +1 -1
- package/dist/identity/hub-grant.js +119 -14
- package/dist/identity/hub-grant.js.map +1 -1
- package/dist/identity/index.d.ts +1 -1
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +1 -1
- package/dist/identity/index.js.map +1 -1
- package/dist/testing/fake-hub.d.ts +20 -0
- package/dist/testing/fake-hub.d.ts.map +1 -1
- package/dist/testing/fake-hub.js +73 -1
- package/dist/testing/fake-hub.js.map +1 -1
- package/package.json +7 -3
- package/src/api/create-api.ts +21 -0
- package/src/apps/index.ts +17 -0
- package/src/apps/routes.ts +30 -0
- package/src/apps/select.ts +175 -0
- package/src/apps/service.ts +198 -0
- package/src/boot.ts +5 -0
- package/src/env.ts +2 -0
- package/src/identity/hub-grant.ts +187 -15
- package/src/identity/index.ts +6 -1
- package/src/testing/fake-hub.ts +83 -1
- package/test/integration/apps-route.test.ts +235 -0
- package/test/integration/hub-grant.test.ts +97 -0
- package/test/unit/apps.test.ts +544 -0
- package/test/unit/fake-hub-me-apps.test.ts +71 -0
- package/test/unit/hub-grant-refusal.test.ts +29 -0
- package/test/unit/hub-grant-sweep.test.ts +151 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { DEFAULT_GRANT_SWEEP_DIALS, HubGrantService } from '../../src/identity/hub-grant.js';
|
|
3
|
+
import type { Logger } from '../../src/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PRDCT-3584. A person back after a coffee used to meet the hub's token
|
|
7
|
+
* endpoint inside their first request. The sweep renews, off any request,
|
|
8
|
+
* the token of every recently seen person whose token is about to expire,
|
|
9
|
+
* through `refresh()` alone, and forgets the people not seen for an hour.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const silent = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as unknown as Logger;
|
|
13
|
+
|
|
14
|
+
function service(nowRef: { t: number }) {
|
|
15
|
+
// The constructor only stores its options; nothing here touches the db
|
|
16
|
+
// (the refresh is stubbed below), so the dummies never run.
|
|
17
|
+
const svc = new HubGrantService({
|
|
18
|
+
db: {} as never,
|
|
19
|
+
issuerUrl: 'http://hub.test',
|
|
20
|
+
clientId: 'tool-x',
|
|
21
|
+
clientSecret: 'secret',
|
|
22
|
+
tokenResource: null,
|
|
23
|
+
key: async () => 'k',
|
|
24
|
+
connectionString: 'postgres://unused',
|
|
25
|
+
logger: silent,
|
|
26
|
+
dials: {
|
|
27
|
+
accessSkewMs: 60_000,
|
|
28
|
+
tokenTimeoutMs: 5_000,
|
|
29
|
+
introspectTimeoutMs: 5_000,
|
|
30
|
+
lockWatchdogMs: 12_000
|
|
31
|
+
},
|
|
32
|
+
now: () => nowRef.t
|
|
33
|
+
});
|
|
34
|
+
const refresh = vi.spyOn(svc, 'renew').mockResolvedValue({ kind: 'ok', accessToken: 'renewed' });
|
|
35
|
+
return { svc, refresh };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const MIN = 60_000;
|
|
39
|
+
|
|
40
|
+
describe('HubGrantService.sweepExpiring', () => {
|
|
41
|
+
it('renews a recently seen person whose cached token expires within the window', async () => {
|
|
42
|
+
const now = { t: 1_000_000 };
|
|
43
|
+
const { svc, refresh } = service(now);
|
|
44
|
+
svc.prime('alice', 'tok', new Date(now.t + 15 * MIN));
|
|
45
|
+
await svc.accessToken('alice'); // seen
|
|
46
|
+
now.t += 13 * MIN; // 2 min left: inside renewAheadMs (3 min)
|
|
47
|
+
expect(await svc.sweepExpiring()).toBe(1);
|
|
48
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(refresh).toHaveBeenCalledWith('alice', DEFAULT_GRANT_SWEEP_DIALS.renewAheadMs);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('leaves a token that still has time, and a person it never saw', async () => {
|
|
53
|
+
const now = { t: 1_000_000 };
|
|
54
|
+
const { svc, refresh } = service(now);
|
|
55
|
+
svc.prime('alice', 'tok', new Date(now.t + 15 * MIN));
|
|
56
|
+
await svc.accessToken('alice');
|
|
57
|
+
svc.prime('bob', 'tok', new Date(now.t + 1 * MIN)); // primed, never read: not seen
|
|
58
|
+
now.t += 5 * MIN; // alice: 10 min left
|
|
59
|
+
expect(await svc.sweepExpiring()).toBe(0);
|
|
60
|
+
expect(refresh).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('forgets a person not seen within the active window instead of renewing them', async () => {
|
|
64
|
+
const now = { t: 1_000_000 };
|
|
65
|
+
const { svc, refresh } = service(now);
|
|
66
|
+
svc.prime('alice', 'tok', new Date(now.t + 15 * MIN));
|
|
67
|
+
await svc.accessToken('alice');
|
|
68
|
+
now.t += 61 * MIN; // seen 61 min ago, token long expired
|
|
69
|
+
expect(await svc.sweepExpiring()).toBe(0);
|
|
70
|
+
expect(refresh).not.toHaveBeenCalled();
|
|
71
|
+
// and she is gone: a later pass with a fresh token in cache still skips her
|
|
72
|
+
svc.prime('alice', 'tok2', new Date(now.t + 2 * MIN));
|
|
73
|
+
expect(await svc.sweepExpiring()).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('never throws when a renewal fails, and says so', async () => {
|
|
77
|
+
const now = { t: 1_000_000 };
|
|
78
|
+
const { svc, refresh } = service(now);
|
|
79
|
+
refresh.mockRejectedValueOnce(new Error('boom'));
|
|
80
|
+
svc.prime('alice', 'tok', new Date(now.t + 2 * MIN));
|
|
81
|
+
await svc.accessToken('alice');
|
|
82
|
+
expect(await svc.sweepExpiring()).toBe(1);
|
|
83
|
+
expect(silent.warn).toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('runs on a timer that skips while draining and can be stopped', async () => {
|
|
87
|
+
vi.useFakeTimers();
|
|
88
|
+
try {
|
|
89
|
+
const now = { t: 1_000_000 };
|
|
90
|
+
const { svc, refresh } = service(now);
|
|
91
|
+
svc.prime('alice', 'tok', new Date(now.t + 2 * MIN));
|
|
92
|
+
await svc.accessToken('alice');
|
|
93
|
+
let draining = true;
|
|
94
|
+
const sweep = svc.startSweep({ isDraining: () => draining, dials: { intervalMs: 1_000 } });
|
|
95
|
+
// the timer never holds the process open (verifier round 5, gap d)
|
|
96
|
+
expect(sweep.timer.hasRef()).toBe(false);
|
|
97
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
98
|
+
expect(refresh).not.toHaveBeenCalled();
|
|
99
|
+
draining = false;
|
|
100
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
101
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
102
|
+
sweep.stop();
|
|
103
|
+
svc.prime('alice', 'tok', new Date(now.t + 2 * MIN));
|
|
104
|
+
await vi.advanceTimersByTimeAsync(5_000);
|
|
105
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
106
|
+
} finally {
|
|
107
|
+
vi.useRealTimers();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('skips a person whose acquisition is already in flight (verifier round 5, gap a)', async () => {
|
|
112
|
+
const now = { t: 1_000_000 };
|
|
113
|
+
const { svc, refresh } = service(now);
|
|
114
|
+
svc.prime('alice', 'tok', new Date(now.t + 2 * MIN));
|
|
115
|
+
await svc.accessToken('alice');
|
|
116
|
+
// a request's acquisition is running: the sweep must not stack a rotation behind it
|
|
117
|
+
(svc as unknown as { inFlight: Map<string, Promise<unknown>> }).inFlight.set(
|
|
118
|
+
'alice',
|
|
119
|
+
new Promise(() => {})
|
|
120
|
+
);
|
|
121
|
+
expect(await svc.sweepExpiring()).toBe(0);
|
|
122
|
+
expect(refresh).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('never runs two passes at once: a slow renewal holds the next tick (verifier round 5, gap e)', async () => {
|
|
126
|
+
vi.useFakeTimers();
|
|
127
|
+
try {
|
|
128
|
+
const now = { t: 1_000_000 };
|
|
129
|
+
const { svc, refresh } = service(now);
|
|
130
|
+
refresh.mockImplementation(
|
|
131
|
+
() => new Promise((resolve) => setTimeout(() => resolve({ kind: 'ok', accessToken: 'r' }), 2_500))
|
|
132
|
+
);
|
|
133
|
+
svc.prime('alice', 'tok', new Date(now.t + 2 * MIN));
|
|
134
|
+
await svc.accessToken('alice');
|
|
135
|
+
const sweep = svc.startSweep({ dials: { intervalMs: 1_000 } });
|
|
136
|
+
await vi.advanceTimersByTimeAsync(3_200); // ticks at 1 s, 2 s, 3 s; the renewal from the first still runs
|
|
137
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
138
|
+
sweep.stop();
|
|
139
|
+
} finally {
|
|
140
|
+
vi.useRealTimers();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('keeps the default dials readable', () => {
|
|
145
|
+
expect(DEFAULT_GRANT_SWEEP_DIALS).toEqual({
|
|
146
|
+
intervalMs: 60_000,
|
|
147
|
+
renewAheadMs: 180_000,
|
|
148
|
+
activeWithinMs: 3_600_000
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|