Skip to content

Web helpers

These are building blocks for signing in to services, receiving callbacks and talking to other apps. Nari doesn’t sign in for you: combine them the way the service needs.

local server = nari.serve(function(request)
-- request: { method, path, query, headers, body }
if request.path == "/callback" then
finish_login(request.query.code)
return "<h1>You can go back to Nari</h1>"
end
return { status = 404, body = "Not here" }
end)
nari.log(server.url) -- http://127.0.0.1:<a free port>
-- server.stop() when you're done
  • It only listens on 127.0.0.1, so only this device reaches it.
  • It picks a free port. Pass { port = 8898 } when the service only accepts a fixed address; it raises an error if that port is taken.
  • The handler returns a string (sent as HTML), a table { status, headers, body }, or nothing (204). If it fails, the browser gets a 500.
  • Servers stop when their thread stops or the plugin is turned off.
local cipher = nari.cipher({ algorithm = "aes-ctr", key = key, iv = iv })
local plain = cipher:update(encrypted)
Key
algorithm aes-ctr, aes-cbc, aes-ecb (16, 24 or 32 byte keys), blowfish-cbc or blowfish-ecb (4 to 56 byte keys).
key, iv Bytes. iv is the starting counter for CTR, and ECB doesn’t use it.
decrypt true by default, false encrypts. CTR does both the same way.

cipher:update(bytes) returns the processed bytes and carries on from where it stopped every time. CBC and ECB take whole blocks (16 bytes for AES, 8 for Blowfish) and don’t add or remove padding. cipher:seek(offset) jumps a CTR cipher to a byte offset, and cipher:reset() starts over. The work runs in Rust, fast enough for audio streams.

nari://plugin/<your plugin id>/anything?you=want opens Nari (or the Nari that’s already open) and fires your plugin’s link event with the whole URL. Services that only redirect to custom schemes can send users back this way.

It works on Android and on Linux (the Flatpak registers the scheme). On Windows and macOS the link works when Nari is started with it.

local verifier = nari.base64_encode(nari.random_bytes(32), true)
local challenge = nari.base64_encode(nari.sha256(verifier, true), true)
local state = nari.base64_encode(nari.random_bytes(16), true)
local server
server = nari.serve(function(request)
if request.path ~= "/callback" or request.query.state ~= state then
return { status = 400, body = "Unexpected request" }
end
server.stop()
nari.http({
method = "POST",
url = "https://accounts.example.com/token",
headers = { ["Content-Type"] = "application/x-www-form-urlencoded" },
body = "grant_type=authorization_code&code=" .. request.query.code
.. "&code_verifier=" .. verifier .. "&redirect_uri=" .. server.url .. "/callback&client_id=MY_ID",
}, function(response)
nari.set("token", nari.json_decode(response.body).access_token, { private = true })
end)
return "<h1>Signed in, you can close this tab</h1>"
end)
nari.open_url("https://accounts.example.com/authorize?response_type=code&client_id=MY_ID"
.. "&redirect_uri=" .. server.url .. "/callback&code_challenge_method=S256"
.. "&code_challenge=" .. challenge .. "&state=" .. state)

For sign-in pages that need cookies instead, use a web view.