ライブラリ · lantern-sdk

SDK: あなたの言語で使う Lantern

C++、C、Python、TypeScript、Go のクライアントとサーバーのライブラリ。すべて同じバージョンで、同じスイートでテストされています。それぞれの機能を並べて見られます。

言語を選ぶ

どのライブラリも同じプロトコルのドラフトを話します。違うのは、用途、必要なもの、そしてどこまで対応しているかです。

まだ公開されていません。インストール方法は最初のリリースで掲載されます。 リリース · GitHub

どれを選ぶか

対応状況(パリティ)

仕様の各ファミリーについて、クライアントとしての読み取りとサーバーとしての提供を、各ライブラリごとに示します。原則: ある言語でできるプロトコルの機能は、同じドラフトで他の言語でもできること。

対応状況(パリティ)
ファミリーC++CPythonTypeScriptGoRust
クライアントサーバークライアントサーバークライアントサーバークライアントサーバークライアントサーバークライアントサーバー
library対応一部対応対応未対応対応対応対応対応対応未対応未対応未対応
titles対応一部対応対応未対応対応対応対応対応対応未対応未対応未対応
changes対応一部対応対応未対応対応対応対応対応対応未対応未対応未対応
sections対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
homebrew対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
saves対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
cheats対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
requests対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
pairing対応一部対応対応未対応対応対応対応対応対応未対応未対応未対応
signatures対応一部対応対応未対応対応対応対応対応対応未対応未対応未対応
admin対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
events対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
peers対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
sync対応未対応対応未対応対応対応対応対応未対応未対応未対応未対応
encrypted_saves対応一部対応対応未対応対応対応対応対応未対応未対応未対応未対応
deltas対応未対応対応未対応対応対応対応対応未対応未対応未対応未対応
対応16/160/1616/160/1616/1616/1616/1616/165/160/160/160/16
C++ クライアント 16/16 · サーバー 0/16
C++
ファミリークライアントサーバー
library対応一部対応
titles対応一部対応
changes対応一部対応
sections対応一部対応
homebrew対応一部対応
saves対応一部対応
cheats対応一部対応
requests対応一部対応
pairing対応一部対応
signatures対応一部対応
admin対応一部対応
events対応一部対応
peers対応一部対応
sync対応未対応
encrypted_saves対応一部対応
deltas対応未対応
C クライアント 16/16 · サーバー 0/16
C
ファミリークライアントサーバー
library対応未対応
titles対応未対応
changes対応未対応
sections対応未対応
homebrew対応未対応
saves対応未対応
cheats対応未対応
requests対応未対応
pairing対応未対応
signatures対応未対応
admin対応未対応
events対応未対応
peers対応未対応
sync対応未対応
encrypted_saves対応未対応
deltas対応未対応
Python クライアント 16/16 · サーバー 16/16
Python
ファミリークライアントサーバー
library対応対応
titles対応対応
changes対応対応
sections対応対応
homebrew対応対応
saves対応対応
cheats対応対応
requests対応対応
pairing対応対応
signatures対応対応
admin対応対応
events対応対応
peers対応対応
sync対応対応
encrypted_saves対応対応
deltas対応対応
TypeScript クライアント 16/16 · サーバー 16/16
TypeScript
ファミリークライアントサーバー
library対応対応
titles対応対応
changes対応対応
sections対応対応
homebrew対応対応
saves対応対応
cheats対応対応
requests対応対応
pairing対応対応
signatures対応対応
admin対応対応
events対応対応
peers対応対応
sync対応対応
encrypted_saves対応対応
deltas対応対応
Go クライアント 5/16 · サーバー 0/16
Go
ファミリークライアントサーバー
library対応未対応
titles対応未対応
changes対応未対応
sections未対応未対応
homebrew未対応未対応
saves未対応未対応
cheats未対応未対応
requests未対応未対応
pairing対応未対応
signatures対応未対応
admin未対応未対応
events未対応未対応
peers未対応未対応
sync未対応未対応
encrypted_saves未対応未対応
deltas未対応未対応
Rust クライアント 0/16 · サーバー 0/16
Rust
ファミリークライアントサーバー
library未対応未対応
titles未対応未対応
changes未対応未対応
sections未対応未対応
homebrew未対応未対応
saves未対応未対応
cheats未対応未対応
requests未対応未対応
pairing未対応未対応
signatures未対応未対応
admin未対応未対応
events未対応未対応
peers未対応未対応
sync未対応未対応
encrypted_saves未対応未対応
deltas未対応未対応

✓ 対応 · 一部対応 · – 未対応。サーバーのファミリーに ✓ が付くのは、適合性スイートが CI で通した場合だけです。

到達した適合レベル

検証済みの各サーバーが、すべてのスコープを持つトークンで CI において到達するレベル(SPEC.md § 17)。

各言語での同じ呼び出し

プロトコルの一つの操作と、各ライブラリでの呼び出し名。定義へのリンク付きです。

各言語での同じ呼び出し
操作C++CPythonTypeScript
ハンドシェイク § 4.2handshakelantern_client_handshakehandshakehandshake
アプリの一覧 § 6.1appslantern_client_appsiter_appsiterApps
不足分の確認 § 6.3missinglantern_client_missingmissingmissing
検証付きダウンロード § 6.6downloadlantern_client_downloaddownload_appdownloadAppTo
ペアリングの要求 § 5.2start_pairinglantern_client_start_pairingpairpair
承認待ち § 5.4poll_pairinglantern_client_poll_pairingwait_for_pairingwaitForPairing
セーブデータの送信 § 9.3upload_savelantern_client_upload_saveupload_saveuploadSave
イベントの購読 § 18eventslantern_client_eventseventsevents
ライブラリの提供 § 17Server未対応LanternAppLanternApp
静的ライブラリの書き出し § 21未対応未対応exportwriteStatic
静的ライブラリの読み込み § 21StaticLibrary未対応StaticLibraryStaticLibrary

Go, Rust の呼び出しは、ライブラリが整い次第ここに載ります。

サンプル

各ライブラリのサンプルフォルダにあるファイルで、CI でそのまま実行されているので動作します。

ライブラリを読む

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 });
  }
}

機器をペアリングする

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;
}

ライブラリを提供する

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)));

静的ライブラリを公開する

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 });
}

信頼できる理由

バージョン

SDK 全体で一つのバージョン。どの言語でも同じです。

バージョン
SDK のバージョン仕様のドラフトC API のバージョン最新リリース
0.5.00.117 (ABI 1)まだありません

lantern-sdk のデータ(コミット 9fc3b4f)。

ゼロからではなく、ライブラリから始める

デモライブラリで試すか、コードを読んでください。