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:
2026-09-09 19:55:54 +04:00
co-authored by Claude Opus 5
parent a40585f419
commit 53421d6cb8
171 changed files with 29275 additions and 51 deletions
+110
View File
@@ -0,0 +1,110 @@
// ---------------------------------------------------------------------------
// 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 {
AuthRequiredEvent,
GrabberProgressEvent,
NotifyEvent,
SettingsChangedEvent,
SpeedGlobalEvent,
TaskAddedEvent,
TaskProgressEvent,
TaskRemovedEvent,
TaskStateEvent,
} from './types.js';
/** Payload for each server-to-client notification, keyed by its wire name. */
export interface EventMap {
/**
* A server asked for credentials. The task sits in retry_wait until the client supplies
* them. Credentials travel to the Secret Service, never back through this event and never
* into a log.
*/
"event.auth.required": AuthRequiredEvent;
/**
* Crawl progress for the Site Grabber wizard. done true means the file list in
* grabber.status is final.
*/
"event.grabber.progress": GrabberProgressEvent;
/**
* Something the user should see: a completion, a failure, a queue finishing. The client
* decides between a toast, a tray balloon and a sound; the daemon does not assume a GUI is
* running.
*/
"event.notify": NotifyEvent;
/**
* Settings were written by some client. Carries only the key names; a client re-reads what
* it cares about. The extension watches for capture.* here and re-fetches capture.getRules
* so its rules never lag the daemon's.
*/
"event.settings.changed": SettingsChangedEvent;
/**
* Aggregate throughput for the status bar, the tray tooltip and the extension popup.
* Emitted at 1 Hz even when nothing is active, so a client can tell 'idle' from
* 'disconnected'.
*/
"event.speed.global": SpeedGlobalEvent;
/**
* A task entered the list. summary is always present so a client can insert the row
* without a follow-up download.get.
*/
"event.task.added": TaskAddedEvent;
/**
* Batched byte counters for every active task. Emitted at no more than 4 Hz as one array,
* never one notification per task: at twenty active downloads that is four messages a
* second instead of eighty. Clients apply a row patch and repaint the touched columns;
* rebuilding a model on this event is a bug.
*/
"event.task.progress": TaskProgressEvent;
/** A task left the list. The client deletes the row; there is nothing further to fetch. */
"event.task.removed": TaskRemovedEvent;
/**
* A task changed lifecycle state. Carries the summary so the row can be repainted in full
* without a round trip, and error whenever the new state is failed or retry_wait.
*/
"event.task.state": TaskStateEvent;
}
export type EventName = keyof EventMap;
export type EventPayload<E extends EventName> = EventMap[E];
/**
* Discriminated on `method`: narrowing an incoming notification gives the
* correctly typed params with no cast at the call site.
*/
export type ServerNotification = {
[E in EventName]: { jsonrpc: '2.0'; method: E; params: EventMap[E] };
}[EventName];
export interface EventMeta {
/** Upper bound on emission rate, where the contract sets one. */
readonly maxRateHz: number | null;
}
export const EVENTS: { readonly [E in EventName]: EventMeta } = {
"event.auth.required": { maxRateHz: null },
"event.grabber.progress": { maxRateHz: 4 },
"event.notify": { maxRateHz: null },
"event.settings.changed": { maxRateHz: null },
"event.speed.global": { maxRateHz: 1 },
"event.task.added": { maxRateHz: null },
"event.task.progress": { maxRateHz: 4 },
"event.task.removed": { maxRateHz: null },
"event.task.state": { maxRateHz: null },
} as const;
export const EVENT_NAMES = Object.keys(EVENTS) as EventName[];
export function isEventName(v: unknown): v is EventName {
return typeof v === 'string' && Object.prototype.hasOwnProperty.call(EVENTS, v);
}
+17
View File
@@ -0,0 +1,17 @@
// ---------------------------------------------------------------------------
// 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/.
// ---------------------------------------------------------------------------
export * from './types.js';
export * from './methods.js';
export * from './events.js';
export * from './validate.js';
+395
View File
@@ -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>>;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff