@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.
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/refreshCredential.d.ts +13 -0
- package/dist/refreshCredential.d.ts.map +1 -0
- package/dist/refreshCredential.fetch.d.ts +13 -0
- package/dist/refreshCredential.fetch.d.ts.map +1 -0
- package/dist/refreshCredential.fetch.test.d.ts +2 -0
- package/dist/refreshCredential.fetch.test.d.ts.map +1 -0
- package/dist/refreshCredential.test.d.ts +2 -0
- package/dist/refreshCredential.test.d.ts.map +1 -0
- package/dist/types.d.ts +32 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vc-plugin.cjs.development.cjs +626 -1
- package/dist/vc-plugin.cjs.development.cjs.map +4 -4
- package/dist/vc-plugin.cjs.production.min.cjs +1 -1
- package/dist/vc-plugin.cjs.production.min.cjs.map +4 -4
- package/dist/vc-plugin.esm.js +635 -1
- package/dist/vc-plugin.esm.js.map +4 -4
- package/dist/vc.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/index.ts +1 -0
- package/src/refreshCredential.fetch.test.ts +110 -0
- package/src/refreshCredential.fetch.ts +159 -0
- package/src/refreshCredential.test.ts +1144 -0
- package/src/refreshCredential.ts +931 -0
- package/src/types.ts +36 -0
- package/src/vc.ts +2 -0
package/dist/vc-plugin.esm.js
CHANGED
|
@@ -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
|