deepseek-ai/deepseek-harness--packages-api-gateway ↗★ 263k

@deepseek-ai/dsh-api-gateway

Typert Remote Host dispatcher and Client API endpoint

Package
@deepseek-ai/dsh-api-gateway
Compatibility
Unverified
Cordis peer range
workspace:~
Version
0.2.1-rc.3
License
MIT
Last updated
Oct 23, 2026

Other repositories with this package name

Install

This plugin has no verified bundle, or compatibility checks failed. Read the repository notes first. Read the full README ↗


description: "Typed Client-to-Host calls and streams: dispatch, validation, cancellation, reconnection, and forwarded Host events." kind: "package-reference"

@deepseek-ai/dsh-api-gateway

English | 中文

Summary

Two-sided Typert RPC endpoint for Host and Client Cordis environments. The Host entry provides ctx.typertGateway, while @deepseek-ai/dsh-api-gateway/client provides ctx.remote; both consume the same generated InvocationDescriptor contract and leave business selection to API Remotes. Connection carries unary request correlation, trust, and response envelopes, while Gateway owns multiplexed Remote streams, each carrying a Client-to-Host uplink on the same logical stream.

Table of Contents


Host service: TypertGatewayService (ctx key: typertGateway)

ctx.typertGateway.invoke() resolves the current descriptor and Cordis Service for each call, validates exact named arguments, resolves registered object or Context identities, and invokes the public business method. Business Services extend TypertRemoteService and mark methods with @Remote or @RemoteScope from dsh-typert-protocol; bindTypertRemote() remains available when another base class owns inheritance.

Strict mode reads generated invocation descriptors from ctx.typert.local. Lookup parameters use the currently active resolver in ctx.typert.lookups: the business package registers the stable declaration and default policy, while Host composition can override resolution behavior with effect-scoped configure(); @RemoteScope resolves its receiver through a registered Host Context adapter. SRC mode is a development fallback for endpoints that have never had a strict definition; it parses simple parameter names and accepts only JSON-safe values for non-lookup parameters. Withdrawing an observed strict definition fails instead of weakening validation. For unary results, Gateway executes the generated codec's encode() when present and otherwise keeps strict JSON values unchanged; SRC recursively detects runtime byte values. Both paths return JSON-compatible metadata and result-relative byte attachments to Connection for transport framing.

The Host entry registers a trusted-host interceptor on Connection's shared /api FetchHandler. Connection passes this composite handler through its HTTP bridge; the handler dispatches claimed endpoints to Gateway and returns 404 for unclaimed requests unless an exact Fetch route owns them. Direct invoke() calls preserve business errors; TypertGatewayError is a RemoteError subclass whose gateway/* codes name the failures owned by dispatch, binding, providers, lookup, Context, arguments, codecs, and the uplink: gateway/uplink-overflow for an uplink whose buffered frames exceed streamInboxBytes, gateway/protocol for an uplink item after end, and the codec code gateway/input-invalid for a rejected uplink item. A resolver that refuses on policy grounds — a cold-resume failure or an ownership fence — throws its own RemoteError, and the code it chose reaches the caller unchanged.

Every call names the Peer it speaks for: InvokeRemoteRequest.peer and the peer argument of wireStream.open() are required, and the Gateway never supplies a default Peer. Dispatch from a disposed Peer fails with gateway/peer-disposed, for unary calls, streams, $events, and $events/result alike, and wire paths check it before they read the payload. The Host service injects remoteAccess and does not start without dsh-api-remote-access. After the endpoint and argument-field checks, every call validates its JSON arguments once against their Typert-compiled types, failing with gateway/input-invalid, and dispatch reuses the validated values. For a call whose Peer principal is not the owner, after that validation and before lookup and Context resolution, the Gateway resolves the resources the call touches and asks ctx.remoteAccess.decide({ principal, action, resources, args }) with the endpoint as the action and the validated arguments as args, or refuses with reason resource-invalid without asking when the resources are invalid; a refused call throws remote-access/denied with details { action, reason }, so a refused call fails before it activates any Session. An owner call is dispatched without resource resolution or a decision.

Resources are read from validated arguments: Typert-compiled codecs drop undeclared object fields, while SRC fallback checks JSON safety without stripping them; lookup and Context identities keep their wire values. For each resource kind, a reader in the method's @Remote({ resources }) or @RemoteScope(key, { resources }) options wins; it is called with the method's positional arguments, where lookup positions hold wire identities and the cancellation position holds the call's signal, and is read from the class metadata of the root Service instance. Otherwise every lookup parameter whose provider declares that kind through resource contributes its wire identity, an omitted optional one contributing none, and a Context method whose Host Context adapter declares that kind contributes its Context identity, read from the arguments before the Context is resolved. Otherwise the kind's declaration in typert.resources supplies either a default field or a synchronous resolver. A field reads a top-level argument of that name or, failing that, the first object argument. A resolver is called once with { args, parameters }: named validated wire values and ordered parameter descriptions containing name and wire. Both defaults return one identity or undefined; arrays are invalid. An explicit reader may return several identities. A resolver or reader that throws, a reader returning an empty list, or an identity, including a Context identity, that is not a non-empty string makes the resolution invalid; a call that names no resource touches the global resource alone, and a call that names resources touches only those. A forwarded event carries every resource its entry declares, so there a reader returning undefined also makes the resolution invalid, while an entry without readers touches global.

A cancellation-aware Remote method declares signal: AbortSignal as its final Host parameter. The signal is descriptor metadata rather than a wire argument: Connection supplies it to the Gateway, and the Gateway injects it after decoded business parameters. SRC recognizes the reserved final name, while strict generation additionally requires the global AbortSignal type.

A stream Remote uses @Remote({ mode: 'stream' }) and returns an Iterable or AsyncIterable. ctx.typertGateway.stream() applies the same endpoint, argument, lookup, and cancellation checks as unary invocation, then returns a cancellation-aware iterable over the business items. The Client opens the Gateway-owned /api/remote.mux WebSocket when its plugin activates and keeps it connected while idle. Connection owns the retry schedule; before each retry it asks the mux to cancel any candidate or active socket and make exactly one fresh physical attempt. The Host sends Ping control frames at the configured websocketHeartbeatIntervalMs interval (two seconds by default), and the browser answers Pong at the WebSocket protocol layer, so idle network intermediaries see traffic without any Remote stream frame. A Pong, any received frame, or a downlink write that does not complete at once counts as peer activity; with the default 'deflate', a frame whose JSON text reaches the compression threshold is deflated asynchronously, so its completion always counts. A socket with no activity across two Ping intervals is terminated, so a congested link that delays the Pong behind data stays connected. With websocketCompression at its 'deflate' default, an upgrade that offers permessage-deflate negotiates the extension, and the Host compresses its own outbound frames whose JSON text reaches 1,024 characters; shorter frames stay uncompressed, uplink frames are compressed at the peer's discretion, and 'none' accepts the same upgrades without the extension. Independently cancellable logical streams share that socket; an in-process Connection carrier provides equivalent streams directly without opening it.

When the launcher supplies ctx.appReady, Gateway registers the WebSocket upgrade route only after successful application startup. Earlier connection attempts remain carrier failures handled by Connection's retry policy, so a restarting Host cannot accept streams while its controllers are still initializing. Unloading cancels a pending readiness subscription and closes any registered carrier. Embedded Hosts without appReady register immediately and own their startup ordering; in-process invocation is unchanged.

Every stream also carries a Client-to-Host uplink on the same logical stream. A method declares the uplink item type as the second type argument of its return type, RemoteStream from dsh-typert-protocol; In defaults to never, and a method that returns Iterable, AsyncIterable, or RemoteStream declares that it reads none. The running method reads its call as this.ctx.invocation: the Gateway derives the receiver from a Context carrying that RemoteInvocation — request, service, peer, signal, and uplink() — so nothing enters the parameter list, and ctx.invocation is undefined on a Context no Remote call derived. uplink() is available once per call. With a descriptor codec it decodes every item before delivery; without one — an SRC method, or a method whose In is never — it delivers JSON-safe unknown values. The Client sends each item as an item frame on the open logical stream, a top-level undefined item as an item frame without value, and its half-close as an end frame; the method observes the half-close as the end of the uplink() iteration and cancellation through signal. A rejected item fails the whole stream with gateway/input-invalid (field uplink). Uplink items are validated one by one at the Host because they arrive from the browser; downlink items are values the Host method produced and pass through unchecked. When the method finishes its downlink the Gateway returns the uplink iterator and drops unread items; items sent to a method that never takes its uplink wait in the inbox until then. Each logical stream buffers at most streamInboxBytes (262144 by default) of uplink frame bytes; overflow fails the stream with gateway/uplink-overflow, and an item after end fails it with gateway/protocol, in both cases without closing the socket. item, end, and cancel frames for a stream id the Host has already finished are dropped; only a duplicate open closes the socket. Gateway-owned streams such as $events return their uplink as soon as they open, so their item frames are dropped instead of buffered. A unary method may also call uplink(); its items are readable only while the method runs. Every stream speaks for an explicitly named Peer: a WebSocket's streams for the Peer Connection admitted at upgrade, whose scope disposal closes the socket, and an in-process carrier's for the Peer its caller passes. A stream method's signal also aborts when that Peer is disposed, so the stream ends with gateway/cancelled, and a $events stream ends with its Peer; a unary call already running is not cancelled.

Stream cancellation and stopping keep read-local state, not a history of delivered items, on the Host downlink, Host uplink, and Client uplink pump. Completing the downlink also wakes pending uplink reads.

Host composition can register one application event source through registerRemoteEvents(). Gateway reserves the internal $events logical endpoint for that source, accepts only empty args, and aborts streams opened by the registration when the source is withdrawn. API Remotes owns the event selection, argument validation, per-Client queues, and the Host home sent in the opening { type: 'ready', clientId, host: { home } } frame. Its source factory attaches incremental listeners synchronously, so the Client publishes the generation and starts baseline reads only after incremental delivery is ready. A $events stream whose Peer principal is not the owner never receives a scoped waterfall it could answer; it receives a broadcast emit frame, whole and untrimmed, only when ctx.remoteAccess.decide({ principal, action: 'event:', resources }) allows it on the resources the frame's readers name, or on global for a frame without readers; the resources are resolved once per frame, a frame whose resources are invalid reaches no non-owner stream without a decision, and a denied frame is dropped for that stream alone. An $events/result reply from such a Peer fails with gateway/invocation-unavailable.

A disposed Peer is refused before event-stream participation checks. $events and $events/result require peer.interactions === 'answer' for owner and non-owner alike; refusal returns gateway/interactions-forbidden. Only owner Peers receive forwarded waterfalls and may answer; a reply must belong to an event stream owned by the same Peer. hasLiveClient() checks uncancelled owner answering streams; non-owner broadcast streams and bare WebSockets do not count. Cancellation excludes a stream even while its iterator awaits cleanup. This synchronous observation does not guarantee delivery or Client provider readiness.

Client service: ClientRemote (ctx key: remote)

The same ./client import selects standalone Node ESM lib/client.node.js or browser loader factory lib/client.browser.js; the Host package root stays separate. The browser carrier accepts the shell-owned stream origin described by Connection; logical stream framing and lifecycle remain unchanged.

ctx.remote.$mount() validates and registers a generated Host-for-Client contribution, then installs concrete direct and scoped methods for the calling Cordis fiber. Each namespace is a traced remote. child Service and unloads after its last method is withdrawn. Duplicate endpoints, namespace collisions, and Client-supplied fields without strict generated codecs fail before methods become callable.

Each unary call checks positional arity, constructs the descriptor's exact named args, and sends the typed values through ctx.connection.rpc.call('/api', endpoint, ...) without executing Client-side schemas; the Host validates the received wire fields before business invocation. A generated stream method returns a RemoteStreamHandle from dsh-typert-protocol and opens one logical stream when it is called, through an in-process Connection carrier when available, otherwise through the shared Gateway WebSocket. The handle iterates the downlink once. send(item) queues an uplink item, sent after the open frame; end() half-closes the uplink; dispose() sends cancel unless a terminal frame has arrived and ends the iteration quietly. Breaking out of the iteration early disposes the handle; after the downlink terminates send throws and end is ignored. Generated cancellation-aware methods accept a final optional AbortSignal; the Client combines it with the contribution mount lifetime before invoki