local createCloud = require('/apis/libcloud'); local createVersion = require('/apis/libversion'); local createEventLoop = require('/apis/eventloop'); local URL_SETTING = 'cloud.url'; local PASSWORD_SETTING = 'cloud.password'; local RECONNECT_EVENT = 'trapos_cloud_reconnect'; local VERIFY_TIMEOUT = 12; -- one hello attempt; daemon helloTimeout is 10s local cloudApi = createCloud(); local rawArgs = table.pack(...); local command = rawArgs[1]; local function isBlank(value) return type(value) ~= 'string' or value == ''; end local function printUsage() print('cloud usage:'); print(); print(' cloud status'); print(' cloud login [--force|-f] [password]'); print(' cloud --version'); print(' cloud --help'); print(); print('settings required:'); print(' cloud.url (gateway origin, e.g. ws://host:4444)'); print('settings optional:'); print(' cloud.password (shared secret for the hello handshake)'); end local function readPassword() write('Password: '); if type(read) == 'function' then return read('*'); end if io and type(io.read) == 'function' then return io.read(); end return ''; end local function parseLoginArgs(args) local force = false; local password = nil; for i = 2, args.n do local arg = args[i]; if arg == '--force' or arg == '-f' then force = true; elseif arg and string.sub(arg, 1, 1) == '-' then return nil, 'unknown option: ' .. arg; elseif not password then password = arg; else return nil, 'unexpected argument: ' .. tostring(arg); end end return { force = force, password = password }, nil; end if command == '--version' or command == '-version' or command == 'version' or command == '-v' then print('v' .. createVersion().forSelf()); return; end if command == '--help' or command == '-help' or command == 'help' or command == '-h' then printUsage(); return; end if command == 'status' then local url = settings.get(URL_SETTING); if isBlank(url) then print('set cloud.url first'); return; end local ok, result = cloudApi.request('probe_server'); if not ok then local code = (type(result) == 'table' and result.code) or 'internal'; local message = (type(result) == 'table' and result.message) or ''; print('cloud status failed: ' .. tostring(code) .. ' ' .. tostring(message)); return; end print('serverOk: ' .. tostring(result.serverOk)); print('opencodeOk: ' .. tostring(result.opencodeOk)); if not result.opencodeOk and result.opencodeDetail then print('opencode: ' .. tostring(result.opencodeDetail)); end return; end if command == 'login' then local opts, parseError = parseLoginArgs(rawArgs); if parseError then print(parseError); printUsage(); return; end if not opts.force and not isBlank(settings.get(PASSWORD_SETTING)) then print('cloud.password is already set; use cloud login --force to replace it'); return; end if isBlank(settings.get(URL_SETTING)) then print('set cloud.url first'); return; end -- Ask the daemon to reconnect with `candidate` (never persisted) and wait for the handshake -- verdict carrying our token. Returns 'ok' | 'unauthorized' | 'timeout' | 'terminate'. Other -- daemon verdicts (tokenless or for a different token) are ignored. local function verifyPassword(candidate) local token = cloudApi.uuid(); local result = nil; local el = createEventLoop(); local function finish(value) if result ~= nil then return; end result = value; el.stopLoop(); end el.register('trapos_cloud_connected', function(_traposId, eventToken) if eventToken == token then finish('ok'); end end); el.register('trapos_cloud_unauthorized', function(_traposId, _err, eventToken) if eventToken == token then finish('unauthorized'); end end); -- terminate stops the default loop on its own; just record it for the caller. el.register('terminate', function() result = 'terminate'; end); el.setTimeout(function() finish('timeout'); end, VERIFY_TIMEOUT); os.queueEvent(RECONNECT_EVENT, 'login-verify', candidate, token); el.runLoop(); return result; end local function restore() os.queueEvent(RECONNECT_EVENT, 'login-restore'); end -- Persist only after a verified password. Roll the live setting back if the save fails so the -- daemon (which reads live settings) never keeps an unsaved secret. No reconnect on success: -- the live socket already authenticated with this secret. local function savePassword(password) local old = settings.get(PASSWORD_SETTING); settings.set(PASSWORD_SETTING, password); if not settings.save() then if old ~= nil then settings.set(PASSWORD_SETTING, old); else settings.unset(PASSWORD_SETTING); end restore(); error('cloud login failed: could not save settings', 0); end print('logged in'); end if opts.password == nil then -- Interactive: re-prompt on a wrong password; ctrl+t cancels. while true do local candidate = readPassword(); if isBlank(candidate) then print('password cannot be empty'); else local result = verifyPassword(candidate); if result == 'ok' then savePassword(candidate); return; elseif result == 'unauthorized' then restore(); print('invalid password, try again (ctrl+t to cancel)'); elseif result == 'timeout' then restore(); print('cloud login failed: could not reach gateway'); return; else -- terminate restore(); error('Terminated', 0); end end end end -- Non-interactive: a wrong password fails with a non-zero exit. if isBlank(opts.password) then error('cloud login failed: password cannot be empty', 0); end local result = verifyPassword(opts.password); if result == 'ok' then savePassword(opts.password); return; elseif result == 'unauthorized' then restore(); error('cloud login failed: invalid password', 0); elseif result == 'timeout' then restore(); error('cloud login failed: could not reach gateway', 0); else -- terminate restore(); error('Terminated', 0); end end printUsage();