#!/usr/bin/env node /** * cli:provision-external-app * * Creates a TEST third-party client on a running API and enables its grants, * so the whole public chain can be proven end to end. * * Usage: * npx --prefer-offline tsx skills/external-api/cli/provision-external-app/index.ts \ * --spec '{"baseUrl":"http://localhost:5000","adminToken":"…","name":"Partenaire test","codes":["crm-factures"]}' */ import { parseArgs } from 'node:util' import { executeEnvelope, failExecute, printEnvelope } from '../../../lib/output.js' import { readSpecArg } from '../../../lib/spec-arg.js' import { validate } from './validate.js' import { execute, type FetchLike } from './execute.js' import { ProvisionExternalAppInputSchema, type ProvisionReport } from './types.js' const COMMAND = 'provision-external-app' const nodeFetch: FetchLike = async (url, init) => { const res = await fetch(url, init) return { ok: res.ok, status: res.status, text: () => res.text() } } async function main(): Promise { const { values } = parseArgs({ options: { spec: { type: 'string' }, 'spec-file': { type: 'string' } }, strict: true, }) const specSrc = readSpecArg(values) if ('error' in specSrc) { printEnvelope(failExecute(COMMAND, [specSrc.error])) process.exit(1) } let raw: unknown try { raw = JSON.parse(specSrc.raw) } catch (err) { printEnvelope(failExecute(COMMAND, [`--spec is not valid JSON: ${(err as Error).message}`])) process.exit(1) } const validation = validate(raw) if (!validation.valid) { printEnvelope(failExecute(COMMAND, validation.errors)) process.exit(1) } try { const spec = ProvisionExternalAppInputSchema.parse(raw) const { report, errors, warnings } = await execute(spec, nodeFetch) if (errors.length > 0 || report === null) { printEnvelope(failExecute(COMMAND, errors)) process.exit(1) } printEnvelope(executeEnvelope(COMMAND, { report, warnings: [...validation.warnings, ...warnings], nextSteps: report.dryRun ? ['Dry run — nothing was created. Re-run without dryRun to provision.'] : [ 'Forge a JWT HS256 assertion with the secret (sub = clientId, exp ≤ iat + 5 min) and POST it to /api/auth/external-app/token.', 'Call an endpoint with ?tenantId= — without it the answer is 400 by design.', 'A code you did NOT grant answers 403 access_denied: that is the per-operation grain working.', ], })) } catch (err) { printEnvelope(failExecute(COMMAND, [(err as Error).message])) process.exit(1) } } void main()