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