Skip to content

@kernlize/bridges ​

Locator parsing, the bridge contract, the registry that routes and authorises, an HTTP bridge with an SSRF policy, four backend-driven bridges for downloads, sharing and mail, hand-off backends that open an app on the user's device, the adapter that feeds a resource service, and user-facing descriptions. Zero dependencies.

FieldValue
npm@kernlize/bridges
JSR@kernl/bridges
Version0.2.1
Entry./dist/index.js, types ./dist/index.d.ts
Dependenciesnone
Peer dependenciesnone
Side effectsnone

Install ​

This package is @kernlize/bridges on npm and @kernl/bridges on JSR, the same code at the same version. Once released:

sh
npm install @kernlize/bridges
# or
deno add jsr:@kernl/bridges

Neither registry has a release yet, so both commands fail. During the alpha the package is built from the kernl source repository, which is private, and consumed as a workspace dependency under its npm name. The package index has the steps.

json
{ "dependencies": { "@kernlize/bridges": "workspace:^" } }

Purpose ​

A bridge resolves a protocol-specific locator to bytes, a job or a conversation on the user's behalf, behind the same grant and quota as everything else (the composed shell wires no per-request consent), and is named by what it does rather than by its protocol. The HTTP bridge is complete. The torrent, NZB, Soulseek and mail bridges are complete up to the backend interface they call; createHandoffBackends supplies all four by handing the locator to an app on the user's device, createWebTorrentBackend downloads magnet links inside the page instead (D-017), and without any backend they report unavailable. The concepts are in Bridges; the opinion is D-010.

Exports ​

Locators ​

ExportSignatureDescription
parseLocator(uri: string) => LocatorScheme and scheme-specific fields for magnet, nzb, slsk, mailto and the web schemes http, https, ws, wss. Any other well-formed scheme is a locator with no fields. Throws invalid:locator or invalid:magnet-missing-hash. Field tables are in Bridges.
formatLocator(locator: Locator) => stringA user-facing label without the scheme: the name where one is known, otherwise the host, peer or hash.
isNzbUrl(url: string) => booleanTrue when the string parses as a URL whose path ends in .nzb (any case). parseLocator uses it to route such web URLs to the nzb scheme.
Locatorinterface{ scheme: string; raw: string; fields: Readonly<Record<string, string | readonly string[]>> }.

Bridge contract ​

ExportSignatureDescription
Bridgeinterface{ id; label; schemes; exposure: BridgeExposure; availability(): BridgeAvailability; resolve(locator, context): Promise<BridgeResult> }.
BridgeContextinterface{ requester: { dTag; hash } | 'shell'; signal?: AbortSignal; headers?: Readonly<Record<string, string>> }.
BridgeResultunion{ kind: 'bytes'; response: Response }, { kind: 'job'; id; label } or { kind: 'conversation'; id; participants: readonly string[] }.
BridgeExposureinterface{ revealsAddress; encryptedInTransit; publicActivity }, three booleans describeBridge renders for the user in the shell settings.
BridgeAvailabilityunion{ available: true; handoff?: true } or { available: false; reason: string }. handoff is set when resolving opens an app on the user's device instead of kernl moving the data. The reason is a bare word such as no-backend.
BridgeAuthorizertype(locator, bridge, context) => boolean | Promise<boolean>; the registry's consent hook. The composed shell supplies none in this draft.

Registry ​

ExportSignatureDescription
createBridgeRegistry(options?: BridgeRegistryOptions) => BridgeRegistryOwns every bridge, routes a locator to the bridge serving its scheme, runs the authorize hook when one is given, reports every outcome to onResolve when one is given.
BridgeRegistryOptionsinterface{ authorize?: BridgeAuthorizer; onResolve?: (event: BridgeResolveEvent) => void }. Without authorize every request is allowed.
BridgeResolveEventinterface{ bridge: string; scheme: string; ok: boolean; reason?: string }. bridge is none when no bridge serves the scheme or the string did not parse.
BridgeRegistryinterfaceregister(bridge) (throws invalid:duplicate-bridge <id>, invalid:scheme-taken <scheme>), list(), forScheme(scheme) (case-insensitive, trailing colon allowed), resolve(uri, context) (rejects with invalid:, unavailable: or denied: reasons).

HTTP bridge ​

ExportSignatureDescription
createHttpBridge(options?: HttpBridgeOptions) => BridgeId http, label downloads. Allowed schemes only, no private or local destinations (re-checked on every redirect hop and, with resolveHost, after name resolution; without it hostnames are judged as literals), a redirect cap, a time limit and a byte limit enforced on the streamed body. In a browser an opaque redirect is refused with denied:redirect-unverifiable, and CORS applies.
HttpBridgeOptionsinterface{ policy?: Partial<HttpPolicy>; fetch?: typeof fetch; resolveHost?: (host: string) => Promise<readonly string[]> }.
DEFAULT_HTTP_POLICYHttpPolicy{ allowedSchemes: ['https:'], maxRedirects: 3, maxBytes: 10485760, timeoutMs: 20000, allowPrivateHosts: [] }. Scheme entries carry the colon, as URL.protocol does.
HttpPolicyinterfaceallowedSchemes, maxRedirects, maxBytes, timeoutMs, allowPrivateHosts.
isPrivateAddress(ip: string) => booleanTrue for literal IPv4 or IPv6 addresses in the unspecified, loopback, private, shared, link-local, IETF protocol assignment, benchmarking, multicast or reserved IPv4 ranges, and the link-local, site-local, unique-local, multicast, discard, local NAT64, 6to4 or Teredo IPv6 ranges. IPv6 is parsed to its full 128 bits (brackets and zone suffix stripped); an address that embeds an IPv4 destination (mapped, compatible, translated, NAT64 64:ff9b::/96) is judged by that destination. False for public addresses and for strings that are not an IP. Ranges are listed in Bridges.
isBlockedHostname(host: string) => booleanTrue for localhost and names under .localhost, .local or .internal, in any case, with or without a trailing dot.

Backend-driven bridges ​

ExportSignatureDescription
createTorrentBridge(backend?: TorrentBackend) => BridgeId torrent, label downloads, scheme magnet. Exposure: reveals address, not encrypted, public activity.
createNzbBridge(backend?: NzbBackend) => BridgeId nzb, label downloads, scheme nzb. Exposure: reveals address, encrypted, not public.
createSoulseekBridge(backend?: SoulseekBackend) => BridgeId soulseek, label sharing, scheme slsk. Exposure: reveals address, not encrypted, public activity.
createMailBridge(backend?: MailBackend) => BridgeId mail, label mail, scheme mailto. Exposure: does not reveal address, encrypted, not public.
BackendTraitsinterface{ handoff?: true | undefined }. Every backend interface extends it; a hand-off backend sets it, and its bridge then reports { available: true, handoff: true }. The bridge reads it on every availability check, so a backend may switch it, as the shell's does when the user changes where downloads go.
TorrentBackendinterfaceadd(infoHash: string, trackers: readonly string[], context: BridgeContext, name?: string): Promise<{ id: string; name: string }>. name is the magnet's display name when it has one.
NzbBackendinterfaceadd(url: string, context: BridgeContext): Promise<{ id: string; name: string }>.
SoulseekBackendinterfacedownload(user: string, path: string, context: BridgeContext): Promise<{ id: string; name: string }>.
MailBackendinterfaceopen(address: string, subject: string | undefined, context: BridgeContext, body?: string): Promise<{ id: string }>.

Without a backend each of the four reports { available: false, reason: 'no-backend' } and resolve() rejects with unavailable:no-backend. A locator missing a field the backend needs rejects with invalid:locator-missing-<field> before the backend is called.

Hand-off backends ​

ExportSignatureDescription
createHandoffBackends(open: HandoffOpener) => HandoffBackendsOne backend per bridge, each with handoff: true. Each rebuilds the canonical address from the parsed fields (a magnet: link with hash, name and trackers; an slsk:// address; a mailto: address with subject and body; the NZB document's https URL) and passes it to open once. Ids come from one shared counter, handoff:1, handoff:2, and so on. The NZB backend throws invalid:nzb-not-https for a document not served over https.
HandoffOpenertype(uri: string) => void | Promise<void>. The shell passes host.openLink, which asks the user before leaving kernl.
HandoffBackendsinterface{ torrent: TorrentBackend; soulseek: SoulseekBackend; mail: MailBackend; nzb: NzbBackend }.

A hand-off makes no connection from kernl; the app on the device does the transfer with its own network, which kernl cannot see or limit. See Bridges.

Downloads inside kernl ​

ExportSignatureDescription
createWebTorrentBackend(options: WebTorrentBackendOptions) => WebTorrentBackendA TorrentBackend without a handoff trait that downloads through a WebTorrent client inside the page. Throws invalid:info-hash for a hash that is not 40 hex characters and unavailable:<why> when the client cannot load or refuses the link. The same hash twice is one job; ids are download:1, download:2, and so on.
WebTorrentBackendOptionsinterface{ client: WebTorrentLike | (() => WebTorrentLike | Promise<WebTorrentLike>); trackers?: readonly string[]; now?: () => number; progressIntervalMs?: number }. A loader runs on the first download and again after a failure. trackers replaces DEFAULT_WEBRTC_TRACKERS; progress-only notifications are at most one per progressIntervalMs (500 by default).
WebTorrentBackendinterfaceTorrentBackend plus jobs(), subscribe(listener) => stop, pause(id), resume(id), remove(id) (drops the job and its stored data) and files(id) => Promise<readonly DownloadedFile[]>. An unknown id throws denied:unknown-download; files on an unfinished job rejects with unavailable:not-finished.
DownloadJobinterface{ id, name, infoHash, state, progress, received, size?, peers, speed, error? }. progress runs from 0 to 1; sizes are bytes; speed is bytes per second and 0 unless downloading.
DownloadStatetype'connecting' | 'downloading' | 'paused' | 'done' | 'failed'.
DownloadedFileinterface{ name: string; size: number; blob: Blob }.
DownloadListenertype(jobs: readonly DownloadJob[]) => void.
WebTorrentLike, WebTorrentTorrentLike, WebTorrentFileLikeinterfacesThe parts of a WebTorrent client, torrent and file the backend uses, so the package has no dependency on the library: add(magnet), remove(torrent, { destroyStore }); a torrent's name, length, downloaded, progress, downloadSpeed, numPeers, done, files, on(event, listener), pause(), resume(); a file's name, length, blob().
DEFAULT_WEBRTC_TRACKERSreadonly string[]wss://tracker.openwebtorrent.com, wss://tracker.webtorrent.dev, wss://tracker.btorrent.xyz.
webTrackers(trackers: readonly string[], defaults?: readonly string[]) => readonly string[]The link's wss:// trackers, then the defaults, without duplicates. A page cannot open the UDP or TCP sockets other trackers need.

NZB, Soulseek and mail have no in-page backend: their protocols need raw TCP sockets, which a browser page cannot open. See Bridges.

Resource resolver ​

ExportSignatureDescription
createBridgeResourceResolver(registry: BridgeRegistry, options?: BridgeResourceResolverOptions) => BridgeResourceResolverAdapts a registry to the fetch(url, init) seam of a resource service. bytes results return their Response; job and conversation results become a 202 Accepted JSON body. Failures reject with the registry's reasons.
BridgeResourceResolvertype(url: string, init: ResourceResolveInit) => Promise<Response>.
ResourceResolveInitinterface{ signal?: AbortSignal; headers?: Record<string, string>; requester?: BridgeContext['requester'] }.
BridgeResourceResolverOptionsinterface{ requesterFor?: (init: ResourceResolveInit) => BridgeContext['requester'] }. Used when the init names no requester; the fallback is 'shell'.

Descriptions ​

ExportSignatureDescription
describeBridge(bridge: Bridge) => readonly DescriptionRow[]Rows for the user: Used for, Status (ready or not available plus a Reason row; a hand-off bridge adds Handled by: an app on your device and Sent through kernl: no), Shares your network address, Encrypted in transit, Activity visible to others. No row names a protocol.
DescriptionRowinterface{ label: string; value: string }.

Example ​

ts
import { createBridgeRegistry, createBridgeResourceResolver, createHandoffBackends, createHttpBridge, createTorrentBridge, formatLocator, type ResourceResolveInit } from '@kernlize/bridges';

declare const lookupAddresses: (host: string) => Promise<string[]>; // the shell's DNS lookup
declare const identityFor: (init: ResourceResolveInit) => { dTag: string; hash: string } | 'shell'; // the shell's window-to-identity map

const registry = createBridgeRegistry({
  authorize: (locator, bridge) => confirm(`Open ${formatLocator(locator)} with ${bridge.label}?`),
  onResolve: (event) => console.log(event),
});
registry.register(createHttpBridge({ resolveHost: lookupAddresses }));
const handoff = createHandoffBackends((uri) => {
  if (confirm(`Leave and open ${uri}?`)) window.open(uri, '_blank', 'noopener,noreferrer');
});
registry.register(createTorrentBridge(handoff.torrent)); // { available: true, handoff: true }

const result = await registry.resolve('https://example.com/file.bin', { requester: 'shell' });
if (result.kind === 'bytes') result.response.status;

// what @kernlize/core hands to kehto's resource service
const fetchLike = createBridgeResourceResolver(registry, { requesterFor: (init) => identityFor(init) });

Scope ​

  • Does not implement any transfer protocol. The hand-off backends only build an address and open it; backends that move data live in the shell app or in packages of their own.
  • Does not prompt. Consent is a BridgeAuthorizer the host can supply to the registry; createKernl's default registry has none.
  • Does not apply the guard. Rate limits and the hidden-window allowance run in @kernlize/guard before a request reaches the registry.
  • Does not know about NAP envelopes. @kernlize/core maps resource.* requests onto createBridgeResourceResolver(registry).

Specification ​

packages/bridges/features/: locators.feature, registry.feature, http-bridge.feature, backend-bridges.feature, handoff-backends.feature, webtorrent-backend.feature, resource-resolver.feature, describe.feature.

API reference ​

Generated module: _kernlize_bridges

MIT licensed. Protocol authority: NIP-5D (nostr-protocol/nips PR #2303) and the NAPs (napplet/naps).