// P9 slice-3 tenant isolation smoke: two tenants (different orgId, same app) prove // cross-tenant denial over HTTP. Tenant A schedules a meeting; tenant B cannot read // it (403) and does not see it in its own list; A reads its own (200). // Requires the service running with IIOS_DEV_TOKENS=1. import 'dotenv/config'; const SERVICE = process.env.SMOKE_URL ?? 'http://localhost:3200'; const APP_ID = 'portal-demo'; const assert = (c, m) => { if (!c) { console.error('✗', m); process.exit(1); } console.log('✓', m); }; const START = '2026-07-02T10:00:00.000Z'; async function devToken(userId, orgId) { const r = await fetch(`${SERVICE}/v1/dev/token`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ appId: APP_ID, userId, name: userId, orgId }), }); if (!r.ok) throw new Error(`devToken ${r.status} (service with IIOS_DEV_TOKENS=1?)`); return (await r.json()).token; } async function call(token, path, method = 'GET', body) { return fetch(`${SERVICE}${path}`, { method, headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: body ? JSON.stringify(body) : undefined, }); } async function json(token, path, method = 'GET', body) { const r = await call(token, path, method, body); if (!r.ok) throw new Error(`${method} ${path} ${r.status}: ${await r.text()}`); return r.json(); } const A = await devToken('a1', 'org_A'); const B = await devToken('b1', 'org_B'); // A schedules a meeting in tenant A's scope. const meeting = await json(A, '/v1/calendar/meetings', 'POST', { meetingType: 'INTERNAL', title: 'A only', startAt: START }); assert(meeting.id && meeting.status === 'SCHEDULED', 'tenant A scheduled a meeting'); // B cannot read A's meeting by id. const bGet = await call(B, `/v1/calendar/meetings/${meeting.id}`); assert(bGet.status === 403, `tenant B reading A's meeting → 403 (got ${bGet.status}) — KG-02`); // B's own list does not include A's meeting. const bList = await json(B, '/v1/calendar/meetings'); assert(!bList.some((m) => m.id === meeting.id), "tenant B's meeting list excludes A's meeting"); // A reads its own meeting fine (same-tenant control). const aGet = await json(A, `/v1/calendar/meetings/${meeting.id}`); assert(aGet.id === meeting.id, 'same-tenant read succeeds (200)'); console.log('\nP9 tenant isolation smoke: PASS'); process.exit(0);