Add clock drift error messages to remaining 18 implementations

When timestamp auth fails (401 with timestamp in error), show helpful message:
- Error: Request timestamp expired (must be within 5 minutes of server time)
- Your computer's clock may have drifted.
- NTP sync commands for Linux/macOS/Windows

Updated: Un.cs, Un.java, un.clj, un.cob, un.erl, un.ex, un.f90, un.forth,
un.fs, un.hs, un.lisp, un.m, un.ml, un.pro, un.r, un.raku, un.scm, un.zig

All 42 implementations now have clock drift detection.
This commit is contained in:
Russell Ballestrini 2025-12-28 15:22:06 -05:00
parent bc0da08a08
commit a5a4f23594
18 changed files with 455 additions and 42 deletions

18
Un.cs
View file

@ -605,13 +605,31 @@ class Un
catch (WebException ex)
{
string error = "";
int statusCode = 0;
if (ex.Response != null)
{
using (StreamReader reader = new StreamReader(ex.Response.GetResponseStream()))
{
error = reader.ReadToEnd();
}
if (ex.Response is HttpWebResponse httpResponse)
{
statusCode = (int)httpResponse.StatusCode;
}
}
// Check for clock drift errors
if (error.Contains("timestamp") && (statusCode == 401 || error.ToLower().Contains("expired") || error.ToLower().Contains("invalid")))
{
Console.Error.WriteLine($"{RED}Error: Request timestamp expired (must be within 5 minutes of server time){RESET}");
Console.Error.WriteLine($"{YELLOW}Your computer's clock may have drifted.{RESET}");
Console.Error.WriteLine("Check your system time and sync with NTP if needed:");
Console.Error.WriteLine(" Linux: sudo ntpdate -s time.nist.gov");
Console.Error.WriteLine(" macOS: sudo sntp -sS time.apple.com");
Console.Error.WriteLine(" Windows: w32tm /resync");
Environment.Exit(1);
}
throw new Exception($"HTTP error - {error}");
}
}

12
Un.java
View file

@ -555,6 +555,18 @@ public class Un {
int status = conn.getResponseCode();
if (status < 200 || status >= 300) {
String error = readStream(conn.getErrorStream());
// Check for clock drift errors
if (error.contains("timestamp") && (status == 401 || error.toLowerCase().contains("expired") || error.toLowerCase().contains("invalid"))) {
System.err.println(RED + "Error: Request timestamp expired (must be within 5 minutes of server time)" + RESET);
System.err.println(YELLOW + "Your computer's clock may have drifted." + RESET);
System.err.println("Check your system time and sync with NTP if needed:");
System.err.println(" Linux: sudo ntpdate -s time.nist.gov");
System.err.println(" macOS: sudo sntp -sS time.apple.com");
System.err.println(" Windows: w32tm /resync");
System.exit(1);
}
throw new Exception("HTTP " + status + " - " + error);
}

30
un.clj
View file

@ -127,6 +127,22 @@
(let [message (str timestamp ":" method ":" path ":" body)]
(hmac-sha256 secret-key message)))
(defn check-clock-drift-error [response]
(let [has-timestamp (or (str/includes? response "timestamp")
(str/includes? response "\"timestamp\""))
has-401 (str/includes? response "401")
has-expired (str/includes? response "expired")
has-invalid (str/includes? response "invalid")]
(when (and has-timestamp (or has-401 has-expired has-invalid))
(binding [*out* *err*]
(println (str red "Error: Request timestamp expired (must be within 5 minutes of server time)" reset))
(println (str yellow "Your computer's clock may have drifted." reset))
(println "Check your system time and sync with NTP if needed:")
(println " Linux: sudo ntpdate -s time.nist.gov")
(println " macOS: sudo sntp -sS time.apple.com")
(println " Windows: w32tm /resync"))
(System/exit 1))))
(defn build-auth-headers [public-key secret-key method path body]
(if secret-key
(let [timestamp (str (quot (System/currentTimeMillis) 1000))
@ -148,6 +164,7 @@
["-d" (str "@" tmp-file)])
{:keys [out]} (apply sh args)]
(io/delete-file tmp-file true)
(check-clock-drift-error out)
out)))
(defn curl-get [api-key endpoint]
@ -155,16 +172,20 @@
auth-headers (build-auth-headers public-key secret-key "GET" endpoint "")
args (concat ["curl" "-s"
(str "https://api.unsandbox.com" endpoint)]
auth-headers)]
(:out (apply sh args))))
auth-headers)
result (:out (apply sh args))]
(check-clock-drift-error result)
result))
(defn curl-delete [api-key endpoint]
(let [[public-key secret-key] (get-api-keys)
auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint "")
args (concat ["curl" "-s" "-X" "DELETE"
(str "https://api.unsandbox.com" endpoint)]
auth-headers)]
(:out (apply sh args))))
auth-headers)
result (:out (apply sh args))]
(check-clock-drift-error result)
result))
(defn curl-portal-post [api-key endpoint json-data]
(let [tmp-file (str "/tmp/un_clj_portal_" (rand-int 999999) ".json")
@ -178,6 +199,7 @@
["-d" (str "@" tmp-file)])
{:keys [out]} (apply sh args)]
(io/delete-file tmp-file true)
(check-clock-drift-error out)
out)))
(defn execute-command [file env-vars artifacts out-dir network vcpu]

22
un.cob
View file

@ -293,14 +293,32 @@
"openssl dgst -sha256 -hmac '"
FUNCTION TRIM(WS-SECRET-KEY)
"' | cut -d' ' -f2); "
"curl -s -X POST https://api.unsandbox.com/execute "
"RESP=$(curl -s -w '\n%{http_code}' -X POST "
"https://api.unsandbox.com/execute "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer "
FUNCTION TRIM(WS-PUBLIC-KEY)
"' "
"-H 'X-Timestamp: '$TS "
"-H 'X-Signature: '$SIG "
"--data-binary \"$BODY\" -o /tmp/unsandbox_resp.json; "
"--data-binary \"$BODY\"); "
"HTTP_CODE=$(echo \"$RESP\" | tail -n1); "
"BODY=$(echo \"$RESP\" | sed '$d'); "
"echo \"$BODY\" > /tmp/unsandbox_resp.json; "
"if echo \"$BODY\" | grep -q '\"timestamp\"' && "
"(echo \"$HTTP_CODE\" | grep -q '401' || "
"echo \"$BODY\" | grep -qi 'expired' || "
"echo \"$BODY\" | grep -qi 'invalid'); then "
"echo -e '\x1b[31mError: Request timestamp expired "
"(must be within 5 minutes of server time)\x1b[0m' >&2; "
"echo -e '\x1b[33mYour computer'"'"'s clock may have "
"drifted.\x1b[0m' >&2; "
"echo 'Check your system time and sync with NTP if "
"needed:' >&2; "
"echo ' Linux: sudo ntpdate -s time.nist.gov' >&2; "
"echo ' macOS: sudo sntp -sS time.apple.com' >&2; "
"echo ' Windows: w32tm /resync' >&2; "
"rm -f /tmp/unsandbox_resp.json; exit 1; fi; "
"jq -r '.stdout // empty' /tmp/unsandbox_resp.json | "
"sed 's/^/\x1b[34m/' | sed 's/$/\x1b[0m/'; "
"jq -r '.stderr // empty' /tmp/unsandbox_resp.json | "

32
un.erl
View file

@ -322,6 +322,25 @@ make_signature(SecretKey, Timestamp, Method, Path, Body) ->
Message = Timestamp ++ ":" ++ Method ++ ":" ++ Path ++ ":" ++ Body,
hmac_sha256(SecretKey, Message).
check_clock_drift_error(Response) ->
HasTimestamp = string:str(Response, "timestamp") > 0 orelse string:str(Response, "\"timestamp\"") > 0,
Has401 = string:str(Response, "401") > 0,
HasExpired = string:str(Response, "expired") > 0,
HasInvalid = string:str(Response, "invalid") > 0,
case HasTimestamp andalso (Has401 orelse HasExpired orelse HasInvalid) of
true ->
io:format(standard_error, "\033[31mError: Request timestamp expired (must be within 5 minutes of server time)\033[0m~n", []),
io:format(standard_error, "\033[33mYour computer's clock may have drifted.\033[0m~n", []),
io:format(standard_error, "Check your system time and sync with NTP if needed:~n", []),
io:format(standard_error, " Linux: sudo ntpdate -s time.nist.gov~n", []),
io:format(standard_error, " macOS: sudo sntp -sS time.apple.com~n", []),
io:format(standard_error, " Windows: w32tm /resync~n", []),
halt(1);
false ->
ok
end.
build_auth_headers(PublicKey, SecretKey, Method, Path, Body) ->
if
SecretKey =/= false ->
@ -404,7 +423,9 @@ curl_post(ApiKey, Endpoint, TmpFile) ->
" -H 'Content-Type: application/json'" ++
AuthHeaders ++
" -d @" ++ TmpFile,
os:cmd(Cmd).
Result = os:cmd(Cmd),
check_clock_drift_error(Result),
Result.
curl_post_portal(ApiKey, Endpoint, Data) ->
TmpFile = write_temp_file(Data),
@ -416,6 +437,7 @@ curl_post_portal(ApiKey, Endpoint, Data) ->
" -d @" ++ TmpFile,
Result = os:cmd(Cmd),
file:delete(TmpFile),
check_clock_drift_error(Result),
Result.
curl_get(ApiKey, Endpoint) ->
@ -423,14 +445,18 @@ curl_get(ApiKey, Endpoint) ->
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "GET", Endpoint, ""),
Cmd = "curl -s https://api.unsandbox.com" ++ Endpoint ++
AuthHeaders,
os:cmd(Cmd).
Result = os:cmd(Cmd),
check_clock_drift_error(Result),
Result.
curl_delete(ApiKey, Endpoint) ->
{PublicKey, SecretKey} = get_api_keys(),
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "DELETE", Endpoint, ""),
Cmd = "curl -s -X DELETE https://api.unsandbox.com" ++ Endpoint ++
AuthHeaders,
os:cmd(Cmd).
Result = os:cmd(Cmd),
check_clock_drift_error(Result),
Result.
%% Argument parsing
parse_exec_args([], Opts) ->

24
un.ex
View file

@ -463,6 +463,7 @@ defmodule Un do
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
File.rm(tmp_file)
check_clock_drift(output)
output
end
@ -497,6 +498,7 @@ defmodule Un do
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
File.rm(tmp_file)
check_clock_drift(output)
output
end
@ -511,6 +513,7 @@ defmodule Un do
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
check_clock_drift(output)
output
end
@ -525,6 +528,7 @@ defmodule Un do
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
check_clock_drift(output)
output
end
@ -554,6 +558,26 @@ defmodule Un do
defp get_opt([_arg | rest], long, short, default) do
get_opt(rest, long, short, default)
end
defp check_clock_drift(response) do
response_lower = String.downcase(response)
# Check if response contains "timestamp" and error indicators
has_timestamp = String.contains?(response_lower, "timestamp")
has_error = String.contains?(response_lower, "401") or
String.contains?(response_lower, "expired") or
String.contains?(response_lower, "invalid")
if has_timestamp and has_error do
IO.puts(:stderr, "#{@red}Error: Request timestamp expired (must be within 5 minutes of server time)#{@reset}")
IO.puts(:stderr, "#{@yellow}Your computer's clock may have drifted.")
IO.puts(:stderr, "Check your system time and sync with NTP if needed:")
IO.puts(:stderr, " Linux: sudo ntpdate -s time.nist.gov")
IO.puts(:stderr, " macOS: sudo sntp -sS time.apple.com")
IO.puts(:stderr, " Windows: w32tm /resync#{@reset}")
System.halt(1)
end
end
end
Un.main(System.argv())

10
un.f90
View file

@ -167,6 +167,16 @@ contains
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'--data-binary "$BODY" -o /tmp/unsandbox_resp.json; ', &
'RESP=$(cat /tmp/unsandbox_resp.json); ', &
'if echo "$RESP" | grep -q "timestamp" && ', &
'(echo "$RESP" | grep -Eq "(401|expired|invalid)"); then ', &
'echo -e "\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m" >&2; ', &
'echo -e "\x1b[33mYour computer'\''s clock may have drifted.\x1b[0m" >&2; ', &
'echo "Check your system time and sync with NTP if needed:" >&2; ', &
'echo " Linux: sudo ntpdate -s time.nist.gov" >&2; ', &
'echo " macOS: sudo sntp -sS time.apple.com" >&2; ', &
'echo -e " Windows: w32tm /resync\x1b[0m" >&2; ', &
'rm -f /tmp/unsandbox_resp.json; exit 1; fi; ', &
'jq -r ".stdout // empty" /tmp/unsandbox_resp.json | ', &
'sed "s/^/\x1b[34m/" | sed "s/$/\x1b[0m/"; ', &
'jq -r ".stderr // empty" /tmp/unsandbox_resp.json | ', &

View file

@ -157,7 +157,21 @@
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:POST:/execute:$BODY\"" r@ write-line throw
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" -o /tmp/unsandbox_resp.json; jq -r '.stdout // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[34m/' | sed 's/$/\\x1b[0m/'; jq -r '.stderr // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[31m/' | sed 's/$/\\x1b[0m/' >&2; rm -f /tmp/unsandbox_resp.json" r@ write-line throw
s" curl -s -X POST https://api.unsandbox.com/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" -o /tmp/unsandbox_resp.json" r@ write-line throw
s" RESP=$(cat /tmp/unsandbox_resp.json)" r@ write-line throw
s" if echo \"$RESP\" | grep -q \"timestamp\" && (echo \"$RESP\" | grep -Eq \"(401|expired|invalid)\"); then" r@ write-line throw
s" echo -e '\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m' >&2" r@ write-line throw
s" echo -e '\\x1b[33mYour computer'\\''s clock may have drifted.\\x1b[0m' >&2" r@ write-line throw
s" echo 'Check your system time and sync with NTP if needed:' >&2" r@ write-line throw
s" echo ' Linux: sudo ntpdate -s time.nist.gov' >&2" r@ write-line throw
s" echo ' macOS: sudo sntp -sS time.apple.com' >&2" r@ write-line throw
s" echo -e ' Windows: w32tm /resync\\x1b[0m' >&2" r@ write-line throw
s" rm -f /tmp/unsandbox_resp.json" r@ write-line throw
s" exit 1" r@ write-line throw
s" fi" r@ write-line throw
s" jq -r '.stdout // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[34m/' | sed 's/$/\\x1b[0m/'" r@ write-line throw
s" jq -r '.stderr // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[31m/' | sed 's/$/\\x1b[0m/' >&2" r@ write-line throw
s" rm -f /tmp/unsandbox_resp.json" r@ write-line throw
r> close-file throw
s" chmod +x /tmp/unsandbox_script.sh && /tmp/unsandbox_script.sh && rm -f /tmp/unsandbox_script.sh" system

22
un.fs
View file

@ -279,6 +279,17 @@ let apiRequest (endpoint: string) (method: string) (data: (string * obj) list op
reader.ReadToEnd()
else
ex.Message
// Check for clock drift error
if errorMsg.Contains("timestamp") && (errorMsg.Contains("401") || errorMsg.Contains("expired") || errorMsg.Contains("invalid")) then
eprintfn "%sError: Request timestamp expired (must be within 5 minutes of server time)%s" red reset
eprintfn "%sYour computer's clock may have drifted.%s" yellow reset
eprintfn "Check your system time and sync with NTP if needed:"
eprintfn " Linux: sudo ntpdate -s time.nist.gov"
eprintfn " macOS: sudo sntp -sS time.apple.com"
eprintfn " Windows: w32tm /resync%s" reset
exit 1
failwithf "HTTP error - %s" errorMsg
let cmdExecute (args: Args) =
@ -432,6 +443,17 @@ let cmdKey (args: Args) =
with _ -> body
else
ex.Message
// Check for clock drift error
if errorMsg.Contains("timestamp") && (errorMsg.Contains("401") || errorMsg.Contains("expired") || errorMsg.Contains("invalid")) then
eprintfn "%sError: Request timestamp expired (must be within 5 minutes of server time)%s" red reset
eprintfn "%sYour computer's clock may have drifted.%s" yellow reset
eprintfn "Check your system time and sync with NTP if needed:"
eprintfn " Linux: sudo ntpdate -s time.nist.gov"
eprintfn " macOS: sudo sntp -sS time.apple.com"
eprintfn " Windows: w32tm /resync%s" reset
exit 1
printfn "Reason: %s" errorMsg
exit 1

36
un.hs
View file

@ -383,6 +383,28 @@ serviceCommand opts = do
putStrLn $ green ++ "Service created" ++ reset
putStrLn stdout
-- Check for clock drift error
checkClockDriftError :: String -> IO ()
checkClockDriftError response = do
let hasTimestamp = "timestamp" `isPrefixOf` dropWhile (/= 't') response ||
"\"timestamp\"" `isInfixOf` response
let has401 = "401" `isInfixOf` response
let hasExpired = "expired" `isInfixOf` response
let hasInvalid = "invalid" `isInfixOf` response
when (hasTimestamp && (has401 || hasExpired || hasInvalid)) $ do
hPutStrLn stderr $ red ++ "Error: Request timestamp expired (must be within 5 minutes of server time)" ++ reset
hPutStrLn stderr $ yellow ++ "Your computer's clock may have drifted." ++ reset
hPutStrLn stderr "Check your system time and sync with NTP if needed:"
hPutStrLn stderr " Linux: sudo ntpdate -s time.nist.gov"
hPutStrLn stderr " macOS: sudo sntp -sS time.apple.com"
hPutStrLn stderr " Windows: w32tm /resync"
exitFailure
where
isInfixOf needle haystack = any (isPrefixOf needle) (tails haystack)
tails [] = [[]]
tails s@(_:xs) = s : tails xs
-- HTTP helpers using curl
curlPost :: String -> String -> String -> IO (ExitCode, String, String)
curlPost apiKey url body = do
@ -395,6 +417,8 @@ curlPost apiKey url body = do
, url
, "-H", "Content-Type: application/json"
] ++ authHeaders ++ ["-d", body]) ""
-- Check for clock drift error
checkClockDriftError stdout
return (exitCode, stdout, stderr)
curlGet :: String -> String -> IO (ExitCode, String, String)
@ -402,16 +426,22 @@ curlGet apiKey url = do
(publicKey, secretKey) <- getApiKeys
let path = drop (length "https://api.unsandbox.com") url
authHeaders <- buildAuthHeaders publicKey secretKey "GET" path ""
readProcessWithExitCode "curl"
(exitCode, stdout, stderr) <- readProcessWithExitCode "curl"
([ "-s", url ] ++ authHeaders) ""
-- Check for clock drift error
checkClockDriftError stdout
return (exitCode, stdout, stderr)
curlDelete :: String -> String -> IO (ExitCode, String, String)
curlDelete apiKey url = do
(publicKey, secretKey) <- getApiKeys
let path = drop (length "https://api.unsandbox.com") url
authHeaders <- buildAuthHeaders publicKey secretKey "DELETE" path ""
readProcessWithExitCode "curl"
(exitCode, stdout, stderr) <- readProcessWithExitCode "curl"
([ "-s", "-X", "DELETE", url ] ++ authHeaders) ""
-- Check for clock drift error
checkClockDriftError stdout
return (exitCode, stdout, stderr)
-- Get API keys from environment
getApiKeys :: IO (String, Maybe String)
@ -615,4 +645,6 @@ curlPostPortal apiKey url body = do
, url
, "-H", "Content-Type: application/json"
] ++ authHeaders ++ ["-d", body]) ""
-- Check for clock drift error
checkClockDriftError stdout
return (exitCode, stdout, stderr)

58
un.lisp
View file

@ -101,40 +101,62 @@
while line do (format out "~a~%" line))
(uiop:wait-process process))))
(defun check-clock-drift (response)
"Check if response indicates clock drift error"
(when (and (search "timestamp" response)
(or (search "401" response)
(search "expired" response)
(search "invalid" response)))
(format t "~aError: Request timestamp expired (must be within 5 minutes of server time)~a~%" *red* *reset*)
(format t "~aYour computer's clock may have drifted.~a~%" *yellow* *reset*)
(format t "Check your system time and sync with NTP if needed:~%")
(format t " Linux: sudo ntpdate -s time.nist.gov~%")
(format t " macOS: sudo sntp -sS time.apple.com~%")
(format t " Windows: w32tm /resync~a~%" *reset*)
(uiop:quit 1)))
(defun curl-post (api-key endpoint json-data)
(let ((tmp-file (write-temp-file json-data)))
(unwind-protect
(destructuring-bind (public-key secret-key) (get-api-keys)
(let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
(base-args (list "curl" "-s" "-X" "POST"
(format nil "https://api.unsandbox.com~a" endpoint)
"-H" "Content-Type: application/json")))
(run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
(let* ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
(base-args (list "curl" "-s" "-X" "POST"
(format nil "https://api.unsandbox.com~a" endpoint)
"-H" "Content-Type: application/json"))
(response (run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
(check-clock-drift response)
response))
(delete-file tmp-file))))
(defun curl-get (api-key endpoint)
(destructuring-bind (public-key secret-key) (get-api-keys)
(let ((auth-headers (build-auth-headers public-key secret-key "GET" endpoint ""))
(base-args (list "curl" "-s"
(format nil "https://api.unsandbox.com~a" endpoint))))
(run-curl (append base-args auth-headers)))))
(let* ((auth-headers (build-auth-headers public-key secret-key "GET" endpoint ""))
(base-args (list "curl" "-s"
(format nil "https://api.unsandbox.com~a" endpoint)))
(response (run-curl (append base-args auth-headers))))
(check-clock-drift response)
response)))
(defun curl-delete (api-key endpoint)
(destructuring-bind (public-key secret-key) (get-api-keys)
(let ((auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint ""))
(base-args (list "curl" "-s" "-X" "DELETE"
(format nil "https://api.unsandbox.com~a" endpoint))))
(run-curl (append base-args auth-headers)))))
(let* ((auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint ""))
(base-args (list "curl" "-s" "-X" "DELETE"
(format nil "https://api.unsandbox.com~a" endpoint)))
(response (run-curl (append base-args auth-headers))))
(check-clock-drift response)
response)))
(defun curl-post-portal (api-key endpoint json-data)
(let ((tmp-file (write-temp-file json-data)))
(unwind-protect
(destructuring-bind (public-key secret-key) (get-api-keys)
(let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
(base-args (list "curl" "-s" "-X" "POST"
(format nil "~a~a" *portal-base* endpoint)
"-H" "Content-Type: application/json")))
(run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
(let* ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
(base-args (list "curl" "-s" "-X" "POST"
(format nil "~a~a" *portal-base* endpoint)
"-H" "Content-Type: application/json"))
(response (run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
(check-clock-drift response)
response))
(delete-file tmp-file))))
(defun get-api-keys ()

20
un.m
View file

@ -94,6 +94,24 @@ void getApiKeys(NSString** publicKey, NSString** secretKey) {
}
}
void checkClockDrift(NSString* response) {
NSString* responseLower = [response lowercaseString];
if ([responseLower rangeOfString:@"timestamp"].location != NSNotFound &&
([responseLower rangeOfString:@"401"].location != NSNotFound ||
[responseLower rangeOfString:@"expired"].location != NSNotFound ||
[responseLower rangeOfString:@"invalid"].location != NSNotFound)) {
fprintf(stderr, "%sError: Request timestamp expired (must be within 5 minutes of server time)%s\n",
[RED UTF8String], [RESET UTF8String]);
fprintf(stderr, "%sYour computer's clock may have drifted.%s\n",
[YELLOW UTF8String], [RESET UTF8String]);
fprintf(stderr, "Check your system time and sync with NTP if needed:\n");
fprintf(stderr, " Linux: sudo ntpdate -s time.nist.gov\n");
fprintf(stderr, " macOS: sudo sntp -sS time.apple.com\n");
fprintf(stderr, " Windows: w32tm /resync%s\n", [RESET UTF8String]);
exit(1);
}
}
NSString* hmacSha256Hex(NSString* key, NSString* message) {
const char* cKey = [key UTF8String];
const char* cMessage = [message UTF8String];
@ -170,6 +188,7 @@ NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* dat
if (responseData) {
NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
fprintf(stderr, "%s\n", [errMsg UTF8String]);
checkClockDrift(errMsg);
}
exit(1);
}
@ -350,6 +369,7 @@ NSDictionary* portalRequest(NSString* endpoint, NSString* method, NSDictionary*
if (responseData) {
NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
fprintf(stderr, "%s\n", [errMsg UTF8String]);
checkClockDrift(errMsg);
}
exit(1);
}

29
un.ml
View file

@ -112,6 +112,31 @@ let escape_json s =
) s;
Buffer.contents buf
(* Check for clock drift errors *)
let check_clock_drift response =
let response_lower = String.lowercase_ascii response in
let contains_substring s sub =
try
let _ = Str.search_forward (Str.regexp_string sub) s 0 in
true
with Not_found -> false
in
let has_timestamp = contains_substring response_lower "timestamp" in
let has_401 = contains_substring response_lower "401" in
let has_expired = contains_substring response_lower "expired" in
let has_invalid = contains_substring response_lower "invalid" in
let has_error = has_401 || has_expired || has_invalid in
if has_timestamp && has_error then begin
Printf.fprintf stderr "%sError: Request timestamp expired (must be within 5 minutes of server time)%s\n" red reset;
Printf.fprintf stderr "%sYour computer's clock may have drifted.\n" yellow;
Printf.fprintf stderr "Check your system time and sync with NTP if needed:\n";
Printf.fprintf stderr " Linux: sudo ntpdate -s time.nist.gov\n";
Printf.fprintf stderr " macOS: sudo sntp -sS time.apple.com\n";
Printf.fprintf stderr " Windows: w32tm /resync%s\n" reset;
exit 1
end
(* Execute curl command *)
let curl_post api_key endpoint json =
let (public_key, secret_key) = get_api_keys () in
@ -130,6 +155,7 @@ let curl_post api_key endpoint json =
let output = read_all "" in
let _ = Unix.close_process_in ic in
Sys.remove tmp_file;
check_clock_drift output;
output
let portal_curl_post api_key endpoint json =
@ -149,6 +175,7 @@ let portal_curl_post api_key endpoint json =
let output = read_all "" in
let _ = Unix.close_process_in ic in
Sys.remove tmp_file;
check_clock_drift output;
output
let curl_get api_key endpoint =
@ -165,6 +192,7 @@ let curl_get api_key endpoint =
in
let output = read_all "" in
let _ = Unix.close_process_in ic in
check_clock_drift output;
output
let curl_delete api_key endpoint =
@ -181,6 +209,7 @@ let curl_delete api_key endpoint =
in
let output = read_all "" in
let _ = Unix.close_process_in ic in
check_clock_drift output;
output
(* Extract JSON value - simple regex-based parser *)

10
un.pro
View file

@ -128,7 +128,7 @@ execute_file(Filename) :-
% Build and execute curl command with HMAC
format(atom(Cmd),
'BODY=$(jq -Rs \'\'\''{language: "~w", code: .}\'\'\'\' < "~w"); TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); curl -s -X POST https://api.unsandbox.com/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_resp.json; jq -r ".stdout // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[34m/" | sed "s/$/\\x1b[0m/"; jq -r ".stderr // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[31m/" | sed "s/$/\\x1b[0m/" >&2; rm -f /tmp/unsandbox_resp.json',
'BODY=$(jq -Rs \'\'\''{language: "~w", code: .}\'\'\'\' < "~w"); TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); curl -s -X POST https://api.unsandbox.com/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_resp.json; RESP=$(cat /tmp/unsandbox_resp.json); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; rm -f /tmp/unsandbox_resp.json; exit 1; fi; jq -r ".stdout // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[34m/" | sed "s/$/\\x1b[0m/"; jq -r ".stderr // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[31m/" | sed "s/$/\\x1b[0m/" >&2; rm -f /tmp/unsandbox_resp.json',
[Language, Filename, SecretKey, PublicKey]),
shell(Cmd, 0).
@ -137,7 +137,7 @@ session_list :-
get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd),
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/sessions:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/sessions -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r \'.sessions[] | "\\(.id) \\(.shell) \\(.status) \\(.created_at)"\' 2>/dev/null || echo "No active sessions"',
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/sessions:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X GET https://api.unsandbox.com/sessions -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE"); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; exit 1; fi; echo "$RESP" | jq -r \'.sessions[] | "\\(.id) \\(.shell) \\(.status) \\(.created_at)"\' 2>/dev/null || echo "No active sessions"',
[SecretKey, PublicKey]),
shell(Cmd, 0).
@ -155,7 +155,7 @@ service_list :-
get_public_key(PublicKey),
get_secret_key(SecretKey),
format(atom(Cmd),
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/services -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r \'.services[] | "\\(.id) \\(.name) \\(.status)"\' 2>/dev/null || echo "No services"',
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X GET https://api.unsandbox.com/services -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE"); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; exit 1; fi; echo "$RESP" | jq -r \'.services[] | "\\(.id) \\(.name) \\(.status)"\' 2>/dev/null || echo "No services"',
[SecretKey, PublicKey]),
shell(Cmd, 0).
@ -252,11 +252,11 @@ validate_key(Extend) :-
( Extend = true
-> % Build command for --extend mode
format(atom(Cmd),
'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null',
'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; exit 1; fi; PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null',
[SecretKey, PortalBase, PublicKey, PortalBase])
; % Build command for normal validation
format(atom(Cmd),
'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json',
'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; RESP=$(cat /tmp/unsandbox_key_resp.json); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; rm -f /tmp/unsandbox_key_resp.json; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json',
[SecretKey, PortalBase, PublicKey])
),
shell(Cmd, 0).

31
un.r
View file

@ -93,6 +93,25 @@ get_api_keys <- function(args_key = NULL) {
return(list(public_key = public_key, secret_key = secret_key))
}
check_clock_drift <- function(response_text) {
response_lower <- tolower(response_text)
has_timestamp <- grepl("timestamp", response_lower, fixed = TRUE)
has_401 <- grepl("401", response_lower, fixed = TRUE)
has_expired <- grepl("expired", response_lower, fixed = TRUE)
has_invalid <- grepl("invalid", response_lower, fixed = TRUE)
has_error <- has_401 || has_expired || has_invalid
if (has_timestamp && has_error) {
cat(sprintf("%sError: Request timestamp expired (must be within 5 minutes of server time)%s\n", RED, RESET), file = stderr())
cat(sprintf("%sYour computer's clock may have drifted.\n", YELLOW), file = stderr())
cat("Check your system time and sync with NTP if needed:\n", file = stderr())
cat(" Linux: sudo ntpdate -s time.nist.gov\n", file = stderr())
cat(" macOS: sudo sntp -sS time.apple.com\n", file = stderr())
cat(sprintf(" Windows: w32tm /resync%s\n", RESET), file = stderr())
quit(status = 1)
}
}
api_request <- function(endpoint, public_key, secret_key, method = "GET", data = NULL) {
url <- paste0(API_BASE, endpoint)
headers <- add_headers(
@ -129,7 +148,9 @@ api_request <- function(endpoint, public_key, secret_key, method = "GET", data =
stop(paste("Unsupported method:", method))
}
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
response_text <- content(response, "text", encoding = "UTF-8")
check_clock_drift(response_text)
result <- fromJSON(response_text)
return(result)
}, error = function(e) {
cat(sprintf("%sError: Request failed: %s%s\n", RED, e$message, RESET), file = stderr())
@ -292,7 +313,9 @@ cmd_key <- function(args) {
tryCatch({
response <- POST(url, headers, encode = "json", timeout(10))
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
response_text <- content(response, "text", encoding = "UTF-8")
check_clock_drift(response_text)
result <- fromJSON(response_text)
if (!is.null(result$public_key)) {
extend_url <- paste0(PORTAL_BASE, "/keys/extend?pk=", result$public_key)
@ -331,7 +354,9 @@ cmd_key <- function(args) {
tryCatch({
response <- POST(url, headers, encode = "json", timeout(10))
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
response_text <- content(response, "text", encoding = "UTF-8")
check_clock_drift(response_text)
result <- fromJSON(response_text)
status <- if (!is.null(result$status)) result$status else "Unknown"

11
un.raku
View file

@ -148,6 +148,17 @@ sub api-request(Str $endpoint, Str $method, %data?, Str :$public-key!, Str :$sec
exit 1;
}
# Check for clock drift errors
if $body.contains('timestamp') && ($body.contains('401') || $body.contains('expired') || $body.contains('invalid')) {
note "{$RED}Error: Request timestamp expired (must be within 5 minutes of server time){$RESET}";
note "{$YELLOW}Your computer's clock may have drifted.{$RESET}";
note "{$YELLOW}Check your system time and sync with NTP if needed:{$RESET}";
note "{$YELLOW} Linux: sudo ntpdate -s time.nist.gov{$RESET}";
note "{$YELLOW} macOS: sudo sntp -sS time.apple.com{$RESET}";
note "{$YELLOW} Windows: w32tm /resync{$RESET}";
exit 1;
}
return from-json($body);
}

48
un.scm
View file

@ -117,6 +117,18 @@
(loop (cons line lines)))))))
(close-pipe port)
(delete-file tmp-file)
;; Check for clock drift errors
(when (and (string-contains output "timestamp")
(or (string-contains output "401")
(string-contains output "expired")
(string-contains output "invalid")))
(format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset)
(format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset)
(format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset)
(format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset)
(format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset)
(format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset)
(exit 1))
output))
(define (curl-get api-key endpoint)
@ -133,6 +145,18 @@
(string-join (reverse lines) "\n")
(loop (cons line lines)))))))
(close-pipe port)
;; Check for clock drift errors
(when (and (string-contains output "timestamp")
(or (string-contains output "401")
(string-contains output "expired")
(string-contains output "invalid")))
(format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset)
(format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset)
(format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset)
(format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset)
(format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset)
(format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset)
(exit 1))
output))
(define (curl-delete api-key endpoint)
@ -149,6 +173,18 @@
(string-join (reverse lines) "\n")
(loop (cons line lines)))))))
(close-pipe port)
;; Check for clock drift errors
(when (and (string-contains output "timestamp")
(or (string-contains output "401")
(string-contains output "expired")
(string-contains output "invalid")))
(format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset)
(format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset)
(format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset)
(format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset)
(format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset)
(format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset)
(exit 1))
output))
(define (curl-post-portal api-key endpoint json-data)
@ -169,6 +205,18 @@
(loop (cons line lines)))))))
(close-pipe port)
(delete-file tmp-file)
;; Check for clock drift errors
(when (and (string-contains output "timestamp")
(or (string-contains output "401")
(string-contains output "expired")
(string-contains output "invalid")))
(format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset)
(format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset)
(format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset)
(format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset)
(format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset)
(format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset)
(exit 1))
output))
(define (get-api-keys)

68
un.zig
View file

@ -362,6 +362,21 @@ pub fn main() !u8 {
defer allocator.free(json_content);
std.fs.cwd().deleteFile(json_file) catch {};
// Check for clock drift errors
if (mem.indexOf(u8, json_content, "timestamp") != null and
(mem.indexOf(u8, json_content, "401") != null or
mem.indexOf(u8, json_content, "expired") != null or
mem.indexOf(u8, json_content, "invalid") != null))
{
std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{});
std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{});
std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{});
std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{});
std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{});
std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{});
return 1;
}
// Simple JSON parsing to find public_key (looking for "public_key":"value")
const pk_prefix = "\"public_key\":\"";
var public_key_value: ?[]const u8 = null;
@ -401,6 +416,21 @@ pub fn main() !u8 {
defer allocator.free(json_content);
std.fs.cwd().deleteFile(json_file) catch {};
// Check for clock drift errors
if (mem.indexOf(u8, json_content, "timestamp") != null and
(mem.indexOf(u8, json_content, "401") != null or
mem.indexOf(u8, json_content, "expired") != null or
mem.indexOf(u8, json_content, "invalid") != null))
{
std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{});
std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{});
std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{});
std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{});
std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{});
std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{});
return 1;
}
// Simple JSON parsing (looking for specific fields)
const status_prefix = "\"status\":\"";
var status: ?[]const u8 = null;
@ -535,14 +565,44 @@ pub fn main() !u8 {
// Execute with curl
const auth_headers = try buildAuthCmd(allocator, "POST", "/execute", json_content, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' {s} -d @{s}", .{ API_BASE, auth_headers, json_file });
const response_file = "/tmp/unsandbox_response.json";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' {s} -d @{s} -o {s}", .{ API_BASE, auth_headers, json_file, response_file });
defer allocator.free(cmd);
const result = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
_ = std.c.system(cmd.ptr);
// Read response to check for clock drift errors
const response_content = fs.cwd().readFileAlloc(allocator, response_file, 10 * 1024 * 1024) catch |err| {
std.debug.print("\x1b[31mError reading response: {}\x1b[0m\n", .{err});
std.fs.cwd().deleteFile(json_file) catch {};
std.fs.cwd().deleteFile(response_file) catch {};
return 1;
};
defer allocator.free(response_content);
// Check for clock drift errors
if (mem.indexOf(u8, response_content, "timestamp") != null and
(mem.indexOf(u8, response_content, "401") != null or
mem.indexOf(u8, response_content, "expired") != null or
mem.indexOf(u8, response_content, "invalid") != null))
{
std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{});
std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{});
std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{});
std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{});
std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{});
std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{});
std.fs.cwd().deleteFile(json_file) catch {};
std.fs.cwd().deleteFile(response_file) catch {};
return 1;
}
// Print response
std.debug.print("{s}\n", .{response_content});
// Cleanup
std.fs.cwd().deleteFile(json_file) catch {};
std.fs.cwd().deleteFile(response_file) catch {};
return if (result == 0) 0 else 1;
return 0;
}