Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 59 additions & 15 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ 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 {
Expand Down Expand Up @@ -65,6 +69,35 @@ 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({
Expand Down Expand Up @@ -124,15 +157,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`);
Expand Down Expand Up @@ -199,15 +233,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,
Expand All @@ -226,15 +261,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,
Expand Down Expand Up @@ -287,14 +323,22 @@ 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,
Expand Down
60 changes: 59 additions & 1 deletion src/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,72 @@ 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
}

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<CodeAppAuthenticator> {
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<string, string>;
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,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
ResolvedIdentity,
} from "./identity.js";
export {
registerCodeApp,
resolve,
searchIdentity,
simulatorUrl,
Expand Down
Loading