@agent-score/commerce 1.3.3 → 1.5.0

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/index.mjs CHANGED
@@ -330,6 +330,13 @@ function readX402PaymentHeader(request) {
330
330
  }
331
331
 
332
332
  // src/identity/a2a.ts
333
+ var UCP_A2A_EXTENSION_URI = "https://ucp.dev/2026-04-08/specification/reference";
334
+ function ucpA2AExtension(capabilities = {}) {
335
+ return {
336
+ uri: UCP_A2A_EXTENSION_URI,
337
+ params: { capabilities }
338
+ };
339
+ }
333
340
  var PROTOCOL_VERSION = "1.0";
334
341
  var CARD_VERSION = 1;
335
342
  function buildA2AAgentCard(input) {
@@ -361,17 +368,62 @@ function buildA2AAgentCard(input) {
361
368
  if (input.description !== void 0) card.description = input.description;
362
369
  if (input.url !== void 0) card.url = input.url;
363
370
  if (input.capabilities !== void 0) card.capabilities = input.capabilities;
371
+ if (input.extensions && input.extensions.length > 0) card.extensions = input.extensions;
364
372
  if (input.extras !== void 0) card.extras = input.extras;
365
373
  return card;
366
374
  }
367
375
 
368
376
  // src/identity/ucp.ts
377
+ function ucpSigningKeyFromJWK(jwk) {
378
+ if (!jwk || typeof jwk !== "object") {
379
+ throw new Error(`ucpSigningKeyFromJWK expected a non-null object; got ${typeof jwk}.`);
380
+ }
381
+ if (typeof jwk.kid !== "string" || !jwk.kid) {
382
+ throw new Error("ucpSigningKeyFromJWK: JWK missing required field `kid` (or non-string).");
383
+ }
384
+ if (typeof jwk.kty !== "string" || !jwk.kty) {
385
+ throw new Error("ucpSigningKeyFromJWK: JWK missing required field `kty` (or non-string).");
386
+ }
387
+ if (jwk.kty !== "OKP" && jwk.kty !== "EC" && jwk.kty !== "RSA") {
388
+ throw new Error(
389
+ `ucpSigningKeyFromJWK: kty=${JSON.stringify(jwk.kty)} is not a supported asymmetric key type (expected OKP, EC, or RSA). Symmetric \`oct\` keys are rejected because they cannot publicly verify a JWS in the trust-mode UCP flow.`
390
+ );
391
+ }
392
+ if ((jwk.kty === "EC" || jwk.kty === "OKP") && (typeof jwk.crv !== "string" || !jwk.crv)) {
393
+ throw new Error(`ucpSigningKeyFromJWK: kty=${jwk.kty} requires a non-empty \`crv\` field (e.g., "P-256" for EC, "Ed25519" for OKP).`);
394
+ }
395
+ return jwk;
396
+ }
369
397
  var DEFAULT_VERSION = "2026-04-17";
370
- var SPEC_URL = "https://ucp.dev/";
371
- var AGENTSCORE_CAPABILITY_NAME = "agentscore-identity";
398
+ var AGENTSCORE_CAPABILITY_NAME = "sh.agentscore.identity";
372
399
  var AGENTSCORE_CAPABILITY_VERSION = "1";
400
+ var AGENTSCORE_DEFAULT_SPEC_URL = "https://agentscore.sh/specification/identity";
401
+ var AGENTSCORE_DEFAULT_SCHEMA_URL = "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json";
402
+ var AGENTSCORE_EXTENDS = ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"];
403
+ var RESERVED_TOP_LEVEL = /* @__PURE__ */ new Set([
404
+ "ucp",
405
+ "signing_keys",
406
+ "signature",
407
+ "__proto__",
408
+ "constructor",
409
+ "prototype"
410
+ ]);
411
+ var RESERVED_UCP_FIELDS = /* @__PURE__ */ new Set([
412
+ "version",
413
+ "name",
414
+ "services",
415
+ "capabilities",
416
+ "payment_handlers",
417
+ "supported_versions",
418
+ "__proto__",
419
+ "constructor",
420
+ "prototype"
421
+ ]);
373
422
  function buildUCPProfile(input) {
374
- const baseCapabilities = [...input.capabilities ?? []];
423
+ const capabilities = {};
424
+ for (const [name, bindings] of Object.entries(input.capabilities ?? {})) {
425
+ capabilities[name] = [...bindings];
426
+ }
375
427
  if (input.data) {
376
428
  const operatorId = input.data.resolved_operator;
377
429
  if (operatorId) {
@@ -379,36 +431,332 @@ function buildUCPProfile(input) {
379
431
  const accountVerification = input.data.account_verification;
380
432
  const claims = {
381
433
  operator_id: operatorId,
382
- kyc_level: accountVerification?.kyc_level ?? operatorVerification?.level ?? "none",
434
+ kyc_level: accountVerification?.kyc_level || operatorVerification?.level || "none",
383
435
  sanctions_clear: accountVerification?.sanctions_clear === true,
384
- age_bracket: accountVerification?.age_bracket ?? "unknown",
385
- jurisdiction: accountVerification?.jurisdiction ?? "",
386
- verified_at: accountVerification?.verified_at ?? operatorVerification?.verified_at ?? null,
436
+ age_bracket: accountVerification?.age_bracket || "unknown",
437
+ jurisdiction: accountVerification?.jurisdiction || "",
438
+ verified_at: accountVerification?.verified_at || operatorVerification?.verified_at || null,
387
439
  verify_url: input.data.verify_url ?? null,
388
440
  issuer: "https://agentscore.sh"
389
441
  };
390
- baseCapabilities.push({
391
- name: AGENTSCORE_CAPABILITY_NAME,
442
+ const agentscoreBinding = {
392
443
  version: AGENTSCORE_CAPABILITY_VERSION,
393
- schema: input.agentscoreSchemaUrl ?? "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json",
444
+ spec: input.agentscore_spec_url ?? AGENTSCORE_DEFAULT_SPEC_URL,
445
+ schema: input.agentscore_schema_url ?? AGENTSCORE_DEFAULT_SCHEMA_URL,
446
+ extends: AGENTSCORE_EXTENDS,
447
+ // `claims` is our vendor extra on the binding; allowed per spec via the
448
+ // `[k: string]: unknown` index signature on UCPCapabilityBinding.
394
449
  claims
395
- });
450
+ };
451
+ const existing = capabilities[AGENTSCORE_CAPABILITY_NAME];
452
+ if (existing) existing.push(agentscoreBinding);
453
+ else capabilities[AGENTSCORE_CAPABILITY_NAME] = [agentscoreBinding];
396
454
  }
397
455
  }
398
- const profile = {
456
+ const ucp = {
399
457
  version: input.version ?? DEFAULT_VERSION,
400
- spec: SPEC_URL,
401
- services: input.services,
402
- capabilities: baseCapabilities,
403
- payment_handlers: input.payment_handlers ?? [],
458
+ services: input.services ?? {},
459
+ capabilities,
460
+ payment_handlers: input.payment_handlers ?? {}
461
+ };
462
+ if (input.name !== void 0) ucp.name = input.name;
463
+ if (input.supported_versions !== void 0) ucp.supported_versions = input.supported_versions;
464
+ if (input.ucp_extras) {
465
+ for (const k of Object.keys(input.ucp_extras)) {
466
+ if (RESERVED_UCP_FIELDS.has(k)) {
467
+ throw new Error(`buildUCPProfile: ucp_extras key "${k}" collides with a reserved \`ucp\` field; rejected.`);
468
+ }
469
+ }
470
+ Object.assign(ucp, input.ucp_extras);
471
+ }
472
+ const profile = {
473
+ ucp,
404
474
  signing_keys: input.signing_keys
405
475
  };
406
- if (input.name !== void 0) profile.name = input.name;
407
- if (input.extras) Object.assign(profile, input.extras);
476
+ if (input.extras) {
477
+ for (const k of Object.keys(input.extras)) {
478
+ if (RESERVED_TOP_LEVEL.has(k)) {
479
+ throw new Error(`buildUCPProfile: extras key "${k}" collides with a reserved profile field; rejected.`);
480
+ }
481
+ }
482
+ Object.assign(profile, input.extras);
483
+ }
408
484
  return profile;
409
485
  }
410
486
  var AGENTSCORE_UCP_CAPABILITY = AGENTSCORE_CAPABILITY_NAME;
411
487
 
488
+ // src/identity/ucp-jwks.ts
489
+ var JOSE_INSTALL_HINT = "Install the optional peer dependency: `npm install jose@^6` (or `bun add jose`). Tested against jose v6.x.";
490
+ var ALLOWED_ALGS = ["EdDSA", "ES256"];
491
+ var PROFILE_TYP = "agentscore-profile+jws";
492
+ var UCPVerificationError = class extends Error {
493
+ constructor(code, message) {
494
+ super(message);
495
+ this.code = code;
496
+ this.name = "UCPVerificationError";
497
+ }
498
+ code;
499
+ };
500
+ async function loadJose() {
501
+ try {
502
+ return await import("jose");
503
+ } catch (err) {
504
+ throw new Error(
505
+ `UCP signing requires the \`jose\` library, which is an optional peer dependency. ${JOSE_INSTALL_HINT}
506
+ Original error: ${err instanceof Error ? err.message : String(err)}`
507
+ );
508
+ }
509
+ }
510
+ function canonicalizeProfile(profile) {
511
+ const stripped = { ...profile };
512
+ delete stripped.signature;
513
+ return stableStringify(stripped);
514
+ }
515
+ function stableStringify(value) {
516
+ if (value === void 0) {
517
+ throw new Error(
518
+ "stableStringify: undefined values are not allowed in canonicalized JSON. Object fields with no value must be omitted."
519
+ );
520
+ }
521
+ if (typeof value === "function" || typeof value === "symbol") {
522
+ throw new Error(`stableStringify: ${typeof value} values are not allowed in canonicalized JSON.`);
523
+ }
524
+ if (typeof value === "bigint") {
525
+ throw new Error("stableStringify: BigInt values are not allowed; use a decimal string.");
526
+ }
527
+ if (value instanceof Date) {
528
+ throw new Error(
529
+ "stableStringify: Date instances are not allowed; serialize to an ISO string before passing."
530
+ );
531
+ }
532
+ if (value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
533
+ throw new Error(
534
+ `stableStringify: ${value.constructor.name} values are not allowed; convert to a plain object/array first.`
535
+ );
536
+ }
537
+ if (ArrayBuffer.isView(value)) {
538
+ throw new Error("stableStringify: typed arrays are not allowed; convert to a plain array first.");
539
+ }
540
+ if (typeof value === "number") {
541
+ if (!Number.isFinite(value)) {
542
+ throw new Error(
543
+ `UCP profile canonicalization rejects non-finite Number ${value}. Use a decimal string for any value that may be NaN/Infinity.`
544
+ );
545
+ }
546
+ if (!Number.isInteger(value)) {
547
+ throw new Error(
548
+ `UCP profile canonicalization rejects non-integer Number ${value}. Use a decimal string (e.g. "9.99") for monetary or fractional fields to preserve cross-language byte-parity.`
549
+ );
550
+ }
551
+ if (!Number.isSafeInteger(value)) {
552
+ throw new Error(
553
+ `stableStringify: integer ${value} exceeds Number.MAX_SAFE_INTEGER. For values >2^53, use a decimal string to preserve cross-language byte parity.`
554
+ );
555
+ }
556
+ }
557
+ if (typeof value === "string") {
558
+ if (value.includes("\u2028") || value.includes("\u2029")) {
559
+ throw new Error(
560
+ "stableStringify: strings containing U+2028 (LINE SEPARATOR) or U+2029 (PARAGRAPH SEPARATOR) are not allowed; cross-language byte parity requires neither be present (Node JSON.stringify on older V8 escapes them; Python json.dumps with ensure_ascii=False does not)."
561
+ );
562
+ }
563
+ return JSON.stringify(value);
564
+ }
565
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
566
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
567
+ const obj = value;
568
+ const keys = Object.keys(obj).sort((a, b) => {
569
+ const aPoints = [...a].map((c) => c.codePointAt(0));
570
+ const bPoints = [...b].map((c) => c.codePointAt(0));
571
+ const len = Math.min(aPoints.length, bPoints.length);
572
+ for (let i = 0; i < len; i += 1) {
573
+ if (aPoints[i] !== bPoints[i]) return aPoints[i] - bPoints[i];
574
+ }
575
+ return aPoints.length - bPoints.length;
576
+ });
577
+ for (const k of keys) {
578
+ if (k.includes("\u2028") || k.includes("\u2029")) {
579
+ throw new Error(
580
+ "stableStringify: object keys containing U+2028 (LINE SEPARATOR) or U+2029 (PARAGRAPH SEPARATOR) are not allowed; cross-language byte parity (Node JSON.stringify on older V8 escapes them; Python json.dumps with ensure_ascii=False does not)."
581
+ );
582
+ }
583
+ }
584
+ const pairs = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`);
585
+ return `{${pairs.join(",")}}`;
586
+ }
587
+ async function generateUCPSigningKey(opts) {
588
+ const jose = await loadJose();
589
+ const alg = opts.alg ?? "EdDSA";
590
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: true });
591
+ const exportedJwk = await jose.exportJWK(publicKey);
592
+ const publicJWK = {
593
+ kid: opts.kid,
594
+ alg,
595
+ use: "sig",
596
+ ...exportedJwk
597
+ };
598
+ return { privateKey, publicJWK };
599
+ }
600
+ async function signUCPProfile(profile, opts) {
601
+ const jose = await loadJose();
602
+ const alg = opts.alg ?? "EdDSA";
603
+ if (!ALLOWED_ALGS.includes(alg)) {
604
+ throw new Error(
605
+ `signUCPProfile: alg ${JSON.stringify(opts.alg)} is not in the supported set [${ALLOWED_ALGS.join(", ")}].`
606
+ );
607
+ }
608
+ if (typeof opts.kid !== "string" || !opts.kid) {
609
+ throw new Error("signUCPProfile: opts.kid must be a non-empty string.");
610
+ }
611
+ const kids = (profile.signing_keys ?? []).map((k) => k.kid);
612
+ if (!kids.includes(opts.kid)) {
613
+ throw new Error(
614
+ `signUCPProfile: kid ${JSON.stringify(opts.kid)} is not present in profile.signing_keys[] (declared kids: ${JSON.stringify(kids)}). Verifiers will not find the key.`
615
+ );
616
+ }
617
+ const canonicalBody = canonicalizeProfile(profile);
618
+ const payloadBytes = new TextEncoder().encode(canonicalBody);
619
+ const signature = await new jose.CompactSign(payloadBytes).setProtectedHeader({ alg, kid: opts.kid, typ: PROFILE_TYP }).sign(opts.signingKey);
620
+ return { ...profile, signature };
621
+ }
622
+ async function verifyUCPProfile(profile, jwks) {
623
+ if (profile === null || typeof profile !== "object" || Array.isArray(profile)) {
624
+ throw new UCPVerificationError(
625
+ "no_signature",
626
+ `UCP profile must be a JSON object; got ${profile === null ? "null" : Array.isArray(profile) ? "array" : typeof profile}.`
627
+ );
628
+ }
629
+ const jose = await loadJose();
630
+ if (!jwks || typeof jwks !== "object" || !Array.isArray(jwks.keys)) {
631
+ throw new UCPVerificationError(
632
+ "malformed_jwks",
633
+ `UCP verifier expected JWKS shape { keys: [...] }; got ${jwks === null ? "null" : typeof jwks === "object" ? "object without keys[] array" : typeof jwks}.`
634
+ );
635
+ }
636
+ const stripped = { ...profile };
637
+ const sig = stripped.signature;
638
+ delete stripped.signature;
639
+ if (typeof sig !== "string" || !sig) {
640
+ throw new UCPVerificationError(
641
+ "no_signature",
642
+ `UCP profile signature must be a non-empty string; got ${sig === void 0 ? "undefined" : typeof sig}.`
643
+ );
644
+ }
645
+ let header;
646
+ try {
647
+ const protectedB64 = sig.split(".")[0];
648
+ if (!protectedB64) throw new Error("JWS protected header segment is empty.");
649
+ const headerJson = new TextDecoder().decode(jose.base64url.decode(protectedB64));
650
+ const parsed = JSON.parse(headerJson);
651
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
652
+ throw new Error("JWS protected header is not a JSON object.");
653
+ }
654
+ header = parsed;
655
+ } catch (err) {
656
+ throw new UCPVerificationError(
657
+ "malformed_jws",
658
+ `JWS protected header is not valid base64url-encoded JSON: ${err instanceof Error ? err.message : String(err)}`
659
+ );
660
+ }
661
+ if (header.typ !== PROFILE_TYP) {
662
+ throw new UCPVerificationError("wrong_typ", `UCP signature typ must be "${PROFILE_TYP}"; got ${String(header.typ)}.`);
663
+ }
664
+ if (!ALLOWED_ALGS.includes(header.alg)) {
665
+ throw new UCPVerificationError("unsupported_alg", `UCP signing alg must be one of ${ALLOWED_ALGS.join(", ")}; got ${String(header.alg)}.`);
666
+ }
667
+ if (typeof header.kid !== "string" || !header.kid) {
668
+ throw new UCPVerificationError(
669
+ "missing_kid",
670
+ `UCP signature header kid must be a non-empty string; got ${header.kid === void 0 ? "undefined" : typeof header.kid}.`
671
+ );
672
+ }
673
+ if ("crit" in header) {
674
+ const crit = header.crit;
675
+ if (!Array.isArray(crit) || crit.length === 0 || !crit.every((c) => typeof c === "string")) {
676
+ throw new UCPVerificationError(
677
+ "malformed_jws",
678
+ `JWS protected header crit must be a non-empty array of strings; got ${JSON.stringify(crit)}.`
679
+ );
680
+ }
681
+ throw new UCPVerificationError(
682
+ "unrecognized_critical_header",
683
+ `JWS protected header advertises unrecognized crit headers: ${JSON.stringify(crit)}.`
684
+ );
685
+ }
686
+ let signedPayload;
687
+ try {
688
+ const verified = await jose.compactVerify(
689
+ sig,
690
+ async (h) => {
691
+ const kid = h.kid;
692
+ if (typeof kid !== "string" || !kid) {
693
+ throw new UCPVerificationError(
694
+ "missing_kid",
695
+ `UCP signature header kid must be a non-empty string; got ${kid === void 0 ? "undefined" : typeof kid}.`
696
+ );
697
+ }
698
+ const matches = jwks.keys.filter(
699
+ (k) => k != null && typeof k === "object" && k.kid === kid
700
+ );
701
+ if (matches.length === 0) throw new UCPVerificationError("kid_not_found", `No JWK in JWKS matching kid=${JSON.stringify(kid)}.`);
702
+ if (matches.length > 1) throw new UCPVerificationError("duplicate_kid", `JWKS contains ${matches.length} keys with kid=${JSON.stringify(kid)}; expected exactly one.`);
703
+ const matchedKey = matches[0];
704
+ if (matchedKey.use != null && matchedKey.use !== "sig") {
705
+ throw new UCPVerificationError("unusable_key", `JWK with kid=${kid} has use=${JSON.stringify(matchedKey.use)}; expected "sig".`);
706
+ }
707
+ if (matchedKey.alg != null && matchedKey.alg !== h.alg) {
708
+ throw new UCPVerificationError(
709
+ "unusable_key",
710
+ `JWK alg ${JSON.stringify(matchedKey.alg)} does not match JWS header alg ${JSON.stringify(h.alg)}.`
711
+ );
712
+ }
713
+ return jose.importJWK(matches[0], h.alg);
714
+ }
715
+ );
716
+ signedPayload = verified.payload;
717
+ } catch (err) {
718
+ if (err instanceof UCPVerificationError) throw err;
719
+ if (err instanceof Error && err.name === "JOSEAlgNotAllowed") {
720
+ throw new UCPVerificationError("unsupported_alg", `UCP signing alg not allowed: ${err.message}`);
721
+ }
722
+ if (err instanceof Error && err.name === "JWSSignatureVerificationFailed") {
723
+ throw new UCPVerificationError("signature_invalid", `UCP signature verification failed: ${err.message}`);
724
+ }
725
+ if (err instanceof Error && err.name === "JWSInvalid") {
726
+ throw new UCPVerificationError("malformed_jws", `Malformed JWS: ${err.message}`);
727
+ }
728
+ if (err instanceof Error && err.name === "JOSENotSupported") {
729
+ throw new UCPVerificationError("unrecognized_critical_header", `UCP signing rejected unrecognized critical header: ${err.message}`);
730
+ }
731
+ throw err;
732
+ }
733
+ let canonicalBody;
734
+ try {
735
+ canonicalBody = canonicalizeProfile(stripped);
736
+ } catch (err) {
737
+ throw new UCPVerificationError(
738
+ "body_mismatch",
739
+ `Failed to canonicalize received profile for verification: ${err instanceof Error ? err.message : String(err)}`
740
+ );
741
+ }
742
+ const expectedPayload = new TextEncoder().encode(canonicalBody);
743
+ if (!constantTimeEqual(signedPayload, expectedPayload)) {
744
+ throw new UCPVerificationError("body_mismatch", "UCP profile body does not match the signed payload (tampered or non-canonical).");
745
+ }
746
+ return true;
747
+ }
748
+ function constantTimeEqual(a, b) {
749
+ if (a.length !== b.length) return false;
750
+ let diff = 0;
751
+ for (let i = 0; i < a.length; i += 1) {
752
+ diff |= a[i] ^ b[i];
753
+ }
754
+ return diff === 0;
755
+ }
756
+ function buildJWKSResponse(keys) {
757
+ return { keys };
758
+ }
759
+
412
760
  // src/identity/policy.ts
413
761
  function policyToGateOptions(policy, base) {
414
762
  if (!policy || !policy.enforcement) return null;
@@ -458,21 +806,29 @@ function shippingStateAllowed(state, country, policy) {
458
806
  export {
459
807
  AGENTSCORE_UCP_CAPABILITY,
460
808
  FIXABLE_DENIAL_REASONS,
809
+ UCPVerificationError,
810
+ UCP_A2A_EXTENSION_URI,
461
811
  buildA2AAgentCard,
462
812
  buildAgentMemoryHint,
463
813
  buildContactSupportNextSteps,
814
+ buildJWKSResponse,
464
815
  buildSignerMismatchBody,
465
816
  buildUCPProfile,
466
817
  denialReasonStatus,
467
818
  denialReasonToBody,
468
819
  extractPaymentSigner,
469
820
  extractPaymentSignerAddress,
821
+ generateUCPSigningKey,
470
822
  isFixableDenial,
471
823
  policyToGateOptions,
472
824
  readX402PaymentHeader,
473
825
  runGateWithEnforcement,
474
826
  shippingCountryAllowed,
475
827
  shippingStateAllowed,
476
- verificationAgentInstructions
828
+ signUCPProfile,
829
+ ucpA2AExtension,
830
+ ucpSigningKeyFromJWK,
831
+ verificationAgentInstructions,
832
+ verifyUCPProfile
477
833
  };
478
834
  //# sourceMappingURL=index.mjs.map