scripts/smoke-skv-ombud.ts
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env npx tsx
/**
* First live call on Accounted's ombud identity at Skatteverket
* (organisationslegitimation + OAuth2 Client Credentials).
*
* Unit tests run the system flow on a stub transport; they cannot tell you
* whether the certificate, client id, gateway keys, scopes and role codes are
* ones Skatteverket accepts. This script makes the real calls and prints what
* the rollout still has to pin (role codes, the register's list envelope,
* what giltigFrom holds), so day one is one command instead of a debugging
* session. Steps, stopping at the first failure with Skatteverket's answer:
*
* 1. Mint a system token. The token and keys are never printed.
* 2. GET /roller: every role code with its description, and which of
* Accounted's two behörigheter each one classifies as.
* 3. GET /ombud/autentisieratOmbud: who appointed Accounted, with role and
* validity.
* 4. --huvudman <orgnr>: read that company's skattekonto (saldo and
* transactions) on the ombud identity.
* 5. --djuplank <orgnr>: mint an "utse ombud" deep link for that company.
* Nothing changes at Skatteverket until someone signs it.
*
* Steps 4 and 5 are calls about one company, so like every such call they
* write a skatteverket_api_audit_log row (null user: no app user made them).
* They need --company <uuid>, the Accounted company the org number belongs to.
* Steps 2 and 3 are Accounted's own register calls as ombud: no company, no
* row.
*
* Reads only at Skatteverket, plus the deep link on request; steps 4 and 5
* also write their audit rows to the environment's database. Test or
* production follows the environment (token URL and API base URLs). System
* auth may still be off in that environment: the script switches it to
* shadow for its own process.
*
* Usage:
* npx tsx scripts/smoke-skv-ombud.ts
* npx tsx scripts/smoke-skv-ombud.ts --huvudman 165566778899 --company <uuid>
* npx tsx scripts/smoke-skv-ombud.ts --djuplank 165566778899 --company <uuid>
* ENV_FILE=.env.production.local npx tsx scripts/smoke-skv-ombud.ts
*/
import { config } from 'dotenv'
// The selected file is the ONLY source of Skatteverket settings: inherited
// SKATTEVERKET_* values are cleared first (dotenv's override only replaces
// keys the file supplies), and a file that cannot be read stops the run.
// Otherwise a mistyped ENV_FILE, or a file missing one setting, would send
// live calls with mixed values to another environment.
const envFile = process.env.ENV_FILE ?? '.env.local'
for (const key of Object.keys(process.env)) {
if (key.startsWith('SKATTEVERKET_')) delete process.env[key]
}
const loaded = config({ path: envFile, override: true })
if (loaded.error) {
console.error(`Cannot read ${envFile}: ${loaded.error.message}`)
process.exit(1)
}
import {
getSystemCertInfo,
getSystemScopes,
getSystemTokenUrl,
isSystemAuthConfigured,
} from '@/extensions/general/skatteverket/lib/system-auth/config'
import { getSystemAccessToken } from '@/extensions/general/skatteverket/lib/system-auth/token-provider'
import {
classifyOmbudRole,
createUtseOmbudDeepLink,
getOmbudApiBaseUrl,
getOmbudRoleDescriptions,
listOmbudGrants,
} from '@/extensions/general/skatteverket/lib/ombud-client'
import { getSaldo, getSkattekontoBaseUrl, getTransaktioner } from '@/extensions/general/skatteverket/lib/skattekonto-client'
const ROLE_ENV = { lasombud: 'SKATTEVERKET_OMBUD_ROLL_LASOMBUD', moms_ombud: 'SKATTEVERKET_OMBUD_ROLL_MOMS' } as const
function argValue(flag: string): string | null {
const i = process.argv.indexOf(flag)
return i >= 0 ? (process.argv[i + 1] ?? null) : null
}
/** The company a per-company call is audited against (steps 4 and 5). */
function companyArg(): string {
const id = argValue('--company')
if (!id || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
console.error('Steps 4 and 5 need --company <uuid>: the call is audited against that company.')
process.exit(1)
}
return id
}
/** 12-digit huvudman; a bare 10-digit org number gets the 16 prefix. */
function huvudman(raw: string): string {
const digits = raw.replace(/\D/g, '')
if (/^\d{12}$/.test(digits)) return digits
if (/^\d{10}$/.test(digits)) return `16${digits}`
throw new Error(`Not an org number: ${raw}`)
}
function describeError(err: unknown): string {
if (err instanceof Error) {
const code = (err as { code?: unknown }).code
return `${err.name}${code ? ` ${String(code)}` : ''}: ${err.message}`
}
return String(err)
}
async function step<T>(label: string, run: () => Promise<T>): Promise<T> {
try {
return await run()
} catch (err) {
console.error(`\nFAILED at ${label}\n ${describeError(err)}`)
process.exit(1)
}
}
async function main() {
process.env.SKATTEVERKET_SYSTEM_AUTH_MODE ||= 'shadow'
if (process.env.SKATTEVERKET_SYSTEM_AUTH_MODE === 'off') process.env.SKATTEVERKET_SYSTEM_AUTH_MODE = 'shadow'
const cert = getSystemCertInfo()
console.log(`Configuration (${envFile})`)
console.log(` token endpoint ${getSystemTokenUrl() ?? '(SKATTEVERKET_SYSTEM_OAUTH_TOKEN_URL missing)'}`)
console.log(` client id ${process.env.SKATTEVERKET_SYSTEM_CLIENT_ID ? 'set' : '(SKATTEVERKET_SYSTEM_CLIENT_ID missing)'}`)
console.log(` gateway keys ${process.env.SKATTEVERKET_SYSTEM_APIGW_CLIENT_ID ? 'system pair' : 'shared SKATTEVERKET_APIGW_* pair'}`)
console.log(` scopes ${getSystemScopes().join(' ')}`)
console.log(` certificate ${cert ? `${cert.subject.replace(/\n/g, ', ')}, valid to ${cert.notAfter.slice(0, 10)}` : '(missing or unreadable)'}`)
console.log(` ombud API ${getOmbudApiBaseUrl()}`)
console.log(` skattekonto API ${getSkattekontoBaseUrl()}`)
if (!isSystemAuthConfigured()) {
console.error('\nSystem auth is not configured: token URL, certificate and key are required.')
process.exit(1)
}
const token = await step('1. token', () => getSystemAccessToken())
console.log(`\n1. Token minted (${token.length} characters, not printed).`)
const roles = await step('2. GET /roller', () => getOmbudRoleDescriptions())
console.log(`\n2. ${roles.length} roles:`)
for (const role of roles) {
const key = classifyOmbudRole(role)
console.log(` ${role.roll.padEnd(12)} ${role.rollbeskrivning ?? ''}${key ? ` <- ${key}` : ''}`)
}
for (const [key, env] of Object.entries(ROLE_ENV)) {
const hit = roles.find((role) => classifyOmbudRole(role) === key)
console.log(hit ? ` pin: ${env}=${hit.roll}` : ` ${key}: no role matched by description; pick its code above and set ${env}`)
}
const grants = await step('3. GET /ombud/autentisieratOmbud', () =>
listOmbudGrants({}, 'ombud_register', { emptyOn404: true })
)
console.log(`\n3. ${grants.length} grants to this ombud:`)
for (const post of grants) {
console.log(
` ${post.huvudman} ${post.roll} ${post.rollbeskrivning ?? ''} giltigFrom ${post.giltigFrom} giltigTom ${post.giltigTom ?? 'tillsvidare'}`
)
}
const readFor = argValue('--huvudman')
if (readFor) {
const orgnr = huvudman(readFor)
const actor = { companyId: companyArg(), userId: null }
const saldo = await step(`4. skattekonto saldo for ${orgnr}`, () => getSaldo({ mode: 'system' }, orgnr, actor))
const tx = await step(`4. skattekonto transaktioner for ${orgnr}`, () =>
getTransaktioner({ mode: 'system' }, orgnr, undefined, actor)
)
console.log(`\n4. Skattekonto ${orgnr}: saldo fields ${Object.keys(saldo).join(', ')}`)
console.log(` transaktioner response fields ${Object.keys(tx).join(', ')}`)
}
const linkFor = argValue('--djuplank')
if (linkFor) {
const orgnr = huvudman(linkFor)
const actor = { companyId: companyArg(), userId: null }
const link = await step(`5. deep link for ${orgnr}`, () => createUtseOmbudDeepLink(orgnr, actor))
console.log(`\n5. Deep link (valid until ${link.expiresOn}), roles ${Object.values(link.roller).join(', ')}:`)
console.log(` ${link.djuplank}`)
}
console.log('\nAll requested steps passed.')
}
main().catch((err) => {
console.error(describeError(err))
process.exit(1)
})