From 6d848aaccd4e2063367091073b0b2c565c10b6b4 Mon Sep 17 00:00:00 2001 From: Zach Mainen Date: Thu, 16 Jul 2026 10:11:47 -0400 Subject: [PATCH] feat: add 'id' command to read personal identity items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a read-only `dcli id` command that retrieves structured personal identity items already synced into the local vault but previously undisplayable: passports, ID cards, identities, driver's licenses, social security IDs, and tax numbers (fiscal statements). The decryption path is unchanged — decryptTransactions is generic — so the handler mirrors the secure-note handler and exposes every KWDataItem field dynamically, keeping it type-agnostic across all personal-info item types. Supports text and json output and the standard filter syntax. Co-Authored-By: Claude Opus 4.8 (1M context) --- documentation/pages/personal/vault.mdx | 24 +++++++ src/command-handlers/index.ts | 1 + src/command-handlers/personalInfo.ts | 90 ++++++++++++++++++++++++++ src/commands/index.ts | 17 +++++ src/types.ts | 33 +++++++++- 5 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/command-handlers/personalInfo.ts diff --git a/documentation/pages/personal/vault.mdx b/documentation/pages/personal/vault.mdx index 57169eb..ed1f7af 100644 --- a/documentation/pages/personal/vault.mdx +++ b/documentation/pages/personal/vault.mdx @@ -90,6 +90,30 @@ dcli secret [filters] dcli secret title=api_keys -o json ``` +## Get a personal ID + +The `id` command retrieves structured personal identity items stored in your vault: +passports, ID cards, identity records, driver's licenses, social security IDs, and tax +numbers (fiscal statements). Use filters the same way as the other commands. By default +it matches on the item type and name fields; you can also select a different output among +`text, json`. The JSON option outputs all matching items with every field they contain. + +```sh copy +dcli id [filters] +``` + +```sh copy filename="Return every personal ID item as JSON" +dcli id -o json +``` + +```sh copy filename="Return only passports" +dcli id type=passport +``` + +```sh copy filename="Return the tax number for someone named John" +dcli id type=fiscalstatement firstName=john +``` + ## Options By default an automatic synchronization is performed once per hour. diff --git a/src/command-handlers/index.ts b/src/command-handlers/index.ts index 7bd94f4..db4051f 100644 --- a/src/command-handlers/index.ts +++ b/src/command-handlers/index.ts @@ -7,6 +7,7 @@ export * from './lock.js'; export * from './logout.js'; export * from './teamMcp.js'; export * from './passwords.js'; +export * from './personalInfo.js'; export * from './read.js'; export * from './publicAPI.js'; export * from './secrets.js'; diff --git a/src/command-handlers/personalInfo.ts b/src/command-handlers/personalInfo.ts new file mode 100644 index 0000000..8d65432 --- /dev/null +++ b/src/command-handlers/personalInfo.ts @@ -0,0 +1,90 @@ +import Database from 'better-sqlite3'; +import { BackupEditTransaction, LocalConfiguration, PersonalInfoTransactionContent, VaultId } from '../types.js'; +import { decryptTransactions } from '../modules/crypto/index.js'; +import { filterMatches } from '../utils/index.js'; +import { connectAndPrepare } from '../modules/database/index.js'; +import { logger } from '../logger.js'; + +/** Vault transaction types that hold structured personal identity information. */ +export const PERSONAL_INFO_TYPES = [ + 'IDENTITY', + 'PASSPORT', + 'IDCARD', + 'DRIVERLICENSE', + 'SOCIALSECURITYID', + 'FISCALSTATEMENT', +] as const; + +export const runId = async (filters: string[] | null, options: { output: 'text' | 'json' }) => { + const { db, localConfiguration } = await connectAndPrepare({}); + const ids = await getIds({ filters, localConfiguration, db }); + db.close(); + + switch (options.output) { + case 'json': + logger.content(JSON.stringify(ids)); + break; + case 'text': { + if (ids.length === 0) { + throw new Error('No personal ID item found'); + } + const blocks = ids.map((id) => { + const lines = Object.entries(id) + .filter(([, value]) => typeof value === 'string' && value.length > 0) + .map(([key, value]) => ` ${key}: ${value}`); + return `[${id.type}]\n${lines.join('\n')}`; + }); + logger.content(blocks.join('\n\n')); + break; + } + default: + throw new Error('Unable to recognize the output mode.'); + } +}; + +interface GetIds { + filters: string[] | null; + localConfiguration: LocalConfiguration; + db: Database.Database; +} + +export const getIds = async (params: GetIds): Promise => { + const { localConfiguration, filters, db } = params; + + logger.debug(`Retrieving personal ID items: ${filters && filters.length > 0 ? filters.join(' ') : ''}`); + + const placeholders = PERSONAL_INFO_TYPES.map(() => '?').join(', '); + const transactions = db + .prepare(`SELECT * FROM transactions WHERE login = ? AND type IN (${placeholders}) AND action = 'BACKUP_EDIT'`) + .bind(localConfiguration.login, ...PERSONAL_INFO_TYPES) + .all() as BackupEditTransaction[]; + + const decrypted = await decryptTransactions(transactions, localConfiguration); + + // Each item's content is a single root element (KWPassport, KWIDCard, KWIdentity, ...) whose + // KWDataItem entries are the fields. This is type-agnostic: any field the item carries is exposed. + const beautifiedIds = decrypted.map((item, index) => { + const kwType = Object.keys(item.root)[0]; + const dataItems = item.root[kwType]?.KWDataItem ?? []; + const fields = Object.fromEntries( + dataItems + .filter((entry) => entry._cdata !== undefined) + .map((entry) => [ + entry._attributes.key[0].toLowerCase() + entry._attributes.key.slice(1), // FirstName => firstName + entry._cdata as string, + ]) + ) as Record; + + // Transaction type (PASSPORT, IDCARD, ...) is the primary, reliable classifier and filter key. + return { ...fields, kwType, type: transactions[index].type } as VaultId; + }); + + return filterMatches(beautifiedIds, filters, [ + 'type', + 'fullname', + 'firstName', + 'lastName', + 'number', + 'idNumber', + ]); +}; diff --git a/src/commands/index.ts b/src/commands/index.ts index 2cb26eb..64e020d 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -7,6 +7,7 @@ import { runSync, runPassword, runSecureNote, + runId, runLock, runLogout, runRead, @@ -101,6 +102,22 @@ export const rootCommands = (params: { program: Command }) => { ) .action(runSecret); + program + .command('id') + .description( + 'Retrieve a personal ID item (passport, ID card, identity, driver license, tax number, ...) from the local vault' + ) + .addOption( + new Option('-o, --output ', 'How to print the IDs. The JSON option outputs all the matching items') + .choices(['text', 'json']) + .default('text') + ) + .argument( + '[filters...]', + 'Filter items based on any parameter using =; if is not specified in the filter, will default to type and name fields' + ) + .action(runId); + accountsCommands({ program }); devicesCommands({ program }); diff --git a/src/types.ts b/src/types.ts index 64ef69c..df76399 100644 --- a/src/types.ts +++ b/src/types.ts @@ -121,7 +121,8 @@ export interface BackupRemoveTransaction { export type TransactionContent = | AuthentifiantTransactionContent | SecureNoteTransactionContent - | SecretTransactionContent; + | SecretTransactionContent + | PersonalInfoTransactionContent; export interface AuthentifiantTransactionContent { root: { @@ -162,6 +163,36 @@ export interface SecretTransactionContent { }; } +/** + * Structured personal identity items (passport, ID card, identity, driver's license, + * social security id, fiscal statement). The root element name varies by type + * (KWPassport, KWIDCard, KWIdentity, ...), so it is keyed generically. + */ +export interface PersonalInfoTransactionContent { + root: Record< + string, + { + KWDataItem: { + _attributes: { + key: string; + }; + _cdata?: string; + }[]; + } + >; +} + +/** + * A decrypted personal identity item. Fields vary by item type, so all decrypted + * fields are exposed dynamically; `type` is the transaction type (PASSPORT, IDCARD, ...) + * and `kwType` is the content root element name (KWPassport, KWIDCard, ...). + */ +export interface VaultId { + type: string; + kwType: string; + [field: string]: string; +} + export interface VaultCredential { title?: string; email?: string;