@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 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
+ }