diff --git a/.eslintignore b/.eslintignore index 397404e40..02241ffd1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ **/data/ **/build/ **/dist/ - +java/ diff --git a/java/.gitignore b/java/.gitignore new file mode 100644 index 000000000..b3394fbbc --- /dev/null +++ b/java/.gitignore @@ -0,0 +1,19 @@ +.gradle/ +/build/ +**/build/ +**/out/ + +# IDEs +.idea/ +*.iml +*.ipr +*.iws +.vscode/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +.gradle-user/ diff --git a/java/README.md b/java/README.md new file mode 100644 index 000000000..f0ea0ca70 --- /dev/null +++ b/java/README.md @@ -0,0 +1,74 @@ +# Java Libraries + +This folder contains the Java implementations of Keymaster and the Gatekeeper REST client. + +Modules: +- `cid` — minimal CID validation. +- `crypto` — crypto primitives. +- `gatekeeper` — Gatekeeper REST client. +- `keymaster` — wallet + credential operations. + +Compatibility and runtime +- JDK 17 is required (targeted bytecode and test runtime). +- Dependencies are pure-Java artifacts (OkHttp, Jackson, Bouncy Castle, Tink, bitcoinj). +- Gatekeeper default base URL is `http://localhost:4224` (see `GatekeeperClientOptions`). + +Quickstart (Gradle) +```gradle +dependencies { + implementation("org.keychain:cid:") + implementation("org.keychain:crypto:") + implementation("org.keychain:gatekeeper:") + implementation("org.keychain:keymaster:") +} +``` + +Quickstart (Java) +```java +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.keychain.gatekeeper.GatekeeperClient; +import org.keychain.gatekeeper.GatekeeperClientOptions; +import org.keychain.keymaster.Keymaster; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.store.WalletJson; +import org.keychain.keymaster.store.WalletStore; + +Path dataDir = Paths.get(System.getProperty("user.home"), ".keymaster"); +WalletStore store = new WalletJson<>(WalletEncFile.class, dataDir, "wallet.json"); + +GatekeeperClientOptions options = new GatekeeperClientOptions(); +options.baseUrl = "http://localhost:4224"; +GatekeeperClient gatekeeper = new GatekeeperClient(options); + +Keymaster keymaster = new Keymaster(store, gatekeeper, "passphrase"); + +String aliceDid = keymaster.createId("Alice", "hyperswarm"); +System.out.println("DID: " + aliceDid); + +Map emailSchema = new HashMap<>(); +emailSchema.put("$schema", "http://json-schema.org/draft-07/schema#"); +Map properties = new HashMap<>(); +Map email = new HashMap<>(); +email.put("format", "email"); +email.put("type", "string"); +properties.put("email", email); +emailSchema.put("properties", properties); +emailSchema.put("required", List.of("email")); +emailSchema.put("type", "object"); + +String schemaDid = keymaster.createSchema(emailSchema, "hyperswarm"); +Map bound = keymaster.bindCredential(schemaDid, aliceDid); +String credentialDid = keymaster.issueCredential(bound); + +keymaster.publishCredential(credentialDid, true); +``` + +See each module README for usage details: +- `java/cid/README.md` +- `java/crypto/README.md` +- `java/gatekeeper/README.md` +- `java/keymaster/README.md` diff --git a/java/build.gradle b/java/build.gradle new file mode 100644 index 000000000..4c9b27e1c --- /dev/null +++ b/java/build.gradle @@ -0,0 +1,31 @@ +allprojects { + repositories { + mavenCentral() + maven { url = 'https://jitpack.io' } + } +} + +subprojects { + apply plugin: 'java-library' + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } + } + + tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release = 11 + } + + tasks.withType(Test).configureEach { + useJUnitPlatform() + } + + dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' + } +} diff --git a/java/cid/README.md b/java/cid/README.md new file mode 100644 index 000000000..02e1a27e7 --- /dev/null +++ b/java/cid/README.md @@ -0,0 +1,15 @@ +# CID (Java) + +Minimal CID validator used by Keymaster for DID checks. + +## Usage + +```java +import org.keychain.cid.Cid; + +boolean ok = Cid.isValid("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"); +``` + +Notes: +- Supports CIDv0 (base58btc) and CIDv1 (base32 lower-case). +- Intended for validation only (no full CID object model yet). diff --git a/java/cid/build.gradle b/java/cid/build.gradle new file mode 100644 index 000000000..7d82dc72f --- /dev/null +++ b/java/cid/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/java/cid/src/main/java/org/keychain/cid/Base32Lower.java b/java/cid/src/main/java/org/keychain/cid/Base32Lower.java new file mode 100644 index 000000000..223b5ab6b --- /dev/null +++ b/java/cid/src/main/java/org/keychain/cid/Base32Lower.java @@ -0,0 +1,56 @@ +package org.keychain.cid; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; + +public final class Base32Lower { + private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz234567"; + private static final int[] INDEXES = new int[128]; + + static { + Arrays.fill(INDEXES, -1); + for (int i = 0; i < ALPHABET.length(); i += 1) { + char c = ALPHABET.charAt(i); + INDEXES[c] = i; + INDEXES[Character.toUpperCase(c)] = i; + } + } + + private Base32Lower() { + } + + public static byte[] decode(String input) { + if (input == null || input.isEmpty()) { + throw new IllegalArgumentException("input"); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int buffer = 0; + int bitsLeft = 0; + + for (int i = 0; i < input.length(); i += 1) { + char c = input.charAt(i); + if (c == '=') { + continue; + } + if (c >= 128 || INDEXES[c] < 0) { + throw new IllegalArgumentException("input"); + } + + buffer = (buffer << 5) | INDEXES[c]; + bitsLeft += 5; + + while (bitsLeft >= 8) { + bitsLeft -= 8; + out.write((buffer >> bitsLeft) & 0xFF); + if (bitsLeft > 0) { + buffer &= (1 << bitsLeft) - 1; + } else { + buffer = 0; + } + } + } + + return out.toByteArray(); + } +} diff --git a/java/cid/src/main/java/org/keychain/cid/Base58Btc.java b/java/cid/src/main/java/org/keychain/cid/Base58Btc.java new file mode 100644 index 000000000..a415986cd --- /dev/null +++ b/java/cid/src/main/java/org/keychain/cid/Base58Btc.java @@ -0,0 +1,68 @@ +package org.keychain.cid; + +import java.util.Arrays; + +public final class Base58Btc { + private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + private static final int[] INDEXES = new int[128]; + + static { + Arrays.fill(INDEXES, -1); + for (int i = 0; i < ALPHABET.length(); i += 1) { + INDEXES[ALPHABET.charAt(i)] = i; + } + } + + private Base58Btc() { + } + + public static byte[] decode(String input) { + if (input == null || input.isEmpty()) { + throw new IllegalArgumentException("input"); + } + + int zeros = 0; + while (zeros < input.length() && input.charAt(zeros) == '1') { + zeros += 1; + } + + byte[] input58 = new byte[input.length()]; + for (int i = 0; i < input.length(); i += 1) { + char c = input.charAt(i); + if (c >= 128 || INDEXES[c] < 0) { + throw new IllegalArgumentException("input"); + } + input58[i] = (byte) INDEXES[c]; + } + + byte[] decoded = new byte[input.length()]; + int outputStart = decoded.length; + int inputStart = zeros; + while (inputStart < input58.length) { + int mod = divmod256(input58, inputStart); + if (input58[inputStart] == 0) { + inputStart += 1; + } + decoded[--outputStart] = (byte) mod; + } + + while (outputStart < decoded.length && decoded[outputStart] == 0) { + outputStart += 1; + } + + byte[] output = new byte[zeros + (decoded.length - outputStart)]; + System.arraycopy(decoded, outputStart, output, zeros, decoded.length - outputStart); + return output; + } + + private static int divmod256(byte[] number, int startAt) { + int remainder = 0; + for (int i = startAt; i < number.length; i += 1) { + int digit = number[i] & 0xFF; + int temp = remainder * 58 + digit; + number[i] = (byte) (temp / 256); + remainder = temp % 256; + } + return remainder; + } +} diff --git a/java/cid/src/main/java/org/keychain/cid/Cid.java b/java/cid/src/main/java/org/keychain/cid/Cid.java new file mode 100644 index 000000000..3fcb9911b --- /dev/null +++ b/java/cid/src/main/java/org/keychain/cid/Cid.java @@ -0,0 +1,64 @@ +package org.keychain.cid; + +public final class Cid { + private Cid() { + } + + public static boolean isValid(String cid) { + if (cid == null || cid.isBlank()) { + return false; + } + + try { + if (cid.startsWith("Q")) { + return validateV0(cid); + } + return validateV1(cid); + } catch (IllegalArgumentException e) { + return false; + } + } + + private static boolean validateV0(String cid) { + byte[] bytes = Base58Btc.decode(cid); + if (bytes.length != 34) { + return false; + } + return (bytes[0] == 0x12) && (bytes[1] == 0x20); + } + + private static boolean validateV1(String cid) { + byte[] bytes = Multibase.decode(cid); + if (bytes.length < 4) { + return false; + } + + Varint.Decoded version = Varint.decodeUnsigned(bytes, 0); + if (version.value != 1) { + return false; + } + + int offset = version.length; + Varint.Decoded codec = Varint.decodeUnsigned(bytes, offset); + offset += codec.length; + if (offset >= bytes.length) { + return false; + } + + Varint.Decoded multihashCode = Varint.decodeUnsigned(bytes, offset); + offset += multihashCode.length; + if (offset >= bytes.length) { + return false; + } + + Varint.Decoded digestLength = Varint.decodeUnsigned(bytes, offset); + offset += digestLength.length; + + if (digestLength.value < 0 || digestLength.value > Integer.MAX_VALUE) { + return false; + } + + int remaining = bytes.length - offset; + return remaining == (int) digestLength.value; + } +} diff --git a/java/cid/src/main/java/org/keychain/cid/Multibase.java b/java/cid/src/main/java/org/keychain/cid/Multibase.java new file mode 100644 index 000000000..9a2ee1070 --- /dev/null +++ b/java/cid/src/main/java/org/keychain/cid/Multibase.java @@ -0,0 +1,24 @@ +package org.keychain.cid; + +public final class Multibase { + private Multibase() { + } + + public static byte[] decode(String value) { + if (value == null || value.length() < 2) { + throw new IllegalArgumentException("value"); + } + + char prefix = value.charAt(0); + String payload = value.substring(1); + + if (prefix == 'z') { + return Base58Btc.decode(payload); + } + if (prefix == 'b') { + return Base32Lower.decode(payload); + } + + throw new IllegalArgumentException("value"); + } +} diff --git a/java/cid/src/main/java/org/keychain/cid/Varint.java b/java/cid/src/main/java/org/keychain/cid/Varint.java new file mode 100644 index 000000000..1c4deaf9d --- /dev/null +++ b/java/cid/src/main/java/org/keychain/cid/Varint.java @@ -0,0 +1,56 @@ +package org.keychain.cid; + +public final class Varint { + private Varint() { + } + + public static Decoded decodeUnsigned(byte[] input, int offset) { + if (input == null) { + throw new IllegalArgumentException("input"); + } + if (offset < 0 || offset >= input.length) { + throw new IllegalArgumentException("offset"); + } + + long value = 0; + int shift = 0; + + for (int i = 0; i < 10; i += 1) { + int index = offset + i; + if (index >= input.length) { + throw new IllegalArgumentException("varint overflow"); + } + + int b = input[index] & 0xFF; + long bits = b & 0x7FL; + if (bits != 0) { + if (shift >= 63) { + throw new IllegalArgumentException("varint overflow"); + } + long add = bits << shift; + if (add < 0 || value > Long.MAX_VALUE - add) { + throw new IllegalArgumentException("varint overflow"); + } + value += add; + } + + if ((b & 0x80) == 0) { + return new Decoded(value, i + 1); + } + + shift += 7; + } + + throw new IllegalArgumentException("varint overflow"); + } + + public static final class Decoded { + public final long value; + public final int length; + + private Decoded(long value, int length) { + this.value = value; + this.length = length; + } + } +} diff --git a/java/cid/src/test/java/org/keychain/cid/CidTest.java b/java/cid/src/test/java/org/keychain/cid/CidTest.java new file mode 100644 index 000000000..35d1891df --- /dev/null +++ b/java/cid/src/test/java/org/keychain/cid/CidTest.java @@ -0,0 +1,31 @@ +package org.keychain.cid; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class CidTest { + @Test + void validatesCidV0() { + String cid = "QmYwAPJzv5CZsnAzt8auV2V4ZZFZ5JYh5rS4Qh1zS4x2o7"; + assertTrue(Cid.isValid(cid)); + } + + @Test + void validatesCidV1() { + String cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"; + assertTrue(Cid.isValid(cid)); + } + + @Test + void rejectsInvalidCids() { + assertFalse(Cid.isValid(null)); + assertFalse(Cid.isValid("")); + assertFalse(Cid.isValid("Qm")); + assertFalse(Cid.isValid("b")); + assertFalse(Cid.isValid("QmYwAPJzv5CZsnAzt8auV2V4ZZFZ5JYh5rS4Qh1zS4x2oO")); + assertFalse(Cid.isValid("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzd!")); + assertFalse(Cid.isValid("cafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")); + } +} diff --git a/java/crypto/README.md b/java/crypto/README.md new file mode 100644 index 000000000..47644ee72 --- /dev/null +++ b/java/crypto/README.md @@ -0,0 +1,22 @@ +# Keymaster Crypto (Java) + +Core crypto utilities that mirror the JS Keymaster behavior. + +## Usage + +```java +import org.keychain.crypto.HdKey; +import org.keychain.crypto.JwkPair; +import org.keychain.crypto.KeymasterCrypto; +import org.keychain.crypto.KeymasterCryptoImpl; + +KeymasterCrypto crypto = new KeymasterCryptoImpl(); + +String mnemonic = crypto.generateMnemonic(); +HdKey hdKey = crypto.generateHdKey(mnemonic); + +JwkPair jwkPair = crypto.generateRandomJwk(); +String msgHash = crypto.hashMessage("hello"); +String sig = crypto.signHash(msgHash, jwkPair.privateJwk); +boolean ok = crypto.verifySig(msgHash, sig, jwkPair.publicJwk); +``` diff --git a/java/crypto/build.gradle b/java/crypto/build.gradle new file mode 100644 index 000000000..4cc45e77a --- /dev/null +++ b/java/crypto/build.gradle @@ -0,0 +1,7 @@ +dependencies { + api 'org.bouncycastle:bcprov-jdk15to18:1.78.1' + api 'com.google.crypto.tink:tink:1.12.0' + api 'org.bitcoinj:bitcoinj-core:0.16.3' + api 'com.github.erdtman:java-json-canonicalization:1.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.17.1' +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/CanonicalJson.java b/java/crypto/src/main/java/org/keychain/crypto/CanonicalJson.java new file mode 100644 index 000000000..b9dd07014 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/CanonicalJson.java @@ -0,0 +1,27 @@ +package org.keychain.crypto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.erdtman.jcs.JsonCanonicalizer; + +public final class CanonicalJson { + private static final ObjectMapper MAPPER = new ObjectMapper() + .setDefaultPropertyInclusion( + JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.ALWAYS) + ); + + private CanonicalJson() {} + + public static String canonicalize(Object obj) { + if (obj == null) { + throw new IllegalArgumentException("obj must not be null"); + } + + try { + String json = MAPPER.writeValueAsString(obj); + return new JsonCanonicalizer(json).getEncodedString(); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to canonicalize JSON", e); + } + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/EncryptedMnemonic.java b/java/crypto/src/main/java/org/keychain/crypto/EncryptedMnemonic.java new file mode 100644 index 000000000..6653f1400 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/EncryptedMnemonic.java @@ -0,0 +1,15 @@ +package org.keychain.crypto; + +public class EncryptedMnemonic { + public String salt; + public String iv; + public String data; + + public EncryptedMnemonic() {} + + public EncryptedMnemonic(String salt, String iv, String data) { + this.salt = salt; + this.iv = iv; + this.data = data; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/Hashing.java b/java/crypto/src/main/java/org/keychain/crypto/Hashing.java new file mode 100644 index 000000000..b4f62dccc --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/Hashing.java @@ -0,0 +1,29 @@ +package org.keychain.crypto; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.keychain.crypto.util.Hex; + +public final class Hashing { + private Hashing() {} + + public static String sha256Hex(String msg) { + return sha256Hex(msg.getBytes(StandardCharsets.UTF_8)); + } + + public static String sha256Hex(byte[] data) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return Hex.encode(digest.digest(data)); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } + + public static String hashCanonicalJson(Object obj) { + String canonical = CanonicalJson.canonicalize(obj); + return sha256Hex(canonical.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/HdKey.java b/java/crypto/src/main/java/org/keychain/crypto/HdKey.java new file mode 100644 index 000000000..f73b9cb21 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/HdKey.java @@ -0,0 +1,11 @@ +package org.keychain.crypto; + +public class HdKey { + public final String xpriv; + public final String xpub; + + public HdKey(String xpriv, String xpub) { + this.xpriv = xpriv; + this.xpub = xpub; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/HdKeyJson.java b/java/crypto/src/main/java/org/keychain/crypto/HdKeyJson.java new file mode 100644 index 000000000..041082392 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/HdKeyJson.java @@ -0,0 +1,26 @@ +package org.keychain.crypto; + +public class HdKeyJson { + public final String xpriv; + public final String xpub; + public final String chainCode; + public final Integer depth; + public final Integer index; + public final Integer parentFingerprint; + + public HdKeyJson( + String xpriv, + String xpub, + String chainCode, + Integer depth, + Integer index, + Integer parentFingerprint + ) { + this.xpriv = xpriv; + this.xpub = xpub; + this.chainCode = chainCode; + this.depth = depth; + this.index = index; + this.parentFingerprint = parentFingerprint; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/HdKeyUtil.java b/java/crypto/src/main/java/org/keychain/crypto/HdKeyUtil.java new file mode 100644 index 000000000..24d2f81cb --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/HdKeyUtil.java @@ -0,0 +1,59 @@ +package org.keychain.crypto; + +import java.util.Arrays; +import java.util.List; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.HDKeyDerivation; +import org.bitcoinj.crypto.MnemonicCode; +import org.bitcoinj.params.MainNetParams; + +public final class HdKeyUtil { + private HdKeyUtil() {} + + public static DeterministicKey masterFromMnemonic(String mnemonic) { + List words = Arrays.asList(mnemonic.trim().split("\\s+")); + byte[] seed = MnemonicCode.toSeed(words, ""); + return HDKeyDerivation.createMasterPrivateKey(seed); + } + + public static DeterministicKey fromXpriv(String xpriv) { + if (xpriv == null || xpriv.isBlank()) { + throw new IllegalArgumentException("xpriv is required"); + } + return DeterministicKey.deserializeB58(xpriv, MainNetParams.get()); + } + + public static DeterministicKey derivePath(DeterministicKey master, int account, int index) { + if (account < 0 || index < 0) { + throw new IllegalArgumentException("account and index must be >= 0"); + } + + DeterministicKey key = master; + key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(44, true)); + key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(0, true)); + key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(account, true)); + key = HDKeyDerivation.deriveChildKey(key, ChildNumber.ZERO); + key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(index, false)); + return key; + } + + public static byte[] privateKeyBytes(DeterministicKey key) { + byte[] keyBytes = key.getPrivKeyBytes(); + if (keyBytes.length == 32) { + return keyBytes; + } + if (keyBytes.length > 32) { + return Arrays.copyOfRange(keyBytes, keyBytes.length - 32, keyBytes.length); + } + byte[] padded = new byte[32]; + System.arraycopy(keyBytes, 0, padded, 32 - keyBytes.length, keyBytes.length); + return padded; + } + + public static HdKey toHdKey(DeterministicKey master) { + String xpriv = master.serializePrivB58(MainNetParams.get()); + String xpub = master.serializePubB58(MainNetParams.get()); + return new HdKey(xpriv, xpub); + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/JwkPair.java b/java/crypto/src/main/java/org/keychain/crypto/JwkPair.java new file mode 100644 index 000000000..fd43ab5a9 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/JwkPair.java @@ -0,0 +1,11 @@ +package org.keychain.crypto; + +public class JwkPair { + public final JwkPublic publicJwk; + public final JwkPrivate privateJwk; + + public JwkPair(JwkPublic publicJwk, JwkPrivate privateJwk) { + this.publicJwk = publicJwk; + this.privateJwk = privateJwk; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/JwkPrivate.java b/java/crypto/src/main/java/org/keychain/crypto/JwkPrivate.java new file mode 100644 index 000000000..9bfa25198 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/JwkPrivate.java @@ -0,0 +1,10 @@ +package org.keychain.crypto; + +public class JwkPrivate extends JwkPublic { + public final String d; + + public JwkPrivate(String kty, String crv, String x, String y, String d) { + super(kty, crv, x, y); + this.d = d; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/JwkPublic.java b/java/crypto/src/main/java/org/keychain/crypto/JwkPublic.java new file mode 100644 index 000000000..16ffbb115 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/JwkPublic.java @@ -0,0 +1,15 @@ +package org.keychain.crypto; + +public class JwkPublic { + public final String kty; + public final String crv; + public final String x; + public final String y; + + public JwkPublic(String kty, String crv, String x, String y) { + this.kty = kty; + this.crv = crv; + this.x = x; + this.y = y; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/KeymasterCrypto.java b/java/crypto/src/main/java/org/keychain/crypto/KeymasterCrypto.java new file mode 100644 index 000000000..4c0a7850e --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/KeymasterCrypto.java @@ -0,0 +1,27 @@ +package org.keychain.crypto; + +public interface KeymasterCrypto { + String generateMnemonic(); + + HdKey generateHdKey(String mnemonic); + HdKey generateHdKeyJson(HdKeyJson json); + + JwkPair generateJwk(byte[] privateKeyBytes); + JwkPair generateRandomJwk(); + byte[] convertJwkToCompressedBytes(JwkPublic jwk); + + String hashMessage(String msg); + String hashMessage(byte[] bytes); + String hashJson(Object obj); + + String signHash(String msgHashHex, JwkPrivate privateJwk); + boolean verifySig(String msgHashHex, String sigCompactHex, JwkPublic publicJwk); + + String encryptBytes(JwkPublic pubKey, JwkPrivate privKey, byte[] data); + byte[] decryptBytes(JwkPublic pubKey, JwkPrivate privKey, String ciphertextB64Url); + + String encryptMessage(JwkPublic pubKey, JwkPrivate privKey, String message); + String decryptMessage(JwkPublic pubKey, JwkPrivate privKey, String ciphertextB64Url); + + String generateRandomSalt(); +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/KeymasterCryptoImpl.java b/java/crypto/src/main/java/org/keychain/crypto/KeymasterCryptoImpl.java new file mode 100644 index 000000000..c863f69ea --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/KeymasterCryptoImpl.java @@ -0,0 +1,92 @@ +package org.keychain.crypto; + +import java.nio.charset.StandardCharsets; +import org.keychain.crypto.util.Base64Url; +import org.keychain.crypto.util.Bytes; + +public class KeymasterCryptoImpl implements KeymasterCrypto { + @Override + public String generateMnemonic() { + return MnemonicUtil.generateMnemonic(); + } + + @Override + public HdKey generateHdKey(String mnemonic) { + return HdKeyUtil.toHdKey(HdKeyUtil.masterFromMnemonic(mnemonic)); + } + + @Override + public HdKey generateHdKeyJson(HdKeyJson json) { + throw new UnsupportedOperationException("generateHdKeyJson not implemented yet"); + } + + @Override + public JwkPair generateJwk(byte[] privateKeyBytes) { + return Secp256k1Jwk.fromPrivateKey(privateKeyBytes); + } + + @Override + public JwkPair generateRandomJwk() { + byte[] priv = Bytes.random(32); + return Secp256k1Jwk.fromPrivateKey(priv); + } + + @Override + public byte[] convertJwkToCompressedBytes(JwkPublic jwk) { + return Secp256k1Jwk.toCompressed(jwk); + } + + @Override + public String hashMessage(String msg) { + return Hashing.sha256Hex(msg); + } + + @Override + public String hashMessage(byte[] bytes) { + return Hashing.sha256Hex(bytes); + } + + @Override + public String hashJson(Object obj) { + return Hashing.hashCanonicalJson(obj); + } + + @Override + public String signHash(String msgHashHex, JwkPrivate privateJwk) { + return Secp256k1Sign.signHash(msgHashHex, privateJwk); + } + + @Override + public boolean verifySig(String msgHashHex, String sigCompactHex, JwkPublic publicJwk) { + return Secp256k1Sign.verifySig(msgHashHex, sigCompactHex, publicJwk); + } + + @Override + public String encryptBytes(JwkPublic pubKey, JwkPrivate privKey, byte[] data) { + byte[] shared = Secp256k1Ecdh.sharedSecretCompressed(pubKey, privKey); + byte[] key32 = Secp256k1Ecdh.deriveKey32(shared); + return XChaCha20Util.encrypt(key32, data); + } + + @Override + public byte[] decryptBytes(JwkPublic pubKey, JwkPrivate privKey, String ciphertextB64Url) { + byte[] shared = Secp256k1Ecdh.sharedSecretCompressed(pubKey, privKey); + byte[] key32 = Secp256k1Ecdh.deriveKey32(shared); + return XChaCha20Util.decrypt(key32, ciphertextB64Url); + } + + @Override + public String encryptMessage(JwkPublic pubKey, JwkPrivate privKey, String message) { + return encryptBytes(pubKey, privKey, message.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String decryptMessage(JwkPublic pubKey, JwkPrivate privKey, String ciphertextB64Url) { + return new String(decryptBytes(pubKey, privKey, ciphertextB64Url), StandardCharsets.UTF_8); + } + + @Override + public String generateRandomSalt() { + return Base64Url.encode(Bytes.random(32)); + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/MnemonicEncryption.java b/java/crypto/src/main/java/org/keychain/crypto/MnemonicEncryption.java new file mode 100644 index 000000000..4d20c142b --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/MnemonicEncryption.java @@ -0,0 +1,87 @@ +package org.keychain.crypto; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public final class MnemonicEncryption { + private static final String ENC_ALG = "AES/GCM/NoPadding"; + private static final String ENC_KDF = "PBKDF2WithHmacSHA512"; + private static final int ENC_ITER = 100_000; + private static final int IV_LEN = 12; + private static final int SALT_LEN = 16; + private static final int KEY_LEN = 256; + + private static final SecureRandom RNG = new SecureRandom(); + + private MnemonicEncryption() {} + + public static EncryptedMnemonic encrypt(String mnemonic, String passphrase) { + byte[] salt = new byte[SALT_LEN]; + byte[] iv = new byte[IV_LEN]; + RNG.nextBytes(salt); + RNG.nextBytes(iv); + return encrypt(mnemonic, passphrase, salt, iv); + } + + public static EncryptedMnemonic encrypt(String mnemonic, String passphrase, byte[] salt, byte[] iv) { + if (mnemonic == null || passphrase == null) { + throw new IllegalArgumentException("mnemonic and passphrase are required"); + } + if (salt == null || salt.length != SALT_LEN) { + throw new IllegalArgumentException("salt must be 16 bytes"); + } + if (iv == null || iv.length != IV_LEN) { + throw new IllegalArgumentException("iv must be 12 bytes"); + } + + try { + SecretKey key = deriveKey(passphrase, salt); + Cipher cipher = Cipher.getInstance(ENC_ALG); + cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); + byte[] ct = cipher.doFinal(mnemonic.getBytes(StandardCharsets.UTF_8)); + + return new EncryptedMnemonic( + Base64.getEncoder().encodeToString(salt), + Base64.getEncoder().encodeToString(iv), + Base64.getEncoder().encodeToString(ct) + ); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Mnemonic encryption failed", e); + } + } + + public static String decrypt(EncryptedMnemonic blob, String passphrase) { + if (blob == null || passphrase == null) { + throw new IllegalArgumentException("blob and passphrase are required"); + } + + try { + byte[] salt = Base64.getDecoder().decode(blob.salt); + byte[] iv = Base64.getDecoder().decode(blob.iv); + byte[] data = Base64.getDecoder().decode(blob.data); + + SecretKey key = deriveKey(passphrase, salt); + Cipher cipher = Cipher.getInstance(ENC_ALG); + cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + byte[] pt = cipher.doFinal(data); + return new String(pt, StandardCharsets.UTF_8); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Mnemonic decryption failed", e); + } + } + + private static SecretKey deriveKey(String passphrase, byte[] salt) throws GeneralSecurityException { + PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, ENC_ITER, KEY_LEN); + SecretKeyFactory factory = SecretKeyFactory.getInstance(ENC_KDF); + byte[] keyBytes = factory.generateSecret(spec).getEncoded(); + return new SecretKeySpec(keyBytes, "AES"); + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/MnemonicUtil.java b/java/crypto/src/main/java/org/keychain/crypto/MnemonicUtil.java new file mode 100644 index 000000000..1b3d84d69 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/MnemonicUtil.java @@ -0,0 +1,39 @@ +package org.keychain.crypto; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.List; +import org.bitcoinj.crypto.MnemonicCode; +import org.bitcoinj.crypto.MnemonicException; + +public final class MnemonicUtil { + private static final SecureRandom RNG = new SecureRandom(); + + private MnemonicUtil() {} + + public static String generateMnemonic() { + byte[] entropy = new byte[16]; + RNG.nextBytes(entropy); + + try { + List words = MnemonicCode.INSTANCE.toMnemonic(entropy); + return String.join(" ", words); + } catch (MnemonicException e) { + throw new IllegalStateException("Unable to generate mnemonic", e); + } + } + + public static boolean validateMnemonic(String mnemonic) { + if (mnemonic == null || mnemonic.trim().isEmpty()) { + return false; + } + + try { + List words = Arrays.asList(mnemonic.trim().split("\\s+")); + MnemonicCode.INSTANCE.check(words); + return true; + } catch (IllegalArgumentException | MnemonicException e) { + return false; + } + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Ecdh.java b/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Ecdh.java new file mode 100644 index 000000000..f19cbb527 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Ecdh.java @@ -0,0 +1,43 @@ +package org.keychain.crypto; + +import java.math.BigInteger; +import java.util.Arrays; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.math.ec.ECPoint; +import org.keychain.crypto.util.Base64Url; + +public final class Secp256k1Ecdh { + private static final String CURVE = "secp256k1"; + private static final ECDomainParameters DOMAIN; + + static { + var params = SECNamedCurves.getByName(CURVE); + DOMAIN = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + } + + private Secp256k1Ecdh() {} + + public static byte[] sharedSecretCompressed(JwkPublic pubKey, JwkPrivate privKey) { + if (pubKey == null || privKey == null) { + throw new IllegalArgumentException("pubKey and privKey are required"); + } + + BigInteger d = new BigInteger(1, Base64Url.decode(privKey.d)); + ECPoint q = DOMAIN.getCurve().createPoint( + new BigInteger(1, Base64Url.decode(pubKey.x)), + new BigInteger(1, Base64Url.decode(pubKey.y)) + ); + + ECPoint shared = q.multiply(d).normalize(); + return shared.getEncoded(true); + } + + public static byte[] deriveKey32(byte[] sharedCompressed) { + if (sharedCompressed == null || sharedCompressed.length != 33) { + throw new IllegalArgumentException("sharedCompressed must be 33 bytes"); + } + + return Arrays.copyOfRange(sharedCompressed, 0, 32); + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Jwk.java b/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Jwk.java new file mode 100644 index 000000000..7c24c2a63 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Jwk.java @@ -0,0 +1,54 @@ +package org.keychain.crypto; + +import java.math.BigInteger; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.math.ec.ECPoint; +import org.keychain.crypto.util.Base64Url; + +public final class Secp256k1Jwk { + private static final String KTY = "EC"; + private static final String CRV = "secp256k1"; + + private Secp256k1Jwk() {} + + public static JwkPair fromPrivateKey(byte[] privateKeyBytes) { + if (privateKeyBytes == null || privateKeyBytes.length != 32) { + throw new IllegalArgumentException("privateKeyBytes must be 32 bytes"); + } + + BigInteger d = new BigInteger(1, privateKeyBytes); + ECPoint q = SECNamedCurves.getByName(CRV).getG().multiply(d).normalize(); + byte[] uncompressed = q.getEncoded(false); + + byte[] x = new byte[32]; + byte[] y = new byte[32]; + System.arraycopy(uncompressed, 1, x, 0, 32); + System.arraycopy(uncompressed, 33, y, 0, 32); + + String xB64 = Base64Url.encode(x); + String yB64 = Base64Url.encode(y); + String dB64 = Base64Url.encode(privateKeyBytes); + + JwkPublic pub = new JwkPublic(KTY, CRV, xB64, yB64); + JwkPrivate priv = new JwkPrivate(KTY, CRV, xB64, yB64, dB64); + return new JwkPair(pub, priv); + } + + public static byte[] toCompressed(JwkPublic jwk) { + if (jwk == null) { + throw new IllegalArgumentException("jwk must not be null"); + } + + byte[] x = Base64Url.decode(jwk.x); + byte[] y = Base64Url.decode(jwk.y); + if (x.length != 32 || y.length != 32) { + throw new IllegalArgumentException("x and y must be 32 bytes"); + } + + byte prefix = (byte) ((y[y.length - 1] & 1) == 0 ? 0x02 : 0x03); + byte[] out = new byte[33]; + out[0] = prefix; + System.arraycopy(x, 0, out, 1, 32); + return out; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Sign.java b/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Sign.java new file mode 100644 index 000000000..389c387ba --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/Secp256k1Sign.java @@ -0,0 +1,98 @@ +package org.keychain.crypto; + +import java.math.BigInteger; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.math.ec.ECPoint; +import org.keychain.crypto.util.Base64Url; +import org.keychain.crypto.util.Hex; + +public final class Secp256k1Sign { + private static final String CURVE = "secp256k1"; + private static final ECDomainParameters DOMAIN; + + static { + var params = SECNamedCurves.getByName(CURVE); + DOMAIN = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + } + + private Secp256k1Sign() {} + + public static String signHash(String msgHashHex, JwkPrivate privateJwk) { + if (msgHashHex == null || privateJwk == null) { + throw new IllegalArgumentException("msgHashHex and privateJwk are required"); + } + + byte[] msg = Hex.decode(msgHashHex); + byte[] privBytes = Base64Url.decode(privateJwk.d); + BigInteger d = new BigInteger(1, privBytes); + + ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); + signer.init(true, new ECPrivateKeyParameters(d, DOMAIN)); + BigInteger[] rs = signer.generateSignature(msg); + + BigInteger r = rs[0]; + BigInteger s = rs[1]; + BigInteger n = DOMAIN.getN(); + BigInteger halfN = n.shiftRight(1); + if (s.compareTo(halfN) > 0) { + s = n.subtract(s); + } + + byte[] rBytes = toFixedLength(r); + byte[] sBytes = toFixedLength(s); + byte[] sig = new byte[64]; + System.arraycopy(rBytes, 0, sig, 0, 32); + System.arraycopy(sBytes, 0, sig, 32, 32); + return Hex.encode(sig); + } + + public static boolean verifySig(String msgHashHex, String sigCompactHex, JwkPublic publicJwk) { + if (msgHashHex == null || sigCompactHex == null || publicJwk == null) { + return false; + } + + byte[] msg = Hex.decode(msgHashHex); + byte[] sig = Hex.decode(sigCompactHex); + if (sig.length != 64) { + return false; + } + + byte[] rBytes = new byte[32]; + byte[] sBytes = new byte[32]; + System.arraycopy(sig, 0, rBytes, 0, 32); + System.arraycopy(sig, 32, sBytes, 0, 32); + + BigInteger r = new BigInteger(1, rBytes); + BigInteger s = new BigInteger(1, sBytes); + + ECPoint q = DOMAIN.getCurve().createPoint( + new BigInteger(1, Base64Url.decode(publicJwk.x)), + new BigInteger(1, Base64Url.decode(publicJwk.y)) + ); + + ECDSASigner verifier = new ECDSASigner(); + verifier.init(false, new ECPublicKeyParameters(q, DOMAIN)); + return verifier.verifySignature(msg, r, s); + } + + private static byte[] toFixedLength(BigInteger value) { + int length = 32; + byte[] raw = value.toByteArray(); + if (raw.length == length) { + return raw; + } + byte[] out = new byte[length]; + if (raw.length > length) { + System.arraycopy(raw, raw.length - length, out, 0, length); + return out; + } + System.arraycopy(raw, 0, out, length - raw.length, raw.length); + return out; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/XChaCha20Util.java b/java/crypto/src/main/java/org/keychain/crypto/XChaCha20Util.java new file mode 100644 index 000000000..0fb60af85 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/XChaCha20Util.java @@ -0,0 +1,62 @@ +package org.keychain.crypto; + +import java.nio.charset.StandardCharsets; +import com.google.crypto.tink.aead.internal.InsecureNonceXChaCha20Poly1305; +import org.keychain.crypto.util.Base64Url; +import org.keychain.crypto.util.Bytes; +import java.util.function.Supplier; + +public final class XChaCha20Util { + private static final int NONCE_LENGTH = 24; + private static final Supplier NONCE_SUPPLIER = () -> Bytes.random(NONCE_LENGTH); + + private XChaCha20Util() {} + + public static String encrypt(byte[] key32, byte[] plaintext) { + if (key32 == null || key32.length != 32) { + throw new IllegalArgumentException("key32 must be 32 bytes"); + } + + try { + byte[] nonce = NONCE_SUPPLIER.get(); + InsecureNonceXChaCha20Poly1305 cipher = new InsecureNonceXChaCha20Poly1305(key32); + byte[] ciphertext = cipher.encrypt(nonce, plaintext, new byte[0]); + + byte[] out = Bytes.concat(nonce, ciphertext); + return Base64Url.encode(out); + } catch (Exception e) { + throw new IllegalStateException("Encryption failed", e); + } + } + + public static byte[] decrypt(byte[] key32, String nonceCiphertextB64Url) { + if (key32 == null || key32.length != 32) { + throw new IllegalArgumentException("key32 must be 32 bytes"); + } + + byte[] data = Base64Url.decode(nonceCiphertextB64Url); + if (data.length <= NONCE_LENGTH) { + throw new IllegalArgumentException("ciphertext too short"); + } + + byte[] nonce = new byte[NONCE_LENGTH]; + byte[] ciphertext = new byte[data.length - NONCE_LENGTH]; + System.arraycopy(data, 0, nonce, 0, NONCE_LENGTH); + System.arraycopy(data, NONCE_LENGTH, ciphertext, 0, ciphertext.length); + + try { + InsecureNonceXChaCha20Poly1305 cipher = new InsecureNonceXChaCha20Poly1305(key32); + return cipher.decrypt(nonce, ciphertext, new byte[0]); + } catch (Exception e) { + throw new IllegalStateException("Decryption failed", e); + } + } + + public static String encryptString(byte[] key32, String plaintext) { + return encrypt(key32, plaintext.getBytes(StandardCharsets.UTF_8)); + } + + public static String decryptToString(byte[] key32, String nonceCiphertextB64Url) { + return new String(decrypt(key32, nonceCiphertextB64Url), StandardCharsets.UTF_8); + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/util/Base64Url.java b/java/crypto/src/main/java/org/keychain/crypto/util/Base64Url.java new file mode 100644 index 000000000..3f34464a3 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/util/Base64Url.java @@ -0,0 +1,18 @@ +package org.keychain.crypto.util; + +import java.util.Base64; + +public final class Base64Url { + private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding(); + private static final Base64.Decoder DECODER = Base64.getUrlDecoder(); + + private Base64Url() {} + + public static String encode(byte[] data) { + return ENCODER.encodeToString(data); + } + + public static byte[] decode(String data) { + return DECODER.decode(data); + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/util/Bytes.java b/java/crypto/src/main/java/org/keychain/crypto/util/Bytes.java new file mode 100644 index 000000000..8b1fed926 --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/util/Bytes.java @@ -0,0 +1,22 @@ +package org.keychain.crypto.util; + +import java.security.SecureRandom; + +public final class Bytes { + private static final SecureRandom RNG = new SecureRandom(); + + private Bytes() {} + + public static byte[] random(int length) { + byte[] data = new byte[length]; + RNG.nextBytes(data); + return data; + } + + public static byte[] concat(byte[] left, byte[] right) { + byte[] out = new byte[left.length + right.length]; + System.arraycopy(left, 0, out, 0, left.length); + System.arraycopy(right, 0, out, left.length, right.length); + return out; + } +} diff --git a/java/crypto/src/main/java/org/keychain/crypto/util/Hex.java b/java/crypto/src/main/java/org/keychain/crypto/util/Hex.java new file mode 100644 index 000000000..be2d8df2a --- /dev/null +++ b/java/crypto/src/main/java/org/keychain/crypto/util/Hex.java @@ -0,0 +1,34 @@ +package org.keychain.crypto.util; + +public final class Hex { + private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); + + private Hex() {} + + public static String encode(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; i++) { + int v = bytes[i] & 0xFF; + hexChars[i * 2] = HEX_ARRAY[v >>> 4]; + hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] decode(String hex) { + if (hex.length() % 2 != 0) { + throw new IllegalArgumentException("Hex string must have even length"); + } + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + int hi = Character.digit(hex.charAt(i), 16); + int lo = Character.digit(hex.charAt(i + 1), 16); + if (hi == -1 || lo == -1) { + throw new IllegalArgumentException("Invalid hex character"); + } + data[i / 2] = (byte) ((hi << 4) + lo); + } + return data; + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/CryptoVectorsTest.java b/java/crypto/src/test/java/org/keychain/crypto/CryptoVectorsTest.java new file mode 100644 index 000000000..9bd0dc644 --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/CryptoVectorsTest.java @@ -0,0 +1,34 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import org.junit.jupiter.api.Test; + +class CryptoVectorsTest { + @Test + void vectorsMustBePopulated() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + try (InputStream input = getClass().getResourceAsStream("/vectors/crypto-v1.json")) { + assertNotNull(input, "crypto-v1.json should be present in test resources"); + JsonNode root = mapper.readTree(input); + JsonNode vectors = root.get("vectors"); + assertNotNull(vectors, "vectors object is required"); + + boolean populated = true; + populated &= !vectors.get("mnemonic").isNull(); + populated &= !vectors.get("hdKey").isNull(); + populated &= !vectors.get("jwk").isNull(); + populated &= !vectors.get("hash").isNull(); + populated &= !vectors.get("signature").isNull(); + populated &= !vectors.get("encrypt").isNull(); + populated &= !vectors.get("mnemonicEnc").isNull(); + populated &= !vectors.get("ecdh").isNull(); + + assertTrue(populated, "Populate crypto-v1.json with JS reference vectors before enabling tests"); + } + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/EcdhKeyDerivationTest.java b/java/crypto/src/test/java/org/keychain/crypto/EcdhKeyDerivationTest.java new file mode 100644 index 000000000..3a28393d2 --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/EcdhKeyDerivationTest.java @@ -0,0 +1,32 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import org.junit.jupiter.api.Test; +import org.keychain.crypto.util.Hex; + +class EcdhKeyDerivationTest { + @Test + void derivedKeyMatchesVector() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode vectors; + try (InputStream input = getClass().getResourceAsStream("/vectors/crypto-v1.json")) { + assertNotNull(input, "crypto-v1.json should be present in test resources"); + JsonNode root = mapper.readTree(input); + vectors = root.get("vectors"); + } + + JsonNode ecdhNode = vectors.get("ecdh"); + String sharedHex = ecdhNode.get("sharedSecretCompressedHex").asText(); + String expectedKeyHex = ecdhNode.get("key32Hex").asText(); + + byte[] shared = Hex.decode(sharedHex); + byte[] key = Secp256k1Ecdh.deriveKey32(shared); + + assertEquals(expectedKeyHex, Hex.encode(key)); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/EcdhVectorsTest.java b/java/crypto/src/test/java/org/keychain/crypto/EcdhVectorsTest.java new file mode 100644 index 000000000..2cd91efad --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/EcdhVectorsTest.java @@ -0,0 +1,53 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import org.junit.jupiter.api.Test; +import org.keychain.crypto.util.Hex; + +class EcdhVectorsTest { + @Test + void sharedSecretMatchesVector() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode vectors; + try (InputStream input = getClass().getResourceAsStream("/vectors/crypto-v1.json")) { + assertNotNull(input, "crypto-v1.json should be present in test resources"); + JsonNode root = mapper.readTree(input); + vectors = root.get("vectors"); + } + + JsonNode encryptNode = vectors.get("encrypt"); + JwkPrivate senderPrivate = toPrivate(encryptNode.get("sender").get("privateJwk")); + JwkPublic receiverPublic = toPublic(encryptNode.get("receiver").get("publicJwk")); + + byte[] shared = Secp256k1Ecdh.sharedSecretCompressed(receiverPublic, senderPrivate); + String sharedHex = Hex.encode(shared); + + JsonNode ecdhNode = vectors.get("ecdh"); + String expectedSharedHex = ecdhNode.get("sharedSecretCompressedHex").asText(); + assertEquals(expectedSharedHex, sharedHex); + } + + private static JwkPublic toPublic(JsonNode node) { + return new JwkPublic( + node.get("kty").asText(), + node.get("crv").asText(), + node.get("x").asText(), + node.get("y").asText() + ); + } + + private static JwkPrivate toPrivate(JsonNode node) { + return new JwkPrivate( + node.get("kty").asText(), + node.get("crv").asText(), + node.get("x").asText(), + node.get("y").asText(), + node.get("d").asText() + ); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/EncryptionVectorsTest.java b/java/crypto/src/test/java/org/keychain/crypto/EncryptionVectorsTest.java new file mode 100644 index 000000000..e5cdc3b36 --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/EncryptionVectorsTest.java @@ -0,0 +1,54 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import org.junit.jupiter.api.Test; + +class EncryptionVectorsTest { + @Test + void decryptsJsCiphertext() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode vectors; + try (InputStream input = getClass().getResourceAsStream("/vectors/crypto-v1.json")) { + assertNotNull(input, "crypto-v1.json should be present in test resources"); + JsonNode root = mapper.readTree(input); + vectors = root.get("vectors"); + } + + JsonNode encryptNode = vectors.get("encrypt"); + String ciphertext = encryptNode.get("ciphertext").asText(); + String plaintext = encryptNode.get("plaintext").asText(); + + JwkPrivate senderPrivate = toPrivate(encryptNode.get("sender").get("privateJwk")); + JwkPublic receiverPublic = toPublic(encryptNode.get("receiver").get("publicJwk")); + + byte[] shared = Secp256k1Ecdh.sharedSecretCompressed(receiverPublic, senderPrivate); + byte[] key32 = Secp256k1Ecdh.deriveKey32(shared); + + String decrypted = XChaCha20Util.decryptToString(key32, ciphertext); + assertEquals(plaintext, decrypted); + } + + private static JwkPublic toPublic(JsonNode node) { + return new JwkPublic( + node.get("kty").asText(), + node.get("crv").asText(), + node.get("x").asText(), + node.get("y").asText() + ); + } + + private static JwkPrivate toPrivate(JsonNode node) { + return new JwkPrivate( + node.get("kty").asText(), + node.get("crv").asText(), + node.get("x").asText(), + node.get("y").asText(), + node.get("d").asText() + ); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/HashingVectorsTest.java b/java/crypto/src/test/java/org/keychain/crypto/HashingVectorsTest.java new file mode 100644 index 000000000..4856c8cd2 --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/HashingVectorsTest.java @@ -0,0 +1,31 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import org.junit.jupiter.api.Test; + +class HashingVectorsTest { + @Test + void canonicalHashMatchesVector() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode vectors; + try (InputStream input = getClass().getResourceAsStream("/vectors/crypto-v1.json")) { + assertNotNull(input, "crypto-v1.json should be present in test resources"); + JsonNode root = mapper.readTree(input); + vectors = root.get("vectors"); + } + + JsonNode hashNode = vectors.get("hash"); + JsonNode inputNode = hashNode.get("input"); + String expectedHex = hashNode.get("hashHex").asText(); + + Object input = mapper.treeToValue(inputNode, Object.class); + String actualHex = Hashing.hashCanonicalJson(input); + + assertEquals(expectedHex, actualHex); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/HdKeyUtilTest.java b/java/crypto/src/test/java/org/keychain/crypto/HdKeyUtilTest.java new file mode 100644 index 000000000..74431ca37 --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/HdKeyUtilTest.java @@ -0,0 +1,19 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.bitcoinj.crypto.DeterministicKey; + +class HdKeyUtilTest { + @Test + void derivePathReturnsKey() { + String mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + DeterministicKey master = HdKeyUtil.masterFromMnemonic(mnemonic); + DeterministicKey child = HdKeyUtil.derivePath(master, 0, 0); + + assertNotNull(child); + assertEquals(32, HdKeyUtil.privateKeyBytes(child).length); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/KeyMaterialVectorsTest.java b/java/crypto/src/test/java/org/keychain/crypto/KeyMaterialVectorsTest.java new file mode 100644 index 000000000..5eee02a06 --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/KeyMaterialVectorsTest.java @@ -0,0 +1,51 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import org.bitcoinj.crypto.DeterministicKey; +import org.junit.jupiter.api.Test; + +class KeyMaterialVectorsTest { + @Test + void mnemonicHdKeyAndJwkMatchVectors() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode vectors; + try (InputStream input = getClass().getResourceAsStream("/vectors/crypto-v1.json")) { + assertNotNull(input, "crypto-v1.json should be present in test resources"); + JsonNode root = mapper.readTree(input); + vectors = root.get("vectors"); + } + + JsonNode mnemonicNode = vectors.get("mnemonic"); + String mnemonic = mnemonicNode.get("phrase").asText(); + assertNotNull(mnemonic); + + DeterministicKey master = HdKeyUtil.masterFromMnemonic(mnemonic); + HdKey hdKey = HdKeyUtil.toHdKey(master); + + JsonNode hdKeyNode = vectors.get("hdKey"); + assertEquals(hdKeyNode.get("xpriv").asText(), hdKey.xpriv); + assertEquals(hdKeyNode.get("xpub").asText(), hdKey.xpub); + + int account = hdKeyNode.get("account").asInt(); + int index = hdKeyNode.get("index").asInt(); + + DeterministicKey derived = HdKeyUtil.derivePath(master, account, index); + JwkPair jwkPair = Secp256k1Jwk.fromPrivateKey(HdKeyUtil.privateKeyBytes(derived)); + + JsonNode jwkNode = vectors.get("jwk"); + JsonNode publicNode = jwkNode.get("publicJwk"); + JsonNode privateNode = jwkNode.get("privateJwk"); + + assertEquals(publicNode.get("kty").asText(), jwkPair.publicJwk.kty); + assertEquals(publicNode.get("crv").asText(), jwkPair.publicJwk.crv); + assertEquals(publicNode.get("x").asText(), jwkPair.publicJwk.x); + assertEquals(publicNode.get("y").asText(), jwkPair.publicJwk.y); + + assertEquals(privateNode.get("d").asText(), jwkPair.privateJwk.d); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/MnemonicEncryptionTest.java b/java/crypto/src/test/java/org/keychain/crypto/MnemonicEncryptionTest.java new file mode 100644 index 000000000..d702dab14 --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/MnemonicEncryptionTest.java @@ -0,0 +1,46 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import org.junit.jupiter.api.Test; + +class MnemonicEncryptionTest { + @Test + void roundTripEncryptDecrypt() { + String mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + String passphrase = "passphrase"; + + EncryptedMnemonic enc = MnemonicEncryption.encrypt(mnemonic, passphrase); + String dec = MnemonicEncryption.decrypt(enc, passphrase); + + assertEquals(mnemonic, dec); + } + + @Test + void matchesVector() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode vectors; + try (InputStream input = getClass().getResourceAsStream("/vectors/crypto-v1.json")) { + assertNotNull(input, "crypto-v1.json should be present in test resources"); + JsonNode root = mapper.readTree(input); + vectors = root.get("vectors"); + } + + JsonNode node = vectors.get("mnemonicEnc"); + String mnemonic = node.get("mnemonic").asText(); + String passphrase = node.get("passphrase").asText(); + + EncryptedMnemonic enc = new EncryptedMnemonic( + node.get("salt").asText(), + node.get("iv").asText(), + node.get("data").asText() + ); + + String decrypted = MnemonicEncryption.decrypt(enc, passphrase); + assertEquals(mnemonic, decrypted); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/MnemonicUtilTest.java b/java/crypto/src/test/java/org/keychain/crypto/MnemonicUtilTest.java new file mode 100644 index 000000000..5ca88bfeb --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/MnemonicUtilTest.java @@ -0,0 +1,21 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MnemonicUtilTest { + @Test + void generatedMnemonicIsValid() { + String mnemonic = MnemonicUtil.generateMnemonic(); + assertTrue(MnemonicUtil.validateMnemonic(mnemonic)); + assertEquals(12, mnemonic.trim().split("\\s+").length); + } + + @Test + void invalidMnemonicReturnsFalse() { + assertFalse(MnemonicUtil.validateMnemonic("not a real mnemonic")); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/Secp256k1JwkTest.java b/java/crypto/src/test/java/org/keychain/crypto/Secp256k1JwkTest.java new file mode 100644 index 000000000..7f4938deb --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/Secp256k1JwkTest.java @@ -0,0 +1,37 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class Secp256k1JwkTest { + @Test + void jwkGenerationProducesExpectedFields() { + byte[] priv = new byte[32]; + priv[31] = 0x01; + JwkPair pair = Secp256k1Jwk.fromPrivateKey(priv); + + assertNotNull(pair.publicJwk); + assertNotNull(pair.privateJwk); + assertEquals("EC", pair.publicJwk.kty); + assertEquals("secp256k1", pair.publicJwk.crv); + assertEquals(pair.publicJwk.x, pair.privateJwk.x); + assertEquals(pair.publicJwk.y, pair.privateJwk.y); + assertEquals(43, pair.publicJwk.x.length()); + assertEquals(43, pair.publicJwk.y.length()); + assertEquals(43, pair.privateJwk.d.length()); + } + + @Test + void compressedKeyMatchesPrefix() { + byte[] priv = new byte[32]; + priv[31] = 0x01; + JwkPair pair = Secp256k1Jwk.fromPrivateKey(priv); + byte[] compressed = Secp256k1Jwk.toCompressed(pair.publicJwk); + + assertEquals(33, compressed.length); + assertTrue(compressed[0] == 0x02 || compressed[0] == 0x03); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/SignatureVectorsTest.java b/java/crypto/src/test/java/org/keychain/crypto/SignatureVectorsTest.java new file mode 100644 index 000000000..a1998e2e8 --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/SignatureVectorsTest.java @@ -0,0 +1,55 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import org.junit.jupiter.api.Test; + +class SignatureVectorsTest { + @Test + void signaturesMatchVector() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode vectors; + try (InputStream input = getClass().getResourceAsStream("/vectors/crypto-v1.json")) { + assertNotNull(input, "crypto-v1.json should be present in test resources"); + JsonNode root = mapper.readTree(input); + vectors = root.get("vectors"); + } + + JsonNode signatureNode = vectors.get("signature"); + String hashHex = signatureNode.get("hashHex").asText(); + String expectedSignature = signatureNode.get("signatureHex").asText(); + + JsonNode jwkNode = vectors.get("jwk"); + JwkPrivate privateJwk = toPrivate(jwkNode.get("privateJwk")); + JwkPublic publicJwk = toPublic(signatureNode.get("signerPublicJwk")); + + String actualSignature = Secp256k1Sign.signHash(hashHex, privateJwk); + assertEquals(expectedSignature, actualSignature); + + assertTrue(Secp256k1Sign.verifySig(hashHex, expectedSignature, publicJwk)); + } + + private static JwkPublic toPublic(JsonNode node) { + return new JwkPublic( + node.get("kty").asText(), + node.get("crv").asText(), + node.get("x").asText(), + node.get("y").asText() + ); + } + + private static JwkPrivate toPrivate(JsonNode node) { + return new JwkPrivate( + node.get("kty").asText(), + node.get("crv").asText(), + node.get("x").asText(), + node.get("y").asText(), + node.get("d").asText() + ); + } +} diff --git a/java/crypto/src/test/java/org/keychain/crypto/XChaCha20UtilTest.java b/java/crypto/src/test/java/org/keychain/crypto/XChaCha20UtilTest.java new file mode 100644 index 000000000..b8264f94b --- /dev/null +++ b/java/crypto/src/test/java/org/keychain/crypto/XChaCha20UtilTest.java @@ -0,0 +1,21 @@ +package org.keychain.crypto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.keychain.crypto.util.Hex; + +class XChaCha20UtilTest { + @Test + void encryptDecryptRoundTrip() { + byte[] key32 = Hex.decode("021510bc0b4faf0c8aba1a69c27c06bb6253d546a4e40176d4c948068116b66b"); + String plaintext = "hello keymaster"; + + String cipher = XChaCha20Util.encryptString(key32, plaintext); + String roundTrip = XChaCha20Util.decryptToString(key32, cipher); + + assertEquals(plaintext, roundTrip); + assertTrue(cipher.length() > plaintext.length()); + } +} diff --git a/java/crypto/src/test/resources/vectors/crypto-v1.json b/java/crypto/src/test/resources/vectors/crypto-v1.json new file mode 100644 index 000000000..10aaa745e --- /dev/null +++ b/java/crypto/src/test/resources/vectors/crypto-v1.json @@ -0,0 +1,267 @@ +{ + "description": "Keymaster crypto compatibility vectors (generated from JS reference)", + "version": 1, + "vectors": { + "mnemonic": { + "phrase": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + }, + "hdKey": { + "xpriv": "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu", + "xpub": "xpub661MyMwAqRbcFkPHucMnrGNzDwb6teAX1RbKQmqtEF8kK3Z7LZ59qafCjB9eCRLiTVG3uxBxgKvRgbubRhqSKXnGGb1aoaqLrpMBDrVxga8", + "account": 0, + "index": 0, + "path": "m/44'/0'/0'/0/0" + }, + "jwk": { + "publicJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + }, + "privateJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8", + "d": "4oQSnMCSJXmlNbv00aOyV3MJDSjJCbwP7XO14CIsw3I" + } + }, + "hash": { + "input": { + "foo": "bar", + "count": 1, + "list": [ + 1, + 2, + 3 + ] + }, + "hashHex": "6a7b70891da3e87ecb23014864d9e088abcd56c9e53eeabffeb7c6f99895a748" + }, + "signature": { + "hashHex": "6a7b70891da3e87ecb23014864d9e088abcd56c9e53eeabffeb7c6f99895a748", + "signatureHex": "842eee05749f9dc24a6f69d41ba5249b1932ff337a9115c9215698bee3bed740726602e4898fe67eb71b4c30852f5f799fc63ac2ef50df591e790386c83e7e97", + "signerPublicJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + } + }, + "encrypt": { + "plaintext": "hello keymaster", + "ciphertext": "Ir4CF9F76EVA-V7dsVGPZRJ5c6P1XHyWuQK-wUQpcBcEpFnEPSPCsJ_iMTmqx2l-uyAALzJO0A", + "sender": { + "publicJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + }, + "privateJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8", + "d": "4oQSnMCSJXmlNbv00aOyV3MJDSjJCbwP7XO14CIsw3I" + } + }, + "receiver": { + "publicJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "xgR_lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuU", + "y": "GuFo_qY9wzmjxYQZRmzq7vf2MmUyZtDhI2QxqVDP5So" + }, + "privateJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "xgR_lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuU", + "y": "GuFo_qY9wzmjxYQZRmzq7vf2MmUyZtDhI2QxqVDP5So", + "d": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI" + } + } + }, + "ecdh": { + "sharedSecretCompressedHex": "021510bc0b4faf0c8aba1a69c27c06bb6253d546a4e40176d4c948068116b66b3a", + "key32Hex": "021510bc0b4faf0c8aba1a69c27c06bb6253d546a4e40176d4c948068116b66b" + }, + "mnemonicEnc": { + "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "passphrase": "passphrase", + "salt": "AAECAwQFBgcICQoLDA0ODw==", + "iv": "EBESExQVFhcYGRob", + "data": "wx3fGlIor3vtRtYk0WPnbnEOHnM0agBEUugXfr1FoCrNenafh5H5xfsLXMmVUGBR38HreYHMF5yCmoBMAxwFxmIRsFBVQ0XbajAInzL7WF9UysoSsEstXtP7+M0U/ajMkXQuo0I2ZkrOlxtbIg==" + }, + "walletEnc": { + "version": 1, + "seed": { + "mnemonicEnc": { + "salt": "AAECAwQFBgcICQoLDA0ODw==", + "iv": "EBESExQVFhcYGRob", + "data": "wx3fGlIor3vtRtYk0WPnbnEOHnM0agBEUugXfr1FoCrNenafh5H5xfsLXMmVUGBR38HreYHMF5yCmoBMAxwFxmIRsFBVQ0XbajAInzL7WF9UysoSsEstXtP7+M0U/ajMkXQuo0I2ZkrOlxtbIg==" + } + }, + "enc": "NUKhOmTQbN4-BAUml2Jswqrb8tyDjWGXmNiT1SO874b1BiuzuUgcpgFL6wggPmSy1eBGsGbB3O6YoMfk4so-MEq4EbP80kqs8XmxbrUnoQURUPFWb2OFqf9B3ziVYGM06zxhAHarQXmPOfEJrJpS9KFz-QBjB7SvY-fGK8EEjXh0ny_w6N2XyOtD_txHY8uzXS9qF0s30AGl77i9JO7DxDPKFbHpyro1hyIizmlFEOOMFNhZKDbk00Ns5-Z5Sg6mtxFGi1Ha3eAk9y22FvfT2-rXrrgWWX5Cf6Fzja1Jkg", + "passphrase": "passphrase", + "wallet": { + "counter": 1, + "ids": { + "Alice": { + "did": "did:test:alice", + "account": 0, + "index": 0, + "held": [ + "did:test:cred1" + ], + "owned": [ + "did:test:asset1" + ] + } + }, + "current": "Alice", + "names": { + "alias": "did:test:alice" + } + } + } + }, + "operations": { + "createId": { + "registry": "Signet", + "blockid": "0000000000000000000000000000000000000000000000000000000000000001", + "created": "2024-01-02T03:04:05.000Z", + "signed": "2024-01-02T03:04:06.000Z", + "publicJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + }, + "privateJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8", + "d": "4oQSnMCSJXmlNbv00aOyV3MJDSjJCbwP7XO14CIsw3I" + }, + "signedOperation": { + "type": "create", + "created": "2024-01-02T03:04:05.000Z", + "blockid": "0000000000000000000000000000000000000000000000000000000000000001", + "mdip": { + "version": 1, + "type": "agent", + "registry": "Signet" + }, + "publicJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + }, + "signature": { + "signed": "2024-01-02T03:04:06.000Z", + "hash": "c0d7145e19ba28441bbdc7e79ae65eaeb5b024781f53b8dde7fced95f06b7655", + "value": "7e29f6a5e7945be7119e0f9613a972fd8d823e18c11d04c58f7cfe9c0f804a4c53aa00b08613bb0bf1d1260c600e7d300a94bd0057808a299b4175bf0329ff35" + } + } + }, + "updateDID": { + "did": "did:test:alice", + "previd": "prev123", + "blockid": "0000000000000000000000000000000000000000000000000000000000000001", + "signerDid": "did:test:alice", + "signed": "2024-01-02T03:04:07.000Z", + "doc": { + "didDocument": { + "id": "did:test:alice", + "controller": "did:test:alice", + "verificationMethod": [ + { + "id": "#key-1", + "controller": "did:test:alice", + "type": "EcdsaSecp256k1", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + } + } + ], + "authentication": [ + "#key-1" + ] + }, + "didDocumentMetadata": { + "created": "2024-01-01T00:00:00.000Z", + "updated": "2024-01-02T00:00:00.000Z", + "versionId": "v1", + "confirmed": true + }, + "didResolutionMetadata": { + "contentType": "application/did+ld+json" + }, + "didDocumentData": { + "foo": "bar" + }, + "mdip": { + "version": 1, + "type": "agent", + "registry": "Signet" + } + }, + "privateJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8", + "d": "4oQSnMCSJXmlNbv00aOyV3MJDSjJCbwP7XO14CIsw3I" + }, + "signedOperation": { + "type": "update", + "did": "did:test:alice", + "previd": "prev123", + "blockid": "0000000000000000000000000000000000000000000000000000000000000001", + "doc": { + "didDocument": { + "id": "did:test:alice", + "controller": "did:test:alice", + "verificationMethod": [ + { + "id": "#key-1", + "controller": "did:test:alice", + "type": "EcdsaSecp256k1", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + } + } + ], + "authentication": [ + "#key-1" + ] + }, + "didDocumentData": { + "foo": "bar" + }, + "mdip": { + "version": 1, + "type": "agent", + "registry": "Signet" + } + }, + "signature": { + "signer": "did:test:alice", + "signed": "2024-01-02T03:04:07.000Z", + "hash": "1c0e760fe15a1aa25644b927e28784c23a65e52d9dc00aa9b1fdfb6320c4341a", + "value": "4b2986479d4451c47a89835c611b4fdcfc5be9c9915d427c68b6e88614be935434845a2875d137007c5aa1ad90cae3c7384e220d470334904540de2415ddf8d1" + } + } + } + } +} \ No newline at end of file diff --git a/java/demo/build.gradle b/java/demo/build.gradle new file mode 100644 index 000000000..fd854b452 --- /dev/null +++ b/java/demo/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'org.springframework.boot' version '2.7.18' + id 'io.spring.dependency-management' version '1.1.6' + id 'java' +} + +group = 'org.keychain' +version = '0.0.1-SNAPSHOT' + +def vaadinVersion = '23.3.8' + +def bootJarTask = tasks.named('bootJar', org.springframework.boot.gradle.tasks.bundling.BootJar) + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':keymaster') + implementation project(':gatekeeper') + implementation project(':crypto') + implementation project(':cid') + + implementation "com.vaadin:vaadin-spring-boot-starter:${vaadinVersion}" + implementation 'org.yaml:snakeyaml' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +dependencyManagement { + imports { + mavenBom "com.vaadin:vaadin-bom:${vaadinVersion}" + } +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} + +bootJarTask.configure { + archiveBaseName = 'keymaster-demo' +} diff --git a/java/demo/data/.gitignore b/java/demo/data/.gitignore new file mode 100644 index 000000000..10ba553aa --- /dev/null +++ b/java/demo/data/.gitignore @@ -0,0 +1 @@ +wallet.json diff --git a/java/demo/frontend/generated/index.ts b/java/demo/frontend/generated/index.ts new file mode 100644 index 000000000..c20aa03f7 --- /dev/null +++ b/java/demo/frontend/generated/index.ts @@ -0,0 +1,32 @@ +/****************************************************************************** + * This file is auto-generated by Vaadin. + * If you want to customize the entry point, you can copy this file or create + * your own `index.ts` in your frontend directory. + * By default, the `index.ts` file should be in `./frontend/` folder. + * + * NOTE: + * - You need to restart the dev-server after adding the new `index.ts` file. + * After that, all modifications to `index.ts` are recompiled automatically. + * - `index.js` is also supported if you don't want to use TypeScript. + ******************************************************************************/ + +// import Vaadin client-router to handle client-side and server-side navigation +import { Router } from '@vaadin/router'; + +// import Flow module to enable navigation to Vaadin server-side views +import { Flow } from 'Frontend/generated/jar-resources/Flow.js'; + +const { serverSideRoutes } = new Flow({ + imports: () => import('../../target/frontend/generated-flow-imports.js') +}); + +const routes = [ + // for client-side, place routes below (more info https://vaadin.com/docs/v15/flow/typescript/creating-routes.html) + + // for server-side, the next magic line sends all unmatched routes: + ...serverSideRoutes // IMPORTANT: this must be the last entry in the array +]; + +// Vaadin router needs an outlet in the index.html page to display views +const router = new Router(document.querySelector('#outlet')); +router.setRoutes(routes); diff --git a/java/demo/frontend/generated/jar-resources/Flow.d.ts b/java/demo/frontend/generated/jar-resources/Flow.d.ts new file mode 100644 index 000000000..efbbbe845 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/Flow.d.ts @@ -0,0 +1,74 @@ +export interface FlowConfig { + imports?: () => void; +} +interface AppConfig { + productionMode: boolean; + appId: string; + uidl: any; + clientRouting: boolean; +} +interface AppInitResponse { + appConfig: AppConfig; + pushScript?: string; +} +interface Router { + render: (ctx: NavigationParameters, shouldUpdateHistory: boolean) => Promise; +} +interface HTMLRouterContainer extends HTMLElement { + onBeforeEnter?: (ctx: NavigationParameters, cmd: PreventAndRedirectCommands, router: Router) => void | Promise; + onBeforeLeave?: (ctx: NavigationParameters, cmd: PreventCommands, router: Router) => void | Promise; + serverConnected?: (cancel: boolean, url?: NavigationParameters) => void; +} +interface FlowRoute { + action: (params: NavigationParameters) => Promise; + path: string; +} +export interface NavigationParameters { + pathname: string; + search: string; +} +export interface PreventCommands { + prevent: () => any; +} +export interface PreventAndRedirectCommands extends PreventCommands { + redirect: (route: string) => any; +} +/** + * Client API for flow UI operations. + */ +export declare class Flow { + config: FlowConfig; + response?: AppInitResponse; + pathname: string; + container: HTMLRouterContainer; + private isActive; + private baseRegex; + private appShellTitle; + constructor(config?: FlowConfig); + /** + * Return a `route` object for vaadin-router in an one-element array. + * + * The `FlowRoute` object `path` property handles any route, + * and the `action` returns the flow container without updating the content, + * delaying the actual Flow server call to the `onBeforeEnter` phase. + * + * This is a specific API for its use with `vaadin-router`. + */ + get serverSideRoutes(): [FlowRoute]; + loadingStarted(): void; + loadingFinished(): void; + private get action(); + private flowLeave; + private flowNavigate; + private getFlowRoutePath; + private getFlowRouteQuery; + private flowInit; + private loadScript; + private injectAppIdScript; + private flowInitClient; + private flowInitUi; + private addConnectionIndicator; + private offlineStubAction; + private isFlowClientLoaded; +} +export {}; diff --git a/java/demo/frontend/generated/jar-resources/Flow.js b/java/demo/frontend/generated/jar-resources/Flow.js new file mode 100644 index 000000000..66ab4ae11 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/Flow.js @@ -0,0 +1,324 @@ +import { ConnectionIndicator, ConnectionState } from '@vaadin/common-frontend'; +class FlowUiInitializationError extends Error { +} +// flow uses body for keeping references +const flowRoot = window.document.body; +const $wnd = window; +/** + * Client API for flow UI operations. + */ +export class Flow { + constructor(config) { + this.response = undefined; + this.pathname = ''; + // flag used to inform Testbench whether a server route is in progress + this.isActive = false; + this.baseRegex = /^\//; + flowRoot.$ = flowRoot.$ || []; + this.config = config || {}; + // TB checks for the existence of window.Vaadin.Flow in order + // to consider that TB needs to wait for `initFlow()`. + $wnd.Vaadin = $wnd.Vaadin || {}; + $wnd.Vaadin.Flow = $wnd.Vaadin.Flow || {}; + $wnd.Vaadin.Flow.clients = { + TypeScript: { + isActive: () => this.isActive + } + }; + // Regular expression used to remove the app-context + const elm = document.head.querySelector('base'); + this.baseRegex = new RegExp(`^${ + // IE11 does not support document.baseURI + (document.baseURI || (elm && elm.href) || '/').replace(/^https?:\/\/[^/]+/i, '')}`); + this.appShellTitle = document.title; + // Put a vaadin-connection-indicator in the dom + this.addConnectionIndicator(); + } + /** + * Return a `route` object for vaadin-router in an one-element array. + * + * The `FlowRoute` object `path` property handles any route, + * and the `action` returns the flow container without updating the content, + * delaying the actual Flow server call to the `onBeforeEnter` phase. + * + * This is a specific API for its use with `vaadin-router`. + */ + get serverSideRoutes() { + return [ + { + path: '(.*)', + action: this.action + } + ]; + } + loadingStarted() { + // Make Testbench know that server request is in progress + this.isActive = true; + $wnd.Vaadin.connectionState.loadingStarted(); + } + loadingFinished() { + // Make Testbench know that server request has finished + this.isActive = false; + $wnd.Vaadin.connectionState.loadingFinished(); + } + get action() { + // Return a function which is bound to the flow instance, thus we can use + // the syntax `...serverSideRoutes` in vaadin-router. + return async (params) => { + // Store last action pathname so as we can check it in events + this.pathname = params.pathname; + if ($wnd.Vaadin.connectionState.online) { + try { + await this.flowInit(); + } + catch (error) { + if (error instanceof FlowUiInitializationError) { + // error initializing Flow: assume connection lost + $wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST; + return this.offlineStubAction(); + } + else { + throw error; + } + } + } + else { + // insert an offline stub + return this.offlineStubAction(); + } + // When an action happens, navigation will be resolved `onBeforeEnter` + this.container.onBeforeEnter = (ctx, cmd) => this.flowNavigate(ctx, cmd); + // For covering the 'server -> client' use case + this.container.onBeforeLeave = (ctx, cmd) => this.flowLeave(ctx, cmd); + return this.container; + }; + } + // Send a remote call to `JavaScriptBootstrapUI` to check + // whether navigation has to be cancelled. + async flowLeave(ctx, cmd) { + // server -> server, viewing offline stub, or browser is offline + const { connectionState } = $wnd.Vaadin; + if (this.pathname === ctx.pathname || !this.isFlowClientLoaded() || connectionState.offline) { + return Promise.resolve({}); + } + // 'server -> client' + return new Promise((resolve) => { + this.loadingStarted(); + // The callback to run from server side to cancel navigation + this.container.serverConnected = (cancel) => { + resolve(cmd && cancel ? cmd.prevent() : {}); + this.loadingFinished(); + }; + // Call server side to check whether we can leave the view + flowRoot.$server.leaveNavigation(this.getFlowRoutePath(ctx), this.getFlowRouteQuery(ctx)); + }); + } + // Send the remote call to `JavaScriptBootstrapUI` to render the flow + // route specified by the context + async flowNavigate(ctx, cmd) { + if (this.response) { + return new Promise((resolve) => { + this.loadingStarted(); + // The callback to run from server side once the view is ready + this.container.serverConnected = (cancel, redirectContext) => { + if (cmd && cancel) { + resolve(cmd.prevent()); + } + else if (cmd && cmd.redirect && redirectContext) { + resolve(cmd.redirect(redirectContext.pathname)); + } + else { + this.container.style.display = ''; + resolve(this.container); + } + this.loadingFinished(); + }; + // Call server side to navigate to the given route + flowRoot.$server.connectClient(this.container.localName, this.container.id, this.getFlowRoutePath(ctx), this.getFlowRouteQuery(ctx), this.appShellTitle, history.state); + }); + } + else { + // No server response => offline or erroneous connection + return Promise.resolve(this.container); + } + } + getFlowRoutePath(context) { + return decodeURIComponent(context.pathname).replace(this.baseRegex, ''); + } + getFlowRouteQuery(context) { + return (context.search && context.search.substring(1)) || ''; + } + // import flow client modules and initialize UI in server side. + async flowInit(serverSideRouting = false) { + // Do not start flow twice + if (!this.isFlowClientLoaded()) { + // show flow progress indicator + this.loadingStarted(); + // Initialize server side UI + this.response = await this.flowInitUi(serverSideRouting); + // Enable or disable server side routing + this.response.appConfig.clientRouting = !serverSideRouting; + const { pushScript, appConfig } = this.response; + if (typeof pushScript === 'string') { + await this.loadScript(pushScript); + } + const { appId } = appConfig; + // Load bootstrap script with server side parameters + const bootstrapMod = await import('./FlowBootstrap'); + await bootstrapMod.init(this.response); + // Load custom modules defined by user + if (typeof this.config.imports === 'function') { + this.injectAppIdScript(appId); + await this.config.imports(); + } + // Load flow-client module + const clientMod = await import('./FlowClient'); + await this.flowInitClient(clientMod); + if (!serverSideRouting) { + // we use a custom tag for the flow app container + const tag = `flow-container-${appId.toLowerCase()}`; + this.container = document.createElement(tag); + flowRoot.$[appId] = this.container; + this.container.id = appId; + } + // hide flow progress indicator + this.loadingFinished(); + } + // It might be that components created from server expect that their content has been rendered. + // Appending eagerly the container we avoid these kind of errors. + // Note that the client router will move this container to the outlet if the navigation succeed + if (this.container && !this.container.isConnected) { + this.container.style.display = 'none'; + document.body.appendChild(this.container); + } + return this.response; + } + async loadScript(url) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.onload = () => resolve(); + script.onerror = reject; + script.src = url; + document.body.appendChild(script); + }); + } + injectAppIdScript(appId) { + const appIdWithoutHashCode = appId.substring(0, appId.lastIndexOf('-')); + const scriptAppId = document.createElement('script'); + scriptAppId.type = 'module'; + scriptAppId.setAttribute('data-app-id', appIdWithoutHashCode); + document.body.append(scriptAppId); + } + // After the flow-client javascript module has been loaded, this initializes flow UI + // in the browser. + async flowInitClient(clientMod) { + clientMod.init(); + // client init is async, we need to loop until initialized + return new Promise((resolve) => { + const intervalId = setInterval(() => { + // client `isActive() == true` while initializing or processing + const initializing = Object.keys($wnd.Vaadin.Flow.clients) + .filter((key) => key !== 'TypeScript') + .reduce((prev, id) => prev || $wnd.Vaadin.Flow.clients[id].isActive(), false); + if (!initializing) { + clearInterval(intervalId); + resolve(); + } + }, 5); + }); + } + // Returns the `appConfig` object + async flowInitUi(serverSideRouting) { + // appConfig was sent in the index.html request + const initial = $wnd.Vaadin && $wnd.Vaadin.TypeScript && $wnd.Vaadin.TypeScript.initial; + if (initial) { + $wnd.Vaadin.TypeScript.initial = undefined; + return Promise.resolve(initial); + } + // send a request to the `JavaScriptBootstrapHandler` + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + const httpRequest = xhr; + const serverRoutingParam = serverSideRouting ? '&serverSideRouting' : ''; + const requestPath = `?v-r=init&location=${encodeURIComponent(this.getFlowRoutePath(location))}&query=${encodeURIComponent(this.getFlowRouteQuery(location))}${serverRoutingParam}`; + httpRequest.open('GET', requestPath); + httpRequest.onerror = () => reject(new FlowUiInitializationError(`Invalid server response when initializing Flow UI. + ${httpRequest.status} + ${httpRequest.responseText}`)); + httpRequest.onload = () => { + const contentType = httpRequest.getResponseHeader('content-type'); + if (contentType && contentType.indexOf('application/json') !== -1) { + resolve(JSON.parse(httpRequest.responseText)); + } + else { + httpRequest.onerror(); + } + }; + httpRequest.send(); + }); + } + // Create shared connection state store and connection indicator + addConnectionIndicator() { + // add connection indicator to DOM + ConnectionIndicator.create(); + // Listen to browser online/offline events and update the loading indicator accordingly. + // Note: if flow-client is loaded, it instead handles the state transitions. + $wnd.addEventListener('online', () => { + if (!this.isFlowClientLoaded()) { + // Send an HTTP HEAD request for sw.js to verify server reachability. + // We do not expect sw.js to be cached, so the request goes to the + // server rather than being served from local cache. + // Require network-level failure to revert the state to CONNECTION_LOST + // (HTTP error code is ok since it still verifies server's presence). + $wnd.Vaadin.connectionState.state = ConnectionState.RECONNECTING; + const http = new XMLHttpRequest(); + http.open('HEAD', 'sw.js'); + http.onload = () => { + $wnd.Vaadin.connectionState.state = ConnectionState.CONNECTED; + }; + http.onerror = () => { + $wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST; + }; + // Postpone request to reduce potential net::ERR_INTERNET_DISCONNECTED + // errors that sometimes occurs even if browser says it is online + setTimeout(() => http.send(), 50); + } + }); + $wnd.addEventListener('offline', () => { + if (!this.isFlowClientLoaded()) { + $wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST; + } + }); + } + async offlineStubAction() { + const offlineStub = document.createElement('iframe'); + const offlineStubPath = './offline-stub.html'; + offlineStub.setAttribute('src', offlineStubPath); + offlineStub.setAttribute('style', 'width: 100%; height: 100%; border: 0'); + this.response = undefined; + let onlineListener; + const removeOfflineStubAndOnlineListener = () => { + if (onlineListener !== undefined) { + $wnd.Vaadin.connectionState.removeStateChangeListener(onlineListener); + onlineListener = undefined; + } + }; + offlineStub.onBeforeEnter = (ctx, _cmds, router) => { + onlineListener = () => { + if ($wnd.Vaadin.connectionState.online) { + removeOfflineStubAndOnlineListener(); + router.render(ctx, false); + } + }; + $wnd.Vaadin.connectionState.addStateChangeListener(onlineListener); + }; + offlineStub.onBeforeLeave = (_ctx, _cmds, _router) => { + removeOfflineStubAndOnlineListener(); + }; + return offlineStub; + } + isFlowClientLoaded() { + return this.response !== undefined; + } +} +//# sourceMappingURL=Flow.js.map \ No newline at end of file diff --git a/java/demo/frontend/generated/jar-resources/Flow.js.map b/java/demo/frontend/generated/jar-resources/Flow.js.map new file mode 100644 index 000000000..bbd8ef2a0 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/Flow.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Flow.js","sourceRoot":"","sources":["../../../../src/main/frontend/Flow.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,eAAe,EAGhB,MAAM,yBAAyB,CAAC;AAMjC,MAAM,yBAA0B,SAAQ,KAAK;CAAG;AA+ChD,wCAAwC;AACxC,MAAM,QAAQ,GAAa,MAAM,CAAC,QAAQ,CAAC,IAAW,CAAC;AACvD,MAAM,IAAI,GAAG,MAME,CAAC;AAEhB;;GAEG;AACH,MAAM,OAAO,IAAI;IAaf,YAAY,MAAmB;QAX/B,aAAQ,GAAqB,SAAS,CAAC;QACvC,aAAQ,GAAG,EAAE,CAAC;QAId,sEAAsE;QAC9D,aAAQ,GAAG,KAAK,CAAC;QAEjB,cAAS,GAAG,KAAK,CAAC;QAIxB,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;QAE3B,6DAA6D;QAC7D,sDAAsD;QACtD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,GAAG;YACzB,UAAU,EAAE;gBACV,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ;aAC9B;SACF,CAAC;QAEF,oDAAoD;QACpD,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,GAAG,IAAI,MAAM,CACzB,IAAI;QACF,yCAAyC;QACzC,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,CACjF,EAAE,CACH,CAAC;QACF,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC;QACpC,+CAA+C;QAC/C,IAAI,CAAC,sBAAsB,EAAE,CAAC;IAChC,CAAC;IAED;;;;;;;;OAQG;IACH,IAAI,gBAAgB;QAClB,OAAO;YACL;gBACE,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE,IAAI,CAAC,MAAM;aACpB;SACF,CAAC;IACJ,CAAC;IAED,cAAc;QACZ,yDAAyD;QACzD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,cAAc,EAAE,CAAC;IAC/C,CAAC;IAED,eAAe;QACb,uDAAuD;QACvD,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,eAAe,EAAE,CAAC;IAChD,CAAC;IAED,IAAY,MAAM;QAChB,yEAAyE;QACzE,qDAAqD;QACrD,OAAO,KAAK,EAAE,MAA4B,EAAE,EAAE;YAC5C,6DAA6D;YAC7D,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;YAEhC,IAAI,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE;gBACtC,IAAI;oBACF,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;iBACvB;gBAAC,OAAO,KAAK,EAAE;oBACd,IAAI,KAAK,YAAY,yBAAyB,EAAE;wBAC9C,kDAAkD;wBAClD,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,GAAG,eAAe,CAAC,eAAe,CAAC;wBACpE,OAAO,IAAI,CAAC,iBAAiB,EAAE,CAAC;qBACjC;yBAAM;wBACL,MAAM,KAAK,CAAC;qBACb;iBACF;aACF;iBAAM;gBACL,yBAAyB;gBACzB,OAAO,IAAI,CAAC,iBAAiB,EAAE,CAAC;aACjC;YAED,sEAAsE;YACtE,IAAI,CAAC,SAAS,CAAC,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACzE,+CAA+C;YAC/C,IAAI,CAAC,SAAS,CAAC,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACtE,OAAO,IAAI,CAAC,SAAS,CAAC;QACxB,CAAC,CAAC;IACJ,CAAC;IAED,yDAAyD;IACzD,0CAA0C;IAClC,KAAK,CAAC,SAAS,CAAC,GAAyB,EAAE,GAAqB;QACtE,gEAAgE;QAChE,MAAM,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACxC,IAAI,IAAI,CAAC,QAAQ,KAAK,GAAG,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,eAAe,CAAC,OAAO,EAAE;YAC3F,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;SAC5B;QACD,qBAAqB;QACrB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,IAAI,CAAC,cAAc,EAAE,CAAC;YACtB,4DAA4D;YAC5D,IAAI,CAAC,SAAS,CAAC,eAAe,GAAG,CAAC,MAAM,EAAE,EAAE;gBAC1C,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBAC5C,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,CAAC,CAAC;YAEF,0DAA0D;YAC1D,QAAQ,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5F,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qEAAqE;IACrE,iCAAiC;IACzB,KAAK,CAAC,YAAY,CAAC,GAAyB,EAAE,GAAgC;QACpF,IAAI,IAAI,CAAC,QAAQ,EAAE;YACjB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBAC7B,IAAI,CAAC,cAAc,EAAE,CAAC;gBACtB,8DAA8D;gBAC9D,IAAI,CAAC,SAAS,CAAC,eAAe,GAAG,CAAC,MAAM,EAAE,eAAsC,EAAE,EAAE;oBAClF,IAAI,GAAG,IAAI,MAAM,EAAE;wBACjB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;qBACxB;yBAAM,IAAI,GAAG,IAAI,GAAG,CAAC,QAAQ,IAAI,eAAe,EAAE;wBACjD,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC;qBACjD;yBAAM;wBACL,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC;wBAClC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;qBACzB;oBACD,IAAI,CAAC,eAAe,EAAE,CAAC;gBACzB,CAAC,CAAC;gBAEF,kDAAkD;gBAClD,QAAQ,CAAC,OAAO,CAAC,aAAa,CAC5B,IAAI,CAAC,SAAS,CAAC,SAAS,EACxB,IAAI,CAAC,SAAS,CAAC,EAAE,EACjB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAC1B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAC3B,IAAI,CAAC,aAAa,EAClB,OAAO,CAAC,KAAK,CACd,CAAC;YACJ,CAAC,CAAC,CAAC;SACJ;aAAM;YACL,wDAAwD;YACxD,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;SACxC;IACH,CAAC;IAEO,gBAAgB,CAAC,OAAwC;QAC/D,OAAO,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC1E,CAAC;IACO,iBAAiB,CAAC,OAAwC;QAChE,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/D,CAAC;IAED,+DAA+D;IACvD,KAAK,CAAC,QAAQ,CAAC,iBAAiB,GAAG,KAAK;QAC9C,0BAA0B;QAC1B,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE;YAC9B,+BAA+B;YAC/B,IAAI,CAAC,cAAc,EAAE,CAAC;YAEtB,4BAA4B;YAC5B,IAAI,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;YAEzD,wCAAwC;YACxC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,aAAa,GAAG,CAAC,iBAAiB,CAAC;YAE3D,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;YAEhD,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE;gBAClC,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;aACnC;YACD,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;YAE5B,oDAAoD;YACpD,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;YACrD,MAAM,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAEvC,sCAAsC;YACtC,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,UAAU,EAAE;gBAC7C,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;aAC7B;YAED,0BAA0B;YAC1B,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;YAC/C,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;YAErC,IAAI,CAAC,iBAAiB,EAAE;gBACtB,iDAAiD;gBACjD,MAAM,GAAG,GAAG,kBAAkB,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACpD,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;gBAC7C,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC;gBACnC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,KAAK,CAAC;aAC3B;YAED,+BAA+B;YAC/B,IAAI,CAAC,eAAe,EAAE,CAAC;SACxB;QAED,+FAA+F;QAC/F,iEAAiE;QACjE,+FAA+F;QAC/F,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;YACjD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YACtC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;SAC3C;QACD,OAAO,IAAI,CAAC,QAAS,CAAC;IACxB,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,GAAW;QAClC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAChD,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;YAChC,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC;YACxB,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC;YACjB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,iBAAiB,CAAC,KAAa;QACrC,MAAM,oBAAoB,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;QACxE,MAAM,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACrD,WAAW,CAAC,IAAI,GAAG,QAAQ,CAAC;QAC5B,WAAW,CAAC,YAAY,CAAC,aAAa,EAAE,oBAAoB,CAAC,CAAC;QAC9D,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACpC,CAAC;IAED,oFAAoF;IACpF,kBAAkB;IACV,KAAK,CAAC,cAAc,CAAC,SAAc;QACzC,SAAS,CAAC,IAAI,EAAE,CAAC;QACjB,0DAA0D;QAC1D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;gBAClC,+DAA+D;gBAC/D,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;qBACvD,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,YAAY,CAAC;qBACrC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC,CAAC;gBAChF,IAAI,CAAC,YAAY,EAAE;oBACjB,aAAa,CAAC,UAAU,CAAC,CAAC;oBAC1B,OAAO,EAAE,CAAC;iBACX;YACH,CAAC,EAAE,CAAC,CAAC,CAAC;QACR,CAAC,CAAC,CAAC;IACL,CAAC;IAED,iCAAiC;IACzB,KAAK,CAAC,UAAU,CAAC,iBAA0B;QACjD,+CAA+C;QAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC;QACxF,IAAI,OAAO,EAAE;YACX,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,GAAG,SAAS,CAAC;YAC3C,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;SACjC;QAED,qDAAqD;QACrD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,GAAG,GAAG,IAAI,cAAc,EAAE,CAAC;YACjC,MAAM,WAAW,GAAG,GAAU,CAAC;YAC/B,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;YACzE,MAAM,WAAW,GAAG,sBAAsB,kBAAkB,CAC1D,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAChC,UAAU,kBAAkB,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,GAAG,kBAAkB,EAAE,CAAC;YAEvF,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;YAErC,WAAW,CAAC,OAAO,GAAG,GAAG,EAAE,CACzB,MAAM,CACJ,IAAI,yBAAyB,CAC3B;UACF,WAAW,CAAC,MAAM;UAClB,WAAW,CAAC,YAAY,EAAE,CACzB,CACF,CAAC;YAEJ,WAAW,CAAC,MAAM,GAAG,GAAG,EAAE;gBACxB,MAAM,WAAW,GAAG,WAAW,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAC;gBAClE,IAAI,WAAW,IAAI,WAAW,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,EAAE;oBACjE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC;iBAC/C;qBAAM;oBACL,WAAW,CAAC,OAAO,EAAE,CAAC;iBACvB;YACH,CAAC,CAAC;YACF,WAAW,CAAC,IAAI,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,gEAAgE;IACxD,sBAAsB;QAC5B,kCAAkC;QAClC,mBAAmB,CAAC,MAAM,EAAE,CAAC;QAE7B,wFAAwF;QACxF,4EAA4E;QAC5E,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE;YACnC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE;gBAC9B,qEAAqE;gBACrE,kEAAkE;gBAClE,oDAAoD;gBACpD,uEAAuE;gBACvE,qEAAqE;gBACrE,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,GAAG,eAAe,CAAC,YAAY,CAAC;gBACjE,MAAM,IAAI,GAAG,IAAI,cAAc,EAAE,CAAC;gBAClC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAC3B,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE;oBACjB,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,GAAG,eAAe,CAAC,SAAS,CAAC;gBAChE,CAAC,CAAC;gBACF,IAAI,CAAC,OAAO,GAAG,GAAG,EAAE;oBAClB,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,GAAG,eAAe,CAAC,eAAe,CAAC;gBACtE,CAAC,CAAC;gBACF,sEAAsE;gBACtE,iEAAiE;gBACjE,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;aACnC;QACH,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE;YACpC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE;gBAC9B,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,GAAG,eAAe,CAAC,eAAe,CAAC;aACrE;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,iBAAiB;QAC7B,MAAM,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAwB,CAAC;QAC5E,MAAM,eAAe,GAAG,qBAAqB,CAAC;QAC9C,WAAW,CAAC,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;QACjD,WAAW,CAAC,YAAY,CAAC,OAAO,EAAE,sCAAsC,CAAC,CAAC;QAC1E,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;QAE1B,IAAI,cAAyD,CAAC;QAC9D,MAAM,kCAAkC,GAAG,GAAG,EAAE;YAC9C,IAAI,cAAc,KAAK,SAAS,EAAE;gBAChC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,yBAAyB,CAAC,cAAc,CAAC,CAAC;gBACtE,cAAc,GAAG,SAAS,CAAC;aAC5B;QACH,CAAC,CAAC;QAEF,WAAW,CAAC,aAAa,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YACjD,cAAc,GAAG,GAAG,EAAE;gBACpB,IAAI,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE;oBACtC,kCAAkC,EAAE,CAAC;oBACrC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;iBAC3B;YACH,CAAC,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,sBAAsB,CAAC,cAAc,CAAC,CAAC;QACrE,CAAC,CAAC;QACF,WAAW,CAAC,aAAa,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACnD,kCAAkC,EAAE,CAAC;QACvC,CAAC,CAAC;QACF,OAAO,WAAW,CAAC;IACrB,CAAC;IAEO,kBAAkB;QACxB,OAAO,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC;IACrC,CAAC;CACF","sourcesContent":["import {\n ConnectionIndicator,\n ConnectionState,\n ConnectionStateChangeListener,\n ConnectionStateStore\n} from '@vaadin/common-frontend';\n\nexport interface FlowConfig {\n imports?: () => void;\n}\n\nclass FlowUiInitializationError extends Error {}\n\ninterface AppConfig {\n productionMode: boolean;\n appId: string;\n uidl: any;\n clientRouting: boolean;\n}\n\ninterface AppInitResponse {\n appConfig: AppConfig;\n pushScript?: string;\n}\n\ninterface Router {\n render: (ctx: NavigationParameters, shouldUpdateHistory: boolean) => Promise;\n}\n\ninterface HTMLRouterContainer extends HTMLElement {\n onBeforeEnter?: (ctx: NavigationParameters, cmd: PreventAndRedirectCommands, router: Router) => void | Promise;\n onBeforeLeave?: (ctx: NavigationParameters, cmd: PreventCommands, router: Router) => void | Promise;\n serverConnected?: (cancel: boolean, url?: NavigationParameters) => void;\n}\n\ninterface FlowRoute {\n action: (params: NavigationParameters) => Promise;\n path: string;\n}\n\ninterface FlowRoot {\n $: any;\n $server: any;\n}\n\nexport interface NavigationParameters {\n pathname: string;\n search: string;\n}\n\nexport interface PreventCommands {\n prevent: () => any;\n}\n\nexport interface PreventAndRedirectCommands extends PreventCommands {\n redirect: (route: string) => any;\n}\n\n// flow uses body for keeping references\nconst flowRoot: FlowRoot = window.document.body as any;\nconst $wnd = window as any as {\n Vaadin: {\n Flow: any;\n TypeScript: any;\n connectionState: ConnectionStateStore;\n };\n} & EventTarget;\n\n/**\n * Client API for flow UI operations.\n */\nexport class Flow {\n config: FlowConfig;\n response?: AppInitResponse = undefined;\n pathname = '';\n\n container!: HTMLRouterContainer;\n\n // flag used to inform Testbench whether a server route is in progress\n private isActive = false;\n\n private baseRegex = /^\\//;\n private appShellTitle: string;\n\n constructor(config?: FlowConfig) {\n flowRoot.$ = flowRoot.$ || [];\n this.config = config || {};\n\n // TB checks for the existence of window.Vaadin.Flow in order\n // to consider that TB needs to wait for `initFlow()`.\n $wnd.Vaadin = $wnd.Vaadin || {};\n $wnd.Vaadin.Flow = $wnd.Vaadin.Flow || {};\n $wnd.Vaadin.Flow.clients = {\n TypeScript: {\n isActive: () => this.isActive\n }\n };\n\n // Regular expression used to remove the app-context\n const elm = document.head.querySelector('base');\n this.baseRegex = new RegExp(\n `^${\n // IE11 does not support document.baseURI\n (document.baseURI || (elm && elm.href) || '/').replace(/^https?:\\/\\/[^/]+/i, '')\n }`\n );\n this.appShellTitle = document.title;\n // Put a vaadin-connection-indicator in the dom\n this.addConnectionIndicator();\n }\n\n /**\n * Return a `route` object for vaadin-router in an one-element array.\n *\n * The `FlowRoute` object `path` property handles any route,\n * and the `action` returns the flow container without updating the content,\n * delaying the actual Flow server call to the `onBeforeEnter` phase.\n *\n * This is a specific API for its use with `vaadin-router`.\n */\n get serverSideRoutes(): [FlowRoute] {\n return [\n {\n path: '(.*)',\n action: this.action\n }\n ];\n }\n\n loadingStarted() {\n // Make Testbench know that server request is in progress\n this.isActive = true;\n $wnd.Vaadin.connectionState.loadingStarted();\n }\n\n loadingFinished() {\n // Make Testbench know that server request has finished\n this.isActive = false;\n $wnd.Vaadin.connectionState.loadingFinished();\n }\n\n private get action(): (params: NavigationParameters) => Promise {\n // Return a function which is bound to the flow instance, thus we can use\n // the syntax `...serverSideRoutes` in vaadin-router.\n return async (params: NavigationParameters) => {\n // Store last action pathname so as we can check it in events\n this.pathname = params.pathname;\n\n if ($wnd.Vaadin.connectionState.online) {\n try {\n await this.flowInit();\n } catch (error) {\n if (error instanceof FlowUiInitializationError) {\n // error initializing Flow: assume connection lost\n $wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;\n return this.offlineStubAction();\n } else {\n throw error;\n }\n }\n } else {\n // insert an offline stub\n return this.offlineStubAction();\n }\n\n // When an action happens, navigation will be resolved `onBeforeEnter`\n this.container.onBeforeEnter = (ctx, cmd) => this.flowNavigate(ctx, cmd);\n // For covering the 'server -> client' use case\n this.container.onBeforeLeave = (ctx, cmd) => this.flowLeave(ctx, cmd);\n return this.container;\n };\n }\n\n // Send a remote call to `JavaScriptBootstrapUI` to check\n // whether navigation has to be cancelled.\n private async flowLeave(ctx: NavigationParameters, cmd?: PreventCommands): Promise {\n // server -> server, viewing offline stub, or browser is offline\n const { connectionState } = $wnd.Vaadin;\n if (this.pathname === ctx.pathname || !this.isFlowClientLoaded() || connectionState.offline) {\n return Promise.resolve({});\n }\n // 'server -> client'\n return new Promise((resolve) => {\n this.loadingStarted();\n // The callback to run from server side to cancel navigation\n this.container.serverConnected = (cancel) => {\n resolve(cmd && cancel ? cmd.prevent() : {});\n this.loadingFinished();\n };\n\n // Call server side to check whether we can leave the view\n flowRoot.$server.leaveNavigation(this.getFlowRoutePath(ctx), this.getFlowRouteQuery(ctx));\n });\n }\n\n // Send the remote call to `JavaScriptBootstrapUI` to render the flow\n // route specified by the context\n private async flowNavigate(ctx: NavigationParameters, cmd?: PreventAndRedirectCommands): Promise {\n if (this.response) {\n return new Promise((resolve) => {\n this.loadingStarted();\n // The callback to run from server side once the view is ready\n this.container.serverConnected = (cancel, redirectContext?: NavigationParameters) => {\n if (cmd && cancel) {\n resolve(cmd.prevent());\n } else if (cmd && cmd.redirect && redirectContext) {\n resolve(cmd.redirect(redirectContext.pathname));\n } else {\n this.container.style.display = '';\n resolve(this.container);\n }\n this.loadingFinished();\n };\n\n // Call server side to navigate to the given route\n flowRoot.$server.connectClient(\n this.container.localName,\n this.container.id,\n this.getFlowRoutePath(ctx),\n this.getFlowRouteQuery(ctx),\n this.appShellTitle,\n history.state\n );\n });\n } else {\n // No server response => offline or erroneous connection\n return Promise.resolve(this.container);\n }\n }\n\n private getFlowRoutePath(context: NavigationParameters | Location): string {\n return decodeURIComponent(context.pathname).replace(this.baseRegex, '');\n }\n private getFlowRouteQuery(context: NavigationParameters | Location): string {\n return (context.search && context.search.substring(1)) || '';\n }\n\n // import flow client modules and initialize UI in server side.\n private async flowInit(serverSideRouting = false): Promise {\n // Do not start flow twice\n if (!this.isFlowClientLoaded()) {\n // show flow progress indicator\n this.loadingStarted();\n\n // Initialize server side UI\n this.response = await this.flowInitUi(serverSideRouting);\n\n // Enable or disable server side routing\n this.response.appConfig.clientRouting = !serverSideRouting;\n\n const { pushScript, appConfig } = this.response;\n\n if (typeof pushScript === 'string') {\n await this.loadScript(pushScript);\n }\n const { appId } = appConfig;\n\n // Load bootstrap script with server side parameters\n const bootstrapMod = await import('./FlowBootstrap');\n await bootstrapMod.init(this.response);\n\n // Load custom modules defined by user\n if (typeof this.config.imports === 'function') {\n this.injectAppIdScript(appId);\n await this.config.imports();\n }\n\n // Load flow-client module\n const clientMod = await import('./FlowClient');\n await this.flowInitClient(clientMod);\n\n if (!serverSideRouting) {\n // we use a custom tag for the flow app container\n const tag = `flow-container-${appId.toLowerCase()}`;\n this.container = document.createElement(tag);\n flowRoot.$[appId] = this.container;\n this.container.id = appId;\n }\n\n // hide flow progress indicator\n this.loadingFinished();\n }\n\n // It might be that components created from server expect that their content has been rendered.\n // Appending eagerly the container we avoid these kind of errors.\n // Note that the client router will move this container to the outlet if the navigation succeed\n if (this.container && !this.container.isConnected) {\n this.container.style.display = 'none';\n document.body.appendChild(this.container);\n }\n return this.response!;\n }\n\n private async loadScript(url: string): Promise {\n return new Promise((resolve, reject) => {\n const script = document.createElement('script');\n script.onload = () => resolve();\n script.onerror = reject;\n script.src = url;\n document.body.appendChild(script);\n });\n }\n\n private injectAppIdScript(appId: string) {\n const appIdWithoutHashCode = appId.substring(0, appId.lastIndexOf('-'));\n const scriptAppId = document.createElement('script');\n scriptAppId.type = 'module';\n scriptAppId.setAttribute('data-app-id', appIdWithoutHashCode);\n document.body.append(scriptAppId);\n }\n\n // After the flow-client javascript module has been loaded, this initializes flow UI\n // in the browser.\n private async flowInitClient(clientMod: any): Promise {\n clientMod.init();\n // client init is async, we need to loop until initialized\n return new Promise((resolve) => {\n const intervalId = setInterval(() => {\n // client `isActive() == true` while initializing or processing\n const initializing = Object.keys($wnd.Vaadin.Flow.clients)\n .filter((key) => key !== 'TypeScript')\n .reduce((prev, id) => prev || $wnd.Vaadin.Flow.clients[id].isActive(), false);\n if (!initializing) {\n clearInterval(intervalId);\n resolve();\n }\n }, 5);\n });\n }\n\n // Returns the `appConfig` object\n private async flowInitUi(serverSideRouting: boolean): Promise {\n // appConfig was sent in the index.html request\n const initial = $wnd.Vaadin && $wnd.Vaadin.TypeScript && $wnd.Vaadin.TypeScript.initial;\n if (initial) {\n $wnd.Vaadin.TypeScript.initial = undefined;\n return Promise.resolve(initial);\n }\n\n // send a request to the `JavaScriptBootstrapHandler`\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n const httpRequest = xhr as any;\n const serverRoutingParam = serverSideRouting ? '&serverSideRouting' : '';\n const requestPath = `?v-r=init&location=${encodeURIComponent(\n this.getFlowRoutePath(location)\n )}&query=${encodeURIComponent(this.getFlowRouteQuery(location))}${serverRoutingParam}`;\n\n httpRequest.open('GET', requestPath);\n\n httpRequest.onerror = () =>\n reject(\n new FlowUiInitializationError(\n `Invalid server response when initializing Flow UI.\n ${httpRequest.status}\n ${httpRequest.responseText}`\n )\n );\n\n httpRequest.onload = () => {\n const contentType = httpRequest.getResponseHeader('content-type');\n if (contentType && contentType.indexOf('application/json') !== -1) {\n resolve(JSON.parse(httpRequest.responseText));\n } else {\n httpRequest.onerror();\n }\n };\n httpRequest.send();\n });\n }\n\n // Create shared connection state store and connection indicator\n private addConnectionIndicator() {\n // add connection indicator to DOM\n ConnectionIndicator.create();\n\n // Listen to browser online/offline events and update the loading indicator accordingly.\n // Note: if flow-client is loaded, it instead handles the state transitions.\n $wnd.addEventListener('online', () => {\n if (!this.isFlowClientLoaded()) {\n // Send an HTTP HEAD request for sw.js to verify server reachability.\n // We do not expect sw.js to be cached, so the request goes to the\n // server rather than being served from local cache.\n // Require network-level failure to revert the state to CONNECTION_LOST\n // (HTTP error code is ok since it still verifies server's presence).\n $wnd.Vaadin.connectionState.state = ConnectionState.RECONNECTING;\n const http = new XMLHttpRequest();\n http.open('HEAD', 'sw.js');\n http.onload = () => {\n $wnd.Vaadin.connectionState.state = ConnectionState.CONNECTED;\n };\n http.onerror = () => {\n $wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;\n };\n // Postpone request to reduce potential net::ERR_INTERNET_DISCONNECTED\n // errors that sometimes occurs even if browser says it is online\n setTimeout(() => http.send(), 50);\n }\n });\n $wnd.addEventListener('offline', () => {\n if (!this.isFlowClientLoaded()) {\n $wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;\n }\n });\n }\n\n private async offlineStubAction() {\n const offlineStub = document.createElement('iframe') as HTMLRouterContainer;\n const offlineStubPath = './offline-stub.html';\n offlineStub.setAttribute('src', offlineStubPath);\n offlineStub.setAttribute('style', 'width: 100%; height: 100%; border: 0');\n this.response = undefined;\n\n let onlineListener: ConnectionStateChangeListener | undefined;\n const removeOfflineStubAndOnlineListener = () => {\n if (onlineListener !== undefined) {\n $wnd.Vaadin.connectionState.removeStateChangeListener(onlineListener);\n onlineListener = undefined;\n }\n };\n\n offlineStub.onBeforeEnter = (ctx, _cmds, router) => {\n onlineListener = () => {\n if ($wnd.Vaadin.connectionState.online) {\n removeOfflineStubAndOnlineListener();\n router.render(ctx, false);\n }\n };\n $wnd.Vaadin.connectionState.addStateChangeListener(onlineListener);\n };\n offlineStub.onBeforeLeave = (_ctx, _cmds, _router) => {\n removeOfflineStubAndOnlineListener();\n };\n return offlineStub;\n }\n\n private isFlowClientLoaded(): boolean {\n return this.response !== undefined;\n }\n}\n"]} \ No newline at end of file diff --git a/java/demo/frontend/generated/jar-resources/FlowBootstrap.d.ts b/java/demo/frontend/generated/jar-resources/FlowBootstrap.d.ts new file mode 100644 index 000000000..0398d5762 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/FlowBootstrap.d.ts @@ -0,0 +1 @@ +export const init: (appInitResponse: any) => void; diff --git a/java/demo/frontend/generated/jar-resources/FlowBootstrap.js b/java/demo/frontend/generated/jar-resources/FlowBootstrap.js new file mode 100644 index 000000000..a8d948545 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/FlowBootstrap.js @@ -0,0 +1,262 @@ +/* This is a copy of the regular `BootstrapHandler.js` in the flow-server + module, but with the following modifications: + - The main function is exported as an ES module for lazy initialization. + - Application configuration is passed as a parameter instead of using + replacement placeholders as in the regular bootstrapping. + - It reuses `Vaadin.Flow.clients` if exists. + - Fixed lint errors. + */ +const init = function (appInitResponse) { + window.Vaadin = window.Vaadin || {}; + window.Vaadin.Flow = window.Vaadin.Flow || {}; + + var apps = {}; + var widgetsets = {}; + + var log; + if (typeof window.console === undefined || !window.location.search.match(/[&?]debug(&|$)/)) { + /* If no console.log present, just use a no-op */ + log = function () {}; + } else if (typeof window.console.log === 'function') { + /* If it's a function, use it with apply */ + log = function () { + window.console.log.apply(window.console, arguments); + }; + } else { + /* In IE, its a native function for which apply is not defined, but it works + without a proper 'this' reference */ + log = window.console.log; + } + + var isInitializedInDom = function (appId) { + var appDiv = document.getElementById(appId); + if (!appDiv) { + return false; + } + for (var i = 0; i < appDiv.childElementCount; i++) { + var className = appDiv.childNodes[i].className; + /* If the app div contains a child with the class + 'v-app-loading' we have only received the HTML + but not yet started the widget set + (UIConnector removes the v-app-loading div). */ + if (className && className.indexOf('v-app-loading') != -1) { + return false; + } + } + return true; + }; + + /* + * Needed for Testbench compatibility, but prevents any Vaadin 7 app from + * bootstrapping unless the legacy vaadinBootstrap.js file is loaded before + * this script. + */ + window.Vaadin = window.Vaadin || {}; + window.Vaadin.Flow = window.Vaadin.Flow || {}; + + /* + * Needed for wrapping custom javascript functionality in the components (i.e. connectors) + */ + window.Vaadin.Flow.tryCatchWrapper = function (originalFunction, component) { + return function () { + try { + // eslint-disable-next-line + const result = originalFunction.apply(this, arguments); + return result; + } catch (error) { + console.error( + `There seems to be an error in ${component}: +${error.message} +Please submit an issue to https://github.com/vaadin/flow-components/issues/new/choose` + ); + } + }; + }; + + if (!window.Vaadin.Flow.initApplication) { + window.Vaadin.Flow.clients = window.Vaadin.Flow.clients || {}; + + window.Vaadin.Flow.initApplication = function (appId, config) { + var testbenchId = appId.replace(/-\d+$/, ''); + + if (apps[appId]) { + if ( + window.Vaadin && + window.Vaadin.Flow && + window.Vaadin.Flow.clients && + window.Vaadin.Flow.clients[testbenchId] && + window.Vaadin.Flow.clients[testbenchId].initializing + ) { + throw new Error('Application ' + appId + ' is already being initialized'); + } + if (isInitializedInDom(appId)) { + throw new Error('Application ' + appId + ' already initialized'); + } + } + + log('init application', appId, config); + + window.Vaadin.Flow.clients[testbenchId] = { + isActive: function () { + return true; + }, + initializing: true, + productionMode: mode + }; + + var getConfig = function (name) { + var value = config[name]; + return value; + }; + + /* Export public data */ + var app = { + getConfig: getConfig + }; + apps[appId] = app; + + if (!window.name) { + window.name = appId + '-' + Math.random(); + } + + var widgetset = 'client'; + widgetsets[widgetset] = { + pendingApps: [] + }; + if (widgetsets[widgetset].callback) { + log('Starting from bootstrap', appId); + widgetsets[widgetset].callback(appId); + } else { + log('Setting pending startup', appId); + widgetsets[widgetset].pendingApps.push(appId); + } + + return app; + }; + window.Vaadin.Flow.getAppIds = function () { + var ids = []; + for (var id in apps) { + if (Object.prototype.hasOwnProperty.call(apps, id)) { + ids.push(id); + } + } + return ids; + }; + window.Vaadin.Flow.getApp = function (appId) { + return apps[appId]; + }; + window.Vaadin.Flow.registerWidgetset = function (widgetset, callback) { + log('Widgetset registered', widgetset); + var ws = widgetsets[widgetset]; + if (ws && ws.pendingApps) { + ws.callback = callback; + for (var i = 0; i < ws.pendingApps.length; i++) { + var appId = ws.pendingApps[i]; + log('Starting from register widgetset', appId); + callback(appId); + } + ws.pendingApps = null; + } + }; + window.Vaadin.Flow.getBrowserDetailsParameters = function () { + var params = {}; + + /* Screen height and width */ + params['v-sh'] = window.screen.height; + params['v-sw'] = window.screen.width; + /* Browser window dimensions */ + params['v-wh'] = window.innerHeight; + params['v-ww'] = window.innerWidth; + /* Body element dimensions */ + params['v-bh'] = document.body.clientHeight; + params['v-bw'] = document.body.clientWidth; + + /* Current time */ + var date = new Date(); + params['v-curdate'] = date.getTime(); + + /* Current timezone offset (including DST shift) */ + var tzo1 = date.getTimezoneOffset(); + + /* Compare the current tz offset with the first offset from the end + of the year that differs --- if less that, we are in DST, otherwise + we are in normal time */ + var dstDiff = 0; + var rawTzo = tzo1; + for (var m = 12; m > 0; m--) { + date.setUTCMonth(m); + var tzo2 = date.getTimezoneOffset(); + if (tzo1 != tzo2) { + dstDiff = tzo1 > tzo2 ? tzo1 - tzo2 : tzo2 - tzo1; + rawTzo = tzo1 > tzo2 ? tzo1 : tzo2; + break; + } + } + + /* Time zone offset */ + params['v-tzo'] = tzo1; + + /* DST difference */ + params['v-dstd'] = dstDiff; + + /* Time zone offset without DST */ + params['v-rtzo'] = rawTzo; + + /* DST in effect? */ + params['v-dston'] = tzo1 != rawTzo; + + /* Time zone id (if available) */ + try { + params['v-tzid'] = Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch (err) { + params['v-tzid'] = ''; + } + + /* Window name */ + if (window.name) { + params['v-wn'] = window.name; + } + + /* Detect touch device support */ + var supportsTouch = false; + try { + document.createEvent('TouchEvent'); + supportsTouch = true; + } catch (e) { + /* Chrome and IE10 touch detection */ + supportsTouch = 'ontouchstart' in window || typeof navigator.msMaxTouchPoints !== 'undefined'; + } + params['v-td'] = supportsTouch; + + /* Device Pixel Ratio */ + params['v-pr'] = window.devicePixelRatio; + + if (navigator.platform) { + params['v-np'] = navigator.platform; + } + + /* Stringify each value (they are parsed on the server side) */ + Object.keys(params).forEach(function (key) { + var value = params[key]; + if (typeof value !== 'undefined') { + params[key] = value.toString(); + } + }); + return params; + }; + } + + log('Flow bootstrap loaded'); + if (appInitResponse.appConfig.productionMode && typeof window.__gwtStatsEvent != 'function') { + window.Vaadin.Flow.gwtStatsEvents = []; + window.__gwtStatsEvent = function (event) { + window.Vaadin.Flow.gwtStatsEvents.push(event); + return true; + }; + } + var config = appInitResponse.appConfig; + var mode = appInitResponse.appConfig.productionMode; + window.Vaadin.Flow.initApplication(config.appId, config); +}; + +export { init }; diff --git a/java/demo/frontend/generated/jar-resources/FlowClient.d.ts b/java/demo/frontend/generated/jar-resources/FlowClient.d.ts new file mode 100644 index 000000000..7b21f9085 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/FlowClient.d.ts @@ -0,0 +1 @@ +export const init: () => void; diff --git a/java/demo/frontend/generated/jar-resources/FlowClient.js b/java/demo/frontend/generated/jar-resources/FlowClient.js new file mode 100644 index 000000000..d158aa3a4 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/FlowClient.js @@ -0,0 +1,1101 @@ +export function init() { +function client(){var Jb='',Kb=0,Lb='gwt.codesvr=',Mb='gwt.hosted=',Nb='gwt.hybrid',Ob='client',Pb='#',Qb='?',Rb='/',Sb=1,Tb='img',Ub='clear.cache.gif',Vb='baseUrl',Wb='script',Xb='client.nocache.js',Yb='base',Zb='//',$b='meta',_b='name',ac='gwt:property',bc='content',cc='=',dc='gwt:onPropertyErrorFn',ec='Bad handler "',fc='" for "gwt:onPropertyErrorFn"',gc='gwt:onLoadErrorFn',hc='" for "gwt:onLoadErrorFn"',ic='user.agent',jc='webkit',kc='safari',lc='msie',mc=10,nc=11,oc='ie10',pc=9,qc='ie9',rc=8,sc='ie8',tc='gecko',uc='gecko1_8',vc=2,wc=3,xc=4,yc='Single-script hosted mode not yet implemented. See issue ',zc='http://code.google.com/p/google-web-toolkit/issues/detail?id=2079',Ac='EC03A171F4D51CC301EF01449491173F',Bc=':1',Cc=':',Dc='DOMContentLoaded',Ec=50;var l=Jb,m=Kb,n=Lb,o=Mb,p=Nb,q=Ob,r=Pb,s=Qb,t=Rb,u=Sb,v=Tb,w=Ub,A=Vb,B=Wb,C=Xb,D=Yb,F=Zb,G=$b,H=_b,I=ac,J=bc,K=cc,L=dc,M=ec,N=fc,O=gc,P=hc,Q=ic,R=jc,S=kc,T=lc,U=mc,V=nc,W=oc,X=pc,Y=qc,Z=rc,$=sc,_=tc,ab=uc,bb=vc,cb=wc,db=xc,eb=yc,fb=zc,gb=Ac,hb=Bc,ib=Cc,jb=Dc,kb=Ec;var lb=window,mb=document,nb,ob,pb=l,qb={},rb=[],sb=[],tb=[],ub=m,vb,wb;if(!lb.__gwt_stylesLoaded){lb.__gwt_stylesLoaded={}}if(!lb.__gwt_scriptsLoaded){lb.__gwt_scriptsLoaded={}}function xb(){var b=false;try{var c=lb.location.search;return (c.indexOf(n)!=-1||(c.indexOf(o)!=-1||lb.external&&lb.external.gwtOnLoad))&&c.indexOf(p)==-1}catch(a){}xb=function(){return b};return b} +function yb(){if(nb&&ob){nb(vb,q,pb,ub)}} +function zb(){function e(a){var b=a.lastIndexOf(r);if(b==-1){b=a.length}var c=a.indexOf(s);if(c==-1){c=a.length}var d=a.lastIndexOf(t,Math.min(c,b));return d>=m?a.substring(m,d+u):l} +function f(a){if(a.match(/^\w+:\/\//)){}else{var b=mb.createElement(v);b.src=a+w;a=e(b.src)}return a} +function g(){var a=Cb(A);if(a!=null){return a}return l} +function h(){var a=mb.getElementsByTagName(B);for(var b=m;bm){return a[a.length-u].href}return l} +function j(){var a=mb.location;return a.href==a.protocol+F+a.host+a.pathname+a.search+a.hash} +var k=g();if(k==l){k=h()}if(k==l){k=i()}if(k==l&&j()){k=e(mb.location.href)}k=f(k);return k} +function Ab(){var b=document.getElementsByTagName(G);for(var c=m,d=b.length;c=m){f=g.substring(m,i);h=g.substring(i+u)}else{f=g;h=l}qb[f]=h}}else if(f==L){g=e.getAttribute(J);if(g){try{wb=eval(g)}catch(a){alert(M+g+N)}}}else if(f==O){g=e.getAttribute(J);if(g){try{vb=eval(g)}catch(a){alert(M+g+P)}}}}}} +var Bb=function(a,b){return b in rb[a]};var Cb=function(a){var b=qb[a];return b==null?null:b};function Db(a,b){var c=tb;for(var d=m,e=a.length-u;d=U&&b=X&&b=Z&&b=V}())return ab;return S};rb[Q]={'gecko1_8':m,'ie10':u,'ie8':bb,'ie9':cb,'safari':db};client.onScriptLoad=function(a){client=null;nb=a;yb()};if(xb()){alert(eb+fb);return}zb();Ab();try{var Fb;Db([ab],gb);Db([S],gb+hb);Fb=tb[Eb(Q)];var Gb=Fb.indexOf(ib);if(Gb!=-1){ub=Number(Fb.substring(Gb+u))}}catch(a){return}var Hb;function Ib(){if(!ob){ob=true;yb();if(mb.removeEventListener){mb.removeEventListener(jb,Ib,false)}if(Hb){clearInterval(Hb)}}} +if(mb.addEventListener){mb.addEventListener(jb,function(){Ib()},false)}var Hb=setInterval(function(){if(/loaded|complete/.test(mb.readyState)){Ib()}},kb)} +client();(function () {var $gwt_version = "2.9.0";var $wnd = window;var $doc = $wnd.document;var $moduleName, $moduleBase;var $stats = $wnd.__gwtStatsEvent ? function(a) {$wnd.__gwtStatsEvent(a)} : null;var $strongName = 'EC03A171F4D51CC301EF01449491173F';function I(){} +function Ik(){} +function Ek(){} +function Gk(){} +function fj(){} +function bj(){} +function lj(){} +function Pj(){} +function Yj(){} +function nc(){} +function uc(){} +function ul(){} +function zl(){} +function El(){} +function Gl(){} +function Ql(){} +function Qo(){} +function Zo(){} +function Zm(){} +function _m(){} +function bn(){} +function Ln(){} +function Nn(){} +function Nt(){} +function Gt(){} +function Kt(){} +function Jq(){} +function Pr(){} +function Rr(){} +function Tr(){} +function Vr(){} +function ts(){} +function xs(){} +function hu(){} +function Yu(){} +function bw(){} +function fw(){} +function uw(){} +function uE(){} +function dy(){} +function Dy(){} +function Fy(){} +function rz(){} +function vz(){} +function CA(){} +function kB(){} +function qC(){} +function _F(){} +function eH(){} +function pH(){} +function rH(){} +function tH(){} +function KH(){} +function hA(){eA()} +function T(a){S=a;Jb()} +function zj(a,b){a.b=b} +function Bj(a,b){a.d=b} +function Cj(a,b){a.e=b} +function Dj(a,b){a.f=b} +function Ej(a,b){a.g=b} +function Fj(a,b){a.h=b} +function Gj(a,b){a.i=b} +function Ij(a,b){a.k=b} +function Jj(a,b){a.l=b} +function Kj(a,b){a.m=b} +function Lj(a,b){a.n=b} +function Mj(a,b){a.o=b} +function Nj(a,b){a.p=b} +function Oj(a,b){a.q=b} +function ns(a,b){a.g=b} +function qu(a,b){a.b=b} +function JH(a,b){a.a=b} +function bc(a){this.a=a} +function dc(a){this.a=a} +function pk(a){this.a=a} +function rk(a){this.a=a} +function sl(a){this.a=a} +function xl(a){this.a=a} +function Cl(a){this.a=a} +function Kl(a){this.a=a} +function Ml(a){this.a=a} +function Ol(a){this.a=a} +function Sl(a){this.a=a} +function Ul(a){this.a=a} +function xm(a){this.a=a} +function dn(a){this.a=a} +function hn(a){this.a=a} +function un(a){this.a=a} +function Bn(a){this.a=a} +function Dn(a){this.a=a} +function Fn(a){this.a=a} +function Pn(a){this.a=a} +function An(a){this.c=a} +function lo(a){this.a=a} +function oo(a){this.a=a} +function po(a){this.a=a} +function vo(a){this.a=a} +function Bo(a){this.a=a} +function Lo(a){this.a=a} +function No(a){this.a=a} +function So(a){this.a=a} +function Uo(a){this.a=a} +function Wo(a){this.a=a} +function $o(a){this.a=a} +function ep(a){this.a=a} +function yp(a){this.a=a} +function Qp(a){this.a=a} +function sq(a){this.a=a} +function Hq(a){this.a=a} +function Lq(a){this.a=a} +function Nq(a){this.a=a} +function zq(a){this.b=a} +function zs(a){this.a=a} +function Gs(a){this.a=a} +function Is(a){this.a=a} +function Us(a){this.a=a} +function Ys(a){this.a=a} +function ur(a){this.a=a} +function wr(a){this.a=a} +function yr(a){this.a=a} +function Hr(a){this.a=a} +function Kr(a){this.a=a} +function ft(a){this.a=a} +function nt(a){this.a=a} +function pt(a){this.a=a} +function rt(a){this.a=a} +function tt(a){this.a=a} +function vt(a){this.a=a} +function wt(a){this.a=a} +function Et(a){this.a=a} +function Yt(a){this.a=a} +function fu(a){this.a=a} +function ju(a){this.a=a} +function uu(a){this.a=a} +function wu(a){this.a=a} +function Ku(a){this.a=a} +function Qu(a){this.a=a} +function Wu(a){this.a=a} +function ru(a){this.c=a} +function Ts(a){this.c=a} +function fv(a){this.a=a} +function hv(a){this.a=a} +function Bv(a){this.a=a} +function Fv(a){this.a=a} +function Fw(a){this.a=a} +function dw(a){this.a=a} +function Gw(a){this.a=a} +function Iw(a){this.a=a} +function Mw(a){this.a=a} +function Ow(a){this.a=a} +function Tw(a){this.a=a} +function Jy(a){this.a=a} +function Ly(a){this.a=a} +function Zy(a){this.a=a} +function Iy(a){this.b=a} +function bz(a){this.a=a} +function fz(a){this.a=a} +function tz(a){this.a=a} +function zz(a){this.a=a} +function Bz(a){this.a=a} +function Fz(a){this.a=a} +function Mz(a){this.a=a} +function Oz(a){this.a=a} +function Qz(a){this.a=a} +function Sz(a){this.a=a} +function Uz(a){this.a=a} +function _z(a){this.a=a} +function bA(a){this.a=a} +function tA(a){this.a=a} +function wA(a){this.a=a} +function EA(a){this.a=a} +function GA(a){this.e=a} +function iB(a){this.a=a} +function mB(a){this.a=a} +function oB(a){this.a=a} +function KB(a){this.a=a} +function ZB(a){this.a=a} +function _B(a){this.a=a} +function bC(a){this.a=a} +function mC(a){this.a=a} +function oC(a){this.a=a} +function EC(a){this.a=a} +function dD(a){this.a=a} +function qE(a){this.a=a} +function sE(a){this.a=a} +function vE(a){this.a=a} +function lF(a){this.a=a} +function JG(a){this.a=a} +function jG(a){this.b=a} +function wG(a){this.c=a} +function NH(a){this.a=a} +function kk(a){throw a} +function Ui(a){return a.e} +function UA(a,b){Wv(b,a)} +function Ix(a,b){_x(b,a)} +function Ox(a,b){$x(b,a)} +function Sx(a,b){Ex(b,a)} +function yv(a,b){b.kb(a)} +function XD(b,a){b.log(a)} +function YD(b,a){b.warn(a)} +function RD(b,a){b.data=a} +function At(a,b){NC(a.a,b)} +function BC(a){bB(a.a,a.b)} +function R(){this.a=xb()} +function wj(){this.a=++vj} +function Yk(){this.d=null} +function gj(){Hp();Lp()} +function Hp(){Hp=bj;Gp=[]} +function eA(){eA=bj;dA=rA()} +function kb(){ab.call(this)} +function BE(){ab.call(this)} +function zE(){kb.call(this)} +function sF(){kb.call(this)} +function DG(){kb.call(this)} +function Zp(a,b){a.push(b)} +function Z(a,b){a.e=b;W(a,b)} +function Hj(a,b){a.j=b;gk=!b} +function VD(b,a){b.debug(a)} +function WD(b,a){b.error(a)} +function Zr(a){a.i||$r(a.a)} +function Yb(a){return a.H()} +function Ym(a){return Dm(a)} +function Q(a){return xb()-a.a} +function hc(a){gc();fc.J(a)} +function Ns(a){Ms(a)&&Qs(a)} +function mk(a){S=a;!!a&&Jb()} +function pm(a,b){a.a.add(b.d)} +function Wm(a,b,c){a.set(b,c)} +function cB(a,b,c){a.Ub(c,b)} +function om(a,b,c){jm(a,c,b)} +function ty(a,b){b.forEach(a)} +function _D(b,a){b.replace(a)} +function MD(b,a){b.display=a} +function ol(a){fl();this.a=a} +function fB(a){eB.call(this,a)} +function HB(a){eB.call(this,a)} +function WB(a){eB.call(this,a)} +function pE(a){lb.call(this,a)} +function xE(a){lb.call(this,a)} +function yE(a){xE.call(this,a)} +function jF(a){lb.call(this,a)} +function kF(a){lb.call(this,a)} +function tF(a){nb.call(this,a)} +function uF(a){lb.call(this,a)} +function wF(a){jF.call(this,a)} +function UF(){vE.call(this,'')} +function VF(){vE.call(this,'')} +function XF(a){xE.call(this,a)} +function bG(a){lb.call(this,a)} +function GE(a){return WH(a),a} +function gF(a){return WH(a),a} +function Wc(a,b){return $c(a,b)} +function nE(b,a){return a in b} +function LE(a){KE(a);return a.i} +function Wz(a){Ux(a.b,a.a,a.c)} +function EH(a,b,c){b.ib($F(c))} +function Zn(a,b){a.d?_n(b):pl()} +function lv(a,b){a.c.forEach(b)} +function xc(a,b){return UE(a,b)} +function rr(a,b){return a.a>b.a} +function $F(a){return Ic(a,5).e} +function mE(a){return Object(a)} +function ml(a,b){++el;b.eb(a,bl)} +function Rm(a,b){wC(new sn(b,a))} +function Lx(a,b){wC(new _y(b,a))} +function Mx(a,b){wC(new dz(b,a))} +function py(a,b,c){kC(fy(a,c,b))} +function ZG(a,b,c){b.ib(a.a[c])} +function OG(a,b){while(a.mc(b));} +function oH(a,b){Ic(a,106).dc(b)} +function yH(a,b){uH(a);a.a.lc(b)} +function iC(a,b){a.e||a.c.add(b)} +function hj(b,a){return b.exec(a)} +function pb(){pb=bj;ob=new I} +function Qb(){Qb=bj;Pb=new Zo} +function au(){au=bj;_t=new hu} +function LA(){LA=bj;KA=new kB} +function ZF(){ZF=bj;YF=new uE} +function Db(){Db=bj;!!(gc(),fc)} +function Xi(){Vi==null&&(Vi=[])} +function cF(){lb.call(this,null)} +function Ob(){yb!=0&&(yb=0);Cb=-1} +function Cu(){this.a=new $wnd.Map} +function UC(){this.c=new $wnd.Map} +function PA(a){dB(a.a);return a.g} +function TA(a){dB(a.a);return a.c} +function MA(a,b){return $A(a.a,b)} +function MB(a,b){return $A(a.a,b)} +function yB(a,b){return $A(a.a,b)} +function sy(a,b){return Wl(a.b,b)} +function gm(a,b){return Nc(a.b[b])} +function Qx(a,b){return qx(b.a,a)} +function Ub(a){return !!a.b||!!a.g} +function $j(a,b){this.b=a;this.a=b} +function fn(a,b){this.b=a;this.a=b} +function kn(a,b){this.a=a;this.b=b} +function mn(a,b){this.a=a;this.b=b} +function on(a,b){this.a=a;this.b=b} +function qn(a,b){this.a=a;this.b=b} +function sn(a,b){this.a=a;this.b=b} +function so(a,b){this.a=a;this.b=b} +function Il(a,b){this.a=a;this.b=b} +function cm(a,b){this.a=a;this.b=b} +function em(a,b){this.a=a;this.b=b} +function tm(a,b){this.a=a;this.b=b} +function vm(a,b){this.a=a;this.b=b} +function Cs(a,b){this.a=a;this.b=b} +function Es(a,b){this.a=a;this.b=b} +function xo(a,b){this.b=a;this.a=b} +function zo(a,b){this.b=a;this.a=b} +function Xr(a,b){this.b=a;this.a=b} +function yu(a,b){this.b=a;this.a=b} +function ip(a,b){this.b=a;this.c=b} +function Mu(a,b){this.a=a;this.b=b} +function Ou(a,b){this.a=a;this.b=b} +function zv(a,b){this.a=a;this.b=b} +function Dv(a,b){this.a=a;this.b=b} +function Hv(a,b){this.a=a;this.b=b} +function Ny(a,b){this.b=a;this.a=b} +function Py(a,b){this.b=a;this.a=b} +function Vy(a,b){this.b=a;this.a=b} +function _y(a,b){this.b=a;this.a=b} +function dz(a,b){this.b=a;this.a=b} +function nz(a,b){this.a=a;this.b=b} +function pz(a,b){this.a=a;this.b=b} +function Hz(a,b){this.a=a;this.b=b} +function Zz(a,b){this.a=a;this.b=b} +function lA(a,b){this.a=a;this.b=b} +function qB(a,b){this.a=a;this.b=b} +function xB(a,b){this.d=a;this.e=b} +function nA(a,b){this.b=a;this.a=b} +function dC(a,b){this.a=a;this.b=b} +function CC(a,b){this.a=a;this.b=b} +function FC(a,b){this.a=a;this.b=b} +function Fq(a,b){ip.call(this,a,b)} +function sp(a,b){ip.call(this,a,b)} +function vD(a,b){ip.call(this,a,b)} +function DD(a,b){ip.call(this,a,b)} +function lH(a,b){ip.call(this,a,b)} +function nH(a,b){this.a=a;this.b=b} +function HH(a,b){this.a=a;this.b=b} +function OH(a,b){this.b=a;this.a=b} +function bE(c,a,b){c.setItem(a,b)} +function QH(a,b,c){a.splice(b,0,c)} +function Rt(a,b,c,d){Qt(a,b.d,c,d)} +function Kx(a,b,c){Yx(a,b);zx(c.e)} +function _q(a,b){Tq(a,(qr(),or),b)} +function VC(a){OC(a.a,a.d,a.c,a.b)} +function Nb(a){$wnd.clearTimeout(a)} +function nj(a){$wnd.clearTimeout(a)} +function dE(b,a){b.clearTimeout(a)} +function cE(b,a){b.clearInterval(a)} +function dx(b,a){Yw();delete b[a]} +function gA(a,b){lC(b);dA.delete(a)} +function LF(a,b){return a.substr(b)} +function xp(a,b){return vp(b,wp(a))} +function hF(a){return ad((WH(a),a))} +function Yc(a){return typeof a===mI} +function H(a,b){return _c(a)===_c(b)} +function _c(a){return a==null?null:a} +function bd(a){ZH(a==null);return a} +function pA(a){a.length=0;return a} +function RF(a,b){a.a+=''+b;return a} +function SF(a,b){a.a+=''+b;return a} +function TF(a,b){a.a+=''+b;return a} +function CH(a,b,c){oH(b,c);return b} +function gr(a,b){Tq(a,(qr(),pr),b.a)} +function nm(a,b){return a.a.has(b.d)} +function EF(a,b){return a.indexOf(b)} +function aE(b,a){return b.getItem(a)} +function jE(a){return a&&a.valueOf()} +function lE(a){return a&&a.valueOf()} +function FG(a){return a!=null?O(a):0} +function mj(a){$wnd.clearInterval(a)} +function U(a){a.h=zc(mi,pI,31,0,0,1)} +function hk(a){gk&&VD($wnd.console,a)} +function jk(a){gk&&WD($wnd.console,a)} +function nk(a){gk&&XD($wnd.console,a)} +function ok(a){gk&&YD($wnd.console,a)} +function Fo(a){gk&&WD($wnd.console,a)} +function Fr(a){this.a=a;lj.call(this)} +function vs(a){this.a=a;lj.call(this)} +function dt(a){this.a=a;lj.call(this)} +function Dt(a){this.a=new UC;this.c=a} +function ww(){ww=bj;vw=new $wnd.Map} +function Yw(){Yw=bj;Xw=new $wnd.Map} +function HG(){HG=bj;GG=new JG(null)} +function FE(){FE=bj;DE=false;EE=true} +function Xq(a){!!a.b&&er(a,(qr(),nr))} +function ar(a){!!a.b&&er(a,(qr(),or))} +function jr(a){!!a.b&&er(a,(qr(),pr))} +function jl(a){Yo((Qb(),Pb),new Ol(a))} +function bm(a,b){Ic(tk(a,ze),29).ab(b)} +function bB(a,b){return a.a.delete(b)} +function qv(a,b){return a.h.delete(b)} +function sv(a,b){return a.b.delete(b)} +function qy(a,b,c){return fy(a,c.a,b)} +function MH(a,b,c){return CH(a.a,b,c)} +function DH(a,b,c){JH(a,MH(b,a.a,c))} +function Px(a,b){var c;c=qx(b,a);kC(c)} +function ry(a,b){return Jm(a.b.root,b)} +function rA(){return new $wnd.WeakMap} +function QF(a){return a==null?sI:ej(a)} +function as(a){return xJ in a?a[xJ]:-1} +function is(a){Yo((Qb(),Pb),new Is(a))} +function xn(a){Yo((Qb(),Pb),new Fn(a))} +function Pp(a){Yo((Qb(),Pb),new Qp(a))} +function cq(a){Yo((Qb(),Pb),new sq(a))} +function wy(a){Yo((Qb(),Pb),new Uz(a))} +function WF(a){vE.call(this,(WH(a),a))} +function ab(){U(this);V(this);this.F()} +function qG(){this.a=zc(ji,pI,1,0,5,1)} +function fI(){fI=bj;cI=new I;eI=new I} +function TH(a){if(!a){throw Ui(new zE)}} +function UH(a){if(!a){throw Ui(new DG)}} +function ZH(a){if(!a){throw Ui(new cF)}} +function at(a){if(a.a){ij(a.a);a.a=null}} +function AB(a,b){dB(a.a);a.c.forEach(b)} +function NB(a,b){dB(a.a);a.b.forEach(b)} +function jC(a){if(a.d||a.e){return}hC(a)} +function OD(a,b,c,d){return GD(a,b,c,d)} +function FF(a,b,c){return a.indexOf(b,c)} +function GF(a,b){return a.lastIndexOf(b)} +function PD(a,b){return a.appendChild(b)} +function QD(b,a){return b.appendChild(a)} +function IG(a,b){return a.a!=null?a.a:b} +function Sc(a,b){return a!=null&&Hc(a,b)} +function tb(a){return a==null?null:a.name} +function bI(a){return a.$H||(a.$H=++aI)} +function Jn(a){return ''+Kn(Hn.pb()-a,3)} +function Uc(a){return typeof a==='number'} +function Xc(a){return typeof a==='string'} +function MF(a,b,c){return a.substr(b,c-b)} +function ql(a,b,c){fl();return a.set(c,b)} +function ZD(d,a,b,c){d.pushState(a,b,c)} +function ND(d,a,b,c){d.setProperty(a,b,c)} +function Zu(a,b){GD(b,kJ,new fv(a),false)} +function $s(a,b){b.a.b==(rp(),qp)&&at(a)} +function KE(a){if(a.i!=null){return}YE(a)} +function Kc(a){ZH(a==null||Uc(a));return a} +function Jc(a){ZH(a==null||Tc(a));return a} +function Lc(a){ZH(a==null||Yc(a));return a} +function Pc(a){ZH(a==null||Xc(a));return a} +function rl(a){fl();el==0?a.I():dl.push(a)} +function Pk(a){a.f=[];a.g=[];a.a=0;a.b=xb()} +function dB(a){var b;b=sC;!!b&&fC(b,a.b)} +function hp(a){return a.b!=null?a.b:''+a.c} +function Tc(a){return typeof a==='boolean'} +function SD(b,a){return b.createElement(a)} +function kc(a){gc();return parseInt(a)||-1} +function $D(d,a,b,c){d.replaceState(a,b,c)} +function Jz(a,b){uy(a.a,a.c,a.d,a.b,Pc(b))} +function Ho(a,b){Io(a,b,Ic(tk(a.a,ud),9).n)} +function sB(a,b){GA.call(this,a);this.a=b} +function BH(a,b){wH.call(this,a);this.a=b} +function eB(a){this.a=new $wnd.Set;this.b=a} +function im(){this.a=new $wnd.Map;this.b=[]} +function wC(a){tC==null&&(tC=[]);tC.push(a)} +function xC(a){vC==null&&(vC=[]);vC.push(a)} +function sb(a){return a==null?null:a.message} +function $c(a,b){return a&&b&&a instanceof b} +function HF(a,b,c){return a.lastIndexOf(b,c)} +function qj(a,b){return $wnd.setInterval(a,b)} +function rj(a,b){return $wnd.setTimeout(a,b)} +function HE(a,b){return WH(a),_c(a)===_c(b)} +function CF(a,b){return WH(a),_c(a)===_c(b)} +function Eb(a,b,c){return a.apply(b,c);var d} +function Xb(a,b){a.b=Zb(a.b,[b,false]);Vb(a)} +function Ar(a,b){b.a.b==(rp(),qp)&&Dr(a,-1)} +function cp(){this.b=(rp(),op);this.a=new UC} +function Do(a,b,c){this.a=a;this.b=b;this.c=c} +function uq(a,b,c){this.a=a;this.c=b;this.b=c} +function zw(a,b,c){this.a=a;this.c=b;this.g=c} +function Vw(a,b,c){this.b=a;this.a=b;this.c=c} +function Ty(a,b,c){this.b=a;this.c=b;this.a=c} +function Ry(a,b,c){this.c=a;this.b=b;this.a=c} +function Xy(a,b,c){this.a=a;this.b=b;this.c=c} +function hz(a,b,c){this.a=a;this.b=b;this.c=c} +function jz(a,b,c){this.a=a;this.b=b;this.c=c} +function lz(a,b,c){this.a=a;this.b=b;this.c=c} +function xz(a,b,c){this.c=a;this.b=b;this.a=c} +function Dz(a,b,c){this.b=a;this.a=b;this.c=c} +function Xz(a,b,c){this.b=a;this.a=b;this.c=c} +function sr(a,b,c){ip.call(this,a,b);this.a=c} +function Nr(a,b,c){a.ib(pF(QA(Ic(c.e,14),b)))} +function mt(a,b,c){a.set(c,(dB(b.a),Pc(b.g)))} +function xk(a,b,c){wk(a,b,c._());a.b.set(b,c)} +function Ic(a,b){ZH(a==null||Hc(a,b));return a} +function Oc(a,b){ZH(a==null||$c(a,b));return a} +function gE(a){if(a==null){return 0}return +a} +function jv(a,b){a.b.add(b);return new Hv(a,b)} +function kv(a,b){a.h.add(b);return new Dv(a,b)} +function WA(a,b){a.d=true;NA(a,b);xC(new mB(a))} +function lC(a){a.e=true;hC(a);a.c.clear();gC(a)} +function Bw(a){a.b?cE($wnd,a.c):dE($wnd,a.c)} +function MG(a){HG();return !a?GG:new JG(WH(a))} +function oj(a,b){return jI(function(){a.M(b)})} +function Qw(a,b){return Rw(new Tw(a),b,19,true)} +function sm(a,b,c){return a.set(c,(dB(b.a),b.g))} +function LD(b,a){return b.getPropertyValue(a)} +function Kp(a){return $wnd.Vaadin.Flow.getApp(a)} +function lb(a){U(this);this.g=a;V(this);this.F()} +function eu(a){au();this.c=[];this.a=_t;this.d=a} +function ou(a,b){this.a=a;this.b=b;lj.call(this)} +function lr(a,b){this.a=a;this.b=b;lj.call(this)} +function mG(a,b){a.a[a.a.length]=b;return true} +function nG(a,b){VH(b,a.a.length);return a.a[b]} +function RE(a,b){var c;c=OE(a,b);c.e=2;return c} +function Ws(a,b){var c;c=ad(gF(Kc(b.a)));_s(a,c)} +function MC(a,b,c,d){var e;e=QC(a,b,c);e.push(d)} +function KC(a,b){a.a==null&&(a.a=[]);a.a.push(b)} +function uk(a,b,c){a.a.delete(c);a.a.set(c,b._())} +function JD(a,b,c,d){a.removeEventListener(b,c,d)} +function KD(b,a){return b.getPropertyPriority(a)} +function Bc(a){return Array.isArray(a)&&a.pc===fj} +function Rc(a){return !Array.isArray(a)&&a.pc===fj} +function Vc(a){return a!=null&&Zc(a)&&!(a.pc===fj)} +function BG(a){return new BH(null,AG(a,a.length))} +function AG(a,b){return PG(b,a.length),new $G(a,b)} +function nl(a){++el;Zn(Ic(tk(a.a,we),57),new Gl)} +function Su(a){a.a=yt(Ic(tk(a.d,Gf),13),new Wu(a))} +function sj(a){a.onreadystatechange=function(){}} +function ik(a){$wnd.setTimeout(function(){a.N()},0)} +function Lv(a,b){var c;c=b;return Ic(a.a.get(c),6)} +function PE(a,b,c){var d;d=OE(a,b);aF(c,d);return d} +function OE(a,b){var c;c=new ME;c.f=a;c.d=b;return c} +function Zb(a,b){!a&&(a=[]);a[a.length]=b;return a} +function fl(){fl=bj;dl=[];bl=new ul;cl=new zl} +function rF(){rF=bj;qF=zc(ei,pI,27,256,0,1)} +function iI(){if(dI==256){cI=eI;eI=new I;dI=0}++dI} +function WH(a){if(a==null){throw Ui(new sF)}return a} +function Mc(a){ZH(a==null||Array.isArray(a));return a} +function zx(a){var b;b=a.a;tv(a,null);tv(a,b);tw(a)} +function UG(a,b){WH(b);while(a.c=0){a.a=new dt(a);kj(a.a,b)}} +function wH(a){if(!a){this.b=null;new qG}else{this.b=a}} +function Kz(a,b,c,d){this.a=a;this.c=b;this.d=c;this.b=d} +function ZC(a,b,c,d){this.a=a;this.d=b;this.c=c;this.b=d} +function As(a,b,c,d){this.a=a;this.d=b;this.b=c;this.c=d} +function TD(a,b,c,d){this.b=a;this.c=b;this.a=c;this.d=d} +function $G(a,b){this.c=0;this.d=b;this.b=17488;this.a=a} +function bt(a){this.b=a;ap(Ic(tk(a,Ke),11),new ft(this))} +function Ut(a,b){var c;c=Ic(tk(a.a,Of),36);bu(c,b);du(c)} +function Tv(a,b,c,d){Ov(a,b)&&Rt(Ic(tk(a.c,Kf),28),b,c,d)} +function Sq(a,b){Jo(Ic(tk(a.c,Fe),22),'',b,'',null,null)} +function Io(a,b,c){Jo(a,c.caption,c.message,b,c.url,null)} +function Cc(a,b,c){TH(c==null||wc(a,c));return a[b]=c} +function Nc(a){ZH(a==null||Zc(a)&&!(a.pc===fj));return a} +function V(a){if(a.j){a.e!==qI&&a.F();a.h=null}return a} +function WC(a,b,c){this.a=a;this.d=b;this.c=null;this.b=c} +function XC(a,b,c){this.a=a;this.d=b;this.c=null;this.b=c} +function zC(a,b){var c;c=sC;sC=a;try{b.I()}finally{sC=c}} +function $(a,b){var c;c=LE(a.nc);return b==null?c:c+': '+b} +function BF(a,b){YH(b,a.length);return a.charCodeAt(b)} +function Xm(a,b,c,d,e){a.splice.apply(a,[b,c,d].concat(e))} +function Mk(a,b,c){Xk(Dc(xc(cd,1),pI,90,15,[b,c]));VC(a.e)} +function tp(){rp();return Dc(xc(Je,1),pI,60,0,[op,pp,qp])} +function tr(){qr();return Dc(xc(Xe,1),pI,63,0,[nr,or,pr])} +function ED(){CD();return Dc(xc(Ih,1),pI,43,0,[AD,zD,BD])} +function mH(){kH();return Dc(xc(Gi,1),pI,48,0,[hH,iH,jH])} +function xH(a,b){var c;return AH(a,new qG,(c=new NH(b),c))} +function XH(a,b){if(a<0||a>b){throw Ui(new xE(tK+a+uK+b))}} +function Kw(a,b){vA(b).forEach(cj(Ow.prototype.ib,Ow,[a]))} +function rv(a,b){_c(b.U(a))===_c((FE(),EE))&&a.b.delete(b)} +function ho(a,b,c){this.a=a;this.c=b;this.b=c;lj.call(this)} +function jo(a,b,c){this.a=a;this.c=b;this.b=c;lj.call(this)} +function fo(a,b,c){this.b=a;this.d=b;this.c=c;this.a=new R} +function fE(c,a,b){return c.setTimeout(jI(a.Yb).bind(a),b)} +function eE(c,a,b){return c.setInterval(jI(a.Yb).bind(a),b)} +function Qc(a){return a.nc||Array.isArray(a)&&xc(fd,1)||fd} +function BA(a){if(!zA){return a}return $wnd.Polymer.dom(a)} +function WE(a){if(a.cc()){return null}var b=a.h;return $i[b]} +function Km(a){var b;b=a.f;while(!!b&&!b.a){b=b.f}return b} +function gc(){gc=bj;var a,b;b=!mc();a=new uc;fc=b?new nc:a} +function ID(a,b){Rc(a)?a.nb(b):(a.handleEvent(b),undefined)} +function VH(a,b){if(a<0||a>=b){throw Ui(new xE(tK+a+uK+b))}} +function YH(a,b){if(a<0||a>=b){throw Ui(new XF(tK+a+uK+b))}} +function Or(a){ek('applyDefaultTheme',(FE(),a?true:false))} +function $r(a){a&&a.afterServerUpdate&&a.afterServerUpdate()} +function hq(a){$wnd.vaadinPush.atmosphere.unsubscribeUrl(a)} +function Tx(a,b,c){return a.push(OA(OB(ov(b.e,1),c),b.b[c]))} +function yA(a,b,c,d){return a.splice.apply(a,[b,c].concat(d))} +function OC(a,b,c,d){a.b>0?KC(a,new ZC(a,b,c,d)):PC(a,b,c,d)} +function NA(a,b){if(!a.b&&a.c&&EG(b,a.g)){return}XA(a,b,true)} +function cu(a){a.a=_t;if(!a.b){return}Qs(Ic(tk(a.d,uf),19))} +function UE(a,b){var c=a.a=a.a||[];return c[b]||(c[b]=a.Zb(b))} +function Hw(a,b){vA(b).forEach(cj(Mw.prototype.ib,Mw,[a.a]))} +function dj(a){function b(){} +;b.prototype=a||{};return new b} +function QE(a,b,c,d){var e;e=OE(a,b);aF(c,e);e.e=d?8:0;return e} +function Lk(a){var b;b={};b[GI]=mE(a.a);b[HI]=mE(a.b);return b} +function AC(a){this.a=a;this.b=[];this.c=new $wnd.Set;hC(this)} +function AE(a,b){U(this);this.f=b;this.g=a;V(this);this.F()} +function rb(a){pb();nb.call(this,a);this.a='';this.b=a;this.a=''} +function Cp(a){a?($wnd.location=a):$wnd.location.reload(false)} +function jq(){return $wnd.vaadinPush&&$wnd.vaadinPush.atmosphere} +function xq(a,b,c){return MF(a.b,b,$wnd.Math.min(a.b.length,c))} +function aD(a,b,c,d){return cD(new $wnd.XMLHttpRequest,a,b,c,d)} +function wD(){uD();return Dc(xc(Hh,1),pI,44,0,[tD,rD,sD,qD])} +function Gq(){Eq();return Dc(xc(Qe,1),pI,52,0,[Bq,Aq,Dq,Cq])} +function hD(a){if(a.length>2){lD(a[0],'OS major');lD(a[1],hK)}} +function VA(a){if(a.c){a.d=true;XA(a,null,false);xC(new oB(a))}} +function vG(a){UH(a.a-1} +function ad(a){return Math.max(Math.min(a,2147483647),-2147483648)|0} +function yD(){yD=bj;xD=jp((uD(),Dc(xc(Hh,1),pI,44,0,[tD,rD,sD,qD])))} +function Zq(a){Dr(Ic(tk(a.c,df),56),Ic(tk(a.c,ud),9).f);Tq(a,(qr(),nr),null)} +function M(a){return Xc(a)?pi:Uc(a)?Zh:Tc(a)?Wh:Rc(a)?a.nc:Bc(a)?a.nc:Qc(a)} +function RH(a,b){return yc(b)!=10&&Dc(M(b),b.oc,b.__elementTypeId$,yc(b),a),a} +function Ep(a,b,c){c==null?BA(a).removeAttribute(b):BA(a).setAttribute(b,c)} +function PC(a,b,c,d){var e,f;e=RC(a,b,c);f=qA(e,d);f&&e.length==0&&TC(a,b,c)} +function hw(a,b){var c,d,e;e=ad(lE(a[SJ]));d=ov(b,e);c=a['key'];return OB(d,c)} +function AH(a,b,c){var d;uH(a);d=new KH;d.a=b;a.a.lc(new OH(d,c));return d.a} +function zc(a,b,c,d,e,f){var g;g=Ac(e,d);e!=10&&Dc(xc(a,f),b,c,e,g);return g} +function CB(a,b,c,d){var e,f;e=d;f=yA(a.c,b,c,e);aB(a.a,new IA(a,b,f,d,false))} +function pv(a,b,c,d){var e;e=c.Xb();!!e&&(b[Kv(a.g,ad((WH(d),d)))]=e,undefined)} +function oG(a,b,c){for(;ca||a>b){throw Ui(new yE('fromIndex: 0, toIndex: '+a+', length: '+b))}} +function xF(a,b,c){if(a==null){debugger;throw Ui(new BE)}this.a=yI;this.d=a;this.b=b;this.c=c} +function Vv(a,b,c,d,e){if(!Jv(a,b)){debugger;throw Ui(new BE)}Tt(Ic(tk(a.c,Kf),28),b,c,d,e)} +function Uv(a,b,c,d,e,f){if(!Jv(a,b)){debugger;throw Ui(new BE)}St(Ic(tk(a.c,Kf),28),b,c,d,e,f)} +function Hx(a,b,c,d){var e,f,g;g=c[LJ];e="path='"+wb(g)+"'";f=new nz(a,g);yx(a,b,d,f,null,e)} +function Qv(a,b){var c;if(b!=a.e){c=b.a;!!c&&(Yw(),!!c[RJ])&&cx((Yw(),c[RJ]));Yv(a,b);b.f=null}} +function Xx(a,b){var c;c=Ic(b.d.get(a),45);b.d.delete(a);if(!c){debugger;throw Ui(new BE)}c.Jb()} +function rx(a,b,c,d){var e;e=ov(d,a);NB(e,cj(Ny.prototype.eb,Ny,[b,c]));return MB(e,new Py(b,c))} +function Vp(c,a){var b=c.getConfig(a);if(b===null||b===undefined){return null}else{return b+''}} +function Up(c,a){var b=c.getConfig(a);if(b===null||b===undefined){return null}else{return pF(b)}} +function nu(b){if(b.readyState!=1){return false}try{b.send();return true}catch(a){return false}} +function du(a){if(_t!=a.a||a.c.length==0){return}a.b=true;a.a=new fu(a);Yo((Qb(),Pb),new ju(a))} +function Vb(a){if(!a.i){a.i=true;!a.f&&(a.f=new bc(a));_b(a.f,1);!a.h&&(a.h=new dc(a));_b(a.h,50)}} +function Sj(a,b){if(!b){Ns(Ic(tk(a.a,uf),19))}else{Ct(Ic(tk(a.a,Gf),13));ds(Ic(tk(a.a,sf),20),b)}} +function $q(a,b,c){_p(b)&&zt(Ic(tk(a.c,Gf),13));dr(c)||Uq(a,'Invalid JSON from server: '+c,null)} +function Dr(a,b){gk&&XD($wnd.console,'Setting heartbeat interval to '+b+'sec.');a.a=b;Br(a)} +function HC(b,c,d){return jI(function(){var a=Array.prototype.slice.call(arguments);d.Fb(b,c,a)})} +function _b(b,c){Qb();function d(){var a=jI(Yb)(b);a&&$wnd.setTimeout(d,c)} +$wnd.setTimeout(d,c)} +function _v(a,b){var c;if(Sc(a,30)){c=Ic(a,30);ad((WH(b),b))==2?BB(c,(dB(c.a),c.c.length)):zB(c)}} +function Ti(a){var b;if(Sc(a,5)){return a}b=a&&a.__java$exception;if(!b){b=new rb(a);hc(b)}return b} +function vp(a,b){var c;if(a==null){return null}c=up('context://',b,a);c=up('base://','',c);return c} +function HD(b){var c=b.handler;if(!c){c=jI(function(a){ID(b,a)});c.listener=b;b.handler=c}return c} +function iE(c){return $wnd.JSON.stringify(c,function(a,b){if(a=='$H'){return undefined}return b},0)} +function fs(a,b){if(b==-1){return true}if(b==a.f+1){return true}if(a.f==-1){return true}return false} +function pD(a,b,c){var d,e;b<0?(e=0):(e=b);c<0||c>a.length?(d=a.length):(d=c);return a.substr(e,d-e)} +function Wn(a,b){var c,d;c=new oo(a);d=new $wnd.Function(a);eo(a,new vo(d),new xo(b,c),new zo(b,c))} +function ll(a,b){var c;c=new $wnd.Map;b.forEach(cj(Il.prototype.eb,Il,[a,c]));c.size==0||rl(new Ml(c))} +function Aj(a,b){var c;c='/'.length;if(!CF(b.substr(b.length-c,c),'/')){debugger;throw Ui(new BE)}a.c=b} +function Hu(a,b){var c;c=!!b.a&&!HE((FE(),DE),PA(OB(ov(b,0),KJ)));if(!c||!b.f){return c}return Hu(a,b.f)} +function jx(a,b){var c;if(b.d.has(a)){debugger;throw Ui(new BE)}c=OD(b.b,a,new Fz(b),false);b.d.set(a,c)} +function Qt(a,b,c,d){var e;e={};e[RI]=FJ;e[GJ]=Object(b);e[FJ]=c;!!d&&(e['data']=d,undefined);Ut(a,e)} +function Dc(a,b,c,d,e){e.nc=a;e.oc=b;e.pc=fj;e.__elementTypeId$=c;e.__elementTypeCategory$=d;return e} +function By(a,b,c,d){if(d==null){!!c&&(delete c['for'],undefined)}else{!c&&(c={});c['for']=d}Tv(a.g,a,b,c)} +function bq(a,b,c){DF(b,'true')||DF(b,'false')?(a.a[c]=DF(b,'true'),undefined):(a.a[c]=b,undefined)} +function br(a,b){gk&&($wnd.console.log('Reopening push connection'),undefined);_p(b)&&Tq(a,(qr(),or),null)} +function cr(a,b){Jo(Ic(tk(a.c,Fe),22),'',b+' could not be loaded. Push will not work.','',null,null)} +function Bt(a){var b,c;c=Ic(tk(a.c,Ke),11).b==(rp(),qp);b=a.b||Ic(tk(a.c,Of),36).b;(c||!b)&&fk('connected')} +function Y(a){var b,c,d,e;for(b=(a.h==null&&(a.h=(gc(),e=fc.K(a),ic(e))),a.h),c=0,d=b.length;c-129&&a<128){b=a+128;c=(rF(),qF)[b];!c&&(c=qF[b]=new lF(a));return c}return new lF(a)} +function Pv(a){var b,c;if(!a.c.has(0)){return true}c=ov(a,0);b=Jc(PA(OB(c,'visible')));return !HE((FE(),DE),b)} +function ux(a){var b,c;b=nv(a.e,24);for(c=0;c<(dB(b.a),b.c.length);c++){kx(a,Ic(b.c[c],6))}return yB(b,new bz(a))} +function Mv(a,b){var c,d,e;e=vA(a.a);for(c=0;c0){c=Ic(a.b.splice(0,1)[0],14);qm(c,b)||Wv(Ic(tk(a.c,gg),10),c);yC()}} +function pl(){fl();var a,b;--el;if(el==0&&dl.length!=0){try{for(b=0;b>>0,b.toString(16))}return a.toString()} +function cv(a){var b;if(!CF(kJ,a.type)){debugger;throw Ui(new BE)}b=a;return b.altKey||b.ctrlKey||b.metaKey||b.shiftKey} +function Uu(a,b,c){if(a==null){debugger;throw Ui(new BE)}if(b==null){debugger;throw Ui(new BE)}this.c=a;this.b=b;this.d=c} +function Ct(a){if(a.b){throw Ui(new kF('Trying to start a new request while another is active'))}a.b=true;At(a,new Gt)} +function Pm(a){while(a.parentNode&&(a=a.parentNode)){if(a.toString()==='[object ShadowRoot]'){return true}}return false} +function qm(a,b){var c,d;c=Oc(b.get(a.e.e.d),$wnd.Map);if(c!=null&&c.has(a.f)){d=c.get(a.f);WA(a,d);return true}return false} +function ox(a,b){var c,d;c=nv(b,11);for(d=0;d<(dB(c.a),c.c.length);d++){BA(a).classList.add(Pc(c.c[d]))}return yB(c,new Mz(a))} +function Jp(a){var b,c,d,e;b=(e=new Pj,e.a=a,Np(e,Kp(a)),e);c=new Tj(b);Gp.push(c);d=Kp(a).getConfig('uidl');Sj(c,d)} +function wp(a){var b,c;b=Ic(tk(a.a,ud),9).c;c='/'.length;if(!CF(b.substr(b.length-c,c),'/')){debugger;throw Ui(new BE)}return b} +function ax(a,b){if(typeof a.get===mI){var c=a.get(b);if(typeof c===kI&&typeof c[aJ]!==uI){return {nodeId:c[aJ]}}}return null} +function OB(a,b){var c;c=Ic(a.b.get(b),14);if(!c){c=new YA(b,a,CF('innerHTML',b)&&a.d==1);a.b.set(b,c);aB(a.a,new sB(a,c))}return c} +function cx(c){Yw();var b=c['}p'].promises;b!==undefined&&b.forEach(function(a){a[1](Error('Client is resynchronizing'))})} +function dk(){return /iPad|iPhone|iPod/.test(navigator.platform)||navigator.platform==='MacIntel'&&navigator.maxTouchPoints>1} +function ck(){this.a=new nD($wnd.navigator.userAgent);this.a.b?'ontouchstart' in window:this.a.f?!!navigator.msMaxTouchPoints:bk()} +function $n(a){this.b=new $wnd.Set;this.a=new $wnd.Map;this.d=!!($wnd.HTMLImports&&$wnd.HTMLImports.whenReady);this.c=a;Tn(this)} +function kr(a){this.c=a;ap(Ic(tk(a,Ke),11),new ur(this));GD($wnd,'offline',new wr(this),false);GD($wnd,'online',new yr(this),false)} +function uD(){uD=bj;tD=new vD('STYLESHEET',0);rD=new vD('JAVASCRIPT',1);sD=new vD('JS_MODULE',2);qD=new vD('DYNAMIC_IMPORT',3)} +function Hm(a){var b;if(Bm==null){return}b=Oc(Bm.get(a),$wnd.Set);if(b!=null){Bm.delete(a);b.forEach(cj(bn.prototype.ib,bn,[]))}} +function hC(a){var b;a.d=true;gC(a);a.e||wC(new mC(a));if(a.c.size!=0){b=a.c;a.c=new $wnd.Set;b.forEach(cj(qC.prototype.ib,qC,[]))}} +function Wt(a,b,c,d,e){var f;f={};f[RI]='mSync';f[GJ]=mE(b.d);f['feature']=Object(c);f['property']=d;f[ZI]=e==null?null:e;Ut(a,f)} +function Xj(a,b,c){var d;if(a==c.d){d=new $wnd.Function('callback','callback();');d.call(null,b);return FE(),true}return FE(),false} +function mc(){if(Error.stackTraceLimit>0){$wnd.Error.stackTraceLimit=Error.stackTraceLimit=64;return true}return 'stack' in new Error} +function zm(a){return typeof a.update==mI&&a.updateComplete instanceof Promise&&typeof a.shouldUpdate==mI&&typeof a.firstUpdated==mI} +function iF(a){var b;b=eF(a);if(b>3.4028234663852886E38){return Infinity}else if(b<-3.4028234663852886E38){return -Infinity}return b} +function IE(a){if(a>=48&&a<48+$wnd.Math.min(10,10)){return a-48}if(a>=97&&a<97){return a-97+10}if(a>=65&&a<65){return a-65+10}return -1} +function _E(a,b){var c=0;while(!b[c]||b[c]==''){c++}var d=b[c++];for(;cd&&Cc(b,d,null);return b} +function DF(a,b){WH(a);if(b==null){return false}if(CF(a,b)){return true}return a.length==b.length&&CF(a.toLowerCase(),b.toLowerCase())} +function kE(b){var c;try{return c=$wnd.JSON.parse(b),c}catch(a){a=Ti(a);if(Sc(a,7)){throw Ui(new pE("Can't parse "+b))}else throw Ui(a)}} +function Rk(a){this.d=a;'scrollRestoration' in history&&(history.scrollRestoration='manual');GD($wnd,NI,new Bo(this),false);Ok(this,true)} +function Eq(){Eq=bj;Bq=new Fq('CONNECT_PENDING',0);Aq=new Fq('CONNECTED',1);Dq=new Fq('DISCONNECT_PENDING',2);Cq=new Fq('DISCONNECTED',3)} +function er(a,b){if(a.b!=b){return}a.b=null;a.a=0;fk('connected');gk&&($wnd.console.log('Re-established connection to server'),undefined)} +function Tt(a,b,c,d,e){var f;f={};f[RI]='attachExistingElementById';f[GJ]=mE(b.d);f[HJ]=Object(c);f[IJ]=Object(d);f['attachId']=e;Ut(a,f)} +function kl(a){gk&&($wnd.console.log('Finished loading eager dependencies, loading lazy.'),undefined);a.forEach(cj(Ql.prototype.eb,Ql,[]))} +function Cr(a){ij(a.c);gk&&($wnd.console.debug('Sending heartbeat request...'),undefined);aD(a.d,null,'text/plain; charset=utf-8',new Hr(a))} +function Rv(a){AB(nv(a.e,24),cj(bw.prototype.ib,bw,[]));lv(a.e,cj(fw.prototype.eb,fw,[]));a.a.forEach(cj(dw.prototype.eb,dw,[a]));a.d=true} +function hI(a){fI();var b,c,d;c=':'+a;d=eI[c];if(d!=null){return ad((WH(d),d))}d=cI[c];b=d==null?gI(a):ad((WH(d),d));iI();eI[c]=b;return b} +function O(a){return Xc(a)?hI(a):Uc(a)?ad((WH(a),a)):Tc(a)?(WH(a),a)?1231:1237:Rc(a)?a.t():Bc(a)?bI(a):!!a&&!!a.hashCode?a.hashCode():bI(a)} +function wk(a,b,c){if(a.a.has(b)){debugger;throw Ui(new CE((KE(b),'Registry already has a class of type '+b.i+' registered')))}a.a.set(b,c)} +function rw(a,b){qw();var c;if(a.g.f){debugger;throw Ui(new CE('Binding state node while processing state tree changes'))}c=sw(a);c.Mb(a,b,ow)} +function IA(a,b,c,d,e){this.e=a;if(c==null){debugger;throw Ui(new BE)}if(d==null){debugger;throw Ui(new BE)}this.c=b;this.d=c;this.a=d;this.b=e} +function Zx(a,b){var c,d;d=OB(b,_J);dB(d.a);d.c||WA(d,a.getAttribute(_J));c=OB(b,aK);Pm(a)&&(dB(c.a),!c.c)&&!!a.style&&WA(c,a.style.display)} +function Yl(a,b,c,d){var e,f;if(!d){f=Ic(tk(a.g.c,Vd),59);e=Ic(f.a.get(c),27);if(!e){f.b[b]=c;f.a.set(c,pF(b));return pF(b)}return e}return d} +function ky(a,b){var c,d;while(b!=null){for(c=a.length-1;c>-1;c--){d=Ic(a[c],6);if(b.isSameNode(d.a)){return d.d}}b=BA(b.parentNode)}return -1} +function _l(a,b,c){var d;if(Zl(a.a,c)){d=Ic(a.e.get(Zg),78);if(!d||!d.a.has(c)){return}OA(OB(b,c),a.a[c]).N()}else{PB(b,c)||WA(OB(b,c),null)}} +function jm(a,b,c){var d,e;e=Lv(Ic(tk(a.c,gg),10),ad((WH(b),b)));if(e.c.has(1)){d=new $wnd.Map;NB(ov(e,1),cj(xm.prototype.eb,xm,[d]));c.set(b,d)}} +function QC(a,b,c){var d,e;e=Oc(a.c.get(b),$wnd.Map);if(e==null){e=new $wnd.Map;a.c.set(b,e)}d=Mc(e.get(c));if(d==null){d=[];e.set(c,d)}return d} +function jy(a){var b;hx==null&&(hx=new $wnd.Map);b=Lc(hx.get(a));if(b==null){b=Lc(new $wnd.Function(FJ,XJ,'return ('+a+')'));hx.set(a,b)}return b} +function ps(){if($wnd.performance&&$wnd.performance.timing){return (new Date).getTime()-$wnd.performance.timing.responseStart}else{return -1}} +function Sw(a,b,c,d){var e,f,g,h,i;i=Nc(a._());h=d.d;for(g=0;g=1&&lD(a[0],'OS major');if(a.length>=2){b=EF(a[1],OF(45));if(b>-1){c=a[1].substr(0,b-0);lD(c,hK)}else{lD(a[1],hK)}}} +function X(a,b,c){var d,e,f,g,h;Y(a);for(e=(a.i==null&&(a.i=zc(ri,pI,5,0,0,1)),a.i),f=0,g=e.length;f0){hk('Scheduling heartbeat in '+a.a+' seconds');jj(a.c,a.a*1000)}else{gk&&($wnd.console.debug('Disabling heartbeat'),undefined);ij(a.c)}} +function ht(a){var b,c,d,e;b=OB(ov(Ic(tk(a.a,gg),10).e,5),'parameters');e=(dB(b.a),Ic(b.g,6));d=ov(e,6);c=new $wnd.Map;NB(d,cj(tt.prototype.eb,tt,[c]));return c} +function Wv(a,b){var c,d;if(!b){debugger;throw Ui(new BE)}d=b.e;c=d.e;if(mm(Ic(tk(a.c,Xd),51),b)||!Ov(a,c)){return}Wt(Ic(tk(a.c,Kf),28),c,d.d,b.f,(dB(b.a),b.g))} +function dv(a,b){var c;c=$wnd.location.pathname;if(c==null){debugger;throw Ui(new CE('window.location.path should never be null'))}if(c!=a){return false}return b} +function LC(a,b,c){var d;if(!b){throw Ui(new uF('Cannot add a handler with a null type'))}a.b>0?KC(a,new XC(a,b,c)):(d=QC(a,b,null),d.push(c));return new WC(a,b,c)} +function Yx(a,b){var c,d,e;Zx(a,b);e=OB(b,_J);dB(e.a);e.c&&Cy(Ic(tk(b.e.g.c,ud),9),a,_J,(dB(e.a),e.g));c=OB(b,aK);dB(c.a);if(c.c){d=(dB(c.a),ej(c.g));MD(a.style,d)}} +function bp(a,b){if(b.c!=a.b.c+1){throw Ui(new jF('Tried to move from state '+hp(a.b)+' to '+(b.b!=null?b.b:''+b.c)+' which is not allowed'))}a.b=b;NC(a.a,new ep(a))} +function ss(a){var b;if(a==null){return null}if(!CF(a.substr(0,9),'for(;;);[')||(b=']'.length,!CF(a.substr(a.length-b,b),']'))){return null}return MF(a,9,a.length-1)} +function Yi(b,c,d,e){Xi();var f=Vi;$moduleName=c;$moduleBase=d;Si=e;function g(){for(var a=0;a=0;d--){if(CF(a[d].d,b)||CF(a[d].d,c)){a.length>=d+1&&a.splice(0,d+1);break}}return a} +function St(a,b,c,d,e,f){var g;g={};g[RI]='attachExistingElement';g[GJ]=mE(b.d);g[HJ]=Object(c);g[IJ]=Object(d);g['attachTagName']=e;g['attachIndex']=Object(f);Ut(a,g)} +function Qm(a){var b=typeof $wnd.Polymer===mI&&$wnd.Polymer.Element&&a instanceof $wnd.Polymer.Element;var c=a.constructor.polymerElementVersion!==undefined;return b||c} +function Rw(a,b,c,d){var e,f,g,h;h=nv(b,c);dB(h.a);if(h.c.length>0){f=Nc(a._());for(e=0;e<(dB(h.a),h.c.length);e++){g=Pc(h.c[e]);Zw(f,g,b,d)}}return yB(h,new Vw(a,b,d))} +function iy(a,b){var c,d,e,f,g;c=BA(b).childNodes;for(e=0;ed&&(YH(b-1,a.length),a.charCodeAt(b-1)<=32)){--b}return d>0||b=65536){b=55296+(a-65536>>10&1023)&65535;c=56320+(a-65536&1023)&65535;return String.fromCharCode(b)+(''+String.fromCharCode(c))}else{return String.fromCharCode(a&65535)}} +function Ib(a){a&&Sb((Qb(),Pb));--yb;if(yb<0){debugger;throw Ui(new CE('Negative entryDepth value at exit '+yb))}if(a){if(yb!=0){debugger;throw Ui(new CE('Depth not 0'+yb))}if(Cb!=-1){Nb(Cb);Cb=-1}}} +function zy(a,b,c,d){var e,f,g,h,i,j,k;e=false;for(h=0;h2000){Bb=a;Cb=$wnd.setTimeout(Ob,10)}}if(yb++==0){Rb((Qb(),Pb));return true}return false} +function yq(a){var b,c,d;if(a.a>=a.b.length){debugger;throw Ui(new BE)}if(a.a==0){c=''+a.b.length+'|';b=4095-c.length;d=c+MF(a.b,0,$wnd.Math.min(a.b.length,b));a.a+=b}else{d=xq(a,a.a,a.a+4095);a.a+=4095}return d} +function es(a){var b,c,d,e;if(a.h.length==0){return false}e=-1;for(b=0;b=f&&(YH(b,a.length),a.charCodeAt(b)!=32)){--b}if(b==f){return}d=a.substr(b+1,c-(b+1));e=KF(d,'\\.',0);hD(e)} +function Bu(a,b){var c,d,e,f,g,h;if(!b){debugger;throw Ui(new BE)}for(d=(g=oE(b),g),e=0,f=d.length;e=0;d--){TF((g.a+=i,g),Pc(c[d]));i='.'}return g.a} +function fq(a,b){var c,d,e,f,g;if(jq()){cq(b.a)}else{f=(Ic(tk(a.d,ud),9).j?(e='VAADIN/static/push/vaadinPush-min.js'):(e='VAADIN/static/push/vaadinPush.js'),e);gk&&XD($wnd.console,'Loading '+f);d=Ic(tk(a.d,we),57);g=Ic(tk(a.d,ud),9).l+f;c=new uq(a,f,b);Xn(d,g,c,false,WI)}} +function JC(a,b){var c,d,e,f,g,h;if(hE(b)==1){c=b;h=ad(lE(c[0]));switch(h){case 0:{g=ad(lE(c[1]));d=(f=g,Ic(a.a.get(f),6)).a;return d}case 1:return e=Mc(c[1]),e;case 2:return HC(ad(lE(c[1])),ad(lE(c[2])),Ic(tk(a.c,Kf),28));default:throw Ui(new jF(fK+iE(c)));}}else{return b}} +function bs(a,b){var c,d,e,f,g;gk&&($wnd.console.log('Handling dependencies'),undefined);c=new $wnd.Map;for(e=(CD(),Dc(xc(Ih,1),pI,43,0,[AD,zD,BD])),f=0,g=e.length;fa.a){a.a==0?gk&&XD($wnd.console,'Updating client-to-server id to '+b+' based on server'):ok('Server expects next client-to-server id to be '+b+' but we were going to use '+a.a+'. Will use '+b+'.');a.a=b}} +function bo(a,b,c){a.onload=jI(function(){a.onload=null;a.onerror=null;a.onreadystatechange=null;b.gb(c)});a.onerror=jI(function(){a.onload=null;a.onerror=null;a.onreadystatechange=null;b.fb(c)});a.onreadystatechange=function(){('loaded'===a.readyState||'complete'===a.readyState)&&a.onload(arguments[0])}} +function Os(a,b,c){var d,e,f,g,h,i,j,k;Ct(Ic(tk(a.c,Gf),13));i={};d=Ic(tk(a.c,sf),20).b;CF(d,'init')||(i['csrfToken']=d,undefined);i['rpc']=b;i[xJ]=mE(Ic(tk(a.c,sf),20).f);i[AJ]=mE(a.a++);if(c){for(f=(j=oE(c),j),g=0,h=f.length;g0){k=Cx(a,b);d=!k?null:BA(k.a).nextSibling}else{d=null}for(g=0;g=a.f.length||a.a>=a.g.length){ok('No matching scroll position found (entries X:'+a.f.length+', Y:'+a.g.length+') for opened history index ('+a.a+'). '+MI);Pk(a);return}c=hF(Kc(a.f[a.a]));d=hF(Kc(a.g[a.a]));b?(a.e=yt(Ic(tk(a.d,Gf),13),new Do(a,c,d))):Xk(Dc(xc(cd,1),pI,90,15,[c,d]))} +function Bx(b,c){var d,e,f,g,h;if(!c){return -1}try{h=BA(Nc(c));f=[];f.push(b);for(e=0;e0&&(YH(0,a.length),a.charCodeAt(0)==45||(YH(0,a.length),a.charCodeAt(0)==43))?1:0;for(b=e;b2147483647){throw Ui(new wF(qK+a+'"'))}return f} +function KF(a,b,c){var d,e,f,g,h,i,j,k;d=new RegExp(b,'g');j=zc(pi,pI,2,0,6,1);e=0;k=a;g=null;while(true){i=d.exec(k);if(i==null||k==''||e==c-1&&c>0){j[e]=k;break}else{h=i.index;j[e]=k.substr(0,h);k=MF(k,h+i[0].length,k.length);d.lastIndex=0;if(g==k){j[e]=k.substr(0,1);k=k.substr(1)}g=k;++e}}if(c==0&&a.length>0){f=j.length;while(f>0&&j[f-1]==''){--f}f=14&&c<=16));case 11:return b!=null&&Yc(b);case 12:return b!=null&&(typeof b===kI||typeof b==mI);case 0:return Hc(b,a.__elementTypeId$);case 2:return Zc(b)&&!(b.pc===fj);case 1:return Zc(b)&&!(b.pc===fj)||Hc(b,a.__elementTypeId$);default:return true;}} +function Wl(b,c){if(document.body.$&&document.body.$.hasOwnProperty&&document.body.$.hasOwnProperty(c)){return document.body.$[c]}else if(b.shadowRoot){return b.shadowRoot.getElementById(c)}else if(b.getElementById){return b.getElementById(c)}else if(c&&c.match('^[a-zA-Z0-9-_]*$')){return b.querySelector('#'+c)}else{return Array.from(b.querySelectorAll('[id]')).find(function(a){return a.id==c})}} +function eq(a,b){var c,d;if(!_p(a)){throw Ui(new kF('This server to client push connection should not be used to send client to server messages'))}if(a.f==(Eq(),Aq)){d=Dp(b);nk('Sending push ('+a.g+') message to server: '+d);if(CF(a.g,pJ)){c=new zq(d);while(c.a=QA((d=ov(Ic(tk(Ic(tk(a.c,Ef),37).a,gg),10).e,9),OB(d,'reconnectAttempts')),10000)?Rq(a):fr(a,c)} +function Xl(a,b,c,d){var e,f,g,h,i,j,k,l,m,n,o,p,q,r;j=null;g=BA(a.a).childNodes;o=new $wnd.Map;e=!b;i=-1;for(m=0;m. User has navigated out of site in an unrecognized way.');Pk(b)}}else{Pk(b)}} +function Cy(a,b,c,d){var e,f,g,h,i;if(d==null||Xc(d)){Ep(b,c,Pc(d))}else{f=d;if(0==hE(f)){g=f;if(!('uri' in g)){debugger;throw Ui(new CE("Implementation error: JsonObject is recieved as an attribute value for '"+c+"' but it has no "+'uri'+' key'))}i=g['uri'];if(a.q&&!i.match(/^(?:[a-zA-Z]+:)?\/\//)){e=a.l;e=(h='/'.length,CF(e.substr(e.length-h,h),'/')?e:e+'/');BA(b).setAttribute(c,e+(''+i))}else{i==null?BA(b).removeAttribute(c):BA(b).setAttribute(c,i)}}else{Ep(b,c,ej(d))}}} +function Gx(a,b,c){var d,e,f,g,h,i,j,k,l,m,n,o,p;p=Ic(c.e.get(Zg),78);if(!p||!p.a.has(a)){return}k=KF(a,'\\.',0);g=c;f=null;e=0;j=k.length;for(m=k,n=0,o=m.length;n=f){debugger;throw Ui(new BE)}return g.length==0?null:g}else{return a}} +function ly(a,b,c,d,e){var f,g,h;h=Lv(e,ad(a));if(!h.c.has(1)){return}if(!gy(h,b)){debugger;throw Ui(new CE('Host element is not a parent of the node whose property has changed. This is an implementation error. Most likely it means that there are several StateTrees on the same page (might be possible with portlets) and the target StateTree should not be passed into the method as an argument but somehow detected from the host element. Another option is that host element is calculated incorrectly.'))}f=ov(h,1);g=OB(f,c);OA(g,d).N()} +function Go(a,b,c,d){var e,f,g,h,i,j;h=$doc;j=h.createElement('div');j.className='v-system-error';if(a!=null){f=h.createElement('div');f.className='caption';f.textContent=a;j.appendChild(f);gk&&WD($wnd.console,a)}if(b!=null){i=h.createElement('div');i.className='message';i.textContent=b;j.appendChild(i);gk&&WD($wnd.console,b)}if(c!=null){g=h.createElement('div');g.className='details';g.textContent=c;j.appendChild(g);gk&&WD($wnd.console,c)}if(d!=null){e=h.querySelector(d);!!e&&PD(Nc(IG(MG(e.shadowRoot),e)),j)}else{QD(h.body,j)}return j} +function Eu(h,e,f){var g={};g.getNode=jI(function(a){var b=e.get(a);if(b==null){throw new ReferenceError('There is no a StateNode for the given argument.')}return b});g.$appId=h.Gb().replace(/-\d+$/,'');g.registry=h.a;g.attachExistingElement=jI(function(a,b,c,d){Xl(g.getNode(a),b,c,d)});g.populateModelProperties=jI(function(a,b){$l(g.getNode(a),b)});g.registerUpdatableModelProperties=jI(function(a,b){am(g.getNode(a),b)});g.stopApplication=jI(function(){f.N()});g.scrollPositionHandlerAfterServerNavigation=jI(function(a){bm(g.registry,a)});return g} +function qc(a,b){var c,d,e,f,g,h,i,j,k;j='';if(b.length==0){return a.L(yI,wI,-1,-1)}k=NF(b);CF(k.substr(0,3),'at ')&&(k=k.substr(3));k=k.replace(/\[.*?\]/g,'');g=k.indexOf('(');if(g==-1){g=k.indexOf('@');if(g==-1){j=k;k=''}else{j=NF(k.substr(g+1));k=NF(k.substr(0,g))}}else{c=k.indexOf(')',g);j=k.substr(g+1,c-(g+1));k=NF(k.substr(0,g))}g=EF(k,OF(46));g!=-1&&(k=k.substr(g+1));(k.length==0||CF(k,'Anonymous function'))&&(k=wI);h=GF(j,OF(58));e=HF(j,OF(58),h-1);i=-1;d=-1;f=yI;if(h!=-1&&e!=-1){f=j.substr(0,e);i=kc(j.substr(e+1,h-(e+1)));d=kc(j.substr(h+1))}return a.L(f,k,i,d)} +function Np(a,b){var c,d,e;c=Vp(b,'serviceUrl');Oj(a,Tp(b,'webComponentMode'));zj(a,Tp(b,'clientRouting'));if(c==null){Jj(a,Bp('.'));Aj(a,Bp(Vp(b,mJ)))}else{a.l=c;Aj(a,Bp(c+(''+Vp(b,mJ))))}Nj(a,Up(b,'v-uiId').a);Dj(a,Up(b,'heartbeatInterval').a);Gj(a,Up(b,'maxMessageSuspendTimeout').a);Kj(a,(d=b.getConfig(nJ),d?d.vaadinVersion:null));e=b.getConfig(nJ);Sp();Lj(a,b.getConfig('sessExpMsg'));Hj(a,!Tp(b,'debug'));Ij(a,Tp(b,'requestTiming'));Cj(a,b.getConfig('webcomponents'));Bj(a,Tp(b,'devToolsEnabled'));Fj(a,Vp(b,'liveReloadUrl'));Ej(a,Vp(b,'liveReloadBackend'));Mj(a,Vp(b,'springBootLiveReloadPort'))} +function wb(b){var c=function(a){return typeof a!=uI};var d=function(a){return a.replace(/\r\n/g,'')};if(c(b.outerHTML))return d(b.outerHTML);c(b.innerHTML)&&b.cloneNode&&$doc.createElement('div').appendChild(b.cloneNode(true)).innerHTML;if(c(b.nodeType)&&b.nodeType==3){return "'"+b.data.replace(/ /g,'\u25AB').replace(/\u00A0/,'\u25AA')+"'"}if(typeof c(b.htmlText)&&b.collapse){var e=b.htmlText;if(e){return 'IETextRange ['+d(e)+']'}else{var f=b.duplicate();f.pasteHTML('|');var g='IETextRange '+d(b.parentElement().outerHTML);f.moveStart('character',-1);f.pasteHTML('');return g}}return b.toString?b.toString():'[JavaScriptObject]'} +function Vm(a,b,c){var d,e,f;f=[];if(a.c.has(1)){if(!Sc(b,42)){debugger;throw Ui(new CE('Received an inconsistent NodeFeature for a node that has a ELEMENT_PROPERTIES feature. It should be NodeMap, but it is: '+b))}e=Ic(b,42);NB(e,cj(on.prototype.eb,on,[f,c]));f.push(MB(e,new kn(f,c)))}else if(a.c.has(16)){if(!Sc(b,30)){debugger;throw Ui(new CE('Received an inconsistent NodeFeature for a node that has a TEMPLATE_MODELLIST feature. It should be NodeList, but it is: '+b))}d=Ic(b,30);f.push(yB(d,new dn(c)))}if(f.length==0){debugger;throw Ui(new CE('Node should have ELEMENT_PROPERTIES or TEMPLATE_MODELLIST feature'))}f.push(kv(a,new hn(f)))} +function Dk(a,b){this.a=new $wnd.Map;this.b=new $wnd.Map;wk(this,xd,a);wk(this,ud,b);wk(this,we,new $n(this));wk(this,Le,new yp(this));wk(this,Sd,new ol(this));wk(this,Fe,new Lo(this));xk(this,Ke,new Ek);wk(this,gg,new Zv(this));wk(this,Gf,new Dt(this));wk(this,sf,new os(this));wk(this,uf,new Ts(this));wk(this,Of,new eu(this));wk(this,Kf,new Yt(this));wk(this,Zf,new Ku(this));xk(this,Vf,new Gk);xk(this,Vd,new Ik);wk(this,Xd,new rm(this));wk(this,df,new Er(this));wk(this,Ve,new kr(this));wk(this,Uf,new mu(this));wk(this,Cf,new kt(this));wk(this,Ef,new vt(this));b.b||(b.q?wk(this,ze,new Yk):wk(this,ze,new Rk(this)));wk(this,yf,new bt(this))} +function cy(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o;l=e.e;o=Pc(PA(OB(ov(b,0),'tag')));h=false;if(!a){h=true;gk&&YD($wnd.console,bK+d+" is not found. The requested tag name is '"+o+"'")}else if(!(!!a&&DF(o,a.tagName))){h=true;ok(bK+d+" has the wrong tag name '"+a.tagName+"', the requested tag name is '"+o+"'")}if(h){Vv(l.g,l,b.d,-1,c);return false}if(!l.c.has(20)){return true}k=ov(l,20);m=Ic(PA(OB(k,YJ)),6);if(!m){return true}j=nv(m,2);g=null;for(i=0;i<(dB(j.a),j.c.length);i++){n=Ic(j.c[i],6);f=n.a;if(K(f,a)){g=pF(n.d);break}}if(g){gk&&YD($wnd.console,bK+d+" has been already attached previously via the node id='"+g+"'");Vv(l.g,l,b.d,g.a,c);return false}return true} +function Gu(b,c,d,e){var f,g,h,i,j,k,l,m,n;if(c.length!=d.length+1){debugger;throw Ui(new BE)}try{j=new ($wnd.Function.bind.apply($wnd.Function,[null].concat(c)));j.apply(Eu(b,e,new Qu(b)),d)}catch(a){a=Ti(a);if(Sc(a,7)){i=a;gk&&ik(new pk(i));gk&&($wnd.console.error('Exception is thrown during JavaScript execution. Stacktrace will be dumped separately.'),undefined);if(!Ic(tk(b.a,ud),9).j){g=new WF('[');h='';for(l=c,m=0,n=l.length;m=0){g=b.substr(f+3);g=JF(g,mK,'$1');this.a=iF(g)}}else if(this.l){g=LF(b,b.indexOf('webkit/')+7);g=JF(g,nK,'$1');this.a=iF(g)}else if(this.k){g=LF(b,b.indexOf(jK)+8);g=JF(g,nK,'$1');this.a=iF(g);this.a>7&&(this.a=7)}else this.c&&(this.a=0)}catch(a){a=Ti(a);if(Sc(a,7)){c=a;ZF();'Browser engine version parsing failed for: '+b+' '+c.D()}else throw Ui(a)}try{if(this.f){if(b.indexOf('msie')!=-1){if(this.k);else{e=LF(b,b.indexOf('msie ')+5);e=pD(e,0,EF(e,OF(59)));mD(e)}}else{f=b.indexOf('rv:');if(f>=0){g=b.substr(f+3);g=JF(g,mK,'$1');mD(g)}}}else if(this.d){d=b.indexOf(' firefox/')+9;mD(pD(b,d,d+5))}else if(this.b){iD(b)}else if(this.j){d=b.indexOf(' version/');if(d>=0){d+=9;mD(pD(b,d,d+5))}}else if(this.i){d=b.indexOf(' version/');d!=-1?(d+=9):(d=b.indexOf('opera/')+6);mD(pD(b,d,d+5))}else if(this.c){d=b.indexOf(' edge/')+6;b.indexOf(' edg/')!=-1?(d=b.indexOf(' edg/')+5):b.indexOf(kK)!=-1?(d=b.indexOf(kK)+6):b.indexOf(lK)!=-1&&(d=b.indexOf(lK)+8);mD(pD(b,d,d+8))}}catch(a){a=Ti(a);if(Sc(a,7)){c=a;ZF();'Browser version parsing failed for: '+b+' '+c.D()}else throw Ui(a)}if(b.indexOf('windows ')!=-1){b.indexOf('windows phone')!=-1}else if(b.indexOf('android')!=-1){fD(b)}else if(b.indexOf('linux')!=-1);else if(b.indexOf('macintosh')!=-1||b.indexOf('mac osx')!=-1||b.indexOf('mac os x')!=-1){this.g=b.indexOf('ipad')!=-1;this.h=b.indexOf('iphone')!=-1;(this.g||this.h)&&jD(b)}else b.indexOf('; cros ')!=-1&&gD(b)} +var kI='object',lI='[object Array]',mI='function',nI='java.lang',oI='com.google.gwt.core.client',pI={4:1},qI='__noinit__',rI={4:1,7:1,8:1,5:1},sI='null',tI='com.google.gwt.core.client.impl',uI='undefined',vI='Working array length changed ',wI='anonymous',xI='fnStack',yI='Unknown',zI='must be non-negative',AI='must be positive',BI='com.google.web.bindery.event.shared',CI='com.vaadin.client',DI='url',EI={67:1},FI={33:1},GI='historyIndex',HI='historyResetToken',II='xPositions',JI='yPositions',KI='scrollPos-',LI='Failed to get session storage: ',MI='Unable to restore scroll positions. History.state has been manipulated or user has navigated away from site in an unrecognized way.',NI='beforeunload',OI='scrollPositionX',QI='scrollPositionY',RI='type',SI={47:1},TI={25:1},UI={18:1},VI={24:1},WI='text/javascript',XI='constructor',YI='properties',ZI='value',$I='com.vaadin.client.flow.reactive',_I={15:1},aJ='nodeId',bJ='Root node for node ',cJ=' could not be found',dJ=' is not an Element',eJ={65:1},fJ={82:1},gJ={46:1},hJ={91:1},iJ='script',jJ='stylesheet',kJ='click',lJ='com.vaadin.flow.shared',mJ='contextRootUrl',nJ='versionInfo',oJ='v-uiId=',pJ='websocket',qJ='transport',rJ='application/json; charset=UTF-8',sJ='VAADIN/push',tJ='com.vaadin.client.communication',uJ={92:1},vJ='dialogText',wJ='dialogTextGaveUp',xJ='syncId',yJ='resynchronize',zJ='Received message with server id ',AJ='clientId',BJ='Vaadin-Security-Key',CJ='Vaadin-Push-ID',DJ='sessionExpired',EJ='pushServletMapping',FJ='event',GJ='node',HJ='attachReqId',IJ='attachAssignedId',JJ='com.vaadin.client.flow',KJ='bound',LJ='payload',MJ='subTemplate',NJ={45:1},OJ='Node is null',PJ='Node is not created for this tree',QJ='Node id is not registered with this tree',RJ='$server',SJ='feat',TJ='remove',UJ='com.vaadin.client.flow.binding',VJ='intermediate',WJ='elemental.util',XJ='element',YJ='shadowRoot',ZJ='The HTML node for the StateNode with id=',$J='An error occurred when Flow tried to find a state node matching the element ',_J='hidden',aK='styleDisplay',bK='Element addressed by the ',cK='dom-repeat',dK='dom-change',eK='com.vaadin.client.flow.nodefeature',fK='Unsupported complex type in ',gK='com.vaadin.client.gwt.com.google.web.bindery.event.shared',hK='OS minor',iK=' headlesschrome/',jK='trident/',kK=' edga/',lK=' edgios/',mK='(\\.[0-9]+).+',nK='([0-9]+\\.[0-9]+).*',oK='com.vaadin.flow.shared.ui',pK='java.io',qK='For input string: "',rK='java.util',sK='java.util.stream',tK='Index: ',uK=', Size: ',vK='user.agent';var _,$i,Vi,Si=-1;$wnd.goog=$wnd.goog||{};$wnd.goog.global=$wnd.goog.global||$wnd;_i();aj(1,null,{},I);_.r=function J(a){return H(this,a)};_.s=function L(){return this.nc};_.t=function N(){return bI(this)};_.u=function P(){var a;return LE(M(this))+'@'+(a=O(this)>>>0,a.toString(16))};_.equals=function(a){return this.r(a)};_.hashCode=function(){return this.t()};_.toString=function(){return this.u()};var Ec,Fc,Gc;aj(68,1,{68:1},ME);_.Zb=function NE(a){var b;b=new ME;b.e=4;a>1?(b.c=UE(this,a-1)):(b.c=this);return b};_.$b=function TE(){KE(this);return this.b};_._b=function VE(){return LE(this)};_.ac=function XE(){KE(this);return this.g};_.bc=function ZE(){return (this.e&4)!=0};_.cc=function $E(){return (this.e&1)!=0};_.u=function bF(){return ((this.e&2)!=0?'interface ':(this.e&1)!=0?'':'class ')+(KE(this),this.i)};_.e=0;var JE=1;var ji=PE(nI,'Object',1);var Yh=PE(nI,'Class',68);aj(97,1,{},R);_.a=0;var dd=PE(oI,'Duration',97);var S=null;aj(5,1,{4:1,5:1});_.w=function bb(a){return new Error(a)};_.A=function db(){return this.e};_.B=function eb(){var a;return a=Ic(xH(zH(BG((this.i==null&&(this.i=zc(ri,pI,5,0,0,1)),this.i)),new _F),gH(new rH,new pH,new tH,Dc(xc(Gi,1),pI,48,0,[(kH(),iH)]))),93),pG(a,zc(ji,pI,1,a.a.length,5,1))};_.C=function fb(){return this.f};_.D=function gb(){return this.g};_.F=function hb(){Z(this,cb(this.w($(this,this.g))));hc(this)};_.u=function jb(){return $(this,this.D())};_.e=qI;_.j=true;var ri=PE(nI,'Throwable',5);aj(7,5,{4:1,7:1,5:1});var ai=PE(nI,'Exception',7);aj(8,7,rI,mb);var li=PE(nI,'RuntimeException',8);aj(54,8,rI,nb);var fi=PE(nI,'JsException',54);aj(122,54,rI);var hd=PE(tI,'JavaScriptExceptionBase',122);aj(26,122,{26:1,4:1,7:1,8:1,5:1},rb);_.D=function ub(){return qb(this),this.c};_.G=function vb(){return _c(this.b)===_c(ob)?null:this.b};var ob;var ed=PE(oI,'JavaScriptException',26);var fd=PE(oI,'JavaScriptObject$',0);aj(320,1,{});var gd=PE(oI,'Scheduler',320);var yb=0,zb=false,Ab,Bb=0,Cb=-1;aj(132,320,{});_.e=false;_.i=false;var Pb;var ld=PE(tI,'SchedulerImpl',132);aj(133,1,{},bc);_.H=function cc(){this.a.e=true;Tb(this.a);this.a.e=false;return this.a.i=Ub(this.a)};var jd=PE(tI,'SchedulerImpl/Flusher',133);aj(134,1,{},dc);_.H=function ec(){this.a.e&&_b(this.a.f,1);return this.a.i};var kd=PE(tI,'SchedulerImpl/Rescuer',134);var fc;aj(330,1,{});var pd=PE(tI,'StackTraceCreator/Collector',330);aj(123,330,{},nc);_.J=function oc(a){var b={},j;var c=[];a[xI]=c;var d=arguments.callee.caller;while(d){var e=(gc(),d.name||(d.name=jc(d.toString())));c.push(e);var f=':'+e;var g=b[f];if(g){var h,i;for(h=0,i=g.length;h0){Sn(this.b,this.c);return false}else if(a==0){Rn(this.b,this.c);return true}else if(Q(this.a)>60000){Rn(this.b,this.c);return false}else{return true}};var le=PE(CI,'ResourceLoader/1',196);aj(197,41,{},ho);_.N=function io(){this.a.b.has(this.c)||Rn(this.a,this.b)};var me=PE(CI,'ResourceLoader/2',197);aj(201,41,{},jo);_.N=function ko(){this.a.b.has(this.c)?Sn(this.a,this.b):Rn(this.a,this.b)};var ne=PE(CI,'ResourceLoader/3',201);aj(202,1,TI,lo);_.fb=function mo(a){Rn(this.a,a)};_.gb=function no(a){Sn(this.a,a)};var oe=PE(CI,'ResourceLoader/4',202);aj(62,1,{},oo);var pe=PE(CI,'ResourceLoader/ResourceLoadEvent',62);aj(101,1,TI,po);_.fb=function qo(a){Rn(this.a,a)};_.gb=function ro(a){Sn(this.a,a)};var re=PE(CI,'ResourceLoader/SimpleLoadListener',101);aj(195,1,TI,so);_.fb=function to(a){Rn(this.a,a)};_.gb=function uo(a){var b;if((!ak&&(ak=new ck),ak).a.b||(!ak&&(ak=new ck),ak).a.f||(!ak&&(ak=new ck),ak).a.c){b=co(this.b);if(b==0){Rn(this.a,a);return}}Sn(this.a,a)};var se=PE(CI,'ResourceLoader/StyleSheetLoadListener',195);aj(198,1,FI,vo);_._=function wo(){return this.a.call(null)};var te=PE(CI,'ResourceLoader/lambda$0$Type',198);aj(199,1,UI,xo);_.N=function yo(){this.b.gb(this.a)};var ue=PE(CI,'ResourceLoader/lambda$1$Type',199);aj(200,1,UI,zo);_.N=function Ao(){this.b.fb(this.a)};var ve=PE(CI,'ResourceLoader/lambda$2$Type',200);aj(160,1,{},Bo);_.nb=function Co(a){Nk(this.a)};var xe=PE(CI,'ScrollPositionHandler/0methodref$onBeforeUnload$Type',160);aj(161,1,hJ,Do);_.ob=function Eo(a){Mk(this.a,this.b,this.c)};_.b=0;_.c=0;var ye=PE(CI,'ScrollPositionHandler/lambda$1$Type',161);aj(22,1,{22:1},Lo);var Fe=PE(CI,'SystemErrorHandler',22);aj(165,1,{},No);_.qb=function Oo(a,b){var c;c=b;Fo(c.D())};_.rb=function Po(a){var b;nk('Received xhr HTTP session resynchronization message: '+a.responseText);vk(this.a.a);bp(Ic(tk(this.a.a,Ke),11),(rp(),pp));b=rs(ss(a.responseText));ds(Ic(tk(this.a.a,sf),20),b);Nj(Ic(tk(this.a.a,ud),9),b['uiId']);Yo((Qb(),Pb),new So(this))};var Ce=PE(CI,'SystemErrorHandler/1',165);aj(166,1,{},Qo);_.ib=function Ro(a){Ko(Pc(a))};var Ae=PE(CI,'SystemErrorHandler/1/0methodref$recreateNodes$Type',166);aj(167,1,{},So);_.I=function To(){yH(BG(Ic(tk(this.a.a.a,ud),9).e),new Qo)};var Be=PE(CI,'SystemErrorHandler/1/lambda$0$Type',167);aj(163,1,{},Uo);_.nb=function Vo(a){Cp(this.a)};var De=PE(CI,'SystemErrorHandler/lambda$0$Type',163);aj(164,1,{},Wo);_.nb=function Xo(a){Mo(this.a,a)};var Ee=PE(CI,'SystemErrorHandler/lambda$1$Type',164);aj(136,132,{},Zo);_.a=0;var He=PE(CI,'TrackingScheduler',136);aj(137,1,{},$o);_.I=function _o(){this.a.a--};var Ge=PE(CI,'TrackingScheduler/lambda$0$Type',137);aj(11,1,{11:1},cp);var Ke=PE(CI,'UILifecycle',11);aj(171,337,{},ep);_.P=function fp(a){Ic(a,92).sb(this)};_.Q=function gp(){return dp};var dp=null;var Ie=PE(CI,'UILifecycle/StateChangeEvent',171);aj(21,1,{4:1,32:1,21:1});_.r=function kp(a){return this===a};_.t=function lp(){return bI(this)};_.u=function mp(){return this.b!=null?this.b:''+this.c};_.c=0;var $h=PE(nI,'Enum',21);aj(60,21,{60:1,4:1,32:1,21:1},sp);var op,pp,qp;var Je=QE(CI,'UILifecycle/UIState',60,tp);aj(336,1,pI);var Gh=PE(lJ,'VaadinUriResolver',336);aj(50,336,{50:1,4:1},yp);_.tb=function Ap(a){return xp(this,a)};var Le=PE(CI,'URIResolver',50);var Fp=false,Gp;aj(116,1,{},Qp);_.I=function Rp(){Mp(this.a)};var Me=PE('com.vaadin.client.bootstrap','Bootstrapper/lambda$0$Type',116);aj(102,1,{},gq);_.ub=function iq(){return Ic(tk(this.d,sf),20).f};_.vb=function kq(a){this.f=(Eq(),Cq);Jo(Ic(tk(Ic(tk(this.d,Ve),16).c,Fe),22),'','Client unexpectedly disconnected. Ensure client timeout is disabled.','',null,null)};_.wb=function lq(a){this.f=(Eq(),Bq);Ic(tk(this.d,Ve),16);gk&&($wnd.console.log('Push connection closed'),undefined)};_.xb=function mq(a){this.f=(Eq(),Cq);Sq(Ic(tk(this.d,Ve),16),'Push connection using '+a[qJ]+' failed!')};_.yb=function nq(a){var b,c;c=a['responseBody'];b=rs(ss(c));if(!b){$q(Ic(tk(this.d,Ve),16),this,c);return}else{nk('Received push ('+this.g+') message: '+c);ds(Ic(tk(this.d,sf),20),b)}};_.zb=function oq(a){nk('Push connection established using '+a[qJ]);dq(this,a)};_.Ab=function pq(a,b){this.f==(Eq(),Aq)&&(this.f=Bq);br(Ic(tk(this.d,Ve),16),this)};_.Bb=function qq(a){nk('Push connection re-established using '+a[qJ]);dq(this,a)};_.Cb=function rq(){ok('Push connection using primary method ('+this.a[qJ]+') failed. Trying with '+this.a['fallbackTransport'])};var Ue=PE(tJ,'AtmospherePushConnection',102);aj(252,1,{},sq);_.I=function tq(){Wp(this.a)};var Ne=PE(tJ,'AtmospherePushConnection/0methodref$connect$Type',252);aj(254,1,TI,uq);_.fb=function vq(a){cr(Ic(tk(this.a.d,Ve),16),a.a)};_.gb=function wq(a){if(jq()){nk(this.c+' loaded');cq(this.b.a)}else{cr(Ic(tk(this.a.d,Ve),16),a.a)}};var Oe=PE(tJ,'AtmospherePushConnection/1',254);aj(249,1,{},zq);_.a=0;var Pe=PE(tJ,'AtmospherePushConnection/FragmentedMessage',249);aj(52,21,{52:1,4:1,32:1,21:1},Fq);var Aq,Bq,Cq,Dq;var Qe=QE(tJ,'AtmospherePushConnection/State',52,Gq);aj(251,1,uJ,Hq);_.sb=function Iq(a){aq(this.a,a)};var Re=PE(tJ,'AtmospherePushConnection/lambda$0$Type',251);aj(250,1,VI,Jq);_.I=function Kq(){};var Se=PE(tJ,'AtmospherePushConnection/lambda$1$Type',250);aj(365,$wnd.Function,{},Lq);_.eb=function Mq(a,b){bq(this.a,Pc(a),Pc(b))};aj(253,1,VI,Nq);_.I=function Oq(){cq(this.a)};var Te=PE(tJ,'AtmospherePushConnection/lambda$3$Type',253);var Ve=RE(tJ,'ConnectionStateHandler');aj(224,1,{16:1},kr);_.a=0;_.b=null;var _e=PE(tJ,'DefaultConnectionStateHandler',224);aj(226,41,{},lr);_.N=function mr(){this.a.d=null;Qq(this.a,this.b)};var We=PE(tJ,'DefaultConnectionStateHandler/1',226);aj(63,21,{63:1,4:1,32:1,21:1},sr);_.a=0;var nr,or,pr;var Xe=QE(tJ,'DefaultConnectionStateHandler/Type',63,tr);aj(225,1,uJ,ur);_.sb=function vr(a){Yq(this.a,a)};var Ye=PE(tJ,'DefaultConnectionStateHandler/lambda$0$Type',225);aj(227,1,{},wr);_.nb=function xr(a){Rq(this.a)};var Ze=PE(tJ,'DefaultConnectionStateHandler/lambda$1$Type',227);aj(228,1,{},yr);_.nb=function zr(a){Zq(this.a)};var $e=PE(tJ,'DefaultConnectionStateHandler/lambda$2$Type',228);aj(56,1,{56:1},Er);_.a=-1;var df=PE(tJ,'Heartbeat',56);aj(221,41,{},Fr);_.N=function Gr(){Cr(this.a)};var af=PE(tJ,'Heartbeat/1',221);aj(223,1,{},Hr);_.qb=function Ir(a,b){!b?Wq(Ic(tk(this.a.b,Ve),16),a):Vq(Ic(tk(this.a.b,Ve),16),b);Br(this.a)};_.rb=function Jr(a){Xq(Ic(tk(this.a.b,Ve),16));Br(this.a)};var bf=PE(tJ,'Heartbeat/2',223);aj(222,1,uJ,Kr);_.sb=function Lr(a){Ar(this.a,a)};var cf=PE(tJ,'Heartbeat/lambda$0$Type',222);aj(173,1,{},Pr);_.ib=function Qr(a){ek('firstDelay',pF(Ic(a,27).a))};var ef=PE(tJ,'LoadingIndicatorConfigurator/0methodref$setFirstDelay$Type',173);aj(174,1,{},Rr);_.ib=function Sr(a){ek('secondDelay',pF(Ic(a,27).a))};var ff=PE(tJ,'LoadingIndicatorConfigurator/1methodref$setSecondDelay$Type',174);aj(175,1,{},Tr);_.ib=function Ur(a){ek('thirdDelay',pF(Ic(a,27).a))};var gf=PE(tJ,'LoadingIndicatorConfigurator/2methodref$setThirdDelay$Type',175);aj(176,1,gJ,Vr);_.mb=function Wr(a){Or(SA(Ic(a.e,14)))};var hf=PE(tJ,'LoadingIndicatorConfigurator/lambda$3$Type',176);aj(177,1,gJ,Xr);_.mb=function Yr(a){Nr(this.b,this.a,a)};_.a=0;var jf=PE(tJ,'LoadingIndicatorConfigurator/lambda$4$Type',177);aj(20,1,{20:1},os);_.a=0;_.b='init';_.d=false;_.e=0;_.f=-1;_.i=null;_.m=0;var sf=PE(tJ,'MessageHandler',20);aj(188,1,VI,ts);_.I=function us(){!AA&&$wnd.Polymer!=null&&CF($wnd.Polymer.version.substr(0,'1.'.length),'1.')&&(AA=true,gk&&($wnd.console.log('Polymer micro is now loaded, using Polymer DOM API'),undefined),zA=new CA,undefined)};var kf=PE(tJ,'MessageHandler/0methodref$updateApiImplementation$Type',188);aj(187,41,{},vs);_.N=function ws(){_r(this.a)};var lf=PE(tJ,'MessageHandler/1',187);aj(353,$wnd.Function,{},xs);_.ib=function ys(a){Zr(Ic(a,6))};aj(61,1,{61:1},zs);var mf=PE(tJ,'MessageHandler/PendingUIDLMessage',61);aj(189,1,VI,As);_.I=function Bs(){ks(this.a,this.d,this.b,this.c)};_.c=0;var nf=PE(tJ,'MessageHandler/lambda$1$Type',189);aj(191,1,_I,Cs);_.hb=function Ds(){xC(new Es(this.a,this.b))};var of=PE(tJ,'MessageHandler/lambda$3$Type',191);aj(190,1,_I,Es);_.hb=function Fs(){hs(this.a,this.b)};var pf=PE(tJ,'MessageHandler/lambda$4$Type',190);aj(193,1,_I,Gs);_.hb=function Hs(){is(this.a)};var qf=PE(tJ,'MessageHandler/lambda$5$Type',193);aj(192,1,{},Is);_.I=function Js(){this.a.forEach(cj(xs.prototype.ib,xs,[]))};var rf=PE(tJ,'MessageHandler/lambda$6$Type',192);aj(19,1,{19:1},Ts);_.a=0;_.d=0;var uf=PE(tJ,'MessageSender',19);aj(185,1,VI,Us);_.I=function Vs(){Ls(this.a)};var tf=PE(tJ,'MessageSender/lambda$0$Type',185);aj(168,1,gJ,Ys);_.mb=function Zs(a){Ws(this.a,a)};var vf=PE(tJ,'PollConfigurator/lambda$0$Type',168);aj(74,1,{74:1},bt);_.Db=function ct(){var a;a=Ic(tk(this.b,gg),10);Tv(a,a.e,'ui-poll',null)};_.a=null;var yf=PE(tJ,'Poller',74);aj(170,41,{},dt);_.N=function et(){var a;a=Ic(tk(this.a.b,gg),10);Tv(a,a.e,'ui-poll',null)};var wf=PE(tJ,'Poller/1',170);aj(169,1,uJ,ft);_.sb=function gt(a){$s(this.a,a)};var xf=PE(tJ,'Poller/lambda$0$Type',169);aj(49,1,{49:1},kt);var Cf=PE(tJ,'PushConfiguration',49);aj(233,1,gJ,nt);_.mb=function ot(a){jt(this.a,a)};var zf=PE(tJ,'PushConfiguration/0methodref$onPushModeChange$Type',233);aj(234,1,_I,pt);_.hb=function qt(){Ss(Ic(tk(this.a.a,uf),19),true)};var Af=PE(tJ,'PushConfiguration/lambda$1$Type',234);aj(235,1,_I,rt);_.hb=function st(){Ss(Ic(tk(this.a.a,uf),19),false)};var Bf=PE(tJ,'PushConfiguration/lambda$2$Type',235);aj(359,$wnd.Function,{},tt);_.eb=function ut(a,b){mt(this.a,Ic(a,14),Pc(b))};aj(37,1,{37:1},vt);var Ef=PE(tJ,'ReconnectConfiguration',37);aj(172,1,VI,wt);_.I=function xt(){Pq(this.a)};var Df=PE(tJ,'ReconnectConfiguration/lambda$0$Type',172);aj(13,1,{13:1},Dt);_.b=false;var Gf=PE(tJ,'RequestResponseTracker',13);aj(186,1,{},Et);_.I=function Ft(){Bt(this.a)};var Ff=PE(tJ,'RequestResponseTracker/lambda$0$Type',186);aj(248,337,{},Gt);_.P=function Ht(a){bd(a);null.qc()};_.Q=function It(){return null};var Hf=PE(tJ,'RequestStartingEvent',248);aj(162,337,{},Kt);_.P=function Lt(a){Ic(a,91).ob(this)};_.Q=function Mt(){return Jt};var Jt;var If=PE(tJ,'ResponseHandlingEndedEvent',162);aj(289,337,{},Nt);_.P=function Ot(a){bd(a);null.qc()};_.Q=function Pt(){return null};var Jf=PE(tJ,'ResponseHandlingStartedEvent',289);aj(28,1,{28:1},Yt);_.Eb=function Zt(a,b,c){Qt(this,a,b,c)};_.Fb=function $t(a,b,c){var d;d={};d[RI]='channel';d[GJ]=Object(a);d['channel']=Object(b);d['args']=c;Ut(this,d)};var Kf=PE(tJ,'ServerConnector',28);aj(36,1,{36:1},eu);_.b=false;var _t;var Of=PE(tJ,'ServerRpcQueue',36);aj(215,1,UI,fu);_.N=function gu(){cu(this.a)};var Lf=PE(tJ,'ServerRpcQueue/0methodref$doFlush$Type',215);aj(214,1,UI,hu);_.N=function iu(){au()};var Mf=PE(tJ,'ServerRpcQueue/lambda$0$Type',214);aj(216,1,{},ju);_.I=function ku(){this.a.a.N()};var Nf=PE(tJ,'ServerRpcQueue/lambda$2$Type',216);aj(72,1,{72:1},mu);_.b=false;var Uf=PE(tJ,'XhrConnection',72);aj(232,41,{},ou);_.N=function pu(){nu(this.b)&&this.a.b&&jj(this,250)};var Pf=PE(tJ,'XhrConnection/1',232);aj(229,1,{},ru);_.qb=function su(a,b){var c;c=new yu(a,this.a);if(!b){ir(Ic(tk(this.c.a,Ve),16),c);return}else{gr(Ic(tk(this.c.a,Ve),16),c)}};_.rb=function tu(a){var b,c;nk('Server visit took '+Jn(this.b)+'ms');c=a.responseText;b=rs(ss(c));if(!b){hr(Ic(tk(this.c.a,Ve),16),new yu(a,this.a));return}jr(Ic(tk(this.c.a,Ve),16));gk&&XD($wnd.console,'Received xhr message: '+c);ds(Ic(tk(this.c.a,sf),20),b)};_.b=0;var Qf=PE(tJ,'XhrConnection/XhrResponseHandler',229);aj(230,1,{},uu);_.nb=function vu(a){this.a.b=true};var Rf=PE(tJ,'XhrConnection/lambda$0$Type',230);aj(231,1,hJ,wu);_.ob=function xu(a){this.a.b=false};var Sf=PE(tJ,'XhrConnection/lambda$1$Type',231);aj(105,1,{},yu);var Tf=PE(tJ,'XhrConnectionError',105);aj(58,1,{58:1},Cu);var Vf=PE(JJ,'ConstantPool',58);aj(85,1,{85:1},Ku);_.Gb=function Lu(){return Ic(tk(this.a,ud),9).a};var Zf=PE(JJ,'ExecuteJavaScriptProcessor',85);aj(218,1,EI,Mu);_.U=function Nu(a){var b;return xC(new Ou(this.a,(b=this.b,b))),FE(),true};var Wf=PE(JJ,'ExecuteJavaScriptProcessor/lambda$0$Type',218);aj(217,1,_I,Ou);_.hb=function Pu(){Fu(this.a,this.b)};var Xf=PE(JJ,'ExecuteJavaScriptProcessor/lambda$1$Type',217);aj(219,1,UI,Qu);_.N=function Ru(){Ju(this.a)};var Yf=PE(JJ,'ExecuteJavaScriptProcessor/lambda$2$Type',219);aj(309,1,{},Uu);var _f=PE(JJ,'FragmentHandler',309);aj(310,1,hJ,Wu);_.ob=function Xu(a){Tu(this.a)};var $f=PE(JJ,'FragmentHandler/0methodref$onResponseHandlingEnded$Type',310);aj(308,1,{},Yu);var ag=PE(JJ,'NodeUnregisterEvent',308);aj(183,1,{},fv);_.nb=function gv(a){av(this.a,a)};var bg=PE(JJ,'RouterLinkHandler/lambda$0$Type',183);aj(184,1,VI,hv);_.I=function iv(){Cp(this.a)};var cg=PE(JJ,'RouterLinkHandler/lambda$1$Type',184);aj(6,1,{6:1},vv);_.Hb=function wv(){return mv(this)};_.Ib=function xv(){return this.g};_.d=0;_.i=false;var fg=PE(JJ,'StateNode',6);aj(346,$wnd.Function,{},zv);_.eb=function Av(a,b){pv(this.a,this.b,Ic(a,34),Kc(b))};aj(347,$wnd.Function,{},Bv);_.ib=function Cv(a){yv(this.a,Ic(a,107))};var Jh=RE('elemental.events','EventRemover');aj(152,1,NJ,Dv);_.Jb=function Ev(){qv(this.a,this.b)};var dg=PE(JJ,'StateNode/lambda$2$Type',152);aj(348,$wnd.Function,{},Fv);_.ib=function Gv(a){rv(this.a,Ic(a,67))};aj(153,1,NJ,Hv);_.Jb=function Iv(){sv(this.a,this.b)};var eg=PE(JJ,'StateNode/lambda$4$Type',153);aj(10,1,{10:1},Zv);_.Kb=function $v(){return this.e};_.Lb=function aw(a,b,c,d){var e;if(Ov(this,a)){e=Nc(c);Xt(Ic(tk(this.c,Kf),28),a,b,e,d)}};_.d=false;_.f=false;var gg=PE(JJ,'StateTree',10);aj(351,$wnd.Function,{},bw);_.ib=function cw(a){lv(Ic(a,6),cj(fw.prototype.eb,fw,[]))};aj(352,$wnd.Function,{},dw);_.eb=function ew(a,b){var c;Qv(this.a,(c=Ic(a,6),Kc(b),c))};aj(340,$wnd.Function,{},fw);_.eb=function gw(a,b){_v(Ic(a,34),Kc(b))};var ow,pw;aj(178,1,{},uw);var hg=PE(UJ,'Binder/BinderContextImpl',178);var ig=RE(UJ,'BindingStrategy');aj(80,1,{80:1},zw);_.b=false;_.g=0;var vw;var lg=PE(UJ,'Debouncer',80);aj(339,1,{});_.b=false;_.c=0;var Oh=PE(WJ,'Timer',339);aj(313,339,{},Fw);var jg=PE(UJ,'Debouncer/1',313);aj(314,339,{},Gw);var kg=PE(UJ,'Debouncer/2',314);aj(380,$wnd.Function,{},Iw);_.eb=function Jw(a,b){var c;Hw(this,(c=Oc(a,$wnd.Map),Nc(b),c))};aj(381,$wnd.Function,{},Mw);_.ib=function Nw(a){Kw(this.a,Oc(a,$wnd.Map))};aj(382,$wnd.Function,{},Ow);_.ib=function Pw(a){Lw(this.a,Ic(a,80))};aj(305,1,FI,Tw);_._=function Uw(){return ex(this.a)};var mg=PE(UJ,'ServerEventHandlerBinder/lambda$0$Type',305);aj(306,1,eJ,Vw);_.jb=function Ww(a){Sw(this.b,this.a,this.c,a)};_.c=false;var ng=PE(UJ,'ServerEventHandlerBinder/lambda$1$Type',306);var Xw;aj(255,1,{317:1},dy);_.Mb=function ey(a,b,c){mx(this,a,b,c)};_.Nb=function hy(a){return wx(a)};_.Pb=function my(a,b){var c,d,e;d=Object.keys(a);e=new Xz(d,a,b);c=Ic(b.e.get(pg),77);!c?Ux(e.b,e.a,e.c):(c.a=e)};_.Qb=function ny(r,s){var t=this;var u=s._propertiesChanged;u&&(s._propertiesChanged=function(a,b,c){jI(function(){t.Pb(b,r)})();u.apply(this,arguments)});var v=r.Ib();var w=s.ready;s.ready=function(){w.apply(this,arguments);Hm(s);var q=function(){var o=s.root.querySelector(cK);if(o){s.removeEventListener(dK,q)}else{return}if(!o.constructor.prototype.$propChangedModified){o.constructor.prototype.$propChangedModified=true;var p=o.constructor.prototype._propertiesChanged;o.constructor.prototype._propertiesChanged=function(a,b,c){p.apply(this,arguments);var d=Object.getOwnPropertyNames(b);var e='items.';var f;for(f=0;f0){var i=h.substr(0,g);var j=h.substr(g+1);var k=a.items[i];if(k&&k.nodeId){var l=k.nodeId;var m=k[j];var n=this.__dataHost;while(!n.localName||n.__dataHost){n=n.__dataHost}jI(function(){ly(l,n,j,m,v)})()}}}}}}};s.root&&s.root.querySelector(cK)?q():s.addEventListener(dK,q)}};_.Ob=function oy(a){if(a.c.has(0)){return true}return !!a.g&&K(a,a.g.e)};var gx,hx;var Ug=PE(UJ,'SimpleElementBindingStrategy',255);aj(370,$wnd.Function,{},Dy);_.ib=function Ey(a){Ic(a,45).Jb()};aj(374,$wnd.Function,{},Fy);_.ib=function Gy(a){Ic(a,18).N()};aj(103,1,{},Hy);var og=PE(UJ,'SimpleElementBindingStrategy/BindingContext',103);aj(77,1,{77:1},Iy);var pg=PE(UJ,'SimpleElementBindingStrategy/InitialPropertyUpdate',77);aj(256,1,{},Jy);_.Rb=function Ky(a){Ix(this.a,a)};var qg=PE(UJ,'SimpleElementBindingStrategy/lambda$0$Type',256);aj(257,1,{},Ly);_.Rb=function My(a){Jx(this.a,a)};var rg=PE(UJ,'SimpleElementBindingStrategy/lambda$1$Type',257);aj(366,$wnd.Function,{},Ny);_.eb=function Oy(a,b){var c;py(this.b,this.a,(c=Ic(a,14),Pc(b),c))};aj(266,1,fJ,Py);_.lb=function Qy(a){qy(this.b,this.a,a)};var sg=PE(UJ,'SimpleElementBindingStrategy/lambda$11$Type',266);aj(267,1,gJ,Ry);_.mb=function Sy(a){ay(this.c,this.b,this.a)};var tg=PE(UJ,'SimpleElementBindingStrategy/lambda$12$Type',267);aj(268,1,_I,Ty);_.hb=function Uy(){Kx(this.b,this.c,this.a)};var ug=PE(UJ,'SimpleElementBindingStrategy/lambda$13$Type',268);aj(269,1,VI,Vy);_.I=function Wy(){this.b.Rb(this.a)};var vg=PE(UJ,'SimpleElementBindingStrategy/lambda$14$Type',269);aj(270,1,VI,Xy);_.I=function Yy(){this.a[this.b]=Dm(this.c)};var wg=PE(UJ,'SimpleElementBindingStrategy/lambda$15$Type',270);aj(272,1,eJ,Zy);_.jb=function $y(a){Lx(this.a,a)};var xg=PE(UJ,'SimpleElementBindingStrategy/lambda$16$Type',272);aj(271,1,_I,_y);_.hb=function az(){Dx(this.b,this.a)};var yg=PE(UJ,'SimpleElementBindingStrategy/lambda$17$Type',271);aj(274,1,eJ,bz);_.jb=function cz(a){Mx(this.a,a)};var zg=PE(UJ,'SimpleElementBindingStrategy/lambda$18$Type',274);aj(273,1,_I,dz);_.hb=function ez(){Nx(this.b,this.a)};var Ag=PE(UJ,'SimpleElementBindingStrategy/lambda$19$Type',273);aj(258,1,{},fz);_.Rb=function gz(a){Ox(this.a,a)};var Bg=PE(UJ,'SimpleElementBindingStrategy/lambda$2$Type',258);aj(275,1,UI,hz);_.N=function iz(){Fx(this.a,this.b,this.c,false)};var Cg=PE(UJ,'SimpleElementBindingStrategy/lambda$20$Type',275);aj(276,1,UI,jz);_.N=function kz(){Fx(this.a,this.b,this.c,false)};var Dg=PE(UJ,'SimpleElementBindingStrategy/lambda$21$Type',276);aj(277,1,UI,lz);_.N=function mz(){Hx(this.a,this.b,this.c,false)};var Eg=PE(UJ,'SimpleElementBindingStrategy/lambda$22$Type',277);aj(278,1,FI,nz);_._=function oz(){return ry(this.a,this.b)};var Fg=PE(UJ,'SimpleElementBindingStrategy/lambda$23$Type',278);aj(279,1,FI,pz);_._=function qz(){return sy(this.a,this.b)};var Gg=PE(UJ,'SimpleElementBindingStrategy/lambda$24$Type',279);aj(367,$wnd.Function,{},rz);_.eb=function sz(a,b){var c;lC((c=Ic(a,75),Pc(b),c))};aj(368,$wnd.Function,{},tz);_.ib=function uz(a){ty(this.a,Oc(a,$wnd.Map))};aj(369,$wnd.Function,{},vz);_.eb=function wz(a,b){var c;(c=Ic(a,45),Pc(b),c).Jb()};aj(259,1,{107:1},xz);_.kb=function yz(a){Vx(this.c,this.b,this.a)};var Hg=PE(UJ,'SimpleElementBindingStrategy/lambda$3$Type',259);aj(371,$wnd.Function,{},zz);_.eb=function Az(a,b){var c;Px(this.a,(c=Ic(a,14),Pc(b),c))};aj(280,1,fJ,Bz);_.lb=function Cz(a){Qx(this.a,a)};var Ig=PE(UJ,'SimpleElementBindingStrategy/lambda$31$Type',280);aj(281,1,VI,Dz);_.I=function Ez(){Rx(this.b,this.a,this.c)};var Jg=PE(UJ,'SimpleElementBindingStrategy/lambda$32$Type',281);aj(282,1,{},Fz);_.nb=function Gz(a){Sx(this.a,a)};var Kg=PE(UJ,'SimpleElementBindingStrategy/lambda$33$Type',282);aj(372,$wnd.Function,{},Hz);_.ib=function Iz(a){Tx(this.a,this.b,Pc(a))};aj(283,1,{},Kz);_.ib=function Lz(a){Jz(this,a)};var Lg=PE(UJ,'SimpleElementBindingStrategy/lambda$35$Type',283);aj(284,1,eJ,Mz);_.jb=function Nz(a){vy(this.a,a)};var Mg=PE(UJ,'SimpleElementBindingStrategy/lambda$37$Type',284);aj(285,1,FI,Oz);_._=function Pz(){return this.a.b};var Ng=PE(UJ,'SimpleElementBindingStrategy/lambda$38$Type',285);aj(373,$wnd.Function,{},Qz);_.ib=function Rz(a){this.a.push(Ic(a,6))};aj(261,1,_I,Sz);_.hb=function Tz(){wy(this.a)};var Og=PE(UJ,'SimpleElementBindingStrategy/lambda$4$Type',261);aj(260,1,{},Uz);_.I=function Vz(){xy(this.a)};var Pg=PE(UJ,'SimpleElementBindingStrategy/lambda$5$Type',260);aj(263,1,UI,Xz);_.N=function Yz(){Wz(this)};var Qg=PE(UJ,'SimpleElementBindingStrategy/lambda$6$Type',263);aj(262,1,FI,Zz);_._=function $z(){return this.a[this.b]};var Rg=PE(UJ,'SimpleElementBindingStrategy/lambda$7$Type',262);aj(265,1,fJ,_z);_.lb=function aA(a){wC(new bA(this.a))};var Sg=PE(UJ,'SimpleElementBindingStrategy/lambda$8$Type',265);aj(264,1,_I,bA);_.hb=function cA(){lx(this.a)};var Tg=PE(UJ,'SimpleElementBindingStrategy/lambda$9$Type',264);aj(286,1,{317:1},hA);_.Mb=function iA(a,b,c){fA(a,b)};_.Nb=function jA(a){return $doc.createTextNode('')};_.Ob=function kA(a){return a.c.has(7)};var dA;var Xg=PE(UJ,'TextBindingStrategy',286);aj(287,1,VI,lA);_.I=function mA(){eA();RD(this.a,Pc(PA(this.b)))};var Vg=PE(UJ,'TextBindingStrategy/lambda$0$Type',287);aj(288,1,{107:1},nA);_.kb=function oA(a){gA(this.b,this.a)};var Wg=PE(UJ,'TextBindingStrategy/lambda$1$Type',288);aj(345,$wnd.Function,{},tA);_.ib=function uA(a){this.a.add(a)};aj(349,$wnd.Function,{},wA);_.eb=function xA(a,b){this.a.push(a)};var zA,AA=false;aj(297,1,{},CA);var Yg=PE('com.vaadin.client.flow.dom','PolymerDomApiImpl',297);aj(78,1,{78:1},DA);var Zg=PE('com.vaadin.client.flow.model','UpdatableModelProperties',78);aj(379,$wnd.Function,{},EA);_.ib=function FA(a){this.a.add(Pc(a))};aj(87,1,{});_.Sb=function HA(){return this.e};var yh=PE($I,'ReactiveValueChangeEvent',87);aj(53,87,{53:1},IA);_.Sb=function JA(){return Ic(this.e,30)};_.b=false;_.c=0;var $g=PE(eK,'ListSpliceEvent',53);aj(14,1,{14:1,318:1},YA);_.Tb=function ZA(a){return _A(this.a,a)};_.b=false;_.c=false;_.d=false;var KA;var ih=PE(eK,'MapProperty',14);aj(86,1,{});var xh=PE($I,'ReactiveEventRouter',86);aj(241,86,{},fB);_.Ub=function gB(a,b){Ic(a,46).mb(Ic(b,79))};_.Vb=function hB(a){return new iB(a)};var ah=PE(eK,'MapProperty/1',241);aj(242,1,gJ,iB);_.mb=function jB(a){jC(this.a)};var _g=PE(eK,'MapProperty/1/0methodref$onValueChange$Type',242);aj(240,1,UI,kB);_.N=function lB(){LA()};var bh=PE(eK,'MapProperty/lambda$0$Type',240);aj(243,1,_I,mB);_.hb=function nB(){this.a.d=false};var dh=PE(eK,'MapProperty/lambda$1$Type',243);aj(244,1,_I,oB);_.hb=function pB(){this.a.d=false};var eh=PE(eK,'MapProperty/lambda$2$Type',244);aj(245,1,UI,qB);_.N=function rB(){UA(this.a,this.b)};var fh=PE(eK,'MapProperty/lambda$3$Type',245);aj(88,87,{88:1},sB);_.Sb=function tB(){return Ic(this.e,42)};var gh=PE(eK,'MapPropertyAddEvent',88);aj(79,87,{79:1},uB);_.Sb=function vB(){return Ic(this.e,14)};var hh=PE(eK,'MapPropertyChangeEvent',79);aj(34,1,{34:1});_.d=0;var jh=PE(eK,'NodeFeature',34);aj(30,34,{34:1,30:1,318:1},DB);_.Tb=function EB(a){return _A(this.a,a)};_.Wb=function FB(a){var b,c,d;c=[];for(b=0;b=0?':'+this.c:'')+')'};_.c=0;var mi=PE(nI,'StackTraceElement',31);Gc={4:1,113:1,32:1,2:1};var pi=PE(nI,'String',2);aj(69,84,{113:1},UF,VF,WF);var ni=PE(nI,'StringBuilder',69);aj(126,70,rI,XF);var oi=PE(nI,'StringIndexOutOfBoundsException',126);aj(494,1,{});var YF;aj(108,1,EI,_F);_.U=function aG(a){return $F(a)};var qi=PE(nI,'Throwable/lambda$0$Type',108);aj(96,8,rI,bG);var si=PE(nI,'UnsupportedOperationException',96);aj(334,1,{106:1});_.dc=function cG(a){throw Ui(new bG('Add not supported on this collection'))};_.u=function dG(){var a,b,c;c=new cH;for(b=this.ec();b.hc();){a=b.ic();bH(c,a===this?'(this Collection)':a==null?sI:ej(a))}return !c.a?c.c:c.e.length==0?c.a.a:c.a.a+(''+c.e)};var ti=PE(rK,'AbstractCollection',334);aj(335,334,{106:1,93:1});_.gc=function eG(a,b){throw Ui(new bG('Add not supported on this list'))};_.dc=function fG(a){this.gc(this.fc(),a);return true};_.r=function gG(a){var b,c,d,e,f;if(a===this){return true}if(!Sc(a,40)){return false}f=Ic(a,93);if(this.a.length!=f.a.length){return false}e=new wG(f);for(c=new wG(this);c.a Element[]; +export declare const licenseCheckOk: (data: Product) => void; +export declare const licenseCheckFailed: (data: ProductAndMessage) => void; +export declare const licenseCheckNoKey: (data: ProductAndMessage) => void; +export declare const licenseInit: () => void; diff --git a/java/demo/frontend/generated/jar-resources/License.js b/java/demo/frontend/generated/jar-resources/License.js new file mode 100644 index 000000000..5f0f61782 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/License.js @@ -0,0 +1,110 @@ +const noLicenseFallbackTimeout = 1000; +export const findAll = (element, tags) => { + const lightDom = Array.from(element.querySelectorAll(tags.join(', '))); + const shadowDom = Array.from(element.querySelectorAll('*')) + .filter((e) => e.shadowRoot) + .flatMap((e) => findAll(e.shadowRoot, tags)); + return [...lightDom, ...shadowDom]; +}; +let licenseCheckListener = false; +const showNoLicenseFallback = (element, productAndMessage) => { + if (!licenseCheckListener) { + // When a license check has succeeded, refresh so that all elements are properly shown again + window.addEventListener('message', (e) => { + if (e.data === 'validate-license') { + window.location.reload(); + } + }, false); + licenseCheckListener = true; + } + const overlay = element._overlayElement; + if (overlay) { + if (overlay.shadowRoot) { + const defaultSlot = overlay.shadowRoot.querySelector('slot:not([name])'); + if (defaultSlot && defaultSlot.assignedElements().length > 0) { + showNoLicenseFallback(defaultSlot.assignedElements()[0], productAndMessage); + return; + } + } + showNoLicenseFallback(overlay, productAndMessage); + return; + } + const htmlMessage = productAndMessage.messageHtml + ? productAndMessage.messageHtml + : `${productAndMessage.message}

Component: ${productAndMessage.product.name} ${productAndMessage.product.version}

`.replace(/https:([^ ]*)/g, "https:$1"); + if (element.isConnected) { + element.outerHTML = `
${htmlMessage}
`; + } +}; +const productTagNames = {}; +const productChecking = {}; +const productMissingLicense = {}; +const productCheckOk = {}; +const key = (product) => { + return `${product.name}_${product.version}`; +}; +const checkLicenseIfNeeded = (cvdlElement) => { + var _a; + const { cvdlName, version } = cvdlElement.constructor; + const product = { name: cvdlName, version }; + const tagName = cvdlElement.tagName.toLowerCase(); + productTagNames[cvdlName] = (_a = productTagNames[cvdlName]) !== null && _a !== void 0 ? _a : []; + productTagNames[cvdlName].push(tagName); + const failedLicenseCheck = productMissingLicense[key(product)]; + if (failedLicenseCheck) { + // Has been checked and the check failed + setTimeout(() => showNoLicenseFallback(cvdlElement, failedLicenseCheck), noLicenseFallbackTimeout); + } + if (productMissingLicense[key(product)] || productCheckOk[key(product)]) { + // Already checked + } + else if (!productChecking[key(product)]) { + // Has not been checked + productChecking[key(product)] = true; + window.Vaadin.devTools.checkLicense(product); + } +}; +export const licenseCheckOk = (data) => { + productCheckOk[key(data)] = true; + // eslint-disable-next-line no-console + console.debug('License check ok for', data); +}; +export const licenseCheckFailed = (data) => { + const productName = data.product.name; + productMissingLicense[key(data.product)] = data; + // eslint-disable-next-line no-console + console.error('License check failed for', productName); + const tags = productTagNames[productName]; + if ((tags === null || tags === void 0 ? void 0 : tags.length) > 0) { + findAll(document, tags).forEach((element) => { + setTimeout(() => showNoLicenseFallback(element, productMissingLicense[key(data.product)]), noLicenseFallbackTimeout); + }); + } +}; +export const licenseCheckNoKey = (data) => { + const keyUrl = data.message; + const productName = data.product.name; + data.messageHtml = `No license found. Go here to start a trial or retrieve your license.`; + productMissingLicense[key(data.product)] = data; + // eslint-disable-next-line no-console + console.error('No license found when checking', productName); + const tags = productTagNames[productName]; + if ((tags === null || tags === void 0 ? void 0 : tags.length) > 0) { + findAll(document, tags).forEach((element) => { + setTimeout(() => showNoLicenseFallback(element, productMissingLicense[key(data.product)]), noLicenseFallbackTimeout); + }); + } +}; +export const licenseInit = () => { + // Process already registered elements + window.Vaadin.devTools.createdCvdlElements.forEach((element) => { + checkLicenseIfNeeded(element); + }); + // Handle new elements directly + window.Vaadin.devTools.createdCvdlElements = { + push: (element) => { + checkLicenseIfNeeded(element); + } + }; +}; +//# sourceMappingURL=License.js.map \ No newline at end of file diff --git a/java/demo/frontend/generated/jar-resources/License.js.map b/java/demo/frontend/generated/jar-resources/License.js.map new file mode 100644 index 000000000..49a8674cd --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/License.js.map @@ -0,0 +1 @@ +{"version":3,"file":"License.js","sourceRoot":"","sources":["../../../../src/main/frontend/License.ts"],"names":[],"mappings":"AAAA,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAatC,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,OAAwC,EAAE,IAAc,EAAa,EAAE;IAC7F,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvE,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;SACxD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;SAC3B,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,UAAW,EAAE,IAAI,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,QAAQ,EAAE,GAAG,SAAS,CAAC,CAAC;AACrC,CAAC,CAAC;AAEF,IAAI,oBAAoB,GAAG,KAAK,CAAC;AAEjC,MAAM,qBAAqB,GAAG,CAAC,OAAgB,EAAE,iBAAoC,EAAE,EAAE;IACvF,IAAI,CAAC,oBAAoB,EAAE;QACzB,4FAA4F;QAC5F,MAAM,CAAC,gBAAgB,CACrB,SAAS,EACT,CAAC,CAAC,EAAE,EAAE;YACJ,IAAI,CAAC,CAAC,IAAI,KAAK,kBAAkB,EAAE;gBACjC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;aAC1B;QACH,CAAC,EACD,KAAK,CACN,CAAC;QACF,oBAAoB,GAAG,IAAI,CAAC;KAC7B;IACD,MAAM,OAAO,GAAI,OAAe,CAAC,eAAe,CAAC;IACjD,IAAI,OAAO,EAAE;QACX,IAAI,OAAO,CAAC,UAAU,EAAE;YACtB,MAAM,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC;YACzE,IAAI,WAAW,IAAI,WAAW,CAAC,gBAAgB,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE;gBAC5D,qBAAqB,CAAC,WAAW,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC;gBAC5E,OAAO;aACR;SACF;QACD,qBAAqB,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;QAClD,OAAO;KACR;IAED,MAAM,WAAW,GAAG,iBAAiB,CAAC,WAAW;QAC/C,CAAC,CAAC,iBAAiB,CAAC,WAAW;QAC/B,CAAC,CAAC,GAAG,iBAAiB,CAAC,OAAO,kBAAkB,iBAAiB,CAAC,OAAO,CAAC,IAAI,IAAI,iBAAiB,CAAC,OAAO,CAAC,OAAO,MAAM,CAAC,OAAO,CAC7H,gBAAgB,EAChB,iCAAiC,CAClC,CAAC;IAEN,IAAI,OAAO,CAAC,WAAW,EAAE;QACvB,OAAO,CAAC,SAAS,GAAG,sGAAsG,WAAW,qBAAqB,CAAC;KAC5J;AACH,CAAC,CAAC;AAEF,MAAM,eAAe,GAA6B,EAAE,CAAC;AACrD,MAAM,eAAe,GAA4B,EAAE,CAAC;AACpD,MAAM,qBAAqB,GAAsC,EAAE,CAAC;AACpE,MAAM,cAAc,GAA4B,EAAE,CAAC;AAEnD,MAAM,GAAG,GAAG,CAAC,OAAgB,EAAU,EAAE;IACvC,OAAO,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;AAC9C,CAAC,CAAC;AAEF,MAAM,oBAAoB,GAAG,CAAC,WAAoB,EAAE,EAAE;;IACpD,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,WAAW,CAAC,WAGzC,CAAC;IACF,MAAM,OAAO,GAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IACrD,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IAClD,eAAe,CAAC,QAAQ,CAAC,GAAG,MAAA,eAAe,CAAC,QAAQ,CAAC,mCAAI,EAAE,CAAC;IAC5D,eAAe,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAExC,MAAM,kBAAkB,GAAG,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/D,IAAI,kBAAkB,EAAE;QACtB,wCAAwC;QACxC,UAAU,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,WAAW,EAAE,kBAAkB,CAAC,EAAE,wBAAwB,CAAC,CAAC;KACpG;IAED,IAAI,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE;QACvE,kBAAkB;KACnB;SAAM,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE;QACzC,uBAAuB;QACvB,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;QACpC,MAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;KACvD;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAa,EAAE,EAAE;IAC9C,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;IAEjC,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAuB,EAAE,EAAE;IAC5D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IACtC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAChD,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,WAAW,CAAC,CAAC;IAEvD,MAAM,IAAI,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC1C,IAAI,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,MAAM,IAAG,CAAC,EAAE;QACpB,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC1C,UAAU,CACR,GAAG,EAAE,CAAC,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAC9E,wBAAwB,CACzB,CAAC;QACJ,CAAC,CAAC,CAAC;KACJ;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAuB,EAAE,EAAE;IAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;IAE5B,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IACtC,IAAI,CAAC,WAAW,GAAG,sGAAsG,MAAM,0DAA0D,CAAC;IAC1L,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAChD,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,WAAW,CAAC,CAAC;IAE7D,MAAM,IAAI,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC1C,IAAI,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,MAAM,IAAG,CAAC,EAAE;QACpB,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC1C,UAAU,CACR,GAAG,EAAE,CAAC,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAC9E,wBAAwB,CACzB,CAAC;QACJ,CAAC,CAAC,CAAC;KACJ;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,EAAE;IAC9B,sCAAsC;IACrC,MAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,OAAgB,EAAE,EAAE;QAC/E,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,+BAA+B;IAC9B,MAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,GAAG;QACpD,IAAI,EAAE,CAAC,OAAgB,EAAE,EAAE;YACzB,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC,CAAC","sourcesContent":["const noLicenseFallbackTimeout = 1000;\n\nexport interface Product {\n name: string;\n version: string;\n}\n\nexport interface ProductAndMessage {\n message: string;\n messageHtml?: string;\n product: Product;\n}\n\nexport const findAll = (element: Element | ShadowRoot | Document, tags: string[]): Element[] => {\n const lightDom = Array.from(element.querySelectorAll(tags.join(', ')));\n const shadowDom = Array.from(element.querySelectorAll('*'))\n .filter((e) => e.shadowRoot)\n .flatMap((e) => findAll(e.shadowRoot!, tags));\n return [...lightDom, ...shadowDom];\n};\n\nlet licenseCheckListener = false;\n\nconst showNoLicenseFallback = (element: Element, productAndMessage: ProductAndMessage) => {\n if (!licenseCheckListener) {\n // When a license check has succeeded, refresh so that all elements are properly shown again\n window.addEventListener(\n 'message',\n (e) => {\n if (e.data === 'validate-license') {\n window.location.reload();\n }\n },\n false\n );\n licenseCheckListener = true;\n }\n const overlay = (element as any)._overlayElement;\n if (overlay) {\n if (overlay.shadowRoot) {\n const defaultSlot = overlay.shadowRoot.querySelector('slot:not([name])');\n if (defaultSlot && defaultSlot.assignedElements().length > 0) {\n showNoLicenseFallback(defaultSlot.assignedElements()[0], productAndMessage);\n return;\n }\n }\n showNoLicenseFallback(overlay, productAndMessage);\n return;\n }\n\n const htmlMessage = productAndMessage.messageHtml\n ? productAndMessage.messageHtml\n : `${productAndMessage.message}

Component: ${productAndMessage.product.name} ${productAndMessage.product.version}

`.replace(\n /https:([^ ]*)/g,\n \"https:$1\"\n );\n\n if (element.isConnected) {\n element.outerHTML = `
${htmlMessage}
`;\n }\n};\n\nconst productTagNames: Record = {};\nconst productChecking: Record = {};\nconst productMissingLicense: Record = {};\nconst productCheckOk: Record = {};\n\nconst key = (product: Product): string => {\n return `${product.name}_${product.version}`;\n};\n\nconst checkLicenseIfNeeded = (cvdlElement: Element) => {\n const { cvdlName, version } = cvdlElement.constructor as CustomElementConstructor & {\n cvdlName: string;\n version: string;\n };\n const product: Product = { name: cvdlName, version };\n const tagName = cvdlElement.tagName.toLowerCase();\n productTagNames[cvdlName] = productTagNames[cvdlName] ?? [];\n productTagNames[cvdlName].push(tagName);\n\n const failedLicenseCheck = productMissingLicense[key(product)];\n if (failedLicenseCheck) {\n // Has been checked and the check failed\n setTimeout(() => showNoLicenseFallback(cvdlElement, failedLicenseCheck), noLicenseFallbackTimeout);\n }\n\n if (productMissingLicense[key(product)] || productCheckOk[key(product)]) {\n // Already checked\n } else if (!productChecking[key(product)]) {\n // Has not been checked\n productChecking[key(product)] = true;\n (window as any).Vaadin.devTools.checkLicense(product);\n }\n};\n\nexport const licenseCheckOk = (data: Product) => {\n productCheckOk[key(data)] = true;\n\n // eslint-disable-next-line no-console\n console.debug('License check ok for', data);\n};\n\nexport const licenseCheckFailed = (data: ProductAndMessage) => {\n const productName = data.product.name;\n productMissingLicense[key(data.product)] = data;\n // eslint-disable-next-line no-console\n console.error('License check failed for', productName);\n\n const tags = productTagNames[productName];\n if (tags?.length > 0) {\n findAll(document, tags).forEach((element) => {\n setTimeout(\n () => showNoLicenseFallback(element, productMissingLicense[key(data.product)]),\n noLicenseFallbackTimeout\n );\n });\n }\n};\n\nexport const licenseCheckNoKey = (data: ProductAndMessage) => {\n const keyUrl = data.message;\n\n const productName = data.product.name;\n data.messageHtml = `No license found. Go here to start a trial or retrieve your license.`;\n productMissingLicense[key(data.product)] = data;\n // eslint-disable-next-line no-console\n console.error('No license found when checking', productName);\n\n const tags = productTagNames[productName];\n if (tags?.length > 0) {\n findAll(document, tags).forEach((element) => {\n setTimeout(\n () => showNoLicenseFallback(element, productMissingLicense[key(data.product)]),\n noLicenseFallbackTimeout\n );\n });\n }\n};\n\nexport const licenseInit = () => {\n // Process already registered elements\n (window as any).Vaadin.devTools.createdCvdlElements.forEach((element: Element) => {\n checkLicenseIfNeeded(element);\n });\n\n // Handle new elements directly\n (window as any).Vaadin.devTools.createdCvdlElements = {\n push: (element: Element) => {\n checkLicenseIfNeeded(element);\n }\n };\n};\n"]} \ No newline at end of file diff --git a/java/demo/frontend/generated/jar-resources/comboBoxConnector.js b/java/demo/frontend/generated/jar-resources/comboBoxConnector.js new file mode 100644 index 000000000..1168c0fbe --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/comboBoxConnector.js @@ -0,0 +1,284 @@ +import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; +import { timeOut } from '@polymer/polymer/lib/utils/async.js'; +import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js'; + +(function () { + const tryCatchWrapper = function (callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Combo Box'); + }; + + window.Vaadin.Flow.comboBoxConnector = { + initLazy: (comboBox) => + tryCatchWrapper(function (comboBox) { + // Check whether the connector was already initialized for the ComboBox + if (comboBox.$connector) { + return; + } + + comboBox.$connector = {}; + + // holds pageIndex -> callback pairs of subsequent indexes (current active range) + const pageCallbacks = {}; + let cache = {}; + let lastFilter = ''; + const placeHolder = new window.Vaadin.ComboBoxPlaceholder(); + + const serverFacade = (() => { + // Private variables + let lastFilterSentToServer = ''; + let dataCommunicatorResetNeeded = false; + + // Public methods + const needsDataCommunicatorReset = () => (dataCommunicatorResetNeeded = true); + const getLastFilterSentToServer = () => lastFilterSentToServer; + const requestData = (startIndex, endIndex, params) => { + const count = endIndex - startIndex; + const filter = params.filter; + + comboBox.$server.setRequestedRange(startIndex, count, filter); + lastFilterSentToServer = filter; + if (dataCommunicatorResetNeeded) { + comboBox.$server.resetDataCommunicator(); + dataCommunicatorResetNeeded = false; + } + }; + + return { + needsDataCommunicatorReset, + getLastFilterSentToServer, + requestData + }; + })(); + + const clearPageCallbacks = (pages = Object.keys(pageCallbacks)) => { + // Flush and empty the existing requests + pages.forEach((page) => { + pageCallbacks[page]([], comboBox.size); + delete pageCallbacks[page]; + + // Empty the comboBox's internal cache without invoking observers by filling + // the filteredItems array with placeholders (comboBox will request for data when it + // encounters a placeholder) + const pageStart = parseInt(page) * comboBox.pageSize; + const pageEnd = pageStart + comboBox.pageSize; + const end = Math.min(pageEnd, comboBox.filteredItems.length); + for (let i = pageStart; i < end; i++) { + comboBox.filteredItems[i] = placeHolder; + } + }); + }; + + comboBox.dataProvider = function (params, callback) { + if (params.pageSize != comboBox.pageSize) { + throw 'Invalid pageSize'; + } + + if (comboBox._clientSideFilter) { + // For clientside filter we first make sure we have all data which we also + // filter based on comboBox.filter. While later we only filter clientside data. + + if (cache[0]) { + performClientSideFilter(cache[0], params.filter, callback); + return; + } else { + // If client side filter is enabled then we need to first ask all data + // and filter it on client side, otherwise next time when user will + // input another filter, eg. continue to type, the local cache will be only + // what was received for the first filter, which may not be the whole + // data from server (keep in mind that client side filter is enabled only + // when the items count does not exceed one page). + params.filter = ''; + } + } + + const filterChanged = params.filter !== lastFilter; + if (filterChanged) { + cache = {}; + lastFilter = params.filter; + this._filterDebouncer = Debouncer.debounce(this._filterDebouncer, timeOut.after(500), () => { + if (serverFacade.getLastFilterSentToServer() === params.filter) { + // Fixes the case when the filter changes + // to something else and back to the original value + // within debounce timeout, and the + // DataCommunicator thinks it doesn't need to send data + serverFacade.needsDataCommunicatorReset(); + } + if (params.filter !== lastFilter) { + throw new Error("Expected params.filter to be '" + lastFilter + "' but was '" + params.filter + "'"); + } + // Remove the debouncer before clearing page callbacks. + // This makes sure that they are executed. + this._filterDebouncer = undefined; + // Call the method again after debounce. + clearPageCallbacks(); + comboBox.dataProvider(params, callback); + }); + return; + } + + // Postpone the execution of new callbacks if there is an active debouncer. + // They will be executed when the page callbacks are cleared within the debouncer. + if (this._filterDebouncer) { + pageCallbacks[params.page] = callback; + return; + } + + if (cache[params.page]) { + // This may happen after skipping pages by scrolling fast + commitPage(params.page, callback); + } else { + pageCallbacks[params.page] = callback; + const maxRangeCount = Math.max(params.pageSize * 2, 500); // Max item count in active range + const activePages = Object.keys(pageCallbacks).map((page) => parseInt(page)); + const rangeMin = Math.min(...activePages); + const rangeMax = Math.max(...activePages); + + if (activePages.length * params.pageSize > maxRangeCount) { + if (params.page === rangeMin) { + clearPageCallbacks([String(rangeMax)]); + } else { + clearPageCallbacks([String(rangeMin)]); + } + comboBox.dataProvider(params, callback); + } else if (rangeMax - rangeMin + 1 !== activePages.length) { + // Wasn't a sequential page index, clear the cache so combo-box will request for new pages + clearPageCallbacks(); + } else { + // The requested page was sequential, extend the requested range + const startIndex = params.pageSize * rangeMin; + const endIndex = params.pageSize * (rangeMax + 1); + + serverFacade.requestData(startIndex, endIndex, params); + } + } + }; + + comboBox.$connector.clear = tryCatchWrapper((start, length) => { + const firstPageToClear = Math.floor(start / comboBox.pageSize); + const numberOfPagesToClear = Math.ceil(length / comboBox.pageSize); + + for (let i = firstPageToClear; i < firstPageToClear + numberOfPagesToClear; i++) { + delete cache[i]; + } + }); + + comboBox.$connector.filter = tryCatchWrapper(function (item, filter) { + filter = filter ? filter.toString().toLowerCase() : ''; + return comboBox._getItemLabel(item, comboBox.itemLabelPath).toString().toLowerCase().indexOf(filter) > -1; + }); + + comboBox.$connector.set = tryCatchWrapper(function (index, items, filter) { + if (filter != serverFacade.getLastFilterSentToServer()) { + return; + } + + if (index % comboBox.pageSize != 0) { + throw 'Got new data to index ' + index + ' which is not aligned with the page size of ' + comboBox.pageSize; + } + + if (index === 0 && items.length === 0 && pageCallbacks[0]) { + // Makes sure that the dataProvider callback is called even when server + // returns empty data set (no items match the filter). + cache[0] = []; + return; + } + + const firstPageToSet = index / comboBox.pageSize; + const updatedPageCount = Math.ceil(items.length / comboBox.pageSize); + + for (let i = 0; i < updatedPageCount; i++) { + let page = firstPageToSet + i; + let slice = items.slice(i * comboBox.pageSize, (i + 1) * comboBox.pageSize); + + cache[page] = slice; + } + }); + + comboBox.$connector.updateData = tryCatchWrapper(function (items) { + const itemsMap = new Map(items.map((item) => [item.key, item])); + + comboBox.filteredItems = comboBox.filteredItems.map((item) => { + return itemsMap.get(item.key) || item; + }); + }); + + comboBox.$connector.updateSize = tryCatchWrapper(function (newSize) { + if (!comboBox._clientSideFilter) { + // FIXME: It may be that this size set is unnecessary, since when + // providing data to combobox via callback we may use data's size. + // However, if this size reflect the whole data size, including + // data not fetched yet into client side, and combobox expect it + // to be set as such, the at least, we don't need it in case the + // filter is clientSide only, since it'll increase the height of + // the popup at only at first user filter to this size, while the + // filtered items count are less. + comboBox.size = newSize; + } + }); + + comboBox.$connector.reset = tryCatchWrapper(function () { + clearPageCallbacks(); + cache = {}; + comboBox.clearCache(); + }); + + comboBox.$connector.confirm = tryCatchWrapper(function (id, filter) { + if (filter != serverFacade.getLastFilterSentToServer()) { + return; + } + + // We're done applying changes from this batch, resolve pending + // callbacks + let activePages = Object.getOwnPropertyNames(pageCallbacks); + for (let i = 0; i < activePages.length; i++) { + let page = activePages[i]; + + if (cache[page]) { + commitPage(page, pageCallbacks[page]); + } + } + + // Let server know we're done + comboBox.$server.confirmUpdate(id); + }); + + const commitPage = tryCatchWrapper(function (page, callback) { + let data = cache[page]; + + if (comboBox._clientSideFilter) { + performClientSideFilter(data, comboBox.filter, callback); + } else { + // Remove the data if server-side filtering, but keep it for client-side + // filtering + delete cache[page]; + + // FIXME: It may be that we ought to provide data.length instead of + // comboBox.size and remove updateSize function. + callback(data, comboBox.size); + } + }); + + // Perform filter on client side (here) using the items from specified page + // and submitting the filtered items to specified callback. + // The filter used is the one from combobox, not the lastFilter stored since + // that may not reflect user's input. + const performClientSideFilter = tryCatchWrapper(function (page, filter, callback) { + let filteredItems = page; + + if (filter) { + filteredItems = page.filter((item) => comboBox.$connector.filter(item, filter)); + } + + callback(filteredItems, filteredItems.length); + }); + + // Prevent setting the custom value as the 'value'-prop automatically + comboBox.addEventListener( + 'custom-value-set', + tryCatchWrapper((e) => e.preventDefault()) + ); + })(comboBox) + }; +})(); + +window.Vaadin.ComboBoxPlaceholder = ComboBoxPlaceholder; diff --git a/java/demo/frontend/generated/jar-resources/confirmDialogConnector.js b/java/demo/frontend/generated/jar-resources/confirmDialogConnector.js new file mode 100644 index 000000000..98fea9124 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/confirmDialogConnector.js @@ -0,0 +1,40 @@ +(function () { + function copyClassName(dialog) { + const overlay = dialog._overlayElement; + if (overlay) { + overlay.className = dialog.className; + } + } + + const observer = new MutationObserver((records) => { + records.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + copyClassName(mutation.target); + } + }); + }); + + window.Vaadin.Flow.confirmDialogConnector = { + initLazy: function (dialog) { + if (dialog.$connector) { + return; + } + + dialog.$connector = {}; + + dialog.addEventListener('opened-changed', (e) => { + if (e.detail.value) { + copyClassName(dialog); + } + }); + + observer.observe(dialog, { + attributes: true, + attributeFilter: ['class'] + }); + + // Copy initial class + copyClassName(dialog); + } + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/contextMenuConnector.js b/java/demo/frontend/generated/jar-resources/contextMenuConnector.js new file mode 100644 index 000000000..edbd5a523 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/contextMenuConnector.js @@ -0,0 +1,117 @@ +(function () { + function tryCatchWrapper(callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Context Menu'); + } + + function getContainer(appId, nodeId) { + try { + return window.Vaadin.Flow.clients[appId].getByNodeId(nodeId); + } catch (error) { + console.error('Could not get node %s from app %s', nodeId, appId); + console.error(error); + } + } + + /** + * Initializes the connector for a context menu element. + * + * @param {HTMLElement} contextMenu + * @param {string} appId + */ + function initLazy(contextMenu, appId) { + if (contextMenu.$connector) { + return; + } + + contextMenu.$connector = { + /** + * Generates and assigns the items to the context menu. + * + * @param {number} nodeId + */ + generateItems: tryCatchWrapper((nodeId) => { + const items = generateItemsTree(appId, nodeId); + + contextMenu.items = items; + }) + }; + } + + /** + * Generates an items tree compatible with the context-menu web component + * by traversing the given Flow DOM tree of context menu item nodes + * whose root node is identified by the `nodeId` argument. + * + * The app id is required to access the store of Flow DOM nodes. + * + * @param {string} appId + * @param {number} nodeId + */ + function generateItemsTree(appId, nodeId) { + const container = getContainer(appId, nodeId); + if (!container) { + return; + } + + return Array.from(container.children).map((child) => { + const item = { + component: child, + checked: child._checked, + theme: child.__theme + }; + if (child.localName == 'vaadin-context-menu-item' && child._containerNodeId) { + item.children = generateItemsTree(appId, child._containerNodeId); + } + child._item = item; + return item; + }); + } + + /** + * Sets the checked state for a context menu item. + * + * This method is supposed to be called when the context menu item is closed, + * so there is no need for triggering a re-render eagarly. + * + * @param {HTMLElement} component + * @param {boolean} checked + */ + function setChecked(component, checked) { + if (component._item) { + component._item.checked = checked; + } + } + + /** + * Sets the theme for a context menu item. + * + * This method is supposed to be called when the context menu item is closed, + * so there is no need for triggering a re-render eagarly. + * + * @param {HTMLElement} component + * @param {string | undefined | null} theme + */ + function setTheme(component, theme) { + if (component._item) { + component._item.theme = theme; + } + } + + window.Vaadin.Flow.contextMenuConnector = { + initLazy(...args) { + return tryCatchWrapper(initLazy)(...args); + }, + + generateItemsTree(...args) { + return tryCatchWrapper(generateItemsTree)(...args); + }, + + setChecked(...args) { + return tryCatchWrapper(setChecked)(...args); + }, + + setTheme(...args) { + return tryCatchWrapper(setTheme)(...args); + } + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/contextMenuTargetConnector.js b/java/demo/frontend/generated/jar-resources/contextMenuTargetConnector.js new file mode 100644 index 000000000..9e2b3bb70 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/contextMenuTargetConnector.js @@ -0,0 +1,70 @@ +import * as Gestures from '@vaadin/component-base/src/gestures.js'; + +(function () { + function tryCatchWrapper(callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Context Menu Target'); + } + + function init(target) { + if (target.$contextMenuTargetConnector) { + return; + } + + target.$contextMenuTargetConnector = { + openOnHandler: tryCatchWrapper(function (e) { + e.preventDefault(); + e.stopPropagation(); + this.$contextMenuTargetConnector.openEvent = e; + let detail = {}; + if (target.getContextMenuBeforeOpenDetail) { + detail = target.getContextMenuBeforeOpenDetail(e); + } + target.dispatchEvent( + new CustomEvent('vaadin-context-menu-before-open', { + detail: detail + }) + ); + }), + + updateOpenOn: tryCatchWrapper(function (eventType) { + this.removeListener(); + this.openOnEventType = eventType; + + customElements.whenDefined('vaadin-context-menu').then( + tryCatchWrapper(() => { + if (Gestures.gestures[eventType]) { + Gestures.addListener(target, eventType, this.openOnHandler); + } else { + target.addEventListener(eventType, this.openOnHandler); + } + }) + ); + }), + + removeListener: tryCatchWrapper(function () { + if (this.openOnEventType) { + if (Gestures.gestures[this.openOnEventType]) { + Gestures.removeListener(target, this.openOnEventType, this.openOnHandler); + } else { + target.removeEventListener(this.openOnEventType, this.openOnHandler); + } + } + }), + + openMenu: tryCatchWrapper(function (contextMenu) { + contextMenu.open(this.openEvent); + }), + + removeConnector: tryCatchWrapper(function () { + this.removeListener(); + target.$contextMenuTargetConnector = undefined; + }) + }; + } + + window.Vaadin.Flow.contextMenuTargetConnector = { + init(...args) { + return tryCatchWrapper(init)(...args); + } + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/copy-to-clipboard.js b/java/demo/frontend/generated/jar-resources/copy-to-clipboard.js new file mode 100644 index 000000000..475842a87 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/copy-to-clipboard.js @@ -0,0 +1,150 @@ +"use strict"; + +var deselectCurrent = function () { + var selection = document.getSelection(); + if (!selection.rangeCount) { + return function () {}; + } + var active = document.activeElement; + + var ranges = []; + for (var i = 0; i < selection.rangeCount; i++) { + ranges.push(selection.getRangeAt(i)); + } + + switch (active.tagName.toUpperCase()) { // .toUpperCase handles XHTML + case 'INPUT': + case 'TEXTAREA': + active.blur(); + break; + + default: + active = null; + break; + } + + selection.removeAllRanges(); + return function () { + selection.type === 'Caret' && + selection.removeAllRanges(); + + if (!selection.rangeCount) { + ranges.forEach(function(range) { + selection.addRange(range); + }); + } + + active && + active.focus(); + }; +}; + + +var clipboardToIE11Formatting = { + "text/plain": "Text", + "text/html": "Url", + "default": "Text" +} + +var defaultMessage = "Copy to clipboard: #{key}, Enter"; + +function format(message) { + var copyKey = (/mac os x/i.test(navigator.userAgent) ? "⌘" : "Ctrl") + "+C"; + return message.replace(/#{\s*key\s*}/g, copyKey); +} + +export function copy(text, options) { + var debug, + message, + reselectPrevious, + range, + selection, + mark, + success = false; + if (!options) { + options = {}; + } + debug = options.debug || false; + try { + reselectPrevious = deselectCurrent(); + + range = document.createRange(); + selection = document.getSelection(); + + mark = document.createElement("span"); + mark.textContent = text; + // reset user styles for span element + mark.style.all = "unset"; + // prevents scrolling to the end of the page + mark.style.position = "fixed"; + mark.style.top = 0; + mark.style.clip = "rect(0, 0, 0, 0)"; + // used to preserve spaces and line breaks + mark.style.whiteSpace = "pre"; + // do not inherit user-select (it may be `none`) + mark.style.webkitUserSelect = "text"; + mark.style.MozUserSelect = "text"; + mark.style.msUserSelect = "text"; + mark.style.userSelect = "text"; + mark.addEventListener("copy", function(e) { + e.stopPropagation(); + if (options.format) { + e.preventDefault(); + if (typeof e.clipboardData === "undefined") { // IE 11 + debug && console.warn("unable to use e.clipboardData"); + debug && console.warn("trying IE specific stuff"); + window.clipboardData.clearData(); + var format = clipboardToIE11Formatting[options.format] || clipboardToIE11Formatting["default"] + window.clipboardData.setData(format, text); + } else { // all other browsers + e.clipboardData.clearData(); + e.clipboardData.setData(options.format, text); + } + } + if (options.onCopy) { + e.preventDefault(); + options.onCopy(e.clipboardData); + } + }); + + document.body.appendChild(mark); + + range.selectNodeContents(mark); + selection.addRange(range); + + var successful = document.execCommand("copy"); + if (!successful) { + throw new Error("copy command was unsuccessful"); + } + success = true; + } catch (err) { + debug && console.error("unable to copy using execCommand: ", err); + debug && console.warn("trying IE specific stuff"); + try { + window.clipboardData.setData(options.format || "text", text); + options.onCopy && options.onCopy(window.clipboardData); + success = true; + } catch (err) { + debug && console.error("unable to copy using clipboardData: ", err); + debug && console.error("falling back to prompt"); + message = format("message" in options ? options.message : defaultMessage); + window.prompt(message, text); + } + } finally { + if (selection) { + if (typeof selection.removeRange == "function") { + selection.removeRange(range); + } else { + selection.removeAllRanges(); + } + } + + if (mark) { + document.body.removeChild(mark); + } + reselectPrevious(); + } + + return success; +} + diff --git a/java/demo/frontend/generated/jar-resources/datepickerConnector.js b/java/demo/frontend/generated/jar-resources/datepickerConnector.js new file mode 100644 index 000000000..d25deb36f --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/datepickerConnector.js @@ -0,0 +1,159 @@ +import dateFnsFormat from 'date-fns/format'; +import dateFnsParse from 'date-fns/parse'; +import dateFnsIsValid from 'date-fns/isValid'; +import { extractDateParts, parseDate as _parseDate } from '@vaadin/date-picker/src/vaadin-date-picker-helper.js'; + +(function () { + const tryCatchWrapper = function (callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Date Picker'); + }; + + window.Vaadin.Flow.datepickerConnector = { + initLazy: (datepicker) => + tryCatchWrapper(function (datepicker) { + // Check whether the connector was already initialized for the datepicker + if (datepicker.$connector) { + return; + } + + datepicker.$connector = {}; + + const createLocaleBasedDateFormat = function (locale) { + try { + // Check whether the locale is supported or not + new Date().toLocaleDateString(locale); + } catch (e) { + console.warn('The locale is not supported, using default locale setting(en-US).'); + return 'M/d/yyyy'; + } + + // format test date and convert to date-fns pattern + const testDate = new Date(Date.UTC(1234, 4, 6)); + let pattern = testDate.toLocaleDateString(locale, { timeZone: 'UTC' }); + pattern = pattern + // escape date-fns pattern letters by enclosing them in single quotes + .replace(/([a-zA-Z]+)/g, "'$1'") + // insert date placeholder + .replace('06', 'dd') + .replace('6', 'd') + // insert month placeholder + .replace('05', 'MM') + .replace('5', 'M') + // insert year placeholder + .replace('1234', 'yyyy'); + const isValidPattern = pattern.includes('d') && pattern.includes('M') && pattern.includes('y'); + if (!isValidPattern) { + console.warn('The locale is not supported, using default locale setting(en-US).'); + return 'M/d/yyyy'; + } + + return pattern; + }; + + const createFormatterAndParser = tryCatchWrapper(function (formats) { + if (!formats || formats.length === 0) { + throw new Error('Array of custom date formats is null or empty'); + } + + function getShortYearFormat(format) { + if (format.includes('yyyy') && !format.includes('yyyyy')) { + return format.replace('yyyy', 'yy'); + } + if (format.includes('YYYY') && !format.includes('YYYYY')) { + return format.replace('YYYY', 'YY'); + } + return undefined; + } + + function isShortYearFormat(format) { + if (format.includes('y')) { + return !format.includes('yyy'); + } + if (format.includes('Y')) { + return !format.includes('YYY'); + } + return false; + } + + function formatDate(dateParts) { + const format = formats[0]; + const date = _parseDate(`${dateParts.year}-${dateParts.month + 1}-${dateParts.day}`); + + return dateFnsFormat(date, format); + } + + function parseDate(dateString) { + const referenceDate = _getReferenceDate(); + for (let format of formats) { + // We first try to match the date with the shorter version. + const shortYearFormat = getShortYearFormat(format); + if (shortYearFormat) { + const shortYearFormatDate = dateFnsParse(dateString, shortYearFormat, referenceDate); + if (dateFnsIsValid(shortYearFormatDate)) { + let yearValue = shortYearFormatDate.getFullYear(); + // The last parsed year check handles the case where a four-digit year is parsed, then formatted + // as a two-digit year, and then parsed again. In this case we want to keep the century of the + // originally parsed year, instead of using the century of the reference date. + if ( + datepicker.$connector._lastParsedYear && + yearValue === datepicker.$connector._lastParsedYear % 100 + ) { + yearValue = datepicker.$connector._lastParsedYear; + } + return { + day: shortYearFormatDate.getDate(), + month: shortYearFormatDate.getMonth(), + year: yearValue + }; + } + } + const date = dateFnsParse(dateString, format, referenceDate); + + if (dateFnsIsValid(date)) { + let yearValue = date.getFullYear(); + if ( + datepicker.$connector._lastParsedYear && + yearValue % 100 === datepicker.$connector._lastParsedYear % 100 && + isShortYearFormat(format) + ) { + yearValue = datepicker.$connector._lastParsedYear; + } else { + datepicker.$connector._lastParsedYear = yearValue; + } + return { + day: date.getDate(), + month: date.getMonth(), + year: yearValue + }; + } + } + datepicker.$connector._lastParsedYear = undefined; + return false; + } + + return { + formatDate: formatDate, + parseDate: parseDate + }; + }); + + function _getReferenceDate() { + const { referenceDate } = datepicker.i18n; + return referenceDate ? new Date(referenceDate.year, referenceDate.month, referenceDate.day) : new Date(); + } + + datepicker.$connector.updateI18n = tryCatchWrapper(function (locale, i18n) { + // Either use custom formats specified in I18N, or create format from locale + const hasCustomFormats = i18n && i18n.dateFormats && i18n.dateFormats.length > 0; + if (i18n && i18n.referenceDate) { + i18n.referenceDate = extractDateParts(new Date(i18n.referenceDate)); + } + const usedFormats = hasCustomFormats ? i18n.dateFormats : [createLocaleBasedDateFormat(locale)]; + const formatterAndParser = createFormatterAndParser(usedFormats); + + // Merge current web component I18N settings with new I18N settings and the formatting and parsing functions + datepicker.i18n = Object.assign({}, datepicker.i18n, i18n, formatterAndParser); + }); + })(datepicker) + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/dialogConnector.js b/java/demo/frontend/generated/jar-resources/dialogConnector.js new file mode 100644 index 000000000..60a41ce4d --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/dialogConnector.js @@ -0,0 +1,38 @@ +(function () { + function copyClassName(dialog) { + const overlay = dialog.$.overlay; + if (overlay) { + overlay.className = dialog.className; + } + } + + const observer = new MutationObserver((records) => { + records.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + copyClassName(mutation.target); + } + }); + }); + + window.Vaadin.Flow.dialogConnector = { + initLazy: function (dialog) { + if (dialog.$connector) { + return; + } + dialog.$connector = {}; + + dialog.addEventListener('opened-changed', (e) => { + if (e.detail.value) { + copyClassName(dialog); + } + }); + + observer.observe(dialog, { + attributes: true, + attributeFilter: ['class'] + }); + + copyClassName(dialog); + } + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/dndConnector-es6.js b/java/demo/frontend/generated/jar-resources/dndConnector-es6.js new file mode 100644 index 000000000..699684da3 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/dndConnector-es6.js @@ -0,0 +1 @@ +import './dndConnector.js'; diff --git a/java/demo/frontend/generated/jar-resources/dndConnector.js b/java/demo/frontend/generated/jar-resources/dndConnector.js new file mode 100644 index 000000000..13170ec2d --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/dndConnector.js @@ -0,0 +1,110 @@ +window.Vaadin = window.Vaadin || {}; +window.Vaadin.Flow = window.Vaadin.Flow || {}; +window.Vaadin.Flow.dndConnector = { + __ondragenterListener: function (event) { + // TODO filter by data type + // TODO prevent dropping on itself (by default) + const effect = event.currentTarget['__dropEffect']; + if (!event.currentTarget.hasAttribute('disabled')) { + if (effect) { + event.dataTransfer.dropEffect = effect; + } + + if (effect && effect !== 'none') { + /* #7108: if drag moves on top of drop target's children, first another ondragenter event + * is fired and then a ondragleave event. This happens again once the drag + * moves on top of another children, or back on top of the drop target element. + * Thus need to "cancel" the following ondragleave, to not remove class name. + * Drop event will happen even when dropped to a child element. */ + if (event.currentTarget.classList.contains('v-drag-over-target')) { + event.currentTarget['__skip-leave'] = true; + } else { + event.currentTarget.classList.add('v-drag-over-target'); + } + // enables browser specific pseudo classes (at least FF) + event.preventDefault(); + event.stopPropagation(); // don't let parents know + } + } + }, + + __ondragoverListener: function (event) { + // TODO filter by data type + // TODO filter by effectAllowed != dropEffect due to Safari & IE11 ? + if (!event.currentTarget.hasAttribute('disabled')) { + const effect = event.currentTarget['__dropEffect']; + if (effect) { + event.dataTransfer.dropEffect = effect; + } + // allows the drop && don't let parents know + event.preventDefault(); + event.stopPropagation(); + } + }, + + __ondragleaveListener: function (event) { + if (event.currentTarget['__skip-leave']) { + event.currentTarget['__skip-leave'] = false; + } else { + event.currentTarget.classList.remove('v-drag-over-target'); + } + // #7109 need to stop or any parent drop target might not get highlighted, + // as ondragenter for it is fired before the child gets dragleave. + event.stopPropagation(); + }, + + __ondropListener: function (event) { + const effect = event.currentTarget['__dropEffect']; + if (effect) { + event.dataTransfer.dropEffect = effect; + } + event.currentTarget.classList.remove('v-drag-over-target'); + // prevent browser handling && don't let parents know + event.preventDefault(); + event.stopPropagation(); + }, + + updateDropTarget: function (element) { + if (element['__active']) { + element.addEventListener('dragenter', this.__ondragenterListener, false); + element.addEventListener('dragover', this.__ondragoverListener, false); + element.addEventListener('dragleave', this.__ondragleaveListener, false); + element.addEventListener('drop', this.__ondropListener, false); + } else { + element.removeEventListener('dragenter', this.__ondragenterListener, false); + element.removeEventListener('dragover', this.__ondragoverListener, false); + element.removeEventListener('dragleave', this.__ondragleaveListener, false); + element.removeEventListener('drop', this.__ondropListener, false); + element.classList.remove('v-drag-over-target'); + } + }, + + /** DRAG SOURCE METHODS: */ + + __dragstartListener: function (event) { + event.stopPropagation(); + event.dataTransfer.setData('text/plain', ''); + if (event.currentTarget.hasAttribute('disabled')) { + event.preventDefault(); + } else { + if (event.currentTarget['__effectAllowed']) { + event.dataTransfer.effectAllowed = event.currentTarget['__effectAllowed']; + } + event.currentTarget.classList.add('v-dragged'); + } + }, + + __dragendListener: function (event) { + event.currentTarget.classList.remove('v-dragged'); + }, + + updateDragSource: function (element) { + if (element['draggable']) { + element.addEventListener('dragstart', this.__dragstartListener, false); + element.addEventListener('dragend', this.__dragendListener, false); + } else { + element.removeEventListener('dragstart', this.__dragstartListener, false); + element.removeEventListener('dragend', this.__dragendListener, false); + } + } +}; diff --git a/java/demo/frontend/generated/jar-resources/flow-component-renderer.js b/java/demo/frontend/generated/jar-resources/flow-component-renderer.js new file mode 100644 index 000000000..0f6ad78ae --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/flow-component-renderer.js @@ -0,0 +1,155 @@ +import '@polymer/polymer/lib/elements/dom-if.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; +import { idlePeriod } from '@polymer/polymer/lib/utils/async.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +class FlowComponentRenderer extends PolymerElement { + static get template() { + return html` + + + `; + } + + static get is() { + return 'flow-component-renderer'; + } + static get properties() { + return { + nodeid: Number, + appid: String, + }; + } + static get observers() { + return ['_attachRenderedComponentIfAble(appid, nodeid)']; + } + + ready() { + super.ready(); + this.addEventListener('click', function (event) { + if ( + this.firstChild && + typeof this.firstChild.click === 'function' && + event.target === this + ) { + event.stopPropagation(); + this.firstChild.click(); + } + }); + this.addEventListener('animationend', this._onAnimationEnd); + } + + _asyncAttachRenderedComponentIfAble() { + this._debouncer = Debouncer.debounce(this._debouncer, idlePeriod, () => + this._attachRenderedComponentIfAble() + ); + } + + _attachRenderedComponentIfAble() { + if (!this.nodeid || !this.appid) { + return; + } + const renderedComponent = this._getRenderedComponent(); + if (this.firstChild) { + if (!renderedComponent) { + this._asyncAttachRenderedComponentIfAble(); + } else if (this.firstChild !== renderedComponent) { + this.replaceChild(renderedComponent, this.firstChild); + this._defineFocusTarget(); + this.onComponentRendered(); + } else { + this._defineFocusTarget(); + this.onComponentRendered(); + } + } else { + if (renderedComponent) { + this.appendChild(renderedComponent); + this._defineFocusTarget(); + this.onComponentRendered(); + } else { + this._asyncAttachRenderedComponentIfAble(); + } + } + } + + _getRenderedComponent() { + try { + return window.Vaadin.Flow.clients[this.appid].getByNodeId(this.nodeid); + } catch (error) { + console.error( + 'Could not get node %s from app %s', + this.nodeid, + this.appid + ); + console.error(error); + } + return null; + } + + onComponentRendered() { + // subclasses can override this method to execute custom logic on resize + } + + /* Setting the `focus-target` attribute to the first focusable descendant + starting from the firstChild necessary for the focus to be delegated + within the flow-component-renderer when used inside a vaadin-grid cell */ + _defineFocusTarget() { + var focusable = this._getFirstFocusableDescendant(this.firstChild); + if (focusable !== null) { + focusable.setAttribute('focus-target', 'true'); + } + } + + _getFirstFocusableDescendant(node) { + if (this._isFocusable(node)) { + return node; + } + if (node.hasAttribute && (node.hasAttribute('disabled') || node.hasAttribute('hidden'))) { + return null; + } + if (!node.children) { + return null; + } + for (var i = 0; i < node.children.length; i++) { + var focusable = this._getFirstFocusableDescendant(node.children[i]); + if (focusable !== null) { + return focusable; + } + } + return null; + } + + _isFocusable(node) { + if ( + node.hasAttribute && + typeof node.hasAttribute === 'function' && + (node.hasAttribute('disabled') || node.hasAttribute('hidden')) + ) { + return false; + } + + return node.tabIndex === 0; + } + + _onAnimationEnd(e) { + // ShadyCSS applies scoping suffixes to animation names + // To ensure that child is attached once element is unhidden + // for when it was filtered out from, eg, ComboBox + // https://github.com/vaadin/vaadin-flow-components/issues/437 + if (e.animationName.indexOf('flow-component-renderer-appear') === 0) { + this._attachRenderedComponentIfAble(); + } + } +} +window.customElements.define(FlowComponentRenderer.is, FlowComponentRenderer); diff --git a/java/demo/frontend/generated/jar-resources/gridConnector.js b/java/demo/frontend/generated/jar-resources/gridConnector.js new file mode 100644 index 000000000..50cdbfc78 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/gridConnector.js @@ -0,0 +1,1187 @@ +import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; +import { timeOut, animationFrame } from '@polymer/polymer/lib/utils/async.js'; +import { Grid } from '@vaadin/grid/src/vaadin-grid.js'; +import { ItemCache } from '@vaadin/grid/src/vaadin-grid-data-provider-mixin.js'; +import { isFocusable } from '@vaadin/grid/src/vaadin-grid-active-item-mixin.js'; + +(function () { + const tryCatchWrapper = function (callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Grid'); + }; + + let isItemCacheInitialized = false; + + window.Vaadin.Flow.gridConnector = { + initLazy: (grid) => + tryCatchWrapper(function (grid) { + // Check whether the connector was already initialized for the grid + if (grid.$connector) { + return; + } + + // Make sure ItemCache patching is done only once, but delay it for when + // a server grid is initialized + if (!isItemCacheInitialized) { + isItemCacheInitialized = true; + // Storing original implementation of the method to be used for client + // side only grids + ItemCache.prototype.ensureSubCacheForScaledIndexOriginal = ItemCache.prototype.ensureSubCacheForScaledIndex; + ItemCache.prototype.ensureSubCacheForScaledIndex = tryCatchWrapper(function (scaledIndex) { + if (!this.grid.$connector) { + this.ensureSubCacheForScaledIndexOriginal(scaledIndex); + return; + } + + if (!this.itemCaches[scaledIndex]) { + this.grid.$connector.beforeEnsureSubCacheForScaledIndex(this, scaledIndex); + } + }); + + ItemCache.prototype.isLoadingOriginal = ItemCache.prototype.isLoading; + ItemCache.prototype.isLoading = tryCatchWrapper(function () { + if (!this.grid.$connector) { + return this.isLoadingOriginal(); + } + + return Boolean( + this.grid.$connector.hasEnsureSubCacheQueue() || + Object.keys(this.pendingRequests).length || + Object.keys(this.itemCaches).filter((index) => { + return this.itemCaches[index].isLoading(); + })[0] + ); + }); + + ItemCache.prototype.doEnsureSubCacheForScaledIndex = tryCatchWrapper(function (scaledIndex) { + if (!this.itemCaches[scaledIndex]) { + const subCache = new ItemCache.prototype.constructor(this.grid, this, this.items[scaledIndex]); + subCache.itemkeyCaches = {}; + if (!this.itemkeyCaches) { + this.itemkeyCaches = {}; + } + this.itemCaches[scaledIndex] = subCache; + this.itemkeyCaches[this.grid.getItemId(subCache.parentItem)] = subCache; + this.grid._loadPage(0, subCache); + } + }); + + ItemCache.prototype.getCacheAndIndexByKey = tryCatchWrapper(function (key) { + for (let index in this.items) { + if (this.grid.getItemId(this.items[index]) === key) { + return { cache: this, scaledIndex: index }; + } + } + const keys = Object.keys(this.itemkeyCaches); + for (let i = 0; i < keys.length; i++) { + const expandedKey = keys[i]; + const subCache = this.itemkeyCaches[expandedKey]; + let cacheAndIndex = subCache.getCacheAndIndexByKey(key); + if (cacheAndIndex) { + return cacheAndIndex; + } + } + return undefined; + }); + + ItemCache.prototype.getLevel = tryCatchWrapper(function () { + let cache = this; + let level = 0; + while (cache.parentCache) { + cache = cache.parentCache; + level++; + } + return level; + }); + } + + const rootPageCallbacks = {}; + const treePageCallbacks = {}; + const cache = {}; + + /* parentRequestDelay - optimizes parent requests by batching several requests + * into one request. Delay in milliseconds. Disable by setting to 0. + * parentRequestBatchMaxSize - maximum size of the batch. + */ + const parentRequestDelay = 50; + const parentRequestBatchMaxSize = 20; + + let parentRequestQueue = []; + let parentRequestDebouncer; + let ensureSubCacheQueue = []; + let ensureSubCacheDebouncer; + + const rootRequestDelay = 150; + let rootRequestDebouncer; + + let lastRequestedRanges = {}; + const root = 'null'; + lastRequestedRanges[root] = [0, 0]; + + const validSelectionModes = ['SINGLE', 'NONE', 'MULTI']; + let selectedKeys = {}; + let selectionMode = 'SINGLE'; + + let sorterDirectionsSetFromServer = false; + + grid.size = 0; // To avoid NaN here and there before we get proper data + grid.itemIdPath = 'key'; + + grid.$connector = {}; + + grid.$connector.hasEnsureSubCacheQueue = tryCatchWrapper(() => ensureSubCacheQueue.length > 0); + + grid.$connector.hasParentRequestQueue = tryCatchWrapper(() => parentRequestQueue.length > 0); + + grid.$connector.hasRootRequestQueue = tryCatchWrapper(() => { + return Object.keys(rootPageCallbacks).length > 0 || (rootRequestDebouncer && rootRequestDebouncer.isActive()); + }); + + grid.$connector.beforeEnsureSubCacheForScaledIndex = tryCatchWrapper(function (targetCache, scaledIndex) { + // add call to queue + ensureSubCacheQueue.push({ + cache: targetCache, + scaledIndex: scaledIndex, + itemkey: grid.getItemId(targetCache.items[scaledIndex]), + level: targetCache.getLevel() + }); + + ensureSubCacheDebouncer = Debouncer.debounce(ensureSubCacheDebouncer, animationFrame, () => { + while (ensureSubCacheQueue.length) { + grid.$connector.flushEnsureSubCache(); + } + }); + }); + + grid.$connector.doSelection = tryCatchWrapper(function (items, userOriginated) { + if (selectionMode === 'NONE' || !items.length || (userOriginated && grid.hasAttribute('disabled'))) { + return; + } + if (selectionMode === 'SINGLE') { + grid.selectedItems = []; + selectedKeys = {}; + } + + // For single selection mode, "deselect all" selects a single item `null`, + // which should not end up in the selected items + const sanitizedItems = items.filter((item) => item !== null); + grid.selectedItems = grid.selectedItems.concat(sanitizedItems); + + items.forEach((item) => { + if (item) { + selectedKeys[item.key] = item; + if (userOriginated) { + item.selected = true; + grid.$server.select(item.key); + } + } + const isSelectedItemDifferentOrNull = !grid.activeItem || !item || item.key != grid.activeItem.key; + if (!userOriginated && selectionMode === 'SINGLE' && isSelectedItemDifferentOrNull) { + grid.activeItem = item; + } + }); + }); + + grid.$connector.doDeselection = tryCatchWrapper(function (items, userOriginated) { + if (selectionMode === 'NONE' || !items.length || (userOriginated && grid.hasAttribute('disabled'))) { + return; + } + + const updatedSelectedItems = grid.selectedItems.slice(); + while (items.length) { + const itemToDeselect = items.shift(); + for (let i = 0; i < updatedSelectedItems.length; i++) { + const selectedItem = updatedSelectedItems[i]; + if (itemToDeselect && itemToDeselect.key === selectedItem.key) { + updatedSelectedItems.splice(i, 1); + break; + } + } + if (itemToDeselect) { + delete selectedKeys[itemToDeselect.key]; + if (userOriginated) { + delete itemToDeselect.selected; + grid.$server.deselect(itemToDeselect.key); + } + } + } + grid.selectedItems = updatedSelectedItems; + }); + + grid.__activeItemChanged = tryCatchWrapper(function (newVal, oldVal) { + if (selectionMode != 'SINGLE') { + return; + } + if (!newVal) { + if (oldVal && selectedKeys[oldVal.key]) { + if (grid.__deselectDisallowed) { + grid.activeItem = oldVal; + } else { + grid.$connector.doDeselection([oldVal], true); + } + } + } else if (!selectedKeys[newVal.key]) { + grid.$connector.doSelection([newVal], true); + } + }); + grid._createPropertyObserver('activeItem', '__activeItemChanged', true); + + grid.__activeItemChangedDetails = tryCatchWrapper(function (newVal, oldVal) { + if (grid.__disallowDetailsOnClick) { + return; + } + // when grid is attached, newVal is not set and oldVal is undefined + // do nothing + if (newVal == null && oldVal === undefined) { + return; + } + if (newVal && !newVal.detailsOpened) { + grid.$server.setDetailsVisible(newVal.key); + } else { + grid.$server.setDetailsVisible(null); + } + }); + grid._createPropertyObserver('activeItem', '__activeItemChangedDetails', true); + + grid.$connector._getPageIfSameLevel = tryCatchWrapper(function (parentKey, index, defaultPage) { + let cacheAndIndex = grid._cache.getCacheAndIndex(index); + let parentItem = cacheAndIndex.cache.parentItem; + let parentKeyOfIndex = parentItem ? grid.getItemId(parentItem) : root; + if (parentKey !== parentKeyOfIndex) { + return defaultPage; + } else { + return grid._getPageForIndex(cacheAndIndex.scaledIndex); + } + }); + + grid.$connector.getCacheByKey = tryCatchWrapper(function (key) { + let cacheAndIndex = grid._cache.getCacheAndIndexByKey(key); + if (cacheAndIndex) { + return cacheAndIndex.cache; + } + return undefined; + }); + + grid.$connector.flushEnsureSubCache = tryCatchWrapper(function () { + let pendingFetch = ensureSubCacheQueue.splice(0, 1)[0]; + let itemkey = pendingFetch.itemkey; + + const visibleRows = grid._getVisibleRows(); + let start = visibleRows[0].index; + let end = visibleRows[visibleRows.length - 1].index; + + let buffer = end - start; + let firstNeededIndex = Math.max(0, start - buffer); + let lastNeededIndex = Math.min(end + buffer, grid._effectiveSize); + + // only fetch if given item is still in visible range + for (let index = firstNeededIndex; index <= lastNeededIndex; index++) { + let item = grid._cache.getItemForIndex(index); + + if (grid.getItemId(item) === itemkey) { + if (grid._isExpanded(item)) { + pendingFetch.cache.doEnsureSubCacheForScaledIndex(pendingFetch.scaledIndex); + return true; + } else { + break; + } + } + } + return false; + }); + + grid.$connector.flushParentRequests = tryCatchWrapper(function () { + let pendingFetches = parentRequestQueue.splice(0, parentRequestBatchMaxSize); + + if (pendingFetches.length) { + grid.$server.setParentRequestedRanges(pendingFetches); + return true; + } + return false; + }); + + grid.$connector.beforeParentRequest = tryCatchWrapper(function (firstIndex, size, parentKey) { + // add request in queue + parentRequestQueue.push({ + firstIndex: firstIndex, + size: size, + parentKey: parentKey + }); + + parentRequestDebouncer = Debouncer.debounce(parentRequestDebouncer, timeOut.after(parentRequestDelay), () => { + while (parentRequestQueue.length) { + grid.$connector.flushParentRequests(); + } + }); + }); + + grid.$connector.fetchPage = tryCatchWrapper(function (fetch, page, parentKey) { + // Determine what to fetch based on scroll position and not only + // what grid asked for + + // The buffer size could be multiplied by some constant defined by the user, + // if he needs to reduce the number of items sent to the Grid to improve performance + // or to increase it to make Grid smoother when scrolling + const visibleRows = grid._getVisibleRows(); + let start = visibleRows.length > 0 ? visibleRows[0].index : 0; + let end = visibleRows.length > 0 ? visibleRows[visibleRows.length - 1].index : 0; + let buffer = end - start; + + let firstNeededIndex = Math.max(0, start - buffer); + let lastNeededIndex = Math.min(end + buffer, grid._effectiveSize); + + let firstNeededPage = page; + let lastNeededPage = page; + for (let idx = firstNeededIndex; idx <= lastNeededIndex; idx++) { + firstNeededPage = Math.min( + firstNeededPage, + grid.$connector._getPageIfSameLevel(parentKey, idx, firstNeededPage) + ); + lastNeededPage = Math.max( + lastNeededPage, + grid.$connector._getPageIfSameLevel(parentKey, idx, lastNeededPage) + ); + } + + let firstPage = Math.max(0, firstNeededPage); + let lastPage = + parentKey !== root ? lastNeededPage : Math.min(lastNeededPage, Math.floor(grid.size / grid.pageSize)); + let lastRequestedRange = lastRequestedRanges[parentKey]; + if (!lastRequestedRange) { + lastRequestedRange = [-1, -1]; + } + if (lastRequestedRange[0] != firstPage || lastRequestedRange[1] != lastPage) { + lastRequestedRange = [firstPage, lastPage]; + lastRequestedRanges[parentKey] = lastRequestedRange; + let count = lastPage - firstPage + 1; + fetch(firstPage * grid.pageSize, count * grid.pageSize); + } + }); + + grid.dataProvider = tryCatchWrapper(function (params, callback) { + if (params.pageSize != grid.pageSize) { + throw 'Invalid pageSize'; + } + + let page = params.page; + + if (params.parentItem) { + let parentUniqueKey = grid.getItemId(params.parentItem); + if (!treePageCallbacks[parentUniqueKey]) { + treePageCallbacks[parentUniqueKey] = {}; + } + + let parentCache = grid.$connector.getCacheByKey(parentUniqueKey); + let itemCache = + parentCache && parentCache.itemkeyCaches ? parentCache.itemkeyCaches[parentUniqueKey] : undefined; + if (cache[parentUniqueKey] && cache[parentUniqueKey][page] && itemCache) { + // workaround: sometimes grid-element gives page index that overflows + page = Math.min(page, Math.floor(cache[parentUniqueKey].size / grid.pageSize)); + + // Ensure grid isn't in loading state when the callback executes + ensureSubCacheQueue = []; + callback(cache[parentUniqueKey][page], cache[parentUniqueKey].size); + + // Flush after the callback to have the grid rows up-to-date + updateAllGridRowsInDomBasedOnCache(); + // Prevent sub-caches from being created (& data requests sent) for items + // that may no longer be visible + ensureSubCacheQueue = []; + // Eliminate flickering on eager fetch mode + grid.requestContentUpdate(); + } else { + treePageCallbacks[parentUniqueKey][page] = callback; + + grid.$connector.fetchPage( + (firstIndex, size) => grid.$connector.beforeParentRequest(firstIndex, size, params.parentItem.key), + page, + parentUniqueKey + ); + } + } else { + // workaround: sometimes grid-element gives page index that overflows + page = Math.min(page, Math.floor(grid.size / grid.pageSize)); + + if (cache[root] && cache[root][page]) { + callback(cache[root][page]); + } else { + rootPageCallbacks[page] = callback; + + rootRequestDebouncer = Debouncer.debounce( + rootRequestDebouncer, + timeOut.after(grid._hasData ? rootRequestDelay : 0), + () => { + grid.$connector.fetchPage( + (firstIndex, size) => grid.$server.setRequestedRange(firstIndex, size), + page, + root + ); + } + ); + } + } + }); + + const sorterChangeListener = tryCatchWrapper(function (_, oldValue) { + if (oldValue !== undefined && !sorterDirectionsSetFromServer) { + grid.$server.sortersChanged( + grid._sorters.map(function (sorter) { + return { + path: sorter.path, + direction: sorter.direction + }; + }) + ); + } + }); + + grid.$connector.setSorterDirections = tryCatchWrapper(function (directions) { + sorterDirectionsSetFromServer = true; + setTimeout( + tryCatchWrapper(() => { + try { + const sorters = Array.from(grid.querySelectorAll('vaadin-grid-sorter')); + + // Sorters for hidden columns are removed from DOM but stored in the web component. + // We need to ensure that all the sorters are reset when using `grid.sort(null)`. + grid._sorters.forEach((sorter) => { + if (!sorters.includes(sorter)) { + sorters.push(sorter); + } + }); + + sorters.forEach((sorter) => { + if (!directions.filter((d) => d.column === sorter.getAttribute('path'))[0]) { + sorter.direction = null; + } + }); + + // Apply directions in correct order, depending on configured multi-sort priority. + // For the default "prepend" mode, directions need to be applied in reverse, in + // order for the sort indicators to match the order on the server. For "append" + // just keep the order passed from the server. + if (grid.multiSortPriority !== 'append') { + directions = directions.reverse() + } + directions.forEach(({ column, direction }) => { + sorters.forEach((sorter) => { + if (sorter.getAttribute('path') === column && sorter.direction !== direction) { + sorter.direction = direction; + } + }); + }); + } finally { + sorterDirectionsSetFromServer = false; + } + }) + ); + }); + grid._createPropertyObserver('_previousSorters', sorterChangeListener); + + grid._updateItem = tryCatchWrapper(function (row, item) { + Grid.prototype._updateItem.call(grid, row, item); + + // There might be inactive component renderers on hidden rows that still refer to the + // same component instance as one of the renderers on a visible row. Making the + // inactive/hidden renderer attach the component might steal it from a visible/active one. + if (!row.hidden) { + // make sure that component renderers are updated + Array.from(row.children).forEach((cell) => { + if (cell._content && cell._content.__templateInstance && cell._content.__templateInstance.children) { + Array.from(cell._content.__templateInstance.children).forEach((content) => { + if (content._attachRenderedComponentIfAble) { + content._attachRenderedComponentIfAble(); + } + // In hierarchy column of tree grid, the component renderer is inside its content, + // this updates it renderer from innerContent + if (content.children) { + Array.from(content.children).forEach((innerContent) => { + if (innerContent._attachRenderedComponentIfAble) { + innerContent._attachRenderedComponentIfAble(); + } + }); + } + }); + } + }); + } + // since no row can be selected when selection mode is NONE + // if selectionMode is set to NONE, remove aria-selected attribute from the row + if (selectionMode === validSelectionModes[1]) { + // selectionMode === NONE + row.removeAttribute('aria-selected'); + Array.from(row.children).forEach((cell) => cell.removeAttribute('aria-selected')); + } + }); + + const itemExpandedChanged = tryCatchWrapper(function (item, expanded) { + // method available only for the TreeGrid server-side component + if (item == undefined || grid.$server.updateExpandedState == undefined) { + return; + } + let parentKey = grid.getItemId(item); + grid.$server.updateExpandedState(parentKey, expanded); + + if (!expanded) { + delete cache[parentKey]; + let parentCache = grid.$connector.getCacheByKey(parentKey); + if (parentCache && parentCache.itemkeyCaches && parentCache.itemkeyCaches[parentKey]) { + delete parentCache.itemkeyCaches[parentKey]; + } + if (parentCache && parentCache.itemkeyCaches) { + Object.keys(parentCache.itemCaches) + .filter((idx) => parentCache.items[idx].key === parentKey) + .forEach((idx) => delete parentCache.itemCaches[idx]); + } + delete lastRequestedRanges[parentKey]; + } + }); + + // Patch grid.expandItem and grid.collapseItem to have + // itemExpandedChanged run when either happens. + grid.expandItem = tryCatchWrapper(function (item) { + itemExpandedChanged(item, true); + Grid.prototype.expandItem.call(grid, item); + }); + + grid.collapseItem = tryCatchWrapper(function (item) { + itemExpandedChanged(item, false); + Grid.prototype.collapseItem.call(grid, item); + }); + + const itemsUpdated = function (items) { + if (!items || !Array.isArray(items)) { + throw 'Attempted to call itemsUpdated with an invalid value: ' + JSON.stringify(items); + } + let detailsOpenedItems = Array.from(grid.detailsOpenedItems); + let updatedSelectedItem = false; + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + if (!item) { + continue; + } + if (item.detailsOpened) { + if (grid._getItemIndexInArray(item, detailsOpenedItems) < 0) { + detailsOpenedItems.push(item); + } + } else if (grid._getItemIndexInArray(item, detailsOpenedItems) >= 0) { + detailsOpenedItems.splice(grid._getItemIndexInArray(item, detailsOpenedItems), 1); + } + if (selectedKeys[item.key]) { + selectedKeys[item.key] = item; + item.selected = true; + updatedSelectedItem = true; + } + } + grid.detailsOpenedItems = detailsOpenedItems; + if (updatedSelectedItem) { + // IE 11 Object doesn't support method values + grid.selectedItems = Object.keys(selectedKeys).map(function (e) { + return selectedKeys[e]; + }); + } + }; + + /** + * Updates the cache for the given page for grid or tree-grid. + * + * @param page index of the page to update + * @param parentKey the key of the parent item for the page + * @returns an array of the updated items for the page, or undefined if no items were cached for the page + */ + const updateGridCache = function (page, parentKey) { + let items; + if ((parentKey || root) !== root) { + items = cache[parentKey][page]; + let parentCache = grid.$connector.getCacheByKey(parentKey); + if (parentCache && parentCache.itemkeyCaches) { + let _cache = parentCache.itemkeyCaches[parentKey]; + const callbacksForParentKey = treePageCallbacks[parentKey]; + const callback = callbacksForParentKey && callbacksForParentKey[page]; + _updateGridCache(page, items, callback, _cache); + } + } else { + items = cache[root][page]; + _updateGridCache(page, items, rootPageCallbacks[page], grid._cache); + } + return items; + }; + + const _updateGridCache = function (page, items, callback, levelcache) { + // Force update unless there's a callback waiting + if (!callback) { + let rangeStart = page * grid.pageSize; + let rangeEnd = rangeStart + grid.pageSize; + if (!items) { + if (levelcache && levelcache.items) { + for (let idx = rangeStart; idx < rangeEnd; idx++) { + delete levelcache.items[idx]; + } + } + } else { + if (levelcache && levelcache.items) { + for (let idx = rangeStart; idx < rangeEnd; idx++) { + if (levelcache.items[idx]) { + levelcache.items[idx] = items[idx - rangeStart]; + } + } + } + } + } + }; + + /** + * Updates all visible grid rows in DOM. + */ + const updateAllGridRowsInDomBasedOnCache = function () { + grid._cache.updateSize(); + grid._effectiveSize = grid._cache.effectiveSize; + grid.__updateVisibleRows(); + }; + + /** + * Update the given items in DOM if currently visible. + * + * @param array items the items to update in DOM + */ + const updateGridItemsInDomBasedOnCache = function (items) { + if (!items || !grid.$ || grid.$.items.childElementCount === 0) { + return; + } + + const itemKeys = items.map((item) => item.key); + const indexes = grid + ._getVisibleRows() + .filter((row) => row._item && itemKeys.includes(row._item.key)) + .map((row) => row.index); + if (indexes.length > 0) { + grid.__updateVisibleRows(indexes[0], indexes[indexes.length - 1]); + } + }; + + grid.$connector.set = tryCatchWrapper(function (index, items, parentKey) { + if (index % grid.pageSize != 0) { + throw 'Got new data to index ' + index + ' which is not aligned with the page size of ' + grid.pageSize; + } + let pkey = parentKey || root; + + const firstPage = index / grid.pageSize; + const updatedPageCount = Math.ceil(items.length / grid.pageSize); + + for (let i = 0; i < updatedPageCount; i++) { + let page = firstPage + i; + let slice = items.slice(i * grid.pageSize, (i + 1) * grid.pageSize); + if (!cache[pkey]) { + cache[pkey] = {}; + } + cache[pkey][page] = slice; + + grid.$connector.doSelection(slice.filter((item) => item.selected)); + grid.$connector.doDeselection( + slice.filter((item) => !item.selected && selectedKeys[item.key]) + ); + + const updatedItems = updateGridCache(page, pkey); + if (updatedItems) { + itemsUpdated(updatedItems); + updateGridItemsInDomBasedOnCache(updatedItems); + } + } + }); + + const itemToCacheLocation = function (item) { + let parent = item.parentUniqueKey || root; + if (cache[parent]) { + for (let page in cache[parent]) { + for (let index in cache[parent][page]) { + if (grid.getItemId(cache[parent][page][index]) === grid.getItemId(item)) { + return { page: page, index: index, parentKey: parent }; + } + } + } + } + return null; + }; + + /** + * Updates the given items for a hierarchical grid. + * + * @param updatedItems the updated items array + */ + grid.$connector.updateHierarchicalData = tryCatchWrapper(function (updatedItems) { + let pagesToUpdate = []; + // locate and update the items in cache + // find pages that need updating + for (let i = 0; i < updatedItems.length; i++) { + let cacheLocation = itemToCacheLocation(updatedItems[i]); + if (cacheLocation) { + cache[cacheLocation.parentKey][cacheLocation.page][cacheLocation.index] = updatedItems[i]; + let key = cacheLocation.parentKey + ':' + cacheLocation.page; + if (!pagesToUpdate[key]) { + pagesToUpdate[key] = { + parentKey: cacheLocation.parentKey, + page: cacheLocation.page + }; + } + } + } + // IE11 doesn't work with the transpiled version of the forEach. + let keys = Object.keys(pagesToUpdate); + for (let i = 0; i < keys.length; i++) { + let pageToUpdate = pagesToUpdate[keys[i]]; + const affectedUpdatedItems = updateGridCache(pageToUpdate.page, pageToUpdate.parentKey); + if (affectedUpdatedItems) { + itemsUpdated(affectedUpdatedItems); + updateGridItemsInDomBasedOnCache(affectedUpdatedItems); + } + } + }); + + /** + * Updates the given items for a non-hierarchical grid. + * + * @param updatedItems the updated items array + */ + grid.$connector.updateFlatData = tryCatchWrapper(function (updatedItems) { + // update (flat) caches + for (let i = 0; i < updatedItems.length; i++) { + let cacheLocation = itemToCacheLocation(updatedItems[i]); + if (cacheLocation) { + // update connector cache + cache[cacheLocation.parentKey][cacheLocation.page][cacheLocation.index] = updatedItems[i]; + + // update grid's cache + const index = parseInt(cacheLocation.page) * grid.pageSize + parseInt(cacheLocation.index); + if (grid._cache.items[index]) { + grid._cache.items[index] = updatedItems[i]; + } + } + } + itemsUpdated(updatedItems); + + updateGridItemsInDomBasedOnCache(updatedItems); + }); + + grid.$connector.clearExpanded = tryCatchWrapper(function () { + grid.expandedItems = []; + ensureSubCacheQueue = []; + parentRequestQueue = []; + }); + + grid.$connector.clear = tryCatchWrapper(function (index, length, parentKey) { + let pkey = parentKey || root; + if (!cache[pkey] || Object.keys(cache[pkey]).length === 0) { + return; + } + if (index % grid.pageSize != 0) { + throw ( + 'Got cleared data for index ' + index + ' which is not aligned with the page size of ' + grid.pageSize + ); + } + + let firstPage = Math.floor(index / grid.pageSize); + let updatedPageCount = Math.ceil(length / grid.pageSize); + + for (let i = 0; i < updatedPageCount; i++) { + let page = firstPage + i; + let items = cache[pkey][page]; + grid.$connector.doDeselection(items.filter((item) => selectedKeys[item.key])); + items.forEach((item) => grid.closeItemDetails(item)); + delete cache[pkey][page]; + const updatedItems = updateGridCache(page, parentKey); + if (updatedItems) { + itemsUpdated(updatedItems); + } + updateGridItemsInDomBasedOnCache(items); + } + let cacheToClear = grid._cache; + if (parentKey) { + const cacheAndIndex = grid._cache.getCacheAndIndexByKey(pkey); + cacheToClear = cacheAndIndex.cache.itemCaches[cacheAndIndex.scaledIndex]; + } + const endIndex = index + updatedPageCount * grid.pageSize; + for (let itemIndex = index; itemIndex < endIndex; itemIndex++) { + delete cacheToClear.items[itemIndex]; + const subcacheToClear = cacheToClear.itemCaches[itemIndex]; + delete cacheToClear.itemCaches[itemIndex]; + const itemKeyToRemove = subcacheToClear && subcacheToClear.parentItem.key; + if (itemKeyToRemove) { + delete cacheToClear.itemkeyCaches[itemKeyToRemove]; + } + } + grid._cache.updateSize(); + grid._effectiveSize = grid._cache.effectiveSize; + }); + + grid.$connector.reset = tryCatchWrapper(function () { + grid.size = 0; + deleteObjectContents(cache); + deleteObjectContents(grid._cache.items); + deleteObjectContents(lastRequestedRanges); + if (ensureSubCacheDebouncer) { + ensureSubCacheDebouncer.cancel(); + } + if (parentRequestDebouncer) { + parentRequestDebouncer.cancel(); + } + if (rootRequestDebouncer) { + rootRequestDebouncer.cancel(); + } + ensureSubCacheDebouncer = undefined; + parentRequestDebouncer = undefined; + ensureSubCacheQueue = []; + parentRequestQueue = []; + updateAllGridRowsInDomBasedOnCache(); + }); + + const deleteObjectContents = (obj) => Object.keys(obj).forEach((key) => delete obj[key]); + + grid.$connector.updateSize = (newSize) => (grid.size = newSize); + + grid.$connector.updateUniqueItemIdPath = (path) => (grid.itemIdPath = path); + + grid.$connector.expandItems = tryCatchWrapper(function (items) { + let newExpandedItems = Array.from(grid.expandedItems); + items.filter((item) => !grid._isExpanded(item)).forEach((item) => newExpandedItems.push(item)); + grid.expandedItems = newExpandedItems; + }); + + grid.$connector.collapseItems = tryCatchWrapper(function (items) { + let newExpandedItems = Array.from(grid.expandedItems); + items.forEach((item) => { + let index = grid._getItemIndexInArray(item, newExpandedItems); + if (index >= 0) { + newExpandedItems.splice(index, 1); + } + }); + grid.expandedItems = newExpandedItems; + items.forEach((item) => grid.$connector.removeFromQueue(item)); + }); + + grid.$connector.removeFromQueue = tryCatchWrapper(function (item) { + let itemId = grid.getItemId(item); + delete treePageCallbacks[itemId]; + grid.$connector.removeFromArray(ensureSubCacheQueue, (item) => item.itemkey === itemId); + grid.$connector.removeFromArray(parentRequestQueue, (item) => item.parentKey === itemId); + }); + + grid.$connector.removeFromArray = tryCatchWrapper(function (array, removeTest) { + if (array.length) { + for (let index = array.length - 1; index--; ) { + if (removeTest(array[index])) { + array.splice(index, 1); + } + } + } + }); + + grid.$connector.confirmParent = tryCatchWrapper(function (id, parentKey, levelSize) { + if (!cache[parentKey]) { + cache[parentKey] = {}; + } + cache[parentKey].size = levelSize; + if (levelSize === 0) { + cache[parentKey][0] = []; + } + + let outstandingRequests = Object.getOwnPropertyNames(treePageCallbacks[parentKey] || {}); + for (let i = 0; i < outstandingRequests.length; i++) { + let page = outstandingRequests[i]; + + let lastRequestedRange = lastRequestedRanges[parentKey] || [0, 0]; + + const callback = treePageCallbacks[parentKey][page]; + if ( + (cache[parentKey] && cache[parentKey][page]) || + page < lastRequestedRange[0] || + page > lastRequestedRange[1] + ) { + delete treePageCallbacks[parentKey][page]; + let items = cache[parentKey][page] || new Array(levelSize); + callback(items, levelSize); + } else if (callback && levelSize === 0) { + // The parent item has 0 child items => resolve the callback with an empty array + delete treePageCallbacks[parentKey][page]; + callback([], levelSize); + } + } + // Let server know we're done + grid.$server.confirmParentUpdate(id, parentKey); + + if (!grid.loading) { + grid.__updateVisibleRows(); + } + }); + + grid.$connector.confirm = tryCatchWrapper(function (id) { + // We're done applying changes from this batch, resolve outstanding + // callbacks + let outstandingRequests = Object.getOwnPropertyNames(rootPageCallbacks); + for (let i = 0; i < outstandingRequests.length; i++) { + let page = outstandingRequests[i]; + let lastRequestedRange = lastRequestedRanges[root] || [0, 0]; + + const lastAvailablePage = grid.size ? Math.ceil(grid.size / grid.pageSize) - 1 : 0; + // It's possible that the lastRequestedRange includes a page that's beyond lastAvailablePage if the grid's size got reduced during an ongoing data request + const lastRequestedRangeEnd = Math.min(lastRequestedRange[1], lastAvailablePage); + // Resolve if we have data or if we don't expect to get data + const callback = rootPageCallbacks[page]; + if ((cache[root] && cache[root][page]) || page < lastRequestedRange[0] || +page > lastRequestedRangeEnd) { + delete rootPageCallbacks[page]; + + if (cache[root][page]) { + // Cached data is available, resolve the callback + callback(cache[root][page]); + } else { + // No cached data, resolve the callback with an empty array + callback(new Array(grid.pageSize)); + // Request grid for content update + grid.requestContentUpdate(); + } + + // Makes sure to push all new rows before this stack execution is done so any timeout expiration called after will be applied on a fully updated grid + //Resolves https://github.com/vaadin/vaadin-grid-flow/issues/511 + if (grid._debounceIncreasePool) { + grid._debounceIncreasePool.flush(); + } + } else if (callback && grid.size === 0) { + // The grid has 0 items => resolve the callback with an empty array + delete rootPageCallbacks[page]; + callback([]); + } + } + + // Let server know we're done + grid.$server.confirmUpdate(id); + }); + + grid.$connector.ensureHierarchy = tryCatchWrapper(function () { + for (let parentKey in cache) { + if (parentKey !== root) { + delete cache[parentKey]; + } + } + deleteObjectContents(lastRequestedRanges); + + grid._cache.itemCaches = {}; + grid._cache.itemkeyCaches = {}; + + updateAllGridRowsInDomBasedOnCache(); + }); + + grid.$connector.setSelectionMode = tryCatchWrapper(function (mode) { + if ((typeof mode === 'string' || mode instanceof String) && validSelectionModes.indexOf(mode) >= 0) { + selectionMode = mode; + selectedKeys = {}; + grid.$connector.updateMultiSelectable(); + } else { + throw 'Attempted to set an invalid selection mode'; + } + }); + + /* + * Manage aria-multiselectable attribute depending on the selection mode. + * see more: https://github.com/vaadin/web-components/issues/1536 + * or: https://www.w3.org/TR/wai-aria-1.1/#aria-multiselectable + * For selection mode SINGLE, set the aria-multiselectable attribute to false + */ + grid.$connector.updateMultiSelectable = tryCatchWrapper(function () { + if (!grid.$) { + return; + } + + if (selectionMode === validSelectionModes[0]) { + grid.$.table.setAttribute('aria-multiselectable', false); + // For selection mode NONE, remove the aria-multiselectable attribute + } else if (selectionMode === validSelectionModes[1]) { + grid.$.table.removeAttribute('aria-multiselectable'); + // For selection mode MULTI, set aria-multiselectable to true + } else { + grid.$.table.setAttribute('aria-multiselectable', true); + } + }); + + // Have the multi-selectable state updated on attach + grid._createPropertyObserver('isAttached', () => grid.$connector.updateMultiSelectable()); + + // TODO: should be removed once https://github.com/vaadin/vaadin-grid/issues/1471 gets implemented + grid.$connector.setVerticalScrollingEnabled = tryCatchWrapper(function (enabled) { + // There are two scollable containers in grid so apply the changes for both + setVerticalScrollingEnabled(grid.$.table, enabled); + }); + + const setVerticalScrollingEnabled = function (scrollable, enabled) { + // Prevent Y axis scrolling with CSS. This will hide the vertical scrollbar. + scrollable.style.overflowY = enabled ? '' : 'hidden'; + // Clean up an existing listener + scrollable.removeEventListener('wheel', scrollable.__wheelListener); + // Add a wheel event listener with the horizontal scrolling prevention logic + !enabled && + scrollable.addEventListener( + 'wheel', + (scrollable.__wheelListener = tryCatchWrapper((e) => { + if (e.deltaX) { + // If there was some horizontal delta related to the wheel event, force the vertical + // delta to 0 and let grid process the wheel event normally + Object.defineProperty(e, 'deltaY', { value: 0 }); + } else { + // If there was verical delta only, skip the grid's wheel event processing to + // enable scrolling the page even if grid isn't scrolled to end + e.stopImmediatePropagation(); + } + })) + ); + }; + + grid.addEventListener( + 'vaadin-context-menu-before-open', + tryCatchWrapper(function (e) { + const { key, columnId } = e.detail; + grid.$server.updateContextMenuTargetItem(key, columnId); + }) + ); + + grid.getContextMenuBeforeOpenDetail = tryCatchWrapper(function (event) { + // For `contextmenu` events, we need to access the source event, + // when using open on click we just use the click event itself + const sourceEvent = event.detail.sourceEvent || event; + const eventContext = grid.getEventContext(sourceEvent); + const key = (eventContext.item && eventContext.item.key) || ''; + const columnId = (eventContext.column && eventContext.column.id) || ''; + return { key, columnId }; + }); + + grid.addEventListener( + 'click', + tryCatchWrapper((e) => _fireClickEvent(e, 'item-click')) + ); + grid.addEventListener( + 'dblclick', + tryCatchWrapper((e) => _fireClickEvent(e, 'item-double-click')) + ); + + grid.addEventListener( + 'column-resize', + tryCatchWrapper((e) => { + const cols = grid._getColumnsInOrder().filter((col) => !col.hidden); + + cols.forEach((col) => { + col.dispatchEvent(new CustomEvent('column-drag-resize')); + }); + + grid.dispatchEvent( + new CustomEvent('column-drag-resize', { + detail: { + resizedColumnKey: e.detail.resizedColumn._flowId + } + }) + ); + }) + ); + + grid.addEventListener( + 'column-reorder', + tryCatchWrapper((e) => { + const columns = grid._columnTree + .slice(0) + .pop() + .filter((c) => c._flowId) + .sort((b, a) => b._order - a._order) + .map((c) => c._flowId); + + grid.dispatchEvent( + new CustomEvent('column-reorder-all-columns', { + detail: { columns } + }) + ); + }) + ); + + grid.addEventListener( + 'cell-focus', + tryCatchWrapper((e) => { + const eventContext = grid.getEventContext(e); + const expectedSectionValues = ['header', 'body', 'footer']; + + if (expectedSectionValues.indexOf(eventContext.section) === -1) { + return; + } + + grid.dispatchEvent( + new CustomEvent('grid-cell-focus', { + detail: { + itemKey: eventContext.item ? eventContext.item.key : null, + + internalColumnId: eventContext.column ? eventContext.column._flowId : null, + + section: eventContext.section + } + }) + ); + }) + ); + + function _fireClickEvent(event, eventName) { + // Click event was handled by the component inside grid, do nothing. + if (event.defaultPrevented) { + return; + } + + const target = event.target; + const eventContext = grid.getEventContext(event); + const section = eventContext.section; + + if (isFocusable(target) || target instanceof HTMLLabelElement) { + return; + } + + if (eventContext.item && section !== 'details') { + event.itemKey = eventContext.item.key; + // if you have a details-renderer, getEventContext().column is undefined + if (eventContext.column) { + event.internalColumnId = eventContext.column._flowId; + } + grid.dispatchEvent(new CustomEvent(eventName, { detail: event })); + } + } + + grid.cellClassNameGenerator = tryCatchWrapper(function (column, rowData) { + const style = rowData.item.style; + if (!style) { + return; + } + return (style.row || '') + ' ' + ((column && style[column._flowId]) || ''); + }); + + grid.dropFilter = tryCatchWrapper((rowData) => !rowData.item.dropDisabled); + + grid.dragFilter = tryCatchWrapper((rowData) => !rowData.item.dragDisabled); + + grid.addEventListener( + 'grid-dragstart', + tryCatchWrapper((e) => { + if (grid._isSelected(e.detail.draggedItems[0])) { + // Dragging selected (possibly multiple) items + if (grid.__selectionDragData) { + Object.keys(grid.__selectionDragData).forEach((type) => { + e.detail.setDragData(type, grid.__selectionDragData[type]); + }); + } else { + (grid.__dragDataTypes || []).forEach((type) => { + e.detail.setDragData(type, e.detail.draggedItems.map((item) => item.dragData[type]).join('\n')); + }); + } + + if (grid.__selectionDraggedItemsCount > 1) { + e.detail.setDraggedItemsCount(grid.__selectionDraggedItemsCount); + } + } else { + // Dragging just one (non-selected) item + (grid.__dragDataTypes || []).forEach((type) => { + e.detail.setDragData(type, e.detail.draggedItems[0].dragData[type]); + }); + } + }) + ); + })(grid) + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/index.d.ts b/java/demo/frontend/generated/jar-resources/index.d.ts new file mode 100644 index 000000000..d4e6e0db6 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/index.d.ts @@ -0,0 +1 @@ +export * from './Flow'; diff --git a/java/demo/frontend/generated/jar-resources/index.js b/java/demo/frontend/generated/jar-resources/index.js new file mode 100644 index 000000000..6c1482785 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/index.js @@ -0,0 +1,2 @@ +export * from './Flow'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/java/demo/frontend/generated/jar-resources/index.js.map b/java/demo/frontend/generated/jar-resources/index.js.map new file mode 100644 index 000000000..c662ab4bb --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/main/frontend/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC","sourcesContent":["export * from './Flow';\n"]} \ No newline at end of file diff --git a/java/demo/frontend/generated/jar-resources/ironListConnector.js b/java/demo/frontend/generated/jar-resources/ironListConnector.js new file mode 100644 index 000000000..82d297f3d --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/ironListConnector.js @@ -0,0 +1,166 @@ +import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; +import { timeOut } from '@polymer/polymer/lib/utils/async.js'; + +window.Vaadin.Flow.ironListConnector = { + initLazy: function (list) { + // Check whether the connector was already initialized for the Iron list + if (list.$connector) { + return; + } + + const extraItemsBuffer = 20; + + let lastRequestedRange = [0, 0]; + + list.$connector = {}; + list.$connector.placeholderItem = { __placeholder: true }; + + const updateRequestedItem = function () { + /* + * TODO Iron list seems to do a small index adjustment after scrolling + * has stopped. This causes a redundant request to be sent to make a + * corresponding minimal change to the buffer. We should avoid these + * requests by making the logic skip doing a request if the available + * buffer is within some tolerance compared to the requested buffer. + */ + let firstNeededItem = list._virtualStart; + let lastNeededItem = list._virtualEnd; + + let first = Math.max(0, firstNeededItem - extraItemsBuffer); + let last = Math.min(lastNeededItem + extraItemsBuffer, list.items.length); + + if (lastRequestedRange[0] != first || lastRequestedRange[1] != last) { + lastRequestedRange = [first, last]; + const count = 1 + last - first; + list.$server.setRequestedRange(first, count); + } + }; + + let requestDebounce; + const scheduleUpdateRequest = function () { + requestDebounce = Debouncer.debounce(requestDebounce, timeOut.after(10), updateRequestedItem); + }; + + /* + * Ensure all items that iron list will be looking at are actually defined. + * If this is not done, the component will keep looking ahead through the + * array until finding enough present items to render. In our case, that's + * a really slow way of achieving nothing since the rest of the array is + * empty. + */ + const originalAssign = list._assignModels; + list._assignModels = function () { + const tempItems = []; + const start = list._virtualStart; + const count = Math.min(list.items.length, list._physicalCount); + for (let i = 0; i < count; i++) { + if (list.items[start + i] === undefined) { + tempItems.push(i); + list.items[start + i] = list.$connector.placeholderItem; + } + } + + originalAssign.apply(list, arguments); + + /* + * TODO: Keep track of placeholder items in the "active" range and + * avoid deleting them so that the next pass will be faster. Instead, + * the end of each pass should only delete placeholders that are no + * longer needed. + */ + for (let i = 0; i < tempItems.length; i++) { + delete list.items[start + tempItems[i]]; + } + + /* + * Check if we need to do anything once things have settled down. + * This method is called multiple times in sequence for the same user + * action, but we only want to do the check once. + */ + scheduleUpdateRequest(); + }; + + list.items = []; + + list.$connector.set = function (index, items) { + for (let i = 0; i < items.length; i++) { + const itemsIndex = index + i; + list.items[itemsIndex] = items[i]; + } + // Do a full render since dirty detection for splices is broken + list._render(); + }; + + list.$connector.updateData = function (items) { + // Find the items by key inside the list update them + const oldItems = list.items; + const mapByKey = {}; + let leftToUpdate = items.length; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + mapByKey[item.key] = item; + } + + for (let i = 0; i < oldItems.length; i++) { + const oldItem = oldItems[i]; + const newItem = mapByKey[oldItem.key]; + if (newItem) { + list.items[i] = newItem; + list.notifyPath('items.' + i); + leftToUpdate--; + if (leftToUpdate == 0) { + break; + } + } + } + }; + + list.$connector.clear = function (index, length) { + for (let i = 0; i < length; i++) { + const itemsIndex = index + i; + delete list.items[itemsIndex]; + + // Most likely a no-op since the affected index isn't in view + list.notifyPath('items.' + itemsIndex); + } + }; + + list.$connector.updateSize = function (newSize) { + const delta = newSize - list.items.length; + if (delta > 0) { + list.items.length = newSize; + + list.notifySplices('items', [ + { + index: newSize - delta, + removed: [], + addedCount: delta, + object: list.items, + type: 'splice' + } + ]); + } else if (delta < 0) { + const removed = list.items.slice(newSize, list.items.length); + list.items.splice(newSize); + list.notifySplices('items', [ + { + index: newSize, + removed: removed, + addedCount: 0, + object: list.items, + type: 'splice' + } + ]); + } + }; + + list.$connector.setPlaceholderItem = function (placeholderItem) { + if (!placeholderItem) { + placeholderItem = {}; + } + placeholderItem.__placeholder = true; + list.$connector.placeholderItem = placeholderItem; + }; + } +}; diff --git a/java/demo/frontend/generated/jar-resources/ironListStyles.js b/java/demo/frontend/generated/jar-resources/ironListStyles.js new file mode 100644 index 000000000..96b50e2d3 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/ironListStyles.js @@ -0,0 +1,12 @@ +import '@polymer/polymer/lib/elements/custom-style.js'; +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = ``; + +document.head.appendChild($_documentContainer.content); diff --git a/java/demo/frontend/generated/jar-resources/lit-renderer.ts b/java/demo/frontend/generated/jar-resources/lit-renderer.ts new file mode 100644 index 000000000..081a40a9c --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/lit-renderer.ts @@ -0,0 +1,108 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable max-params */ +import { html, render } from 'lit'; + +type RenderRoot = HTMLElement & { __litRenderer?: Renderer; _$litPart$?: any }; + +type ItemModel = { item: any; index: number }; + +type Renderer = ((root: RenderRoot, rendererOwner: HTMLElement, model: ItemModel) => void) & { __rendererId?: string }; + +type Component = HTMLElement & { [key: string]: Renderer | undefined }; + +const _window = window as any; +_window.Vaadin = _window.Vaadin || {}; + +/** + * Assigns the component a renderer function which uses Lit to render + * the given template expression inside the render root element. + * + * @param component The host component to which the renderer runction is to be set + * @param rendererName The name of the renderer function + * @param templateExpression The content of the template literal passed to Lit for rendering. + * @param returnChannel A channel to the server. + * Calling it will end up invoking a handler in the server-side LitRenderer. + * @param clientCallables A list of function names that can be called from within the template literal. + * @param propertyNamespace LitRenderer-specific namespace for properties. + * Needed to avoid property name collisions between renderers. + */ +_window.Vaadin.setLitRenderer = ( + component: Component, + rendererName: string, + templateExpression: string, + returnChannel: (name: string, itemKey: string, args: any[]) => void, + clientCallables: string[], + propertyNamespace: string, +) => { + // Dynamically created function that renders the templateExpression + // inside the given root element using Lit + const renderFunction = Function(` + "use strict"; + + const [render, html, returnChannel] = arguments; + + return (root, {item, index}, itemKey) => { + ${clientCallables + .map((clientCallable) => { + // Map all the client-callables as inline functions so they can be accessed from the template literal + return ` + const ${clientCallable} = (...args) => { + if (itemKey !== undefined) { + returnChannel('${clientCallable}', itemKey, args[0] instanceof Event ? [] : [...args]); + } + }`; + }) + .join('')} + + render(html\`${templateExpression}\`, root) + } + `)(render, html, returnChannel); + + const renderer: Renderer = (root, _, { index, item }) => { + // Clean up the root element of any existing content + // (and Lit's _$litPart$ property) from other renderers + // TODO: Remove once https://github.com/vaadin/web-components/issues/2235 is done + if (root.__litRenderer !== renderer) { + root.innerHTML = ''; + delete root._$litPart$; + root.__litRenderer = renderer; + } + + // Map a new item that only includes the properties defined by + // this specific LitRenderer instance. The renderer instance specific + // "propertyNamespace" prefix is stripped from the property name at this point: + // + // item: { key: "2", lr_3_lastName: "Tyler"} + // -> + // mappedItem: { lastName: "Tyler" } + const mappedItem: { [key: string]: any } = {}; + for (const key in item) { + if (key.startsWith(propertyNamespace)) { + mappedItem[key.replace(propertyNamespace, '')] = item[key]; + } + } + + renderFunction(root, { index, item: mappedItem }, item.key); + }; + + renderer.__rendererId = propertyNamespace; + component[rendererName] = renderer; +}; + +/** + * Removes the renderer function with the given name from the component + * if the propertyNamespace matches the renderer's id. + * + * @param component The host component whose renderer function is to be removed + * @param rendererName The name of the renderer function + * @param rendererId The rendererId of the function to be removed + */ +_window.Vaadin.unsetLitRenderer = (component: Component, rendererName: string, rendererId: string) => { + // The check for __rendererId property is necessary since the renderer function + // may get overridden by another renderer, for example, by one coming from + // vaadin-template-renderer. We don't want LitRenderer registration cleanup to + // unintentionally remove the new renderer. + if (component[rendererName]?.__rendererId === rendererId) { + component[rendererName] = undefined; + } +}; diff --git a/java/demo/frontend/generated/jar-resources/loginOverlayConnector.js b/java/demo/frontend/generated/jar-resources/loginOverlayConnector.js new file mode 100644 index 000000000..b56477bef --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/loginOverlayConnector.js @@ -0,0 +1,40 @@ +(function () { + function copyClassName(loginOverlay) { + const overlayWrapper = loginOverlay.$.vaadinLoginOverlayWrapper; + if (overlayWrapper) { + overlayWrapper.className = loginOverlay.className; + } + } + + const observer = new MutationObserver((records) => { + records.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + copyClassName(mutation.target); + } + }); + }); + + window.Vaadin.Flow.loginOverlayConnector = { + initLazy: function (loginOverlay) { + if (loginOverlay.$connector) { + return; + } + + loginOverlay.$connector = {}; + + loginOverlay.addEventListener('opened-changed', (e) => { + if (e.detail.value) { + copyClassName(loginOverlay); + } + }); + + observer.observe(loginOverlay, { + attributes: true, + attributeFilter: ['class'] + }); + + // Copy initial class + copyClassName(loginOverlay); + } + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/lumo-includes.ts b/java/demo/frontend/generated/jar-resources/lumo-includes.ts new file mode 100644 index 000000000..bd4834fe1 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/lumo-includes.ts @@ -0,0 +1,9 @@ +import { color } from '@vaadin/vaadin-lumo-styles/color.js'; +import { typography } from '@vaadin/vaadin-lumo-styles/typography.js'; + +const tpl = document.createElement('template'); +tpl.innerHTML = ``; +document.head.appendChild(tpl.content); diff --git a/java/demo/frontend/generated/jar-resources/material-includes.ts b/java/demo/frontend/generated/jar-resources/material-includes.ts new file mode 100644 index 000000000..07ba838ae --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/material-includes.ts @@ -0,0 +1,9 @@ +import { colorLight } from '@vaadin/vaadin-material-styles'; +import { typography } from '@vaadin/vaadin-material-styles'; + +const tpl = document.createElement('template'); +tpl.innerHTML = ``; +document.head.appendChild(tpl.content); diff --git a/java/demo/frontend/generated/jar-resources/menubarConnector.js b/java/demo/frontend/generated/jar-resources/menubarConnector.js new file mode 100644 index 000000000..72c808e4b --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/menubarConnector.js @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +import './contextMenuConnector.js'; + +(function () { + const tryCatchWrapper = function (callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Menu Bar'); + }; + + /** + * Initializes the connector for a menu bar element. + * + * @param {HTMLElement} menubar + * @param {string} appId + */ + function initLazy(menubar, appId) { + if (menubar.$connector) { + return; + } + + const observer = new MutationObserver((records) => { + const hasChangedAttributes = records.some((entry) => { + const oldValue = entry.oldValue; + const newValue = entry.target.getAttribute(entry.attributeName); + return oldValue !== newValue; + }); + + if (hasChangedAttributes) { + menubar.$connector.generateItems(); + } + }); + + menubar.$connector = { + /** + * Generates and assigns the items to the menu bar. + * + * When the method is called without providing a node id, + * the previously generated items tree will be used. + * That can be useful if you only want to sync the disabled and hidden properties of root items. + * + * @param {number | undefined} nodeId + */ + generateItems: tryCatchWrapper((nodeId) => { + if (!menubar.shadowRoot) { + // workaround for https://github.com/vaadin/flow/issues/5722 + setTimeout(() => menubar.$connector.generateItems(nodeId)); + return; + } + + if (nodeId) { + menubar.__generatedItems = window.Vaadin.Flow.contextMenuConnector.generateItemsTree(appId, nodeId); + } + + let items = menubar.__generatedItems || []; + + // Propagate disabled state from items to parent buttons + items.forEach((item) => (item.disabled = item.component.disabled)); + + // Remove hidden items entirely from the array. Just hiding them + // could cause the overflow button to be rendered without items. + // + // The items-prop needs to be set even when all items are visible + // to update the disabled state and re-render buttons. + items = items.filter((item) => !item.component.hidden); + + // Observe for hidden and disabled attributes in case they are changed by Flow. + // When a change occurs, the observer will re-generate items on top of the existing tree + // to sync the new attribute values with the corresponding properties in the items array. + items.forEach((item) => { + observer.observe(item.component, { + attributeFilter: ['hidden', 'disabled'], + attributeOldValue: true + }); + }); + + menubar.items = items; + + // Propagate click events from the menu buttons to the item components + menubar._buttons.forEach((button) => { + if (button.item && button.item.component) { + button.addEventListener('click', (e) => { + if (e.composedPath().indexOf(button.item.component) === -1) { + button.item.component.click(); + e.stopPropagation(); + } + }); + } + }); + }) + }; + } + + window.Vaadin.Flow.menubarConnector = { + initLazy(...args) { + return tryCatchWrapper(initLazy)(...args); + } + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/messageListConnector.js b/java/demo/frontend/generated/jar-resources/messageListConnector.js new file mode 100644 index 000000000..6e1a325ed --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/messageListConnector.js @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +(function () { + const tryCatchWrapper = function (callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Message List'); + }; + + window.Vaadin.Flow.messageListConnector = { + setItems: (list, items, locale) => + tryCatchWrapper(function (list, items, locale) { + const formatter = new Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: 'numeric' + }); + list.items = items.map((item) => + item.time + ? Object.assign(item, { + time: formatter.format(new Date(item.time)) + }) + : item + ); + })(list, items, locale) + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/notificationConnector.js b/java/demo/frontend/generated/jar-resources/notificationConnector.js new file mode 100644 index 000000000..beee883c2 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/notificationConnector.js @@ -0,0 +1,38 @@ +(function () { + function copyClassName(notification) { + const card = notification._card; + if (card) { + card.className = notification.className; + } + } + + const observer = new MutationObserver((records) => { + records.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + copyClassName(mutation.target); + } + }); + }); + + window.Vaadin.Flow.notificationConnector = { + initLazy: function (notification) { + if (notification.$connector) { + return; + } + notification.$connector = {}; + + notification.addEventListener('opened-changed', (e) => { + if (e.detail.value) { + copyClassName(notification); + } + }); + + observer.observe(notification, { + attributes: true, + attributeFilter: ['class'] + }); + + copyClassName(notification); + } + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/selectConnector.js b/java/demo/frontend/generated/jar-resources/selectConnector.js new file mode 100644 index 000000000..52fc0a237 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/selectConnector.js @@ -0,0 +1,36 @@ +(function () { + const tryCatchWrapper = function (callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Select'); + }; + + window.Vaadin.Flow.selectConnector = { + initLazy: (select) => + tryCatchWrapper(function (select) { + const _findListBoxElement = tryCatchWrapper(function () { + for (let i = 0; i < select.childElementCount; i++) { + const child = select.children[i]; + if ('VAADIN-SELECT-LIST-BOX' === child.tagName.toUpperCase()) { + return child; + } + } + }); + + // do not init this connector twice for the given select + if (select.$connector) { + return; + } + + select.$connector = {}; + + select.renderer = tryCatchWrapper(function (root) { + const listBox = _findListBoxElement(); + if (listBox) { + if (root.firstChild) { + root.removeChild(root.firstChild); + } + root.appendChild(listBox); + } + }); + })(select) + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/tooltip.ts b/java/demo/frontend/generated/jar-resources/tooltip.ts new file mode 100644 index 000000000..a9c862828 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/tooltip.ts @@ -0,0 +1,11 @@ +import { Tooltip } from '@vaadin/tooltip'; + +const _window = window as any; +_window.Vaadin = _window.Vaadin || {}; +_window.Vaadin.Flow = _window.Vaadin.Flow || {}; + +_window.Vaadin.Flow.tooltip = { + setDefaultHideDelay: (hideDelay: number) => Tooltip.setDefaultHideDelay(hideDelay), + setDefaultFocusDelay: (focusDelay: number) => Tooltip.setDefaultFocusDelay(focusDelay), + setDefaultHoverDelay: (hoverDelay: number) => Tooltip.setDefaultHoverDelay(hoverDelay), +} diff --git a/java/demo/frontend/generated/jar-resources/vaadin-big-decimal-field.js b/java/demo/frontend/generated/jar-resources/vaadin-big-decimal-field.js new file mode 100644 index 000000000..3f4b28f0e --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/vaadin-big-decimal-field.js @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +(function() { + + let memoizedTemplate; + + customElements.whenDefined('vaadin-text-field').then(() => { + + class BigDecimalFieldElement extends customElements.get('vaadin-text-field') { + + static get template() { + if (!memoizedTemplate) { + memoizedTemplate = super.template.cloneNode(true); + memoizedTemplate.innerHTML += + ``; + } + return memoizedTemplate; + } + + static get is() { + return 'vaadin-big-decimal-field'; + } + + static get properties() { + return { + _decimalSeparator: { + type: String, + value: '.', + observer: '__decimalSeparatorChanged' + } + } + } + + ready() { + super.ready(); + this.inputElement.setAttribute('inputmode', 'decimal'); + } + + __decimalSeparatorChanged(separator, oldSeparator) { + this.allowedCharPattern = '[\\d-+' + separator + ']'; + + if (this.value && oldSeparator) { + this.value = this.value.split(oldSeparator).join(separator); + } + } + + } + + customElements.define(BigDecimalFieldElement.is, BigDecimalFieldElement); + + }); +})(); diff --git a/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.d.ts b/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.d.ts new file mode 100644 index 000000000..a7583c0c1 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.d.ts @@ -0,0 +1,119 @@ +import { LitElement } from 'lit'; +import { Product } from './License'; +interface Feature { + id: string; + title: string; + moreInfoLink: string; + requiresServerRestart: boolean; + enabled: boolean; +} +declare enum ConnectionStatus { + ACTIVE = "active", + INACTIVE = "inactive", + UNAVAILABLE = "unavailable", + ERROR = "error" +} +export declare class Connection extends Object { + static HEARTBEAT_INTERVAL: number; + status: ConnectionStatus; + webSocket?: WebSocket; + constructor(url?: string); + onHandshake(): void; + onReload(): void; + onConnectionError(_: string): void; + onStatusChange(_: ConnectionStatus): void; + onMessage(message: any): void; + handleMessage(msg: any): void; + handleError(msg: any): void; + setActive(yes: boolean): void; + setStatus(status: ConnectionStatus): void; + private send; + setFeature(featureId: string, enabled: boolean): void; + sendTelemetry(browserData: any): void; + sendLicenseCheck(product: Product): void; +} +declare enum MessageType { + LOG = "log", + INFORMATION = "information", + WARNING = "warning", + ERROR = "error" +} +interface Message { + id: number; + type: MessageType; + message: string; + details?: string; + link?: string; + persistentId?: string; + dontShowAgain: boolean; + deleted: boolean; +} +export declare class VaadinDevTools extends LitElement { + static BLUE_HSL: import("lit").CSSResult; + static GREEN_HSL: import("lit").CSSResult; + static GREY_HSL: import("lit").CSSResult; + static YELLOW_HSL: import("lit").CSSResult; + static RED_HSL: import("lit").CSSResult; + static MAX_LOG_ROWS: number; + static get styles(): import("lit").CSSResult; + static DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE: string; + static ACTIVE_KEY_IN_SESSION_STORAGE: string; + static TRIGGERED_KEY_IN_SESSION_STORAGE: string; + static TRIGGERED_COUNT_KEY_IN_SESSION_STORAGE: string; + static AUTO_DEMOTE_NOTIFICATION_DELAY: number; + static HOTSWAP_AGENT: string; + static JREBEL: string; + static SPRING_BOOT_DEVTOOLS: string; + static BACKEND_DISPLAY_NAME: Record; + static get isActive(): boolean; + static notificationDismissed(persistentId: string): boolean; + url?: string; + liveReloadDisabled?: boolean; + backend?: string; + springBootLiveReloadPort?: number; + expanded: boolean; + messages: Message[]; + splashMessage?: string; + notifications: Message[]; + frontendStatus: ConnectionStatus; + javaStatus: ConnectionStatus; + private tabs; + private activeTab; + private serverInfo; + private features; + private unreadErrors; + private root; + private javaConnection?; + private frontendConnection?; + private nextMessageId; + private disableEventListener?; + private transitionDuration; + elementTelemetry(): void; + openWebSocketConnection(): void; + getDedicatedWebSocketUrl(): string | undefined; + getSpringBootWebSocketUrl(location: any): string; + connectedCallback(): void; + format(o: any): string; + catchErrors(): void; + disconnectedCallback(): void; + toggleExpanded(): void; + showSplashMessage(msg: string | undefined): void; + demoteSplashMessage(): void; + checkLicense(productInfo: Product): void; + log(type: MessageType, message: string, details?: string, link?: string): void; + showNotification(type: MessageType, message: string, details?: string, link?: string, persistentId?: string): void; + dismissNotification(id: number): void; + findNotificationIndex(id: number): number; + toggleDontShowAgain(id: number): void; + setActive(yes: boolean): void; + getStatusColor(status: ConnectionStatus | undefined): import("lit").CSSResult; + renderMessage(messageObject: Message): import("lit-html").TemplateResult<1>; + render(): import("lit-html").TemplateResult<1>; + renderLog(): import("lit-html").TemplateResult<1>; + activateLog(): void; + renderInfo(): import("lit-html").TemplateResult<1>; + private renderFeatures; + copyInfoToClipboard(): void; + toggleFeatureFlag(e: Event, feature: Feature): void; +} +export {}; diff --git a/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.js b/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.js new file mode 100644 index 000000000..547fd0da8 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.js @@ -0,0 +1,1477 @@ +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +import { css, html, LitElement, nothing } from 'lit'; +import { property, query, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { copy } from './copy-to-clipboard.js'; +import { licenseCheckFailed, licenseCheckNoKey, licenseCheckOk, licenseInit } from './License'; +var ConnectionStatus; +(function (ConnectionStatus) { + ConnectionStatus["ACTIVE"] = "active"; + ConnectionStatus["INACTIVE"] = "inactive"; + ConnectionStatus["UNAVAILABLE"] = "unavailable"; + ConnectionStatus["ERROR"] = "error"; +})(ConnectionStatus || (ConnectionStatus = {})); +export class Connection extends Object { + constructor(url) { + super(); + this.status = ConnectionStatus.UNAVAILABLE; + if (url) { + this.webSocket = new WebSocket(url); + this.webSocket.onmessage = (msg) => this.handleMessage(msg); + this.webSocket.onerror = (err) => this.handleError(err); + this.webSocket.onclose = (_) => { + if (this.status !== ConnectionStatus.ERROR) { + this.setStatus(ConnectionStatus.UNAVAILABLE); + } + this.webSocket = undefined; + }; + } + setInterval(() => { + if (this.webSocket && self.status !== ConnectionStatus.ERROR && this.status !== ConnectionStatus.UNAVAILABLE) { + this.webSocket.send(''); + } + }, Connection.HEARTBEAT_INTERVAL); + } + onHandshake() { + // Intentionally empty + } + onReload() { + // Intentionally empty + } + onConnectionError(_) { + // Intentionally empty + } + onStatusChange(_) { + // Intentionally empty + } + onMessage(message) { + // eslint-disable-next-line no-console + console.error('Unknown message received from the live reload server:', message); + } + handleMessage(msg) { + let json; + try { + json = JSON.parse(msg.data); + } + catch (e) { + this.handleError(`[${e.name}: ${e.message}`); + return; + } + if (json.command === 'hello') { + this.setStatus(ConnectionStatus.ACTIVE); + this.onHandshake(); + } + else if (json.command === 'reload') { + if (this.status === ConnectionStatus.ACTIVE) { + this.onReload(); + } + } + else if (json.command === 'license-check-ok') { + licenseCheckOk(json.data); + } + else if (json.command === 'license-check-failed') { + licenseCheckFailed(json.data); + } + else if (json.command === 'license-check-nokey') { + licenseCheckNoKey(json.data); + } + else { + this.onMessage(json); + } + } + handleError(msg) { + // eslint-disable-next-line no-console + console.error(msg); + this.setStatus(ConnectionStatus.ERROR); + if (msg instanceof Event && this.webSocket) { + this.onConnectionError(`Error in WebSocket connection to ${this.webSocket.url}`); + } + else { + this.onConnectionError(msg); + } + } + setActive(yes) { + if (!yes && this.status === ConnectionStatus.ACTIVE) { + this.setStatus(ConnectionStatus.INACTIVE); + } + else if (yes && this.status === ConnectionStatus.INACTIVE) { + this.setStatus(ConnectionStatus.ACTIVE); + } + } + setStatus(status) { + if (this.status !== status) { + this.status = status; + this.onStatusChange(status); + } + } + send(command, data) { + const message = JSON.stringify({ command, data }); + if (!this.webSocket) { + // eslint-disable-next-line no-console + console.error(`Unable to send message ${command}. No websocket is available`); + } + else if (this.webSocket.readyState !== WebSocket.OPEN) { + this.webSocket.addEventListener('open', () => this.webSocket.send(message)); + } + else { + this.webSocket.send(message); + } + } + setFeature(featureId, enabled) { + this.send('setFeature', { featureId, enabled }); + } + sendTelemetry(browserData) { + this.send('reportTelemetry', { browserData }); + } + sendLicenseCheck(product) { + this.send('checkLicense', product); + } +} +Connection.HEARTBEAT_INTERVAL = 180000; +var MessageType; +(function (MessageType) { + MessageType["LOG"] = "log"; + MessageType["INFORMATION"] = "information"; + MessageType["WARNING"] = "warning"; + MessageType["ERROR"] = "error"; +})(MessageType || (MessageType = {})); +export class VaadinDevTools extends LitElement { + constructor() { + super(...arguments); + this.expanded = false; + this.messages = []; + this.notifications = []; + this.frontendStatus = ConnectionStatus.UNAVAILABLE; + this.javaStatus = ConnectionStatus.UNAVAILABLE; + this.tabs = [ + { id: 'log', title: 'Log', render: this.renderLog, activate: this.activateLog }, + { id: 'info', title: 'Info', render: this.renderInfo }, + { id: 'features', title: 'Feature Flags', render: this.renderFeatures } + ]; + this.activeTab = 'log'; + this.serverInfo = { + flowVersion: '', + vaadinVersion: '', + javaVersion: '', + osVersion: '', + productName: '' + }; + this.features = []; + this.unreadErrors = false; + this.nextMessageId = 1; + this.transitionDuration = 0; + } + static get styles() { + return css ` + :host { + --dev-tools-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, + 'Helvetica Neue', sans-serif; + --dev-tools-font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; + + --dev-tools-font-size: 0.8125rem; + --dev-tools-font-size-small: 0.75rem; + + --dev-tools-text-color: rgba(255, 255, 255, 0.8); + --dev-tools-text-color-secondary: rgba(255, 255, 255, 0.65); + --dev-tools-text-color-emphasis: rgba(255, 255, 255, 0.95); + --dev-tools-text-color-active: rgba(255, 255, 255, 1); + + --dev-tools-background-color-inactive: rgba(45, 45, 45, 0.25); + --dev-tools-background-color-active: rgba(45, 45, 45, 0.98); + --dev-tools-background-color-active-blurred: rgba(45, 45, 45, 0.85); + + --dev-tools-border-radius: 0.5rem; + --dev-tools-box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05), 0 4px 12px -2px rgba(0, 0, 0, 0.4); + + --dev-tools-blue-hsl: ${this.BLUE_HSL}; + --dev-tools-blue-color: hsl(var(--dev-tools-blue-hsl)); + --dev-tools-green-hsl: ${this.GREEN_HSL}; + --dev-tools-green-color: hsl(var(--dev-tools-green-hsl)); + --dev-tools-grey-hsl: ${this.GREY_HSL}; + --dev-tools-grey-color: hsl(var(--dev-tools-grey-hsl)); + --dev-tools-yellow-hsl: ${this.YELLOW_HSL}; + --dev-tools-yellow-color: hsl(var(--dev-tools-yellow-hsl)); + --dev-tools-red-hsl: ${this.RED_HSL}; + --dev-tools-red-color: hsl(var(--dev-tools-red-hsl)); + + /* Needs to be in ms, used in JavaScript as well */ + --dev-tools-transition-duration: 180ms; + + all: initial; + + direction: ltr; + cursor: default; + font: normal 400 var(--dev-tools-font-size) / 1.125rem var(--dev-tools-font-family); + color: var(--dev-tools-text-color); + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + + position: fixed; + z-index: 20000; + pointer-events: none; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column-reverse; + align-items: flex-end; + } + + .dev-tools { + pointer-events: auto; + display: flex; + align-items: center; + position: fixed; + z-index: inherit; + right: 0.5rem; + bottom: 0.5rem; + min-width: 1.75rem; + height: 1.75rem; + max-width: 1.75rem; + border-radius: 0.5rem; + padding: 0.375rem; + box-sizing: border-box; + background-color: var(--dev-tools-background-color-inactive); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05); + color: var(--dev-tools-text-color); + transition: var(--dev-tools-transition-duration); + white-space: nowrap; + line-height: 1rem; + } + + .dev-tools:hover, + .dev-tools.active { + background-color: var(--dev-tools-background-color-active); + box-shadow: var(--dev-tools-box-shadow); + } + + .dev-tools.active { + max-width: calc(100% - 1rem); + } + + .dev-tools .dev-tools-icon { + flex: none; + pointer-events: none; + display: inline-block; + width: 1rem; + height: 1rem; + fill: #fff; + transition: var(--dev-tools-transition-duration); + margin: 0; + } + + .dev-tools.active .dev-tools-icon { + opacity: 0; + position: absolute; + transform: scale(0.5); + } + + .dev-tools .status-blip { + flex: none; + display: block; + width: 6px; + height: 6px; + border-radius: 50%; + z-index: 20001; + background: var(--dev-tools-grey-color); + position: absolute; + top: -1px; + right: -1px; + } + + .dev-tools .status-description { + overflow: hidden; + text-overflow: ellipsis; + padding: 0 0.25rem; + } + + .dev-tools.error { + background-color: hsla(var(--dev-tools-red-hsl), 0.15); + animation: bounce 0.5s; + animation-iteration-count: 2; + } + + .switch { + display: inline-flex; + align-items: center; + } + + .switch input { + opacity: 0; + width: 0; + height: 0; + position: absolute; + } + + .switch .slider { + display: block; + flex: none; + width: 28px; + height: 18px; + border-radius: 9px; + background-color: rgba(255, 255, 255, 0.3); + transition: var(--dev-tools-transition-duration); + margin-right: 0.5rem; + } + + .switch:focus-within .slider, + .switch .slider:hover { + background-color: rgba(255, 255, 255, 0.35); + transition: none; + } + + .switch input:focus-visible ~ .slider { + box-shadow: 0 0 0 2px var(--dev-tools-background-color-active), 0 0 0 4px var(--dev-tools-blue-color); + } + + .switch .slider::before { + content: ''; + display: block; + margin: 2px; + width: 14px; + height: 14px; + background-color: #fff; + transition: var(--dev-tools-transition-duration); + border-radius: 50%; + } + + .switch input:checked + .slider { + background-color: var(--dev-tools-green-color); + } + + .switch input:checked + .slider::before { + transform: translateX(10px); + } + + .switch input:disabled + .slider::before { + background-color: var(--dev-tools-grey-color); + } + + .window.hidden { + opacity: 0; + transform: scale(0); + position: absolute; + } + + .window.visible { + transform: none; + opacity: 1; + pointer-events: auto; + } + + .window.visible ~ .dev-tools { + opacity: 0; + pointer-events: none; + } + + .window.visible ~ .dev-tools .dev-tools-icon, + .window.visible ~ .dev-tools .status-blip { + transition: none; + opacity: 0; + } + + .window { + border-radius: var(--dev-tools-border-radius); + overflow: hidden; + margin: 0.5rem; + width: 30rem; + max-width: calc(100% - 1rem); + max-height: calc(100vh - 1rem); + flex-shrink: 1; + background-color: var(--dev-tools-background-color-active); + color: var(--dev-tools-text-color); + transition: var(--dev-tools-transition-duration); + transform-origin: bottom right; + display: flex; + flex-direction: column; + box-shadow: var(--dev-tools-box-shadow); + outline: none; + } + + .window-toolbar { + display: flex; + flex: none; + align-items: center; + padding: 0.375rem; + white-space: nowrap; + order: 1; + background-color: rgba(0, 0, 0, 0.2); + gap: 0.5rem; + } + + .tab { + color: var(--dev-tools-text-color-secondary); + font: inherit; + font-size: var(--dev-tools-font-size-small); + font-weight: 500; + line-height: 1; + padding: 0.25rem 0.375rem; + background: none; + border: none; + margin: 0; + border-radius: 0.25rem; + transition: var(--dev-tools-transition-duration); + } + + .tab:hover, + .tab.active { + color: var(--dev-tools-text-color-active); + } + + .tab.active { + background-color: rgba(255, 255, 255, 0.12); + } + + .tab.unreadErrors::after { + content: '•'; + color: hsl(var(--dev-tools-red-hsl)); + font-size: 1.5rem; + position: absolute; + transform: translate(0, -50%); + } + + .ahreflike { + font-weight: 500; + color: var(--dev-tools-text-color-secondary); + text-decoration: underline; + cursor: pointer; + } + + .ahreflike:hover { + color: var(--dev-tools-text-color-emphasis); + } + + .button { + all: initial; + font-family: inherit; + font-size: var(--dev-tools-font-size-small); + line-height: 1; + white-space: nowrap; + background-color: rgba(0, 0, 0, 0.2); + color: inherit; + font-weight: 600; + padding: 0.25rem 0.375rem; + border-radius: 0.25rem; + } + + .button:focus, + .button:hover { + color: var(--dev-tools-text-color-emphasis); + } + + .minimize-button { + flex: none; + width: 1rem; + height: 1rem; + color: inherit; + background-color: transparent; + border: 0; + padding: 0; + margin: 0 0 0 auto; + opacity: 0.8; + } + + .minimize-button:hover { + opacity: 1; + } + + .minimize-button svg { + max-width: 100%; + } + + .message.information { + --dev-tools-notification-color: var(--dev-tools-blue-color); + } + + .message.warning { + --dev-tools-notification-color: var(--dev-tools-yellow-color); + } + + .message.error { + --dev-tools-notification-color: var(--dev-tools-red-color); + } + + .message { + display: flex; + padding: 0.1875rem 0.75rem 0.1875rem 2rem; + background-clip: padding-box; + } + + .message.log { + padding-left: 0.75rem; + } + + .message-content { + margin-right: 0.5rem; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + } + + .message-heading { + position: relative; + display: flex; + align-items: center; + margin: 0.125rem 0; + } + + .message.log { + color: var(--dev-tools-text-color-secondary); + } + + .message:not(.log) .message-heading { + font-weight: 500; + } + + .message.has-details .message-heading { + color: var(--dev-tools-text-color-emphasis); + font-weight: 600; + } + + .message-heading::before { + position: absolute; + margin-left: -1.5rem; + display: inline-block; + text-align: center; + font-size: 0.875em; + font-weight: 600; + line-height: calc(1.25em - 2px); + width: 14px; + height: 14px; + box-sizing: border-box; + border: 1px solid transparent; + border-radius: 50%; + } + + .message.information .message-heading::before { + content: 'i'; + border-color: currentColor; + color: var(--dev-tools-notification-color); + } + + .message.warning .message-heading::before, + .message.error .message-heading::before { + content: '!'; + color: var(--dev-tools-background-color-active); + background-color: var(--dev-tools-notification-color); + } + + .features-tray { + padding: 0.75rem; + flex: auto; + overflow: auto; + animation: fade-in var(--dev-tools-transition-duration) ease-in; + user-select: text; + } + + .features-tray p { + margin-top: 0; + color: var(--dev-tools-text-color-secondary); + } + + .features-tray .feature { + display: flex; + align-items: center; + gap: 1rem; + padding-bottom: 0.5em; + } + + .message .message-details { + font-weight: 400; + color: var(--dev-tools-text-color-secondary); + margin: 0.25rem 0; + } + + .message .message-details[hidden] { + display: none; + } + + .message .message-details p { + display: inline; + margin: 0; + margin-right: 0.375em; + word-break: break-word; + } + + .message .persist { + color: var(--dev-tools-text-color-secondary); + white-space: nowrap; + margin: 0.375rem 0; + display: flex; + align-items: center; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + } + + .message .persist::before { + content: ''; + width: 1em; + height: 1em; + border-radius: 0.2em; + margin-right: 0.375em; + background-color: rgba(255, 255, 255, 0.3); + } + + .message .persist:hover::before { + background-color: rgba(255, 255, 255, 0.4); + } + + .message .persist.on::before { + background-color: rgba(255, 255, 255, 0.9); + } + + .message .persist.on::after { + content: ''; + order: -1; + position: absolute; + width: 0.75em; + height: 0.25em; + border: 2px solid var(--dev-tools-background-color-active); + border-width: 0 0 2px 2px; + transform: translate(0.05em, -0.05em) rotate(-45deg) scale(0.8, 0.9); + } + + .message .dismiss-message { + font-weight: 600; + align-self: stretch; + display: flex; + align-items: center; + padding: 0 0.25rem; + margin-left: 0.5rem; + color: var(--dev-tools-text-color-secondary); + } + + .message .dismiss-message:hover { + color: var(--dev-tools-text-color); + } + + .notification-tray { + display: flex; + flex-direction: column-reverse; + align-items: flex-end; + margin: 0.5rem; + flex: none; + } + + .window.hidden + .notification-tray { + margin-bottom: 3rem; + } + + .notification-tray .message { + pointer-events: auto; + background-color: var(--dev-tools-background-color-active); + color: var(--dev-tools-text-color); + max-width: 30rem; + box-sizing: border-box; + border-radius: var(--dev-tools-border-radius); + margin-top: 0.5rem; + transition: var(--dev-tools-transition-duration); + transform-origin: bottom right; + animation: slideIn var(--dev-tools-transition-duration); + box-shadow: var(--dev-tools-box-shadow); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + } + + .notification-tray .message.animate-out { + animation: slideOut forwards var(--dev-tools-transition-duration); + } + + .notification-tray .message .message-details { + max-height: 10em; + overflow: hidden; + } + + .message-tray { + flex: auto; + overflow: auto; + max-height: 20rem; + user-select: text; + } + + .message-tray .message { + animation: fade-in var(--dev-tools-transition-duration) ease-in; + padding-left: 2.25rem; + } + + .message-tray .message.warning { + background-color: hsla(var(--dev-tools-yellow-hsl), 0.09); + } + + .message-tray .message.error { + background-color: hsla(var(--dev-tools-red-hsl), 0.09); + } + + .message-tray .message.error .message-heading { + color: hsl(var(--dev-tools-red-hsl)); + } + + .message-tray .message.warning .message-heading { + color: hsl(var(--dev-tools-yellow-hsl)); + } + + .message-tray .message + .message { + border-top: 1px solid rgba(255, 255, 255, 0.07); + } + + .message-tray .dismiss-message, + .message-tray .persist { + display: none; + } + + .info-tray { + padding: 0.75rem; + position: relative; + flex: auto; + overflow: auto; + animation: fade-in var(--dev-tools-transition-duration) ease-in; + user-select: text; + } + + .info-tray dl { + margin: 0; + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 0.75rem; + position: relative; + } + + .info-tray dt { + grid-column: 1; + color: var(--dev-tools-text-color-emphasis); + } + + .info-tray dt:not(:first-child)::before { + content: ''; + width: 100%; + position: absolute; + height: 1px; + background-color: rgba(255, 255, 255, 0.1); + margin-top: -0.375rem; + } + + .info-tray dd { + grid-column: 2; + margin: 0; + } + + .info-tray :is(dt, dd):not(:last-child) { + margin-bottom: 0.75rem; + } + + .info-tray dd + dd { + margin-top: -0.5rem; + } + + .info-tray .live-reload-status::before { + content: '•'; + color: var(--status-color); + width: 0.75rem; + display: inline-block; + font-size: 1rem; + line-height: 0.5rem; + } + + .info-tray .copy { + position: fixed; + z-index: 1; + top: 0.5rem; + right: 0.5rem; + } + + .info-tray .switch { + vertical-align: -4px; + } + + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0%); + opacity: 1; + } + } + + @keyframes slideOut { + from { + transform: translateX(0%); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } + } + + @keyframes fade-in { + 0% { + opacity: 0; + } + } + + @keyframes bounce { + 0% { + transform: scale(0.8); + } + 50% { + transform: scale(1.5); + background-color: hsla(var(--dev-tools-red-hsl), 1); + } + 100% { + transform: scale(1); + } + } + + @supports (backdrop-filter: blur(1px)) { + .dev-tools, + .window, + .notification-tray .message { + backdrop-filter: blur(8px); + } + .dev-tools:hover, + .dev-tools.active, + .window, + .notification-tray .message { + background-color: var(--dev-tools-background-color-active-blurred); + } + } + `; + } + static get isActive() { + const active = window.sessionStorage.getItem(VaadinDevTools.ACTIVE_KEY_IN_SESSION_STORAGE); + return active === null || active !== 'false'; + } + static notificationDismissed(persistentId) { + const shown = window.localStorage.getItem(VaadinDevTools.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE); + return shown !== null && shown.includes(persistentId); + } + elementTelemetry() { + let data = {}; + try { + // localstorage data is collected by vaadin-usage-statistics.js + const localStorageStatsString = localStorage.getItem('vaadin.statistics.basket'); + if (!localStorageStatsString) { + // Do not send empty data + return; + } + data = JSON.parse(localStorageStatsString); + } + catch (e) { + // In case of parse errors don't send anything + return; + } + if (this.frontendConnection) { + this.frontendConnection.sendTelemetry(data); + } + } + openWebSocketConnection() { + this.frontendStatus = ConnectionStatus.UNAVAILABLE; + this.javaStatus = ConnectionStatus.UNAVAILABLE; + const onConnectionError = (msg) => this.log(MessageType.ERROR, msg); + const onReload = () => { + if (this.liveReloadDisabled) { + return; + } + this.showSplashMessage('Reloading…'); + const lastReload = window.sessionStorage.getItem(VaadinDevTools.TRIGGERED_COUNT_KEY_IN_SESSION_STORAGE); + const nextReload = lastReload ? parseInt(lastReload, 10) + 1 : 1; + window.sessionStorage.setItem(VaadinDevTools.TRIGGERED_COUNT_KEY_IN_SESSION_STORAGE, nextReload.toString()); + window.sessionStorage.setItem(VaadinDevTools.TRIGGERED_KEY_IN_SESSION_STORAGE, 'true'); + window.location.reload(); + }; + const frontendConnection = new Connection(this.getDedicatedWebSocketUrl()); + frontendConnection.onHandshake = () => { + this.log(MessageType.LOG, 'Vaadin development mode initialized'); + if (!VaadinDevTools.isActive) { + frontendConnection.setActive(false); + } + this.elementTelemetry(); + }; + frontendConnection.onConnectionError = onConnectionError; + frontendConnection.onReload = onReload; + frontendConnection.onStatusChange = (status) => { + this.frontendStatus = status; + }; + frontendConnection.onMessage = (message) => { + if ((message === null || message === void 0 ? void 0 : message.command) === 'serverInfo') { + this.serverInfo = message.data; + } + else if ((message === null || message === void 0 ? void 0 : message.command) === 'featureFlags') { + this.features = message.data.features; + } + else { + // eslint-disable-next-line no-console + console.error('Unknown message from front-end connection:', JSON.stringify(message)); + } + }; + this.frontendConnection = frontendConnection; + let javaConnection; + if (this.backend === VaadinDevTools.SPRING_BOOT_DEVTOOLS && this.springBootLiveReloadPort) { + javaConnection = new Connection(this.getSpringBootWebSocketUrl(window.location)); + javaConnection.onHandshake = () => { + if (!VaadinDevTools.isActive) { + javaConnection.setActive(false); + } + }; + javaConnection.onReload = onReload; + javaConnection.onConnectionError = onConnectionError; + } + else if (this.backend === VaadinDevTools.JREBEL || this.backend === VaadinDevTools.HOTSWAP_AGENT) { + javaConnection = frontendConnection; + } + else { + javaConnection = new Connection(undefined); + } + const prevOnStatusChange = javaConnection.onStatusChange; + javaConnection.onStatusChange = (status) => { + prevOnStatusChange(status); + this.javaStatus = status; + }; + const prevOnHandshake = javaConnection.onHandshake; + javaConnection.onHandshake = () => { + prevOnHandshake(); + if (this.backend) { + this.log(MessageType.INFORMATION, `Java live reload available: ${VaadinDevTools.BACKEND_DISPLAY_NAME[this.backend]}`); + } + }; + this.javaConnection = javaConnection; + if (!this.backend) { + this.showNotification(MessageType.WARNING, 'Java live reload unavailable', 'Live reload for Java changes is currently not set up. Find out how to make use of this functionality to boost your workflow.', 'https://vaadin.com/docs/latest/flow/configuration/live-reload', 'liveReloadUnavailable'); + } + } + getDedicatedWebSocketUrl() { + function getAbsoluteUrl(relative) { + // Use innerHTML to obtain an absolute URL + const div = document.createElement('div'); + div.innerHTML = ``; + return div.firstChild.href; + } + if (this.url === undefined) { + return undefined; + } + const connectionBaseUrl = getAbsoluteUrl(this.url); + if (!connectionBaseUrl.startsWith('http://') && !connectionBaseUrl.startsWith('https://')) { + // eslint-disable-next-line no-console + console.error('The protocol of the url should be http or https for live reload to work.'); + return undefined; + } + return `${connectionBaseUrl.replace(/^http/, 'ws')}?v-r=push&debug_window`; + } + getSpringBootWebSocketUrl(location) { + const { hostname } = location; + const wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws'; + if (hostname.endsWith('gitpod.io')) { + // Gitpod uses `port-url` instead of `url:port` + const hostnameWithoutPort = hostname.replace(/.*?-/, ''); + return `${wsProtocol}://${this.springBootLiveReloadPort}-${hostnameWithoutPort}`; + } + else { + return `${wsProtocol}://${hostname}:${this.springBootLiveReloadPort}`; + } + } + connectedCallback() { + super.connectedCallback(); + this.catchErrors(); + // when focus or clicking anywhere, move the splash message to the message tray + this.disableEventListener = (_) => this.demoteSplashMessage(); + document.body.addEventListener('focus', this.disableEventListener); + document.body.addEventListener('click', this.disableEventListener); + this.openWebSocketConnection(); + const lastReload = window.sessionStorage.getItem(VaadinDevTools.TRIGGERED_KEY_IN_SESSION_STORAGE); + if (lastReload) { + const now = new Date(); + const reloaded = `${`0${now.getHours()}`.slice(-2)}:${`0${now.getMinutes()}`.slice(-2)}:${`0${now.getSeconds()}`.slice(-2)}`; + this.showSplashMessage(`Page reloaded at ${reloaded}`); + window.sessionStorage.removeItem(VaadinDevTools.TRIGGERED_KEY_IN_SESSION_STORAGE); + } + this.transitionDuration = parseInt(window.getComputedStyle(this).getPropertyValue('--dev-tools-transition-duration'), 10); + const windowAny = window; + windowAny.Vaadin = windowAny.Vaadin || {}; + windowAny.Vaadin.devTools = Object.assign(this, windowAny.Vaadin.devTools); + licenseInit(); + } + format(o) { + return o.toString(); + } + catchErrors() { + // Process stored messages + const queue = window.Vaadin.ConsoleErrors; + if (queue) { + queue.forEach((args) => { + this.log(MessageType.ERROR, args.map((o) => this.format(o)).join(' ')); + }); + } + // Install new handler that immediately processes messages + window.Vaadin.ConsoleErrors = { + push: (args) => { + this.log(MessageType.ERROR, args.map((o) => this.format(o)).join(' ')); + } + }; + } + disconnectedCallback() { + if (this.disableEventListener) { + document.body.removeEventListener('focus', this.disableEventListener); + document.body.removeEventListener('click', this.disableEventListener); + } + super.disconnectedCallback(); + } + toggleExpanded() { + this.notifications.slice().forEach((notification) => this.dismissNotification(notification.id)); + this.expanded = !this.expanded; + if (this.expanded) { + this.root.focus(); + } + } + showSplashMessage(msg) { + this.splashMessage = msg; + if (this.splashMessage) { + if (this.expanded) { + this.demoteSplashMessage(); + } + else { + // automatically move notification to message tray after a certain amount of time + setTimeout(() => { + this.demoteSplashMessage(); + }, VaadinDevTools.AUTO_DEMOTE_NOTIFICATION_DELAY); + } + } + } + demoteSplashMessage() { + if (this.splashMessage) { + this.log(MessageType.LOG, this.splashMessage); + } + this.showSplashMessage(undefined); + } + checkLicense(productInfo) { + if (this.frontendConnection) { + this.frontendConnection.sendLicenseCheck(productInfo); + } + else { + licenseCheckFailed({ message: 'Internal error: no connection', product: productInfo }); + } + } + log(type, message, details, link) { + const id = this.nextMessageId; + this.nextMessageId += 1; + this.messages.push({ + id, + type, + message, + details, + link, + dontShowAgain: false, + deleted: false + }); + while (this.messages.length > VaadinDevTools.MAX_LOG_ROWS) { + this.messages.shift(); + } + this.requestUpdate(); + this.updateComplete.then(() => { + // Scroll into view + const lastMessage = this.renderRoot.querySelector('.message-tray .message:last-child'); + if (this.expanded && lastMessage) { + setTimeout(() => lastMessage.scrollIntoView({ behavior: 'smooth' }), this.transitionDuration); + this.unreadErrors = false; + } + else if (type === MessageType.ERROR) { + this.unreadErrors = true; + } + }); + } + showNotification(type, message, details, link, persistentId) { + if (persistentId === undefined || !VaadinDevTools.notificationDismissed(persistentId)) { + // Do not open persistent message if another is already visible with the same persistentId + const matchingVisibleNotifications = this.notifications + .filter((notification) => notification.persistentId === persistentId) + .filter((notification) => !notification.deleted); + if (matchingVisibleNotifications.length > 0) { + return; + } + const id = this.nextMessageId; + this.nextMessageId += 1; + this.notifications.push({ + id, + type, + message, + details, + link, + persistentId, + dontShowAgain: false, + deleted: false + }); + // automatically move notification to message tray after a certain amount of time unless it contains a link + if (link === undefined) { + setTimeout(() => { + this.dismissNotification(id); + }, VaadinDevTools.AUTO_DEMOTE_NOTIFICATION_DELAY); + } + this.requestUpdate(); + } + else { + this.log(type, message, details, link); + } + } + dismissNotification(id) { + const index = this.findNotificationIndex(id); + if (index !== -1 && !this.notifications[index].deleted) { + const notification = this.notifications[index]; + // user is explicitly dismissing a notification---after that we won't bug them with it + if (notification.dontShowAgain && + notification.persistentId && + !VaadinDevTools.notificationDismissed(notification.persistentId)) { + let dismissed = window.localStorage.getItem(VaadinDevTools.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE); + dismissed = dismissed === null ? notification.persistentId : `${dismissed},${notification.persistentId}`; + window.localStorage.setItem(VaadinDevTools.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE, dismissed); + } + notification.deleted = true; + this.log(notification.type, notification.message, notification.details, notification.link); + // give some time for the animation + setTimeout(() => { + const idx = this.findNotificationIndex(id); + if (idx !== -1) { + this.notifications.splice(idx, 1); + this.requestUpdate(); + } + }, this.transitionDuration); + } + } + findNotificationIndex(id) { + let index = -1; + this.notifications.some((notification, idx) => { + if (notification.id === id) { + index = idx; + return true; + } + else { + return false; + } + }); + return index; + } + toggleDontShowAgain(id) { + const index = this.findNotificationIndex(id); + if (index !== -1 && !this.notifications[index].deleted) { + const notification = this.notifications[index]; + notification.dontShowAgain = !notification.dontShowAgain; + this.requestUpdate(); + } + } + setActive(yes) { + var _a, _b; + (_a = this.frontendConnection) === null || _a === void 0 ? void 0 : _a.setActive(yes); + (_b = this.javaConnection) === null || _b === void 0 ? void 0 : _b.setActive(yes); + window.sessionStorage.setItem(VaadinDevTools.ACTIVE_KEY_IN_SESSION_STORAGE, yes ? 'true' : 'false'); + } + getStatusColor(status) { + if (status === ConnectionStatus.ACTIVE) { + return css `hsl(${VaadinDevTools.GREEN_HSL})`; + } + else if (status === ConnectionStatus.INACTIVE) { + return css `hsl(${VaadinDevTools.GREY_HSL})`; + } + else if (status === ConnectionStatus.UNAVAILABLE) { + return css `hsl(${VaadinDevTools.YELLOW_HSL})`; + } + else if (status === ConnectionStatus.ERROR) { + return css `hsl(${VaadinDevTools.RED_HSL})`; + } + else { + return css `none`; + } + } + /* eslint-disable lit/no-template-arrow */ + renderMessage(messageObject) { + return html ` + + `; + } + /* eslint-disable lit/no-template-map */ + render() { + return html `
e.key === 'Escape' && this.toggleExpanded()} + > +
+ ${this.tabs.map((tab) => html ` `)} + +
+ ${this.tabs.map((tab) => (this.activeTab === tab.id ? tab.render.call(this) : nothing))} +
+ +
${this.notifications.map((msg) => this.renderMessage(msg))}
+
this.toggleExpanded()} + > + ${this.unreadErrors + ? html ` + + + + + + ` + : html ``} + + + ${this.splashMessage ? html `${this.splashMessage}
` : nothing} + `; + } + renderLog() { + return html `
${this.messages.map((msg) => this.renderMessage(msg))}
`; + } + activateLog() { + this.unreadErrors = false; + this.updateComplete.then(() => { + const lastMessage = this.renderRoot.querySelector('.message-tray .message:last-child'); + if (lastMessage) { + lastMessage.scrollIntoView(); + } + }); + } + renderInfo() { + return html `
+ +
+
${this.serverInfo.productName}
+
${this.serverInfo.vaadinVersion}
+
Flow
+
${this.serverInfo.flowVersion}
+
Java
+
${this.serverInfo.javaVersion}
+
OS
+
${this.serverInfo.osVersion}
+
Browser
+
${navigator.userAgent}
+
+ Live reload + +
+
+ Java ${this.javaStatus} ${this.backend ? `(${VaadinDevTools.BACKEND_DISPLAY_NAME[this.backend]})` : ''} +
+
+ Front end ${this.frontendStatus} +
+
+
`; + } + renderFeatures() { + return html `
+ ${this.features.map((feature) => html `
+ + Learn more +
`)} +
`; + } + copyInfoToClipboard() { + const items = this.renderRoot.querySelectorAll('.info-tray dt, .info-tray dd'); + const text = Array.from(items) + .map((message) => (message.localName === 'dd' ? ': ' : '\n') + message.textContent.trim()) + .join('') + .replace(/^\n/, ''); + copy(text); + this.showNotification(MessageType.INFORMATION, 'Environment information copied to clipboard', undefined, undefined, 'versionInfoCopied'); + } + toggleFeatureFlag(e, feature) { + const enabled = e.target.checked; + if (this.frontendConnection) { + this.frontendConnection.setFeature(feature.id, enabled); + this.showNotification(MessageType.INFORMATION, `“${feature.title}” ${enabled ? 'enabled' : 'disabled'}`, feature.requiresServerRestart ? 'This feature requires a server restart' : undefined, undefined, `feature${feature.id}${enabled ? 'Enabled' : 'Disabled'}`); + } + else { + this.log(MessageType.ERROR, `Unable to toggle feature ${feature.title}: No server connection available`); + } + } +} +VaadinDevTools.BLUE_HSL = css `206, 100%, 70%`; +VaadinDevTools.GREEN_HSL = css `145, 80%, 42%`; +VaadinDevTools.GREY_HSL = css `0, 0%, 50%`; +VaadinDevTools.YELLOW_HSL = css `38, 98%, 64%`; +VaadinDevTools.RED_HSL = css `355, 100%, 68%`; +VaadinDevTools.MAX_LOG_ROWS = 1000; +VaadinDevTools.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE = 'vaadin.live-reload.dismissedNotifications'; +VaadinDevTools.ACTIVE_KEY_IN_SESSION_STORAGE = 'vaadin.live-reload.active'; +VaadinDevTools.TRIGGERED_KEY_IN_SESSION_STORAGE = 'vaadin.live-reload.triggered'; +VaadinDevTools.TRIGGERED_COUNT_KEY_IN_SESSION_STORAGE = 'vaadin.live-reload.triggeredCount'; +VaadinDevTools.AUTO_DEMOTE_NOTIFICATION_DELAY = 5000; +VaadinDevTools.HOTSWAP_AGENT = 'HOTSWAP_AGENT'; +VaadinDevTools.JREBEL = 'JREBEL'; +VaadinDevTools.SPRING_BOOT_DEVTOOLS = 'SPRING_BOOT_DEVTOOLS'; +VaadinDevTools.BACKEND_DISPLAY_NAME = { + HOTSWAP_AGENT: 'HotswapAgent', + JREBEL: 'JRebel', + SPRING_BOOT_DEVTOOLS: 'Spring Boot Devtools' +}; +__decorate([ + property({ type: String }) +], VaadinDevTools.prototype, "url", void 0); +__decorate([ + property({ type: Boolean, attribute: true }) +], VaadinDevTools.prototype, "liveReloadDisabled", void 0); +__decorate([ + property({ type: String }) +], VaadinDevTools.prototype, "backend", void 0); +__decorate([ + property({ type: Number }) +], VaadinDevTools.prototype, "springBootLiveReloadPort", void 0); +__decorate([ + property({ type: Boolean, attribute: false }) +], VaadinDevTools.prototype, "expanded", void 0); +__decorate([ + property({ type: Array, attribute: false }) +], VaadinDevTools.prototype, "messages", void 0); +__decorate([ + property({ type: String, attribute: false }) +], VaadinDevTools.prototype, "splashMessage", void 0); +__decorate([ + property({ type: Array, attribute: false }) +], VaadinDevTools.prototype, "notifications", void 0); +__decorate([ + property({ type: String, attribute: false }) +], VaadinDevTools.prototype, "frontendStatus", void 0); +__decorate([ + property({ type: String, attribute: false }) +], VaadinDevTools.prototype, "javaStatus", void 0); +__decorate([ + state() +], VaadinDevTools.prototype, "tabs", void 0); +__decorate([ + state() +], VaadinDevTools.prototype, "activeTab", void 0); +__decorate([ + state() +], VaadinDevTools.prototype, "serverInfo", void 0); +__decorate([ + state() +], VaadinDevTools.prototype, "features", void 0); +__decorate([ + state() +], VaadinDevTools.prototype, "unreadErrors", void 0); +__decorate([ + query('.window') +], VaadinDevTools.prototype, "root", void 0); +if (customElements.get('vaadin-dev-tools') === undefined) { + customElements.define('vaadin-dev-tools', VaadinDevTools); +} +//# sourceMappingURL=vaadin-dev-tools.js.map \ No newline at end of file diff --git a/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.js.map b/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.js.map new file mode 100644 index 000000000..55ba4dd01 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/vaadin-dev-tools.js.map @@ -0,0 +1 @@ +{"version":3,"file":"vaadin-dev-tools.js","sourceRoot":"","sources":["../../../../src/main/frontend/vaadin-dev-tools.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,6DAA6D;AAC7D,aAAa;AACb,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAW,WAAW,EAAE,MAAM,WAAW,CAAC;AAyBxG,IAAK,gBAKJ;AALD,WAAK,gBAAgB;IACnB,qCAAiB,CAAA;IACjB,yCAAqB,CAAA;IACrB,+CAA2B,CAAA;IAC3B,mCAAe,CAAA;AACjB,CAAC,EALI,gBAAgB,KAAhB,gBAAgB,QAKpB;AAED,MAAM,OAAO,UAAW,SAAQ,MAAM;IAMpC,YAAY,GAAY;QACtB,KAAK,EAAE,CAAC;QAJV,WAAM,GAAqB,gBAAgB,CAAC,WAAW,CAAC;QAMtD,IAAI,GAAG,EAAE;YACP,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC;YACpC,IAAI,CAAC,SAAS,CAAC,SAAS,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YAC5D,IAAI,CAAC,SAAS,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;YACxD,IAAI,CAAC,SAAS,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,EAAE;gBAC7B,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,KAAK,EAAE;oBAC1C,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC;iBAC9C;gBACD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;YAC7B,CAAC,CAAC;SACH;QAED,WAAW,CAAC,GAAG,EAAE;YACf,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,WAAW,EAAE;gBAC5G,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;aACzB;QACH,CAAC,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC;IACpC,CAAC;IAED,WAAW;QACT,sBAAsB;IACxB,CAAC;IAED,QAAQ;QACN,sBAAsB;IACxB,CAAC;IAED,iBAAiB,CAAC,CAAS;QACzB,sBAAsB;IACxB,CAAC;IAED,cAAc,CAAC,CAAmB;QAChC,sBAAsB;IACxB,CAAC;IAED,SAAS,CAAC,OAAY;QACpB,sCAAsC;QACtC,OAAO,CAAC,KAAK,CAAC,uDAAuD,EAAE,OAAO,CAAC,CAAC;IAClF,CAAC;IAED,aAAa,CAAC,GAAQ;QACpB,IAAI,IAAI,CAAC;QACT,IAAI;YACF,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;SAC7B;QAAC,OAAO,CAAM,EAAE;YACf,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAC7C,OAAO;SACR;QACD,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE;YAC5B,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;YACxC,IAAI,CAAC,WAAW,EAAE,CAAC;SACpB;aAAM,IAAI,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE;YACpC,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,MAAM,EAAE;gBAC3C,IAAI,CAAC,QAAQ,EAAE,CAAC;aACjB;SACF;aAAM,IAAI,IAAI,CAAC,OAAO,KAAK,kBAAkB,EAAE;YAC9C,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SAC3B;aAAM,IAAI,IAAI,CAAC,OAAO,KAAK,sBAAsB,EAAE;YAClD,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SAC/B;aAAM,IAAI,IAAI,CAAC,OAAO,KAAK,qBAAqB,EAAE;YACjD,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SAC9B;aAAM;YACL,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;SACtB;IACH,CAAC;IAED,WAAW,CAAC,GAAQ;QAClB,sCAAsC;QACtC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,GAAG,YAAY,KAAK,IAAI,IAAI,CAAC,SAAS,EAAE;YAC1C,IAAI,CAAC,iBAAiB,CAAC,oCAAoC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC;SAClF;aAAM;YACL,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;SAC7B;IACH,CAAC;IAED,SAAS,CAAC,GAAY;QACpB,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,MAAM,EAAE;YACnD,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;SAC3C;aAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,QAAQ,EAAE;YAC3D,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;SACzC;IACH,CAAC;IAED,SAAS,CAAC,MAAwB;QAChC,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,EAAE;YAC1B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;SAC7B;IACH,CAAC;IAEO,IAAI,CAAC,OAAe,EAAE,IAAS;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE;YACnB,sCAAsC;YACtC,OAAO,CAAC,KAAK,CAAC,0BAA0B,OAAO,6BAA6B,CAAC,CAAC;SAC/E;aAAM,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE;YACvD,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;SAC9E;aAAM;YACL,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;SAC9B;IACH,CAAC;IAED,UAAU,CAAC,SAAiB,EAAE,OAAgB;QAC5C,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,aAAa,CAAC,WAAgB;QAC5B,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,gBAAgB,CAAC,OAAgB;QAC/B,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IACrC,CAAC;;AAxHM,6BAAkB,GAAG,MAAM,CAAC;AA2HrC,IAAK,WAKJ;AALD,WAAK,WAAW;IACd,0BAAW,CAAA;IACX,0CAA2B,CAAA;IAC3B,kCAAmB,CAAA;IACnB,8BAAe,CAAA;AACjB,CAAC,EALI,WAAW,KAAX,WAAW,QAKf;AAaD,MAAM,OAAO,cAAe,SAAQ,UAAU;IAA9C;;QA2tBE,aAAQ,GAAY,KAAK,CAAC;QAG1B,aAAQ,GAAc,EAAE,CAAC;QAMzB,kBAAa,GAAc,EAAE,CAAC;QAG9B,mBAAc,GAAqB,gBAAgB,CAAC,WAAW,CAAC;QAGhE,eAAU,GAAqB,gBAAgB,CAAC,WAAW,CAAC;QAGpD,SAAI,GAAmB;YAC7B,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE;YAC/E,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE;YACtD,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE;SACxE,CAAC;QAGM,cAAS,GAAW,KAAK,CAAC;QAG1B,eAAU,GAAe;YAC/B,WAAW,EAAE,EAAE;YACf,aAAa,EAAE,EAAE;YACjB,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,EAAE;YACb,WAAW,EAAE,EAAE;SAChB,CAAC;QAGM,aAAQ,GAAc,EAAE,CAAC;QAGzB,iBAAY,GAAG,KAAK,CAAC;QAQrB,kBAAa,GAAW,CAAC,CAAC;QAI1B,uBAAkB,GAAW,CAAC,CAAC;IAwlBzC,CAAC;IA/1CC,MAAM,KAAK,MAAM;QACf,OAAO,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;gCAsBkB,IAAI,CAAC,QAAQ;;iCAEZ,IAAI,CAAC,SAAS;;gCAEf,IAAI,CAAC,QAAQ;;kCAEX,IAAI,CAAC,UAAU;;+BAElB,IAAI,CAAC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0oBtC,CAAC;IACJ,CAAC;IAkBD,MAAM,KAAK,QAAQ;QACjB,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,6BAA6B,CAAC,CAAC;QAC3F,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,OAAO,CAAC;IAC/C,CAAC;IAED,MAAM,CAAC,qBAAqB,CAAC,YAAoB;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,cAAc,CAAC,wCAAwC,CAAC,CAAC;QACnG,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACxD,CAAC;IAqED,gBAAgB;QACd,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI;YACF,+DAA+D;YAC/D,MAAM,uBAAuB,GAAG,YAAY,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC;YACjF,IAAI,CAAC,uBAAuB,EAAE;gBAC5B,yBAAyB;gBACzB,OAAO;aACR;YACD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;SAC5C;QAAC,OAAO,CAAC,EAAE;YACV,8CAA8C;YAC9C,OAAO;SACR;QAED,IAAI,IAAI,CAAC,kBAAkB,EAAE;YAC3B,IAAI,CAAC,kBAAkB,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;SAC7C;IACH,CAAC;IAED,uBAAuB;QACrB,IAAI,CAAC,cAAc,GAAG,gBAAgB,CAAC,WAAW,CAAC;QACnD,IAAI,CAAC,UAAU,GAAG,gBAAgB,CAAC,WAAW,CAAC;QAE/C,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAC5E,MAAM,QAAQ,GAAG,GAAG,EAAE;YACpB,IAAI,IAAI,CAAC,kBAAkB,EAAE;gBAC3B,OAAO;aACR;YACD,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAAC,CAAC;YACrC,MAAM,UAAU,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,sCAAsC,CAAC,CAAC;YACxG,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjE,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,sCAAsC,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC5G,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,gCAAgC,EAAE,MAAM,CAAC,CAAC;YACvF,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC3B,CAAC,CAAC;QAEF,MAAM,kBAAkB,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,wBAAwB,EAAE,CAAC,CAAC;QAC3E,kBAAkB,CAAC,WAAW,GAAG,GAAG,EAAE;YACpC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,qCAAqC,CAAC,CAAC;YACjE,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE;gBAC5B,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;aACrC;YACD,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,CAAC,CAAC;QACF,kBAAkB,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QACzD,kBAAkB,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACvC,kBAAkB,CAAC,cAAc,GAAG,CAAC,MAAwB,EAAE,EAAE;YAC/D,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC;QAC/B,CAAC,CAAC;QACF,kBAAkB,CAAC,SAAS,GAAG,CAAC,OAAY,EAAE,EAAE;YAC9C,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,OAAO,MAAK,YAAY,EAAE;gBACrC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,IAAkB,CAAC;aAC9C;iBAAM,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,OAAO,MAAK,cAAc,EAAE;gBAC9C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,QAAqB,CAAC;aACpD;iBAAM;gBACL,sCAAsC;gBACtC,OAAO,CAAC,KAAK,CAAC,4CAA4C,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;aACtF;QACH,CAAC,CAAC;QACF,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAE7C,IAAI,cAA0B,CAAC;QAC/B,IAAI,IAAI,CAAC,OAAO,KAAK,cAAc,CAAC,oBAAoB,IAAI,IAAI,CAAC,wBAAwB,EAAE;YACzF,cAAc,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,yBAAyB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YACjF,cAAc,CAAC,WAAW,GAAG,GAAG,EAAE;gBAChC,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE;oBAC5B,cAAc,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;iBACjC;YACH,CAAC,CAAC;YACF,cAAc,CAAC,QAAQ,GAAG,QAAQ,CAAC;YACnC,cAAc,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;SACtD;aAAM,IAAI,IAAI,CAAC,OAAO,KAAK,cAAc,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,KAAK,cAAc,CAAC,aAAa,EAAE;YAClG,cAAc,GAAG,kBAAkB,CAAC;SACrC;aAAM;YACL,cAAc,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;SAC5C;QACD,MAAM,kBAAkB,GAAG,cAAc,CAAC,cAAc,CAAC;QACzD,cAAc,CAAC,cAAc,GAAG,CAAC,MAAM,EAAE,EAAE;YACzC,kBAAkB,CAAC,MAAM,CAAC,CAAC;YAC3B,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC;QAC3B,CAAC,CAAC;QACF,MAAM,eAAe,GAAG,cAAc,CAAC,WAAW,CAAC;QACnD,cAAc,CAAC,WAAW,GAAG,GAAG,EAAE;YAChC,eAAe,EAAE,CAAC;YAClB,IAAI,IAAI,CAAC,OAAO,EAAE;gBAChB,IAAI,CAAC,GAAG,CACN,WAAW,CAAC,WAAW,EACvB,+BAA+B,cAAc,CAAC,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CACnF,CAAC;aACH;QACH,CAAC,CAAC;QACF,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QAErC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YACjB,IAAI,CAAC,gBAAgB,CACnB,WAAW,CAAC,OAAO,EACnB,8BAA8B,EAC9B,8HAA8H,EAC9H,+DAA+D,EAC/D,uBAAuB,CACxB,CAAC;SACH;IACH,CAAC;IAED,wBAAwB;QACtB,SAAS,cAAc,CAAC,QAAgB;YACtC,0CAA0C;YAC1C,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC1C,GAAG,CAAC,SAAS,GAAG,YAAY,QAAQ,KAAK,CAAC;YAC1C,OAAQ,GAAG,CAAC,UAA8B,CAAC,IAAI,CAAC;QAClD,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,EAAE;YAC1B,OAAO,SAAS,CAAC;SAClB;QACD,MAAM,iBAAiB,GAAG,cAAc,CAAC,IAAI,CAAC,GAAI,CAAC,CAAC;QAEpD,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;YACzF,sCAAsC;YACtC,OAAO,CAAC,KAAK,CAAC,0EAA0E,CAAC,CAAC;YAC1F,OAAO,SAAS,CAAC;SAClB;QACD,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,wBAAwB,CAAC;IAC7E,CAAC;IAED,yBAAyB,CAAC,QAAa;QACrC,MAAM,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC;QAC9B,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QACjE,IAAI,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE;YAClC,+CAA+C;YAC/C,MAAM,mBAAmB,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACzD,OAAO,GAAG,UAAU,MAAM,IAAI,CAAC,wBAAwB,IAAI,mBAAmB,EAAE,CAAC;SAClF;aAAM;YACL,OAAO,GAAG,UAAU,MAAM,QAAQ,IAAI,IAAI,CAAC,wBAAwB,EAAE,CAAC;SACvE;IACH,CAAC;IAED,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,CAAC,WAAW,EAAE,CAAC;QAEnB,+EAA+E;QAC/E,IAAI,CAAC,oBAAoB,GAAG,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACnE,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,oBAAoB,CAAC,CAAC;QACnE,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,oBAAoB,CAAC,CAAC;QACnE,IAAI,CAAC,uBAAuB,EAAE,CAAC;QAE/B,MAAM,UAAU,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,gCAAgC,CAAC,CAAC;QAClG,IAAI,UAAU,EAAE;YACd,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,KAAK,CAChF,CAAC,CAAC,CACH,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC,iBAAiB,CAAC,oBAAoB,QAAQ,EAAE,CAAC,CAAC;YACvD,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,cAAc,CAAC,gCAAgC,CAAC,CAAC;SACnF;QAED,IAAI,CAAC,kBAAkB,GAAG,QAAQ,CAChC,MAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,gBAAgB,CAAC,iCAAiC,CAAC,EACjF,EAAE,CACH,CAAC;QAEF,MAAM,SAAS,GAAG,MAAa,CAAC;QAChC,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,IAAI,EAAE,CAAC;QAC1C,SAAS,CAAC,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAE3E,WAAW,EAAE,CAAC;IAChB,CAAC;IACD,MAAM,CAAC,CAAM;QACX,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtB,CAAC;IACD,WAAW;QACT,0BAA0B;QAC1B,MAAM,KAAK,GAAI,MAAc,CAAC,MAAM,CAAC,aAAsB,CAAC;QAC5D,IAAI,KAAK,EAAE;YACT,KAAK,CAAC,OAAO,CAAC,CAAC,IAAW,EAAE,EAAE;gBAC5B,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YACzE,CAAC,CAAC,CAAC;SACJ;QACD,0DAA0D;QACzD,MAAc,CAAC,MAAM,CAAC,aAAa,GAAG;YACrC,IAAI,EAAE,CAAC,IAAW,EAAE,EAAE;gBACpB,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YACzE,CAAC;SACF,CAAC;IACJ,CAAC;IAED,oBAAoB;QAClB,IAAI,IAAI,CAAC,oBAAoB,EAAE;YAC7B,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,oBAAqB,CAAC,CAAC;YACvE,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,oBAAqB,CAAC,CAAC;SACxE;QACD,KAAK,CAAC,oBAAoB,EAAE,CAAC;IAC/B,CAAC;IAED,cAAc;QACZ,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;QAChG,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC/B,IAAI,IAAI,CAAC,QAAQ,EAAE;YACjB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;SACnB;IACH,CAAC;IAED,iBAAiB,CAAC,GAAuB;QACvC,IAAI,CAAC,aAAa,GAAG,GAAG,CAAC;QACzB,IAAI,IAAI,CAAC,aAAa,EAAE;YACtB,IAAI,IAAI,CAAC,QAAQ,EAAE;gBACjB,IAAI,CAAC,mBAAmB,EAAE,CAAC;aAC5B;iBAAM;gBACL,iFAAiF;gBACjF,UAAU,CAAC,GAAG,EAAE;oBACd,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBAC7B,CAAC,EAAE,cAAc,CAAC,8BAA8B,CAAC,CAAC;aACnD;SACF;IACH,CAAC;IAED,mBAAmB;QACjB,IAAI,IAAI,CAAC,aAAa,EAAE;YACtB,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;SAC/C;QACD,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC;IAED,YAAY,CAAC,WAAoB;QAC/B,IAAI,IAAI,CAAC,kBAAkB,EAAE;YAC3B,IAAI,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC;SACvD;aAAM;YACL,kBAAkB,CAAC,EAAE,OAAO,EAAE,+BAA+B,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;SACxF;IACH,CAAC;IAED,GAAG,CAAC,IAAiB,EAAE,OAAe,EAAE,OAAgB,EAAE,IAAa;QACrE,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC;QAC9B,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YACjB,EAAE;YACF,IAAI;YACJ,OAAO;YACP,OAAO;YACP,IAAI;YACJ,aAAa,EAAE,KAAK;YACpB,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,cAAc,CAAC,YAAY,EAAE;YACzD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;SACvB;QACD,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE;YAC5B,mBAAmB;YACnB,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,mCAAmC,CAAC,CAAC;YACvF,IAAI,IAAI,CAAC,QAAQ,IAAI,WAAW,EAAE;gBAChC,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;gBAC9F,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;aAC3B;iBAAM,IAAI,IAAI,KAAK,WAAW,CAAC,KAAK,EAAE;gBACrC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;aAC1B;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,gBAAgB,CAAC,IAAiB,EAAE,OAAe,EAAE,OAAgB,EAAE,IAAa,EAAE,YAAqB;QACzG,IAAI,YAAY,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,qBAAqB,CAAC,YAAa,CAAC,EAAE;YACtF,0FAA0F;YAC1F,MAAM,4BAA4B,GAAG,IAAI,CAAC,aAAa;iBACpD,MAAM,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,YAAY,KAAK,YAAY,CAAC;iBACpE,MAAM,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;YACnD,IAAI,4BAA4B,CAAC,MAAM,GAAG,CAAC,EAAE;gBAC3C,OAAO;aACR;YACD,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC;YAC9B,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC;YACxB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;gBACtB,EAAE;gBACF,IAAI;gBACJ,OAAO;gBACP,OAAO;gBACP,IAAI;gBACJ,YAAY;gBACZ,aAAa,EAAE,KAAK;gBACpB,OAAO,EAAE,KAAK;aACf,CAAC,CAAC;YACH,2GAA2G;YAC3G,IAAI,IAAI,KAAK,SAAS,EAAE;gBACtB,UAAU,CAAC,GAAG,EAAE;oBACd,IAAI,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;gBAC/B,CAAC,EAAE,cAAc,CAAC,8BAA8B,CAAC,CAAC;aACnD;YACD,IAAI,CAAC,aAAa,EAAE,CAAC;SACtB;aAAM;YACL,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;SACxC;IACH,CAAC;IAED,mBAAmB,CAAC,EAAU;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC;QAC7C,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE;YACtD,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAE/C,sFAAsF;YACtF,IACE,YAAY,CAAC,aAAa;gBAC1B,YAAY,CAAC,YAAY;gBACzB,CAAC,cAAc,CAAC,qBAAqB,CAAC,YAAY,CAAC,YAAY,CAAC,EAChE;gBACA,IAAI,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,cAAc,CAAC,wCAAwC,CAAC,CAAC;gBACrG,SAAS,GAAG,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,IAAI,YAAY,CAAC,YAAY,EAAE,CAAC;gBACzG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,cAAc,CAAC,wCAAwC,EAAE,SAAS,CAAC,CAAC;aACjG;YAED,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;YAC5B,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;YAE3F,mCAAmC;YACnC,UAAU,CAAC,GAAG,EAAE;gBACd,MAAM,GAAG,GAAG,IAAI,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC;gBAC3C,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE;oBACd,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;oBAClC,IAAI,CAAC,aAAa,EAAE,CAAC;iBACtB;YACH,CAAC,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;SAC7B;IACH,CAAC;IAED,qBAAqB,CAAC,EAAU;QAC9B,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC;QACf,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,GAAG,EAAE,EAAE;YAC5C,IAAI,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE;gBAC1B,KAAK,GAAG,GAAG,CAAC;gBACZ,OAAO,IAAI,CAAC;aACb;iBAAM;gBACL,OAAO,KAAK,CAAC;aACd;QACH,CAAC,CAAC,CAAC;QACH,OAAO,KAAK,CAAC;IACf,CAAC;IAED,mBAAmB,CAAC,EAAU;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC;QAC7C,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE;YACtD,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC/C,YAAY,CAAC,aAAa,GAAG,CAAC,YAAY,CAAC,aAAa,CAAC;YACzD,IAAI,CAAC,aAAa,EAAE,CAAC;SACtB;IACH,CAAC;IAED,SAAS,CAAC,GAAY;;QACpB,MAAA,IAAI,CAAC,kBAAkB,0CAAE,SAAS,CAAC,GAAG,CAAC,CAAC;QACxC,MAAA,IAAI,CAAC,cAAc,0CAAE,SAAS,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IACtG,CAAC;IAED,cAAc,CAAC,MAAoC;QACjD,IAAI,MAAM,KAAK,gBAAgB,CAAC,MAAM,EAAE;YACtC,OAAO,GAAG,CAAA,OAAO,cAAc,CAAC,SAAS,GAAG,CAAC;SAC9C;aAAM,IAAI,MAAM,KAAK,gBAAgB,CAAC,QAAQ,EAAE;YAC/C,OAAO,GAAG,CAAA,OAAO,cAAc,CAAC,QAAQ,GAAG,CAAC;SAC7C;aAAM,IAAI,MAAM,KAAK,gBAAgB,CAAC,WAAW,EAAE;YAClD,OAAO,GAAG,CAAA,OAAO,cAAc,CAAC,UAAU,GAAG,CAAC;SAC/C;aAAM,IAAI,MAAM,KAAK,gBAAgB,CAAC,KAAK,EAAE;YAC5C,OAAO,GAAG,CAAA,OAAO,cAAc,CAAC,OAAO,GAAG,CAAC;SAC5C;aAAM;YACL,OAAO,GAAG,CAAA,MAAM,CAAC;SAClB;IACH,CAAC;IAED,0CAA0C;IAC1C,aAAa,CAAC,aAAsB;QAClC,OAAO,IAAI,CAAA;;yBAEU,aAAa,CAAC,IAAI,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,IAAI,aAAa,CAAC,OAAO;YAC1G,aAAa,CAAC,IAAI;YAChB,CAAC,CAAC,aAAa;YACf,CAAC,CAAC,EAAE;;;yCAG2B,aAAa,CAAC,OAAO;kDACZ,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI;cACjF,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA,MAAM,aAAa,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE;cAClE,aAAa,CAAC,IAAI;YAClB,CAAC,CAAC,IAAI,CAAA,8BAA8B,aAAa,CAAC,IAAI,kCAAkC;YACxF,CAAC,CAAC,EAAE;;YAEN,aAAa,CAAC,YAAY;YAC1B,CAAC,CAAC,IAAI,CAAA;iCACe,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK;yBAClD,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,aAAa,CAAC,EAAE,CAAC;;;qBAGpD;YACT,CAAC,CAAC,EAAE;;8CAE8B,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,aAAa,CAAC,EAAE,CAAC;;KAEzF,CAAC;IACJ,CAAC;IAED,wCAAwC;IACxC,MAAM;QACJ,OAAO,IAAI,CAAA;wBACS,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ;;mBAEzC,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,IAAI,CAAC,cAAc,EAAE;;;YAGxE,IAAI,CAAC,IAAI,CAAC,GAAG,CACb,CAAC,GAAG,EAAE,EAAE,CACN,IAAI,CAAA;wBACM,QAAQ,CAAC;YACf,GAAG,EAAE,IAAI;YACT,MAAM,EAAE,IAAI,CAAC,SAAS,KAAK,GAAG,CAAC,EAAE;YACjC,YAAY,EAAE,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC,YAAY;SACpD,CAAC;sBACI,GAAG,CAAC,EAAE;yBACH,GAAG,EAAE;YACZ,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,EAAE,CAAC;YACxB,IAAI,GAAG,CAAC,QAAQ;gBAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,CAAC;;kBAEC,GAAG,CAAC,KAAK;yBACF,CACd;oEACyD,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE;;;;;;;;;;;;;UAarF,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;;;uCAGxD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;;2BAEpE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;iBAChF,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE;;UAElC,IAAI,CAAC,YAAY;YACjB,CAAC,CAAC,IAAI,CAAA;;;;;;;;;;;;;;;;;;;;mBAoBG;YACT,CAAC,CAAC,IAAI,CAAA;;;;;;;;;;;;;;;;mBAgBG;;;;yDAIsC,IAAI,CAAC,cAAc,CAChE,IAAI,CAAC,cAAc,CACpB,SAAS,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC;;UAE9C,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAA,oCAAoC,IAAI,CAAC,aAAa,eAAe,CAAC,CAAC,CAAC,OAAO;aACrG,CAAC;IACZ,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAA,6BAA6B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;IACtG,CAAC;IACD,WAAW;QACT,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE;YAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,mCAAmC,CAAC,CAAC;YACvF,IAAI,WAAW,EAAE;gBACf,WAAW,CAAC,cAAc,EAAE,CAAC;aAC9B;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAA;2CAC4B,IAAI,CAAC,mBAAmB;;cAErD,IAAI,CAAC,UAAU,CAAC,WAAW;cAC3B,IAAI,CAAC,UAAU,CAAC,aAAa;;cAE7B,IAAI,CAAC,UAAU,CAAC,WAAW;;cAE3B,IAAI,CAAC,UAAU,CAAC,WAAW;;cAE3B,IAAI,CAAC,UAAU,CAAC,SAAS;;cAEzB,SAAS,CAAC,SAAS;;;;;;;0BAOP,IAAI,CAAC,kBAAkB;YACnC,CAAC,CAAC,IAAI,CAAC,cAAc,KAAK,gBAAgB,CAAC,WAAW;gBACpD,IAAI,CAAC,cAAc,KAAK,gBAAgB,CAAC,KAAK,CAAC;gBAC/C,CAAC,IAAI,CAAC,UAAU,KAAK,gBAAgB,CAAC,WAAW,IAAI,IAAI,CAAC,UAAU,KAAK,gBAAgB,CAAC,KAAK,CAAC,CAAC;0BACvF,IAAI,CAAC,cAAc,KAAK,gBAAgB,CAAC,MAAM;YAC3D,IAAI,CAAC,UAAU,KAAK,gBAAgB,CAAC,MAAM;wBACjC,CAAC,CAAa,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAE,CAAC,CAAC,MAA2B,CAAC,OAAO,CAAC;;;;;gEAKjC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC;iBACnF,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,cAAc,CAAC,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;;gEAEhD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,cAAc,CAAC;sBAClF,IAAI,CAAC,cAAc;;;WAG9B,CAAC;IACV,CAAC;IAEO,cAAc;QACpB,OAAO,IAAI,CAAA;QACP,IAAI,CAAC,QAAQ,CAAC,GAAG,CACjB,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAA;;;;mCAIU,OAAO,CAAC,EAAE;;yBAEpB,OAAO,CAAC,OAAO;wBAChB,CAAC,CAAa,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,EAAE,OAAO,CAAC;;;cAG/D,OAAO,CAAC,KAAK;;uCAEY,OAAO,CAAC,YAAY;eAC5C,CACR;WACI,CAAC;IACV,CAAC;IAED,mBAAmB;QACjB,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,8BAA8B,CAAC,CAAC;QAC/E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;aAC3B,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,WAAY,CAAC,IAAI,EAAE,CAAC;aAC1F,IAAI,CAAC,EAAE,CAAC;aACR,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,CAAC,CAAC;QACX,IAAI,CAAC,gBAAgB,CACnB,WAAW,CAAC,WAAW,EACvB,6CAA6C,EAC7C,SAAS,EACT,SAAS,EACT,mBAAmB,CACpB,CAAC;IACJ,CAAC;IAED,iBAAiB,CAAC,CAAQ,EAAE,OAAgB;QAC1C,MAAM,OAAO,GAAI,CAAC,CAAC,MAA4B,CAAC,OAAO,CAAC;QACxD,IAAI,IAAI,CAAC,kBAAkB,EAAE;YAC3B,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;YACxD,IAAI,CAAC,gBAAgB,CACnB,WAAW,CAAC,WAAW,EACvB,IAAI,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,EACxD,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC,wCAAwC,CAAC,CAAC,CAAC,SAAS,EACpF,SAAS,EACT,UAAU,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,CAC1D,CAAC;SACH;aAAM;YACL,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE,4BAA4B,OAAO,CAAC,KAAK,kCAAkC,CAAC,CAAC;SAC1G;IACH,CAAC;;AAr2CM,uBAAQ,GAAG,GAAG,CAAA,gBAAgB,CAAC;AAC/B,wBAAS,GAAG,GAAG,CAAA,eAAe,CAAC;AAC/B,uBAAQ,GAAG,GAAG,CAAA,YAAY,CAAC;AAC3B,yBAAU,GAAG,GAAG,CAAA,cAAc,CAAC;AAC/B,sBAAO,GAAG,GAAG,CAAA,gBAAgB,CAAC;AAC9B,2BAAY,GAAG,IAAI,CAAC;AA8qBpB,uDAAwC,GAAG,2CAA2C,CAAC;AACvF,4CAA6B,GAAG,2BAA2B,CAAC;AAC5D,+CAAgC,GAAG,8BAA8B,CAAC;AAClE,qDAAsC,GAAG,mCAAmC,CAAC;AAE7E,6CAA8B,GAAG,IAAI,CAAC;AAEtC,4BAAa,GAAG,eAAe,CAAC;AAChC,qBAAM,GAAG,QAAQ,CAAC;AAClB,mCAAoB,GAAG,sBAAsB,CAAC;AAC9C,mCAAoB,GAA2B;IACpD,aAAa,EAAE,cAAc;IAC7B,MAAM,EAAE,QAAQ;IAChB,oBAAoB,EAAE,sBAAsB;CAC7C,CAAC;AAaF;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;2CACd;AAGb;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;0DAChB;AAG7B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;+CACV;AAGjB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;gEACO;AAGlC;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;gDACpB;AAG1B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;gDACnB;AAGzB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;qDACtB;AAGvB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;qDACd;AAG9B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;sDACmB;AAGhE;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;kDACe;AAG5D;IADC,KAAK,EAAE;4CAKN;AAGF;IADC,KAAK,EAAE;iDAC0B;AAGlC;IADC,KAAK,EAAE;kDAON;AAGF;IADC,KAAK,EAAE;gDACyB;AAGjC;IADC,KAAK,EAAE;oDACqB;AAG7B;IADC,KAAK,CAAC,SAAS,CAAC;4CACU;AAmmB7B,IAAI,cAAc,CAAC,GAAG,CAAC,kBAAkB,CAAC,KAAK,SAAS,EAAE;IACxD,cAAc,CAAC,MAAM,CAAC,kBAAkB,EAAE,cAAc,CAAC,CAAC;CAC3D","sourcesContent":["import { css, html, LitElement, nothing } from 'lit';\nimport { property, query, state } from 'lit/decorators.js';\nimport { classMap } from 'lit/directives/class-map.js';\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nimport { copy } from './copy-to-clipboard.js';\nimport { licenseCheckFailed, licenseCheckNoKey, licenseCheckOk, Product, licenseInit } from './License';\n\ninterface ServerInfo {\n vaadinVersion: string;\n flowVersion: string;\n javaVersion: string;\n osVersion: string;\n productName: string;\n}\n\ninterface Feature {\n id: string;\n title: string;\n moreInfoLink: string;\n requiresServerRestart: boolean;\n enabled: boolean;\n}\n\ninterface Tab {\n id: 'log' | 'info' | 'features';\n title: string;\n render: () => unknown;\n activate?: () => void;\n}\n\nenum ConnectionStatus {\n ACTIVE = 'active',\n INACTIVE = 'inactive',\n UNAVAILABLE = 'unavailable',\n ERROR = 'error'\n}\n\nexport class Connection extends Object {\n static HEARTBEAT_INTERVAL = 180000;\n\n status: ConnectionStatus = ConnectionStatus.UNAVAILABLE;\n webSocket?: WebSocket;\n\n constructor(url?: string) {\n super();\n\n if (url) {\n this.webSocket = new WebSocket(url);\n this.webSocket.onmessage = (msg) => this.handleMessage(msg);\n this.webSocket.onerror = (err) => this.handleError(err);\n this.webSocket.onclose = (_) => {\n if (this.status !== ConnectionStatus.ERROR) {\n this.setStatus(ConnectionStatus.UNAVAILABLE);\n }\n this.webSocket = undefined;\n };\n }\n\n setInterval(() => {\n if (this.webSocket && self.status !== ConnectionStatus.ERROR && this.status !== ConnectionStatus.UNAVAILABLE) {\n this.webSocket.send('');\n }\n }, Connection.HEARTBEAT_INTERVAL);\n }\n\n onHandshake() {\n // Intentionally empty\n }\n\n onReload() {\n // Intentionally empty\n }\n\n onConnectionError(_: string) {\n // Intentionally empty\n }\n\n onStatusChange(_: ConnectionStatus) {\n // Intentionally empty\n }\n\n onMessage(message: any) {\n // eslint-disable-next-line no-console\n console.error('Unknown message received from the live reload server:', message);\n }\n\n handleMessage(msg: any) {\n let json;\n try {\n json = JSON.parse(msg.data);\n } catch (e: any) {\n this.handleError(`[${e.name}: ${e.message}`);\n return;\n }\n if (json.command === 'hello') {\n this.setStatus(ConnectionStatus.ACTIVE);\n this.onHandshake();\n } else if (json.command === 'reload') {\n if (this.status === ConnectionStatus.ACTIVE) {\n this.onReload();\n }\n } else if (json.command === 'license-check-ok') {\n licenseCheckOk(json.data);\n } else if (json.command === 'license-check-failed') {\n licenseCheckFailed(json.data);\n } else if (json.command === 'license-check-nokey') {\n licenseCheckNoKey(json.data);\n } else {\n this.onMessage(json);\n }\n }\n\n handleError(msg: any) {\n // eslint-disable-next-line no-console\n console.error(msg);\n this.setStatus(ConnectionStatus.ERROR);\n if (msg instanceof Event && this.webSocket) {\n this.onConnectionError(`Error in WebSocket connection to ${this.webSocket.url}`);\n } else {\n this.onConnectionError(msg);\n }\n }\n\n setActive(yes: boolean) {\n if (!yes && this.status === ConnectionStatus.ACTIVE) {\n this.setStatus(ConnectionStatus.INACTIVE);\n } else if (yes && this.status === ConnectionStatus.INACTIVE) {\n this.setStatus(ConnectionStatus.ACTIVE);\n }\n }\n\n setStatus(status: ConnectionStatus) {\n if (this.status !== status) {\n this.status = status;\n this.onStatusChange(status);\n }\n }\n\n private send(command: string, data: any) {\n const message = JSON.stringify({ command, data });\n if (!this.webSocket) {\n // eslint-disable-next-line no-console\n console.error(`Unable to send message ${command}. No websocket is available`);\n } else if (this.webSocket.readyState !== WebSocket.OPEN) {\n this.webSocket.addEventListener('open', () => this.webSocket!.send(message));\n } else {\n this.webSocket.send(message);\n }\n }\n\n setFeature(featureId: string, enabled: boolean) {\n this.send('setFeature', { featureId, enabled });\n }\n sendTelemetry(browserData: any) {\n this.send('reportTelemetry', { browserData });\n }\n sendLicenseCheck(product: Product) {\n this.send('checkLicense', product);\n }\n}\n\nenum MessageType {\n LOG = 'log',\n INFORMATION = 'information',\n WARNING = 'warning',\n ERROR = 'error'\n}\n\ninterface Message {\n id: number;\n type: MessageType;\n message: string;\n details?: string;\n link?: string;\n persistentId?: string;\n dontShowAgain: boolean;\n deleted: boolean;\n}\n\nexport class VaadinDevTools extends LitElement {\n static BLUE_HSL = css`206, 100%, 70%`;\n static GREEN_HSL = css`145, 80%, 42%`;\n static GREY_HSL = css`0, 0%, 50%`;\n static YELLOW_HSL = css`38, 98%, 64%`;\n static RED_HSL = css`355, 100%, 68%`;\n static MAX_LOG_ROWS = 1000;\n\n static get styles() {\n return css`\n :host {\n --dev-tools-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,\n 'Helvetica Neue', sans-serif;\n --dev-tools-font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',\n monospace;\n\n --dev-tools-font-size: 0.8125rem;\n --dev-tools-font-size-small: 0.75rem;\n\n --dev-tools-text-color: rgba(255, 255, 255, 0.8);\n --dev-tools-text-color-secondary: rgba(255, 255, 255, 0.65);\n --dev-tools-text-color-emphasis: rgba(255, 255, 255, 0.95);\n --dev-tools-text-color-active: rgba(255, 255, 255, 1);\n\n --dev-tools-background-color-inactive: rgba(45, 45, 45, 0.25);\n --dev-tools-background-color-active: rgba(45, 45, 45, 0.98);\n --dev-tools-background-color-active-blurred: rgba(45, 45, 45, 0.85);\n\n --dev-tools-border-radius: 0.5rem;\n --dev-tools-box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05), 0 4px 12px -2px rgba(0, 0, 0, 0.4);\n\n --dev-tools-blue-hsl: ${this.BLUE_HSL};\n --dev-tools-blue-color: hsl(var(--dev-tools-blue-hsl));\n --dev-tools-green-hsl: ${this.GREEN_HSL};\n --dev-tools-green-color: hsl(var(--dev-tools-green-hsl));\n --dev-tools-grey-hsl: ${this.GREY_HSL};\n --dev-tools-grey-color: hsl(var(--dev-tools-grey-hsl));\n --dev-tools-yellow-hsl: ${this.YELLOW_HSL};\n --dev-tools-yellow-color: hsl(var(--dev-tools-yellow-hsl));\n --dev-tools-red-hsl: ${this.RED_HSL};\n --dev-tools-red-color: hsl(var(--dev-tools-red-hsl));\n\n /* Needs to be in ms, used in JavaScript as well */\n --dev-tools-transition-duration: 180ms;\n\n all: initial;\n\n direction: ltr;\n cursor: default;\n font: normal 400 var(--dev-tools-font-size) / 1.125rem var(--dev-tools-font-family);\n color: var(--dev-tools-text-color);\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n\n position: fixed;\n z-index: 20000;\n pointer-events: none;\n bottom: 0;\n right: 0;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column-reverse;\n align-items: flex-end;\n }\n\n .dev-tools {\n pointer-events: auto;\n display: flex;\n align-items: center;\n position: fixed;\n z-index: inherit;\n right: 0.5rem;\n bottom: 0.5rem;\n min-width: 1.75rem;\n height: 1.75rem;\n max-width: 1.75rem;\n border-radius: 0.5rem;\n padding: 0.375rem;\n box-sizing: border-box;\n background-color: var(--dev-tools-background-color-inactive);\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05);\n color: var(--dev-tools-text-color);\n transition: var(--dev-tools-transition-duration);\n white-space: nowrap;\n line-height: 1rem;\n }\n\n .dev-tools:hover,\n .dev-tools.active {\n background-color: var(--dev-tools-background-color-active);\n box-shadow: var(--dev-tools-box-shadow);\n }\n\n .dev-tools.active {\n max-width: calc(100% - 1rem);\n }\n\n .dev-tools .dev-tools-icon {\n flex: none;\n pointer-events: none;\n display: inline-block;\n width: 1rem;\n height: 1rem;\n fill: #fff;\n transition: var(--dev-tools-transition-duration);\n margin: 0;\n }\n\n .dev-tools.active .dev-tools-icon {\n opacity: 0;\n position: absolute;\n transform: scale(0.5);\n }\n\n .dev-tools .status-blip {\n flex: none;\n display: block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n z-index: 20001;\n background: var(--dev-tools-grey-color);\n position: absolute;\n top: -1px;\n right: -1px;\n }\n\n .dev-tools .status-description {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0 0.25rem;\n }\n\n .dev-tools.error {\n background-color: hsla(var(--dev-tools-red-hsl), 0.15);\n animation: bounce 0.5s;\n animation-iteration-count: 2;\n }\n\n .switch {\n display: inline-flex;\n align-items: center;\n }\n\n .switch input {\n opacity: 0;\n width: 0;\n height: 0;\n position: absolute;\n }\n\n .switch .slider {\n display: block;\n flex: none;\n width: 28px;\n height: 18px;\n border-radius: 9px;\n background-color: rgba(255, 255, 255, 0.3);\n transition: var(--dev-tools-transition-duration);\n margin-right: 0.5rem;\n }\n\n .switch:focus-within .slider,\n .switch .slider:hover {\n background-color: rgba(255, 255, 255, 0.35);\n transition: none;\n }\n\n .switch input:focus-visible ~ .slider {\n box-shadow: 0 0 0 2px var(--dev-tools-background-color-active), 0 0 0 4px var(--dev-tools-blue-color);\n }\n\n .switch .slider::before {\n content: '';\n display: block;\n margin: 2px;\n width: 14px;\n height: 14px;\n background-color: #fff;\n transition: var(--dev-tools-transition-duration);\n border-radius: 50%;\n }\n\n .switch input:checked + .slider {\n background-color: var(--dev-tools-green-color);\n }\n\n .switch input:checked + .slider::before {\n transform: translateX(10px);\n }\n\n .switch input:disabled + .slider::before {\n background-color: var(--dev-tools-grey-color);\n }\n\n .window.hidden {\n opacity: 0;\n transform: scale(0);\n position: absolute;\n }\n\n .window.visible {\n transform: none;\n opacity: 1;\n pointer-events: auto;\n }\n\n .window.visible ~ .dev-tools {\n opacity: 0;\n pointer-events: none;\n }\n\n .window.visible ~ .dev-tools .dev-tools-icon,\n .window.visible ~ .dev-tools .status-blip {\n transition: none;\n opacity: 0;\n }\n\n .window {\n border-radius: var(--dev-tools-border-radius);\n overflow: hidden;\n margin: 0.5rem;\n width: 30rem;\n max-width: calc(100% - 1rem);\n max-height: calc(100vh - 1rem);\n flex-shrink: 1;\n background-color: var(--dev-tools-background-color-active);\n color: var(--dev-tools-text-color);\n transition: var(--dev-tools-transition-duration);\n transform-origin: bottom right;\n display: flex;\n flex-direction: column;\n box-shadow: var(--dev-tools-box-shadow);\n outline: none;\n }\n\n .window-toolbar {\n display: flex;\n flex: none;\n align-items: center;\n padding: 0.375rem;\n white-space: nowrap;\n order: 1;\n background-color: rgba(0, 0, 0, 0.2);\n gap: 0.5rem;\n }\n\n .tab {\n color: var(--dev-tools-text-color-secondary);\n font: inherit;\n font-size: var(--dev-tools-font-size-small);\n font-weight: 500;\n line-height: 1;\n padding: 0.25rem 0.375rem;\n background: none;\n border: none;\n margin: 0;\n border-radius: 0.25rem;\n transition: var(--dev-tools-transition-duration);\n }\n\n .tab:hover,\n .tab.active {\n color: var(--dev-tools-text-color-active);\n }\n\n .tab.active {\n background-color: rgba(255, 255, 255, 0.12);\n }\n\n .tab.unreadErrors::after {\n content: '•';\n color: hsl(var(--dev-tools-red-hsl));\n font-size: 1.5rem;\n position: absolute;\n transform: translate(0, -50%);\n }\n\n .ahreflike {\n font-weight: 500;\n color: var(--dev-tools-text-color-secondary);\n text-decoration: underline;\n cursor: pointer;\n }\n\n .ahreflike:hover {\n color: var(--dev-tools-text-color-emphasis);\n }\n\n .button {\n all: initial;\n font-family: inherit;\n font-size: var(--dev-tools-font-size-small);\n line-height: 1;\n white-space: nowrap;\n background-color: rgba(0, 0, 0, 0.2);\n color: inherit;\n font-weight: 600;\n padding: 0.25rem 0.375rem;\n border-radius: 0.25rem;\n }\n\n .button:focus,\n .button:hover {\n color: var(--dev-tools-text-color-emphasis);\n }\n\n .minimize-button {\n flex: none;\n width: 1rem;\n height: 1rem;\n color: inherit;\n background-color: transparent;\n border: 0;\n padding: 0;\n margin: 0 0 0 auto;\n opacity: 0.8;\n }\n\n .minimize-button:hover {\n opacity: 1;\n }\n\n .minimize-button svg {\n max-width: 100%;\n }\n\n .message.information {\n --dev-tools-notification-color: var(--dev-tools-blue-color);\n }\n\n .message.warning {\n --dev-tools-notification-color: var(--dev-tools-yellow-color);\n }\n\n .message.error {\n --dev-tools-notification-color: var(--dev-tools-red-color);\n }\n\n .message {\n display: flex;\n padding: 0.1875rem 0.75rem 0.1875rem 2rem;\n background-clip: padding-box;\n }\n\n .message.log {\n padding-left: 0.75rem;\n }\n\n .message-content {\n margin-right: 0.5rem;\n -webkit-user-select: text;\n -moz-user-select: text;\n user-select: text;\n }\n\n .message-heading {\n position: relative;\n display: flex;\n align-items: center;\n margin: 0.125rem 0;\n }\n\n .message.log {\n color: var(--dev-tools-text-color-secondary);\n }\n\n .message:not(.log) .message-heading {\n font-weight: 500;\n }\n\n .message.has-details .message-heading {\n color: var(--dev-tools-text-color-emphasis);\n font-weight: 600;\n }\n\n .message-heading::before {\n position: absolute;\n margin-left: -1.5rem;\n display: inline-block;\n text-align: center;\n font-size: 0.875em;\n font-weight: 600;\n line-height: calc(1.25em - 2px);\n width: 14px;\n height: 14px;\n box-sizing: border-box;\n border: 1px solid transparent;\n border-radius: 50%;\n }\n\n .message.information .message-heading::before {\n content: 'i';\n border-color: currentColor;\n color: var(--dev-tools-notification-color);\n }\n\n .message.warning .message-heading::before,\n .message.error .message-heading::before {\n content: '!';\n color: var(--dev-tools-background-color-active);\n background-color: var(--dev-tools-notification-color);\n }\n\n .features-tray {\n padding: 0.75rem;\n flex: auto;\n overflow: auto;\n animation: fade-in var(--dev-tools-transition-duration) ease-in;\n user-select: text;\n }\n\n .features-tray p {\n margin-top: 0;\n color: var(--dev-tools-text-color-secondary);\n }\n\n .features-tray .feature {\n display: flex;\n align-items: center;\n gap: 1rem;\n padding-bottom: 0.5em;\n }\n\n .message .message-details {\n font-weight: 400;\n color: var(--dev-tools-text-color-secondary);\n margin: 0.25rem 0;\n }\n\n .message .message-details[hidden] {\n display: none;\n }\n\n .message .message-details p {\n display: inline;\n margin: 0;\n margin-right: 0.375em;\n word-break: break-word;\n }\n\n .message .persist {\n color: var(--dev-tools-text-color-secondary);\n white-space: nowrap;\n margin: 0.375rem 0;\n display: flex;\n align-items: center;\n position: relative;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n }\n\n .message .persist::before {\n content: '';\n width: 1em;\n height: 1em;\n border-radius: 0.2em;\n margin-right: 0.375em;\n background-color: rgba(255, 255, 255, 0.3);\n }\n\n .message .persist:hover::before {\n background-color: rgba(255, 255, 255, 0.4);\n }\n\n .message .persist.on::before {\n background-color: rgba(255, 255, 255, 0.9);\n }\n\n .message .persist.on::after {\n content: '';\n order: -1;\n position: absolute;\n width: 0.75em;\n height: 0.25em;\n border: 2px solid var(--dev-tools-background-color-active);\n border-width: 0 0 2px 2px;\n transform: translate(0.05em, -0.05em) rotate(-45deg) scale(0.8, 0.9);\n }\n\n .message .dismiss-message {\n font-weight: 600;\n align-self: stretch;\n display: flex;\n align-items: center;\n padding: 0 0.25rem;\n margin-left: 0.5rem;\n color: var(--dev-tools-text-color-secondary);\n }\n\n .message .dismiss-message:hover {\n color: var(--dev-tools-text-color);\n }\n\n .notification-tray {\n display: flex;\n flex-direction: column-reverse;\n align-items: flex-end;\n margin: 0.5rem;\n flex: none;\n }\n\n .window.hidden + .notification-tray {\n margin-bottom: 3rem;\n }\n\n .notification-tray .message {\n pointer-events: auto;\n background-color: var(--dev-tools-background-color-active);\n color: var(--dev-tools-text-color);\n max-width: 30rem;\n box-sizing: border-box;\n border-radius: var(--dev-tools-border-radius);\n margin-top: 0.5rem;\n transition: var(--dev-tools-transition-duration);\n transform-origin: bottom right;\n animation: slideIn var(--dev-tools-transition-duration);\n box-shadow: var(--dev-tools-box-shadow);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n }\n\n .notification-tray .message.animate-out {\n animation: slideOut forwards var(--dev-tools-transition-duration);\n }\n\n .notification-tray .message .message-details {\n max-height: 10em;\n overflow: hidden;\n }\n\n .message-tray {\n flex: auto;\n overflow: auto;\n max-height: 20rem;\n user-select: text;\n }\n\n .message-tray .message {\n animation: fade-in var(--dev-tools-transition-duration) ease-in;\n padding-left: 2.25rem;\n }\n\n .message-tray .message.warning {\n background-color: hsla(var(--dev-tools-yellow-hsl), 0.09);\n }\n\n .message-tray .message.error {\n background-color: hsla(var(--dev-tools-red-hsl), 0.09);\n }\n\n .message-tray .message.error .message-heading {\n color: hsl(var(--dev-tools-red-hsl));\n }\n\n .message-tray .message.warning .message-heading {\n color: hsl(var(--dev-tools-yellow-hsl));\n }\n\n .message-tray .message + .message {\n border-top: 1px solid rgba(255, 255, 255, 0.07);\n }\n\n .message-tray .dismiss-message,\n .message-tray .persist {\n display: none;\n }\n\n .info-tray {\n padding: 0.75rem;\n position: relative;\n flex: auto;\n overflow: auto;\n animation: fade-in var(--dev-tools-transition-duration) ease-in;\n user-select: text;\n }\n\n .info-tray dl {\n margin: 0;\n display: grid;\n grid-template-columns: max-content 1fr;\n column-gap: 0.75rem;\n position: relative;\n }\n\n .info-tray dt {\n grid-column: 1;\n color: var(--dev-tools-text-color-emphasis);\n }\n\n .info-tray dt:not(:first-child)::before {\n content: '';\n width: 100%;\n position: absolute;\n height: 1px;\n background-color: rgba(255, 255, 255, 0.1);\n margin-top: -0.375rem;\n }\n\n .info-tray dd {\n grid-column: 2;\n margin: 0;\n }\n\n .info-tray :is(dt, dd):not(:last-child) {\n margin-bottom: 0.75rem;\n }\n\n .info-tray dd + dd {\n margin-top: -0.5rem;\n }\n\n .info-tray .live-reload-status::before {\n content: '•';\n color: var(--status-color);\n width: 0.75rem;\n display: inline-block;\n font-size: 1rem;\n line-height: 0.5rem;\n }\n\n .info-tray .copy {\n position: fixed;\n z-index: 1;\n top: 0.5rem;\n right: 0.5rem;\n }\n\n .info-tray .switch {\n vertical-align: -4px;\n }\n\n @keyframes slideIn {\n from {\n transform: translateX(100%);\n opacity: 0;\n }\n to {\n transform: translateX(0%);\n opacity: 1;\n }\n }\n\n @keyframes slideOut {\n from {\n transform: translateX(0%);\n opacity: 1;\n }\n to {\n transform: translateX(100%);\n opacity: 0;\n }\n }\n\n @keyframes fade-in {\n 0% {\n opacity: 0;\n }\n }\n\n @keyframes bounce {\n 0% {\n transform: scale(0.8);\n }\n 50% {\n transform: scale(1.5);\n background-color: hsla(var(--dev-tools-red-hsl), 1);\n }\n 100% {\n transform: scale(1);\n }\n }\n\n @supports (backdrop-filter: blur(1px)) {\n .dev-tools,\n .window,\n .notification-tray .message {\n backdrop-filter: blur(8px);\n }\n .dev-tools:hover,\n .dev-tools.active,\n .window,\n .notification-tray .message {\n background-color: var(--dev-tools-background-color-active-blurred);\n }\n }\n `;\n }\n\n static DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE = 'vaadin.live-reload.dismissedNotifications';\n static ACTIVE_KEY_IN_SESSION_STORAGE = 'vaadin.live-reload.active';\n static TRIGGERED_KEY_IN_SESSION_STORAGE = 'vaadin.live-reload.triggered';\n static TRIGGERED_COUNT_KEY_IN_SESSION_STORAGE = 'vaadin.live-reload.triggeredCount';\n\n static AUTO_DEMOTE_NOTIFICATION_DELAY = 5000;\n\n static HOTSWAP_AGENT = 'HOTSWAP_AGENT';\n static JREBEL = 'JREBEL';\n static SPRING_BOOT_DEVTOOLS = 'SPRING_BOOT_DEVTOOLS';\n static BACKEND_DISPLAY_NAME: Record = {\n HOTSWAP_AGENT: 'HotswapAgent',\n JREBEL: 'JRebel',\n SPRING_BOOT_DEVTOOLS: 'Spring Boot Devtools'\n };\n\n static get isActive() {\n const active = window.sessionStorage.getItem(VaadinDevTools.ACTIVE_KEY_IN_SESSION_STORAGE);\n return active === null || active !== 'false';\n }\n\n static notificationDismissed(persistentId: string) {\n const shown = window.localStorage.getItem(VaadinDevTools.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE);\n return shown !== null && shown.includes(persistentId);\n }\n\n @property({ type: String })\n url?: string;\n\n @property({ type: Boolean, attribute: true })\n liveReloadDisabled?: boolean;\n\n @property({ type: String })\n backend?: string;\n\n @property({ type: Number })\n springBootLiveReloadPort?: number;\n\n @property({ type: Boolean, attribute: false })\n expanded: boolean = false;\n\n @property({ type: Array, attribute: false })\n messages: Message[] = [];\n\n @property({ type: String, attribute: false })\n splashMessage?: string;\n\n @property({ type: Array, attribute: false })\n notifications: Message[] = [];\n\n @property({ type: String, attribute: false })\n frontendStatus: ConnectionStatus = ConnectionStatus.UNAVAILABLE;\n\n @property({ type: String, attribute: false })\n javaStatus: ConnectionStatus = ConnectionStatus.UNAVAILABLE;\n\n @state()\n private tabs: readonly Tab[] = [\n { id: 'log', title: 'Log', render: this.renderLog, activate: this.activateLog },\n { id: 'info', title: 'Info', render: this.renderInfo },\n { id: 'features', title: 'Feature Flags', render: this.renderFeatures }\n ];\n\n @state()\n private activeTab: string = 'log';\n\n @state()\n private serverInfo: ServerInfo = {\n flowVersion: '',\n vaadinVersion: '',\n javaVersion: '',\n osVersion: '',\n productName: ''\n };\n\n @state()\n private features: Feature[] = [];\n\n @state()\n private unreadErrors = false;\n\n @query('.window')\n private root!: HTMLElement;\n\n private javaConnection?: Connection;\n private frontendConnection?: Connection;\n\n private nextMessageId: number = 1;\n\n private disableEventListener?: EventListener;\n\n private transitionDuration: number = 0;\n\n elementTelemetry() {\n let data = {};\n try {\n // localstorage data is collected by vaadin-usage-statistics.js\n const localStorageStatsString = localStorage.getItem('vaadin.statistics.basket');\n if (!localStorageStatsString) {\n // Do not send empty data\n return;\n }\n data = JSON.parse(localStorageStatsString);\n } catch (e) {\n // In case of parse errors don't send anything\n return;\n }\n\n if (this.frontendConnection) {\n this.frontendConnection.sendTelemetry(data);\n }\n }\n\n openWebSocketConnection() {\n this.frontendStatus = ConnectionStatus.UNAVAILABLE;\n this.javaStatus = ConnectionStatus.UNAVAILABLE;\n\n const onConnectionError = (msg: string) => this.log(MessageType.ERROR, msg);\n const onReload = () => {\n if (this.liveReloadDisabled) {\n return;\n }\n this.showSplashMessage('Reloading…');\n const lastReload = window.sessionStorage.getItem(VaadinDevTools.TRIGGERED_COUNT_KEY_IN_SESSION_STORAGE);\n const nextReload = lastReload ? parseInt(lastReload, 10) + 1 : 1;\n window.sessionStorage.setItem(VaadinDevTools.TRIGGERED_COUNT_KEY_IN_SESSION_STORAGE, nextReload.toString());\n window.sessionStorage.setItem(VaadinDevTools.TRIGGERED_KEY_IN_SESSION_STORAGE, 'true');\n window.location.reload();\n };\n\n const frontendConnection = new Connection(this.getDedicatedWebSocketUrl());\n frontendConnection.onHandshake = () => {\n this.log(MessageType.LOG, 'Vaadin development mode initialized');\n if (!VaadinDevTools.isActive) {\n frontendConnection.setActive(false);\n }\n this.elementTelemetry();\n };\n frontendConnection.onConnectionError = onConnectionError;\n frontendConnection.onReload = onReload;\n frontendConnection.onStatusChange = (status: ConnectionStatus) => {\n this.frontendStatus = status;\n };\n frontendConnection.onMessage = (message: any) => {\n if (message?.command === 'serverInfo') {\n this.serverInfo = message.data as ServerInfo;\n } else if (message?.command === 'featureFlags') {\n this.features = message.data.features as Feature[];\n } else {\n // eslint-disable-next-line no-console\n console.error('Unknown message from front-end connection:', JSON.stringify(message));\n }\n };\n this.frontendConnection = frontendConnection;\n\n let javaConnection: Connection;\n if (this.backend === VaadinDevTools.SPRING_BOOT_DEVTOOLS && this.springBootLiveReloadPort) {\n javaConnection = new Connection(this.getSpringBootWebSocketUrl(window.location));\n javaConnection.onHandshake = () => {\n if (!VaadinDevTools.isActive) {\n javaConnection.setActive(false);\n }\n };\n javaConnection.onReload = onReload;\n javaConnection.onConnectionError = onConnectionError;\n } else if (this.backend === VaadinDevTools.JREBEL || this.backend === VaadinDevTools.HOTSWAP_AGENT) {\n javaConnection = frontendConnection;\n } else {\n javaConnection = new Connection(undefined);\n }\n const prevOnStatusChange = javaConnection.onStatusChange;\n javaConnection.onStatusChange = (status) => {\n prevOnStatusChange(status);\n this.javaStatus = status;\n };\n const prevOnHandshake = javaConnection.onHandshake;\n javaConnection.onHandshake = () => {\n prevOnHandshake();\n if (this.backend) {\n this.log(\n MessageType.INFORMATION,\n `Java live reload available: ${VaadinDevTools.BACKEND_DISPLAY_NAME[this.backend]}`\n );\n }\n };\n this.javaConnection = javaConnection;\n\n if (!this.backend) {\n this.showNotification(\n MessageType.WARNING,\n 'Java live reload unavailable',\n 'Live reload for Java changes is currently not set up. Find out how to make use of this functionality to boost your workflow.',\n 'https://vaadin.com/docs/latest/flow/configuration/live-reload',\n 'liveReloadUnavailable'\n );\n }\n }\n\n getDedicatedWebSocketUrl(): string | undefined {\n function getAbsoluteUrl(relative: string) {\n // Use innerHTML to obtain an absolute URL\n const div = document.createElement('div');\n div.innerHTML = ``;\n return (div.firstChild as HTMLLinkElement).href;\n }\n if (this.url === undefined) {\n return undefined;\n }\n const connectionBaseUrl = getAbsoluteUrl(this.url!);\n\n if (!connectionBaseUrl.startsWith('http://') && !connectionBaseUrl.startsWith('https://')) {\n // eslint-disable-next-line no-console\n console.error('The protocol of the url should be http or https for live reload to work.');\n return undefined;\n }\n return `${connectionBaseUrl.replace(/^http/, 'ws')}?v-r=push&debug_window`;\n }\n\n getSpringBootWebSocketUrl(location: any) {\n const { hostname } = location;\n const wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';\n if (hostname.endsWith('gitpod.io')) {\n // Gitpod uses `port-url` instead of `url:port`\n const hostnameWithoutPort = hostname.replace(/.*?-/, '');\n return `${wsProtocol}://${this.springBootLiveReloadPort}-${hostnameWithoutPort}`;\n } else {\n return `${wsProtocol}://${hostname}:${this.springBootLiveReloadPort}`;\n }\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.catchErrors();\n\n // when focus or clicking anywhere, move the splash message to the message tray\n this.disableEventListener = (_: any) => this.demoteSplashMessage();\n document.body.addEventListener('focus', this.disableEventListener);\n document.body.addEventListener('click', this.disableEventListener);\n this.openWebSocketConnection();\n\n const lastReload = window.sessionStorage.getItem(VaadinDevTools.TRIGGERED_KEY_IN_SESSION_STORAGE);\n if (lastReload) {\n const now = new Date();\n const reloaded = `${`0${now.getHours()}`.slice(-2)}:${`0${now.getMinutes()}`.slice(\n -2\n )}:${`0${now.getSeconds()}`.slice(-2)}`;\n this.showSplashMessage(`Page reloaded at ${reloaded}`);\n window.sessionStorage.removeItem(VaadinDevTools.TRIGGERED_KEY_IN_SESSION_STORAGE);\n }\n\n this.transitionDuration = parseInt(\n window.getComputedStyle(this).getPropertyValue('--dev-tools-transition-duration'),\n 10\n );\n\n const windowAny = window as any;\n windowAny.Vaadin = windowAny.Vaadin || {};\n windowAny.Vaadin.devTools = Object.assign(this, windowAny.Vaadin.devTools);\n\n licenseInit();\n }\n format(o: any): string {\n return o.toString();\n }\n catchErrors() {\n // Process stored messages\n const queue = (window as any).Vaadin.ConsoleErrors as any[];\n if (queue) {\n queue.forEach((args: any[]) => {\n this.log(MessageType.ERROR, args.map((o) => this.format(o)).join(' '));\n });\n }\n // Install new handler that immediately processes messages\n (window as any).Vaadin.ConsoleErrors = {\n push: (args: any[]) => {\n this.log(MessageType.ERROR, args.map((o) => this.format(o)).join(' '));\n }\n };\n }\n\n disconnectedCallback() {\n if (this.disableEventListener) {\n document.body.removeEventListener('focus', this.disableEventListener!);\n document.body.removeEventListener('click', this.disableEventListener!);\n }\n super.disconnectedCallback();\n }\n\n toggleExpanded() {\n this.notifications.slice().forEach((notification) => this.dismissNotification(notification.id));\n this.expanded = !this.expanded;\n if (this.expanded) {\n this.root.focus();\n }\n }\n\n showSplashMessage(msg: string | undefined) {\n this.splashMessage = msg;\n if (this.splashMessage) {\n if (this.expanded) {\n this.demoteSplashMessage();\n } else {\n // automatically move notification to message tray after a certain amount of time\n setTimeout(() => {\n this.demoteSplashMessage();\n }, VaadinDevTools.AUTO_DEMOTE_NOTIFICATION_DELAY);\n }\n }\n }\n\n demoteSplashMessage() {\n if (this.splashMessage) {\n this.log(MessageType.LOG, this.splashMessage);\n }\n this.showSplashMessage(undefined);\n }\n\n checkLicense(productInfo: Product) {\n if (this.frontendConnection) {\n this.frontendConnection.sendLicenseCheck(productInfo);\n } else {\n licenseCheckFailed({ message: 'Internal error: no connection', product: productInfo });\n }\n }\n\n log(type: MessageType, message: string, details?: string, link?: string) {\n const id = this.nextMessageId;\n this.nextMessageId += 1;\n this.messages.push({\n id,\n type,\n message,\n details,\n link,\n dontShowAgain: false,\n deleted: false\n });\n while (this.messages.length > VaadinDevTools.MAX_LOG_ROWS) {\n this.messages.shift();\n }\n this.requestUpdate();\n this.updateComplete.then(() => {\n // Scroll into view\n const lastMessage = this.renderRoot.querySelector('.message-tray .message:last-child');\n if (this.expanded && lastMessage) {\n setTimeout(() => lastMessage.scrollIntoView({ behavior: 'smooth' }), this.transitionDuration);\n this.unreadErrors = false;\n } else if (type === MessageType.ERROR) {\n this.unreadErrors = true;\n }\n });\n }\n\n showNotification(type: MessageType, message: string, details?: string, link?: string, persistentId?: string) {\n if (persistentId === undefined || !VaadinDevTools.notificationDismissed(persistentId!)) {\n // Do not open persistent message if another is already visible with the same persistentId\n const matchingVisibleNotifications = this.notifications\n .filter((notification) => notification.persistentId === persistentId)\n .filter((notification) => !notification.deleted);\n if (matchingVisibleNotifications.length > 0) {\n return;\n }\n const id = this.nextMessageId;\n this.nextMessageId += 1;\n this.notifications.push({\n id,\n type,\n message,\n details,\n link,\n persistentId,\n dontShowAgain: false,\n deleted: false\n });\n // automatically move notification to message tray after a certain amount of time unless it contains a link\n if (link === undefined) {\n setTimeout(() => {\n this.dismissNotification(id);\n }, VaadinDevTools.AUTO_DEMOTE_NOTIFICATION_DELAY);\n }\n this.requestUpdate();\n } else {\n this.log(type, message, details, link);\n }\n }\n\n dismissNotification(id: number) {\n const index = this.findNotificationIndex(id);\n if (index !== -1 && !this.notifications[index].deleted) {\n const notification = this.notifications[index];\n\n // user is explicitly dismissing a notification---after that we won't bug them with it\n if (\n notification.dontShowAgain &&\n notification.persistentId &&\n !VaadinDevTools.notificationDismissed(notification.persistentId)\n ) {\n let dismissed = window.localStorage.getItem(VaadinDevTools.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE);\n dismissed = dismissed === null ? notification.persistentId : `${dismissed},${notification.persistentId}`;\n window.localStorage.setItem(VaadinDevTools.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE, dismissed);\n }\n\n notification.deleted = true;\n this.log(notification.type, notification.message, notification.details, notification.link);\n\n // give some time for the animation\n setTimeout(() => {\n const idx = this.findNotificationIndex(id);\n if (idx !== -1) {\n this.notifications.splice(idx, 1);\n this.requestUpdate();\n }\n }, this.transitionDuration);\n }\n }\n\n findNotificationIndex(id: number): number {\n let index = -1;\n this.notifications.some((notification, idx) => {\n if (notification.id === id) {\n index = idx;\n return true;\n } else {\n return false;\n }\n });\n return index;\n }\n\n toggleDontShowAgain(id: number) {\n const index = this.findNotificationIndex(id);\n if (index !== -1 && !this.notifications[index].deleted) {\n const notification = this.notifications[index];\n notification.dontShowAgain = !notification.dontShowAgain;\n this.requestUpdate();\n }\n }\n\n setActive(yes: boolean) {\n this.frontendConnection?.setActive(yes);\n this.javaConnection?.setActive(yes);\n window.sessionStorage.setItem(VaadinDevTools.ACTIVE_KEY_IN_SESSION_STORAGE, yes ? 'true' : 'false');\n }\n\n getStatusColor(status: ConnectionStatus | undefined) {\n if (status === ConnectionStatus.ACTIVE) {\n return css`hsl(${VaadinDevTools.GREEN_HSL})`;\n } else if (status === ConnectionStatus.INACTIVE) {\n return css`hsl(${VaadinDevTools.GREY_HSL})`;\n } else if (status === ConnectionStatus.UNAVAILABLE) {\n return css`hsl(${VaadinDevTools.YELLOW_HSL})`;\n } else if (status === ConnectionStatus.ERROR) {\n return css`hsl(${VaadinDevTools.RED_HSL})`;\n } else {\n return css`none`;\n }\n }\n\n /* eslint-disable lit/no-template-arrow */\n renderMessage(messageObject: Message) {\n return html`\n \n
\n
${messageObject.message}
\n
\n ${messageObject.persistentId\n ? html` this.toggleDontShowAgain(messageObject.id)}\n >\n Don’t show again\n
`\n : ''}\n \n
this.dismissNotification(messageObject.id)}>Dismiss
\n \n `;\n }\n\n /* eslint-disable lit/no-template-map */\n render() {\n return html` e.key === 'Escape' && this.toggleExpanded()}\n >\n
\n ${this.tabs.map(\n (tab) =>\n html` {\n this.activeTab = tab.id;\n if (tab.activate) tab.activate.call(this);\n }}\n >\n ${tab.title}\n `\n )}\n \n
\n ${this.tabs.map((tab) => (this.activeTab === tab.id ? tab.render.call(this) : nothing))}\n \n\n
${this.notifications.map((msg) => this.renderMessage(msg))}
\n this.toggleExpanded()}\n >\n ${this.unreadErrors\n ? html`\n \n \n \n \n \n `\n : html`\n \n \n \n \n `}\n\n \n ${this.splashMessage ? html`${this.splashMessage}` : nothing}\n `;\n }\n\n renderLog() {\n return html`
${this.messages.map((msg) => this.renderMessage(msg))}
`;\n }\n activateLog() {\n this.unreadErrors = false;\n this.updateComplete.then(() => {\n const lastMessage = this.renderRoot.querySelector('.message-tray .message:last-child');\n if (lastMessage) {\n lastMessage.scrollIntoView();\n }\n });\n }\n\n renderInfo() {\n return html`
\n \n
\n
${this.serverInfo.productName}
\n
${this.serverInfo.vaadinVersion}
\n
Flow
\n
${this.serverInfo.flowVersion}
\n
Java
\n
${this.serverInfo.javaVersion}
\n
OS
\n
${this.serverInfo.osVersion}
\n
Browser
\n
${navigator.userAgent}
\n
\n Live reload\n \n
\n
\n Java ${this.javaStatus} ${this.backend ? `(${VaadinDevTools.BACKEND_DISPLAY_NAME[this.backend]})` : ''}\n
\n
\n Front end ${this.frontendStatus}\n
\n
\n
`;\n }\n\n private renderFeatures() {\n return html`
\n ${this.features.map(\n (feature) => html`
\n \n Learn more\n
`\n )}\n
`;\n }\n\n copyInfoToClipboard() {\n const items = this.renderRoot.querySelectorAll('.info-tray dt, .info-tray dd');\n const text = Array.from(items)\n .map((message) => (message.localName === 'dd' ? ': ' : '\\n') + message.textContent!.trim())\n .join('')\n .replace(/^\\n/, '');\n copy(text);\n this.showNotification(\n MessageType.INFORMATION,\n 'Environment information copied to clipboard',\n undefined,\n undefined,\n 'versionInfoCopied'\n );\n }\n\n toggleFeatureFlag(e: Event, feature: Feature) {\n const enabled = (e.target! as HTMLInputElement).checked;\n if (this.frontendConnection) {\n this.frontendConnection.setFeature(feature.id, enabled);\n this.showNotification(\n MessageType.INFORMATION,\n `“${feature.title}” ${enabled ? 'enabled' : 'disabled'}`,\n feature.requiresServerRestart ? 'This feature requires a server restart' : undefined,\n undefined,\n `feature${feature.id}${enabled ? 'Enabled' : 'Disabled'}`\n );\n } else {\n this.log(MessageType.ERROR, `Unable to toggle feature ${feature.title}: No server connection available`);\n }\n }\n}\n\nif (customElements.get('vaadin-dev-tools') === undefined) {\n customElements.define('vaadin-dev-tools', VaadinDevTools);\n}\n"]} \ No newline at end of file diff --git a/java/demo/frontend/generated/jar-resources/vaadin-grid-flow-selection-column.js b/java/demo/frontend/generated/jar-resources/vaadin-grid-flow-selection-column.js new file mode 100644 index 000000000..3c368de58 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/vaadin-grid-flow-selection-column.js @@ -0,0 +1,156 @@ +import '@vaadin/grid/vaadin-grid-column.js'; +import { GridColumn } from '@vaadin/grid/src/vaadin-grid-column.js'; +{ + class GridFlowSelectionColumnElement extends GridColumn { + + static get is() { + return 'vaadin-grid-flow-selection-column'; + } + + static get properties() { + return { + + /** + * Automatically sets the width of the column based on the column contents when this is set to `true`. + */ + autoWidth: { + type: Boolean, + value: true + }, + + /** + * Width of the cells for this column. + */ + width: { + type: String, + value: '56px' + }, + + /** + * Flex grow ratio for the cell widths. When set to 0, cell width is fixed. + */ + flexGrow: { + type: Number, + value: 0 + }, + + /** + * When true, all the items are selected. + */ + selectAll: { + type: Boolean, + value: false, + notify: true + }, + + /** + * Whether to display the select all checkbox in indeterminate state, + * which means some, but not all, items are selected + */ + indeterminate: { + type: Boolean, + value: false, + notify: true + }, + + selectAllHidden: Boolean + }; + } + + constructor() { + super(); + this._boundOnSelectEvent = this._onSelectEvent.bind(this); + this._boundOnDeselectEvent = this._onDeselectEvent.bind(this); + } + + static get observers() { + return [ + '_onHeaderRendererOrBindingChanged(_headerRenderer, _headerCell, path, header, selectAll, indeterminate, selectAllHidden)' + ]; + } + + /** @private */ + connectedCallback() { + super.connectedCallback(); + if (this._grid) { + this._grid.addEventListener('select', this._boundOnSelectEvent); + this._grid.addEventListener('deselect', this._boundOnDeselectEvent); + } + } + + /** @private */ + disconnectedCallback() { + super.disconnectedCallback(); + if (this._grid) { + this._grid.removeEventListener('select', this._boundOnSelectEvent); + this._grid.removeEventListener('deselect', this._boundOnDeselectEvent); + } + } + + /** + * Renders the Select All checkbox to the header cell. + * + * @override + */ + _defaultHeaderRenderer(root, _column) { + let checkbox = root.firstElementChild; + if (!checkbox) { + checkbox = document.createElement('vaadin-checkbox'); + checkbox.id = 'selectAllCheckbox'; + checkbox.setAttribute('aria-label', 'Select All'); + checkbox.classList.add('vaadin-grid-select-all-checkbox'); + checkbox.addEventListener('click', this._onSelectAllClick.bind(this)); + root.appendChild(checkbox); + } + + const checked = this.selectAll; + checkbox.hidden = this.selectAllHidden; + checkbox.checked = checked; + checkbox.indeterminate = this.indeterminate; + } + + /** + * Renders the Select Row checkbox to the body cell. + * + * @override + */ + _defaultRenderer(root, _column, { item, selected }) { + let checkbox = root.firstElementChild; + if (!checkbox) { + checkbox = document.createElement('vaadin-checkbox'); + checkbox.setAttribute('aria-label', 'Select Row'); + checkbox.addEventListener('click', this._onSelectClick.bind(this)); + root.appendChild(checkbox); + } + + checkbox.__item = item; + checkbox.checked = selected; + } + + _onSelectClick(e) { + e.currentTarget.checked ? this._grid.$connector.doDeselection([e.currentTarget.__item], true) : this._grid.$connector.doSelection([e.currentTarget.__item], true); + } + + _onSelectAllClick(e) { + e.preventDefault(); + if (this._grid.hasAttribute('disabled')) { + e.currentTarget.checked = !e.currentTarget.checked; + return; + } + this.selectAll ? this.$server.deselectAll() : this.$server.selectAll(); + } + + _onSelectEvent(e) { + } + + _onDeselectEvent(e) { + if (e.detail.userOriginated) { + this.selectAll = false; + } + } + } + + customElements.define(GridFlowSelectionColumnElement.is, GridFlowSelectionColumnElement); + + Vaadin.GridFlowSelectionColumnElement = GridFlowSelectionColumnElement; +} diff --git a/java/demo/frontend/generated/jar-resources/vaadin-time-picker/helpers.js b/java/demo/frontend/generated/jar-resources/vaadin-time-picker/helpers.js new file mode 100644 index 000000000..dd3c3281f --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/vaadin-time-picker/helpers.js @@ -0,0 +1,183 @@ +// map from unicode eastern arabic number characters to arabic numbers +const EASTERN_ARABIC_DIGIT_MAP = { + '\\u0660': '0', + '\\u0661': '1', + '\\u0662': '2', + '\\u0663': '3', + '\\u0664': '4', + '\\u0665': '5', + '\\u0666': '6', + '\\u0667': '7', + '\\u0668': '8', + '\\u0669': '9' +}; + +/** + * Escapes the given string so it can be safely used in a regexp. + * + * @param {string} string + * @return {string} + */ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Parses eastern arabic number characters to arabic numbers (0-9) + * + * @param {string} digits + * @return {string} + */ +function parseEasternArabicDigits(digits) { + return digits.replace(/[\u0660-\u0669]/g, function (char) { + const unicode = '\\u0' + char.charCodeAt(0).toString(16); + return EASTERN_ARABIC_DIGIT_MAP[unicode]; + }); +} + +/** + * @param {string} locale + * @param {Date} testTime + * @return {string | null} + */ +function getAmOrPmString(locale, testTime) { + const testTimeString = testTime.toLocaleTimeString(locale); + + // AM/PM string is anything from one letter in eastern arabic to standard two letters, + // to having space in between, dots ... + // cannot disqualify whitespace since some locales use a. m. / p. m. + // TODO when more scripts support is added (than Arabic), need to exclude those numbers too + const amOrPmRegExp = /[^\d\u0660-\u0669]/; + + const matches = + // In most locales, the time ends with AM/PM: + testTimeString.match(new RegExp(`${amOrPmRegExp.source}+$`, 'g')) || + // In some locales, the time starts with AM/PM e.g in Chinese: + testTimeString.match(new RegExp(`^${amOrPmRegExp.source}+`, 'g')); + + return matches && matches[0].trim(); +} + +/** + * @param {string} locale + * @return {string | null} + */ +export function getSeparator(locale) { + let timeString = TEST_PM_TIME.toLocaleTimeString(locale); + + // Since the next regex picks first non-number-whitespace, + // need to discard possible PM from beginning (eg. chinese locale) + const pmString = getPmString(locale); + if (pmString && timeString.startsWith(pmString)) { + timeString = timeString.replace(pmString, ''); + } + + const matches = timeString.match(/[^\u0660-\u0669\s\d]/); + return matches && matches[0]; +} + +/** + * Searches for either an AM or PM token in the given time string + * depending on what is provided in `amOrPmString`. + * + * The search is case and space insensitive. + * + * @example + * `searchAmOrPmToken('1 P M', 'PM')` => `'P M'` + * + * @example + * `searchAmOrPmToken('1 a.m.', 'A. M.')` => `a.m.` + * + * @param {string} timeString + * @param {string} amOrPmString + * @return {string | null} + */ +export function searchAmOrPmToken(timeString, amOrPmString) { + if (!amOrPmString) return null; + + // Create a regexp string for searching for AM/PM without space-sensitivity. + const tokenRegExpString = amOrPmString.split(/\s*/).map(escapeRegExp).join('\\s*'); + + // Create a regexp without case-sensitivity. + const tokenRegExp = new RegExp(tokenRegExpString, 'i'); + + // Match the regexp against the time string. + const tokenMatches = timeString.match(tokenRegExp); + if (tokenMatches) { + return tokenMatches[0]; + } +} + +export const TEST_PM_TIME = new Date('August 19, 1975 23:15:30'); + +export const TEST_AM_TIME = new Date('August 19, 1975 05:15:30'); + +/** + * @param {string} locale + * @return {string} + */ +export function getPmString(locale) { + return getAmOrPmString(locale, TEST_PM_TIME); +} + +/** + * @param {string} locale + * @return {string} + */ +export function getAmString(locale) { + return getAmOrPmString(locale, TEST_AM_TIME); +} + +/** + * @param {string} digits + * @return {number} + */ +export function parseDigitsIntoInteger(digits) { + return parseInt(parseEasternArabicDigits(digits)); +} + +/** + * @param {string} milliseconds + * @return {number} + */ +export function parseMillisecondsIntoInteger(milliseconds) { + milliseconds = parseEasternArabicDigits(milliseconds); + // digits are either .1 .01 or .001 so need to "shift" + if (milliseconds.length === 1) { + milliseconds += '00'; + } else if (milliseconds.length === 2) { + milliseconds += '0'; + } + return parseInt(milliseconds); +} + +/** + * @param {string} timeString + * @param {number} milliseconds + * @param {string} amString + * @param {string} pmString + * @return {string} + */ +export function formatMilliseconds(timeString, milliseconds, amString, pmString) { + // might need to inject milliseconds between seconds and AM/PM + let cleanedTimeString = timeString; + if (timeString.endsWith(amString)) { + cleanedTimeString = timeString.replace(' ' + amString, ''); + } else if (timeString.endsWith(pmString)) { + cleanedTimeString = timeString.replace(' ' + pmString, ''); + } + if (milliseconds) { + let millisecondsString = milliseconds < 10 ? '0' : ''; + millisecondsString += milliseconds < 100 ? '0' : ''; + millisecondsString += milliseconds; + cleanedTimeString += '.' + millisecondsString; + } else { + cleanedTimeString += '.000'; + } + if (timeString.endsWith(amString)) { + cleanedTimeString = cleanedTimeString + ' ' + amString; + } else if (timeString.endsWith(pmString)) { + cleanedTimeString = cleanedTimeString + ' ' + pmString; + } + return cleanedTimeString; +} diff --git a/java/demo/frontend/generated/jar-resources/vaadin-time-picker/timepickerConnector.js b/java/demo/frontend/generated/jar-resources/vaadin-time-picker/timepickerConnector.js new file mode 100644 index 000000000..b41daf980 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/vaadin-time-picker/timepickerConnector.js @@ -0,0 +1,178 @@ +import { + TEST_PM_TIME, + formatMilliseconds, + parseMillisecondsIntoInteger, + parseDigitsIntoInteger, + getAmString, + getPmString, + getSeparator, + searchAmOrPmToken +} from './helpers.js'; + +(function () { + const tryCatchWrapper = function (callback) { + return window.Vaadin.Flow.tryCatchWrapper(callback, 'Vaadin Time Picker'); + }; + + // Execute callback when predicate returns true. + // Try again later if predicate returns false. + function when(predicate, callback, timeout = 0) { + if (predicate()) { + callback(); + } else { + setTimeout(() => when(predicate, callback, 200), timeout); + } + } + + window.Vaadin.Flow.timepickerConnector = { + initLazy: (timepicker) => + tryCatchWrapper(function (timepicker) { + // Check whether the connector was already initialized for the timepicker + if (timepicker.$connector) { + return; + } + + timepicker.$connector = {}; + + timepicker.$connector.setLocale = tryCatchWrapper(function (locale) { + // capture previous value if any + let previousValueObject; + if (timepicker.value && timepicker.value !== '') { + previousValueObject = timepicker.i18n.parseTime(timepicker.value); + } + + try { + // Check whether the locale is supported by the browser or not + TEST_PM_TIME.toLocaleTimeString(locale); + } catch (e) { + locale = 'en-US'; + // FIXME should do a callback for server to throw an exception ? + throw new Error( + 'vaadin-time-picker: The locale ' + + locale + + ' is not supported, falling back to default locale setting(en-US).' + ); + } + + // 1. 24 or 12 hour clock, if latter then what are the am/pm strings ? + const pmString = getPmString(locale); + const amString = getAmString(locale); + + // 2. What is the separator ? + const separator = getSeparator(locale); + + const includeSeconds = function () { + return timepicker.step && timepicker.step < 60; + }; + + const includeMilliSeconds = function () { + return timepicker.step && timepicker.step < 1; + }; + + let cachedTimeString; + let cachedTimeObject; + + timepicker.i18n = { + formatTime: tryCatchWrapper(function (timeObject) { + if (!timeObject) return; + + const timeToBeFormatted = new Date(); + timeToBeFormatted.setHours(timeObject.hours); + timeToBeFormatted.setMinutes(timeObject.minutes); + timeToBeFormatted.setSeconds(timeObject.seconds !== undefined ? timeObject.seconds : 0); + + // the web component expects the correct granularity used for the time string, + // thus need to format the time object in correct granularity by passing the format options + let localeTimeString = timeToBeFormatted.toLocaleTimeString(locale, { + hour: 'numeric', + minute: 'numeric', + second: includeSeconds() ? 'numeric' : undefined + }); + + // milliseconds not part of the time format API + if (includeMilliSeconds()) { + localeTimeString = formatMilliseconds(localeTimeString, timeObject.milliseconds, amString, pmString); + } + + return localeTimeString; + }), + + parseTime: tryCatchWrapper(function (timeString) { + if (timeString && timeString === cachedTimeString && cachedTimeObject) { + return cachedTimeObject; + } + + if (!timeString) { + // when nothing is returned, the component shows the invalid state for the input + return; + } + + const amToken = searchAmOrPmToken(timeString, amString); + const pmToken = searchAmOrPmToken(timeString, pmString); + + const numbersOnlyTimeString = timeString + .replace(amToken || '', '') + .replace(pmToken || '', '') + .trim(); + + // A regexp that allows to find the numbers with optional separator and continuing searching after it. + const numbersRegExp = new RegExp('([\\d\\u0660-\\u0669]){1,2}(?:' + separator + ')?', 'g'); + + let hours = numbersRegExp.exec(numbersOnlyTimeString); + if (hours) { + hours = parseDigitsIntoInteger(hours[0].replace(separator, '')); + // handle 12 am -> 0 + // do not do anything if am & pm are not used or if those are the same, + // as with locale bg-BG there is always ч. at the end of the time + if (amToken !== pmToken) { + if (hours === 12 && amToken) { + hours = 0; + } + if (hours !== 12 && pmToken) { + hours += 12; + } + } + const minutes = numbersRegExp.exec(numbersOnlyTimeString); + const seconds = minutes && numbersRegExp.exec(numbersOnlyTimeString); + // detecting milliseconds from input, expects am/pm removed from end, eg. .0 or .00 or .000 + const millisecondRegExp = /[[\.][\d\u0660-\u0669]{1,3}$/; + // reset to end or things can explode + let milliseconds = seconds && includeMilliSeconds() && millisecondRegExp.exec(numbersOnlyTimeString); + // handle case where last numbers are seconds and . is the separator (invalid regexp match) + if (milliseconds && milliseconds['index'] <= seconds['index']) { + milliseconds = undefined; + } + // hours is a number at this point, others are either arrays or null + // the string in [0] from the arrays includes the separator too + cachedTimeObject = hours !== undefined && { + hours: hours, + minutes: minutes ? parseDigitsIntoInteger(minutes[0].replace(separator, '')) : 0, + seconds: seconds ? parseDigitsIntoInteger(seconds[0].replace(separator, '')) : 0, + milliseconds: + minutes && seconds && milliseconds + ? parseMillisecondsIntoInteger(milliseconds[0].replace('.', '')) + : 0 + }; + cachedTimeString = timeString; + return cachedTimeObject; + } + }) + }; + + if (previousValueObject) { + when( + () => timepicker.$, + () => { + const newValue = timepicker.i18n.formatTime(previousValueObject); + // FIXME works but uses private API, needs fixes in web component + if (timepicker.inputElement.value !== newValue) { + timepicker.inputElement.value = newValue; + timepicker.$.comboBox.value = newValue; + } + } + ); + } + }); + })(timepicker) + }; +})(); diff --git a/java/demo/frontend/generated/jar-resources/virtualListConnector.js b/java/demo/frontend/generated/jar-resources/virtualListConnector.js new file mode 100644 index 000000000..eda42f6b0 --- /dev/null +++ b/java/demo/frontend/generated/jar-resources/virtualListConnector.js @@ -0,0 +1,130 @@ +import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; +import { timeOut } from '@polymer/polymer/lib/utils/async.js'; + +window.Vaadin.Flow.virtualListConnector = { + initLazy: function (list) { + // Check whether the connector was already initialized for the virtual list + if (list.$connector) { + return; + } + + const extraItemsBuffer = 20; + + let lastRequestedRange = [0, 0]; + + list.$connector = {}; + list.$connector.placeholderItem = { __placeholder: true }; + + const updateRequestedItem = function () { + /* + * TODO virtual list seems to do a small index adjustment after scrolling + * has stopped. This causes a redundant request to be sent to make a + * corresponding minimal change to the buffer. We should avoid these + * requests by making the logic skip doing a request if the available + * buffer is within some tolerance compared to the requested buffer. + */ + const visibleIndexes = [...list.children] + .filter((el) => '__virtualListIndex' in el) + .map((el) => el.__virtualListIndex); + const firstNeededItem = Math.min(...visibleIndexes); + const lastNeededItem = Math.max(...visibleIndexes); + + let first = Math.max(0, firstNeededItem - extraItemsBuffer); + let last = Math.min(lastNeededItem + extraItemsBuffer, list.items.length); + + if (lastRequestedRange[0] != first || lastRequestedRange[1] != last) { + lastRequestedRange = [first, last]; + const count = 1 + last - first; + list.$server.setRequestedRange(first, count); + } + }; + + const scheduleUpdateRequest = function () { + list.__requestDebounce = Debouncer.debounce(list.__requestDebounce, timeOut.after(50), updateRequestedItem); + }; + + requestAnimationFrame(() => updateRequestedItem); + + // Add an observer function that will invoke on virtualList.renderer property + // change and then patches it with a wrapper renderer + list.patchVirtualListRenderer = function () { + if (!list.renderer || list.renderer.__virtualListConnectorPatched) { + // The list either doesn't have a renderer yet or it's already been patched + return; + } + + const originalRenderer = list.renderer; + + const renderer = (root, list, model) => { + root.__virtualListIndex = model.index; + + if (model.item === undefined) { + originalRenderer.call(list, root, list, { + ...model, + item: list.$connector.placeholderItem + }); + } else { + originalRenderer.call(list, root, list, model); + } + + /* + * Check if we need to do anything once things have settled down. + * This method is called multiple times in sequence for the same user + * action, but we only want to do the check once. + */ + scheduleUpdateRequest(); + }; + renderer.__virtualListConnectorPatched = true; + renderer.__rendererId = originalRenderer.__rendererId; + + list.renderer = renderer; + }; + + list._createPropertyObserver('renderer', 'patchVirtualListRenderer', true); + list.patchVirtualListRenderer(); + + list.items = []; + + list.$connector.set = function (index, items) { + list.items.splice(index, items.length, ...items); + list.items = [...list.items]; + }; + + list.$connector.clear = function (index, length) { + // How many items, starting from "index", should be set as undefined + const clearCount = Math.min(length, list.items.length - index); + list.$connector.set(index, [...Array(clearCount)]); + }; + + list.$connector.updateData = function (items) { + const updatedItemsMap = items.reduce((map, item) => { + map[item.key] = item; + return map; + }, {}); + + list.items = list.items.map((item) => { + // Items can be undefined if they are outside the viewport + if (!item) { + return item; + } + // Replace existing item with updated item, + // return existing item as fallback if it was not updated + return updatedItemsMap[item.key] || item; + }); + }; + + list.$connector.updateSize = function (newSize) { + const delta = newSize - list.items.length; + if (delta > 0) { + list.items = [...list.items, ...Array(delta)]; + } else if (delta < 0) { + list.items = list.items.slice(0, newSize); + } + }; + + list.$connector.setPlaceholderItem = function (placeholderItem = {}) { + placeholderItem.__placeholder = true; + list.$connector.placeholderItem = placeholderItem; + }; + } +}; diff --git a/java/demo/frontend/generated/vaadin-featureflags.ts b/java/demo/frontend/generated/vaadin-featureflags.ts new file mode 100644 index 000000000..d33994382 --- /dev/null +++ b/java/demo/frontend/generated/vaadin-featureflags.ts @@ -0,0 +1,11 @@ +// @ts-nocheck +window.Vaadin = window.Vaadin || {}; +window.Vaadin.featureFlags = window.Vaadin.featureFlags || {}; +window.Vaadin.featureFlags.exampleFeatureFlag = false; +window.Vaadin.featureFlags.hillaPush = false; +window.Vaadin.featureFlags.hillaEngine = false; +window.Vaadin.featureFlags.oldLicenseChecker = false; +window.Vaadin.featureFlags.collaborationEngineBackend = false; +window.Vaadin.featureFlags.webpackForFrontendBuild = false; +window.Vaadin.featureFlags.enforceFieldValidation = false; +export {}; \ No newline at end of file diff --git a/java/demo/frontend/generated/vaadin.ts b/java/demo/frontend/generated/vaadin.ts new file mode 100644 index 000000000..3c8cdfca6 --- /dev/null +++ b/java/demo/frontend/generated/vaadin.ts @@ -0,0 +1,5 @@ +import './vaadin-featureflags.ts'; + +import './index'; + +import 'Frontend/generated/jar-resources/vaadin-dev-tools.js'; diff --git a/java/demo/frontend/generated/vite-devmode.ts b/java/demo/frontend/generated/vite-devmode.ts new file mode 100644 index 000000000..f9f7a5fba --- /dev/null +++ b/java/demo/frontend/generated/vite-devmode.ts @@ -0,0 +1,31 @@ +// @ts-ignore +if (import.meta.hot) { + // @ts-ignore + const hot = import.meta.hot; + + const isLiveReloadDisabled = () => { + // Checks if live reload is disabled in the debug window + return sessionStorage.getItem('vaadin.live-reload.active') === 'false'; + }; + + const preventViteReload = (payload: any) => { + // Changing the path prevents Vite from reloading + payload.path = '/_fake/path.html'; + }; + + let pendingNavigationTo: string | undefined = undefined; + + window.addEventListener('vaadin-router-go', (routerEvent: any) => { + pendingNavigationTo = routerEvent.detail.pathname + routerEvent.detail.search; + }); + hot.on('vite:beforeFullReload', (payload: any) => { + if (isLiveReloadDisabled()) { + preventViteReload(payload); + } + if (pendingNavigationTo) { + // Force reload with the new URL + location.href = pendingNavigationTo; + preventViteReload(payload); + } + }); +} diff --git a/java/demo/frontend/index.html b/java/demo/frontend/index.html new file mode 100644 index 000000000..a5cdd4018 --- /dev/null +++ b/java/demo/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/java/demo/package-lock.json b/java/demo/package-lock.json new file mode 100644 index 000000000..9c6b24afb --- /dev/null +++ b/java/demo/package-lock.json @@ -0,0 +1,8266 @@ +{ + "name": "no-name", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "no-name", + "license": "UNLICENSED", + "dependencies": { + "@polymer/iron-icon": "3.0.1", + "@polymer/iron-iconset-svg": "3.0.1", + "@polymer/iron-list": "3.1.0", + "@polymer/iron-meta": "3.0.1", + "@polymer/iron-resizable-behavior": "3.0.1", + "@polymer/polymer": "3.5.1", + "@vaadin/accordion": "23.3.8", + "@vaadin/app-layout": "23.3.8", + "@vaadin/avatar": "23.3.8", + "@vaadin/avatar-group": "23.3.8", + "@vaadin/bundles": "23.3.8", + "@vaadin/button": "23.3.8", + "@vaadin/checkbox": "23.3.8", + "@vaadin/checkbox-group": "23.3.8", + "@vaadin/combo-box": "23.3.8", + "@vaadin/common-frontend": "0.0.17", + "@vaadin/component-base": "23.3.8", + "@vaadin/confirm-dialog": "23.3.8", + "@vaadin/context-menu": "23.3.8", + "@vaadin/custom-field": "23.3.8", + "@vaadin/date-picker": "23.3.8", + "@vaadin/date-time-picker": "23.3.8", + "@vaadin/details": "23.3.8", + "@vaadin/dialog": "23.3.8", + "@vaadin/email-field": "23.3.8", + "@vaadin/field-base": "23.3.8", + "@vaadin/field-highlighter": "23.3.8", + "@vaadin/form-layout": "23.3.8", + "@vaadin/grid": "23.3.8", + "@vaadin/horizontal-layout": "23.3.8", + "@vaadin/icon": "23.3.8", + "@vaadin/icons": "23.3.8", + "@vaadin/input-container": "23.3.8", + "@vaadin/integer-field": "23.3.8", + "@vaadin/item": "23.3.8", + "@vaadin/list-box": "23.3.8", + "@vaadin/lit-renderer": "23.3.8", + "@vaadin/login": "23.3.8", + "@vaadin/menu-bar": "23.3.8", + "@vaadin/message-input": "23.3.8", + "@vaadin/message-list": "23.3.8", + "@vaadin/multi-select-combo-box": "23.3.8", + "@vaadin/notification": "23.3.8", + "@vaadin/number-field": "23.3.8", + "@vaadin/password-field": "23.3.8", + "@vaadin/polymer-legacy-adapter": "23.3.8", + "@vaadin/progress-bar": "23.3.8", + "@vaadin/radio-group": "23.3.8", + "@vaadin/router": "1.7.4", + "@vaadin/scroller": "23.3.8", + "@vaadin/select": "23.3.8", + "@vaadin/split-layout": "23.3.8", + "@vaadin/tabs": "23.3.8", + "@vaadin/tabsheet": "23.3.8", + "@vaadin/text-area": "23.3.8", + "@vaadin/text-field": "23.3.8", + "@vaadin/time-picker": "23.3.8", + "@vaadin/tooltip": "23.3.8", + "@vaadin/upload": "23.3.8", + "@vaadin/vaadin-accordion": "23.3.8", + "@vaadin/vaadin-app-layout": "23.3.8", + "@vaadin/vaadin-avatar": "23.3.8", + "@vaadin/vaadin-button": "23.3.8", + "@vaadin/vaadin-checkbox": "23.3.8", + "@vaadin/vaadin-combo-box": "23.3.8", + "@vaadin/vaadin-confirm-dialog": "23.3.8", + "@vaadin/vaadin-context-menu": "23.3.8", + "@vaadin/vaadin-custom-field": "23.3.8", + "@vaadin/vaadin-date-picker": "23.3.8", + "@vaadin/vaadin-date-time-picker": "23.3.8", + "@vaadin/vaadin-details": "23.3.8", + "@vaadin/vaadin-development-mode-detector": "2.0.6", + "@vaadin/vaadin-dialog": "23.3.8", + "@vaadin/vaadin-form-layout": "23.3.8", + "@vaadin/vaadin-grid": "23.3.8", + "@vaadin/vaadin-icon": "23.3.8", + "@vaadin/vaadin-icons": "23.3.8", + "@vaadin/vaadin-item": "23.3.8", + "@vaadin/vaadin-list-box": "23.3.8", + "@vaadin/vaadin-list-mixin": "23.3.8", + "@vaadin/vaadin-login": "23.3.8", + "@vaadin/vaadin-lumo-styles": "23.3.8", + "@vaadin/vaadin-material-styles": "23.3.8", + "@vaadin/vaadin-menu-bar": "23.3.8", + "@vaadin/vaadin-messages": "23.3.8", + "@vaadin/vaadin-notification": "23.3.8", + "@vaadin/vaadin-ordered-layout": "23.3.8", + "@vaadin/vaadin-overlay": "23.3.8", + "@vaadin/vaadin-progress-bar": "23.3.8", + "@vaadin/vaadin-radio-button": "23.3.8", + "@vaadin/vaadin-select": "23.3.8", + "@vaadin/vaadin-split-layout": "23.3.8", + "@vaadin/vaadin-tabs": "23.3.8", + "@vaadin/vaadin-template-renderer": "23.3.8", + "@vaadin/vaadin-text-field": "23.3.8", + "@vaadin/vaadin-themable-mixin": "23.3.8", + "@vaadin/vaadin-time-picker": "23.3.8", + "@vaadin/vaadin-upload": "23.3.8", + "@vaadin/vaadin-usage-statistics": "2.1.2", + "@vaadin/vaadin-virtual-list": "23.3.8", + "@vaadin/vertical-layout": "23.3.8", + "@vaadin/virtual-list": "23.3.8", + "construct-style-sheets-polyfill": "3.1.0", + "date-fns": "2.29.3", + "lit": "2.6.1" + }, + "devDependencies": { + "@rollup/plugin-replace": "3.1.0", + "@rollup/pluginutils": "4.1.0", + "async": "3.2.2", + "glob": "7.2.3", + "mkdirp": "1.0.4", + "rollup-plugin-brotli": "3.1.0", + "strip-css-comments": "5.0.0", + "transform-ast": "2.4.4", + "typescript": "4.9.3", + "vite": "3.2.5", + "vite-plugin-checker": "0.5.4", + "workbox-build": "6.5.4", + "workbox-core": "6.5.4", + "workbox-precaching": "6.5.4" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz", + "integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz", + "integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz", + "integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz", + "integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.6", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.6", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-wc/dedupe-mixin": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz", + "integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==", + "license": "MIT" + }, + "node_modules/@polymer/iron-a11y-keys-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz", + "integrity": "sha512-lnrjKq3ysbBPT/74l0Fj0U9H9C35Tpw2C/tpJ8a+5g8Y3YJs1WSZYnEl1yOkw6sEyaxOq/1DkzH0+60gGu5/PQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-flex-layout": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz", + "integrity": "sha512-7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-icon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-icon/-/iron-icon-3.0.1.tgz", + "integrity": "sha512-QLPwirk+UPZNaLnMew9VludXA4CWUCenRewgEcGYwdzVgDPCDbXxy6vRJjmweZobMQv/oVLppT2JZtJFnPxX6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-iconset-svg": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-iconset-svg/-/iron-iconset-svg-3.0.1.tgz", + "integrity": "sha512-XNwURbNHRw6u2fJe05O5fMYye6GSgDlDqCO+q6K1zAnKIrpgZwf2vTkBd5uCcZwsN0FyCB3mvNZx4jkh85dRDw==", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-list": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@polymer/iron-list/-/iron-list-3.1.0.tgz", + "integrity": "sha512-Eiv6xd3h3oPmn8SXFntXVfC3ZnegH+KHAxiKLKcOASFSRY3mHnr2AdcnExUJ9ItoCMA5UzKaM/0U22eWzGERtA==", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-scroll-target-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-meta": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz", + "integrity": "sha512-pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-resizable-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-resizable-behavior/-/iron-resizable-behavior-3.0.1.tgz", + "integrity": "sha512-FyHxRxFspVoRaeZSWpT3y0C9awomb4tXXolIJcZ7RvXhMP632V5lez+ch5G5SwK0LpnAPkg35eB0LPMFv+YMMQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-scroll-target-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-scroll-target-behavior/-/iron-scroll-target-behavior-3.0.1.tgz", + "integrity": "sha512-xg1WanG25BIkQE8rhuReqY9zx1K5M7F+YAIYpswEp5eyDIaZ1Y3vUmVeQ3KG+hiSugzI1M752azXN7kvyhOBcQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/polymer": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.5.1.tgz", + "integrity": "sha512-JlAHuy+1qIC6hL1ojEUfIVD58fzTpJAoCxFwV5yr0mYTXV1H8bz5zy0+rC963Cgr9iNXQ4T9ncSjC2fkF9BQfw==", + "license": "BSD-3-Clause", + "dependencies": { + "@webcomponents/shadycss": "^1.9.1" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-babel/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-babel/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-replace": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-3.1.0.tgz", + "integrity": "sha512-pA3XRUrSKybVYqmH5TqWNZpGxF+VV+1GrYchKgCNIj2vsSOX7CVm2RCtx8p2nrC7xvkziYyK+lSi74T93MU3YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.0.tgz", + "integrity": "sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", + "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@vaadin/accordion": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/accordion/-/accordion-23.3.8.tgz", + "integrity": "sha512-/XJ3hZdKbsovaCMwIHpOWWeZmHz+L6LE5Nr+0good8d9joiCGkmLPwxqRweepjCO82Hlmjrn5SUwcsD0vwnNTQ==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/details": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/app-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/app-layout/-/app-layout-23.3.8.tgz", + "integrity": "sha512-SHpvSOjf3wvjuufmvkpwPitSumgZDZculQikjjg7X+NqBsJsdz1duUYBbHABlYSK4J9qC1kopabq54/8w5Uk+A==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/avatar": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/avatar/-/avatar-23.3.8.tgz", + "integrity": "sha512-ky4FS4/va3cB2cHecsSh+muGpCCtDqHJmszDqPF/zDOp+dm1ENVHoc+zW41b7t29cL+8L+ifZcYmRhJsEbcj+g==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/item": "~23.3.8", + "@vaadin/list-box": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/tooltip": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/avatar-group": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/avatar-group/-/avatar-group-23.3.8.tgz", + "integrity": "sha512-v7VS6PBnTKWeDIQGD+r0AKgdpe/vUbYZNIOXL34DthysBeG8/xYrXXKZ41hmla5IKSuPX8p2nyU3yjEeWjKrCw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/avatar": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/item": "~23.3.8", + "@vaadin/list-box": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/bundles": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/bundles/-/bundles-23.3.8.tgz", + "integrity": "sha512-WZTnlztUYrVWq8gWXjoimYGaghOZGVOmHKLHh5Ac1HnQroae/qyoaAyw7kO1vJqZW+tyOrlwnt4W9fN7NZd9eQ==", + "license": "(Apache-2.0 OR SEE LICENSE IN https://vaadin.com/license/cvdl-4.0)", + "peerDependencies": { + "@polymer/iron-flex-layout": "3.0.1", + "@polymer/iron-icon": "3.0.1", + "@polymer/iron-iconset-svg": "3.0.1", + "@polymer/iron-media-query": "3.0.1", + "@polymer/iron-meta": "3.0.1", + "@polymer/polymer": "3.5.1", + "@vaadin/accordion": "23.3.8", + "@vaadin/app-layout": "23.3.8", + "@vaadin/avatar": "23.3.8", + "@vaadin/avatar-group": "23.3.8", + "@vaadin/board": "23.3.8", + "@vaadin/button": "23.3.8", + "@vaadin/charts": "23.3.8", + "@vaadin/checkbox": "23.3.8", + "@vaadin/checkbox-group": "23.3.8", + "@vaadin/combo-box": "23.3.8", + "@vaadin/component-base": "23.3.8", + "@vaadin/confirm-dialog": "23.3.8", + "@vaadin/context-menu": "23.3.8", + "@vaadin/cookie-consent": "23.3.8", + "@vaadin/crud": "23.3.8", + "@vaadin/custom-field": "23.3.8", + "@vaadin/date-picker": "23.3.8", + "@vaadin/date-time-picker": "23.3.8", + "@vaadin/details": "23.3.8", + "@vaadin/dialog": "23.3.8", + "@vaadin/email-field": "23.3.8", + "@vaadin/field-base": "23.3.8", + "@vaadin/field-highlighter": "23.3.8", + "@vaadin/form-layout": "23.3.8", + "@vaadin/grid": "23.3.8", + "@vaadin/grid-pro": "23.3.8", + "@vaadin/horizontal-layout": "23.3.8", + "@vaadin/icon": "23.3.8", + "@vaadin/icons": "23.3.8", + "@vaadin/input-container": "23.3.8", + "@vaadin/integer-field": "23.3.8", + "@vaadin/item": "23.3.8", + "@vaadin/list-box": "23.3.8", + "@vaadin/lit-renderer": "23.3.8", + "@vaadin/login": "23.3.8", + "@vaadin/map": "23.3.8", + "@vaadin/menu-bar": "23.3.8", + "@vaadin/message-input": "23.3.8", + "@vaadin/message-list": "23.3.8", + "@vaadin/multi-select-combo-box": "23.3.8", + "@vaadin/notification": "23.3.8", + "@vaadin/number-field": "23.3.8", + "@vaadin/overlay": "23.3.8", + "@vaadin/password-field": "23.3.8", + "@vaadin/polymer-legacy-adapter": "23.3.8", + "@vaadin/progress-bar": "23.3.8", + "@vaadin/radio-group": "23.3.8", + "@vaadin/rich-text-editor": "23.3.8", + "@vaadin/scroller": "23.3.8", + "@vaadin/select": "23.3.8", + "@vaadin/split-layout": "23.3.8", + "@vaadin/tabs": "23.3.8", + "@vaadin/tabsheet": "23.3.8", + "@vaadin/text-area": "23.3.8", + "@vaadin/text-field": "23.3.8", + "@vaadin/time-picker": "23.3.8", + "@vaadin/tooltip": "23.3.8", + "@vaadin/upload": "23.3.8", + "@vaadin/vaadin-development-mode-detector": "2.0.5", + "@vaadin/vaadin-list-mixin": "23.3.8", + "@vaadin/vaadin-lumo-styles": "23.3.8", + "@vaadin/vaadin-themable-mixin": "23.3.8", + "@vaadin/vaadin-usage-statistics": "2.1.2", + "@vaadin/vertical-layout": "23.3.8", + "@vaadin/virtual-list": "23.3.8", + "cookieconsent": "3.1.1", + "highcharts": "9.2.2", + "lit": "2.6.1", + "ol": "6.13.0", + "quickselect": "2.0.0", + "rbush": "3.0.1" + }, + "peerDependenciesMeta": { + "@polymer/iron-flex-layout": { + "optional": true + }, + "@polymer/iron-icon": { + "optional": true + }, + "@polymer/iron-iconset-svg": { + "optional": true + }, + "@polymer/iron-media-query": { + "optional": true + }, + "@polymer/iron-meta": { + "optional": true + }, + "@polymer/polymer": { + "optional": true + }, + "@vaadin/accordion": { + "optional": true + }, + "@vaadin/app-layout": { + "optional": true + }, + "@vaadin/avatar": { + "optional": true + }, + "@vaadin/avatar-group": { + "optional": true + }, + "@vaadin/board": { + "optional": true + }, + "@vaadin/button": { + "optional": true + }, + "@vaadin/charts": { + "optional": true + }, + "@vaadin/checkbox": { + "optional": true + }, + "@vaadin/checkbox-group": { + "optional": true + }, + "@vaadin/combo-box": { + "optional": true + }, + "@vaadin/component-base": { + "optional": true + }, + "@vaadin/confirm-dialog": { + "optional": true + }, + "@vaadin/context-menu": { + "optional": true + }, + "@vaadin/cookie-consent": { + "optional": true + }, + "@vaadin/crud": { + "optional": true + }, + "@vaadin/custom-field": { + "optional": true + }, + "@vaadin/date-picker": { + "optional": true + }, + "@vaadin/date-time-picker": { + "optional": true + }, + "@vaadin/details": { + "optional": true + }, + "@vaadin/dialog": { + "optional": true + }, + "@vaadin/email-field": { + "optional": true + }, + "@vaadin/field-base": { + "optional": true + }, + "@vaadin/field-highlighter": { + "optional": true + }, + "@vaadin/form-layout": { + "optional": true + }, + "@vaadin/grid": { + "optional": true + }, + "@vaadin/grid-pro": { + "optional": true + }, + "@vaadin/horizontal-layout": { + "optional": true + }, + "@vaadin/icon": { + "optional": true + }, + "@vaadin/icons": { + "optional": true + }, + "@vaadin/input-container": { + "optional": true + }, + "@vaadin/integer-field": { + "optional": true + }, + "@vaadin/item": { + "optional": true + }, + "@vaadin/list-box": { + "optional": true + }, + "@vaadin/lit-renderer": { + "optional": true + }, + "@vaadin/login": { + "optional": true + }, + "@vaadin/map": { + "optional": true + }, + "@vaadin/menu-bar": { + "optional": true + }, + "@vaadin/message-input": { + "optional": true + }, + "@vaadin/message-list": { + "optional": true + }, + "@vaadin/multi-select-combo-box": { + "optional": true + }, + "@vaadin/notification": { + "optional": true + }, + "@vaadin/number-field": { + "optional": true + }, + "@vaadin/overlay": { + "optional": true + }, + "@vaadin/password-field": { + "optional": true + }, + "@vaadin/polymer-legacy-adapter": { + "optional": true + }, + "@vaadin/progress-bar": { + "optional": true + }, + "@vaadin/radio-group": { + "optional": true + }, + "@vaadin/rich-text-editor": { + "optional": true + }, + "@vaadin/scroller": { + "optional": true + }, + "@vaadin/select": { + "optional": true + }, + "@vaadin/split-layout": { + "optional": true + }, + "@vaadin/tabs": { + "optional": true + }, + "@vaadin/tabsheet": { + "optional": true + }, + "@vaadin/text-area": { + "optional": true + }, + "@vaadin/text-field": { + "optional": true + }, + "@vaadin/time-picker": { + "optional": true + }, + "@vaadin/tooltip": { + "optional": true + }, + "@vaadin/upload": { + "optional": true + }, + "@vaadin/vaadin-development-mode-detector": { + "optional": true + }, + "@vaadin/vaadin-list-mixin": { + "optional": true + }, + "@vaadin/vaadin-lumo-styles": { + "optional": true + }, + "@vaadin/vaadin-themable-mixin": { + "optional": true + }, + "@vaadin/vaadin-usage-statistics": { + "optional": true + }, + "@vaadin/vertical-layout": { + "optional": true + }, + "@vaadin/virtual-list": { + "optional": true + }, + "cookieconsent": { + "optional": true + }, + "highcharts": { + "optional": true + }, + "lit": { + "optional": true + }, + "ol": { + "optional": true + }, + "quickselect": { + "optional": true + }, + "rbush": { + "optional": true + } + } + }, + "node_modules/@vaadin/button": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/button/-/button-23.3.8.tgz", + "integrity": "sha512-22Fx70y6hPDgVQhDyYz/0YxpLm8CKnFErHvW/k59iVLNwT4IwRZYUjqudgwIowEySsJb/2634ivH4LtqDmuj0g==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/checkbox": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/checkbox/-/checkbox-23.3.8.tgz", + "integrity": "sha512-YNIb/D0Kx+tMaD+DDP0KzsUF/qSq88zPyvM3qN0J6fTsz2cpwO0lO0o4HKUgefU+HiqAKrLzVqr9GWUS7mJQhw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/checkbox-group": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/checkbox-group/-/checkbox-group-23.3.8.tgz", + "integrity": "sha512-6WLEuARNj/lYgFf0saj4udX7m6ZAPp2l7nhqDoWV37h37aDFATkWgY/su//yEU2WSxtBcmqedWhNzolnRurWnQ==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/checkbox": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/combo-box": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/combo-box/-/combo-box-23.3.8.tgz", + "integrity": "sha512-ZJVk/mhXzu3AC0KyM6QGnlet1c4d5xyQ7J418CASAUmdccj2xwELiWo3ppHcKfpwqeF1qSzid0aco4TWsDEoTw==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/input-container": "~23.3.8", + "@vaadin/item": "~23.3.8", + "@vaadin/lit-renderer": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/common-frontend": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@vaadin/common-frontend/-/common-frontend-0.0.17.tgz", + "integrity": "sha512-M4tg10cYgdDqQAXfGfXpQ90eHm+xL6ynAFEDgtc2IxXVWXKYU8jGK08SM5yOcZ4wDk0ETyHMtQlKUPDNkz6Qfw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.1" + }, + "peerDependencies": { + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/component-base": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/component-base/-/component-base-23.3.8.tgz", + "integrity": "sha512-N0ewZPUbV8Nt5Nup45NK3dvsAzzlaSyeL89CNad1UNP9jIdnXwcYVNBrupaXKT4aNmo1+RGc7+hC3Ve6wdxTOg==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/vaadin-development-mode-detector": "^2.0.0", + "@vaadin/vaadin-usage-statistics": "^2.1.0", + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/confirm-dialog": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/confirm-dialog/-/confirm-dialog-23.3.8.tgz", + "integrity": "sha512-fK3mo5fj1DQGt1Re/EflULUVmeyqy5+jLZ/jJqjQFwYSIfjR7/UJfirM7a9i5f/Xt4Mrx6sETlWfMuJE30Wu/Q==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/dialog": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/context-menu": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/context-menu/-/context-menu-23.3.8.tgz", + "integrity": "sha512-nyBu0Cz3zvgi9lnL+IM44VuZ6pj5Fd6mQvGfqmpXaUVycM3ZrR7XDmjBUZqI6v89ULNvmRHZyqZRvcsULyDYXg==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/item": "~23.3.8", + "@vaadin/list-box": "~23.3.8", + "@vaadin/lit-renderer": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/custom-field": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/custom-field/-/custom-field-23.3.8.tgz", + "integrity": "sha512-JXz3nc3dSzY8gds3HFKA9LQUkWBHvZVFn47yDevKQdjwY48/loMyTJoiRuTZ5vHQZk0599HcacCFLj4yRbNVeg==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/date-picker": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/date-picker/-/date-picker-23.3.8.tgz", + "integrity": "sha512-UY8nrLVC6RMV7cKhWDHP9nUu0fC2YG5Mb9SL4622d8wE0nJox9oPh61e/Qa1rj1bUL7hxl/AqgCS+9pbX5O3eQ==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.2.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/input-container": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/date-time-picker": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/date-time-picker/-/date-time-picker-23.3.8.tgz", + "integrity": "sha512-GOW6RElVYJbZMIQQGaF4rLu7Sy1XuW7HH0Lz6X9S86xAJjjq1r8egzrLaFVIBfPFyOe+zbUHJvpkBNrvuG0Atw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/custom-field": "~23.3.8", + "@vaadin/date-picker": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/time-picker": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/details": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/details/-/details-23.3.8.tgz", + "integrity": "sha512-3qZ12xJjsRMrFNzzk1sbGnl1TRmEiXmgQSr2M0A8EC6/rZIAb9MynIm4HttrV16Qbug7JP3O9/USd2QM7zC4Sg==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/dialog": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/dialog/-/dialog-23.3.8.tgz", + "integrity": "sha512-JWqijQz4I7s6RXACNW5ttkbTZe9kr3FqRw943S/eJitBCr6bNnB/ct183gcTKE2u49Gdvl8Igto95lVy8sPkLg==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/lit-renderer": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/email-field": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/email-field/-/email-field-23.3.8.tgz", + "integrity": "sha512-TvRvEm1Xn3PY3AVdujGsFibpbPworYeOzv/1RssUHvhXPROstjAAYzTaGbAbUtVlK7fF1dt1egUeae7pi5WOEg==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/text-field": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/field-base": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/field-base/-/field-base-23.3.8.tgz", + "integrity": "sha512-Ixb5b/X6kjmY5zt9/0796JSDL0HoL00YBwFpoLv8zrGNI9eIkRJzvaJYYYRD8z/qKFZKJesn741LH2tGtZ05qg==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/field-highlighter": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/field-highlighter/-/field-highlighter-23.3.8.tgz", + "integrity": "sha512-7L63B69AzRwqTLKbN3XuA427XbJng/mAWtjOU+8utzM4UJZzz7Vo6mPKqM2LGa1qL65eqlTFF0CveDdIW3UxfA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8", + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/form-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/form-layout/-/form-layout-23.3.8.tgz", + "integrity": "sha512-V5WHswXN+KfuBpX4DwCFs4jBxRItllpk8Ki6VFZNVmQQh3060bRYSLf5jEgO8ddR+XkIsy4f8RuBh9mpfEDbiQ==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/grid": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/grid/-/grid-23.3.8.tgz", + "integrity": "sha512-o1TuHsl9u5eSe5IrqUKkVdv8Tm43cvY8Cfs/WOViBOjAT8VXck1ctUDGdQZMYEuIKZocYWEnNA0+9KSbBeocbQ==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/checkbox": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/lit-renderer": "~23.3.8", + "@vaadin/text-field": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/horizontal-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/horizontal-layout/-/horizontal-layout-23.3.8.tgz", + "integrity": "sha512-mnDMEUXhrvozFJofl/PGbjFlFMHxC5v/Nwk/ndla1EuizxTJkNf/DdXCJfwqrMALXOYkdXO2GV++MBs5PxkXxg==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/icon": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/icon/-/icon-23.3.8.tgz", + "integrity": "sha512-PT6nWZYY5sIuUBpPKlpwz/n/QdSEKszSEhDqdDGB+OS19Dhn+o4Ackw0QtmeEcnuX2brl8z+Ln10WaWasz4/vA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8", + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/icons": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/icons/-/icons-23.3.8.tgz", + "integrity": "sha512-jR4tQyOT3mJffZVDgDEdcfMapw+UWvbTefjIoNJeqgXLwEJ18Iku9D0W2nEGp+7tpmkNPW2rQZsmg8KTPi6GQQ==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/iron-icon": "^3.0.0", + "@polymer/iron-iconset-svg": "^3.0.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/icon": "~23.3.8" + } + }, + "node_modules/@vaadin/input-container": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/input-container/-/input-container-23.3.8.tgz", + "integrity": "sha512-soMGphDbEeH9WgjTDRZih80wGcABL+rsUGsftKYs/L6nK39ZHCFoIWd/X5bQNU6kvIuFtQZwgf6Grix8KwrSfw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/integer-field": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/integer-field/-/integer-field-23.3.8.tgz", + "integrity": "sha512-ElQcZgOAGCtwkMgiTLC2cvKDC3FVu99078pqBMVyrpXqb8jktpKM1c5WOok1+nvfE5kC6taw3jVcwI05Nf6uEg==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/number-field": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8" + } + }, + "node_modules/@vaadin/item": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/item/-/item-23.3.8.tgz", + "integrity": "sha512-nqMc6OXiesQHbEeewiOPL4LOmzNaQyiRMrgHsMSlbtur1F5wQCiKsiVqWYPS+/adhtcS8TPn+9ppliyH5AhVag==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/list-box": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/list-box/-/list-box-23.3.8.tgz", + "integrity": "sha512-sEPkoxPQcqSSM0UCEEAmchUMC5pjbMX3WNzeo3YV+c5+H/EGR9hFeYJPYhmS5fmcw1Z1qsKjsSDL7LzVzSGwjQ==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/item": "~23.3.8", + "@vaadin/vaadin-list-mixin": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/lit-renderer": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/lit-renderer/-/lit-renderer-23.3.8.tgz", + "integrity": "sha512-3RIDgz7oym2iXG0jVMEsjKmVkpaIPm8maQCwhJcp6ANQmDg1lgQtoubvMTJYAVCs6luosQM2Uk45E4BLzGpBqw==", + "license": "Apache-2.0", + "dependencies": { + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/login": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/login/-/login-23.3.8.tgz", + "integrity": "sha512-M42NGONCSrtdkiVrB5C5mdtyETXG4e+ZpAjnLVx0anFdChu8WTug4FV6gqOgqrPf4sOsF3tOsruP6hiwr3BcTw==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/password-field": "~23.3.8", + "@vaadin/text-field": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/menu-bar": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/menu-bar/-/menu-bar-23.3.8.tgz", + "integrity": "sha512-i4jfGST4tIbEyvcuoO8EQGoLDMjzmT/UbtJB2nTebzqUiY1z0Og69+AaY7eQQS7sOTmtC4wjSW+4difDhYVRxQ==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/context-menu": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/message-input": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/message-input/-/message-input-23.3.8.tgz", + "integrity": "sha512-y9sgt9gbHrtyNO0kddR2WdjG/AcQPIFVI0jysAfvKoIKBcUwFAE35sZyh7Ef1EOKZy1691XpqTVhXY+83qsz8A==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/text-area": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/message-list": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/message-list/-/message-list-23.3.8.tgz", + "integrity": "sha512-+dpe2ww0cg99Ir5NFDAUzHfIN3jyA+WF89grbWlDaVnBZonEijdrJUgCGKFubl1qu5ZHiXHE4vKPGMGmopGV0g==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/avatar": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/multi-select-combo-box": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/multi-select-combo-box/-/multi-select-combo-box-23.3.8.tgz", + "integrity": "sha512-isPK52yYjTfzHq7LZjhEH0r6cVr2E7weKSK3HTEtN3DrkrjLHuW1dkbM7IBVOeYHEejknpIe6hsuykLX+kf9Tw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/combo-box": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/input-container": "~23.3.8", + "@vaadin/lit-renderer": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/notification": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/notification/-/notification-23.3.8.tgz", + "integrity": "sha512-3uZzeDy1lbHbO7a8XYFwgBJrR2IrYKvJFs+KGag26RZRf8saLDMjqZNoM1NgdJy4I+Jtx5W2SzQx2TA6hfg52w==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/lit-renderer": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8", + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/number-field": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/number-field/-/number-field-23.3.8.tgz", + "integrity": "sha512-0K+hVjTS8OJV7GDe6Nbr9dOBkDl/bO2GR2/L2vlTaTcgLXl9UXIyzqa0+ZxLLjpe4yAHFU8niDdN6MmlWjY+0Q==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/input-container": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/overlay": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/overlay/-/overlay-23.3.8.tgz", + "integrity": "sha512-sngdoFTrcEvz0nE3T0nK9CR2aO8AU6xySHugP1LADxwjwAYGAWeSljzf8yk5520bBIOz+7M0NbFSk7qqJhpK2Q==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/password-field": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/password-field/-/password-field-23.3.8.tgz", + "integrity": "sha512-jNVdhFMUi12wnfHLzG23VuWLcWFMatZ0fRjpOxknGQqm0PI58XV5XUIJDbayPFhhFPy50ne1riO5X9Lo0xkAxg==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/text-field": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/polymer-legacy-adapter": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/polymer-legacy-adapter/-/polymer-legacy-adapter-23.3.8.tgz", + "integrity": "sha512-joAmp1nHTKSfmJfqXAxnAebFiyOXiZv4F07d5Aks38HFIZHrMUpynGYvCqaGTeIdiG2xgKXHs0wl5vJg85e5vw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/vaadin-themable-mixin": "~23.3.8", + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/progress-bar": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/progress-bar/-/progress-bar-23.3.8.tgz", + "integrity": "sha512-BNx3aOjSrVsZuZ3RMMMynPXQfnjmCM+vt2l7OoeqB0xNRswEU+DPHeAbYYms1oAkPY0y/BeTI8vijGk+1rNgGg==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/radio-group": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/radio-group/-/radio-group-23.3.8.tgz", + "integrity": "sha512-T4/SWFUHK8G+9Y/hfM28uvp083pDuxGgepTBh7f2Eh52cN/d4ehhsmEsrvzkaZdx2ZsKEJUPzmi+tIGB2qq8hA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/router": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@vaadin/router/-/router-1.7.4.tgz", + "integrity": "sha512-B4JVtzFVUMlsjuJHNXEMfNZrM4QDrdeOMc6EEigiHYxwF82py6yDdP6SWP0aPoP3f6aQHt51tLWdXSpkKpWf7A==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/vaadin-usage-statistics": "^2.1.0", + "path-to-regexp": "2.4.0" + } + }, + "node_modules/@vaadin/scroller": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/scroller/-/scroller-23.3.8.tgz", + "integrity": "sha512-HuzjgZNTTKfy56RODNC2nseJF5PP6AcqasndU1ivAzAFzf4MQmNyRImcmAcz3LeXJz7QCmhyaMhxKh6jXzLSSA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/select": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/select/-/select-23.3.8.tgz", + "integrity": "sha512-5D0/QzUE+gSRyd0kbQEi/wxjYU34dMtnrarJWSzJogpHQwsmJTLPKwjVV4qp9mq1AQzlE5Emdv0bD6/btt2EfA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.2.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/input-container": "~23.3.8", + "@vaadin/item": "~23.3.8", + "@vaadin/list-box": "~23.3.8", + "@vaadin/lit-renderer": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-list-mixin": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/split-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/split-layout/-/split-layout-23.3.8.tgz", + "integrity": "sha512-uSMtdyfglrqHE/9lrubYGXELHdGd2pzAcSc5aLXhRidV2Qg+IOGb7evq2y3sXD+UAn2Vy1nXpZmR0uB7Dn+rHA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/tabs": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/tabs/-/tabs-23.3.8.tgz", + "integrity": "sha512-kKakZ0mdO6G9K7CN/IhUD/3BbqCHBAxTHmreltw12hhoJ6ue48rqhdNPlLW+XU2a7dzcOvwn5xYGnWDpSe5hsg==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/item": "~23.3.8", + "@vaadin/vaadin-list-mixin": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/tabsheet": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/tabsheet/-/tabsheet-23.3.8.tgz", + "integrity": "sha512-x4423gjZEmibh2h0s0/2Xs/poohW+JRpGPxpK06sfzEFUc+kAIuTBFtEdk2Z8IKAs6hSCCLf0rEWDykqvk+Auw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/scroller": "~23.3.8", + "@vaadin/tabs": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/text-area": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/text-area/-/text-area-23.3.8.tgz", + "integrity": "sha512-p4NUNPtCaGFanbbSy6rdjtJuMgk0slSUhDtw4i8C/OkunpBxlYD98VTajLdWCovUXmL8cUAk4Tfs4pe6d405WA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/input-container": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/text-field": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/text-field/-/text-field-23.3.8.tgz", + "integrity": "sha512-rASUOn7pnSRZWX+td7QSBMThbPEp9iWDNkZEunhuOObGQybnSI+7v8dJUmxZDqsA7uHU6HcyVW+sfD8l0RF/cw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/input-container": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/time-picker": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/time-picker/-/time-picker-23.3.8.tgz", + "integrity": "sha512-95V2FLm1z2IcCYY1clUY9SNjEu/1mKQOl7fkBNJcjqEx2TxvUAduR3h4aU0VDamnAfSwgGI7PWSUjWjKfsmjOQ==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/combo-box": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/field-base": "~23.3.8", + "@vaadin/input-container": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/tooltip": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/tooltip/-/tooltip-23.3.8.tgz", + "integrity": "sha512-a1+pjrr4Xv/sh065wP3eLvWM8L5gs/29cWO3y1SbN3EC5xr+XtsAvn7qBTDWSlPypUNKKgwBxZjEDflJ64tgaw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/overlay": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/upload": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/upload/-/upload-23.3.8.tgz", + "integrity": "sha512-93J+IDXMKw0XTa7lZPIEF5ZQ9AvVSyxj6emTgDiMEYIuUTwPZT7Oe7iJo8pvFO/dWpQSs6ZBqcUcz3cjgXF7xA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/button": "~23.3.8", + "@vaadin/component-base": "~23.3.8", + "@vaadin/progress-bar": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-accordion": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-accordion/-/vaadin-accordion-23.3.8.tgz", + "integrity": "sha512-TpXTA204MG5sPXWbqLsCa/aeMYRVEO2HRCFksHBpT5gtxEFZ7uy7PGc2FXD3V3/PR8OgZta8ubNcEbYzZbH6Fg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/accordion": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-app-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-app-layout/-/vaadin-app-layout-23.3.8.tgz", + "integrity": "sha512-J07LoDRx/8pyWvaobLikvEW0BIwYyskwgVLep1p2s4XbN9fwqqLNKBRzeYfWlkJMF+pv6RPsJuLsYvLY70vHGg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/app-layout": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-avatar": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-avatar/-/vaadin-avatar-23.3.8.tgz", + "integrity": "sha512-iEduoqSKO09MmK6XD231L9WIBIiY1IgzZZxQtt96OIXGjFgRek9+v2jCPCPride9GIo47kxJCh6TcS2lRGG2vA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/avatar": "~23.3.8", + "@vaadin/avatar-group": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-button": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-button/-/vaadin-button-23.3.8.tgz", + "integrity": "sha512-f3RlJgdwsg7XuCIJTjhkK07yLqC/Ta/45Z3kSixRkNZeNFBTz1/HHN3OfB2vnDnhvQ6zPVqYQHeLrwh8+gQetA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/button": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-checkbox": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-checkbox/-/vaadin-checkbox-23.3.8.tgz", + "integrity": "sha512-Z4rCKBCVcKOp/58njpEEyvH5Gq13PRmrrOiFyeLOO2K3c6s3xM0E5tYycBGwaW6d25Y1Z38Olo5qN0pZa03arA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/checkbox": "~23.3.8", + "@vaadin/checkbox-group": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-combo-box": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-combo-box/-/vaadin-combo-box-23.3.8.tgz", + "integrity": "sha512-Ob9PW60B7svycRQcvnLET8OcySM+c+3ec7yG4lEDliOsJHcxqQwPRyKVIWKOLzheCdPWHY/zi4OsQYeXPXBvaQ==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/combo-box": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-confirm-dialog": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-confirm-dialog/-/vaadin-confirm-dialog-23.3.8.tgz", + "integrity": "sha512-/gIr7QT4MemjTAITar9yDAgTTL9CwPJsFw5FPDnDnmWf3dCazgrHrL0GaLK89D0e/+RR1NTe4Be0YzvUXoltug==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/confirm-dialog": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-context-menu": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-context-menu/-/vaadin-context-menu-23.3.8.tgz", + "integrity": "sha512-CL0mzcA16rsDaNrSKg9alQ3hLuDjgkALVmhzfktq9WAfg4rPvh2BdY13djKb8NUx0Br5qcaoavTOhEt1mibcMQ==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/context-menu": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-custom-field": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-custom-field/-/vaadin-custom-field-23.3.8.tgz", + "integrity": "sha512-ejvZ+W7gQZDLTmLh/cSUlSc+QQogLZa4s4uUvcxR+syznHUPg8ZQLcjYuJGzfo2nrZo/JDyI0Xkk+uRVQCg3Og==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/custom-field": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-date-picker": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-date-picker/-/vaadin-date-picker-23.3.8.tgz", + "integrity": "sha512-vKirMDoPozHgJ4k7t2n2djy41LYvfCYvDiad681kYughOt4T+R0uMFLws+WxghUoTZa2RCyKLYqp/mYzRO260w==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/date-picker": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-date-time-picker": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-date-time-picker/-/vaadin-date-time-picker-23.3.8.tgz", + "integrity": "sha512-mouw6PNgVAXvUVMIWqApRQ0z9uEUrtsGAEvsAPzc2btvruCjq/AVnihHd93bpDKd6hAWETqK977CER15jFysaA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/date-time-picker": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-details": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-details/-/vaadin-details-23.3.8.tgz", + "integrity": "sha512-+7gKrHL8+q08CcaR1Dsl6aJbKY0Ac59xn/s2grVSNBmHJkHPP1VK/g4ly9bckiBjy1QjHDp3J2Ws1qY+q2FMwg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/details": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-development-mode-detector": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-development-mode-detector/-/vaadin-development-mode-detector-2.0.6.tgz", + "integrity": "sha512-N6a5nLT/ytEUlpPo+nvdCKIGoyNjPsj3rzPGvGYK8x9Ceg76OTe1xI/GtN71mRW9e2HUScR0kCNOkl1Z63YDjw==", + "license": "Apache-2.0" + }, + "node_modules/@vaadin/vaadin-dialog": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-dialog/-/vaadin-dialog-23.3.8.tgz", + "integrity": "sha512-M/KFCeuG5u0iq/JMtoL2Q/2OI+CxO3D5g891caW1kqveZOwif5jJY1WtUjnZDOFRpv8mlvazBsVIFte3QttrsA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/dialog": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-form-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-form-layout/-/vaadin-form-layout-23.3.8.tgz", + "integrity": "sha512-ZNi9qspjH2fsqzGRR49fGbjeQIdwirAwZQvGyahF+g4RW/9emNcrmx29+SXwyP6cYzh2FzcYwmFC06Y8mnOIvg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/form-layout": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-grid": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-grid/-/vaadin-grid-23.3.8.tgz", + "integrity": "sha512-Jk2wLJqh5/MF+sNRleaZxDJj8D66wgXEMpo9Gi48IAHFCIljgR3fRw6IylnBOQ3r0ChGSftmRIAW/SRN8eMkMA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/grid": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-icon": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-icon/-/vaadin-icon-23.3.8.tgz", + "integrity": "sha512-kbVC1TO88jau7przMx2s0z+jUnXI7Xc1i6aVyuonEYe/fWal7OT8JwsGSdXxQt0gHylKn46lIBg2lNlLbi62KA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/icon": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-icons": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-icons/-/vaadin-icons-23.3.8.tgz", + "integrity": "sha512-NoJxorZ4cKrujJqIARyfOaI0AYQEqF/D6i0X7VgloD1EJEUoWD/vkarEMdIChw7ikSTzN/jreeA1RfkRffFLaQ==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/icons": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-item": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-item/-/vaadin-item-23.3.8.tgz", + "integrity": "sha512-F7uDZ9EO7Wz0Mw0gRo0kpxzVS9WmU1JEMppBrxfKzByyldolGO8XoTPKjwwxMeriN7V7jBv3dnSfc4hHfFcJfg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/item": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-list-box": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-list-box/-/vaadin-list-box-23.3.8.tgz", + "integrity": "sha512-NkKCI4FAqW59xYTdXYPKRxRiYYNJZk6/fqcclWRObtJUHc5EAF3MtzgaCH+p9FHIjsAsJkJmxKBP3fW5KcSmiw==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/list-box": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-list-mixin": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-list-mixin/-/vaadin-list-mixin-23.3.8.tgz", + "integrity": "sha512-rUNf7ntDNOfbX7oH59rFCnVX+zjsoifctcNWTI2kZ/G/MGmETWsQnKqlBOGpLYsw7CCzG6nYDxbRL/PESME6Cw==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-login": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-login/-/vaadin-login-23.3.8.tgz", + "integrity": "sha512-HrWs9+jqm5Rg+Qhca5MgsH6QdbzYyadPBJrr96MBHR584dOYHtftpW/RuRa4l0gOwRL8Gmv7YildaqxjbZgtYg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/login": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-lumo-styles": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-lumo-styles/-/vaadin-lumo-styles-23.3.8.tgz", + "integrity": "sha512-N6nfNCF/H3Vg1gemJZCeMXpWHV7TvUdqbFjKkFIipeI/VXuRXo1icAjGMQUbhs2DZnb89xsP+krRtn5lcfbeow==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/iron-icon": "^3.0.0", + "@polymer/iron-iconset-svg": "^3.0.0", + "@polymer/polymer": "^3.0.0", + "@vaadin/icon": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-material-styles": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-material-styles/-/vaadin-material-styles-23.3.8.tgz", + "integrity": "sha512-mIEngTejkEyDcgIvovIkXBEgVrH4Qx0JMSwGirjp97LRUAXTe5Lz2tzfTHRjLCccDkSdQaQNt4f9jCwhj6HcKA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-menu-bar": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-menu-bar/-/vaadin-menu-bar-23.3.8.tgz", + "integrity": "sha512-7ghRXR2bus3d5cap/iAZ5DA2RLjH7d5jg/CC3d0IvfSdUF+VALWDsB0ui36ZKNLprGHrt5iEQyY5bGSIjpMwMg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/menu-bar": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-messages": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-messages/-/vaadin-messages-23.3.8.tgz", + "integrity": "sha512-O8Pic1UV9Myh7hHCE+Oar3Fo9H3XiTZcmxgRPwsX9MFM7KPoaQ2vYfAQfclxnqSCz7B+EgCI+Yqv42YpRXQavA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/message-input": "~23.3.8", + "@vaadin/message-list": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-notification": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-notification/-/vaadin-notification-23.3.8.tgz", + "integrity": "sha512-dwqCeSwa7Y+Z7W2H2HRaYekRWiWkJIee2d0SjtVDAMZd/EnGz7uSQ+tUKoGdGi/Xvo/5YNlN9qX7cfA0OWa8lA==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/notification": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-ordered-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-ordered-layout/-/vaadin-ordered-layout-23.3.8.tgz", + "integrity": "sha512-oaL4fjVN6A0Bei56vmWWAB6TUWKGZZ+qNrH24R87FYpSG85SGkoH+B1mkv0RtmfyRxxH/jZNlnKbBVRbrtn+ag==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/horizontal-layout": "~23.3.8", + "@vaadin/scroller": "~23.3.8", + "@vaadin/vertical-layout": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-overlay": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-overlay/-/vaadin-overlay-23.3.8.tgz", + "integrity": "sha512-alRKsG5YFcD0VduOR78pcQuqOI66FiY19pRuYB+tDPIX9RTfZOYc0wS51jNdCIIJXYog6vYU7owBwGYI2ezvBw==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/overlay": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-progress-bar": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-progress-bar/-/vaadin-progress-bar-23.3.8.tgz", + "integrity": "sha512-XS49afqNmc2SJwzCdTWHhNOIjycnyL/ojo5uO0XgAMWZV7y2pBINHxSxVv+tQJk/zPtgRcEyf05nP/HSysBJWg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/progress-bar": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-radio-button": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-radio-button/-/vaadin-radio-button-23.3.8.tgz", + "integrity": "sha512-LJ6aKo1ge1oj4xN7lQDmOoTYOMor94PFRZWuxRJOQV6dlELKaAU+R4EaYCwDy1Ip2Sqs3CevrK+oBm4vLoMDLQ==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/radio-group": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-select": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-select/-/vaadin-select-23.3.8.tgz", + "integrity": "sha512-tKl6l+JcqpbJjq629EatvS5XFw7LI+kg/zQNMPTmH/1y88EciOZOPBc6vlSPKxB4D+8hIXetdOi49+iXeAt+Hw==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/select": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-split-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-split-layout/-/vaadin-split-layout-23.3.8.tgz", + "integrity": "sha512-2qc3UwdaaHeGXFNbcWB4eCcb3ygZ+R3fP3q+NcOOV3qI6tk2rvLoekdnsxjXCp3ObtHx/YngfpOtn+Xl2B4BHg==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/split-layout": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-tabs": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-tabs/-/vaadin-tabs-23.3.8.tgz", + "integrity": "sha512-FXzWtfzyp3m//MTk8qDfD9oQd7GgSAD976zs/3EIzHl2RpqO9UbKj3Y/mANoWQgFFcZidUlIfUfzrp86kfx2lw==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/tabs": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-template-renderer": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-template-renderer/-/vaadin-template-renderer-23.3.8.tgz", + "integrity": "sha512-CzF9QwaFzlXYh6lgOA3LN/+hSk4w+SIav+gb4VDzv1jsB7hDOOmQOx7TpsWY3C5RfLk+12cwnUWkuE+FNHkxnQ==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/polymer-legacy-adapter": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-text-field": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-text-field/-/vaadin-text-field-23.3.8.tgz", + "integrity": "sha512-MTrdLe055dE5FAh3vYEcqIMkPThAdJnx+Azu9WfZINB4iIXBqa5iFCLfdAJB/VEE+TvflSORH5km0bMXmmVAKw==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/email-field": "~23.3.8", + "@vaadin/integer-field": "~23.3.8", + "@vaadin/number-field": "~23.3.8", + "@vaadin/password-field": "~23.3.8", + "@vaadin/text-area": "~23.3.8", + "@vaadin/text-field": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-themable-mixin": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-themable-mixin/-/vaadin-themable-mixin-23.3.8.tgz", + "integrity": "sha512-ePzyNzxY7QqSO2hUSv0l1mjm58QhPTmD1DznKECNMIOFeOjZlw1eMVxtwTrahlvwtErLmCYIU3xRwjqB9R4Zig==", + "license": "Apache-2.0", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "lit": "^2.0.0" + } + }, + "node_modules/@vaadin/vaadin-time-picker": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-time-picker/-/vaadin-time-picker-23.3.8.tgz", + "integrity": "sha512-EheRMIZylIes7xk6tbVJ8LDNDY0ae0WObEWg47255+GnN2rYVrxObeZyLLGSqoHdhQueaKNdaYt2sk0mzDfbig==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/time-picker": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-upload": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-upload/-/vaadin-upload-23.3.8.tgz", + "integrity": "sha512-o2Oh59t9QoBW9IH9IzxpLozfH3EwID8UpZmr4VCeYxwTycLLQmvZ9cTWBoBQYIXrpa+HaTKWX+XTNFUCIHw6TQ==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/upload": "~23.3.8" + } + }, + "node_modules/@vaadin/vaadin-usage-statistics": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-usage-statistics/-/vaadin-usage-statistics-2.1.2.tgz", + "integrity": "sha512-xKs1PvRfTXsG0eWWcImLXWjv7D+f1vfoIvovppv6pZ5QX8xgcxWUdNgERlOOdGt3CTuxQXukTBW3+Qfva+OXSg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@vaadin/vaadin-development-mode-detector": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@vaadin/vaadin-virtual-list": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-virtual-list/-/vaadin-virtual-list-23.3.8.tgz", + "integrity": "sha512-Eir2bq9qGzVJ/QZZRnukW1vusM7IEQZmXEaN0Mtc4hAXu6hzbzZHhnGzsq+ouEDIFQIGP6iXz4rywB1+PUmJgw==", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/virtual-list": "~23.3.8" + } + }, + "node_modules/@vaadin/vertical-layout": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/vertical-layout/-/vertical-layout-23.3.8.tgz", + "integrity": "sha512-iIAmBA4c3ncXj85Ps/XY2fSeOWo0NAXSHvJZ5aLRrlxxTJx0D3fDOKzoyes9333FSzozHCF9E4fgAx3jgrgx6Q==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@vaadin/virtual-list": { + "version": "23.3.8", + "resolved": "https://registry.npmjs.org/@vaadin/virtual-list/-/virtual-list-23.3.8.tgz", + "integrity": "sha512-u81uYO9ElknvhotZL/sqgmbjONZ0t1w1YhCCGLpDuKCftt2KK9txmO2jmarFwj4fO3ogY4OaGvWDkU3FetPiPA==", + "license": "Apache-2.0", + "dependencies": { + "@polymer/polymer": "^3.0.0", + "@vaadin/component-base": "~23.3.8", + "@vaadin/lit-renderer": "~23.3.8", + "@vaadin/vaadin-lumo-styles": "~23.3.8", + "@vaadin/vaadin-material-styles": "~23.3.8", + "@vaadin/vaadin-themable-mixin": "~23.3.8" + } + }, + "node_modules/@webcomponents/shadycss": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.11.2.tgz", + "integrity": "sha512-vRq+GniJAYSBmTRnhCYPAPq6THYqovJ/gzGThWbgEZUQaBccndGTi1hdiUP15HzEco0I6t4RCtXyX0rsSmwgPw==", + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/construct-style-sheets-polyfill": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-3.1.0.tgz", + "integrity": "sha512-HBLKP0chz8BAY6rBdzda11c3wAZeCZ+kIG4weVC2NM3AXzxx09nhe8t0SQNdloAvg5GLuHwq/0SPOOSPvtCcKw==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dash-ast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", + "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "license": "MIT", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lit": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.6.1.tgz", + "integrity": "sha512-DT87LD64f8acR7uVp7kZfhLRrHkfC/N4BVzAtnw9Yg8087mbBJ//qedwdwX0kzDbxgPccWRW6mFwGbRQIxy0pw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.2.0", + "lit-html": "^2.6.0" + } + }, + "node_modules/lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-source-map": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", + "integrity": "sha512-PGSmS0kfnTnMJCzJ16BLLCEe6oeYCamKFFdQKshi4BmM6FUwipjVOcBFGxqtQtirtAG4iZvHlqST9CpZKqlRjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mutexify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz", + "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-tick": "^1.0.0" + } + }, + "node_modules/nanobench": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nanobench/-/nanobench-2.1.1.tgz", + "integrity": "sha512-z+Vv7zElcjN+OpzAxAquUayFLGK3JI/ubCl0Oh64YQqsTGG09CGqieJVQw4ui8huDnnAgrvTv93qi5UaOoNj8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-process-hrtime": "^0.1.2", + "chalk": "^1.1.3", + "mutexify": "^1.1.0", + "pretty-hrtime": "^1.0.2" + }, + "bin": { + "nanobench": "run.js", + "nanobench-compare": "compare.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-brotli": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-brotli/-/rollup-plugin-brotli-3.1.0.tgz", + "integrity": "sha512-vXRPVd9B1x+aaXeBdmLKNNsai9AH3o0Qikf4u0m1icKqgi3qVA4UhOfwGaPYoAHML1GLMUnR//PDhiMHXN/M6g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=11.7.0" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stringify-object/node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-css-comments": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-css-comments/-/strip-css-comments-5.0.0.tgz", + "integrity": "sha512-943vUh0ZxvxO6eK+TzY2F4nVN7a+ZdRK4KIdwHaGMvMrXTrAsJBRudOR3Zi0bLTuVSbF0CQXis4uw04uCabWkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-regexp": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/transform-ast": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/transform-ast/-/transform-ast-2.4.4.tgz", + "integrity": "sha512-AxjeZAcIOUO2lev2GDe3/xZ1Q0cVGjIMk5IsriTy8zbWlsEnjeB025AhkhBJHoy997mXpLd4R+kRbvnnQVuQHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-node": "^1.3.0", + "convert-source-map": "^1.5.1", + "dash-ast": "^1.0.0", + "is-buffer": "^2.0.0", + "magic-string": "^0.23.2", + "merge-source-map": "1.0.4", + "nanobench": "^2.1.1" + } + }, + "node_modules/transform-ast/node_modules/magic-string": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.23.2.tgz", + "integrity": "sha512-oIUZaAxbcxYIp4AyLafV6OVKoB3YouZs0UTCJ8mOKBHNyJgGDaMJ4TgA+VylJh6fx7EQCC52XkbURxxG9IoJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz", + "integrity": "sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-checker": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.5.4.tgz", + "integrity": "sha512-T6y+OHXqwOjGrCErbhzg5x79NQZV46cgLwYTxuMQnDzAfA6skh2i8PIHcKks8ZlxopzbkvMb5vwc2DpNXiHJdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "ansi-escapes": "^4.3.0", + "chalk": "^4.1.1", + "chokidar": "^3.5.1", + "commander": "^8.0.0", + "fast-glob": "^3.2.7", + "lodash.debounce": "^4.0.8", + "lodash.pick": "^4.4.0", + "npm-run-path": "^4.0.1", + "strip-ansi": "^6.0.0", + "tiny-invariant": "^1.1.0", + "vscode-languageclient": "^7.0.0", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-uri": "^3.0.2" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "eslint": ">=7", + "meow": "^9.0.0", + "optionator": "^0.9.1", + "stylelint": ">=13", + "typescript": "*", + "vite": ">=2.0.0", + "vls": "*", + "vti": "*", + "vue-tsc": "*" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vls": { + "optional": true + }, + "vti": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/vite-plugin-checker/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-plugin-checker/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/vite-plugin-checker/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/vite-plugin-checker/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-plugin-checker/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", + "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.4", + "semver": "^7.3.4", + "vscode-languageserver-protocol": "3.16.0" + }, + "engines": { + "vscode": "^1.52.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workbox-background-sync": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", + "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", + "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-build": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.4.tgz", + "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.5.4", + "workbox-broadcast-update": "6.5.4", + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-google-analytics": "6.5.4", + "workbox-navigation-preload": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-range-requests": "6.5.4", + "workbox-recipes": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4", + "workbox-streams": "6.5.4", + "workbox-sw": "6.5.4", + "workbox-window": "6.5.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", + "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-core": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.4.tgz", + "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.4.tgz", + "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-google-analytics": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", + "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "6.5.4", + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", + "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-precaching": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.4.tgz", + "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-range-requests": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", + "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-recipes": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.4.tgz", + "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-routing": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.4.tgz", + "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-strategies": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.4.tgz", + "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-streams": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.4.tgz", + "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4" + } + }, + "node_modules/workbox-sw": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.4.tgz", + "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.4.tgz", + "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.5.4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/java/demo/package.json b/java/demo/package.json new file mode 100644 index 000000000..85639e6d2 --- /dev/null +++ b/java/demo/package.json @@ -0,0 +1,360 @@ +{ + "name": "no-name", + "license": "UNLICENSED", + "dependencies": { + "@polymer/iron-icon": "3.0.1", + "@polymer/iron-iconset-svg": "3.0.1", + "@polymer/iron-list": "3.1.0", + "@polymer/iron-meta": "3.0.1", + "@polymer/iron-resizable-behavior": "3.0.1", + "@polymer/polymer": "3.5.1", + "@vaadin/accordion": "23.3.8", + "@vaadin/app-layout": "23.3.8", + "@vaadin/avatar": "23.3.8", + "@vaadin/avatar-group": "23.3.8", + "@vaadin/bundles": "23.3.8", + "@vaadin/button": "23.3.8", + "@vaadin/checkbox": "23.3.8", + "@vaadin/checkbox-group": "23.3.8", + "@vaadin/combo-box": "23.3.8", + "@vaadin/common-frontend": "0.0.17", + "@vaadin/component-base": "23.3.8", + "@vaadin/confirm-dialog": "23.3.8", + "@vaadin/context-menu": "23.3.8", + "@vaadin/custom-field": "23.3.8", + "@vaadin/date-picker": "23.3.8", + "@vaadin/date-time-picker": "23.3.8", + "@vaadin/details": "23.3.8", + "@vaadin/dialog": "23.3.8", + "@vaadin/email-field": "23.3.8", + "@vaadin/field-base": "23.3.8", + "@vaadin/field-highlighter": "23.3.8", + "@vaadin/form-layout": "23.3.8", + "@vaadin/grid": "23.3.8", + "@vaadin/horizontal-layout": "23.3.8", + "@vaadin/icon": "23.3.8", + "@vaadin/icons": "23.3.8", + "@vaadin/input-container": "23.3.8", + "@vaadin/integer-field": "23.3.8", + "@vaadin/item": "23.3.8", + "@vaadin/list-box": "23.3.8", + "@vaadin/lit-renderer": "23.3.8", + "@vaadin/login": "23.3.8", + "@vaadin/menu-bar": "23.3.8", + "@vaadin/message-input": "23.3.8", + "@vaadin/message-list": "23.3.8", + "@vaadin/multi-select-combo-box": "23.3.8", + "@vaadin/notification": "23.3.8", + "@vaadin/number-field": "23.3.8", + "@vaadin/password-field": "23.3.8", + "@vaadin/polymer-legacy-adapter": "23.3.8", + "@vaadin/progress-bar": "23.3.8", + "@vaadin/radio-group": "23.3.8", + "@vaadin/router": "1.7.4", + "@vaadin/scroller": "23.3.8", + "@vaadin/select": "23.3.8", + "@vaadin/split-layout": "23.3.8", + "@vaadin/tabs": "23.3.8", + "@vaadin/tabsheet": "23.3.8", + "@vaadin/text-area": "23.3.8", + "@vaadin/text-field": "23.3.8", + "@vaadin/time-picker": "23.3.8", + "@vaadin/tooltip": "23.3.8", + "@vaadin/upload": "23.3.8", + "@vaadin/vaadin-accordion": "23.3.8", + "@vaadin/vaadin-app-layout": "23.3.8", + "@vaadin/vaadin-avatar": "23.3.8", + "@vaadin/vaadin-button": "23.3.8", + "@vaadin/vaadin-checkbox": "23.3.8", + "@vaadin/vaadin-combo-box": "23.3.8", + "@vaadin/vaadin-confirm-dialog": "23.3.8", + "@vaadin/vaadin-context-menu": "23.3.8", + "@vaadin/vaadin-custom-field": "23.3.8", + "@vaadin/vaadin-date-picker": "23.3.8", + "@vaadin/vaadin-date-time-picker": "23.3.8", + "@vaadin/vaadin-details": "23.3.8", + "@vaadin/vaadin-development-mode-detector": "2.0.6", + "@vaadin/vaadin-dialog": "23.3.8", + "@vaadin/vaadin-form-layout": "23.3.8", + "@vaadin/vaadin-grid": "23.3.8", + "@vaadin/vaadin-icon": "23.3.8", + "@vaadin/vaadin-icons": "23.3.8", + "@vaadin/vaadin-item": "23.3.8", + "@vaadin/vaadin-list-box": "23.3.8", + "@vaadin/vaadin-list-mixin": "23.3.8", + "@vaadin/vaadin-login": "23.3.8", + "@vaadin/vaadin-lumo-styles": "23.3.8", + "@vaadin/vaadin-material-styles": "23.3.8", + "@vaadin/vaadin-menu-bar": "23.3.8", + "@vaadin/vaadin-messages": "23.3.8", + "@vaadin/vaadin-notification": "23.3.8", + "@vaadin/vaadin-ordered-layout": "23.3.8", + "@vaadin/vaadin-overlay": "23.3.8", + "@vaadin/vaadin-progress-bar": "23.3.8", + "@vaadin/vaadin-radio-button": "23.3.8", + "@vaadin/vaadin-select": "23.3.8", + "@vaadin/vaadin-split-layout": "23.3.8", + "@vaadin/vaadin-tabs": "23.3.8", + "@vaadin/vaadin-template-renderer": "23.3.8", + "@vaadin/vaadin-text-field": "23.3.8", + "@vaadin/vaadin-themable-mixin": "23.3.8", + "@vaadin/vaadin-time-picker": "23.3.8", + "@vaadin/vaadin-upload": "23.3.8", + "@vaadin/vaadin-usage-statistics": "2.1.2", + "@vaadin/vaadin-virtual-list": "23.3.8", + "@vaadin/vertical-layout": "23.3.8", + "@vaadin/virtual-list": "23.3.8", + "construct-style-sheets-polyfill": "3.1.0", + "date-fns": "2.29.3", + "lit": "2.6.1" + }, + "devDependencies": { + "@rollup/plugin-replace": "3.1.0", + "@rollup/pluginutils": "4.1.0", + "async": "3.2.2", + "glob": "7.2.3", + "mkdirp": "1.0.4", + "rollup-plugin-brotli": "3.1.0", + "strip-css-comments": "5.0.0", + "transform-ast": "2.4.4", + "typescript": "4.9.3", + "vite": "3.2.5", + "vite-plugin-checker": "0.5.4", + "workbox-build": "6.5.4", + "workbox-core": "6.5.4", + "workbox-precaching": "6.5.4" + }, + "vaadin": { + "dependencies": { + "@polymer/iron-icon": "3.0.1", + "@polymer/iron-iconset-svg": "3.0.1", + "@polymer/iron-list": "3.1.0", + "@polymer/iron-meta": "3.0.1", + "@polymer/iron-resizable-behavior": "3.0.1", + "@polymer/polymer": "3.5.1", + "@vaadin/accordion": "23.3.8", + "@vaadin/app-layout": "23.3.8", + "@vaadin/avatar": "23.3.8", + "@vaadin/avatar-group": "23.3.8", + "@vaadin/bundles": "23.3.8", + "@vaadin/button": "23.3.8", + "@vaadin/checkbox": "23.3.8", + "@vaadin/checkbox-group": "23.3.8", + "@vaadin/combo-box": "23.3.8", + "@vaadin/common-frontend": "0.0.17", + "@vaadin/component-base": "23.3.8", + "@vaadin/confirm-dialog": "23.3.8", + "@vaadin/context-menu": "23.3.8", + "@vaadin/custom-field": "23.3.8", + "@vaadin/date-picker": "23.3.8", + "@vaadin/date-time-picker": "23.3.8", + "@vaadin/details": "23.3.8", + "@vaadin/dialog": "23.3.8", + "@vaadin/email-field": "23.3.8", + "@vaadin/field-base": "23.3.8", + "@vaadin/field-highlighter": "23.3.8", + "@vaadin/form-layout": "23.3.8", + "@vaadin/grid": "23.3.8", + "@vaadin/horizontal-layout": "23.3.8", + "@vaadin/icon": "23.3.8", + "@vaadin/icons": "23.3.8", + "@vaadin/input-container": "23.3.8", + "@vaadin/integer-field": "23.3.8", + "@vaadin/item": "23.3.8", + "@vaadin/list-box": "23.3.8", + "@vaadin/lit-renderer": "23.3.8", + "@vaadin/login": "23.3.8", + "@vaadin/menu-bar": "23.3.8", + "@vaadin/message-input": "23.3.8", + "@vaadin/message-list": "23.3.8", + "@vaadin/multi-select-combo-box": "23.3.8", + "@vaadin/notification": "23.3.8", + "@vaadin/number-field": "23.3.8", + "@vaadin/password-field": "23.3.8", + "@vaadin/polymer-legacy-adapter": "23.3.8", + "@vaadin/progress-bar": "23.3.8", + "@vaadin/radio-group": "23.3.8", + "@vaadin/router": "1.7.4", + "@vaadin/scroller": "23.3.8", + "@vaadin/select": "23.3.8", + "@vaadin/split-layout": "23.3.8", + "@vaadin/tabs": "23.3.8", + "@vaadin/tabsheet": "23.3.8", + "@vaadin/text-area": "23.3.8", + "@vaadin/text-field": "23.3.8", + "@vaadin/time-picker": "23.3.8", + "@vaadin/tooltip": "23.3.8", + "@vaadin/upload": "23.3.8", + "@vaadin/vaadin-accordion": "23.3.8", + "@vaadin/vaadin-app-layout": "23.3.8", + "@vaadin/vaadin-avatar": "23.3.8", + "@vaadin/vaadin-button": "23.3.8", + "@vaadin/vaadin-checkbox": "23.3.8", + "@vaadin/vaadin-combo-box": "23.3.8", + "@vaadin/vaadin-confirm-dialog": "23.3.8", + "@vaadin/vaadin-context-menu": "23.3.8", + "@vaadin/vaadin-custom-field": "23.3.8", + "@vaadin/vaadin-date-picker": "23.3.8", + "@vaadin/vaadin-date-time-picker": "23.3.8", + "@vaadin/vaadin-details": "23.3.8", + "@vaadin/vaadin-development-mode-detector": "2.0.6", + "@vaadin/vaadin-dialog": "23.3.8", + "@vaadin/vaadin-form-layout": "23.3.8", + "@vaadin/vaadin-grid": "23.3.8", + "@vaadin/vaadin-icon": "23.3.8", + "@vaadin/vaadin-icons": "23.3.8", + "@vaadin/vaadin-item": "23.3.8", + "@vaadin/vaadin-list-box": "23.3.8", + "@vaadin/vaadin-list-mixin": "23.3.8", + "@vaadin/vaadin-login": "23.3.8", + "@vaadin/vaadin-lumo-styles": "23.3.8", + "@vaadin/vaadin-material-styles": "23.3.8", + "@vaadin/vaadin-menu-bar": "23.3.8", + "@vaadin/vaadin-messages": "23.3.8", + "@vaadin/vaadin-notification": "23.3.8", + "@vaadin/vaadin-ordered-layout": "23.3.8", + "@vaadin/vaadin-overlay": "23.3.8", + "@vaadin/vaadin-progress-bar": "23.3.8", + "@vaadin/vaadin-radio-button": "23.3.8", + "@vaadin/vaadin-select": "23.3.8", + "@vaadin/vaadin-split-layout": "23.3.8", + "@vaadin/vaadin-tabs": "23.3.8", + "@vaadin/vaadin-template-renderer": "23.3.8", + "@vaadin/vaadin-text-field": "23.3.8", + "@vaadin/vaadin-themable-mixin": "23.3.8", + "@vaadin/vaadin-time-picker": "23.3.8", + "@vaadin/vaadin-upload": "23.3.8", + "@vaadin/vaadin-usage-statistics": "2.1.2", + "@vaadin/vaadin-virtual-list": "23.3.8", + "@vaadin/vertical-layout": "23.3.8", + "@vaadin/virtual-list": "23.3.8", + "construct-style-sheets-polyfill": "3.1.0", + "date-fns": "2.29.3", + "lit": "2.6.1" + }, + "devDependencies": { + "@rollup/plugin-replace": "3.1.0", + "@rollup/pluginutils": "4.1.0", + "async": "3.2.2", + "glob": "7.2.3", + "mkdirp": "1.0.4", + "rollup-plugin-brotli": "3.1.0", + "strip-css-comments": "5.0.0", + "transform-ast": "2.4.4", + "typescript": "4.9.3", + "vite": "3.2.5", + "vite-plugin-checker": "0.5.4", + "workbox-build": "6.5.4", + "workbox-core": "6.5.4", + "workbox-precaching": "6.5.4" + }, + "hash": "b2b04023cecebe3e220ab9e309ba38e7e7d3b7732ae1121335bca86643d1daf5" + }, + "overrides": { + "@vaadin/bundles": "$@vaadin/bundles", + "@vaadin/accordion": "$@vaadin/accordion", + "@vaadin/app-layout": "$@vaadin/app-layout", + "@vaadin/avatar": "$@vaadin/avatar", + "@vaadin/avatar-group": "$@vaadin/avatar-group", + "@vaadin/button": "$@vaadin/button", + "@vaadin/checkbox": "$@vaadin/checkbox", + "@vaadin/checkbox-group": "$@vaadin/checkbox-group", + "@vaadin/combo-box": "$@vaadin/combo-box", + "@vaadin/component-base": "$@vaadin/component-base", + "@vaadin/confirm-dialog": "$@vaadin/confirm-dialog", + "@vaadin/context-menu": "$@vaadin/context-menu", + "@vaadin/custom-field": "$@vaadin/custom-field", + "@vaadin/date-picker": "$@vaadin/date-picker", + "@vaadin/date-time-picker": "$@vaadin/date-time-picker", + "@vaadin/details": "$@vaadin/details", + "@vaadin/dialog": "$@vaadin/dialog", + "@vaadin/email-field": "$@vaadin/email-field", + "@vaadin/field-base": "$@vaadin/field-base", + "@vaadin/field-highlighter": "$@vaadin/field-highlighter", + "@vaadin/form-layout": "$@vaadin/form-layout", + "@vaadin/grid": "$@vaadin/grid", + "@vaadin/horizontal-layout": "$@vaadin/horizontal-layout", + "@vaadin/icon": "$@vaadin/icon", + "@vaadin/icons": "$@vaadin/icons", + "@vaadin/input-container": "$@vaadin/input-container", + "@vaadin/integer-field": "$@vaadin/integer-field", + "@polymer/iron-icon": "$@polymer/iron-icon", + "@polymer/iron-iconset-svg": "$@polymer/iron-iconset-svg", + "@polymer/iron-list": "$@polymer/iron-list", + "@polymer/iron-meta": "$@polymer/iron-meta", + "@polymer/iron-resizable-behavior": "$@polymer/iron-resizable-behavior", + "@vaadin/item": "$@vaadin/item", + "@vaadin/list-box": "$@vaadin/list-box", + "@vaadin/lit-renderer": "$@vaadin/lit-renderer", + "@vaadin/login": "$@vaadin/login", + "@vaadin/menu-bar": "$@vaadin/menu-bar", + "@vaadin/message-input": "$@vaadin/message-input", + "@vaadin/message-list": "$@vaadin/message-list", + "@vaadin/multi-select-combo-box": "$@vaadin/multi-select-combo-box", + "@vaadin/notification": "$@vaadin/notification", + "@vaadin/number-field": "$@vaadin/number-field", + "@vaadin/password-field": "$@vaadin/password-field", + "@vaadin/polymer-legacy-adapter": "$@vaadin/polymer-legacy-adapter", + "@vaadin/progress-bar": "$@vaadin/progress-bar", + "@vaadin/radio-group": "$@vaadin/radio-group", + "@vaadin/scroller": "$@vaadin/scroller", + "@vaadin/select": "$@vaadin/select", + "@vaadin/split-layout": "$@vaadin/split-layout", + "@vaadin/tabs": "$@vaadin/tabs", + "@vaadin/tabsheet": "$@vaadin/tabsheet", + "@vaadin/text-area": "$@vaadin/text-area", + "@vaadin/text-field": "$@vaadin/text-field", + "@vaadin/time-picker": "$@vaadin/time-picker", + "@vaadin/tooltip": "$@vaadin/tooltip", + "@vaadin/upload": "$@vaadin/upload", + "@vaadin/vaadin-accordion": "$@vaadin/vaadin-accordion", + "@vaadin/vaadin-app-layout": "$@vaadin/vaadin-app-layout", + "@vaadin/vaadin-avatar": "$@vaadin/vaadin-avatar", + "@vaadin/vaadin-button": "$@vaadin/vaadin-button", + "@vaadin/vaadin-checkbox": "$@vaadin/vaadin-checkbox", + "@vaadin/vaadin-combo-box": "$@vaadin/vaadin-combo-box", + "@vaadin/vaadin-confirm-dialog": "$@vaadin/vaadin-confirm-dialog", + "@vaadin/vaadin-context-menu": "$@vaadin/vaadin-context-menu", + "@vaadin/vaadin-custom-field": "$@vaadin/vaadin-custom-field", + "@vaadin/vaadin-date-picker": "$@vaadin/vaadin-date-picker", + "@vaadin/vaadin-date-time-picker": "$@vaadin/vaadin-date-time-picker", + "@vaadin/vaadin-details": "$@vaadin/vaadin-details", + "@vaadin/vaadin-development-mode-detector": "$@vaadin/vaadin-development-mode-detector", + "@vaadin/vaadin-dialog": "$@vaadin/vaadin-dialog", + "@vaadin/vaadin-form-layout": "$@vaadin/vaadin-form-layout", + "@vaadin/vaadin-grid": "$@vaadin/vaadin-grid", + "@vaadin/vaadin-icon": "$@vaadin/vaadin-icon", + "@vaadin/vaadin-icons": "$@vaadin/vaadin-icons", + "@vaadin/vaadin-item": "$@vaadin/vaadin-item", + "@vaadin/vaadin-list-box": "$@vaadin/vaadin-list-box", + "@vaadin/vaadin-list-mixin": "$@vaadin/vaadin-list-mixin", + "@vaadin/vaadin-login": "$@vaadin/vaadin-login", + "@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles", + "@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles", + "@vaadin/vaadin-menu-bar": "$@vaadin/vaadin-menu-bar", + "@vaadin/vaadin-messages": "$@vaadin/vaadin-messages", + "@vaadin/vaadin-notification": "$@vaadin/vaadin-notification", + "@vaadin/vaadin-ordered-layout": "$@vaadin/vaadin-ordered-layout", + "@vaadin/vaadin-overlay": "$@vaadin/vaadin-overlay", + "@vaadin/vaadin-progress-bar": "$@vaadin/vaadin-progress-bar", + "@vaadin/vaadin-radio-button": "$@vaadin/vaadin-radio-button", + "@vaadin/router": "$@vaadin/router", + "@vaadin/vaadin-select": "$@vaadin/vaadin-select", + "@vaadin/vaadin-split-layout": "$@vaadin/vaadin-split-layout", + "@vaadin/vaadin-tabs": "$@vaadin/vaadin-tabs", + "@vaadin/vaadin-template-renderer": "$@vaadin/vaadin-template-renderer", + "@vaadin/vaadin-text-field": "$@vaadin/vaadin-text-field", + "@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin", + "@vaadin/vaadin-time-picker": "$@vaadin/vaadin-time-picker", + "@vaadin/vaadin-upload": "$@vaadin/vaadin-upload", + "@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics", + "@vaadin/vaadin-virtual-list": "$@vaadin/vaadin-virtual-list", + "@vaadin/vertical-layout": "$@vaadin/vertical-layout", + "@vaadin/virtual-list": "$@vaadin/virtual-list", + "@vaadin/common-frontend": "$@vaadin/common-frontend", + "construct-style-sheets-polyfill": "$construct-style-sheets-polyfill", + "lit": "$lit", + "@polymer/polymer": "$@polymer/polymer", + "date-fns": "$date-fns" + } +} diff --git a/java/demo/src/main/java/org/keychain/demo/DemoApplication.java b/java/demo/src/main/java/org/keychain/demo/DemoApplication.java new file mode 100644 index 000000000..eeef98670 --- /dev/null +++ b/java/demo/src/main/java/org/keychain/demo/DemoApplication.java @@ -0,0 +1,11 @@ +package org.keychain.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/java/demo/src/main/java/org/keychain/demo/config/KeymasterConfig.java b/java/demo/src/main/java/org/keychain/demo/config/KeymasterConfig.java new file mode 100644 index 000000000..28ba4e1fe --- /dev/null +++ b/java/demo/src/main/java/org/keychain/demo/config/KeymasterConfig.java @@ -0,0 +1,43 @@ +package org.keychain.demo.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "keymaster") +public class KeymasterConfig { + private String gatekeeperUrl; + private String registry; + private String walletDir; + private String walletFile; + + public String getGatekeeperUrl() { + return gatekeeperUrl; + } + + public void setGatekeeperUrl(String gatekeeperUrl) { + this.gatekeeperUrl = gatekeeperUrl; + } + + public String getRegistry() { + return registry; + } + + public void setRegistry(String registry) { + this.registry = registry; + } + + public String getWalletDir() { + return walletDir; + } + + public void setWalletDir(String walletDir) { + this.walletDir = walletDir; + } + + public String getWalletFile() { + return walletFile; + } + + public void setWalletFile(String walletFile) { + this.walletFile = walletFile; + } +} diff --git a/java/demo/src/main/java/org/keychain/demo/config/KeymasterConfiguration.java b/java/demo/src/main/java/org/keychain/demo/config/KeymasterConfiguration.java new file mode 100644 index 000000000..2f72d7ef1 --- /dev/null +++ b/java/demo/src/main/java/org/keychain/demo/config/KeymasterConfiguration.java @@ -0,0 +1,30 @@ +package org.keychain.demo.config; + +import java.nio.file.Path; +import org.keychain.gatekeeper.GatekeeperClient; +import org.keychain.gatekeeper.GatekeeperClientOptions; +import org.keychain.gatekeeper.GatekeeperInterface; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.store.WalletJson; +import org.keychain.keymaster.store.WalletStore; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(KeymasterConfig.class) +public class KeymasterConfiguration { + @Bean + public GatekeeperInterface gatekeeperClient(KeymasterConfig config) { + GatekeeperClientOptions options = new GatekeeperClientOptions(); + options.baseUrl = config.getGatekeeperUrl(); + return new GatekeeperClient(options); + } + + @Bean + public WalletStore walletStore(KeymasterConfig config) { + Path walletDir = Path.of(config.getWalletDir()); + return new WalletJson<>(WalletEncFile.class, walletDir, config.getWalletFile()); + } + +} diff --git a/java/demo/src/main/java/org/keychain/demo/service/KeymasterService.java b/java/demo/src/main/java/org/keychain/demo/service/KeymasterService.java new file mode 100644 index 000000000..3a60fb81a --- /dev/null +++ b/java/demo/src/main/java/org/keychain/demo/service/KeymasterService.java @@ -0,0 +1,369 @@ +package org.keychain.demo.service; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.model.CheckWalletResult; +import org.keychain.keymaster.model.FixWalletResult; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.model.WalletFile; +import org.keychain.keymaster.store.WalletJsonMemory; +import org.keychain.keymaster.store.WalletStore; +import org.keychain.demo.config.KeymasterConfig; +import org.keychain.gatekeeper.GatekeeperInterface; +import org.keychain.keymaster.Keymaster; +import org.keychain.keymaster.CreateAssetOptions; +import org.springframework.stereotype.Service; + +@Service +public class KeymasterService { + private final ObjectMapper mapper; + private final WalletStore walletStore; + private final GatekeeperInterface gatekeeper; + private final String registry; + private Keymaster keymaster; + + public KeymasterService( + WalletStore walletStore, + GatekeeperInterface gatekeeper, + KeymasterConfig config + ) { + this.walletStore = walletStore; + this.gatekeeper = gatekeeper; + this.registry = config.getRegistry(); + this.mapper = new ObjectMapper(); + this.mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + public Keymaster getKeymaster() { + return requireKeymaster(); + } + + public boolean isReady() { + return keymaster != null; + } + + public boolean hasWallet() { + return walletStore.loadWallet() != null; + } + + public void initWithPassphrase(String passphrase, boolean createIfMissing) { + if (passphrase == null || passphrase.isBlank()) { + throw new IllegalArgumentException("Passphrase is required"); + } + Keymaster instance = new Keymaster(walletStore, gatekeeper, passphrase, registry); + if (createIfMissing && walletStore.loadWallet() == null) { + instance.newWallet(null, true); + } else { + instance.loadWallet(); + } + this.keymaster = instance; + } + + public void resetWallet(String passphrase) { + if (passphrase == null || passphrase.isBlank()) { + throw new IllegalArgumentException("Passphrase is required"); + } + Keymaster instance = new Keymaster(walletStore, gatekeeper, passphrase, registry); + instance.newWallet(null, true); + this.keymaster = instance; + } + + public void initWithUploadedWallet(String passphrase, WalletEncFile wallet) { + if (passphrase == null || passphrase.isBlank()) { + throw new IllegalArgumentException("Passphrase is required"); + } + if (wallet == null || wallet.version != 1 || wallet.seed == null || wallet.seed.mnemonicEnc == null) { + throw new IllegalArgumentException("Unsupported wallet file"); + } + + WalletJsonMemory memory = new WalletJsonMemory<>(WalletEncFile.class); + memory.saveWallet(wallet, true); + Keymaster temp = new Keymaster(memory, gatekeeper, passphrase, registry); + temp.loadWallet(); + + walletStore.saveWallet(wallet, true); + Keymaster instance = new Keymaster(walletStore, gatekeeper, passphrase, registry); + instance.loadWallet(); + this.keymaster = instance; + } + + public WalletEncFile parseWalletEncFile(String json) { + try { + return mapper.readValue(json, WalletEncFile.class); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid wallet file", e); + } + } + + private Keymaster requireKeymaster() { + if (keymaster == null) { + throw new IllegalStateException("Passphrase not set"); + } + return keymaster; + } + + public String currentId() { + return requireKeymaster().getCurrentId(); + } + + public List listIds() { + return requireKeymaster().listIds(); + } + + public String createId(String name) { + return requireKeymaster().createId(name); + } + + public String createId(String name, String registry) { + return requireKeymaster().createId(name, registry); + } + + public boolean renameId(String currentName, String newName) { + return requireKeymaster().renameId(currentName, newName); + } + + public boolean removeId(String name) { + return requireKeymaster().removeId(name); + } + + public boolean backupId(String id) { + return requireKeymaster().backupId(id); + } + + public String recoverId(String did) { + return requireKeymaster().recoverId(did); + } + + public boolean rotateKeys() { + return requireKeymaster().rotateKeys(); + } + + public String validateName(String name) { + return requireKeymaster().validateName(name); + } + + public void setCurrentId(String name) { + requireKeymaster().setCurrentId(name); + } + + public MdipDocument resolveDID(String nameOrDid) { + return requireKeymaster().resolveDID(nameOrDid); + } + + public Object resolveAsset(String id) { + return requireKeymaster().resolveAsset(id); + } + + public java.util.Map listNames(boolean includeIds) { + return requireKeymaster().listNames(includeIds); + } + + public java.util.List listGroups() { + return requireKeymaster().listGroups(); + } + + public String createChallenge(java.util.Map challenge, String registry) { + if (challenge == null) { + challenge = new java.util.LinkedHashMap<>(); + } + CreateAssetOptions options = null; + if (registry != null && !registry.isBlank()) { + options = new CreateAssetOptions(); + options.registry = registry; + } + return requireKeymaster().createChallenge(challenge, options); + } + + public String createResponse(String challengeDid) { + return requireKeymaster().createResponse(challengeDid); + } + + public java.util.Map verifyResponse(String responseDid) { + return requireKeymaster().verifyResponse(responseDid); + } + + public String issueCredential(java.util.Map credential, String registry) { + org.keychain.keymaster.IssueCredentialOptions options = new org.keychain.keymaster.IssueCredentialOptions(); + if (registry != null && !registry.isBlank()) { + options.registry = registry; + } + return requireKeymaster().issueCredential(credential, options); + } + + public String createGroup(String name, String registry) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Name is required"); + } + CreateAssetOptions options = new CreateAssetOptions(); + if (registry != null && !registry.isBlank()) { + options.registry = registry; + } + options.name = name; + return requireKeymaster().createGroup(name, options); + } + + public boolean addGroupMember(String groupId, String memberId) { + return requireKeymaster().addGroupMember(groupId, memberId); + } + + public boolean removeGroupMember(String groupId, String memberId) { + return requireKeymaster().removeGroupMember(groupId, memberId); + } + + public boolean testGroup(String groupId, String memberId) { + return requireKeymaster().testGroup(groupId, memberId); + } + + public boolean addName(String name, String did) { + return requireKeymaster().addName(name, did); + } + + public boolean removeName(String name) { + return requireKeymaster().removeName(name); + } + + public java.util.List listCredentials() { + return requireKeymaster().listCredentials(null); + } + + public java.util.List listIssued() { + return requireKeymaster().listIssued(null); + } + + public boolean acceptCredential(String did) { + return requireKeymaster().acceptCredential(did); + } + + public boolean removeCredential(String did) { + return requireKeymaster().removeCredential(did); + } + + public boolean updateCredential(String did, java.util.Map credential) { + return requireKeymaster().updateCredential(did, credential); + } + + public boolean revokeCredential(String did) { + return requireKeymaster().revokeCredential(did); + } + + public Object decryptJSON(String did) { + return requireKeymaster().decryptJSON(did); + } + + public java.util.Map bindCredential(String schemaId, String subjectId) { + return requireKeymaster().bindCredential(schemaId, subjectId); + } + + public String issueCredential(java.util.Map credential) { + return requireKeymaster().issueCredential(credential); + } + + public java.util.Map publishCredential(String did, boolean reveal) { + return requireKeymaster().publishCredential(did, reveal); + } + + public String unpublishCredential(String did) { + return requireKeymaster().unpublishCredential(did); + } + + public List listSchemas() { + return requireKeymaster().listSchemas(); + } + + public Object getSchema(String did) { + return requireKeymaster().getSchema(did); + } + + public boolean setSchema(String did, Object schema) { + return requireKeymaster().setSchema(did, schema); + } + + public boolean testSchema(String did) { + return requireKeymaster().testSchema(did); + } + + public String createSchema(Object schema, String registry, String name) { + String did = registry == null || registry.isBlank() + ? requireKeymaster().createSchema(schema) + : requireKeymaster().createSchema(schema, registry); + if (name != null && !name.isBlank()) { + requireKeymaster().addName(name, did); + } + return did; + } + + public Object parseJson(String json) { + try { + return mapper.readValue(json, Object.class); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid JSON", e); + } + } + + public void sendResponse(String callbackUrl, String responseDid) { + if (callbackUrl == null || callbackUrl.isBlank()) { + throw new IllegalArgumentException("Callback URL is required"); + } + if (responseDid == null || responseDid.isBlank()) { + throw new IllegalArgumentException("Response DID is required"); + } + try { + HttpClient client = HttpClient.newHttpClient(); + String body = "{\"response\":\"" + responseDid + "\"}"; + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(callbackUrl)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + client.send(request, HttpResponse.BodyHandlers.discarding()); + } catch (Exception e) { + throw new IllegalStateException("Failed to send response", e); + } + } + + public String prettyJson(Object value) { + try { + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(value); + } catch (Exception e) { + return String.valueOf(value); + } + } + + public WalletFile loadWallet() { + return requireKeymaster().loadWallet(); + } + + public WalletFile newWallet(String mnemonic, boolean overwrite) { + return requireKeymaster().newWallet(mnemonic, overwrite); + } + + public String decryptMnemonic() { + return requireKeymaster().decryptMnemonic(); + } + + public String backupWallet() { + return requireKeymaster().backupWallet(); + } + + public WalletFile recoverWallet() { + return requireKeymaster().recoverWallet(); + } + + public CheckWalletResult checkWallet() { + return requireKeymaster().checkWallet(); + } + + public FixWalletResult fixWallet() { + return requireKeymaster().fixWallet(); + } + + public WalletEncFile exportEncryptedWallet() { + return requireKeymaster().exportEncryptedWallet(); + } +} diff --git a/java/demo/src/main/java/org/keychain/demo/ui/MainView.java b/java/demo/src/main/java/org/keychain/demo/ui/MainView.java new file mode 100644 index 000000000..b0a98b761 --- /dev/null +++ b/java/demo/src/main/java/org/keychain/demo/ui/MainView.java @@ -0,0 +1,3182 @@ +package org.keychain.demo.ui; + +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.tabs.Tab; +import com.vaadin.flow.component.tabs.Tabs; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.component.upload.Upload; +import com.vaadin.flow.component.upload.UploadI18N; +import com.vaadin.flow.component.upload.receivers.MemoryBuffer; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.StreamResource; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.keychain.demo.config.KeymasterConfig; +import org.keychain.demo.service.KeymasterService; +import org.keychain.gatekeeper.GatekeeperInterface; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.model.CheckWalletResult; +import org.keychain.keymaster.model.FixWalletResult; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.model.WalletFile; + +@Route("") +public class MainView extends VerticalLayout { + private static final String DEFAULT_SCHEMA_JSON = String.join("\n", + "{", + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",", + " \"type\": \"object\",", + " \"properties\": {", + " \"propertyName\": {", + " \"type\": \"string\"", + " }", + " },", + " \"required\": [", + " \"propertyName\"", + " ]", + "}" + ); + private final KeymasterService keymasterService; + private final GatekeeperInterface gatekeeper; + private final KeymasterConfig config; + + private final Span currentAliasValue = new Span("-"); + private final Span currentDidValue = new Span("-"); + private final ComboBox currentIdSelect = new ComboBox<>(); + private final TextArea docsArea = new TextArea(); + private final Div identitySelectorPanel = new Div(); + private final Div identityDocsPanel = new Div(); + private final HorizontalLayout identityActionsRow = new HorizontalLayout(); + private final Div identityCreatePanel = new Div(); + private final TextField createInlineNameField = new TextField("Name"); + private final ComboBox createInlineRegistrySelect = new ComboBox<>("Registry"); + private final Button createInlineButton = new Button("CREATE"); + private final Button cancelInlineButton = new Button("CANCEL"); + private final Button createIdButton = new Button("CREATE"); + private final Button renameIdButton = new Button("RENAME"); + private final Button removeIdButton = new Button("REMOVE"); + private final Button backupIdButton = new Button("BACKUP"); + private final Button recoverIdButton = new Button("RECOVER"); + private final Button rotateKeysButton = new Button("ROTATE KEYS"); + private final ComboBox schemaSelect = new ComboBox<>(); + private final Button schemaCopyButton = new Button(VaadinIcon.COPY.create()); + private final Button schemaCreateButton = new Button("CREATE"); + private final TextArea schemaArea = new TextArea(); + private final Dialog schemaCreateDialog = new Dialog(); + private final TextField schemaNameField = new TextField("Schema name"); + private final ComboBox schemaRegistrySelect = new ComboBox<>("Registry"); + private final TextArea schemaCreateArea = new TextArea("Schema JSON"); + private boolean schemaOwned = false; + private String selectedSchemaDid = null; + private final VerticalLayout schemaDetails = new VerticalLayout(); + private final TextField groupNameField = new TextField("Group name"); + private final ComboBox groupRegistrySelect = new ComboBox<>("Registry"); + private final ComboBox groupSelect = new ComboBox<>(); + private final Button groupCopyButton = new Button(VaadinIcon.COPY.create()); + private final Button groupCreateButton = new Button("CREATE"); + private final TextArea groupArea = new TextArea(); + private final VerticalLayout groupDetails = new VerticalLayout(); + private final TextField groupMemberField = new TextField("Member DID"); + private final Button groupAddButton = new Button("ADD"); + private final Button groupRemoveButton = new Button("REMOVE"); + private final Button groupTestButton = new Button("TEST"); + private final HorizontalLayout groupMemberButtonsRow = new HorizontalLayout(); + private final Div groupActionResultWrap = new Div(); + private final ComboBox groupMemberSelect = new ComboBox<>(); + private final Span groupActionResult = new Span(); + private String selectedGroupDid = null; + private java.util.List agentNames = new java.util.ArrayList<>(); + private java.util.List schemaNames = new java.util.ArrayList<>(); + private java.util.List groupNames = new java.util.ArrayList<>(); + private final TextField authChallengeField = new TextField(); + private final TextField authResponseField = new TextField(); + private final Span authDidValue = new Span(); + private final TextArea authStringArea = new TextArea(); + private final Dialog authChallengeDialog = new Dialog(); + private final TextArea authChallengeJsonArea = new TextArea(); + private final Button authNewButton = new Button("NEW"); + private final Button authResolveButton = new Button("RESOLVE"); + private final Button authRespondButton = new Button("RESPOND"); + private final Button authClearChallengeButton = new Button("CLEAR"); + private final Button authDecryptButton = new Button("DECRYPT"); + private final Button authVerifyButton = new Button("VERIFY"); + private final Button authSendButton = new Button("SEND"); + private final Button authClearResponseButton = new Button("CLEAR"); + private boolean disableSendResponse = true; + private String callbackUrl = null; + private final TextField didNameField = new TextField("Name"); + private final TextField didValueField = new TextField("DID"); + private final ComboBox didSelect = new ComboBox<>(); + private final TextArea didDocsArea = new TextArea(); + private java.util.Map didNameMap = new java.util.HashMap<>(); + private java.util.Map didToName = new java.util.HashMap<>(); + private java.util.Map manifest = new java.util.HashMap<>(); + private final Button didResolveButton = new Button("RESOLVE"); + private final Button didAddButton = new Button("ADD"); + private final Button didResolveSelectedButton = new Button("RESOLVE"); + private final Button didRemoveButton = new Button("REMOVE"); + private final Button didCopyButton = new Button(VaadinIcon.COPY.create()); + private final ComboBox heldSelect = new ComboBox<>(); + private final TextArea heldArea = new TextArea(); + private String selectedHeldDid = null; + private final TextField heldDidField = new TextField("Credential DID"); + private final Button heldResolveFieldButton = new Button("RESOLVE"); + private final Button heldDecryptFieldButton = new Button("DECRYPT"); + private final Button heldAcceptFieldButton = new Button("ACCEPT"); + private final Button heldResolveSelectedButton = new Button("RESOLVE"); + private final Button heldDecryptSelectedButton = new Button("DECRYPT"); + private final Button heldCopyButton = new Button(VaadinIcon.COPY.create()); + private final Button heldRemoveButton = new Button("REMOVE"); + private final Button heldPublishButton = new Button("PUBLISH"); + private final Button heldRevealButton = new Button("REVEAL"); + private final Button heldUnpublishButton = new Button("UNPUBLISH"); + private final ComboBox issuedSelect = new ComboBox<>(); + private final Button issuedCopyButton = new Button(VaadinIcon.COPY.create()); + private final TextArea issuedArea = new TextArea(); + private String selectedIssuedDid = null; + private String issuedOriginal = ""; + private boolean issuedEditable = false; + private final ComboBox issueSubjectSelect = new ComboBox<>(); + private final ComboBox issueSchemaSelect = new ComboBox<>(); + private final ComboBox issueRegistrySelect = new ComboBox<>(); + private final TextArea issueArea = new TextArea(); + private final Span issueResult = new Span(); + private final Button issueEditButton = new Button("EDIT"); + private final Button issueButton = new Button("ISSUE"); + + private final Dialog createDialog = new Dialog(); + private final TextField createNameField = new TextField("Name"); + private final ComboBox createRegistrySelect = new ComboBox<>("Registry"); + private final Dialog renameDialog = new Dialog(); + private final TextField renameField = new TextField("New name"); + private final Dialog recoverDialog = new Dialog(); + private final TextField recoverField = new TextField("DID"); + private final ConfirmDialog removeConfirm = new ConfirmDialog(); + private final ConfirmDialog newWalletConfirm = new ConfirmDialog(); + private final ConfirmDialog recoverWalletConfirm = new ConfirmDialog(); + private final ConfirmDialog fixWalletConfirm = new ConfirmDialog(); + private final Dialog importDialog = new Dialog(); + private final TextArea importMnemonic = new TextArea("Mnemonic"); + private final Dialog mnemonicDialog = new Dialog(); + private final TextArea mnemonicArea = new TextArea("Mnemonic"); + private final Dialog walletDialog = new Dialog(); + private final TextArea walletArea = new TextArea("Wallet"); + private final Dialog passphraseDialog = new Dialog(); + private final PasswordField passphraseField = new PasswordField("Passphrase"); + private final PasswordField passphraseConfirmField = new PasswordField("Confirm passphrase"); + private final Span passphraseHint = new Span(); + private final Span passphraseError = new Span(); + private final Button passphraseSubmit = new Button(); + private final Button passphraseCancel = new Button("Cancel"); + private boolean passphraseCreateMode = false; + private boolean passphraseResetMode = false; + private Anchor downloadAnchor; + private final Button checkWalletButton = new Button("CHECK"); + private final MemoryBuffer uploadBuffer = new MemoryBuffer(); + private final Upload upload = new Upload(uploadBuffer); + private WalletEncFile pendingWallet = null; + private boolean refreshing = false; + private String currentIdName = null; + private boolean ready = false; + private boolean hasCurrentId = false; + private final Tabs tabs = new Tabs(); + private final Tab identitiesTab = new Tab(VaadinIcon.USER.create(), new Span("IDENTITIES")); + private final Tab didsTab = new Tab(VaadinIcon.LIST.create(), new Span("DIDS")); + private final Tab schemasTab = new Tab(VaadinIcon.CLIPBOARD_TEXT.create(), new Span("SCHEMAS")); + private final Tab groupsTab = new Tab(VaadinIcon.GROUP.create(), new Span("GROUPS")); + private final Tab credentialsTab = new Tab(VaadinIcon.DIPLOMA.create(), new Span("CREDENTIALS")); + private final Tab authTab = new Tab(VaadinIcon.KEY.create(), new Span("AUTH")); + private final Tab walletTab = new Tab(VaadinIcon.WALLET.create(), new Span("WALLET")); + private final VerticalLayout identityContent = new VerticalLayout(); + private final VerticalLayout didsContent = new VerticalLayout(); + private final VerticalLayout schemasContent = new VerticalLayout(); + private final VerticalLayout groupsContent = new VerticalLayout(); + private final VerticalLayout credentialsContent = new VerticalLayout(); + private final VerticalLayout authContent = new VerticalLayout(); + private final Tabs credentialTabs = new Tabs(); + private final Tab heldTab = new Tab("HELD"); + private final Tab issueTab = new Tab("ISSUE"); + private final Tab issuedTab = new Tab("ISSUED"); + private final VerticalLayout heldContent = new VerticalLayout(); + private final VerticalLayout issueContent = new VerticalLayout(); + private final VerticalLayout issuedContent = new VerticalLayout(); + private final VerticalLayout walletContent = new VerticalLayout(); + + public MainView(KeymasterService keymasterService, GatekeeperInterface gatekeeper, KeymasterConfig config) { + this.keymasterService = keymasterService; + this.gatekeeper = gatekeeper; + this.config = config; + + setSpacing(true); + setPadding(true); + setSizeFull(); + setMaxWidth("980px"); + + currentAliasValue.getStyle().set("font-weight", "600"); + + add(header()); + add(idLine()); + add(tabsRow()); + add(contentPanel()); + + configureCreateDialog(); + configureRenameDialog(); + configureRecoverDialog(); + configureRemoveConfirm(); + configureNewWalletConfirm(); + configureRecoverWalletConfirm(); + configureFixWalletConfirm(); + configureImportDialog(); + configureMnemonicDialog(); + configureWalletDialog(); + configureSchemaCreateDialog(); + configurePassphraseDialog(); + configureAuthChallengeDialog(); + setReady(false); + openPassphraseDialog(); + refresh(); + } + + private Div header() { + Div header = new Div(); + H2 title = new H2("Keymaster Browser Wallet Demo"); + title.getStyle() + .set("margin-top", "0") + .set("margin-bottom", "8px"); + header.add(title); + return header; + } + + private Div idLine() { + Div line = new Div(); + currentAliasValue.getStyle().set("margin-left", "16px"); + currentDidValue.getStyle().set("margin-left", "16px"); + line.add(new Text("ID:"), currentAliasValue, currentDidValue); + return line; + } + + private Tabs tabsRow() { + tabs.add(identitiesTab, didsTab, schemasTab, groupsTab, credentialsTab, authTab, walletTab); + tabs.setSelectedTab(identitiesTab); + tabs.addSelectedChangeListener(event -> updateTabVisibility()); + return tabs; + } + + private VerticalLayout contentPanel() { + identityContent.setPadding(false); + identityContent.setSpacing(true); + identityContent.add(identityCreatePanel(), identitySelector(), actionsRow(), docsPanel()); + + didsContent.setPadding(false); + didsContent.setSpacing(true); + didsContent.setVisible(false); + didsContent.add(didsActionsRow(), didsSelectorRow(), didsDocsPanel()); + + schemasContent.setPadding(false); + schemasContent.setSpacing(true); + schemasContent.setVisible(false); + schemasContent.add(schemaHeaderRow(), schemaDetailsPanel()); + + schemaSelect.addValueChangeListener(event -> { + if (refreshing) { + return; + } + String value = event.getValue(); + if (value != null && !value.isBlank()) { + selectSchema(value); + } + updateSchemaSelectionState(); + }); + + groupsContent.setPadding(false); + groupsContent.setSpacing(true); + groupsContent.setVisible(false); + groupsContent.add(groupHeaderRow(), groupDetailsPanel()); + + groupSelect.addValueChangeListener(event -> { + if (refreshing) { + return; + } + String value = event.getValue(); + if (value != null && !value.isBlank()) { + selectGroup(value); + } + updateGroupSelectionState(); + }); + + credentialsContent.setPadding(false); + credentialsContent.setSpacing(true); + credentialsContent.setVisible(false); + credentialsContent.add(credentialsTabsRow(), credentialsPanel()); + + authContent.setPadding(false); + authContent.setSpacing(true); + authContent.setVisible(false); + authContent.add(authPanel()); + + walletContent.setPadding(false); + walletContent.setSpacing(true); + walletContent.setVisible(false); + walletContent.add(walletActionsRow(), walletActionsRowTwo()); + + VerticalLayout panel = new VerticalLayout( + identityContent, + didsContent, + schemasContent, + groupsContent, + credentialsContent, + authContent, + walletContent + ); + panel.setPadding(false); + panel.setSpacing(true); + return panel; + } + + private Div identitySelector() { + identitySelectorPanel.removeAll(); + currentIdSelect.setWidth("320px"); + currentIdSelect.setPlaceholder("Select identity"); + currentIdSelect.getElement().getStyle().set("--lumo-contrast-20pct", "#666"); + currentIdSelect.getElement().getStyle().set("--lumo-contrast-30pct", "#555"); + currentIdSelect.getElement().getStyle().set("border", "1px solid #666"); + currentIdSelect.getElement().getStyle().set("border-radius", "6px"); + currentIdSelect.addValueChangeListener(event -> { + if (refreshing) { + return; + } + String value = event.getValue(); + if (value != null && !value.isBlank()) { + setCurrentId(value); + } + }); + identitySelectorPanel.add(currentIdSelect); + return identitySelectorPanel; + } + + private Div identityCreatePanel() { + identityCreatePanel.removeAll(); + createInlineNameField.setWidth("320px"); + createInlineRegistrySelect.setWidth("220px"); + + createInlineButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + cancelInlineButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + createInlineButton.addClickListener(event -> submitCreateInline()); + cancelInlineButton.addClickListener(event -> clearInlineCreate()); + + HorizontalLayout fields = new HorizontalLayout(createInlineNameField, createInlineRegistrySelect); + HorizontalLayout actions = new HorizontalLayout(createInlineButton, cancelInlineButton); + identityCreatePanel.add(fields, actions); + return identityCreatePanel; + } + + private HorizontalLayout actionsRow() { + identityActionsRow.removeAll(); + createIdButton.addClickListener(event -> openCreateDialog()); + renameIdButton.addClickListener(event -> openRenameDialog()); + removeIdButton.addClickListener(event -> confirmRemove()); + backupIdButton.addClickListener(event -> backupId()); + recoverIdButton.addClickListener(event -> openRecoverDialog()); + rotateKeysButton.addClickListener(event -> rotateKeys()); + + createIdButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + renameIdButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + removeIdButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + backupIdButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + recoverIdButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + rotateKeysButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + identityActionsRow.add( + createIdButton, + renameIdButton, + removeIdButton, + backupIdButton, + recoverIdButton, + rotateKeysButton + ); + identityActionsRow.setSpacing(true); + return identityActionsRow; + } + + private Div docsPanel() { + identityDocsPanel.removeAll(); + docsArea.setWidth("800px"); + docsArea.setHeight("600px"); + docsArea.setReadOnly(true); + identityDocsPanel.add(docsArea); + return identityDocsPanel; + } + + private HorizontalLayout didsActionsRow() { + didNameField.setWidth("200px"); + didValueField.setWidth("420px"); + didResolveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + didAddButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + didResolveButton.addClickListener(event -> resolveDidInput()); + didAddButton.addClickListener(event -> addDidName()); + didNameField.setValueChangeMode(ValueChangeMode.EAGER); + didValueField.setValueChangeMode(ValueChangeMode.EAGER); + didNameField.addValueChangeListener(event -> updateDidActionsState()); + didValueField.addValueChangeListener(event -> updateDidActionsState()); + updateDidActionsState(); + + HorizontalLayout row = new HorizontalLayout(didNameField, didValueField, didResolveButton, didAddButton); + row.setSpacing(true); + row.setAlignItems(Alignment.END); + return row; + } + + private HorizontalLayout didsSelectorRow() { + didSelect.setWidth("520px"); + didSelect.setPlaceholder("Select named DID"); + didSelect.addValueChangeListener(event -> { + if (refreshing) { + return; + } + updateDidSelectionState(); + }); + wireCopyButton(didCopyButton, this::copySelectedDid); + didResolveSelectedButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + didRemoveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + didResolveSelectedButton.addClickListener(event -> resolveSelectedName()); + didRemoveButton.addClickListener(event -> removeSelectedName()); + updateDidSelectionState(); + + HorizontalLayout row = new HorizontalLayout(didSelect, didCopyButton, didResolveSelectedButton, didRemoveButton); + row.setSpacing(true); + row.setAlignItems(Alignment.END); + return row; + } + + private Div didsDocsPanel() { + Div panel = new Div(); + didDocsArea.setWidth("800px"); + didDocsArea.setHeight("600px"); + didDocsArea.setReadOnly(true); + panel.add(didDocsArea); + return panel; + } + + private HorizontalLayout schemaActionsRow() { + Button test = new Button("TEST", event -> testSchema()); + Button save = new Button("SAVE", event -> saveSchema()); + + test.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + HorizontalLayout row = new HorizontalLayout(test, save); + row.setSpacing(true); + return row; + } + + private Div schemaSelector() { + Div panel = new Div(); + schemaSelect.setWidth("520px"); + schemaSelect.setPlaceholder("Select schema"); + schemaSelect.addValueChangeListener(event -> { + if (refreshing) { + return; + } + String value = event.getValue(); + if (value != null && !value.isBlank()) { + selectSchema(value); + } + }); + panel.add(schemaSelect); + return panel; + } + + private Div schemaViewer() { + Div panel = new Div(); + schemaArea.setWidth("800px"); + schemaArea.setHeight("420px"); + schemaArea.setReadOnly(true); + panel.add(schemaArea); + return panel; + } + + private HorizontalLayout schemaHeaderRow() { + schemaNameField.setWidth("320px"); + schemaRegistrySelect.setWidth("220px"); + schemaSelect.setWidth("320px"); + schemaSelect.setPlaceholder("Select schema"); + + schemaCreateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + schemaCreateButton.addClickListener(event -> openSchemaCreateDialog()); + schemaCreateButton.setEnabled(false); + schemaNameField.setValueChangeMode(ValueChangeMode.EAGER); + schemaNameField.addValueChangeListener(event -> updateSchemaCreateState()); + + HorizontalLayout top = new HorizontalLayout(schemaNameField, schemaCreateButton, schemaRegistrySelect); + top.setSpacing(true); + top.setAlignItems(Alignment.END); + + wireCopyButton(schemaCopyButton, this::copySchemaDid); + HorizontalLayout bottom = new HorizontalLayout(schemaSelect, schemaCopyButton); + bottom.setSpacing(true); + + VerticalLayout panel = new VerticalLayout(top, bottom); + panel.setPadding(false); + panel.setSpacing(true); + HorizontalLayout row = new HorizontalLayout(panel); + row.setPadding(false); + return row; + } + + private VerticalLayout schemaDetailsPanel() { + schemaDetails.removeAll(); + schemaDetails.setPadding(false); + schemaDetails.setSpacing(true); + schemaDetails.setVisible(false); + schemaDetails.add(schemaActionsRow(), schemaViewer()); + return schemaDetails; + } + + private HorizontalLayout groupHeaderRow() { + groupNameField.setWidth("320px"); + groupRegistrySelect.setWidth("220px"); + groupSelect.setWidth("320px"); + groupSelect.setPlaceholder("Select group"); + + groupCreateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + groupCreateButton.addClickListener(event -> createGroup()); + groupCreateButton.setEnabled(false); + groupNameField.setValueChangeMode(ValueChangeMode.EAGER); + groupNameField.addValueChangeListener(event -> updateGroupCreateState()); + + HorizontalLayout top = new HorizontalLayout(groupNameField, groupCreateButton, groupRegistrySelect); + top.setSpacing(true); + top.setAlignItems(Alignment.END); + + wireCopyButton(groupCopyButton, this::copyGroupDid); + HorizontalLayout bottom = new HorizontalLayout(groupSelect, groupCopyButton); + bottom.setSpacing(true); + + VerticalLayout panel = new VerticalLayout(top, bottom); + panel.setPadding(false); + panel.setSpacing(true); + HorizontalLayout row = new HorizontalLayout(panel); + row.setPadding(false); + return row; + } + + private VerticalLayout groupDetailsPanel() { + groupArea.setWidth("800px"); + groupArea.setHeight("420px"); + groupArea.setReadOnly(true); + + groupDetails.removeAll(); + groupDetails.setPadding(false); + groupDetails.setSpacing(true); + groupDetails.setVisible(false); + groupDetails.add(groupMemberActionsRow(), groupArea); + return groupDetails; + } + + private VerticalLayout groupMemberActionsRow() { + groupMemberField.setWidth("520px"); + groupMemberSelect.setWidth("520px"); + groupMemberSelect.setPlaceholder("Select member"); + groupMemberField.setValueChangeMode(ValueChangeMode.EAGER); + groupMemberField.addValueChangeListener(event -> updateGroupMemberActionState()); + groupMemberSelect.addValueChangeListener(event -> updateGroupMemberActionState()); + + groupAddButton.addClickListener(event -> addGroupMember()); + groupRemoveButton.addClickListener(event -> removeGroupMember()); + groupTestButton.addClickListener(event -> testGroupMember()); + + groupAddButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + groupRemoveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + groupTestButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + groupActionResultWrap.removeAll(); + groupActionResultWrap.add(groupActionResult); + groupActionResultWrap.getStyle().set("display", "flex"); + groupActionResultWrap.getStyle().set("align-items", "center"); + groupActionResultWrap.setHeight("var(--lumo-size-m)"); + + HorizontalLayout addTestRow = new HorizontalLayout(groupMemberField, groupAddButton, groupTestButton, groupActionResultWrap); + addTestRow.setSpacing(true); + addTestRow.setAlignItems(Alignment.END); + + groupMemberButtonsRow.removeAll(); + groupMemberButtonsRow.add(groupMemberSelect, groupRemoveButton); + groupMemberButtonsRow.setSpacing(true); + groupMemberButtonsRow.setAlignItems(Alignment.END); + + VerticalLayout panel = new VerticalLayout(addTestRow, groupMemberButtonsRow); + panel.setPadding(false); + panel.setSpacing(true); + return panel; + } + + private Tabs credentialsTabsRow() { + credentialTabs.add(heldTab, issueTab, issuedTab); + credentialTabs.setSelectedTab(heldTab); + credentialTabs.addSelectedChangeListener(event -> updateCredentialTabVisibility()); + return credentialTabs; + } + + private VerticalLayout credentialsPanel() { + heldContent.setPadding(false); + heldContent.setSpacing(true); + heldContent.add(heldActionsRow(), heldSelector(), heldViewer()); + + issueContent.setPadding(false); + issueContent.setSpacing(true); + issueContent.setVisible(false); + issueContent.add(issueSelectorsRow(), issueEditor(), issueActionsRow()); + + issuedContent.setPadding(false); + issuedContent.setSpacing(true); + issuedContent.setVisible(false); + issuedContent.add(issuedSelector(), issuedActionsRow(), issuedViewer()); + + VerticalLayout panel = new VerticalLayout(heldContent, issueContent, issuedContent); + panel.setPadding(false); + panel.setSpacing(true); + return panel; + } + + private VerticalLayout authPanel() { + authChallengeField.setWidth("540px"); + authResponseField.setWidth("540px"); + authChallengeField.getStyle().set("font-family", "monospace"); + authResponseField.getStyle().set("font-family", "monospace"); + authChallengeField.getStyle().set("font-size", "0.8em"); + authResponseField.getStyle().set("font-size", "0.8em"); + authChallengeField.setValueChangeMode(ValueChangeMode.EAGER); + authResponseField.setValueChangeMode(ValueChangeMode.EAGER); + + authStringArea.setWidth("800px"); + authStringArea.setHeight("600px"); + authStringArea.setReadOnly(true); + + authNewButton.addClickListener(event -> openAuthChallengeDialog()); + authResolveButton.addClickListener(event -> resolveChallenge(authChallengeField.getValue())); + authRespondButton.addClickListener(event -> createResponse()); + authClearChallengeButton.addClickListener(event -> clearChallenge()); + + authNewButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + authResolveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + authRespondButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + authClearChallengeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + HorizontalLayout challengeActions = new HorizontalLayout( + authNewButton, + authResolveButton, + authRespondButton, + authClearChallengeButton + ); + challengeActions.setSpacing(true); + + VerticalLayout challengeBox = new VerticalLayout(authChallengeField, challengeActions); + challengeBox.setPadding(false); + challengeBox.setSpacing(true); + + authDecryptButton.addClickListener(event -> decryptResponse(authResponseField.getValue())); + authVerifyButton.addClickListener(event -> verifyResponse()); + authSendButton.addClickListener(event -> sendResponse()); + authClearResponseButton.addClickListener(event -> clearResponse()); + + authDecryptButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + authVerifyButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + authSendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + authClearResponseButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + HorizontalLayout responseActions = new HorizontalLayout( + authDecryptButton, + authVerifyButton, + authSendButton, + authClearResponseButton + ); + responseActions.setSpacing(true); + + VerticalLayout responseBox = new VerticalLayout(authResponseField, responseActions); + responseBox.setPadding(false); + responseBox.setSpacing(true); + + HorizontalLayout challengeRow = new HorizontalLayout(new Span("Challenge"), challengeBox); + challengeRow.setWidthFull(); + challengeRow.setAlignItems(Alignment.START); + challengeRow.getStyle().set("gap", "24px"); + + HorizontalLayout responseRow = new HorizontalLayout(new Span("Response"), responseBox); + responseRow.setWidthFull(); + responseRow.setAlignItems(Alignment.START); + responseRow.getStyle().set("gap", "24px"); + + authChallengeField.addValueChangeListener(event -> updateAuthButtons()); + authResponseField.addValueChangeListener(event -> updateAuthButtons()); + + VerticalLayout panel = new VerticalLayout( + challengeRow, + responseRow, + authDidValue, + authStringArea + ); + panel.setPadding(false); + panel.setSpacing(true); + updateAuthButtons(); + return panel; + } + + private HorizontalLayout heldActionsRow() { + heldDidField.setWidth("520px"); + heldResolveFieldButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + heldDecryptFieldButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + heldAcceptFieldButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + heldResolveFieldButton.addClickListener(event -> resolveHeldInput()); + heldDecryptFieldButton.addClickListener(event -> decryptHeldInput()); + heldAcceptFieldButton.addClickListener(event -> acceptHeldInput()); + heldDidField.setValueChangeMode(ValueChangeMode.EAGER); + heldDidField.addValueChangeListener(event -> updateHeldInputState()); + updateHeldInputState(); + + HorizontalLayout row = new HorizontalLayout( + heldDidField, + heldResolveFieldButton, + heldDecryptFieldButton, + heldAcceptFieldButton + ); + row.setSpacing(true); + row.setAlignItems(Alignment.END); + return row; + } + + private Div heldSelector() { + Div panel = new Div(); + heldSelect.setWidth("520px"); + heldSelect.setPlaceholder("Select held credential"); + heldSelect.addValueChangeListener(event -> { + if (refreshing) { + return; + } + String value = event.getValue(); + if (value != null && !value.isBlank()) { + selectedHeldDid = value; + heldArea.clear(); + updateHeldSelectionState(); + } else { + selectedHeldDid = null; + heldArea.clear(); + updateHeldSelectionState(); + } + }); + heldResolveSelectedButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + heldDecryptSelectedButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + heldRemoveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + heldPublishButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + heldRevealButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + heldUnpublishButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + heldResolveSelectedButton.addClickListener(event -> resolveHeldSelected()); + heldDecryptSelectedButton.addClickListener(event -> decryptHeldSelected()); + wireCopyButton(heldCopyButton, this::copyHeldDid); + heldRemoveButton.addClickListener(event -> removeHeld()); + heldPublishButton.addClickListener(event -> publishHeld(false)); + heldRevealButton.addClickListener(event -> publishHeld(true)); + heldUnpublishButton.addClickListener(event -> unpublishHeld()); + + HorizontalLayout selectRow = new HorizontalLayout(heldSelect, heldCopyButton); + selectRow.setSpacing(true); + + HorizontalLayout actionsRow = new HorizontalLayout( + heldResolveSelectedButton, + heldDecryptSelectedButton, + heldRemoveButton, + heldPublishButton, + heldRevealButton, + heldUnpublishButton + ); + actionsRow.setSpacing(true); + actionsRow.setAlignItems(Alignment.END); + panel.add(selectRow, actionsRow); + return panel; + } + + private Div heldViewer() { + Div panel = new Div(); + heldArea.setWidth("800px"); + heldArea.setHeight("600px"); + heldArea.setReadOnly(true); + panel.add(heldArea); + return panel; + } + + private HorizontalLayout issuedActionsRow() { + Button resolve = new Button("RESOLVE", event -> resolveIssued()); + Button decrypt = new Button("DECRYPT", event -> decryptIssued()); + Button update = new Button("UPDATE", event -> updateIssued()); + Button revoke = new Button("REVOKE", event -> revokeIssued()); + + resolve.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + decrypt.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + update.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + revoke.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + HorizontalLayout row = new HorizontalLayout(resolve, decrypt, update, revoke); + row.setSpacing(true); + return row; + } + + private Div issuedSelector() { + Div panel = new Div(); + issuedSelect.setWidth("520px"); + issuedSelect.setPlaceholder("Select issued credential"); + issuedSelect.addValueChangeListener(event -> { + if (refreshing) { + return; + } + String value = event.getValue(); + if (value != null && !value.isBlank()) { + selectedIssuedDid = value; + } + updateIssuedSelectionState(); + }); + wireCopyButton(issuedCopyButton, this::copyIssuedDid); + HorizontalLayout selectRow = new HorizontalLayout(issuedSelect, issuedCopyButton); + selectRow.setSpacing(true); + panel.add(selectRow); + return panel; + } + + private Div issuedViewer() { + Div panel = new Div(); + issuedArea.setWidth("800px"); + issuedArea.setHeight("600px"); + issuedArea.setReadOnly(true); + panel.add(issuedArea); + return panel; + } + + private HorizontalLayout issueSelectorsRow() { + issueSubjectSelect.setWidth("280px"); + issueSubjectSelect.setPlaceholder("Select subject"); + issueSchemaSelect.setWidth("280px"); + issueSchemaSelect.setPlaceholder("Select schema"); + + issueSubjectSelect.addValueChangeListener(event -> updateIssueButtons()); + issueSchemaSelect.addValueChangeListener(event -> updateIssueButtons()); + + issueEditButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + issueEditButton.addClickListener(event -> editCredential()); + issueEditButton.setEnabled(false); + + HorizontalLayout row = new HorizontalLayout(issueSubjectSelect, issueSchemaSelect, issueEditButton); + row.setSpacing(true); + return row; + } + + private Div issueEditor() { + Div panel = new Div(); + issueArea.setWidth("800px"); + issueArea.setHeight("600px"); + issueArea.setValueChangeMode(ValueChangeMode.EAGER); + issueArea.addValueChangeListener(event -> updateIssueButtons()); + panel.add(issueArea); + return panel; + } + + private HorizontalLayout issueActionsRow() { + issueButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + issueButton.addClickListener(event -> issueCredential()); + issueButton.setEnabled(false); + + issueRegistrySelect.setWidth("220px"); + HorizontalLayout row = new HorizontalLayout(issueButton, issueRegistrySelect, issueResult); + row.setSpacing(true); + return row; + } + + private HorizontalLayout walletActionsRow() { + Button newWallet = new Button("NEW", event -> confirmNewWallet()); + Button importWallet = new Button("IMPORT", event -> openImportDialog()); + Button backupWallet = new Button("BACKUP", event -> backupWallet()); + Button recoverWallet = new Button("RECOVER", event -> confirmRecoverWallet()); + checkWalletButton.addClickListener(event -> checkWallet()); + + newWallet.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + importWallet.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + backupWallet.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + recoverWallet.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + checkWalletButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + HorizontalLayout row = new HorizontalLayout(newWallet, importWallet, backupWallet, recoverWallet, checkWalletButton); + row.setSpacing(true); + return row; + } + + private HorizontalLayout walletActionsRowTwo() { + Button showMnemonic = new Button("SHOW MNEMONIC", event -> showMnemonic()); + Button showWallet = new Button("SHOW WALLET", event -> showWallet()); + Button downloadButton = new Button("DOWNLOAD", event -> prepareWalletDownload()); + Button uploadButton = new Button("UPLOAD"); + + showMnemonic.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + showWallet.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + downloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + uploadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + downloadAnchor = new Anchor(); + downloadAnchor.getElement().setAttribute("download", true); + downloadAnchor.getStyle().set("text-decoration", "none"); + downloadAnchor.add(downloadButton); + + upload.setAcceptedFileTypes("application/json"); + upload.setMaxFiles(1); + upload.setAutoUpload(true); + upload.setDropAllowed(false); + upload.setUploadButton(uploadButton); + upload.getElement().getStyle().set("border", "0"); + upload.getElement().getStyle().set("padding", "0"); + upload.getElement().getStyle().set("display", "inline-flex"); + upload.getElement().getStyle().set("align-items", "center"); + UploadI18N i18n = new UploadI18N(); + i18n.setDropFiles(new UploadI18N.DropFiles().setOne("").setMany("")); + i18n.setAddFiles(new UploadI18N.AddFiles().setOne("Upload").setMany("Upload")); + upload.setI18n(i18n); + upload.addSucceededListener(event -> handleWalletUpload()); + + HorizontalLayout row = new HorizontalLayout(showMnemonic, showWallet, downloadAnchor, upload); + row.setSpacing(true); + return row; + } + + private void configureCreateDialog() { + createDialog.setHeaderTitle("Create ID"); + createDialog.setCloseOnOutsideClick(false); + createDialog.setCloseOnEsc(true); + + createNameField.setWidth("280px"); + createRegistrySelect.setWidth("220px"); + + Button create = new Button("Create", event -> submitCreate()); + create.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + Button cancel = new Button("Cancel", event -> createDialog.close()); + + HorizontalLayout fields = new HorizontalLayout(createNameField, createRegistrySelect); + HorizontalLayout actions = new HorizontalLayout(create, cancel); + + createDialog.add(fields, actions); + add(createDialog); + } + + private void configureRenameDialog() { + renameDialog.setHeaderTitle("Rename ID"); + renameDialog.setCloseOnOutsideClick(false); + renameDialog.setCloseOnEsc(true); + renameField.setWidth("320px"); + + Button rename = new Button("Rename", event -> submitRename()); + rename.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + Button cancel = new Button("Cancel", event -> renameDialog.close()); + + HorizontalLayout actions = new HorizontalLayout(rename, cancel); + renameDialog.add(renameField, actions); + add(renameDialog); + } + + private void configureRecoverDialog() { + recoverDialog.setHeaderTitle("Recover ID"); + recoverDialog.setCloseOnOutsideClick(false); + recoverDialog.setCloseOnEsc(true); + recoverField.setWidth("360px"); + + Button recover = new Button("Recover", event -> submitRecover()); + recover.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + Button cancel = new Button("Cancel", event -> recoverDialog.close()); + + HorizontalLayout actions = new HorizontalLayout(recover, cancel); + recoverDialog.add(recoverField, actions); + add(recoverDialog); + } + + private void configureRemoveConfirm() { + removeConfirm.setHeader("Remove ID"); + removeConfirm.setConfirmText("Remove"); + removeConfirm.setCancelText("Cancel"); + removeConfirm.setCancelable(true); + removeConfirm.addConfirmListener(event -> removeSelectedId()); + add(removeConfirm); + } + + private void configureNewWalletConfirm() { + newWalletConfirm.setHeader("New Wallet"); + newWalletConfirm.setText("Overwrite wallet with new one?"); + newWalletConfirm.setConfirmText("Overwrite"); + newWalletConfirm.setCancelText("Cancel"); + newWalletConfirm.addConfirmListener(event -> { + setReady(false); + openPassphraseDialog(true, true); + }); + add(newWalletConfirm); + } + + private void configureRecoverWalletConfirm() { + recoverWalletConfirm.setHeader("Recover Wallet"); + recoverWalletConfirm.setText("Overwrite wallet from backup?"); + recoverWalletConfirm.setConfirmText("Recover"); + recoverWalletConfirm.setCancelText("Cancel"); + recoverWalletConfirm.setCancelable(true); + recoverWalletConfirm.addConfirmListener(event -> recoverWallet()); + add(recoverWalletConfirm); + } + + private void configureFixWalletConfirm() { + fixWalletConfirm.setHeader("Fix Wallet"); + fixWalletConfirm.setConfirmText("Fix"); + fixWalletConfirm.setCancelText("Cancel"); + fixWalletConfirm.addConfirmListener(event -> fixWallet()); + add(fixWalletConfirm); + } + + private void configureImportDialog() { + importDialog.setHeaderTitle("Import Wallet"); + importDialog.setCloseOnOutsideClick(false); + importDialog.setCloseOnEsc(true); + importMnemonic.setWidth("520px"); + importMnemonic.setHeight("120px"); + + Button importButton = new Button("Import", event -> submitImport()); + importButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + Button cancel = new Button("Cancel", event -> importDialog.close()); + + HorizontalLayout actions = new HorizontalLayout(importButton, cancel); + importDialog.add(importMnemonic, actions); + add(importDialog); + } + + private void configureMnemonicDialog() { + mnemonicDialog.setHeaderTitle("Mnemonic"); + mnemonicDialog.setCloseOnOutsideClick(true); + mnemonicDialog.setCloseOnEsc(true); + mnemonicArea.setWidth("520px"); + mnemonicArea.setHeight("120px"); + mnemonicArea.setReadOnly(true); + + Button close = new Button("Close", event -> mnemonicDialog.close()); + mnemonicDialog.add(mnemonicArea, close); + add(mnemonicDialog); + } + + private void configureWalletDialog() { + walletDialog.setHeaderTitle("Wallet"); + walletDialog.setCloseOnOutsideClick(true); + walletDialog.setCloseOnEsc(true); + walletArea.setWidth("720px"); + walletArea.setHeight("420px"); + walletArea.setReadOnly(true); + + Button close = new Button("Close", event -> walletDialog.close()); + walletDialog.add(walletArea, close); + add(walletDialog); + } + + private void configureSchemaCreateDialog() { + schemaCreateDialog.setHeaderTitle("Create Schema"); + schemaCreateDialog.setCloseOnOutsideClick(false); + schemaCreateDialog.setCloseOnEsc(true); + schemaCreateArea.setWidth("720px"); + schemaCreateArea.setHeight("320px"); + + Button create = new Button("Create", event -> submitSchemaCreate()); + create.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + Button cancel = new Button("Cancel", event -> schemaCreateDialog.close()); + + HorizontalLayout actions = new HorizontalLayout(create, cancel); + schemaCreateDialog.add(schemaCreateArea, actions); + add(schemaCreateDialog); + } + + private void configurePassphraseDialog() { + passphraseDialog.setCloseOnOutsideClick(false); + passphraseDialog.setCloseOnEsc(false); + passphraseField.setWidth("320px"); + passphraseConfirmField.setWidth("320px"); + passphraseField.setValueChangeMode(ValueChangeMode.EAGER); + passphraseConfirmField.setValueChangeMode(ValueChangeMode.EAGER); + + passphraseField.addValueChangeListener(event -> updatePassphraseSubmitState()); + passphraseConfirmField.addValueChangeListener(event -> updatePassphraseSubmitState()); + passphraseField.addKeyPressListener(Key.ENTER, event -> submitPassphraseIfReady()); + passphraseConfirmField.addKeyPressListener(Key.ENTER, event -> submitPassphraseIfReady()); + + passphraseHint.getStyle().set("font-size", "0.9em"); + passphraseError.getStyle().set("color", "var(--lumo-error-text-color)"); + passphraseError.getStyle().set("font-size", "0.9em"); + + passphraseSubmit.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + passphraseSubmit.addClickListener(event -> submitPassphrase()); + passphraseCancel.addClickListener(event -> cancelPassphrase()); + passphraseDialog.addOpenedChangeListener(event -> { + if (event.isOpened()) { + passphraseField.focus(); + } + }); + + HorizontalLayout actions = new HorizontalLayout(passphraseSubmit, passphraseCancel); + VerticalLayout content = new VerticalLayout( + passphraseHint, + passphraseField, + passphraseConfirmField, + passphraseError, + actions + ); + content.setPadding(false); + content.setSpacing(true); + + passphraseDialog.add(content); + add(passphraseDialog); + } + + private void configureAuthChallengeDialog() { + authChallengeDialog.setHeaderTitle("New Challenge"); + authChallengeDialog.setCloseOnOutsideClick(false); + authChallengeDialog.setCloseOnEsc(true); + authChallengeJsonArea.setWidth("720px"); + authChallengeJsonArea.setHeight("320px"); + authChallengeJsonArea.setPlaceholder("{ }"); + + Button create = new Button("Create", event -> submitAuthChallenge()); + create.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + Button cancel = new Button("Cancel", event -> authChallengeDialog.close()); + + HorizontalLayout actions = new HorizontalLayout(create, cancel); + authChallengeDialog.add(authChallengeJsonArea, actions); + add(authChallengeDialog); + } + + private void submitPassphraseIfReady() { + if (passphraseSubmit.isEnabled()) { + submitPassphrase(); + } + } + + private void openCreateDialog() { + createNameField.clear(); + List registries = loadRegistries(); + createRegistrySelect.setItems(registries); + if (!registries.isEmpty()) { + createRegistrySelect.setValue(selectRegistry(registries, createRegistrySelect.getValue())); + } + createDialog.open(); + } + + private void openRenameDialog() { + String selected = currentIdSelect.getValue(); + if (selected == null || selected.isBlank()) { + showError("Select an ID first"); + return; + } + renameField.clear(); + renameDialog.open(); + } + + private void openRecoverDialog() { + recoverField.clear(); + recoverDialog.open(); + } + + private void confirmRemove() { + String selected = currentIdSelect.getValue(); + if (selected == null || selected.isBlank()) { + showError("Select an ID first"); + return; + } + removeConfirm.setText("Are you sure you want to remove " + selected + "?"); + removeConfirm.open(); + } + + private void submitCreate() { + String name = createNameField.getValue(); + String registry = createRegistrySelect.getValue(); + submitCreate(name, registry, () -> createDialog.close()); + } + + private void submitCreateInline() { + String name = createInlineNameField.getValue(); + String registry = createInlineRegistrySelect.getValue(); + submitCreate(name, registry, this::clearInlineCreate); + } + + private void submitCreate(String name, String registry, Runnable onSuccess) { + if (name == null || name.isBlank()) { + showError("Name is required"); + return; + } + if (registry == null || registry.isBlank()) { + registry = config.getRegistry(); + } + + try { + keymasterService.createId(name.trim(), registry); + if (onSuccess != null) { + onSuccess.run(); + } + refresh(); + showSuccess("Created ID: " + name.trim()); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void submitRename() { + String selected = currentIdSelect.getValue(); + String newName = renameField.getValue(); + if (selected == null || selected.isBlank()) { + showError("Select an ID first"); + return; + } + if (newName == null || newName.isBlank()) { + showError("New name is required"); + return; + } + try { + keymasterService.renameId(selected, newName.trim()); + renameDialog.close(); + refresh(); + showSuccess("Renamed ID to: " + newName.trim()); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void submitRecover() { + String did = recoverField.getValue(); + if (did == null || did.isBlank()) { + showError("DID is required"); + return; + } + try { + String message = keymasterService.recoverId(did.trim()); + recoverDialog.close(); + refresh(); + showSuccess(message); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void clearInlineCreate() { + createInlineNameField.clear(); + createInlineRegistrySelect.clear(); + } + + private void removeSelectedId() { + String selected = currentIdSelect.getValue(); + if (selected == null || selected.isBlank()) { + showError("Select an ID first"); + return; + } + try { + keymasterService.removeId(selected); + refresh(); + showSuccess("Removed ID: " + selected); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void backupId() { + String selected = currentIdSelect.getValue(); + if (selected == null || selected.isBlank()) { + showError("Select an ID first"); + return; + } + try { + boolean ok = keymasterService.backupId(selected); + if (ok) { + showSuccess("Backup succeeded for " + selected); + } else { + showError("Backup failed for " + selected); + } + refresh(); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void rotateKeys() { + try { + keymasterService.rotateKeys(); + refresh(); + showSuccess("Keys rotated"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private List loadRegistries() { + try { + List registries = gatekeeper.listRegistries(); + if (registries == null || registries.isEmpty()) { + return Collections.singletonList(config.getRegistry()); + } + return registries; + } catch (Exception e) { + showError(e.getMessage()); + return Collections.singletonList(config.getRegistry()); + } + } + + private void refresh() { + refreshing = true; + String previousId = currentIdName; + if (!keymasterService.isReady()) { + currentIdSelect.setItems(Collections.emptyList()); + currentIdSelect.clear(); + currentAliasValue.setText("-"); + currentDidValue.setText("-"); + docsArea.clear(); + manifest = new java.util.HashMap<>(); + updateHeldSelectionState(); + hasCurrentId = false; + refreshNames(); + refreshHeld(); + refreshIssued(); + refreshAuth(); + setTabVisibilityForCurrentId(); + updateIdentityVisibility(); + refreshing = false; + return; + } + String currentId = null; + try { + currentId = keymasterService.currentId(); + } catch (Exception e) { + showError(e.getMessage()); + } + + hasCurrentId = currentId != null && !currentId.isBlank(); + boolean idChanged = (previousId == null && hasCurrentId) + || (previousId != null && !previousId.equals(currentId)); + if (idChanged) { + resetForUserChange(); + } + List ids = Collections.emptyList(); + try { + ids = keymasterService.listIds(); + } catch (Exception e) { + showError(e.getMessage()); + } + + currentIdSelect.setItems(ids); + currentIdSelect.setValue(currentId); + + currentAliasValue.setText(currentId != null ? currentId : "-"); + currentIdName = currentId; + + String did = "-"; + if (currentId != null && !currentId.isBlank()) { + try { + MdipDocument doc = keymasterService.resolveDID(currentId); + if (doc != null && doc.didDocument != null && doc.didDocument.id != null) { + did = doc.didDocument.id; + } + } catch (Exception e) { + showError(e.getMessage()); + } + } + currentDidValue.setText(did); + + setTabVisibilityForCurrentId(); + updateIdentityVisibility(); + if (hasCurrentId) { + updateDocs(currentId); + refreshNames(); + refreshHeld(); + refreshIssued(); + refreshAuth(); + } else { + docsArea.clear(); + manifest = new java.util.HashMap<>(); + updateHeldSelectionState(); + refreshNames(); + refreshHeld(); + refreshIssued(); + refreshAuth(); + } + refreshing = false; + } + + private void resetForUserChange() { + didNameField.clear(); + didValueField.clear(); + didSelect.clear(); + didDocsArea.clear(); + updateDidActionsState(); + updateDidSelectionState(); + + schemaNameField.clear(); + schemaRegistrySelect.clear(); + schemaSelect.clear(); + schemaArea.clear(); + schemaOwned = false; + selectedSchemaDid = null; + schemaDetails.setVisible(false); + schemaCreateArea.clear(); + schemaCreateDialog.close(); + updateSchemaCreateState(); + updateSchemaSelectionState(); + + groupNameField.clear(); + groupRegistrySelect.clear(); + groupSelect.clear(); + groupArea.clear(); + groupDetails.setVisible(false); + selectedGroupDid = null; + groupMemberField.clear(); + groupMemberSelect.clear(); + groupActionResult.setText(""); + updateGroupMemberControls(false); + updateGroupSelectionState(); + updateGroupCreateState(); + + heldDidField.clear(); + heldSelect.clear(); + heldArea.clear(); + selectedHeldDid = null; + updateHeldInputState(); + updateHeldSelectionState(); + + issuedSelect.clear(); + issuedArea.clear(); + selectedIssuedDid = null; + issuedOriginal = ""; + issuedEditable = false; + updateIssuedSelectionState(); + + issueSubjectSelect.clear(); + issueSchemaSelect.clear(); + issueRegistrySelect.clear(); + issueArea.clear(); + issueResult.setText(""); + updateIssueButtons(); + + authChallengeField.clear(); + authResponseField.clear(); + authDidValue.setText(""); + authStringArea.clear(); + callbackUrl = null; + disableSendResponse = true; + authChallengeDialog.close(); + updateAuthButtons(); + + walletArea.clear(); + mnemonicArea.clear(); + importMnemonic.clear(); + createNameField.clear(); + createRegistrySelect.clear(); + renameField.clear(); + recoverField.clear(); + createInlineNameField.clear(); + createInlineRegistrySelect.clear(); + } + + private void updateTabVisibility() { + if (!hasCurrentId && tabs.getSelectedTab() != identitiesTab && tabs.getSelectedTab() != walletTab) { + tabs.setSelectedTab(identitiesTab); + } + Tab selected = tabs.getSelectedTab(); + identityContent.setVisible(selected == identitiesTab); + didsContent.setVisible(selected == didsTab); + schemasContent.setVisible(selected == schemasTab); + groupsContent.setVisible(selected == groupsTab); + credentialsContent.setVisible(selected == credentialsTab); + authContent.setVisible(selected == authTab); + walletContent.setVisible(selected == walletTab); + } + + private void updateIdentityVisibility() { + boolean showIdentityDetails = hasCurrentId; + identityCreatePanel.setVisible(!hasCurrentId); + identitySelectorPanel.setVisible(showIdentityDetails); + identityDocsPanel.setVisible(showIdentityDetails); + identityActionsRow.setVisible(showIdentityDetails); + renameIdButton.setVisible(showIdentityDetails); + removeIdButton.setVisible(showIdentityDetails); + backupIdButton.setVisible(showIdentityDetails); + recoverIdButton.setVisible(showIdentityDetails); + rotateKeysButton.setVisible(showIdentityDetails); + + if (!hasCurrentId) { + List registries = loadRegistries(); + createInlineRegistrySelect.setItems(registries); + if (!registries.isEmpty()) { + createInlineRegistrySelect.setValue(selectRegistry(registries, createInlineRegistrySelect.getValue())); + } + } + } + + private void setTabVisibilityForCurrentId() { + boolean show = hasCurrentId; + didsTab.setVisible(show); + schemasTab.setVisible(show); + groupsTab.setVisible(show); + credentialsTab.setVisible(show); + authTab.setVisible(show); + updateTabVisibility(); + } + + private void updateCredentialTabVisibility() { + Tab selected = credentialTabs.getSelectedTab(); + heldContent.setVisible(selected == heldTab); + issueContent.setVisible(selected == issueTab); + issuedContent.setVisible(selected == issuedTab); + } + + private void openPassphraseDialog() { + openPassphraseDialog(!keymasterService.hasWallet(), false); + } + + private void openPassphraseDialog(boolean createMode, boolean resetMode) { + passphraseCreateMode = createMode; + passphraseResetMode = resetMode; + passphraseDialog.setHeaderTitle(passphraseCreateMode ? "Set Passphrase" : "Unlock Wallet"); + if (passphraseCreateMode) { + passphraseHint.setText("Create a passphrase to encrypt the wallet."); + } else if (pendingWallet != null) { + passphraseHint.setText("Enter your passphrase to unlock the uploaded wallet."); + } else { + passphraseHint.setText("Enter your passphrase to unlock the wallet."); + } + passphraseSubmit.setText(passphraseCreateMode ? "Set Passphrase" : "Unlock"); + passphraseCancel.setVisible(allowPassphraseCancel()); + passphraseField.clear(); + passphraseConfirmField.clear(); + passphraseConfirmField.setVisible(passphraseCreateMode); + passphraseError.setText(""); + setPassphraseBusy(false); + passphraseDialog.open(); + } + + private void submitPassphrase() { + String passphrase = passphraseField.getValue(); + if (passphrase == null || passphrase.isBlank()) { + passphraseError.setText("Passphrase is required"); + return; + } + if (passphraseCreateMode) { + String confirm = passphraseConfirmField.getValue(); + if (confirm == null || confirm.isBlank() || !passphrase.equals(confirm)) { + passphraseError.setText("Passphrases do not match"); + return; + } + } + setPassphraseBusy(true); + UI ui = UI.getCurrent(); + ui.setPollInterval(500); + CompletableFuture.runAsync(() -> { + if (pendingWallet != null) { + keymasterService.initWithUploadedWallet(passphrase, pendingWallet); + } else if (passphraseResetMode) { + keymasterService.resetWallet(passphrase); + } else { + keymasterService.initWithPassphrase(passphrase, passphraseCreateMode); + } + }).whenComplete((ignored, error) -> ui.access(() -> { + ui.setPollInterval(-1); + if (error == null) { + pendingWallet = null; + setReady(true); + passphraseDialog.close(); + refresh(); + } else { + passphraseError.setText("Incorrect passphrase"); + } + setPassphraseBusy(false); + })); + } + + private void setReady(boolean ready) { + this.ready = ready; + identityContent.setEnabled(ready); + didsContent.setEnabled(ready); + schemasContent.setEnabled(ready); + groupsContent.setEnabled(ready); + credentialsContent.setEnabled(ready); + authContent.setEnabled(ready); + walletContent.setEnabled(ready); + currentIdSelect.setEnabled(ready); + tabs.setEnabled(ready); + } + + private void updatePassphraseSubmitState() { + String passphrase = passphraseField.getValue(); + boolean hasPassphrase = passphrase != null && !passphrase.isBlank(); + if (passphraseCreateMode) { + String confirm = passphraseConfirmField.getValue(); + boolean hasConfirm = confirm != null && !confirm.isBlank(); + passphraseSubmit.setEnabled(hasPassphrase && hasConfirm && passphrase.equals(confirm)); + } else { + passphraseSubmit.setEnabled(hasPassphrase); + } + } + + private void setPassphraseBusy(boolean busy) { + passphraseField.setEnabled(!busy); + passphraseConfirmField.setEnabled(!busy && passphraseCreateMode); + if (busy) { + passphraseSubmit.setEnabled(false); + passphraseCancel.setEnabled(false); + } else { + updatePassphraseSubmitState(); + passphraseCancel.setEnabled(allowPassphraseCancel()); + } + } + + private boolean allowPassphraseCancel() { + return pendingWallet != null || passphraseResetMode; + } + + private void cancelPassphrase() { + pendingWallet = null; + passphraseDialog.close(); + setReady(true); + refresh(); + } + + private void handleWalletUpload() { + try (InputStream input = uploadBuffer.getInputStream()) { + String json = new String(input.readAllBytes(), StandardCharsets.UTF_8); + WalletEncFile wallet = keymasterService.parseWalletEncFile(json); + pendingWallet = wallet; + setReady(false); + upload.clearFileList(); + openPassphraseDialog(false, false); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void confirmNewWallet() { + setReady(false); + openPassphraseDialog(true, true); + } + + private void openImportDialog() { + importMnemonic.clear(); + importDialog.open(); + } + + private void openSchemaCreateDialog() { + String name = schemaNameField.getValue(); + if (name == null || name.isBlank()) { + showError("Schema name is required"); + return; + } + try { + keymasterService.validateName(name); + java.util.Map names = keymasterService.listNames(true); + if (names != null && names.containsKey(name.trim())) { + showError("Invalid parameter: name already used"); + return; + } + } catch (Exception e) { + showError(e.getMessage()); + return; + } + schemaCreateArea.setValue(DEFAULT_SCHEMA_JSON); + List registries = loadRegistries(); + String selected = schemaRegistrySelect.getValue(); + schemaRegistrySelect.setItems(registries); + schemaRegistrySelect.setValue(selectRegistry(registries, selected)); + schemaCreateDialog.open(); + } + + private void refreshNames() { + if (!keymasterService.isReady() || !hasCurrentId) { + didNameMap = new java.util.HashMap<>(); + didToName = new java.util.HashMap<>(); + agentNames = new java.util.ArrayList<>(); + schemaNames = new java.util.ArrayList<>(); + groupNames = new java.util.ArrayList<>(); + + didSelect.setItems(Collections.emptyList()); + didSelect.clear(); + didDocsArea.clear(); + updateDidActionsState(); + updateDidSelectionState(); + + schemaSelect.setItems(Collections.emptyList()); + schemaSelect.clear(); + schemaArea.clear(); + schemaDetails.setVisible(false); + updateSchemaSelectionState(); + + groupSelect.setItems(Collections.emptyList()); + groupSelect.clear(); + groupArea.clear(); + groupDetails.setVisible(false); + selectedGroupDid = null; + groupActionResult.setText(""); + groupMemberField.clear(); + groupMemberSelect.clear(); + updateGroupMemberControls(false); + updateGroupSelectionState(); + + issueSubjectSelect.setItems(Collections.emptyList()); + issueSchemaSelect.setItems(Collections.emptyList()); + issueRegistrySelect.setItems(Collections.emptyList()); + updateIssueButtons(); + + return; + } + + java.util.Map namesMap; + try { + namesMap = keymasterService.listNames(false); + } catch (Exception e) { + showError(e.getMessage()); + return; + } + + didNameMap = new java.util.HashMap<>(namesMap); + didToName = new java.util.HashMap<>(); + java.util.List names = new java.util.ArrayList<>(didNameMap.keySet()); + java.util.Collections.sort(names); + + java.util.List ids; + try { + ids = keymasterService.listIds(); + } catch (Exception e) { + ids = new java.util.ArrayList<>(); + } + + for (String id : ids) { + if (id == null || id.isBlank() || didNameMap.containsKey(id)) { + continue; + } + try { + MdipDocument doc = keymasterService.resolveDID(id); + if (doc != null && doc.didDocument != null && doc.didDocument.id != null) { + didNameMap.put(id, doc.didDocument.id); + } + } catch (Exception ignored) { + // ignore unresolved ids + } + } + + names = new java.util.ArrayList<>(didNameMap.keySet()); + java.util.Collections.sort(names); + + for (java.util.Map.Entry entry : didNameMap.entrySet()) { + didToName.put(entry.getValue(), entry.getKey()); + } + + java.util.Set agentSet = new java.util.LinkedHashSet<>(ids); + java.util.List schemas = new java.util.ArrayList<>(); + java.util.List groups = new java.util.ArrayList<>(); + + for (String name : names) { + try { + MdipDocument doc = keymasterService.resolveDID(name); + if (doc != null && doc.mdip != null && "agent".equals(doc.mdip.type)) { + agentSet.add(name); + continue; + } + if (doc != null && doc.didDocumentData instanceof java.util.Map) { + java.util.Map data = (java.util.Map) doc.didDocumentData; + if (data.containsKey("schema")) { + schemas.add(name); + continue; + } + if (data.containsKey("group")) { + groups.add(name); + } + } + } catch (Exception ignored) { + // Skip unresolvable names. + } + } + + agentNames = new java.util.ArrayList<>(agentSet); + schemaNames = schemas; + groupNames = groups; + + didSelect.setItems(names); + if (didSelect.getValue() == null || !didNameMap.containsKey(didSelect.getValue())) { + didSelect.clear(); + didDocsArea.clear(); + } + updateDidSelectionState(); + + schemaSelect.setItems(schemaNames); + if (selectedSchemaDid != null && schemaNames.contains(selectedSchemaDid)) { + schemaSelect.setValue(selectedSchemaDid); + schemaDetails.setVisible(true); + } else { + selectedSchemaDid = null; + schemaSelect.clear(); + schemaArea.clear(); + schemaDetails.setVisible(false); + } + updateSchemaSelectionState(); + + groupSelect.setItems(groupNames); + if (selectedGroupDid != null && groupNames.contains(selectedGroupDid)) { + groupSelect.setValue(selectedGroupDid); + groupDetails.setVisible(true); + } else { + selectedGroupDid = null; + groupSelect.clear(); + groupArea.clear(); + groupDetails.setVisible(false); + groupMemberField.clear(); + groupMemberSelect.clear(); + groupActionResult.setText(""); + updateGroupMemberControls(false); + } + updateGroupSelectionState(); + + issueSubjectSelect.setItems(agentNames); + if (issueSubjectSelect.getValue() != null && !agentNames.contains(issueSubjectSelect.getValue())) { + issueSubjectSelect.clear(); + issueArea.clear(); + issueResult.setText(""); + } + + issueSchemaSelect.setItems(schemaNames); + if (issueSchemaSelect.getValue() != null && !schemaNames.contains(issueSchemaSelect.getValue())) { + issueSchemaSelect.clear(); + issueArea.clear(); + issueResult.setText(""); + } + + List registries = loadRegistries(); + issueRegistrySelect.setItems(registries); + issueRegistrySelect.setValue(selectRegistry(registries, issueRegistrySelect.getValue())); + String schemaRegistry = schemaRegistrySelect.getValue(); + schemaRegistrySelect.setItems(registries); + schemaRegistrySelect.setValue(selectRegistry(registries, schemaRegistry)); + String groupRegistry = groupRegistrySelect.getValue(); + groupRegistrySelect.setItems(registries); + groupRegistrySelect.setValue(selectRegistry(registries, groupRegistry)); + + updateIssueButtons(); + } + + private void refreshHeld() { + if (!keymasterService.isReady() || !hasCurrentId) { + heldSelect.setItems(Collections.emptyList()); + heldSelect.clear(); + heldArea.clear(); + heldDidField.clear(); + updateHeldSelectionState(); + return; + } + try { + List held = keymasterService.listCredentials(); + heldSelect.setItems(held); + if (selectedHeldDid != null && held.contains(selectedHeldDid)) { + heldSelect.setValue(selectedHeldDid); + } else { + selectedHeldDid = null; + heldSelect.clear(); + heldArea.clear(); + } + updateHeldSelectionState(); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void refreshIssued() { + if (!keymasterService.isReady() || !hasCurrentId) { + issuedSelect.setItems(Collections.emptyList()); + issuedSelect.clear(); + issuedArea.clear(); + issuedEditable = false; + issuedOriginal = ""; + updateIssuedSelectionState(); + return; + } + try { + List issued = keymasterService.listIssued(); + issuedSelect.setItems(issued); + if (selectedIssuedDid != null && issued.contains(selectedIssuedDid)) { + issuedSelect.setValue(selectedIssuedDid); + } else { + selectedIssuedDid = null; + issuedSelect.clear(); + issuedArea.clear(); + issuedEditable = false; + issuedOriginal = ""; + } + updateIssuedSelectionState(); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void editCredential() { + String subject = issueSubjectSelect.getValue(); + String schema = issueSchemaSelect.getValue(); + if (subject == null || subject.isBlank() || schema == null || schema.isBlank()) { + showError("Select a subject and schema"); + return; + } + try { + java.util.Map bound = keymasterService.bindCredential(schema, subject); + issueArea.setValue(keymasterService.prettyJson(bound)); + issueResult.setText(""); + updateIssueButtons(); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void issueCredential() { + String json = issueArea.getValue(); + if (json == null || json.isBlank()) { + showError("Credential JSON is required"); + return; + } + String registry = issueRegistrySelect.getValue(); + if (!canUseRegistry(registry, "credential")) { + return; + } + try { + Object parsed = keymasterService.parseJson(json); + if (!(parsed instanceof java.util.Map)) { + showError("Credential must be an object"); + return; + } + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) parsed; + String did = keymasterService.issueCredential(map, registry); + issueResult.setText(did); + updateIssueButtons(); + refreshIssued(); + showSuccess("Credential issued"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void updateIssueButtons() { + String subject = issueSubjectSelect.getValue(); + String schema = issueSchemaSelect.getValue(); + boolean canEdit = subject != null && !subject.isBlank() && schema != null && !schema.isBlank(); + issueEditButton.setEnabled(canEdit); + + String json = issueArea.getValue(); + boolean hasRegistry = issueRegistrySelect.getValue() != null && !issueRegistrySelect.getValue().isBlank(); + boolean canIssue = json != null && !json.isBlank() && hasRegistry; + issueButton.setEnabled(canIssue); + } + + private void resolveIssued() { + String did = issuedSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + MdipDocument doc = keymasterService.resolveDID(did); + issuedArea.setReadOnly(true); + issuedArea.setValue(keymasterService.prettyJson(doc)); + issuedEditable = false; + issuedOriginal = issuedArea.getValue(); + selectedIssuedDid = did; + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void decryptIssued() { + String did = issuedSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + Object credential = keymasterService.decryptJSON(did); + String json = keymasterService.prettyJson(credential); + issuedArea.setReadOnly(false); + issuedArea.setValue(json); + issuedEditable = true; + issuedOriginal = json; + selectedIssuedDid = did; + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void updateIssued() { + String did = issuedSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + if (!issuedEditable) { + showError("Decrypt the credential before updating"); + return; + } + String json = issuedArea.getValue(); + if (json == null || json.isBlank()) { + showError("Credential JSON is required"); + return; + } + if (json.equals(issuedOriginal)) { + showError("No changes to update"); + return; + } + try { + Object parsed = keymasterService.parseJson(json); + if (!(parsed instanceof java.util.Map)) { + showError("Credential must be an object"); + return; + } + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) parsed; + boolean ok = keymasterService.updateCredential(did, map); + if (!ok) { + showError("Credential not updated"); + return; + } + issuedOriginal = json; + resetHeldSelection(); + showSuccess("Credential updated"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void revokeIssued() { + String did = issuedSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + boolean ok = keymasterService.revokeCredential(did); + if (!ok) { + showError("Credential not revoked"); + return; + } + refreshIssued(); + resetHeldSelection(); + showSuccess("Credential revoked"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void resolveHeld() { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + MdipDocument doc = keymasterService.resolveDID(did); + heldArea.setValue(keymasterService.prettyJson(doc)); + selectedHeldDid = did; + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void decryptHeld() { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + Object credential = keymasterService.decryptJSON(did); + heldArea.setValue(keymasterService.prettyJson(credential)); + selectedHeldDid = did; + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void acceptHeld() { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + boolean ok = keymasterService.acceptCredential(did); + if (!ok) { + showError("Credential not accepted"); + return; + } + refreshHeld(); + showSuccess("Credential accepted"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void resolveHeldInput() { + String did = heldDidField.getValue(); + try { + MdipDocument doc = keymasterService.resolveDID(did.trim()); + heldArea.setValue(keymasterService.prettyJson(doc)); + selectedHeldDid = did.trim(); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void resolveHeldSelected() { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + MdipDocument doc = keymasterService.resolveDID(did); + selectedHeldDid = did; + heldArea.setValue(keymasterService.prettyJson(doc)); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void decryptHeldInput() { + String did = heldDidField.getValue(); + try { + Object credential = keymasterService.decryptJSON(did.trim()); + heldArea.setValue(keymasterService.prettyJson(credential)); + selectedHeldDid = did.trim(); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void decryptHeldSelected() { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + Object credential = keymasterService.decryptJSON(did); + selectedHeldDid = did; + heldArea.setValue(keymasterService.prettyJson(credential)); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void acceptHeldInput() { + String did = heldDidField.getValue(); + try { + boolean ok = keymasterService.acceptCredential(did.trim()); + if (!ok) { + showError("Credential not accepted"); + return; + } + refreshHeld(); + showSuccess("Credential accepted"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void removeHeld() { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + boolean ok = keymasterService.removeCredential(did); + if (!ok) { + showError("Credential not removed"); + return; + } + refreshHeld(); + showSuccess("Credential removed"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void publishHeld(boolean reveal) { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + Object result = keymasterService.publishCredential(did, reveal); + heldArea.setValue(keymasterService.prettyJson(result)); + if (hasCurrentId && currentIdName != null && !currentIdName.isBlank()) { + updateDocs(currentIdName); + } + showSuccess(reveal ? "Credential revealed" : "Credential published"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void unpublishHeld() { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a credential first"); + return; + } + try { + keymasterService.unpublishCredential(did); + if (hasCurrentId && currentIdName != null && !currentIdName.isBlank()) { + updateDocs(currentIdName); + } + showSuccess("Credential unpublished"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void resolveDidInput() { + String didOrName = didValueField.getValue(); + if (didOrName == null || didOrName.isBlank()) { + didOrName = didNameField.getValue(); + } + if (didOrName == null || didOrName.isBlank()) { + showError("Name or DID is required"); + return; + } + try { + MdipDocument doc = keymasterService.resolveDID(didOrName.trim()); + didDocsArea.setValue(keymasterService.prettyJson(doc)); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void resolveSelectedName() { + String name = didSelect.getValue(); + if (name == null || name.isBlank()) { + showError("Select a name first"); + return; + } + try { + MdipDocument doc = keymasterService.resolveDID(name.trim()); + didDocsArea.setValue(keymasterService.prettyJson(doc)); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void addDidName() { + String name = didNameField.getValue(); + String did = didValueField.getValue(); + if (name == null || name.isBlank()) { + showError("Name is required"); + return; + } + if (did == null || did.isBlank()) { + showError("DID is required"); + return; + } + try { + keymasterService.addName(name.trim(), did.trim()); + refreshNames(); + showSuccess("Name added"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void removeSelectedName() { + String name = didSelect.getValue(); + if (name == null || name.isBlank()) { + showError("Select a name first"); + return; + } + try { + keymasterService.removeName(name); + refreshNames(); + showSuccess("Name removed"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void updateDidActionsState() { + String did = didValueField.getValue(); + String name = didNameField.getValue(); + boolean hasDid = did != null && !did.isBlank(); + boolean hasName = name != null && !name.isBlank(); + + didResolveButton.setEnabled(hasDid); + didAddButton.setEnabled(hasDid && hasName); + } + + private void updateDidSelectionState() { + String selected = didSelect.getValue(); + setButtonsEnabledOnValue(selected, didResolveSelectedButton, didRemoveButton, didCopyButton); + } + + private void copySelectedDid() { + String name = didSelect.getValue(); + if (name == null || name.isBlank()) { + return; + } + String did = didNameMap.get(name); + if (did == null || did.isBlank()) { + showError("No DID found for selection"); + return; + } + copyToClipboard(did); + } + + private void updateSchemaSelectionState() { + String selected = schemaSelect.getValue(); + setButtonsEnabledOnValue(selected, schemaCopyButton); + } + + private void updateGroupSelectionState() { + String selected = groupSelect.getValue(); + setButtonsEnabledOnValue(selected, groupCopyButton); + } + + private void updateIssuedSelectionState() { + String selected = issuedSelect.getValue(); + setButtonsEnabledOnValue(selected, issuedCopyButton); + } + + private void resetHeldSelection() { + heldSelect.clear(); + selectedHeldDid = null; + heldArea.clear(); + updateHeldSelectionState(); + } + + private void setButtonsEnabledOnValue(String value, Button... buttons) { + boolean enabled = value != null && !value.isBlank(); + for (Button button : buttons) { + button.setEnabled(enabled); + } + } + + private void copySchemaDid() { + String name = schemaSelect.getValue(); + if (name == null || name.isBlank()) { + return; + } + String did = didNameMap.get(name); + if (did == null || did.isBlank()) { + showError("No DID found for selection"); + return; + } + copyToClipboard(did); + } + + private void copyGroupDid() { + String name = groupSelect.getValue(); + if (name == null || name.isBlank()) { + return; + } + String did = didNameMap.get(name); + if (did == null || did.isBlank()) { + showError("No DID found for selection"); + return; + } + copyToClipboard(did); + } + + private void copyHeldDid() { + String did = heldSelect.getValue(); + if (did == null || did.isBlank()) { + return; + } + copyToClipboard(did); + } + + private void copyIssuedDid() { + String did = issuedSelect.getValue(); + if (did == null || did.isBlank()) { + return; + } + copyToClipboard(did); + } + + private void wireCopyButton(Button button, Runnable action) { + button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + button.addClickListener(event -> action.run()); + } + + private void copyToClipboard(String value) { + UI ui = UI.getCurrent(); + ui.getPage().executeJs("navigator.clipboard.writeText($0)", value) + .then(success -> showSuccess("Copied to clipboard")); + } + + private void selectSchema(String did) { + try { + MdipDocument doc = keymasterService.resolveDID(did); + Object schema = doc != null && doc.didDocumentData instanceof java.util.Map + ? ((java.util.Map) doc.didDocumentData).get("schema") + : null; + schemaOwned = doc != null && doc.didDocumentMetadata != null && Boolean.TRUE.equals(doc.didDocumentMetadata.isOwned); + selectedSchemaDid = did; + schemaArea.setReadOnly(!schemaOwned); + schemaArea.setValue(keymasterService.prettyJson(schema)); + schemaDetails.setVisible(true); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void selectGroup(String did) { + try { + MdipDocument doc = keymasterService.resolveDID(did); + groupArea.setValue(keymasterService.prettyJson(doc)); + groupDetails.setVisible(true); + selectedGroupDid = did; + groupActionResult.setText(""); + + String controller = doc != null && doc.didDocument != null ? doc.didDocument.controller : null; + String currentDid = currentDidValue.getText(); + boolean canEdit = controller != null && currentDid != null + && !currentDid.isBlank() && !"-".equals(currentDid) + && controller.equals(currentDid); + updateGroupMemberControls(canEdit); + + java.util.List members = extractGroupMembers(doc); + updateGroupMemberSelect(members); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void updateGroupMemberControls(boolean canEdit) { + groupAddButton.setVisible(canEdit); + groupRemoveButton.setVisible(canEdit); + groupTestButton.setVisible(true); + groupMemberSelect.setVisible(canEdit); + groupMemberButtonsRow.setVisible(canEdit); + updateGroupMemberActionState(); + } + + private void updateGroupMemberActionState() { + String member = groupMemberField.getValue(); + boolean hasMemberText = member != null && !member.isBlank(); + String selected = groupMemberSelect.getValue(); + boolean hasSelection = selected != null && !selected.isBlank(); + + groupAddButton.setEnabled(hasMemberText); + groupTestButton.setEnabled(hasMemberText); + groupRemoveButton.setEnabled(hasSelection); + } + + private java.util.List extractGroupMembers(MdipDocument doc) { + java.util.List members = new java.util.ArrayList<>(); + if (doc == null || !(doc.didDocumentData instanceof java.util.Map)) { + return members; + } + java.util.Map data = (java.util.Map) doc.didDocumentData; + Object groupObj = data.get("group"); + if (!(groupObj instanceof java.util.Map)) { + return members; + } + Object membersObj = ((java.util.Map) groupObj).get("members"); + if (membersObj instanceof java.util.List) { + for (Object item : (java.util.List) membersObj) { + if (item instanceof String) { + members.add((String) item); + } + } + } + return members; + } + + private void updateGroupMemberSelect(java.util.List memberDids) { + java.util.List items = new java.util.ArrayList<>(); + for (String did : memberDids) { + String name = didToName.get(did); + items.add(name != null ? name : did); + } + java.util.Collections.sort(items); + groupMemberSelect.setItems(items); + groupMemberSelect.clear(); + } + + private void createGroup() { + String name = groupNameField.getValue(); + String registry = groupRegistrySelect.getValue(); + if (!canUseRegistry(registry, "group")) { + return; + } + try { + String did = keymasterService.createGroup(name.trim(), registry); + groupNameField.clear(); + refreshNames(); + groupSelect.setValue(name.trim()); + selectGroup(name.trim()); + showSuccess("Group created"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void refreshAuth() { + if (!keymasterService.isReady() || !hasCurrentId) { + authChallengeField.clear(); + authResponseField.clear(); + authDidValue.setText(""); + authStringArea.clear(); + callbackUrl = null; + disableSendResponse = true; + updateAuthButtons(); + } + } + + private void openAuthChallengeDialog() { + authChallengeJsonArea.setValue("{ }"); + authChallengeDialog.open(); + } + + private void submitAuthChallenge() { + String text = authChallengeJsonArea.getValue(); + java.util.Map map = new java.util.LinkedHashMap<>(); + if (text != null && !text.isBlank()) { + try { + Object parsed = keymasterService.parseJson(text); + if (!(parsed instanceof java.util.Map)) { + showError("Challenge JSON must be an object"); + return; + } + @SuppressWarnings("unchecked") + java.util.Map parsedMap = (java.util.Map) parsed; + map = parsedMap; + } catch (Exception e) { + showError(e.getMessage()); + return; + } + } + if (!canUseRegistry(config.getRegistry(), "challenge")) { + return; + } + try { + String did = keymasterService.createChallenge(map, null); + authChallengeDialog.close(); + authChallengeField.setValue(did); + resolveChallenge(did); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void resolveChallenge(String did) { + try { + Object asset = keymasterService.resolveAsset(did.trim()); + setAuthOutput(did.trim(), asset); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void createResponse() { + String challengeDid = authChallengeField.getValue(); + try { + if (!canUseRegistry(config.getRegistry(), "response")) { + return; + } + clearResponse(); + String responseDid = keymasterService.createResponse(challengeDid.trim()); + authResponseField.setValue(responseDid); + + Object asset = keymasterService.resolveAsset(challengeDid.trim()); + callbackUrl = extractCallback(asset); + disableSendResponse = callbackUrl == null || callbackUrl.isBlank(); + updateAuthButtons(); + + decryptResponse(responseDid); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void clearChallenge() { + authChallengeField.clear(); + updateAuthButtons(); + } + + private void decryptResponse(String responseDid) { + try { + Object decrypted = keymasterService.decryptJSON(responseDid.trim()); + setAuthOutput(responseDid.trim(), decrypted); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void verifyResponse() { + String responseDid = authResponseField.getValue(); + try { + java.util.Map result = keymasterService.verifyResponse(responseDid.trim()); + boolean match = result != null && Boolean.TRUE.equals(result.get("match")); + if (match) { + showSuccess("Response is VALID"); + } else { + showError("Response is NOT VALID"); + } + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void clearResponse() { + authResponseField.clear(); + disableSendResponse = true; + callbackUrl = null; + updateAuthButtons(); + } + + private void sendResponse() { + String responseDid = authResponseField.getValue(); + if (callbackUrl == null || callbackUrl.isBlank()) { + showError("No callback configured for this challenge"); + return; + } + disableSendResponse = true; + updateAuthButtons(); + UI ui = UI.getCurrent(); + CompletableFuture.runAsync(() -> { + try { + keymasterService.sendResponse(callbackUrl, responseDid.trim()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }).whenComplete((ignored, error) -> ui.access(() -> { + if (error != null) { + showError(error.getCause() != null ? error.getCause().getMessage() : error.getMessage()); + } else { + showSuccess("Response sent"); + } + disableSendResponse = callbackUrl == null || callbackUrl.isBlank(); + updateAuthButtons(); + })); + } + + private void setAuthOutput(String did, Object value) { + authDidValue.setText(did); + authStringArea.setValue(keymasterService.prettyJson(value)); + updateAuthButtons(); + } + + private void updateAuthButtons() { + String challenge = authChallengeField.getValue(); + String response = authResponseField.getValue(); + String current = authDidValue.getText(); + + boolean hasChallenge = challenge != null && !challenge.isBlank(); + boolean hasResponse = response != null && !response.isBlank(); + boolean canResolve = hasChallenge && (current == null || !challenge.equals(current)); + boolean canDecrypt = hasResponse && (current == null || !response.equals(current)); + + authResolveButton.setEnabled(canResolve); + authRespondButton.setEnabled(hasChallenge); + authClearChallengeButton.setEnabled(hasChallenge); + + authDecryptButton.setEnabled(canDecrypt); + authVerifyButton.setEnabled(hasResponse); + authSendButton.setEnabled(!disableSendResponse); + authClearResponseButton.setEnabled(hasResponse); + } + + private String extractCallback(Object asset) { + if (!(asset instanceof java.util.Map)) { + return null; + } + java.util.Map map = (java.util.Map) asset; + Object challengeObj = map.get("challenge"); + if (!(challengeObj instanceof java.util.Map)) { + return null; + } + Object callback = ((java.util.Map) challengeObj).get("callback"); + return callback instanceof String ? (String) callback : null; + } + + private void addGroupMember() { + String group = groupSelect.getValue(); + String member = groupMemberField.getValue(); + if (group == null || group.isBlank()) { + showError("Select a group first"); + return; + } + try { + boolean ok = keymasterService.addGroupMember(group, member.trim()); + if (!ok) { + showError("Member not added"); + return; + } + selectGroup(group); + showSuccess("Member added"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void removeGroupMember() { + String group = groupSelect.getValue(); + String selected = groupMemberSelect.getValue(); + if (group == null || group.isBlank()) { + showError("Select a group first"); + return; + } + try { + String memberDid = didNameMap.containsKey(selected) ? didNameMap.get(selected) : selected; + boolean ok = keymasterService.removeGroupMember(group, memberDid.trim()); + if (!ok) { + showError("Member not removed"); + return; + } + selectGroup(group); + showSuccess("Member removed"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void testGroupMember() { + String group = groupSelect.getValue(); + String member = groupMemberField.getValue(); + if (group == null || group.isBlank()) { + showError("Select a group first"); + return; + } + try { + boolean ok = keymasterService.testGroup(group, member.trim()); + groupActionResult.setText(ok ? "Member found" : "Not a member"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void refreshSelectedSchema() { + String did = schemaSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a schema first"); + return; + } + selectSchema(did); + } + + private void submitSchemaCreate() { + String name = schemaNameField.getValue(); + String registry = schemaRegistrySelect.getValue(); + String schemaJson = schemaCreateArea.getValue(); + Object schema = null; + + if (!canUseRegistry(registry, "schema")) { + return; + } + if (schemaJson != null && !schemaJson.isBlank()) { + try { + schema = keymasterService.parseJson(schemaJson); + } catch (Exception e) { + showError(e.getMessage()); + return; + } + } + + try { + String did = keymasterService.createSchema(schema, registry, name); + schemaCreateDialog.close(); + refreshNames(); + schemaSelect.setValue(name.trim()); + selectSchema(name.trim()); + showSuccess("Schema created"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void saveSchema() { + String did = schemaSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a schema first"); + return; + } + if (!schemaOwned) { + showError("You must own the schema to update it"); + return; + } + String schemaJson = schemaArea.getValue(); + if (schemaJson == null || schemaJson.isBlank()) { + showError("Schema JSON is required"); + return; + } + try { + Object schema = keymasterService.parseJson(schemaJson); + keymasterService.setSchema(did, schema); + selectSchema(did); + showSuccess("Schema updated"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void testSchema() { + String did = schemaSelect.getValue(); + if (did == null || did.isBlank()) { + showError("Select a schema first"); + return; + } + try { + boolean ok = keymasterService.testSchema(did); + if (ok) { + showSuccess("Schema is valid"); + } else { + showError("Schema is invalid"); + } + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void confirmRecoverWallet() { + recoverWalletConfirm.open(); + } + + private void submitImport() { + String mnemonic = importMnemonic.getValue(); + if (mnemonic == null || mnemonic.isBlank()) { + showError("Mnemonic is required"); + return; + } + try { + keymasterService.newWallet(mnemonic.trim(), true); + keymasterService.recoverWallet(); + importDialog.close(); + refresh(); + showSuccess("Imported wallet"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void backupWallet() { + try { + keymasterService.backupWallet(); + showSuccess("Wallet backup successful"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void recoverWallet() { + try { + keymasterService.recoverWallet(); + refresh(); + showSuccess("Wallet recovered"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void checkWallet() { + checkWalletButton.setEnabled(false); + try { + CheckWalletResult result = keymasterService.checkWallet(); + if (result == null) { + showError("Wallet check failed"); + return; + } + + if (result.invalid == 0 && result.deleted == 0) { + showError(result.checked + " DIDs checked, no problems found"); + return; + } + + fixWalletConfirm.setText(result.checked + " DIDs checked\n" + + result.invalid + " invalid DIDs found\n" + + result.deleted + " deleted DIDs found\n\nFix wallet?"); + fixWalletConfirm.open(); + } catch (Exception e) { + showError(e.getMessage()); + } finally { + checkWalletButton.setEnabled(true); + } + } + + private void fixWallet() { + try { + FixWalletResult result = keymasterService.fixWallet(); + if (result == null) { + showError("Wallet fix failed"); + return; + } + showError(result.idsRemoved + " IDs removed\n" + + result.ownedRemoved + " owned DIDs removed\n" + + result.heldRemoved + " held DIDs removed\n" + + result.namesRemoved + " names removed"); + refresh(); + } catch (Exception e) { + showError(e.getMessage()); + } finally { + } + } + + private void showMnemonic() { + try { + String mnemonic = keymasterService.decryptMnemonic(); + mnemonicArea.setValue(mnemonic != null ? mnemonic : ""); + mnemonicDialog.open(); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void showWallet() { + try { + WalletFile wallet = keymasterService.loadWallet(); + walletArea.setValue(keymasterService.prettyJson(wallet)); + walletDialog.open(); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void prepareWalletDownload() { + try { + WalletEncFile wallet = keymasterService.exportEncryptedWallet(); + String json = keymasterService.prettyJson(wallet); + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + StreamResource resource = new StreamResource("keymaster-wallet.json", + () -> new ByteArrayInputStream(bytes)); + downloadAnchor.setHref(resource); + downloadAnchor.getElement().callJsFunction("click"); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void setCurrentId(String selected) { + try { + if (selected.equals(currentIdName)) { + return; + } + keymasterService.setCurrentId(selected); + refresh(); + showSuccess("Current ID set to: " + selected); + } catch (Exception e) { + showError(e.getMessage()); + } + } + + private void updateDocs(String nameOrDid) { + if (nameOrDid == null || nameOrDid.isBlank()) { + docsArea.clear(); + manifest = new java.util.HashMap<>(); + updateHeldSelectionState(); + return; + } + + try { + MdipDocument doc = keymasterService.resolveDID(nameOrDid); + docsArea.setValue(keymasterService.prettyJson(doc)); + manifest = extractManifest(doc); + updateHeldSelectionState(); + } catch (Exception e) { + docsArea.setValue(String.valueOf(e.getMessage())); + showError(e.getMessage()); + } + } + + private boolean canUseRegistry(String targetRegistry, String assetType) { + if (targetRegistry == null || targetRegistry.isBlank()) { + return true; + } + if (currentIdName == null || currentIdName.isBlank()) { + return true; + } + try { + MdipDocument doc = keymasterService.resolveDID(currentIdName); + String registry = doc != null && doc.mdip != null ? doc.mdip.registry : null; + if ("local".equalsIgnoreCase(registry) && !"local".equalsIgnoreCase(targetRegistry)) { + showError("Local agent cannot create a non-local " + assetType); + return false; + } + } catch (Exception e) { + showError(e.getMessage()); + return false; + } + return true; + } + + private void updateHeldSelectionState() { + String did = heldSelect.getValue(); + boolean hasSelection = did != null && !did.isBlank(); + + heldResolveSelectedButton.setEnabled(hasSelection); + heldCopyButton.setEnabled(hasSelection); + + if (!hasSelection) { + heldDecryptSelectedButton.setEnabled(false); + heldRemoveButton.setEnabled(false); + heldPublishButton.setEnabled(false); + heldRevealButton.setEnabled(false); + heldUnpublishButton.setEnabled(false); + return; + } + + boolean deactivatedEmpty = isDeactivatedEmptyCredential(did); + if (deactivatedEmpty) { + boolean unpublished = credentialUnpublished(did); + heldDecryptSelectedButton.setEnabled(false); + heldPublishButton.setEnabled(false); + heldRevealButton.setEnabled(false); + heldRemoveButton.setEnabled(true); + heldUnpublishButton.setEnabled(!unpublished); + return; + } + + heldDecryptSelectedButton.setEnabled(true); + boolean unpublished = credentialUnpublished(did); + boolean published = credentialPublished(did); + boolean revealed = credentialRevealed(did); + + heldRemoveButton.setEnabled(unpublished); + heldPublishButton.setEnabled(!published); + heldRevealButton.setEnabled(!revealed); + heldUnpublishButton.setEnabled(!unpublished); + } + + private boolean credentialPublished(String did) { + if (manifest == null || manifest.isEmpty()) { + return false; + } + Object entry = manifest.get(did); + if (!(entry instanceof java.util.Map)) { + return false; + } + Object credential = ((java.util.Map) entry).get("credential"); + return credential == null; + } + + private boolean credentialRevealed(String did) { + if (manifest == null || manifest.isEmpty()) { + return false; + } + Object entry = manifest.get(did); + if (!(entry instanceof java.util.Map)) { + return false; + } + Object credential = ((java.util.Map) entry).get("credential"); + return credential != null; + } + + private boolean credentialUnpublished(String did) { + if (manifest == null || manifest.isEmpty()) { + return true; + } + return !manifest.containsKey(did); + } + + private boolean isDeactivatedEmptyCredential(String did) { + try { + MdipDocument doc = keymasterService.resolveDID(did); + if (doc == null || doc.didDocumentMetadata == null) { + return false; + } + if (!Boolean.TRUE.equals(doc.didDocumentMetadata.deactivated)) { + return false; + } + Object data = doc.didDocumentData; + if (data == null) { + return true; + } + if (data instanceof java.util.Map) { + return ((java.util.Map) data).isEmpty(); + } + return false; + } catch (Exception e) { + return false; + } + } + + private void updateSchemaCreateState() { + String name = schemaNameField.getValue(); + schemaCreateButton.setEnabled(name != null && !name.isBlank()); + } + + private void updateGroupCreateState() { + String name = groupNameField.getValue(); + groupCreateButton.setEnabled(name != null && !name.isBlank()); + } + + private void updateHeldInputState() { + String did = heldDidField.getValue(); + boolean hasValue = did != null && !did.isBlank(); + heldResolveFieldButton.setEnabled(hasValue); + heldDecryptFieldButton.setEnabled(hasValue); + heldAcceptFieldButton.setEnabled(hasValue); + } + + private java.util.Map extractManifest(MdipDocument doc) { + if (doc == null || !(doc.didDocumentData instanceof java.util.Map)) { + return new java.util.HashMap<>(); + } + java.util.Map data = (java.util.Map) doc.didDocumentData; + Object manifestObj = data.get("manifest"); + if (manifestObj instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) manifestObj; + return new java.util.HashMap<>(map); + } + return new java.util.HashMap<>(); + } + + private String selectRegistry(List registries, String current) { + if (current != null && registries.contains(current)) { + return current; + } + for (String registry : registries) { + if ("hyperswarm".equalsIgnoreCase(registry)) { + return registry; + } + } + return registries.isEmpty() ? null : registries.get(0); + } + + private void showError(String message) { + if (message == null || message.isBlank()) { + message = "Unexpected error"; + } + Notification notification = Notification.show(message, 4000, Notification.Position.TOP_END); + notification.addThemeVariants(NotificationVariant.LUMO_ERROR); + } + + private void showSuccess(String message) { + if (message == null || message.isBlank()) { + message = "Success"; + } + Notification notification = Notification.show(message, 2500, Notification.Position.TOP_END); + notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS); + } +} diff --git a/java/demo/src/main/resources/application.yml b/java/demo/src/main/resources/application.yml new file mode 100644 index 000000000..f93f16e81 --- /dev/null +++ b/java/demo/src/main/resources/application.yml @@ -0,0 +1,20 @@ +keymaster: + gatekeeperUrl: http://localhost:4224 + registry: hyperswarm + walletDir: data + walletFile: wallet.json + +spring: + devtools: + restart: + enabled: true + livereload: + enabled: true + +server: + port: 8080 + +vaadin: + liveReload: + enabled: true + whitelisted-packages: com.vaadin,org.keychain.demo diff --git a/java/demo/target/frontend/generated-flow-imports.d.ts b/java/demo/target/frontend/generated-flow-imports.d.ts new file mode 100644 index 000000000..a01e75596 --- /dev/null +++ b/java/demo/target/frontend/generated-flow-imports.d.ts @@ -0,0 +1 @@ +export declare const addCssBlock: (block: string, before?: boolean) => void; \ No newline at end of file diff --git a/java/demo/target/frontend/generated-flow-imports.js b/java/demo/target/frontend/generated-flow-imports.js new file mode 100644 index 000000000..e5514b909 --- /dev/null +++ b/java/demo/target/frontend/generated-flow-imports.js @@ -0,0 +1,98 @@ +export const addCssBlock = function(block, before = false) { + const tpl = document.createElement('template'); + tpl.innerHTML = block; + document.head[before ? 'insertBefore' : 'appendChild'](tpl.content, document.head.firstChild); +}; + +import '@vaadin/grid/theme/lumo/vaadin-grid.js'; +import '@vaadin/grid/theme/lumo/vaadin-grid-column.js'; +import '@polymer/iron-icon/iron-icon.js'; +import '@polymer/iron-list/iron-list.js'; +import '@vaadin/accordion/theme/lumo/vaadin-accordion-panel.js'; +import '@vaadin/accordion/theme/lumo/vaadin-accordion.js'; +import '@vaadin/app-layout/theme/lumo/vaadin-app-layout.js'; +import '@vaadin/app-layout/theme/lumo/vaadin-drawer-toggle.js'; +import '@vaadin/avatar-group/theme/lumo/vaadin-avatar-group.js'; +import '@vaadin/avatar/theme/lumo/vaadin-avatar.js'; +import '@vaadin/button/theme/lumo/vaadin-button.js'; +import '@vaadin/checkbox-group/theme/lumo/vaadin-checkbox-group.js'; +import '@vaadin/checkbox/theme/lumo/vaadin-checkbox.js'; +import '@vaadin/combo-box/theme/lumo/vaadin-combo-box.js'; +import '@vaadin/common-frontend/ConnectionIndicator.js'; +import '@vaadin/confirm-dialog/theme/lumo/vaadin-confirm-dialog.js'; +import '@vaadin/context-menu/theme/lumo/vaadin-context-menu.js'; +import '@vaadin/custom-field/theme/lumo/vaadin-custom-field.js'; +import '@vaadin/date-picker/theme/lumo/vaadin-date-picker.js'; +import '@vaadin/date-time-picker/theme/lumo/vaadin-date-time-picker.js'; +import '@vaadin/details/theme/lumo/vaadin-details.js'; +import '@vaadin/dialog/theme/lumo/vaadin-dialog.js'; +import '@vaadin/email-field/theme/lumo/vaadin-email-field.js'; +import '@vaadin/field-highlighter/theme/lumo/vaadin-field-highlighter.js'; +import '@vaadin/form-layout/theme/lumo/vaadin-form-item.js'; +import '@vaadin/form-layout/theme/lumo/vaadin-form-layout.js'; +import '@vaadin/grid/theme/lumo/vaadin-grid-column-group.js'; +import '@vaadin/grid/theme/lumo/vaadin-grid-sorter.js'; +import '@vaadin/grid/theme/lumo/vaadin-grid-tree-toggle.js'; +import '@vaadin/horizontal-layout/theme/lumo/vaadin-horizontal-layout.js'; +import '@vaadin/icon/vaadin-icon.js'; +import '@vaadin/icons/vaadin-iconset.js'; +import '@vaadin/integer-field/theme/lumo/vaadin-integer-field.js'; +import '@vaadin/item/theme/lumo/vaadin-item.js'; +import '@vaadin/list-box/theme/lumo/vaadin-list-box.js'; +import '@vaadin/login/theme/lumo/vaadin-login-form.js'; +import '@vaadin/login/theme/lumo/vaadin-login-overlay.js'; +import '@vaadin/menu-bar/theme/lumo/vaadin-menu-bar.js'; +import '@vaadin/message-input/theme/lumo/vaadin-message-input.js'; +import '@vaadin/message-list/theme/lumo/vaadin-message-list.js'; +import '@vaadin/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js'; +import '@vaadin/notification/theme/lumo/vaadin-notification.js'; +import '@vaadin/number-field/theme/lumo/vaadin-number-field.js'; +import '@vaadin/password-field/theme/lumo/vaadin-password-field.js'; +import '@vaadin/polymer-legacy-adapter/style-modules.js'; +import '@vaadin/polymer-legacy-adapter/template-renderer.js'; +import '@vaadin/progress-bar/theme/lumo/vaadin-progress-bar.js'; +import '@vaadin/radio-group/theme/lumo/vaadin-radio-button.js'; +import '@vaadin/radio-group/theme/lumo/vaadin-radio-group.js'; +import '@vaadin/scroller/vaadin-scroller.js'; +import '@vaadin/select/theme/lumo/vaadin-select.js'; +import '@vaadin/split-layout/theme/lumo/vaadin-split-layout.js'; +import '@vaadin/tabs/theme/lumo/vaadin-tab.js'; +import '@vaadin/tabs/theme/lumo/vaadin-tabs.js'; +import '@vaadin/tabsheet/theme/lumo/vaadin-tabsheet.js'; +import '@vaadin/text-area/theme/lumo/vaadin-text-area.js'; +import '@vaadin/text-field/theme/lumo/vaadin-text-field.js'; +import '@vaadin/time-picker/theme/lumo/vaadin-time-picker.js'; +import '@vaadin/tooltip/theme/lumo/vaadin-tooltip.js'; +import '@vaadin/upload/src/vaadin-upload-file.js'; +import '@vaadin/upload/theme/lumo/vaadin-upload.js'; +import '@vaadin/vaadin-lumo-styles/color.js'; +import '@vaadin/vaadin-lumo-styles/sizing.js'; +import '@vaadin/vaadin-lumo-styles/spacing.js'; +import '@vaadin/vaadin-lumo-styles/style.js'; +import '@vaadin/vaadin-lumo-styles/typography.js'; +import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; +import '@vaadin/vertical-layout/theme/lumo/vaadin-vertical-layout.js'; +import '@vaadin/virtual-list/vaadin-virtual-list.js'; +import 'Frontend/generated/jar-resources/comboBoxConnector.js'; +import 'Frontend/generated/jar-resources/confirmDialogConnector.js'; +import 'Frontend/generated/jar-resources/contextMenuConnector.js'; +import 'Frontend/generated/jar-resources/contextMenuTargetConnector.js'; +import 'Frontend/generated/jar-resources/datepickerConnector.js'; +import 'Frontend/generated/jar-resources/dialogConnector.js'; +import 'Frontend/generated/jar-resources/dndConnector-es6.js'; +import 'Frontend/generated/jar-resources/flow-component-renderer.js'; +import 'Frontend/generated/jar-resources/gridConnector.js'; +import 'Frontend/generated/jar-resources/ironListConnector.js'; +import 'Frontend/generated/jar-resources/ironListStyles.js'; +import 'Frontend/generated/jar-resources/lit-renderer.ts'; +import 'Frontend/generated/jar-resources/loginOverlayConnector.js'; +import 'Frontend/generated/jar-resources/lumo-includes.ts'; +import 'Frontend/generated/jar-resources/menubarConnector.js'; +import 'Frontend/generated/jar-resources/messageListConnector.js'; +import 'Frontend/generated/jar-resources/notificationConnector.js'; +import 'Frontend/generated/jar-resources/selectConnector.js'; +import 'Frontend/generated/jar-resources/tooltip.ts'; +import 'Frontend/generated/jar-resources/vaadin-big-decimal-field.js'; +import 'Frontend/generated/jar-resources/vaadin-grid-flow-selection-column.js'; +import 'Frontend/generated/jar-resources/vaadin-time-picker/timepickerConnector.js'; +import 'Frontend/generated/jar-resources/virtualListConnector.js'; \ No newline at end of file diff --git a/java/demo/target/frontend/versions.json b/java/demo/target/frontend/versions.json new file mode 100644 index 000000000..6dae1c8d3 --- /dev/null +++ b/java/demo/target/frontend/versions.json @@ -0,0 +1,121 @@ +{ + "@vaadin/bundles": "23.3.8", + "@vaadin/accordion": "23.3.8", + "@vaadin/app-layout": "23.3.8", + "@vaadin/avatar": "23.3.8", + "@vaadin/avatar-group": "23.3.8", + "@vaadin/button": "23.3.8", + "@vaadin/checkbox": "23.3.8", + "@vaadin/checkbox-group": "23.3.8", + "@vaadin/combo-box": "23.3.8", + "@vaadin/component-base": "23.3.8", + "@vaadin/confirm-dialog": "23.3.8", + "@vaadin/context-menu": "23.3.8", + "@vaadin/custom-field": "23.3.8", + "@vaadin/date-picker": "23.3.8", + "@vaadin/date-time-picker": "23.3.8", + "@vaadin/details": "23.3.8", + "@vaadin/dialog": "23.3.8", + "@vaadin/email-field": "23.3.8", + "@vaadin/field-base": "23.3.8", + "@vaadin/field-highlighter": "23.3.8", + "@vaadin/form-layout": "23.3.8", + "@vaadin/grid": "23.3.8", + "@vaadin/horizontal-layout": "23.3.8", + "@vaadin/icon": "23.3.8", + "@vaadin/icons": "23.3.8", + "@vaadin/input-container": "23.3.8", + "@vaadin/integer-field": "23.3.8", + "@polymer/iron-icon": "3.0.1", + "@polymer/iron-iconset-svg": "3.0.1", + "@polymer/iron-list": "3.1.0", + "@polymer/iron-meta": "3.0.1", + "@polymer/iron-resizable-behavior": "3.0.1", + "@vaadin/item": "23.3.8", + "@vaadin/list-box": "23.3.8", + "@vaadin/lit-renderer": "23.3.8", + "@vaadin/login": "23.3.8", + "@vaadin/menu-bar": "23.3.8", + "@vaadin/message-input": "23.3.8", + "@vaadin/message-list": "23.3.8", + "@vaadin/multi-select-combo-box": "23.3.8", + "@vaadin/notification": "23.3.8", + "@vaadin/number-field": "23.3.8", + "@vaadin/password-field": "23.3.8", + "@vaadin/polymer-legacy-adapter": "23.3.8", + "@vaadin/progress-bar": "23.3.8", + "@vaadin/radio-group": "23.3.8", + "@vaadin/scroller": "23.3.8", + "@vaadin/select": "23.3.8", + "@vaadin/split-layout": "23.3.8", + "@vaadin/tabs": "23.3.8", + "@vaadin/tabsheet": "23.3.8", + "@vaadin/text-area": "23.3.8", + "@vaadin/text-field": "23.3.8", + "@vaadin/time-picker": "23.3.8", + "@vaadin/tooltip": "23.3.8", + "@vaadin/upload": "23.3.8", + "@vaadin/vaadin-accordion": "23.3.8", + "@vaadin/vaadin-app-layout": "23.3.8", + "@vaadin/vaadin-avatar": "23.3.8", + "@vaadin/vaadin-button": "23.3.8", + "@vaadin/vaadin-checkbox": "23.3.8", + "@vaadin/vaadin-combo-box": "23.3.8", + "@vaadin/vaadin-confirm-dialog": "23.3.8", + "@vaadin/vaadin-context-menu": "23.3.8", + "@vaadin/vaadin-custom-field": "23.3.8", + "@vaadin/vaadin-date-picker": "23.3.8", + "@vaadin/vaadin-date-time-picker": "23.3.8", + "@vaadin/vaadin-details": "23.3.8", + "@vaadin/vaadin-development-mode-detector": "2.0.6", + "@vaadin/vaadin-dialog": "23.3.8", + "@vaadin/vaadin-form-layout": "23.3.8", + "@vaadin/vaadin-grid": "23.3.8", + "@vaadin/vaadin-icon": "23.3.8", + "@vaadin/vaadin-icons": "23.3.8", + "@vaadin/vaadin-item": "23.3.8", + "@vaadin/vaadin-list-box": "23.3.8", + "@vaadin/vaadin-list-mixin": "23.3.8", + "@vaadin/vaadin-login": "23.3.8", + "@vaadin/vaadin-lumo-styles": "23.3.8", + "@vaadin/vaadin-material-styles": "23.3.8", + "@vaadin/vaadin-menu-bar": "23.3.8", + "@vaadin/vaadin-messages": "23.3.8", + "@vaadin/vaadin-notification": "23.3.8", + "@vaadin/vaadin-ordered-layout": "23.3.8", + "@vaadin/vaadin-overlay": "23.3.8", + "@vaadin/vaadin-progress-bar": "23.3.8", + "@vaadin/vaadin-radio-button": "23.3.8", + "@vaadin/router": "1.7.4", + "@vaadin/vaadin-select": "23.3.8", + "@vaadin/vaadin-split-layout": "23.3.8", + "@vaadin/vaadin-tabs": "23.3.8", + "@vaadin/vaadin-template-renderer": "23.3.8", + "@vaadin/vaadin-text-field": "23.3.8", + "@vaadin/vaadin-themable-mixin": "23.3.8", + "@vaadin/vaadin-time-picker": "23.3.8", + "@vaadin/vaadin-upload": "23.3.8", + "@vaadin/vaadin-usage-statistics": "2.1.2", + "@vaadin/vaadin-virtual-list": "23.3.8", + "@vaadin/vertical-layout": "23.3.8", + "@vaadin/virtual-list": "23.3.8", + "@polymer/polymer": "3.5.1", + "@vaadin/common-frontend": "0.0.17", + "construct-style-sheets-polyfill": "3.1.0", + "date-fns": "2.29.3", + "lit": "2.6.1", + "@rollup/plugin-replace": "3.1.0", + "@rollup/pluginutils": "4.1.0", + "async": "3.2.2", + "glob": "7.2.3", + "mkdirp": "1.0.4", + "rollup-plugin-brotli": "3.1.0", + "strip-css-comments": "5.0.0", + "transform-ast": "2.4.4", + "typescript": "4.9.3", + "vite": "3.2.5", + "vite-plugin-checker": "0.5.4", + "workbox-build": "6.5.4", + "workbox-core": "6.5.4", + "workbox-precaching": "6.5.4" +} diff --git a/java/demo/target/plugins/application-theme-plugin/application-theme-plugin.js b/java/demo/target/plugins/application-theme-plugin/application-theme-plugin.js new file mode 100644 index 000000000..b5046c179 --- /dev/null +++ b/java/demo/target/plugins/application-theme-plugin/application-theme-plugin.js @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +const { processThemeResources, extractThemeName, findParentThemes } = require('./theme-handle'); + +/** + * The application theme plugin is for generating, collecting and copying of theme files for the application theme. + * + * The plugin should be supplied with the paths for + * + * themeResourceFolder - theme folder where flow copies local and jar resource frontend files + * themeProjectFolders - array of possible locations for theme folders inside the project + * projectStaticAssetsOutputFolder - path to where static assets should be put + * frontendGeneratedFolder - the path to where frontend auto-generated files are put + * + * @throws Error in constructor if required option is not received + */ +class ApplicationThemePlugin { + constructor(options) { + this.options = options; + + if (!this.options.themeResourceFolder) { + throw new Error('Missing themeResourceFolder path'); + } + if (!this.options.projectStaticAssetsOutputFolder) { + throw new Error('Missing projectStaticAssetsOutputFolder path'); + } + if (!this.options.themeProjectFolders) { + throw new Error('Missing themeProjectFolders path array'); + } + if (!this.options.frontendGeneratedFolder) { + throw new Error('Missing frontendGeneratedFolder path'); + } + } + + apply(compiler) { + const logger = compiler.getInfrastructureLogger('ApplicationThemePlugin'); + + compiler.hooks.afterEnvironment.tap('ApplicationThemePlugin', () => processThemeResources(this.options, logger)); + } +} + +module.exports = { ApplicationThemePlugin, processThemeResources, extractThemeName, findParentThemes }; diff --git a/java/demo/target/plugins/application-theme-plugin/package.json b/java/demo/target/plugins/application-theme-plugin/package.json new file mode 100644 index 000000000..0e900e7e5 --- /dev/null +++ b/java/demo/target/plugins/application-theme-plugin/package.json @@ -0,0 +1,26 @@ +{ + "description": "application-theme-plugin", + "keywords": [ + "plugin", + "application theme" + ], + "repository": "vaadin/flow", + "name": "@vaadin/application-theme-plugin", + "version": "0.4.2", + "main": "application-theme-plugin.js", + "author": "Vaadin Ltd", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/vaadin/flow/issues" + }, + "dependencies": { + "mkdirp": "0.5.6", + "glob": "7.2.3" + }, + "files": [ + "application-theme-plugin.js", + "theme-generator.js", + "theme-copy.js", + "theme-handle.js" + ] +} diff --git a/java/demo/target/plugins/application-theme-plugin/theme-copy.js b/java/demo/target/plugins/application-theme-plugin/theme-copy.js new file mode 100644 index 000000000..76e352fe3 --- /dev/null +++ b/java/demo/target/plugins/application-theme-plugin/theme-copy.js @@ -0,0 +1,203 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * This contains functions and features used to copy theme files. + */ + +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const mkdirp = require('mkdirp'); + +const ignoredFileExtensions = ['.css', '.js', '.json']; + +/** + * Copy theme static resources to static assets folder. All files in the theme + * folder will be copied excluding css, js and json files that will be + * handled by webpack and not be shared as static files. + * + * @param {string} themeFolder Folder with theme file + * @param {string} projectStaticAssetsOutputFolder resources output folder + * @param {object} logger plugin logger + */ +function copyThemeResources(themeFolder, projectStaticAssetsOutputFolder, logger) { + const staticAssetsThemeFolder = path.resolve(projectStaticAssetsOutputFolder, 'themes', path.basename(themeFolder)); + const collection = collectFolders(themeFolder, logger); + + // Only create assets folder if there are files to copy. + if (collection.files.length > 0) { + mkdirp.sync(staticAssetsThemeFolder); + // create folders with + collection.directories.forEach((directory) => { + const relativeDirectory = path.relative(themeFolder, directory); + const targetDirectory = path.resolve(staticAssetsThemeFolder, relativeDirectory); + + mkdirp.sync(targetDirectory); + }); + + collection.files.forEach((file) => { + const relativeFile = path.relative(themeFolder, file); + const targetFile = path.resolve(staticAssetsThemeFolder, relativeFile); + copyFileIfAbsentOrNewer(file, targetFile, logger); + }); + } +} + +/** + * Collect all folders with copyable files and all files to be copied. + * Foled will not be added if no files in folder or subfolders. + * + * Files will not contain files with ignored extensions and folders only containing ignored files will not be added. + * + * @param folderToCopy folder we will copy files from + * @param logger plugin logger + * @return {{directories: [], files: []}} object containing directories to create and files to copy + */ +function collectFolders(folderToCopy, logger) { + const collection = { directories: [], files: [] }; + logger.trace('files in directory', fs.readdirSync(folderToCopy)); + fs.readdirSync(folderToCopy).forEach((file) => { + const fileToCopy = path.resolve(folderToCopy, file); + try { + if (fs.statSync(fileToCopy).isDirectory()) { + logger.debug('Going through directory', fileToCopy); + const result = collectFolders(fileToCopy, logger); + if (result.files.length > 0) { + collection.directories.push(fileToCopy); + logger.debug('Adding directory', fileToCopy); + collection.directories.push.apply(collection.directories, result.directories); + collection.files.push.apply(collection.files, result.files); + } + } else if (!ignoredFileExtensions.includes(path.extname(fileToCopy))) { + logger.debug('Adding file', fileToCopy); + collection.files.push(fileToCopy); + } + } catch (error) { + handleNoSuchFileError(fileToCopy, error, logger); + } + }); + return collection; +} + +/** + * Copy any static node_modules assets marked in theme.json to + * project static assets folder. + * + * The theme.json content for assets is set up as: + * { + * assets: { + * "node_module identifier": { + * "copy-rule": "target/folder", + * } + * } + * } + * + * This would mean that an asset would be built as: + * "@fortawesome/fontawesome-free": { + * "svgs/regular/**": "fortawesome/icons" + * } + * Where '@fortawesome/fontawesome-free' is the npm package, 'svgs/regular/**' is what should be copied + * and 'fortawesome/icons' is the target directory under projectStaticAssetsOutputFolder where things + * will get copied to. + * + * Note! there can be multiple copy-rules with target folders for one npm package asset. + * + * @param {string} themeName name of the theme we are copying assets for + * @param {json} themeProperties theme properties json with data on assets + * @param {string} projectStaticAssetsOutputFolder project output folder where we copy assets to under theme/[themeName] + * @param {object} logger plugin logger + */ +function copyStaticAssets(themeName, themeProperties, projectStaticAssetsOutputFolder, logger) { + const assets = themeProperties['assets']; + if (!assets) { + logger.debug('no assets to handle no static assets were copied'); + return; + } + + fs.mkdirSync(projectStaticAssetsOutputFolder, { + recursive: true + }); + const missingModules = checkModules(Object.keys(assets)); + if (missingModules.length > 0) { + throw Error( + "Missing npm modules '" + + missingModules.join("', '") + + "' for assets marked in 'theme.json'.\n" + + "Install package(s) by adding a @NpmPackage annotation or install it using 'npm/pnpm i'" + ); + } + Object.keys(assets).forEach((module) => { + const copyRules = assets[module]; + Object.keys(copyRules).forEach((copyRule) => { + const nodeSources = path.resolve('node_modules/', module, copyRule); + const files = glob.sync(nodeSources, { nodir: true }); + const targetFolder = path.resolve(projectStaticAssetsOutputFolder, 'themes', themeName, copyRules[copyRule]); + + fs.mkdirSync(targetFolder, { + recursive: true + }); + files.forEach((file) => { + const copyTarget = path.resolve(targetFolder, path.basename(file)); + copyFileIfAbsentOrNewer(file, copyTarget, logger); + }); + }); + }); +} + +function checkModules(modules) { + const missing = []; + + modules.forEach((module) => { + if (!fs.existsSync(path.resolve('node_modules/', module))) { + missing.push(module); + } + }); + + return missing; +} + +/** + * Copies given file to a given target path, if target file doesn't exist or if + * file to copy is newer. + * @param {string} fileToCopy path of the file to copy + * @param {string} copyTarget path of the target file + * @param {object} logger plugin logger + */ +function copyFileIfAbsentOrNewer(fileToCopy, copyTarget, logger) { + try { + if (!fs.existsSync(copyTarget) || fs.statSync(copyTarget).mtime < fs.statSync(fileToCopy).mtime) { + logger.trace('Copying: ', fileToCopy, '=>', copyTarget); + fs.copyFileSync(fileToCopy, copyTarget); + } + } catch (error) { + handleNoSuchFileError(fileToCopy, error, logger); + } +} + +// Ignores errors due to file missing during theme processing +// This may happen for example when an IDE creates a temporary file +// and then immediately deletes it +function handleNoSuchFileError(file, error, logger) { + if (error.code === 'ENOENT') { + logger.warn('Ignoring not existing file ' + file + + '. File may have been deleted during theme processing.'); + } else { + throw error; + } +} + +module.exports = { checkModules, copyStaticAssets, copyThemeResources }; diff --git a/java/demo/target/plugins/application-theme-plugin/theme-generator.js b/java/demo/target/plugins/application-theme-plugin/theme-generator.js new file mode 100644 index 000000000..e958fb421 --- /dev/null +++ b/java/demo/target/plugins/application-theme-plugin/theme-generator.js @@ -0,0 +1,304 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * This file handles the generation of the '[theme-name].js' to + * the themes/[theme-name] folder according to properties from 'theme.json'. + */ +const glob = require('glob'); +const path = require('path'); +const fs = require('fs'); +const { checkModules } = require('./theme-copy'); + +// Special folder inside a theme for component themes that go inside the component shadow root +const themeComponentsFolder = 'components'; +// The contents of a global CSS file with this name in a theme is always added to +// the document. E.g. @font-face must be in this +const documentCssFile = 'document.css'; +// styles.css is the only entrypoint css file with document.css. Everything else should be imported using css @import +const stylesCssFile = 'styles.css'; + +const headerImport = `import 'construct-style-sheets-polyfill'; +`; + +const createLinkReferences = ` +const createLinkReferences = (css, target) => { + // Unresolved urls are written as '@import url(text);' or '@import "text";' to the css + // media query can be present on @media tag or on @import directive after url + // Note that with Vite production build there is no space between @import and "text" + // [0] is the full match + // [1] matches the media query + // [2] matches the url + // [3] matches the quote char surrounding in '@import "..."' + // [4] matches the url in '@import "..."' + // [5] matches media query on @import statement + const importMatcher = /(?:@media\\s(.+?))?(?:\\s{)?\\@import\\s*(?:url\\(\\s*['"]?(.+?)['"]?\\s*\\)|(["'])((?:\\\\.|[^\\\\])*?)\\3)([^;]*);(?:})?/g + + // Only cleanup if comment exist + if(/\\/\\*(.|[\\r\\n])*?\\*\\//gm.exec(css) != null) { + // clean up comments + css = stripCssComments(css); + } + + var match; + var styleCss = css; + + // For each external url import add a link reference + while((match = importMatcher.exec(css)) !== null) { + styleCss = styleCss.replace(match[0], ""); + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = match[2] || match[4]; + const media = match[1] || match[5]; + if (media) { + link.media = media; + } + // For target document append to head else append to target + if (target === document) { + document.head.appendChild(link); + } else { + target.appendChild(link); + } + }; + return styleCss; +}; +`; + +const injectGlobalCssMethod = ` +// target: Document | ShadowRoot +export const injectGlobalCss = (css, target, first) => { + if(target === document) { + const hash = getHash(css); + if (window.Vaadin.theme.injectedGlobalCss.indexOf(hash) !== -1) { + return; + } + window.Vaadin.theme.injectedGlobalCss.push(hash); + } + const sheet = new CSSStyleSheet(); + sheet.replaceSync(createLinkReferences(css,target)); + if (first) { + target.adoptedStyleSheets = [sheet, ...target.adoptedStyleSheets]; + } else { + target.adoptedStyleSheets = [...target.adoptedStyleSheets, sheet]; + } +}; +`; + +/** + * Generate the [themeName].js file for themeFolder which collects all required information from the folder. + * + * @param {string} themeFolder folder of the theme + * @param {string} themeName name of the handled theme + * @param {JSON} themeProperties content of theme.json + * @param {boolean} productionMode true if making a production build. + * @returns {string} theme file content + */ +function generateThemeFile(themeFolder, themeName, themeProperties, productionMode) { + const styles = path.resolve(themeFolder, stylesCssFile); + const document = path.resolve(themeFolder, documentCssFile); + const componentsFiles = glob.sync('*.css', { + cwd: path.resolve(themeFolder, themeComponentsFolder), + nodir: true + }); + + let themeFile = headerImport; + + if (componentsFiles.length > 0) { + themeFile += "import { unsafeCSS, registerStyles } from '@vaadin/vaadin-themable-mixin/register-styles';\n"; + } + + if (themeProperties.parent) { + themeFile += `import {applyTheme as applyBaseTheme} from './theme-${themeProperties.parent}.generated.js';\n`; + } + themeFile += `import stripCssComments from 'strip-css-comments';\n`; + + themeFile += createLinkReferences; + themeFile += injectGlobalCssMethod; + + const imports = []; + const globalCssCode = []; + const lumoCssCode = []; + const componentCssCode = []; + const parentTheme = themeProperties.parent ? 'applyBaseTheme(target);\n' : ''; + + const themeIdentifier = '_vaadintheme_' + themeName + '_'; + const lumoCssFlag = '_vaadinthemelumoimports_'; + const globalCssFlag = themeIdentifier + 'globalCss'; + const componentCssFlag = themeIdentifier + 'componentCss'; + + if (!fs.existsSync(styles)) { + if (productionMode) { + throw new Error(`styles.css file is missing and is needed for '${themeName}' in folder '${themeFolder}'`); + } + fs.writeFileSync( + styles, + '/* Import your application global css files here or add the styles directly to this file */', + 'utf8' + ); + } + + // styles.css will always be available as we write one if it doesn't exist. + let filename = path.basename(styles); + let variable = camelCase(filename); + imports.push(`import ${variable} from 'themes/${themeName}/${filename}?inline';\n`); + /* Lumo must be first so that custom styles override Lumo styles */ + const lumoImports = themeProperties.lumoImports || ['color', 'typography']; + if (lumoImports && lumoImports.length > 0) { + lumoImports.forEach((lumoImport) => { + imports.push(`import { ${lumoImport} } from '@vaadin/vaadin-lumo-styles/${lumoImport}.js';\n`); + }); + + lumoImports.forEach((lumoImport) => { + lumoCssCode.push(`injectGlobalCss(${lumoImport}.cssText, target, true);\n`); + }); + } + + globalCssCode.push(`injectGlobalCss(${variable}.toString(), target);\n `); + if (fs.existsSync(document)) { + filename = path.basename(document); + variable = camelCase(filename); + imports.push(`import ${variable} from 'themes/${themeName}/${filename}?inline';\n`); + globalCssCode.push(`injectGlobalCss(${variable}.toString(), document);\n `); + } + + let i = 0; + if (themeProperties.documentCss) { + const missingModules = checkModules(themeProperties.documentCss); + if (missingModules.length > 0) { + throw Error( + "Missing npm modules or files '" + + missingModules.join("', '") + + "' for documentCss marked in 'theme.json'.\n" + + "Install or update package(s) by adding a @NpmPackage annotation or install it using 'npm/pnpm i'" + ); + } + themeProperties.documentCss.forEach((cssImport) => { + const variable = 'module' + i++; + imports.push(`import ${variable} from '${cssImport}?inline';\n`); + // Due to chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=336876 font-face will not work + // inside shadowRoot so we need to inject it there also. + globalCssCode.push(`if(target !== document) { + injectGlobalCss(${variable}.toString(), target); + }\n `); + globalCssCode.push(`injectGlobalCss(${variable}.toString(), document);\n `); + }); + } + if (themeProperties.importCss) { + const missingModules = checkModules(themeProperties.importCss); + if (missingModules.length > 0) { + throw Error( + "Missing npm modules or files '" + + missingModules.join("', '") + + "' for importCss marked in 'theme.json'.\n" + + "Install or update package(s) by adding a @NpmPackage annotation or install it using 'npm/pnpm i'" + ); + } + themeProperties.importCss.forEach((cssPath) => { + const variable = 'module' + i++; + imports.push(`import ${variable} from '${cssPath}';\n`); + globalCssCode.push(`injectGlobalCss(${variable}.toString(), target);\n`); + }); + } + + componentsFiles.forEach((componentCss) => { + const filename = path.basename(componentCss); + const tag = filename.replace('.css', ''); + const variable = camelCase(filename); + imports.push(`import ${variable} from 'themes/${themeName}/${themeComponentsFolder}/${filename}?inline';\n`); + // Don't format as the generated file formatting will get wonky! + const componentString = `registerStyles( + '${tag}', + unsafeCSS(${variable}.toString()) + ); + `; + componentCssCode.push(componentString); + }); + + themeFile += imports.join(''); + themeFile += ` +window.Vaadin = window.Vaadin || {}; +window.Vaadin.theme = window.Vaadin.theme || {}; +window.Vaadin.theme.injectedGlobalCss = []; + +/** + * Calculate a 32 bit FNV-1a hash + * Found here: https://gist.github.com/vaiorabbit/5657561 + * Ref.: http://isthe.com/chongo/tech/comp/fnv/ + * + * @param {string} str the input value + * @returns {string} 32 bit (as 8 byte hex string) + */ +function hashFnv32a(str) { + /*jshint bitwise:false */ + let i, l, hval = 0x811c9dc5; + + for (i = 0, l = str.length; i < l; i++) { + hval ^= str.charCodeAt(i); + hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); + } + + // Convert to 8 digit hex string + return ("0000000" + (hval >>> 0).toString(16)).substr(-8); +} + +/** + * Calculate a 64 bit hash for the given input. + * Double hash is used to significantly lower the collision probability. + * + * @param {string} input value to get hash for + * @returns {string} 64 bit (as 16 byte hex string) + */ +function getHash(input) { + let h1 = hashFnv32a(input); // returns 32 bit (as 8 byte hex string) + return h1 + hashFnv32a(h1 + input); +} +`; + + // Don't format as the generated file formatting will get wonky! + // If targets check that we only register the style parts once, checks exist for global css and component css + const themeFileApply = `export const applyTheme = (target) => { + ${parentTheme} + ${globalCssCode.join('')} + + if (!document['${componentCssFlag}']) { + ${componentCssCode.join('')} + document['${componentCssFlag}'] = true; + } + ${lumoCssCode.join('')} +} +`; + + themeFile += themeFileApply; + + return themeFile; +} + +/** + * Make given string into camelCase. + * + * @param {string} str string to make into cameCase + * @returns {string} camelCased version + */ +function camelCase(str) { + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) { + return index === 0 ? word.toLowerCase() : word.toUpperCase(); + }) + .replace(/\s+/g, '') + .replace(/\.|\-/g, ''); +} + +module.exports = generateThemeFile; diff --git a/java/demo/target/plugins/application-theme-plugin/theme-handle.js b/java/demo/target/plugins/application-theme-plugin/theme-handle.js new file mode 100644 index 000000000..9df0dcab1 --- /dev/null +++ b/java/demo/target/plugins/application-theme-plugin/theme-handle.js @@ -0,0 +1,264 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * This file contains functions for look up and handle the theme resources + * for application theme plugin. + */ +const fs = require('fs'); +const path = require('path'); +const generateThemeFile = require('./theme-generator'); +const { copyStaticAssets, copyThemeResources } = require('./theme-copy'); + +// matches theme name in './theme-my-theme.generated.js' +const nameRegex = /theme-(.*)\.generated\.js/; + +let prevThemeName = undefined; +let firstThemeName = undefined; + +/** + * Looks up for a theme resources in a current project and in jar dependencies, + * copies the found resources and generates/updates meta data for webpack + * compilation. + * + * @param {object} options application theme plugin mandatory options, + * @see {@link ApplicationThemePlugin} + * + * @param logger application theme plugin logger + */ +function processThemeResources(options, logger) { + const themeName = extractThemeName(options.frontendGeneratedFolder); + if (themeName) { + if (!prevThemeName && !firstThemeName) { + firstThemeName = themeName; + } else if ( + (prevThemeName && prevThemeName !== themeName && firstThemeName !== themeName) || + (!prevThemeName && firstThemeName !== themeName) + ) { + // Warning message is shown to the developer when: + // 1. He is switching to any theme, which is differ from one being set up + // on application startup, by changing theme name in `@Theme()` + // 2. He removes or comments out `@Theme()` to see how the app + // looks like without theming, and then again brings `@Theme()` back + // with a themeName which is differ from one being set up on application + // startup. + const warning = `Attention: Active theme is switched to '${themeName}'.`; + const description = ` + Note that adding new style sheet files to '/themes/${themeName}/components', + may not be taken into effect until the next application restart. + Changes to already existing style sheet files are being reloaded as before.`; + logger.warn('*******************************************************************'); + logger.warn(warning); + logger.warn(description); + logger.warn('*******************************************************************'); + } + prevThemeName = themeName; + + findThemeFolderAndHandleTheme(themeName, options, logger); + } else { + // This is needed in the situation that the user decides to comment or + // remove the @Theme(...) completely to see how the application looks + // without any theme. Then when the user brings back one of the themes, + // the previous theme should be undefined to enable us to detect the change. + prevThemeName = undefined; + logger.debug('Skipping Vaadin application theme handling.'); + logger.trace('Most likely no @Theme annotation for application or only themeClass used.'); + } +} + +/** + * Search for the given theme in the project and resource folders. + * + * @param {string} themeName name of theme to find + * @param {object} options application theme plugin mandatory options, + * @see {@link ApplicationThemePlugin} + * @param logger application theme plugin logger + * @return true or false for if theme was found + */ +function findThemeFolderAndHandleTheme(themeName, options, logger) { + let themeFound = false; + for (let i = 0; i < options.themeProjectFolders.length; i++) { + const themeProjectFolder = options.themeProjectFolders[i]; + if (fs.existsSync(themeProjectFolder)) { + logger.debug("Searching themes folder '" + themeProjectFolder + "' for theme '" + themeName + "'"); + const handled = handleThemes(themeName, themeProjectFolder, options, logger); + if (handled) { + if (themeFound) { + throw new Error( + "Found theme files in '" + + themeProjectFolder + + "' and '" + + themeFound + + "'. Theme should only be available in one folder" + ); + } + logger.debug("Found theme files from '" + themeProjectFolder + "'"); + themeFound = themeProjectFolder; + } + } + } + + if (fs.existsSync(options.themeResourceFolder)) { + if (themeFound && fs.existsSync(path.resolve(options.themeResourceFolder, themeName))) { + throw new Error( + "Theme '" + + themeName + + "'should not exist inside a jar and in the project at the same time\n" + + 'Extending another theme is possible by adding { "parent": "my-parent-theme" } entry to the theme.json file inside your theme folder.' + ); + } + logger.debug( + "Searching theme jar resource folder '" + options.themeResourceFolder + "' for theme '" + themeName + "'" + ); + handleThemes(themeName, options.themeResourceFolder, options, logger); + themeFound = true; + } + return themeFound; +} + +/** + * Copies static resources for theme and generates/writes the + * [theme-name].generated.js for webpack to handle. + * + * Note! If a parent theme is defined it will also be handled here so that the parent theme generated file is + * generated in advance of the theme generated file. + * + * @param {string} themeName name of theme to handle + * @param {string} themesFolder folder containing application theme folders + * @param {object} options application theme plugin mandatory options, + * @see {@link ApplicationThemePlugin} + * @param {object} logger plugin logger instance + * + * @throws Error if parent theme defined, but can't locate parent theme + * + * @returns true if theme was found else false. + */ +function handleThemes(themeName, themesFolder, options, logger) { + const themeFolder = path.resolve(themesFolder, themeName); + if (fs.existsSync(themeFolder)) { + logger.debug('Found theme ', themeName, ' in folder ', themeFolder); + + const themeProperties = getThemeProperties(themeFolder); + + // If theme has parent handle parent theme immediately. + if (themeProperties.parent) { + const found = findThemeFolderAndHandleTheme(themeProperties.parent, options, logger); + if (!found) { + throw new Error( + "Could not locate files for defined parent theme '" + + themeProperties.parent + + "'.\n" + + 'Please verify that dependency is added or theme folder exists.' + ); + } + } + copyStaticAssets(themeName, themeProperties, options.projectStaticAssetsOutputFolder, logger); + copyThemeResources(themeFolder, options.projectStaticAssetsOutputFolder, logger); + const themeFile = generateThemeFile(themeFolder, themeName, themeProperties, !options.devMode); + + fs.writeFileSync(path.resolve(options.frontendGeneratedFolder, 'theme-' + themeName + '.generated.js'), themeFile); + return true; + } + return false; +} + +function getThemeProperties(themeFolder) { + const themePropertyFile = path.resolve(themeFolder, 'theme.json'); + if (!fs.existsSync(themePropertyFile)) { + return {}; + } + const themePropertyFileAsString = fs.readFileSync(themePropertyFile); + if (themePropertyFileAsString.length === 0) { + return {}; + } + return JSON.parse(themePropertyFileAsString); +} + +/** + * Extracts current theme name from auto-generated 'theme.js' file located on a + * given folder. + * @param frontendGeneratedFolder folder in project containing 'theme.js' file + * @returns {string} current theme name + */ +function extractThemeName(frontendGeneratedFolder) { + if (!frontendGeneratedFolder) { + throw new Error( + "Couldn't extract theme name from 'theme.js'," + + ' because the path to folder containing this file is empty. Please set' + + ' the a correct folder path in ApplicationThemePlugin constructor' + + ' parameters.' + ); + } + const generatedThemeFile = path.resolve(frontendGeneratedFolder, 'theme.js'); + if (fs.existsSync(generatedThemeFile)) { + // read theme name from the 'generated/theme.js' as there we always + // mark the used theme for webpack to handle. + const themeName = nameRegex.exec(fs.readFileSync(generatedThemeFile, { encoding: 'utf8' }))[1]; + if (!themeName) { + throw new Error("Couldn't parse theme name from '" + generatedThemeFile + "'."); + } + return themeName; + } else { + return ''; + } +} + +/** + * Finds all the parent themes located in the project themes folders and in + * the JAR dependencies with respect to the given custom theme with + * {@code themeName}. + * @param {string} themeName given custom theme name to look parents for + * @param {object} options application theme plugin mandatory options, + * @see {@link ApplicationThemePlugin} + * @returns {string[]} array of paths to found parent themes with respect to the + * given custom theme + */ +function findParentThemes(themeName, options) { + const existingThemeFolders = [options.themeResourceFolder, ...options.themeProjectFolders].filter((folder) => + fs.existsSync(folder) + ); + return collectParentThemes(themeName, existingThemeFolders, false); +} + +function collectParentThemes(themeName, themeFolders, isParent) { + let foundParentThemes = []; + themeFolders.forEach((folder) => { + const themeFolder = path.resolve(folder, themeName); + if (fs.existsSync(themeFolder)) { + const themeProperties = getThemeProperties(themeFolder); + + if (themeProperties.parent) { + foundParentThemes.push(...collectParentThemes(themeProperties.parent, themeFolders, true)); + if (!foundParentThemes.length) { + throw new Error( + "Could not locate files for defined parent theme '" + + themeProperties.parent + + "'.\n" + + 'Please verify that dependency is added or theme folder exists.' + ); + } + } + // Add a theme path to result collection only if a given themeName + // is supposed to be a parent theme + if (isParent) { + foundParentThemes.push(themeFolder); + } + } + }); + return foundParentThemes; +} + +module.exports = { processThemeResources, extractThemeName, findParentThemes }; diff --git a/java/demo/target/plugins/build-status-plugin/build-status-plugin.js b/java/demo/target/plugins/build-status-plugin/build-status-plugin.js new file mode 100644 index 000000000..afa5a8788 --- /dev/null +++ b/java/demo/target/plugins/build-status-plugin/build-status-plugin.js @@ -0,0 +1,57 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * This plugin handles communicates to Java dev-mode handler status of compilation + */ +class BuildStatusPlugin { + constructor(options = {}) { + this.options = options; + } + + apply(compiler) { + const logger = compiler.getInfrastructureLogger('build-status'); + + compiler.hooks.done.tap('done', (stats) => { + // Defer notification and let the webpack-dev-server finish + setTimeout(() => { + const errors = stats.compilation.errors; + const warnings = stats.compilation.warnings; + if (errors.length > 0) { + logger.info( + `${humanReadable(errors.length, 'error')} and ${humanReadable(warnings.length, 'warning')} were reported.` + ); + logger.info(': Failed to compile.'); + } else { + if (warnings.length > 0) { + logger.info(`${humanReadable(warnings.length, 'warning')} were reported.`); + } + logger.info(': Compiled.'); + } + }, 0); + }); + } +} + +function humanReadable(count, label) { + if (count % 10 == 1) { + return `${count} ${label}`; + } else { + return `${count} ${label}s`; + } +} + +module.exports = BuildStatusPlugin; diff --git a/java/demo/target/plugins/build-status-plugin/package.json b/java/demo/target/plugins/build-status-plugin/package.json new file mode 100644 index 000000000..0fed60d35 --- /dev/null +++ b/java/demo/target/plugins/build-status-plugin/package.json @@ -0,0 +1,18 @@ +{ + "description": "build-status-plugin", + "keywords": [ + "plugin" + ], + "repository": "vaadin/flow", + "name": "@vaadin/build-status-plugin", + "version": "1.0.0", + "main": "build-status-plugin.js", + "author": "Vaadin Ltd", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/vaadin/flow/issues" + }, + "files": [ + "build-status-plugin.js" + ] +} diff --git a/java/demo/target/plugins/rollup-plugin-postcss-lit-custom/package.json b/java/demo/target/plugins/rollup-plugin-postcss-lit-custom/package.json new file mode 100644 index 000000000..13f61b072 --- /dev/null +++ b/java/demo/target/plugins/rollup-plugin-postcss-lit-custom/package.json @@ -0,0 +1,18 @@ +{ + "description": "rollup-plugin-postcss-lit-custom", + "keywords": [ + "plugin" + ], + "repository": "vaadin/flow", + "name": "@vaadin/rollup-plugin-postcss-lit-custom", + "version": "1.0.0", + "main": "index.js", + "author": "Vaadin Ltd", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/vaadin/flow/issues" + }, + "files": [ + "rollup-plugin-postcss-lit.js" + ] +} diff --git a/java/demo/target/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js b/java/demo/target/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js new file mode 100644 index 000000000..9fd2a38b6 --- /dev/null +++ b/java/demo/target/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js @@ -0,0 +1,92 @@ +/** + * MIT License + +Copyright (c) 2019 Umberto Pepato + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +// This is https://github.com/umbopepato/rollup-plugin-postcss-lit 2.0.0 + https://github.com/umbopepato/rollup-plugin-postcss-lit/pull/54 +// to make it work with Vite 3 +// Once / if https://github.com/umbopepato/rollup-plugin-postcss-lit/pull/54 is merged this should be removed and rollup-plugin-postcss-lit should be used instead + +const createFilter = require('@rollup/pluginutils').createFilter; +const transformAst = require('transform-ast'); + +const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g; + +const escape = (str) => + str + .replace(assetUrlRE, '${unsafeCSSTag("__VITE_ASSET__$1__$2")}') + .replace(/`/g, '\\`') + .replace(/\\(?!`)/g, '\\\\'); + +module.exports = function postcssLit(options = {}) { + const defaultOptions = { + include: '**/*.{css,sss,pcss,styl,stylus,sass,scss,less}', + exclude: null, + importPackage: 'lit' + }; + + const opts = { ...defaultOptions, ...options }; + const filter = createFilter(opts.include, opts.exclude); + + return { + name: 'postcss-lit', + enforce: 'post', + transform(code, id) { + if (!filter(id)) return; + const ast = this.parse(code, {}); + // export default const css; + let defaultExportName; + + // export default '...'; + let isDeclarationLiteral = false; + const magicString = transformAst(code, { ast: ast }, (node) => { + if (node.type === 'ExportDefaultDeclaration') { + defaultExportName = node.declaration.name; + + isDeclarationLiteral = node.declaration.type === 'Literal'; + } + }); + + if (!defaultExportName && !isDeclarationLiteral) { + return; + } + magicString.walk((node) => { + if (defaultExportName && node.type === 'VariableDeclaration') { + const exportedVar = node.declarations.find((d) => d.id.name === defaultExportName); + if (exportedVar) { + exportedVar.init.edit.update(`cssTag\`${escape(exportedVar.init.value)}\``); + } + } + + if (isDeclarationLiteral && node.type === 'ExportDefaultDeclaration') { + node.declaration.edit.update(`cssTag\`${escape(node.declaration.value)}\``); + } + }); + magicString.prepend(`import {css as cssTag, unsafeCSS as unsafeCSSTag} from '${opts.importPackage}';\n`); + return { + code: magicString.toString(), + map: magicString.generateMap({ + hires: true + }) + }; + } + }; +}; diff --git a/java/demo/target/plugins/theme-live-reload-plugin/package.json b/java/demo/target/plugins/theme-live-reload-plugin/package.json new file mode 100644 index 000000000..abb155494 --- /dev/null +++ b/java/demo/target/plugins/theme-live-reload-plugin/package.json @@ -0,0 +1,20 @@ +{ + "description": "theme-live-reload-plugin", + "keywords": [ + "plugin", + "application theme", + "live-reload" + ], + "repository": "vaadin/flow", + "name": "@vaadin/theme-live-reload-plugin", + "version": "2.0.0", + "main": "theme-live-reload-plugin.js", + "author": "Vaadin Ltd", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/vaadin/flow/issues" + }, + "files": [ + "theme-live-reload-plugin.js" + ] +} diff --git a/java/demo/target/plugins/theme-live-reload-plugin/theme-live-reload-plugin.js b/java/demo/target/plugins/theme-live-reload-plugin/theme-live-reload-plugin.js new file mode 100644 index 000000000..a49b0da88 --- /dev/null +++ b/java/demo/target/plugins/theme-live-reload-plugin/theme-live-reload-plugin.js @@ -0,0 +1,115 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * This plugin handles adding/deleting theme resources events and triggers + * theme meta data re-generation and application theme update on the fly. + */ +class ThemeLiveReloadPlugin { + /** + * Create a new instance of ThemeLiveReloadPlugin + * @param processThemeResourcesCallback callback which is called on + * adding/deleting of theme resource files to re-generate theme meta + * data and apply theme changes to application. + */ + constructor(processThemeResourcesCallback) { + if (!processThemeResourcesCallback || typeof processThemeResourcesCallback !== 'function') { + throw new Error( + "Couldn't instantiate a ThemeLiveReloadPlugin" + + ' instance, because theme resources process callback is not set' + + ' and, thus, no information provided what to do upon' + + ' adding/deleting theme resource files. Please provide this' + + ' callback as a ThemeLiveReloadPlugin constructor parameter.' + ); + } + this.processThemeResourcesCallback = processThemeResourcesCallback; + // Component style sheet might be deleted from parent theme folder, so + // the regexp does not contain the exact theme name + this.componentStyleFileRegexp = /(\\|\/)themes\1([\s\S]*)\1components\1(.*)\.css$/; + // There might be several theme generated files in the generated + // folder, so the regexp does not contain the exact theme name + this.themeGeneratedFileRegexp = /theme-[\s\S]*?\.generated\.js$/; + } + + apply(compiler) { + // Adds a hook for theme files change event + compiler.hooks.watchRun.tapAsync('ThemeLiveReloadPlugin', (compilation, callback) => { + const logger = compiler.getInfrastructureLogger('ThemeLiveReloadPlugin'); + const changedFilesMap = compiler.watchFileSystem.watcher.mtimes; + if (changedFilesMap !== {}) { + let themeName = undefined; + let themeGeneratedFileChanged = false; + let themeGeneratedFileDeleted = false; + let deletedComponentStyleFile = undefined; + const changedFilesPaths = Object.keys(changedFilesMap); + logger.debug('Detected changes in the following files ' + changedFilesPaths); + changedFilesPaths.forEach((changedFilePath) => { + const file = `${changedFilePath}`; + const themeGeneratedFileChangedNow = file.match(this.themeGeneratedFileRegexp); + const timestamp = changedFilesMap[changedFilePath]; + // null or negative timestamp means file delete + const fileRemoved = timestamp === null || timestamp < 0; + + if (themeGeneratedFileChangedNow) { + themeGeneratedFileChanged = true; + if (fileRemoved) { + themeGeneratedFileDeleted = true; + } + } else if (fileRemoved) { + const matchResult = file.match(this.componentStyleFileRegexp); + if (matchResult) { + themeName = matchResult[2]; + deletedComponentStyleFile = file; + } + } + }); + // This is considered as a workaround for + // https://github.com/vaadin/flow/issues/9948: delete component + // styles and theme generated file in one run to not have webpack + // compile error + if (deletedComponentStyleFile && !themeGeneratedFileDeleted) { + logger.warn( + "Custom theme component style sheet '" + + deletedComponentStyleFile + + "' has been deleted.\n\n" + + "You should also delete './frontend/generated/theme-" + + themeName + + ".generated.js' (simultaneously) with the component stylesheet'.\n" + + "Otherwise it will cause a webpack compilation error 'no such file or directory', as component style sheets are referenced from " + + "'./frontend/generated/theme-" + + themeName + + ".generated.js'.\n\n" + + "If you encounter a 'no such file or directory' error in your application, just click on the overlay (or refresh the browser page), and it should disappear.\n\n" + + 'It should then be possible to continue working on the application and theming.\n' + + "If it doesn't help, you need to restart the application." + ); + } + + // Webpack watches to the changes in theme-[my-theme].generated.js + // because it is referenced from theme.js. Changes in this file + // should not trigger the theme handling callback (which + // re-generates theme-[my-theme].generated.js), + // otherwise it will get into infinite re-compilation loop. + if (themeGeneratedFileDeleted || !themeGeneratedFileChanged) { + this.processThemeResourcesCallback(logger); + } + } + callback(); + }); + } +} + +module.exports = ThemeLiveReloadPlugin; diff --git a/java/demo/target/plugins/theme-loader/package.json b/java/demo/target/plugins/theme-loader/package.json new file mode 100644 index 000000000..c44a301fc --- /dev/null +++ b/java/demo/target/plugins/theme-loader/package.json @@ -0,0 +1,20 @@ +{ + "description": "theme-loader updates css urls targeting './' to instead be 'VAADIN/static/' as used by app theme", + "keywords": [ + "plugin", + "application theme" + ], + "repository": "vaadin/flow", + "name": "@vaadin/theme-loader", + "version": "0.0.4", + "main": "theme-loader.js", + "author": "Vaadin Ltd", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/vaadin/flow/issues" + }, + "files": [ + "theme-loader.js", + "theme-loader-utils.js" + ] +} diff --git a/java/demo/target/plugins/theme-loader/theme-loader-utils.js b/java/demo/target/plugins/theme-loader/theme-loader-utils.js new file mode 100644 index 000000000..0beafa200 --- /dev/null +++ b/java/demo/target/plugins/theme-loader/theme-loader-utils.js @@ -0,0 +1,80 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +// Collect groups [url(] ['|"]optional './|../', file part and end of url +const urlMatcher = /(url\(\s*)(\'|\")?(\.\/|\.\.\/)(\S*)(\2\s*\))/g; + + +function assetsContains(fileUrl, themeFolder, logger) { + const themeProperties = getThemeProperties(themeFolder); + if (!themeProperties) { + logger.debug('No theme properties found.'); + return false; + } + const assets = themeProperties['assets']; + if (!assets) { + logger.debug('No defined assets in theme properties'); + return false; + } + // Go through each asset module + for (let module of Object.keys(assets)) { + const copyRules = assets[module]; + // Go through each copy rule + for (let copyRule of Object.keys(copyRules)) { + // if file starts with copyRule target check if file with path after copy target can be found + if (fileUrl.startsWith(copyRules[copyRule])) { + const targetFile = fileUrl.replace(copyRules[copyRule], ''); + const files = glob.sync(path.resolve('node_modules/', module, copyRule), { nodir: true }); + + for (let file of files) { + if (file.endsWith(targetFile)) return true; + } + } + } + } + return false; +} + +function getThemeProperties(themeFolder) { + const themePropertyFile = path.resolve(themeFolder, 'theme.json'); + if (!fs.existsSync(themePropertyFile)) { + return {}; + } + const themePropertyFileAsString = fs.readFileSync(themePropertyFile); + if (themePropertyFileAsString.length === 0) { + return {}; + } + return JSON.parse(themePropertyFileAsString); +} + + +function rewriteCssUrls(source, handledResourceFolder, themeFolder, logger, options) { + source = source.replace(urlMatcher, function (match, url, quoteMark, replace, fileUrl, endString) { + let absolutePath = path.resolve(handledResourceFolder, replace, fileUrl); + const existingThemeResource = absolutePath.startsWith(themeFolder) && fs.existsSync(absolutePath); + if ( + existingThemeResource || assetsContains(fileUrl, themeFolder, logger) + ) { + // Adding ./ will skip css-loader, which should be done for asset files + const skipLoader = existingThemeResource ? '' : './'; + const frontendThemeFolder = skipLoader + 'themes/' + path.basename(themeFolder); + logger.debug( + 'Updating url for file', + "'" + replace + fileUrl + "'", + 'to use', + "'" + frontendThemeFolder + '/' + fileUrl + "'" + ); + const pathResolved = absolutePath.substring(themeFolder.length).replace(/\\/g, '/'); + + // keep the url the same except replace the ./ or ../ to themes/[themeFolder] + return url + (quoteMark??'') + frontendThemeFolder + pathResolved + endString; + } else if (options.devMode) { + logger.log("No rewrite for '", match, "' as the file was not found."); + } + return match; + }); + return source; +} + +module.exports = { rewriteCssUrls }; diff --git a/java/demo/target/plugins/theme-loader/theme-loader.js b/java/demo/target/plugins/theme-loader/theme-loader.js new file mode 100644 index 000000000..9bb6b8986 --- /dev/null +++ b/java/demo/target/plugins/theme-loader/theme-loader.js @@ -0,0 +1,34 @@ +const loaderUtils = require('loader-utils'); +const fs = require('fs'); +const path = require('path'); +const { rewriteCssUrls } = require('./theme-loader-utils'); + +/** + * This custom loader handles rewriting urls for the application theme css files. + * URLs starting with ./ or ../ are checked against the filesystem and converted if a file exists. + * URLs going outside of the application theme folder are not accepted and will not be rewritten. + * + * @param source file contents to handle + * @param map source map for file + */ +module.exports = function (source, map) { + const options = loaderUtils.getOptions(this); + const handledResourceFolder = path.dirname(this._module.resource); + const logger = this.getLogger('theme-loader'); + + let themeFolder = handledResourceFolder; + // Recurse up until we find the themes folder or don't have 'themes' on the path. + while (themeFolder.indexOf('themes') > 1 && path.basename(path.resolve(themeFolder, '..')) !== 'themes') { + themeFolder = path.resolve(themeFolder, '..'); + } + // If we have found no themes folder return without doing anything. + if (path.basename(path.resolve(themeFolder, '..')) !== 'themes') { + this.callback(null, source, map); + return; + } + + logger.log("Using '", themeFolder, "' for the application theme base folder."); + + source = rewriteCssUrls(source, handledResourceFolder, themeFolder, logger, options); + this.callback(null, source, map); +}; diff --git a/java/demo/target/vaadin-dev-server-settings.json b/java/demo/target/vaadin-dev-server-settings.json new file mode 100644 index 000000000..2fcc85d7a --- /dev/null +++ b/java/demo/target/vaadin-dev-server-settings.json @@ -0,0 +1,16 @@ +{ + "frontendFolder": "/mnt/dev/github/kc/java/demo/./frontend", + "themeFolder": "themes", + "themeResourceFolder": "/mnt/dev/github/kc/java/demo/./frontend/generated/jar-resources", + "staticOutput": "/mnt/dev/github/kc/java/demo/target/classes/META-INF/VAADIN/webapp/VAADIN/static", + "generatedFolder": "generated", + "statsOutput": "/mnt/dev/github/kc/java/demo/target/classes/META-INF/VAADIN/config", + "frontendBundleOutput": "/mnt/dev/github/kc/java/demo/target/classes/META-INF/VAADIN/webapp", + "jarResourcesFolder": "/mnt/dev/github/kc/java/demo/./frontend/generated/jar-resources", + "generatedFlowImportsFolder": "target/frontend", + "themeName": "", + "clientServiceWorkerSource": "/mnt/dev/github/kc/java/demo/target/sw.ts", + "pwaEnabled": false, + "offlineEnabled": false, + "offlinePath": "'offline.html'" +} \ No newline at end of file diff --git a/java/demo/tsconfig.json b/java/demo/tsconfig.json new file mode 100644 index 000000000..37f32e77e --- /dev/null +++ b/java/demo/tsconfig.json @@ -0,0 +1,39 @@ +// This TypeScript configuration file is generated by vaadin-maven-plugin. +// This is needed for TypeScript compiler to compile your TypeScript code in the project. +// It is recommended to commit this file to the VCS. +// You might want to change the configurations to fit your preferences +// For more information about the configurations, please refer to http://www.typescriptlang.org/docs/handbook/tsconfig-json.html +{ + "flow_version": "23.3.4", + "compilerOptions": { + "sourceMap": true, + "jsx": "react-jsx", + "inlineSources": true, + "module": "esNext", + "target": "es2020", + "moduleResolution": "node", + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "baseUrl": "frontend", + "paths": { + "@vaadin/flow-frontend": ["generated/jar-resources"], + "@vaadin/flow-frontend/*": ["generated/jar-resources/*"], + "Frontend/*": ["*"] + } + }, + "include": [ + "frontend/**/*", + "types.d.ts" + ], + "exclude": [ + "frontend/generated/jar-resources/**" + ] +} diff --git a/java/demo/types.d.ts b/java/demo/types.d.ts new file mode 100644 index 000000000..250302bd6 --- /dev/null +++ b/java/demo/types.d.ts @@ -0,0 +1,10 @@ +// This TypeScript modules definition file is generated by vaadin-maven-plugin. +// You can not directly import your different static files into TypeScript, +// This is needed for TypeScript compiler to declare and export as a TypeScript module. +// It is recommended to commit this file to the VCS. +// You might want to change the configurations to fit your preferences +declare module '*.css' { + import { CSSResultGroup } from 'lit'; + const content: CSSResultGroup; + export default content; +} diff --git a/java/demo/vite.config.ts b/java/demo/vite.config.ts new file mode 100644 index 000000000..4d6a0222e --- /dev/null +++ b/java/demo/vite.config.ts @@ -0,0 +1,9 @@ +import { UserConfigFn } from 'vite'; +import { overrideVaadinConfig } from './vite.generated'; + +const customConfig: UserConfigFn = (env) => ({ + // Here you can add custom Vite parameters + // https://vitejs.dev/config/ +}); + +export default overrideVaadinConfig(customConfig); diff --git a/java/demo/vite.generated.ts b/java/demo/vite.generated.ts new file mode 100644 index 000000000..0c90ddf6f --- /dev/null +++ b/java/demo/vite.generated.ts @@ -0,0 +1,639 @@ +/** + * NOTICE: this is an auto-generated file + * + * This file has been generated by the `flow:prepare-frontend` maven goal. + * This file will be overwritten on every run. Any custom changes should be made to vite.config.ts + */ +import path from 'path'; +import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'; +import * as net from 'net'; + +import { processThemeResources } from './target/plugins/application-theme-plugin/theme-handle.js'; +import { rewriteCssUrls } from './target/plugins/theme-loader/theme-loader-utils.js'; +import settings from './target/vaadin-dev-server-settings.json'; +import { defineConfig, mergeConfig, PluginOption, ResolvedConfig, UserConfigFn, OutputOptions, AssetInfo, ChunkInfo } from 'vite'; +import { getManifest } from 'workbox-build'; + +import * as rollup from 'rollup'; +import brotli from 'rollup-plugin-brotli'; +import replace from '@rollup/plugin-replace'; +import checker from 'vite-plugin-checker'; +import postcssLit from './target/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js'; + +const appShellUrl = '.'; + +const frontendFolder = path.resolve(__dirname, settings.frontendFolder); +const themeFolder = path.resolve(frontendFolder, settings.themeFolder); +const statsFolder = path.resolve(__dirname, settings.statsOutput); +const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput); +const jarResourcesFolder = path.resolve(__dirname, settings.jarResourcesFolder); +const generatedFlowImportsFolder = path.resolve(__dirname, settings.generatedFlowImportsFolder); +const themeResourceFolder = path.resolve(__dirname, settings.themeResourceFolder); + +const statsFile = path.resolve(statsFolder, 'stats.json'); + +const projectStaticAssetsFolders = [ + path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'), + path.resolve(__dirname, 'src', 'main', 'resources', 'static'), + frontendFolder +]; + +// Folders in the project which can contain application themes +const themeProjectFolders = projectStaticAssetsFolders.map((folder) => path.resolve(folder, settings.themeFolder)); + +const themeOptions = { + devMode: false, + // The following matches folder 'frontend/generated/themes/' + // (not 'frontend/themes') for theme in JAR that is copied there + themeResourceFolder: path.resolve(themeResourceFolder, settings.themeFolder), + themeProjectFolders: themeProjectFolders, + projectStaticAssetsOutputFolder: path.resolve(__dirname, settings.staticOutput), + frontendGeneratedFolder: path.resolve(frontendFolder, settings.generatedFolder) +}; + +const hasExportedWebComponents = existsSync(path.resolve(frontendFolder, 'web-component.html')); + +// Block debug and trace logs. +console.trace = () => {}; +console.debug = () => {}; + +function injectManifestToSWPlugin(): rollup.Plugin { + const rewriteManifestIndexHtmlUrl = (manifest) => { + const indexEntry = manifest.find((entry) => entry.url === 'index.html'); + if (indexEntry) { + indexEntry.url = appShellUrl; + } + + return { manifest, warnings: [] }; + }; + + return { + name: 'vaadin:inject-manifest-to-sw', + async transform(code, id) { + if (/sw\.(ts|js)$/.test(id)) { + const { manifestEntries } = await getManifest({ + globDirectory: frontendBundleFolder, + globPatterns: ['**/*'], + globIgnores: ['**/*.br'], + manifestTransforms: [rewriteManifestIndexHtmlUrl], + maximumFileSizeToCacheInBytes: 100 * 1024 * 1024, // 100mb, + }); + + return code.replace('self.__WB_MANIFEST', JSON.stringify(manifestEntries)); + } + } + } +} + +function buildSWPlugin(opts): PluginOption { + let config: ResolvedConfig; + const devMode = opts.devMode; + + const swObj = {} + + async function build(action: 'generate' | 'write', additionalPlugins: rollup.Plugin[] = []) { + const includedPluginNames = [ + 'alias', + 'vite:resolve', + 'vite:esbuild', + 'rollup-plugin-dynamic-import-variables', + 'vite:esbuild-transpile', + 'vite:terser', + ] + const plugins: rollup.Plugin[] = config.plugins.filter((p) => { + return includedPluginNames.includes(p.name) + }); + plugins.push( + replace({ + values: { + 'process.env.NODE_ENV': JSON.stringify(config.mode), + ...config.define, + }, + preventAssignment: true + }) + ); + if (additionalPlugins) { + plugins.push(...additionalPlugins); + } + const bundle = await rollup.rollup({ + input: path.resolve(settings.clientServiceWorkerSource), + plugins + }); + + try { + return await bundle[action]({ + file: path.resolve(frontendBundleFolder, 'sw.js'), + format: 'es', + exports: 'none', + sourcemap: config.command === 'serve' || config.build.sourcemap, + inlineDynamicImports: true, + }); + } finally { + await bundle.close(); + } + } + + return { + name: 'vaadin:build-sw', + enforce: 'post', + async configResolved(resolvedConfig) { + config = resolvedConfig; + }, + async buildStart() { + if (devMode) { + const { output } = await build('generate'); + swObj.code = output[0].code; + swObj.map = output[0].map; + } + }, + async load(id) { + if (id.endsWith('sw.js')) { + return ''; + } + }, + async transform(_code, id) { + if (id.endsWith('sw.js')) { + return swObj; + } + }, + async closeBundle() { + await build('write', [ + injectManifestToSWPlugin(), + brotli(), + ]); + } + } +} + +function statsExtracterPlugin(): PluginOption { + return { + name: 'vaadin:stats', + enforce: 'post', + async writeBundle(options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }) { + const modules = Object.values(bundle).flatMap((b) => (b.modules ? Object.keys(b.modules) : [])); + const nodeModulesFolders = modules.filter((id) => id.includes('node_modules')); + const npmModules = nodeModulesFolders + .map((id) => id.replace(/.*node_modules./, '')) + .map((id) => { + const parts = id.split('/'); + if (id.startsWith('@')) { + return parts[0] + '/' + parts[1]; + } else { + return parts[0]; + } + }) + .sort() + .filter((value, index, self) => self.indexOf(value) === index); + + mkdirSync(path.dirname(statsFile), { recursive: true }); + writeFileSync(statsFile, JSON.stringify({ npmModules }, null, 1)); + } + }; +} +function vaadinBundlesPlugin(): PluginOption { + type ExportInfo = + | string + | { + namespace?: string; + source: string; + }; + + type ExposeInfo = { + exports: ExportInfo[]; + }; + + type PackageInfo = { + version: string; + exposes: Record; + }; + + type BundleJson = { + packages: Record; + }; + + const disabledMessage = 'Vaadin component dependency bundles are disabled.'; + + const modulesDirectory = path.resolve(__dirname, 'node_modules').replace(/\\/g, '/'); + + let vaadinBundleJson: BundleJson; + + function parseModuleId(id: string): { packageName: string; modulePath: string } { + const [scope, scopedPackageName] = id.split('/', 3); + const packageName = scope.startsWith('@') ? `${scope}/${scopedPackageName}` : scope; + const modulePath = `.${id.substring(packageName.length)}`; + return { + packageName, + modulePath + }; + } + + function getExports(id: string): string[] | undefined { + const { packageName, modulePath } = parseModuleId(id); + const packageInfo = vaadinBundleJson.packages[packageName]; + + if (!packageInfo) return; + + const exposeInfo: ExposeInfo = packageInfo.exposes[modulePath]; + if (!exposeInfo) return; + + const exportsSet = new Set(); + for (const e of exposeInfo.exports) { + if (typeof e === 'string') { + exportsSet.add(e); + } else { + const { namespace, source } = e; + if (namespace) { + exportsSet.add(namespace); + } else { + const sourceExports = getExports(source); + if (sourceExports) { + sourceExports.forEach((e) => exportsSet.add(e)); + } + } + } + } + return Array.from(exportsSet); + } + + function getExportBinding(binding: string) { + return binding === 'default' ? '_default as default' : binding; + } + + function getImportAssigment(binding: string) { + return binding === 'default' ? 'default: _default' : binding; + } + + return { + name: 'vaadin:bundles', + enforce: 'pre', + apply(config, { command }) { + if (command !== 'serve') return false; + + try { + const vaadinBundleJsonPath = require.resolve('@vaadin/bundles/vaadin-bundle.json'); + vaadinBundleJson = JSON.parse(readFileSync(vaadinBundleJsonPath, { encoding: 'utf8' })); + } catch (e: unknown) { + if (typeof e === 'object' && (e as { code: string }).code === 'MODULE_NOT_FOUND') { + vaadinBundleJson = { packages: {} }; + console.info(`@vaadin/bundles npm package is not found, ${disabledMessage}`); + return false; + } else { + throw e; + } + } + + const versionMismatches: Array<{ name: string; bundledVersion: string; installedVersion: string }> = []; + for (const [name, packageInfo] of Object.entries(vaadinBundleJson.packages)) { + let installedVersion: string | undefined = undefined; + try { + const { version: bundledVersion } = packageInfo; + const installedPackageJsonFile = path.resolve(modulesDirectory, name, 'package.json'); + const packageJson = JSON.parse(readFileSync(installedPackageJsonFile, { encoding: 'utf8' })); + installedVersion = packageJson.version; + if (installedVersion && installedVersion !== bundledVersion) { + versionMismatches.push({ + name, + bundledVersion, + installedVersion + }); + } + } catch (_) { + // ignore package not found + } + } + if (versionMismatches.length) { + console.info(`@vaadin/bundles has version mismatches with installed packages, ${disabledMessage}`); + console.info(`Packages with version mismatches: ${JSON.stringify(versionMismatches, undefined, 2)}`); + vaadinBundleJson = { packages: {} }; + return false; + } + + return true; + }, + async config(config) { + return mergeConfig( + { + optimizeDeps: { + exclude: [ + // Vaadin bundle + '@vaadin/bundles', + ...Object.keys(vaadinBundleJson.packages) + ] + } + }, + config + ); + }, + load(rawId) { + const [path, params] = rawId.split('?'); + if (!path.startsWith(modulesDirectory)) return; + + const id = path.substring(modulesDirectory.length + 1); + const bindings = getExports(id); + if (bindings === undefined) return; + + const cacheSuffix = params ? `?${params}` : ''; + const bundlePath = `@vaadin/bundles/vaadin.js${cacheSuffix}`; + + return `import { init as VaadinBundleInit, get as VaadinBundleGet } from '${bundlePath}'; +await VaadinBundleInit('default'); +const { ${bindings.map(getImportAssigment).join(', ')} } = (await VaadinBundleGet('./node_modules/${id}'))(); +export { ${bindings.map(getExportBinding).join(', ')} };`; + } + }; +} + +function themePlugin(opts): PluginOption { + const fullThemeOptions = {...themeOptions, devMode: opts.devMode }; + return { + name: 'vaadin:theme', + config() { + processThemeResources(fullThemeOptions, console); + }, + configureServer(server) { + function handleThemeFileCreateDelete(themeFile, stats) { + if (themeFile.startsWith(themeFolder)) { + const changed = path.relative(themeFolder, themeFile) + console.debug('Theme file ' + (!!stats ? 'created' : 'deleted'), changed); + processThemeResources(fullThemeOptions, console); + } + } + server.watcher.on('add', handleThemeFileCreateDelete); + server.watcher.on('unlink', handleThemeFileCreateDelete); + }, + handleHotUpdate(context) { + const contextPath = path.resolve(context.file); + const themePath = path.resolve(themeFolder); + if (contextPath.startsWith(themePath)) { + const changed = path.relative(themePath, contextPath); + + console.debug('Theme file changed', changed); + + if (changed.startsWith(settings.themeName)) { + processThemeResources(fullThemeOptions, console); + } + } + }, + async resolveId(id, importer) { + // force theme generation if generated theme sources does not yet exist + // this may happen for example during Java hot reload when updating + // @Theme annotation value + if (path.resolve(themeOptions.frontendGeneratedFolder, "theme.js") === importer && + !existsSync(path.resolve(themeOptions.frontendGeneratedFolder, id))) { + console.debug('Generate theme file ' + id + ' not existing. Processing theme resource'); + processThemeResources(fullThemeOptions, console); + return; + } + if (!id.startsWith(settings.themeFolder)) { + return; + } + + for (const location of [themeResourceFolder, frontendFolder]) { + const result = await this.resolve(path.resolve(location, id)); + if (result) { + return result; + } + } + }, + async transform(raw, id, options) { + // rewrite urls for the application theme css files + const [bareId, query] = id.split('?'); + if (!bareId?.startsWith(themeFolder) || !bareId?.endsWith('.css')) { + return; + } + const [themeName] = bareId.substring(themeFolder.length + 1).split('/'); + return rewriteCssUrls(raw, path.dirname(bareId), path.resolve(themeFolder, themeName), console, opts); + } + }; +} +function lenientLitImportPlugin(): PluginOption { + return { + name: 'vaadin:lenient-lit-import', + async transform(code, id) { + const decoratorImports = [ + /import (.*?) from (['"])(lit\/decorators)(['"])/, + /import (.*?) from (['"])(lit-element\/decorators)(['"])/ + ]; + const directiveImports = [ + /import (.*?) from (['"])(lit\/directives\/)([^\\.]*?)(['"])/, + /import (.*?) from (['"])(lit-html\/directives\/)([^\\.]*?)(['"])/ + ]; + + decoratorImports.forEach((decoratorImport) => { + let decoratorMatch; + while ((decoratorMatch = code.match(decoratorImport))) { + console.warn( + `Warning: the file ${id} imports from '${decoratorMatch[3]}' when it should import from '${decoratorMatch[3]}.js'` + ); + code = code.replace(decoratorImport, 'import $1 from $2$3.js$4'); + } + }); + + directiveImports.forEach((directiveImport) => { + let directiveMatch; + while ((directiveMatch = code.match(directiveImport))) { + console.warn( + `Warning: the file ${id} imports from '${directiveMatch[3]}${directiveMatch[4]}' when it should import from '${directiveMatch[3]}${directiveMatch[4]}.js'` + ); + code = code.replace(directiveImport, 'import $1 from $2$3$4.js$5'); + } + }); + + return code; + } + }; +} + +function runWatchDog(watchDogPort, watchDogHost) { + const client = net.Socket(); + client.setEncoding('utf8'); + client.on('error', function (err) { + console.log('Watchdog connection error. Terminating vite process...', err); + client.destroy(); + process.exit(0); + }); + client.on('close', function () { + client.destroy(); + runWatchDog(watchDogPort, watchDogHost); + }); + + client.connect(watchDogPort, watchDogHost || 'localhost'); +} + +let spaMiddlewareForceRemoved = false; + +const allowedFrontendFolders = [ + frontendFolder, + path.resolve(generatedFlowImportsFolder), // Contains only generated-flow-imports + path.resolve(__dirname, 'node_modules') +]; + +function setHmrPortToServerPort(): PluginOption { + return { + name: 'set-hmr-port-to-server-port', + configResolved(config) { + if (config.server.strictPort && config.server.hmr !== false) { + if (config.server.hmr === true) config.server.hmr = {}; + config.server.hmr = config.server.hmr || {}; + config.server.hmr.clientPort = config.server.port; + } + } + }; +} +function showRecompileReason(): PluginOption { + return { + name: 'vaadin:why-you-compile', + handleHotUpdate(context) { + console.log('Recompiling because', context.file, 'changed'); + } + }; +} + +export const vaadinConfig: UserConfigFn = (env) => { + const devMode = env.mode === 'development'; + + if (devMode && process.env.watchDogPort) { + // Open a connection with the Java dev-mode handler in order to finish + // vite when it exits or crashes. + runWatchDog(process.env.watchDogPort, process.env.watchDogHost); + } + + return { + root: frontendFolder, + base: '', + resolve: { + alias: { + '@vaadin/flow-frontend': jarResourcesFolder, + Frontend: frontendFolder + }, + preserveSymlinks: true + }, + define: { + OFFLINE_PATH: settings.offlinePath, + VITE_ENABLED: 'true' + }, + server: { + host: '127.0.0.1', + strictPort: true, + fs: { + allow: allowedFrontendFolders + } + }, + build: { + outDir: frontendBundleFolder, + assetsDir: 'VAADIN/build', + rollupOptions: { + input: { + indexhtml: path.resolve(frontendFolder, 'index.html'), + + ...hasExportedWebComponents + ? { webcomponenthtml: path.resolve(frontendFolder, 'web-component.html') } + : {} + } + } + }, + optimizeDeps: { + entries: [ + // Pre-scan entrypoints in Vite to avoid reloading on first open + 'generated/vaadin.ts' + ], + exclude: [ + '@vaadin/router', + '@vaadin/vaadin-license-checker', + '@vaadin/vaadin-usage-statistics', + 'workbox-core', + 'workbox-precaching', + 'workbox-routing', + 'workbox-strategies' + ] + }, + plugins: [ + !devMode && brotli(), + devMode && vaadinBundlesPlugin(), + devMode && setHmrPortToServerPort(), + devMode && showRecompileReason(), + settings.offlineEnabled && buildSWPlugin({ devMode }), + !devMode && statsExtracterPlugin(), + themePlugin({devMode}), + lenientLitImportPlugin(), + postcssLit({ + include: ['**/*.css', '**/*.css\?*'], + exclude: [ + `${themeFolder}/**/*.css`, + `${themeFolder}/**/*.css\?*`, + `${themeResourceFolder}/**/*.css`, + `${themeResourceFolder}/**/*.css\?*`, + '**/*\?html-proxy*' + ] + }), + { + name: 'vaadin:force-remove-html-middleware', + transformIndexHtml: { + enforce: 'pre', + transform(_html, { server }) { + if (server && !spaMiddlewareForceRemoved) { + server.middlewares.stack = server.middlewares.stack.filter((mw) => { + const handleName = '' + mw.handle; + return !handleName.includes('viteHtmlFallbackMiddleware'); + }); + spaMiddlewareForceRemoved = true; + } + } + } + }, + hasExportedWebComponents && { + name: 'vaadin:inject-entrypoints-to-web-component-html', + transformIndexHtml: { + enforce: 'pre', + transform(_html, { path, server }) { + if (path !== '/web-component.html') { + return; + } + + return [ + { + tag: 'script', + attrs: { type: 'module', src: `/generated/vaadin-web-component.ts` }, + injectTo: 'head' + } + ] + } + } + }, + { + name: 'vaadin:inject-entrypoints-to-index-html', + transformIndexHtml: { + enforce: 'pre', + transform(_html, { path, server }) { + if (path !== '/index.html') { + return; + } + + const scripts = []; + + if (devMode) { + scripts.push({ + tag: 'script', + attrs: { type: 'module', src: `/generated/vite-devmode.ts` }, + injectTo: 'head' + }); + } + scripts.push({ + tag: 'script', + attrs: { type: 'module', src: '/generated/vaadin.ts' }, + injectTo: 'head' + }); + return scripts; + } + } + }, + checker({ + typescript: true + }) + ] + }; +}; + +export const overrideVaadinConfig = (customConfig: UserConfigFn) => { + return defineConfig((env) => mergeConfig(vaadinConfig(env), customConfig(env))); +}; diff --git a/java/gatekeeper/README.md b/java/gatekeeper/README.md new file mode 100644 index 000000000..c08128fd1 --- /dev/null +++ b/java/gatekeeper/README.md @@ -0,0 +1,17 @@ +# Gatekeeper Client (Java) + +HTTP client for the Gatekeeper REST API. + +## Usage + +```java +import org.keychain.gatekeeper.GatekeeperClient; +import org.keychain.gatekeeper.GatekeeperClientOptions; +import org.keychain.gatekeeper.model.MdipDocument; + +GatekeeperClientOptions options = new GatekeeperClientOptions(); +options.baseUrl = "http://localhost:4224"; + +GatekeeperClient gatekeeper = new GatekeeperClient(options); +MdipDocument doc = gatekeeper.resolveDID("did:test:example", null); +``` diff --git a/java/gatekeeper/build.gradle b/java/gatekeeper/build.gradle new file mode 100644 index 000000000..11ee2bee8 --- /dev/null +++ b/java/gatekeeper/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api 'com.squareup.okhttp3:okhttp:4.12.0' + api 'com.fasterxml.jackson.core:jackson-databind:2.17.1' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClient.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClient.java new file mode 100644 index 000000000..748d638b7 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClient.java @@ -0,0 +1,516 @@ +package org.keychain.gatekeeper; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.keychain.gatekeeper.model.BlockId; +import org.keychain.gatekeeper.model.BlockInfo; +import org.keychain.gatekeeper.model.GatekeeperError; +import org.keychain.gatekeeper.model.GatekeeperEvent; +import org.keychain.gatekeeper.model.GetDIDOptions; +import org.keychain.gatekeeper.model.GetStatusResult; +import org.keychain.gatekeeper.model.ImportBatchResult; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.gatekeeper.model.Operation; +import org.keychain.gatekeeper.model.ProcessEventsResult; +import org.keychain.gatekeeper.model.ResolveDIDOptions; +import org.keychain.gatekeeper.model.VerifyDbOptions; +import org.keychain.gatekeeper.model.VerifyDbResult; + +public class GatekeeperClient implements GatekeeperInterface { + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + private static final MediaType OCTET_STREAM = MediaType.get("application/octet-stream"); + private static final MediaType TEXT_PLAIN = MediaType.get("text/plain; charset=utf-8"); + + private final OkHttpClient http; + private final ObjectMapper mapper; + private final HttpUrl baseUrl; + private String headerName; + private String headerValue; + + public GatekeeperClient(GatekeeperClientOptions options) { + Objects.requireNonNull(options, "options is required"); + this.mapper = new ObjectMapper() + .setDefaultPropertyInclusion( + JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.ALWAYS) + ); + String base = options.url != null ? options.url : options.baseUrl; + if (base.endsWith("/api/v1")) { + base = base.substring(0, base.length() - "/api/v1".length()); + } else if (base.endsWith("/api/v1/")) { + base = base.substring(0, base.length() - "/api/v1/".length()); + } + this.baseUrl = HttpUrl.parse(base + "/api/v1"); + if (this.baseUrl == null) { + throw new IllegalArgumentException("Invalid baseUrl"); + } + + Duration connectTimeout = options.connectTimeout != null ? options.connectTimeout : Duration.ofSeconds(10); + Duration readTimeout = options.readTimeout != null ? options.readTimeout : Duration.ofSeconds(30); + + this.http = new OkHttpClient.Builder() + .connectTimeout(connectTimeout) + .readTimeout(readTimeout) + .build(); + + this.headerName = options.headerName; + this.headerValue = options.headerValue; + + if (Boolean.TRUE.equals(options.waitUntilReady)) { + waitUntilReady(options); + } + } + + public void addCustomHeader(String header, String value) { + this.headerName = header; + this.headerValue = value; + } + + public void removeCustomHeader(String header) { + if (header != null && header.equals(this.headerName)) { + this.headerName = null; + this.headerValue = null; + } + } + + @Override + @SuppressWarnings("unchecked") + public java.util.List listRegistries() { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("registries") + .build(); + return getJson(url, java.util.List.class); + } + + public boolean isReady() { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("ready") + .build(); + try { + Boolean ok = getJson(url, Boolean.class); + return Boolean.TRUE.equals(ok); + } catch (Exception e) { + return false; + } + } + + public void waitUntilReady(GatekeeperClientOptions options) { + int intervalSeconds = options.intervalSeconds != null ? options.intervalSeconds : 5; + boolean chatty = Boolean.TRUE.equals(options.chatty); + int becomeChattyAfter = options.becomeChattyAfter != null ? options.becomeChattyAfter : 0; + int maxRetries = options.maxRetries != null ? options.maxRetries : 0; + int retries = 0; + + if (chatty) { + System.out.println("Connecting to gatekeeper at " + baseUrl); + } + + while (true) { + if (isReady()) { + if (chatty) { + System.out.println("Gatekeeper service is ready!"); + } + return; + } + + if (chatty) { + System.out.println("Waiting for Gatekeeper to be ready..."); + } + + retries += 1; + if (maxRetries > 0 && retries > maxRetries) { + return; + } + if (!chatty && becomeChattyAfter > 0 && retries > becomeChattyAfter) { + System.out.println("Connecting to gatekeeper at " + baseUrl); + chatty = true; + } + + try { + //noinspection BusyWait + Thread.sleep(Math.max(1, intervalSeconds) * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + + @Override + public boolean resetDb() { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("db") + .addPathSegment("reset") + .build(); + return getJson(url, Boolean.class); + } + + @Override + public VerifyDbResult verifyDb(VerifyDbOptions options) { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("db") + .addPathSegment("verify") + .build(); + return getJson(url, VerifyDbResult.class); + } + + public int getVersion() { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("version") + .build(); + Integer version = getJson(url, Integer.class); + return version != null ? version : 0; + } + + public GetStatusResult getStatus() { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("status") + .build(); + return getJson(url, GetStatusResult.class); + } + + @Override + public String createDID(Operation operation) { + return postJson("/did", operation, String.class); + } + + @Override + public MdipDocument resolveDID(String did, ResolveDIDOptions options) { + HttpUrl.Builder url = baseUrl.newBuilder().addPathSegment("did").addPathSegment(did); + if (options != null) { + if (options.versionTime != null) { + url.addQueryParameter("versionTime", options.versionTime); + } + if (options.versionSequence != null) { + url.addQueryParameter("versionSequence", options.versionSequence.toString()); + } + if (options.confirm != null) { + url.addQueryParameter("confirm", options.confirm.toString()); + } + if (options.verify != null) { + url.addQueryParameter("verify", options.verify.toString()); + } + } + return getJson(url.build(), MdipDocument.class); + } + + @Override + public boolean updateDID(Operation operation) { + return postJson("/did", operation, Boolean.class); + } + + @Override + public boolean deleteDID(Operation operation) { + return postJson("/did", operation, Boolean.class); + } + + @Override + public Object getDIDs(GetDIDOptions options) { + Object body = options != null ? options : new HashMap(); + return postJson("/dids", body, Object.class); + } + + @Override + public java.util.List> exportDIDs(java.util.List dids) { + Map payload = new HashMap<>(); + payload.put("dids", dids); + return postJsonType("/dids/export", payload, new TypeReference<>() {}); + } + + @Override + public ImportBatchResult importDIDs(java.util.List> dids) { + return postJson("/dids/import", dids, ImportBatchResult.class); + } + + @Override + public boolean removeDIDs(java.util.List dids) { + return postJson("/dids/remove", dids, Boolean.class); + } + + @Override + public java.util.List exportBatch(java.util.List dids) { + Map payload = new HashMap<>(); + payload.put("dids", dids); + return postJsonType("/batch/export", payload, new TypeReference<>() {}); + } + + @Override + public ImportBatchResult importBatch(java.util.List batch) { + return postJson("/batch/import", batch, ImportBatchResult.class); + } + + @Override + public ProcessEventsResult processEvents() { + Map payload = new HashMap<>(); + return postJson("/events/process", payload, ProcessEventsResult.class); + } + + @Override + public java.util.List getQueue(String registry) { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("queue") + .addPathSegment(registry) + .build(); + return getJsonType(url, new TypeReference<>() {}); + } + + @Override + public boolean clearQueue(String registry, java.util.List events) { + String path = "/queue/" + registry + "/clear"; + return postJson(path, events, Boolean.class); + } + + @Override + public String addData(byte[] data) { + return postBytes("/cas/data", data, OCTET_STREAM); + } + + @Override + public byte[] getData(String cid) { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("cas") + .addPathSegment("data") + .addPathSegment(cid) + .build(); + try { + return getBytes(url); + } catch (GatekeeperClientException e) { + if (e.statusCode == 404) { + return null; + } + throw e; + } + } + + @Override + public String addJSON(Object json) { + return postJson("/cas/json", json, String.class); + } + + @Override + public Object getJSON(String cid) { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("cas") + .addPathSegment("json") + .addPathSegment(cid) + .build(); + try { + return getJson(url, Object.class); + } catch (GatekeeperClientException e) { + if (e.statusCode == 404) { + return null; + } + throw e; + } + } + + @Override + public String addText(String text) { + return postBytes("/cas/text", text != null ? text.getBytes(StandardCharsets.UTF_8) : new byte[0], TEXT_PLAIN); + } + + @Override + public String getText(String cid) { + HttpUrl url = baseUrl.newBuilder() + .addPathSegment("cas") + .addPathSegment("text") + .addPathSegment(cid) + .build(); + try { + return getText(url); + } catch (GatekeeperClientException e) { + if (e.statusCode == 404) { + return null; + } + throw e; + } + } + + @Override + public BlockInfo getBlock(String registry) { + return getBlock(registry, null); + } + + @Override + public BlockInfo getBlock(String registry, BlockId blockId) { + HttpUrl.Builder builder = baseUrl.newBuilder() + .addPathSegment("block") + .addPathSegment(registry); + if (blockId != null && blockId.value != null) { + builder.addPathSegment(blockId.value.toString()); + } else { + builder.addPathSegment("latest"); + } + try { + return getJson(builder.build(), BlockInfo.class); + } catch (GatekeeperClientException e) { + if (e.statusCode == 404) { + return null; + } + throw e; + } + } + + @Override + public boolean addBlock(String registry, BlockInfo blockInfo) { + String path = "/block/" + registry; + return postJson(path, blockInfo, Boolean.class); + } + + @Override + public String generateDID(Operation operation) { + return postJson("/did/generate", operation, String.class); + } + + private T postJson(String path, Object body, Class responseType) { + Request request = buildPostRequest(path, body); + return execute(request, responseType); + } + + private T postJsonType(String path, Object body, TypeReference typeRef) { + Request request = buildPostRequest(path, body); + return execute(request, typeRef); + } + + private Request buildPostRequest(String path, Object body) { + try { + String json = mapper.writeValueAsString(body); + RequestBody requestBody = RequestBody.create(json, JSON); + return new Request.Builder() + .url(baseUrl.newBuilder().addPathSegments(trimLeadingSlash(path)).build()) + .post(requestBody) + .build(); + } catch (IOException e) { + throw new IllegalStateException("Failed to serialize request", e); + } + } + + private String postBytes(String path, byte[] body, MediaType contentType) { + RequestBody requestBody = RequestBody.create(body != null ? body : new byte[0], contentType); + Request request = new Request.Builder() + .url(baseUrl.newBuilder().addPathSegments(trimLeadingSlash(path)).build()) + .post(requestBody) + .build(); + return execute(request, String.class); + } + + private T getJson(HttpUrl url, Class responseType) { + Request request = new Request.Builder() + .url(url) + .get() + .build(); + return execute(request, responseType); + } + + private T getJsonType(HttpUrl url, TypeReference typeRef) { + Request request = new Request.Builder() + .url(url) + .get() + .build(); + return execute(request, typeRef); + } + + private T execute(Request request, Class responseType) { + Request.Builder builder = request.newBuilder(); + if (headerName != null && headerValue != null) { + builder.addHeader(headerName, headerValue); + } + + try (Response response = http.newCall(builder.build()).execute()) { + String body = response.body() != null ? response.body().string() : ""; + if (!response.isSuccessful()) { + GatekeeperError error = GatekeeperErrorParser.parse(body); + throw new GatekeeperClientException("Gatekeeper request failed", response.code(), error); + } + + if (responseType == String.class) { + try { + return responseType.cast(mapper.readValue(body, String.class)); + } catch (Exception e) { + return responseType.cast(body); + } + } + + if (body.isEmpty()) { + return null; + } + + return mapper.readValue(body, responseType); + } catch (IOException e) { + throw new IllegalStateException("Gatekeeper request failed", e); + } + } + + private T execute(Request request, TypeReference typeRef) { + Request.Builder builder = request.newBuilder(); + if (headerName != null && headerValue != null) { + builder.addHeader(headerName, headerValue); + } + + try (Response response = http.newCall(builder.build()).execute()) { + String body = response.body() != null ? response.body().string() : ""; + if (!response.isSuccessful()) { + GatekeeperError error = GatekeeperErrorParser.parse(body); + throw new GatekeeperClientException("Gatekeeper request failed", response.code(), error); + } + if (body.isEmpty()) { + return null; + } + return mapper.readValue(body, typeRef); + } catch (IOException e) { + throw new IllegalStateException("Gatekeeper request failed", e); + } + } + + private byte[] getBytes(HttpUrl url) { + Request request = new Request.Builder() + .url(url) + .get() + .build(); + Request.Builder builder = request.newBuilder(); + if (headerName != null && headerValue != null) { + builder.addHeader(headerName, headerValue); + } + + try (Response response = http.newCall(builder.build()).execute()) { + ResponseBody responseBody = response.body(); + byte[] bytes = responseBody != null ? responseBody.bytes() : new byte[0]; + if (!response.isSuccessful()) { + String body = new String(bytes, StandardCharsets.UTF_8); + GatekeeperError error = GatekeeperErrorParser.parse(body); + throw new GatekeeperClientException("Gatekeeper request failed", response.code(), error); + } + return bytes; + } catch (IOException e) { + throw new IllegalStateException("Gatekeeper request failed", e); + } + } + + private String getText(HttpUrl url) { + Request request = new Request.Builder() + .url(url) + .get() + .build(); + return execute(request, String.class); + } + + private static String trimLeadingSlash(String path) { + if (path == null) { + return ""; + } + return path.startsWith("/") ? path.substring(1) : path; + } +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClientException.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClientException.java new file mode 100644 index 000000000..cf22e11cb --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClientException.java @@ -0,0 +1,14 @@ +package org.keychain.gatekeeper; + +import org.keychain.gatekeeper.model.GatekeeperError; + +public class GatekeeperClientException extends RuntimeException { + public final int statusCode; + public final GatekeeperError error; + + public GatekeeperClientException(String message, int statusCode, GatekeeperError error) { + super(message); + this.statusCode = statusCode; + this.error = error; + } +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClientOptions.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClientOptions.java new file mode 100644 index 000000000..5663cdc51 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperClientOptions.java @@ -0,0 +1,23 @@ +package org.keychain.gatekeeper; + +import java.time.Duration; + +public class GatekeeperClientOptions { + public String baseUrl; + public String url; + public Boolean waitUntilReady; + public Integer intervalSeconds; + public Boolean chatty; + public Integer becomeChattyAfter; + public Integer maxRetries; + public Duration connectTimeout; + public Duration readTimeout; + public String headerName; + public String headerValue; + + public GatekeeperClientOptions() { + this.baseUrl = "http://localhost:4224"; + this.connectTimeout = Duration.ofSeconds(10); + this.readTimeout = Duration.ofSeconds(30); + } +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperErrorParser.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperErrorParser.java new file mode 100644 index 000000000..57f458fe9 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperErrorParser.java @@ -0,0 +1,24 @@ +package org.keychain.gatekeeper; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.keychain.gatekeeper.model.GatekeeperError; + +public final class GatekeeperErrorParser { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private GatekeeperErrorParser() {} + + public static GatekeeperError parse(String body) { + if (body == null || body.isBlank()) { + return null; + } + + try { + return MAPPER.readValue(body, GatekeeperError.class); + } catch (Exception e) { + GatekeeperError error = new GatekeeperError(); + error.message = body; + return error; + } + } +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperInterface.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperInterface.java new file mode 100644 index 000000000..ec476cab4 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/GatekeeperInterface.java @@ -0,0 +1,43 @@ +package org.keychain.gatekeeper; + +import java.util.List; +import org.keychain.gatekeeper.model.BlockId; +import org.keychain.gatekeeper.model.BlockInfo; +import org.keychain.gatekeeper.model.GatekeeperEvent; +import org.keychain.gatekeeper.model.GetDIDOptions; +import org.keychain.gatekeeper.model.ImportBatchResult; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.gatekeeper.model.Operation; +import org.keychain.gatekeeper.model.ProcessEventsResult; +import org.keychain.gatekeeper.model.ResolveDIDOptions; +import org.keychain.gatekeeper.model.VerifyDbOptions; +import org.keychain.gatekeeper.model.VerifyDbResult; + +public interface GatekeeperInterface { + List listRegistries(); + boolean resetDb(); + VerifyDbResult verifyDb(VerifyDbOptions options); + String createDID(Operation operation); + MdipDocument resolveDID(String did, ResolveDIDOptions options); + boolean updateDID(Operation operation); + boolean deleteDID(Operation operation); + Object getDIDs(GetDIDOptions options); + List> exportDIDs(List dids); + ImportBatchResult importDIDs(List> dids); + boolean removeDIDs(List dids); + List exportBatch(List dids); + ImportBatchResult importBatch(List batch); + ProcessEventsResult processEvents(); + List getQueue(String registry); + boolean clearQueue(String registry, List events); + String addData(byte[] data); + byte[] getData(String cid); + String addJSON(Object json); + Object getJSON(String cid); + String addText(String text); + String getText(String cid); + BlockInfo getBlock(String registry); + BlockInfo getBlock(String registry, BlockId blockId); + boolean addBlock(String registry, BlockInfo blockInfo); + String generateDID(Operation operation); +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/BlockId.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/BlockId.java new file mode 100644 index 000000000..3edd59425 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/BlockId.java @@ -0,0 +1,14 @@ +package org.keychain.gatekeeper.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +public class BlockId { + @JsonValue + public Object value; + + public BlockId() {} + + public BlockId(Object value) { + this.value = value; + } +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/BlockInfo.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/BlockInfo.java new file mode 100644 index 000000000..8681256f7 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/BlockInfo.java @@ -0,0 +1,9 @@ +package org.keychain.gatekeeper.model; + +public class BlockInfo { + public Integer height; + public String hash; + public Long time; + + public BlockInfo() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/CheckDIDsResult.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/CheckDIDsResult.java new file mode 100644 index 000000000..773e62eca --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/CheckDIDsResult.java @@ -0,0 +1,14 @@ +package org.keychain.gatekeeper.model; + +import java.util.List; +import java.util.Map; + +public class CheckDIDsResult { + public int total; + public CheckDIDsResultByType byType; + public Map byRegistry; + public Map byVersion; + public List eventsQueue; + + public CheckDIDsResult() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/CheckDIDsResultByType.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/CheckDIDsResultByType.java new file mode 100644 index 000000000..5f074992d --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/CheckDIDsResultByType.java @@ -0,0 +1,12 @@ +package org.keychain.gatekeeper.model; + +public class CheckDIDsResultByType { + public int agents; + public int assets; + public int confirmed; + public int unconfirmed; + public int ephemeral; + public int invalid; + + public CheckDIDsResultByType() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/DocumentMetadata.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/DocumentMetadata.java new file mode 100644 index 000000000..2fa493383 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/DocumentMetadata.java @@ -0,0 +1,16 @@ +package org.keychain.gatekeeper.model; + +public class DocumentMetadata { + public String created; + public String updated; + public String canonicalId; + public String versionId; + public String version; + public Boolean confirmed; + public Boolean deactivated; + public Boolean isOwned; + public String deleted; + public Object timestamp; + + public DocumentMetadata() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/EcdsaJwkPublic.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/EcdsaJwkPublic.java new file mode 100644 index 000000000..71f0a68c1 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/EcdsaJwkPublic.java @@ -0,0 +1,10 @@ +package org.keychain.gatekeeper.model; + +public class EcdsaJwkPublic { + public String kty; + public String crv; + public String x; + public String y; + + public EcdsaJwkPublic() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GatekeeperError.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GatekeeperError.java new file mode 100644 index 000000000..be79e805e --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GatekeeperError.java @@ -0,0 +1,8 @@ +package org.keychain.gatekeeper.model; + +public class GatekeeperError { + public String type; + public String message; + + public GatekeeperError() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GatekeeperEvent.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GatekeeperEvent.java new file mode 100644 index 000000000..3ea0e56c4 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GatekeeperEvent.java @@ -0,0 +1,15 @@ +package org.keychain.gatekeeper.model; + +import java.util.List; + +public class GatekeeperEvent { + public String registry; + public String time; + public List ordinal; + public Operation operation; + public String did; + public String opid; + public MdipRegistration blockchain; + + public GatekeeperEvent() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GetDIDOptions.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GetDIDOptions.java new file mode 100644 index 000000000..148e2e9bf --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GetDIDOptions.java @@ -0,0 +1,14 @@ +package org.keychain.gatekeeper.model; + +import java.util.List; + +public class GetDIDOptions { + public List dids; + public String updatedAfter; + public String updatedBefore; + public Boolean confirm; + public Boolean verify; + public Boolean resolve; + + public GetDIDOptions() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GetStatusResult.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GetStatusResult.java new file mode 100644 index 000000000..0ce34cde9 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/GetStatusResult.java @@ -0,0 +1,9 @@ +package org.keychain.gatekeeper.model; + +public class GetStatusResult { + public long uptimeSeconds; + public CheckDIDsResult dids; + public MemoryUsage memoryUsage; + + public GetStatusResult() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ImportBatchResult.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ImportBatchResult.java new file mode 100644 index 000000000..492311893 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ImportBatchResult.java @@ -0,0 +1,10 @@ +package org.keychain.gatekeeper.model; + +public class ImportBatchResult { + public int queued; + public int processed; + public int rejected; + public int total; + + public ImportBatchResult() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Mdip.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Mdip.java new file mode 100644 index 000000000..2d64a8778 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Mdip.java @@ -0,0 +1,14 @@ +package org.keychain.gatekeeper.model; + +public class Mdip { + public int version; + public String type; + public String registry; + public String validUntil; + public String prefix; + public String opid; + public MdipRegistration registration; + public String created; + + public Mdip() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MdipDocument.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MdipDocument.java new file mode 100644 index 000000000..ef9b13e00 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MdipDocument.java @@ -0,0 +1,42 @@ +package org.keychain.gatekeeper.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public class MdipDocument { + public DidDocument didDocument; + public DocumentMetadata didDocumentMetadata; + public DidResolutionMetadata didResolutionMetadata; + public Object didDocumentData; + public Mdip mdip; + + public MdipDocument() {} + + public static class DidDocument { + @JsonProperty("@context") + public List context; + public String id; + public String controller; + public List verificationMethod; + public List authentication; + + public DidDocument() {} + } + + public static class VerificationMethod { + public String id; + public String controller; + public String type; + public EcdsaJwkPublic publicKeyJwk; + + public VerificationMethod() {} + } + + public static class DidResolutionMetadata { + public String contentType; + public String retrieved; + public String error; + + public DidResolutionMetadata() {} + } +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MdipRegistration.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MdipRegistration.java new file mode 100644 index 000000000..4ff4a66f4 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MdipRegistration.java @@ -0,0 +1,11 @@ +package org.keychain.gatekeeper.model; + +public class MdipRegistration { + public Integer height; + public Integer index; + public String txid; + public String batch; + public Integer opidx; + + public MdipRegistration() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MemoryUsage.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MemoryUsage.java new file mode 100644 index 000000000..f542a93d8 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/MemoryUsage.java @@ -0,0 +1,11 @@ +package org.keychain.gatekeeper.model; + +public class MemoryUsage { + public Long rss; + public Long heapTotal; + public Long heapUsed; + public Long external; + public Long arrayBuffers; + + public MemoryUsage() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Operation.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Operation.java new file mode 100644 index 000000000..094b21082 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Operation.java @@ -0,0 +1,17 @@ +package org.keychain.gatekeeper.model; + +public class Operation { + public String type; + public String created; + public String blockid; + public String did; + public String previd; + public Mdip mdip; + public String controller; + public Object data; + public MdipDocument doc; + public EcdsaJwkPublic publicJwk; + public Signature signature; + + public Operation() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ProcessEventsResult.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ProcessEventsResult.java new file mode 100644 index 000000000..99ea2c9fe --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ProcessEventsResult.java @@ -0,0 +1,11 @@ +package org.keychain.gatekeeper.model; + +public class ProcessEventsResult { + public Boolean busy; + public Integer added; + public Integer merged; + public Integer rejected; + public Integer pending; + + public ProcessEventsResult() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ResolveDIDOptions.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ResolveDIDOptions.java new file mode 100644 index 000000000..0e6643673 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/ResolveDIDOptions.java @@ -0,0 +1,10 @@ +package org.keychain.gatekeeper.model; + +public class ResolveDIDOptions { + public String versionTime; + public Integer versionSequence; + public Boolean confirm; + public Boolean verify; + + public ResolveDIDOptions() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Signature.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Signature.java new file mode 100644 index 000000000..d2f3da260 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/Signature.java @@ -0,0 +1,10 @@ +package org.keychain.gatekeeper.model; + +public class Signature { + public String signer; + public String signed; + public String hash; + public String value; + + public Signature() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/VerifyDbOptions.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/VerifyDbOptions.java new file mode 100644 index 000000000..d7c4fee37 --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/VerifyDbOptions.java @@ -0,0 +1,7 @@ +package org.keychain.gatekeeper.model; + +public class VerifyDbOptions { + public Boolean chatty; + + public VerifyDbOptions() {} +} diff --git a/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/VerifyDbResult.java b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/VerifyDbResult.java new file mode 100644 index 000000000..b16a0d3ee --- /dev/null +++ b/java/gatekeeper/src/main/java/org/keychain/gatekeeper/model/VerifyDbResult.java @@ -0,0 +1,10 @@ +package org.keychain.gatekeeper.model; + +public class VerifyDbResult { + public int total; + public int verified; + public int expired; + public int invalid; + + public VerifyDbResult() {} +} diff --git a/java/gatekeeper/src/test/java/org/keychain/gatekeeper/GatekeeperClientTest.java b/java/gatekeeper/src/test/java/org/keychain/gatekeeper/GatekeeperClientTest.java new file mode 100644 index 000000000..df6e9ae60 --- /dev/null +++ b/java/gatekeeper/src/test/java/org/keychain/gatekeeper/GatekeeperClientTest.java @@ -0,0 +1,164 @@ +package org.keychain.gatekeeper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.keychain.gatekeeper.model.BlockInfo; +import org.keychain.gatekeeper.model.Mdip; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.gatekeeper.model.Operation; +import org.keychain.gatekeeper.model.ResolveDIDOptions; + +class GatekeeperClientTest { + private MockWebServer server; + private GatekeeperClient client; + private ObjectMapper mapper; + + @BeforeEach + void setup() throws Exception { + server = new MockWebServer(); + server.start(); + GatekeeperClientOptions options = new GatekeeperClientOptions(); + options.baseUrl = server.url("/").toString().replaceAll("/$", ""); + client = new GatekeeperClient(options); + mapper = new ObjectMapper(); + } + + @AfterEach + void teardown() throws Exception { + server.shutdown(); + } + + @Test + void createDidPostsOperation() throws Exception { + server.enqueue(new MockResponse().setBody("\"did:test:123\"").setResponseCode(200)); + + Operation op = new Operation(); + op.type = "create"; + op.blockid = "blockhash"; + Mdip mdip = new Mdip(); + mdip.version = 1; + mdip.type = "agent"; + mdip.registry = "Signet"; + op.mdip = mdip; + String did = client.createDID(op); + + var recorded = server.takeRequest(); + assertEquals("POST", recorded.getMethod()); + assertEquals("/api/v1/did", recorded.getPath()); + assertEquals("did:test:123", did); + + JsonNode actual = mapper.readTree(recorded.getBody().readUtf8()); + JsonNode expected = mapper.readTree( + "{\"type\":\"create\",\"blockid\":\"blockhash\",\"mdip\":{\"version\":1,\"type\":\"agent\",\"registry\":\"Signet\"}}" + ); + assertEquals(expected, actual); + } + + @Test + void resolveDidBuildsQueryParams() throws Exception { + MdipDocument doc = new MdipDocument(); + server.enqueue(new MockResponse().setBody(mapper.writeValueAsString(doc)).setResponseCode(200)); + + ResolveDIDOptions options = new ResolveDIDOptions(); + options.confirm = true; + options.verify = false; + options.versionSequence = 2; + options.versionTime = "2024-01-01T00:00:00Z"; + + MdipDocument result = client.resolveDID("did:test:abc", options); + assertNotNull(result); + + var recorded = server.takeRequest(); + assertEquals("GET", recorded.getMethod()); + assertNotNull(recorded.getPath()); + assertTrue(recorded.getPath().startsWith("/api/v1/did/did:test:abc?")); + assertNotNull(recorded.getRequestUrl()); + assertEquals("true", recorded.getRequestUrl().queryParameter("confirm")); + assertEquals("false", recorded.getRequestUrl().queryParameter("verify")); + assertEquals("2", recorded.getRequestUrl().queryParameter("versionSequence")); + assertEquals("2024-01-01T00:00:00Z", recorded.getRequestUrl().queryParameter("versionTime")); + } + + @Test + void updateAndDeleteUsePostDid() throws Exception { + server.enqueue(new MockResponse().setBody("true").setResponseCode(200)); + server.enqueue(new MockResponse().setBody("true").setResponseCode(200)); + + Operation op = new Operation(); + op.type = "update"; + op.did = "did:test:abc"; + op.previd = "prev1"; + assertTrue(client.updateDID(op)); + + Operation del = new Operation(); + del.type = "delete"; + del.did = "did:test:abc"; + assertTrue(client.deleteDID(del)); + + var updateReq = server.takeRequest(); + assertEquals("POST", updateReq.getMethod()); + assertEquals("/api/v1/did", updateReq.getPath()); + JsonNode updateBody = mapper.readTree(updateReq.getBody().readUtf8()); + JsonNode expectedUpdate = mapper.readTree( + "{\"type\":\"update\",\"did\":\"did:test:abc\",\"previd\":\"prev1\"}" + ); + assertEquals(expectedUpdate, updateBody); + + var deleteReq = server.takeRequest(); + assertEquals("POST", deleteReq.getMethod()); + assertEquals("/api/v1/did", deleteReq.getPath()); + JsonNode deleteBody = mapper.readTree(deleteReq.getBody().readUtf8()); + JsonNode expectedDelete = mapper.readTree("{\"type\":\"delete\",\"did\":\"did:test:abc\"}"); + assertEquals(expectedDelete, deleteBody); + } + + @Test + void getBlockUsesRegistryPath() throws Exception { + BlockInfo block = new BlockInfo(); + block.hash = "hash"; + server.enqueue(new MockResponse().setBody(mapper.writeValueAsString(block)).setResponseCode(200)); + + BlockInfo result = client.getBlock("local"); + assertEquals("hash", result.hash); + + var recorded = server.takeRequest(); + assertEquals("GET", recorded.getMethod()); + assertEquals("/api/v1/block/local/latest", recorded.getPath()); + } + + @Test + void baseUrlStripsApiSuffix() throws Exception { + server.enqueue(new MockResponse().setBody("{}").setResponseCode(200)); + + GatekeeperClientOptions options = new GatekeeperClientOptions(); + options.baseUrl = server.url("/api/v1/").toString(); + GatekeeperClient localClient = new GatekeeperClient(options); + + localClient.getBlock("local"); + var recorded = server.takeRequest(); + assertEquals("/api/v1/block/local/latest", recorded.getPath()); + } + + @Test + void errorsThrowGatekeeperClientException() { + server.enqueue(new MockResponse().setResponseCode(400).setBody("{\"type\":\"Invalid\",\"message\":\"bad\"}")); + + Operation op = new Operation(); + op.type = "create"; + + GatekeeperClientException error = assertThrows(GatekeeperClientException.class, () -> client.createDID(op)); + assertEquals(400, error.statusCode); + assertNotNull(error.error); + assertEquals("Invalid", error.error.type); + } +} diff --git a/java/gradle.properties b/java/gradle.properties new file mode 100644 index 000000000..02c4f5d0f --- /dev/null +++ b/java/gradle.properties @@ -0,0 +1 @@ +org.gradle.java.home=/usr/lib/jvm/java-11-openjdk-amd64 diff --git a/java/gradle/wrapper/gradle-wrapper.jar b/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e6441136f Binary files /dev/null and b/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/java/gradle/wrapper/gradle-wrapper.properties b/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..b82aa23a4 --- /dev/null +++ b/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java/gradlew b/java/gradlew new file mode 100755 index 000000000..1aa94a426 --- /dev/null +++ b/java/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/java/gradlew.bat b/java/gradlew.bat new file mode 100644 index 000000000..7101f8e46 --- /dev/null +++ b/java/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java/keymaster/README.md b/java/keymaster/README.md new file mode 100644 index 000000000..416bf628a --- /dev/null +++ b/java/keymaster/README.md @@ -0,0 +1,28 @@ +# Keymaster (Java) + +Keymaster manages an encrypted wallet and signs Gatekeeper operations. + +## Usage + +```java +import java.nio.file.Path; +import java.nio.file.Paths; +import org.keychain.gatekeeper.GatekeeperClient; +import org.keychain.gatekeeper.GatekeeperClientOptions; +import org.keychain.keymaster.Keymaster; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.store.WalletJson; +import org.keychain.keymaster.store.WalletStore; + +Path dataDir = Paths.get(System.getProperty("user.home"), ".keymaster"); +WalletStore store = new WalletJson<>(WalletEncFile.class, dataDir, "wallet.json"); + +GatekeeperClientOptions options = new GatekeeperClientOptions(); +options.baseUrl = "http://localhost:4224"; +GatekeeperClient gatekeeper = new GatekeeperClient(options); + +Keymaster keymaster = new Keymaster(store, gatekeeper, "passphrase"); +keymaster.newWallet(null, true); + +String did = keymaster.createId("Alice", "hyperswarm"); +``` diff --git a/java/keymaster/build.gradle b/java/keymaster/build.gradle new file mode 100644 index 000000000..dbe32ad36 --- /dev/null +++ b/java/keymaster/build.gradle @@ -0,0 +1,28 @@ +dependencies { + implementation project(':cid') + implementation project(':crypto') + implementation project(':gatekeeper') + testRuntimeOnly 'org.slf4j:slf4j-nop:1.7.36' +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} + +tasks.named('test', Test) { + useJUnitPlatform { + excludeTags 'live' + } +} + +tasks.register('liveTest', Test) { + description = 'Runs live Gatekeeper credential tests' + group = 'verification' + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + useJUnitPlatform { + includeTags 'live' + } + outputs.upToDateWhen { false } + outputs.cacheIf { false } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/BindCredentialOptions.java b/java/keymaster/src/main/java/org/keychain/keymaster/BindCredentialOptions.java new file mode 100644 index 000000000..b164f05ed --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/BindCredentialOptions.java @@ -0,0 +1,11 @@ +package org.keychain.keymaster; + +import java.util.Map; + +public class BindCredentialOptions { + public String validFrom; + public String validUntil; + public Map credential; + + public BindCredentialOptions() {} +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/CreateAssetOptions.java b/java/keymaster/src/main/java/org/keychain/keymaster/CreateAssetOptions.java new file mode 100644 index 000000000..fb24a9086 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/CreateAssetOptions.java @@ -0,0 +1,10 @@ +package org.keychain.keymaster; + +public class CreateAssetOptions { + public String registry; + public String controller; + public String validUntil; + public String name; + + public CreateAssetOptions() {} +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/CreateIdOptions.java b/java/keymaster/src/main/java/org/keychain/keymaster/CreateIdOptions.java new file mode 100644 index 000000000..45ff8aca2 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/CreateIdOptions.java @@ -0,0 +1,7 @@ +package org.keychain.keymaster; + +public class CreateIdOptions { + public String registry; + + public CreateIdOptions() {} +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/CreateResponseOptions.java b/java/keymaster/src/main/java/org/keychain/keymaster/CreateResponseOptions.java new file mode 100644 index 000000000..67f8b5450 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/CreateResponseOptions.java @@ -0,0 +1,8 @@ +package org.keychain.keymaster; + +public class CreateResponseOptions extends EncryptOptions { + public Integer retries; + public Integer delay; + + public CreateResponseOptions() {} +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/EncryptOptions.java b/java/keymaster/src/main/java/org/keychain/keymaster/EncryptOptions.java new file mode 100644 index 000000000..2b88e387a --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/EncryptOptions.java @@ -0,0 +1,8 @@ +package org.keychain.keymaster; + +public class EncryptOptions extends CreateAssetOptions { + public Boolean encryptForSender; + public Boolean includeHash; + + public EncryptOptions() {} +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/IssueCredentialOptions.java b/java/keymaster/src/main/java/org/keychain/keymaster/IssueCredentialOptions.java new file mode 100644 index 000000000..8e7a9ae77 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/IssueCredentialOptions.java @@ -0,0 +1,9 @@ +package org.keychain.keymaster; + +public class IssueCredentialOptions extends EncryptOptions { + public String schema; + public String subject; + public String validFrom; + + public IssueCredentialOptions() {} +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/JwkConverter.java b/java/keymaster/src/main/java/org/keychain/keymaster/JwkConverter.java new file mode 100644 index 000000000..4dc7ad96c --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/JwkConverter.java @@ -0,0 +1,20 @@ +package org.keychain.keymaster; + +import org.keychain.crypto.JwkPublic; +import org.keychain.gatekeeper.model.EcdsaJwkPublic; + +public final class JwkConverter { + private JwkConverter() {} + + public static EcdsaJwkPublic toEcdsaJwkPublic(JwkPublic jwk) { + if (jwk == null) { + return null; + } + EcdsaJwkPublic out = new EcdsaJwkPublic(); + out.kty = jwk.kty; + out.crv = jwk.crv; + out.x = jwk.x; + out.y = jwk.y; + return out; + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/Keymaster.java b/java/keymaster/src/main/java/org/keychain/keymaster/Keymaster.java new file mode 100644 index 000000000..32e1f6a82 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/Keymaster.java @@ -0,0 +1,2841 @@ +package org.keychain.keymaster; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.bitcoinj.crypto.DeterministicKey; +import org.keychain.cid.Cid; +import org.keychain.crypto.HdKeyUtil; +import org.keychain.crypto.JwkPair; +import org.keychain.crypto.KeymasterCrypto; +import org.keychain.crypto.KeymasterCryptoImpl; +import org.keychain.crypto.MnemonicEncryption; +import org.keychain.gatekeeper.GatekeeperInterface; +import org.keychain.gatekeeper.model.BlockInfo; +import org.keychain.gatekeeper.model.DocumentMetadata; +import org.keychain.gatekeeper.model.EcdsaJwkPublic; +import org.keychain.gatekeeper.model.Mdip; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.gatekeeper.model.Operation; +import org.keychain.gatekeeper.model.ResolveDIDOptions; +import org.keychain.gatekeeper.model.Signature; +import org.keychain.keymaster.model.Seed; +import org.keychain.keymaster.model.IDInfo; +import org.keychain.keymaster.model.Group; +import org.keychain.keymaster.model.CheckWalletResult; +import org.keychain.keymaster.model.FixWalletResult; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.model.WalletFile; +import org.keychain.keymaster.store.WalletStore; +import org.keychain.keymaster.store.WalletJsonMapper; + +public class Keymaster { + private static final DateTimeFormatter ISO_MILLIS = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC); + private static final String DEFAULT_REGISTRY = "hyperswarm"; + private static final String EPOCH_ISO = ISO_MILLIS.format(Instant.EPOCH); + private static final int MAX_NAME_LENGTH = 32; + private static Supplier NOW_SUPPLIER = Instant::now; + private final KeymasterWalletManager walletManager; + private final KeymasterCrypto crypto; + private final String passphrase; + private final GatekeeperInterface gatekeeper; + private final OperationFactory operationFactory; + private final String defaultRegistry; + private final String ephemeralRegistry; + + public Keymaster( + WalletStore store, + GatekeeperInterface gatekeeper, + KeymasterCrypto crypto, + OperationFactory operationFactory, + String passphrase, + String defaultRegistry + ) { + if (store == null) { + throw new IllegalArgumentException("store is required"); + } + if (crypto == null) { + throw new IllegalArgumentException("crypto is required"); + } + if (operationFactory == null) { + throw new IllegalArgumentException("operationFactory is required"); + } + if (passphrase == null || passphrase.isEmpty()) { + throw new IllegalArgumentException("passphrase is required"); + } + + this.crypto = crypto; + this.passphrase = passphrase; + this.gatekeeper = gatekeeper; + this.operationFactory = operationFactory; + this.defaultRegistry = normalizeRegistry(defaultRegistry); + this.ephemeralRegistry = this.defaultRegistry; + this.walletManager = new KeymasterWalletManager(store, crypto, passphrase); + } + + public Keymaster( + WalletStore store, + GatekeeperInterface gatekeeper, + KeymasterCrypto crypto, + OperationFactory operationFactory, + String passphrase + ) { + this(store, gatekeeper, crypto, operationFactory, passphrase, DEFAULT_REGISTRY); + } + + public Keymaster( + WalletStore store, + GatekeeperInterface gatekeeper, + KeymasterCrypto crypto, + String passphrase + ) { + this(store, gatekeeper, crypto, new OperationFactory(crypto), passphrase, DEFAULT_REGISTRY); + } + + public Keymaster( + WalletStore store, + GatekeeperInterface gatekeeper, + KeymasterCrypto crypto, + String passphrase, + String defaultRegistry + ) { + this(store, gatekeeper, crypto, new OperationFactory(crypto), passphrase, defaultRegistry); + } + + public Keymaster(WalletStore store, KeymasterCrypto crypto, String passphrase) { + this(store, null, crypto, passphrase, DEFAULT_REGISTRY); + } + + public Keymaster(WalletStore store, GatekeeperInterface gatekeeper, String passphrase) { + this(store, gatekeeper, new KeymasterCryptoImpl(), passphrase, DEFAULT_REGISTRY); + } + + public Keymaster(WalletStore store, GatekeeperInterface gatekeeper, String passphrase, String defaultRegistry) { + this(store, gatekeeper, new KeymasterCryptoImpl(), passphrase, defaultRegistry); + } + + public Keymaster(WalletStore store, String passphrase) { + this(store, null, new KeymasterCryptoImpl(), passphrase, DEFAULT_REGISTRY); + } + + public WalletFile loadWallet() { + WalletFile wallet = walletManager.loadWallet(); + if (wallet == null) { + return newWallet(null, false); + } + return wallet; + } + + public boolean saveWallet(WalletFile wallet, boolean overwrite) { + return walletManager.saveWallet(wallet, overwrite); + } + + public boolean saveWallet(WalletEncFile wallet, boolean overwrite) { + if (wallet == null) { + throw new IllegalArgumentException("wallet is required"); + } + if (wallet.salt != null || wallet.iv != null || wallet.data != null) { + throw new IllegalStateException("Keymaster: Unsupported wallet version."); + } + if (wallet.version != 1 || wallet.seed == null || wallet.seed.mnemonicEnc == null || wallet.enc == null) { + throw new IllegalStateException("Keymaster: Unsupported wallet version."); + } + + try { + walletManager.decryptStoredWallet(wallet); + } catch (IllegalStateException e) { + throw new IllegalStateException("Keymaster: Incorrect passphrase."); + } + + try { + return walletManager.saveStoredWallet(wallet, overwrite); + } catch (IllegalStateException e) { + throw new IllegalStateException("Keymaster: Incorrect passphrase."); + } + } + + public boolean mutateWallet(Consumer mutator) { + loadWallet(); + return walletManager.mutateWallet(mutator); + } + + public java.util.Map listNames(boolean includeIds) { + WalletFile wallet = loadWallet(); + java.util.Map names = new java.util.HashMap<>(); + if (wallet.names != null) { + names.putAll(wallet.names); + } + if (includeIds && wallet.ids != null) { + for (java.util.Map.Entry entry : wallet.ids.entrySet()) { + names.put(entry.getKey(), entry.getValue().did); + } + } + return names; + } + + public java.util.List listIds() { + WalletFile wallet = loadWallet(); + if (wallet.ids == null) { + return new java.util.ArrayList<>(); + } + return new java.util.ArrayList<>(wallet.ids.keySet()); + } + + public String getCurrentId() { + WalletFile wallet = loadWallet(); + return wallet.current; + } + + public boolean setCurrentId(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("name is required"); + } + mutateWallet(wallet -> { + if (wallet.ids == null || !wallet.ids.containsKey(name)) { + throw new IllegalArgumentException("unknown id"); + } + wallet.current = name; + }); + return true; + } + + public boolean removeId(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("name is required"); + } + mutateWallet(wallet -> { + if (wallet.ids == null || !wallet.ids.containsKey(name)) { + throw new IllegalArgumentException("unknown id"); + } + wallet.ids.remove(name); + if (name.equals(wallet.current)) { + wallet.current = wallet.ids.isEmpty() ? "" : wallet.ids.keySet().iterator().next(); + } + }); + return true; + } + + public boolean renameId(String currentName, String newName) { + if (currentName == null || currentName.isBlank()) { + throw new IllegalArgumentException("current name is required"); + } + mutateWallet(wallet -> { + String validNew = validateNameInternal(newName, null); + if (wallet.ids == null || !wallet.ids.containsKey(currentName)) { + throw new IllegalArgumentException("unknown id"); + } + if (wallet.ids.containsKey(validNew)) { + throw new IllegalArgumentException("name already used"); + } + IDInfo info = wallet.ids.get(currentName); + wallet.ids.put(validNew, info); + wallet.ids.remove(currentName); + if (currentName.equals(wallet.current)) { + wallet.current = validNew; + } + }); + return true; + } + + public boolean addName(String name, String did) { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + mutateWallet(wallet -> { + if (wallet.names == null) { + wallet.names = new java.util.HashMap<>(); + } + String valid = validateNameInternal(name, wallet); + wallet.names.put(valid, did); + }); + return true; + } + + public String getName(String name) { + WalletFile wallet = loadWallet(); + if (wallet.names != null && wallet.names.containsKey(name)) { + return wallet.names.get(name); + } + return null; + } + + public boolean removeName(String name) { + mutateWallet(wallet -> { + if (wallet.names == null || !wallet.names.containsKey(name)) { + return; + } + wallet.names.remove(name); + }); + return true; + } + + public WalletFile newWallet(String mnemonic, boolean overwrite) { + String phrase = mnemonic != null ? mnemonic : crypto.generateMnemonic(); + + Seed seed = new Seed(); + seed.mnemonicEnc = MnemonicEncryption.encrypt(phrase, passphrase); + + WalletFile wallet = new WalletFile(); + wallet.version = 1; + wallet.seed = seed; + wallet.counter = 0; + wallet.ids = new HashMap<>(); + + boolean ok = saveWallet(wallet, overwrite); + if (!ok) { + throw new IllegalStateException("save wallet failed"); + } + + return wallet; + } + + public String decryptMnemonic() { + WalletFile wallet = loadWallet(); + if (wallet == null || wallet.seed == null || wallet.seed.mnemonicEnc == null) { + throw new IllegalStateException("wallet mnemonic not available"); + } + return MnemonicEncryption.decrypt(wallet.seed.mnemonicEnc, passphrase); + } + + public String getMnemonicForDerivation(WalletFile wallet) { + if (wallet == null || wallet.seed == null || wallet.seed.mnemonicEnc == null) { + throw new IllegalStateException("wallet mnemonic not available"); + } + return MnemonicEncryption.decrypt(wallet.seed.mnemonicEnc, passphrase); + } + + public WalletEncFile exportEncryptedWallet() { + WalletFile wallet = loadWallet(); + WalletCrypto walletCrypto = new WalletCrypto(crypto, passphrase); + return walletCrypto.encryptForStorage(wallet); + } + + public java.util.List listRegistries() { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + return gatekeeper.listRegistries(); + } + + public CheckWalletResult checkWallet() { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + + WalletFile wallet = loadWallet(); + resolveSeedBank(); + + CheckWalletResult result = new CheckWalletResult(); + + if (wallet.ids != null) { + for (IDInfo id : wallet.ids.values()) { + tallyDid(id.did, result); + } + + for (IDInfo id : wallet.ids.values()) { + if (id.owned != null) { + for (String did : id.owned) { + tallyDid(did, result); + } + } + if (id.held != null) { + for (String did : id.held) { + tallyDid(did, result); + } + } + } + } + + if (wallet.names != null) { + for (String did : wallet.names.values()) { + tallyDid(did, result); + } + } + + return result; + } + + public FixWalletResult fixWallet() { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + + FixWalletResult result = new FixWalletResult(); + mutateWallet(wallet -> { + if (wallet.ids != null) { + java.util.Iterator> iterator = wallet.ids.entrySet().iterator(); + while (iterator.hasNext()) { + java.util.Map.Entry entry = iterator.next(); + if (shouldRemoveDid(entry.getValue().did)) { + iterator.remove(); + result.idsRemoved += 1; + } + } + + for (IDInfo id : wallet.ids.values()) { + if (id.owned != null) { + for (int i = 0; i < id.owned.size(); i += 1) { + if (shouldRemoveDid(id.owned.get(i))) { + id.owned.remove(i); + i -= 1; + result.ownedRemoved += 1; + } + } + } + if (id.held != null) { + for (int i = 0; i < id.held.size(); i += 1) { + if (shouldRemoveDid(id.held.get(i))) { + id.held.remove(i); + i -= 1; + result.heldRemoved += 1; + } + } + } + } + } + + if (wallet.names != null) { + java.util.Iterator> iterator = wallet.names.entrySet().iterator(); + while (iterator.hasNext()) { + java.util.Map.Entry entry = iterator.next(); + if (shouldRemoveDid(entry.getValue())) { + iterator.remove(); + result.namesRemoved += 1; + } + } + } + }); + + return result; + } + + public MdipDocument resolveSeedBank() { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + + JwkPair keypair = hdKeyPair(); + Operation operation = new Operation(); + operation.type = "create"; + operation.created = EPOCH_ISO; + + Mdip mdip = new Mdip(); + mdip.version = 1; + mdip.type = "agent"; + mdip.registry = defaultRegistry; + operation.mdip = mdip; + operation.publicJwk = JwkConverter.toEcdsaJwkPublic(keypair.publicJwk); + + String msgHash = crypto.hashJson(operation); + Signature signature = new Signature(); + signature.signed = EPOCH_ISO; + signature.hash = msgHash; + signature.value = crypto.signHash(msgHash, keypair.privateJwk); + operation.signature = signature; + + String did = gatekeeper.createDID(operation); + return gatekeeper.resolveDID(did, null); + } + + public boolean backupId() { + return backupId(null); + } + + public boolean backupId(String id) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + WalletFile wallet = loadWallet(); + String name = id; + if (name == null || name.isBlank()) { + name = wallet.current; + } + if (name == null || name.isBlank()) { + throw new IllegalStateException("Keymaster: No current ID"); + } + IDInfo idInfo = fetchIdInfo(name, wallet); + JwkPair keypair = hdKeyPair(); + + java.util.Map data = new java.util.LinkedHashMap<>(); + data.put("name", name); + data.put("id", idInfo); + String msg; + try { + msg = WalletJsonMapper.mapper().writeValueAsString(data); + } catch (Exception e) { + throw new IllegalStateException("backupId: unable to serialize data", e); + } + + String backup = crypto.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg); + MdipDocument doc = resolveDID(idInfo.did); + String registry = doc != null && doc.mdip != null ? doc.mdip.registry : null; + if (registry == null || registry.isBlank()) { + throw new IllegalArgumentException("no registry found for agent DID"); + } + + java.util.Map payload = new java.util.LinkedHashMap<>(); + payload.put("backup", backup); + String vaultDid = createAsset(payload, registry, name, null); + + if (!(doc.didDocumentData instanceof java.util.Map)) { + doc.didDocumentData = new java.util.LinkedHashMap<>(); + } + @SuppressWarnings("unchecked") + java.util.Map docData = (java.util.Map) doc.didDocumentData; + docData.put("vault", vaultDid); + doc.didDocumentData = docData; + return updateDID(doc); + } + + public String recoverId(String did) { + try { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + JwkPair keypair = hdKeyPair(); + MdipDocument doc = resolveDID(did); + if (doc == null || !(doc.didDocumentData instanceof java.util.Map)) { + throw new IllegalArgumentException("didDocumentData missing vault"); + } + + @SuppressWarnings("unchecked") + java.util.Map docData = (java.util.Map) doc.didDocumentData; + Object vaultObj = docData.get("vault"); + if (!(vaultObj instanceof String)) { + throw new IllegalArgumentException("didDocumentData missing vault"); + } + + Object vaultObjData = resolveAsset((String) vaultObj); + if (!(vaultObjData instanceof java.util.Map)) { + throw new IllegalArgumentException("backup not found in vault"); + } + @SuppressWarnings("unchecked") + java.util.Map vaultData = (java.util.Map) vaultObjData; + Object backupObj = vaultData.get("backup"); + if (!(backupObj instanceof String)) { + throw new IllegalArgumentException("backup not found in vault"); + } + + String decrypted = crypto.decryptMessage(keypair.publicJwk, keypair.privateJwk, (String) backupObj); + @SuppressWarnings("unchecked") + java.util.Map data = WalletJsonMapper.mapper().readValue(decrypted, java.util.Map.class); + Object nameObj = data.get("name"); + Object idObj = data.get("id"); + if (!(nameObj instanceof String) || !(idObj instanceof java.util.Map)) { + throw new IllegalArgumentException("Invalid backup data"); + } + @SuppressWarnings("unchecked") + java.util.Map idMap = (java.util.Map) idObj; + IDInfo idInfo = WalletJsonMapper.mapper().convertValue(idMap, IDInfo.class); + String name = (String) nameObj; + + mutateWallet(wallet -> { + if (wallet.ids == null) { + wallet.ids = new java.util.LinkedHashMap<>(); + } + if (wallet.ids.containsKey(name)) { + throw new IllegalStateException(name + " already exists in wallet"); + } + wallet.ids.put(name, idInfo); + wallet.current = name; + wallet.counter = wallet.counter + 1; + }); + + return name; + } catch (Exception e) { + if (e instanceof IllegalStateException) { + throw (IllegalStateException) e; + } + throw new IllegalArgumentException("did"); + } + } + + public boolean updateSeedBank(MdipDocument doc) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + if (doc == null || doc.didDocument == null || doc.didDocument.id == null || doc.didDocument.id.isBlank()) { + throw new IllegalArgumentException("Invalid parameter: seed bank missing DID"); + } + + JwkPair keypair = hdKeyPair(); + String did = doc.didDocument.id; + MdipDocument current = gatekeeper.resolveDID(did, null); + String previd = current != null && current.didDocumentMetadata != null ? current.didDocumentMetadata.versionId : null; + + Operation operation = new Operation(); + operation.type = "update"; + operation.did = did; + operation.previd = previd; + operation.doc = doc; + + String msgHash = crypto.hashJson(operation); + Signature signature = new Signature(); + signature.signer = did; + signature.signed = nowIso(); + signature.hash = msgHash; + signature.value = crypto.signHash(msgHash, keypair.privateJwk); + operation.signature = signature; + + return gatekeeper.updateDID(operation); + } + + public String backupWallet() { + return backupWallet(defaultRegistry, null); + } + + public String backupWallet(String registry) { + return backupWallet(registry, null); + } + + public String backupWallet(String registry, WalletFile wallet) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + if (registry == null || registry.isBlank()) { + registry = defaultRegistry; + } + if (wallet == null) { + wallet = loadWallet(); + } + + JwkPair keypair = hdKeyPair(); + MdipDocument seedBank = resolveSeedBank(); + String msg; + try { + msg = WalletJsonMapper.mapper().writeValueAsString(wallet); + } catch (Exception e) { + throw new IllegalStateException("backup wallet failed", e); + } + + String backup = crypto.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg); + + Operation operation = new Operation(); + operation.type = "create"; + operation.created = nowIso(); + + Mdip mdip = new Mdip(); + mdip.version = 1; + mdip.type = "asset"; + mdip.registry = registry; + operation.mdip = mdip; + operation.controller = seedBank.didDocument != null ? seedBank.didDocument.id : null; + java.util.Map data = new java.util.LinkedHashMap<>(); + data.put("backup", backup); + operation.data = data; + + String msgHash = crypto.hashJson(operation); + Signature signature = new Signature(); + signature.signer = operation.controller; + signature.signed = nowIso(); + signature.hash = msgHash; + signature.value = crypto.signHash(msgHash, keypair.privateJwk); + operation.signature = signature; + + String backupDid = gatekeeper.createDID(operation); + + if (seedBank.didDocumentData instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map seedData = (java.util.Map) seedBank.didDocumentData; + seedData.put("wallet", backupDid); + seedBank.didDocumentData = seedData; + updateSeedBank(seedBank); + } + + return backupDid; + } + + public WalletFile recoverWallet() { + return recoverWallet(null); + } + + public WalletFile recoverWallet(String did) { + try { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + + if (did == null || did.isBlank()) { + MdipDocument seedBank = resolveSeedBank(); + if (seedBank.didDocumentData instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map data = (java.util.Map) seedBank.didDocumentData; + Object walletDid = data.get("wallet"); + if (walletDid instanceof String) { + did = (String) walletDid; + } + } + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("No backup DID found"); + } + } + + JwkPair keypair = hdKeyPair(); + Object assetObj = resolveAsset(did); + if (!(assetObj instanceof java.util.Map)) { + throw new IllegalArgumentException("No asset data found"); + } + @SuppressWarnings("unchecked") + java.util.Map data = (java.util.Map) assetObj; + + Object backupObj = data.get("backup"); + if (!(backupObj instanceof String)) { + throw new IllegalArgumentException("Asset \"backup\" is missing or not a string"); + } + + String backup = crypto.decryptMessage(keypair.publicJwk, keypair.privateJwk, (String) backupObj); + WalletFile recovered = WalletJsonMapper.mapper().readValue(backup, WalletFile.class); + + WalletFile upgraded = walletManager.upgradeWallet(recovered); + if (upgraded.version != null && upgraded.version == 1 && upgraded.seed != null && upgraded.seed.mnemonicEnc != null) { + String mnemonic = decryptMnemonic(); + upgraded.seed.mnemonicEnc = MnemonicEncryption.encrypt(mnemonic, passphrase); + } + + mutateWallet(current -> replaceWallet(current, upgraded)); + return loadWallet(); + } catch (Exception e) { + return loadWallet(); + } + } + + public String createId(String name, String registry) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + String targetRegistry = registry == null || registry.isBlank() ? defaultRegistry : registry; + + final String[] createdDid = new String[1]; + mutateWallet(wallet -> { + String validName = validateNameInternal(name, wallet); + + int account = wallet.counter; + int index = 0; + JwkPair keypair = getCurrentKeypairFromPath(wallet, account, index); + + BlockInfo block = gatekeeper.getBlock(targetRegistry); + String blockid = block != null ? block.hash : null; + + Operation signed = operationFactory.createSignedCreateIdOperation( + targetRegistry, + JwkConverter.toEcdsaJwkPublic(keypair.publicJwk), + keypair.privateJwk, + blockid + ); + + String did = gatekeeper.createDID(signed); + createdDid[0] = did; + + IDInfo idInfo = new IDInfo(); + idInfo.did = did; + idInfo.account = account; + idInfo.index = index; + + wallet.ids.put(validName, idInfo); + wallet.counter = account + 1; + wallet.current = validName; + }); + + return createdDid[0]; + } + + public String createId(String name, CreateIdOptions options) { + String registry = options != null ? options.registry : null; + return createId(name, registry); + } + + public String createId(String name) { + return createId(name, (CreateIdOptions) null); + } + + public Operation createIdOperation(String name) { + return createIdOperation(name, 0, (String) null); + } + + public Operation createIdOperation(String name, int account) { + return createIdOperation(name, account, (String) null); + } + + public Operation createIdOperation(String name, CreateIdOptions options) { + return createIdOperation(name, 0, options); + } + + public Operation createIdOperation(String name, int account, String registry) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + if (account < 0) { + throw new IllegalArgumentException("account must be non-negative"); + } + String targetRegistry = registry == null || registry.isBlank() ? defaultRegistry : registry; + + WalletFile wallet = loadWallet(); + validateNameInternal(name, wallet); + JwkPair keypair = getCurrentKeypairFromPath(wallet, account, 0); + + BlockInfo block = gatekeeper.getBlock(targetRegistry); + String blockid = block != null ? block.hash : null; + + return operationFactory.createSignedCreateIdOperation( + targetRegistry, + JwkConverter.toEcdsaJwkPublic(keypair.publicJwk), + keypair.privateJwk, + blockid + ); + } + + public Operation createIdOperation(String name, int account, CreateIdOptions options) { + String registry = options != null ? options.registry : null; + return createIdOperation(name, account, registry); + } + + public String createAsset(Object data, String registry) { + return createAsset(data, registry, null, null); + } + + public String createAsset(Object data) { + return createAsset(data, defaultRegistry, null, null); + } + + public String createAsset(Object data, String registry, String controllerDid, String validUntil) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + if (data == null) { + throw new IllegalArgumentException("data is required"); + } + if (registry == null || registry.isBlank()) { + throw new IllegalArgumentException("registry is required"); + } + validateValidUntil(validUntil); + + WalletFile wallet = loadWallet(); + IDInfo idInfo = controllerDid != null ? fetchIdInfo(controllerDid, wallet) : fetchIdInfo(null, wallet); + String signerDid = idInfo.did; + JwkPair keypair = fetchKeyPair(signerDid); + if (keypair == null) { + throw new IllegalStateException("no keypair available for controller"); + } + BlockInfo block = gatekeeper.getBlock(registry); + String blockid = block != null ? block.hash : null; + + Operation signed = operationFactory.createSignedCreateAssetOperation( + registry, + signerDid, + data, + blockid, + validUntil, + keypair.privateJwk, + signerDid + ); + + String did = gatekeeper.createDID(signed); + if (validUntil == null) { + mutateWallet(updated -> { + IDInfo current = getCurrentIdInfo(updated); + if (current.owned == null) { + current.owned = new ArrayList<>(); + } + List owned = current.owned; + if (!owned.contains(did)) { + owned.add(did); + } + }); + } + return did; + } + + public String createAsset(Object data, CreateAssetOptions options) { + if (options == null) { + return createAsset(data, defaultRegistry, null, null); + } + String registry = options.registry != null ? options.registry : defaultRegistry; + String validUntil = options.validUntil; + String controller = options.controller; + String did = createAsset(data, registry, controller, validUntil); + if (options.name != null && !options.name.isBlank()) { + addName(options.name, did); + } + return did; + } + + public String createSchema(String registry) { + return createSchema(null, registry); + } + + public String createSchema(Object schema, String registry) { + CreateAssetOptions options = new CreateAssetOptions(); + if (registry != null && !registry.isBlank()) { + options.registry = registry; + } + return createSchema(schema, options); + } + + public String createSchema(Object schema, CreateAssetOptions options) { + if (schema == null) { + schema = defaultSchema(); + } + if (!validateSchema(schema)) { + throw new IllegalArgumentException("Invalid parameter: schema"); + } + java.util.Map data = new java.util.HashMap<>(); + data.put("schema", schema); + return createAsset(data, options); + } + + public String createSchema(Object schema) { + return createSchema(schema, (CreateAssetOptions) null); + } + + public String createSchema() { + return createSchema(null, (CreateAssetOptions) null); + } + + public String createSchema(CreateAssetOptions options) { + return createSchema(null, options); + } + + public String createChallenge() { + return createChallenge(new java.util.LinkedHashMap<>(), null); + } + + public String createChallenge(java.util.Map challenge) { + return createChallenge(challenge, null); + } + + public String createChallenge(java.util.Map challenge, CreateAssetOptions options) { + if (challenge == null) { + throw new IllegalArgumentException("challenge"); + } + if (challenge instanceof java.util.List) { + throw new IllegalArgumentException("challenge"); + } + Object credentialsObj = challenge.get("credentials"); + if (credentialsObj != null && !(credentialsObj instanceof java.util.List)) { + throw new IllegalArgumentException("challenge.credentials"); + } + + CreateAssetOptions effective = options != null ? options : new CreateAssetOptions(); + if (effective.registry == null || effective.registry.isBlank()) { + effective.registry = ephemeralRegistry; + } + if (effective.validUntil == null) { + effective.validUntil = ISO_MILLIS.format(Instant.now().plusSeconds(3600)); + } + + java.util.Map payload = new java.util.LinkedHashMap<>(); + payload.put("challenge", challenge); + return createAsset(payload, effective); + } + + public Object getSchema(String did) { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + Object data = resolveAsset(did); + if (data instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) data; + if (map.containsKey("schema")) { + return map.get("schema"); + } + // legacy schemas stored directly + if (map.containsKey("properties")) { + return map; + } + } + return null; + } + + public boolean setSchema(String did, Object schema) { + if (!validateSchema(schema)) { + throw new IllegalArgumentException("Invalid parameter: schema"); + } + java.util.Map data = new java.util.HashMap<>(); + data.put("schema", schema); + return updateAsset(did, data); + } + + public boolean testSchema(String did) { + try { + Object schema = getSchema(did); + if (!(schema instanceof java.util.Map)) { + return false; + } + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) schema; + if (map.isEmpty()) { + return false; + } + return validateSchema(schema); + } catch (IllegalArgumentException e) { + return false; + } + } + + public java.util.Map createTemplate(String schemaId) { + if (!testSchema(schemaId)) { + throw new IllegalArgumentException("Invalid parameter: schemaId"); + } + Object schema = getSchema(schemaId); + java.util.Map template = generateSchema(schema); + template.put("$schema", schemaId); + return template; + } + + public java.util.List listSchemas(String ownerDid) { + java.util.List assets = listAssets(ownerDid); + java.util.List schemas = new java.util.ArrayList<>(); + for (String did : assets) { + if (testSchema(did)) { + schemas.add(did); + } + } + return schemas; + } + + public java.util.List listSchemas() { + return listSchemas(null); + } + + public String cloneAsset(String id) { + return cloneAsset(id, null, null); + } + + public String cloneAsset(String id, String registry, String controller) { + MdipDocument assetDoc = resolveDID(id); + if (assetDoc == null || assetDoc.mdip == null || !"asset".equals(assetDoc.mdip.type)) { + throw new IllegalArgumentException("id"); + } + + java.util.Map assetData = new java.util.LinkedHashMap<>(); + if (assetDoc.didDocumentData instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map data = (java.util.Map) assetDoc.didDocumentData; + assetData.putAll(data); + } + assetData.put("cloned", assetDoc.didDocument != null ? assetDoc.didDocument.id : null); + + String targetRegistry = registry; + if (targetRegistry == null || targetRegistry.isBlank()) { + targetRegistry = assetDoc.mdip.registry ; + } + + return createAsset(assetData, targetRegistry, controller, null); + } + + public String createResponse(String challengeDid) { + return createResponse(challengeDid, null); + } + + public String createResponse(String challengeDid, CreateResponseOptions options) { + CreateResponseOptions effective = options != null ? options : new CreateResponseOptions(); + int retries = effective.retries != null ? effective.retries : 0; + int delay = effective.delay != null ? effective.delay : 1000; + + if (effective.registry == null || effective.registry.isBlank()) { + effective.registry = ephemeralRegistry; + } + if (effective.validUntil == null) { + effective.validUntil = ISO_MILLIS.format(Instant.now().plusSeconds(3600)); + } + boolean encryptForSender = effective.encryptForSender == null || effective.encryptForSender; + + MdipDocument doc = resolveWithRetries(challengeDid, retries, delay); + if (doc == null) { + throw new IllegalArgumentException("challengeDID does not resolve"); + } + + Object result = resolveAsset(challengeDid); + if (!(result instanceof java.util.Map)) { + throw new IllegalArgumentException("Invalid parameter: challengeDID"); + } + @SuppressWarnings("unchecked") + java.util.Map resultMap = (java.util.Map) result; + Object challengeObj = resultMap.get("challenge"); + if (!(challengeObj instanceof java.util.Map)) { + throw new IllegalArgumentException("Invalid parameter: challengeDID"); + } + @SuppressWarnings("unchecked") + java.util.Map challenge = (java.util.Map) challengeObj; + + String requestor = doc.didDocument != null ? doc.didDocument.controller : null; + if (requestor == null || requestor.isBlank()) { + throw new IllegalArgumentException("requestor undefined"); + } + + java.util.List matches = new java.util.ArrayList<>(); + Object credentialsObj = challenge.get("credentials"); + if (credentialsObj instanceof java.util.List) { + @SuppressWarnings("unchecked") + java.util.List credentials = (java.util.List) credentialsObj; + for (Object item : credentials) { + if (!(item instanceof java.util.Map)) { + continue; + } + @SuppressWarnings("unchecked") + java.util.Map spec = (java.util.Map) item; + String match = findMatchingCredential(spec); + if (match != null) { + matches.add(match); + } + } + } + + java.util.List> pairs = new java.util.ArrayList<>(); + for (String vcDid : matches) { + String plaintext = decryptMessage(vcDid); + String vpDid = encryptMessage( + plaintext, + requestor, + true, + effective.registry, + effective.validUntil, + encryptForSender + ); + java.util.Map pair = new java.util.LinkedHashMap<>(); + pair.put("vc", vcDid); + pair.put("vp", vpDid); + pairs.add(pair); + } + + int requested = credentialsObj instanceof java.util.List ? ((java.util.List) credentialsObj).size() : 0; + int fulfilled = matches.size(); + boolean match = requested == fulfilled; + + java.util.Map response = new java.util.LinkedHashMap<>(); + response.put("challenge", challengeDid); + response.put("credentials", pairs); + response.put("requested", requested); + response.put("fulfilled", fulfilled); + response.put("match", match); + + java.util.Map wrapper = new java.util.LinkedHashMap<>(); + wrapper.put("response", response); + return encryptJsonInternal( + wrapper, + requestor, + false, + effective.registry, + effective.validUntil, + encryptForSender + ); + } + + public java.util.Map verifyResponse(String responseDid) { + return verifyResponse(responseDid, null); + } + + public java.util.Map verifyResponse(String responseDid, CreateResponseOptions options) { + CreateResponseOptions effective = options != null ? options : new CreateResponseOptions(); + int retries = effective.retries != null ? effective.retries : 0; + int delay = effective.delay != null ? effective.delay : 1000; + + MdipDocument responseDoc = resolveWithRetries(responseDid, retries, delay); + if (responseDoc == null) { + throw new IllegalArgumentException("responseDID does not resolve"); + } + + Object wrapperObj = decryptJSON(responseDid); + if (!(wrapperObj instanceof java.util.Map)) { + throw new IllegalArgumentException("responseDID not a valid challenge response"); + } + @SuppressWarnings("unchecked") + java.util.Map wrapper = (java.util.Map) wrapperObj; + Object responseObj = wrapper.get("response"); + if (!(responseObj instanceof java.util.Map)) { + throw new IllegalArgumentException("responseDID not a valid challenge response"); + } + @SuppressWarnings("unchecked") + java.util.Map response = (java.util.Map) responseObj; + + Object challengeDidObj = response.get("challenge"); + if (!(challengeDidObj instanceof String)) { + throw new IllegalArgumentException("challenge not found"); + } + Object result = resolveAsset((String) challengeDidObj); + if (!(result instanceof java.util.Map)) { + throw new IllegalArgumentException("challenge not found"); + } + @SuppressWarnings("unchecked") + java.util.Map resultMap = (java.util.Map) result; + Object challengeObj = resultMap.get("challenge"); + if (!(challengeObj instanceof java.util.Map)) { + throw new IllegalArgumentException("Invalid parameter: challengeDID"); + } + @SuppressWarnings("unchecked") + java.util.Map challenge = (java.util.Map) challengeObj; + + java.util.List> vps = new java.util.ArrayList<>(); + Object credentialsObj = response.get("credentials"); + if (credentialsObj instanceof java.util.List) { + for (Object entry : (java.util.List) credentialsObj) { + if (!(entry instanceof java.util.Map)) { + continue; + } + @SuppressWarnings("unchecked") + java.util.Map pair = (java.util.Map) entry; + Object vcObj = pair.get("vc"); + Object vpObj = pair.get("vp"); + if (!(vcObj instanceof String) || !(vpObj instanceof String)) { + continue; + } + + Object vcData = resolveAsset((String) vcObj); + Object vpData = resolveAsset((String) vpObj); + if (!(vcData instanceof java.util.Map) || !(vpData instanceof java.util.Map)) { + continue; + } + + @SuppressWarnings("unchecked") + java.util.Map vcDataMap = (java.util.Map) vcData; + @SuppressWarnings("unchecked") + java.util.Map vpDataMap = (java.util.Map) vpData; + Object vcEncryptedObj = vcDataMap.get("encrypted"); + Object vpEncryptedObj = vpDataMap.get("encrypted"); + if (!(vcEncryptedObj instanceof java.util.Map) || !(vpEncryptedObj instanceof java.util.Map)) { + continue; + } + @SuppressWarnings("unchecked") + java.util.Map vcEncrypted = (java.util.Map) vcEncryptedObj; + @SuppressWarnings("unchecked") + java.util.Map vpEncrypted = (java.util.Map) vpEncryptedObj; + Object vcHash = vcEncrypted.get("cipher_hash"); + Object vpHash = vpEncrypted.get("cipher_hash"); + if (vcHash == null || !vcHash.equals(vpHash)) { + continue; + } + + Object vpPlain = decryptJSON((String) vpObj); + if (!(vpPlain instanceof java.util.Map)) { + continue; + } + @SuppressWarnings("unchecked") + java.util.Map vp = (java.util.Map) vpPlain; + if (!verifySignature(vp)) { + continue; + } + + Object typesObj = vp.get("type"); + if (!(typesObj instanceof java.util.List)) { + continue; + } + + if (((java.util.List) typesObj).size() >= 2) { + Object schemaObj = ((java.util.List) typesObj).get(1); + if (schemaObj instanceof String) { + String schema = (String) schemaObj; + Object challengeCredentialsObj = challenge.get("credentials"); + if (challengeCredentialsObj instanceof java.util.List) { + java.util.Map matchSpec = null; + for (Object specObj : (java.util.List) challengeCredentialsObj) { + if (specObj instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map spec = (java.util.Map) specObj; + if (schema.equals(spec.get("schema"))) { + matchSpec = spec; + break; + } + } + } + if (matchSpec != null) { + Object issuersObj = matchSpec.get("issuers"); + if (issuersObj instanceof java.util.List) { + Object issuerObj = vp.get("issuer"); + if (issuerObj instanceof String) { + if (!((java.util.List) issuersObj).contains(issuerObj)) { + continue; + } + } + } + } else { + continue; + } + } + } + } + + vps.add(vp); + } + } + + response.put("vps", vps); + Object challengeCredentialsObj = challenge.get("credentials"); + int requested = challengeCredentialsObj instanceof java.util.List ? ((java.util.List) challengeCredentialsObj).size() : 0; + response.put("match", vps.size() == requested); + response.put("responder", responseDoc.didDocument != null ? responseDoc.didDocument.controller : null); + + return response; + } + + public String createGroup(String name) { + return createGroup(name, null); + } + + public String createGroup(String name, CreateAssetOptions options) { + java.util.Map group = new java.util.LinkedHashMap<>(); + group.put("name", name); + group.put("members", new java.util.ArrayList<>()); + + java.util.Map payload = new java.util.LinkedHashMap<>(); + payload.put("group", group); + return createAsset(payload, options); + } + + public Group getGroup(String id) { + Object asset = resolveAsset(id); + if (!(asset instanceof java.util.Map)) { + return null; + } + + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) asset; + if (map.isEmpty()) { + return null; + } + + if (map.containsKey("members")) { + return WalletJsonMapper.mapper().convertValue(map, Group.class); + } + + Object groupObj = map.get("group"); + if (groupObj == null) { + return null; + } + + return WalletJsonMapper.mapper().convertValue(groupObj, Group.class); + } + + public boolean addGroupMember(String groupId, String memberId) { + String groupDid = lookupDID(groupId); + String memberDid = lookupDID(memberId); + + if (memberDid.equals(groupDid)) { + throw new IllegalArgumentException("Invalid parameter: can't add a group to itself"); + } + + try { + resolveDID(memberDid); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid parameter: memberId"); + } + + Group group = getGroup(groupId); + if (group == null || group.members == null) { + throw new IllegalArgumentException("Invalid parameter: groupId"); + } + + if (group.members.contains(memberDid)) { + return true; + } + + boolean isMember = testGroup(memberId, groupId); + if (isMember) { + throw new IllegalArgumentException("Invalid parameter: can't create mutual membership"); + } + + java.util.Set members = new java.util.LinkedHashSet<>(group.members); + members.add(memberDid); + group.members = new java.util.ArrayList<>(members); + + java.util.Map payload = new java.util.LinkedHashMap<>(); + payload.put("group", WalletJsonMapper.mapper().convertValue(group, java.util.Map.class)); + return updateAsset(groupDid, payload); + } + + public boolean removeGroupMember(String groupId, String memberId) { + String groupDid = lookupDID(groupId); + String memberDid = lookupDID(memberId); + Group group = getGroup(groupDid); + + if (group == null || group.members == null) { + throw new IllegalArgumentException("Invalid parameter: groupId"); + } + + try { + resolveDID(memberDid); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid parameter: memberId"); + } + + if (!group.members.contains(memberDid)) { + return true; + } + + java.util.Set members = new java.util.LinkedHashSet<>(group.members); + members.remove(memberDid); + group.members = new java.util.ArrayList<>(members); + + java.util.Map payload = new java.util.LinkedHashMap<>(); + payload.put("group", WalletJsonMapper.mapper().convertValue(group, java.util.Map.class)); + return updateAsset(groupDid, payload); + } + + public boolean testGroup(String groupId) { + return testGroup(groupId, null); + } + + public boolean testGroup(String groupId, String memberId) { + try { + Group group = getGroup(groupId); + if (group == null) { + return false; + } + + if (memberId == null || memberId.isBlank()) { + return true; + } + + String didMember = lookupDID(memberId); + boolean isMember = group.members != null && group.members.contains(didMember); + + if (!isMember && group.members != null) { + for (String did : group.members) { + isMember = testGroup(did, didMember); + if (isMember) { + break; + } + } + } + + return isMember; + } catch (Exception e) { + return false; + } + } + + public java.util.List listGroups(String owner) { + java.util.List assets = listAssets(owner); + java.util.List groups = new java.util.ArrayList<>(); + + for (String did : assets) { + if (testGroup(did)) { + groups.add(did); + } + } + + return groups; + } + + public java.util.List listGroups() { + return listGroups(null); + } + + public java.util.Map bindCredential(String schemaId, String subjectId) { + return bindCredential(schemaId, subjectId, null, null, null); + } + + public java.util.Map bindCredential( + String schemaId, + String subjectId, + BindCredentialOptions options + ) { + if (options == null) { + return bindCredential(schemaId, subjectId, null, null, null); + } + return bindCredential(schemaId, subjectId, options.validFrom, options.validUntil, options.credential); + } + + public java.util.Map bindCredential( + String schemaId, + String subjectId, + String validFrom, + String validUntil, + java.util.Map credential + ) { + if (schemaId == null || schemaId.isBlank()) { + throw new IllegalArgumentException("schemaId"); + } + if (subjectId == null || subjectId.isBlank()) { + throw new IllegalArgumentException("subjectId"); + } + + String from = validFrom != null ? validFrom : nowIso(); + String type = lookupDID(schemaId); + String subjectDid = lookupDID(subjectId); + IDInfo issuer = fetchIdInfo(null); + + java.util.Map boundCredential = credential; + if (boundCredential == null) { + Object schema = getSchema(type); + boundCredential = generateSchema(schema); + } + + java.util.Map subject = new java.util.LinkedHashMap<>(); + subject.put("id", subjectDid); + + java.util.List types = new java.util.ArrayList<>(); + types.add("VerifiableCredential"); + types.add(type); + + java.util.List context = new java.util.ArrayList<>(); + context.add("https://www.w3.org/ns/credentials/v2"); + context.add("https://www.w3.org/ns/credentials/examples/v2"); + + java.util.Map vc = new java.util.LinkedHashMap<>(); + vc.put("@context", context); + vc.put("type", types); + vc.put("issuer", issuer.did); + vc.put("validFrom", from); + if (validUntil != null) { + vc.put("validUntil", validUntil); + } + vc.put("credentialSubject", subject); + vc.put("credential", boundCredential); + + return vc; + } + + public String issueCredential(java.util.Map credential) { + return issueCredential(credential, null); + } + + public String issueCredential(java.util.Map credential, IssueCredentialOptions options) { + if (credential == null) { + throw new IllegalArgumentException("credential is required"); + } + + java.util.Map bound = credential; + if (options != null && options.schema != null && options.subject != null) { + bound = bindCredential( + options.schema, + options.subject, + options.validFrom, + options.validUntil, + credential + ); + } + + Object issuer = bound.get("issuer"); + IDInfo current = fetchIdInfo(null); + if (issuer == null || !issuer.equals(current.did)) { + throw new IllegalArgumentException("credential.issuer"); + } + + java.util.Map signed = addSignatureInternal(bound, null); + Object subjectObj = signed.get("credentialSubject"); + if (!(subjectObj instanceof java.util.Map)) { + throw new IllegalArgumentException("credential.credentialSubject.id"); + } + + @SuppressWarnings("unchecked") + java.util.Map subject = (java.util.Map) subjectObj; + Object subjectId = subject.get("id"); + if (!(subjectId instanceof String) || ((String) subjectId).isBlank()) { + throw new IllegalArgumentException("credential.credentialSubject.id"); + } + + String targetRegistry = options != null ? options.registry : null; + String targetValidUntil = options != null ? options.validUntil : null; + boolean encryptForSender = options == null || options.encryptForSender == null || options.encryptForSender; + return encryptJsonInternal(signed, (String) subjectId, true, targetRegistry, targetValidUntil, encryptForSender); + } + + public java.util.Map getCredential(String id) { + if (id == null || id.isBlank()) { + throw new IllegalArgumentException("id is required"); + } + String did = lookupDID(id); + Object vc = decryptJSON(did); + if (!isVerifiableCredential(vc)) { + return null; + } + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) vc; + return map; + } + + public boolean removeCredential(String id) { + String did = lookupDID(id); + return removeFromHeld(did); + } + + public java.util.List listCredentials(String id) { + IDInfo idInfo = fetchIdInfo(id); + if (idInfo.held == null) { + return new java.util.ArrayList<>(); + } + return new java.util.ArrayList<>(idInfo.held); + } + + public boolean acceptCredential(String id) { + try { + IDInfo current = fetchIdInfo(null); + String did = lookupDID(id); + Object vc = decryptJSON(did); + if (isVerifiableCredential(vc)) { + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) vc; + Object subjectObj = map.get("credentialSubject"); + if (subjectObj instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map subject = (java.util.Map) subjectObj; + Object subjectId = subject.get("id"); + if (subjectId instanceof String && !current.did.equals(subjectId)) { + return false; + } + } + } + return addToHeld(did); + } catch (Exception e) { + return false; + } + } + + public java.util.List listIssued(String issuer) { + IDInfo id = fetchIdInfo(issuer); + java.util.List issued = new java.util.ArrayList<>(); + + if (id.owned != null) { + for (String did : id.owned) { + try { + Object credential = decryptJSON(did); + if (isVerifiableCredential(credential)) { + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) credential; + if (id.did != null && id.did.equals(map.get("issuer"))) { + issued.add(did); + } + } + } catch (Exception ignored) { + // Skip any malformed or non-credential assets. + } + } + } + + return issued; + } + + public boolean updateCredential(String did, java.util.Map credential) { + String credentialDid = lookupDID(did); + Object original = decryptJSON(credentialDid); + if (!isVerifiableCredential(original)) { + throw new IllegalArgumentException("did is not a credential"); + } + if (credential == null || + credential.get("credential") == null || + credential.get("credentialSubject") == null) { + throw new IllegalArgumentException("credential"); + } + + Object subjectObj = credential.get("credentialSubject"); + if (!(subjectObj instanceof java.util.Map)) { + throw new IllegalArgumentException("credential"); + } + @SuppressWarnings("unchecked") + java.util.Map subject = (java.util.Map) subjectObj; + Object subjectId = subject.get("id"); + if (!(subjectId instanceof String) || ((String) subjectId).isBlank()) { + throw new IllegalArgumentException("credential"); + } + + java.util.Map unsigned = new java.util.LinkedHashMap<>(credential); + unsigned.remove("signature"); + java.util.Map signed = addSignatureInternal(unsigned, null); + + String msg; + try { + msg = WalletJsonMapper.mapper().writeValueAsString(signed); + } catch (Exception e) { + throw new IllegalArgumentException("credential"); + } + + IDInfo sender = fetchIdInfo(null); + JwkPair senderKeypair = fetchKeyPair(null); + if (senderKeypair == null) { + throw new IllegalArgumentException("no valid sender keypair"); + } + + ResolveDIDOptions options = new ResolveDIDOptions(); + options.confirm = true; + MdipDocument holderDoc = gatekeeper.resolveDID((String) subjectId, options); + EcdsaJwkPublic receivePublicJwk = getPublicKeyJwk(holderDoc); + org.keychain.crypto.JwkPublic receiverCrypto = new org.keychain.crypto.JwkPublic( + receivePublicJwk.kty, + receivePublicJwk.crv, + receivePublicJwk.x, + receivePublicJwk.y + ); + + String cipherSender = crypto.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg); + String cipherReceiver = crypto.encryptMessage(receiverCrypto, senderKeypair.privateJwk, msg); + String msgHash = crypto.hashMessage(msg); + + MdipDocument doc = resolveDID(credentialDid); + java.util.Map encrypted = new java.util.LinkedHashMap<>(); + encrypted.put("sender", sender.did); + encrypted.put("created", nowIso()); + encrypted.put("cipher_hash", msgHash); + encrypted.put("cipher_sender", cipherSender); + encrypted.put("cipher_receiver", cipherReceiver); + + java.util.Map payload = new java.util.LinkedHashMap<>(); + payload.put("encrypted", encrypted); + doc.didDocumentData = payload; + return updateDID(doc); + } + + public boolean revokeCredential(String credential) { + return revokeDID(credential); + } + + public java.util.Map publishCredential(String did) { + return publishCredential(did, false); + } + + public java.util.Map publishCredential(String did, PublishCredentialOptions options) { + boolean reveal = options != null && Boolean.TRUE.equals(options.reveal); + return publishCredential(did, reveal); + } + + public java.util.Map publishCredential(String did, boolean reveal) { + IDInfo id = fetchIdInfo(null); + String credentialDid = lookupDID(did); + Object vcObj = decryptJSON(credentialDid); + if (!isVerifiableCredential(vcObj)) { + throw new IllegalArgumentException("did is not a credential"); + } + + @SuppressWarnings("unchecked") + java.util.Map vc = (java.util.Map) vcObj; + Object subjectObj = vc.get("credentialSubject"); + if (subjectObj instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map subject = (java.util.Map) subjectObj; + Object subjectId = subject.get("id"); + if (!(subjectId instanceof String) || !id.did.equals(subjectId)) { + throw new IllegalArgumentException("only subject can publish a credential"); + } + } + + MdipDocument doc = resolveDID(id.did); + if (!(doc.didDocumentData instanceof java.util.Map)) { + doc.didDocumentData = new java.util.LinkedHashMap<>(); + } + + @SuppressWarnings("unchecked") + java.util.Map data = (java.util.Map) doc.didDocumentData; + Object manifestObj = data.get("manifest"); + java.util.Map manifest; + if (manifestObj instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map manifestMap = (java.util.Map) manifestObj; + manifest = manifestMap; + } else { + manifest = new java.util.LinkedHashMap<>(); + } + + java.util.Map stored = vc; + if (!reveal) { + stored = new java.util.LinkedHashMap<>(vc); + stored.put("credential", null); + } + + manifest.put(credentialDid, stored); + data.put("manifest", manifest); + doc.didDocumentData = data; + + boolean ok = updateDID(doc); + if (!ok) { + throw new IllegalStateException("update DID failed"); + } + return stored; + } + + public String unpublishCredential(String did) { + IDInfo id = fetchIdInfo(null); + MdipDocument doc = resolveDID(id.did); + String credentialDid = lookupDID(did); + + if (doc.didDocumentData instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map data = (java.util.Map) doc.didDocumentData; + Object manifestObj = data.get("manifest"); + if (manifestObj instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map manifest = (java.util.Map) manifestObj; + if (manifest.containsKey(credentialDid)) { + manifest.remove(credentialDid); + data.put("manifest", manifest); + doc.didDocumentData = data; + updateDID(doc); + return "OK credential " + did + " removed from manifest"; + } + } + } + + throw new IllegalArgumentException("did"); + } + + public MdipDocument resolveDID(String did) { + return resolveDID(did, null); + } + + public MdipDocument resolveDID(String did, ResolveDIDOptions options) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + String actualDid = lookupDID(did); + MdipDocument doc = gatekeeper.resolveDID(actualDid, options); + if (doc != null && doc.didResolutionMetadata != null && doc.didResolutionMetadata.error != null) { + String error = doc.didResolutionMetadata.error; + if ("notFound".equals(error)) { + throw new IllegalArgumentException("unknown"); + } + if ("invalidDid".equals(error)) { + throw new IllegalArgumentException("bad format"); + } + throw new IllegalArgumentException("did"); + } + return augmentDidMetadata(doc); + } + + private MdipDocument augmentDidMetadata(MdipDocument doc) { + if (doc != null) { + String controller = null; + if (doc.didDocument != null) { + controller = doc.didDocument.controller != null ? doc.didDocument.controller : doc.didDocument.id; + } + DocumentMetadata metadata = doc.didDocumentMetadata != null ? doc.didDocumentMetadata : new DocumentMetadata(); + metadata.isOwned = controller != null && idInWallet(controller); + doc.didDocumentMetadata = metadata; + } + return doc; + } + + public boolean testAgent(String id) { + MdipDocument doc = resolveDID(id); + return doc != null && doc.mdip != null && "agent".equals(doc.mdip.type); + } + + public boolean rotateKeys() { + final boolean[] ok = {false}; + mutateWallet(wallet -> { + IDInfo id = getCurrentIdInfo(wallet); + int nextIndex = id.index + 1; + + DeterministicKey master = walletManager.getHdKeyFromCacheOrMnemonic(wallet); + DeterministicKey derived = HdKeyUtil.derivePath(master, id.account, nextIndex); + JwkPair keypair = crypto.generateJwk(HdKeyUtil.privateKeyBytes(derived)); + + MdipDocument doc = resolveDID(id.did); + if (doc.didDocumentMetadata == null || !Boolean.TRUE.equals(doc.didDocumentMetadata.confirmed)) { + throw new IllegalStateException("Keymaster: Cannot rotate keys"); + } + if (doc.didDocument == null || doc.didDocument.verificationMethod == null) { + throw new IllegalStateException("DID Document missing verificationMethod"); + } + + MdipDocument.VerificationMethod method = doc.didDocument.verificationMethod.get(0); + method.id = "#key-" + (nextIndex + 1); + method.publicKeyJwk = JwkConverter.toEcdsaJwkPublic(keypair.publicJwk); + doc.didDocument.authentication = java.util.List.of(method.id); + + ok[0] = updateDID(doc); + if (!ok[0]) { + throw new IllegalStateException("Cannot rotate keys"); + } + + id.index = nextIndex; + }); + + return ok[0]; + } + + public boolean verifySignature(java.util.Map obj) { + if (obj == null) { + return false; + } + Object signatureObj = obj.get("signature"); + if (!(signatureObj instanceof java.util.Map)) { + return false; + } + @SuppressWarnings("unchecked") + java.util.Map signature = (java.util.Map) signatureObj; + Object signerObj = signature.get("signer"); + if (!(signerObj instanceof String) || ((String) signerObj).isBlank()) { + return false; + } + + java.util.Map copy = new java.util.LinkedHashMap<>(obj); + copy.remove("signature"); + String msgHash = crypto.hashJson(copy); + + Object hashObj = signature.get("hash"); + if (hashObj instanceof String && !msgHash.equals(hashObj)) { + return false; + } + + Object signedObj = signature.get("signed"); + ResolveDIDOptions options = null; + if (signedObj instanceof String) { + options = new ResolveDIDOptions(); + options.versionTime = (String) signedObj; + } + MdipDocument doc = resolveDID((String) signerObj, options); + EcdsaJwkPublic publicJwk = getPublicKeyJwk(doc); + org.keychain.crypto.JwkPublic cryptoJwk = new org.keychain.crypto.JwkPublic( + publicJwk.kty, + publicJwk.crv, + publicJwk.x, + publicJwk.y + ); + + try { + return crypto.verifySig(msgHash, (String) signature.get("value"), cryptoJwk); + } catch (Exception e) { + return false; + } + } + + private MdipDocument resolveWithRetries(String did, int retries, int delayMs) { + while (retries >= 0) { + try { + return resolveDID(did); + } catch (Exception e) { + if (retries == 0) { + throw e; + } + retries -= 1; + try { + Thread.sleep(delayMs); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("interrupted"); + } + } + } + return null; + } + + private String findMatchingCredential(java.util.Map credential) { + IDInfo id = fetchIdInfo(null); + if (id.held == null) { + return null; + } + + for (String did : id.held) { + try { + Object docObj = decryptJSON(did); + if (!isVerifiableCredential(docObj)) { + continue; + } + @SuppressWarnings("unchecked") + java.util.Map doc = (java.util.Map) docObj; + Object subjectObj = doc.get("credentialSubject"); + if (!(subjectObj instanceof java.util.Map)) { + continue; + } + @SuppressWarnings("unchecked") + java.util.Map subject = (java.util.Map) subjectObj; + Object subjectId = subject.get("id"); + if (!(subjectId instanceof String) || !id.did.equals(subjectId)) { + continue; + } + + Object issuersObj = credential.get("issuers"); + if (issuersObj instanceof java.util.List) { + Object issuerObj = doc.get("issuer"); + if (issuerObj instanceof String && !((java.util.List) issuersObj).contains(issuerObj)) { + continue; + } + } + + Object schemaObj = credential.get("schema"); + Object typeObj = doc.get("type"); + if (schemaObj instanceof String && typeObj instanceof java.util.List) { + if (!((java.util.List) typeObj).contains(schemaObj)) { + continue; + } + } + + return did; + } catch (Exception ignored) { + // Not encrypted or not a VC. + } + } + + return null; + } + + public String lookupDID(String nameOrDid) { + if (nameOrDid == null || nameOrDid.isBlank()) { + throw new IllegalArgumentException("name or did is required"); + } + if (nameOrDid.startsWith("did:")) { + return nameOrDid; + } + + WalletFile wallet = loadWallet(); + if (wallet != null && wallet.names != null && wallet.names.containsKey(nameOrDid)) { + return wallet.names.get(nameOrDid); + } + if (wallet != null && wallet.ids != null && wallet.ids.containsKey(nameOrDid)) { + return wallet.ids.get(nameOrDid).did; + } + + throw new IllegalArgumentException("unknown id"); + } + + public Object resolveAsset(String did) { + try { + MdipDocument doc = resolveDID(did); + if (doc == null || doc.mdip == null || !"asset".equals(doc.mdip.type)) { + return new java.util.LinkedHashMap(); + } + if (doc.didDocumentMetadata != null && Boolean.TRUE.equals(doc.didDocumentMetadata.deactivated)) { + return new java.util.LinkedHashMap(); + } + if (doc.didDocument == null || doc.didDocument.controller == null || doc.didDocument.controller.isBlank()) { + return new java.util.LinkedHashMap(); + } + return Objects.requireNonNullElseGet(doc.didDocumentData, LinkedHashMap::new); + } catch (IllegalArgumentException e) { + String msg = e.getMessage(); + if ("unknown id".equals(msg) || "bad format".equals(msg) || "unknown".equals(msg)) { + throw e; + } + return new java.util.LinkedHashMap(); + } catch (Exception e) { + return new java.util.LinkedHashMap(); + } + } + + public boolean updateAsset(String did, java.util.Map data) { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + if (data == null) { + throw new IllegalArgumentException("data is required"); + } + + MdipDocument doc = resolveDID(did); + java.util.Map merged = new java.util.HashMap<>(); + Object currentData = doc.didDocumentData; + if (currentData instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map current = (java.util.Map) currentData; + merged.putAll(current); + } + for (java.util.Map.Entry entry : data.entrySet()) { + if (entry.getValue() == null) { + merged.remove(entry.getKey()); + } else { + merged.put(entry.getKey(), entry.getValue()); + } + } + doc.didDocumentData = merged; + return updateDID(doc); + } + + public boolean transferAsset(String assetDid, String controllerDid) { + if (assetDid == null || assetDid.isBlank()) { + throw new IllegalArgumentException("asset did is required"); + } + if (controllerDid == null || controllerDid.isBlank()) { + throw new IllegalArgumentException("controller did is required"); + } + + String resolvedAssetDid = lookupDID(assetDid); + MdipDocument assetDoc = resolveDID(resolvedAssetDid); + if (assetDoc.mdip == null || !"asset".equals(assetDoc.mdip.type)) { + throw new IllegalArgumentException("asset did is not an asset"); + } + if (assetDoc.didDocument == null || assetDoc.didDocument.id == null) { + throw new IllegalArgumentException("asset didDocument is missing"); + } + + MdipDocument controllerDoc = resolveDID(controllerDid); + if (controllerDoc.mdip == null || !"agent".equals(controllerDoc.mdip.type)) { + throw new IllegalArgumentException("controller did is not an agent"); + } + String resolvedControllerDid = controllerDoc.didDocument != null && controllerDoc.didDocument.id != null + ? controllerDoc.didDocument.id + : lookupDID(controllerDid); + + String prevOwner = assetDoc.didDocument.controller; + if (resolvedControllerDid.equals(prevOwner)) { + return true; + } + + assetDoc.didDocument.controller = resolvedControllerDid; + + boolean ok = updateDID(assetDoc); + if (ok && prevOwner != null) { + removeFromOwned(resolvedAssetDid, prevOwner); + try { + addToOwned(resolvedAssetDid, resolvedControllerDid); + } catch (IllegalArgumentException ignored) { + // controller not in wallet + } + } + + return ok; + } + + public java.util.List listAssets(String ownerDid) { + IDInfo idInfo = fetchIdInfo(ownerDid); + return idInfo.owned != null ? idInfo.owned : java.util.Collections.emptyList(); + } + + public java.util.List listAssets() { + return listAssets(null); + } + + private static boolean validateSchema(Object schema) { + try { + generateSchema(schema); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + private static java.util.Map generateSchema(Object schema) { + if (!(schema instanceof java.util.Map)) { + throw new IllegalArgumentException("schema"); + } + + @SuppressWarnings("unchecked") + java.util.Map schemaMap = (java.util.Map) schema; + Object propsObj = schemaMap.get("properties"); + if (schemaMap.get("$schema") == null || propsObj == null) { + throw new IllegalArgumentException("schema"); + } + if (!(propsObj instanceof java.util.Map)) { + throw new IllegalArgumentException("schema"); + } + + @SuppressWarnings("unchecked") + java.util.Map props = (java.util.Map) propsObj; + java.util.Map template = new java.util.LinkedHashMap<>(); + for (String key : props.keySet()) { + template.put(key, "TBD"); + } + return template; + } + + private static java.util.Map defaultSchema() { + java.util.Map root = new java.util.LinkedHashMap<>(); + root.put("$schema", "http://json-schema.org/draft-07/schema#"); + root.put("type", "object"); + + java.util.Map properties = new java.util.LinkedHashMap<>(); + java.util.Map propertyName = new java.util.LinkedHashMap<>(); + propertyName.put("type", "string"); + properties.put("propertyName", propertyName); + root.put("properties", properties); + + java.util.List required = new java.util.ArrayList<>(); + required.add("propertyName"); + root.put("required", required); + return root; + } + + private static String nowIso() { + return ISO_MILLIS.format(NOW_SUPPLIER.get()); + } + + public java.util.Map addSignature(java.util.Map obj) { + return addSignatureInternal(obj, null); + } + + public java.util.Map addSignature( + java.util.Map obj, + String controllerDid + ) { + return addSignatureInternal(obj, controllerDid); + } + + private java.util.Map addSignatureInternal( + java.util.Map obj, + String controllerDid + ) { + if (obj == null) { + throw new IllegalArgumentException("obj"); + } + + IDInfo id = fetchIdInfo(controllerDid); + JwkPair keypair = fetchKeyPair(controllerDid); + if (keypair == null) { + throw new IllegalArgumentException("addSignature: no keypair"); + } + + java.util.Map unsigned = new java.util.LinkedHashMap<>(obj); + unsigned.remove("signature"); + + String msgHash = crypto.hashJson(unsigned); + String signatureValue = crypto.signHash(msgHash, keypair.privateJwk); + + java.util.Map signature = new java.util.LinkedHashMap<>(); + signature.put("signer", id.did); + signature.put("signed", nowIso()); + signature.put("hash", msgHash); + signature.put("value", signatureValue); + + java.util.Map signed = new java.util.LinkedHashMap<>(unsigned); + signed.put("signature", signature); + return signed; + } + + public String validateName(String name) { + return validateNameInternal(name, null); + } + + private static String validateNameInternal(String name, WalletFile wallet) { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Invalid parameter: name must be a non-empty string"); + } + String trimmed = name.trim(); + if (trimmed.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("Invalid parameter: name too long"); + } + for (int i = 0; i < trimmed.length(); i += 1) { + if (Character.isISOControl(trimmed.charAt(i))) { + throw new IllegalArgumentException("Invalid parameter: name contains unprintable characters"); + } + } + if (wallet != null) { + if (wallet.names != null && wallet.names.containsKey(trimmed)) { + throw new IllegalArgumentException("Invalid parameter: name already used"); + } + if (wallet.ids != null && wallet.ids.containsKey(trimmed)) { + throw new IllegalArgumentException("Invalid parameter: name already used"); + } + } + return trimmed; + } + + private static String normalizeRegistry(String registry) { + if (registry == null || registry.isBlank()) { + return DEFAULT_REGISTRY; + } + return registry; + } + + private void tallyDid(String did, CheckWalletResult result) { + result.checked += 1; + if (!isValidDID(did)) { + result.invalid += 1; + return; + } + try { + MdipDocument doc = resolveDID(did); + if (doc == null) { + result.invalid += 1; + return; + } + if (doc.didDocumentMetadata != null && Boolean.TRUE.equals(doc.didDocumentMetadata.deactivated)) { + result.deleted += 1; + } + } catch (Exception e) { + result.invalid += 1; + } + } + + private boolean shouldRemoveDid(String did) { + if (!isValidDID(did)) { + return true; + } + try { + MdipDocument doc = resolveDID(did); + if (doc == null) { + return true; + } + return doc.didDocumentMetadata != null && Boolean.TRUE.equals(doc.didDocumentMetadata.deactivated); + } catch (Exception e) { + return true; + } + } + + private String encryptJsonInternal(Object json, String receiverDid, boolean includeHash) { + return encryptJsonInternal(json, receiverDid, includeHash, null, null, true); + } + + private String encryptJsonInternal( + Object json, + String receiverDid, + boolean includeHash, + String registry, + String validUntil + ) { + return encryptJsonInternal(json, receiverDid, includeHash, registry, validUntil, true); + } + + private String encryptJsonInternal( + Object json, + String receiverDid, + boolean includeHash, + String registry, + String validUntil, + boolean encryptForSender + ) { + String plaintext; + try { + plaintext = WalletJsonMapper.mapper().writeValueAsString(json); + } catch (Exception e) { + throw new IllegalArgumentException("json"); + } + return encryptMessage(plaintext, receiverDid, includeHash, registry, validUntil, encryptForSender); + } + + public String encryptMessage(String msg, String receiverDid) { + return encryptMessage(msg, receiverDid, (EncryptOptions) null); + } + + public String encryptMessage(String msg, String receiverDid, boolean includeHash) { + EncryptOptions options = new EncryptOptions(); + options.includeHash = includeHash; + return encryptMessage(msg, receiverDid, options); + } + + public String encryptMessage(String msg, String receiverDid, EncryptOptions options) { + EncryptOptions effective = options != null ? options : new EncryptOptions(); + boolean includeHash = effective.includeHash != null && effective.includeHash; + boolean encryptForSender = effective.encryptForSender == null || effective.encryptForSender; + return encryptMessage( + msg, + receiverDid, + includeHash, + effective.registry, + effective.validUntil, + encryptForSender + ); + } + + private String encryptMessage( + String msg, + String receiverDid, + boolean includeHash, + String registry, + String validUntil, + boolean encryptForSender + ) { + if (receiverDid == null || receiverDid.isBlank()) { + throw new IllegalArgumentException("receiver did is required"); + } + String resolvedReceiver = lookupDID(receiverDid); + IDInfo sender = fetchIdInfo(null); + JwkPair senderKeypair = fetchKeyPair(null); + if (senderKeypair == null) { + throw new IllegalArgumentException("no valid sender keypair"); + } + + ResolveDIDOptions options = new ResolveDIDOptions(); + options.confirm = true; + MdipDocument doc = gatekeeper.resolveDID(resolvedReceiver, options); + EcdsaJwkPublic receiverJwk = getPublicKeyJwk(doc); + org.keychain.crypto.JwkPublic receiverCrypto = new org.keychain.crypto.JwkPublic( + receiverJwk.kty, + receiverJwk.crv, + receiverJwk.x, + receiverJwk.y + ); + + String cipherSender = encryptForSender + ? crypto.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg) + : null; + String cipherReceiver = crypto.encryptMessage(receiverCrypto, senderKeypair.privateJwk, msg); + String cipherHash = includeHash ? crypto.hashMessage(msg) : null; + + java.util.Map encrypted = new java.util.LinkedHashMap<>(); + encrypted.put("sender", sender.did); + encrypted.put("created", nowIso()); + if (cipherHash != null) { + encrypted.put("cipher_hash", cipherHash); + } + encrypted.put("cipher_sender", cipherSender); + encrypted.put("cipher_receiver", cipherReceiver); + + java.util.Map payload = new java.util.LinkedHashMap<>(); + payload.put("encrypted", encrypted); + String targetRegistry = registry == null || registry.isBlank() ? defaultRegistry : registry; + return createAsset(payload, targetRegistry, null, validUntil); + } + + public String encryptJSON(Object json, String receiverDid) { + return encryptJSON(json, receiverDid, (EncryptOptions) null); + } + + public String encryptJSON(Object json, String receiverDid, boolean includeHash) { + EncryptOptions options = new EncryptOptions(); + options.includeHash = includeHash; + return encryptJSON(json, receiverDid, options); + } + + public String encryptJSON(Object json, String receiverDid, EncryptOptions options) { + EncryptOptions effective = options != null ? options : new EncryptOptions(); + boolean includeHash = effective.includeHash != null && effective.includeHash; + boolean encryptForSender = effective.encryptForSender == null || effective.encryptForSender; + return encryptJsonInternal( + json, + receiverDid, + includeHash, + effective.registry, + effective.validUntil, + encryptForSender + ); + } + + public Object decryptJSON(String did) { + String plaintext = decryptMessage(did); + try { + return WalletJsonMapper.mapper().readValue(plaintext, Object.class); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid parameter: did not encrypted JSON"); + } + } + + public String decryptMessage(String did) { + WalletFile wallet = loadWallet(); + IDInfo id = fetchIdInfo(null, wallet); + Object assetObj = resolveAsset(did); + if (!(assetObj instanceof java.util.Map)) { + throw new IllegalArgumentException("Invalid parameter: did not encrypted"); + } + @SuppressWarnings("unchecked") + java.util.Map data = (java.util.Map) assetObj; + Object nested = data.get("encrypted"); + java.util.Map encrypted; + if (nested instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map nestedMap = (java.util.Map) nested; + encrypted = nestedMap; + } else { + encrypted = data; + } + + Object senderObj = encrypted.get("sender"); + Object createdObj = encrypted.get("created"); + Object senderCipherObj = encrypted.get("cipher_sender"); + Object receiverCipherObj = encrypted.get("cipher_receiver"); + if (!(senderObj instanceof String) || !(createdObj instanceof String)) { + throw new IllegalArgumentException("Invalid parameter: did not encrypted"); + } + String senderDid = (String) senderObj; + String created = (String) createdObj; + String cipherSender = senderCipherObj instanceof String ? (String) senderCipherObj : null; + String cipherReceiver = receiverCipherObj instanceof String ? (String) receiverCipherObj : null; + if (cipherSender == null && cipherReceiver == null) { + throw new IllegalArgumentException("Invalid parameter: did not encrypted"); + } + + ResolveDIDOptions options = new ResolveDIDOptions(); + options.confirm = true; + options.versionTime = created; + MdipDocument senderDoc = gatekeeper.resolveDID(senderDid, options); + EcdsaJwkPublic senderPublicJwk = getPublicKeyJwk(senderDoc); + org.keychain.crypto.JwkPublic senderCrypto = new org.keychain.crypto.JwkPublic( + senderPublicJwk.kty, + senderPublicJwk.crv, + senderPublicJwk.x, + senderPublicJwk.y + ); + + String ciphertext = senderDid.equals(id.did) && cipherSender != null ? cipherSender : cipherReceiver; + if (ciphertext == null) { + throw new IllegalArgumentException("Invalid parameter: did not encrypted"); + } + return decryptWithDerivedKeys(wallet, id, senderCrypto, ciphertext); + } + + private String decryptWithDerivedKeys( + WalletFile wallet, + IDInfo id, + org.keychain.crypto.JwkPublic senderPublicJwk, + String ciphertext + ) { + DeterministicKey master = walletManager.getHdKeyFromCacheOrMnemonic(wallet); + for (int index = id.index; index >= 0; index -= 1) { + DeterministicKey derived = HdKeyUtil.derivePath(master, id.account, index); + JwkPair receiver = crypto.generateJwk(HdKeyUtil.privateKeyBytes(derived)); + try { + return crypto.decryptMessage(senderPublicJwk, receiver.privateJwk, ciphertext); + } catch (Exception ignored) { + // try older keys + } + } + throw new IllegalArgumentException("ID can't decrypt ciphertext"); + } + + private static boolean isVerifiableCredential(Object obj) { + if (!(obj instanceof java.util.Map)) { + return false; + } + @SuppressWarnings("unchecked") + java.util.Map map = (java.util.Map) obj; + Object context = map.get("@context"); + Object type = map.get("type"); + Object issuer = map.get("issuer"); + Object subject = map.get("credentialSubject"); + return context instanceof java.util.List && + type instanceof java.util.List && + issuer != null && + subject != null; + } + + public boolean addToOwned(String did, String ownerDid) { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + mutateWallet(wallet -> { + IDInfo idInfo = fetchIdInfo(ownerDid, wallet); + if (idInfo.owned == null) { + idInfo.owned = new java.util.ArrayList<>(); + } + if (!idInfo.owned.contains(did)) { + idInfo.owned.add(did); + } + }); + return true; + } + + public boolean addToHeld(String did) { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + mutateWallet(wallet -> { + IDInfo idInfo = getCurrentIdInfo(wallet); + if (idInfo.held == null) { + idInfo.held = new java.util.ArrayList<>(); + } + if (!idInfo.held.contains(did)) { + idInfo.held.add(did); + } + }); + return true; + } + + public boolean removeFromHeld(String did) { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + final boolean[] changed = {false}; + mutateWallet(wallet -> { + IDInfo idInfo = getCurrentIdInfo(wallet); + if (idInfo.held == null) { + return; + } + if (idInfo.held.removeIf(did::equals)) { + changed[0] = true; + } + }); + return changed[0]; + } + + public boolean removeFromOwned(String did, String ownerDid) { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + final boolean[] ownerFound = {false}; + mutateWallet(wallet -> { + try { + IDInfo idInfo = fetchIdInfo(ownerDid, wallet); + if (idInfo.owned == null) { + return; + } + ownerFound[0] = true; + idInfo.owned.removeIf(item -> did.equals(item)); + } catch (IllegalArgumentException ignored) { + ownerFound[0] = false; + } + }); + return ownerFound[0]; + } + + public IDInfo fetchIdInfo(String nameOrDid) { + return fetchIdInfo(nameOrDid, null); + } + + public boolean idInWallet(String did) { + try { + fetchIdInfo(did); + return true; + } catch (Exception e) { + return false; + } + } + + public IDInfo fetchIdInfo(String nameOrDid, WalletFile wallet) { + WalletFile currentWallet = wallet != null ? wallet : loadWallet(); + if (currentWallet == null) { + throw new IllegalStateException("wallet not loaded"); + } + + IDInfo idInfo = null; + if (nameOrDid == null || nameOrDid.isBlank()) { + if (currentWallet.current == null || currentWallet.current.isBlank()) { + throw new IllegalStateException("Keymaster: No current ID"); + } + idInfo = currentWallet.ids != null ? currentWallet.ids.get(currentWallet.current) : null; + } else if (nameOrDid.startsWith("did")) { + if (currentWallet.ids != null) { + for (IDInfo info : currentWallet.ids.values()) { + if (didMatch(nameOrDid, info.did)) { + idInfo = info; + break; + } + } + } + } else { + idInfo = currentWallet.ids != null ? currentWallet.ids.get(nameOrDid) : null; + } + + if (idInfo == null) { + throw new IllegalArgumentException("unknown id"); + } + + return idInfo; + } + + public JwkPair fetchKeyPair(String nameOrDid) { + WalletFile wallet = loadWallet(); + IDInfo id = fetchIdInfo(nameOrDid, wallet); + DeterministicKey master = walletManager.getHdKeyFromCacheOrMnemonic(wallet); + + if (gatekeeper == null) { + DeterministicKey derived = HdKeyUtil.derivePath(master, id.account, id.index); + return crypto.generateJwk(HdKeyUtil.privateKeyBytes(derived)); + } + + ResolveDIDOptions options = new ResolveDIDOptions(); + options.confirm = true; + MdipDocument doc = gatekeeper.resolveDID(id.did, options); + EcdsaJwkPublic confirmed = getPublicKeyJwk(doc); + + for (int i = id.index; i >= 0; i -= 1) { + DeterministicKey derived = HdKeyUtil.derivePath(master, id.account, i); + JwkPair keypair = crypto.generateJwk(HdKeyUtil.privateKeyBytes(derived)); + if (confirmed != null && confirmed.x != null && confirmed.y != null) { + if (confirmed.x.equals(keypair.publicJwk.x) && confirmed.y.equals(keypair.publicJwk.y)) { + return keypair; + } + } + } + + return null; + } + + public boolean updateDID(MdipDocument doc) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + if (doc == null || doc.didDocument == null || doc.didDocument.id == null || doc.didDocument.id.isBlank()) { + throw new IllegalArgumentException("doc.didDocument.id is required"); + } + + String did = doc.didDocument.id; + MdipDocument current = resolveDID(did); + String previd = current != null && current.didDocumentMetadata != null ? current.didDocumentMetadata.versionId : null; + String registry = current != null && current.mdip != null ? current.mdip.registry : null; + + if (current != null) { + MdipDocument currentCopy = WalletJsonMapper.mapper().convertValue(current, MdipDocument.class); + MdipDocument docCopy = WalletJsonMapper.mapper().convertValue(doc, MdipDocument.class); + currentCopy.didDocumentMetadata = null; + currentCopy.didResolutionMetadata = null; + docCopy.didDocumentMetadata = null; + docCopy.didResolutionMetadata = null; + String currentHash = crypto.hashJson(currentCopy); + String updateHash = crypto.hashJson(docCopy); + if (currentHash.equals(updateHash)) { + return true; + } + } + + BlockInfo block = gatekeeper.getBlock(registry); + String blockid = block != null ? block.hash : null; + + String signerDid = current != null && current.didDocument != null + ? (current.didDocument.controller != null ? current.didDocument.controller : current.didDocument.id) + : did; + + JwkPair keypair = fetchKeyPair(signerDid); + if (keypair == null) { + throw new IllegalStateException("addSignature: no keypair"); + } + Operation signed = operationFactory.createSignedUpdateDidOperation( + did, + previd, + blockid, + doc, + keypair.privateJwk, + signerDid + ); + + return gatekeeper.updateDID(signed); + } + + public boolean deleteDID(String did) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + + MdipDocument current = resolveDID(did); + String previd = current != null && current.didDocumentMetadata != null ? current.didDocumentMetadata.versionId : null; + String registry = current != null && current.mdip != null ? current.mdip.registry : null; + + BlockInfo block = gatekeeper.getBlock(registry); + String blockid = block != null ? block.hash : null; + + Operation operation = new Operation(); + operation.type = "delete"; + operation.did = did; + operation.previd = previd; + operation.blockid = blockid; + + String signerDid = current != null && current.didDocument != null + ? (current.didDocument.controller != null ? current.didDocument.controller : current.didDocument.id) + : did; + + JwkPair keypair = fetchKeyPair(signerDid); + if (keypair == null) { + throw new IllegalStateException("addSignature: no keypair"); + } + Operation signed = new OperationSignerImpl(crypto).sign(operation, keypair.privateJwk, signerDid); + return gatekeeper.deleteDID(signed); + } + + public boolean revokeDID(String id) { + if (id == null || id.isBlank()) { + throw new IllegalArgumentException("id is required"); + } + String did = lookupDID(id); + MdipDocument current = resolveDID(did); + String controller = current != null && current.didDocument != null ? current.didDocument.controller : null; + + boolean ok = deleteDID(did); + if (ok && controller != null && !controller.isBlank()) { + removeFromOwned(did, controller); + } + return ok; + } + + public BlockInfo getBlock(String registry) { + if (gatekeeper == null) { + throw new IllegalStateException("gatekeeper not configured"); + } + if (registry == null || registry.isBlank()) { + throw new IllegalArgumentException("registry is required"); + } + return gatekeeper.getBlock(registry); + } + + IDInfo getCurrentIdInfo(WalletFile wallet) { + if (wallet == null) { + throw new IllegalStateException("wallet not loaded"); + } + if (wallet.current == null || wallet.current.isBlank()) { + throw new IllegalStateException("Keymaster: No current ID"); + } + if (wallet.ids == null || !wallet.ids.containsKey(wallet.current)) { + throw new IllegalStateException("current id not found in wallet"); + } + return wallet.ids.get(wallet.current); + } + + JwkPair getCurrentKeypair(WalletFile wallet) { + IDInfo id = getCurrentIdInfo(wallet); + return getCurrentKeypairFromPath(wallet, id.account, id.index); + } + + private JwkPair getCurrentKeypairFromPath(WalletFile wallet, int account, int index) { + DeterministicKey master = walletManager.getHdKeyFromCacheOrMnemonic(wallet); + DeterministicKey derived = HdKeyUtil.derivePath(master, account, index); + return crypto.generateJwk(HdKeyUtil.privateKeyBytes(derived)); + } + + public JwkPair hdKeyPair() { + WalletFile wallet = loadWallet(); + DeterministicKey master = walletManager.getHdKeyFromCacheOrMnemonic(wallet); + return crypto.generateJwk(HdKeyUtil.privateKeyBytes(master)); + } + + private static void replaceWallet(WalletFile target, WalletFile source) { + target.version = source.version; + target.seed = source.seed; + target.counter = source.counter; + target.ids = source.ids; + target.current = source.current; + target.names = source.names; + target.extras = source.extras; + } + + private static boolean didMatch(String did1, String did2) { + if (did1 == null || did2 == null) { + return false; + } + String suffix1 = did1.substring(did1.lastIndexOf(':') + 1); + String suffix2 = did2.substring(did2.lastIndexOf(':') + 1); + return suffix1.equals(suffix2); + } + + private void validateValidUntil(String validUntil) { + if (validUntil == null) { + return; + } + if (validUntil.isBlank()) { + throw new IllegalArgumentException("options.validUntil"); + } + try { + Instant.parse(validUntil); + return; + } catch (java.time.format.DateTimeParseException ignored) { + // fall through + } + try { + java.time.LocalDate.parse(validUntil); + } catch (java.time.format.DateTimeParseException ignored) { + throw new IllegalArgumentException("options.validUntil"); + } + } + + private static boolean isValidDID(String did) { + if (did == null || !did.startsWith("did:")) { + return false; + } + String[] parts = did.split(":"); + if (parts.length < 3) { + return false; + } + String suffix = parts[parts.length - 1]; + if (suffix == null || suffix.isBlank()) { + return false; + } + return Cid.isValid(suffix); + } + + public EcdsaJwkPublic getPublicKeyJwk(MdipDocument doc) { + if (doc == null || doc.didDocument == null || doc.didDocument.verificationMethod == null) { + throw new IllegalArgumentException("Missing didDocument."); + } + if (doc.didDocument.verificationMethod.isEmpty()) { + throw new IllegalArgumentException("The DID document does not contain any verification methods."); + } + EcdsaJwkPublic publicKeyJwk = doc.didDocument.verificationMethod.get(0).publicKeyJwk; + if (publicKeyJwk == null) { + throw new IllegalArgumentException("The publicKeyJwk is missing in the first verification method."); + } + return publicKeyJwk; + } + + public String getAgentDID(MdipDocument doc) { + if (doc == null || doc.mdip == null || !"agent".equals(doc.mdip.type)) { + throw new IllegalArgumentException("Document is not an agent"); + } + if (doc.didDocument == null || doc.didDocument.id == null || doc.didDocument.id.isBlank()) { + throw new IllegalArgumentException("Agent document does not have a DID"); + } + return doc.didDocument.id; + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/KeymasterWalletManager.java b/java/keymaster/src/main/java/org/keychain/keymaster/KeymasterWalletManager.java new file mode 100644 index 000000000..6b5297969 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/KeymasterWalletManager.java @@ -0,0 +1,318 @@ +package org.keychain.keymaster; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import org.bitcoinj.crypto.DeterministicKey; +import org.keychain.crypto.HdKeyUtil; +import org.keychain.crypto.KeymasterCrypto; +import org.keychain.crypto.MnemonicEncryption; +import org.keychain.keymaster.model.Seed; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.model.WalletFile; +import org.keychain.keymaster.store.WalletJsonMapper; +import org.keychain.keymaster.store.WalletStore; + +public class KeymasterWalletManager { + private final WalletStore store; + private final KeymasterCrypto crypto; + private final ObjectMapper mapper; + private final String passphrase; + private final ReentrantLock writeLock = new ReentrantLock(); + private WalletFile walletCache; + private DeterministicKey hdkeyCache; + + public KeymasterWalletManager( + WalletStore store, + KeymasterCrypto crypto, + String passphrase + ) { + this.store = store; + this.crypto = crypto; + this.mapper = WalletJsonMapper.mapper(); + this.passphrase = passphrase; + } + + public WalletFile loadWallet() { + if (walletCache != null) { + return walletCache; + } + + WalletEncFile stored = store.loadWallet(); + if (stored == null) { + return null; + } + + WalletFile wallet; + if (isLegacyEncrypted(stored)) { + wallet = decryptLegacyEncrypted(stored); + wallet = upgradeWallet(wallet); + store.saveWallet(encryptWalletForStorage(wallet), true); + } else if (isLegacyV0(stored)) { + wallet = upgradeWallet(toWalletFile(stored)); + store.saveWallet(encryptWalletForStorage(wallet), true); + } else if (isV1WithEnc(stored)) { + wallet = decryptWalletFromStorage(stored); + } else if (isV1Decrypted(stored)) { + wallet = toWalletFile(stored); + } else { + throw new IllegalStateException("Keymaster: Unsupported wallet version."); + } + + walletCache = wallet; + return walletCache; + } + + public boolean saveWallet(WalletFile wallet, boolean overwrite) { + WalletFile upgraded = upgradeWallet(wallet); + WalletEncFile stored = encryptWalletForStorage(upgraded); + boolean ok = store.saveWallet(stored, overwrite); + if (ok) { + walletCache = upgraded; + } + return ok; + } + + boolean saveStoredWallet(WalletEncFile stored, boolean overwrite) { + boolean ok = store.saveWallet(stored, overwrite); + if (ok) { + walletCache = decryptWalletFromStorage(stored); + } + return ok; + } + + void decryptStoredWallet(WalletEncFile stored) { + decryptWalletFromStorage(stored); + } + + public boolean mutateWallet(Consumer mutator) { + writeLock.lock(); + try { + WalletFile wallet = loadWallet(); + if (wallet == null) { + throw new IllegalStateException("No wallet loaded"); + } + + WalletFile working = deepCopyWallet(wallet); + String before = toJson(mapper, walletToMap(working)); + mutator.accept(working); + String after = toJson(mapper, walletToMap(working)); + + if (Objects.equals(before, after)) { + return false; + } + + boolean ok = saveWallet(working, true); + if (ok) { + walletCache = working; + } + return ok; + } finally { + writeLock.unlock(); + } + } + + DeterministicKey getHdKeyFromCacheOrMnemonic(WalletFile wallet) { + if (hdkeyCache != null) { + return hdkeyCache; + } + + String mnemonic = MnemonicEncryption.decrypt(wallet.seed.mnemonicEnc, passphrase); + hdkeyCache = HdKeyUtil.masterFromMnemonic(mnemonic); + return hdkeyCache; + } + + WalletFile upgradeWallet(WalletFile wallet) { + if (wallet == null) { + throw new IllegalArgumentException("wallet is required"); + } + + if (wallet.version != null && wallet.version == 1 && wallet.seed != null && wallet.seed.mnemonicEnc != null) { + return wallet; + } + + boolean legacy = (wallet.version == null || wallet.version == 0) + && wallet.seed != null + && wallet.seed.hdkey != null + && wallet.seed.mnemonic != null; + + if (legacy) { + DeterministicKey key = HdKeyUtil.fromXpriv(wallet.seed.hdkey.xpriv); + var jwk = crypto.generateJwk(HdKeyUtil.privateKeyBytes(key)); + String mnemonic = crypto.decryptMessage(jwk.publicJwk, jwk.privateJwk, wallet.seed.mnemonic); + + Seed seed = new Seed(); + seed.mnemonicEnc = MnemonicEncryption.encrypt(mnemonic, passphrase); + + WalletFile upgraded = new WalletFile(); + upgraded.version = 1; + upgraded.seed = seed; + upgraded.counter = wallet.counter; + upgraded.ids = wallet.ids != null ? wallet.ids : new HashMap<>(); + upgraded.current = wallet.current; + upgraded.names = wallet.names; + upgraded.extras = wallet.extras; + + hdkeyCache = HdKeyUtil.masterFromMnemonic(mnemonic); + return upgraded; + } + + throw new IllegalStateException("Keymaster: Unsupported wallet version."); + } + + private WalletEncFile encryptWalletForStorage(WalletFile wallet) { + if (wallet == null || wallet.seed == null || wallet.seed.mnemonicEnc == null) { + throw new IllegalArgumentException("wallet.seed.mnemonicEnc is required"); + } + + Seed safeSeed = new Seed(); + safeSeed.mnemonicEnc = wallet.seed.mnemonicEnc; + + String plaintext = toJson(mapper, walletToMap(wallet)); + DeterministicKey master = getHdKeyFromCacheOrMnemonic(wallet); + var jwk = crypto.generateJwk(HdKeyUtil.privateKeyBytes(master)); + String enc = crypto.encryptMessage(jwk.publicJwk, jwk.privateJwk, plaintext); + + WalletEncFile stored = new WalletEncFile(); + stored.version = wallet.version != null ? wallet.version : 1; + stored.seed = safeSeed; + stored.enc = enc; + return stored; + } + + private WalletFile decryptWalletFromStorage(WalletEncFile stored) { + if (stored == null || stored.seed == null || stored.seed.mnemonicEnc == null) { + throw new IllegalArgumentException("stored.seed.mnemonicEnc is required"); + } + + WalletFile wallet = new WalletFile(); + wallet.version = stored.version; + wallet.seed = stored.seed; + + DeterministicKey master = getHdKeyFromCacheOrMnemonic(wallet); + var jwk = crypto.generateJwk(HdKeyUtil.privateKeyBytes(master)); + String plaintext = crypto.decryptMessage(jwk.publicJwk, jwk.privateJwk, stored.enc); + + Map data = fromJson(mapper, plaintext); + data.put("version", stored.version); + data.put("seed", stored.seed); + + return mapper.convertValue(data, WalletFile.class); + } + + private boolean isLegacyEncrypted(WalletEncFile stored) { + return stored != null && stored.salt != null && stored.iv != null && stored.data != null; + } + + private boolean isLegacyV0(WalletEncFile stored) { + return stored != null + && (stored.version == 0) + && stored.seed != null + && stored.seed.hdkey != null + && stored.seed.mnemonic != null; + } + + private boolean isV1WithEnc(WalletEncFile stored) { + return stored != null + && stored.version == 1 + && stored.enc != null + && stored.seed != null + && stored.seed.mnemonicEnc != null; + } + + private boolean isV1Decrypted(WalletEncFile stored) { + return stored != null + && stored.version == 1 + && stored.enc == null + && stored.seed != null + && stored.seed.mnemonicEnc != null; + } + + private WalletFile decryptLegacyEncrypted(WalletEncFile stored) { + if (passphrase == null || passphrase.isBlank()) { + throw new IllegalStateException("KC_ENCRYPTED_PASSPHRASE not set"); + } + + try { + byte[] salt = Base64.getDecoder().decode(stored.salt); + byte[] iv = Base64.getDecoder().decode(stored.iv); + byte[] combined = Base64.getDecoder().decode(stored.data); + + SecretKey key = deriveLegacyKey(passphrase, salt); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + byte[] plaintext = cipher.doFinal(combined); + String json = new String(plaintext, StandardCharsets.UTF_8); + return mapper.readValue(json, WalletFile.class); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Incorrect passphrase."); + } catch (Exception e) { + throw new IllegalStateException("Failed to parse legacy wallet", e); + } + } + + private static SecretKey deriveLegacyKey(String passphrase, byte[] salt) throws GeneralSecurityException { + PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, 100_000, 256); + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + byte[] keyBytes = factory.generateSecret(spec).getEncoded(); + return new SecretKeySpec(keyBytes, "AES"); + } + + private WalletFile toWalletFile(WalletEncFile stored) { + Map data = new HashMap<>(); + if (stored.extra != null) { + data.putAll(stored.extra); + } + data.put("version", stored.version); + if (stored.seed != null) { + data.put("seed", stored.seed); + } + return mapper.convertValue(data, WalletFile.class); + } + + private static Map walletToMap(WalletFile wallet) { + ObjectMapper mapper = WalletJsonMapper.mapper(); + Map data = mapper.convertValue(wallet, new TypeReference<>() {}); + data.remove("version"); + data.remove("seed"); + return data; + } + + private static String toJson(ObjectMapper mapper, Map data) { + try { + return mapper.writeValueAsString(data); + } catch (Exception e) { + throw new IllegalStateException("Failed to serialize wallet", e); + } + } + + private static Map fromJson(ObjectMapper mapper, String data) { + try { + return mapper.readValue(data, new TypeReference<>() {}); + } catch (Exception e) { + throw new IllegalStateException("Failed to parse wallet", e); + } + } + + private WalletFile deepCopyWallet(WalletFile wallet) { + try { + String json = mapper.writeValueAsString(wallet); + return mapper.readValue(json, WalletFile.class); + } catch (Exception e) { + throw new IllegalStateException("Failed to clone wallet", e); + } + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/OperationBuilder.java b/java/keymaster/src/main/java/org/keychain/keymaster/OperationBuilder.java new file mode 100644 index 000000000..4dbc0069d --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/OperationBuilder.java @@ -0,0 +1,111 @@ +package org.keychain.keymaster; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Objects; +import org.keychain.gatekeeper.model.EcdsaJwkPublic; +import org.keychain.gatekeeper.model.Mdip; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.gatekeeper.model.Operation; + +public class OperationBuilder { + private static final DateTimeFormatter ISO_MILLIS = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC); + private final Clock clock; + + public OperationBuilder() { + this(Clock.systemUTC()); + } + + public OperationBuilder(Clock clock) { + this.clock = Objects.requireNonNull(clock, "clock is required"); + } + + public Operation createIdOperation(String registry, EcdsaJwkPublic publicJwk, String blockid) { + if (registry == null || registry.isBlank()) { + throw new IllegalArgumentException("registry is required"); + } + if (publicJwk == null) { + throw new IllegalArgumentException("publicJwk is required"); + } + + Mdip mdip = new Mdip(); + mdip.version = 1; + mdip.type = "agent"; + mdip.registry = registry; + + Operation operation = new Operation(); + operation.type = "create"; + operation.created = nowIso(); + operation.blockid = blockid; + operation.mdip = mdip; + operation.publicJwk = publicJwk; + return operation; + } + + public Operation createAssetOperation( + String registry, + String controller, + Object data, + String blockid, + String validUntil + ) { + if (registry == null || registry.isBlank()) { + throw new IllegalArgumentException("registry is required"); + } + if (controller == null || controller.isBlank()) { + throw new IllegalArgumentException("controller is required"); + } + if (data == null) { + throw new IllegalArgumentException("data is required"); + } + + Mdip mdip = new Mdip(); + mdip.version = 1; + mdip.type = "asset"; + mdip.registry = registry; + mdip.validUntil = validUntil; + + Operation operation = new Operation(); + operation.type = "create"; + operation.created = nowIso(); + operation.blockid = blockid; + operation.mdip = mdip; + operation.controller = controller; + operation.data = data; + return operation; + } + + public Operation updateDidOperation(String did, String previd, String blockid, MdipDocument doc) { + if (did == null || did.isBlank()) { + throw new IllegalArgumentException("did is required"); + } + if (doc == null) { + throw new IllegalArgumentException("doc is required"); + } + + Operation operation = new Operation(); + operation.type = "update"; + operation.did = did; + operation.previd = previd; + operation.blockid = blockid; + operation.doc = stripMetadata(doc); + return operation; + } + + private String nowIso() { + return ISO_MILLIS.format(Instant.now(clock)); + } + + private static MdipDocument stripMetadata(MdipDocument doc) { + MdipDocument copy = new MdipDocument(); + copy.didDocument = doc.didDocument; + copy.didDocumentData = doc.didDocumentData; + copy.mdip = doc.mdip; + copy.didDocumentMetadata = null; + copy.didResolutionMetadata = null; + return copy; + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/OperationFactory.java b/java/keymaster/src/main/java/org/keychain/keymaster/OperationFactory.java new file mode 100644 index 000000000..a89d93037 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/OperationFactory.java @@ -0,0 +1,57 @@ +package org.keychain.keymaster; + +import java.util.Objects; +import org.keychain.crypto.JwkPrivate; +import org.keychain.crypto.KeymasterCrypto; +import org.keychain.gatekeeper.model.EcdsaJwkPublic; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.gatekeeper.model.Operation; + +public class OperationFactory { + private final OperationBuilder builder; + private final OperationSigner signer; + + public OperationFactory(KeymasterCrypto crypto) { + this(new OperationBuilder(), new OperationSignerImpl(crypto)); + } + + public OperationFactory(OperationBuilder builder, OperationSigner signer) { + this.builder = Objects.requireNonNull(builder, "builder is required"); + this.signer = Objects.requireNonNull(signer, "signer is required"); + } + + public Operation createSignedCreateIdOperation( + String registry, + EcdsaJwkPublic publicJwk, + JwkPrivate privateJwk, + String blockid + ) { + Operation operation = builder.createIdOperation(registry, publicJwk, blockid); + return signer.signCreate(operation, privateJwk); + } + + public Operation createSignedCreateAssetOperation( + String registry, + String controller, + Object data, + String blockid, + String validUntil, + JwkPrivate privateJwk, + String signerDid + ) { + Operation operation = builder.createAssetOperation(registry, controller, data, blockid, validUntil); + return signer.sign(operation, privateJwk, signerDid); + } + + public Operation createSignedUpdateDidOperation( + String did, + String previd, + String blockid, + MdipDocument doc, + JwkPrivate privateJwk, + String signerDid + ) { + Operation operation = builder.updateDidOperation(did, previd, blockid, doc); + return signer.sign(operation, privateJwk, signerDid); + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/OperationSigner.java b/java/keymaster/src/main/java/org/keychain/keymaster/OperationSigner.java new file mode 100644 index 000000000..83d6f5a6c --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/OperationSigner.java @@ -0,0 +1,10 @@ +package org.keychain.keymaster; + +import org.keychain.crypto.JwkPrivate; +import org.keychain.gatekeeper.model.Operation; + +public interface OperationSigner { + Operation sign(Operation operation, JwkPrivate privateJwk, String signerDid); + + Operation signCreate(Operation operation, JwkPrivate privateJwk); +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/OperationSignerImpl.java b/java/keymaster/src/main/java/org/keychain/keymaster/OperationSignerImpl.java new file mode 100644 index 000000000..ff054ddfd --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/OperationSignerImpl.java @@ -0,0 +1,101 @@ +package org.keychain.keymaster; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Objects; +import org.keychain.crypto.JwkPrivate; +import org.keychain.crypto.KeymasterCrypto; +import org.keychain.gatekeeper.model.Operation; +import org.keychain.gatekeeper.model.Signature; + +public class OperationSignerImpl implements OperationSigner { + private static final DateTimeFormatter ISO_MILLIS = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC); + private final KeymasterCrypto crypto; + private final Clock clock; + + public OperationSignerImpl(KeymasterCrypto crypto) { + this(crypto, Clock.systemUTC()); + } + + public OperationSignerImpl(KeymasterCrypto crypto, Clock clock) { + this.crypto = Objects.requireNonNull(crypto, "crypto is required"); + this.clock = Objects.requireNonNull(clock, "clock is required"); + } + + @Override + public Operation sign(Operation operation, JwkPrivate privateJwk, String signerDid) { + if (operation == null) { + throw new IllegalArgumentException("operation is required"); + } + if (privateJwk == null) { + throw new IllegalArgumentException("privateJwk is required"); + } + if (signerDid == null || signerDid.isBlank()) { + throw new IllegalArgumentException("signerDid is required"); + } + + String msgHash = crypto.hashJson(copyWithoutSignature(operation)); + String signatureValue = crypto.signHash(msgHash, privateJwk); + + Signature signature = new Signature(); + signature.signer = signerDid; + signature.signed = nowIso(); + signature.hash = msgHash; + signature.value = signatureValue; + + Operation signed = copyOperation(operation); + signed.signature = signature; + return signed; + } + + @Override + public Operation signCreate(Operation operation, JwkPrivate privateJwk) { + if (operation == null) { + throw new IllegalArgumentException("operation is required"); + } + if (privateJwk == null) { + throw new IllegalArgumentException("privateJwk is required"); + } + + String msgHash = crypto.hashJson(copyWithoutSignature(operation)); + String signatureValue = crypto.signHash(msgHash, privateJwk); + + Signature signature = new Signature(); + signature.signed = nowIso(); + signature.hash = msgHash; + signature.value = signatureValue; + + Operation signed = copyOperation(operation); + signed.signature = signature; + return signed; + } + + private String nowIso() { + return ISO_MILLIS.format(Instant.now(clock)); + } + + private static Operation copyWithoutSignature(Operation operation) { + Operation copy = copyOperation(operation); + copy.signature = null; + return copy; + } + + private static Operation copyOperation(Operation operation) { + Operation copy = new Operation(); + copy.type = operation.type; + copy.created = operation.created; + copy.blockid = operation.blockid; + copy.did = operation.did; + copy.previd = operation.previd; + copy.mdip = operation.mdip; + copy.controller = operation.controller; + copy.data = operation.data; + copy.doc = operation.doc; + copy.publicJwk = operation.publicJwk; + copy.signature = operation.signature; + return copy; + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/PublishCredentialOptions.java b/java/keymaster/src/main/java/org/keychain/keymaster/PublishCredentialOptions.java new file mode 100644 index 000000000..bdd6e84f3 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/PublishCredentialOptions.java @@ -0,0 +1,7 @@ +package org.keychain.keymaster; + +public class PublishCredentialOptions { + public Boolean reveal; + + public PublishCredentialOptions() {} +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/WalletCrypto.java b/java/keymaster/src/main/java/org/keychain/keymaster/WalletCrypto.java new file mode 100644 index 000000000..bafa2d1a0 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/WalletCrypto.java @@ -0,0 +1,61 @@ +package org.keychain.keymaster; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import org.keychain.crypto.KeymasterCrypto; +import org.keychain.crypto.HdKeyUtil; +import org.keychain.crypto.MnemonicEncryption; +import org.keychain.keymaster.model.Seed; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.model.WalletFile; +import org.keychain.keymaster.store.WalletJsonMapper; + +public class WalletCrypto { + private final KeymasterCrypto crypto; + private final ObjectMapper mapper; + private final String passphrase; + + public WalletCrypto(KeymasterCrypto crypto, String passphrase) { + this.crypto = crypto; + this.mapper = WalletJsonMapper.mapper(); + this.passphrase = passphrase; + } + + public WalletEncFile encryptForStorage(WalletFile wallet) { + if (wallet == null || wallet.seed == null || wallet.seed.mnemonicEnc == null) { + throw new IllegalArgumentException("wallet.seed.mnemonicEnc is required"); + } + + Seed safeSeed = new Seed(); + safeSeed.mnemonicEnc = wallet.seed.mnemonicEnc; + + String plaintext = toJson(mapper, walletToMap(wallet)); + String mnemonic = MnemonicEncryption.decrypt(wallet.seed.mnemonicEnc, passphrase); + var master = HdKeyUtil.masterFromMnemonic(mnemonic); + var jwk = crypto.generateJwk(HdKeyUtil.privateKeyBytes(master)); + String enc = crypto.encryptMessage(jwk.publicJwk, jwk.privateJwk, plaintext); + + WalletEncFile stored = new WalletEncFile(); + stored.version = wallet.version != null ? wallet.version : 1; + stored.seed = safeSeed; + stored.enc = enc; + return stored; + } + + private static Map walletToMap(WalletFile wallet) { + ObjectMapper mapper = WalletJsonMapper.mapper(); + Map data = mapper.convertValue(wallet, new TypeReference<>() {}); + data.remove("version"); + data.remove("seed"); + return data; + } + + private static String toJson(ObjectMapper mapper, Map data) { + try { + return mapper.writeValueAsString(data); + } catch (Exception e) { + throw new IllegalStateException("Failed to serialize wallet", e); + } + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/model/CheckWalletResult.java b/java/keymaster/src/main/java/org/keychain/keymaster/model/CheckWalletResult.java new file mode 100644 index 000000000..f8a0ed3d9 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/model/CheckWalletResult.java @@ -0,0 +1,7 @@ +package org.keychain.keymaster.model; + +public class CheckWalletResult { + public int checked; + public int invalid; + public int deleted; +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/model/FixWalletResult.java b/java/keymaster/src/main/java/org/keychain/keymaster/model/FixWalletResult.java new file mode 100644 index 000000000..40e6e8891 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/model/FixWalletResult.java @@ -0,0 +1,8 @@ +package org.keychain.keymaster.model; + +public class FixWalletResult { + public int idsRemoved; + public int ownedRemoved; + public int heldRemoved; + public int namesRemoved; +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/model/Group.java b/java/keymaster/src/main/java/org/keychain/keymaster/model/Group.java new file mode 100644 index 000000000..c012be3b5 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/model/Group.java @@ -0,0 +1,10 @@ +package org.keychain.keymaster.model; + +import java.util.List; + +public class Group { + public String name; + public List members; + + public Group() {} +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/model/HdKey.java b/java/keymaster/src/main/java/org/keychain/keymaster/model/HdKey.java new file mode 100644 index 000000000..aaf18ea72 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/model/HdKey.java @@ -0,0 +1,6 @@ +package org.keychain.keymaster.model; + +public class HdKey { + public String xpriv; + public String xpub; +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/model/IDInfo.java b/java/keymaster/src/main/java/org/keychain/keymaster/model/IDInfo.java new file mode 100644 index 000000000..882b831a6 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/model/IDInfo.java @@ -0,0 +1,13 @@ +package org.keychain.keymaster.model; + +import java.util.List; +import java.util.Map; + +public class IDInfo { + public String did; + public int account; + public int index; + public List held; + public List owned; + public Map extras; +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/model/Seed.java b/java/keymaster/src/main/java/org/keychain/keymaster/model/Seed.java new file mode 100644 index 000000000..944fb91a8 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/model/Seed.java @@ -0,0 +1,11 @@ +package org.keychain.keymaster.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.keychain.crypto.EncryptedMnemonic; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Seed { + public String mnemonic; + public HdKey hdkey; + public EncryptedMnemonic mnemonicEnc; +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/model/WalletEncFile.java b/java/keymaster/src/main/java/org/keychain/keymaster/model/WalletEncFile.java new file mode 100644 index 000000000..b7d45a707 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/model/WalletEncFile.java @@ -0,0 +1,29 @@ +package org.keychain.keymaster.model; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.HashMap; +import java.util.Map; + +public class WalletEncFile { + public int version; + public Seed seed; + public String enc; + public String salt; + public String iv; + public String data; + + @JsonIgnore + public Map extra = new HashMap<>(); + + @JsonAnySetter + public void addExtra(String key, Object value) { + extra.put(key, value); + } + + @JsonAnyGetter + public Map getExtra() { + return extra; + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/model/WalletFile.java b/java/keymaster/src/main/java/org/keychain/keymaster/model/WalletFile.java new file mode 100644 index 000000000..0c2fdcf7b --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/model/WalletFile.java @@ -0,0 +1,13 @@ +package org.keychain.keymaster.model; + +import java.util.Map; + +public class WalletFile { + public Integer version; + public Seed seed; + public int counter; + public Map ids; + public String current; + public Map names; + public Map extras; +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJson.java b/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJson.java new file mode 100644 index 000000000..e862634c5 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJson.java @@ -0,0 +1,62 @@ +package org.keychain.keymaster.store; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +public class WalletJson implements WalletStore { + private final ObjectMapper mapper; + private final Class type; + private final Path dataFolder; + private final Path walletPath; + + public WalletJson(Class type, Path dataFolder, String walletFileName) { + this.mapper = WalletJsonMapper.mapper(); + this.type = type; + this.dataFolder = dataFolder; + this.walletPath = dataFolder.resolve(walletFileName); + } + + @Override + public boolean saveWallet(T wallet, boolean overwrite) { + if (Files.exists(walletPath) && !overwrite) { + return false; + } + + try { + if (!Files.exists(dataFolder)) { + Files.createDirectories(dataFolder); + } + + String json = mapper.writeValueAsString(wallet); + Path tmp = Files.createTempFile(dataFolder, "wallet", ".tmp"); + Files.writeString(tmp, json); + try { + Files.move(tmp, walletPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp, walletPath, StandardCopyOption.REPLACE_EXISTING); + } + return true; + } catch (IOException e) { + throw new IllegalStateException("Failed to save wallet", e); + } + } + + @Override + public T loadWallet() { + if (!Files.exists(walletPath)) { + return null; + } + + try { + String json = Files.readString(walletPath); + return mapper.readValue(json, type); + } catch (IOException e) { + throw new IllegalStateException("Failed to load wallet", e); + } + } + +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJsonMapper.java b/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJsonMapper.java new file mode 100644 index 000000000..d6b96b46a --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJsonMapper.java @@ -0,0 +1,15 @@ +package org.keychain.keymaster.store; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public final class WalletJsonMapper { + private static final ObjectMapper MAPPER = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + private WalletJsonMapper() {} + + public static ObjectMapper mapper() { + return MAPPER; + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJsonMemory.java b/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJsonMemory.java new file mode 100644 index 000000000..11de8d6a1 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletJsonMemory.java @@ -0,0 +1,42 @@ +package org.keychain.keymaster.store; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class WalletJsonMemory implements WalletStore { + private final ObjectMapper mapper; + private final Class type; + private String walletCache; + + public WalletJsonMemory(Class type) { + this.mapper = WalletJsonMapper.mapper(); + this.type = type; + } + + @Override + public boolean saveWallet(T wallet, boolean overwrite) { + if (walletCache != null && !overwrite) { + return false; + } + + try { + walletCache = mapper.writeValueAsString(wallet); + return true; + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to serialize wallet", e); + } + } + + @Override + public T loadWallet() { + if (walletCache == null) { + return null; + } + + try { + return mapper.readValue(walletCache, type); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Failed to parse wallet", e); + } + } +} diff --git a/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletStore.java b/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletStore.java new file mode 100644 index 000000000..eaafa0cc5 --- /dev/null +++ b/java/keymaster/src/main/java/org/keychain/keymaster/store/WalletStore.java @@ -0,0 +1,6 @@ +package org.keychain.keymaster.store; + +public interface WalletStore { + boolean saveWallet(T wallet, boolean overwrite); + T loadWallet(); +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveAssetTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveAssetTest.java new file mode 100644 index 000000000..2befa5e57 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveAssetTest.java @@ -0,0 +1,430 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.testutil.LiveTestSupport; + +@Tag("live") +class LiveAssetTest { + @TempDir + private Path tempDir; + + private Keymaster newKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + @Test + void createAssetFromObjectAnchor() { + Keymaster keymaster = newKeymaster(); + String ownerDid = keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertEquals(dataDid, doc.didDocument.id); + assertEquals(ownerDid, doc.didDocument.controller); + assertEquals(mockAnchor, doc.didDocumentData); + } + + @Test + void createAssetFromStringAnchor() { + Keymaster keymaster = newKeymaster(); + String ownerDid = keymaster.createId("Bob"); + String mockAnchor = "mockAnchor"; + String dataDid = keymaster.createAsset(mockAnchor); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertEquals(dataDid, doc.didDocument.id); + assertEquals(ownerDid, doc.didDocument.controller); + assertEquals(mockAnchor, doc.didDocumentData); + } + + @Test + void createAssetFromListAnchor() { + Keymaster keymaster = newKeymaster(); + String ownerDid = keymaster.createId("Bob"); + List mockAnchor = List.of(1, 2, 3); + String dataDid = keymaster.createAsset(mockAnchor); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertEquals(dataDid, doc.didDocument.id); + assertEquals(ownerDid, doc.didDocument.controller); + assertEquals(mockAnchor, doc.didDocumentData); + } + + @Test + void createAssetForDifferentValidId() { + Keymaster keymaster = newKeymaster(); + String ownerDid = keymaster.createId("Bob"); + String mockAnchor = "mockAnchor"; + + keymaster.createId("Alice"); + + CreateAssetOptions options = new CreateAssetOptions(); + options.registry = LiveTestSupport.DEFAULT_REGISTRY; + options.controller = "Bob"; + String dataDid = keymaster.createAsset(mockAnchor, options); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertEquals(dataDid, doc.didDocument.id); + assertEquals(ownerDid, doc.didDocument.controller); + assertEquals(mockAnchor, doc.didDocumentData); + } + + @Test + void createAssetWithSpecifiedName() { + Keymaster keymaster = newKeymaster(); + String ownerDid = keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String mockName = "mockName"; + + CreateAssetOptions options = new CreateAssetOptions(); + options.name = mockName; + String dataDid = keymaster.createAsset(mockAnchor, options); + MdipDocument doc = keymaster.resolveDID(mockName); + + assertEquals(dataDid, doc.didDocument.id); + assertEquals(ownerDid, doc.didDocument.controller); + assertEquals(mockAnchor, doc.didDocumentData); + } + + @Test + void createAssetThrowsWhenNoCurrentId() { + Keymaster keymaster = newKeymaster(); + Map mockAnchor = Map.of("name", "mockAnchor"); + + IllegalStateException error = assertThrows(IllegalStateException.class, () -> + keymaster.createAsset(mockAnchor) + ); + assertEquals("Keymaster: No current ID", error.getMessage()); + } + + @Test + void createAssetThrowsWhenNameAlreadyUsed() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + CreateAssetOptions options = new CreateAssetOptions(); + options.name = "Bob"; + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.createAsset(Map.of(), options) + ); + assertEquals("Invalid parameter: name already used", error.getMessage()); + } + + @Test + void createAssetThrowsOnInvalidData() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.createAsset(null) + ); + assertEquals("data is required", error.getMessage()); + } + + @Test + void cloneAssetDid() { + Keymaster keymaster = newKeymaster(); + String ownerDid = keymaster.createId("Bob"); + Map mockData = Map.of("name", "mockData"); + String assetDid = keymaster.createAsset(mockData); + String cloneDid = keymaster.cloneAsset(assetDid); + MdipDocument doc = keymaster.resolveDID(cloneDid); + + assertNotEquals(assetDid, cloneDid); + assertEquals(ownerDid, doc.didDocument.controller); + + Map expectedData = Map.of("name", "mockData", "cloned", assetDid); + assertEquals(expectedData, doc.didDocumentData); + } + + @Test + void cloneAssetName() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockData = Map.of("name", "mockData"); + String assetDid = keymaster.createAsset(mockData); + keymaster.addName("asset", assetDid); + String cloneDid = keymaster.cloneAsset("asset"); + MdipDocument doc = keymaster.resolveDID(cloneDid); + + Map expectedData = Map.of("name", "mockData", "cloned", assetDid); + assertEquals(expectedData, doc.didDocumentData); + } + + @Test + void cloneEmptyAsset() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String assetDid = keymaster.createAsset(Map.of()); + keymaster.addName("asset", assetDid); + String cloneDid = keymaster.cloneAsset("asset"); + MdipDocument doc = keymaster.resolveDID(cloneDid); + + Map expectedData = Map.of("cloned", assetDid); + assertEquals(expectedData, doc.didDocumentData); + } + + @Test + void cloneAClone() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockData = Map.of("name", "mockData"); + String assetDid = keymaster.createAsset(mockData); + String cloneDid1 = keymaster.cloneAsset(assetDid); + String cloneDid2 = keymaster.cloneAsset(cloneDid1); + MdipDocument doc = keymaster.resolveDID(cloneDid2); + + Map expectedData = Map.of("name", "mockData", "cloned", cloneDid1); + assertEquals(expectedData, doc.didDocumentData); + } + + @Test + void cloneAssetThrowsOnInvalidAsset() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.cloneAsset(bob) + ); + assertEquals("id", error.getMessage()); + } + + @Test + void transferAssetDidToAgentDid() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + + assertTrue(keymaster.transferAsset(dataDid, alice)); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertEquals(alice, doc.didDocument.controller); + + List assetsAlice = keymaster.listAssets("Alice"); + List assetsBob = keymaster.listAssets("Bob"); + + assertEquals(List.of(dataDid), assetsAlice); + assertEquals(List.of(), assetsBob); + } + + @Test + void transferAssetNameToAgentName() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + keymaster.addName("asset", dataDid); + + assertTrue(keymaster.transferAsset("asset", "Alice")); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertEquals(alice, doc.didDocument.controller); + + List assetsAlice = keymaster.listAssets("Alice"); + List assetsBob = keymaster.listAssets("Bob"); + + assertEquals(List.of(dataDid), assetsAlice); + assertEquals(List.of(), assetsBob); + } + + @Test + void transferAssetNoOpWhenControllerUnchanged() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + + assertTrue(keymaster.transferAsset(dataDid, bob)); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertEquals(bob, doc.didDocument.controller); + assertEquals("1", doc.didDocumentMetadata.version); + } + + @Test + void transferAssetThrowsOnInvalidDid() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.transferAsset("mockDID", bob) + ); + assertEquals("unknown id", error.getMessage()); + } + + @Test + void transferAssetThrowsWhenDidIsAgent() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.transferAsset(bob, bob) + ); + assertEquals("asset did is not an asset", error.getMessage()); + } + + @Test + void transferAssetThrowsOnInvalidController() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.transferAsset(dataDid, dataDid) + ); + assertEquals("controller did is not an agent", error.getMessage()); + } + + @Test + void transferAssetThrowsWhenAssetNotOwned() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> { + keymaster.removeId(bob); + keymaster.transferAsset(dataDid, alice); + }); + assertEquals("unknown id", error.getMessage()); + } + + @Test + void listAssetsEmpty() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + List assets = keymaster.listAssets(); + assertEquals(List.of(), assets); + } + + @Test + void listAssetsOwnedByCurrentId() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + + List assets = keymaster.listAssets(); + assertEquals(List.of(dataDid), assets); + } + + @Test + void listAssetsOwnedBySpecifiedId() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + + keymaster.createId("Bob"); + List assetsBob = keymaster.listAssets(); + List assetsAlice = keymaster.listAssets("Alice"); + + assertEquals(List.of(), assetsBob); + assertEquals(List.of(dataDid), assetsAlice); + } + + @Test + void listAssetsExcludesEphemeralAssets() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String validUntil = java.time.Instant.now().plusSeconds(60).toString(); + + CreateAssetOptions options = new CreateAssetOptions(); + options.validUntil = validUntil; + keymaster.createAsset(mockAnchor, options); + + List assets = keymaster.listAssets(); + assertEquals(List.of(), assets); + } + + @Test + void resolveAssetReturnsData() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAsset = Map.of("name", "mockAnchor"); + String did = keymaster.createAsset(mockAsset); + + Object asset = keymaster.resolveAsset(did); + assertEquals(mockAsset, asset); + } + + @Test + void resolveAssetReturnsEmptyOnInvalidDid() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Bob"); + + Object asset = keymaster.resolveAsset(agentDid); + assertEquals(Map.of(), asset); + } + + @Test + void resolveAssetReturnsEmptyWhenRevoked() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAsset = Map.of("name", "mockAnchor"); + String did = keymaster.createAsset(mockAsset); + keymaster.revokeDID(did); + + Object asset = keymaster.resolveAsset(did); + assertEquals(Map.of(), asset); + } + + @Test + void updateAssetReplacesData() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAsset1 = Map.of("name", "original"); + Map mockAsset2 = Map.of("name", "updated"); + String did = keymaster.createAsset(mockAsset1); + assertTrue(keymaster.updateAsset(did, mockAsset2)); + Object asset = keymaster.resolveAsset(did); + + assertEquals(mockAsset2, asset); + } + + @Test + void updateAssetMergesData() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAsset1 = Map.of("key1", "val1"); + Map mockAsset2 = Map.of("key2", "val2"); + String did = keymaster.createAsset(mockAsset1); + assertTrue(keymaster.updateAsset(did, mockAsset2)); + Object asset = keymaster.resolveAsset(did); + + assertEquals(Map.of("key1", "val1", "key2", "val2"), asset); + } + + @Test + void updateAssetRemovesUndefinedProperty() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAsset1 = Map.of("key1", "val1", "key2", "val2"); + Map mockAsset2 = new java.util.HashMap<>(); + mockAsset2.put("key2", null); + String did = keymaster.createAsset(mockAsset1); + assertTrue(keymaster.updateAsset(did, mockAsset2)); + Object asset = keymaster.resolveAsset(did); + + assertEquals(Map.of("key1", "val1"), asset); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveChallengeTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveChallengeTest.java new file mode 100644 index 000000000..ad0094183 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveChallengeTest.java @@ -0,0 +1,96 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.testutil.LiveTestSupport; +import org.keychain.keymaster.testutil.TestFixtures; + +@Tag("live") +class LiveChallengeTest { + @TempDir + Path tempDir; + + private Keymaster newKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + @Test + void createChallengeEmptyDefaults() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + String did = keymaster.createChallenge(); + MdipDocument doc = keymaster.resolveDID(did); + + assertEquals(did, doc.didDocument.id); + assertEquals(alice, doc.didDocument.controller); + assertEquals(Map.of("challenge", Map.of()), doc.didDocumentData); + + long now = System.currentTimeMillis(); + long validUntil = java.time.Instant.parse(doc.mdip.validUntil).toEpochMilli(); + long ttl = validUntil - now; + assertTrue(ttl < 60 * 60 * 1000); + } + + @Test + void createChallengeEmptyWithExpiry() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + String validUntil = "2025-01-01"; + + CreateAssetOptions options = new CreateAssetOptions(); + options.validUntil = validUntil; + String did = keymaster.createChallenge(Map.of(), options); + MdipDocument doc = keymaster.resolveDID(did); + + assertEquals(did, doc.didDocument.id); + assertEquals(alice, doc.didDocument.controller); + assertEquals(Map.of("challenge", Map.of()), doc.didDocumentData); + assertEquals(validUntil, doc.mdip.validUntil); + } + + @Test + void createChallengeWithCredentials() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + + keymaster.setCurrentId("Alice"); + String credentialDid = keymaster.createSchema(TestFixtures.mockSchema()); + Map challenge = Map.of( + "credentials", + List.of(Map.of( + "schema", credentialDid, + "issuers", List.of(alice, bob) + )) + ); + + String did = keymaster.createChallenge(challenge); + MdipDocument doc = keymaster.resolveDID(did); + + assertEquals(did, doc.didDocument.id); + assertEquals(alice, doc.didDocument.controller); + assertEquals(Map.of("challenge", challenge), doc.didDocumentData); + } + + @Test + void createChallengeInvalidValidUntil() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + CreateAssetOptions options = new CreateAssetOptions(); + options.validUntil = "mockDate"; + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.createChallenge(Map.of(), options) + ); + assertEquals("options.validUntil", error.getMessage()); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveCredentialTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveCredentialTest.java new file mode 100644 index 000000000..37c57b3c2 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveCredentialTest.java @@ -0,0 +1,492 @@ +package org.keychain.keymaster; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.model.IDInfo; +import org.keychain.keymaster.model.WalletFile; +import org.junit.jupiter.api.Test; +import org.keychain.keymaster.testutil.LiveTestSupport; +import org.keychain.keymaster.testutil.TestFixtures; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("live") +class LiveCredentialTest { + @TempDir + Path tempDir; + + protected Keymaster liveKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + private void assertCredentialShape(Map vc) { + assertTrue(vc.containsKey("@context"), "missing @context"); + assertTrue(vc.containsKey("type"), "missing type"); + assertTrue(vc.containsKey("issuer"), "missing issuer"); + assertTrue(vc.containsKey("credentialSubject"), "missing credentialSubject"); + } + + @Test + @Tag("live") + void createSchemaAndTemplate() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Alice"); + + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + java.util.Map template = keymaster.createTemplate(schemaDid); + + org.junit.jupiter.api.Assertions.assertNotNull(schemaDid); + org.junit.jupiter.api.Assertions.assertNotNull(template); + org.junit.jupiter.api.Assertions.assertEquals(schemaDid, template.get("$schema")); + org.junit.jupiter.api.Assertions.assertEquals("TBD", template.get("email")); + } + + @Test + @Tag("live") + void bindAndIssueCredential() { + Keymaster keymaster = liveKeymaster(); + String subjectDid = keymaster.createId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + + java.util.Map bound = keymaster.bindCredential(schemaDid, subjectDid); + String credentialDid = keymaster.issueCredential(bound); + + java.util.Map vc = keymaster.getCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertNotNull(vc); + org.junit.jupiter.api.Assertions.assertEquals(subjectDid, vc.get("issuer")); + assertCredentialShape(vc); + Object subjectObj = vc.get("credentialSubject"); + org.junit.jupiter.api.Assertions.assertInstanceOf(java.util.Map.class, subjectObj); + @SuppressWarnings("unchecked") + java.util.Map subject = (java.util.Map) subjectObj; + org.junit.jupiter.api.Assertions.assertEquals(subjectDid, subject.get("id")); + Object credObj = vc.get("credential"); + org.junit.jupiter.api.Assertions.assertInstanceOf(java.util.Map.class, credObj); + @SuppressWarnings("unchecked") + java.util.Map cred = (java.util.Map) credObj; + org.junit.jupiter.api.Assertions.assertEquals("TBD", cred.get("email")); + org.junit.jupiter.api.Assertions.assertTrue(keymaster.verifySignature(vc)); + + WalletFile wallet = keymaster.loadWallet(); + IDInfo owner = wallet.ids.get("Alice"); + org.junit.jupiter.api.Assertions.assertTrue(owner.owned.contains(credentialDid)); + } + + @Test + @Tag("live") + void bindCredentialWithDefaults() { + Keymaster keymaster = liveKeymaster(); + String subjectDid = keymaster.createId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + Map custom = new java.util.LinkedHashMap<>(); + custom.put("email", "alice@example.com"); + + Map vc = keymaster.bindCredential(schemaDid, subjectDid, null, null, custom); + + org.junit.jupiter.api.Assertions.assertEquals(subjectDid, vc.get("issuer")); + Object subjectObj = vc.get("credentialSubject"); + org.junit.jupiter.api.Assertions.assertInstanceOf(Map.class, subjectObj); + @SuppressWarnings("unchecked") + Map subject = (Map) subjectObj; + org.junit.jupiter.api.Assertions.assertEquals(subjectDid, subject.get("id")); + Object credObj = vc.get("credential"); + org.junit.jupiter.api.Assertions.assertInstanceOf(Map.class, credObj); + @SuppressWarnings("unchecked") + Map cred = (Map) credObj; + org.junit.jupiter.api.Assertions.assertEquals("alice@example.com", cred.get("email")); + assertCredentialShape(vc); + } + + @Test + @Tag("live") + void bindCredentialForDifferentUser() { + Keymaster keymaster = liveKeymaster(); + String issuerDid = keymaster.createId("Alice"); + String subjectDid = keymaster.createId("Bob"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + + keymaster.setCurrentId("Alice"); + Map vc = keymaster.bindCredential(schemaDid, subjectDid); + + org.junit.jupiter.api.Assertions.assertEquals(issuerDid, vc.get("issuer")); + Object subjectObj = vc.get("credentialSubject"); + org.junit.jupiter.api.Assertions.assertInstanceOf(Map.class, subjectObj); + @SuppressWarnings("unchecked") + Map subject = (Map) subjectObj; + org.junit.jupiter.api.Assertions.assertEquals(subjectDid, subject.get("id")); + assertCredentialShape(vc); + } + + @Test + @Tag("live") + void publishCredentialReveal() { + Keymaster keymaster = liveKeymaster(); + String subjectDid = keymaster.createId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + java.util.Map bound = keymaster.bindCredential(schemaDid, subjectDid); + String credentialDid = keymaster.issueCredential(bound); + + PublishCredentialOptions reveal = new PublishCredentialOptions(); + reveal.reveal = true; + keymaster.publishCredential(credentialDid, reveal); + + MdipDocument doc = keymaster.resolveDID(subjectDid); + Map manifest = manifestFromDoc(doc); + Object entryObj = manifest.get(credentialDid); + org.junit.jupiter.api.Assertions.assertInstanceOf(Map.class, entryObj); + @SuppressWarnings("unchecked") + Map entry = (Map) entryObj; + org.junit.jupiter.api.Assertions.assertNotNull(entry.get("credential")); + Map vc = keymaster.getCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertNotNull(vc); + assertCredentialShape(vc); + } + + @Test + @Tag("live") + void publishCredentialNoReveal() { + Keymaster keymaster = liveKeymaster(); + String subjectDid = keymaster.createId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + java.util.Map bound = keymaster.bindCredential(schemaDid, subjectDid); + String credentialDid = keymaster.issueCredential(bound); + + PublishCredentialOptions noReveal = new PublishCredentialOptions(); + noReveal.reveal = false; + keymaster.publishCredential(credentialDid, noReveal); + + MdipDocument doc = keymaster.resolveDID(subjectDid); + Map manifest = manifestFromDoc(doc); + Object entryObj = manifest.get(credentialDid); + org.junit.jupiter.api.Assertions.assertInstanceOf(Map.class, entryObj); + @SuppressWarnings("unchecked") + Map entry = (Map) entryObj; + org.junit.jupiter.api.Assertions.assertTrue(entry.containsKey("credential")); + org.junit.jupiter.api.Assertions.assertNull(entry.get("credential")); + } + + @Test + @Tag("live") + void unpublishCredential() { + Keymaster keymaster = liveKeymaster(); + String subjectDid = keymaster.createId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + java.util.Map bound = keymaster.bindCredential(schemaDid, subjectDid); + String credentialDid = keymaster.issueCredential(bound); + PublishCredentialOptions reveal = new PublishCredentialOptions(); + reveal.reveal = true; + keymaster.publishCredential(credentialDid, reveal); + + keymaster.unpublishCredential(credentialDid); + + MdipDocument doc = keymaster.resolveDID(subjectDid); + Map manifest = manifestFromDoc(doc); + org.junit.jupiter.api.Assertions.assertTrue(manifest.isEmpty()); + } + + @Test + @Tag("live") + void getCredential() { + Keymaster keymaster = liveKeymaster(); + String credentialDid = issueCredential(keymaster, "Alice", "Alice"); + + Map credential = keymaster.getCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertNotNull(credential); + Object typesObj = credential.get("type"); + org.junit.jupiter.api.Assertions.assertInstanceOf(List.class, typesObj); + @SuppressWarnings("unchecked") + List types = (List) typesObj; + org.junit.jupiter.api.Assertions.assertTrue(types.contains("VerifiableCredential")); + } + + @Test + @Tag("live") + void listIssued() { + Keymaster keymaster = liveKeymaster(); + String credentialDid = issueCredential(keymaster, "Bob", "Bob"); + + List issued = keymaster.listIssued("Bob"); + org.junit.jupiter.api.Assertions.assertTrue(issued.contains(credentialDid)); + } + + @Test + @Tag("live") + void listIssuedReturnsOnlyIssuerCredentials() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Alice"); + keymaster.createId("Bob"); + keymaster.setCurrentId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + + Map boundAlice = keymaster.bindCredential(schemaDid, keymaster.fetchIdInfo("Bob").did); + String aliceCred = keymaster.issueCredential(boundAlice); + + keymaster.setCurrentId("Bob"); + Map boundBob = keymaster.bindCredential(schemaDid, keymaster.fetchIdInfo("Bob").did); + String bobCred = keymaster.issueCredential(boundBob); + + List issuedByAlice = keymaster.listIssued("Alice"); + org.junit.jupiter.api.Assertions.assertTrue(issuedByAlice.contains(aliceCred)); + org.junit.jupiter.api.Assertions.assertFalse(issuedByAlice.contains(bobCred)); + + List issuedByBob = keymaster.listIssued("Bob"); + org.junit.jupiter.api.Assertions.assertTrue(issuedByBob.contains(bobCred)); + org.junit.jupiter.api.Assertions.assertFalse(issuedByBob.contains(aliceCred)); + } + + @Test + @Tag("live") + void listIssuedEmpty() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Bob"); + + List issued = keymaster.listIssued("Bob"); + org.junit.jupiter.api.Assertions.assertTrue(issued.isEmpty()); + } + + @Test + @Tag("live") + void listCredentialsEmpty() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Bob"); + + List held = keymaster.listCredentials("Bob"); + org.junit.jupiter.api.Assertions.assertTrue(held.isEmpty()); + } + + @Test + @Tag("live") + void listCredentialsHeld() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Alice"); + keymaster.createId("Bob"); + keymaster.setCurrentId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + String bobDid = keymaster.fetchIdInfo("Bob").did; + Map bound = keymaster.bindCredential(schemaDid, bobDid); + String credentialDid = keymaster.issueCredential(bound); + String credentialDid2 = keymaster.issueCredential(bound); + + keymaster.setCurrentId("Bob"); + boolean accepted = keymaster.acceptCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertTrue(accepted); + org.junit.jupiter.api.Assertions.assertTrue(keymaster.acceptCredential(credentialDid2)); + + List held = keymaster.listCredentials("Bob"); + org.junit.jupiter.api.Assertions.assertTrue(held.contains(credentialDid)); + org.junit.jupiter.api.Assertions.assertTrue(held.contains(credentialDid2)); + } + + @Test + @Tag("live") + void acceptAndRemoveCredential() { + Keymaster keymaster = liveKeymaster(); + String credentialDid = issueCredential(keymaster, "Alice", "Bob"); + + keymaster.setCurrentId("Bob"); + boolean accepted = keymaster.acceptCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertTrue(accepted); + + List held = keymaster.listCredentials("Bob"); + org.junit.jupiter.api.Assertions.assertTrue(held.contains(credentialDid)); + + WalletFile wallet = keymaster.loadWallet(); + IDInfo owner = wallet.ids.get("Alice"); + IDInfo holder = wallet.ids.get("Bob"); + org.junit.jupiter.api.Assertions.assertTrue(owner.owned.contains(credentialDid)); + org.junit.jupiter.api.Assertions.assertTrue(holder.held.contains(credentialDid)); + + boolean removed = keymaster.removeCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertTrue(removed); + List after = keymaster.listCredentials("Bob"); + org.junit.jupiter.api.Assertions.assertFalse(after.contains(credentialDid)); + } + + @Test + @Tag("live") + void acceptCredentialCannotDecrypt() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Alice"); + keymaster.createId("Bob"); + keymaster.createId("Carol"); + + keymaster.setCurrentId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + Map bound = keymaster.bindCredential(schemaDid, keymaster.fetchIdInfo("Bob").did); + String credentialDid = keymaster.issueCredential(bound); + + keymaster.setCurrentId("Carol"); + boolean ok = keymaster.acceptCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertFalse(ok); + } + + @Test + @Tag("live") + void acceptCredentialWrongSubject() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Alice"); + keymaster.createId("Bob"); + keymaster.createId("Carol"); + + keymaster.setCurrentId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + Map bound = keymaster.bindCredential(schemaDid, keymaster.fetchIdInfo("Bob").did); + String credentialDid = keymaster.issueCredential(bound); + Map vc = keymaster.getCredential(credentialDid); + String wrappedForCarol = keymaster.encryptJSON(vc, "Carol"); + + keymaster.setCurrentId("Carol"); + boolean ok = keymaster.acceptCredential(wrappedForCarol); + org.junit.jupiter.api.Assertions.assertFalse(ok); + } + + @Test + @Tag("live") + void acceptCredentialInvalidDid() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Alice"); + keymaster.createId("Bob"); + + keymaster.setCurrentId("Bob"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + + boolean ok = keymaster.acceptCredential(schemaDid); + org.junit.jupiter.api.Assertions.assertFalse(ok); + } + + @Test + @Tag("live") + void removeCredentialReturnsFalseWhenNotHeld() { + Keymaster keymaster = liveKeymaster(); + String agentDid = keymaster.createId("Alice"); + + boolean ok = keymaster.removeCredential(agentDid); + org.junit.jupiter.api.Assertions.assertFalse(ok); + } + + @Test + @Tag("live") + void getCredentialReturnsNullForNonCredential() { + Keymaster keymaster = liveKeymaster(); + String bobDid = keymaster.createId("Bob"); + String did = keymaster.encryptJSON(TestFixtures.mockJson(), bobDid); + + Map credential = keymaster.getCredential(did); + org.junit.jupiter.api.Assertions.assertNull(credential); + } + + @Test + @Tag("live") + void issueCredentialFromTemplate() { + Keymaster keymaster = liveKeymaster(); + String subjectDid = keymaster.createId("Alice"); + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + Map template = keymaster.createTemplate(schemaDid); + + java.time.Instant now = java.time.Instant.now(); + String validFrom = now.toString(); + String validUntil = now.plusSeconds(3600).toString(); + + IssueCredentialOptions options = new IssueCredentialOptions(); + options.subject = subjectDid; + options.schema = schemaDid; + options.validFrom = validFrom; + options.validUntil = validUntil; + + String did = keymaster.issueCredential(template, options); + Map vc = keymaster.getCredential(did); + + org.junit.jupiter.api.Assertions.assertEquals(validFrom, vc.get("validFrom")); + org.junit.jupiter.api.Assertions.assertEquals(validUntil, vc.get("validUntil")); + assertCredentialShape(vc); + } + + @Test + @Tag("live") + void publishCredentialRejectsNonCredential() { + Keymaster keymaster = liveKeymaster(); + String bobDid = keymaster.createId("Bob"); + String did = keymaster.encryptJSON(TestFixtures.mockJson(), bobDid); + + PublishCredentialOptions noReveal = new PublishCredentialOptions(); + noReveal.reveal = false; + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> + keymaster.publishCredential(did, noReveal) + ); + } + + @Test + @Tag("live") + void updateCredential() { + Keymaster keymaster = liveKeymaster(); + String credentialDid = issueCredential(keymaster, "Alice", "Alice"); + + Map vc = keymaster.getCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertNotNull(vc); + vc.put("validUntil", java.time.Instant.now().plusSeconds(3600).toString()); + + boolean ok = keymaster.updateCredential(credentialDid, vc); + org.junit.jupiter.api.Assertions.assertTrue(ok); + + MdipDocument doc = keymaster.resolveDID(credentialDid); + org.junit.jupiter.api.Assertions.assertNotNull(doc.didDocumentMetadata); + org.junit.jupiter.api.Assertions.assertEquals("2", doc.didDocumentMetadata.version); + } + + @Test + @Tag("live") + void revokeCredential() { + Keymaster keymaster = liveKeymaster(); + String credentialDid = issueCredential(keymaster, "Alice", "Alice"); + + WalletFile before = keymaster.loadWallet(); + IDInfo owner = before.ids.get("Alice"); + org.junit.jupiter.api.Assertions.assertTrue(owner.owned.contains(credentialDid)); + + boolean ok = keymaster.revokeCredential(credentialDid); + org.junit.jupiter.api.Assertions.assertTrue(ok); + + MdipDocument revoked = keymaster.resolveDID(credentialDid); + org.junit.jupiter.api.Assertions.assertNotNull(revoked.didDocumentMetadata); + org.junit.jupiter.api.Assertions.assertEquals(true, revoked.didDocumentMetadata.deactivated); + + WalletFile after = keymaster.loadWallet(); + org.junit.jupiter.api.Assertions.assertFalse(after.ids.get("Alice").owned.contains(credentialDid)); + } + + private Map manifestFromDoc(MdipDocument doc) { + if (doc == null || doc.didDocumentData == null) { + return Collections.emptyMap(); + } + if (!(doc.didDocumentData instanceof Map)) { + return Collections.emptyMap(); + } + @SuppressWarnings("unchecked") + Map data = (Map) doc.didDocumentData; + Object manifestObj = data.get("manifest"); + if (!(manifestObj instanceof Map)) { + return Collections.emptyMap(); + } + @SuppressWarnings("unchecked") + Map manifest = (Map) manifestObj; + return manifest; + } + + private String issueCredential(Keymaster keymaster, String issuerName, String subjectName) { + String subjectDid = keymaster.createId(issuerName); + if (!issuerName.equals(subjectName)) { + keymaster.createId(subjectName); + subjectDid = keymaster.fetchIdInfo(subjectName).did; + keymaster.setCurrentId(issuerName); + } + + String schemaDid = keymaster.createSchema(TestFixtures.mockSchema()); + Map bound = keymaster.bindCredential(schemaDid, subjectDid); + return keymaster.issueCredential(bound); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveCryptoTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveCryptoTest.java new file mode 100644 index 000000000..2e481c360 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveCryptoTest.java @@ -0,0 +1,290 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.nio.file.Path; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.crypto.KeymasterCryptoImpl; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.testutil.LiveTestSupport; + +@Tag("live") +class LiveCryptoTest { + @TempDir + Path tempDir; + private static final Map MOCK_JSON = Map.of( + "key", "value", + "list", java.util.List.of(1, 2, 3), + "obj", Map.of("name", "some object") + ); + + private Keymaster newKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + private String randomString() { + int length = 1024; + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder result = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int index = (int) (Math.random() * chars.length()); + result.append(chars.charAt(index)); + } + return result.toString(); + } + + private String extractCipherHash(MdipDocument doc) { + Object data = doc.didDocumentData; + if (!(data instanceof Map)) { + fail("missing encrypted payload"); + return null; + } + @SuppressWarnings("unchecked") + Map payload = (Map) data; + Object encrypted = payload.get("encrypted"); + if (!(encrypted instanceof Map)) { + fail("missing encrypted payload"); + return null; + } + @SuppressWarnings("unchecked") + Map encryptedMap = (Map) encrypted; + Object cipherHash = encryptedMap.get("cipher_hash"); + return cipherHash != null ? cipherHash.toString() : null; + } + + @Test + void encryptMessageShort() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + + String msg = "Hi Bob!"; + EncryptOptions options = new EncryptOptions(); + options.includeHash = true; + String encryptDid = keymaster.encryptMessage(msg, did, options); + MdipDocument doc = keymaster.resolveDID(encryptDid); + String msgHash = new KeymasterCryptoImpl().hashMessage(msg); + + assertEquals(msgHash, extractCipherHash(doc)); + } + + @Test + void encryptMessageLong() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + + String msg = randomString(); + EncryptOptions options = new EncryptOptions(); + options.includeHash = true; + String encryptDid = keymaster.encryptMessage(msg, did, options); + MdipDocument doc = keymaster.resolveDID(encryptDid); + String msgHash = new KeymasterCryptoImpl().hashMessage(msg); + + assertEquals(msgHash, extractCipherHash(doc)); + } + + @Test + void decryptMessageShortSameId() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + + String msg = "Hi Bob!"; + String encryptDid = keymaster.encryptMessage(msg, did); + String decipher = keymaster.decryptMessage(encryptDid); + + assertEquals(msg, decipher); + } + + @Test + void decryptMessageShortAfterRotateKeysConfirmed() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + String msg = "Hi Bob!"; + + keymaster.rotateKeys(); + String encryptDid = keymaster.encryptMessage(msg, did); + keymaster.rotateKeys(); + String decipher = keymaster.decryptMessage(encryptDid); + + assertEquals(msg, decipher); + } + + @Test + void decryptMessageShortAfterRotateKeysUnconfirmed() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + String msg = "Hi Bob!"; + + keymaster.rotateKeys(); + String encryptDid = keymaster.encryptMessage(msg, did); + String decipher = keymaster.decryptMessage(encryptDid); + + assertEquals(msg, decipher); + } + + @Test + void decryptMessageShortByAnotherId() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + + keymaster.setCurrentId("Alice"); + String msg = "Hi Bob!"; + String encryptDid = keymaster.encryptMessage(msg, bob); + + keymaster.setCurrentId("Bob"); + String decipher = keymaster.decryptMessage(encryptDid); + + assertEquals(msg, decipher); + } + + @Test + void decryptMessageLongByAnotherId() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + + keymaster.setCurrentId("Alice"); + String msg = randomString(); + String encryptDid = keymaster.encryptMessage(msg, bob); + + keymaster.setCurrentId("Bob"); + String decipher = keymaster.decryptMessage(encryptDid); + + assertEquals(msg, decipher); + } + + @Test + void decryptMessageInvalidDid() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Alice"); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.decryptMessage(did) + ); + assertTrue(error.getMessage().contains("did not encrypted")); + } + + @Test + void encryptJsonValid() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + keymaster.resolveDID(bob); + + String did = keymaster.encryptJSON(MOCK_JSON, bob); + Object data = keymaster.resolveAsset(did); + if (!(data instanceof Map)) { + fail("missing encrypted payload"); + } + @SuppressWarnings("unchecked") + Map dataMap = (Map) data; + Object encrypted = dataMap.get("encrypted"); + if (!(encrypted instanceof Map)) { + fail("missing encrypted payload"); + } + @SuppressWarnings("unchecked") + Map encryptedMap = (Map) encrypted; + assertEquals(bob, encryptedMap.get("sender")); + } + + @Test + void decryptJsonValid() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + String did = keymaster.encryptJSON(MOCK_JSON, bob); + + Object decipher = keymaster.decryptJSON(did); + assertEquals(MOCK_JSON, decipher); + } + + @Test + void addSignatureAddsSignature() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + + Map signed = keymaster.addSignature(MOCK_JSON); + Object signatureObj = signed.get("signature"); + if (!(signatureObj instanceof Map)) { + fail("missing signature"); + } + @SuppressWarnings("unchecked") + Map signature = (Map) signatureObj; + String msgHash = new KeymasterCryptoImpl().hashJson(MOCK_JSON); + + assertEquals(did, signature.get("signer")); + assertEquals(msgHash, signature.get("hash")); + assertInstanceOf(String.class, signature.get("value")); + } + + @Test + void addSignatureNoCurrentId() { + Keymaster keymaster = newKeymaster(); + IllegalStateException error = assertThrows( + IllegalStateException.class, + () -> keymaster.addSignature(MOCK_JSON) + ); + assertEquals("Keymaster: No current ID", error.getMessage()); + } + + @Test + void verifySignatureValid() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + Map signed = keymaster.addSignature(MOCK_JSON); + assertTrue(keymaster.verifySignature(signed)); + } + + @Test + void verifySignatureMissingSignature() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + assertFalse(keymaster.verifySignature(MOCK_JSON)); + } + + @Test + void verifySignatureInvalidSignature() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + Map signed = keymaster.addSignature(MOCK_JSON); + @SuppressWarnings("unchecked") + Map signature = (Map) signed.get("signature"); + String value = (String) signature.get("value"); + signature.put("value", value.substring(1)); + assertFalse(keymaster.verifySignature(signed)); + } + + @Test + void verifySignatureMissingSigner() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + Map signed = keymaster.addSignature(MOCK_JSON); + @SuppressWarnings("unchecked") + Map signature = (Map) signed.get("signature"); + signature.remove("signer"); + assertFalse(keymaster.verifySignature(signed)); + } + + @Test + void verifySignatureInvalidHash() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + Map signed = keymaster.addSignature(MOCK_JSON); + @SuppressWarnings("unchecked") + Map signature = (Map) signed.get("signature"); + signature.put("hash", "1"); + assertFalse(keymaster.verifySignature(signed)); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveGroupTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveGroupTest.java new file mode 100644 index 000000000..5a6d2d334 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveGroupTest.java @@ -0,0 +1,508 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.model.Group; +import org.keychain.keymaster.testutil.LiveTestSupport; + +@Tag("live") +class LiveGroupTest { + @TempDir + private Path tempDir; + + private Keymaster newKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + @Test + void createGroupCreatesNamedGroup() { + Keymaster keymaster = newKeymaster(); + String ownerDid = keymaster.createId("Bob"); + String groupName = "mockGroup"; + String groupDid = keymaster.createGroup(groupName); + MdipDocument doc = keymaster.resolveDID(groupDid); + + assertEquals(groupDid, doc.didDocument.id); + assertEquals(ownerDid, doc.didDocument.controller); + + Map expectedGroup = Map.of( + "group", + Map.of( + "name", + groupName, + "members", + List.of() + ) + ); + + assertEquals(expectedGroup, doc.didDocumentData); + } + + @Test + void createGroupWithDifferentDidName() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupName = "mockGroup"; + String didName = "mockName"; + + CreateAssetOptions options = new CreateAssetOptions(); + options.name = didName; + keymaster.createGroup(groupName, options); + + MdipDocument doc = keymaster.resolveDID(didName); + Map expectedGroup = Map.of( + "group", + Map.of( + "name", + groupName, + "members", + List.of() + ) + ); + + assertEquals(expectedGroup, doc.didDocumentData); + } + + @Test + void getGroupReturnsGroup() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupName = "mock"; + String groupDid = keymaster.createGroup(groupName); + + Group group = keymaster.getGroup(groupDid); + + assertNotNull(group); + assertEquals(groupName, group.name); + assertEquals(List.of(), group.members); + } + + @Test + void getGroupReturnsNullOnInvalidDid() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + + Group group = keymaster.getGroup(did); + + assertNull(group); + } + + @Test + void getGroupReturnsOldStyleGroup() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map oldGroup = Map.of( + "name", + "mock", + "members", + List.of() + ); + String groupDid = keymaster.createAsset(oldGroup); + + Group group = keymaster.getGroup(groupDid); + + assertNotNull(group); + assertEquals("mock", group.name); + assertEquals(List.of(), group.members); + } + + @Test + void getGroupReturnsNullForNonGroupDid() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Bob"); + + Group group = keymaster.getGroup(agentDid); + + assertNull(group); + } + + @Test + void addGroupMemberAddsDidMember() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupName = "mockGroup"; + String groupDid = keymaster.createGroup(groupName); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + boolean ok = keymaster.addGroupMember(groupDid, dataDid); + + assertTrue(ok); + Group group = keymaster.getGroup(groupDid); + assertNotNull(group); + assertEquals(groupName, group.name); + assertEquals(List.of(dataDid), group.members); + } + + @Test + void addGroupMemberAddsAliasMember() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupName = "mockGroup"; + String groupDid = keymaster.createGroup(groupName); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + String alias = "mockAlias"; + keymaster.addName(alias, dataDid); + boolean ok = keymaster.addGroupMember(groupDid, alias); + + assertTrue(ok); + Group group = keymaster.getGroup(groupDid); + assertNotNull(group); + assertEquals(List.of(dataDid), group.members); + } + + @Test + void addGroupMemberRejectsUnknownAlias() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.addGroupMember(groupDid, "mockAlias") + ); + + assertEquals("unknown id", error.getMessage()); + } + + @Test + void addGroupMemberViaGroupAlias() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupName = "mockGroup"; + String groupDid = keymaster.createGroup(groupName); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + String alias = "mockAlias"; + keymaster.addName(alias, groupDid); + boolean ok = keymaster.addGroupMember(alias, dataDid); + + assertTrue(ok); + Group group = keymaster.getGroup(groupDid); + assertNotNull(group); + assertEquals(List.of(dataDid), group.members); + } + + @Test + void addGroupMemberRejectsUnknownGroupAlias() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.addGroupMember("mockAlias", dataDid) + ); + + assertEquals("unknown id", error.getMessage()); + } + + @Test + void addGroupMemberOnlyAddsOnce() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + keymaster.addGroupMember(groupDid, dataDid); + keymaster.addGroupMember(groupDid, dataDid); + keymaster.addGroupMember(groupDid, dataDid); + boolean ok = keymaster.addGroupMember(groupDid, dataDid); + + assertTrue(ok); + Group group = keymaster.getGroup(groupDid); + assertNotNull(group); + assertEquals(List.of(dataDid), group.members); + } + + @Test + void addGroupMemberDoesNotIncrementVersionWhenDuplicate() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + keymaster.addGroupMember(groupDid, dataDid); + MdipDocument doc1 = keymaster.resolveDID(groupDid); + String version1 = doc1.didDocumentMetadata.version; + + keymaster.addGroupMember(groupDid, dataDid); + MdipDocument doc2 = keymaster.resolveDID(groupDid); + String version2 = doc2.didDocumentMetadata.version; + + assertEquals(version1, version2); + } + + @Test + void addGroupMemberAddsMultipleMembers() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + int memberCount = 5; + + for (int i = 0; i < memberCount; i += 1) { + String dataDid = keymaster.createAsset(Map.of("name", "mock-" + i)); + keymaster.addGroupMember(groupDid, dataDid); + } + + Group group = keymaster.getGroup(groupDid); + assertNotNull(group); + assertEquals(memberCount, group.members.size()); + } + + @Test + void addGroupMemberRejectsInvalidMemberDid() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.addGroupMember(groupDid, "did:mock") + ); + + assertEquals("Invalid parameter: memberId", error.getMessage()); + } + + @Test + void addGroupMemberRejectsNonGroup() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Bob"); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + IllegalArgumentException error1 = assertThrows(IllegalArgumentException.class, () -> + keymaster.addGroupMember(agentDid, dataDid) + ); + assertEquals("Invalid parameter: groupId", error1.getMessage()); + + IllegalArgumentException error2 = assertThrows(IllegalArgumentException.class, () -> + keymaster.addGroupMember(dataDid, agentDid) + ); + assertEquals("Invalid parameter: groupId", error2.getMessage()); + } + + @Test + void addGroupMemberRejectsSelfMembership() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("group"); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.addGroupMember(groupDid, groupDid) + ); + assertEquals("Invalid parameter: can't add a group to itself", error.getMessage()); + } + + @Test + void addGroupMemberRejectsMutualMembership() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String group1 = keymaster.createGroup("group-1"); + String group2 = keymaster.createGroup("group-2"); + String group3 = keymaster.createGroup("group-3"); + + keymaster.addGroupMember(group1, group2); + keymaster.addGroupMember(group2, group3); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.addGroupMember(group3, group1) + ); + assertEquals("Invalid parameter: can't create mutual membership", error.getMessage()); + } + + @Test + void removeGroupMemberRemovesDidMember() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupName = "mockGroup"; + String groupDid = keymaster.createGroup(groupName); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + keymaster.addGroupMember(groupDid, dataDid); + + boolean ok = keymaster.removeGroupMember(groupDid, dataDid); + + assertTrue(ok); + Group group = keymaster.getGroup(groupDid); + assertNotNull(group); + assertEquals(groupName, group.name); + assertEquals(List.of(), group.members); + } + + @Test + void removeGroupMemberRemovesAliasMember() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupName = "mockGroup"; + String groupDid = keymaster.createGroup(groupName); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + keymaster.addGroupMember(groupDid, dataDid); + + String alias = "mockAlias"; + keymaster.addName(alias, dataDid); + boolean ok = keymaster.removeGroupMember(groupDid, alias); + + assertTrue(ok); + Group group = keymaster.getGroup(groupDid); + assertNotNull(group); + assertEquals(List.of(), group.members); + } + + @Test + void removeGroupMemberOkWhenNotMember() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupName = "mockGroup"; + String groupDid = keymaster.createGroup(groupName); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + boolean ok = keymaster.removeGroupMember(groupDid, dataDid); + + assertTrue(ok); + Group group = keymaster.getGroup(groupDid); + assertNotNull(group); + assertEquals(List.of(), group.members); + } + + @Test + void removeGroupMemberDoesNotIncrementVersionWhenMissing() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + + MdipDocument doc1 = keymaster.resolveDID(groupDid); + String version1 = doc1.didDocumentMetadata.version; + + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + keymaster.removeGroupMember(groupDid, dataDid); + + MdipDocument doc2 = keymaster.resolveDID(groupDid); + String version2 = doc2.didDocumentMetadata.version; + + assertEquals(version1, version2); + } + + @Test + void removeGroupMemberRejectsInvalidMemberDid() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> + keymaster.removeGroupMember(groupDid, "did:mock") + ); + assertEquals("Invalid parameter: memberId", error.getMessage()); + } + + @Test + void removeGroupMemberRejectsNonGroup() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Bob"); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + IllegalArgumentException error1 = assertThrows(IllegalArgumentException.class, () -> + keymaster.removeGroupMember(agentDid, dataDid) + ); + assertEquals("Invalid parameter: groupId", error1.getMessage()); + + IllegalArgumentException error2 = assertThrows(IllegalArgumentException.class, () -> + keymaster.removeGroupMember(dataDid, agentDid) + ); + assertEquals("Invalid parameter: groupId", error2.getMessage()); + } + + @Test + void testGroupReturnsTrueWhenMemberPresent() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + keymaster.addGroupMember(groupDid, dataDid); + + boolean test = keymaster.testGroup(groupDid, dataDid); + + assertTrue(test); + } + + @Test + void testGroupReturnsFalseWhenMemberAbsent() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + boolean test = keymaster.testGroup(groupDid, dataDid); + + assertFalse(test); + } + + @Test + void testGroupReturnsTrueWhenGroupOnly() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String groupDid = keymaster.createGroup("mockGroup"); + + boolean test = keymaster.testGroup(groupDid); + + assertTrue(test); + } + + @Test + void testGroupReturnsFalseWhenNonGroupOnly() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String dataDid = keymaster.createAsset(Map.of("name", "mockData")); + + boolean test = keymaster.testGroup(dataDid); + + assertFalse(test); + } + + @Test + void testGroupReturnsTrueForRecursiveMembers() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String group1 = keymaster.createGroup("level-1"); + String group2 = keymaster.createGroup("level-2"); + String group3 = keymaster.createGroup("level-3"); + String group4 = keymaster.createGroup("level-4"); + String group5 = keymaster.createGroup("level-5"); + + keymaster.addGroupMember(group1, group2); + keymaster.addGroupMember(group2, group3); + keymaster.addGroupMember(group3, group4); + keymaster.addGroupMember(group4, group5); + + assertTrue(keymaster.testGroup(group1, group2)); + assertTrue(keymaster.testGroup(group1, group3)); + assertTrue(keymaster.testGroup(group1, group4)); + assertTrue(keymaster.testGroup(group1, group5)); + } + + @Test + void listGroupsReturnsGroupsOnly() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + String group1 = keymaster.createGroup("mock-1"); + String group2 = keymaster.createGroup("mock-2"); + String group3 = keymaster.createGroup("mock-3"); + String schema1 = keymaster.createSchema(); + keymaster.addToOwned("did:test:mock53", null); + + List groups = keymaster.listGroups(); + + assertTrue(groups.contains(group1)); + assertTrue(groups.contains(group2)); + assertTrue(groups.contains(group3)); + assertFalse(groups.contains(schema1)); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveIdTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveIdTest.java new file mode 100644 index 000000000..f4c26b73c --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveIdTest.java @@ -0,0 +1,235 @@ +package org.keychain.keymaster; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.crypto.KeymasterCryptoImpl; +import org.keychain.gatekeeper.GatekeeperInterface; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.gatekeeper.model.Operation; +import org.keychain.keymaster.model.WalletFile; +import org.keychain.keymaster.testutil.LiveTestSupport; + +@Tag("live") +class LiveIdTest { + @TempDir + Path tempDir; + + protected Keymaster liveKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + protected GatekeeperInterface gatekeeperClient() { + return LiveTestSupport.gatekeeperClient(); + } + + protected String testRegistry() { + return LiveTestSupport.DEFAULT_REGISTRY; + } + + @Test + @Tag("live") + void createIdDefaultRegistry() { + Keymaster keymaster = liveKeymaster(); + String did = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID(did); + + org.junit.jupiter.api.Assertions.assertEquals(LiveTestSupport.DEFAULT_REGISTRY, doc.mdip.registry); + } + + @Test + @Tag("live") + void createIdCustomDefaultRegistry() { + String customRegistry = testRegistry(); + Keymaster keymaster = LiveTestSupport.keymaster(tempDir, customRegistry); + + String did = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID(did); + + org.junit.jupiter.api.Assertions.assertEquals(customRegistry, doc.mdip.registry); + } + + @Test + @Tag("live") + void createIdStoresWalletEntry() { + Keymaster keymaster = liveKeymaster(); + + String did = keymaster.createId("Bob"); + WalletFile wallet = keymaster.loadWallet(); + + org.junit.jupiter.api.Assertions.assertEquals(did, wallet.ids.get("Bob").did); + org.junit.jupiter.api.Assertions.assertEquals("Bob", wallet.current); + } + + @Test + @Tag("live") + void createIdUnicodeName() { + Keymaster keymaster = liveKeymaster(); + + String name = "ҽ× ʍɑϲհíղɑ"; + String did = keymaster.createId(name); + WalletFile wallet = keymaster.loadWallet(); + + org.junit.jupiter.api.Assertions.assertEquals(did, wallet.ids.get(name).did); + org.junit.jupiter.api.Assertions.assertEquals(name, wallet.current); + } + + @Test + @Tag("live") + void createIdOperationMatchesRegistry() { + Keymaster keymaster = liveKeymaster(); + String customRegistry = testRegistry(); + Operation op = keymaster.createIdOperation("Alice", 0, customRegistry); + + org.junit.jupiter.api.Assertions.assertEquals(customRegistry, op.mdip.registry); + org.junit.jupiter.api.Assertions.assertEquals("create", op.type); + org.junit.jupiter.api.Assertions.assertNotNull(op.publicJwk); + org.junit.jupiter.api.Assertions.assertNotNull(op.signature); + } + + @Test + @Tag("live") + void createIdOperationDefaultRegistry() { + Keymaster keymaster = liveKeymaster(); + + Operation op = keymaster.createIdOperation("Bob"); + + org.junit.jupiter.api.Assertions.assertEquals("create", op.type); + org.junit.jupiter.api.Assertions.assertEquals(LiveTestSupport.DEFAULT_REGISTRY, op.mdip.registry); + org.junit.jupiter.api.Assertions.assertNotNull(op.publicJwk); + org.junit.jupiter.api.Assertions.assertNotNull(op.signature); + } + + @Test + @Tag("live") + void createIdOperationSignatureValid() { + Keymaster keymaster = liveKeymaster(); + Operation op = keymaster.createIdOperation("Frank"); + + java.util.Map unsigned = new java.util.LinkedHashMap<>(); + unsigned.put("type", op.type); + unsigned.put("created", op.created); + unsigned.put("mdip", op.mdip); + unsigned.put("publicJwk", op.publicJwk); + + String msgHash = new KeymasterCryptoImpl().hashJson(unsigned); + org.junit.jupiter.api.Assertions.assertEquals(msgHash, op.signature.hash); + + org.keychain.crypto.JwkPublic pub = new org.keychain.crypto.JwkPublic( + op.publicJwk.kty, + op.publicJwk.crv, + op.publicJwk.x, + op.publicJwk.y + ); + boolean isValid = new KeymasterCryptoImpl().verifySig(msgHash, op.signature.value, pub); + org.junit.jupiter.api.Assertions.assertTrue(isValid); + } + + @Test + @Tag("live") + void createIdOperationDoesNotMutateWallet() { + Keymaster keymaster = liveKeymaster(); + + WalletFile before = keymaster.loadWallet(); + int counterBefore = before.counter; + + keymaster.createIdOperation("Dave"); + + WalletFile after = keymaster.loadWallet(); + org.junit.jupiter.api.Assertions.assertEquals(counterBefore, after.counter); + org.junit.jupiter.api.Assertions.assertFalse(after.ids.containsKey("Dave")); + org.junit.jupiter.api.Assertions.assertEquals(before.current, after.current); + } + + @Test + @Tag("live") + void createIdOperationCreatesResolvableDid() { + Keymaster keymaster = liveKeymaster(); + + Operation op = keymaster.createIdOperation("Grace"); + String did = gatekeeperClient().createDID(op); + + MdipDocument doc = keymaster.resolveDID(did); + org.junit.jupiter.api.Assertions.assertEquals(did, doc.didDocument.id); + org.junit.jupiter.api.Assertions.assertEquals("agent", doc.mdip.type); + } + + @Test + @Tag("live") + void removeAndRenameId() { + Keymaster keymaster = liveKeymaster(); + String did = keymaster.createId("Bob"); + boolean renamed = keymaster.renameId("Bob", "Alice"); + org.junit.jupiter.api.Assertions.assertTrue(renamed); + + org.junit.jupiter.api.Assertions.assertEquals("Alice", keymaster.loadWallet().current); + org.junit.jupiter.api.Assertions.assertEquals(did, keymaster.loadWallet().ids.get("Alice").did); + + boolean removed = keymaster.removeId("Alice"); + org.junit.jupiter.api.Assertions.assertTrue(removed); + + org.junit.jupiter.api.Assertions.assertTrue(keymaster.loadWallet().ids.isEmpty()); + org.junit.jupiter.api.Assertions.assertEquals("", keymaster.loadWallet().current); + org.junit.jupiter.api.Assertions.assertEquals(did, keymaster.resolveDID(did).didDocument.id); + } + + @Test + @Tag("live") + void setGetCurrentIdAndListIds() { + Keymaster keymaster = liveKeymaster(); + keymaster.createId("Alice"); + keymaster.createId("Bob"); + keymaster.createId("Carol"); + keymaster.createId("Victor"); + + keymaster.setCurrentId("Carol"); + String current = keymaster.getCurrentId(); + org.junit.jupiter.api.Assertions.assertEquals("Carol", current); + + List ids = keymaster.listIds(); + org.junit.jupiter.api.Assertions.assertEquals(4, ids.size()); + org.junit.jupiter.api.Assertions.assertTrue(ids.contains("Alice")); + org.junit.jupiter.api.Assertions.assertTrue(ids.contains("Bob")); + org.junit.jupiter.api.Assertions.assertTrue(ids.contains("Carol")); + org.junit.jupiter.api.Assertions.assertTrue(ids.contains("Victor")); + } + + @Test + @Tag("live") + void backupAndRecoverId() { + Keymaster keymaster = liveKeymaster(); + String did = keymaster.createId("Bob"); + boolean ok = keymaster.backupId(); + org.junit.jupiter.api.Assertions.assertTrue(ok); + + String mnemonic = keymaster.decryptMnemonic(); + keymaster.newWallet(mnemonic, true); + org.junit.jupiter.api.Assertions.assertTrue(keymaster.loadWallet().ids.isEmpty()); + + String name = keymaster.recoverId(did); + org.junit.jupiter.api.Assertions.assertEquals("Bob", name); + org.junit.jupiter.api.Assertions.assertTrue(keymaster.loadWallet().ids.containsKey("Bob")); + } + + @Test + @Tag("live") + void testAgent() { + Keymaster keymaster = liveKeymaster(); + String did = keymaster.createId("Bob"); + boolean isAgent = keymaster.testAgent(did); + org.junit.jupiter.api.Assertions.assertTrue(isAgent); + } + + @Test + @Tag("live") + void testAgentOnAsset() { + Keymaster keymaster = liveKeymaster(); + + keymaster.createId("Bob"); + String assetDid = keymaster.createAsset(Map.of("name", "mockAnchor"), LiveTestSupport.DEFAULT_REGISTRY); + org.junit.jupiter.api.Assertions.assertFalse(keymaster.testAgent(assetDid)); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveNameTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveNameTest.java new file mode 100644 index 000000000..59f6c431a --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveNameTest.java @@ -0,0 +1,195 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.keymaster.model.WalletFile; +import org.keychain.keymaster.testutil.LiveTestSupport; + +@Tag("live") +class LiveNameTest { + @TempDir + Path tempDir; + + private Keymaster newKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + @Test + void addNameCreatesNewName() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + boolean ok = keymaster.addName("Jack", bob); + WalletFile wallet = keymaster.loadWallet(); + + assertTrue(ok); + assertEquals(bob, wallet.names.get("Jack")); + } + + @Test + void addNameUnicodeName() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + String name = "ҽ× ʍɑϲհíղɑ"; + + boolean ok = keymaster.addName(name, bob); + WalletFile wallet = keymaster.loadWallet(); + + assertTrue(ok); + assertEquals(bob, wallet.names.get(name)); + } + + @Test + void addNameRejectsDuplicate() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + + keymaster.addName("Jack", alice); + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.addName("Jack", bob) + ); + assertEquals("Invalid parameter: name already used", error.getMessage()); + } + + @Test + void addNameRejectsIdName() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.addName("Alice", alice) + ); + assertEquals("Invalid parameter: name already used", error.getMessage()); + } + + @Test + void addNameRejectsEmptyName() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + String expected = "Invalid parameter: name must be a non-empty string"; + + IllegalArgumentException blank = assertThrows( + IllegalArgumentException.class, + () -> keymaster.addName("", alice) + ); + assertEquals(expected, blank.getMessage()); + + IllegalArgumentException whitespace = assertThrows( + IllegalArgumentException.class, + () -> keymaster.addName(" ", alice) + ); + assertEquals(expected, whitespace.getMessage()); + } + + @Test + void addNameRejectsTooLong() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.addName("1234567890123456789012345678901234567890", alice) + ); + assertEquals("Invalid parameter: name too long", error.getMessage()); + } + + @Test + void addNameRejectsUnprintable() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.addName("hello\nworld!", alice) + ); + assertEquals("Invalid parameter: name contains unprintable characters", error.getMessage()); + } + + @Test + void getNameReturnsDid() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + boolean ok = keymaster.addName("Jack", bob); + String did = keymaster.getName("Jack"); + + assertTrue(ok); + assertEquals(bob, did); + } + + @Test + void getNameUnknownReturnsNull() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + String did = keymaster.getName("Jack"); + assertNull(did); + } + + @Test + void removeNameRemovesName() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + + keymaster.addName("Jack", bob); + keymaster.removeName("Jack"); + + WalletFile wallet = keymaster.loadWallet(); + assertNull(wallet.names.get("Jack")); + } + + @Test + void removeNameMissingReturnsTrue() { + Keymaster keymaster = newKeymaster(); + boolean ok = keymaster.removeName("Jack"); + + assertTrue(ok); + } + + @Test + void listNamesReturnsAllNames() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + + for (int i = 0; i < 10; i += 1) { + keymaster.addName("name-" + i, bob); + } + + java.util.Map names = keymaster.listNames(false); + assertEquals(10, names.size()); + for (String name : names.keySet()) { + assertEquals(bob, names.get(name)); + } + } + + @Test + void listNamesIncludesIdsWhenRequested() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + String alice = keymaster.createId("Alice"); + + for (int i = 0; i < 10; i += 1) { + keymaster.addName("name-" + i, bob); + } + + java.util.Map names = keymaster.listNames(true); + assertEquals(12, names.size()); + assertEquals(bob, names.get("Bob")); + assertEquals(alice, names.get("Alice")); + } + + @Test + void listNamesEmptyWhenNoNamesAdded() { + Keymaster keymaster = newKeymaster(); + java.util.Map names = keymaster.listNames(false); + assertEquals(0, names.size()); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveResponseTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveResponseTest.java new file mode 100644 index 000000000..a1905250c --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveResponseTest.java @@ -0,0 +1,334 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.keymaster.model.WalletFile; +import org.keychain.keymaster.testutil.LiveTestSupport; +import org.keychain.keymaster.testutil.TestFixtures; + +@Tag("live") +class LiveResponseTest { + @TempDir + Path tempDir; + + private Keymaster newKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + @Test + void createResponseValidSimpleChallenge() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + keymaster.createId("Victor"); + + keymaster.setCurrentId("Alice"); + + String credentialDid = keymaster.createSchema(TestFixtures.mockSchema()); + Map boundCredential = keymaster.bindCredential(credentialDid, bob); + String vcDid = keymaster.issueCredential(boundCredential); + + keymaster.setCurrentId("Bob"); + boolean ok = keymaster.acceptCredential(vcDid); + assertTrue(ok); + + WalletFile wallet = keymaster.loadWallet(); + assertTrue(wallet.ids.get("Alice").owned.contains(vcDid)); + assertTrue(wallet.ids.get("Bob").held.contains(vcDid)); + + keymaster.setCurrentId("Victor"); + Map challenge = Map.of( + "credentials", + List.of(Map.of( + "schema", credentialDid, + "issuers", List.of(alice) + )) + ); + String challengeDid = keymaster.createChallenge(challenge); + + keymaster.setCurrentId("Bob"); + String responseDid = keymaster.createResponse(challengeDid); + Object responseObj = keymaster.decryptJSON(responseDid); + if (!(responseObj instanceof Map)) { + throw new IllegalStateException("response did not decrypt"); + } + @SuppressWarnings("unchecked") + Map responseMap = (Map) responseObj; + @SuppressWarnings("unchecked") + Map response = (Map) responseMap.get("response"); + @SuppressWarnings("unchecked") + List> credentials = + (List>) response.get("credentials"); + + assertEquals(challengeDid, response.get("challenge")); + assertEquals(1, credentials.size()); + assertEquals(vcDid, credentials.get(0).get("vc")); + } + + @Test + void verifyResponseEmptyChallenge() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + + keymaster.setCurrentId("Alice"); + String challengeDid = keymaster.createChallenge(); + + keymaster.setCurrentId("Bob"); + String responseDid = keymaster.createResponse(challengeDid); + + keymaster.setCurrentId("Alice"); + Map verify = keymaster.verifyResponse(responseDid); + + assertEquals(challengeDid, verify.get("challenge")); + assertEquals(0, verify.get("requested")); + assertEquals(0, verify.get("fulfilled")); + assertEquals(true, verify.get("match")); + assertEquals(bob, verify.get("responder")); + assertEquals(List.of(), verify.get("credentials")); + assertEquals(List.of(), verify.get("vps")); + } + + @Test + void verifyResponseSingleCredentialMatch() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + String carol = keymaster.createId("Carol"); + keymaster.createId("Victor"); + + keymaster.setCurrentId("Alice"); + String credential1 = keymaster.createSchema(TestFixtures.mockSchema()); + Map bc1 = keymaster.bindCredential(credential1, carol); + String vc1 = keymaster.issueCredential(bc1); + + keymaster.setCurrentId("Carol"); + keymaster.acceptCredential(vc1); + + keymaster.setCurrentId("Victor"); + Map challenge = Map.of( + "credentials", + List.of(Map.of("schema", credential1)) + ); + String challengeDid = keymaster.createChallenge(challenge); + + keymaster.setCurrentId("Carol"); + String responseDid = keymaster.createResponse(challengeDid); + + keymaster.setCurrentId("Victor"); + Map verify = keymaster.verifyResponse(responseDid); + + assertEquals(true, verify.get("match")); + assertEquals(challengeDid, verify.get("challenge")); + assertEquals(1, verify.get("requested")); + assertEquals(1, verify.get("fulfilled")); + @SuppressWarnings("unchecked") + List vps = (List) verify.get("vps"); + assertEquals(1, vps.size()); + } + + @Test + void verifyResponseSingleCredentialNoMatch() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + keymaster.createId("Carol"); + keymaster.createId("Victor"); + + keymaster.setCurrentId("Alice"); + String credential1 = keymaster.createSchema(TestFixtures.mockSchema()); + + keymaster.setCurrentId("Victor"); + Map challenge = Map.of( + "credentials", + List.of(Map.of("schema", credential1)) + ); + String challengeDid = keymaster.createChallenge(challenge); + + keymaster.setCurrentId("Carol"); + String responseDid = keymaster.createResponse(challengeDid); + + keymaster.setCurrentId("Victor"); + Map verify = keymaster.verifyResponse(responseDid); + + assertEquals(false, verify.get("match")); + assertEquals(challengeDid, verify.get("challenge")); + assertEquals(1, verify.get("requested")); + assertEquals(0, verify.get("fulfilled")); + @SuppressWarnings("unchecked") + List vps = (List) verify.get("vps"); + assertEquals(0, vps.size()); + } + + @Test + void verifyResponseUpdatedCredential() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + String carol = keymaster.createId("Carol"); + keymaster.createId("Victor"); + + keymaster.setCurrentId("Alice"); + String credential1 = keymaster.createSchema(TestFixtures.mockSchema()); + Map bc1 = keymaster.bindCredential(credential1, carol); + String vc1 = keymaster.issueCredential(bc1); + + keymaster.setCurrentId("Carol"); + keymaster.acceptCredential(vc1); + + keymaster.setCurrentId("Alice"); + Map credential2 = keymaster.getCredential(vc1); + @SuppressWarnings("unchecked") + Map updated = (Map) credential2.get("credential"); + updated.put("email", "updated@email.com"); + credential2.put("credential", updated); + keymaster.updateCredential(vc1, credential2); + + keymaster.setCurrentId("Victor"); + Map challenge = Map.of( + "credentials", + List.of(Map.of("schema", credential1)) + ); + String challengeDid = keymaster.createChallenge(challenge); + + keymaster.setCurrentId("Carol"); + String responseDid = keymaster.createResponse(challengeDid); + + keymaster.setCurrentId("Victor"); + Map verify = keymaster.verifyResponse(responseDid); + + assertEquals(true, verify.get("match")); + assertEquals(challengeDid, verify.get("challenge")); + assertEquals(1, verify.get("requested")); + assertEquals(1, verify.get("fulfilled")); + @SuppressWarnings("unchecked") + List vps = (List) verify.get("vps"); + assertEquals(1, vps.size()); + } + + @Test + void verifyResponseWorkflowWithRevocations() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + String carol = keymaster.createId("Carol"); + keymaster.createId("Victor"); + + keymaster.setCurrentId("Alice"); + String schema1 = keymaster.createSchema(TestFixtures.mockSchema()); + String schema2 = keymaster.createSchema(TestFixtures.mockSchema()); + Map bc1 = keymaster.bindCredential(schema1, carol); + Map bc2 = keymaster.bindCredential(schema2, carol); + String vc1 = keymaster.issueCredential(bc1); + String vc2 = keymaster.issueCredential(bc2); + + keymaster.setCurrentId("Bob"); + String schema3 = keymaster.createSchema(TestFixtures.mockSchema()); + String schema4 = keymaster.createSchema(TestFixtures.mockSchema()); + Map bc3 = keymaster.bindCredential(schema3, carol); + Map bc4 = keymaster.bindCredential(schema4, carol); + String vc3 = keymaster.issueCredential(bc3); + String vc4 = keymaster.issueCredential(bc4); + + keymaster.setCurrentId("Carol"); + keymaster.acceptCredential(vc1); + keymaster.acceptCredential(vc2); + keymaster.acceptCredential(vc3); + keymaster.acceptCredential(vc4); + + keymaster.setCurrentId("Victor"); + Map challenge = Map.of( + "credentials", + List.of( + Map.of("schema", schema1, "issuers", List.of(alice)), + Map.of("schema", schema2, "issuers", List.of(alice)), + Map.of("schema", schema3, "issuers", List.of(bob)), + Map.of("schema", schema4, "issuers", List.of(bob)) + ) + ); + String challengeDid = keymaster.createChallenge(challenge); + + keymaster.setCurrentId("Carol"); + String responseDid = keymaster.createResponse(challengeDid); + Object responseObj = keymaster.decryptJSON(responseDid); + @SuppressWarnings("unchecked") + Map responseMap = (Map) responseObj; + @SuppressWarnings("unchecked") + Map response = (Map) responseMap.get("response"); + @SuppressWarnings("unchecked") + List> credentials = + (List>) response.get("credentials"); + assertEquals(challengeDid, response.get("challenge")); + assertEquals(4, credentials.size()); + + keymaster.setCurrentId("Victor"); + Map verify1 = keymaster.verifyResponse(responseDid); + assertEquals(true, verify1.get("match")); + @SuppressWarnings("unchecked") + List vps1 = (List) verify1.get("vps"); + assertEquals(4, vps1.size()); + + keymaster.setCurrentId("Alice"); + keymaster.rotateKeys(); + keymaster.setCurrentId("Bob"); + keymaster.rotateKeys(); + keymaster.setCurrentId("Carol"); + keymaster.rotateKeys(); + keymaster.setCurrentId("Victor"); + keymaster.rotateKeys(); + + Map verify2 = keymaster.verifyResponse(responseDid); + assertEquals(true, verify2.get("match")); + @SuppressWarnings("unchecked") + List vps2 = (List) verify2.get("vps"); + assertEquals(4, vps2.size()); + + keymaster.setCurrentId("Alice"); + keymaster.revokeCredential(vc1); + + keymaster.setCurrentId("Victor"); + Map verify3 = keymaster.verifyResponse(responseDid); + assertEquals(false, verify3.get("match")); + @SuppressWarnings("unchecked") + List vps3 = (List) verify3.get("vps"); + assertEquals(3, vps3.size()); + + keymaster.setCurrentId("Bob"); + keymaster.revokeCredential(vc3); + + keymaster.setCurrentId("Victor"); + Map verify4 = keymaster.verifyResponse(responseDid); + assertEquals(false, verify4.get("match")); + @SuppressWarnings("unchecked") + List vps4 = (List) verify4.get("vps"); + assertEquals(2, vps4.size()); + } + + @Test + void createResponseInvalidChallengeDid() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + + IllegalArgumentException error = org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> keymaster.createResponse(alice) + ); + assertEquals("Invalid parameter: challengeDID", error.getMessage()); + } + + @Test + void verifyResponseInvalidDidNotEncrypted() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + + IllegalArgumentException error = org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> keymaster.verifyResponse(alice) + ); + assertEquals("Invalid parameter: did not encrypted", error.getMessage()); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveSchemaTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveSchemaTest.java new file mode 100644 index 000000000..43c0b0435 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveSchemaTest.java @@ -0,0 +1,228 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.testutil.LiveTestSupport; +import org.keychain.keymaster.testutil.TestFixtures; + +@Tag("live") +class LiveSchemaTest { + @TempDir + private Path tempDir; + + private Keymaster newKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + @Test + void createSchemaFromTemplate() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + String did = keymaster.createSchema(TestFixtures.mockSchema()); + MdipDocument doc = keymaster.resolveDID(did); + + @SuppressWarnings("unchecked") + Map docData = (Map) doc.didDocumentData; + @SuppressWarnings("unchecked") + Map schema = (Map) docData.get("schema"); + + assertEquals(did, doc.didDocument.id); + assertEquals(TestFixtures.mockSchema(), schema); + } + + @Test + void createDefaultSchema() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + String did = keymaster.createSchema(); + MdipDocument doc = keymaster.resolveDID(did); + + @SuppressWarnings("unchecked") + Map docData = (Map) doc.didDocumentData; + @SuppressWarnings("unchecked") + Map schema = (Map) docData.get("schema"); + + Map expectedSchema = Map.of( + "$schema", "http://json-schema.org/draft-07/schema#", + "type", "object", + "properties", Map.of( + "propertyName", Map.of("type", "string") + ), + "required", List.of("propertyName") + ); + + assertEquals(expectedSchema, schema); + } + + @Test + void createSchemaRejectsInvalidSchema() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.createSchema(Map.of("mock", "not a schema")) + ); + assertEquals("Invalid parameter: schema", error.getMessage()); + } + + @Test + void createSchemaRejectsMissingProperties() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.createSchema(Map.of("$schema", "http://json-schema.org/draft-07/schema#")) + ); + assertEquals("Invalid parameter: schema", error.getMessage()); + } + + @Test + void listSchemasReturnsOnlySchemas() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + String schema1 = keymaster.createSchema(); + String schema2 = keymaster.createSchema(); + String schema3 = keymaster.createSchema(); + String other = keymaster.createAsset(Map.of("name", "notSchema")); + + List schemas = keymaster.listSchemas(); + assertTrue(schemas.contains(schema1)); + assertTrue(schemas.contains(schema2)); + assertTrue(schemas.contains(schema3)); + assertFalse(schemas.contains(other)); + } + + @Test + void getSchemaReturnsSchema() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String did = keymaster.createSchema(TestFixtures.mockSchema()); + + Object schema = keymaster.getSchema(did); + assertEquals(TestFixtures.mockSchema(), schema); + } + + @Test + void getSchemaInvalidIdReturnsNull() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + + Object schema = keymaster.getSchema(did); + assertNull(schema); + } + + @Test + void getSchemaReturnsLegacySchema() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String did = keymaster.createAsset(TestFixtures.mockSchema()); + + Object schema = keymaster.getSchema(did); + assertEquals(TestFixtures.mockSchema(), schema); + } + + @Test + void getSchemaInvalidDidThrows() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.getSchema("bogus") + ); + assertEquals("unknown id", error.getMessage()); + } + + @Test + void setSchemaUpdatesSchema() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String did = keymaster.createSchema(); + + boolean ok = keymaster.setSchema(did, TestFixtures.mockSchema()); + Object schema = keymaster.getSchema(did); + + assertTrue(ok); + assertEquals(TestFixtures.mockSchema(), schema); + } + + @Test + void setSchemaRejectsInvalidSchema() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String did = keymaster.createSchema(); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.setSchema(did, Map.of("mock", "not a schema")) + ); + assertEquals("Invalid parameter: schema", error.getMessage()); + } + + @Test + void testSchemaReturnsTrueForSchema() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String did = keymaster.createSchema(); + keymaster.setSchema(did, TestFixtures.mockSchema()); + + boolean isSchema = keymaster.testSchema(did); + assertTrue(isSchema); + } + + @Test + void testSchemaReturnsFalseForNonSchema() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Bob"); + + assertFalse(keymaster.testSchema(agentDid)); + } + + @Test + void testSchemaReturnsFalseForInvalidDid() { + Keymaster keymaster = newKeymaster(); + assertFalse(keymaster.testSchema("mock7")); + } + + @Test + void createTemplateFromSchema() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String did = keymaster.createSchema(); + keymaster.setSchema(did, TestFixtures.mockSchema()); + + Map template = keymaster.createTemplate(did); + Map expected = Map.of( + "$schema", did, + "email", "TBD" + ); + assertEquals(expected, template); + } + + @Test + void createTemplateMissingDidThrows() { + Keymaster keymaster = newKeymaster(); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.createTemplate("bogus") + ); + assertEquals("Invalid parameter: schemaId", error.getMessage()); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveUtilsTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveUtilsTest.java new file mode 100644 index 000000000..6cd12a4c6 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveUtilsTest.java @@ -0,0 +1,360 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.Map; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.testutil.LiveTestSupport; + +@Tag("live") +class LiveUtilsTest { + @TempDir + Path tempDir; + + private Keymaster newKeymaster() { + return LiveTestSupport.keymaster(tempDir); + } + + @Test + void resolveDidResolvesNewId() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID(did); + + assertEquals(did, doc.didDocument.id); + } + + @Test + void resolveDidResolvesIdName() { + Keymaster keymaster = newKeymaster(); + String did = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID("Bob"); + + assertEquals(did, doc.didDocument.id); + } + + @Test + void resolveDidResolvesAssetName() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + keymaster.addName("mock", dataDid); + + MdipDocument docByDid = keymaster.resolveDID(dataDid); + MdipDocument docByName = keymaster.resolveDID("mock"); + + assertEquals(docByDid.didDocument.id, docByName.didDocument.id); + assertEquals(docByDid.didDocumentData, docByName.didDocumentData); + } + + @Test + void resolveDidInvalidNameThrows() { + Keymaster keymaster = newKeymaster(); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.resolveDID("mock") + ); + assertEquals("unknown id", error.getMessage()); + } + + @Test + void updateDidMissingIdThrows() { + Keymaster keymaster = newKeymaster(); + MdipDocument doc = new MdipDocument(); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.updateDID(doc) + ); + assertTrue(error.getMessage().contains("doc.didDocument.id")); + } + + @Test + void updateDidUpdatesAsset() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + MdipDocument doc = keymaster.resolveDID(dataDid); + + Map dataUpdated = Map.of("name", "updated"); + doc.didDocumentData = dataUpdated; + + boolean ok = keymaster.updateDID(doc); + MdipDocument doc2 = keymaster.resolveDID(dataDid); + + assertTrue(ok); + assertEquals(dataUpdated, doc2.didDocumentData); + assertEquals("2", doc2.didDocumentMetadata.version); + } + + @Test + void updateDidNoChangesKeepsVersion() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor", "val", 1234); + String dataDid = keymaster.createAsset(mockAnchor); + MdipDocument doc = keymaster.resolveDID(dataDid); + + doc.didDocumentData = Map.of("val", 1234, "name", "mockAnchor"); + boolean ok = keymaster.updateDID(doc); + MdipDocument doc2 = keymaster.resolveDID(dataDid); + + assertTrue(ok); + assertEquals(mockAnchor, doc2.didDocumentData); + assertEquals("1", doc2.didDocumentMetadata.version); + } + + @Test + void updateDidOwnerInWallet() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + keymaster.createId("Alice"); + + keymaster.setCurrentId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + MdipDocument doc = keymaster.resolveDID(dataDid); + + Map dataUpdated = Map.of("name", "updated"); + doc.didDocumentData = dataUpdated; + + keymaster.setCurrentId("Alice"); + + boolean ok = keymaster.updateDID(doc); + MdipDocument doc2 = keymaster.resolveDID(dataDid); + + assertTrue(ok); + assertEquals(bob, doc2.didDocument.controller); + assertEquals(dataUpdated, doc2.didDocumentData); + assertEquals("2", doc2.didDocumentMetadata.version); + } + + @Test + void updateDidOwnerNotInWalletThrows() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + keymaster.createId("Alice"); + keymaster.setCurrentId("Bob"); + + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + MdipDocument doc = keymaster.resolveDID(dataDid); + + doc.didDocumentData = Map.of("name", "updated"); + + keymaster.setCurrentId("Alice"); + keymaster.removeId("Bob"); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.updateDID(doc) + ); + assertEquals("unknown id", error.getMessage()); + } + + @Test + void revokeDidRevokesAsset() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + + boolean ok = keymaster.revokeDID(dataDid); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertTrue(ok); + assertEquals(dataDid, doc.didDocument.id); + assertEquals(Map.of(), doc.didDocumentData); + assertEquals(true, doc.didDocumentMetadata.deactivated); + } + + @Test + void revokeDidWhenCurrentNotOwner() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + keymaster.createId("Alice"); + + keymaster.setCurrentId("Bob"); + Map mockAnchor = Map.of("name", "mockAnchor"); + String dataDid = keymaster.createAsset(mockAnchor); + + keymaster.setCurrentId("Alice"); + boolean ok = keymaster.revokeDID(dataDid); + MdipDocument doc = keymaster.resolveDID(dataDid); + + assertTrue(ok); + assertEquals(dataDid, doc.didDocument.id); + assertEquals(Map.of(), doc.didDocumentData); + assertEquals(true, doc.didDocumentMetadata.deactivated); + } + + @Test + void removeFromOwnedReturnsFalseWhenNothingOwned() { + Keymaster keymaster = newKeymaster(); + String owner = keymaster.createId("Alice"); + + boolean ok = keymaster.removeFromOwned("did:mock", owner); + assertFalse(ok); + } + + @Test + void rotateKeysUpdatesDidDoc() { + Keymaster keymaster = newKeymaster(); + String alice = keymaster.createId("Alice"); + MdipDocument doc = keymaster.resolveDID(alice); + MdipDocument.VerificationMethod vm = doc.didDocument.verificationMethod.get(0); + org.keychain.gatekeeper.model.EcdsaJwkPublic pubkey = vm.publicKeyJwk; + + for (int i = 0; i < 3; i += 1) { + keymaster.rotateKeys(); + + doc = keymaster.resolveDID(alice); + vm = doc.didDocument.verificationMethod.get(0); + + assertNotEquals(pubkey.x, vm.publicKeyJwk.x); + assertNotEquals(pubkey.y, vm.publicKeyJwk.y); + + pubkey = vm.publicKeyJwk; + } + } + + @Test + void rotateKeysDecryptsMessages() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + String bob = keymaster.createId("Bob"); + java.util.List secrets = new java.util.ArrayList<>(); + String msg = "Hi Bob!"; + + for (int i = 0; i < 3; i += 1) { + keymaster.setCurrentId("Alice"); + String did = keymaster.encryptMessage(msg, bob); + secrets.add(did); + + keymaster.rotateKeys(); + + keymaster.setCurrentId("Bob"); + keymaster.rotateKeys(); + } + + for (String secret : secrets) { + keymaster.setCurrentId("Alice"); + String decipher1 = keymaster.decryptMessage(secret); + assertEquals(msg, decipher1); + + keymaster.setCurrentId("Bob"); + String decipher2 = keymaster.decryptMessage(secret); + assertEquals(msg, decipher2); + } + } + + @Test + void getPublicKeyJwkReturnsFirstKey() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID(bob); + org.keychain.gatekeeper.model.EcdsaJwkPublic publicKeyJwk = keymaster.getPublicKeyJwk(doc); + + assertEquals(doc.didDocument.verificationMethod.get(0).publicKeyJwk, publicKeyJwk); + } + + @Test + void getPublicKeyJwkThrowsWhenNotAgentDoc() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String did = keymaster.createAsset(Map.of("name", "mockAnchor")); + MdipDocument doc = keymaster.resolveDID(did); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.getPublicKeyJwk(doc) + ); + assertEquals("Missing didDocument.", error.getMessage()); + } + + @Test + void getPublicKeyJwkThrowsWhenDidDocumentMissing() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID(bob); + doc.didDocument = null; + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.getPublicKeyJwk(doc) + ); + assertEquals("Missing didDocument.", error.getMessage()); + } + + @Test + void getPublicKeyJwkThrowsWhenKeyMissing() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID(bob); + doc.didDocument.verificationMethod.get(0).publicKeyJwk = null; + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.getPublicKeyJwk(doc) + ); + assertEquals("The publicKeyJwk is missing in the first verification method.", error.getMessage()); + } + + @Test + void getAgentDidReturnsDid() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID(bob); + + String did = keymaster.getAgentDID(doc); + assertEquals(bob, did); + } + + @Test + void getAgentDidThrowsWhenNotAgent() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String did = keymaster.createAsset(Map.of("name", "mockAnchor")); + MdipDocument doc = keymaster.resolveDID(did); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.getAgentDID(doc) + ); + assertEquals("Document is not an agent", error.getMessage()); + } + + @Test + void getAgentDidThrowsWhenDidMissing() { + Keymaster keymaster = newKeymaster(); + String bob = keymaster.createId("Bob"); + MdipDocument doc = keymaster.resolveDID(bob); + doc.didDocument = null; + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> keymaster.getAgentDID(doc) + ); + assertEquals("Agent document does not have a DID", error.getMessage()); + } + + @Test + void listRegistriesReturnsLocalAndHyperswarm() { + Keymaster keymaster = newKeymaster(); + java.util.List registries = keymaster.listRegistries(); + + assertTrue(registries.contains("local")); + assertTrue(registries.contains("hyperswarm")); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/LiveWalletTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/LiveWalletTest.java new file mode 100644 index 000000000..4b1181e0d --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/LiveWalletTest.java @@ -0,0 +1,883 @@ +package org.keychain.keymaster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.crypto.HdKeyUtil; +import org.keychain.crypto.JwkPair; +import org.keychain.crypto.KeymasterCryptoImpl; +import org.keychain.crypto.MnemonicEncryption; +import org.keychain.gatekeeper.GatekeeperInterface; +import org.keychain.gatekeeper.model.MdipDocument; +import org.keychain.keymaster.model.CheckWalletResult; +import org.keychain.keymaster.model.FixWalletResult; +import org.keychain.keymaster.model.HdKey; +import org.keychain.keymaster.model.IDInfo; +import org.keychain.keymaster.model.Seed; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.model.WalletFile; +import org.keychain.keymaster.store.WalletJson; +import org.keychain.keymaster.store.WalletJsonMapper; +import org.keychain.keymaster.testutil.LiveTestSupport; +import org.keychain.keymaster.testutil.TestFixtures; + +@Tag("live") +class LiveWalletTest { + private static final String PASSPHRASE = LiveTestSupport.DEFAULT_PASSPHRASE; + private static final String MNEMONIC = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + @TempDir + Path tempDir; + + private WalletJson newStore(String name) { + return LiveTestSupport.walletStore(tempDir.resolve(name)); + } + + private Keymaster newKeymaster(WalletJson store) { + return new Keymaster(store, LiveTestSupport.gatekeeperClient(), PASSPHRASE, LiveTestSupport.DEFAULT_REGISTRY); + } + + private Keymaster newKeymaster() { + return newKeymaster(newStore("wallet")); + } + + private GatekeeperInterface gatekeeperClient() { + return LiveTestSupport.gatekeeperClient(); + } + + @Test + void loadWalletCreatesWalletOnFirstLoad() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + WalletFile wallet = keymaster.loadWallet(); + + assertNotNull(wallet); + assertEquals(1, wallet.version); + assertEquals(0, wallet.counter); + assertNotNull(wallet.seed); + assertNotNull(wallet.seed.mnemonicEnc); + assertNotNull(wallet.seed.mnemonicEnc.salt); + assertNotNull(wallet.seed.mnemonicEnc.iv); + assertNotNull(wallet.seed.mnemonicEnc.data); + assertNotNull(wallet.ids); + assertEquals(0, wallet.ids.size()); + } + + @Test + void loadWalletReturnsSameInstanceFromCache() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + WalletFile wallet1 = keymaster.loadWallet(); + WalletFile wallet2 = keymaster.loadWallet(); + + assertSame(wallet1, wallet2); + } + + @Test + void loadWalletThrowsOnIncorrectPassphrase() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + keymaster.newWallet(MNEMONIC, true); + + try { + Keymaster wrong = new Keymaster(store, LiveTestSupport.gatekeeperClient(), "incorrect", LiveTestSupport.DEFAULT_REGISTRY); + wrong.loadWallet(); + throw new IllegalStateException("expected exception"); + } catch (IllegalStateException e) { + assertEquals("Mnemonic decryption failed", e.getMessage()); + } + } + + @Test + void saveWalletStoresAndLoads() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + WalletFile wallet = buildWallet(0); + assertTrue(keymaster.saveWallet(wallet, true)); + + WalletFile loaded = keymaster.loadWallet(); + assertEquals(wallet.counter, loaded.counter); + assertEquals(wallet.version, loaded.version); + assertEquals(wallet.seed.mnemonicEnc.salt, loaded.seed.mnemonicEnc.salt); + assertEquals(wallet.seed.mnemonicEnc.iv, loaded.seed.mnemonicEnc.iv); + assertEquals(wallet.seed.mnemonicEnc.data, loaded.seed.mnemonicEnc.data); + } + + @Test + void saveWalletIgnoresOverwriteFlagIfUnnecessary() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + WalletFile wallet = buildWallet(0); + assertTrue(keymaster.saveWallet(wallet, false)); + WalletFile loaded = keymaster.loadWallet(); + assertEquals(wallet.counter, loaded.counter); + } + + @Test + void saveWalletOverwritesExistingWallet() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + WalletFile wallet = buildWallet(0); + assertTrue(keymaster.saveWallet(wallet, true)); + + WalletFile updated = buildWallet(1); + assertTrue(keymaster.saveWallet(updated, true)); + + WalletFile loaded = keymaster.loadWallet(); + assertEquals(1, loaded.counter); + } + + @Test + void saveWalletDoesNotOverwriteWhenFlagFalse() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + WalletFile wallet = buildWallet(0); + assertTrue(keymaster.saveWallet(wallet, true)); + + WalletFile updated = buildWallet(2); + assertFalse(keymaster.saveWallet(updated, false)); + + WalletFile loaded = keymaster.loadWallet(); + assertEquals(0, loaded.counter); + } + + @Test + void saveWalletOverwritesInLoop() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + for (int i = 0; i < 5; i += 1) { + WalletFile wallet = buildWallet(i); + assertTrue(keymaster.saveWallet(wallet, true)); + WalletFile loaded = keymaster.loadWallet(); + assertEquals(i, loaded.counter); + } + } + + @Test + void saveWalletPersistsAugmentedWallet() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + WalletFile wallet = buildWallet(0); + IDInfo info = new IDInfo(); + info.did = "did:test:QmYwAPJzv5CZsnAzt8auV2V4ZZFZ5JYh5rS4Qh1zS4x2o7"; + info.account = 0; + info.index = 0; + wallet.ids.put("Bob", info); + wallet.extras = new HashMap<>(); + wallet.extras.put("meta", "value"); + assertTrue(keymaster.saveWallet(wallet, true)); + + WalletFile loaded = keymaster.loadWallet(); + assertNotNull(loaded.ids.get("Bob")); + assertEquals("value", loaded.extras.get("meta")); + } + + @Test + void newWalletOverwritesExistingWhenAllowed() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + WalletFile wallet1 = keymaster.loadWallet(); + WalletFile wallet2 = keymaster.newWallet(null, true); + + assertNotEquals(wallet1.seed.mnemonicEnc.data, wallet2.seed.mnemonicEnc.data); + } + + @Test + void newWalletDoesNotOverwriteByDefault() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + keymaster.loadWallet(); + + try { + keymaster.newWallet(null, false); + throw new IllegalStateException("expected exception"); + } catch (IllegalStateException e) { + assertEquals("save wallet failed", e.getMessage()); + } + } + + @Test + void newWalletCreatesFromMnemonic() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + keymaster.newWallet(MNEMONIC, true); + String decrypted = keymaster.decryptMnemonic(); + assertEquals(MNEMONIC, decrypted); + } + + @Test + void decryptMnemonicReturns12Words() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + String mnemonic = keymaster.decryptMnemonic(); + List words = List.of(mnemonic.split(" ")); + assertEquals(12, words.size()); + } + + @Test + void exportEncryptedWalletReturnsEncryptedPayload() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + keymaster.loadWallet(); + + WalletEncFile exported = keymaster.exportEncryptedWallet(); + assertNotNull(exported); + assertNotNull(exported.seed); + assertNotNull(exported.seed.mnemonicEnc); + assertNotNull(exported.enc); + } + + @Test + void mutateWalletCreatesWalletIfMissing() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + + keymaster.mutateWallet(wallet -> wallet.counter = 1); + + WalletFile wallet = keymaster.loadWallet(); + assertEquals(1, wallet.counter); + } + + @Test + void loadWalletUpgradesLegacyV0() { + WalletJson store = newStore("wallet"); + + WalletFile legacy = buildLegacyV0Wallet(); + WalletEncFile stored = new WalletEncFile(); + stored.version = 0; + stored.seed = legacy.seed; + stored.extra.put("counter", legacy.counter); + stored.extra.put("ids", legacy.ids); + stored.extra.put("current", legacy.current); + + store.saveWallet(stored, true); + Keymaster keymaster = newKeymaster(store); + + WalletFile upgraded = keymaster.loadWallet(); + assertEquals(1, upgraded.version); + assertNotNull(upgraded.seed); + assertNotNull(upgraded.seed.mnemonicEnc); + assertEquals(legacy.counter, upgraded.counter); + assertEquals(legacy.ids.size(), upgraded.ids.size()); + } + + @Test + void loadWalletUpgradesLegacyEncryptedWrapper() { + WalletJson store = newStore("wallet"); + WalletFile legacy = buildLegacyV0Wallet(); + WalletEncFile wrapper = buildLegacyEncryptedWrapper(legacy); + store.saveWallet(wrapper, true); + + Keymaster keymaster = newKeymaster(store); + WalletFile upgraded = keymaster.loadWallet(); + + assertEquals(1, upgraded.version); + assertNotNull(upgraded.seed); + assertNotNull(upgraded.seed.mnemonicEnc); + } + + @Test + void loadWalletV1EncryptedDoesNotExposeHdKey() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + keymaster.loadWallet(); + WalletEncFile exported = keymaster.exportEncryptedWallet(); + + WalletJson store2 = newStore("wallet2"); + store2.saveWallet(exported, true); + Keymaster keymaster2 = newKeymaster(store2); + WalletFile loaded = keymaster2.loadWallet(); + + assertNotNull(loaded.seed); + assertNull(loaded.seed.hdkey); + } + + @Test + void loadWalletV1EncryptedCacheDoesNotExposeHdKey() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + keymaster.loadWallet(); + WalletEncFile exported = keymaster.exportEncryptedWallet(); + + WalletJson store2 = newStore("wallet2"); + store2.saveWallet(exported, true); + Keymaster keymaster2 = newKeymaster(store2); + keymaster2.loadWallet(); + WalletFile loaded = keymaster2.loadWallet(); + + assertNotNull(loaded.seed); + assertNull(loaded.seed.hdkey); + } + + @Test + void saveWalletUpgradesLegacyV0() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + WalletFile legacy = buildLegacyV0Wallet(); + + assertTrue(keymaster.saveWallet(legacy, true)); + + WalletEncFile stored = store.loadWallet(); + assertNotNull(stored); + assertEquals(1, stored.version); + assertNotNull(stored.seed); + assertNotNull(stored.seed.mnemonicEnc); + assertNotNull(stored.enc); + } + + @Test + void saveWalletLegacyUpgradeDoesNotUseStaleCache() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + keymaster.newWallet(null, true); + + WalletFile legacy = buildLegacyV0Wallet(); + assertTrue(keymaster.saveWallet(legacy, true)); + } + + @Test + void saveWalletEncryptsV1AndRemovesHdKey() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + WalletFile wallet = keymaster.newWallet(MNEMONIC, true); + + wallet.seed.hdkey = toModelHdKey(HdKeyUtil.masterFromMnemonic(MNEMONIC)); + + assertTrue(keymaster.saveWallet(wallet, true)); + + WalletEncFile stored = store.loadWallet(); + assertNotNull(stored.seed); + assertNull(stored.seed.hdkey); + assertNotNull(stored.enc); + } + + @Test + void saveWalletAcceptsV1EncryptedWallet() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + WalletEncFile exported = keymaster.exportEncryptedWallet(); + + WalletJson store2 = newStore("wallet2"); + Keymaster keymaster2 = newKeymaster(store2); + assertTrue(keymaster2.saveWallet(exported, true)); + } + + @Test + void saveWalletRejectsEncryptedWalletWithWrongPassphrase() { + WalletJson store = newStore("wallet"); + Keymaster keymaster = newKeymaster(store); + WalletEncFile exported = keymaster.exportEncryptedWallet(); + + WalletJson store2 = newStore("wallet2"); + Keymaster wrong = new Keymaster(store2, LiveTestSupport.gatekeeperClient(), "incorrect", LiveTestSupport.DEFAULT_REGISTRY); + try { + wrong.saveWallet(exported, true); + throw new IllegalStateException("expected exception"); + } catch (IllegalStateException e) { + assertEquals("Keymaster: Incorrect passphrase.", e.getMessage()); + } + } + + @Test + void loadWalletThrowsOnUnsupportedVersion() { + WalletJson store = newStore("wallet"); + WalletEncFile stored = new WalletEncFile(); + stored.version = 1; + stored.seed = new Seed(); + store.saveWallet(stored, true); + + Keymaster keymaster = newKeymaster(store); + try { + keymaster.loadWallet(); + throw new IllegalStateException("expected exception"); + } catch (IllegalStateException e) { + assertEquals("Keymaster: Unsupported wallet version.", e.getMessage()); + } + } + + @Test + void updateSeedBankThrowsOnMissingDid() { + Keymaster keymaster = newKeymaster(); + + try { + keymaster.updateSeedBank(new MdipDocument()); + throw new IllegalStateException("expected exception"); + } catch (IllegalArgumentException e) { + assertEquals("Invalid parameter: seed bank missing DID", e.getMessage()); + } + } + + @Test + void resolveSeedBankIsDeterministic() { + Keymaster keymaster = newKeymaster(); + + MdipDocument bank1 = keymaster.resolveSeedBank(); + MdipDocument bank2 = keymaster.resolveSeedBank(); + + assertNotNull(bank1); + assertNotNull(bank2); + assertNotNull(bank1.didDocument); + assertNotNull(bank2.didDocument); + assertEquals(bank1.didDocument.id, bank2.didDocument.id); + } + + @Test + void backupWalletReturnsDid() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + String did = keymaster.backupWallet(); + MdipDocument doc = keymaster.resolveDID(did); + + assertNotNull(doc); + assertEquals(did, doc.didDocument.id); + } + + @Test + void backupWalletStoresDidInSeedBank() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + + String did = keymaster.backupWallet(); + MdipDocument bank = keymaster.resolveSeedBank(); + assertNotNull(bank); + assertNotNull(bank.didDocumentData); + assertInstanceOf(Map.class, bank.didDocumentData); + @SuppressWarnings("unchecked") + Map data = (Map) bank.didDocumentData; + assertEquals(did, data.get("wallet")); + } + + @Test + void recoverWalletFromSeedBank() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + WalletFile wallet = keymaster.loadWallet(); + String mnemonic = keymaster.decryptMnemonic(); + keymaster.backupWallet(); + + keymaster.newWallet(mnemonic, true); + WalletFile recovered = keymaster.recoverWallet(); + + assertEquals(wallet.counter, recovered.counter); + assertEquals(wallet.version, recovered.version); + assertNotNull(recovered.seed); + assertNotNull(recovered.seed.mnemonicEnc); + assertEquals(wallet.current, recovered.current); + assertIdsMatch(wallet.ids, recovered.ids); + } + + @Test + void recoverOverExistingWallet() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + keymaster.loadWallet(); + keymaster.backupWallet(); + keymaster.createId("Alice"); + + WalletFile recovered = keymaster.recoverWallet(); + + assertEquals(1, recovered.counter); + assertEquals("Bob", recovered.current); + assertNotNull(recovered.seed); + assertNotNull(recovered.seed.mnemonicEnc); + assertNotNull(recovered.ids); + assertTrue(recovered.ids.containsKey("Bob")); + } + + @Test + void recoverAugmentedWalletFromSeedBank() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + WalletFile wallet = keymaster.loadWallet(); + String mnemonic = keymaster.decryptMnemonic(); + + IDInfo idInfo = wallet.ids.get("Bob"); + idInfo.extras = new HashMap<>(); + idInfo.extras.put("icon", "smiley"); + wallet.extras = new HashMap<>(); + wallet.extras.put("foo", "bar"); + keymaster.saveWallet(wallet, true); + keymaster.backupWallet(); + + keymaster.newWallet(mnemonic, true); + WalletFile recovered = keymaster.recoverWallet(); + + assertNotNull(recovered.extras); + assertEquals("bar", recovered.extras.get("foo")); + assertNotNull(recovered.ids.get("Bob").extras); + assertEquals("smiley", recovered.ids.get("Bob").extras.get("icon")); + } + + @Test + void recoverV0WalletFromSeedBank() { + Keymaster keymaster = newKeymaster(); + WalletFile legacy = buildLegacyV0Wallet(); + keymaster.saveWallet(legacy, true); + String mnemonic = keymaster.decryptMnemonic(); + keymaster.backupWallet(LiveTestSupport.DEFAULT_REGISTRY, legacy); + + keymaster.newWallet(mnemonic, true); + WalletFile recovered = keymaster.recoverWallet(); + assertNotNull(recovered); + assertIdsMatch(legacy.ids, recovered.ids); + } + + @Test + void recoverWalletFromBackupDid() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + WalletFile wallet = keymaster.loadWallet(); + String mnemonic = keymaster.decryptMnemonic(); + String did = keymaster.backupWallet(); + + keymaster.newWallet(mnemonic, true); + WalletFile recovered = keymaster.recoverWallet(did); + + assertEquals(wallet.counter, recovered.counter); + assertEquals(wallet.version, recovered.version); + assertNotNull(recovered.seed); + assertNotNull(recovered.seed.mnemonicEnc); + assertEquals(wallet.current, recovered.current); + assertIdsMatch(wallet.ids, recovered.ids); + } + + @Test + void recoverDoesNothingWhenNoBackup() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Bob"); + String mnemonic = keymaster.decryptMnemonic(); + + keymaster.newWallet(mnemonic, true); + WalletFile recovered = keymaster.recoverWallet(); + + assertTrue(recovered.ids.isEmpty()); + } + + @Test + void recoverDoesNothingWhenBackupDidInvalid() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Bob"); + String mnemonic = keymaster.decryptMnemonic(); + + keymaster.newWallet(mnemonic, true); + WalletFile recovered = keymaster.recoverWallet(agentDid); + + assertTrue(recovered.ids.isEmpty()); + } + + @Test + void checkWalletEmpty() { + Keymaster keymaster = newKeymaster(); + + CheckWalletResult result = keymaster.checkWallet(); + assertEquals(0, result.checked); + assertEquals(0, result.invalid); + assertEquals(0, result.deleted); + } + + @Test + void checkWalletSingleId() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + + CheckWalletResult result = keymaster.checkWallet(); + assertEquals(1, result.checked); + assertEquals(0, result.invalid); + assertEquals(0, result.deleted); + } + + @Test + void checkWalletDetectsRevokedId() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Alice"); + keymaster.revokeDID(agentDid); + + CheckWalletResult result = keymaster.checkWallet(); + assertEquals(1, result.checked); + assertEquals(0, result.invalid); + assertEquals(1, result.deleted); + } + + @Test + void checkWalletDetectsRemovedDids() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Alice"); + String schemaDid = keymaster.createSchema(); + keymaster.addName("schema", schemaDid); + gatekeeperClient().removeDIDs(List.of(agentDid, schemaDid)); + + CheckWalletResult result = keymaster.checkWallet(); + assertEquals(3, result.checked); + assertEquals(3, result.invalid); + assertEquals(0, result.deleted); + } + + @Test + void checkWalletDetectsInvalidDids() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + keymaster.addToOwned("did:test:mock1", null); + keymaster.addToHeld("did:test:mock2"); + + CheckWalletResult result = keymaster.checkWallet(); + assertEquals(3, result.checked); + assertEquals(2, result.invalid); + assertEquals(0, result.deleted); + } + + @Test + void checkWalletDetectsRevokedCredentials() { + Keymaster keymaster = newKeymaster(); + List credentials = setupCredentials(keymaster); + keymaster.addName("credential-0", credentials.get(0)); + keymaster.addName("credential-2", credentials.get(2)); + keymaster.revokeCredential(credentials.get(0)); + keymaster.revokeCredential(credentials.get(2)); + + CheckWalletResult result = keymaster.checkWallet(); + assertEquals(16, result.checked); + assertEquals(0, result.invalid); + assertEquals(4, result.deleted); + } + + @Test + void fixWalletEmpty() { + Keymaster keymaster = newKeymaster(); + + FixWalletResult result = keymaster.fixWallet(); + assertEquals(0, result.idsRemoved); + assertEquals(0, result.ownedRemoved); + assertEquals(0, result.heldRemoved); + assertEquals(0, result.namesRemoved); + } + + @Test + void fixWalletSingleId() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + + FixWalletResult result = keymaster.fixWallet(); + assertEquals(0, result.idsRemoved); + assertEquals(0, result.ownedRemoved); + assertEquals(0, result.heldRemoved); + assertEquals(0, result.namesRemoved); + } + + @Test + void fixWalletRemovesRevokedId() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Alice"); + keymaster.revokeDID(agentDid); + + FixWalletResult result = keymaster.fixWallet(); + assertEquals(1, result.idsRemoved); + assertEquals(0, result.ownedRemoved); + assertEquals(0, result.heldRemoved); + assertEquals(0, result.namesRemoved); + } + + @Test + void fixWalletRemovesDeletedDids() { + Keymaster keymaster = newKeymaster(); + String agentDid = keymaster.createId("Alice"); + String schemaDid = keymaster.createSchema(); + keymaster.addName("schema", schemaDid); + gatekeeperClient().removeDIDs(List.of(agentDid, schemaDid)); + + FixWalletResult result = keymaster.fixWallet(); + assertEquals(1, result.idsRemoved); + assertEquals(0, result.ownedRemoved); + assertEquals(0, result.heldRemoved); + assertEquals(1, result.namesRemoved); + } + + @Test + void fixWalletRemovesInvalidDids() { + Keymaster keymaster = newKeymaster(); + keymaster.createId("Alice"); + keymaster.addToOwned("did:test:mock1", null); + keymaster.addToHeld("did:test:mock2"); + + FixWalletResult result = keymaster.fixWallet(); + assertEquals(0, result.idsRemoved); + assertEquals(1, result.ownedRemoved); + assertEquals(1, result.heldRemoved); + assertEquals(0, result.namesRemoved); + } + + @Test + void fixWalletRemovesRevokedCredentials() { + Keymaster keymaster = newKeymaster(); + List credentials = setupCredentials(keymaster); + keymaster.addName("credential-0", credentials.get(0)); + keymaster.addName("credential-2", credentials.get(2)); + keymaster.revokeCredential(credentials.get(0)); + keymaster.revokeCredential(credentials.get(2)); + + FixWalletResult result = keymaster.fixWallet(); + assertEquals(0, result.idsRemoved); + assertEquals(0, result.ownedRemoved); + assertEquals(2, result.heldRemoved); + assertEquals(2, result.namesRemoved); + } + + private static WalletFile buildWallet(int counter) { + WalletFile wallet = new WalletFile(); + wallet.version = 1; + wallet.counter = counter; + wallet.ids = new HashMap<>(); + + Seed seed = new Seed(); + seed.mnemonicEnc = MnemonicEncryption.encrypt(MNEMONIC, PASSPHRASE); + wallet.seed = seed; + return wallet; + } + + private static WalletFile buildLegacyV0Wallet() { + WalletFile wallet = new WalletFile(); + wallet.version = 0; + wallet.counter = 1; + wallet.ids = new HashMap<>(); + + IDInfo info = new IDInfo(); + info.did = "did:test:QmYwAPJzv5CZsnAzt8auV2V4ZZFZ5JYh5rS4Qh1zS4x2o7"; + info.account = 0; + info.index = 0; + wallet.ids.put("id_1", info); + wallet.current = "id_1"; + + var master = HdKeyUtil.masterFromMnemonic(MNEMONIC); + HdKey hdkey = toModelHdKey(master); + KeymasterCryptoImpl crypto = new KeymasterCryptoImpl(); + JwkPair keypair = crypto.generateJwk(HdKeyUtil.privateKeyBytes(master)); + + Seed seed = new Seed(); + seed.hdkey = hdkey; + seed.mnemonic = crypto.encryptMessage(keypair.publicJwk, keypair.privateJwk, MNEMONIC); + wallet.seed = seed; + return wallet; + } + + private static WalletEncFile buildLegacyEncryptedWrapper(WalletFile wallet) { + try { + byte[] salt = new byte[16]; + byte[] iv = new byte[12]; + new SecureRandom().nextBytes(salt); + new SecureRandom().nextBytes(iv); + + SecretKey key = deriveLegacyKey(salt); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); + byte[] plaintext = WalletJsonMapper.mapper().writeValueAsString(wallet).getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = cipher.doFinal(plaintext); + + WalletEncFile stored = new WalletEncFile(); + stored.salt = Base64.getEncoder().encodeToString(salt); + stored.iv = Base64.getEncoder().encodeToString(iv); + stored.data = Base64.getEncoder().encodeToString(ciphertext); + return stored; + } catch (Exception e) { + throw new IllegalStateException("Failed to build legacy wrapper", e); + } + } + + private static SecretKey deriveLegacyKey(byte[] salt) throws GeneralSecurityException { + PBEKeySpec spec = new PBEKeySpec(PASSPHRASE.toCharArray(), salt, 100_000, 256); + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + byte[] keyBytes = factory.generateSecret(spec).getEncoded(); + return new SecretKeySpec(keyBytes, "AES"); + } + + private static HdKey toModelHdKey(org.bitcoinj.crypto.DeterministicKey master) { + org.keychain.crypto.HdKey cryptoHdKey = HdKeyUtil.toHdKey(master); + HdKey hdkey = new HdKey(); + hdkey.xpriv = cryptoHdKey.xpriv; + hdkey.xpub = cryptoHdKey.xpub; + return hdkey; + } + + private static void assertIdsMatch(Map expected, Map actual) { + assertNotNull(expected); + assertNotNull(actual); + assertEquals(expected.keySet(), actual.keySet()); + for (Map.Entry entry : expected.entrySet()) { + IDInfo expectedInfo = entry.getValue(); + IDInfo actualInfo = actual.get(entry.getKey()); + assertNotNull(actualInfo); + assertEquals(expectedInfo.did, actualInfo.did); + assertEquals(expectedInfo.account, actualInfo.account); + assertEquals(expectedInfo.index, actualInfo.index); + } + } + + private static List setupCredentials(Keymaster keymaster) { + keymaster.createId("Alice"); + keymaster.createId("Bob"); + String carol = keymaster.createId("Carol"); + keymaster.createId("Victor"); + + keymaster.setCurrentId("Alice"); + String credential1 = keymaster.createSchema(TestFixtures.mockSchema()); + String credential2 = keymaster.createSchema(TestFixtures.mockSchema()); + + Map bc1 = keymaster.bindCredential(credential1, carol); + Map bc2 = keymaster.bindCredential(credential2, carol); + + String vc1 = keymaster.issueCredential(bc1); + String vc2 = keymaster.issueCredential(bc2); + + keymaster.setCurrentId("Bob"); + String credential3 = keymaster.createSchema(TestFixtures.mockSchema()); + String credential4 = keymaster.createSchema(TestFixtures.mockSchema()); + + Map bc3 = keymaster.bindCredential(credential3, carol); + Map bc4 = keymaster.bindCredential(credential4, carol); + + String vc3 = keymaster.issueCredential(bc3); + String vc4 = keymaster.issueCredential(bc4); + + keymaster.setCurrentId("Carol"); + keymaster.acceptCredential(vc1); + keymaster.acceptCredential(vc2); + keymaster.acceptCredential(vc3); + keymaster.acceptCredential(vc4); + + return List.of(vc1, vc2, vc3, vc4); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/store/WalletJsonMemoryTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/store/WalletJsonMemoryTest.java new file mode 100644 index 000000000..9d25254c4 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/store/WalletJsonMemoryTest.java @@ -0,0 +1,60 @@ +package org.keychain.keymaster.store; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import org.junit.jupiter.api.Test; +import org.keychain.keymaster.model.Seed; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.model.WalletFile; + +class WalletJsonMemoryTest { + @Test + void saveLoadRoundTrip() { + WalletJsonMemory store = new WalletJsonMemory<>(WalletEncFile.class); + WalletEncFile wallet = new WalletEncFile(); + wallet.version = 1; + wallet.seed = new Seed(); + wallet.enc = "cipher"; + + assertTrue(store.saveWallet(wallet, false)); + WalletEncFile loaded = store.loadWallet(); + assertNotNull(loaded); + assertEquals(1, loaded.version); + assertEquals("cipher", loaded.enc); + } + + @Test + void overwriteIsRespected() { + WalletJsonMemory store = new WalletJsonMemory<>(WalletFile.class); + WalletFile wallet1 = new WalletFile(); + wallet1.version = 1; + wallet1.counter = 0; + wallet1.seed = new Seed(); + wallet1.ids = new HashMap<>(); + + WalletFile wallet2 = new WalletFile(); + wallet2.version = 1; + wallet2.counter = 1; + wallet2.seed = new Seed(); + wallet2.ids = new HashMap<>(); + + assertTrue(store.saveWallet(wallet1, false)); + assertFalse(store.saveWallet(wallet2, false)); + assertTrue(store.saveWallet(wallet2, true)); + + WalletFile loaded = store.loadWallet(); + assertNotNull(loaded); + assertEquals(1, loaded.counter); + } + + @Test + void loadReturnsNullIfEmpty() { + WalletJsonMemory store = new WalletJsonMemory<>(WalletFile.class); + assertNull(store.loadWallet()); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/store/WalletJsonTest.java b/java/keymaster/src/test/java/org/keychain/keymaster/store/WalletJsonTest.java new file mode 100644 index 000000000..2da1e8cbf --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/store/WalletJsonTest.java @@ -0,0 +1,67 @@ +package org.keychain.keymaster.store; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.keychain.keymaster.model.Seed; +import org.keychain.keymaster.model.WalletFile; + +class WalletJsonTest { + @TempDir + Path tempDir; + + @Test + void saveLoadRoundTrip() { + WalletJson store = new WalletJson<>(WalletFile.class, tempDir, "wallet.json"); + WalletFile wallet = new WalletFile(); + wallet.version = 1; + wallet.counter = 0; + wallet.seed = new Seed(); + wallet.ids = new HashMap<>(); + + assertTrue(store.saveWallet(wallet, false)); + WalletFile loaded = store.loadWallet(); + assertNotNull(loaded); + assertEquals(1, loaded.version); + assertEquals(0, loaded.counter); + } + + @Test + void overwriteIsRespected() { + WalletJson store = new WalletJson<>(WalletFile.class, tempDir, "wallet.json"); + WalletFile wallet1 = new WalletFile(); + wallet1.version = 1; + wallet1.counter = 0; + wallet1.seed = new Seed(); + wallet1.ids = new HashMap<>(); + + WalletFile wallet2 = new WalletFile(); + wallet2.version = 1; + wallet2.counter = 2; + wallet2.seed = new Seed(); + wallet2.ids = new HashMap<>(); + + assertTrue(store.saveWallet(wallet1, false)); + assertFalse(store.saveWallet(wallet2, false)); + assertTrue(store.saveWallet(wallet2, true)); + + WalletFile loaded = store.loadWallet(); + assertNotNull(loaded); + assertEquals(2, loaded.counter); + } + + @Test + void loadReturnsNullIfMissing() { + WalletJson store = new WalletJson<>(WalletFile.class, tempDir, "wallet.json"); + assertNull(store.loadWallet()); + assertFalse(Files.exists(tempDir.resolve("wallet.json"))); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/testutil/LiveTestSupport.java b/java/keymaster/src/test/java/org/keychain/keymaster/testutil/LiveTestSupport.java new file mode 100644 index 000000000..2c1213701 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/testutil/LiveTestSupport.java @@ -0,0 +1,43 @@ +package org.keychain.keymaster.testutil; + +import java.nio.file.Path; +import org.keychain.gatekeeper.GatekeeperClient; +import org.keychain.gatekeeper.GatekeeperClientOptions; +import org.keychain.gatekeeper.GatekeeperInterface; +import org.keychain.keymaster.Keymaster; +import org.keychain.keymaster.model.WalletEncFile; +import org.keychain.keymaster.store.WalletJson; + +public final class LiveTestSupport { + public static final String DEFAULT_GATEKEEPER_URL = "http://localhost:4224"; + public static final String ENV_GATEKEEPER_URL = "KC_GATEKEEPER_URL"; + public static final String DEFAULT_REGISTRY = "local"; + public static final String DEFAULT_PASSPHRASE = "passphrase"; + public static final String DEFAULT_WALLET_FILE = "wallet.json"; + + private LiveTestSupport() { + } + + public static GatekeeperInterface gatekeeperClient() { + GatekeeperClientOptions options = new GatekeeperClientOptions(); + String override = System.getenv(ENV_GATEKEEPER_URL); + if (override != null && !override.isBlank()) { + options.baseUrl = override; + } else { + options.baseUrl = DEFAULT_GATEKEEPER_URL; + } + return new GatekeeperClient(options); + } + + public static WalletJson walletStore(Path tempDir) { + return new WalletJson<>(WalletEncFile.class, tempDir, DEFAULT_WALLET_FILE); + } + + public static Keymaster keymaster(Path tempDir) { + return new Keymaster(walletStore(tempDir), gatekeeperClient(), DEFAULT_PASSPHRASE, DEFAULT_REGISTRY); + } + + public static Keymaster keymaster(Path tempDir, String registry) { + return new Keymaster(walletStore(tempDir), gatekeeperClient(), DEFAULT_PASSPHRASE, registry); + } +} diff --git a/java/keymaster/src/test/java/org/keychain/keymaster/testutil/TestFixtures.java b/java/keymaster/src/test/java/org/keychain/keymaster/testutil/TestFixtures.java new file mode 100644 index 000000000..5650da2f5 --- /dev/null +++ b/java/keymaster/src/test/java/org/keychain/keymaster/testutil/TestFixtures.java @@ -0,0 +1,37 @@ +package org.keychain.keymaster.testutil; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class TestFixtures { + private TestFixtures() { + } + + public static Map mockJson() { + Map json = new LinkedHashMap<>(); + json.put("key", "value"); + json.put("list", List.of(1, 2, 3)); + Map obj = new LinkedHashMap<>(); + obj.put("name", "some object"); + json.put("obj", obj); + return json; + } + + public static Map mockSchema() { + Map schema = new LinkedHashMap<>(); + schema.put("$schema", "http://json-schema.org/draft-07/schema#"); + + Map email = new LinkedHashMap<>(); + email.put("format", "email"); + email.put("type", "string"); + + Map properties = new LinkedHashMap<>(); + properties.put("email", email); + + schema.put("properties", properties); + schema.put("required", List.of("email")); + schema.put("type", "object"); + return schema; + } +} diff --git a/java/keymaster/src/test/resources/vectors/operations-v1.json b/java/keymaster/src/test/resources/vectors/operations-v1.json new file mode 100644 index 000000000..804c3f7b8 --- /dev/null +++ b/java/keymaster/src/test/resources/vectors/operations-v1.json @@ -0,0 +1,137 @@ +{ + "createId": { + "registry": "Signet", + "blockid": "0000000000000000000000000000000000000000000000000000000000000001", + "created": "2024-01-02T03:04:05.000Z", + "signed": "2024-01-02T03:04:06.000Z", + "publicJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + }, + "privateJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8", + "d": "4oQSnMCSJXmlNbv00aOyV3MJDSjJCbwP7XO14CIsw3I" + }, + "signedOperation": { + "type": "create", + "created": "2024-01-02T03:04:05.000Z", + "blockid": "0000000000000000000000000000000000000000000000000000000000000001", + "mdip": { + "version": 1, + "type": "agent", + "registry": "Signet" + }, + "publicJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + }, + "signature": { + "signed": "2024-01-02T03:04:06.000Z", + "hash": "c0d7145e19ba28441bbdc7e79ae65eaeb5b024781f53b8dde7fced95f06b7655", + "value": "7e29f6a5e7945be7119e0f9613a972fd8d823e18c11d04c58f7cfe9c0f804a4c53aa00b08613bb0bf1d1260c600e7d300a94bd0057808a299b4175bf0329ff35" + } + } + }, + "updateDID": { + "did": "did:test:alice", + "previd": "prev123", + "blockid": "0000000000000000000000000000000000000000000000000000000000000001", + "signerDid": "did:test:alice", + "signed": "2024-01-02T03:04:07.000Z", + "doc": { + "didDocument": { + "id": "did:test:alice", + "controller": "did:test:alice", + "verificationMethod": [ + { + "id": "#key-1", + "controller": "did:test:alice", + "type": "EcdsaSecp256k1", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + } + } + ], + "authentication": [ + "#key-1" + ] + }, + "didDocumentMetadata": { + "created": "2024-01-01T00:00:00.000Z", + "updated": "2024-01-02T00:00:00.000Z", + "versionId": "v1", + "confirmed": true + }, + "didResolutionMetadata": { + "contentType": "application/did+ld+json" + }, + "didDocumentData": { + "foo": "bar" + }, + "mdip": { + "version": 1, + "type": "agent", + "registry": "Signet" + } + }, + "privateJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8", + "d": "4oQSnMCSJXmlNbv00aOyV3MJDSjJCbwP7XO14CIsw3I" + }, + "signedOperation": { + "type": "update", + "did": "did:test:alice", + "previd": "prev123", + "blockid": "0000000000000000000000000000000000000000000000000000000000000001", + "doc": { + "didDocument": { + "id": "did:test:alice", + "controller": "did:test:alice", + "verificationMethod": [ + { + "id": "#key-1", + "controller": "did:test:alice", + "type": "EcdsaSecp256k1", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qutS3XSUw2EEneZ8xoDoPry7vb6xNjfZLNhF9wMIr14", + "y": "k3AWQTMpTl_RZ5Zy_nhmwwfa-XKBoo9m3KfLtSkZgk8" + } + } + ], + "authentication": [ + "#key-1" + ] + }, + "didDocumentData": { + "foo": "bar" + }, + "mdip": { + "version": 1, + "type": "agent", + "registry": "Signet" + } + }, + "signature": { + "signer": "did:test:alice", + "signed": "2024-01-02T03:04:07.000Z", + "hash": "1c0e760fe15a1aa25644b927e28784c23a65e52d9dc00aa9b1fdfb6320c4341a", + "value": "4b2986479d4451c47a89835c611b4fdcfc5be9c9915d427c68b6e88614be935434845a2875d137007c5aa1ad90cae3c7384e220d470334904540de2415ddf8d1" + } + } + } +} \ No newline at end of file diff --git a/java/keymaster/src/test/resources/vectors/wallet-v1.json b/java/keymaster/src/test/resources/vectors/wallet-v1.json new file mode 100644 index 000000000..49055d528 --- /dev/null +++ b/java/keymaster/src/test/resources/vectors/wallet-v1.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "seed": { + "mnemonicEnc": { + "salt": "AAECAwQFBgcICQoLDA0ODw==", + "iv": "EBESExQVFhcYGRob", + "data": "wx3fGlIor3vtRtYk0WPnbnEOHnM0agBEUugXfr1FoCrNenafh5H5xfsLXMmVUGBR38HreYHMF5yCmoBMAxwFxmIRsFBVQ0XbajAInzL7WF9UysoSsEstXtP7+M0U/ajMkXQuo0I2ZkrOlxtbIg==" + } + }, + "enc": "NUKhOmTQbN4-BAUml2Jswqrb8tyDjWGXmNiT1SO874b1BiuzuUgcpgFL6wggPmSy1eBGsGbB3O6YoMfk4so-MEq4EbP80kqs8XmxbrUnoQURUPFWb2OFqf9B3ziVYGM06zxhAHarQXmPOfEJrJpS9KFz-QBjB7SvY-fGK8EEjXh0ny_w6N2XyOtD_txHY8uzXS9qF0s30AGl77i9JO7DxDPKFbHpyro1hyIizmlFEOOMFNhZKDbk00Ns5-Z5Sg6mtxFGi1Ha3eAk9y22FvfT2-rXrrgWWX5Cf6Fzja1Jkg", + "passphrase": "passphrase", + "wallet": { + "counter": 1, + "ids": { + "Alice": { + "did": "did:test:alice", + "account": 0, + "index": 0, + "held": [ + "did:test:cred1" + ], + "owned": [ + "did:test:asset1" + ] + } + }, + "current": "Alice", + "names": { + "alias": "did:test:alice" + } + } +} \ No newline at end of file diff --git a/java/settings.gradle b/java/settings.gradle new file mode 100644 index 000000000..35703d15b --- /dev/null +++ b/java/settings.gradle @@ -0,0 +1,7 @@ +rootProject.name = 'kc-java' + +include 'crypto' +include 'cid' +include 'gatekeeper' +include 'keymaster' +include 'demo' diff --git a/services/mediators/satoshi-inscription/package-lock.json b/services/mediators/satoshi-inscription/package-lock.json index 9d7a2d077..981a0b963 100644 --- a/services/mediators/satoshi-inscription/package-lock.json +++ b/services/mediators/satoshi-inscription/package-lock.json @@ -1,12 +1,12 @@ { "name": "inscription-mediator", - "version": "1.4.0-beta.2", + "version": "1.4.0-beta.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "inscription-mediator", - "version": "1.4.0-beta.2", + "version": "1.4.0-beta.5", "hasInstallScript": true, "license": "MIT", "dependencies": {