Elige tu lenguaje
Cada biblioteca habla el mismo borrador del protocolo. Lo que cambia: para qué sirve, qué necesita y hasta dónde llega.
- Versión del SDK 0.5.0
- Borrador de la especificación 0.11
- Versión de la API C 7
Aún no hay nada publicado: las líneas de instalación aparecerán con la primera release. Releases · GitHub
¿Cuál me conviene?
- Homebrew en una consola: C++
- Una app móvil (Swift, Kotlin): C
- Agregar Lantern a una app Flask o FastAPI: Python
- Node, un navegador o Hono: TypeScript
C++23
Completa para el borrador Cliente y servidor
Cliente 16/16 Servidor 0/16
- Necesita
- C++23 (CMake 3.28)
- Paquete
lantern- Funciona con
- libcurl, mbedTLS, POSIX sockets
C
Completa para el borrador Cliente
Cliente 16/16 Servidor 0/16
- Necesita
- C11, over the C++ library
- Paquete
lantern (C ABI)- Funciona con
- Swift, Kotlin (JNI)
Python
Completa para el borrador Cliente y servidor
Cliente 16/16 Servidor 16/16
- Necesita
- Python 3.11+
- Paquete
lantern-protocol- Funciona con
- Flask, FastAPI, httpx
TypeScript
Completa para el borrador Cliente y servidor
Cliente 16/16 Servidor 16/16
- Necesita
- Node 22.18+ or a browser
- Paquete
lantern-protocol- Funciona con
- node:http, Hono (web Requests)
Go
En construcción Cliente y servidor
Cliente 5/16 Servidor 0/16
- Necesita
- Go 1.24+
- Paquete
github.com/JigSawFr/ lantern-sdk/ go - Funciona con
- net/http
Rust
En construcción Cliente y servidor
Cliente 0/16 Servidor 0/16
- Necesita
- Rust 1.90+
- Paquete
lantern-protocol
Paridad
Cada familia de la especificación, leída por un cliente y servida por un servidor, en cada biblioteca. La regla: lo que un lenguaje hace del protocolo, los demás lo hacen, en el mismo borrador.
| Familia | C++ | C | Python | TypeScript | Go | Rust | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Cliente | Servidor | Cliente | Servidor | Cliente | Servidor | Cliente | Servidor | Cliente | Servidor | Cliente | Servidor | |
library | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no |
titles | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no |
changes | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no |
sections | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
homebrew | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
saves | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
cheats | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
requests | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
pairing | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no |
signatures | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no |
admin | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
events | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
peers | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
sync | Sí | Aún no | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
encrypted_saves | Sí | En parte | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
deltas | Sí | Aún no | Sí | Aún no | Sí | Sí | Sí | Sí | Aún no | Aún no | Aún no | Aún no |
| Cubierto | 16/16 | 0/16 | 16/16 | 0/16 | 16/16 | 16/16 | 16/16 | 16/16 | 5/16 | 0/16 | 0/16 | 0/16 |
C++ Cliente 16/16 · Servidor 0/16
| Familia | Cliente | Servidor |
|---|---|---|
library | Sí | En parte |
titles | Sí | En parte |
changes | Sí | En parte |
sections | Sí | En parte |
homebrew | Sí | En parte |
saves | Sí | En parte |
cheats | Sí | En parte |
requests | Sí | En parte |
pairing | Sí | En parte |
signatures | Sí | En parte |
admin | Sí | En parte |
events | Sí | En parte |
peers | Sí | En parte |
sync | Sí | Aún no |
encrypted_saves | Sí | En parte |
deltas | Sí | Aún no |
C Cliente 16/16 · Servidor 0/16
| Familia | Cliente | Servidor |
|---|---|---|
library | Sí | Aún no |
titles | Sí | Aún no |
changes | Sí | Aún no |
sections | Sí | Aún no |
homebrew | Sí | Aún no |
saves | Sí | Aún no |
cheats | Sí | Aún no |
requests | Sí | Aún no |
pairing | Sí | Aún no |
signatures | Sí | Aún no |
admin | Sí | Aún no |
events | Sí | Aún no |
peers | Sí | Aún no |
sync | Sí | Aún no |
encrypted_saves | Sí | Aún no |
deltas | Sí | Aún no |
Python Cliente 16/16 · Servidor 16/16
| Familia | Cliente | Servidor |
|---|---|---|
library | Sí | Sí |
titles | Sí | Sí |
changes | Sí | Sí |
sections | Sí | Sí |
homebrew | Sí | Sí |
saves | Sí | Sí |
cheats | Sí | Sí |
requests | Sí | Sí |
pairing | Sí | Sí |
signatures | Sí | Sí |
admin | Sí | Sí |
events | Sí | Sí |
peers | Sí | Sí |
sync | Sí | Sí |
encrypted_saves | Sí | Sí |
deltas | Sí | Sí |
TypeScript Cliente 16/16 · Servidor 16/16
| Familia | Cliente | Servidor |
|---|---|---|
library | Sí | Sí |
titles | Sí | Sí |
changes | Sí | Sí |
sections | Sí | Sí |
homebrew | Sí | Sí |
saves | Sí | Sí |
cheats | Sí | Sí |
requests | Sí | Sí |
pairing | Sí | Sí |
signatures | Sí | Sí |
admin | Sí | Sí |
events | Sí | Sí |
peers | Sí | Sí |
sync | Sí | Sí |
encrypted_saves | Sí | Sí |
deltas | Sí | Sí |
Go Cliente 5/16 · Servidor 0/16
| Familia | Cliente | Servidor |
|---|---|---|
library | Sí | Aún no |
titles | Sí | Aún no |
changes | Sí | Aún no |
sections | Aún no | Aún no |
homebrew | Aún no | Aún no |
saves | Aún no | Aún no |
cheats | Aún no | Aún no |
requests | Aún no | Aún no |
pairing | Sí | Aún no |
signatures | Sí | Aún no |
admin | Aún no | Aún no |
events | Aún no | Aún no |
peers | Aún no | Aún no |
sync | Aún no | Aún no |
encrypted_saves | Aún no | Aún no |
deltas | Aún no | Aún no |
Rust Cliente 0/16 · Servidor 0/16
| Familia | Cliente | Servidor |
|---|---|---|
library | Aún no | Aún no |
titles | Aún no | Aún no |
changes | Aún no | Aún no |
sections | Aún no | Aún no |
homebrew | Aún no | Aún no |
saves | Aún no | Aún no |
cheats | Aún no | Aún no |
requests | Aún no | Aún no |
pairing | Aún no | Aún no |
signatures | Aún no | Aún no |
admin | Aún no | Aún no |
events | Aún no | Aún no |
peers | Aún no | Aún no |
sync | Aún no | Aún no |
encrypted_saves | Aún no | Aún no |
deltas | Aún no | Aún no |
✓ sí · en parte · – aún no. Un servidor solo tiene ✓ en una familia si la suite de conformidad la pasa en la CI.
Niveles de conformidad alcanzados
Los niveles (SPEC.md § 17) que cada servidor verificado alcanza en la CI, con un token que tiene todos los ámbitos.
La misma llamada en cada lenguaje
Un gesto del protocolo y el nombre de su llamada en cada biblioteca, con un enlace a su definición.
| Gesto | C++ | C | Python | TypeScript |
|---|---|---|---|---|
| Apretón de manos § 4.2 | handshake | lantern_client_handshake | handshake | handshake |
| Listar las apps § 6.1 | apps | lantern_client_apps | iter_apps | iterApps |
| Lo que falta § 6.3 | missing | lantern_client_missing | missing | missing |
| Descarga verificada § 6.6 | download | lantern_client_download | download_app | downloadAppTo |
| Pedir el emparejamiento § 5.2 | start_pairing | lantern_client_start_pairing | pair | pair |
| Esperar la aprobación § 5.4 | poll_pairing | lantern_client_poll_pairing | wait_for_pairing | waitForPairing |
| Enviar una partida guardada § 9.3 | upload_save | lantern_client_upload_save | upload_save | uploadSave |
| Seguir los eventos § 18 | events | lantern_client_events | events | events |
| Servir una biblioteca § 17 | Server | Aún no | LanternApp | LanternApp |
| Escribir una biblioteca estática § 21 | Aún no | Aún no | export | writeStatic |
| Leer una biblioteca estática § 21 | StaticLibrary | Aún no | StaticLibrary | StaticLibrary |
Las llamadas de Go, Rust aparecerán aquí a medida que avancen sus bibliotecas.
Ejemplos
Archivos de la carpeta de ejemplos de cada biblioteca, ejecutados tal cual en la CI: funcionan.
Leer una biblioteca
The README's example: the handshake, a listing, a verified download · cpp/examples/read_library.cpp
// The README's example: the handshake, a listing, a verified download.
//
// read_library http://127.0.0.1:8080 [platform]
//
// Over the loopback adapter (plain HTTP on 127.0.0.1 only); an app gives
// its own Transport, with TLS.
#include <cstdio>
#include <string>
#include <lantern/lantern.hpp>
#include "posix_http.hpp"
namespace
{
// Bytes kept in memory; an app writes them to its storage.
class Counting final : public lantern::DownloadSink
{
public:
std::uint64_t bytes = 0;
lantern::Result<void> write(std::span<const std::byte> chunk) override
{
bytes += chunk.size();
return {};
}
lantern::Result<void> restart() override
{
bytes = 0;
return {};
}
};
int report(const lantern::Error& error)
{
const std::string_view category = lantern::category_name(error.category());
const std::string_view code = lantern::code_name(error.effective_code());
std::fprintf(stderr, "error (%.*s, %u %.*s): %.*s\n", static_cast<int>(category.size()), category.data(),
static_cast<unsigned>(error.status()), static_cast<int>(code.size()), code.data(),
static_cast<int>(error.detail().size()), error.detail().data());
return 1;
}
} // namespace
int main(int argc, char** argv)
{
if (argc < 2)
{
std::fprintf(stderr, "usage: read_library ORIGIN [platform]\n");
return 2;
}
lantern::posix::LoopbackTransport transport;
lantern::ClientConfig config;
config.origin = argv[1];
config.user_agent = "ReadLibrary/0.3";
config.platform = argc > 2 ? argv[2] : "switch";
auto client = lantern::Client::create(transport, config);
if (!client)
return report(client.error());
// 1. The handshake: the server's name, its base path, what it offers
const auto server = client->handshake();
if (!server)
return report(server.error());
if (!*server)
{
std::fprintf(stderr, "the server speaks a newer schema than this client reads\n");
return 1;
}
std::printf("%s (%s)\n", (*server)->name.c_str(), (*server)->uid.c_str());
// 2. The listing, page by page
lantern::AppsQuery query;
query.page_size = 50;
std::string cursor;
std::optional<lantern::App> first;
do
{
query.cursor = cursor;
const auto page = client->apps(query);
if (!page)
return report(page.error());
if (!*page)
break; // a newer schema: the listing is unavailable to this client
for (const lantern::App& app : (*page)->items)
{
std::printf(" %-24s %-6s v%-10llu %s\n", app.name.c_str(), app.type.c_str(),
static_cast<unsigned long long>(app.version_code), app.app_id.c_str());
if (!first)
first.emplace(app, client->allocator());
}
cursor.assign((*page)->next);
} while (!cursor.empty());
// 3. A download, checked against the listing's size and SHA-256: success
// only when the whole file's digest matches
if (first)
{
Counting sink;
lantern::DownloadState state;
const auto done = client->download({ first->file, first->size, first->sha256 }, state, sink);
if (!done)
return report(done.error());
std::printf("downloaded %s: %llu bytes, SHA-256 verified\n", first->name.c_str(),
static_cast<unsigned long long>(sink.bytes));
}
return 0;
}
Reads a Lantern server's library, page by page, and downloads its first app, checked, over libcurl: c/README.md's example · c/examples/read_library.c
/* Reads a Lantern server's library, page by page, and downloads its first
* app, checked, over libcurl: c/README.md's example.
* read_library https://shelf.example.org [TOKEN SEED_HEX] */
#include <lantern/lantern.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static lantern_status write_file(void* user, const uint8_t* bytes, size_t size)
{
return fwrite(bytes, 1, size, (FILE*)user) == size ? LANTERN_OK : LANTERN_ERROR_STORAGE;
}
/* The server sends the file from its start again */
static lantern_status restart_file(void* user)
{
return fseek((FILE*)user, 0, SEEK_SET) == 0 ? LANTERN_OK : LANTERN_ERROR_STORAGE;
}
static int report(const char* what, const lantern_error* error)
{
fprintf(stderr, "%s: %s (%s, HTTP %u, %s)\n", what, error->detail, lantern_status_name(error->status),
(unsigned)error->http_status, error->code_text);
return EXIT_FAILURE;
}
static bool seed_of(const char* hex, uint8_t seed[32])
{
return lantern_digest_from_hex(hex, seed); /* 64 lower-case hex digits, as a digest */
}
int main(int argc, char** argv)
{
if (argc != 2 && argc != 4)
{
fprintf(stderr, "usage: %s ORIGIN [TOKEN SEED_HEX]\n", argv[0]);
return EXIT_FAILURE;
}
if (lantern_version() < LANTERN_C_API_VERSION)
{
fprintf(stderr, "liblantern_c is older than its header\n");
return EXIT_FAILURE;
}
lantern_error error = LANTERN_INIT(lantern_error);
lantern_trust* trust = NULL;
lantern_transport* transport = NULL;
lantern_random* random = NULL;
lantern_client* client = NULL;
lantern_handshake* server = NULL;
lantern_apps_page* first = NULL;
int status = EXIT_FAILURE;
/* libcurl's TLS, the server's key checked by the trust (SPEC.md 5.3) */
lantern_curl_options options = LANTERN_INIT(lantern_curl_options);
if (lantern_trust_create(NULL, &trust, &error) != LANTERN_OK
|| lantern_curl_global_init(&error) != LANTERN_OK)
goto done;
options.trust = trust;
if (lantern_transport_create_curl(&options, NULL, &transport, &error) != LANTERN_OK
|| lantern_random_create_system(NULL, &random, &error) != LANTERN_OK)
goto done;
lantern_client_config config = LANTERN_INIT(lantern_client_config);
config.origin = argv[1];
config.user_agent = "ReadLibrary/1.0";
config.platform = "switch";
config.transport = transport;
config.random = random;
config.trust = trust;
if (lantern_client_create(&config, &client, &error) != LANTERN_OK)
goto done;
if (argc == 4)
{
/* A token bound to a device key (SPEC.md 5.4): every request is signed */
uint8_t seed[32];
lantern_device_key* key = NULL;
if (!seed_of(argv[3], seed) || lantern_device_key_from_seed(seed, NULL, &key, &error) != LANTERN_OK)
goto done;
const lantern_status set = lantern_client_set_credentials(client, argv[2], key, &error);
lantern_device_key_free(key); /* the client keeps a copy */
if (set != LANTERN_OK)
goto done;
}
const lantern_status shake = lantern_client_handshake(client, &server, &error);
if (shake == LANTERN_ABSENT)
{
fprintf(stderr, "a server of a newer protocol: update the app\n");
goto done;
}
if (shake != LANTERN_OK)
goto done;
printf("%s\n", lantern_handshake_get_name(server).data);
/* Every page, by its cursor; the first page is kept for its first app */
char cursor[512] = "";
do
{
lantern_apps_query query = LANTERN_INIT(lantern_apps_query);
query.cursor = cursor;
lantern_apps_page* page = NULL;
if (lantern_client_apps(client, &query, &page, &error) != LANTERN_OK)
goto done;
for (size_t i = 0; i < lantern_apps_page_get_items_count(page); ++i)
{
const lantern_app* app = lantern_apps_page_get_items_at(page, i);
printf(" %s (%s)\n", lantern_app_get_name(app).data, lantern_app_get_app_id(app).data);
}
snprintf(cursor, sizeof(cursor), "%s", lantern_apps_page_get_next(page).data);
if (first == NULL)
first = page;
else
lantern_apps_page_free(page);
} while (cursor[0] != '\0');
const lantern_app* app = lantern_apps_page_get_items_at(first, 0);
if (app == NULL)
{
status = EXIT_SUCCESS;
goto done;
}
/* The file, its size and SHA-256 as the listing gave them */
lantern_download_target target = LANTERN_INIT(lantern_download_target);
target.url = lantern_app_get_file(app).data;
lantern_app_get_size(app, &target.size);
lantern_app_get_sha256(app, target.sha256);
FILE* file = fopen("first-app.part", "wb");
if (file == NULL)
goto done;
lantern_download_callbacks callbacks = LANTERN_INIT(lantern_download_callbacks);
callbacks.user = file;
callbacks.write = write_file;
callbacks.restart = restart_file;
lantern_download_sink* sink = NULL;
lantern_download_state* state = NULL;
lantern_status got = lantern_download_sink_create(&callbacks, NULL, &sink, &error);
if (got == LANTERN_OK)
got = lantern_download_state_create(NULL, &state, &error);
if (got == LANTERN_OK)
got = lantern_client_download(client, &target, state, sink, &error);
lantern_download_sink_free(sink);
lantern_download_state_free(state);
fclose(file);
/* Only now, with the SHA-256 checked, is the file the app's */
if (got == LANTERN_OK && rename("first-app.part", "first-app.bin") == 0)
status = EXIT_SUCCESS;
done:
if (status != EXIT_SUCCESS && error.status != LANTERN_OK)
report("lantern", &error);
lantern_apps_page_free(first);
lantern_handshake_free(server);
lantern_client_free(client);
lantern_transport_free(transport);
lantern_random_free(random);
lantern_trust_free(trust);
return status;
}
Reads a Lantern library: who serves it, its apps, what a console is missing, and one verified download · python/examples/read_library.py
"""Reads a Lantern library: who serves it, its apps, what a console is missing, and one verified download.
python examples/read_library.py https://shelf.example.org/ [platform]
The download is checked against the SHA-256 the server lists and renamed only when it matches; a `.part`
file is resumed with a Range request after a cut (SPEC.md § 6.6).
"""
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
import lantern
def main(url: str, platform: str = "switch") -> None:
with lantern.LanternClient(url, platform=platform, user_agent="ReadLibrary/1.0") as client:
hello = client.handshake()
print(f"{hello.name}: {', '.join(sorted(hello.features))}")
apps = list(client.iter_apps(page_size=5)) # every page, cursor by cursor
for app in apps:
print(f" {app.name} {app.version_name or app.version_code} ({app.size} bytes)")
# What a console that holds the first app, at version 0, is missing.
if apps:
page = client.missing([(apps[0].app_id, 0)], want=("update", "dlc"))
print(f"missing: {len(page.items)} on the first page")
# The smallest non-empty file, downloaded to a temporary folder and checked on the way.
if apps:
smallest = min((a for a in apps if a.size), key=lambda a: a.size)
with tempfile.TemporaryDirectory() as folder:
saved = client.download_app(smallest, Path(folder) / f"{smallest.app_id}.pkg")
print(
f"downloaded {smallest.name}: {saved.stat().st_size} bytes, sha256 {smallest.sha256[:12]}… matched"
)
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit(__doc__)
main(*sys.argv[1:3])
Reads a Lantern library: who serves it, its apps, what a console is missing, and one verified download · typescript/examples/read-library.ts
/**
* Reads a Lantern library: who serves it, its apps, what a console is missing, and one verified download.
*
* node --conditions=source examples/read-library.ts https://shelf.example.org/ [platform]
*
* (In your own project, `npm install lantern-protocol` and run it with plain `node`.) The download is checked
* against the SHA-256 the server lists and renamed only when it matches; a `.part` file is resumed with a Range
* request after a cut (SPEC.md § 6.6).
*/
import { mkdtempSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { NodeLanternClient } from "lantern-protocol/node";
const [url, platform = "switch"] = process.argv.slice(2);
if (url === undefined) {
console.error("usage: read-library.ts <url> [platform]");
process.exit(2);
}
const client = new NodeLanternClient(url, { platform, userAgent: "ReadLibrary/1.0" });
const hello = await client.handshake();
console.log(`${hello.name}: ${Object.keys(hello.features).sort().join(", ")}`);
const apps = [];
for await (const app of client.iterApps({ pageSize: 5 })) {
// every page, cursor by cursor
apps.push(app);
console.log(` ${app.name} ${app.version_name ?? app.version_code} (${app.size} bytes)`);
}
// What a console that holds the first app, at version 0, is missing.
const first = apps[0];
if (first !== undefined) {
const page = await client.missing([[first.app_id, 0]]);
console.log(`missing: ${page.items.length} on the first page`);
}
// The smallest non-empty file, downloaded to a temporary folder and checked on the way.
const smallest = apps.filter((a) => a.size > 0).sort((a, b) => a.size - b.size)[0];
if (smallest !== undefined) {
const folder = mkdtempSync(join(tmpdir(), "lantern-"));
try {
const saved = await client.downloadAppTo(smallest, join(folder, `${smallest.app_id}.pkg`));
console.log(
`downloaded ${smallest.name}: ${statSync(saved).size} bytes, sha256 ${smallest.sha256.slice(0, 12)}… matched`,
);
} finally {
rmSync(folder, { recursive: true, force: true });
}
}
Emparejar un dispositivo
The README's pairing example: a device key, the code and the QR code's address shown, the polls signed, the token kept · cpp/examples/pair_device.cpp
// The README's pairing example: a device key, the code and the QR code's
// address shown, the polls signed, the token kept.
//
// pair_device http://127.0.0.1:8080 "Lea's Switch"
//
// Over the loopback adapter (plain HTTP on 127.0.0.1 only); an app gives
// its own Transport, with TLS and a ServerTrust (adapters/curl,
// adapters/mbedtls), and keeps the credentials where its system keeps
// secrets.
#include <array>
#include <chrono>
#include <cstdio>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <lantern/lantern.hpp>
#include "posix_http.hpp"
#include "posix_system.hpp"
namespace
{
// Kept in memory here; an app writes them where its system protects secrets.
class Kept final : public lantern::CredentialStore
{
public:
lantern::Result<std::optional<lantern::Credentials>> load(std::string_view, lantern::Allocator) override
{
return std::optional<lantern::Credentials> {};
}
lantern::Result<void> store(std::string_view uid, const lantern::Credentials& credentials) override
{
std::printf("kept for %.*s: a token%s\n", static_cast<int>(uid.size()), uid.data(),
credentials.key ? " and its device key" : "");
return {};
}
lantern::Result<void> erase(std::string_view) override { return {}; }
};
int report(const lantern::Error& error)
{
std::fprintf(stderr, "error (%u): %.*s\n", static_cast<unsigned>(error.status()),
static_cast<int>(error.detail().size()), error.detail().data());
return 1;
}
} // namespace
int main(int argc, char** argv)
{
if (argc < 3)
{
std::fprintf(stderr, "usage: pair_device ORIGIN DEVICE-NAME\n");
return 2;
}
lantern::posix::LoopbackTransport transport;
lantern::posix::SystemRandom random;
lantern::ClientConfig config;
config.origin = argv[1];
config.user_agent = "PairDevice/0.5";
config.platform = "switch";
config.random = &random;
auto client = lantern::Client::create(transport, config);
if (!client)
return report(client.error());
if (const auto server = client->handshake(); !server || !*server)
return server ? 1 : report(server.error());
const std::array<std::string_view, 2> scopes { "library", "saves" };
lantern::PairingOptions options;
options.device = argv[2];
options.scopes = scopes;
auto pairing = client->start_pairing(options); // with a new device key
if (!pairing)
return report(pairing.error());
// The code, and the address a QR code holds (the app draws it): the
// user approves on the server's page, or an admin's device
std::printf("code %s\nopen %s\n", pairing->code.code.c_str(), pairing->code.verify_complete.c_str());
Kept store;
for (;;)
{
std::this_thread::sleep_for(std::chrono::seconds(pairing->wait));
const auto polled = client->poll_pairing(*pairing, &store); // signed by the device key
if (!polled)
return report(polled.error());
if (*polled == lantern::PollState::paired)
break;
}
const auto server = client->handshake(); // signed from now on
if (!server || !*server)
return server ? 1 : report(server.error());
std::printf("paired as %s\n", (*server)->account.name.c_str());
return 0;
}
Servir una biblioteca
The server core over a small fictional library, for a look and for the conformance run · cpp/examples/demo_server.cpp
// The server core over a small fictional library, for a look and for the
// conformance run:
//
// lantern_demo_server [port] [--private] [--no-encrypted-saves]
// python3 spec/conformance/run.py http://127.0.0.1:PORT/ --origin https://app.example.org
//
// It serves the library (Lantern Core: the handshake, the apps, what a
// device is missing, the files, with its changes and sections, its titles
// with their images, its homebrew and its cheat sets) to anyone, takes cheat
// sets from a token with the scope cheats.write, keeps save backups (also
// encrypted ones) in memory for the accounts with the scope saves, keeps the
// requests for games of the accounts with the scope requests, tells what
// changes as events (a stream and the long poll of `changes`, § 18), and pairs
// devices: the accounts of demo_accounts.hpp, a token bound to a key, signed
// requests, invitations. An account with the scope admin runs the
// administration API: accounts, devices, pairings, invitations, requests.
// `--private` asks for a token on the library; `--no-encrypted-saves` keeps
// no backup encrypted on the device (a 400, § 9.4). Plain HTTP on 127.0.0.1; it
// prints its URL and serves until it is stopped. No port, or 0: a free one.
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <string_view>
#include <thread>
#include <lantern/lantern.hpp>
#include "demo_accounts.hpp"
#include "demo_library.hpp"
#include "posix_system.hpp"
#include "server_posix.hpp"
namespace
{
std::atomic<bool> stop { false };
extern "C" void on_signal(int /*signal*/)
{
stop.store(true);
}
int report(const lantern::Error& error)
{
std::fprintf(
stderr, "demo server: %.*s\n", static_cast<int>(error.detail().size()), error.detail().data());
return 1;
}
} // namespace
int main(int argc, char** argv)
{
unsigned port = 0;
bool is_private = false;
bool encrypted = true;
for (int i = 1; i < argc; ++i)
{
if (std::string_view(argv[i]) == "--private")
{
is_private = true;
continue;
}
if (std::string_view(argv[i]) == "--no-encrypted-saves")
{
encrypted = false;
continue;
}
char* end = nullptr;
const long n = std::strtol(argv[i], &end, 10);
if (end == argv[i] || *end != '\0' || n < 0 || n > 65535)
{
std::fprintf(stderr, "usage: lantern_demo_server [port] [--private] [--no-encrypted-saves]\n");
return 2;
}
port = static_cast<unsigned>(n);
}
lantern::MemoryLibrary library(lantern::demo::base);
if (auto filled = lantern::demo::fill(library); !filled)
return report(filled.error());
lantern::SystemClock clock;
lantern::MemoryAccountStore accounts;
if (auto filled = lantern::demo::fill_accounts(accounts, clock.unix_seconds()); !filled)
return report(filled.error());
lantern::MemorySaveStore saves;
lantern::MemoryRequestStore requests;
const std::array<std::string_view, 1> origins { lantern::demo::origin };
lantern::ServerOptions options;
options.uid = lantern::demo::uid;
options.name = "Demo shelf";
options.base = lantern::demo::base;
options.scheme = "http";
options.cors_origins = origins;
options.software_name = "lantern-sdk C++ demo";
options.software_version = "0.1";
options.accounts = &accounts;
options.require_auth = is_private;
options.saves = &saves;
options.requests = &requests;
options.encrypted_saves = encrypted;
// Events: a line on a stream every 5 seconds at least, which keeps the conformance run short
options.events = true;
options.heartbeat = 5;
options.wait_limit = 30;
lantern::posix::SystemRandom random;
auto server = lantern::Server::create(library, options, random, clock);
if (!server)
return report(server.error());
lantern::posix::ListenOptions listen;
listen.port = static_cast<std::uint16_t>(port);
// A cheat set is 1 MiB of text at most (§ 13), escaped in its JSON body
listen.max_body = std::size_t { 3 } << 20U;
// A save of up to save_limits.size is streamed to the store, never buffered
listen.max_stream = options.save_limits.size;
auto listener = lantern::posix::HttpListener::listen(*server, listen);
if (!listener)
return report(listener.error());
std::signal(SIGINT, on_signal);
std::signal(SIGTERM, on_signal);
std::printf("http://127.0.0.1:%u/\n", static_cast<unsigned>(listener->port()));
std::fflush(stdout);
listener->serve(stop);
// The streams still open are told, then the listener ends them
server->shutdown();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return 0;
}
Adds Lantern to a FastAPI app: the same one-game library as serve_flask.py · python/examples/serve_fastapi.py
"""Adds Lantern to a FastAPI app: the same one-game library as serve_flask.py.
uvicorn --factory examples.serve_fastapi:make_app --port 8080
python examples/serve_fastapi.py 8080
"""
from __future__ import annotations
import sys
from fastapi import FastAPI
from lantern.server import LanternApp, MemoryLibrary
from lantern.server.fastapi import include
def make_app() -> FastAPI:
library = MemoryLibrary(base="/api/lantern")
library.add_app_file(
data=b"a made-up game",
platform="switch",
title_id="0100ABCD00010000",
app_id="0100ABCD00010000",
type="base",
version_code=0,
name="Frost Garden",
format="nsp",
version_name="1.0.0",
)
app = FastAPI()
include(app, LanternApp(library, uid="3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name="Living room shelf"))
return app
if __name__ == "__main__":
import uvicorn
uvicorn.run(make_app(), host="127.0.0.1", port=int(sys.argv[1]) if len(sys.argv) > 1 else 8080)
Adds Lantern to a Flask app: a library of one made-up game, served anonymously · python/examples/serve_flask.py
"""Adds Lantern to a Flask app: a library of one made-up game, served anonymously.
python examples/serve_flask.py 8080 # then: curl http://127.0.0.1:8080/.well-known/lantern
`MemoryLibrary` holds apps in memory; a real server implements `lantern.server.Library` over its own
files and database. For tokens, pairing and signed requests, pass `accounts=MemoryAccounts(...)`
(or an `authenticate` hook) to `LanternApp`.
"""
from __future__ import annotations
import sys
from flask import Flask
from lantern.server import LanternApp, MemoryLibrary
from lantern.server.flask import init_app
def make_app() -> Flask:
library = MemoryLibrary(base="/api/lantern")
library.add_app_file(
data=b"a made-up game",
platform="switch",
title_id="0100ABCD00010000",
app_id="0100ABCD00010000",
type="base",
version_code=0,
name="Frost Garden",
format="nsp",
version_name="1.0.0",
)
app = Flask(__name__)
init_app(app, LanternApp(library, uid="3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name="Living room shelf"))
return app
if __name__ == "__main__":
make_app().run(host="127.0.0.1", port=int(sys.argv[1]) if len(sys.argv) > 1 else 8080)
Adds Lantern to a Node server: a library of one made-up game, served anonymously by node:http · typescript/examples/serve-node.ts
/**
* Adds Lantern to a Node server: a library of one made-up game, served anonymously by node:http.
*
* node --conditions=source examples/serve-node.ts 8080 # then: curl http://127.0.0.1:8080/.well-known/lantern
*
* `MemoryLibrary` holds apps in memory; a real server implements `Library` over its own files and database.
* For tokens, pairing and signed requests, pass `accounts: new MemoryAccounts(...)` (or `authenticate`) to
* `LanternApp`. Behind Hono or any framework of web Requests, use `fetchHandler(app)` instead of `listen()`:
* `hono.all("/*", (c) => handler(c.req.raw))`.
*/
import { LanternApp, listen, MemoryLibrary } from "lantern-protocol/server";
export function makeApp(): LanternApp {
const library = new MemoryLibrary({ base: "/api/lantern" });
library.addAppFile({
data: new TextEncoder().encode("a made-up game"),
platform: "switch",
title_id: "0100ABCD00010000",
app_id: "0100ABCD00010000",
type: "base",
version_code: 0,
version_name: "1.0.0",
name: "Frost Garden",
format: "nsp",
});
return new LanternApp(library, { uid: "3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name: "Living room shelf" });
}
const served = await listen(makeApp(), { port: Number(process.argv[2] ?? 8080), host: "127.0.0.1" });
console.log(`serving ${served.url}`);
process.on("SIGTERM", () => void served.close().then(() => process.exit(0)));
process.on("SIGINT", () => void served.close().then(() => process.exit(0)));
Publicar una biblioteca estática
A static library: a library written as JSON files (GitHub Pages, a NAS, a USB stick), then read back · python/examples/static_library.py
"""A static library: a library written as JSON files (GitHub Pages, a NAS, a USB stick), then read back.
python examples/static_library.py out/ # serve out/ with any web server, or open it as a folder
SPEC.md § 21. The files are the same documents a live server sends, with their SHA-256 and a handshake at
`lantern.json`, so a client reads them with no server code behind them.
"""
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
from lantern.server import MemoryLibrary
from lantern.static import StaticLibrary, export
def main(out: Path) -> None:
library = MemoryLibrary(base="/api/lantern")
library.add_app_file(
data=b"a made-up game",
platform="switch",
title_id="0100ABCD00010000",
app_id="0100ABCD00010000",
type="base",
version_code=0,
name="Frost Garden",
format="nsp",
version_name="1.0.0",
)
summary = export(library, out, uid="3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name="Shelf on a stick")
print(f"wrote {summary}")
with StaticLibrary(out) as shelf:
print(shelf.handshake().name, [app.name for app in shelf.iter_apps("switch")])
if __name__ == "__main__":
if len(sys.argv) > 1:
main(Path(sys.argv[1]))
else:
with tempfile.TemporaryDirectory() as folder:
main(Path(folder) / "shelf")
A static library: a library written as JSON files (GitHub Pages, a NAS, a USB stick), then read back (StaticLibrary.open("https://…") reads one over HTTP) · typescript/examples/static-library.ts
/**
* A static library: a library written as JSON files (GitHub Pages, a NAS, a USB stick), then read back (`StaticLibrary.open("https://…")` reads one over HTTP).
*
* node --conditions=source examples/static-library.ts out/
*
* SPEC.md § 21. The files are the same documents a live server sends, with their SHA-256 and a handshake in
* `lantern.json`, so a client reads them with no server code behind them.
*/
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { StaticLibrary } from "lantern-protocol";
import { FileStaticSource, libraryInput, writeStatic } from "lantern-protocol/node";
import { MemoryLibrary } from "lantern-protocol/server";
const library = new MemoryLibrary({ base: "/api/lantern" });
library.addAppFile({
data: new TextEncoder().encode("a made-up game"),
platform: "switch",
title_id: "0100ABCD00010000",
app_id: "0100ABCD00010000",
type: "base",
version_code: 0,
version_name: "1.0.0",
name: "Frost Garden",
format: "nsp",
});
const given = process.argv[2];
const out = given ?? join(mkdtempSync(join(tmpdir(), "lantern-static-")), "shelf");
try {
const summary = await writeStatic(
await libraryInput(library, { uid: "3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name: "Shelf on a stick" }),
out,
);
console.log(`wrote ${JSON.stringify(summary)}`);
const shelf = await StaticLibrary.open(new FileStaticSource(out));
const names: string[] = [];
for await (const app of shelf.apps("switch")) names.push(app.name ?? app.app_id);
console.log(shelf.hello.name, JSON.stringify(names));
} finally {
if (given === undefined) rmSync(join(out, ".."), { recursive: true, force: true });
}
Lo que las hace fiables
Suite de conformidad
Cada servidor pasa la suite de la especificación en la CI, nivel por nivel.
Un servidor de prueba hostil
Cada cliente rechaza hashes falsos, una llave cambiada, manifiestos hostiles y un esquema más reciente.
Vectores compartidos
Los mismos vectores de prueba se ejecutan en cada lenguaje.
Versiones firmadas
Las versiones se construirán desde la etiqueta con la CI, de forma reproducible, con un SBOM y procedencia SLSA.
Versiones
Una sola versión para todo el SDK, la misma en cada lenguaje.
| Versión del SDK | Borrador de la especificación | Versión de la API C | Última release |
|---|---|---|---|
| 0.5.0 | 0.11 | 7 (ABI 1) | ninguna todavía |
Datos de lantern-sdk en el commit 9fc3b4f.
Empieza desde una biblioteca, no desde cero
Prueba una con la biblioteca de demostración, o lee el código.