proto: freeze the wire contract at 1.0.0
Schemas for the whole v1 surface: 38 methods, 9 events, 25 named types and the
JSON-RPC envelope, with x-privileged / x-transports / x-deadlineMs / x-errors
annotations that both generators emit as data rather than prose.
Four generators over one IR (contracts/codegen/schema_ir.py), so the C++ structs,
the TypeScript types and the OpenRPC document cannot disagree about what the
contract says:
gen_cpp.py -> core/generated/velox_proto.{hpp,cpp}
gen_ts.py -> extension/src/shared/protocol/
gen_openrpc.py -> contracts/openrpc.json
gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp
Inbound parsing never throws: parse<T>() returns std::expected<T, ParseError> and
nlohmann's throwing ADL from_json is deliberately not emitted. Schema constraints
(minimum, maxLength, pattern, ...) become real runtime checks in both languages —
the daemon does not trust the extension and the extension does not trust the
daemon.
59 golden fixtures: a success case per method, 12 error cases, 9 events. Replayed
by tests/conformance/ against both the generated C++ and a live server over both
transports. tools/mockd serves the same fixtures with unhappy-path flags so the
GUI and EXT lanes never wait for veloxd.
run.sh also proves capture.offer fails open: with a daemon answering slower than
750 ms the client gives up and lets Firefox take the download.
core/generated/ is libveloxproto, a separate target from libveloxcore, which
still never sees JSON — see docs/adr/0009.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// GENERATED FILE — DO NOT EDIT.
|
||||
//
|
||||
// Source: contracts/schema/**
|
||||
// Generator: contracts/codegen/gen_ts.py
|
||||
// Contract: v1.0.0
|
||||
//
|
||||
// Hand-editing this file is a merge blocker. Fix the schema and regenerate:
|
||||
// python3 contracts/codegen/gen_ts.py
|
||||
// Only lane PROTO commits to contracts/.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
import type {
|
||||
BulkTaskResult,
|
||||
CaptureGetRulesParams,
|
||||
CaptureOfferParams,
|
||||
CaptureOfferResult,
|
||||
CaptureRules,
|
||||
CategoryListParams,
|
||||
CategoryListResult,
|
||||
CategoryRemoveParams,
|
||||
CategoryRemoveResult,
|
||||
CategoryUpsertParams,
|
||||
CategoryUpsertResult,
|
||||
DownloadAddBatchParams,
|
||||
DownloadAddBatchResult,
|
||||
DownloadAddResult,
|
||||
DownloadCancelParams,
|
||||
DownloadGetParams,
|
||||
DownloadListParams,
|
||||
DownloadListResult,
|
||||
DownloadPauseParams,
|
||||
DownloadProbeParams,
|
||||
DownloadProbeResult,
|
||||
DownloadRefreshUrlParams,
|
||||
DownloadRefreshUrlResult,
|
||||
DownloadRemoveParams,
|
||||
DownloadRemoveResult,
|
||||
DownloadResumeParams,
|
||||
DownloadSpec,
|
||||
DownloadStartParams,
|
||||
DownloadUpdateParams,
|
||||
GrabberHarvestParams,
|
||||
GrabberHarvestResult,
|
||||
GrabberStartParams,
|
||||
GrabberStartResult,
|
||||
GrabberStatusParams,
|
||||
GrabberStatusResult,
|
||||
Limiter,
|
||||
LimiterGetParams,
|
||||
MediaAddVariantParams,
|
||||
MediaAddVariantResult,
|
||||
MediaListVariantsParams,
|
||||
MediaListVariantsResult,
|
||||
QueueListParams,
|
||||
QueueListResult,
|
||||
QueueReorderParams,
|
||||
QueueReorderResult,
|
||||
QueueStartParams,
|
||||
QueueStartResult,
|
||||
QueueStopParams,
|
||||
QueueStopResult,
|
||||
QueueUpsertParams,
|
||||
QueueUpsertResult,
|
||||
RulesListParams,
|
||||
RulesListResult,
|
||||
RulesUpsertParams,
|
||||
RulesUpsertResult,
|
||||
ScheduleGetParams,
|
||||
ScheduleGetResult,
|
||||
ScheduleSetParams,
|
||||
ScheduleSetResult,
|
||||
SessionHelloParams,
|
||||
SessionHelloResult,
|
||||
SessionPairParams,
|
||||
SessionPairResult,
|
||||
SessionSubscribeParams,
|
||||
SessionSubscribeResult,
|
||||
SettingsGetParams,
|
||||
SettingsGetResult,
|
||||
SettingsSetParams,
|
||||
SettingsSetResult,
|
||||
TaskDetail,
|
||||
TaskSummary,
|
||||
} from './types.js';
|
||||
|
||||
/** Params and result for every method, keyed by its wire name. */
|
||||
export interface MethodMap {
|
||||
/**
|
||||
* The daemon's capture policy, so the extension's shouldCapture decision cannot drift from
|
||||
* the daemon's. Fetched on connect and whenever event.settings.changed names a capture.*
|
||||
* key. If this call fails the extension keeps its last known rules and stays fail-open.
|
||||
*/
|
||||
"capture.getRules": { params: CaptureGetRulesParams; result: CaptureRules };
|
||||
/**
|
||||
* Firefox offers an intercepted response to the daemon. The daemon MUST reply within 750
|
||||
* ms; the extension abandons the offer and lets Firefox download normally on timeout. This
|
||||
* deadline is the whole reason capture fails open, and it is conformance-tested: a daemon
|
||||
* that is slow, down, or erroring must never cost the user a download.
|
||||
*/
|
||||
"capture.offer": { params: CaptureOfferParams; result: CaptureOfferResult };
|
||||
/**
|
||||
* Every category with its folder and extension list. The extension calls this to populate
|
||||
* its default-category picker, which is why it is not privileged; it is read-only and
|
||||
* exposes only paths the user already configured.
|
||||
*/
|
||||
"category.list": { params: CategoryListParams; result: CategoryListResult };
|
||||
/**
|
||||
* Delete a user-created category. Built-in categories are refused with -32602. Tasks filed
|
||||
* under it are reassigned to reassignTo, or to the default category when that is null; no
|
||||
* task is ever orphaned.
|
||||
*/
|
||||
"category.remove": { params: CategoryRemoveParams; result: CategoryRemoveResult };
|
||||
/**
|
||||
* Create or replace a category. Omit categoryId to create; supply it to replace. Changing
|
||||
* saveDir does not move existing files — the GUI asks separately and issues
|
||||
* download.update per task, so a re-point is never a surprise mass file move.
|
||||
*/
|
||||
"category.upsert": { params: CategoryUpsertParams; result: CategoryUpsertResult };
|
||||
/**
|
||||
* Create one task. saveDir is canonicalized and checked against saveTo.allowedRoots before
|
||||
* anything is written; a path that escapes them is refused with -32011 and no file is
|
||||
* created.
|
||||
*/
|
||||
"download.add": { params: DownloadSpec; result: DownloadAddResult };
|
||||
/**
|
||||
* Create many tasks in one call: the clipboard blob, the wildcard expander, and the
|
||||
* extension's 'Download all links'. Partial success is normal and is reported per item
|
||||
* rather than failing the whole batch.
|
||||
*/
|
||||
"download.addBatch": { params: DownloadAddBatchParams; result: DownloadAddBatchResult };
|
||||
/**
|
||||
* Stop the given tasks and mark them cancelled. The .veloxpart file is kept so the user
|
||||
* can still resume from the list; download.remove is what deletes bytes.
|
||||
*/
|
||||
"download.cancel": { params: DownloadCancelParams; result: BulkTaskResult };
|
||||
/**
|
||||
* Full detail for one task, including per-segment state. Backs the progress dialog. Poll
|
||||
* it no faster than the progress dialog repaints; the table must use events instead.
|
||||
*/
|
||||
"download.get": { params: DownloadGetParams; result: TaskDetail };
|
||||
/**
|
||||
* The main table. Filtering, sorting and paging all happen in the daemon so the GUI never
|
||||
* materializes 100k rows to show 40. Called once on connect; after that the table is
|
||||
* maintained from events, never re-fetched on a progress tick.
|
||||
*/
|
||||
"download.list": { params: DownloadListParams; result: DownloadListResult };
|
||||
/**
|
||||
* Suspend transfers and flush every segment's progress to the .veloxpart.meta file, so a
|
||||
* pause is indistinguishable from a crash as far as resume is concerned. Never loses bytes
|
||||
* already written.
|
||||
*/
|
||||
"download.pause": { params: DownloadPauseParams; result: BulkTaskResult };
|
||||
/**
|
||||
* Ask what is at a URL without creating a task. Populates the File Info dialog. Runs a
|
||||
* HEAD, falling back to a ranged GET when HEAD is refused, which is also how resumability
|
||||
* is established. Never blocks the RPC loop; the dialog opens immediately and fills in
|
||||
* when this lands.
|
||||
*/
|
||||
"download.probe": { params: DownloadProbeParams; result: DownloadProbeResult };
|
||||
/**
|
||||
* IDM's 'Refresh Download Address'. Point an existing task at a freshly-issued URL when a
|
||||
* signed link has expired, keeping every byte already on disk. The daemon re-probes and
|
||||
* compares size and validator: if they still match, the transfer resumes from where it
|
||||
* stopped; if they do not, it says so rather than silently restarting.
|
||||
*/
|
||||
"download.refreshUrl": { params: DownloadRefreshUrlParams; result: DownloadRefreshUrlResult };
|
||||
/**
|
||||
* Drop tasks from the list, optionally deleting the bytes on disk. Privileged: this is the
|
||||
* only method that destroys user data, and the extension is never allowed to reach it. The
|
||||
* daemon deletes the .veloxpart and .veloxpart.meta pair, and the finished file only when
|
||||
* deleteFile is true.
|
||||
*/
|
||||
"download.remove": { params: DownloadRemoveParams; result: DownloadRemoveResult };
|
||||
/**
|
||||
* Continue paused tasks. Resumption is revalidated with If-Range against the stored ETag
|
||||
* or Last-Modified; a 200 where 206 was expected means the file changed on the server, and
|
||||
* the task moves to failed with a clear error rather than corrupting the part file.
|
||||
*/
|
||||
"download.resume": { params: DownloadResumeParams; result: BulkTaskResult };
|
||||
/**
|
||||
* Begin or restart the given tasks. A task in 'queued' jumps its queue; a task already
|
||||
* downloading is a no-op reported as changed false.
|
||||
*/
|
||||
"download.start": { params: DownloadStartParams; result: BulkTaskResult };
|
||||
/**
|
||||
* Change a task's mutable fields. Moving saveDir or filename moves the file on disk in the
|
||||
* same operation, which is what makes dragging a row onto a category work as one RPC.
|
||||
* Privileged: it can name a destination path.
|
||||
*/
|
||||
"download.update": { params: DownloadUpdateParams; result: TaskSummary };
|
||||
/**
|
||||
* Turn selected crawl results into tasks. This is the only grabber call that creates
|
||||
* downloads, and it names exactly the files the user ticked — a crawl never starts a
|
||||
* download on its own.
|
||||
*/
|
||||
"grabber.harvest": { params: GrabberHarvestParams; result: GrabberHarvestResult };
|
||||
/**
|
||||
* Start a depth-limited crawl. Nothing is downloaded by this call: it only walks pages and
|
||||
* collects candidate links, which the wizard then shows for selection. Privileged because
|
||||
* an unbounded crawl is a resource commitment the browser must not be able to make on the
|
||||
* user's behalf.
|
||||
*/
|
||||
"grabber.start": { params: GrabberStartParams; result: GrabberStartResult };
|
||||
/**
|
||||
* Poll one crawl. Also delivered as event.grabber.progress; the poll exists so the wizard
|
||||
* can be reopened on a job it did not start and still catch up.
|
||||
*/
|
||||
"grabber.status": { params: GrabberStatusParams; result: GrabberStatusResult };
|
||||
/**
|
||||
* Current global speed limit. Privileged: changing or reading the limiter belongs to the
|
||||
* GUI and CLI; the extension shows throughput from event.speed.global instead.
|
||||
*/
|
||||
"limiter.get": { params: LimiterGetParams; result: Limiter };
|
||||
/**
|
||||
* Set the global token-bucket limit. With applyToRunning true the change re-tunes
|
||||
* transfers already in flight instead of taking effect only on the next task — the Speed
|
||||
* Limiter window's 'apply now' button.
|
||||
*/
|
||||
"limiter.set": { params: Limiter; result: Limiter };
|
||||
/**
|
||||
* Turn one enumerated variant into a task. The daemon fetches the segments in parallel and
|
||||
* muxes them with ffmpeg; the result is an ordinary task that appears in the list like any
|
||||
* other download. Refused with -32602 when the variant is DRM-protected.
|
||||
*/
|
||||
"media.addVariant": { params: MediaAddVariantParams; result: MediaAddVariantResult };
|
||||
/**
|
||||
* Parse an HLS or DASH manifest in the daemon and enumerate its renditions. The extension
|
||||
* never parses a manifest — that logic lives in one language, in one place. Variants with
|
||||
* drm true are reported so the UI can grey them out; DRM-protected streams are refused,
|
||||
* not attempted.
|
||||
*/
|
||||
"media.listVariants": { params: MediaListVariantsParams; result: MediaListVariantsResult };
|
||||
/**
|
||||
* Every queue with its run state and ordering. Not privileged: the extension's 'Add to
|
||||
* Queue' picker needs it.
|
||||
*/
|
||||
"queue.list": { params: QueueListParams; result: QueueListResult };
|
||||
/**
|
||||
* Rewrite a queue's run order. taskIds must be a permutation of the queue's current
|
||||
* membership; anything else is -32602 rather than a partial reorder, so a stale drag from
|
||||
* an out-of-date view cannot quietly reshuffle the queue.
|
||||
*/
|
||||
"queue.reorder": { params: QueueReorderParams; result: QueueReorderResult };
|
||||
/**
|
||||
* Start a queue running. The scheduler then admits up to maxConcurrent tasks from it, in
|
||||
* order, and keeps that many running until the queue drains or is stopped.
|
||||
*/
|
||||
"queue.start": { params: QueueStartParams; result: QueueStartResult };
|
||||
/**
|
||||
* Stop admitting new tasks from a queue. Tasks already running are paused when
|
||||
* pauseRunning is true, and otherwise allowed to finish — the difference between 'stop the
|
||||
* queue' and 'stop everything', which IDM conflates and users trip over.
|
||||
*/
|
||||
"queue.stop": { params: QueueStopParams; result: QueueStopResult };
|
||||
/**
|
||||
* Create or replace a queue, including its schedule and concurrency cap. Omit queueId to
|
||||
* create. taskIds in the payload is ignored — membership changes through download.update
|
||||
* and queue.reorder so that two clients editing at once cannot silently drop a task.
|
||||
*/
|
||||
"queue.upsert": { params: QueueUpsertParams; result: QueueUpsertResult };
|
||||
/**
|
||||
* The rules engine's table, in priority order. Privileged: these are the daemon's routing
|
||||
* policy. The extension gets its own narrowed view through capture.getRules instead.
|
||||
*/
|
||||
"rules.list": { params: RulesListParams; result: RulesListResult };
|
||||
/**
|
||||
* Create, replace, or delete rules in one atomic write. 'upsert' carries the rules to
|
||||
* store and 'remove' the ruleIds to drop; applying both at once means a reprioritisation
|
||||
* never leaves the table in a half-valid state.
|
||||
*/
|
||||
"rules.upsert": { params: RulesUpsertParams; result: RulesUpsertResult };
|
||||
/**
|
||||
* The schedule for one queue, or every schedule when queueId is null. Backs the Scheduler
|
||||
* window.
|
||||
*/
|
||||
"schedule.get": { params: ScheduleGetParams; result: ScheduleGetResult };
|
||||
/**
|
||||
* Set or clear a queue's schedule. A null schedule clears it and leaves the queue under
|
||||
* manual control. Times are local wall-clock and are re-evaluated on a DST change rather
|
||||
* than being resolved to absolute instants at set time.
|
||||
*/
|
||||
"schedule.set": { params: ScheduleSetParams; result: ScheduleSetResult };
|
||||
/**
|
||||
* First call on every connection, on every transport. The daemon compares protocolVersion
|
||||
* majors and refuses a mismatch with -32001 so a stale GUI or extension fails loudly on
|
||||
* connect instead of subtly at the tenth field. On the WebSocket transport a valid token
|
||||
* is required unless the client is about to call session.pair.
|
||||
*/
|
||||
"session.hello": { params: SessionHelloParams; result: SessionHelloResult };
|
||||
/**
|
||||
* WebSocket transport only. Triggers a GUI or desktop-notification prompt showing a
|
||||
* four-digit code; the user must approve before a token is issued. Failed attempts are
|
||||
* rate-limited to 5/min followed by a 60 s lockout (-32014) so a token cannot be
|
||||
* brute-forced by another local process. The daemon stores only a hash of the token.
|
||||
*/
|
||||
"session.pair": { params: SessionPairParams; result: SessionPairResult };
|
||||
/**
|
||||
* Choose which notifications this connection receives. Subscribing replaces the previous
|
||||
* selection rather than adding to it, so a client can narrow its firehose without
|
||||
* reconnecting. Nothing is delivered until this is called.
|
||||
*/
|
||||
"session.subscribe": { params: SessionSubscribeParams; result: SessionSubscribeResult };
|
||||
/**
|
||||
* Read settings. keys null means everything. Privileged: the settings bag names local
|
||||
* filesystem paths and the allowed write roots, which the extension has no business
|
||||
* enumerating — it gets capture.getRules instead.
|
||||
*/
|
||||
"settings.get": { params: SettingsGetParams; result: SettingsGetResult };
|
||||
/**
|
||||
* Write settings. Only the keys present in values change. Rejected with -32602 if a key is
|
||||
* unknown or a value fails the Settings schema, and with -32011 if a directory key names a
|
||||
* path that cannot be written. Emits event.settings.changed with exactly the keys that
|
||||
* took effect.
|
||||
*/
|
||||
"settings.set": { params: SettingsSetParams; result: SettingsSetResult };
|
||||
}
|
||||
|
||||
export type MethodName = keyof MethodMap;
|
||||
export type Params<M extends MethodName> = MethodMap[M]['params'];
|
||||
export type Result<M extends MethodName> = MethodMap[M]['result'];
|
||||
|
||||
export type Transport = 'uds' | 'ws';
|
||||
|
||||
export interface MethodMeta {
|
||||
/** Refused over the WebSocket transport with -32003. */
|
||||
readonly privileged: boolean;
|
||||
readonly transports: readonly Transport[];
|
||||
/** How long a client waits before giving up on this call. */
|
||||
readonly deadlineMs: number;
|
||||
/** Error codes this method is documented to return. */
|
||||
readonly errors: readonly number[];
|
||||
}
|
||||
|
||||
export const METHODS: { readonly [M in MethodName]: MethodMeta } = {
|
||||
"capture.getRules": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 2000, errors: [] },
|
||||
"capture.offer": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 750, errors: [-32011] },
|
||||
"category.list": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 2000, errors: [] },
|
||||
"category.remove": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003, -32602] },
|
||||
"category.upsert": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003, -32011] },
|
||||
"download.add": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 5000, errors: [-32011, -32012, -32013] },
|
||||
"download.addBatch": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 30000, errors: [-32011, -32012] },
|
||||
"download.cancel": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 5000, errors: [-32010] },
|
||||
"download.get": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 5000, errors: [-32010] },
|
||||
"download.list": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 5000, errors: [] },
|
||||
"download.pause": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 5000, errors: [-32010] },
|
||||
"download.probe": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 30000, errors: [-32013] },
|
||||
"download.refreshUrl": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 30000, errors: [-32010, -32013] },
|
||||
"download.remove": { privileged: true, transports: ['uds'], deadlineMs: 10000, errors: [-32003, -32010] },
|
||||
"download.resume": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 5000, errors: [-32010] },
|
||||
"download.start": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 5000, errors: [-32010] },
|
||||
"download.update": { privileged: true, transports: ['uds'], deadlineMs: 30000, errors: [-32003, -32010, -32011] },
|
||||
"grabber.harvest": { privileged: true, transports: ['uds'], deadlineMs: 30000, errors: [-32003, -32011] },
|
||||
"grabber.start": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003] },
|
||||
"grabber.status": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003, -32602] },
|
||||
"limiter.get": { privileged: true, transports: ['uds'], deadlineMs: 2000, errors: [-32003] },
|
||||
"limiter.set": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003, -32602] },
|
||||
"media.addVariant": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 30000, errors: [-32011, -32602, -32013] },
|
||||
"media.listVariants": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 30000, errors: [-32013] },
|
||||
"queue.list": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 2000, errors: [] },
|
||||
"queue.reorder": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003, -32602] },
|
||||
"queue.start": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003] },
|
||||
"queue.stop": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003] },
|
||||
"queue.upsert": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003] },
|
||||
"rules.list": { privileged: true, transports: ['uds'], deadlineMs: 2000, errors: [-32003] },
|
||||
"rules.upsert": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003] },
|
||||
"schedule.get": { privileged: true, transports: ['uds'], deadlineMs: 2000, errors: [-32003] },
|
||||
"schedule.set": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003, -32602] },
|
||||
"session.hello": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 2000, errors: [-32001, -32002] },
|
||||
"session.pair": { privileged: false, transports: ['ws'], deadlineMs: 120000, errors: [-32003, -32014] },
|
||||
"session.subscribe": { privileged: false, transports: ['uds', 'ws'], deadlineMs: 2000, errors: [] },
|
||||
"settings.get": { privileged: true, transports: ['uds'], deadlineMs: 2000, errors: [-32003] },
|
||||
"settings.set": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003, -32602, -32011] },
|
||||
} as const;
|
||||
|
||||
export const METHOD_NAMES = Object.keys(METHODS) as MethodName[];
|
||||
|
||||
export function isMethodName(v: unknown): v is MethodName {
|
||||
return typeof v === 'string' && Object.prototype.hasOwnProperty.call(METHODS, v);
|
||||
}
|
||||
|
||||
/** Methods this transport may call. The extension checks before sending so a
|
||||
* privileged call fails in one place rather than as a puzzling -32003. */
|
||||
export function isAllowedOn(method: MethodName, transport: Transport): boolean {
|
||||
return (METHODS[method].transports as readonly string[]).includes(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* The typed client surface. Every transport implements this; the generated
|
||||
* signature is what stops a caller passing download.add's params to download.get.
|
||||
*/
|
||||
export interface VeloxClient {
|
||||
call<M extends MethodName>(method: M, params: Params<M>): Promise<Result<M>>;
|
||||
}
|
||||
Reference in New Issue
Block a user