The design lists a controller as "Example production"; the pages showed the
controllerId the controller reports, because the report contract carries no
display name. The name is cloud access's own data about a binding: never
reported by the controller, never sent to it, no part of @serve.zone/interfaces.
- The binding stores displayName, null for "listed by its host"; the browser
API answers name = displayName ?? hostname and the self-reported
controllerId beside it.
- Code entry takes an optional displayName, kept on the enrollment record
beside the contract's pending claim (Cloudly's Administrator sees the claim
unchanged) and stored with the binding at confirmation; blank lists the
controller by its host.
- POST /api/controllers/rename { organizationId, bindingId, displayName,
csrfToken }: owner and admin under claim's authority (the role table is
unchanged), observed live, after origin, session, body and CSRF.
- A name is 1 to 64 printable characters (code points; letters, marks,
numbers, punctuation, symbols, spaces) once trimmed, judged by
normalizeCloudAccessDisplayName in server and bundle alike, with the body,
before CSRF and before idp.global is asked (name-invalid, 400; never a
counted code failure).
- The pages: a Name field in Connect; every list, record, alert, update,
member entry and Home card names the controller by its name, the Controllers
list with the host beneath; Rename on the record for owners and admins; what
the controller calls itself stays on its record; the Alerts filter keys
controllers by binding, since names may repeat. Names render as text.
- Tests: rename per role with a live demotion, every refusal, the bounds
(65 characters, 64 and 65 emoji, line breaks, tab, control, bidi,
zero-width, private-use and lone-surrogate characters) with no introspection
for a bad name, markup names answered literally (test.webapi); code entry's
name, its bounds, not counted and not shown to Cloudly (test.enrollment);
the world named as the design names it, names with hosts beneath, and a
markup name shown literally and never run on five pages (test.ui.render).
@serve.zone/cloudaccess
@serve.zone/cloudaccess is serve.zone cloud access (AGL-1181, Linus ruling 141), the service behind cloudaccess.serve.zone. It is a directory of serve.zone controllers bound to idp.global organizations, and a relay that passes a browser's TLS through to the controller untouched, so a controller behind NAT can be opened from outside. Cloud access is a directory, never an authority: who may do what comes from idp.global, and what a controller allows stays on that controller.
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.
Install
pnpm add @serve.zone/cloudaccess
The package installs the cloudaccess command. It runs on Node.js.
Running cloud access
cloudaccess serve starts the embedded storage engine, the web server of its own names on a loopback port, and the relay on the public ports in front of it. It stops on SIGINT or SIGTERM (exit 0) and when the storage engine exits on its own (exit 1), so a supervisor restarts it. One process serves one data directory.
| Variable | Meaning |
|---|---|
CLOUDACCESS_DATA_DIR |
Normalized absolute directory of the service state |
CLOUDACCESS_TLS_CERT_PATH |
PEM certificate chain of the service's own names |
CLOUDACCESS_TLS_KEY_PATH |
PEM private key of that certificate |
CLOUDACCESS_LISTEN_HOST |
Interface address to bind; default 0.0.0.0 |
CLOUDACCESS_PORT |
The relay's TLS port; default 443 |
CLOUDACCESS_HTTP_PORT |
The relay's plain-HTTP port, which only redirects verified names to https; default 80 |
CLOUDACCESS_PUBLIC_ORIGIN |
The bare https origin people reach the service at, on a DNS name; default https://cloudaccess.serve.zone. Its host and tunnel.<host> are the service's own names |
CLOUDACCESS_IDP_ISSUER |
The idp.global issuer, a bare https origin; default https://idp.global |
CLOUDACCESS_IDP_CLIENT_ID |
The OAuth client id of cloud access at idp.global |
CLOUDACCESS_IDP_CLIENT_SECRET_PATH |
Absolute path of the file holding the OAuth client secret |
CLOUDACCESS_SESSION_KEY_PATH |
Absolute path of the file holding the session key: 32 random bytes, base64 (openssl rand -base64 32) |
CLOUDACCESS_DOH_URL |
The DNS-over-HTTPS endpoint (RFC 8484) the hostname proof asks, an https URL; default https://cloudflare-dns.com/dns-query. Its certificate is verified against the web PKI roots plus NODE_EXTRA_CA_CERTS |
CLOUDACCESS_RELAY_ADDRESSES |
The relay's public addresses, comma-separated, at most 6: what customers write into their A/AAAA records. Only the pointsAtRelay diagnostic and the records to copy use them; default none |
Below CLOUDACCESS_DATA_DIR the service keeps nosqldb/, the NoSQLDB storage root, and run/nosqldb.sock, the engine's socket in a directory of mode 0700, so only the service's own user reaches the engine. The certificate must cover both own names. The certificate and the secret files are read at start; a renewed certificate takes effect at the next start. Keep the secret files outside the data directory: the session key seals the stored idp.global tokens, and a backup of the data directory must not carry it.
GET /healthz answers 200 while the storage engine runs and answers a read, and 503 otherwise. The sign-in routes, the app's pages and the browser API below answer as described; every other request answers 404, and every answer denies framing (X-Frame-Options: DENY, frame-ancestors 'none') and inline script (script-src 'self').
The relay
The relay owns the public ports. On the TLS port it reads each connection's ClientHello, without answering or decrypting anything, and routes by its server name:
- The public origin's host (
cloudaccess.serve.zone, fromCLOUDACCESS_PUBLIC_ORIGIN) is spliced to the web server on loopback, which terminates its TLS with the service's certificate. - The tunnel name (
tunnel.cloudaccess.serve.zone) goes to the tunnel, which terminates its TLS with the same certificate in the relay's own process: controllers' stream connections end there (see Relay streams). - A verified customer name is spliced, untouched, to one controller stream: the bytes read so far first, then every byte both ways. TLS for it ends at the controller's host; the relay never terminates it. One client connection is one controller stream, and nothing is pooled. The stream is asked for only after a complete hello for a verified name has arrived, and the request names the binding and the client address, never a target. A stream is spliced only if the name still routes to the binding it was asked of once it is open (
route-changedotherwise). - Anything else is closed: no
server_name, an unknown or unverified name, or bytes that are not one well-formed ClientHello within 16 KiB and 10 seconds.
A health probe through the relay must send an own name as SNI: a connection by address alone carries no server_name and is closed.
On the plain-HTTP port it answers a verified name's request with 301 to https://<name>/ and closes every other connection, the own names' included. Nothing is forwarded there.
Verified names come from the hostname proof below, and controller streams from the stream broker (see Relay streams).
The limits are constants in cloudAccessRelayLimits:
| Limit | Value |
|---|---|
| ClientHello | complete within 16 KiB and 10 s |
| Per binding | 256 streams at once, 32 of them still opening; 100 Mbit/s both ways together, bursting 25 MB |
| Per binding and client address (IPv6 by its /64) | 32 connections at once, 60 new a minute |
| All connections | 10,000 on both ports, controllers' tunnel connections included, at most 256 from one address; 2,000 of them still sending their ClientHello, at most 32 from one address |
| Opening a controller stream | 10 s, the stream token's lifetime; the browser is closed then |
| A tunnel connection's TLS handshake and preamble | 10 s together |
| Idle cut | a spliced stream with no bytes either way for 120 s is closed |
| Plain HTTP | the request head within 8 KiB and 10 s |
A relayed stream holds two of the 10,000 connections: the browser's and its controller's tunnel. A tunnel connection counts against its address's 256 until its token verified; from then on it is its binding's stream, so a controller behind one NAT address carries all 256 of its binding's streams.
The idle cut is resource hygiene for connections nobody uses, set well above the 30-second WebSocket heartbeat. It is not a session limit: sessions live on each controller, which sets and enforces its own.
Each relayed stream leaves one record in the stream log (relaystreams): its id (the streamId its openCloudAccessStream carried, so the controller's record of the stream meets this one), when it started, its binding, the client address, the bytes each way and its duration, never its content or name. Records are removed 90 days after the stream ended (cloudAccessOwnerDefaults.retentionDays); the sweep runs at start and hourly. No process log line carries a client address.
The hostname proof
A customer name is routed only while all four checks of its binding pass (ruling 141 C4):
| Check | Passes when |
|---|---|
origin |
The binding's publicOrigin is a canonical https: origin on port 443 whose host is a DNS name, neither serve.zone nor below it; that host is the binding's one name |
address |
The name has A/AAAA records of its own: no CNAME or DNAME anywhere in either lookup |
txt |
TXT _cloudaccess.<host> holds ca1=<bindingId>, published at that name itself |
caa |
The effective CAA set (RFC 8659 tree climbing) has an issue record, every issue record is letsencrypt.org; validationmethods=dns-01 (an accounturi may be added, nothing else), and issuewild is absent or the same. A climb through a CNAME or DNAME fails it |
Whether the A/AAAA records point at the relay is shown as pointsAtRelay and is never a condition. Without the CAA rule, whoever runs the relay could get a certificate for the name by HTTP-01 and read every relayed session; the rule is the owner default O8 (cloudAccessOwnerDefaults.requiresCaa).
Every lookup goes over DNS-over-HTTPS to CLOUDACCESS_DOH_URL with verified TLS, never over plain DNS. A verified name belongs to one binding and a binding has one verified name; the database refuses a second holder. Another binding takes the name only once the holder's ca1 value is gone from the TXT, in one compare-and-set write, and never on a TXT answer older than the holder's own proof.
Each binding's proof runs again 24 hours after it ran, an hour after a lookup error, and on demand at most once a minute. A definite negative (NXDOMAIN, a missing TXT value, a failing CAA set, an alias) removes the route at once; a lookup error keeps the current state, and a route holds at most 72 hours after its last passing proof. A removed route is logged (name-route-removed) and handed to the alert sink. The constants are in cloudAccessHostnameProofPolicy; cloudAccessNameProofRecords(host, bindingId, relayAddresses) gives the records to publish, ready to copy.
Relay streams
A controller sits behind NAT and accepts nothing inbound. For each browser connection to a verified name, cloud access asks the controller to dial out, and splices the browser to that outbound connection (ruling 141 C6 and C10 step 8, amendment 141a.8):
- The broker (
CloudAccessStreamBroker) finds the binding's counted session: the one live control socket registeredactivewith the highest epoch. A tie, or no active socket, routes nothing. - It sends that session nothing unless its
interfacesVersionis at least32.89.0(cloudAccessOpenStreamSince), the release that declaredopenCloudAccessStream. - It mints a stream token: 32 random bytes, bound to the binding, valid 10 seconds, used once. The token book keeps only its SHA-256 (a presented token is hashed and the digests compared in constant time), and the token is never logged. At most 32 tokens per binding are out at once.
- It sends
openCloudAccessStream { streamId, token, clientAddress }over that socket:streamIdis 16 random bytes as lowercase hex,tokenis unpadded base64url,clientAddressis the browser's canonical address. The body has exactly these three members and never a target. - It waits for the controller's tunnel connection. A refusal from the controller, the browser leaving, 10 seconds passing, or the binding losing its counted socket (a disconnect, a socket closing, an epoch conflict) gives the stream up and voids the token; the browser is closed.
The tunnel (CloudAccessTunnel) takes every connection to the tunnel name. It terminates TLS with the service's certificate, then reads the preamble, exactly as @serve.zone/interfaces' cloudAccessTunnelPreamble defines it:
- the 13 ASCII bytes
szca-tunnel-1(the tag, always the first 13 bytes; another layout would get another tag, and the tunnel accepts only tags it knows, closing a connection at its first byte that cannot begin one); - the 32 raw bytes of the token (the base64url of
openCloudAccessStreamdecoded), 45 bytes in all.
Bytes after the preamble belong to the stream. A token that completed a stream already (token-replayed), was given up (token-withdrawn), came after its lifetime (token-expired), or was never minted (token-unknown) closes the connection, as does a failed handshake, another tag, or a preamble not complete within 10 seconds. A verified token hands the connection to the browser waiting for it: the relay writes the browser's ClientHello first, then splices bytes both ways. One browser connection is one tunnel connection.
What a controller's connector does on openCloudAccessStream (the Cloudly side of the contract):
- Validate the body with
data.validateOpenCloudAccessStreamRequestand refuse anything else withrequest-invalid(a target among its members included); refusenot-activeon a socket of a site that is not active andstream-limitover its own limits. Serve the request on a dedicatedTypedRouterthat registersopenCloudAccessStreamand nothing else. - Dial its configured tunnel name (
tunnel.cloudaccess.serve.zone:443), never an address taken from the request, with Node's default verification: WebPKI chain and host name, nocaoption, norejectUnauthorized: false, no customcheckServerIdentity, no pin. - Write nothing before the handshake verified (
secureConnect). Then write the 45-byte preamble in one write: the tag, thenBuffer.from(token, 'base64url'). - Connect the host's relay ingress and splice bytes both ways, passing a FIN on. Record the stream with its
streamIdand theclientAddressas cloud access reported it, never as the peer.
cloudaccess ships a test double of this connector (test/helpers.connector.ts) that the end-to-end test runs in a child process trusting the test authority only through NODE_EXTRA_CA_CERTS.
Enrollment and the control socket
A controller (Cloudly) dials one typedsocket to cloud access, wss://<public origin host>/ (cloudAccessControlPath /; in production wss://cloudaccess.serve.zone), through the relay like any own-name connection, and speaks the Cloudly_CloudAccess requests of @serve.zone/interfaces 32.89.0 (ruling 141 C3 and C5, amendment 141a). The WebSocket upgrade is admitted only on that host and path; every message is capped at 327,680 bytes, the contract's 262,144-byte report bound plus 64 KiB, so a report one byte above the bound is answered report-too-large rather than cutting the socket. A socket that has neither authenticated nor holds a live enrollment is closed 10 seconds after it opened or after its last enrollment ended. Every refusal is the contract's ICloudAccessRefusalErrorData, and its text never carries a bearer or a pairing code.
| Request | What cloud access does |
|---|---|
enrollCloudAccessBinding |
On an unauthenticated socket: assigns a random 128-bit bindingId and a pairing code (XXXX-XXXX, 8 Crockford base32 symbols, kept only as its SHA-256), valid 10 minutes. The enrollment lives on its socket: it is void when the socket closes, when it is confirmed or rejected, or after 10 minutes. At most 10 per address an hour and 10,000 open in all (rate-limited). controllerId is display only; everything is keyed by bindingId |
getCloudAccessEnrollmentState |
waiting, or claimed with the pending claim, for the enrolling socket only (enrollment-gone otherwise) |
confirmCloudAccessEnrollment |
Stores the binding for the claimed organization, ends the enrollment and starts the binding's hostname proof (not-claimed before a code entry); the relay routes the name once the proof passes |
rejectCloudAccessEnrollment |
Ends the enrollment; no binding remains |
authenticateCloudAccessBinding |
Compares the SHA-256 of the bearer with the binding's in constant time, then tags the socket with the server-owned cloudaccess-binding tag ({ bindingId }), which the client cannot set. not-confirmed while the claim waits; binding-gone for every other miss. The bearer is never logged |
registerCloudAccessSession |
Registers the site: epoch, active or shadow, releases; answers { interfacesVersion: '32.89.0' } |
reportCloudAccessStatus |
Every 30 s from the counted socket: the contract's validator first (report-too-large when size is the one reason, request-invalid otherwise), then not-active or epoch-conflict, then report-too-soon within 10 s of the last accepted one. The latest report per binding is stored |
getCloudAccessBindingState |
The binding, its organization and name, its hostname proof as it last ran (txt, address and caa, each unchecked until the first run; pointsAtRelay; routed) and the records to publish (TXT _cloudaccess.<host> = "ca1=<bindingId>", an A or AAAA record per relay address, the CAA set). recheck: true runs the proof now, refused rate-limited within a minute of its last run |
disconnectCloudAccessBinding |
Removes the binding's route and proof at once (hostnameProof.forget), deletes the binding with its bearer hash, its report and its alerts, and closes the binding's sockets |
Only the live socket that registered active with the highest epoch is counted: its reports count, and it is the target of openCloudAccessStream, the one request cloud access ever sends a controller (CloudAccess.controllerSessions.streamTarget(bindingId); the request goes out with controlSocket.openStreamRequest(session)). Two live sockets active with the same highest epoch route nothing and count no report until one leaves; cloud access then holds an open epoch-conflict alert for the binding. A binding not heard of for 10 minutes gets cloud access's own not-reporting alert, which its next report resolves, and the hostname proof's removal of a binding's route opens name-route-removed until the name is routed again. A stored report, whose people map is personal data, is removed when no newer one replaced it within the owner's retention (O7, 90 days); resolved alerts are kept as long.
Code entry
POST /api/code-entry with { organizationId, pairingCode, csrfToken }, and optionally displayName, is cloud access's own browser API: it needs Origin to be the public origin, the session cookie and its CSRF token. Cloud access asks idp.global live (no cached observation) whether the person may claim in that organization of their sign-in (owner or admin), then reads the code (case is ignored, hyphens are stripped, I and L read as 1 and O as 0, at most 32 characters) and consumes it, recording the pending claim: the organization's id and name, and the person's subject, name and email. It answers 200 { state: 'claimed', organizationId, organizationName, controllerId, name, hostname }; the claim waits for the controller's Administrator to confirm it in Cloudly, and ends with the code otherwise. Refusals are { error }: code-invalid (400), code-expired (410), code-used (409), rate-limited (429), sign-in-required (401), origin-refused, csrf-refused, organization-unknown, role-insufficient, membership-absent (403), claimant-incomplete (422: idp.global named no name or email) and authority-unavailable (503).
displayName is the name the controller is listed as here, for everyone in the organization: 1 to 64 printable characters once trimmed (letters, marks, numbers, punctuation, symbols and spaces; no control or format characters, no line breaks), else name-invalid (400), judged with the body before the CSRF token and never counted as a failed code. Blank or absent, the controller is listed by its host, and name answers the host. The name is cloud access's own data about the binding: it is kept with the pending claim, stored with the binding at confirmation, never part of the claim Cloudly's Administrator is shown, and never sent to the controller.
Failed codes, including entries refused for lack of authority, count against the person (5), the browser's address (20; an IPv4 address or an IPv6 /64, as the relay saw the browser) and everyone (above 1,000 the breaker pauses code entry for everyone), each within 10 minutes. The web server sees the relay's loopback connection as its peer; the relay names the browser behind each of those connections by its local port (CloudAccessRelay.clientAddressOfLoopbackPort).
Sign-in
People sign in with idp.global through @idp.global/sdk/relyingparty. Cloud access is registered at idp.global as a global app with the redirect <CLOUDACCESS_PUBLIC_ORIGIN>/auth/callback and asks for the scopes openid profile email organizations roles. idp.global names in the organizations claim only the organizations connected to cloud access.
| Route | What it does |
|---|---|
GET /auth/login?return=/path |
Starts a sign-in: redirects to idp.global with PKCE, state and nonce |
GET /auth/callback |
Completes it once and sets the session cookie, then redirects to /auth/finalize. A sign-in declined at idp.global goes to /?signin=failed; one that fails answers { error: 'authentication_failed' } and opens nothing |
GET /auth/finalize |
Sends the browser to its return path; without the session that sign-in opened, to /?signin=failed |
GET /auth/session |
Answers { session, person, organizations }, or { session: null } |
POST /auth/logout |
Ends the session; needs Origin to be the public origin and { csrfToken } in the body |
The session cookie is __Host-cloudaccess_session: HttpOnly, Secure, SameSite=Lax, Path=/, no Domain. A session lives 12 hours. Its idp.global tokens are stored sealed (AES-256-GCM) under a key derived from the session key file, and the CSRF token is derived per session under another; neither key enters the database.
GET /auth/session answers the session (sessionId, principalId, expiresAt, csrfToken), the person (displayName, email) and the organizations this session may act in, those of the sign-in that opened it: { id, name, slug, roles, actions, observedAt }, keyed by the idp.global organization id, where roles are the person's idp.global role keys as last observed and actions what the role table grants for them. That list is for display; every decision observes idp.global again.
Authority
What a person may do in an organization comes from idp.global, live, through the role table:
| Role | May |
|---|---|
| owner, admin | claim, disconnect |
| editor | mute |
| viewer | read |
| guest, outlaw | nothing |
Each row also grants the rows below it, and outlaw grants nothing whatever else a person holds. CloudAccess.authority.authorize({ session, person, organizationId, action }), with the session and person CloudAccess.resolveCookieSession(cookieHeader) answers, answers a grant or throws CloudAccessAuthorityRefusal with one code: organization-unknown, role-insufficient, membership-absent, sign-in-required or authority-unavailable. A write (claim, disconnect, mute) introspects at idp.global for every decision; a read may reuse an observation for up to 60 seconds, so a revocation reaches reads within a minute. Any failure to ask idp.global refuses. Only organizations of the session's own sign-in are asked about: idp.global freezes a login's organizations, so a session opened before an organization was connected cannot act in it until the person signs in again.
The people map
Cloud access keeps the people who signed in: their idp.global subject, name and email, the organizations of their latest sign-in with the role keys last observed, and their last sign-in here; and for each sign-in the organization ids it named, kept at least as long as its session lives (12 hours), removed at the person's next sign-in after that, and at the latest with the person. It grants nothing. A person is removed, with their sessions and sign-in records, 90 days after their last sign-in here (cloudAccessOwnerDefaults.retentionDays); the removal runs at start and after each sign-in, one at a time, and a sign-in while one runs gets another after it (CloudAccess.signIn.flush() waits for them). CloudAccess.directory.peopleOf(organizationId) lists the people of one organization, latest sign-in first.
The browser app
The pages of serve.zone/design (cloudly-dcrouter/CloudAccess-*) on dees-catalog's dees-appui shell, served at /, /controllers, /alerts, /updates, /members, /settings and /connect from the bundle pnpm build writes to dist_serve/bundle.js:
- Sign-in: idp.global is the only way in.
- Home: the controllers, nodes, alerts and updates at a glance, a card per controller with Open and Details, and what needs attention.
- Connect: the pairing code Cloudly shows, the organization (owners and admins only) and the name the controller is listed as here (its address when left empty); the claim waits for the controller's Administrator to confirm it in Cloudly.
- Controllers: the list, each controller by its name here with its address beneath, and the selected controller's record: locations, the address checks of its name, open alerts, versions, your role there as the controller reports it, what it calls itself, and Open. Owners and admins may rename or disconnect it here.
- Alerts: the open alerts of every controller, muted and resolved ones under the list's filter. Editors and up mute an alert here for 24 hours; the controller keeps it. Cloud access sends no email yet.
- Updates: read-only, what each controller runs and is offered; Open in Cloudly is disabled while the controller does not report.
- Members: only the people the controllers report and the people who signed in here (idp.global lists no members to apps), with their idp.global role as last observed and their Cloudly role on each controller.
- Settings: the organization as idp.global names it, the notifications drawn as not sent yet, the relay's one region, and session limits as each controller's.
Open is a link to https://<verified name>/ in a new tab, built by the server while the relay routes the name and the controller reports; cloud access never frames a controller. Every string a controller reports, and every name a person gives a controller, is shown as text. Every page names a controller by its name here: given at code entry or since, its host until then.
The browser API (JSON; refusals are { error }):
| Route | Who | Answers |
|---|---|---|
GET /api/organization?organizationId= |
viewer and up (an observation of at most 60 s) | the organization, its controllers with their reports, proofs and openUrl, its alerts with their mutes, its members |
POST /api/alerts/mute { organizationId, alertId, csrfToken } |
editor and up, asked live | { alertId, mutedUntil } |
POST /api/controllers/disconnect { organizationId, bindingId, csrfToken } |
owner and admin, asked live | { bindingId, disconnected: true } |
POST /api/controllers/rename { organizationId, bindingId, displayName, csrfToken } |
owner and admin (claim: who may claim a controller names it), asked live |
{ bindingId, name }, the name trimmed |
POST /api/code-entry |
owner and admin, asked live | see Code entry |
Writes need Origin to be the public origin and the session's CSRF token. Refusals: sign-in-required (401), request-invalid, name-invalid (400; a name as in Code entry), origin-refused, csrf-refused, organization-unknown, role-insufficient, membership-absent (403), alert-unknown, controller-unknown (404), authority-unavailable and cloud-access-unavailable (503).
A controller's alerts follow its reports: an attention item opens an alert the first time its key appears and resolves it when the key goes; cloud access's own alerts (not-reporting, epoch-conflict, name-route-removed) sit beside them. Resolved alerts are kept 90 days.
Programmatic use
import { CloudAccess, readCloudAccessServeConfig } from '@serve.zone/cloudaccess';
const cloudAccess = new CloudAccess(readCloudAccessServeConfig(process.env));
await cloudAccess.start();
await cloudAccess.stop();
CloudAccess options extend the serve configuration with logSink (structured log entries; default one JSON line each on stdout and stderr), clock (the clock sessions, authority, the people map and the relay read), provider (the idp.global client; default an IdpOidcProvider for the configuration, tests pass the SDK's RelyingPartyProviderDouble), onStorageFailure, nameRegistry (an ICloudAccessNameRegistry; default the hostname proof, tests of the relay pass a CloudAccessMemoryNameRegistry), nameProofAlerts (receives the proof's route alerts; default the bindings' alert store), bundleFile (the browser bundle; default dist_serve/bundle.js of the package) and streamBroker (an ICloudAccessStreamBroker; default the service's CloudAccessStreamBroker). While it runs, directory, authority, relay (its stats), streamBroker and tunnel (their stats), streamLog, hostnameProof, bindings (bindings, reports and alerts), enrollments, controllerSessions, controlSocket and codeEntry serve in-process callers, and port, httpPort and webPort answer the bound ports. CloudAccessRelay can run on its own with any registry, broker, tunnel, recorder and limits. hostnameProof.checkName(bindingId, publicOrigin) runs a binding's proof and answers its state (the four checks, each unchecked, passing, failing or lookup-error with its time and a detail of at most 500 characters; pointsAtRelay; routed; checkedAt and nextCheckAt), recheck(bindingId) brings it forward ('rate-limited' within a minute of the last run), stateOf(bindingId) reads it, isVerified(name) answers the binding a name routes to, and forget(bindingId) removes a binding's route and proof. runCli(argv, io) runs the CLI and resolves with its exit code.
TlsClientHelloReader is the relay's ClientHello reader: new TlsClientHelloReader(16 * 1024), then push(chunk) with each chunk of a connection as it arrives, until the reading is complete (serverName, normalized, or null without server_name; helloBytes) or invalid (reason). It reads a hello split across TLS records and TCP segments, never reads past its bound, and leaves the bytes after the hello unread. normalizeServerName(name) answers the lower-case form names are compared in, or null for anything that is not a DNS host name.
The service starts while idp.global is out of reach: discovery is tried again every 30 seconds, and until it succeeds a sign-in fails and every authority check refuses.
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.