qurl-go

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT

README

qurl-go

Use the LayerV qURL Platform from Go: protect a private URL once, then mint short-lived access links for it.

LayerV hosts qURL. Your Go app keeps a tiny surface area: protect the URL, create a portal link for the returned resource, and share the link.

Portal recipients do not need LayerV credentials, API keys, keypairs, or SDK state. They open the qURL link. Credentials are only for software that protects URLs or creates portals.

Go Reference CI

Why qURL

Agents and services increasingly need to reach private MCP servers, APIs, and internal tools. Every standing public endpoint becomes inventory for scanners, fingerprinting, credential attacks, and AI-assisted probing before a legitimate user or agent ever arrives.

qURL is an invisibility primitive for authenticated access. A portal is cryptographic, just-in-time permission for one actor to reach one private resource without turning that resource into public inventory.

Install

go get github.com/layervai/qurl-go/qurl@latest

Requires Go 1.26+.

Quickstart

package main

import (
	"context"
	"time"

	"github.com/layervai/qurl-go/qurl"
)

func issuePortal(ctx context.Context) (string, error) {
	client, err := qurl.OpenClient()
	if err != nil {
		return "", err
	}

	resource, err := client.ProtectURL(ctx, "https://internal.example.com/dashboard")
	if err != nil {
		return "", err
	}

	portal, err := resource.CreatePortal(ctx, qurl.ValidFor(5*time.Minute))
	if err != nil {
		return "", err
	}
	return portal.Link, nil
}

If qURL Connector already protects the service, use its immutable connector slug:

resource, err := client.GetConnectorResourceBySlug(ctx, "prod-dashboard")
if err != nil {
	return err
}
portal, err := resource.CreatePortal(ctx, qurl.ValidFor(5*time.Minute))

If you persist the resource id, future calls can reconstruct the handle without another lookup:

resource := client.ResourceByID(resourceID)
portal, err := resource.CreatePortal(ctx, qurl.ValidFor(time.Hour))

Connect to LayerV

Only software that protects URLs or creates portals needs LayerV credentials. A user or agent that only receives and opens a qURL link does not set up anything.

Application issuers normally run the LayerV setup flow once, then use:

client, err := qurl.OpenClient()

For protected external credential storage, implement qurl.CredentialProvider and pass it to qurl.NewClient.

Native qURL Connector lifecycle

RegisterAgentRuntime is the only agent enrollment front door. Assignment, optional account OTP, REG/RAK, and completion travel directly over authenticated NHP UDP. There is no public HTTP assignment, registration, refresh, recovery, or completion API in this SDK.

Trusted deployment configuration supplies one LayerV-owned Hub DNS name, UDP port, and pinned server public key:

store := qurl.FileAgentState("/var/lib/layerv/qurl/agent-state.json")
hub := qurl.HubBootstrap{
	Host:               "hub.nhp.layerv.ai",
	Port:               62206,
	ServerPublicKeyB64: configuredHubPublicKey,
}

client, binding, err := qurl.RegisterAgentRuntime(ctx, enrollmentCredential, store,
	qurl.WithAgentRuntimeHub(hub),
	qurl.WithAgentRuntimeMetadata(hostname, version),
)
if err != nil {
	return err
}
defer binding.Destroy()

The Hub assigns the cell. Before REG, the SDK durably persists the exact ticket, registration identity/metadata, authority-provided UDP endpoint and server identity, and a one-way identity of the caller's enrollment credential. A lost RAK therefore restarts by re-driving the same REG to the same pinned cell—even after ticket expiry—before any new Hub assignment, within the released 90-day recovery horizon. The authenticated assignment-ticket expiry anchors that deadline. An authenticated non-commit verdict may replace the current ticket, but the first ticket-expiry anchor and its absolute deadline remain unchanged across the replacement, RAK, retries, restarts, and pending completion. Plaintext enrollment credentials and OTP codes are never persisted. The SDK never calculates a cell address or involves the browser relay in native discovery.

Recovery must use the identical persisted hostname and version because those bytes are part of the exact REG replay. Change that metadata only after activation completes. The v0.5 assignment lease must also expire strictly after its ticket; pending-state validation enforces that producer invariant.

The default policy accepts unattended connector_bootstrap, bootstrap, and durable agent credentials. Interactive account enrollment must explicitly add both WithAgentRuntimeAllowedRegistrationKeyKinds(RegistrationKeyKindAccount) and WithAgentRuntimeOTPProvider.

For account enrollment, the one-way OTP dispatch intentionally occurs before the pending-activation save. A save failure cannot have sent REG; a later explicit attempt may obtain a new ticket and dispatch that ticket's single OTP. Pending-activation recovery invokes the provider without another OTP dispatch and uses the RegisterAgentRuntime caller context clamped to the persisted recovery deadline because exact replay may outlive the ticket window. Set an earlier outer context deadline when the provider should return sooner.

Every RegisterAgentRuntime enrollment credential must be a server-minted encoded token whose total string length is at least 32 bytes, including any prefix. Shorter values and user-chosen passwords are rejected before state mutation or network I/O. This requirement is part of the initial pre-1.0 native-UDP contract for all credential kinds. This is a syntax and total-length check, not an entropy measurement; the minting authority must use cryptographically random token material.

Warm starts call OpenRegisteredAgentRuntime, which loads completed state without network I/O. RefreshAgentRuntime refreshes an expiring assignment only through the pinned Hub. It accepts endpoint revisions within the same cell and assignment generation; a cell or generation move returns *AgentAssignmentChangedError for explicit caller handling.

After the provisioned workflow has deliberately accepted that authority move, the caller opts into one fresh authenticated refresh:

client, binding, err := qurl.RefreshAgentRuntime(ctx, hub, store,
	qurl.WithAgentRuntimeReassignmentAdoption(),
)

The option has no placement arguments: it can adopt only the Hub LRT returned by that call, and only when the assignment generation advances. The SDK writes the complete cell, endpoint, pinned server key, and lease snapshot in one save; it never derives, probes, or contacts the new cell during refresh. Without the option, the same response continues to return *AgentAssignmentChangedError without changing durable state.

For each Connector cycle, take the private key once, create one RunID, and knock with the resource's placement-neutral KnockResourceID:

privateKey := binding.TakeDeviceStaticPrivateKey()
defer clear(privateKey)

connector, err := client.EnsureConnectorResource(ctx, "prod-dashboard")
if err != nil {
	return err
}
runID, err := qurl.NewCycleRunID()
if err != nil {
	return err
}
admission, err := qurl.KnockRegisteredAgent(ctx, binding, privateKey,
	connector.Resource.KnockResourceID,
	qurl.NativeKnockOptions{RunID: runID},
)

The returned Client uses HTTPS only for steady-state resource CRUD. WithAgentClientBaseURL and WithAgentClientHTTPClient configure only that resource client; they never affect Hub or cell UDP transport.

Agent state may be stored in a strict 0600 file under a 0700 directory, in a sealed file using NewSealedFileAgentState, in AWS Secrets Manager or SSM via awsstore, or in a custom AgentStateStore. The schema is native-only and duplicate fields, unknown fields, negative schema versions, and versions newer than this SDK fail closed before lifecycle network I/O. An older SDK therefore cannot open state containing fields introduced by a newer SDK; treat a downgrade as an explicit state-schema migration or reprovisioning operation rather than deleting state.

See Register an agent for the complete lifecycle, storage contract, recovery boundaries, and error table.

Most recipients open qURL links directly and do not use this SDK. Programmatic recipients install opener trust configuration and call:

portal, err := qurl.EnterPortal(ctx, link)

EnterPortal fails closed when no provider is installed.

Guides

Error handling

Match errors by type or sentinel, not message text:

Error Meaning
qurl.ErrInvalidClientConfig Resource-client credentials or options are malformed
qurl.ErrInvalidRegisterConfig Native lifecycle inputs are malformed
qurl.ErrAssignmentRecoveryRequired Hub assignment exhausted its bounded logical operation
qurl.ErrAgentBindingPersistence A state save failed or its acknowledgement was lost; reload before retry because the refreshed assignment may already be durable
qurl.ErrCompletionRecoveryRequired Resume the exact persisted completion candidate
qurl.ErrAgentRecoveryExpired The pending activation/completion reached the 90-day boundary; no datagram is sent at or after it, though the call may already have sent earlier traffic; use explicit NHP-native recovery or reprovisioning
qurl.ErrAgentRecoveryMigrationRequired A legacy pending completion has no authenticated finite-deadline anchor; preserve it and use explicit NHP-native recovery or reprovisioning
*qurl.NativeCredentialRecoveryRequiredError Completed native credential state is absent or malformed; explicit native recovery or reprovisioning is required
*qurl.AgentAssignmentChangedError The Hub assigned a new cell or generation; deliberately re-run refresh with WithAgentRuntimeReassignmentAdoption to accept a newer generation
*qurl.APIError LayerV returned a non-2xx steady-state resource response
*qurl.ServerDenyError qURL denied an authenticated NHP operation

Security notes

  • Treat LayerV credentials, agent state, and qURL links like credentials. Do not log them.
  • Pin Hub and assigned-cell identities; do not derive or infer their addresses.
  • Keep the exact pending activation across ambiguous RAK delivery and the exact pending completion candidate across ambiguous completion delivery.
  • Wipe the private-key bytes taken from AgentRuntimeBinding after the knock.
  • Keep issuer credentials in protected state, KMS, a secret manager, or another protected store.
  • Browser relay traffic and native UDP agent traffic are separate trust paths.

Changes

Unreleased
  • Added the qURL Connector native UDP lifecycle: Hub assignment, assigned-cell OTP/REG/completion, direct knock, strict golden-vector conformance, crash-safe activation/completion, and explicit opt-in assignment reassignment adoption.
  • Bounded native registration recovery to 90 days after the first authenticated assignment-ticket expiry, with a per-datagram deadline fence, immutable replacement anchor, and fail-closed pre-v6 pending-state migration.
  • Registration retry budgets are per phase, so one call can span initial Hub, first REG, replacement Hub, second REG, and completion budgets. Use an outer context deadline when a smaller aggregate wall-clock ceiling is required.
  • Removed the superseded public HTTP agent assignment/registration lifecycle. Steady-state resource CRUD remains HTTPS, and browser relay behavior is unchanged.
  • Added sealed full-AgentState storage and AWS-backed AgentState stores.

License

MIT © LayerV AI

Directories

Path Synopsis
internal
agentstatecontract
Package agentstatecontract holds wire-neutral AgentState constants shared by the root SDK and in-repository storage modules.
Package agentstatecontract holds wire-neutral AgentState constants shared by the root SDK and in-repository storage modules.
cryptoutil
Package cryptoutil owns the module's small cryptographic-randomness and secret-buffer primitives.
Package cryptoutil owns the module's small cryptographic-randomness and secret-buffer primitives.
nhpcontract
Package nhpcontract holds wire limits shared by the public qurl runtime and the internal NHP codec.
Package nhpcontract holds wire limits shared by the public qurl runtime and the internal NHP codec.
qv2
Package qv2 is the internal cryptographic core of the qURL Go SDK: it verifies incoming qURL links and mints new ones.
Package qv2 is the internal cryptographic core of the qURL Go SDK: it verifies incoming qURL links and mints new ones.
testkeys
Package testkeys generates throwaway public keys for the SDK's runnable examples and tests, so the example files don't each re-implement the same key-gen helpers.
Package testkeys generates throwaway public keys for the SDK's runnable examples and tests, so the example files don't each re-implement the same key-gen helpers.
udpfence
Package udpfence carries an internal, fail-closed authorization check from a lifecycle transaction to the native UDP write boundary.
Package udpfence carries an internal, fail-closed authorization check from a lifecycle transaction to the native UDP write boundary.
x25519key
Package x25519key validates X25519 public identities at control-plane and transport trust boundaries.
Package x25519key validates X25519 public identities at control-plane and transport trust boundaries.
Package qurl is the Go SDK for the LayerV qURL Platform.
Package qurl is the Go SDK for the LayerV qURL Platform.
Package relayknock is the low-level NHP relay-knock layer of the qURL Go SDK.
Package relayknock is the low-level NHP relay-knock layer of the qURL Go SDK.
internal/nhpwire
Package nhpwire is the internal NHP Noise wire codec shared by the public relayknock package (client/initiator API) and the relayknocktest test-support package (server/responder helpers).
Package nhpwire is the internal NHP Noise wire codec shared by the public relayknock package (client/initiator API) and the relayknocktest test-support package (server/responder helpers).
nativeudp
Package nativeudp is the native NHP-over-UDP transport of the qURL Go SDK.
Package nativeudp is the native NHP-over-UDP transport of the qURL Go SDK.
relayknocktest
Package relayknocktest provides the server/responder-role NHP wire helpers a test double (or cross-language conformance tooling) needs to stand in for an NHP relay+server: build a server-originated reply, and open an initiator packet an agent posted.
Package relayknocktest provides the server/responder-role NHP wire helpers a test double (or cross-language conformance tooling) needs to stand in for an NHP relay+server: build a server-originated reply, and open an initiator packet an agent posted.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL