session-durability-b-kn.test.mjs
sha256:fbe982a22c05c6fe2e93876f250deecdb43d883f648b4e1810c7546caa9f17db
docs: activate KNOWTATION- board identity and preserve livi…
Human
minor
⚠ breaking
5 days ago
| 1 | /** |
| 2 | * SESSION-DURABILITY-b-KN — seven-tier matrix for hosted human-session continuity. |
| 3 | * Freeze: ~/scooling/docs/reviews/2026-08-28-session-durability.md §§4,6,7 (KN boundary). |
| 4 | */ |
| 5 | |
| 6 | import { describe, it, before, after } from 'node:test'; |
| 7 | import assert from 'node:assert/strict'; |
| 8 | import http from 'node:http'; |
| 9 | import crypto from 'node:crypto'; |
| 10 | import fs from 'node:fs'; |
| 11 | import path from 'node:path'; |
| 12 | import { fileURLToPath, pathToFileURL } from 'node:url'; |
| 13 | import { |
| 14 | parseHostedHubJwtExpirySeconds, |
| 15 | verifyHumanSessionAccessToken, |
| 16 | humanSessionClaimsShapeOk, |
| 17 | admitHumanSessionPayload, |
| 18 | isEstablishRefreshBrowserOriginAllowed, |
| 19 | acceptIncludesRefreshTokenCli, |
| 20 | REFRESH_TOKEN_CLI_ACCEPT, |
| 21 | HUMAN_SESSION_LIFETIME_MIN_SECONDS, |
| 22 | HUMAN_SESSION_LIFETIME_MAX_SECONDS, |
| 23 | } from '../hub/lib/human-session-admission.mjs'; |
| 24 | import { isWwwApexPair } from '../hub/gateway/cors-middleware.mjs'; |
| 25 | import { |
| 26 | createEstablishRefreshHandler, |
| 27 | REFRESH_COOKIE_NAME, |
| 28 | } from '../hub/auth-session.mjs'; |
| 29 | |
| 30 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 31 | const ROOT = path.resolve(__dirname, '..'); |
| 32 | const SECRET = 'session-durability-kn-test-secret-32b'; |
| 33 | const HUB_JS = fs.readFileSync(path.join(ROOT, 'web', 'hub', 'hub.js'), 'utf8'); |
| 34 | const INDEX_HTML = fs.readFileSync(path.join(ROOT, 'web', 'hub', 'index.html'), 'utf8'); |
| 35 | const SERVER_SRC = fs.readFileSync(path.join(ROOT, 'hub', 'gateway', 'server.mjs'), 'utf8'); |
| 36 | const AUTH_SESSION_SRC = fs.readFileSync(path.join(ROOT, 'hub', 'auth-session.mjs'), 'utf8'); |
| 37 | const CLI_AUTH_SRC = fs.readFileSync(path.join(ROOT, 'scripts', 'lib', 'hub-session-auth.mjs'), 'utf8'); |
| 38 | |
| 39 | function makeJwt(payload, secret = SECRET) { |
| 40 | const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); |
| 41 | const body = Buffer.from(JSON.stringify(payload)).toString('base64url'); |
| 42 | const sig = crypto |
| 43 | .createHmac('sha256', secret) |
| 44 | .update(`${header}.${body}`) |
| 45 | .digest('base64url'); |
| 46 | return `${header}.${body}.${sig}`; |
| 47 | } |
| 48 | |
| 49 | function sessionPayload(overrides = {}) { |
| 50 | const now = Math.floor(Date.now() / 1000); |
| 51 | return { |
| 52 | sub: 'google:durability-user', |
| 53 | provider: 'google', |
| 54 | id: 'durability-user', |
| 55 | name: 'Durability', |
| 56 | role: 'member', |
| 57 | type: 'session', |
| 58 | iat: now - 60, |
| 59 | exp: now - 60 + HUMAN_SESSION_LIFETIME_MIN_SECONDS, |
| 60 | ...overrides, |
| 61 | }; |
| 62 | } |
| 63 | |
| 64 | function mockRes() { |
| 65 | const headers = {}; |
| 66 | const cookies = {}; |
| 67 | /** @type {string[]} */ |
| 68 | const cleared = []; |
| 69 | return { |
| 70 | statusCode: 200, |
| 71 | body: null, |
| 72 | set(name, value) { |
| 73 | headers[String(name).toLowerCase()] = String(value); |
| 74 | }, |
| 75 | cookie(name, value) { |
| 76 | cookies[name] = value; |
| 77 | }, |
| 78 | clearCookie(name) { |
| 79 | cleared.push(name); |
| 80 | }, |
| 81 | status(code) { |
| 82 | this.statusCode = code; |
| 83 | return this; |
| 84 | }, |
| 85 | json(payload) { |
| 86 | this.body = payload; |
| 87 | return this; |
| 88 | }, |
| 89 | headers, |
| 90 | cookies, |
| 91 | cleared, |
| 92 | }; |
| 93 | } |
| 94 | |
| 95 | function startServer(app) { |
| 96 | const srv = http.createServer(app); |
| 97 | return new Promise((resolve, reject) => { |
| 98 | srv.listen(0, '127.0.0.1', (err) => { |
| 99 | if (err) return reject(err); |
| 100 | resolve({ |
| 101 | url: `http://127.0.0.1:${srv.address().port}`, |
| 102 | close: () => new Promise((r) => srv.close(() => r())), |
| 103 | }); |
| 104 | }); |
| 105 | }); |
| 106 | } |
| 107 | |
| 108 | async function createGateway(envExtra = {}) { |
| 109 | process.env.NETLIFY = '1'; |
| 110 | process.env.SESSION_SECRET = SECRET; |
| 111 | process.env.BILLING_ENFORCE = 'false'; |
| 112 | process.env.CANISTER_URL = ''; |
| 113 | process.env.BRIDGE_URL = ''; |
| 114 | delete process.env.HUB_JWT_EXPIRY; |
| 115 | for (const [k, v] of Object.entries(envExtra)) { |
| 116 | if (v == null) delete process.env[k]; |
| 117 | else process.env[k] = v; |
| 118 | } |
| 119 | const entry = pathToFileURL(path.join(ROOT, 'hub', 'gateway', 'server.mjs')).href; |
| 120 | const { app } = await import(`${entry}?sdkn=${Date.now()}-${Math.random()}`); |
| 121 | return startServer(app); |
| 122 | } |
| 123 | |
| 124 | // ─── 1. UNIT ───────────────────────────────────────────────────────────────── |
| 125 | |
| 126 | describe('SESSION-DURABILITY-b-KN unit', () => { |
| 127 | it('hosted expiry parser accepts integer seconds and N[smhd] in 3h–24h only', () => { |
| 128 | assert.equal(parseHostedHubJwtExpirySeconds('24h').ok, true); |
| 129 | assert.equal(parseHostedHubJwtExpirySeconds('24h').seconds, 86400); |
| 130 | assert.equal(parseHostedHubJwtExpirySeconds('3h').seconds, 10800); |
| 131 | assert.equal(parseHostedHubJwtExpirySeconds(43200).seconds, 43200); |
| 132 | assert.equal(parseHostedHubJwtExpirySeconds('10800').seconds, 10800); |
| 133 | assert.equal(parseHostedHubJwtExpirySeconds('2h').ok, false); |
| 134 | assert.equal(parseHostedHubJwtExpirySeconds('25h').ok, false); |
| 135 | assert.equal(parseHostedHubJwtExpirySeconds('24 h').ok, false); |
| 136 | assert.equal(parseHostedHubJwtExpirySeconds('1.5h').ok, false); |
| 137 | assert.equal(parseHostedHubJwtExpirySeconds('abc').ok, false); |
| 138 | assert.equal(parseHostedHubJwtExpirySeconds(10800.5).ok, false); |
| 139 | }); |
| 140 | |
| 141 | it('type:session + mandatory integer iat/exp admission', () => { |
| 142 | const now = Math.floor(Date.now() / 1000); |
| 143 | assert.equal(humanSessionClaimsShapeOk(sessionPayload()), true); |
| 144 | assert.equal(admitHumanSessionPayload(sessionPayload(), now).ok, true); |
| 145 | assert.equal(admitHumanSessionPayload(sessionPayload({ type: 'agent_access' }), now).ok, false); |
| 146 | assert.equal(admitHumanSessionPayload(sessionPayload({ type: undefined }), now).code, 'SESSION_INVALID'); |
| 147 | assert.equal( |
| 148 | admitHumanSessionPayload(sessionPayload({ iat: now - 10, exp: now + 900 }), now).code, |
| 149 | 'SESSION_INVALID', |
| 150 | ); |
| 151 | }); |
| 152 | |
| 153 | it('SESSION_EXPIRED versus SESSION_INVALID for verified tokens', () => { |
| 154 | const now = Math.floor(Date.now() / 1000); |
| 155 | const live = makeJwt(sessionPayload({ iat: now - 60, exp: now - 60 + 10800 })); |
| 156 | assert.equal(verifyHumanSessionAccessToken(live, SECRET, null, now).ok, true); |
| 157 | |
| 158 | const expiredOkShape = makeJwt( |
| 159 | sessionPayload({ iat: now - 10801, exp: now - 1, type: 'session' }), |
| 160 | ); |
| 161 | assert.equal( |
| 162 | verifyHumanSessionAccessToken(expiredOkShape, SECRET, null, now).code, |
| 163 | 'SESSION_EXPIRED', |
| 164 | ); |
| 165 | |
| 166 | const shortLived = makeJwt(sessionPayload({ iat: now - 10, exp: now + 900, type: 'session' })); |
| 167 | assert.equal(verifyHumanSessionAccessToken(shortLived, SECRET, null, now).code, 'SESSION_INVALID'); |
| 168 | |
| 169 | const missingType = makeJwt({ |
| 170 | sub: 'google:1', |
| 171 | iat: now - 60, |
| 172 | exp: now - 60 + 10800, |
| 173 | }); |
| 174 | assert.equal(verifyHumanSessionAccessToken(missingType, SECRET, null, now).code, 'SESSION_INVALID'); |
| 175 | |
| 176 | const agent = makeJwt(sessionPayload({ type: 'agent_access' })); |
| 177 | assert.equal(verifyHumanSessionAccessToken(agent, SECRET, null, now).code, 'SESSION_INVALID'); |
| 178 | |
| 179 | assert.equal(verifyHumanSessionAccessToken('not-a-jwt', SECRET, null, now).code, 'SESSION_INVALID'); |
| 180 | }); |
| 181 | |
| 182 | it('CLI Accept helper and Origin allowlist including www/apex pairing', () => { |
| 183 | assert.equal(acceptIncludesRefreshTokenCli(REFRESH_TOKEN_CLI_ACCEPT), true); |
| 184 | assert.equal(acceptIncludesRefreshTokenCli('application/json'), false); |
| 185 | assert.equal( |
| 186 | isEstablishRefreshBrowserOriginAllowed( |
| 187 | 'https://knowtation.store', |
| 188 | ['https://knowtation.store', 'https://www.knowtation.store'], |
| 189 | 'https://api.knowtation.store', |
| 190 | isWwwApexPair, |
| 191 | ), |
| 192 | true, |
| 193 | ); |
| 194 | // www request against apex-only allowlist (pairing branch). |
| 195 | assert.equal( |
| 196 | isEstablishRefreshBrowserOriginAllowed( |
| 197 | 'https://www.knowtation.store', |
| 198 | ['https://knowtation.store'], |
| 199 | 'https://api.knowtation.store', |
| 200 | isWwwApexPair, |
| 201 | ), |
| 202 | true, |
| 203 | ); |
| 204 | assert.equal( |
| 205 | isEstablishRefreshBrowserOriginAllowed( |
| 206 | 'https://knowtation.store', |
| 207 | ['https://www.knowtation.store'], |
| 208 | 'https://api.knowtation.store', |
| 209 | isWwwApexPair, |
| 210 | ), |
| 211 | true, |
| 212 | ); |
| 213 | assert.equal( |
| 214 | isEstablishRefreshBrowserOriginAllowed( |
| 215 | 'https://evil.example', |
| 216 | ['https://knowtation.store'], |
| 217 | 'https://api.knowtation.store', |
| 218 | isWwwApexPair, |
| 219 | ), |
| 220 | false, |
| 221 | ); |
| 222 | assert.equal( |
| 223 | isEstablishRefreshBrowserOriginAllowed( |
| 224 | 'null', |
| 225 | ['https://knowtation.store'], |
| 226 | 'https://api.knowtation.store', |
| 227 | isWwwApexPair, |
| 228 | ), |
| 229 | false, |
| 230 | ); |
| 231 | }); |
| 232 | |
| 233 | it('gateway signs with integer JWT_EXPIRY_SECONDS', () => { |
| 234 | assert.match(SERVER_SRC, /expiresIn:\s*JWT_EXPIRY_SECONDS/); |
| 235 | assert.match(SERVER_SRC, /expires_in:\s*JWT_EXPIRY_SECONDS/); |
| 236 | assert.match(SERVER_SRC, /parseHostedHubJwtExpirySeconds/); |
| 237 | }); |
| 238 | }); |
| 239 | |
| 240 | // ─── 2. INTEGRATION (handler + HTTP) ───────────────────────────────────────── |
| 241 | |
| 242 | describe('SESSION-DURABILITY-b-KN integration', () => { |
| 243 | it('browser Origin: cookie only, no raw refresh_token even with CLI Accept', async () => { |
| 244 | let issued = false; |
| 245 | const handler = createEstablishRefreshHandler({ |
| 246 | store: { |
| 247 | issue: async (sub) => { |
| 248 | issued = true; |
| 249 | return { token: `${sub}.rawsecret` }; |
| 250 | }, |
| 251 | }, |
| 252 | verifyHumanSession: (t) => |
| 253 | t === 'good' |
| 254 | ? { ok: true, payload: sessionPayload({ sub: 'google:1' }) } |
| 255 | : { ok: false, code: 'SESSION_INVALID' }, |
| 256 | cookieOptions: () => ({ httpOnly: true, secure: true, sameSite: 'none', path: '/api/v1/auth' }), |
| 257 | isBrowserOriginAllowed: () => true, |
| 258 | acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli, |
| 259 | }); |
| 260 | const res = mockRes(); |
| 261 | await handler( |
| 262 | { |
| 263 | headers: { |
| 264 | authorization: 'Bearer good', |
| 265 | origin: 'https://knowtation.store', |
| 266 | accept: REFRESH_TOKEN_CLI_ACCEPT, |
| 267 | }, |
| 268 | }, |
| 269 | res, |
| 270 | ); |
| 271 | assert.equal(res.statusCode, 200); |
| 272 | assert.equal(res.body.established, true); |
| 273 | assert.equal(res.body.refresh_token, undefined); |
| 274 | assert.equal(res.cookies[REFRESH_COOKIE_NAME], 'google:1.rawsecret'); |
| 275 | assert.equal(issued, true); |
| 276 | }); |
| 277 | |
| 278 | it('CLI no-Origin + Accept: refresh_token body, no Set-Cookie', async () => { |
| 279 | const handler = createEstablishRefreshHandler({ |
| 280 | store: { issue: async (sub) => ({ token: `${sub}.cli` }) }, |
| 281 | verifyHumanSession: () => ({ ok: true, payload: sessionPayload({ sub: 'google:2' }) }), |
| 282 | cookieOptions: () => ({ httpOnly: true, path: '/api/v1/auth' }), |
| 283 | isBrowserOriginAllowed: () => false, |
| 284 | acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli, |
| 285 | }); |
| 286 | const res = mockRes(); |
| 287 | await handler( |
| 288 | { |
| 289 | headers: { |
| 290 | authorization: 'Bearer good', |
| 291 | accept: REFRESH_TOKEN_CLI_ACCEPT, |
| 292 | }, |
| 293 | }, |
| 294 | res, |
| 295 | ); |
| 296 | assert.equal(res.statusCode, 200); |
| 297 | assert.equal(res.body.refresh_token, 'google:2.cli'); |
| 298 | assert.equal(res.cookies[REFRESH_COOKIE_NAME], undefined); |
| 299 | }); |
| 300 | |
| 301 | it('no-Origin wrong Accept → 406 and no store issue', async () => { |
| 302 | let issued = false; |
| 303 | const handler = createEstablishRefreshHandler({ |
| 304 | store: { |
| 305 | issue: async () => { |
| 306 | issued = true; |
| 307 | return { token: 'x' }; |
| 308 | }, |
| 309 | }, |
| 310 | verifyHumanSession: () => ({ ok: true, payload: sessionPayload() }), |
| 311 | cookieOptions: () => ({ httpOnly: true, path: '/api/v1/auth' }), |
| 312 | isBrowserOriginAllowed: () => true, |
| 313 | acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli, |
| 314 | }); |
| 315 | const res = mockRes(); |
| 316 | await handler({ headers: { authorization: 'Bearer good', accept: 'application/json' } }, res); |
| 317 | assert.equal(res.statusCode, 406); |
| 318 | assert.equal(issued, false); |
| 319 | }); |
| 320 | |
| 321 | it('malicious / null Origin → 403 and no issue', async () => { |
| 322 | let issued = false; |
| 323 | const handler = createEstablishRefreshHandler({ |
| 324 | store: { |
| 325 | issue: async () => { |
| 326 | issued = true; |
| 327 | return { token: 'x' }; |
| 328 | }, |
| 329 | }, |
| 330 | verifyHumanSession: () => ({ ok: true, payload: sessionPayload() }), |
| 331 | cookieOptions: () => ({ httpOnly: true, path: '/api/v1/auth' }), |
| 332 | isBrowserOriginAllowed: () => false, |
| 333 | acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli, |
| 334 | }); |
| 335 | for (const origin of ['null', 'https://evil.example']) { |
| 336 | issued = false; |
| 337 | const res = mockRes(); |
| 338 | await handler({ headers: { authorization: 'Bearer good', origin } }, res); |
| 339 | assert.equal(res.statusCode, 403); |
| 340 | assert.equal(issued, false); |
| 341 | } |
| 342 | }); |
| 343 | |
| 344 | it('admission failures create neither refresh record nor cookie', async () => { |
| 345 | let issued = false; |
| 346 | const handler = createEstablishRefreshHandler({ |
| 347 | store: { |
| 348 | issue: async () => { |
| 349 | issued = true; |
| 350 | return { token: 'x' }; |
| 351 | }, |
| 352 | }, |
| 353 | verifyHumanSession: (t) => { |
| 354 | if (t === 'expired') return { ok: false, code: 'SESSION_EXPIRED' }; |
| 355 | return { ok: false, code: 'SESSION_INVALID' }; |
| 356 | }, |
| 357 | cookieOptions: () => ({ httpOnly: true, path: '/api/v1/auth' }), |
| 358 | isBrowserOriginAllowed: () => true, |
| 359 | acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli, |
| 360 | }); |
| 361 | const expiredRes = mockRes(); |
| 362 | await handler( |
| 363 | { headers: { authorization: 'Bearer expired', origin: 'https://knowtation.store' } }, |
| 364 | expiredRes, |
| 365 | ); |
| 366 | assert.equal(expiredRes.statusCode, 401); |
| 367 | assert.equal(expiredRes.body.code, 'SESSION_EXPIRED'); |
| 368 | assert.equal(issued, false); |
| 369 | |
| 370 | const badRes = mockRes(); |
| 371 | await handler( |
| 372 | { headers: { authorization: 'Bearer bad', origin: 'https://knowtation.store' } }, |
| 373 | badRes, |
| 374 | ); |
| 375 | assert.equal(badRes.statusCode, 401); |
| 376 | assert.equal(badRes.body.code, 'SESSION_INVALID'); |
| 377 | assert.equal(issued, false); |
| 378 | }); |
| 379 | |
| 380 | let gw; |
| 381 | before(async () => { |
| 382 | gw = await createGateway({ |
| 383 | HUB_CORS_ORIGIN: 'https://knowtation.store,https://www.knowtation.store', |
| 384 | HUB_BASE_URL: 'https://api.knowtation.store', |
| 385 | }); |
| 386 | }); |
| 387 | after(async () => { |
| 388 | await gw.close(); |
| 389 | }); |
| 390 | |
| 391 | it('HTTP session: MCP / missing-type fail introspection', async () => { |
| 392 | const now = Math.floor(Date.now() / 1000); |
| 393 | const mcp = makeJwt(sessionPayload({ type: 'mcp_access' })); |
| 394 | const missing = makeJwt({ |
| 395 | sub: 'google:1', |
| 396 | iat: now - 60, |
| 397 | exp: now - 60 + 10800, |
| 398 | }); |
| 399 | for (const token of [mcp, missing]) { |
| 400 | const res = await fetch(`${gw.url}/api/v1/auth/session`, { |
| 401 | headers: { Authorization: `Bearer ${token}` }, |
| 402 | }); |
| 403 | const body = await res.json(); |
| 404 | assert.equal(res.status, 401); |
| 405 | assert.equal(body.code, 'SESSION_INVALID'); |
| 406 | } |
| 407 | }); |
| 408 | |
| 409 | it('HTTP session: live session returns type:session + iat/exp', async () => { |
| 410 | const token = makeJwt(sessionPayload()); |
| 411 | const res = await fetch(`${gw.url}/api/v1/auth/session`, { |
| 412 | headers: { Authorization: `Bearer ${token}` }, |
| 413 | }); |
| 414 | const body = await res.json(); |
| 415 | assert.equal(res.status, 200); |
| 416 | assert.equal(body.type, 'session'); |
| 417 | assert.equal(typeof body.iat, 'number'); |
| 418 | assert.equal(typeof body.exp, 'number'); |
| 419 | }); |
| 420 | |
| 421 | it('HTTP establish-refresh browser: no refresh_token in body', async () => { |
| 422 | const token = makeJwt(sessionPayload()); |
| 423 | const res = await fetch(`${gw.url}/api/v1/auth/establish-refresh`, { |
| 424 | method: 'POST', |
| 425 | headers: { |
| 426 | Authorization: `Bearer ${token}`, |
| 427 | Origin: 'https://knowtation.store', |
| 428 | Accept: REFRESH_TOKEN_CLI_ACCEPT, |
| 429 | 'Content-Type': 'application/json', |
| 430 | }, |
| 431 | }); |
| 432 | const body = await res.json(); |
| 433 | assert.equal(res.status, 200); |
| 434 | assert.equal(body.established, true); |
| 435 | assert.equal(body.refresh_token, undefined); |
| 436 | const setCookie = res.headers.get('set-cookie') || ''; |
| 437 | assert.match(setCookie, /ktn_refresh=/); |
| 438 | }); |
| 439 | |
| 440 | it('HTTP establish-refresh allows www Origin via apex allowlist pairing', async () => { |
| 441 | const token = makeJwt(sessionPayload()); |
| 442 | const res = await fetch(`${gw.url}/api/v1/auth/establish-refresh`, { |
| 443 | method: 'POST', |
| 444 | headers: { |
| 445 | Authorization: `Bearer ${token}`, |
| 446 | Origin: 'https://www.knowtation.store', |
| 447 | 'Content-Type': 'application/json', |
| 448 | }, |
| 449 | }); |
| 450 | const body = await res.json(); |
| 451 | assert.equal(res.status, 200); |
| 452 | assert.equal(body.established, true); |
| 453 | assert.equal(body.refresh_token, undefined); |
| 454 | }); |
| 455 | }); |
| 456 | |
| 457 | // ─── 3–7. e2e / stress / data-integrity / performance / security (source + contract) ── |
| 458 | |
| 459 | describe('SESSION-DURABILITY-b-KN e2e/source contracts', () => { |
| 460 | it('e2e: durability banner + typed establishPersistentSession in hub UI', () => { |
| 461 | assert.match(INDEX_HTML, /id="hub-session-durability-banner"/); |
| 462 | assert.match(INDEX_HTML, /role="status"/); |
| 463 | assert.match(HUB_JS, /async function establishPersistentSession/); |
| 464 | assert.match(HUB_JS, /SESSION_DURABILITY_WARN_KEY/); |
| 465 | assert.match(HUB_JS, /This session could not be made durable/); |
| 466 | assert.match(HUB_JS, /schema_version !== 1/); |
| 467 | assert.match(HUB_JS, /established !== true/); |
| 468 | }); |
| 469 | |
| 470 | it('e2e: ensureFreshHumanSession + hubApiResponse wired for copy and credentials', () => { |
| 471 | assert.match(HUB_JS, /async function ensureFreshHumanSession/); |
| 472 | assert.match(HUB_JS, /async function hubApiResponse/); |
| 473 | assert.match(HUB_JS, /btnCopyHubApiEnv\.onclick\s*=\s*async/); |
| 474 | assert.match(HUB_JS, /ensureFreshHumanSession\(120\)/); |
| 475 | assert.match(HUB_JS, /hubApiResponse\('\/api\/v1\/auth\/agent\/credentials'/); |
| 476 | assert.doesNotMatch( |
| 477 | HUB_JS, |
| 478 | /fetch\(String\(apiBase[\s\S]{0,80}\/api\/v1\/auth\/agent\/credentials/, |
| 479 | ); |
| 480 | }); |
| 481 | |
| 482 | it('stress: single-flight refresh + mutation zero network retries', () => { |
| 483 | assert.match(HUB_JS, /let refreshInFlight = null/); |
| 484 | assert.match( |
| 485 | HUB_JS, |
| 486 | /method === 'GET' \|\| method === 'HEAD'\s*\?\s*2\s*:\s*0/, |
| 487 | ); |
| 488 | }); |
| 489 | |
| 490 | it('data-integrity: five named consumers; new establish-refresh callers cannot bypass CLI Accept', () => { |
| 491 | assert.match(CLI_AUTH_SRC, /application\/vnd\.knowtation\.refresh-token\+json/); |
| 492 | assert.match(CLI_AUTH_SRC, /mode:\s*0o600/); |
| 493 | const namedConsumers = [ |
| 494 | 'scripts/lib/hub-session-auth.mjs', |
| 495 | 'scripts/hub-session-refresh.mjs', |
| 496 | 'scripts/verify-rhf-d-catalog-consent.mjs', |
| 497 | 'scripts/verify-rhf-kn0-deploy-proof.mjs', |
| 498 | 'web/hub/hub.js', |
| 499 | ]; |
| 500 | for (const rel of namedConsumers) { |
| 501 | assert.ok(fs.existsSync(path.join(ROOT, rel)), rel); |
| 502 | } |
| 503 | assert.match( |
| 504 | fs.readFileSync(path.join(ROOT, 'scripts/hub-session-refresh.mjs'), 'utf8'), |
| 505 | /establishHostedRefreshFromAccess|--save-access-token/, |
| 506 | ); |
| 507 | // Enumerate every runtime caller of establish-refresh. Browser hub.js is Origin mode. |
| 508 | // Every other .mjs/.js caller must go through hub-session-auth (vendor Accept) or be |
| 509 | // the shared helper itself. A new direct fetch with Accept: application/json fails this test. |
| 510 | const skipDir = new Set(['node_modules', '.git', '.muse', 'data', 'backups', 'dist', 'coverage']); |
| 511 | /** @type {string[]} */ |
| 512 | const callSites = []; |
| 513 | function walk(dir) { |
| 514 | for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { |
| 515 | if (skipDir.has(ent.name)) continue; |
| 516 | const full = path.join(dir, ent.name); |
| 517 | if (ent.isDirectory()) { |
| 518 | walk(full); |
| 519 | continue; |
| 520 | } |
| 521 | if (!/\.(mjs|js)$/.test(ent.name)) continue; |
| 522 | const rel = path.relative(ROOT, full); |
| 523 | if (rel.startsWith('test' + path.sep)) continue; |
| 524 | const src = fs.readFileSync(full, 'utf8'); |
| 525 | if (!src.includes('/api/v1/auth/establish-refresh')) continue; |
| 526 | callSites.push(rel); |
| 527 | } |
| 528 | } |
| 529 | walk(ROOT); |
| 530 | const allowedDirect = new Set([ |
| 531 | 'scripts/lib/hub-session-auth.mjs', |
| 532 | 'web/hub/hub.js', |
| 533 | 'hub/gateway/server.mjs', |
| 534 | 'hub/auth-session.mjs', |
| 535 | ]); |
| 536 | for (const rel of callSites) { |
| 537 | if (allowedDirect.has(rel)) continue; |
| 538 | // Named verification scripts must import the shared helper — not fetch establish-refresh. |
| 539 | const src = fs.readFileSync(path.join(ROOT, rel), 'utf8'); |
| 540 | assert.ok( |
| 541 | src.includes('establishHostedRefreshFromAccess') || |
| 542 | src.includes("from './lib/hub-session-auth.mjs'") || |
| 543 | src.includes("from '../lib/hub-session-auth.mjs'") || |
| 544 | src.includes('hub-session-auth.mjs'), |
| 545 | `${rel} calls establish-refresh outside the named delivery-mode contract`, |
| 546 | ); |
| 547 | assert.ok( |
| 548 | !/fetch\([^)]*establish-refresh/.test(src.replace(/\s+/g, ' ')), |
| 549 | `${rel} must not fetch establish-refresh directly (use hub-session-auth)`, |
| 550 | ); |
| 551 | } |
| 552 | assert.ok( |
| 553 | callSites.includes('scripts/lib/hub-session-auth.mjs'), |
| 554 | 'CLI helper must remain an establish-refresh caller', |
| 555 | ); |
| 556 | assert.ok(callSites.includes('web/hub/hub.js'), 'Hub UI must remain an establish-refresh caller'); |
| 557 | }); |
| 558 | |
| 559 | it('performance: one 401 → one refresh + one retry; auth endpoints excluded', () => { |
| 560 | assert.match(HUB_JS, /_retriedAfterRefresh/); |
| 561 | assert.match(HUB_JS, /\/api\/v1\/auth\/establish-refresh/); |
| 562 | assert.match(HUB_JS, /path !== '\/api\/v1\/auth\/refresh'/); |
| 563 | }); |
| 564 | |
| 565 | it('security: browser never returns raw refresh; establish handler mode-split present', () => { |
| 566 | assert.match(AUTH_SESSION_SRC, /hasOrigin/); |
| 567 | assert.match(AUTH_SESSION_SRC, /established:\s*true/); |
| 568 | assert.match(AUTH_SESSION_SRC, /NOT_ACCEPTABLE|406/); |
| 569 | assert.match(HUB_JS, /Object\.prototype\.hasOwnProperty\.call\(data,\s*'refresh_token'\)/); |
| 570 | }); |
| 571 | }); |
File History
1 commit
sha256:fbe982a22c05c6fe2e93876f250deecdb43d883f648b4e1810c7546caa9f17db
docs: activate KNOWTATION- board identity and preserve livi…
Human
minor
⚠
5 days ago