device-oauth-data-integrity.test.mjs
118 lines 3.7 KB
Raw
sha256:c2dbf04d56308f3bbf2d06e6d2eb022b8948b1e827195fe525a44e5e18d5f9c0 feat(auth): Phase B Connect cloud agent (RFC 8628) + Hermes… Human minor ⚠ breaking 10 days ago
1 /**
2 * Phase B — device OAuth: data-integrity tier.
3 *
4 * Corrupt pending store fail-closed, expired entry pruning, and hash-at-rest
5 * invariants for device codes on disk.
6 */
7
8 import { describe, it, afterEach } from 'node:test';
9 import assert from 'node:assert/strict';
10 import fs from 'node:fs/promises';
11 import path from 'node:path';
12 import {
13 createDeviceAuthorization,
14 findByUserCode,
15 pruneExpiredDeviceCodes,
16 listPendingForDisplay,
17 hashDeviceCode,
18 } from '../hub/gateway/device-oauth-store.mjs';
19 import { createTempStrongStore } from './helpers/device-oauth-harness.mjs';
20
21 const cleanups = [];
22 afterEach(async () => {
23 while (cleanups.length) await cleanups.pop()();
24 });
25
26 describe('Phase B data-integrity — store durability', () => {
27 it('corrupt device_pending_codes.json fails closed (empty lookup)', async () => {
28 const tmp = await createTempStrongStore();
29 cleanups.push(tmp.cleanup);
30 await fs.writeFile(path.join(tmp.dir, 'device_pending_codes.json'), '{not-json', 'utf8');
31 const found = await findByUserCode('ABCD-EFGH');
32 assert.equal(found, null);
33 });
34
35 it('malformed device entries are dropped on read', async () => {
36 const tmp = await createTempStrongStore();
37 cleanups.push(tmp.cleanup);
38 const goodHash = hashDeviceCode('device-secret-for-integrity');
39 await fs.writeFile(
40 path.join(tmp.dir, 'device_pending_codes.json'),
41 JSON.stringify({
42 devices: {
43 [goodHash]: {
44 userCode: 'WXYZ-2345',
45 clientId: 'good',
46 expires: Date.now() + 600_000,
47 status: 'pending',
48 },
49 bad: { userCode: 'x' },
50 alsoBad: null,
51 },
52 }),
53 'utf8'
54 );
55 const found = await findByUserCode('WXYZ-2345');
56 assert.ok(found);
57 assert.equal(found.entry.clientId, 'good');
58 });
59
60 it('expired codes are pruned and omitted from pending list', async () => {
61 const tmp = await createTempStrongStore();
62 cleanups.push(tmp.cleanup);
63 const expiredHash = hashDeviceCode('expired-device');
64 const liveHash = hashDeviceCode('live-device');
65 const now = Date.now();
66 await fs.writeFile(
67 path.join(tmp.dir, 'device_pending_codes.json'),
68 JSON.stringify({
69 devices: {
70 [expiredHash]: {
71 userCode: 'EXPI-RED1',
72 clientId: 'old',
73 clientName: 'Old',
74 scopes: ['vault:read'],
75 userId: null,
76 status: 'pending',
77 interval: 5,
78 lastPollAt: 0,
79 expires: now - 1000,
80 vaultId: null,
81 },
82 [liveHash]: {
83 userCode: 'LIVE-CODE',
84 clientId: 'new',
85 clientName: 'New',
86 scopes: ['vault:read'],
87 userId: null,
88 status: 'pending',
89 interval: 5,
90 lastPollAt: 0,
91 expires: now + 600_000,
92 vaultId: null,
93 },
94 },
95 }),
96 'utf8'
97 );
98
99 const { removed } = await pruneExpiredDeviceCodes();
100 assert.equal(removed, 1);
101
102 const pending = await listPendingForDisplay();
103 assert.equal(pending.length, 1);
104 assert.equal(pending[0].userCode, 'LIVE-CODE');
105 });
106
107 it('hash-at-rest: createDeviceAuthorization never persists raw device_code', async () => {
108 const tmp = await createTempStrongStore();
109 cleanups.push(tmp.cleanup);
110 const created = await createDeviceAuthorization();
111 const raw = await fs.readFile(path.join(tmp.dir, 'device_pending_codes.json'), 'utf8');
112 assert.ok(!raw.includes(created.deviceCode));
113 const parsed = JSON.parse(raw);
114 const keys = Object.keys(parsed.devices);
115 assert.equal(keys.length, 1);
116 assert.equal(keys[0], hashDeviceCode(created.deviceCode));
117 });
118 });
File History 1 commit
sha256:c2dbf04d56308f3bbf2d06e6d2eb022b8948b1e827195fe525a44e5e18d5f9c0 feat(auth): Phase B Connect cloud agent (RFC 8628) + Hermes… Human minor 10 days ago