lb-phone
The configuration block, and why it targets Custom rather than a hosted method.
Use the Custom method
Config.UploadMethod accepts the hosted methods lb-phone ships with, and
Custom. Point at Custom.
In lb-phone 2.9.0, the release this guide was written against, every hosted
method holds url = "PRESIGNED_URL": lb-phone asks that host for an upload
address, and the block sends no Authorization header. None of them can be
pointed here, and Custom is the way in.
Custom is fully parameterised: URL, form field, headers, the path to the URL
in the response and the path that signals failure. It has not changed shape
across lb-phone versions, so a block written today keeps working, and it puts no
compatibility layer between the phone and the native API.
The block
In lb-phone/shared/upload.lua, inside UploadMethods.Custom:
Custom = {
Video = {
url = "https://your-instance/api/v1/uploads",
field = "file",
headers = { ["Authorization"] = "API_KEY" },
error = { path = "status", value = "error" },
success = { path = "data.url" },
},
Image = {
url = "https://your-instance/api/v1/uploads",
field = "file",
headers = { ["Authorization"] = "API_KEY" },
error = { path = "status", value = "error" },
success = { path = "data.url" },
},
Audio = {
url = "https://your-instance/api/v1/uploads",
field = "file",
headers = { ["Authorization"] = "API_KEY" },
error = { path = "status", value = "error" },
success = { path = "data.url" },
},
}Leave API_KEY exactly as written: it is a placeholder, and lb-phone swaps it
for the key of that kind from lb-phone/server/apiKeys.lua. The tokens go
there, in a server-side file, rather than in shared/, which every player
downloads when they join (read Three keys, treated as public
for what that does not protect):
API_KEYS = {
Video = "YOUR_VIDEO_TOKEN",
Image = "YOUR_IMAGE_TOKEN",
Audio = "YOUR_AUDIO_TOKEN",
}Then in lb-phone/config/config.lua:
Config.UploadMethod.Video = "Custom"
Config.UploadMethod.Image = "Custom"
Config.UploadMethod.Audio = "Custom"Allow the delivery host, or nothing is saved
This is the step that gets missed, and it fails in the most confusing way available: uploads succeed, the API returns a URL, the file is served correctly, and the photo still never appears in the phone.
lb-phone keeps its own allowlist of hosts it will accept an image from, so that somebody with devtools open cannot inject an arbitrary link. Yours is not on it.
In lb-phone/config/config.lua:
Config.UploadWhitelistedDomains = {
-- the hosts already listed stay as they are
"cdn.your-instance", -- your delivery host, exactly as it appears in the URL
}Use the delivery host, the one media URLs are served from, not the dashboard host the upload was posted to. They are deliberately different origins.
If you skip it the server console says so, once per attempt:
[LB Phone] [ERROR]: <id> tried to save an image with a link that is not allowed:
https://cdn.your-instance/your-org/01K2R7....webpConfig.ExternalWhitelistedDomains, just above it, governs images pasted from
elsewhere. It is empty by default, which means no restriction; if you ever fill
it, the same host has to go in there too.
Why those two paths
success.path = "data.url" is where the URL sits in our response. It is also
where a v3 compatible response puts it, which is why a configuration written for
the v3 routes works against the native endpoint with no facade in between.
error.path = "status" with value = "error" is how the phone detects failure.
Our envelope carries a single top-level status that is either ok or error,
specifically so this check has something stable to read.
Three keys, treated as public
apiKeys.lua keeps the keys out of shared/, so they are not in the files
every player downloads. It does not keep them from players: lb-phone sends each
upload from the player's game, so the key of that kind reaches the game of the
player who uploads, and a curious player can read it there.
So treat every key in that file as exposed. Use three, each scoped to uploading its own kind and nothing else, and give them to no other script. When one turns up where it should not, and that does happen, an image key that cannot upload video is a much smaller problem than one key that can do everything: revoke it and put a new one in its place.
Add no other header to the Custom block. Its headers are the same fixed
strings on every upload, so a fixed Idempotency-Key would return the first
photo's URL for every later upload, for a day. A player-metadata header
filled from PLAYER_DATA fails too: the game's browser refuses to send a header
holding a character outside Latin-1, which blocks every upload of a player
whose name has one, and sends accented letters in a form Aperture cannot
decode.
Video settings on the phone side
Config.Video.Bitrate and Config.Video.MaxSize still govern what the phone
records and sends. Aperture re-encodes afterwards, so you can leave the phone
generous and let the compression profile decide the final size. The clip is
playable at its URL the moment the upload returns, before that re-encode
finishes.