@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.
Files changed (55) hide show
  1. package/dist/api/create-api.d.ts.map +1 -1
  2. package/dist/api/create-api.js +17 -0
  3. package/dist/api/create-api.js.map +1 -1
  4. package/dist/apps/index.d.ts +11 -0
  5. package/dist/apps/index.d.ts.map +1 -0
  6. package/dist/apps/index.js +11 -0
  7. package/dist/apps/index.js.map +1 -0
  8. package/dist/apps/routes.d.ts +14 -0
  9. package/dist/apps/routes.d.ts.map +1 -0
  10. package/dist/apps/routes.js +23 -0
  11. package/dist/apps/routes.js.map +1 -0
  12. package/dist/apps/select.d.ts +64 -0
  13. package/dist/apps/select.d.ts.map +1 -0
  14. package/dist/apps/select.js +168 -0
  15. package/dist/apps/select.js.map +1 -0
  16. package/dist/apps/service.d.ts +74 -0
  17. package/dist/apps/service.d.ts.map +1 -0
  18. package/dist/apps/service.js +165 -0
  19. package/dist/apps/service.js.map +1 -0
  20. package/dist/boot.d.ts.map +1 -1
  21. package/dist/boot.js +5 -0
  22. package/dist/boot.js.map +1 -1
  23. package/dist/env.d.ts +2 -0
  24. package/dist/env.d.ts.map +1 -1
  25. package/dist/env.js +2 -0
  26. package/dist/env.js.map +1 -1
  27. package/dist/identity/hub-grant.d.ts +70 -0
  28. package/dist/identity/hub-grant.d.ts.map +1 -1
  29. package/dist/identity/hub-grant.js +119 -14
  30. package/dist/identity/hub-grant.js.map +1 -1
  31. package/dist/identity/index.d.ts +1 -1
  32. package/dist/identity/index.d.ts.map +1 -1
  33. package/dist/identity/index.js +1 -1
  34. package/dist/identity/index.js.map +1 -1
  35. package/dist/testing/fake-hub.d.ts +20 -0
  36. package/dist/testing/fake-hub.d.ts.map +1 -1
  37. package/dist/testing/fake-hub.js +73 -1
  38. package/dist/testing/fake-hub.js.map +1 -1
  39. package/package.json +7 -3
  40. package/src/api/create-api.ts +21 -0
  41. package/src/apps/index.ts +17 -0
  42. package/src/apps/routes.ts +30 -0
  43. package/src/apps/select.ts +175 -0
  44. package/src/apps/service.ts +198 -0
  45. package/src/boot.ts +5 -0
  46. package/src/env.ts +2 -0
  47. package/src/identity/hub-grant.ts +187 -15
  48. package/src/identity/index.ts +6 -1
  49. package/src/testing/fake-hub.ts +83 -1
  50. package/test/integration/apps-route.test.ts +235 -0
  51. package/test/integration/hub-grant.test.ts +97 -0
  52. package/test/unit/apps.test.ts +544 -0
  53. package/test/unit/fake-hub-me-apps.test.ts +71 -0
  54. package/test/unit/hub-grant-refusal.test.ts +29 -0
  55. 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
+ });