274namespace commercial {
277inline constexpr const char* kLicenseEnvVar =
"SIGNET_COMMERCIAL_LICENSE_KEY";
279inline constexpr const char* kLicenseTierEnvVar =
"SIGNET_COMMERCIAL_LICENSE_TIER";
281inline constexpr const char* kUsageFileEnvVar =
"SIGNET_COMMERCIAL_USAGE_FILE";
283inline constexpr const char* kRuntimeUserEnvVar =
"SIGNET_COMMERCIAL_RUNTIME_USER";
285inline constexpr const char* kRuntimeNodeEnvVar =
"SIGNET_COMMERCIAL_RUNTIME_NODE";
287inline constexpr uint64_t kUsagePersistIntervalRows = 10'000;
291#if defined(SIGNET_EVAL_MAX_ROWS_MONTH_U64)
292inline constexpr uint64_t kDefaultEvalMaxRowsMonth =
293 static_cast<uint64_t
>(SIGNET_EVAL_MAX_ROWS_MONTH_U64);
295inline constexpr uint64_t kDefaultEvalMaxRowsMonth = 50'000'000ull;
300#if defined(SIGNET_EVAL_MAX_USERS_U32)
301inline constexpr uint32_t kDefaultEvalMaxUsers =
302 static_cast<uint32_t
>(SIGNET_EVAL_MAX_USERS_U32);
304inline constexpr uint32_t kDefaultEvalMaxUsers = 3u;
309#if defined(SIGNET_EVAL_MAX_NODES_U32)
310inline constexpr uint32_t kDefaultEvalMaxNodes =
311 static_cast<uint32_t
>(SIGNET_EVAL_MAX_NODES_U32);
313inline constexpr uint32_t kDefaultEvalMaxNodes = 1u;
318#if defined(SIGNET_EVAL_MAX_DAYS_U32)
319inline constexpr uint32_t kDefaultEvalMaxDays =
320 static_cast<uint32_t
>(SIGNET_EVAL_MAX_DAYS_U32);
322inline constexpr uint32_t kDefaultEvalMaxDays = 30u;
327#if defined(SIGNET_EVAL_WARN_PCT_1_U32)
328inline constexpr uint32_t kDefaultEvalWarnPct1 =
329 static_cast<uint32_t
>(SIGNET_EVAL_WARN_PCT_1_U32);
331inline constexpr uint32_t kDefaultEvalWarnPct1 = 80u;
336#if defined(SIGNET_EVAL_WARN_PCT_2_U32)
337inline constexpr uint32_t kDefaultEvalWarnPct2 =
338 static_cast<uint32_t
>(SIGNET_EVAL_WARN_PCT_2_U32);
340inline constexpr uint32_t kDefaultEvalWarnPct2 = 90u;
350struct LicensePolicy {
352 bool evaluation_mode{
false};
354 uint64_t max_rows_month{0};
356 uint32_t max_users{0};
358 uint32_t max_nodes{0};
360 int64_t explicit_expiry_day_utc{0};
362 uint32_t max_eval_days{0};
364 uint32_t warn_pct_1{kDefaultEvalWarnPct1};
366 uint32_t warn_pct_2{kDefaultEvalWarnPct2};
379 bool initialized{
false};
383 uint64_t rows_this_month{0};
385 uint64_t last_persisted_rows_this_month{0};
387 int64_t eval_start_day_utc{0};
389 bool warn_pct_1_emitted{
false};
391 bool warn_pct_2_emitted{
false};
393 std::unordered_set<std::string> users;
395 std::unordered_set<std::string> nodes;
404[[nodiscard]]
inline uint64_t fnv1a64(
const char* data,
size_t size)
noexcept {
405 constexpr uint64_t kOffset = 14695981039346656037ull;
406 constexpr uint64_t kPrime = 1099511628211ull;
408 uint64_t hash = kOffset;
409 for (
size_t i = 0; i < size; ++i) {
410 hash ^=
static_cast<uint8_t
>(data[i]);
417[[nodiscard]]
inline std::string trim_copy(
const std::string& in) {
419 while (start < in.size() && std::isspace(
static_cast<unsigned char>(in[start]))) {
423 size_t end = in.size();
424 while (end > start && std::isspace(
static_cast<unsigned char>(in[end - 1]))) {
428 return in.substr(start, end - start);
432[[nodiscard]]
inline std::string to_lower_ascii(std::string value) {
433 for (
char& ch : value) {
434 ch =
static_cast<char>(std::tolower(
static_cast<unsigned char>(ch)));
440[[nodiscard]]
inline std::string sanitize_identity(std::string value,
441 const char* fallback) {
442 value = trim_copy(value);
447 if (value.size() > 128) {
451 for (
char& c : value) {
452 if (std::isalnum(
static_cast<unsigned char>(c)) ||
453 c ==
'-' || c ==
'_' || c ==
'.' || c ==
'@') {
462[[nodiscard]]
inline std::unordered_map<std::string, std::string>
463parse_claims(
const std::string& text) {
464 std::unordered_map<std::string, std::string> out;
465 std::stringstream ss(text);
468 while (std::getline(ss, token,
';')) {
469 auto pos = token.find(
'=');
470 if (pos == std::string::npos || pos == 0 || pos + 1 >= token.size()) {
473 std::string key = to_lower_ascii(trim_copy(token.substr(0, pos)));
474 std::string val = trim_copy(token.substr(pos + 1));
475 if (!key.empty() && !val.empty()) {
484[[nodiscard]]
inline bool parse_u64(
const std::string& text, uint64_t& out) {
485 if (text.empty())
return false;
488 for (
char ch : text) {
489 if (!std::isdigit(
static_cast<unsigned char>(ch)))
return false;
490 const uint64_t digit =
static_cast<uint64_t
>(ch -
'0');
491 if (value > ((std::numeric_limits<uint64_t>::max)() - digit) / 10ull) {
494 value = (value * 10ull) + digit;
502[[nodiscard]]
inline bool parse_u32(
const std::string& text, uint32_t& out) {
504 if (!parse_u64(text, value) || value > (std::numeric_limits<uint32_t>::max)()) {
507 out =
static_cast<uint32_t
>(value);
512[[nodiscard]]
inline constexpr int64_t days_from_civil(
int y,
unsigned m,
513 unsigned d)
noexcept {
515 const int era = (y >= 0 ? y : y - 399) / 400;
516 const unsigned yoe =
static_cast<unsigned>(y - era * 400);
517 const unsigned mp = (m > 2u) ? (m - 3u) : (m + 9u);
518 const unsigned doy = (153u * mp + 2u) / 5u + d - 1u;
519 const unsigned doe = yoe * 365u + yoe / 4u - yoe / 100u + doy;
520 return static_cast<int64_t
>(era) * 146097 +
static_cast<int64_t
>(doe) - 719468;
524[[nodiscard]]
inline bool is_leap_year(
int y)
noexcept {
525 return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
529[[nodiscard]]
inline bool parse_iso_date_to_epoch_day(
const std::string& iso,
531 if (iso.size() != 10 || iso[4] !=
'-' || iso[7] !=
'-')
return false;
533 auto parse_two = [&](
size_t idx,
int& out) ->
bool {
534 if (!std::isdigit(
static_cast<unsigned char>(iso[idx])) ||
535 !std::isdigit(
static_cast<unsigned char>(iso[idx + 1]))) {
538 out = (iso[idx] -
'0') * 10 + (iso[idx + 1] -
'0');
543 for (
size_t i = 0; i < 4; ++i) {
544 if (!std::isdigit(
static_cast<unsigned char>(iso[i])))
return false;
545 year = year * 10 + (iso[i] -
'0');
550 if (!parse_two(5, month) || !parse_two(8, day))
return false;
551 if (month < 1 || month > 12)
return false;
553 static constexpr int kMonthDays[] = {31,28,31,30,31,30,31,31,30,31,30,31};
554 int max_day = kMonthDays[month - 1];
555 if (month == 2 && is_leap_year(year)) max_day = 29;
556 if (day < 1 || day > max_day)
return false;
558 out_day = days_from_civil(year,
static_cast<unsigned>(month),
static_cast<unsigned>(day));
563[[nodiscard]]
inline int64_t current_epoch_day_utc() {
564 using namespace std::chrono;
565 const auto now_days = floor<days>(system_clock::now());
566 return now_days.time_since_epoch().count();
570[[nodiscard]]
inline int current_month_tag_utc() {
571 using namespace std::chrono;
572 const auto now_days = floor<days>(system_clock::now());
573 const year_month_day ymd{now_days};
574 return static_cast<int>(
static_cast<int>(ymd.year()) * 100 +
575 static_cast<unsigned>(ymd.month()));
582[[nodiscard]]
inline std::string default_usage_state_path() {
584 const char* xdg = std::getenv(
"XDG_STATE_HOME");
586 return std::string(xdg) +
"/signet-forge/usage_state";
589 const char* home = std::getenv(
"HOME");
591 if (!home || !home[0]) home = std::getenv(
"USERPROFILE");
593 if (home && home[0]) {
594 return std::string(home) +
"/.local/state/signet-forge/usage_state";
597#if defined(SIGNET_COMMERCIAL_LICENSE_HASH_U64)
599 std::snprintf(buf,
sizeof(buf),
600 "/tmp/signet_commercial_usage_%016llx.state",
601 static_cast<unsigned long long>(
602 static_cast<uint64_t
>(SIGNET_COMMERCIAL_LICENSE_HASH_U64)));
603 return std::string(buf);
605 return "/tmp/signet_commercial_usage.state";
615[[nodiscard]]
inline std::string usage_state_path() {
616 return default_usage_state_path();
620[[nodiscard]]
inline std::string detect_runtime_user() {
621#if defined(SIGNET_ALLOW_RUNTIME_IDENTITY_ENV) && SIGNET_ALLOW_RUNTIME_IDENTITY_ENV
622 const char* env_user = std::getenv(kRuntimeUserEnvVar);
623 if (env_user !=
nullptr && env_user[0] !=
'\0') {
624 return sanitize_identity(std::string(env_user),
"unknown-user");
629 struct passwd pwd_buf {};
630 struct passwd* pwd =
nullptr;
631 char storage[4096] = {};
632 if (::getpwuid_r(::geteuid(), &pwd_buf, storage,
sizeof(storage), &pwd) == 0 &&
633 pwd !=
nullptr && pwd->pw_name !=
nullptr && pwd->pw_name[0] !=
'\0') {
634 return sanitize_identity(std::string(pwd->pw_name),
"unknown-user");
637 const char* user = std::getenv(
"USER");
638 if (user ==
nullptr || user[0] ==
'\0') {
639 user = std::getenv(
"LOGNAME");
642 const char* user = std::getenv(
"USERNAME");
644 return sanitize_identity(user ? std::string(user) : std::string(),
"unknown-user");
648[[nodiscard]]
inline std::string detect_runtime_node() {
649#if defined(SIGNET_ALLOW_RUNTIME_IDENTITY_ENV) && SIGNET_ALLOW_RUNTIME_IDENTITY_ENV
650 const char* env_node = std::getenv(kRuntimeNodeEnvVar);
651 if (env_node !=
nullptr && env_node[0] !=
'\0') {
652 return sanitize_identity(std::string(env_node),
"unknown-node");
658 if (::gethostname(host,
sizeof(host) - 1) == 0) {
659 host[
sizeof(host) - 1] =
'\0';
660 return sanitize_identity(std::string(host),
"unknown-node");
662 const char* node = std::getenv(
"HOSTNAME");
664 const char* node = std::getenv(
"COMPUTERNAME");
666 return sanitize_identity(node ? std::string(node) : std::string(),
"unknown-node");
670[[nodiscard]]
inline std::vector<std::string> split_csv(
const std::string& text) {
671 std::vector<std::string> out;
672 std::stringstream ss(text);
675 while (std::getline(ss, token,
',')) {
676 token = sanitize_identity(token,
"");
677 if (!token.empty()) {
678 out.push_back(token);
686[[nodiscard]]
inline std::string join_set_csv(
const std::unordered_set<std::string>& values) {
687 std::vector<std::string> ordered;
688 ordered.reserve(values.size());
689 for (
const auto& v : values) {
690 ordered.push_back(v);
692 std::sort(ordered.begin(), ordered.end());
695 for (
size_t i = 0; i < ordered.size(); ++i) {
696 if (i != 0) out.push_back(
',');
703inline void load_usage_state_from_file(
const std::string& path, UsageState& st) {
704 std::ifstream in(path);
710 while (std::getline(in, line)) {
711 auto eq = line.find(
'=');
712 if (eq == std::string::npos)
continue;
714 const std::string key = trim_copy(line.substr(0, eq));
715 const std::string val = trim_copy(line.substr(eq + 1));
718 if (key ==
"month_tag" && parse_u64(val, u64)) {
719 st.month_tag =
static_cast<int>(u64);
720 }
else if (key ==
"rows_this_month" && parse_u64(val, u64)) {
721 st.rows_this_month = u64;
722 }
else if (key ==
"eval_start_day_utc" && parse_u64(val, u64)) {
723 st.eval_start_day_utc =
static_cast<int64_t
>(u64);
724 }
else if (key ==
"warn_pct_1_emitted") {
725 st.warn_pct_1_emitted = (val ==
"1");
726 }
else if (key ==
"warn_pct_2_emitted") {
727 st.warn_pct_2_emitted = (val ==
"1");
728 }
else if (key ==
"users") {
729 for (
auto& token : split_csv(val)) {
730 st.users.insert(token);
732 }
else if (key ==
"nodes") {
733 for (
auto& token : split_csv(val)) {
734 st.nodes.insert(token);
739 st.last_persisted_rows_this_month = st.rows_this_month;
745[[nodiscard]]
inline bool persist_usage_state_to_file(
const std::string& path,
747 const std::string tmp_path = path +
".tmp";
753 if (::lstat(tmp_path.c_str(), &st_link) == 0 && S_ISLNK(st_link.st_mode)) {
756 if (::lstat(path.c_str(), &st_link) == 0 && S_ISLNK(st_link.st_mode)) {
764 int sfd = ::open(tmp_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
765 if (sfd < 0)
return false;
769 out.open(tmp_path, std::ios::out | std::ios::trunc);
771 out.open(tmp_path, std::ios::out | std::ios::trunc);
773 if (!out.is_open())
return false;
775 out <<
"month_tag=" << st.month_tag <<
'\n';
776 out <<
"rows_this_month=" << st.rows_this_month <<
'\n';
777 out <<
"eval_start_day_utc=" << st.eval_start_day_utc <<
'\n';
778 out <<
"warn_pct_1_emitted=" << (st.warn_pct_1_emitted ? 1 : 0) <<
'\n';
779 out <<
"warn_pct_2_emitted=" << (st.warn_pct_2_emitted ? 1 : 0) <<
'\n';
780 out <<
"users=" << join_set_csv(st.users) <<
'\n';
781 out <<
"nodes=" << join_set_csv(st.nodes) <<
'\n';
785 std::remove(tmp_path.c_str());
789 if (std::rename(tmp_path.c_str(), path.c_str()) != 0) {
790 std::remove(tmp_path.c_str());
794 st.last_persisted_rows_this_month = st.rows_this_month;
806[[nodiscard]]
inline LicensePolicy resolve_policy() {
807 LicensePolicy policy;
809 const char* license_key = std::getenv(kLicenseEnvVar);
810 const auto claims = parse_claims(license_key ? std::string(license_key) : std::string());
813 if (
auto it = claims.find(
"tier"); it != claims.end()) {
814 tier = to_lower_ascii(it->second);
816 const char* env_tier = std::getenv(kLicenseTierEnvVar);
817 if (env_tier !=
nullptr && env_tier[0] !=
'\0') {
818 tier = to_lower_ascii(env_tier);
822 policy.evaluation_mode =
823 (tier ==
"eval" || tier ==
"evaluation" || tier ==
"trial" ||
824 tier ==
"testing" || tier ==
"test");
826 if (!policy.evaluation_mode) {
830 policy.max_rows_month = kDefaultEvalMaxRowsMonth;
831 policy.max_users = kDefaultEvalMaxUsers;
832 policy.max_nodes = kDefaultEvalMaxNodes;
833 policy.max_eval_days = kDefaultEvalMaxDays;
834 policy.warn_pct_1 = kDefaultEvalWarnPct1;
835 policy.warn_pct_2 = kDefaultEvalWarnPct2;
837 auto set_u64 = [&](
const char* key, uint64_t& out) {
838 if (
auto it = claims.find(key); it != claims.end()) {
840 if (parse_u64(it->second, parsed)) out = parsed;
844 auto set_u32 = [&](
const char* key, uint32_t& out) {
845 if (
auto it = claims.find(key); it != claims.end()) {
847 if (parse_u32(it->second, parsed)) out = parsed;
851 set_u64(
"max_rows_month", policy.max_rows_month);
852 set_u64(
"rows_month", policy.max_rows_month);
853 set_u32(
"max_users", policy.max_users);
854 set_u32(
"max_nodes", policy.max_nodes);
855 set_u32(
"max_days", policy.max_eval_days);
856 set_u32(
"warn_pct_1", policy.warn_pct_1);
857 set_u32(
"warn_pct_2", policy.warn_pct_2);
859 if (
auto it = claims.find(
"expires_at"); it != claims.end()) {
861 if (parse_iso_date_to_epoch_day(it->second, day)) {
862 policy.explicit_expiry_day_utc = day;
866 if (policy.warn_pct_1 > 100u) policy.warn_pct_1 = 100u;
867 if (policy.warn_pct_2 > 100u) policy.warn_pct_2 = 100u;
868 if (policy.warn_pct_1 > policy.warn_pct_2) {
869 std::swap(policy.warn_pct_1, policy.warn_pct_2);
884#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
886 "commercial feature disabled in this Apache build; "
887 "rebuild with -DSIGNET_ENABLE_COMMERCIAL=ON (AGPL-3.0 commercial tier)"};
889# if defined(SIGNET_REQUIRE_COMMERCIAL_LICENSE) && SIGNET_REQUIRE_COMMERCIAL_LICENSE
890# if !defined(SIGNET_COMMERCIAL_LICENSE_HASH_U64)
892 "commercial tier misconfigured: missing SIGNET_COMMERCIAL_LICENSE_HASH_U64"};
894 const char* license_key = std::getenv(kLicenseEnvVar);
895 if (license_key ==
nullptr || license_key[0] ==
'\0') {
897 std::string(
"missing runtime license key: set ") + kLicenseEnvVar};
900 constexpr uint64_t kExpectedHash =
901 static_cast<uint64_t
>(SIGNET_COMMERCIAL_LICENSE_HASH_U64);
902 const uint64_t actual_hash = fnv1a64(license_key, std::strlen(license_key));
904 if (actual_hash != kExpectedHash) {
906 std::string(
"invalid runtime license key in ") + kLicenseEnvVar};
916inline std::mutex& usage_state_mutex() {
922inline UsageState& usage_state() {
928inline void ensure_usage_state_loaded_locked(UsageState& st) {
929 if (st.initialized)
return;
931 load_usage_state_from_file(usage_state_path(), st);
932 if (st.month_tag == 0) {
933 st.month_tag = current_month_tag_utc();
935 st.initialized =
true;
954[[nodiscard]]
inline expected<void> enforce_eval_limits(
const char* feature_name,
955 uint64_t rows_increment) {
956 static const LicensePolicy policy = resolve_policy();
957 if (!policy.evaluation_mode) {
961 std::lock_guard<std::mutex> lock(usage_state_mutex());
962 UsageState& st = usage_state();
963 ensure_usage_state_loaded_locked(st);
965 bool persist_required =
false;
967 const int current_month = current_month_tag_utc();
968 if (st.month_tag != current_month) {
969 st.month_tag = current_month;
970 st.rows_this_month = 0;
971 st.last_persisted_rows_this_month = 0;
972 st.warn_pct_1_emitted =
false;
973 st.warn_pct_2_emitted =
false;
974 persist_required =
true;
977 const int64_t today = current_epoch_day_utc();
979 if (policy.max_eval_days > 0 && st.eval_start_day_utc == 0) {
980 st.eval_start_day_utc = today;
981 persist_required =
true;
984 int64_t expiry_day = policy.explicit_expiry_day_utc;
985 if (expiry_day == 0 && policy.max_eval_days > 0 && st.eval_start_day_utc > 0) {
986 expiry_day = st.eval_start_day_utc +
static_cast<int64_t
>(policy.max_eval_days) - 1;
989 if (expiry_day > 0 && today > expiry_day) {
991 std::string(feature_name) +
992 ": evaluation period expired; commercial license required"};
995 const std::string user_id = detect_runtime_user();
996 if (st.users.insert(user_id).second) {
997 persist_required =
true;
999 if (policy.max_users > 0 && st.users.size() > policy.max_users) {
1001 std::string(feature_name) +
1002 ": evaluation user threshold exceeded; commercial license required"};
1005 const std::string node_id = detect_runtime_node();
1006 if (st.nodes.insert(node_id).second) {
1007 persist_required =
true;
1009 if (policy.max_nodes > 0 && st.nodes.size() > policy.max_nodes) {
1011 std::string(feature_name) +
1012 ": evaluation node threshold exceeded; commercial license required"};
1015 if (policy.max_rows_month > 0) {
1016 if (rows_increment > 0) {
1017 if (st.rows_this_month >
1018 (std::numeric_limits<uint64_t>::max)() - rows_increment) {
1020 std::string(feature_name) +
1021 ": usage counter overflow; commercial license required"};
1024 const uint64_t projected = st.rows_this_month + rows_increment;
1025 if (projected > policy.max_rows_month) {
1027 std::string(feature_name) +
1028 ": evaluation monthly row threshold exceeded; "
1029 "commercial license required"};
1032 st.rows_this_month = projected;
1033 if ((st.rows_this_month - st.last_persisted_rows_this_month)
1034 >= kUsagePersistIntervalRows ||
1035 st.rows_this_month == policy.max_rows_month) {
1036 persist_required =
true;
1038 }
else if (st.rows_this_month >= policy.max_rows_month) {
1040 std::string(feature_name) +
1041 ": evaluation monthly row threshold reached; "
1042 "commercial license required"};
1045 const long double pct =
1046 (
static_cast<long double>(st.rows_this_month) * 100.0L) /
1047 static_cast<long double>(policy.max_rows_month);
1049 if (policy.warn_pct_1 > 0 && !st.warn_pct_1_emitted &&
1050 pct >=
static_cast<long double>(policy.warn_pct_1)) {
1051 std::fprintf(stderr,
1052 "signet commercial eval warning: %.2Lf%% of monthly row "
1053 "limit used (%llu/%llu)\n",
1055 static_cast<unsigned long long>(st.rows_this_month),
1056 static_cast<unsigned long long>(policy.max_rows_month));
1057 st.warn_pct_1_emitted =
true;
1058 persist_required =
true;
1061 if (policy.warn_pct_2 > 0 && !st.warn_pct_2_emitted &&
1062 pct >=
static_cast<long double>(policy.warn_pct_2)) {
1063 std::fprintf(stderr,
1064 "signet commercial eval warning: %.2Lf%% of monthly row "
1065 "limit used (%llu/%llu)\n",
1067 static_cast<unsigned long long>(st.rows_this_month),
1068 static_cast<unsigned long long>(policy.max_rows_month));
1069 st.warn_pct_2_emitted =
true;
1070 persist_required =
true;
1074 if (persist_required && !persist_usage_state_to_file(usage_state_path(), st)) {
1076 std::string(feature_name) +
1077 ": unable to persist evaluation usage state"};
1094[[nodiscard]]
inline expected<void> require_feature(
const char* feature_name,
1095 uint64_t usage_rows = 0) {
1099 std::string(feature_name) +
": " + gate.
error().
message};
1102 auto policy_gate = enforce_eval_limits(feature_name, usage_rows);
1104 return policy_gate.error();
1120[[nodiscard]]
inline expected<void> record_usage_rows(
const char* feature_name,
1122 return require_feature(feature_name, rows);