From ff80c0111ccc43bad18242aa38af5311779d2909 Mon Sep 17 00:00:00 2001 From: Allan Kimmer Jensen Date: Fri, 10 Apr 2026 10:59:44 +0200 Subject: [PATCH 1/2] feat: auto-register simulator code app when none exists Commands that need a code app (approve, open, copy, save) now automatically register one via the MitID test simulator API if the identity has no active code app. Opt out with --no-register. Also filters for ACTIVE authenticators when resolving, so revoked code apps are no longer picked up. --- src/cli.ts | 67 ++++++++++++++++++++++++++++++++++++++----------- src/identity.ts | 60 ++++++++++++++++++++++++++++++++++++++++++- src/index.ts | 1 + 3 files changed, 112 insertions(+), 16 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 56a726f..9400f3a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,14 +5,14 @@ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { defineCommand, runMain } from "citty"; -import { resolve, simulatorUrl } from "./identity.js"; +import { registerCodeApp, resolve, simulatorUrl } from "./identity.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const pkg = JSON.parse( readFileSync(join(__dirname, "..", "package.json"), "utf-8"), ) as { version: string }; -import type { SavedIdentity } from "./index.js"; +import type { CodeAppAuthenticator, MitIDIdentity, SavedIdentity } from "./index.js"; import { login } from "./login.js"; import { approve, watch } from "./simulator.js"; import { @@ -65,6 +65,30 @@ const queryArg = { required: true as const, }; +const noRegisterArg = { + type: "boolean" as const, + description: "Don't auto-register a code app if none exists", + default: false, +}; + +async function ensureCodeApp( + query: string, + baseUrl: string, + noRegister: boolean, +): Promise<{ identity: MitIDIdentity; codeApp: CodeAppAuthenticator }> { + const result = await resolve(resolveQuery(query), baseUrl); + if (result.codeApp) return { identity: result.identity, codeApp: result.codeApp }; + + if (noRegister) { + throw new Error("No code app authenticator found (auto-register disabled)"); + } + + console.log(" No code app found, registering one..."); + const codeApp = await registerCodeApp(result.identity.identityId, result.identity.ial, baseUrl); + console.log(` Registered code app: ${codeApp.authenticatorId}\n`); + return { identity: result.identity, codeApp }; +} + // --- Subcommands --- const infoCmd = defineCommand({ @@ -124,15 +148,16 @@ const approveCmd = defineCommand({ alias: ["w"], default: false, }, + "no-register": noRegisterArg, env: envArg, }, async run({ args }) { const baseUrl = getBaseUrl(args.env); - const { identity, codeApp } = await resolve( - resolveQuery(args.query), + const { identity, codeApp } = await ensureCodeApp( + args.query, baseUrl, + args["no-register"], ); - if (!codeApp) throw new Error("No code app authenticator found"); console.log(` User: ${identity.userId} (${identity.identityName})`); console.log(` Auth: ${codeApp.authenticatorId}\n`); @@ -199,15 +224,16 @@ const openCmd = defineCommand({ }, args: { query: queryArg, + "no-register": noRegisterArg, env: envArg, }, async run({ args }) { const baseUrl = getBaseUrl(args.env); - const { identity, codeApp } = await resolve( - resolveQuery(args.query), + const { identity, codeApp } = await ensureCodeApp( + args.query, baseUrl, + args["no-register"], ); - if (!codeApp) throw new Error("No code app authenticator found"); const url = simulatorUrl( identity.identityId, @@ -226,15 +252,16 @@ const copyCmd = defineCommand({ }, args: { query: queryArg, + "no-register": noRegisterArg, env: envArg, }, async run({ args }) { const baseUrl = getBaseUrl(args.env); - const { identity, codeApp } = await resolve( - resolveQuery(args.query), + const { identity, codeApp } = await ensureCodeApp( + args.query, baseUrl, + args["no-register"], ); - if (!codeApp) throw new Error("No code app authenticator found"); const url = simulatorUrl( identity.identityId, @@ -287,14 +314,24 @@ const saveCmd = defineCommand({ description: "A note to attach to this identity (e.g. 'has 3 addresses', 'expired CPR')", }, + "no-register": noRegisterArg, env: envArg, }, async run({ args }) { const baseUrl = getBaseUrl(args.env); - const { identity, codeApp } = await resolve( - resolveQuery(args.query), - baseUrl, - ); + const result = await resolve(resolveQuery(args.query), baseUrl); + if (!result.codeApp && !args["no-register"]) { + console.log(" No code app found, registering one..."); + result.codeApp = await registerCodeApp( + result.identity.identityId, + result.identity.ial, + baseUrl, + ); + console.log( + ` Registered code app: ${result.codeApp.authenticatorId}\n`, + ); + } + const { identity, codeApp } = result; const entry: SavedIdentity = { alias: args.alias ?? identity.userId, diff --git a/src/identity.ts b/src/identity.ts index fd0e7f9..b30d84d 100644 --- a/src/identity.ts +++ b/src/identity.ts @@ -134,7 +134,7 @@ export async function resolve( "GET", `/mitid-test-api/v4/identities/${uuid}/authenticators/code-app`, )) as CodeAppAuthenticator[]; - codeApp = apps[0] ?? null; + codeApp = apps.find((a) => a.state === "ACTIVE") ?? null; } catch { // No code app authenticator } @@ -142,6 +142,64 @@ export async function resolve( return { identity, codeApp }; } +interface RegisterSimulatorResponse { + userId: string; + authenticatorId: string; + message: string; +} + +export async function registerCodeApp( + uuid: string, + ial: string = "SUBSTANTIAL", + baseUrl: string = DEFAULT_BASE_URL, +): Promise { + const deviceMetrics = { + osName: "Android", + osVersion: "10", + model: "SM-A515F", + hwGenKey: "true", + jailbrokenStatus: "false", + malwareOnDevice: "false", + appName: "MitID app", + appVersion: "9.9.9", + appIdent: "dk.mitid.app.android", + appInstanceId: crypto.randomUUID(), + sdkVersion: "1.0.0", + swFingerprint: + "4b58eee4672b4ec29682fa35902589fde2bc04ba7de843b1941ba69233b79819", + extra: '{"packageName":"dk.mitid.app.android"}', + }; + + const device = Buffer.from(JSON.stringify(deviceMetrics)).toString("base64"); + const body = { device, pin: "112233", ael: ial, activate: true }; + + const token = await getToken(baseUrl); + const resp = await fetch( + `${baseUrl}/mitid-test-api/v4/identities/${uuid}/authenticators/code-app/simulator`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }, + ); + if (!resp.ok) { + const err = (await resp.json().catch(() => ({}))) as Record; + throw new Error( + `Failed to register code app: ${err.message ?? resp.status}`, + ); + } + + const result = (await resp.json()) as RegisterSimulatorResponse; + return { + authenticatorId: result.authenticatorId, + state: "ACTIVE", + }; +} + export function simulatorUrl( uuid: string, authId: string, diff --git a/src/index.ts b/src/index.ts index bd9e9d1..3f5a69a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ export type { ResolvedIdentity, } from "./identity.js"; export { + registerCodeApp, resolve, searchIdentity, simulatorUrl, From 1e0d65651f818282b66e78b450f27985f4e860e6 Mon Sep 17 00:00:00 2001 From: Allan Kimmer Jensen Date: Fri, 10 Apr 2026 11:04:01 +0200 Subject: [PATCH 2/2] style: fix biome formatting --- src/cli.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 9400f3a..3af140c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,7 +12,11 @@ const pkg = JSON.parse( readFileSync(join(__dirname, "..", "package.json"), "utf-8"), ) as { version: string }; -import type { CodeAppAuthenticator, MitIDIdentity, SavedIdentity } from "./index.js"; +import type { + CodeAppAuthenticator, + MitIDIdentity, + SavedIdentity, +} from "./index.js"; import { login } from "./login.js"; import { approve, watch } from "./simulator.js"; import { @@ -77,14 +81,19 @@ async function ensureCodeApp( noRegister: boolean, ): Promise<{ identity: MitIDIdentity; codeApp: CodeAppAuthenticator }> { const result = await resolve(resolveQuery(query), baseUrl); - if (result.codeApp) return { identity: result.identity, codeApp: result.codeApp }; + if (result.codeApp) + return { identity: result.identity, codeApp: result.codeApp }; if (noRegister) { throw new Error("No code app authenticator found (auto-register disabled)"); } console.log(" No code app found, registering one..."); - const codeApp = await registerCodeApp(result.identity.identityId, result.identity.ial, baseUrl); + const codeApp = await registerCodeApp( + result.identity.identityId, + result.identity.ial, + baseUrl, + ); console.log(` Registered code app: ${codeApp.authenticatorId}\n`); return { identity: result.identity, codeApp }; } @@ -327,9 +336,7 @@ const saveCmd = defineCommand({ result.identity.ial, baseUrl, ); - console.log( - ` Registered code app: ${result.codeApp.authenticatorId}\n`, - ); + console.log(` Registered code app: ${result.codeApp.authenticatorId}\n`); } const { identity, codeApp } = result;