sec-kn-p6-session-secret-rotation.test.mjs
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
9 days ago
| 1 | /** |
| 2 | * SEC-KN-P6-ROTATE — seven-tier coverage for the dual-secret SESSION_SECRET |
| 3 | * rotation helper and its mandatory wire-up (docs/SEC-KN-P6-ROTATE-FREEZE.md |
| 4 | * §6.2 / §7 P6-C1–C3, §8 test matrix). |
| 5 | * |
| 6 | * Frozen requirements: |
| 7 | * - `verifyJwtWithSecretRotation(token, primary, previous)` — try primary, |
| 8 | * fall back to previous (verify-only), fail closed on missing primary. |
| 9 | * - EVERY G10 access-JWT verify site calls the helper (source-scan tier); |
| 10 | * missing one host/path during P1–P2 means OLD tokens 401 on that path. |
| 11 | * - Signing (jwt.sign, bridge GitHub-token encrypt, HMAC) uses primary only. |
| 12 | * - Secrets never appear in thrown messages. |
| 13 | * |
| 14 | * Tiers: unit · integration · e2e · stress · data-integrity · performance · security |
| 15 | */ |
| 16 | |
| 17 | import { test, describe } from 'node:test'; |
| 18 | import assert from 'node:assert/strict'; |
| 19 | import fs from 'node:fs'; |
| 20 | import path from 'node:path'; |
| 21 | import { performance } from 'node:perf_hooks'; |
| 22 | import { fileURLToPath } from 'node:url'; |
| 23 | import jwt from 'jsonwebtoken'; |
| 24 | import { |
| 25 | verifyJwtWithSecretRotation, |
| 26 | resolveSessionSecretPrevious, |
| 27 | } from '../hub/lib/session-secret-rotation.mjs'; |
| 28 | |
| 29 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 30 | const ROOT = path.resolve(__dirname, '..'); |
| 31 | |
| 32 | const HELPER_SRC_PATH = path.join(ROOT, 'hub/lib/session-secret-rotation.mjs'); |
| 33 | const GATEWAY_SERVER = path.join(ROOT, 'hub/gateway/server.mjs'); |
| 34 | const METADATA_BULK = path.join(ROOT, 'hub/gateway/metadata-bulk-canister.mjs'); |
| 35 | const MCP_OAUTH = path.join(ROOT, 'hub/gateway/mcp-oauth-provider.mjs'); |
| 36 | const BRIDGE_SERVER = path.join(ROOT, 'hub/bridge/server.mjs'); |
| 37 | const FLOW_ROUTES = path.join(ROOT, 'hub/bridge/flow-routes.mjs'); |
| 38 | const FLOW_CAPTURE_ROUTES = path.join(ROOT, 'hub/bridge/flow-capture-routes.mjs'); |
| 39 | const TASK_ROUTES = path.join(ROOT, 'hub/bridge/task-routes.mjs'); |
| 40 | |
| 41 | const OLD_SECRET = 'test-old-secret-0123456789abcdef0123456789abcdef'; |
| 42 | const NEW_SECRET = 'test-new-secret-fedcba9876543210fedcba9876543210'; |
| 43 | const ATTACKER_SECRET = 'attacker-secret-not-in-any-domain-x-x-x-x-x-x-x'; |
| 44 | |
| 45 | function sign(secret, claims = {}) { |
| 46 | return jwt.sign({ sub: 'google:p6-user', type: 'session', ...claims }, secret, { expiresIn: '5m' }); |
| 47 | } |
| 48 | |
| 49 | function read(p) { |
| 50 | return fs.readFileSync(p, 'utf8'); |
| 51 | } |
| 52 | |
| 53 | /** Extract a named function block from source (entry to next top-level close). */ |
| 54 | function fnBlock(src, marker) { |
| 55 | const start = src.indexOf(marker); |
| 56 | assert.ok(start >= 0, `source contains ${marker}`); |
| 57 | const end = src.indexOf('\n}', start); |
| 58 | return src.slice(start, end + 2); |
| 59 | } |
| 60 | |
| 61 | // --------------------------------------------------------------------------- |
| 62 | // Tier 1 — unit (helper contract) |
| 63 | // --------------------------------------------------------------------------- |
| 64 | describe('P6 unit — verifyJwtWithSecretRotation contract', () => { |
| 65 | test('accepts a primary-signed token', () => { |
| 66 | const payload = verifyJwtWithSecretRotation(sign(NEW_SECRET), NEW_SECRET, OLD_SECRET); |
| 67 | assert.equal(payload?.sub, 'google:p6-user'); |
| 68 | }); |
| 69 | |
| 70 | test('accepts a previous-signed token when previous is set (rotation window)', () => { |
| 71 | const payload = verifyJwtWithSecretRotation(sign(OLD_SECRET), NEW_SECRET, OLD_SECRET); |
| 72 | assert.equal(payload?.sub, 'google:p6-user'); |
| 73 | }); |
| 74 | |
| 75 | test('rejects a previous-signed token when previous is unset (window closed)', () => { |
| 76 | assert.equal(verifyJwtWithSecretRotation(sign(OLD_SECRET), NEW_SECRET, null), null); |
| 77 | assert.equal(verifyJwtWithSecretRotation(sign(OLD_SECRET), NEW_SECRET, undefined), null); |
| 78 | assert.equal(verifyJwtWithSecretRotation(sign(OLD_SECRET), NEW_SECRET, ''), null); |
| 79 | }); |
| 80 | |
| 81 | test('rejects garbage tokens and attacker-signed tokens', () => { |
| 82 | assert.equal(verifyJwtWithSecretRotation('invalid.invalid.invalid', NEW_SECRET, OLD_SECRET), null); |
| 83 | assert.equal(verifyJwtWithSecretRotation(sign(ATTACKER_SECRET), NEW_SECRET, OLD_SECRET), null); |
| 84 | assert.equal(verifyJwtWithSecretRotation('', NEW_SECRET, OLD_SECRET), null); |
| 85 | assert.equal(verifyJwtWithSecretRotation(null, NEW_SECRET, OLD_SECRET), null); |
| 86 | assert.equal(verifyJwtWithSecretRotation(42, NEW_SECRET, OLD_SECRET), null); |
| 87 | }); |
| 88 | |
| 89 | test('fail-closed: missing/empty primary refuses even a previous-signed token', () => { |
| 90 | assert.equal(verifyJwtWithSecretRotation(sign(OLD_SECRET), '', OLD_SECRET), null); |
| 91 | assert.equal(verifyJwtWithSecretRotation(sign(OLD_SECRET), null, OLD_SECRET), null); |
| 92 | assert.equal(verifyJwtWithSecretRotation(sign(OLD_SECRET), undefined, OLD_SECRET), null); |
| 93 | }); |
| 94 | |
| 95 | test('previous === primary is a misconfig no-op (no second verify, no weakening)', () => { |
| 96 | assert.equal(verifyJwtWithSecretRotation(sign(OLD_SECRET), NEW_SECRET, NEW_SECRET), null); |
| 97 | const ok = verifyJwtWithSecretRotation(sign(NEW_SECRET), NEW_SECRET, NEW_SECRET); |
| 98 | assert.equal(ok?.sub, 'google:p6-user'); |
| 99 | }); |
| 100 | |
| 101 | test('helper module never signs (no jwt.sign in source)', () => { |
| 102 | assert.ok(!read(HELPER_SRC_PATH).includes('jwt.sign'), 'rotation helper must be verify-only'); |
| 103 | }); |
| 104 | |
| 105 | test('resolveSessionSecretPrevious reads only SESSION_SECRET_PREVIOUS and normalizes empty to null', () => { |
| 106 | assert.equal(resolveSessionSecretPrevious({ SESSION_SECRET_PREVIOUS: 'x' }), 'x'); |
| 107 | assert.equal(resolveSessionSecretPrevious({ SESSION_SECRET_PREVIOUS: '' }), null); |
| 108 | assert.equal(resolveSessionSecretPrevious({}), null); |
| 109 | assert.equal(resolveSessionSecretPrevious({ HUB_JWT_SECRET: 'y' }), null); |
| 110 | }); |
| 111 | }); |
| 112 | |
| 113 | // --------------------------------------------------------------------------- |
| 114 | // Tier 2 — integration (gateway decodeVerifiedToken path during the window) |
| 115 | // --------------------------------------------------------------------------- |
| 116 | describe('P6 integration — gateway decodeVerifiedToken path accepts previous during window', () => { |
| 117 | test('decodeVerifiedToken delegates to the rotation helper with both secrets', () => { |
| 118 | const block = fnBlock(read(GATEWAY_SERVER), 'function decodeVerifiedToken(token)'); |
| 119 | assert.ok( |
| 120 | block.includes('verifyJwtWithSecretRotation(token, SESSION_SECRET, SESSION_SECRET_PREVIOUS)'), |
| 121 | 'decodeVerifiedToken uses the dual-secret helper', |
| 122 | ); |
| 123 | }); |
| 124 | |
| 125 | test('behavioral: gateway-shaped session token signed with OLD verifies while previous=OLD', () => { |
| 126 | // Same call shape as decodeVerifiedToken after the wire-up. |
| 127 | const token = jwt.sign( |
| 128 | { sub: 'google:it-user', provider: 'google', id: 'it-user', name: '', role: 'member', type: 'session' }, |
| 129 | OLD_SECRET, |
| 130 | { expiresIn: '24h' }, |
| 131 | ); |
| 132 | const during = verifyJwtWithSecretRotation(token, NEW_SECRET, OLD_SECRET); |
| 133 | assert.equal(during?.sub, 'google:it-user'); |
| 134 | assert.equal(during?.type, 'session'); |
| 135 | }); |
| 136 | |
| 137 | test('verifyToken and resolveHostedActorRole entry also use the helper', () => { |
| 138 | const src = read(GATEWAY_SERVER); |
| 139 | const vt = fnBlock(src, 'function verifyToken(token)'); |
| 140 | assert.ok(vt.includes('verifyJwtWithSecretRotation(token, SESSION_SECRET, SESSION_SECRET_PREVIOUS)')); |
| 141 | const rhar = src.slice(src.indexOf('async function resolveHostedActorRole')); |
| 142 | assert.ok( |
| 143 | rhar.slice(0, 2000).includes('verifyJwtWithSecretRotation(token, SESSION_SECRET, SESSION_SECRET_PREVIOUS)'), |
| 144 | 'resolveHostedActorRole entry verify uses the dual-secret helper', |
| 145 | ); |
| 146 | }); |
| 147 | }); |
| 148 | |
| 149 | // --------------------------------------------------------------------------- |
| 150 | // Tier 3 — e2e (simulated cutover P0 → P1 → P3) |
| 151 | // --------------------------------------------------------------------------- |
| 152 | describe('P6 e2e — simulated cutover', () => { |
| 153 | test('sign with OLD → set PREVIOUS+NEW → verify OK → clear PREVIOUS → verify fail', () => { |
| 154 | // P0: primary = OLD, previous unset. Existing tokens verify. |
| 155 | const oldToken = sign(OLD_SECRET); |
| 156 | assert.ok(verifyJwtWithSecretRotation(oldToken, OLD_SECRET, null)); |
| 157 | |
| 158 | // P1: primary = NEW, previous = OLD. Old AND new tokens verify. |
| 159 | const newToken = sign(NEW_SECRET); |
| 160 | assert.ok(verifyJwtWithSecretRotation(oldToken, NEW_SECRET, OLD_SECRET), 'OLD token verifies during window'); |
| 161 | assert.ok(verifyJwtWithSecretRotation(newToken, NEW_SECRET, OLD_SECRET), 'NEW token verifies during window'); |
| 162 | |
| 163 | // P3: previous unset. Only NEW-signed tokens verify; OLD → refuse (expected 401 upstream). |
| 164 | assert.equal(verifyJwtWithSecretRotation(oldToken, NEW_SECRET, null), null, 'OLD token refused after window'); |
| 165 | assert.ok(verifyJwtWithSecretRotation(newToken, NEW_SECRET, null), 'NEW token still verifies after window'); |
| 166 | }); |
| 167 | |
| 168 | test('cutover never accepts a token signed with neither secret', () => { |
| 169 | const forged = sign(ATTACKER_SECRET); |
| 170 | for (const [primary, previous] of [ |
| 171 | [OLD_SECRET, null], |
| 172 | [NEW_SECRET, OLD_SECRET], |
| 173 | [NEW_SECRET, null], |
| 174 | ]) { |
| 175 | assert.equal(verifyJwtWithSecretRotation(forged, primary, previous), null); |
| 176 | } |
| 177 | }); |
| 178 | }); |
| 179 | |
| 180 | // --------------------------------------------------------------------------- |
| 181 | // Tier 4 — stress (rapid alternating OLD/NEW verifies, concurrent) |
| 182 | // --------------------------------------------------------------------------- |
| 183 | describe('P6 stress — alternating OLD/NEW verify under concurrency', () => { |
| 184 | test('500 interleaved verifications stay correct', async () => { |
| 185 | const oldToken = sign(OLD_SECRET); |
| 186 | const newToken = sign(NEW_SECRET); |
| 187 | const results = await Promise.all( |
| 188 | Array.from({ length: 500 }, (_, i) => |
| 189 | Promise.resolve().then(() => { |
| 190 | const token = i % 2 === 0 ? oldToken : newToken; |
| 191 | const payload = verifyJwtWithSecretRotation(token, NEW_SECRET, OLD_SECRET); |
| 192 | return payload?.sub === 'google:p6-user'; |
| 193 | }), |
| 194 | ), |
| 195 | ); |
| 196 | assert.ok(results.every(Boolean), 'every interleaved verify resolved the correct payload'); |
| 197 | }); |
| 198 | |
| 199 | test('concurrent mixed valid/garbage tokens never cross-contaminate', async () => { |
| 200 | const valid = sign(NEW_SECRET); |
| 201 | const results = await Promise.all( |
| 202 | Array.from({ length: 200 }, (_, i) => |
| 203 | Promise.resolve().then(() => |
| 204 | verifyJwtWithSecretRotation(i % 3 === 0 ? 'garbage.garbage.garbage' : valid, NEW_SECRET, OLD_SECRET), |
| 205 | ), |
| 206 | ), |
| 207 | ); |
| 208 | results.forEach((payload, i) => { |
| 209 | if (i % 3 === 0) assert.equal(payload, null); |
| 210 | else assert.equal(payload?.sub, 'google:p6-user'); |
| 211 | }); |
| 212 | }); |
| 213 | }); |
| 214 | |
| 215 | // --------------------------------------------------------------------------- |
| 216 | // Tier 5 — data-integrity (signing/encrypt paths stay primary-only) |
| 217 | // --------------------------------------------------------------------------- |
| 218 | describe('P6 data-integrity — primary-only signing and encrypt invariants', () => { |
| 219 | /** Extract the full argument text of every `jwt.sign(...)` call via paren matching. */ |
| 220 | function jwtSignCallArgs(src) { |
| 221 | const calls = []; |
| 222 | let idx = src.indexOf('jwt.sign('); |
| 223 | while (idx !== -1) { |
| 224 | let depth = 0; |
| 225 | let end = idx + 'jwt.sign'.length; |
| 226 | for (; end < src.length; end++) { |
| 227 | if (src[end] === '(') depth++; |
| 228 | else if (src[end] === ')') { |
| 229 | depth--; |
| 230 | if (depth === 0) break; |
| 231 | } |
| 232 | } |
| 233 | calls.push(src.slice(idx, end + 1)); |
| 234 | idx = src.indexOf('jwt.sign(', end); |
| 235 | } |
| 236 | return calls; |
| 237 | } |
| 238 | |
| 239 | test('no jwt.sign anywhere uses SESSION_SECRET_PREVIOUS', () => { |
| 240 | for (const p of [GATEWAY_SERVER, MCP_OAUTH, BRIDGE_SERVER, METADATA_BULK]) { |
| 241 | for (const call of jwtSignCallArgs(read(p))) { |
| 242 | assert.ok( |
| 243 | !call.includes('SESSION_SECRET_PREVIOUS') && !call.includes('_sessionSecretPrevious'), |
| 244 | `${path.basename(p)}: jwt.sign never uses the previous secret`, |
| 245 | ); |
| 246 | } |
| 247 | } |
| 248 | }); |
| 249 | |
| 250 | test('bridge HMAC state signing and GitHub-token encrypt stay on SESSION_SECRET only', () => { |
| 251 | const src = read(BRIDGE_SERVER); |
| 252 | assert.ok( |
| 253 | !/createHmac\([^)]*SESSION_SECRET_PREVIOUS/.test(src), |
| 254 | 'signState/verifyState HMAC never keyed by the previous secret', |
| 255 | ); |
| 256 | assert.ok( |
| 257 | !/(scrypt|pbkdf2|createCipheriv|createDecipheriv)[\s\S]{0,200}?SESSION_SECRET_PREVIOUS/.test(src), |
| 258 | 'GitHub-token encrypt/decrypt never keyed by the previous secret (freeze §6.4: re-connect, not dual-decrypt)', |
| 259 | ); |
| 260 | }); |
| 261 | |
| 262 | test('MCP OAuth provider signs access tokens with the primary secret only', () => { |
| 263 | const src = read(MCP_OAUTH); |
| 264 | const signs = src.match(/jwt\.sign\([\s\S]*?\)/g) || []; |
| 265 | assert.ok(signs.length >= 1, 'provider still signs mcp_access tokens'); |
| 266 | for (const s of signs) { |
| 267 | assert.ok(s.includes('this._sessionSecret'), 'mcp_access signing uses primary'); |
| 268 | assert.ok(!s.includes('Previous'), 'mcp_access signing never uses previous'); |
| 269 | } |
| 270 | }); |
| 271 | }); |
| 272 | |
| 273 | // --------------------------------------------------------------------------- |
| 274 | // Tier 6 — performance (dual verify overhead bound) |
| 275 | // --------------------------------------------------------------------------- |
| 276 | describe('P6 performance — dual verify overhead', () => { |
| 277 | test('worst case (primary miss → previous hit) p95 under 5ms per verify', () => { |
| 278 | const oldToken = sign(OLD_SECRET); |
| 279 | // warm-up |
| 280 | for (let i = 0; i < 50; i++) verifyJwtWithSecretRotation(oldToken, NEW_SECRET, OLD_SECRET); |
| 281 | const samples = []; |
| 282 | for (let i = 0; i < 300; i++) { |
| 283 | const t0 = performance.now(); |
| 284 | const payload = verifyJwtWithSecretRotation(oldToken, NEW_SECRET, OLD_SECRET); |
| 285 | samples.push(performance.now() - t0); |
| 286 | assert.ok(payload, 'verify succeeded via previous'); |
| 287 | } |
| 288 | samples.sort((a, b) => a - b); |
| 289 | const p95 = samples[Math.floor(samples.length * 0.95)]; |
| 290 | assert.ok(p95 < 5, `dual-verify p95 ${p95.toFixed(3)}ms must stay under 5ms (two HS256 verifies)`); |
| 291 | }); |
| 292 | |
| 293 | test('no retry/sleep storms in the helper (single fall-through, no loops/timers)', () => { |
| 294 | const src = read(HELPER_SRC_PATH); |
| 295 | assert.ok(!/setTimeout|setInterval|while\s*\(|for\s*\(/.test(src), 'helper is straight-line verify logic'); |
| 296 | }); |
| 297 | }); |
| 298 | |
| 299 | // --------------------------------------------------------------------------- |
| 300 | // Tier 7 — security (regression + no secret leakage + G10 source scan) |
| 301 | // --------------------------------------------------------------------------- |
| 302 | describe('P6 security — regression and leakage', () => { |
| 303 | test('REGRESSION: single-secret-only verify fails the previous-signed case during the window', () => { |
| 304 | // Pre-P6 shape: jwt.verify(token, SESSION_SECRET) with primary = NEW only. |
| 305 | const oldToken = sign(OLD_SECRET); |
| 306 | let singleSecretPayload = null; |
| 307 | try { |
| 308 | singleSecretPayload = jwt.verify(oldToken, NEW_SECRET); |
| 309 | } catch (_) { |
| 310 | singleSecretPayload = null; |
| 311 | } |
| 312 | assert.equal(singleSecretPayload, null, 'single-secret verify drops OLD tokens (the outage the helper prevents)'); |
| 313 | assert.ok( |
| 314 | verifyJwtWithSecretRotation(oldToken, NEW_SECRET, OLD_SECRET), |
| 315 | 'dual-secret helper keeps OLD tokens alive during the drain window', |
| 316 | ); |
| 317 | }); |
| 318 | |
| 319 | test('helper never throws and never echoes secret material', () => { |
| 320 | let threw = null; |
| 321 | try { |
| 322 | verifyJwtWithSecretRotation(sign(ATTACKER_SECRET), NEW_SECRET, OLD_SECRET); |
| 323 | verifyJwtWithSecretRotation('x.y.z', NEW_SECRET, OLD_SECRET); |
| 324 | verifyJwtWithSecretRotation(sign(OLD_SECRET), '', OLD_SECRET); |
| 325 | } catch (e) { |
| 326 | threw = e; |
| 327 | } |
| 328 | assert.equal(threw, null, 'helper swallows verify errors to null'); |
| 329 | const src = read(HELPER_SRC_PATH); |
| 330 | assert.ok(!/console\.(log|error|warn)/.test(src), 'helper never logs (no secret echo path)'); |
| 331 | }); |
| 332 | |
| 333 | test('mcp-oauth verifyAccessToken error text carries no secret values', () => { |
| 334 | const block = fnBlock(read(MCP_OAUTH), 'async verifyAccessToken(token)'); |
| 335 | assert.ok(!block.includes('_sessionSecret}'), 'no secret interpolation in error messages'); |
| 336 | assert.ok(block.includes('Invalid access token'), 'stable generic error prefix retained'); |
| 337 | }); |
| 338 | |
| 339 | test('SEC-KN-3 role-cap suite file still present and asserting (companion gate)', () => { |
| 340 | const p = path.join(ROOT, 'test/sec-kn-3-mcp-access-role-cap.test.mjs'); |
| 341 | assert.ok(fs.existsSync(p), 'SEC-KN-3 suite must stay in-tree (run in the same test pass)'); |
| 342 | }); |
| 343 | }); |
| 344 | |
| 345 | // --------------------------------------------------------------------------- |
| 346 | // Tier 7b — G10 source scan (frozen: every access-JWT verify site uses helper) |
| 347 | // --------------------------------------------------------------------------- |
| 348 | describe('P6 security — G10 source scan: every access-JWT verify site calls the helper', () => { |
| 349 | const HELPER_CALL = 'verifyJwtWithSecretRotation('; |
| 350 | |
| 351 | test('gateway server.mjs: verifyToken, decodeVerifiedToken, resolveHostedActorRole', () => { |
| 352 | const src = read(GATEWAY_SERVER); |
| 353 | assert.ok(fnBlock(src, 'function verifyToken(token)').includes(HELPER_CALL), 'G10 verifyToken'); |
| 354 | assert.ok(fnBlock(src, 'function decodeVerifiedToken(token)').includes(HELPER_CALL), 'G10 decodeVerifiedToken'); |
| 355 | const rhar = src.slice(src.indexOf('async function resolveHostedActorRole')); |
| 356 | assert.ok(rhar.slice(0, 2000).includes(HELPER_CALL), 'G10 resolveHostedActorRole bearer verify'); |
| 357 | assert.ok( |
| 358 | !src.includes('jwt.verify(token, SESSION_SECRET)'), |
| 359 | 'no single-secret jwt.verify(token, SESSION_SECRET) remains in gateway server.mjs', |
| 360 | ); |
| 361 | }); |
| 362 | |
| 363 | test('gateway metadata-bulk-canister.mjs resolveRole', () => { |
| 364 | const src = read(METADATA_BULK); |
| 365 | assert.ok(src.includes(HELPER_CALL), 'G10 metadata bulk role resolve'); |
| 366 | assert.ok(!src.includes('jwt.verify('), 'no raw jwt.verify remains in metadata-bulk-canister.mjs'); |
| 367 | }); |
| 368 | |
| 369 | test('gateway mcp-oauth-provider.mjs verifyAccessToken', () => { |
| 370 | const src = read(MCP_OAUTH); |
| 371 | assert.ok(fnBlock(src, 'async verifyAccessToken(token)').includes(HELPER_CALL), 'G10 MCP OAuth access verify'); |
| 372 | assert.ok(!src.includes('jwt.verify('), 'no raw jwt.verify remains in mcp-oauth-provider.mjs'); |
| 373 | }); |
| 374 | |
| 375 | test('bridge server.mjs userIdFromJwt', () => { |
| 376 | const src = read(BRIDGE_SERVER); |
| 377 | assert.ok(fnBlock(src, 'function userIdFromJwt(token)').includes(HELPER_CALL), 'G10 bridge Bearer verify'); |
| 378 | assert.ok(!src.includes('jwt.verify('), 'no raw jwt.verify remains in bridge server.mjs'); |
| 379 | }); |
| 380 | |
| 381 | test('bridge flow / capture / task route sessionBoundFromReq', () => { |
| 382 | for (const p of [FLOW_ROUTES, FLOW_CAPTURE_ROUTES, TASK_ROUTES]) { |
| 383 | const src = read(p); |
| 384 | assert.ok( |
| 385 | fnBlock(src, 'function sessionBoundFromReq(req)').includes(HELPER_CALL), |
| 386 | `G10 ${path.basename(p)} sessionBoundFromReq`, |
| 387 | ); |
| 388 | assert.ok(!src.includes('jwt.verify('), `no raw jwt.verify remains in ${path.basename(p)}`); |
| 389 | } |
| 390 | }); |
| 391 | |
| 392 | test('both servers resolve SESSION_SECRET_PREVIOUS via the shared resolver', () => { |
| 393 | assert.ok(read(GATEWAY_SERVER).includes('resolveSessionSecretPrevious()'), 'gateway boot-time previous resolve'); |
| 394 | assert.ok(read(BRIDGE_SERVER).includes('resolveSessionSecretPrevious()'), 'bridge boot-time previous resolve'); |
| 395 | }); |
| 396 | }); |
File History
1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
9 days ago