@learncard/vc-plugin 1.5.13 → 1.6.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.
@@ -513,6 +513,638 @@ var verifyPresentation = /* @__PURE__ */ __name((initLearnCard) => {
513
513
  };
514
514
  }, "verifyPresentation");
515
515
 
516
+ // src/refreshCredential.ts
517
+ import {
518
+ CredentialRefreshChallengeValidator,
519
+ JWEValidator,
520
+ VCValidator
521
+ } from "@learncard/types";
522
+ import {
523
+ credentialContentsEqual,
524
+ getCredentialEffectiveTime,
525
+ getCredentialIssuerId,
526
+ getSupportedRefreshService
527
+ } from "@learncard/helpers";
528
+
529
+ // src/refreshCredential.fetch.ts
530
+ var isNodeRuntime = typeof process !== "undefined" && !!process.versions?.node;
531
+ var toResponseHeaders = /* @__PURE__ */ __name((headers) => {
532
+ const result = new Headers();
533
+ for (const [name, value] of Object.entries(headers)) {
534
+ if (Array.isArray(value)) value.forEach((entry) => result.append(name, entry));
535
+ else if (value !== void 0) result.append(name, value);
536
+ }
537
+ return result;
538
+ }, "toResponseHeaders");
539
+ var fetchWithPinnedAddress = /* @__PURE__ */ __name(async (url, init, pinnedAddress) => {
540
+ if (!isNodeRuntime || !pinnedAddress) return globalThis.fetch(url.href, init);
541
+ const specifier = url.protocol === "https:" ? "node:https" : "node:http";
542
+ const client = await import(
543
+ /* @vite-ignore */
544
+ specifier
545
+ );
546
+ return new Promise((resolve, reject) => {
547
+ const lookup = /* @__PURE__ */ __name((_hostname, options, callback) => {
548
+ if (options?.all) callback(null, [pinnedAddress]);
549
+ else callback(null, pinnedAddress.address, pinnedAddress.family);
550
+ }, "lookup");
551
+ let request;
552
+ try {
553
+ const headers = Object.fromEntries(new Headers(init.headers).entries());
554
+ headers["accept-encoding"] = "identity";
555
+ request = client.request(
556
+ url,
557
+ {
558
+ method: init.method,
559
+ headers,
560
+ signal: init.signal,
561
+ lookup,
562
+ // A shared agent could reuse a socket created for a different DNS
563
+ // answer and bypass this request's validated pin.
564
+ agent: false,
565
+ ...url.protocol === "https:" ? { servername: url.hostname } : {}
566
+ },
567
+ (nodeResponse) => {
568
+ try {
569
+ const status = nodeResponse.statusCode;
570
+ if (status === void 0 || !Number.isInteger(status) || status < 200 || status > 599) {
571
+ reject(new RangeError("Invalid refresh response status"));
572
+ nodeResponse.destroy();
573
+ return;
574
+ }
575
+ const hasBody = status !== 204 && status !== 205 && status !== 304;
576
+ const body = hasBody ? new ReadableStream({
577
+ start(controller) {
578
+ nodeResponse.on("data", (chunk) => {
579
+ if (typeof chunk === "string") {
580
+ controller.enqueue(new TextEncoder().encode(chunk));
581
+ } else if (chunk instanceof Uint8Array) {
582
+ controller.enqueue(chunk);
583
+ } else {
584
+ controller.error(
585
+ new TypeError("Unsupported response chunk")
586
+ );
587
+ }
588
+ });
589
+ nodeResponse.on("end", () => controller.close());
590
+ nodeResponse.on("error", (error) => controller.error(error));
591
+ nodeResponse.on(
592
+ "aborted",
593
+ () => controller.error(
594
+ new DOMException(
595
+ "The operation was aborted",
596
+ "AbortError"
597
+ )
598
+ )
599
+ );
600
+ },
601
+ cancel(reason) {
602
+ nodeResponse.destroy(
603
+ reason instanceof Error ? reason : new Error(String(reason))
604
+ );
605
+ }
606
+ }) : null;
607
+ resolve(
608
+ new Response(body, {
609
+ status,
610
+ statusText: nodeResponse.statusMessage,
611
+ headers: toResponseHeaders(nodeResponse.headers)
612
+ })
613
+ );
614
+ } catch (error) {
615
+ reject(error);
616
+ nodeResponse.destroy();
617
+ }
618
+ }
619
+ );
620
+ } catch (error) {
621
+ reject(error);
622
+ return;
623
+ }
624
+ request.on("error", reject);
625
+ request.end();
626
+ });
627
+ }, "fetchWithPinnedAddress");
628
+
629
+ // src/refreshCredential.ts
630
+ var DID_AUTH_SCHEME = "learncarddidauth";
631
+ var DEFAULT_TIMEOUT_MS = 1e4;
632
+ var HEADER_CHALLENGE_TTL_MS = 3e5;
633
+ var MAX_TRACKED_CHALLENGES = 1024;
634
+ var DEFAULT_MAX_REDIRECTS = 3;
635
+ var DEFAULT_MAX_RESPONSE_BYTES = 1048576;
636
+ var ACCEPTED_MEDIA_TYPES = /* @__PURE__ */ new Set([
637
+ "application/json",
638
+ "application/ld+json",
639
+ "application/vc+ld+json",
640
+ "application/vc-ld+json",
641
+ "application/jwe+json"
642
+ ]);
643
+ var ACCEPT_HEADER = [...ACCEPTED_MEDIA_TYPES].join(", ");
644
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
645
+ var failed = /* @__PURE__ */ __name((code, retryable) => ({
646
+ status: "failed",
647
+ code,
648
+ retryable
649
+ }), "failed");
650
+ var unsafeEndpoint = /* @__PURE__ */ __name(() => failed("UNSAFE_ENDPOINT", false), "unsafeEndpoint");
651
+ var malformedResponse = /* @__PURE__ */ __name(() => failed("MALFORMED_RESPONSE", false), "malformedResponse");
652
+ var invalidProof = /* @__PURE__ */ __name(() => failed("INVALID_PROOF", false), "invalidProof");
653
+ var unauthorized = /* @__PURE__ */ __name(() => failed("UNAUTHORIZED", false), "unauthorized");
654
+ var proofVerified = /* @__PURE__ */ __name((check) => !!check && check.errors.length === 0 && check.warnings.length === 0 && check.checks.includes("proof"), "proofVerified");
655
+ var parseIPv4Octets = /* @__PURE__ */ __name((ip) => {
656
+ const parts = ip.split(".");
657
+ if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part) || Number(part) < 0 || Number(part) > 255)) {
658
+ return void 0;
659
+ }
660
+ return parts.map(Number);
661
+ }, "parseIPv4Octets");
662
+ var isPrivateOrReservedIPv4 = /* @__PURE__ */ __name((ip) => {
663
+ const octets = parseIPv4Octets(ip);
664
+ if (!octets) return true;
665
+ const [a, b, c] = octets;
666
+ return a === 0 || // "this" network
667
+ a === 10 || // RFC 1918
668
+ a === 127 || // loopback
669
+ a === 100 && b >= 64 && b <= 127 || // CGNAT
670
+ a === 169 && b === 254 || // link-local + cloud metadata
671
+ a === 172 && b >= 16 && b <= 31 || // RFC 1918
672
+ a === 192 && b === 0 || // IETF protocol assignments + TEST-NET-1
673
+ a === 192 && b === 168 || // RFC 1918
674
+ a === 198 && (b === 18 || b === 19) || // benchmarking
675
+ a === 198 && b === 51 && c === 100 || // TEST-NET-2
676
+ a === 203 && b === 0 && c === 113 || // TEST-NET-3
677
+ a >= 224;
678
+ }, "isPrivateOrReservedIPv4");
679
+ var parseIPv6Hextets = /* @__PURE__ */ __name((raw) => {
680
+ const hasOpeningBracket = raw.startsWith("[");
681
+ const hasClosingBracket = raw.endsWith("]");
682
+ if (hasOpeningBracket !== hasClosingBracket) return void 0;
683
+ const ip = (hasOpeningBracket ? raw.slice(1, -1) : raw).toLowerCase();
684
+ if (!ip || ip.includes("%") || (ip.match(/::/g)?.length ?? 0) > 1) return void 0;
685
+ let normalized = ip;
686
+ const dottedTail = normalized.match(/(?:^|:)(\d{1,3}(?:\.\d{1,3}){3})$/)?.[1];
687
+ if (dottedTail) {
688
+ const octets = parseIPv4Octets(dottedTail);
689
+ if (!octets) return void 0;
690
+ normalized = normalized.slice(0, -dottedTail.length);
691
+ normalized += `${(octets[0] << 8 | octets[1]).toString(16)}:${(octets[2] << 8 | octets[3]).toString(16)}`;
692
+ }
693
+ const hasCompression = normalized.includes("::");
694
+ const [leftRaw, rightRaw = ""] = normalized.split("::");
695
+ const left = leftRaw ? leftRaw.split(":") : [];
696
+ const right = rightRaw ? rightRaw.split(":") : [];
697
+ const tokens = [...left, ...right];
698
+ if (tokens.some((token) => !/^[0-9a-f]{1,4}$/.test(token)) || (hasCompression ? tokens.length >= 8 : tokens.length !== 8)) {
699
+ return void 0;
700
+ }
701
+ const omitted = hasCompression ? 8 - tokens.length : 0;
702
+ return [...left, ...Array(omitted).fill(0), ...right].map(
703
+ (token) => typeof token === "number" ? token : Number.parseInt(token, 16)
704
+ );
705
+ }, "parseIPv6Hextets");
706
+ var isIPv4TranslationOrTransitionPrefix = /* @__PURE__ */ __name((hextets) => {
707
+ const [first, second, third] = hextets;
708
+ const isNat64WellKnown = first === 100 && second === 65435 && hextets.slice(2, 6).every((part) => part === 0);
709
+ const isNat64Local = first === 100 && second === 65435 && third === 1;
710
+ const is6to4 = first === 8194;
711
+ return isNat64WellKnown || isNat64Local || is6to4;
712
+ }, "isIPv4TranslationOrTransitionPrefix");
713
+ var isPrivateOrReservedIPv6 = /* @__PURE__ */ __name((raw) => {
714
+ const hextets = parseIPv6Hextets(raw);
715
+ if (!hextets) return true;
716
+ if (isIPv4TranslationOrTransitionPrefix(hextets)) return true;
717
+ const [first = 0, second = 0] = hextets;
718
+ const isEmbeddedIPv4Prefix = hextets.slice(0, 5).every((part) => part === 0);
719
+ if (isEmbeddedIPv4Prefix && (hextets[5] === 0 || hextets[5] === 65535)) {
720
+ const embedded = `${(hextets[6] ?? 0) >> 8}.${(hextets[6] ?? 0) & 255}.${(hextets[7] ?? 0) >> 8}.${(hextets[7] ?? 0) & 255}`;
721
+ if (isPrivateOrReservedIPv4(embedded)) return true;
722
+ }
723
+ if (hextets.every((part) => part === 0)) return true;
724
+ if (hextets.slice(0, 7).every((part) => part === 0) && hextets[7] === 1) return true;
725
+ if ((first & 65024) === 64512) return true;
726
+ if ((first & 65472) === 65152) return true;
727
+ if ((first & 65280) === 65280) return true;
728
+ if (first === 8193 && second === 3512) return true;
729
+ return false;
730
+ }, "isPrivateOrReservedIPv6");
731
+ var parsePinnedAddress = /* @__PURE__ */ __name((raw) => {
732
+ if (raw.includes(":")) {
733
+ const hextets = parseIPv6Hextets(raw);
734
+ if (!hextets) return void 0;
735
+ return { address: hextets.map((part) => part.toString(16)).join(":"), family: 6 };
736
+ }
737
+ const octets = parseIPv4Octets(raw);
738
+ if (!octets) return void 0;
739
+ return { address: octets.join("."), family: 4 };
740
+ }, "parsePinnedAddress");
741
+ var isNodeRuntime2 = typeof process !== "undefined" && !!process.versions?.node;
742
+ var resolveHostnameWithNodeDns = /* @__PURE__ */ __name(async (hostname) => {
743
+ const specifier = "node:dns/promises";
744
+ const dns = await import(
745
+ /* @vite-ignore */
746
+ specifier
747
+ );
748
+ const answers = await dns.lookup(hostname, { all: true, verbatim: true });
749
+ return answers.map((answer) => answer.address);
750
+ }, "resolveHostnameWithNodeDns");
751
+ var validateRefreshUrl = /* @__PURE__ */ __name(async (rawUrl, options) => {
752
+ let url;
753
+ try {
754
+ url = new URL(rawUrl);
755
+ } catch {
756
+ return { result: unsafeEndpoint() };
757
+ }
758
+ if (url.username || url.password) return { result: unsafeEndpoint() };
759
+ const isHttps = url.protocol === "https:";
760
+ const isHttp = url.protocol === "http:";
761
+ if (!isHttps && !(isHttp && options.allowInsecureHttp)) return { result: unsafeEndpoint() };
762
+ const isIpv6Literal = url.hostname.startsWith("[");
763
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
764
+ if (!hostname) return { result: unsafeEndpoint() };
765
+ if (isIpv6Literal) {
766
+ if (!options.allowPrivateAddresses && isPrivateOrReservedIPv6(hostname)) {
767
+ return { result: unsafeEndpoint() };
768
+ }
769
+ } else if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname)) {
770
+ if (!options.allowPrivateAddresses && isPrivateOrReservedIPv4(hostname)) {
771
+ return { result: unsafeEndpoint() };
772
+ }
773
+ } else if (options.resolveHost) {
774
+ let addresses;
775
+ try {
776
+ addresses = await options.resolveHost(hostname);
777
+ } catch {
778
+ return { result: failed("UNAVAILABLE", true) };
779
+ }
780
+ if (addresses.length === 0) return { result: failed("UNAVAILABLE", true) };
781
+ const parsedAddresses = addresses.map(parsePinnedAddress);
782
+ const pinnedAddress = parsedAddresses[0];
783
+ const hasUnsafeAnswer = parsedAddresses.some(
784
+ (address) => !address || (address.family === 6 ? isPrivateOrReservedIPv6(address.address) : isPrivateOrReservedIPv4(address.address))
785
+ );
786
+ if (!pinnedAddress || hasUnsafeAnswer && !options.allowPrivateAddresses) {
787
+ return { result: unsafeEndpoint() };
788
+ }
789
+ return { url, pinnedAddress };
790
+ }
791
+ return { url };
792
+ }, "validateRefreshUrl");
793
+ var discardGuardedResponse = /* @__PURE__ */ __name(async (guarded) => {
794
+ try {
795
+ await guarded.response.body?.cancel();
796
+ } catch {
797
+ } finally {
798
+ guarded.finish();
799
+ }
800
+ }, "discardGuardedResponse");
801
+ var fetchWithGuards = /* @__PURE__ */ __name(async (initialEndpoint, options, authorization) => {
802
+ if (typeof globalThis.fetch !== "function") {
803
+ return { result: failed("UNAVAILABLE", false) };
804
+ }
805
+ let current = initialEndpoint;
806
+ const initialOrigin = initialEndpoint.url.origin;
807
+ let redirectsRemaining = options.maxRedirects;
808
+ let auth = authorization;
809
+ const deadline = Date.now() + options.timeoutMs;
810
+ for (; ; ) {
811
+ const remainingMs = deadline - Date.now();
812
+ if (remainingMs <= 0) return { result: failed("TIMEOUT", true) };
813
+ const controller = new AbortController();
814
+ const timer = setTimeout(() => controller.abort(), remainingMs);
815
+ let response;
816
+ try {
817
+ const headers = { accept: ACCEPT_HEADER };
818
+ if (options.etag) headers["if-none-match"] = options.etag;
819
+ if (auth) headers.authorization = auth;
820
+ response = await fetchWithPinnedAddress(
821
+ current.url,
822
+ {
823
+ method: "GET",
824
+ redirect: "manual",
825
+ signal: controller.signal,
826
+ headers
827
+ },
828
+ current.pinnedAddress
829
+ );
830
+ } catch (error) {
831
+ clearTimeout(timer);
832
+ const timedOut = controller.signal.aborted || error?.name === "AbortError";
833
+ return { result: failed(timedOut ? "TIMEOUT" : "UNAVAILABLE", true) };
834
+ }
835
+ if (!REDIRECT_STATUSES.has(response.status)) {
836
+ return {
837
+ response,
838
+ endpoint: current,
839
+ signal: controller.signal,
840
+ finish: /* @__PURE__ */ __name(() => clearTimeout(timer), "finish")
841
+ };
842
+ }
843
+ clearTimeout(timer);
844
+ await response.body?.cancel().catch(() => void 0);
845
+ if (redirectsRemaining <= 0) return { result: failed("UNAVAILABLE", false) };
846
+ const location = response.headers.get("location");
847
+ if (!location) return { result: malformedResponse() };
848
+ let next;
849
+ try {
850
+ next = new URL(location, current.url);
851
+ } catch {
852
+ return { result: unsafeEndpoint() };
853
+ }
854
+ const validated = await validateRefreshUrl(next.href, options);
855
+ if ("result" in validated) return validated;
856
+ if (validated.url.origin !== initialOrigin) auth = void 0;
857
+ redirectsRemaining -= 1;
858
+ current = validated;
859
+ }
860
+ }, "fetchWithGuards");
861
+ var readBodyWithLimit = /* @__PURE__ */ __name(async (response, maxBytes, signal) => {
862
+ const declared = Number(response.headers.get("content-length"));
863
+ if (Number.isFinite(declared) && declared > maxBytes) {
864
+ return { result: malformedResponse(), reason: "malformed" };
865
+ }
866
+ if (!response.body) {
867
+ try {
868
+ const body = await response.text();
869
+ return new TextEncoder().encode(body).byteLength > maxBytes ? { result: malformedResponse(), reason: "malformed" } : { body };
870
+ } catch {
871
+ return signal.aborted ? { result: failed("TIMEOUT", true), reason: "timeout" } : { result: malformedResponse(), reason: "malformed" };
872
+ }
873
+ }
874
+ const reader = response.body.getReader();
875
+ const chunks = [];
876
+ let total = 0;
877
+ try {
878
+ for (; ; ) {
879
+ const { done, value } = await reader.read();
880
+ if (done) break;
881
+ total += value?.byteLength ?? 0;
882
+ if (total > maxBytes) {
883
+ await reader.cancel().catch(() => void 0);
884
+ return { result: malformedResponse(), reason: "malformed" };
885
+ }
886
+ if (value) chunks.push(value);
887
+ }
888
+ } catch {
889
+ return signal.aborted ? { result: failed("TIMEOUT", true), reason: "timeout" } : { result: malformedResponse(), reason: "malformed" };
890
+ }
891
+ const merged = new Uint8Array(total);
892
+ let offset = 0;
893
+ for (const chunk of chunks) {
894
+ merged.set(chunk, offset);
895
+ offset += chunk.byteLength;
896
+ }
897
+ return { body: new TextDecoder().decode(merged) };
898
+ }, "readBodyWithLimit");
899
+ var isAcceptedMediaType = /* @__PURE__ */ __name((contentType) => {
900
+ if (!contentType) return false;
901
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
902
+ return ACCEPTED_MEDIA_TYPES.has(mediaType);
903
+ }, "isAcceptedMediaType");
904
+ var parseAuthParams = /* @__PURE__ */ __name((header) => {
905
+ const params = {};
906
+ const pattern = /([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*"([^"]*)"/g;
907
+ let match;
908
+ while (match = pattern.exec(header)) {
909
+ const key = match[1]?.toLowerCase();
910
+ if (key && match[2]) params[key] = match[2];
911
+ }
912
+ return params;
913
+ }, "parseAuthParams");
914
+ var parseDidAuthChallenge = /* @__PURE__ */ __name((response, bodyText, expectedDomain) => {
915
+ const header = response.headers.get("www-authenticate") ?? "";
916
+ const scheme = header.trim().split(/\s/, 1)[0]?.toLowerCase();
917
+ if (scheme !== DID_AUTH_SCHEME) return void 0;
918
+ const headerParams = parseAuthParams(header);
919
+ let challenge = headerParams.challenge;
920
+ let domain = headerParams.domain;
921
+ let expiresAt = Date.now() + HEADER_CHALLENGE_TTL_MS;
922
+ if (bodyText) {
923
+ let body;
924
+ try {
925
+ body = JSON.parse(bodyText);
926
+ } catch {
927
+ return void 0;
928
+ }
929
+ const parsed = CredentialRefreshChallengeValidator.safeParse(body);
930
+ if (!parsed.success) return void 0;
931
+ if (challenge && parsed.data.challenge !== challenge) return void 0;
932
+ if (domain && parsed.data.domain && parsed.data.domain !== domain) return void 0;
933
+ challenge = parsed.data.challenge;
934
+ domain = parsed.data.domain ?? domain;
935
+ expiresAt = Date.parse(parsed.data.expiresAt);
936
+ if (Number.isNaN(expiresAt) || expiresAt <= Date.now()) return void 0;
937
+ }
938
+ if (!challenge || domain && domain !== expectedDomain) return void 0;
939
+ return { challenge, domain: expectedDomain, expiresAt };
940
+ }, "parseDidAuthChallenge");
941
+ var asManagedVersion = /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0, "asManagedVersion");
942
+ var decodeCandidate = /* @__PURE__ */ __name(async (learnCard, json, response) => {
943
+ const headerEtag = response.headers.get("etag") ?? void 0;
944
+ const decryptJwe = /* @__PURE__ */ __name(async (jwe) => {
945
+ const decrypt = learnCard.invoke.decryptDagJwe;
946
+ if (typeof decrypt !== "function") return { failure: failed("UNSUPPORTED_SERVICE", false) };
947
+ let decrypted;
948
+ try {
949
+ decrypted = await decrypt(jwe);
950
+ } catch {
951
+ return { failure: malformedResponse() };
952
+ }
953
+ const parsed = VCValidator.safeParse(decrypted);
954
+ if (!parsed.success) return { failure: malformedResponse() };
955
+ return { credential: parsed.data };
956
+ }, "decryptJwe");
957
+ const envelopeEtag = /* @__PURE__ */ __name((value) => typeof value === "string" ? value : headerEtag, "envelopeEtag");
958
+ if (json !== null && typeof json === "object" && "format" in json) {
959
+ const envelope = json;
960
+ if (envelope.format === "jwe") {
961
+ const jwe = JWEValidator.safeParse(envelope.jwe);
962
+ if (!jwe.success) return malformedResponse();
963
+ const decrypted = await decryptJwe(jwe.data);
964
+ if ("failure" in decrypted) return decrypted.failure;
965
+ return {
966
+ credential: decrypted.credential,
967
+ etag: envelopeEtag(envelope.etag),
968
+ managedVersion: asManagedVersion(envelope.version)
969
+ };
970
+ }
971
+ if (envelope.format === "vc") {
972
+ const parsed = VCValidator.safeParse(envelope.credential);
973
+ if (!parsed.success) return malformedResponse();
974
+ return {
975
+ credential: parsed.data,
976
+ etag: envelopeEtag(envelope.etag),
977
+ managedVersion: asManagedVersion(envelope.version)
978
+ };
979
+ }
980
+ return malformedResponse();
981
+ }
982
+ const bareVc = VCValidator.safeParse(json);
983
+ if (bareVc.success) return { credential: bareVc.data, etag: headerEtag };
984
+ const bareJwe = JWEValidator.safeParse(json);
985
+ if (bareJwe.success) {
986
+ const decrypted = await decryptJwe(bareJwe.data);
987
+ if ("failure" in decrypted) return decrypted.failure;
988
+ return { credential: decrypted.credential, etag: headerEtag };
989
+ }
990
+ return malformedResponse();
991
+ }, "decodeCandidate");
992
+ var getHolderIds = /* @__PURE__ */ __name((vc) => {
993
+ const subject = vc.credentialSubject;
994
+ const subjects = Array.isArray(subject) ? subject : [subject];
995
+ return subjects.map((entry) => typeof entry?.id === "string" ? entry.id : "").sort();
996
+ }, "getHolderIds");
997
+ var holdersEqual = /* @__PURE__ */ __name((first, second) => {
998
+ const firstIds = getHolderIds(first);
999
+ const secondIds = getHolderIds(second);
1000
+ return firstIds.length === secondIds.length && firstIds.every((id, index) => id === secondIds[index]);
1001
+ }, "holdersEqual");
1002
+ var refreshCredential = /* @__PURE__ */ __name((_initLearnCard) => {
1003
+ const usedChallenges = /* @__PURE__ */ new Map();
1004
+ return async (_learnCard, credential, options = {}) => {
1005
+ const resolved = {
1006
+ etag: options.etag,
1007
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
1008
+ maxRedirects: options.maxRedirects ?? DEFAULT_MAX_REDIRECTS,
1009
+ maxResponseBytes: options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES,
1010
+ allowInsecureHttp: options.allowInsecureHttp ?? false,
1011
+ allowPrivateAddresses: options.allowPrivateAddresses ?? false,
1012
+ resolveHost: options.resolveHost ?? (isNodeRuntime2 ? resolveHostnameWithNodeDns : void 0)
1013
+ };
1014
+ const service = getSupportedRefreshService(credential);
1015
+ if (!service) return { status: "unsupported" };
1016
+ try {
1017
+ const check = await _learnCard.invoke.verifyCredential(credential);
1018
+ if (!proofVerified(check)) return invalidProof();
1019
+ } catch {
1020
+ return invalidProof();
1021
+ }
1022
+ if (service.type === "LearnCardCredentialRefresh2026" && (typeof credential.id !== "string" || credential.id.length === 0)) {
1023
+ return failed("ID_MISMATCH", false);
1024
+ }
1025
+ const validated = await validateRefreshUrl(service.id, resolved);
1026
+ if ("result" in validated) return validated.result;
1027
+ const initial = await fetchWithGuards(validated, resolved);
1028
+ if ("result" in initial) return initial.result;
1029
+ let guardedResponse = initial;
1030
+ let response = guardedResponse.response;
1031
+ if (response.status === 401 && service.type === "LearnCardCredentialRefresh2026") {
1032
+ if (guardedResponse.endpoint.url.origin !== validated.url.origin) {
1033
+ await discardGuardedResponse(guardedResponse);
1034
+ return unauthorized();
1035
+ }
1036
+ const challengeRead = await readBodyWithLimit(
1037
+ response,
1038
+ resolved.maxResponseBytes,
1039
+ guardedResponse.signal
1040
+ );
1041
+ guardedResponse.finish();
1042
+ if ("result" in challengeRead) {
1043
+ return challengeRead.reason === "timeout" ? challengeRead.result : unauthorized();
1044
+ }
1045
+ const expectedDomain = guardedResponse.endpoint.url.host;
1046
+ const challenge = parseDidAuthChallenge(response, challengeRead.body, expectedDomain);
1047
+ if (!challenge) return unauthorized();
1048
+ const challengeKey = `${guardedResponse.endpoint.url.origin}|${challenge.challenge}`;
1049
+ const now = Date.now();
1050
+ for (const [key, expiresAt] of usedChallenges) {
1051
+ if (expiresAt <= now) usedChallenges.delete(key);
1052
+ }
1053
+ if (usedChallenges.has(challengeKey)) return unauthorized();
1054
+ if (usedChallenges.size >= MAX_TRACKED_CHALLENGES) return unauthorized();
1055
+ usedChallenges.set(challengeKey, challenge.expiresAt);
1056
+ let vp;
1057
+ try {
1058
+ vp = await _learnCard.invoke.getDidAuthVp({
1059
+ proofFormat: "jwt",
1060
+ challenge: challenge.challenge,
1061
+ domain: expectedDomain
1062
+ });
1063
+ } catch {
1064
+ return unauthorized();
1065
+ }
1066
+ const retry = await fetchWithGuards(
1067
+ guardedResponse.endpoint,
1068
+ resolved,
1069
+ `Bearer ${typeof vp === "string" ? vp : JSON.stringify(vp)}`
1070
+ );
1071
+ if ("result" in retry) return retry.result;
1072
+ guardedResponse = retry;
1073
+ response = guardedResponse.response;
1074
+ }
1075
+ if (response.status === 304) {
1076
+ await discardGuardedResponse(guardedResponse);
1077
+ return {
1078
+ status: "unchanged",
1079
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1080
+ etag: response.headers.get("etag") ?? resolved.etag
1081
+ };
1082
+ }
1083
+ if (response.status === 410) {
1084
+ await discardGuardedResponse(guardedResponse);
1085
+ return failed("REVOKED", false);
1086
+ }
1087
+ if (response.status === 401 || response.status === 403) {
1088
+ await discardGuardedResponse(guardedResponse);
1089
+ return unauthorized();
1090
+ }
1091
+ if (response.status < 200 || response.status > 299) {
1092
+ await discardGuardedResponse(guardedResponse);
1093
+ return failed("UNAVAILABLE", response.status >= 500);
1094
+ }
1095
+ if (!isAcceptedMediaType(response.headers.get("content-type"))) {
1096
+ await discardGuardedResponse(guardedResponse);
1097
+ return malformedResponse();
1098
+ }
1099
+ const bodyRead = await readBodyWithLimit(
1100
+ response,
1101
+ resolved.maxResponseBytes,
1102
+ guardedResponse.signal
1103
+ );
1104
+ guardedResponse.finish();
1105
+ if ("result" in bodyRead) return bodyRead.result;
1106
+ let json;
1107
+ try {
1108
+ json = JSON.parse(bodyRead.body);
1109
+ } catch {
1110
+ return malformedResponse();
1111
+ }
1112
+ if (service.type === "1EdTechCredentialRefresh" && !VCValidator.safeParse(json).success) {
1113
+ return malformedResponse();
1114
+ }
1115
+ const decoded = await decodeCandidate(_learnCard, json, response);
1116
+ if ("status" in decoded) return decoded;
1117
+ const candidate = decoded.credential;
1118
+ try {
1119
+ const check = await _learnCard.invoke.verifyCredential(candidate);
1120
+ if (!proofVerified(check)) return invalidProof();
1121
+ } catch {
1122
+ return invalidProof();
1123
+ }
1124
+ if (candidate.id !== credential.id) {
1125
+ return failed("ID_MISMATCH", false);
1126
+ }
1127
+ if (getCredentialIssuerId(candidate) !== getCredentialIssuerId(credential)) {
1128
+ return failed("ISSUER_MISMATCH", false);
1129
+ }
1130
+ if (!holdersEqual(credential, candidate)) return failed("ID_MISMATCH", false);
1131
+ const currentTime = getCredentialEffectiveTime(credential);
1132
+ const candidateTime = getCredentialEffectiveTime(candidate);
1133
+ if (currentTime !== void 0 && candidateTime !== void 0 && candidateTime < currentTime) {
1134
+ return failed("ROLLBACK", false);
1135
+ }
1136
+ if (credentialContentsEqual(credential, candidate)) {
1137
+ return { status: "unchanged", checkedAt: (/* @__PURE__ */ new Date()).toISOString(), etag: decoded.etag };
1138
+ }
1139
+ return {
1140
+ status: "updated",
1141
+ credential: candidate,
1142
+ ...decoded.etag ? { etag: decoded.etag } : {},
1143
+ ...decoded.managedVersion ? { managedVersion: decoded.managedVersion } : {}
1144
+ };
1145
+ };
1146
+ }, "refreshCredential");
1147
+
516
1148
  // src/vc.ts
517
1149
  var getVCPlugin = /* @__PURE__ */ __name((learnCard) => {
518
1150
  return {
@@ -524,6 +1156,7 @@ var getVCPlugin = /* @__PURE__ */ __name((learnCard) => {
524
1156
  verifyCredential: verifyCredential(learnCard),
525
1157
  issuePresentation: issuePresentation(learnCard),
526
1158
  verifyPresentation: verifyPresentation(learnCard),
1159
+ refreshCredential: refreshCredential(learnCard),
527
1160
  getTestVc: /* @__PURE__ */ __name((_learnCard, subject = "did:example:d23dd687a7dc6787646f2eb98d0") => {
528
1161
  const did = _learnCard.id.did();
529
1162
  return {
@@ -561,6 +1194,7 @@ var getVCPlugin = /* @__PURE__ */ __name((learnCard) => {
561
1194
  };
562
1195
  }, "getVCPlugin");
563
1196
  export {
564
- getVCPlugin
1197
+ getVCPlugin,
1198
+ refreshCredential
565
1199
  };
566
1200
  //# sourceMappingURL=vc-plugin.esm.js.map