@skyglide.com/mainapp
The Skyglide cloud: the controller that the Skyglide clients talk to, its client contract, and the hub that connects a member's devices into one private network, at Skyglide locations and away from them.
It answers devices (the skyglided service, the skyglide command-line client, the apps), home and venue routers,
and the member actions those flows need, and it drives the hub's WireGuard peers and its isolation rules. At venues it
is the signup portal behind openNDS's captive portal and takes payments through PayPal. The member and operator
portals' user interfaces, private DNS and inline inspection come later.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
The contract
Clients call TypedRequest methods over HTTPS (POST /typedrequest) and receive pushes over the typed socket. Every
method is an IReq_ interface in ts_interfaces/requests/; the generated OpenAPI 3.1 document with every contract
and type as JSON Schema is schema/contracts.openapi.json (package export ./schema/contracts.openapi.json).
- Devices: a device-code sign-in (
device.signin.start,device.signin.poll) that the member approves, or a join key;device.enrollwith the device's own WireGuard public key;device.config.get(the device's address in its member's away subnet, the hub, the member network's prefixes, its devices);device.location.get(is this network my own network at a location?); key rotation, leaving, and pushes when the configuration changes or the device is removed. - Routers: home routers pair with a join key and fetch their configuration; venue routers register their key, fetch their desired state, acknowledge it, and report observations and counts.
- Members: who is signed in (
member.me), approving device codes, join keys, devices, home LAN approval, the internet exit. Members sign in with idp.global in the browser (GET /auth/login); member calls ride on that session's cookie, come from the public origin and carry the session's CSRF token ascsrf_token(GET /auth/sessionanswers it).
Sign-in
Members sign in with idp.global, through the relying-party layer of @idp.global/sdk: the
authorization code flow with PKCE under /auth/ (login, callback, finalize, session, logout), HttpOnly cookie
sessions with a session-bound CSRF token. Skyglide stores no password. A member is provisioned under an email address
(seed member now, the signup portal later); the first sign-in whose address idp.global confirms binds that idp.global
user to the member, and every later sign-in finds the member by that user. Anyone else is refused, and the browser
lands on /device?auth=<reason> (not_a_member, email_not_verified, account_conflict, error).
/device is where a member confirms a device: the verification URL every device sign-in hands out
(/device?code=XXXX-XXXX). Signed out, it leads through idp.global and back; signed in, it shows the device and its
code and confirms or denies it in a form bound to the session.
Refusals carry a code in error.data.code (TRefusalCode): not_authorized, device_removed, rate_limited, …
Signup and payments
At a venue, openNDS sends a captured guest to https://<signup host>/opennds/<site> with its encrypted client details
(secure remote FAS, level 3). The portal shows the prices first, signs the guest in with idp.global and starts the
payment: a click on the order button opens a checkout window of 10 minutes at 1 Mbit/s (configurable) so the phone
reaches PayPal, and openNDS's authmon fetches that authorization from the portal. A guest pass is a PayPal order; the
membership is a PayPal subscription. After payment the member sees their Wi-Fi password for skyglide.com (with a
WIFI: QR code), a guest sees their pass. Returning members and guests are recognized when they sign in.
A renewal that fails moves the membership to grace for SKYGLIDE_GRACE_DAYS, then to restricted: the member's Wi-Fi
password still works but leads only to the portal (/restricted), until they pay again. /cancel ("Verträge hier
kündigen") cancels a membership after sign-in; it stays usable until the paid period ends. Every payment gets an
invoice at /invoices/<id>, every refund a credit note. PayPal's webhook is POST /paypal/webhook.
Running
SKYGLIDE_PUBLIC_ORIGIN=https://app.skyglide.com \
SKYGLIDE_DB_URL=mongodb://… \
SKYGLIDE_SECRETS_KEY=<32 bytes, base64> \
SKYGLIDE_SESSION_ENCRYPTION_KEY=<32 bytes, base64> SKYGLIDE_CSRF_HMAC_KEY=<32 bytes, base64> \
IDP_ISSUER=https://idp.global IDP_CLIENT_ID=<client id> IDP_CLIENT_SECRET=<client secret> \
SKYGLIDE_HUB_DEVICE_ENDPOINT=hub.skyglide.com:51820 SKYGLIDE_HUB_DEVICE_PUBLIC_KEY=<key> \
SKYGLIDE_HUB_SITE_ENDPOINT=hub.skyglide.com:51821 SKYGLIDE_HUB_SITE_PUBLIC_KEY=<key> \
SKYGLIDE_HUB_AGENT_SOCKET=/run/skyglide/hub.sock \
SKYGLIDE_SIGNUP_ORIGIN=https://join.skyglide.com \
SKYGLIDE_MEMBERSHIP_PRICE=<monthly gross, e.g. 9.90> SKYGLIDE_GUEST_PASSES=<key:hours:price:devices,…> \
SKYGLIDE_CHECKOUT_MINUTES=10 SKYGLIDE_CHECKOUT_KBPS=1000 SKYGLIDE_CHECKOUT_PER_HOUR=3 SKYGLIDE_CHECKOUT_PER_DAY=6 \
SKYGLIDE_HANDOVER_MINUTES=30 SKYGLIDE_GRACE_DAYS=3 SKYGLIDE_RELEASE_DAYS=30 \
SKYGLIDE_PAYPAL_ENV=sandbox SKYGLIDE_PAYPAL_CLIENT_ID=<id> SKYGLIDE_PAYPAL_CLIENT_SECRET=<secret> SKYGLIDE_PAYPAL_WEBHOOK_ID=<id> \
SKYGLIDE_SELLER_NAME=<legal name> SKYGLIDE_SELLER_ADDRESS='<line>|<line>|<line>' SKYGLIDE_SELLER_VAT_ID=<id> \
SKYGLIDE_SELLER_EMAIL=<address> SKYGLIDE_VAT_RATE=19 SKYGLIDE_INVOICE_PREFIX=SG \
node cli.js controller
Optional: SKYGLIDE_PORT (3000), SKYGLIDE_LISTEN_HOSTNAME (127.0.0.1), SKYGLIDE_DB_BACKEND (nosqldb or
mongodb), SKYGLIDE_DB_NAME, SKYGLIDE_TRUSTED_PROXIES, SKYGLIDE_PORTAL_ADDRESSES, SKYGLIDE_DEVICE_MTU,
SKYGLIDE_SESSION_MAX_AGE_SECONDS (43200), SKYGLIDE_CURRENCY (EUR), SKYGLIDE_PAYPAL_API_BASE (PayPal's, by
environment), SKYGLIDE_PAYPAL_CHECKOUT_HOSTS (more hosts for the walled garden), IDP_CA_CERT_FILE (a
private CA of the issuer), SKYGLIDE_TLS_CERT_FILE with SKYGLIDE_TLS_KEY_FILE (https without a proxy). A missing or
malformed setting stops the start and names itself; idp.global is required. The idp.global client is registered with
the redirect URIs <SKYGLIDE_PUBLIC_ORIGIN>/auth/callback and <SKYGLIDE_SIGNUP_ORIGIN>/join/auth/callback. The
venues' walled garden is the signup host, idp.global's host and PayPal's checkout hosts. When idp.global cannot be reached at the start, devices and
routers keep working, member sign-ins answer as unavailable, and the controller tries again every 30 seconds.
The hub agent runs as root next to the hub's WireGuard interfaces, with a configuration file of its own:
{
"socket_path": "/run/skyglide/hub.sock",
"socket_gid": 990,
"interfaces": [
{ "name": "sgdev", "listen_port": 51820, "private_key_file": "/etc/skyglide/sgdev.key" },
{ "name": "sgsite", "listen_port": 51821, "private_key_file": "/etc/skyglide/sgsite.key" }
],
"egress_interface": "eth0"
}
node cli.js hub-agent --config /etc/skyglide/hub.json
Development
pnpm install
pnpm test
pnpm run test:e2e
pnpm run test:e2e runs the hub agent with userspace WireGuard and three test devices in Docker and checks that a
member's devices reach each other through the hub, that members stay apart, that a forged source address and a
removed device get nowhere, and that a join key works once. Tests sign members in through the real /auth/ routes
against RelyingPartyProviderDouble, the in-process idp.global of @idp.global/sdk/relyingparty/testing
(test/helpers.signin.ts).
With a real idp.global on this host, as nevermind.finance does it:
node cli.js devidp # once per host, keeps running: the local idp.global on https://idp.localhost:8445 (Docker)
node cli.js preview # the controller on https://localhost:3200/device, one member per persona
devidp runs @idp.global/devidp (its personas testuser, testmoderator and testsuperadmin; their passwords
are in the handoff, $XDG_RUNTIME_DIR/skyglide-devidp/runtime.json) and needs Node 24 or 25: its kernel secret
store refuses other versions. preview takes SKYGLIDE_PREVIEW_PORT (3200 to 3263, the callbacks the local
idp.global knows), an in-memory database and throwaway keys; trust the handoff's ca.pem in the browser profile you
test with.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.