dataconv-client-sdk-ts 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,179 +1,856 @@
1
1
  #!/usr/bin/env node
2
+ import { createHash, createHmac, randomUUID } from 'node:crypto';
2
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
3
5
  import path from 'node:path';
4
6
  import { DataConvClient } from './DataConvClient.js';
5
7
  function getArgValue(args, flag) {
6
8
  const index = args.indexOf(flag);
7
- if (index < 0) {
9
+ if (index < 0)
8
10
  return undefined;
9
- }
10
11
  return args[index + 1];
11
12
  }
12
13
  function hasFlag(args, flag) {
13
14
  return args.includes(flag);
14
15
  }
15
- function requireArg(args, flag) {
16
- const value = getArgValue(args, flag);
17
- if (!value || value.startsWith('--')) {
18
- throw new Error(`Missing required argument ${flag}`);
19
- }
20
- return value;
21
- }
22
16
  function usage() {
23
17
  return [
24
- 'Usage:',
25
- ' dataconv-client (--base-url <url> | --service-did <did>) --tenant-id <tenant> --software-id <software> --file <xlsx> --issuer-did <did> [options]',
18
+ 'Usage: dataconv <command> [options]',
26
19
  '',
27
- 'Required:',
28
- ' --base-url <url> Direct service URL',
29
- ' --service-did <did> Logical service DID',
30
- ' --tenant-id <tenant>',
31
- ' --software-id <software>',
32
- ' --file <path>',
33
- ' --issuer-did <did>',
20
+ 'Commands:',
21
+ ' dataconv login --id-token <jwt> [--base-url <url>] [--tenant-id <id>] [--software-id <id>]',
22
+ ' dataconv exchange --scope "dataconv.upload" [--vp-token <jwt>] [--client-assertion <jwt>]',
23
+ ' [--api-key <key>] [--organization <org>] [--organization-did <did:web:...>] ',
24
+ ' [--service-id <publisher-token-exchange|...>] [--operational-subject <did>]',
25
+ ' dataconv upload <ruta.xlsx> [--scope "dataconv.upload"] [--mapping-json <path>] [--header-row-index <n>] [--output-json <path>]',
26
+ ' dataconv search --resource-type <FHIRType> [--scope "dataconv.read"] [--params <json>] [--output-json <path>]',
27
+ ' dataconv patch --thid <thid> [--resource-type <FHIRType>] [--scope "dataconv.patch"] [--output-json <path>]',
28
+ ' dataconv batch --thid <thid> [--resource-type <FHIRType>] [--scope "dataconv.batch"] [--output-json <path>]',
29
+ ' dataconv api-key-create --email <email> --target <endpointId> [--scope <scope1,scope2>] [--instrument <json>]',
30
+ ' dataconv whoami',
34
31
  '',
35
- 'Optional:',
36
- ' --resolved-base-url <url> Local resolution target for --service-did',
37
- ' --jurisdiction <code> Default: ES',
38
- ' --sector <sector> Default: onehealth-research',
39
- ' --resource-type <type> Default: Composition',
40
- ' --output-json <path> Default: ./artifacts/dataconv-upload-response.json',
41
- ' --authorization-bearer <t> Bearer token header value',
42
- ' --id-token <jwt>',
43
- ' --vp-token <jwt>',
44
- ' --retry-times <n> Default: 60',
45
- ' --retry-delay-ms <n> Default: 2000',
46
- ' --debug Print resolved raw endpoint',
32
+ 'Common options:',
33
+ ' --dataspace-name <name> Default: DATACONV_DATASPACE_NAME or GLOBAL-DATACARE',
34
+ ' --base-url <url> Default: DATACONV_BASE_URL or http://localhost:8080',
35
+ ' --issuer-did <did> Default: DATACONV_ISSUER_DID or did:web:globaldatacare.es:employee:loader',
36
+ ' --tenant-id <tenant> Default: DATACONV_TENANT_ID or tenant-a',
37
+ ' --jurisdiction <code> Default: DATACONV_JURISDICTION or es',
38
+ ' --sector <sector> Default: DATACONV_SECTOR or onehealth-research',
39
+ ' --software-id <software> Default: DATACONV_SOFTWARE_ID or qvet',
40
+ ' --resource-type <type> Default: DATACONV_RESOURCE_TYPE or Composition',
41
+ ' --state-file <path> Default: ~/.dataconv/state.json',
42
+ ' --api-key <key> Default: DATACONV_API_KEY',
47
43
  ' --help'
48
44
  ].join('\n');
49
45
  }
50
- function resolveBaseUrl(serviceDid, explicitBaseUrl, resolvedBaseUrl) {
51
- const baseUrl = String(explicitBaseUrl || '').trim();
52
- if (baseUrl) {
53
- return baseUrl;
54
- }
55
- const did = String(serviceDid || '').trim();
56
- const directResolvedBaseUrl = String(resolvedBaseUrl || '').trim();
57
- if (did && directResolvedBaseUrl) {
58
- return directResolvedBaseUrl;
46
+ function commandHelp(command) {
47
+ const common = [
48
+ 'Common options:',
49
+ ' --dataspace-name <name> Default: DATACONV_DATASPACE_NAME or GLOBAL-DATACARE',
50
+ ' --base-url <url> Default: DATACONV_BASE_URL or http://localhost:8080',
51
+ ' --issuer-did <did> Default: DATACONV_ISSUER_DID or did:web:globaldatacare.es:employee:loader',
52
+ ' --tenant-id <tenant> Default: DATACONV_TENANT_ID or tenant-a',
53
+ ' --jurisdiction <code> Default: DATACONV_JURISDICTION or es',
54
+ ' --sector <sector> Default: DATACONV_SECTOR or onehealth-research',
55
+ ' --software-id <software> Default: DATACONV_SOFTWARE_ID or qvet',
56
+ ' --resource-type <type> Default: DATACONV_RESOURCE_TYPE or Composition',
57
+ ' --state-file <path> Default: ~/.dataconv/state.json',
58
+ ' --api-key <key> Default: DATACONV_API_KEY',
59
+ ].join('\n');
60
+ const exchange = [
61
+ 'Command: exchange',
62
+ ' dataconv exchange --scope <scope> [--organization <org>] [--organization-did <did:web:...>] [--service-id <id>] [--operational-subject <did>]',
63
+ '',
64
+ 'Notes:',
65
+ ' - serviceId recomendado: #identity:openid:token:_exchange',
66
+ ' - Respeta contrato OpenAPI: el payload de /exchange NO se altera con service-id ni fallbacks.',
67
+ ' - --organization-did y --service-id se guardan en estado CLI para resolución de endpoints fuera de payload.',
68
+ ' - DID document objetivo para pruebas: <did-web>/.well-known/did.json',
69
+ '',
70
+ 'Fallback env vars (localhost testing):',
71
+ ' PUBLISHER_OPENID_EXCHANGE',
72
+ ' PUBLISHER_DATASET_UPDATE',
73
+ ' PUBLISHER_DATASET_PATCH',
74
+ ' PUBLISHER_DATASET_BATCH',
75
+ ' PUBLISHER_DATASET_SEARCH',
76
+ ].join('\n');
77
+ const upload = [
78
+ 'Command: upload',
79
+ ' dataconv upload <ruta.xlsx> [--scope <scope>] [--mapping-json <path>] [--header-row-index <n>] [--output-json <path>]',
80
+ '',
81
+ 'Expected serviceId: #dataset:{softwareId}:{resourceType}:_update',
82
+ 'Response endpoint: se toma del header Location (publisher-dataset-update-response).',
83
+ ].join('\n');
84
+ const patch = [
85
+ 'Command: patch',
86
+ ' dataconv patch --thid <thid> [--resource-type <FHIRType>] [--scope <scope>] [--output-json <path>]',
87
+ '',
88
+ 'Expected serviceId: #dataset:{softwareId}:{resourceType}:_patch',
89
+ ].join('\n');
90
+ const batch = [
91
+ 'Command: batch',
92
+ ' dataconv batch --thid <thid> [--resource-type <FHIRType>] [--scope <scope>] [--output-json <path>]',
93
+ '',
94
+ 'Expected serviceId: #dataset:{softwareId}:{resourceType}:_batch',
95
+ ].join('\n');
96
+ const search = [
97
+ 'Command: search',
98
+ ' dataconv search --resource-type <FHIRType> [--scope <scope>] [--params <json>] [--output-json <path>]',
99
+ '',
100
+ 'Expected serviceId: #dataset:api:{resourceType}:_search',
101
+ ].join('\n');
102
+ const login = [
103
+ 'Command: login',
104
+ ' dataconv login --id-token <jwt> [--base-url <url>] [--tenant-id <id>] [--software-id <id>] [--vp-token <jwt>]',
105
+ ].join('\n');
106
+ const apiKeyCreate = [
107
+ 'Command: api-key-create',
108
+ ' dataconv api-key-create --email <email> --target <endpointId> [--scope <scope1,scope2>] [--instrument <json>]',
109
+ ].join('\n');
110
+ const whoami = [
111
+ 'Command: whoami',
112
+ ' dataconv whoami',
113
+ ].join('\n');
114
+ const byCommand = {
115
+ login,
116
+ exchange,
117
+ upload,
118
+ patch,
119
+ batch,
120
+ search,
121
+ 'api-key-create': apiKeyCreate,
122
+ whoami,
123
+ };
124
+ const key = String(command || '').trim();
125
+ if (!key) {
126
+ return `${usage()}\n\n${common}\n\nUse: dataconv help <command>`;
59
127
  }
60
- const rawMap = String(process.env.DATACONV_SERVICE_DID_MAP || '').trim();
61
- if (did && rawMap) {
62
- try {
63
- const parsed = JSON.parse(rawMap);
64
- const mapped = String(parsed[did] || '').trim();
65
- if (mapped) {
66
- return mapped;
67
- }
68
- }
69
- catch {
70
- throw new Error('DATACONV_SERVICE_DID_MAP must be valid JSON');
128
+ return `${byCommand[key] || `Comando no soportado: ${key}`}\n\n${common}`;
129
+ }
130
+ function parseObjectJson(value, fieldName) {
131
+ const raw = String(value || '').trim();
132
+ if (!raw)
133
+ return undefined;
134
+ try {
135
+ const parsed = JSON.parse(raw);
136
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
137
+ return parsed;
71
138
  }
139
+ throw new Error('debe ser un objeto JSON');
140
+ }
141
+ catch (error) {
142
+ throw new Error(`${fieldName} inválido: ${error instanceof Error ? error.message : String(error)}`);
72
143
  }
73
- throw new Error('Provide --base-url, or use --service-did together with --resolved-base-url or DATACONV_SERVICE_DID_MAP');
74
144
  }
75
- function parseArgs(argv) {
76
- if (hasFlag(argv, '--help')) {
77
- console.log(usage());
78
- process.exit(0);
145
+ function getDataspaceProfilesFromEnv() {
146
+ const raw = String(process.env.DATACONV_DATASPACE_PROFILES || '').trim();
147
+ if (!raw)
148
+ return {};
149
+ try {
150
+ const parsed = JSON.parse(raw);
151
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
152
+ return {};
153
+ return parsed;
154
+ }
155
+ catch {
156
+ return {};
157
+ }
158
+ }
159
+ function resolveDataspaceName(args, currentState) {
160
+ return (getArgValue(args, '--dataspace-name')
161
+ || process.env.DATACONV_DATASPACE_NAME
162
+ || currentState?.dataspaceName
163
+ || 'GLOBAL-DATACARE').trim();
164
+ }
165
+ function resolveDataspaceProfile(args, currentState) {
166
+ const dataspaceName = resolveDataspaceName(args, currentState);
167
+ const profiles = getDataspaceProfilesFromEnv();
168
+ const profile = profiles[dataspaceName] || {};
169
+ return { dataspaceName, profile };
170
+ }
171
+ function nowStamp() {
172
+ return new Date().toISOString();
173
+ }
174
+ function logInfo(message) {
175
+ console.log(`[${nowStamp()}] INFO ${message}`);
176
+ }
177
+ function logSuccess(message) {
178
+ console.log(`[${nowStamp()}] INFO ✔ ${message}`);
179
+ }
180
+ function parseHeaderRowIndex(value) {
181
+ const raw = String(value || '').trim();
182
+ if (!raw)
183
+ return undefined;
184
+ const parsed = Number(raw);
185
+ if (!Number.isInteger(parsed) || parsed < 1) {
186
+ throw new Error('--header-row-index debe ser un entero >= 1');
187
+ }
188
+ return parsed;
189
+ }
190
+ function toMappingConfigPayload(value) {
191
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
192
+ throw new Error('mapping-json debe ser un objeto JSON');
193
+ }
194
+ const obj = value;
195
+ if (obj.mappingConfig && typeof obj.mappingConfig === 'object' && !Array.isArray(obj.mappingConfig)) {
196
+ return obj.mappingConfig;
197
+ }
198
+ if (obj.schemaConfig && typeof obj.schemaConfig === 'object' && !Array.isArray(obj.schemaConfig)) {
199
+ return obj.schemaConfig;
200
+ }
201
+ if (obj.fieldMap && typeof obj.fieldMap === 'object' && !Array.isArray(obj.fieldMap)) {
202
+ return {
203
+ ...obj,
204
+ fieldMap: obj.fieldMap
205
+ };
79
206
  }
80
- const serviceDid = getArgValue(argv, '--service-did');
81
- const baseUrl = getArgValue(argv, '--base-url');
82
- const resolvedBaseUrl = getArgValue(argv, '--resolved-base-url');
83
207
  return {
84
- baseUrl: resolveBaseUrl(serviceDid, baseUrl, resolvedBaseUrl),
85
- serviceDid: serviceDid || undefined,
86
- issuerDid: requireArg(argv, '--issuer-did'),
87
- tenantId: requireArg(argv, '--tenant-id'),
88
- softwareId: requireArg(argv, '--software-id'),
89
- file: requireArg(argv, '--file'),
90
- jurisdiction: getArgValue(argv, '--jurisdiction') || 'ES',
91
- sector: getArgValue(argv, '--sector') || 'onehealth-research',
92
- resourceType: getArgValue(argv, '--resource-type') || 'Composition',
93
- outputJson: getArgValue(argv, '--output-json') || './artifacts/dataconv-upload-response.json',
94
- authorizationBearer: getArgValue(argv, '--authorization-bearer'),
95
- resolvedBaseUrl: resolvedBaseUrl || undefined,
96
- idToken: getArgValue(argv, '--id-token'),
97
- vpToken: getArgValue(argv, '--vp-token'),
98
- retryTimes: Number(getArgValue(argv, '--retry-times') || 60),
99
- retryDelayMs: Number(getArgValue(argv, '--retry-delay-ms') || 2000),
100
- debug: hasFlag(argv, '--debug')
208
+ fieldMap: obj
101
209
  };
102
210
  }
103
- function toKilobytes(bytes) {
104
- return `${(bytes / 1024).toFixed(1)} KB`;
211
+ async function loadMappingConfigFromFile(filePath, headerRowIndex) {
212
+ const absolutePath = path.resolve(filePath);
213
+ const raw = await readFile(absolutePath, 'utf-8');
214
+ let parsed;
215
+ try {
216
+ parsed = JSON.parse(raw);
217
+ }
218
+ catch (error) {
219
+ throw new Error(`No se pudo parsear mapping-json: ${error instanceof Error ? error.message : String(error)}`);
220
+ }
221
+ const mappingConfig = toMappingConfigPayload(parsed);
222
+ if (headerRowIndex !== undefined) {
223
+ mappingConfig.headerRowIndex = headerRowIndex;
224
+ }
225
+ if (!mappingConfig.fieldMap || typeof mappingConfig.fieldMap !== 'object' || Array.isArray(mappingConfig.fieldMap)) {
226
+ throw new Error('mapping-json debe incluir fieldMap (objeto clave=campo API, valor=columna Excel)');
227
+ }
228
+ return mappingConfig;
229
+ }
230
+ function parseCsv(value) {
231
+ return String(value || '')
232
+ .split(',')
233
+ .map((item) => item.trim())
234
+ .filter(Boolean);
105
235
  }
106
- function buildUploadUrl(options) {
107
- return `${options.baseUrl.replace(/\/+$/, '')}/${encodeURIComponent(options.tenantId)}/cds-${encodeURIComponent(options.jurisdiction)}/v1/${encodeURIComponent(options.sector)}/digitaltwin/${encodeURIComponent(options.softwareId)}/${encodeURIComponent(options.resourceType)}/_upload`;
236
+ function defaultStatePath(explicitPath) {
237
+ const value = String(explicitPath || '').trim();
238
+ if (value)
239
+ return path.resolve(value);
240
+ return path.join(homedir(), '.dataconv', 'state.json');
108
241
  }
109
- function buildPollingUrl(options, thid) {
110
- return `${options.baseUrl.replace(/\/+$/, '')}/${encodeURIComponent(options.tenantId)}/cds-${encodeURIComponent(options.jurisdiction)}/v1/${encodeURIComponent(options.sector)}/digitaltwin/${encodeURIComponent(options.softwareId)}/${encodeURIComponent(options.resourceType)}/_upload-response?thid=${encodeURIComponent(thid)}`;
242
+ function parseCommonArgs(args, base) {
243
+ const { dataspaceName, profile } = resolveDataspaceProfile(args, base);
244
+ return {
245
+ dataspaceName,
246
+ baseUrl: getArgValue(args, '--base-url') || base?.baseUrl || String(profile.baseUrl || '').trim() || process.env.DATACONV_BASE_URL || 'http://localhost:8080',
247
+ issuerDid: getArgValue(args, '--issuer-did') || base?.issuerDid || String(profile.issuerDid || '').trim() || process.env.DATACONV_ISSUER_DID || 'did:web:globaldatacare.es:employee:loader',
248
+ tenantId: getArgValue(args, '--tenant-id') || base?.tenantId || String(profile.tenantId || '').trim() || process.env.DATACONV_TENANT_ID || 'tenant-a',
249
+ jurisdiction: getArgValue(args, '--jurisdiction') || base?.jurisdiction || String(profile.jurisdiction || '').trim() || process.env.DATACONV_JURISDICTION || 'es',
250
+ sector: getArgValue(args, '--sector') || base?.sector || String(profile.sector || '').trim() || process.env.DATACONV_SECTOR || 'onehealth-research',
251
+ softwareId: getArgValue(args, '--software-id') || base?.softwareId || String(profile.softwareId || '').trim() || process.env.DATACONV_SOFTWARE_ID || 'qvet',
252
+ resourceType: getArgValue(args, '--resource-type') || base?.resourceType || String(profile.resourceType || '').trim() || process.env.DATACONV_RESOURCE_TYPE || 'Composition',
253
+ idToken: base?.idToken,
254
+ vpToken: base?.vpToken,
255
+ sessionToken: base?.sessionToken,
256
+ sessionScope: base?.sessionScope,
257
+ sessionExpiresAt: base?.sessionExpiresAt,
258
+ subject: base?.subject,
259
+ organization: base?.organization,
260
+ organizationDid: base?.organizationDid,
261
+ serviceId: getArgValue(args, '--service-id') || base?.serviceId || process.env.DATACONV_SERVICE_ID || undefined,
262
+ publisherTokenExchangeFallback: base?.publisherTokenExchangeFallback || process.env.PUBLISHER_OPENID_EXCHANGE || undefined,
263
+ publisherDatasetUpdateFallback: base?.publisherDatasetUpdateFallback || process.env.PUBLISHER_DATASET_UPDATE || undefined,
264
+ publisherDatasetPatchFallback: base?.publisherDatasetPatchFallback || process.env.PUBLISHER_DATASET_PATCH || undefined,
265
+ publisherDatasetBatchFallback: base?.publisherDatasetBatchFallback || process.env.PUBLISHER_DATASET_BATCH || undefined,
266
+ publisherDatasetSearchFallback: base?.publisherDatasetSearchFallback || process.env.PUBLISHER_DATASET_SEARCH || undefined,
267
+ apiKey: getArgValue(args, '--api-key') || base?.apiKey || process.env.DATACONV_API_KEY || undefined
268
+ };
111
269
  }
112
- function buildDisplayBase(options) {
113
- if (options.serviceDid) {
114
- return options.serviceDid;
270
+ async function loadState(statePath) {
271
+ try {
272
+ const raw = await readFile(statePath, 'utf-8');
273
+ const parsed = JSON.parse(raw);
274
+ return parsed;
275
+ }
276
+ catch {
277
+ return undefined;
115
278
  }
116
- return options.baseUrl;
117
279
  }
118
- function buildDisplayUploadUrl(options) {
119
- return `${buildDisplayBase(options).replace(/\/+$/, '')}/${encodeURIComponent(options.tenantId)}/cds-${encodeURIComponent(options.jurisdiction)}/v1/${encodeURIComponent(options.sector)}/digitaltwin/${encodeURIComponent(options.softwareId)}/${encodeURIComponent(options.resourceType)}/_upload`;
280
+ async function saveState(statePath, state) {
281
+ await mkdir(path.dirname(statePath), { recursive: true });
282
+ await writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8');
120
283
  }
121
- function buildDisplayPollingUrl(options, thid) {
122
- return `${buildDisplayBase(options).replace(/\/+$/, '')}/${encodeURIComponent(options.tenantId)}/cds-${encodeURIComponent(options.jurisdiction)}/v1/${encodeURIComponent(options.sector)}/digitaltwin/${encodeURIComponent(options.softwareId)}/${encodeURIComponent(options.resourceType)}/_upload-response?thid=${encodeURIComponent(thid)}`;
284
+ function decodeJwtPayload(token) {
285
+ const parts = String(token || '').split('.');
286
+ if (parts.length !== 3)
287
+ return {};
288
+ try {
289
+ const padded = parts[1] + '='.repeat((4 - (parts[1].length % 4)) % 4);
290
+ const json = Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf-8');
291
+ const parsed = JSON.parse(json);
292
+ return typeof parsed === 'object' && parsed ? parsed : {};
293
+ }
294
+ catch {
295
+ return {};
296
+ }
123
297
  }
124
- async function main() {
125
- const options = parseArgs(process.argv.slice(2));
126
- const fileBytes = new Uint8Array(await readFile(options.file));
127
- const fileName = path.basename(options.file);
298
+ function normalizeEmail(value) {
299
+ return String(value || '').trim().toLowerCase();
300
+ }
301
+ function hashEmailSameAs(email) {
302
+ return createHash('sha256').update(normalizeEmail(email), 'utf-8').digest('hex').toLowerCase();
303
+ }
304
+ function jwtHs256(payload, secret, kid) {
305
+ const header = { alg: 'HS256', typ: 'JWT' };
306
+ if (kid)
307
+ header.kid = kid;
308
+ const headerRaw = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64url');
309
+ const payloadRaw = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url');
310
+ const signingInput = `${headerRaw}.${payloadRaw}`;
311
+ const signature = createHmac('sha256', secret).update(signingInput, 'utf-8').digest('base64url');
312
+ return `${signingInput}.${signature}`;
313
+ }
314
+ function buildDevVpToken(idToken, holderDid) {
315
+ const idClaims = decodeJwtPayload(idToken);
316
+ const email = String(idClaims.email || idClaims.preferred_username || idClaims.upn || '').trim();
317
+ if (!email) {
318
+ throw new Error('No email found in id_token; provide --vp-token manually or DATACONV_VP_TOKEN');
319
+ }
320
+ const now = Math.floor(Date.now() / 1000);
321
+ const vc = {
322
+ credentialSubject: {
323
+ id: process.env.DATACONV_OPERATIONAL_SUBJECT_DID || holderDid,
324
+ sameAs: hashEmailSameAs(email),
325
+ organization: process.env.DATACONV_ORGANIZATION || '',
326
+ scopes: ['dataconv.upload', 'dataconv.read']
327
+ }
328
+ };
329
+ return jwtHs256({
330
+ iss: holderDid,
331
+ sub: holderDid,
332
+ jti: randomUUID(),
333
+ iat: now,
334
+ exp: now + 300,
335
+ vp: {
336
+ holder: holderDid,
337
+ verifiableCredential: [vc]
338
+ }
339
+ }, process.env.DATACONV_WALLET_SHARED_SECRET || 'dev-wallet-secret', process.env.DATACONV_WALLET_KID || 'wallet-key-1');
340
+ }
341
+ function buildClientAssertion(baseUrl, holderDid, vpToken) {
342
+ const vpClaims = decodeJwtPayload(vpToken);
343
+ const now = Math.floor(Date.now() / 1000);
344
+ return jwtHs256({
345
+ iss: holderDid,
346
+ sub: holderDid,
347
+ aud: `${baseUrl.replace(/\/+$/, '')}/exchange`,
348
+ iat: now,
349
+ exp: now + 300,
350
+ jti: randomUUID(),
351
+ vp_jti: String(vpClaims.jti || '')
352
+ }, process.env.DATACONV_WALLET_SHARED_SECRET || 'dev-wallet-secret', process.env.DATACONV_WALLET_KID || 'wallet-key-1');
353
+ }
354
+ function isSessionValid(state) {
355
+ if (!state?.sessionToken || !state.sessionExpiresAt)
356
+ return false;
357
+ return state.sessionExpiresAt > Date.now() + 5000;
358
+ }
359
+ async function cmdLogin(args, statePath, currentState) {
360
+ const base = parseCommonArgs(args, currentState);
361
+ const idToken = getArgValue(args, '--id-token') || process.env.DATACONV_ID_TOKEN || currentState?.idToken;
362
+ if (!idToken) {
363
+ throw new Error('login requiere --id-token o DATACONV_ID_TOKEN');
364
+ }
365
+ const vpToken = getArgValue(args, '--vp-token') || process.env.DATACONV_VP_TOKEN || currentState?.vpToken;
366
+ const state = {
367
+ ...base,
368
+ idToken,
369
+ vpToken,
370
+ sessionToken: undefined,
371
+ sessionScope: undefined,
372
+ sessionExpiresAt: undefined,
373
+ apiKey: base.apiKey,
374
+ };
375
+ await saveState(statePath, state);
376
+ const claims = decodeJwtPayload(idToken);
377
+ const email = String(claims.email || claims.preferred_username || claims.upn || '').trim();
378
+ logInfo(`Identidad preparada para ${state.dataspaceName || 'GLOBAL-DATACARE'}`);
379
+ if (email) {
380
+ logSuccess(`Usuario autenticado localmente: ${email}`);
381
+ }
382
+ console.log(`Login guardado en ${statePath}`);
383
+ }
384
+ async function cmdExchange(args, statePath, currentState) {
385
+ const state = parseCommonArgs(args, currentState);
386
+ if (!state.idToken) {
387
+ throw new Error('No hay id_token local. Ejecuta primero dataconv login.');
388
+ }
389
+ const scope = getArgValue(args, '--scope') || 'dataconv.upload';
390
+ const holderDid = process.env.DATACONV_WALLET_DID || state.issuerDid;
391
+ const vpToken = getArgValue(args, '--vp-token') || process.env.DATACONV_VP_TOKEN || state.vpToken || buildDevVpToken(state.idToken, holderDid);
392
+ const clientAssertion = getArgValue(args, '--client-assertion') || process.env.DATACONV_CLIENT_ASSERTION || buildClientAssertion(state.baseUrl, holderDid, vpToken);
393
+ const organization = getArgValue(args, '--organization') || process.env.DATACONV_ORGANIZATION || state.organization || '';
394
+ const organizationDid = getArgValue(args, '--organization-did') || process.env.DATACONV_ORGANIZATION_DID || state.organizationDid || '';
395
+ const serviceId = getArgValue(args, '--service-id') || process.env.DATACONV_SERVICE_ID || state.serviceId || '';
396
+ const operationalSubject = getArgValue(args, '--operational-subject') || process.env.DATACONV_OPERATIONAL_SUBJECT_DID || state.subject || '';
397
+ const client = new DataConvClient({
398
+ issuerDid: state.issuerDid,
399
+ alternateName: state.tenantId,
400
+ tenantId: state.tenantId,
401
+ jurisdiction: state.jurisdiction,
402
+ sector: state.sector,
403
+ baseUrl: state.baseUrl,
404
+ idToken: state.idToken,
405
+ vpToken
406
+ });
407
+ const exchanged = await client.exchangeToken({
408
+ subjectToken: state.idToken,
409
+ vpToken,
410
+ clientAssertion,
411
+ scope,
412
+ apiKey: state.apiKey,
413
+ organization,
414
+ operationalSubject
415
+ });
416
+ const expiresAt = Date.now() + Number(exchanged.expires_in || 0) * 1000;
417
+ const nextState = {
418
+ ...state,
419
+ vpToken,
420
+ sessionToken: String(exchanged.access_token || ''),
421
+ sessionScope: String(exchanged.scope || scope),
422
+ sessionExpiresAt: expiresAt,
423
+ subject: String(exchanged.subject || ''),
424
+ organization: String(exchanged.organization || ''),
425
+ organizationDid: organizationDid || undefined,
426
+ serviceId: serviceId || undefined,
427
+ publisherTokenExchangeFallback: state.publisherTokenExchangeFallback,
428
+ publisherDatasetUpdateFallback: state.publisherDatasetUpdateFallback,
429
+ publisherDatasetPatchFallback: state.publisherDatasetPatchFallback,
430
+ publisherDatasetBatchFallback: state.publisherDatasetBatchFallback,
431
+ publisherDatasetSearchFallback: state.publisherDatasetSearchFallback,
432
+ apiKey: state.apiKey,
433
+ };
434
+ await saveState(statePath, nextState);
435
+ logInfo(`Autenticando con ${state.dataspaceName || 'GLOBAL-DATACARE'} (OAuth 2.0)...`);
436
+ if (organizationDid) {
437
+ logInfo(`organizationDid=${organizationDid}`);
438
+ }
439
+ if (serviceId) {
440
+ logInfo(`serviceId=${serviceId}`);
441
+ }
442
+ if (state.publisherTokenExchangeFallback) {
443
+ logInfo(`fallback token-exchange=${state.publisherTokenExchangeFallback}`);
444
+ }
445
+ logSuccess(`Token de sesión obtenido. scope=${nextState.sessionScope} exp=${new Date(expiresAt).toISOString()}`);
446
+ }
447
+ async function cmdUpload(args, statePath, currentState) {
448
+ const sourcePath = args[0];
449
+ if (!sourcePath || sourcePath.startsWith('--')) {
450
+ throw new Error('upload requiere ruta de archivo .xlsx');
451
+ }
452
+ let state = parseCommonArgs(args, currentState);
453
+ const requestedScope = getArgValue(args, '--scope') || `${state.resourceType}/_upload`;
454
+ if (!isSessionValid(state)) {
455
+ const exchangeArgs = ['--scope', requestedScope];
456
+ if (state.apiKey) {
457
+ exchangeArgs.push('--api-key', state.apiKey);
458
+ }
459
+ await cmdExchange(exchangeArgs, statePath, state);
460
+ state = (await loadState(statePath)) || state;
461
+ }
462
+ if (!state.sessionToken) {
463
+ throw new Error('No hay session token válido para upload');
464
+ }
465
+ const outputJson = getArgValue(args, '--output-json') || './artifacts/dataconv-upload-response-cli.json';
466
+ const mappingJsonPath = getArgValue(args, '--mapping-json');
467
+ const headerRowIndex = parseHeaderRowIndex(getArgValue(args, '--header-row-index'));
468
+ const fileBytes = new Uint8Array(await readFile(sourcePath));
469
+ const fileName = path.basename(sourcePath);
470
+ const fileSizeMb = (fileBytes.byteLength / (1024 * 1024)).toFixed(2);
128
471
  const client = new DataConvClient({
129
- issuerDid: options.issuerDid,
130
- alternateName: options.tenantId,
131
- tenantId: options.tenantId,
132
- jurisdiction: options.jurisdiction,
133
- sector: options.sector,
134
- baseUrl: options.baseUrl,
135
- retryTimes: options.retryTimes,
136
- retryDelayMs: options.retryDelayMs,
137
- idToken: options.idToken ?? options.authorizationBearer,
138
- vpToken: options.vpToken
472
+ issuerDid: state.issuerDid,
473
+ alternateName: state.tenantId,
474
+ tenantId: state.tenantId,
475
+ jurisdiction: state.jurisdiction,
476
+ sector: state.sector,
477
+ baseUrl: state.baseUrl,
478
+ idToken: state.idToken,
479
+ vpToken: state.vpToken,
480
+ retryTimes: Number(process.env.DATACONV_RETRY_TIMES || 60),
481
+ retryDelayMs: Number(process.env.DATACONV_RETRY_DELAY_MS || 2000)
139
482
  });
140
- if (options.serviceDid) {
141
- console.log(`Service DID: ${options.serviceDid}`);
142
- }
143
- if (!options.serviceDid || options.debug) {
144
- console.log(`Base URL: ${options.baseUrl}`);
145
- }
146
- console.log(`Tenant ID: ${options.tenantId}`);
147
- console.log(`Software ID: ${options.softwareId}`);
148
- console.log(`Resource type: ${options.resourceType}`);
149
- console.log(`Excel file: ${options.file}`);
150
- console.log(`Excel size: ${toKilobytes(fileBytes.byteLength)}`);
151
- console.log(`Upload request: POST ${buildDisplayUploadUrl(options)}`);
483
+ if (mappingJsonPath) {
484
+ logInfo('Validando y aplicando mapping de configuración...');
485
+ const mappingConfig = await loadMappingConfigFromFile(mappingJsonPath, headerRowIndex);
486
+ const configResponse = await client.createTenantConfigAndWait({
487
+ tenantId: state.tenantId,
488
+ jurisdiction: state.jurisdiction,
489
+ sector: state.sector,
490
+ softwareId: state.softwareId,
491
+ iss: state.issuerDid,
492
+ idToken: state.idToken,
493
+ vpToken: state.vpToken,
494
+ entries: [
495
+ {
496
+ softwareId: state.softwareId,
497
+ config: {
498
+ mappingConfig,
499
+ }
500
+ }
501
+ ]
502
+ });
503
+ const configSummaryIssue = client.getMainIssueDescriptionByResponse(configResponse)
504
+ || client.getMainDiagnosticInfoByResponse(configResponse)
505
+ || '';
506
+ console.log(`Config mapping aplicada para softwareId=${state.softwareId}`);
507
+ if (configSummaryIssue) {
508
+ console.log(`Config summary: ${configSummaryIssue}`);
509
+ }
510
+ }
511
+ logInfo(`Subiendo dataset a ${state.dataspaceName || 'GLOBAL-DATACARE'} (${fileName}, ${fileSizeMb} MB)...`);
152
512
  const uploadResult = await client.uploadWithFile({
153
- softwareId: options.softwareId,
154
- resourceType: options.resourceType,
513
+ softwareId: state.softwareId,
514
+ resourceType: state.resourceType,
155
515
  fileBytes,
156
516
  fileName,
157
- idToken: options.idToken ?? options.authorizationBearer,
158
- vpToken: options.vpToken
517
+ authorizationToken: state.sessionToken,
518
+ idToken: state.idToken,
519
+ vpToken: state.vpToken,
520
+ iss: state.issuerDid,
159
521
  });
160
- console.log(`Upload response Location: ${uploadResult.location || '(missing)'}`);
161
- console.log(`Upload thid: ${uploadResult.thid}`);
162
- console.log(`Polling request: POST ${buildDisplayPollingUrl(options, uploadResult.thid)}`);
522
+ logSuccess(`Upload aceptado. thid=${uploadResult.thid}`);
523
+ logInfo('Esperando resultado de conversión (_upload-response)...');
163
524
  const response = await client.pollUploadResponse({
164
- softwareId: options.softwareId,
165
- resourceType: options.resourceType,
525
+ softwareId: state.softwareId,
526
+ resourceType: state.resourceType,
166
527
  thid: uploadResult.thid,
167
- idToken: options.idToken ?? options.authorizationBearer,
168
- vpToken: options.vpToken
528
+ authorizationToken: state.sessionToken,
529
+ idToken: state.idToken,
530
+ vpToken: state.vpToken,
531
+ iss: state.issuerDid,
169
532
  });
170
- const outputPath = path.resolve(options.outputJson);
533
+ const outputPath = path.resolve(outputJson);
171
534
  await mkdir(path.dirname(outputPath), { recursive: true });
172
535
  await writeFile(outputPath, JSON.stringify(response, null, 2), 'utf-8');
173
- const diagnostic = client.getMainDiagnosticInfoByResponse(response) || '';
174
- console.log(`Polling response saved: ${outputPath}`);
175
- console.log(`Response thid: ${String(response.thid || '')}`);
176
- console.log(`Main diagnostic info: ${diagnostic}`);
536
+ const summaryIssue = client.getMainIssueDescriptionByResponse(response)
537
+ || client.getMainDiagnosticInfoByResponse(response)
538
+ || '';
539
+ logSuccess('PUBLICACIÓN COMPLETADA dataset procesado');
540
+ console.log(`Upload thid=${uploadResult.thid}`);
541
+ console.log(`Respuesta guardada en ${outputPath}`);
542
+ console.log(`Resumen: ${summaryIssue}`);
543
+ }
544
+ async function cmdSearch(args, statePath, currentState) {
545
+ let state = parseCommonArgs(args, currentState);
546
+ const resourceType = getArgValue(args, '--resource-type') || state.resourceType || 'DocumentReference';
547
+ const requestedScope = getArgValue(args, '--scope') || `${resourceType}/_search`;
548
+ if (!isSessionValid(state)) {
549
+ const exchangeArgs = ['--scope', requestedScope];
550
+ if (state.apiKey) {
551
+ exchangeArgs.push('--api-key', state.apiKey);
552
+ }
553
+ await cmdExchange(exchangeArgs, statePath, state);
554
+ state = (await loadState(statePath)) || state;
555
+ }
556
+ if (!state.sessionToken) {
557
+ throw new Error('No hay session token válido para search');
558
+ }
559
+ const params = parseObjectJson(getArgValue(args, '--params'), '--params') || {};
560
+ const outputJson = getArgValue(args, '--output-json');
561
+ const client = new DataConvClient({
562
+ issuerDid: state.issuerDid,
563
+ alternateName: state.tenantId,
564
+ tenantId: state.tenantId,
565
+ jurisdiction: state.jurisdiction,
566
+ sector: state.sector,
567
+ baseUrl: state.baseUrl,
568
+ idToken: state.idToken,
569
+ vpToken: state.vpToken,
570
+ });
571
+ logInfo(`Buscando ${resourceType} en ${state.dataspaceName || 'GLOBAL-DATACARE'}...`);
572
+ const bundle = await client.searchResources({
573
+ tenantId: state.tenantId,
574
+ jurisdiction: state.jurisdiction,
575
+ sector: state.sector,
576
+ resourceType,
577
+ authorizationToken: state.sessionToken,
578
+ searchParams: params,
579
+ });
580
+ const total = Number(bundle?.total || 0);
581
+ const entries = Array.isArray(bundle?.entry) ? bundle.entry.length : 0;
582
+ logSuccess(`Búsqueda completada. total=${total} entries=${entries}`);
583
+ if (outputJson) {
584
+ const outputPath = path.resolve(outputJson);
585
+ await mkdir(path.dirname(outputPath), { recursive: true });
586
+ await writeFile(outputPath, JSON.stringify(bundle, null, 2), 'utf-8');
587
+ console.log(`Resultado guardado en ${outputPath}`);
588
+ }
589
+ else {
590
+ console.log(JSON.stringify({ resourceType, total, entries }, null, 2));
591
+ }
592
+ }
593
+ async function cmdPatch(args, statePath, currentState) {
594
+ let state = parseCommonArgs(args, currentState);
595
+ const thid = getArgValue(args, '--thid');
596
+ if (!thid) {
597
+ throw new Error('patch requiere --thid (obtenido de la respuesta de upload)');
598
+ }
599
+ const resourceType = getArgValue(args, '--resource-type') || state.resourceType || 'excel';
600
+ const requestedScope = getArgValue(args, '--scope') || `${resourceType}/_patch`;
601
+ if (!isSessionValid(state)) {
602
+ const exchangeArgs = ['--scope', requestedScope];
603
+ if (state.apiKey)
604
+ exchangeArgs.push('--api-key', state.apiKey);
605
+ await cmdExchange(exchangeArgs, statePath, state);
606
+ state = (await loadState(statePath)) || state;
607
+ }
608
+ if (!state.sessionToken) {
609
+ throw new Error('No hay session token válido para patch');
610
+ }
611
+ const outputJson = getArgValue(args, '--output-json');
612
+ const client = new DataConvClient({
613
+ issuerDid: state.issuerDid,
614
+ alternateName: state.tenantId,
615
+ tenantId: state.tenantId,
616
+ jurisdiction: state.jurisdiction,
617
+ sector: state.sector,
618
+ baseUrl: state.baseUrl,
619
+ idToken: state.idToken,
620
+ vpToken: state.vpToken,
621
+ });
622
+ logInfo(`Confirmando conversión (_patch) thid=${thid}...`);
623
+ const response = await client.patchConversion({
624
+ softwareId: state.softwareId,
625
+ resourceType,
626
+ thid,
627
+ authorizationToken: state.sessionToken,
628
+ idToken: state.idToken,
629
+ vpToken: state.vpToken,
630
+ iss: state.issuerDid,
631
+ });
632
+ const status = String(response?.body?.status || '');
633
+ const promoted = Number(response?.body?.promotedCount ?? response?.body?.total ?? 0);
634
+ logSuccess(`Patch completado. status=${status} promotedCount=${promoted}`);
635
+ if (outputJson) {
636
+ const outputPath = path.resolve(outputJson);
637
+ await mkdir(path.dirname(outputPath), { recursive: true });
638
+ await writeFile(outputPath, JSON.stringify(response, null, 2), 'utf-8');
639
+ console.log(`Resultado guardado en ${outputPath}`);
640
+ }
641
+ }
642
+ async function cmdBatch(args, statePath, currentState) {
643
+ let state = parseCommonArgs(args, currentState);
644
+ const thid = getArgValue(args, '--thid');
645
+ if (!thid) {
646
+ throw new Error('batch requiere --thid (obtenido de la respuesta de upload)');
647
+ }
648
+ const resourceType = getArgValue(args, '--resource-type') || state.resourceType || 'excel';
649
+ const requestedScope = getArgValue(args, '--scope') || `${resourceType}/_batch`;
650
+ if (!isSessionValid(state)) {
651
+ const exchangeArgs = ['--scope', requestedScope];
652
+ if (state.apiKey)
653
+ exchangeArgs.push('--api-key', state.apiKey);
654
+ await cmdExchange(exchangeArgs, statePath, state);
655
+ state = (await loadState(statePath)) || state;
656
+ }
657
+ if (!state.sessionToken) {
658
+ throw new Error('No hay session token válido para batch');
659
+ }
660
+ const outputJson = getArgValue(args, '--output-json');
661
+ const client = new DataConvClient({
662
+ issuerDid: state.issuerDid,
663
+ alternateName: state.tenantId,
664
+ tenantId: state.tenantId,
665
+ jurisdiction: state.jurisdiction,
666
+ sector: state.sector,
667
+ baseUrl: state.baseUrl,
668
+ idToken: state.idToken,
669
+ vpToken: state.vpToken,
670
+ });
671
+ logInfo(`Confirmando conversión (_batch) thid=${thid}...`);
672
+ const response = await client.batchPromotion({
673
+ softwareId: state.softwareId,
674
+ resourceType,
675
+ thid,
676
+ authorizationToken: state.sessionToken,
677
+ idToken: state.idToken,
678
+ vpToken: state.vpToken,
679
+ iss: state.issuerDid,
680
+ });
681
+ const status = String(response?.body?.status || '');
682
+ const promoted = Number(response?.body?.promotedCount ?? response?.body?.total ?? 0);
683
+ logSuccess(`Batch completado. status=${status} promotedCount=${promoted}`);
684
+ if (outputJson) {
685
+ const outputPath = path.resolve(outputJson);
686
+ await mkdir(path.dirname(outputPath), { recursive: true });
687
+ await writeFile(outputPath, JSON.stringify(response, null, 2), 'utf-8');
688
+ console.log(`Resultado guardado en ${outputPath}`);
689
+ }
690
+ }
691
+ async function cmdWhoami(statePath, currentState) {
692
+ const state = currentState || await loadState(statePath);
693
+ if (!state) {
694
+ console.log('Sin estado local');
695
+ return;
696
+ }
697
+ console.log(JSON.stringify({
698
+ dataspaceName: state.dataspaceName || '',
699
+ baseUrl: state.baseUrl,
700
+ tenantId: state.tenantId,
701
+ jurisdiction: state.jurisdiction,
702
+ sector: state.sector,
703
+ softwareId: state.softwareId,
704
+ resourceType: state.resourceType,
705
+ hasIdToken: Boolean(state.idToken),
706
+ hasVpToken: Boolean(state.vpToken),
707
+ hasSessionToken: Boolean(state.sessionToken),
708
+ sessionScope: state.sessionScope || '',
709
+ sessionExpiresAt: state.sessionExpiresAt ? new Date(state.sessionExpiresAt).toISOString() : '',
710
+ subject: state.subject || '',
711
+ organization: state.organization || '',
712
+ organizationDid: state.organizationDid || '',
713
+ serviceId: state.serviceId || '',
714
+ entityOpenidExchangeFallback: state.publisherTokenExchangeFallback || '',
715
+ publisherDatasetUpdateFallback: state.publisherDatasetUpdateFallback || '',
716
+ publisherDatasetPatchFallback: state.publisherDatasetPatchFallback || '',
717
+ publisherDatasetBatchFallback: state.publisherDatasetBatchFallback || '',
718
+ publisherDatasetSearchFallback: state.publisherDatasetSearchFallback || '',
719
+ hasApiKey: Boolean(state.apiKey)
720
+ }, null, 2));
721
+ }
722
+ async function cmdApiKeyCreate(args, statePath, currentState) {
723
+ let state = parseCommonArgs(args, currentState);
724
+ const email = String(getArgValue(args, '--email') || '').trim();
725
+ if (!email) {
726
+ throw new Error('api-key-create requiere --email');
727
+ }
728
+ const target = String(getArgValue(args, '--target') || '').trim();
729
+ if (!target) {
730
+ throw new Error('api-key-create requiere --target (endpointId/patrón de endpoint)');
731
+ }
732
+ const explicitScopes = parseCsv(getArgValue(args, '--scope'));
733
+ const scopes = explicitScopes.length > 0 ? explicitScopes : [target];
734
+ const instrumentRaw = String(getArgValue(args, '--instrument') || '').trim();
735
+ let instrument = {};
736
+ if (instrumentRaw) {
737
+ try {
738
+ const parsed = JSON.parse(instrumentRaw);
739
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
740
+ instrument = parsed;
741
+ }
742
+ else {
743
+ throw new Error('instrument debe ser JSON objeto');
744
+ }
745
+ }
746
+ catch (error) {
747
+ throw new Error(`instrument inválido: ${error instanceof Error ? error.message : String(error)}`);
748
+ }
749
+ }
750
+ if (!isSessionValid(state) || !state.sessionToken) {
751
+ await cmdExchange(['--scope', 'dataconv.tenant.keys.manage'], statePath, state);
752
+ state = (await loadState(statePath)) || state;
753
+ }
754
+ if (!state.sessionToken) {
755
+ throw new Error('No hay session token válido para gestionar API keys');
756
+ }
757
+ const client = new DataConvClient({
758
+ issuerDid: state.issuerDid,
759
+ alternateName: state.tenantId,
760
+ tenantId: state.tenantId,
761
+ jurisdiction: state.jurisdiction,
762
+ sector: state.sector,
763
+ baseUrl: state.baseUrl,
764
+ idToken: state.idToken,
765
+ vpToken: state.vpToken,
766
+ });
767
+ const result = await client.createTenantApiKeyActions({
768
+ tenantId: state.tenantId,
769
+ jurisdiction: state.jurisdiction,
770
+ sector: state.sector,
771
+ authorizationToken: state.sessionToken,
772
+ actions: [
773
+ {
774
+ '@context': 'https://schema.org',
775
+ '@type': 'UpdateAction',
776
+ agent: { email },
777
+ target,
778
+ scope: scopes,
779
+ instrument,
780
+ actionStatus: 'active',
781
+ }
782
+ ]
783
+ });
784
+ const first = Array.isArray(result.data) ? result.data[0] : undefined;
785
+ const apiKey = String(first?.resource?.apiKey || '');
786
+ if (!apiKey) {
787
+ throw new Error('Respuesta sin apiKey');
788
+ }
789
+ const nextState = {
790
+ ...state,
791
+ apiKey,
792
+ };
793
+ await saveState(statePath, nextState);
794
+ console.log(JSON.stringify({
795
+ identifier: first?.identifier || '',
796
+ actionStatus: first?.actionStatus || '',
797
+ sameAs: first?.agent?.sameAs || '',
798
+ target: first?.target || target,
799
+ scope: first?.scope || scopes,
800
+ apiKey,
801
+ }, null, 2));
802
+ }
803
+ async function main() {
804
+ const argv = process.argv.slice(2);
805
+ if (!argv.length) {
806
+ console.log(commandHelp());
807
+ process.exit(0);
808
+ }
809
+ const command = argv[0];
810
+ const args = argv.slice(1);
811
+ if (command === 'help') {
812
+ console.log(commandHelp(args[0]));
813
+ process.exit(0);
814
+ }
815
+ if (hasFlag(args, '--help')) {
816
+ console.log(commandHelp(command));
817
+ process.exit(0);
818
+ }
819
+ const statePath = defaultStatePath(getArgValue(argv, '--state-file'));
820
+ const state = await loadState(statePath);
821
+ if (command === 'login') {
822
+ await cmdLogin(args, statePath, state);
823
+ return;
824
+ }
825
+ if (command === 'exchange') {
826
+ await cmdExchange(args, statePath, state);
827
+ return;
828
+ }
829
+ if (command === 'upload') {
830
+ await cmdUpload(args, statePath, state);
831
+ return;
832
+ }
833
+ if (command === 'search') {
834
+ await cmdSearch(args, statePath, state);
835
+ return;
836
+ }
837
+ if (command === 'patch') {
838
+ await cmdPatch(args, statePath, state);
839
+ return;
840
+ }
841
+ if (command === 'batch') {
842
+ await cmdBatch(args, statePath, state);
843
+ return;
844
+ }
845
+ if (command === 'api-key-create') {
846
+ await cmdApiKeyCreate(args, statePath, state);
847
+ return;
848
+ }
849
+ if (command === 'whoami') {
850
+ await cmdWhoami(statePath, state);
851
+ return;
852
+ }
853
+ throw new Error(`Comando no soportado: ${command}`);
177
854
  }
178
855
  main().catch((error) => {
179
856
  const message = error instanceof Error ? error.message : String(error);