{"id":50355552,"url":"https://github.com/pairshaped/libero-gleam","last_synced_at":"2026-05-29T22:30:29.515Z","repository":{"id":350766340,"uuid":"1207450487","full_name":"pairshaped/libero-gleam","owner":"pairshaped","description":"Gleam library that renders REST obsolete for SPAs and other Erlang-based clients.","archived":false,"fork":false,"pushed_at":"2026-05-21T21:16:10.000Z","size":2779,"stargazers_count":19,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-21T22:59:59.561Z","etag":null,"topics":["gleam-lang","messaging","spa"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/libero/","language":"Gleam","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pairshaped.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-11T00:42:54.000Z","updated_at":"2026-05-21T21:16:15.000Z","dependencies_parsed_at":"2026-05-21T23:00:09.613Z","dependency_job_id":null,"html_url":"https://github.com/pairshaped/libero-gleam","commit_stats":null,"previous_names":["pairshaped/libero","pairshaped/libero-gleam"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/pairshaped/libero-gleam","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pairshaped%2Flibero-gleam","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pairshaped%2Flibero-gleam/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pairshaped%2Flibero-gleam/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pairshaped%2Flibero-gleam/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pairshaped","download_url":"https://codeload.github.com/pairshaped/libero-gleam/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pairshaped%2Flibero-gleam/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33673627,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gleam-lang","messaging","spa"],"created_at":"2026-05-29T22:30:28.761Z","updated_at":"2026-05-29T22:30:29.510Z","avatar_url":"https://github.com/pairshaped.png","language":"Gleam","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Libero](https://github.com/pairshaped/libero-gleam/blob/master/libero.png?raw=true)\n\n# Libero\n\n[![Package Version](https://img.shields.io/hexpm/v/libero)](https://hex.pm/packages/libero)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/libero/)\n\nLibero helps a Gleam client and server share a typed RPC contract. The contract\nis the main thing: both sides agree on which calls exist, what arguments they\ntake, and what each call returns.\n\nEncoding and decoding are part of that, but they are not the whole point. The\nhard part is keeping the client and server agreement true across the protocol\nlayer: request messages, response decoders, server dispatch, client state, and\nwire-format details all need to match the handler signatures.\n\nLibero treats the server handler as the source of truth. It scans your handler\nfunctions, follows the types used in their signatures, and generates the RPC\nplumbing around them. That gives the client and server a shared typed contract\nwithout hand-written protocol messages or decoders.\n\n## What Libero Replaces\n\nA server handler is a Gleam function that runs on the server:\n\n```gleam\nimport gleam/result.{type Result}\nimport server_context.{type ServerContext}\n\npub fn server_get_items(\n  server_context server_context: ServerContext,\n) -\u003e Result(List(Item), ItemError) {\n  Ok(server_context.items)\n}\n```\n\nLibero treats a public function as an RPC handler when its name starts with\n`server_`, it takes a `ServerContext`, and it returns either a read-only result\nor a result with an updated context. The context type must appear unqualified in\nthe signature (`ServerContext`, not `ctx.ServerContext`). Functions that use a\nqualified context type are silently skipped.\n\nFrom just this handler, Libero writes all of the surrounding RPC code for you:\n\n- A request variant such as `ServerGetItems`, which represents this call at the\n  protocol boundary. The server dispatch decodes it, and generated client or\n  framework code sends the matching shape.\n- An encoder that turns `ServerGetItems` into bytes or JSON\n- Server dispatch code that receives the message and calls `server_get_items`\n- A response shape for `Result(List(Item), ItemError)`\n- A client decoder that turns the response back into Gleam values\n- Client state for loading, success, domain errors, and transport errors\n\nIf the handler signature changes, you simply regenerate instead. The wire format\nis the bytes or JSON sent over the network; Libero owns that shape so application\ncode can stay focused on typed messages and handler results.\n\n## Quick Start\n\nAdd Libero to your project and run the generator:\n\n```sh\ngleam add libero\ngleam run -m libero\n```\n\nLibero scans `src/`, finds RPC handlers, discovers the types they use, and writes\ngenerated files under `src/generated/libero/`.\n\n## Generated Files\n\nAfter `gleam run -m libero`, you will see files like these:\n\n| File | Purpose |\n|------|---------|\n| `src/generated/libero/dispatch.gleam` | Server dispatch code for your handlers |\n| `src/generated/libero/rpc_decoders.gleam` | Gleam wrapper for generated decoders |\n| `src/generated/libero/rpc_decoders_ffi.mjs` | JavaScript decoders for discovered types |\n| `src/generated@rpc_atoms.erl` | Erlang atom pre-registration for safe ETF decoding |\n\nImport the generated server modules in your app like any other Gleam module.\n\n## Transport Is Yours\n\nLibero leaves transport code to your app or framework. WebSocket setup, HTTP\nroutes, reconnect behavior, and app-specific routing stay outside the generator.\n\n## Advanced Usage\n\n### Client Decoders\n\nIf your client lives in another package, mirror the generated JavaScript decoder\nfiles into that package:\n\n```sh\nLIBERO_CLIENT_OUT_DIR=\"../clients/web/src/generated/libero\" gleam run -m libero\n```\n\nThis copies the client decoder output only. Libero still writes the server\ndispatch files to `src/generated/libero/`.\n\n### Library API\n\nYou can also call the pipeline from your own codegen tool:\n\n```gleam\nimport libero\n\nlet assert Ok(endpoints) = libero.scan()\nlet seeds = libero.collect_seeds(endpoints)\nlet assert Ok(discovered) = libero.walk(seeds)\n\nlet dispatch_src = libero.generate_dispatch(endpoints)\nlet decoders_js = libero.generate_decoders_ffi(discovered, endpoints)\nlet decoders_gleam = libero.generate_decoders_gleam()\n```\n\nThe API returns generated source as strings, so you choose where to write it.\n\n### Multiple Protocols\n\nLibero supports ETF for BEAM-first applications and JSON for generated SDKs,\ntools, logs, and easier inspection. Both protocols are owned by the generated\ncontract boundary: app code should call Libero helpers instead of assembling wire\nmessages by hand.\n\nFor untrusted ETF input, decode through the generated helpers or\n`libero/etf/wire.decode_safe`. ETF safe decoding prevents atom and function-term injection,\nbut callers should still set process memory limits for hostile input.\n\n## Security: ETF Threat Model\n\nLibero uses Erlang Term Format (ETF) for its primary wire protocol because ETF\npreserves type fidelity that JSON does not: Int vs Float, BitArray, and\natom-tagged variants all survive the round trip without lossy coercion. This\nmatters for a typed RPC pipeline where the contract depends on exact types.\n\nThe [ERLEF serialisation guide](https://security.erlef.org/secure_coding_and_deployment_hardening/serialisation.html)\nrecommends against using ETF with untrusted parties. Libero does it anyway, with\na defense stack designed for a specific threat model.\n\n### Trust assumptions\n\n- The WebSocket endpoint requires authentication (cookie, session, or token)\n  upstream of the handler. Libero does not enforce this; your transport layer\n  must.\n- The browser is adversarial despite serving your own JS. DevTools, XSS, browser\n  extensions, and MITM (if HTTPS is broken) can all craft arbitrary ETF.\n- The server's BEAM process is trusted. Libero never decodes untrusted ETF into\n  the server without the defenses below.\n\n### Defense stack (in order)\n\n1. **Transport frame size limit.** Your WebSocket server (mist, cowboy, etc.)\n   should cap frame size. This is outside Libero but is the first gate.\n2. **`binary_to_term(Bin, [safe])`** on every decode path. This blocks atom\n   creation (atom-table exhaustion DoS) and function deserialisation\n   (remote code execution via FUN_EXT/EXPORT_EXT). Libero audits for bare\n   `binary_to_term/1` calls; none exist in the codebase.\n3. **Atom pre-registration.** The generated `rpc_atoms` module calls\n   `binary_to_atom/2` for every constructor atom at boot. With `[safe]`,\n   `binary_to_term` only succeeds for atoms that already exist in the table.\n4. **Typed dispatch.** The generated dispatch verifies the decoded term's\n   constructor tag against a known handler set before invoking any handler\n   function. Unknown tags return a wire error, not a crash.\n\n### What would weaken this model\n\n- Adding a bare `binary_to_term/1` call (without `[safe]`) on any request path.\n- Accepting ETF from unauthenticated connections.\n- Passing decoded ETF terms to `erlang:apply/3` or similar without dispatch\n  tag verification.\n- Removing atom pre-registration while still accepting ETF from browsers.\n\nIf you modify Libero's decode path, verify that `[safe]` is present and that the\ndecoded term flows through typed dispatch before reaching handler code.\n\n## More Docs\n\n- [Contract boundary](https://github.com/pairshaped/libero-gleam/blob/master/pages/reference/contract-boundary.md):\n  what Libero owns and what app code owns\n- [ETF wire protocol](https://github.com/pairshaped/libero-gleam/blob/master/pages/protocol/etf-wire-protocol.md):\n  ETF frames, safe decode, and hashed type identity\n- [JSON wire protocol](https://github.com/pairshaped/libero-gleam/blob/master/pages/protocol/json-wire-protocol.md): readable JSON\n  envelopes, validation, and contract hashes\n- [Wire type identity](https://github.com/pairshaped/libero-gleam/blob/master/pages/protocol/wire-type-identity.md):\n  how custom types stay unique across protocols\n- [llms.txt](https://raw.githubusercontent.com/pairshaped/libero-gleam/master/llms.txt):\n  raw package context for language models\n\n## License\n\nMIT. See [LICENSE](https://github.com/pairshaped/libero-gleam/blob/master/LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpairshaped%2Flibero-gleam","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpairshaped%2Flibero-gleam","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpairshaped%2Flibero-gleam/lists"}