@visa/cli 4.1.0-rc.287 → 4.1.0-rc.288
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/README.md +19 -0
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +15 -0
- package/dist/checkout-engine/vgs-gateway/server-mint-client.js +161 -0
- package/dist/cli.js +460 -458
- package/dist/managed-runtime/resolve-and-update.mjs +268 -0
- package/dist/managed-runtime/runtime-readiness.mjs +126 -0
- package/dist/managed-runtime/update-and-restart.mjs +1079 -0
- package/dist/mcp-server/index.js +383 -381
- package/dist/merchant-ucp-mcp/index.js +3 -3
- package/native/bin/win32-arm64/visa-runtime-signer.exe +0 -0
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/native/bin/win32-x64/visa-runtime-signer.exe +0 -0
- package/package.json +3 -2
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -35,6 +35,16 @@ iwr -useb https://app.visacli.sh/install.ps1 | iex
|
|
|
35
35
|
|
|
36
36
|
Node.js 20+ is required. macOS, Windows, and Linux are supported for the v4 wallet flow.
|
|
37
37
|
|
|
38
|
+
### Managed runtime convergence
|
|
39
|
+
|
|
40
|
+
The RC package also installs `visa-runtime-converge` for supervisor-owned
|
|
41
|
+
OpenClaw/Hermes/Telegram deployments. A scheduled invocation resolves the
|
|
42
|
+
preview server's recommended exact RC, stages and integrity-checks it, restarts
|
|
43
|
+
through the configured supervisor, verifies the loaded version/environment, and
|
|
44
|
+
rolls back a failed activation. It never updates an interactive owner-managed
|
|
45
|
+
installation. Configuration and readiness contracts are documented in
|
|
46
|
+
[`packages/visa-cli-openclaw/RUNTIMES.md`](https://github.com/Visa-Crypto-Labs/Visa-mono/blob/staging/packages/visa-cli-openclaw/RUNTIMES.md).
|
|
47
|
+
|
|
38
48
|
## MCP setup
|
|
39
49
|
|
|
40
50
|
The fastest path is to let the CLI write the client config for you:
|
|
@@ -389,6 +399,15 @@ certificate, delegation, Gate authorization and native key binding. The selected
|
|
|
389
399
|
native profile serves actual TAP consumers; unavailable custody or invalid selected
|
|
390
400
|
metadata fails closed. Wallet/card keys and payment authority remain separate.
|
|
391
401
|
|
|
402
|
+
`--agent` accepts either a locally paired agent or the stable UUID of a protected
|
|
403
|
+
agent created by `visa agent enroll-protected`; the latter has no Auth-paired record
|
|
404
|
+
and resolves its owner root from the independently claimed selection. The device
|
|
405
|
+
ceremonies (`connect-device`, `renew`, `resume`, `revoke --purpose tap`) travel the
|
|
406
|
+
public identity-device relay with native possession proofs, so a missing or expired
|
|
407
|
+
`visa agent login` never blocks them; the owner approval and owner revocation pages
|
|
408
|
+
remain required. Hosted purposes (`card:vic`, `wallet:x402`, hosted `tap`, `subway`)
|
|
409
|
+
still need a locally paired agent and its Auth session.
|
|
410
|
+
|
|
392
411
|
`visa agent keychain renew --agent <name-or-id> --purpose tap` creates an independently
|
|
393
412
|
addressed successor for the same runtime and opens the owner review page. Both keys
|
|
394
413
|
survive interruption. `visa agent keychain resume --agent <name-or-id>` resumes the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
1
2
|
import type { VgsCheckoutTarget, VgsPaymentCredential } from '../vgs-live-instrument.js';
|
|
2
3
|
export type ServerMintDeps = {
|
|
3
4
|
/** Injectable for tests — defaults to global fetch. */
|
|
@@ -136,3 +137,17 @@ export declare function serverPostConfirmation(base: string, mintToken: string,
|
|
|
136
137
|
}, deps?: ServerMintDeps): Promise<{
|
|
137
138
|
ok: true;
|
|
138
139
|
}>;
|
|
140
|
+
/** One exact protected attempt. Caller retains its delivery KeyObject across
|
|
141
|
+
* retries; it is never serialized into a request, receipt, log, or agent context. */
|
|
142
|
+
/** Pins are installed with the checkout deployment, never supplied by Auth or the response. */
|
|
143
|
+
export interface ProtectedCardPins {
|
|
144
|
+
executor: KeyObject;
|
|
145
|
+
issuer: KeyObject;
|
|
146
|
+
}
|
|
147
|
+
export declare function serverFetchProtectedCryptogram(base: string, input: {
|
|
148
|
+
financialAdmission: string;
|
|
149
|
+
request: unknown;
|
|
150
|
+
requestHash: string;
|
|
151
|
+
deliveryKey: KeyObject;
|
|
152
|
+
pins: ProtectedCardPins;
|
|
153
|
+
}, deps?: ServerMintDeps): Promise<VgsPaymentCredential>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createDecipheriv, createHash, verify, createPublicKey, diffieHellman, hkdfSync, } from 'node:crypto';
|
|
1
2
|
// Server-side mint client (Phase 1) — the checkout runner mints the payment
|
|
2
3
|
// credential by calling the verify-web deployment's gateway routes with a
|
|
3
4
|
// short-lived, purpose-scoped MINT TOKEN, instead of holding the shared VGS
|
|
@@ -330,3 +331,163 @@ export async function serverPostConfirmation(base, mintToken, input, deps = {})
|
|
|
330
331
|
throw new Error(`server confirmation failed (${res.status}): ${await routeError(res)}`);
|
|
331
332
|
return { ok: true };
|
|
332
333
|
}
|
|
334
|
+
function cardEvidence(token, key, typ, fields) {
|
|
335
|
+
const fail = () => {
|
|
336
|
+
throw new Error('protected_card_evidence_invalid');
|
|
337
|
+
};
|
|
338
|
+
if (key.type !== 'public' ||
|
|
339
|
+
key.asymmetricKeyType !== 'ed25519' ||
|
|
340
|
+
typeof token !== 'string' ||
|
|
341
|
+
token.length > 16384)
|
|
342
|
+
return fail();
|
|
343
|
+
const parts = token.split('.');
|
|
344
|
+
if (parts.length !== 3)
|
|
345
|
+
return fail();
|
|
346
|
+
const jwk = key.export({ format: 'jwk' });
|
|
347
|
+
const kid = createHash('sha256')
|
|
348
|
+
.update(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x }))
|
|
349
|
+
.digest('base64url');
|
|
350
|
+
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
|
|
351
|
+
const claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
352
|
+
if (!header ||
|
|
353
|
+
Object.keys(header).sort().join(',') !== 'alg,kid,typ' ||
|
|
354
|
+
header.alg !== 'EdDSA' ||
|
|
355
|
+
header.typ !== typ ||
|
|
356
|
+
header.kid !== kid ||
|
|
357
|
+
!claims ||
|
|
358
|
+
Object.keys(claims).sort().join(',') !== fields.sort().join(',') ||
|
|
359
|
+
claims.v !== 1 ||
|
|
360
|
+
typeof claims.jti !== 'string' ||
|
|
361
|
+
!claims.jti ||
|
|
362
|
+
!Number.isSafeInteger(claims.iat) ||
|
|
363
|
+
!verify(null, Buffer.from(parts.slice(0, 2).join('.')), key, Buffer.from(parts[2], 'base64url')))
|
|
364
|
+
return fail();
|
|
365
|
+
return claims;
|
|
366
|
+
}
|
|
367
|
+
export async function serverFetchProtectedCryptogram(base, input, deps = {}) {
|
|
368
|
+
const hash = Buffer.from(input.requestHash, 'base64url');
|
|
369
|
+
if (hash.length !== 32 ||
|
|
370
|
+
hash.toString('base64url') !== input.requestHash ||
|
|
371
|
+
input.deliveryKey.type !== 'private' ||
|
|
372
|
+
input.deliveryKey.asymmetricKeyType !== 'x25519')
|
|
373
|
+
throw new Error('protected_card_delivery_key_invalid');
|
|
374
|
+
const response = await (deps.fetchImpl ?? fetch)(`${stripTrailingSlashes(base)}/api/vgs/payment-cryptogram`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers: { 'content-type': 'application/json', 'x-visa-card-protected': 'v1' },
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
financialAdmission: input.financialAdmission,
|
|
379
|
+
request: input.request,
|
|
380
|
+
}),
|
|
381
|
+
redirect: 'error',
|
|
382
|
+
});
|
|
383
|
+
if (response.status === 202)
|
|
384
|
+
throw new Error('protected_card_recovery_pending');
|
|
385
|
+
if (!response.ok)
|
|
386
|
+
throw new Error(`protected_card_refused_${response.status}`);
|
|
387
|
+
const reader = response.body?.getReader();
|
|
388
|
+
if (!reader)
|
|
389
|
+
throw new Error('protected_card_response_invalid');
|
|
390
|
+
const chunks = [];
|
|
391
|
+
let size = 0;
|
|
392
|
+
try {
|
|
393
|
+
for (;;) {
|
|
394
|
+
const part = await reader.read();
|
|
395
|
+
if (part.done)
|
|
396
|
+
break;
|
|
397
|
+
size += part.value.byteLength;
|
|
398
|
+
if (size > 65536) {
|
|
399
|
+
await reader.cancel();
|
|
400
|
+
throw new Error('protected_card_response_invalid');
|
|
401
|
+
}
|
|
402
|
+
chunks.push(part.value);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
reader.releaseLock();
|
|
407
|
+
}
|
|
408
|
+
const envelope = JSON.parse(Buffer.concat(chunks).toString());
|
|
409
|
+
if (!envelope ||
|
|
410
|
+
Object.keys(envelope).sort().join(',') !==
|
|
411
|
+
'acknowledgement,ciphertext,ephemeralPublicJwk,iv,outcome,tag,v' ||
|
|
412
|
+
envelope.v !== 1)
|
|
413
|
+
throw new Error('protected_card_response_invalid');
|
|
414
|
+
const coordinates = [
|
|
415
|
+
'v',
|
|
416
|
+
'aud',
|
|
417
|
+
'jti',
|
|
418
|
+
'attemptId',
|
|
419
|
+
'requestHash',
|
|
420
|
+
'admissionId',
|
|
421
|
+
'issuanceGeneration',
|
|
422
|
+
'iat',
|
|
423
|
+
];
|
|
424
|
+
const outcome = cardEvidence(envelope.outcome, input.pins.executor, 'visa-card-delivery-outcome+jwt', [...coordinates, 'outcome', 'providerRef', 'credentialSha256']);
|
|
425
|
+
const ack = cardEvidence(envelope.acknowledgement, input.pins.issuer, 'visa-card-delivery-state+jwt', [...coordinates, 'action', 'outcomeHash', 'exp']);
|
|
426
|
+
const permit = JSON.parse(Buffer.from(input.financialAdmission.split('.')[1], 'base64url').toString());
|
|
427
|
+
const now = Math.floor(Date.now() / 1000);
|
|
428
|
+
if (outcome.aud !== 'urn:visa:authority:card-outcome' ||
|
|
429
|
+
outcome.outcome !== 'credential_observed' ||
|
|
430
|
+
outcome.requestHash !== input.requestHash ||
|
|
431
|
+
outcome.attemptId !== permit.attemptId ||
|
|
432
|
+
outcome.admissionId !== permit.jti ||
|
|
433
|
+
outcome.issuanceGeneration !== permit.issuanceGeneration ||
|
|
434
|
+
ack.aud !== 'urn:visa:executor:card-vic' ||
|
|
435
|
+
ack.action !== 'recover_only' ||
|
|
436
|
+
ack.requestHash !== outcome.requestHash ||
|
|
437
|
+
ack.attemptId !== outcome.attemptId ||
|
|
438
|
+
ack.admissionId !== outcome.admissionId ||
|
|
439
|
+
ack.issuanceGeneration !== outcome.issuanceGeneration ||
|
|
440
|
+
!Number.isSafeInteger(ack.exp) ||
|
|
441
|
+
ack.exp <= now ||
|
|
442
|
+
ack.iat > now + 5 ||
|
|
443
|
+
ack.exp <= ack.iat ||
|
|
444
|
+
ack.exp - ack.iat > 60 ||
|
|
445
|
+
ack.outcomeHash !== createHash('sha256').update(envelope.outcome).digest('base64url'))
|
|
446
|
+
throw new Error('protected_card_evidence_invalid');
|
|
447
|
+
const jwk = envelope.ephemeralPublicJwk;
|
|
448
|
+
if (!jwk ||
|
|
449
|
+
Object.keys(jwk).sort().join(',') !== 'crv,kty,x' ||
|
|
450
|
+
jwk.kty !== 'OKP' ||
|
|
451
|
+
jwk.crv !== 'X25519')
|
|
452
|
+
throw new Error('protected_card_response_invalid');
|
|
453
|
+
const decode = (value, size) => {
|
|
454
|
+
if (typeof value !== 'string')
|
|
455
|
+
throw new Error('protected_card_response_invalid');
|
|
456
|
+
const decoded = Buffer.from(value, 'base64url');
|
|
457
|
+
if (decoded.toString('base64url') !== value || (size !== undefined && decoded.length !== size))
|
|
458
|
+
throw new Error('protected_card_response_invalid');
|
|
459
|
+
return decoded;
|
|
460
|
+
};
|
|
461
|
+
decode(jwk.x, 32);
|
|
462
|
+
const shared = diffieHellman({
|
|
463
|
+
privateKey: input.deliveryKey,
|
|
464
|
+
publicKey: createPublicKey({ key: jwk, format: 'jwk' }),
|
|
465
|
+
});
|
|
466
|
+
const key = Buffer.from(hkdfSync('sha256', shared, hash, 'visa-card-delivery-v1', 32));
|
|
467
|
+
shared.fill(0);
|
|
468
|
+
let clear;
|
|
469
|
+
try {
|
|
470
|
+
const decipher = createDecipheriv('aes-256-gcm', key, decode(envelope.iv, 12));
|
|
471
|
+
decipher.setAAD(Buffer.from(input.requestHash));
|
|
472
|
+
decipher.setAuthTag(decode(envelope.tag, 16));
|
|
473
|
+
clear = Buffer.concat([decipher.update(decode(envelope.ciphertext)), decipher.final()]);
|
|
474
|
+
if (createHash('sha256').update(clear).digest('base64url') !== outcome.credentialSha256)
|
|
475
|
+
throw new Error('protected_card_credential_invalid');
|
|
476
|
+
const credential = JSON.parse(clear.toString());
|
|
477
|
+
if (!credential ||
|
|
478
|
+
typeof credential.networkToken !== 'string' ||
|
|
479
|
+
typeof credential.cryptogramValue !== 'string' ||
|
|
480
|
+
!Number.isInteger(credential.expMonth) ||
|
|
481
|
+
credential.expMonth < 1 ||
|
|
482
|
+
credential.expMonth > 12 ||
|
|
483
|
+
!Number.isInteger(credential.expYear) ||
|
|
484
|
+
credential.expYear < 2000 ||
|
|
485
|
+
typeof credential.cryptogramType !== 'string')
|
|
486
|
+
throw new Error('protected_card_credential_invalid');
|
|
487
|
+
return credential;
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
key.fill(0);
|
|
491
|
+
clear?.fill(0);
|
|
492
|
+
}
|
|
493
|
+
}
|