@mwguerra/hull 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/assets/hull-logo.png +0 -0
- package/assets/hull-logo.svg +5 -0
- package/bin/hull.js +4 -0
- package/devtools/dist/index.html +29 -0
- package/host/CMakeLists.txt +101 -0
- package/host/README.md +94 -0
- package/host/linux.Dockerfile +26 -0
- package/host/src/bindings/credentials.hpp +35 -0
- package/host/src/bindings/database.hpp +51 -0
- package/host/src/bindings/files.hpp +58 -0
- package/host/src/bindings/http.hpp +84 -0
- package/host/src/bindings/printer.hpp +281 -0
- package/host/src/bindings/storage.hpp +71 -0
- package/host/src/db_core.hpp +198 -0
- package/host/src/dispatcher.hpp +81 -0
- package/host/src/file_store.hpp +91 -0
- package/host/src/keychain.hpp +157 -0
- package/host/src/main.cpp +386 -0
- package/host/src/paths.hpp +62 -0
- package/host/src/secure.hpp +124 -0
- package/host/src/serve.hpp +113 -0
- package/host/test/db_test.cpp +80 -0
- package/host/test/secure_files_test.cpp +68 -0
- package/host/third_party/sqlite/sqlite3.c +269376 -0
- package/host/third_party/sqlite/sqlite3.h +14347 -0
- package/package.json +58 -0
- package/src/bridge/bridge-core.js +92 -0
- package/src/bridge/index.js +139 -0
- package/src/bridge/native-store.js +34 -0
- package/src/cli/build.js +122 -0
- package/src/cli/config.js +102 -0
- package/src/cli/dev.js +158 -0
- package/src/cli/eject.js +39 -0
- package/src/cli/host.js +61 -0
- package/src/cli/index.js +54 -0
- package/src/cli/installer.js +265 -0
- package/src/cli/release.js +178 -0
- package/src/cli/start.js +45 -0
- package/src/cli/timing.js +22 -0
- package/src/cli/vite.js +16 -0
- package/src/react/index.js +30 -0
- package/src/vue/index.js +31 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
// Transport-agnostic bridge core. Bindings register handlers here; the same handlers
|
|
3
|
+
// are then exposed over the webview (native build) AND over HTTP/SSE (browser dev
|
|
4
|
+
// mode). Webview-free on purpose.
|
|
5
|
+
//
|
|
6
|
+
// Handler: (args json) -> reply(result json) — reply may be called sync or async.
|
|
7
|
+
// emit(): C++ -> UI push (settings:changed, etc.); also feeds the dev trace.
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <vector>
|
|
10
|
+
#include <map>
|
|
11
|
+
#include <functional>
|
|
12
|
+
#include <chrono>
|
|
13
|
+
#include <nlohmann/json.hpp>
|
|
14
|
+
|
|
15
|
+
using json = nlohmann::json;
|
|
16
|
+
|
|
17
|
+
using Reply = std::function<void(const json& result)>;
|
|
18
|
+
using Handler = std::function<void(const json& args, Reply reply)>;
|
|
19
|
+
|
|
20
|
+
class Dispatcher {
|
|
21
|
+
public:
|
|
22
|
+
void on(const std::string& name, Handler h) { handlers_[name] = std::move(h); }
|
|
23
|
+
bool has(const std::string& name) const { return handlers_.count(name) != 0; }
|
|
24
|
+
|
|
25
|
+
std::vector<std::string> names() const {
|
|
26
|
+
std::vector<std::string> out;
|
|
27
|
+
out.reserve(handlers_.size());
|
|
28
|
+
for (const auto& kv : handlers_) out.push_back(kv.first);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Where C++ -> UI pushes go (webview eval, or SSE broadcast). Set by the transport.
|
|
33
|
+
void set_emit_sink(std::function<void(const std::string&, const json&)> sink) {
|
|
34
|
+
emit_sink_ = std::move(sink);
|
|
35
|
+
}
|
|
36
|
+
// Enable the dev trace (every call/reply/event mirrored on the "__trace" event).
|
|
37
|
+
void set_trace(bool on) { trace_ = on; }
|
|
38
|
+
bool tracing() const { return trace_; }
|
|
39
|
+
|
|
40
|
+
// C++ -> UI push.
|
|
41
|
+
void emit(const std::string& event, const json& payload) {
|
|
42
|
+
if (trace_ && event != "__trace") {
|
|
43
|
+
raw_emit("__trace", {{"type", "event"}, {"event", event}, {"payload", payload}, {"t", now_ms()}});
|
|
44
|
+
}
|
|
45
|
+
raw_emit(event, payload);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Invoke a handler by name. `reply` is called exactly once (sync or async).
|
|
49
|
+
void invoke(const std::string& name, const json& args, Reply reply) {
|
|
50
|
+
auto it = handlers_.find(name);
|
|
51
|
+
if (it == handlers_.end()) {
|
|
52
|
+
reply(json{{"ok", false}, {"error", "unknown binding: " + name}});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!trace_) { it->second(args, reply); return; }
|
|
56
|
+
|
|
57
|
+
const double t0 = now_ms();
|
|
58
|
+
static long long seq = 0;
|
|
59
|
+
const long long id = ++seq;
|
|
60
|
+
raw_emit("__trace", {{"type", "call"}, {"id", id}, {"name", name}, {"args", args}, {"t", t0}});
|
|
61
|
+
it->second(args, [this, name, id, t0, reply](const json& res) {
|
|
62
|
+
raw_emit("__trace", {{"type", "reply"}, {"id", id}, {"name", name},
|
|
63
|
+
{"ok", res.value("ok", true)}, {"result", res},
|
|
64
|
+
{"durMs", now_ms() - t0}});
|
|
65
|
+
reply(res);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private:
|
|
70
|
+
void raw_emit(const std::string& event, const json& payload) {
|
|
71
|
+
if (emit_sink_) emit_sink_(event, payload);
|
|
72
|
+
}
|
|
73
|
+
static double now_ms() {
|
|
74
|
+
using namespace std::chrono;
|
|
75
|
+
return duration<double, std::milli>(steady_clock::now().time_since_epoch()).count();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
std::map<std::string, Handler> handlers_;
|
|
79
|
+
std::function<void(const std::string&, const json&)> emit_sink_ = nullptr;
|
|
80
|
+
bool trace_ = false;
|
|
81
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
// File storage core (webview-free, so it's unit-testable). Files live under
|
|
3
|
+
// <app_data_dir>/files, named by a sanitized basename (no path traversal). Contents
|
|
4
|
+
// pass through the secure layer: plaintext by default, AES-256-GCM in the secure build.
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <fstream>
|
|
7
|
+
#include <filesystem>
|
|
8
|
+
#include <stdexcept>
|
|
9
|
+
#include "paths.hpp"
|
|
10
|
+
#include "secure.hpp"
|
|
11
|
+
|
|
12
|
+
namespace fs = std::filesystem;
|
|
13
|
+
|
|
14
|
+
namespace appfiles {
|
|
15
|
+
|
|
16
|
+
inline fs::path dir() {
|
|
17
|
+
fs::path d = storage::app_data_dir() / "files";
|
|
18
|
+
fs::create_directories(d);
|
|
19
|
+
storage::lock_down(d);
|
|
20
|
+
return d;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Reject anything that isn't a plain filename (no separators, "..", drive, etc.).
|
|
24
|
+
inline std::string safe_name(const std::string& name) {
|
|
25
|
+
fs::path p(name);
|
|
26
|
+
if (name.empty() || p.filename().string() != name || name == "." || name == "..") {
|
|
27
|
+
throw std::runtime_error("invalid file name (use a plain name, no path separators)");
|
|
28
|
+
}
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
inline std::string b64encode(const std::string& in) {
|
|
33
|
+
static const char* T =
|
|
34
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
35
|
+
std::string out;
|
|
36
|
+
out.reserve(((in.size() + 2) / 3) * 4);
|
|
37
|
+
const auto* d = reinterpret_cast<const unsigned char*>(in.data());
|
|
38
|
+
for (size_t i = 0; i < in.size(); i += 3) {
|
|
39
|
+
int n = d[i] << 16;
|
|
40
|
+
if (i + 1 < in.size()) n |= d[i + 1] << 8;
|
|
41
|
+
if (i + 2 < in.size()) n |= d[i + 2];
|
|
42
|
+
out.push_back(T[(n >> 18) & 63]);
|
|
43
|
+
out.push_back(T[(n >> 12) & 63]);
|
|
44
|
+
out.push_back(i + 1 < in.size() ? T[(n >> 6) & 63] : '=');
|
|
45
|
+
out.push_back(i + 2 < in.size() ? T[n & 63] : '=');
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
inline std::string b64decode(const std::string& in) {
|
|
51
|
+
auto val = [](char c) -> int {
|
|
52
|
+
if (c >= 'A' && c <= 'Z') return c - 'A';
|
|
53
|
+
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
|
54
|
+
if (c >= '0' && c <= '9') return c - '0' + 52;
|
|
55
|
+
if (c == '+') return 62;
|
|
56
|
+
if (c == '/') return 63;
|
|
57
|
+
return -1;
|
|
58
|
+
};
|
|
59
|
+
std::string out;
|
|
60
|
+
int buf = 0, bits = 0;
|
|
61
|
+
for (char c : in) {
|
|
62
|
+
int v = val(c);
|
|
63
|
+
if (v < 0) continue; // skip '=', whitespace, etc.
|
|
64
|
+
buf = (buf << 6) | v;
|
|
65
|
+
bits += 6;
|
|
66
|
+
if (bits >= 8) { bits -= 8; out.push_back((char)((buf >> bits) & 0xFF)); }
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
inline void write_file(const std::string& name, const std::string& bytes) {
|
|
72
|
+
const fs::path target = dir() / safe_name(name);
|
|
73
|
+
const std::string blob = secure::encrypt(bytes);
|
|
74
|
+
fs::path tmp = target; tmp += ".tmp";
|
|
75
|
+
{ std::ofstream f(tmp, std::ios::binary | std::ios::trunc);
|
|
76
|
+
f.write(blob.data(), (std::streamsize)blob.size()); }
|
|
77
|
+
fs::rename(tmp, target);
|
|
78
|
+
storage::lock_down(target);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
inline std::string read_file(const std::string& name) {
|
|
82
|
+
const fs::path target = dir() / safe_name(name);
|
|
83
|
+
std::ifstream f(target, std::ios::binary);
|
|
84
|
+
if (!f) throw std::runtime_error("file not found: " + name);
|
|
85
|
+
std::string blob((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
|
86
|
+
auto plain = secure::decrypt(blob);
|
|
87
|
+
if (!plain) throw std::runtime_error("could not read file (wrong key or corrupt): " + name);
|
|
88
|
+
return *plain;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
} // namespace appfiles
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
// OS keychain access (Windows Credential Manager / macOS Keychain / Linux libsecret).
|
|
3
|
+
// Webview-free so the crypto layer (secure.hpp) and DB (db_core.hpp) can fetch keys
|
|
4
|
+
// without pulling in the web view. The UI-facing bindings live in credentials.hpp.
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <optional>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
|
|
9
|
+
namespace secrets {
|
|
10
|
+
|
|
11
|
+
bool store(const std::string& service, const std::string& account, const std::string& secret);
|
|
12
|
+
std::optional<std::string> load(const std::string& service, const std::string& account);
|
|
13
|
+
bool erase(const std::string& service, const std::string& account);
|
|
14
|
+
|
|
15
|
+
// ----------------------------- Windows -----------------------------
|
|
16
|
+
#if defined(_WIN32)
|
|
17
|
+
} // namespace secrets
|
|
18
|
+
#include <windows.h>
|
|
19
|
+
#include <wincred.h>
|
|
20
|
+
namespace secrets {
|
|
21
|
+
|
|
22
|
+
inline std::wstring target(const std::string& service, const std::string& account) {
|
|
23
|
+
std::string key = service + ":" + account;
|
|
24
|
+
return std::wstring(key.begin(), key.end());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
inline bool store(const std::string& service, const std::string& account,
|
|
28
|
+
const std::string& secret) {
|
|
29
|
+
std::wstring t = target(service, account);
|
|
30
|
+
CREDENTIALW cred{};
|
|
31
|
+
cred.Type = CRED_TYPE_GENERIC;
|
|
32
|
+
cred.TargetName = const_cast<LPWSTR>(t.c_str());
|
|
33
|
+
cred.CredentialBlobSize = static_cast<DWORD>(secret.size());
|
|
34
|
+
cred.CredentialBlob = reinterpret_cast<LPBYTE>(const_cast<char*>(secret.data()));
|
|
35
|
+
cred.Persist = CRED_PERSIST_LOCAL_MACHINE; // per-user store; survives reboot
|
|
36
|
+
return CredWriteW(&cred, 0) == TRUE;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
inline std::optional<std::string> load(const std::string& service,
|
|
40
|
+
const std::string& account) {
|
|
41
|
+
std::wstring t = target(service, account);
|
|
42
|
+
PCREDENTIALW pcred = nullptr;
|
|
43
|
+
if (!CredReadW(t.c_str(), CRED_TYPE_GENERIC, 0, &pcred)) return std::nullopt;
|
|
44
|
+
std::string secret(reinterpret_cast<char*>(pcred->CredentialBlob),
|
|
45
|
+
pcred->CredentialBlobSize);
|
|
46
|
+
CredFree(pcred);
|
|
47
|
+
return secret;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
inline bool erase(const std::string& service, const std::string& account) {
|
|
51
|
+
std::wstring t = target(service, account);
|
|
52
|
+
return CredDeleteW(t.c_str(), CRED_TYPE_GENERIC, 0) == TRUE;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ----------------------------- macOS -----------------------------
|
|
56
|
+
#elif defined(__APPLE__)
|
|
57
|
+
} // namespace secrets
|
|
58
|
+
#include <Security/Security.h>
|
|
59
|
+
namespace secrets {
|
|
60
|
+
|
|
61
|
+
inline bool store(const std::string& service, const std::string& account,
|
|
62
|
+
const std::string& secret) {
|
|
63
|
+
erase(service, account); // replace if present
|
|
64
|
+
OSStatus st = SecKeychainAddGenericPassword(
|
|
65
|
+
nullptr,
|
|
66
|
+
(UInt32)service.size(), service.c_str(),
|
|
67
|
+
(UInt32)account.size(), account.c_str(),
|
|
68
|
+
(UInt32)secret.size(), secret.data(),
|
|
69
|
+
nullptr);
|
|
70
|
+
return st == errSecSuccess;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
inline std::optional<std::string> load(const std::string& service,
|
|
74
|
+
const std::string& account) {
|
|
75
|
+
void* data = nullptr; UInt32 len = 0; SecKeychainItemRef item = nullptr;
|
|
76
|
+
OSStatus st = SecKeychainFindGenericPassword(
|
|
77
|
+
nullptr,
|
|
78
|
+
(UInt32)service.size(), service.c_str(),
|
|
79
|
+
(UInt32)account.size(), account.c_str(),
|
|
80
|
+
&len, &data, &item);
|
|
81
|
+
if (st != errSecSuccess) return std::nullopt;
|
|
82
|
+
std::string secret(static_cast<char*>(data), len);
|
|
83
|
+
SecKeychainItemFreeContent(nullptr, data);
|
|
84
|
+
if (item) CFRelease(item);
|
|
85
|
+
return secret;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
inline bool erase(const std::string& service, const std::string& account) {
|
|
89
|
+
void* data = nullptr; UInt32 len = 0; SecKeychainItemRef item = nullptr;
|
|
90
|
+
OSStatus st = SecKeychainFindGenericPassword(
|
|
91
|
+
nullptr,
|
|
92
|
+
(UInt32)service.size(), service.c_str(),
|
|
93
|
+
(UInt32)account.size(), account.c_str(),
|
|
94
|
+
&len, &data, &item);
|
|
95
|
+
if (st != errSecSuccess) return false;
|
|
96
|
+
if (data) SecKeychainItemFreeContent(nullptr, data);
|
|
97
|
+
bool ok = item && SecKeychainItemDelete(item) == errSecSuccess;
|
|
98
|
+
if (item) CFRelease(item);
|
|
99
|
+
return ok;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ----------------------------- Linux (libsecret) -----------------------------
|
|
103
|
+
#else
|
|
104
|
+
} // namespace secrets
|
|
105
|
+
#include <libsecret/secret.h>
|
|
106
|
+
namespace secrets {
|
|
107
|
+
|
|
108
|
+
inline const SecretSchema* schema() {
|
|
109
|
+
static const SecretSchema s = {
|
|
110
|
+
"com.mwguerra.hull", SECRET_SCHEMA_NONE,
|
|
111
|
+
{
|
|
112
|
+
{"service", SECRET_SCHEMA_ATTRIBUTE_STRING},
|
|
113
|
+
{"account", SECRET_SCHEMA_ATTRIBUTE_STRING},
|
|
114
|
+
{nullptr, SECRET_SCHEMA_ATTRIBUTE_STRING},
|
|
115
|
+
}};
|
|
116
|
+
return &s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
inline bool store(const std::string& service, const std::string& account,
|
|
120
|
+
const std::string& secret) {
|
|
121
|
+
GError* err = nullptr;
|
|
122
|
+
gboolean ok = secret_password_store_sync(
|
|
123
|
+
schema(), SECRET_COLLECTION_DEFAULT,
|
|
124
|
+
(service + ":" + account).c_str(), // label
|
|
125
|
+
secret.c_str(), nullptr, &err,
|
|
126
|
+
"service", service.c_str(),
|
|
127
|
+
"account", account.c_str(), nullptr);
|
|
128
|
+
if (err) { g_error_free(err); return false; }
|
|
129
|
+
return ok == TRUE;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
inline std::optional<std::string> load(const std::string& service,
|
|
133
|
+
const std::string& account) {
|
|
134
|
+
GError* err = nullptr;
|
|
135
|
+
gchar* pw = secret_password_lookup_sync(
|
|
136
|
+
schema(), nullptr, &err,
|
|
137
|
+
"service", service.c_str(),
|
|
138
|
+
"account", account.c_str(), nullptr);
|
|
139
|
+
if (err) { g_error_free(err); return std::nullopt; }
|
|
140
|
+
if (!pw) return std::nullopt;
|
|
141
|
+
std::string secret(pw);
|
|
142
|
+
secret_password_free(pw);
|
|
143
|
+
return secret;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
inline bool erase(const std::string& service, const std::string& account) {
|
|
147
|
+
GError* err = nullptr;
|
|
148
|
+
gboolean ok = secret_password_clear_sync(
|
|
149
|
+
schema(), nullptr, &err,
|
|
150
|
+
"service", service.c_str(),
|
|
151
|
+
"account", account.c_str(), nullptr);
|
|
152
|
+
if (err) { g_error_free(err); return false; }
|
|
153
|
+
return ok == TRUE;
|
|
154
|
+
}
|
|
155
|
+
#endif
|
|
156
|
+
|
|
157
|
+
} // namespace secrets
|