diff --git a/CHANGELOG b/CHANGELOG index 25fbec1de..5bc47a07f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,8 +4,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project _loosely_ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). More specifically: ## unreleased +- ad4m.expression.get() handles literal values client-side to avoid roundtrips, can be overridden with optional flag [PR#498](https://github.com/coasys/ad4m/pull/498) + +### Fixed +- Prolog engine gets respawned when a query caused an error to clean the machine state [PR#483](https://github.com/coasys/ad4m/pull/483) +- Catch panics in Scryer and handle as error instead of killing the engine thread which caused `channel closed` error in future queries [PR#483](https://github.com/coasys/ad4m/pull/483) +- Bootstrap seed creation working with cli: `ad4m dev generate-bootstrap` [PR#247](https://github.com/perspect3vism/ad4m/pull/496) +- Connect capacitor update to include qr code scanning on mobile [PR#483](https://github.com/coasys/ad4m/pull/497) ### Added +- Prolog predicates needed in new Flux mention notification trigger: + - agent_did/1 + - remove_html_tags/2 + - string_includes/2 + - literal_from_url/3 + - json_property/3 +[PR#483](https://github.com/coasys/ad4m/pull/483) +- Triggered notifications are handled through operating system notifications [PR#483](https://github.com/coasys/ad4m/pull/483) + +- App notifications implemented. ADAM apps can register Prolog queries with the executor which will be checked on every perspective change. If the change adds a new match, it will trigger the publishing of a notifications via subscriptions in client interface [PR#475](https://github.com/coasys/ad4m/pull/475), as well as calling a web hook if given [PR#482](https://github.com/coasys/ad4m/pull/482) - Support ADAM executor hosting service alpha [PR#474](https://github.com/coasys/ad4m/pull/474) - Complete instructions in README [PR#473](https://github.com/coasys/ad4m/pull/473) diff --git a/Cargo.lock b/Cargo.lock index 6123183a7..df62af3ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,9 +103,12 @@ dependencies = [ "os_info", "rand 0.8.5", "regex", + "reqwest", "rocket", "rusqlite", "rust-embed", + "rustls 0.23.8", + "rustls-pemfile 2.1.2", "scryer-prolog", "secp256k1", "semver 1.0.21", @@ -113,6 +116,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "tokio", + "tokio-rustls 0.26.0", "tokio-stream", "url 2.4.1", "uuid 1.7.0", @@ -196,7 +200,7 @@ dependencies = [ "cipher", "ctr", "ghash", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -221,7 +225,7 @@ dependencies = [ "cipher", "ctr", "ghash", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -926,6 +930,33 @@ dependencies = [ "shrinkwraprs", ] +[[package]] +name = "aws-lc-rs" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474d7cec9d0a1126fad1b224b767fcbf351c23b0309bb21ec210bcfd379926a5" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7505fc3cb7acbf42699a43a79dd9caa4ed9e99861dfbb837c5c0fb5a0a8d2980" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "backon" version = "0.4.1" @@ -989,6 +1020,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64-simd" version = "0.8.0" @@ -1035,6 +1072,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.4.2", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease 0.2.17", + "proc-macro2 1.0.78", + "quote 1.0.35", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.48", + "which", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1244,7 +1304,7 @@ dependencies = [ "pairing", "rand_core 0.6.4", "serde", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -1487,6 +1547,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfb" version = "0.7.3" @@ -1602,6 +1671,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.1", +] + [[package]] name = "clap" version = "2.34.0" @@ -1678,6 +1758,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "coasys_juniper" version = "0.16.0" @@ -2242,7 +2331,7 @@ checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array 0.14.7", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -2254,7 +2343,7 @@ checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array 0.14.7", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -2286,17 +2375,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ "generic-array 0.14.7", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] name = "crypto-mac" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ "generic-array 0.14.7", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -2379,7 +2468,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -2395,7 +2484,7 @@ dependencies = [ "fiat-crypto", "platforms", "rustc_version 0.4.0", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -3612,7 +3701,7 @@ dependencies = [ "block-buffer 0.10.4", "const-oid", "crypto-common", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -3942,7 +4031,7 @@ dependencies = [ "pkcs8 0.9.0", "rand_core 0.6.4", "sec1 0.3.0", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -3963,7 +4052,7 @@ dependencies = [ "pkcs8 0.10.2", "rand_core 0.6.4", "sec1 0.7.3", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -4264,7 +4353,7 @@ checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ "bitvec", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -4274,7 +4363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "rand_core 0.6.4", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -5113,7 +5202,7 @@ checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff 0.12.1", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -5124,7 +5213,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff 0.13.0", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -5463,7 +5552,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac 0.11.1", + "crypto-mac 0.11.0", "digest 0.9.0", ] @@ -5760,7 +5849,7 @@ dependencies = [ "proptest-derive", "serde", "serde_bytes", - "subtle 2.4.1", + "subtle 2.5.0", "subtle-encoding", "tracing", ] @@ -5859,7 +5948,7 @@ checksum = "dcc25ba91f7898688245db538693d7b26504f91df24709111bb3b1d7506af01a" dependencies = [ "paste", "serde", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -6209,7 +6298,7 @@ dependencies = [ "serde_yaml 0.9.31", "shrinkwraprs", "strum 0.18.0", - "subtle 2.4.1", + "subtle 2.5.0", "subtle-encoding", "thiserror", "tracing", @@ -7458,6 +7547,12 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128" version = "0.2.5" @@ -7653,7 +7748,7 @@ checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" dependencies = [ "crunchy", "digest 0.9.0", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -8127,6 +8222,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "mockall" version = "0.11.4" @@ -9199,7 +9300,7 @@ checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -9210,7 +9311,7 @@ checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -9801,6 +9902,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" +dependencies = [ + "proc-macro2 1.0.78", + "syn 2.0.48", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -9973,7 +10084,7 @@ dependencies = [ "log", "multimap", "petgraph", - "prettyplease", + "prettyplease 0.1.25", "prost", "prost-types", "regex", @@ -10706,7 +10817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ "hmac 0.12.1", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -10988,7 +11099,7 @@ dependencies = [ "rand_core 0.6.4", "signature 2.2.0", "spki 0.7.3", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -11140,6 +11251,21 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79adb16721f56eb2d843e67676896a61ce7a0fa622dc18d3e372477a029d2740" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle 2.5.0", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -11170,6 +11296,22 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + [[package]] name = "rustls-tokio-stream" version = "0.2.16" @@ -11201,6 +11343,18 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.102.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +dependencies = [ + "aws-lc-rs", + "ring 0.17.7", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -11484,7 +11638,7 @@ dependencies = [ "der 0.6.1", "generic-array 0.14.7", "pkcs8 0.9.0", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -11498,7 +11652,7 @@ dependencies = [ "der 0.7.8", "generic-array 0.14.7", "pkcs8 0.10.2", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] @@ -11959,6 +12113,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shrinkwraprs" version = "0.3.0" @@ -12539,9 +12699,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "subtle-encoding" @@ -13722,6 +13882,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.8", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-socks" version = "0.5.1" @@ -14436,7 +14607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", - "subtle 2.4.1", + "subtle 2.5.0", ] [[package]] @@ -16037,7 +16208,7 @@ dependencies = [ "aead", "poly1305", "salsa20", - "subtle 2.4.1", + "subtle 2.5.0", "zeroize", ] diff --git a/cli/src/ad4m_executor.rs b/cli/src/ad4m_executor.rs index 6e3736f20..b7af4c033 100644 --- a/cli/src/ad4m_executor.rs +++ b/cli/src/ad4m_executor.rs @@ -28,7 +28,7 @@ use ad4m_client::*; use anyhow::Result; use clap::{Parser, Subcommand}; use dev::DevFunctions; -use rust_executor::Ad4mConfig; +use rust_executor::{config::TlsConfig, Ad4mConfig}; /// AD4M command line interface. /// https://ad4m.dev @@ -121,6 +121,12 @@ enum Domain { admin_credential: Option, #[arg(long, action)] localhost: Option, + #[arg(short, long, action)] + tls_cert_file: Option, + #[arg(short, long, action)] + tls_key_file: Option, + #[arg(short, long, action)] + log_holochain_metrics: Option, }, RunLocalHcServices {} } @@ -165,9 +171,23 @@ async fn main() -> Result<()> { hc_bootstrap_url, connect_holochain, admin_credential, - localhost + localhost, + tls_cert_file, + tls_key_file, + log_holochain_metrics, } = args.domain { + let tls = if tls_cert_file.is_some() && tls_cert_file.is_some() { + Some(TlsConfig { + cert_file_path: tls_cert_file.unwrap(), + key_file_path: tls_key_file.unwrap() + }) + } else { + if tls_cert_file.is_some() || tls_key_file.is_some() { + println!("To active TLS encryption, please provide arguments: tls_cert_file and tls_key_file!"); + } + None + }; let _ = tokio::spawn(async move { rust_executor::run(Ad4mConfig { app_data_path, @@ -186,7 +206,9 @@ async fn main() -> Result<()> { connect_holochain, admin_credential, localhost, - auto_permit_cap_requests: Some(true) + auto_permit_cap_requests: Some(true), + tls, + log_holochain_metrics }).await; }).await; diff --git a/cli/src/dev.rs b/cli/src/dev.rs index 0371187f1..67e06d1fa 100644 --- a/cli/src/dev.rs +++ b/cli/src/dev.rs @@ -50,6 +50,8 @@ pub async fn run(command: DevFunctions) -> Result<()> { hc_bootstrap_url: None, localhost: None, auto_permit_cap_requests: Some(true), + tls: None, + log_holochain_metrics: None, }) .await .join() @@ -182,6 +184,8 @@ pub async fn run(command: DevFunctions) -> Result<()> { hc_bootstrap_url: None, localhost: None, auto_permit_cap_requests: Some(true), + tls: None, + log_holochain_metrics: None, }) .await .join() diff --git a/connect/README.md b/connect/README.md index de54842fc..a9f001d3f 100644 --- a/connect/README.md +++ b/connect/README.md @@ -90,3 +90,34 @@ ad4mConnect({ process.exit(0); }); ``` + +# Extra steps to be used in capacitor: + +- On Android +```diff + + + + + + ++ + ++ + +``` + +- On IOs +```diff + ++ NSCameraUsageDescription ++ To be able to scan barcodes + +``` + +- Then run `npx cap sync` & `npx cap build` \ No newline at end of file diff --git a/connect/package.json b/connect/package.json index 95c1d5c42..d7a6b5b20 100644 --- a/connect/package.json +++ b/connect/package.json @@ -48,16 +48,18 @@ "devDependencies": { "@apollo/client": "3.7.10", "@coasys/ad4m": "*", + "@types/node": "^16.11.11", "esbuild": "^0.15.5", "esbuild-plugin-lit": "^0.0.10", "graphql-ws": "5.12.0", "np": "^7.6.2", "npm-run-all": "^4.1.5", "typescript": "^4.6.2", - "vite": "^4.1.1", - "@types/node": "^16.11.11" + "vite": "^4.1.1" }, "dependencies": { + "@capacitor-community/barcode-scanner": "^4.0.1", + "@capacitor/core": "^6.1.0", "@undecaf/barcode-detector-polyfill": "^0.9.15", "@undecaf/zbar-wasm": "^0.9.12", "auto-bind": "^5.0.1", @@ -66,5 +68,5 @@ "esbuild-plugin-replace": "^1.4.0", "lit": "^2.3.1" }, - "version": "0.10.0-prerelease" + "version": "0.10.0-prerelease-fix-1" } diff --git a/connect/src/components/ScanQRCode.ts b/connect/src/components/ScanQRCode.ts index f8443b90d..7dc21cd28 100644 --- a/connect/src/components/ScanQRCode.ts +++ b/connect/src/components/ScanQRCode.ts @@ -1,31 +1,56 @@ import { html } from "lit"; +import { Capacitor } from '@capacitor/core'; +import { BarcodeScanner } from '@capacitor-community/barcode-scanner'; export default function ScanQRCode({ changeState, onSuccess, uiState }) { function scanQrcode(e) { - // @ts-ignore - const bd = new BarcodeDetector(); - const video = e.currentTarget; + if (!Capacitor.isNativePlatform()) { + // @ts-ignore + const bd = new BarcodeDetector(); + const video = e.currentTarget; - const capture = async () => { - try { - if (uiState !== "qr") return; - const barcodes = await bd.detect(video); + const capture = async () => { + try { + if (uiState !== "qr") return; + const barcodes = await bd.detect(video); - const log = barcodes.find((code) => code.format === "qr_code"); + const log = barcodes.find((code) => code.format === "qr_code"); - if (log?.rawValue) { - changeState("requestcap"); - onSuccess(log.rawValue); - return; - } else { - requestAnimationFrame(capture); + if (log?.rawValue) { + changeState("requestcap"); + onSuccess(log.rawValue); + return; + } else { + requestAnimationFrame(capture); + } + } catch (err) { + console.error(err); } - } catch (err) { - console.error(err); - } - }; + }; + + capture(); + } else { + BarcodeScanner.checkPermission({ force: true }).then((status) => { + if (status.granted) { + BarcodeScanner.hideBackground(); + + BarcodeScanner.startScan().then((result) => { + if (result.hasContent) { + changeState("requestcap"); + onSuccess(result.content); + } else { + console.log("No content scanned"); + } - capture(); + BarcodeScanner.showBackground(); + }); + } else if (status.denied) { + alert("Please enable camera permissions in your settings."); + } else { + console.error("Permission denied"); + } + }); + } } return html` diff --git a/connect/src/core.ts b/connect/src/core.ts index ec9b98f34..e3f9d4537 100644 --- a/connect/src/core.ts +++ b/connect/src/core.ts @@ -174,7 +174,7 @@ export default class Ad4mConnect { localStorage.setItem('hosting_token', data.token); let token = localStorage.getItem('hosting_token'); - + const response2 = await fetch('https://hosting.ad4m.dev/api/service/info', { method: 'GET', headers: { @@ -188,7 +188,8 @@ export default class Ad4mConnect { if (data.serviceId) { this.setPort(data.port); - this.setUrl(`wss://${data.port}.hosting.ad4m.dev/graphql`); + this.setUrl(data.url); + this.connect(); } } } else { @@ -308,9 +309,13 @@ export default class Ad4mConnect { connected: () => { this.notifyConnectionChange("connected"); }, - closed: () => { + closed: async () => { if (!this.requestedRestart) { - setTimeout(async () => { + if (!this.token) { + this.notifyConnectionChange(!this.token ? "not_connected" : "disconnected"); + this.notifyAuthChange("unauthenticated"); + this.requestedRestart = false; + } else { const client = await this.connect(); if (client) { this.ad4mClient = client; @@ -319,7 +324,7 @@ export default class Ad4mConnect { this.notifyAuthChange("unauthenticated"); this.requestedRestart = false; } - }, 1000); + } } }, }, diff --git a/connect/src/web.ts b/connect/src/web.ts index bd820c84d..cfb78a320 100644 --- a/connect/src/web.ts +++ b/connect/src/web.ts @@ -639,6 +639,7 @@ export class Ad4mConnectElement extends LitElement { } private handleConnectionChange(event: ConnectionStates) { + console.log(event); if (event === "connected") { this.changeUIState("requestcap"); } @@ -829,6 +830,7 @@ export class Ad4mConnectElement extends LitElement { } render() { + console.log(this.authState, this.connectionState, this.uiState, this._isOpen); if (this._isOpen === false) return null; if (this.authState === "authenticated") return null; return html` diff --git a/core/src/expression/ExpressionClient.ts b/core/src/expression/ExpressionClient.ts index ee3533bc0..4fb05a9ef 100644 --- a/core/src/expression/ExpressionClient.ts +++ b/core/src/expression/ExpressionClient.ts @@ -2,6 +2,7 @@ import { ApolloClient, gql } from "@apollo/client/core"; import { InteractionCall, InteractionMeta } from "../language/Language"; import unwrapApolloResult from "../unwrapApolloResult"; import { ExpressionRendered } from "./Expression"; +import { Literal } from "../Literal"; export class ExpressionClient { #apolloClient: ApolloClient @@ -10,7 +11,19 @@ export class ExpressionClient { this.#apolloClient = client } - async get(url: string): Promise { + async get(url: string, alwaysGet: boolean = false): Promise { + if(!alwaysGet){ + try { + let literalValue = Literal.fromUrl(url).get(); + if (typeof literalValue === 'object' && literalValue !== null) { + if ('author' in literalValue && 'timestamp' in literalValue && 'data' in literalValue && 'proof' in literalValue) { + return literalValue; + } + } + } catch(e) {} + } + + const { expression } = unwrapApolloResult(await this.#apolloClient.query({ query: gql`query expression($url: String!) { expression(url: $url) { diff --git a/core/src/runtime/RuntimeClient.ts b/core/src/runtime/RuntimeClient.ts index f7f1849d3..83a26368f 100644 --- a/core/src/runtime/RuntimeClient.ts +++ b/core/src/runtime/RuntimeClient.ts @@ -57,13 +57,11 @@ export class RuntimeClient { this.#messageReceivedCallbacks = [] this.#exceptionOccurredCallbacks = [] this.#notificationTriggeredCallbacks = [] - this.#notificationRequestedCallbacks = [] if(subscribe) { this.subscribeMessageReceived() this.subscribeExceptionOccurred() this.subscribeNotificationTriggered() - this.subscribeNotificationRequested() } } @@ -323,10 +321,6 @@ export class RuntimeClient { this.#notificationTriggeredCallbacks.push(cb) } - addNotificationRequestedCallback(cb: NotificationRequestedCallback) { - this.#notificationRequestedCallbacks.push(cb) - } - subscribeNotificationTriggered() { this.#apolloClient.subscribe({ query: gql` subscription { @@ -342,21 +336,6 @@ export class RuntimeClient { }) } - subscribeNotificationRequested() { - this.#apolloClient.subscribe({ - query: gql` subscription { - runtimeNotificationRequested { ${NOTIFICATION_FIELDS} } - } - `}).subscribe({ - next: result => { - this.#notificationRequestedCallbacks.forEach(cb => { - cb(result.data.runtimeNotificationRequested) - }) - }, - error: (e) => console.error(e) - }) - } - addMessageCallback(cb: MessageCallback) { this.#messageReceivedCallbacks.push(cb) } diff --git a/core/src/runtime/RuntimeResolver.ts b/core/src/runtime/RuntimeResolver.ts index 0c7500606..e9cd0b2d4 100644 --- a/core/src/runtime/RuntimeResolver.ts +++ b/core/src/runtime/RuntimeResolver.ts @@ -317,23 +317,6 @@ export default class RuntimeResolver { return true } - @Subscription({topics: RUNTIME_NOTIFICATION_REQUESTED_TOPIC, nullable: true}) - runtimeNotificationRequested(): Notification { - return { - id: "test-id", - granted: false, - description: "Test description", - appName: "Test app name", - appUrl: "https://example.com", - appIconPath: "https://fluxsocial.io/favicon", - trigger: "triple(X, ad4m://has_type, flux://message)", - perspectiveIds: ["u983ud-jdhh38d"], - webhookUrl: "https://example.com/webhook", - webhookAuth: "test-auth", - - } - } - @Subscription({topics: RUNTIME_NOTIFICATION_TRIGGERED_TOPIC, nullable: true}) runtimeNotificationTriggered(): TriggeredNotification { return { diff --git a/core/src/subject/SubjectEntity.ts b/core/src/subject/SubjectEntity.ts index 581439958..af476f09e 100644 --- a/core/src/subject/SubjectEntity.ts +++ b/core/src/subject/SubjectEntity.ts @@ -43,9 +43,7 @@ export class SubjectEntity { private async getData(id?: string) { const tempId = id ?? this.#baseExpression; - console.log("SubjectEntity: getData") let data = await this.#perspective.getSubjectData(this.#subjectClass, tempId) - console.log("SubjectEntity got data:", data) Object.assign(this, data); this.#baseExpression = tempId; return this diff --git a/executor/src/core/Ad4mCore.ts b/executor/src/core/Ad4mCore.ts index ae6294a67..477c465c2 100644 --- a/executor/src/core/Ad4mCore.ts +++ b/executor/src/core/Ad4mCore.ts @@ -33,6 +33,7 @@ export interface InitHolochainParams { passphrase?: string hcProxyUrl: string, hcBootstrapUrl: string, + logHolochainMetrics?: boolean } export interface HolochainUnlockConfiguration extends HolochainConfiguration { @@ -150,6 +151,7 @@ export default class Ad4mCore { useMdns: params.hcUseMdns, hcProxyUrl: params.hcProxyUrl, hcBootstrapUrl: params.hcBootstrapUrl, + logHolochainMetrics: this.#config.logHolochainMetrics } this.#holochain = new HolochainService(holochainConfig) diff --git a/executor/src/core/Config.ts b/executor/src/core/Config.ts index dd9536583..4e791c450 100644 --- a/executor/src/core/Config.ts +++ b/executor/src/core/Config.ts @@ -34,6 +34,7 @@ export class MainConfig { languageLanguageSettings: object | null = null; swiplPath: string | undefined = undefined; swiplHomePath: string | undefined = undefined; + logHolochainMetrics: boolean = true; constructor(appDataPath = '') { this.rootConfigPath = path.join(appDataPath, 'ad4m'); @@ -67,6 +68,7 @@ export interface CoreConfig { neighbourhoodLanguageSettings?: object languageLanguageSettings?: object adminCredential?: string + logHolochainMetrics?: boolean } @@ -84,6 +86,7 @@ export function init(c: CoreConfig): MainConfig { fs.mkdirSync(d) } + mainConfig.logHolochainMetrics = c.logHolochainMetrics || true; mainConfig.systemLanguages = c.systemLanguages mainConfig.preloadLanguages = c.preloadLanguages if(c.languageAliases) diff --git a/executor/src/core/graphQL-interface/GraphQL.ts b/executor/src/core/graphQL-interface/GraphQL.ts index e7fece224..af922f6d5 100644 --- a/executor/src/core/graphQL-interface/GraphQL.ts +++ b/executor/src/core/graphQL-interface/GraphQL.ts @@ -226,9 +226,9 @@ export function createResolvers(core: Ad4mCore, config: OuterConfig) { }, //@ts-ignore agentGenerate: async (args, context) => { - const {hcPortAdmin, connectHolochain, hcPortApp, hcUseLocalProxy, hcUseMdns, hcUseProxy, hcUseBootstrap, hcProxyUrl, hcBootstrapUrl} = config; + const {hcPortAdmin, connectHolochain, hcPortApp, hcUseLocalProxy, hcUseMdns, hcUseProxy, hcUseBootstrap, hcProxyUrl, hcBootstrapUrl, logHolochainMetrics} = config; - await core.initHolochain({ hcPortAdmin, hcPortApp, hcUseLocalProxy, hcUseMdns, hcUseProxy, hcUseBootstrap, passphrase: args.passphrase, hcProxyUrl, hcBootstrapUrl }); + await core.initHolochain({ hcPortAdmin, hcPortApp, hcUseLocalProxy, hcUseMdns, hcUseProxy, hcUseBootstrap, passphrase: args.passphrase, hcProxyUrl, hcBootstrapUrl, logHolochainMetrics }); console.log("Holochain init complete"); console.log("Wait for agent"); diff --git a/executor/src/core/storage-services/Holochain/HolochainService.ts b/executor/src/core/storage-services/Holochain/HolochainService.ts index 8aea06de3..6ca24a36d 100644 --- a/executor/src/core/storage-services/Holochain/HolochainService.ts +++ b/executor/src/core/storage-services/Holochain/HolochainService.ts @@ -21,6 +21,7 @@ export interface HolochainConfiguration { useProxy?: boolean, useLocalProxy?: boolean; useMdns?: boolean; + logHolochainMetrics?: boolean; } export default class HolochainService { @@ -38,7 +39,8 @@ export default class HolochainService { useProxy, useLocalProxy, useMdns, - dataPath + dataPath, + logHolochainMetrics } = config; this.#dataPath = dataPath @@ -53,7 +55,9 @@ export default class HolochainService { this.#queue = new Map(); this.#languageDnaHashes = new Map(); - this.logDhtStatus(); + if (logHolochainMetrics) { + this.logDhtStatus(); + } } async logDhtStatus() { @@ -258,7 +262,7 @@ export default class HolochainService { //4. Call the zome function try { if (fnName != "sync" && fnName != "current_revision") { - console.debug("\x1b[34m", new Date().toISOString(), "HolochainService calling zome function:", dnaNick, zomeName, fnName, JSON.stringify(payload), "\nFor language with address", lang, "\x1b[0m"); + console.debug("\x1b[34m", new Date().toISOString(), "HolochainService calling zome function:", dnaNick, zomeName, fnName, JSON.stringify(payload).substring(0, 50), "\nFor language with address", lang, "\x1b[0m"); } let result = await HOLOCHAIN_SERVICE.callZomeFunction(installed_app_id, dnaNick, zomeName, fnName, encode(payload)); diff --git a/executor/src/main.ts b/executor/src/main.ts index e94634d2c..411afdd46 100644 --- a/executor/src/main.ts +++ b/executor/src/main.ts @@ -47,7 +47,9 @@ export interface OuterConfig { //Should ad4m-executor connect to an existing holochain instance, or spawn its own connectHolochain?: boolean, //The credential used by admin client to make request - adminCredential?: string + adminCredential?: string, + // Log holochain metrics + logHolochainMetrics?: boolean } export interface SeedFileSchema { @@ -82,7 +84,7 @@ export async function init(config: OuterConfig): Promise { let { appDataPath, networkBootstrapSeed, appLangAliases, bootstrapFixtures, languageLanguageOnly, mocks, gqlPort, adminCredential, runDappServer, - dAppPort + dAppPort, logHolochainMetrics } = config if(!gqlPort) gqlPort = 4000 // Check to see if PORT 2000 & 1337 are available if not returns a random PORT @@ -170,7 +172,8 @@ export async function init(config: OuterConfig): Promise { languageAliases, bootstrapFixtures, languageLanguageOnly, - adminCredential + adminCredential, + logHolochainMetrics } as CoreConfig); core.resolvers = createResolvers(core, config) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb0301a0d..3517581dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -691,6 +691,12 @@ importers: connect: dependencies: + '@capacitor-community/barcode-scanner': + specifier: ^4.0.1 + version: 4.0.1(@capacitor/core@6.1.0) + '@capacitor/core': + specifier: ^6.1.0 + version: 6.1.0 '@undecaf/barcode-detector-polyfill': specifier: ^0.9.15 version: 0.9.20 @@ -1063,12 +1069,18 @@ importers: '@types/ws': specifier: ^7.4.0 version: 7.4.7 + body-parser: + specifier: ^1.20.2 + version: 1.20.2 chai: specifier: '*' version: 5.0.3 chai-as-promised: specifier: '*' version: 7.1.1(chai@5.0.3) + express: + specifier: 4.18.2 + version: 4.18.2 faker: specifier: ^5.1.0 version: 5.5.3 @@ -1078,6 +1090,9 @@ importers: graphql-ws: specifier: ^5.14.2 version: 5.14.3(graphql@15.7.2) + http: + specifier: 0.0.1-security + version: 0.0.1-security json-stable-stringify: specifier: ^1.1.0 version: 1.1.1 @@ -3037,6 +3052,22 @@ packages: resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} dev: false + /@capacitor-community/barcode-scanner@4.0.1(@capacitor/core@6.1.0): + resolution: {integrity: sha512-acuhDU2mqskSeCIQMc5TGNnDszXXs4IqEES+3C2JDiq+MkJMTr+B2Dhq4k55hlkRFMOumMhlnbr2R9G6qyFPhw==} + peerDependencies: + '@capacitor/core': ^5.0.0 + dependencies: + '@capacitor/core': 6.1.0 + '@zxing/browser': 0.1.5(@zxing/library@0.20.0) + '@zxing/library': 0.20.0 + dev: false + + /@capacitor/core@6.1.0: + resolution: {integrity: sha512-Kt4ONm0X9xxJXn9Q73oBaKdzep5B/VJw3VjXa2eGul4cD2k37mJwgjpXSMRnLH0Aju5bCiRL8J/hMAfTlokO6A==} + dependencies: + tslib: 2.6.2 + dev: false + /@changesets/apply-release-plan@7.0.0: resolution: {integrity: sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==} dependencies: @@ -4022,6 +4053,7 @@ packages: /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.2 debug: 4.3.4(supports-color@8.1.1) @@ -4035,6 +4067,7 @@ packages: /@humanwhocodes/object-schema@2.0.2: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + deprecated: Use @eslint/object-schema instead /@ioredis/commands@1.2.0: resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} @@ -7952,6 +7985,31 @@ packages: resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} dev: true + /@zxing/browser@0.1.5(@zxing/library@0.20.0): + resolution: {integrity: sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==} + peerDependencies: + '@zxing/library': ^0.21.0 + dependencies: + '@zxing/library': 0.20.0 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: false + + /@zxing/library@0.20.0: + resolution: {integrity: sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==} + engines: {node: '>= 10.4.0'} + dependencies: + ts-custom-error: 3.3.1 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: false + + /@zxing/text-encoding@0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: false + optional: true + /JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -9086,6 +9144,26 @@ packages: transitivePeerDependencies: - supports-color + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /body@5.1.0: resolution: {integrity: sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==} dependencies: @@ -14657,6 +14735,10 @@ packages: sshpk: 1.18.0 dev: true + /http@0.0.1-security: + resolution: {integrity: sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==} + dev: true + /https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} dev: true @@ -21519,6 +21601,16 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: true + /rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -24197,6 +24289,11 @@ packages: /tryer@1.0.1: resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} + /ts-custom-error@3.3.1: + resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==} + engines: {node: '>=14.0.0'} + dev: false + /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} diff --git a/rust-executor/Cargo.toml b/rust-executor/Cargo.toml index aa7e847b3..9d73c0b2e 100644 --- a/rust-executor/Cargo.toml +++ b/rust-executor/Cargo.toml @@ -84,6 +84,7 @@ scryer-prolog = { version = "0.9.4" } # scryer-prolog = { path = "../../scryer-prolog", features = ["multi_thread"] } ad4m-client = { path = "../rust-client", version="0.10.0-prerelease" } +reqwest = { version = "0.11.20", features = ["json", "native-tls"] } rusqlite = { version = "0.29.0", features = ["bundled"] } fake = { version = "2.9.2", features = ["derive"] } @@ -92,6 +93,9 @@ regex = "1.5.4" json5 = "0.4" include_dir = "0.6.0" +rustls = "0.23" +tokio-rustls = "0.26" +rustls-pemfile = "2" [dev-dependencies] maplit = "1.0.2" diff --git a/rust-executor/src/agent/mod.rs b/rust-executor/src/agent/mod.rs index 672fff9ed..c88c73aba 100644 --- a/rust-executor/src/agent/mod.rs +++ b/rust-executor/src/agent/mod.rs @@ -43,6 +43,18 @@ pub fn did() -> String { did_document().id.clone() } +pub fn check_keys_and_create(did: String) -> did_key::Document { + let wallet_instance = Wallet::instance(); + let mut wallet = wallet_instance.lock().expect("wallet lock"); + let mut wallet_ref = wallet.as_mut().expect("wallet instance"); + let name = "main".to_string(); + if wallet_ref.get_did_document(&name).is_none() { + wallet_ref.initialize_keys(name, did).unwrap() + } else { + did_document() + } +} + pub fn create_signed_expression(data: T) -> Result, AnyError> { let timestamp = chrono::Utc::now(); let signature = hex::encode(sign(&signatures::hash_data_and_timestamp( @@ -293,7 +305,7 @@ impl AgentService { let file = std::fs::read_to_string(self.file.as_str()).expect("Failed to read agent file"); let dump: AgentStore = serde_json::from_str(&file).unwrap(); - self.did = Some(dump.did); + self.did = Some(dump.did.clone()); self.did_document = Some(dump.did_document); self.signing_key_id = Some(dump.signing_key_id); @@ -313,8 +325,11 @@ impl AgentService { self.agent = Some(serde_json::from_str(&file_profile).expect("Failed to parse agent profile")); } else { + let did_clone = dump.did.clone(); + let did = check_keys_and_create(did_clone).id.clone(); + self.agent = Some(Agent { - did: did(), + did, perspective: Some(Perspective { links: vec![] }), direct_message_language: None, }); diff --git a/rust-executor/src/config.rs b/rust-executor/src/config.rs index fc8b4d1f0..533101f30 100644 --- a/rust-executor/src/config.rs +++ b/rust-executor/src/config.rs @@ -2,6 +2,12 @@ use crate::utils; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TlsConfig { + pub cert_file_path: String, + pub key_file_path: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Ad4mConfig { @@ -24,6 +30,8 @@ pub struct Ad4mConfig { pub admin_credential: Option, pub localhost: Option, pub auto_permit_cap_requests: Option, + pub tls: Option, + pub log_holochain_metrics: Option, } impl Ad4mConfig { @@ -76,6 +84,9 @@ impl Ad4mConfig { if self.localhost.is_none() { self.localhost = Some(true); } + if self.log_holochain_metrics.is_none() { + self.log_holochain_metrics = Some(true); + } } pub fn get_json(&self) -> String { @@ -103,6 +114,8 @@ impl Default for Ad4mConfig { admin_credential: None, localhost: None, auto_permit_cap_requests: None, + tls: None, + log_holochain_metrics: None, }; config.prepare(); config diff --git a/rust-executor/src/graphql/mod.rs b/rust-executor/src/graphql/mod.rs index a625d85cf..ec65282ef 100644 --- a/rust-executor/src/graphql/mod.rs +++ b/rust-executor/src/graphql/mod.rs @@ -6,6 +6,7 @@ mod subscription_resolvers; use graphql_types::RequestContext; use mutation_resolvers::*; use query_resolvers::*; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use subscription_resolvers::*; use crate::js_core::JsCoreHandle; @@ -23,6 +24,10 @@ use coasys_juniper_graphql_transport_ws::ConnectionConfig; use coasys_juniper_warp::{playground_filter, subscriptions::serve_graphql_transport_ws}; use warp::{http::Response, Filter}; use std::path::Path; +use tokio_rustls::rustls::ServerConfig; +use tokio_rustls::TlsAcceptor; +use std::fs::File; +use std::io::BufReader; impl coasys_juniper::Context for RequestContext {} @@ -137,6 +142,19 @@ pub async fn start_server(js_core_handle: JsCoreHandle, config: Ad4mConfig) -> R [0, 0, 0, 0] }; - warp::serve(routes).run((address, port)).await; + if let Some(tls_config) = config.tls { + warp::serve(routes) + .tls() + .cert_path(tls_config.cert_file_path) + .key_path(tls_config.key_file_path) + .run((address, port)) + .await; + } else { + warp::serve(routes) + .run((address, port)) + .await; + } + + Ok(()) } diff --git a/rust-executor/src/perspectives/perspective_instance.rs b/rust-executor/src/perspectives/perspective_instance.rs index 4795230a8..ad7ff271c 100644 --- a/rust-executor/src/perspectives/perspective_instance.rs +++ b/rust-executor/src/perspectives/perspective_instance.rs @@ -4,7 +4,7 @@ use std::time::Duration; use serde_json::Value; use scryer_prolog::machine::parsed_results::{QueryMatch, QueryResolution}; use tokio::{join, time}; -use tokio::sync::Mutex; +use tokio::sync::{Mutex, RwLock}; use ad4m_client::literal::Literal; use chrono::DateTime; use deno_core::anyhow::anyhow; @@ -130,7 +130,7 @@ pub struct PerspectiveInstance { prolog_needs_rebuild: Arc>, is_teardown: Arc>, sdna_change_mutex: Arc>, - prolog_update_mutex: Arc>, + prolog_update_mutex: Arc>, link_language: Arc>>, } @@ -149,7 +149,7 @@ impl PerspectiveInstance { prolog_needs_rebuild: Arc::new(Mutex::new(true)), is_teardown: Arc::new(Mutex::new(false)), sdna_change_mutex: Arc::new(Mutex::new(())), - prolog_update_mutex: Arc::new(Mutex::new(())), + prolog_update_mutex: Arc::new(RwLock::new(())), link_language: Arc::new(Mutex::new(None)), } } @@ -792,8 +792,21 @@ impl PerspectiveInstance { } async fn ensure_prolog_engine(&self) -> Result<(), AnyError> { - let mut maybe_prolog_engine = self.prolog_engine.lock().await; - if maybe_prolog_engine.is_none() { + let has_prolog_engine = { + self.prolog_engine.lock().await.is_some() + }; + + let mut rebuild_flag = self.prolog_needs_rebuild.lock().await; + + if !has_prolog_engine || *rebuild_flag == true { + let _update_lock = self.prolog_update_mutex.write().await; + let mut maybe_prolog_engine = self.prolog_engine.lock().await; + if *rebuild_flag == true && maybe_prolog_engine.is_some() { + let old_engine = maybe_prolog_engine.as_ref().unwrap(); + let _ = old_engine.drop(); + *rebuild_flag = false; + } + let mut engine = PrologEngine::new(); engine.spawn().await.map_err(|e| anyhow!("Failed to spawn Prolog engine: {}", e))?; let all_links = self.get_links(&LinkQuery::default()).await?; @@ -801,6 +814,7 @@ impl PerspectiveInstance { engine.load_module_string("facts".to_string(), facts).await?; *maybe_prolog_engine = Some(engine); } + Ok(()) } @@ -809,6 +823,7 @@ impl PerspectiveInstance { pub async fn prolog_query(&self, query: String) -> Result { self.ensure_prolog_engine().await?; + let _read_lock = self.prolog_update_mutex.read().await; let prolog_engine_mutex = self.prolog_engine.lock().await; let prolog_engine_option_ref = prolog_engine_mutex.as_ref(); let prolog_engine = prolog_engine_option_ref.as_ref().expect("Must be some since we initialized the engine above"); @@ -819,10 +834,18 @@ impl PerspectiveInstance { query }; - prolog_engine + let result = prolog_engine .run_query(query) - .await? - .map_err(|e| anyhow!(e)) + .await?; + + match result { + Err(e) => { + let mut flag = self.prolog_needs_rebuild.lock().await; + *flag = true; + Err(anyhow!(e)) + } + Ok(resolution) => Ok(resolution) + } } fn spawn_prolog_facts_update(&self, before: BTreeMap>, diff: DecoratedPerspectiveDiff) { @@ -894,19 +917,35 @@ impl PerspectiveInstance { async fn publish_notification_matches(uuid: String, match_map: BTreeMap>) { for (notification, matches) in match_map { - let payload = TriggeredNotification { - notification: notification.clone(), - perspective_id: uuid.clone(), - trigger_match: prolog_resolution_to_string(QueryResolution::Matches(matches)) - }; + if (matches.len() > 0) { + let payload = TriggeredNotification { + notification: notification.clone(), + perspective_id: uuid.clone(), + trigger_match: prolog_resolution_to_string(QueryResolution::Matches(matches)) + }; - get_global_pubsub() - .await - .publish( - &RUNTIME_NOTIFICATION_TRIGGERED_TOPIC, - &serde_json::to_string(&payload).unwrap(), - ) - .await; + let message = serde_json::to_string(&payload).unwrap(); + + if let Ok(_) = url::Url::parse(¬ification.webhook_url) { + log::info!("Notification webhook - posting to {:?}", notification.webhook_url); + let client = reqwest::Client::new(); + let res = client.post(¬ification.webhook_url) + .bearer_auth(¬ification.webhook_auth) + .header("Content-Type", "application/json") + .body(message.clone()) + .send() + .await; + log::info!("Notification webhook response: {:?}", res); + } + + get_global_pubsub() + .await + .publish( + &RUNTIME_NOTIFICATION_TRIGGERED_TOPIC, + &message, + ) + .await; + } } } diff --git a/rust-executor/src/perspectives/sdna.rs b/rust-executor/src/perspectives/sdna.rs index 0150c5d4e..0c1e68564 100644 --- a/rust-executor/src/perspectives/sdna.rs +++ b/rust-executor/src/perspectives/sdna.rs @@ -141,7 +141,13 @@ pub async fn init_engine_facts(all_links: Vec, neighbou lines.push(":- discontiguous(p3_class_color/2).".to_string()); lines.push(":- discontiguous(p3_instance_color/3).".to_string()); + // library modules lines.push(":- use_module(library(lists)).".to_string()); + lines.push(":- use_module(library(dcgs)).".to_string()); + lines.push(":- use_module(library(charsio)).".to_string()); + lines.push(":- use_module(library(format)).".to_string()); + lines.push(":- use_module(library(assoc)).".to_string()); + lines.push(":- use_module(library(dif)).".to_string()); let lib = r#" :- discontiguous(paginate/4). @@ -171,6 +177,154 @@ takeN(Rest, NextN, PageRest). lines.extend(lib.split('\n').map(|s| s.to_string())); + let literal_html_string_predicates = r#" +% Main predicate to remove HTML tags +remove_html_tags(Input, Output) :- + phrase(strip_html(Output), Input). + +% DCG rule to strip HTML tags +strip_html([]) --> []. +strip_html(Result) --> + "<", !, skip_tag, strip_html(Result). +strip_html([Char|Result]) --> + [Char], + strip_html(Result). + +% DCG rule to skip HTML tags +skip_tag --> ">", !. +skip_tag --> [_], skip_tag. + +% Main predicate to check if Substring is included in String +string_includes(String, Substring) :- + phrase((..., string(Substring), ...), String). + +% DCG rule for any sequence of characters +... --> []. +... --> [_], ... . + +% DCG rule for matching a specific string +string([]) --> []. +string([C|Cs]) --> [C], string(Cs). + + +literal_from_url(Url, Decoded, Scheme) :- + phrase(parse_url(Scheme, Encoded), Url), + phrase(url_decode(Decoded), Encoded). + +% DCG rule to parse the URL +parse_url(Scheme, Encoded) --> + "literal://", scheme(Scheme), ":", string(Encoded). + +scheme(string) --> "string". +scheme(number) --> "number". +scheme(json) --> "json". + +url_decode([]) --> []. +url_decode([H|T]) --> url_decode_char(H), url_decode(T). + +url_decode_char(' ') --> "%20". +url_decode_char('!') --> "%21". +url_decode_char('"') --> "%22". +url_decode_char('#') --> "%23". +url_decode_char('$') --> "%24". +url_decode_char('%') --> "%25". +url_decode_char('&') --> "%26". +url_decode_char('\'') --> "%27". +url_decode_char('(') --> "%28". +url_decode_char(')') --> "%29". +url_decode_char('*') --> "%2A". +url_decode_char('+') --> "%2B". +url_decode_char(',') --> "%2C". +url_decode_char('/') --> "%2F". +url_decode_char(':') --> "%3A". +url_decode_char(';') --> "%3B". +url_decode_char('=') --> "%3D". +url_decode_char('?') --> "%3F". +url_decode_char('@') --> "%40". +url_decode_char('[') --> "%5B". +url_decode_char(']') --> "%5D". +url_decode_char('{') --> "%7B". +url_decode_char('}') --> "%7D". +url_decode_char('<') --> "%3C". +url_decode_char('>') --> "%3E". +url_decode_char('\\') --> "%5C". +url_decode_char('^') --> "%5E". +url_decode_char('_') --> "%5F". +url_decode_char('|') --> "%7C". +url_decode_char('~') --> "%7E". +url_decode_char('`') --> "%60". +url_decode_char('-') --> "%2D". +url_decode_char('.') --> "%2E". + +url_decode_char(Char) --> [Char], { \+ member(Char, "%") }. + "#; + + lines.extend(literal_html_string_predicates.split('\n').map(|s| s.to_string())); + + let json_parser = r#" + % Main predicate to parse JSON and extract a property + json_property(JsonString, Property, Value) :- + phrase(json_dict(Dict), JsonString), + get_assoc(Property, Dict, Value). + + % DCG rules to parse JSON + json_dict(Dict) --> + ws, "{", ws, key_value_pairs(Pairs), ws, "}", ws, + { list_to_assoc(Pairs, Dict) }. + + key_value_pairs([Key-Value|Pairs]) --> + ws, json_string(Key), ws, ":", ws, json_value(Value), ws, ("," -> key_value_pairs(Pairs) ; {Pairs=[]}). + + json_value(Value) --> json_dict(Value). + json_value(Value) --> json_array(Value). + json_value(Value) --> json_string(Value). + json_value(Value) --> json_number(Value). + + json_array([Value|Values]) --> + "[", ws, json_value(Value), ws, ("," -> json_value_list(Values) ; {Values=[]}), ws, "]". + json_value_list([Value|Values]) --> json_value(Value), ws, ("," -> json_value_list(Values) ; {Values=[]}). + + json_string(String) --> + "\"", json_string_chars(String), "\"". + + json_string_chars([]) --> []. + json_string_chars([C|Cs]) --> json_string_char(C), json_string_chars(Cs). + + json_string_char(C) --> [C], { dif(C, '"'), dif(C, '\\') }. + json_string_char('"') --> ['\\', '"']. + json_string_char('\\') --> ['\\', '\\']. + json_string_char('/') --> ['\\', '/']. + json_string_char('\b') --> ['\\', 'b']. + json_string_char('\f') --> ['\\', 'f']. + json_string_char('\n') --> ['\\', 'n']. + json_string_char('\r') --> ['\\', 'r']. + json_string_char('\t') --> ['\\', 't']. + + json_number(Number) --> + number_sequence(Chars), + { atom_chars(Atom, Chars), + atom_number(Atom, Number) }. + + string_chars([]) --> []. + string_chars([C|Cs]) --> [C], { dif(C, '"') }, string_chars(Cs). + + % Simplified number_sequence to handle both integer and fractional parts + number_sequence([D|Ds]) --> digit(D), number_sequence_rest(Ds). + number_sequence_rest([D|Ds]) --> digit(D), number_sequence_rest(Ds). + number_sequence_rest([]) --> []. + + digit(D) --> [D], { member(D, "0123456789.") }. + + ws --> ws_char, ws. + ws --> []. + + ws_char --> [C], { C = ' ' ; C = '\t' ; C = '\n' ; C = '\r' }. + "#; + + lines.extend(json_parser.split('\n').map(|s| s.to_string())); + + lines.push(format!("agent_did(\"{}\").", agent::did())); + let mut author_agents = vec![agent::did()]; if let Some(neughbourhood_author) = neighbourhood_author { author_agents.push(neughbourhood_author); diff --git a/rust-executor/src/perspectives/utils.rs b/rust-executor/src/perspectives/utils.rs index 66b8a19c3..b0e1864c3 100644 --- a/rust-executor/src/perspectives/utils.rs +++ b/rust-executor/src/perspectives/utils.rs @@ -6,19 +6,33 @@ pub fn prolog_value_to_json_string(value: Value) -> String { Value::Float(f) => format!("{}", f), Value::Rational(r) => format!("{}", r), Value::Atom(a) => format!("{}", a.as_str()), - Value::String(s) => - if let Err(_e) = serde_json::from_str::(s.as_str()) { - //treat as string literal - //escape double quotes - format!("\"{}\"", s - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\t", "\\t") - .replace("\r", "\\r")) - } else { - //return valid json string - s - }, + Value::String(s) => { + match s.as_str() { + "true" => String::from("true"), + "false" => String::from("false"), + _ => { + //try unescaping an escaped json string + let wrapped_s = format!("\"{}\"",s); + if let Ok(json_value) = serde_json::from_str::(wrapped_s.as_str()) { + json_value.to_string() + } else { + // try fixing wrong \' escape sequences: + let fixed_s = wrapped_s.replace("\\'", "'"); + if let Ok(json_value) = serde_json::from_str::(fixed_s.as_str()) { + json_value.to_string() + } else { + //treat as string literal + //escape double quotes + format!("\"{}\"", s + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\r", "\\r")) + } + } + } + } + }, Value::List(l) => { let mut string_result = "[".to_string(); for (i, v) in l.iter().enumerate() { diff --git a/rust-executor/src/prolog_service/engine.rs b/rust-executor/src/prolog_service/engine.rs index 4bc2dc0a2..56ff2a960 100644 --- a/rust-executor/src/prolog_service/engine.rs +++ b/rust-executor/src/prolog_service/engine.rs @@ -1,3 +1,5 @@ +use std::panic::AssertUnwindSafe; + use deno_core::anyhow::Error; use scryer_prolog::machine::{parsed_results::QueryResult, Machine}; use tokio::sync::{mpsc, oneshot}; @@ -6,6 +8,7 @@ use tokio::sync::{mpsc, oneshot}; pub enum PrologServiceRequest { RunQuery(String, oneshot::Sender), LoadModuleString(String, Vec, oneshot::Sender), + Drop, } #[derive(Debug)] @@ -56,8 +59,28 @@ impl PrologEngine { while let Some(message) = receiver.recv().await { match message { PrologServiceRequest::RunQuery(query, response) => { - let result = machine.run_query(query); - let _ = response.send(PrologServiceResponse::QueryResult(result)); + match std::panic::catch_unwind(AssertUnwindSafe(|| { + machine.run_query(query) + })) { + Ok(result) => { + let _ = response.send(PrologServiceResponse::QueryResult(result)); + } + Err(e) => { + let error_string = if let Some(string) = e.downcast_ref::() { + format!("Scryer panicked with: {:?}", string) + } else if let Some(&str) = e.downcast_ref::<&str>() { + format!("Scryer panicked with: {:?}", str) + } else { + format!("Scryer panicked with: {:?}", e) + }; + log::error!("{}", error_string); + let _ = response.send( + PrologServiceResponse::QueryResult( + Err(format!("Scryer panicked with: {:?}", error_string)) + ) + ); + } + } } PrologServiceRequest::LoadModuleString( module_name, @@ -73,6 +96,7 @@ impl PrologEngine { machine.consult_module_string(module_name.as_str(), program); let _ = response.send(PrologServiceResponse::LoadModuleResult(Ok(()))); } + PrologServiceRequest::Drop => return } } }) @@ -118,6 +142,12 @@ impl PrologEngine { _ => unreachable!(), } } + + pub fn drop(&self) -> Result<(), Error> { + self.request_sender + .send(PrologServiceRequest::Drop)?; + Ok(()) + } } #[cfg(test)] diff --git a/rust-executor/src/wallet.rs b/rust-executor/src/wallet.rs index d9f1bc63f..04078117a 100644 --- a/rust-executor/src/wallet.rs +++ b/rust-executor/src/wallet.rs @@ -187,6 +187,23 @@ impl Wallet { .insert(name, Key::from(key)); } + pub fn initialize_keys(&mut self, name: String, did: String) -> Option{ + if self.keys.is_none() { + self.keys = Some(Keys::new()); + let key = did_key::resolve(did.as_str()).expect("Failed to get key pair"); + self.keys + .as_mut() + .unwrap() + .by_name + .insert(name.clone(), Key::from(key)); + let key = did_key::resolve(did.as_str()).expect("Failed to get key pair"); + let did_document = key.get_did_document(did_key::Config::default()); + Some(did_document) + } else { + None + } + } + pub fn get_public_key(&self, name: &String) -> Option> { self.keys .as_ref()? diff --git a/tests/js/package.json b/tests/js/package.json index d2f76afee..26a1d6237 100644 --- a/tests/js/package.json +++ b/tests/js/package.json @@ -18,12 +18,13 @@ "prepare-test:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/build-test-language.ps1 && powershell -ExecutionPolicy Bypass -File ./scripts/prepareTestDirectory.ps1 && deno run --allow-all scripts/get-builtin-test-langs.js && pnpm run inject-language-language && pnpm run publish-test-languages && pnpm run inject-publishing-agent", "inject-language-language": "node scripts/injectLanguageLanguageBundle.js", "inject-publishing-agent": "node scripts/injectPublishingAgent.js", - "publish-test-languages": "node --no-warnings=ExperimentalWarning --experimental-specifier-resolution=node --loader ts-node/esm ./utils/publishTestLangs.ts" + "publish-test-languages": "node --no-warnings=ExperimentalWarning --experimental-specifier-resolution=node --loader ts-node/esm ./utils/publishTestLangs.ts", + "test-single-prepare": "node scripts/cleanTestingData.js && pnpm run prepare-test && node scripts/cleanup.js" }, "devDependencies": { "@apollo/client": "3.7.10", - "@peculiar/webcrypto": "^1.1.7", "@coasys/ad4m": "link:../../core", + "@peculiar/webcrypto": "^1.1.7", "@types/chai": "*", "@types/chai-as-promised": "*", "@types/expect": "*", @@ -38,11 +39,14 @@ "@types/sinon": "*", "@types/uuid": "^8.3.0", "@types/ws": "^7.4.0", + "body-parser": "^1.20.2", "chai": "*", "chai-as-promised": "*", + "express": "4.18.2", "faker": "^5.1.0", "fs-extra": "11.2.0", "graphql-ws": "^5.14.2", + "http": "0.0.1-security", "json-stable-stringify": "^1.1.0", "kill-process-by-name": "^1.0.5", "mocha": "*", diff --git a/tests/js/tests/prolog-and-literals.test.ts b/tests/js/tests/prolog-and-literals.test.ts index dc36fead1..ffd18ba8b 100644 --- a/tests/js/tests/prolog-and-literals.test.ts +++ b/tests/js/tests/prolog-and-literals.test.ts @@ -387,6 +387,7 @@ describe("Prolog + Literals", () => { expect(todo2).to.have.property("comments") // @ts-ignore await todo.setState("todo://review") + await sleep(1000) expect(await todo.state).to.equal("todo://review") expect(await todo.comments).to.be.empty diff --git a/tests/js/tests/runtime.ts b/tests/js/tests/runtime.ts index a1e8668bb..9d9d38a9c 100644 --- a/tests/js/tests/runtime.ts +++ b/tests/js/tests/runtime.ts @@ -5,6 +5,12 @@ import { Notification, NotificationInput, TriggeredNotification } from '@coasys/ import sinon from 'sinon'; import { sleep } from '../utils/utils'; import { ExceptionType, Link } from '@coasys/ad4m'; +// Imports needed for webhook tests: +// (deactivated for now because these imports break the test suite on CI) +// (( local execution works - I leave this here for manualy local testing )) +//import express from 'express'; +//import bodyParser from 'body-parser'; +//import { Server } from 'http'; const PERSPECT3VISM_AGENT = "did:key:zQ3shkkuZLvqeFgHdgZgFMUx8VGkgVWsLA83w2oekhZxoCW2n" const DIFF_SYNC_OFFICIAL = fs.readFileSync("./scripts/perspective-diff-sync-hash").toString(); @@ -295,5 +301,112 @@ export default function runtimeTests(testContext: TestContext) { //@ts-ignore expect(match.Target).to.equal("test://target2") }) + + + + // See comments on the imports at the top + // breaks CI for some reason but works locally + // leaving this here for manual local testing + /* + it("should trigger a notification and call the webhook", async () => { + const ad4mClient = testContext.ad4mClient! + const webhookUrl = 'http://localhost:8080/webhook'; + const webhookAuth = 'Test Webhook Auth' + // Setup Express server + const app = express(); + app.use(bodyParser.json()); + + let webhookCalled = false; + let webhookGotAuth = "" + let webhookGotBody = null + + app.post('/webhook', (req, res) => { + webhookCalled = true; + webhookGotAuth = req.headers['authorization']?.substring("Bearer ".length)||""; + webhookGotBody = req.body; + res.status(200).send({ success: true }); + }); + + let server: Server|void + let serverRunning = new Promise((done) => { + server = app.listen(8080, () => { + console.log('Test server running on port 8080'); + done() + }); + }) + + await serverRunning + + + let triggerPredicate = "ad4m://notification_webhook" + let notificationPerspective = await ad4mClient.perspective.add("notification test perspective") + let otherPerspective = await ad4mClient.perspective.add("other perspective") + + const notification: NotificationInput = { + description: "ad4m://notification predicate used", + appName: "ADAM tests", + appUrl: "Test App URL", + appIconPath: "Test App Icon Path", + trigger: `triple(Source, "${triggerPredicate}", Target)`, + perspectiveIds: [notificationPerspective.uuid], + webhookUrl: webhookUrl, + webhookAuth: webhookAuth + } + + // Request to install a new notification + const notificationId = await ad4mClient.runtime.requestInstallNotification(notification); + sleep(1000) + // Grant the notification + const granted = await ad4mClient.runtime.grantNotification(notificationId) + expect(granted).to.be.true + + // Ensuring no false positives + await notificationPerspective.add(new Link({source: "control://source", target: "control://target"})) + await sleep(1000) + expect(webhookCalled).to.be.false + + // Ensuring only selected perspectives will trigger + await otherPerspective.add(new Link({source: "control://source", predicate: triggerPredicate, target: "control://target"})) + await sleep(1000) + expect(webhookCalled).to.be.false + + // Happy path + await notificationPerspective.add(new Link({source: "test://source", predicate: triggerPredicate, target: "test://target1"})) + await sleep(1000) + expect(webhookCalled).to.be.true + expect(webhookGotAuth).to.equal(webhookAuth) + expect(webhookGotBody).to.be.not.be.null + let triggeredNotification = webhookGotBody as unknown as TriggeredNotification + let triggerMatch = JSON.parse(triggeredNotification.triggerMatch) + expect(triggerMatch.length).to.equal(1) + let match = triggerMatch[0] + //@ts-ignore + expect(match.Source).to.equal("test://source") + //@ts-ignore + expect(match.Target).to.equal("test://target1") + + // Reset webhookCalled for the next test + webhookCalled = false; + webhookGotAuth = "" + webhookGotBody = null + + await notificationPerspective.add(new Link({source: "test://source", predicate: triggerPredicate, target: "test://target2"})) + await sleep(1000) + expect(webhookCalled).to.be.true + expect(webhookGotAuth).to.equal(webhookAuth) + triggeredNotification = webhookGotBody as unknown as TriggeredNotification + triggerMatch = JSON.parse(triggeredNotification.triggerMatch) + expect(triggerMatch.length).to.equal(1) + match = triggerMatch[0] + //@ts-ignore + expect(match.Source).to.equal("test://source") + //@ts-ignore + expect(match.Target).to.equal("test://target2") + + // Close the server after the test + //@ts-ignore + server!.close() + }) + */ } } diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 40a45be26..2f5d94c39 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -4,6 +4,7 @@ import { useContext, useEffect, useState } from "react"; import TrustAgent from "./components/TrustAgent"; import Navigation from "./components/Navigation"; import Auth from "./components/Auth"; +import Notification from "./components/Notification"; import { Ad4minContext } from "./context/Ad4minContext"; import { AgentProvider } from "./context/AgentContext"; import { Route, Routes } from "react-router-dom"; @@ -19,7 +20,7 @@ import TrayMessage from "./components/TrayMessage"; const App = () => { const [opened, setOpened] = useState(false); const { - state: { candidate, did, auth }, + state: { candidate, did, auth, notifications }, methods: { handleTrustAgent }, } = useContext(Ad4minContext); @@ -85,6 +86,7 @@ const App = () => { )} {auth && } + {notifications.map((notification) => ())} ); }; diff --git a/ui/src/components/Notification.tsx b/ui/src/components/Notification.tsx new file mode 100644 index 000000000..d0404d77c --- /dev/null +++ b/ui/src/components/Notification.tsx @@ -0,0 +1,127 @@ +import { useContext, useEffect, useState } from "react"; +import { Ad4minContext } from "../context/Ad4minContext" +import { Notification as NotificationType } from "@coasys/ad4m/lib/src/runtime/RuntimeResolver"; import { PerspectiveProxy } from "@coasys/ad4m"; +; + +const Notification = ({ notification }: { notification: NotificationType }) => { + const { + state: { client }, + methods: { handleNotification }, + } = useContext(Ad4minContext); + + const [requestModalOpened, setRequestModalOpened] = useState(true); + const [perspectives, setPerspectives] = useState([]); + + useEffect(() => { + setRequestModalOpened(true); + getPerspectives(); + }, [notification]); + + + const getPerspectives = async () => { + const perspectives = await client?.perspective.all(); + const filteredPerspectives = perspectives?.filter((perspective) => notification.perspectiveIds.includes(perspective.uuid)); + setPerspectives(filteredPerspectives); + } + + const permitNotification = async () => { + // @ts-ignore + let result = await client!.runtime.grantNotification(notification?.id); + + console.log(`permit result: ${result}`); + + // @ts-ignore + handleNotification(notification); + + closeRequestModal(); + }; + + const closeRequestModal = () => { + setRequestModalOpened(false); + }; + + return ( +
+ {requestModalOpened && ( + setRequestModalOpened(e.target.open)} + > + + + + + Authorize Notification + + + + +
+ +
+
+ {notification?.appName} + + {notification?.description} + +
+
+ +
+ + Perspectives: + +
    + {perspectives?.map((perspective) => ( +
  • + + {perspective.name} + +
  • + ))} +
+
+
+ + + Notification Trigger: {notification?.trigger} + + +
+ { + notification?.webhookUrl && ( +
+ + + Webhook URL: {notification?.webhookUrl} + + + + + +

+ Caution: This notification will be sent to the above URL and the data can be leaked outside of the app. Please make sure you trust the app. +

+
+ ) + } +
+ + + + Close + + + Confirm + + +
+
+
+ )} +
+ ); +}; + +export default Notification; diff --git a/ui/src/context/Ad4minContext.tsx b/ui/src/context/Ad4minContext.tsx index b453ae0c2..66f8fab9b 100644 --- a/ui/src/context/Ad4minContext.tsx +++ b/ui/src/context/Ad4minContext.tsx @@ -1,5 +1,6 @@ import { Ad4mClient, ExceptionType } from "@coasys/ad4m"; -import { ExceptionInfo } from "@coasys/ad4m/lib/src/runtime/RuntimeResolver"; +import { sendNotification } from "@tauri-apps/api/notification"; +import { ExceptionInfo, Notification as NotificationType } from "@coasys/ad4m/lib/src/runtime/RuntimeResolver"; import { createContext, useCallback, useEffect, useState } from "react"; import { buildAd4mClient, @@ -22,6 +23,7 @@ type State = { connected: boolean; connectedLaoding: boolean; expertMode: boolean; + notifications: NotificationType[]; }; type ContextProps = { @@ -32,6 +34,7 @@ type ContextProps = { handleTrustAgent: (str: string) => void; handleLogin: (client: Ad4mClient, login: Boolean, did: string) => void; toggleExpertMode: () => void; + handleNotification: (notification: NotificationType) => void; }; }; @@ -48,6 +51,7 @@ const initialState: ContextProps = { connected: false, connectedLaoding: true, expertMode: getForVersion("expertMode") === "true", + notifications: [], }, methods: { configureEndpoint: () => null, @@ -55,6 +59,7 @@ const initialState: ContextProps = { handleTrustAgent: () => null, handleLogin: () => null, toggleExpertMode: () => null, + handleNotification: () => null, }, }; @@ -128,6 +133,14 @@ export function Ad4minProvider({ children }: any) { auth: exception.addon!, })); } + + if (exception.type === ExceptionType.InstallNotificationRequest) { + setState((prev) => ({ + ...prev, + notifications: [...prev.notifications, JSON.parse(exception.addon!)], + })); + } + Notification.requestPermission().then((response) => { if (response === "granted") { new Notification(exception.title, { body: exception.message }); @@ -139,6 +152,21 @@ export function Ad4minProvider({ children }: any) { return null; }); + + // @ts-ignore + client.runtime.addNotificationTriggeredCallback((notification) => { + console.log("Notification triggered: ", notification); + const match = notification.triggerMatch; + const parsed = JSON.parse(match); + const firstMatch = parsed[0]; + const title = firstMatch?.Title + sendNotification({ + icon: notification.notification.appIconPath, + title: notification.notification.appName + (title ? ": " + title : ""), + body: firstMatch?.Description || "Received a new notification", + //body: match + }); + }) } }, [] @@ -224,6 +252,19 @@ export function Ad4minProvider({ children }: any) { })); }; + const handleNotification = (notification: NotificationType) => { + setState((prev) => { + const filteredNotifications = prev.notifications.filter( + (n) => n.id !== notification.id + ); + + return { + ...prev, + notifications: filteredNotifications, + } + }); + } + const configureEndpoint = async (url: string) => { if (url) { setState((prev) => ({ @@ -270,6 +311,7 @@ export function Ad4minProvider({ children }: any) { resetEndpoint, handleLogin, toggleExpertMode, + handleNotification }, }} >