The last contract gap blocking an M1 definition-of-done item: CORE's "401
handled" has no return path without it, and B2a's sibling F2 was accepted in
proto-answers-m1.md but never actually landed.
download.provideAuth {taskId, username, password, save?} -> {ok}, exactly as
proposed there. Privileged and Unix-socket-only: a credential-bearing method
must never be reachable from the browser, which is the other half of the
promise event.auth.required's own description already makes ("never back
through this event, never into a log"). It answers the challenge; it does not
itself resume the task -- the daemon retries with the credential attached and
the ordinary event.task.state reports the task leaving retry_wait, the same
as any other state change.
save only tells the daemon whether to persist the credential in the Secret
Service for next time, or use it for this attempt alone -- it never touches
SQLite or a log either way, in keeping with CLAUDE.md's secrets rule.
Three fixtures: the success path, -32010 for a task that no longer exists
(credentials submitted for it are simply discarded), and -32003 confirming
the extension has no path to this method under any transport.
mockd gets a real handler rather than falling through to the generic fixture
responder: it validates the taskId exists (so the -32010 fixture is
replayable) and actually transitions the task out of retry_wait.
Minor bump, 1.1.0 -> 1.2.0: additive method, no existing type touched.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
410 lines
21 KiB
TypeScript
410 lines
21 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// GENERATED FILE — DO NOT EDIT.
|
|
//
|
|
// Source: contracts/schema/**
|
|
// Generator: contracts/codegen/gen_ts.py
|
|
// Contract: v1.2.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,
|
|
DownloadProvideAuthParams,
|
|
DownloadProvideAuthResult,
|
|
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 };
|
|
/**
|
|
* Answer an event.auth.required challenge. The task sits in retry_wait until this arrives;
|
|
* on success the daemon retries with the credentials attached and the task resumes on its
|
|
* own — this method does not itself start the transfer. Privileged and Unix-socket-only: a
|
|
* credential-bearing method must never be reachable from the browser, which is exactly the
|
|
* boundary event.auth.required's own description draws ('never back through this event,
|
|
* never into a log') — this is the other half of that promise. Credentials are handed to
|
|
* the Secret Service, never to SQLite and never logged; save only tells the daemon whether
|
|
* to persist them there for next time, or use them for this attempt alone.
|
|
*/
|
|
"download.provideAuth": { params: DownloadProvideAuthParams; result: DownloadProvideAuthResult };
|
|
/**
|
|
* 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.provideAuth": { privileged: true, transports: ['uds'], deadlineMs: 5000, errors: [-32003, -32010] },
|
|
"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>>;
|
|
}
|