railiance-platform/scripts/mas_evidence_iam_session.mjs
codex ca7c46b775 Align the MAS IAM rule and read num_uses from the raw writer response.
Adds the one-session Scaleway IAM grant script, merges the publisher
bucket probes into one statement to fit the 10-statement limit, keeps
the Playwright cache under the account home during attended login, and
records the publisher-gate runs through the num_uses rejection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 49552@bnt-lap001
Assistant-Session: 575802ee-c0c0-42a9-b2a1-9204c9f5b3f4
2026-11-02 19:28:28 +01:00

80 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/** One attended Console login. The helper compares and corrects the MAS IAM rule. */
import {spawn} from 'node:child_process';
import {readFileSync} from 'node:fs';
import {dirname, join, resolve} from 'node:path';
import {fileURLToPath, pathToFileURL} from 'node:url';
import {parseArgs} from 'node:util';
import {createInterface} from 'node:readline/promises';
const {values} = parseArgs({options: {'playwright-module': {type: 'string'}}});
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const grant = join(root, 'scripts', 'mas_evidence_iam_grant.py');
let browser;
let child;
async function sessionAuth(page) {
if (new URL(page.url()).origin !== 'https://console.scaleway.com') {
throw new Error('console_page_required');
}
return page.evaluate(() => {
const names = Object.keys(localStorage).filter((name) => name.startsWith('_scw_session_console_scaleway_com_'));
if (names.length !== 1) throw new Error('unambiguous_session_required');
const session = JSON.parse(localStorage.getItem(names[0]));
if (new Date(session.jwt.expiresAt).getTime() < Date.now() + 30000) {
throw new Error('fresh_console_session_required');
}
return {session_token: session.token, user_agent: session.jwt.userAgent};
});
}
function runGrant(auth) {
return new Promise((resolveResult, reject) => {
child = spawn('python3', [grant, '--execute'], {stdio: ['pipe', 'pipe', 'pipe']});
let output = '';
child.stdout.on('data', (data) => {
if (output.length < 65536) output += data.toString();
});
child.stderr.on('data', () => {});
child.on('error', () => reject(new Error('grant_child_failed')));
child.on('close', (code) => {
child = null;
resolveResult({exit_code: code, output});
});
child.stdin.end(JSON.stringify(auth));
});
}
try {
const modulePath = resolve(values['playwright-module'] || '');
const pkg = JSON.parse(readFileSync(join(dirname(modulePath), 'package.json'), 'utf8'));
if (pkg.name !== 'playwright' || pkg.version !== '1.62.1') throw new Error('locked_playwright_required');
const imported = await import(pathToFileURL(modulePath).href);
const playwright = imported.chromium ? imported : imported.default;
if (!playwright?.chromium?.launch) throw new Error('playwright_export_unusable');
const {chromium} = playwright;
browser = await chromium.launch({headless: false});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://console.scaleway.com/', {waitUntil: 'domcontentloaded', timeout: 60000});
const reader = createInterface({input: process.stdin, output: process.stdout});
console.log('Console opened. Sign in, then press Enter. The session token stays in this process.');
await reader.question('');
reader.close();
const outcome = await runGrant(await sessionAuth(page));
process.stdout.write(outcome.output.endsWith('\n') ? outcome.output : `${outcome.output}\n`);
process.exitCode = outcome.exit_code === 0 ? 0 : 1;
try {
await page.goto('https://account.scaleway.com/logout', {waitUntil: 'domcontentloaded', timeout: 30000});
} catch {}
await context.clearCookies();
} catch (error) {
const code = typeof error?.message === 'string' && /^[a-z0-9_]+$/.test(error.message)
? error.message
: (error?.name || 'Error');
console.error(`IAM grant session failed: ${code}; no credential values printed.`);
process.exitCode = 1;
} finally {
if (child) child.kill('SIGTERM');
if (browser) await browser.close();
}