companion-loopback-guard-data-integrity.test.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Tier 5 — DATA-INTEGRITY: purity, determinism, and no-mutation guarantees. |
| 3 | * |
| 4 | * The guard is a decision authority; its trustworthiness depends on being a PURE function of its |
| 5 | * inputs. This tier proves: identical inputs → identical verdict; inputs are never mutated; the |
| 6 | * verdict shape is invariant; reason codes come only from the frozen constant set; and the |
| 7 | * rate-state helpers produce new state without aliasing the input. |
| 8 | * |
| 9 | * Reference: docs/COMPANION-APP-PHASE-2-LOOPBACK-SECURITY.md (purity / fail-closed invariants). |
| 10 | */ |
| 11 | import { describe, it } from 'node:test'; |
| 12 | import assert from 'node:assert/strict'; |
| 13 | import { |
| 14 | verifyLoopbackRequest, |
| 15 | createLoopbackRateState, |
| 16 | recordLoopbackRequest, |
| 17 | LOOPBACK_GUARD_REASONS, |
| 18 | } from '../lib/companion-loopback-guard.mjs'; |
| 19 | |
| 20 | const PORT = '53550'; |
| 21 | const TOKEN = 'integ-' + 'f'.repeat(40); |
| 22 | const ALLOWED_HOSTS = [`127.0.0.1:${PORT}`, `localhost:${PORT}`]; |
| 23 | const REASON_VALUES = new Set(Object.values(LOOPBACK_GUARD_REASONS)); |
| 24 | |
| 25 | function base(overrides = {}) { |
| 26 | return { |
| 27 | method: 'POST', |
| 28 | headers: { Host: `127.0.0.1:${PORT}`, Origin: `http://127.0.0.1:${PORT}`, 'Sec-Fetch-Site': 'same-origin' }, |
| 29 | token: TOKEN, |
| 30 | expectedToken: TOKEN, |
| 31 | allowedHosts: ALLOWED_HOSTS, |
| 32 | now: 1000, |
| 33 | rateState: createLoopbackRateState(), |
| 34 | ...overrides, |
| 35 | }; |
| 36 | } |
| 37 | |
| 38 | describe('Data-integrity — determinism', () => { |
| 39 | it('identical inputs produce identical verdicts across 10k calls', () => { |
| 40 | const input = base(); |
| 41 | const first = verifyLoopbackRequest(input); |
| 42 | for (let i = 0; i < 10_000; i++) { |
| 43 | assert.deepEqual(verifyLoopbackRequest(input), first); |
| 44 | } |
| 45 | }); |
| 46 | |
| 47 | it('each distinct rejection class is stable', () => { |
| 48 | const cases = [ |
| 49 | base({ method: 'DELETE' }), |
| 50 | base({ headers: { Host: 'bad.example' } }), |
| 51 | base({ headers: { Host: `127.0.0.1:${PORT}`, 'Sec-Fetch-Site': 'cross-site' } }), |
| 52 | base({ token: 'x' }), |
| 53 | base({ rateState: { windowMs: 1000, maxRequests: 1, timestamps: [1000] } }), |
| 54 | ]; |
| 55 | for (const c of cases) { |
| 56 | const a = verifyLoopbackRequest(c); |
| 57 | const b = verifyLoopbackRequest(c); |
| 58 | assert.deepEqual(a, b); |
| 59 | } |
| 60 | }); |
| 61 | }); |
| 62 | |
| 63 | describe('Data-integrity — no input mutation', () => { |
| 64 | it('verifyLoopbackRequest does not mutate headers, allowedHosts, or rateState', () => { |
| 65 | const headers = { Host: `127.0.0.1:${PORT}`, Origin: `http://127.0.0.1:${PORT}`, 'Sec-Fetch-Site': 'same-origin' }; |
| 66 | const allowedHosts = [`127.0.0.1:${PORT}`]; |
| 67 | const rateState = createLoopbackRateState({ windowMs: 1000, maxRequests: 5 }); |
| 68 | const headersSnapshot = JSON.stringify(headers); |
| 69 | const hostsSnapshot = JSON.stringify(allowedHosts); |
| 70 | const rateSnapshot = JSON.stringify(rateState); |
| 71 | |
| 72 | verifyLoopbackRequest({ method: 'POST', headers, token: TOKEN, expectedToken: TOKEN, allowedHosts, now: 1, rateState }); |
| 73 | |
| 74 | assert.equal(JSON.stringify(headers), headersSnapshot); |
| 75 | assert.equal(JSON.stringify(allowedHosts), hostsSnapshot); |
| 76 | assert.equal(JSON.stringify(rateState), rateSnapshot); |
| 77 | }); |
| 78 | |
| 79 | it('recordLoopbackRequest returns a new object and leaves the input untouched', () => { |
| 80 | const s0 = createLoopbackRateState({ windowMs: 1000, maxRequests: 5 }); |
| 81 | const before = JSON.stringify(s0); |
| 82 | const s1 = recordLoopbackRequest(s0, 10); |
| 83 | assert.notEqual(s1, s0, 'must be a new reference'); |
| 84 | assert.notEqual(s1.timestamps, s0.timestamps, 'timestamps array must be a new array'); |
| 85 | assert.equal(JSON.stringify(s0), before, 'input not mutated'); |
| 86 | }); |
| 87 | }); |
| 88 | |
| 89 | describe('Data-integrity — verdict shape and reason domain', () => { |
| 90 | it('every verdict has exactly { allow, status, reason } with valid types', () => { |
| 91 | const inputs = [ |
| 92 | base(), |
| 93 | base({ method: 'PUT' }), |
| 94 | base({ headers: { Host: 'bad' } }), |
| 95 | base({ token: undefined }), |
| 96 | base({ rateState: undefined }), |
| 97 | ]; |
| 98 | for (const inp of inputs) { |
| 99 | const v = verifyLoopbackRequest(inp); |
| 100 | assert.deepEqual(Object.keys(v).sort(), ['allow', 'reason', 'status']); |
| 101 | assert.equal(typeof v.allow, 'boolean'); |
| 102 | assert.ok([200, 401, 403, 429].includes(v.status)); |
| 103 | assert.ok(REASON_VALUES.has(v.reason), `reason ${v.reason} must be a known constant`); |
| 104 | } |
| 105 | }); |
| 106 | |
| 107 | it('allow===true iff status===200 iff reason===ok', () => { |
| 108 | const inputs = [base(), base({ token: 'bad' }), base({ method: 'PUT' }), base({ rateState: undefined })]; |
| 109 | for (const inp of inputs) { |
| 110 | const v = verifyLoopbackRequest(inp); |
| 111 | assert.equal(v.allow, v.status === 200); |
| 112 | assert.equal(v.allow, v.reason === LOOPBACK_GUARD_REASONS.OK); |
| 113 | } |
| 114 | }); |
| 115 | |
| 116 | it('LOOPBACK_GUARD_REASONS is frozen', () => { |
| 117 | assert.equal(Object.isFrozen(LOOPBACK_GUARD_REASONS), true); |
| 118 | }); |
| 119 | }); |
| 120 | |
| 121 | describe('Data-integrity — no global / env state read', () => { |
| 122 | it('produces the same verdict regardless of surrounding env vars', () => { |
| 123 | const v1 = verifyLoopbackRequest(base()); |
| 124 | process.env.KNOWTATION_FAKE_TEST_VAR = 'tampered'; |
| 125 | const v2 = verifyLoopbackRequest(base()); |
| 126 | delete process.env.KNOWTATION_FAKE_TEST_VAR; |
| 127 | assert.deepEqual(v1, v2); |
| 128 | }); |
| 129 | }); |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
1 day ago