nonecap 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -89,6 +89,94 @@ interface SolveList {
89
89
  data: Solve[];
90
90
  has_more: boolean;
91
91
  }
92
+ /**
93
+ * What the downstream target did with a solve's token.
94
+ *
95
+ * Only `accepted` and `rejected` count toward the acceptance rate — the other
96
+ * three are recorded but kept out of the denominator, so a client-side outage
97
+ * can't look like a token-quality regression.
98
+ */
99
+ type FeedbackOutcome =
100
+ /** Downstream accepted the token. */
101
+ "accepted"
102
+ /** Downstream rejected the token. */
103
+ | "rejected"
104
+ /** Token was submitted but the verdict couldn't be determined. */
105
+ | "unknown"
106
+ /** Token was never submitted downstream (expired, aborted, deduped). */
107
+ | "unused"
108
+ /** Downstream failed for a non-token reason (network, 5xx, maintenance). */
109
+ | "error";
110
+ /**
111
+ * One verdict to report, for {@link NoneCap.feedback.report} and
112
+ * {@link NoneCap.feedback.reportMany}.
113
+ */
114
+ interface FeedbackReport {
115
+ /** The solve id returned by `solves.create` / `solve()`. Must be your own, `solved` solve. */
116
+ solve_id: string;
117
+ outcome: FeedbackOutcome;
118
+ /**
119
+ * The raw downstream boolean, when your target has one (OSIPTEL's `estado`:
120
+ * `false` = accepted, `true` = rejected). Stored verbatim. The API rejects an
121
+ * item whose `estado` disagrees with its `outcome`.
122
+ */
123
+ estado?: boolean | null;
124
+ /** Freeform downstream reason or code. Truncated to 512 chars server-side. */
125
+ reason?: string | null;
126
+ /** When the verdict happened. Advisory only — the server stamps its own timestamps. */
127
+ reported_at?: string | Date | null;
128
+ }
129
+ /** A recorded feedback resource. */
130
+ interface Feedback {
131
+ object: "feedback";
132
+ solve_id: string;
133
+ outcome: FeedbackOutcome;
134
+ estado: boolean | null;
135
+ reason: string | null;
136
+ reported_at: string | null;
137
+ /** How many times this solve's verdict has been written. 1 on first report. */
138
+ report_count: number;
139
+ created_at: string;
140
+ updated_at: string;
141
+ }
142
+ /** What happened to one item of a batch. */
143
+ type FeedbackStatus =
144
+ /** First report for this solve. */
145
+ "recorded"
146
+ /** Overwrote an earlier report. */
147
+ | "updated"
148
+ /** Identical to what was already stored — no write. */
149
+ | "unchanged"
150
+ /** Rejected; see `error`. */
151
+ | "error";
152
+ /** One item's result, in the same position as the report you sent. */
153
+ interface FeedbackResult {
154
+ solve_id: string;
155
+ status: FeedbackStatus;
156
+ /** Set only when `status === "error"`. Note the bare shape: no `error` wrapper. */
157
+ error: {
158
+ code: ErrorCode;
159
+ message: string;
160
+ param: string | null;
161
+ } | null;
162
+ }
163
+ /**
164
+ * The result of {@link NoneCap.feedback.reportMany}.
165
+ *
166
+ * Items are resolved independently, so one bad solve id never discards the
167
+ * rest — which also means a broken integration reports zero failures at the
168
+ * HTTP level. Check `failed` (or scan `results`), not just the absence of a
169
+ * thrown error.
170
+ */
171
+ interface FeedbackBatch {
172
+ object: "feedback_batch";
173
+ recorded: number;
174
+ updated: number;
175
+ unchanged: number;
176
+ failed: number;
177
+ /** One entry per report you sent, in request order. */
178
+ results: FeedbackResult[];
179
+ }
92
180
  /** Your account, including the current credit balance. */
93
181
  interface Account {
94
182
  object: "account";
@@ -98,7 +186,15 @@ interface Account {
98
186
  created_at: string;
99
187
  }
100
188
  /** Error codes returned in the API's error envelope. */
101
- type ErrorCode = "unauthorized" | "forbidden" | "account_locked" | "insufficient_credits" | "invalid_request" | "validation_error" | "not_found" | "conflict" | "rate_limited" | "ext_daily_limit" | "concurrency_limit_exceeded" | "internal_error" | "pool_exhausted";
189
+ type ErrorCode = "unauthorized" | "forbidden" | "account_locked" | "insufficient_credits" | "invalid_request" | "validation_error" | "not_found" | "conflict" | "rate_limited" | "ext_daily_limit" | "concurrency_limit_exceeded"
190
+ /** hCaptcha is rate-limiting this sitekey; the submit was shed. Honour `Retry-After`. */
191
+ | "sitekey_rate_limited"
192
+ /** The submitting API key hit its own spend cap. */
193
+ | "key_credit_limit_exceeded"
194
+ /** Feedback: the solve is missing, not yours, or never produced a token. */
195
+ | "not_eligible"
196
+ /** Feedback: a first report arrived after the reporting window closed. */
197
+ | "expired_window" | "internal_error" | "pool_exhausted";
102
198
  /** The Stripe-style error envelope every non-2xx response carries. */
103
199
  interface ErrorEnvelope {
104
200
  error: {
@@ -108,6 +204,8 @@ interface ErrorEnvelope {
108
204
  };
109
205
  }
110
206
 
207
+ /** The API caps one `POST /v1/feedback` call at 500 items. */
208
+ declare const FEEDBACK_BATCH_MAX = 500;
111
209
  /** Is this a terminal solve status? */
112
210
  declare function isTerminal(status: SolveStatus): boolean;
113
211
  /** A `fetch` implementation. Defaults to the global `fetch`. */
@@ -138,7 +236,7 @@ interface WaitOptions {
138
236
  /** Abort the request. */
139
237
  signal?: AbortSignal;
140
238
  }
141
- /** Options for {@link NoneCap.solve}. */
239
+ /** Options for {@link NoneCap.solve} and {@link SolveHandle.result}. */
142
240
  interface SolveHelperOptions {
143
241
  /**
144
242
  * Give up after this many milliseconds and throw {@link TimeoutError}.
@@ -148,6 +246,11 @@ interface SolveHelperOptions {
148
246
  /** Abort waiting. */
149
247
  signal?: AbortSignal;
150
248
  }
249
+ /** Options for {@link NoneCap.solves.start}. */
250
+ interface SolveStartOptions {
251
+ /** Abort the submission. */
252
+ signal?: AbortSignal;
253
+ }
151
254
  /**
152
255
  * The NoneCap API client.
153
256
  *
@@ -169,6 +272,22 @@ declare class NoneCap {
169
272
  cancel: (id: string, options?: {
170
273
  signal?: AbortSignal;
171
274
  }) => Promise<Solve>;
275
+ /**
276
+ * Submit a solve and return a {@link SolveHandle} for it.
277
+ *
278
+ * Unlike {@link NoneCap.solve}, this returns as soon as the submission is
279
+ * accepted, so `handle.id` is available immediately. Use the handle to
280
+ * `cancel()` the solve or `await handle.result()` for the finished solve.
281
+ *
282
+ * ```ts
283
+ * const handle = await nc.solves.start({ type: "hcaptcha", sitekey, url });
284
+ * // ...later, e.g. on shutdown:
285
+ * await handle.cancel();
286
+ * // or wait for it:
287
+ * const { token } = await handle.result();
288
+ * ```
289
+ */
290
+ start: (params: SolveCreateParams, options?: SolveStartOptions) => Promise<SolveHandle>;
172
291
  /** Fetch one page of solves. */
173
292
  list: (params?: SolveListParams, options?: {
174
293
  signal?: AbortSignal;
@@ -184,6 +303,50 @@ declare class NoneCap {
184
303
  signal?: AbortSignal;
185
304
  }) => AsyncIterableIterator<Solve>;
186
305
  };
306
+ /**
307
+ * Report what your downstream target did with the tokens we minted.
308
+ *
309
+ * Feedback is free, own-solves-only, and idempotent per solve id: reporting
310
+ * the same solve again corrects the earlier verdict. Only `solved` solves can
311
+ * be reported, and only within the API's reporting window (~30 days from the
312
+ * solve; corrections to an already-reported solve are never re-gated by it).
313
+ */
314
+ readonly feedback: {
315
+ /**
316
+ * Report one solve's downstream outcome, returning the recorded feedback.
317
+ *
318
+ * Unlike {@link NoneCap.feedback.reportMany}, a rejected report throws:
319
+ * {@link NotFoundError} if the solve isn't yours or never minted a token,
320
+ * {@link ValidationError} if a field is wrong or the reporting window closed.
321
+ *
322
+ * ```ts
323
+ * await nc.feedback.report({ solve_id: solve.id, outcome: "rejected", reason: "..." });
324
+ * ```
325
+ */
326
+ report: (report: FeedbackReport, options?: {
327
+ signal?: AbortSignal;
328
+ }) => Promise<Feedback>;
329
+ /**
330
+ * Report many verdicts at once — the path to use at volume. Buffer verdicts
331
+ * as you learn them and flush them here.
332
+ *
333
+ * The call succeeds even when individual items are rejected: each is
334
+ * resolved on its own so one stale id never discards the rest. Check
335
+ * `failed` rather than relying on a thrown error.
336
+ *
337
+ * More than {@link FEEDBACK_BATCH_MAX} reports are split across sequential
338
+ * requests and merged into one result, in request order. An empty array is
339
+ * a no-op that returns an empty batch without a request.
340
+ *
341
+ * ```ts
342
+ * const batch = await nc.feedback.reportMany(buffer);
343
+ * if (batch.failed) console.warn(batch.results.filter((r) => r.status === "error"));
344
+ * ```
345
+ */
346
+ reportMany: (reports: readonly FeedbackReport[], options?: {
347
+ signal?: AbortSignal;
348
+ }) => Promise<FeedbackBatch>;
349
+ };
187
350
  /** Fetch your account, including the current credit balance. */
188
351
  me(options?: {
189
352
  signal?: AbortSignal;
@@ -194,9 +357,58 @@ declare class NoneCap {
194
357
  * Uses the server's long-poll under the hood and keeps polling until the
195
358
  * solve is terminal or `timeout` elapses. Throws {@link SolveFailedError} if
196
359
  * the solve fails/expires/is cancelled, or {@link TimeoutError} on timeout.
360
+ *
361
+ * This is sugar for `(await solves.start(params)).result(options)`. When you
362
+ * need to cancel the solve while it runs, use {@link NoneCap.solves.start}
363
+ * and keep the {@link SolveHandle}.
197
364
  */
198
365
  solve(params: SolveCreateParams, options?: SolveHelperOptions): Promise<Solve>;
199
366
  }
367
+ /**
368
+ * A handle on a started solve, returned by {@link NoneCap.solves.start}.
369
+ *
370
+ * The handle is the cancellable lifecycle object for one solve. It is a plain
371
+ * object, not a promise: `await`ing it does nothing useful. Wait for the result
372
+ * with {@link SolveHandle.result}, and cancel with {@link SolveHandle.cancel}.
373
+ * You do not construct it yourself — `solves.start()` hands you one with `id`
374
+ * already resolved.
375
+ */
376
+ declare class SolveHandle {
377
+ #private;
378
+ /** The id of the started solve, available as soon as `start()` resolved. */
379
+ readonly id: string;
380
+ constructor(client: NoneCap, solve: Solve);
381
+ /** The last-known solve state for this handle. */
382
+ get solve(): Solve;
383
+ /**
384
+ * Long-poll until the solve is terminal, returning the solved solve. Throws
385
+ * {@link SolveFailedError} if it fails/expires/is cancelled, or
386
+ * {@link TimeoutError} on timeout (with `solveId`/`solve` attached so you can
387
+ * still {@link SolveHandle.cancel} it).
388
+ *
389
+ * Memoizes the *terminal* outcome: once the solve actually finishes, every
390
+ * later call returns that same settled result. A {@link TimeoutError} is not
391
+ * memoized — it just means this wait gave up, so a later call (e.g. with a
392
+ * larger `timeout`) resumes polling. Concurrent calls share one in-flight
393
+ * poll, so they never issue duplicate requests. The first caller's
394
+ * `timeout`/`signal` drive the shared poll; a later call starts a fresh one.
395
+ */
396
+ result(options?: SolveHelperOptions): Promise<Solve>;
397
+ /**
398
+ * Cancel the solve. Cancelled solves are never charged, so this is for clean
399
+ * shutdown / freeing slots / stopping early, not cost protection.
400
+ *
401
+ * If the solve already reached a terminal state the API answers 409; that is
402
+ * not an error a caller cares about, so this swallows {@link ConflictError}
403
+ * and returns the current solve state instead. A cancel that lands on a
404
+ * terminal state (cancelled, or whatever the 409-swallow read back) settles
405
+ * this handle's {@link SolveHandle.result}, so any in-flight or later wait
406
+ * stops polling and reflects that terminal state.
407
+ */
408
+ cancel(options?: {
409
+ signal?: AbortSignal;
410
+ }): Promise<Solve>;
411
+ }
200
412
 
201
413
  /**
202
414
  * Base class for every error this library throws. Catch `NoneCapError` to
@@ -251,8 +463,24 @@ declare class SolveFailedError extends NoneCapError {
251
463
  readonly solve: Solve;
252
464
  constructor(solve: Solve);
253
465
  }
254
- /** Thrown by {@link NoneCap.solve} when the client-side timeout elapses first. */
466
+ /**
467
+ * Thrown by {@link NoneCap.solve} (and {@link SolveHandle.result}) when the
468
+ * client-side timeout elapses first. When the timeout came from waiting on a
469
+ * solve, `solveId` and the last-known `solve` are attached so you can still
470
+ * cancel the in-flight solve. They are `undefined` for transport-level timeouts.
471
+ */
255
472
  declare class TimeoutError extends NoneCapError {
473
+ /** The id of the in-flight solve, when the timeout came from a solve wait. */
474
+ readonly solveId: string | undefined;
475
+ /** The last-known solve state, so you can cancel it after a wait timed out. */
476
+ readonly solve: Solve | undefined;
477
+ constructor(message: string, opts?: {
478
+ code?: ErrorCode;
479
+ status?: number;
480
+ param?: string | null;
481
+ solveId?: string;
482
+ solve?: Solve;
483
+ });
256
484
  }
257
485
 
258
- export { APIError, type Account, AuthenticationError, ConflictError, ConnectionError, type ErrorCode, type ErrorEnvelope, type FetchLike, InsufficientCreditsError, NoneCap, NoneCapError, type NoneCapOptions, NotFoundError, PermissionError, type Proxy, RateLimitError, type Solve, type SolveCreateParams, type SolveError, SolveFailedError, type SolveHelperOptions, type SolveList, type SolveListParams, type SolveStatus, type SolveType, TimeoutError, ValidationError, type WaitOptions, isTerminal };
486
+ export { APIError, type Account, AuthenticationError, ConflictError, ConnectionError, type ErrorCode, type ErrorEnvelope, FEEDBACK_BATCH_MAX, type Feedback, type FeedbackBatch, type FeedbackOutcome, type FeedbackReport, type FeedbackResult, type FeedbackStatus, type FetchLike, InsufficientCreditsError, NoneCap, NoneCapError, type NoneCapOptions, NotFoundError, PermissionError, type Proxy, RateLimitError, type Solve, type SolveCreateParams, type SolveError, SolveFailedError, SolveHandle, type SolveHelperOptions, type SolveList, type SolveListParams, type SolveStartOptions, type SolveStatus, type SolveType, TimeoutError, ValidationError, type WaitOptions, isTerminal };
package/dist/index.d.ts CHANGED
@@ -89,6 +89,94 @@ interface SolveList {
89
89
  data: Solve[];
90
90
  has_more: boolean;
91
91
  }
92
+ /**
93
+ * What the downstream target did with a solve's token.
94
+ *
95
+ * Only `accepted` and `rejected` count toward the acceptance rate — the other
96
+ * three are recorded but kept out of the denominator, so a client-side outage
97
+ * can't look like a token-quality regression.
98
+ */
99
+ type FeedbackOutcome =
100
+ /** Downstream accepted the token. */
101
+ "accepted"
102
+ /** Downstream rejected the token. */
103
+ | "rejected"
104
+ /** Token was submitted but the verdict couldn't be determined. */
105
+ | "unknown"
106
+ /** Token was never submitted downstream (expired, aborted, deduped). */
107
+ | "unused"
108
+ /** Downstream failed for a non-token reason (network, 5xx, maintenance). */
109
+ | "error";
110
+ /**
111
+ * One verdict to report, for {@link NoneCap.feedback.report} and
112
+ * {@link NoneCap.feedback.reportMany}.
113
+ */
114
+ interface FeedbackReport {
115
+ /** The solve id returned by `solves.create` / `solve()`. Must be your own, `solved` solve. */
116
+ solve_id: string;
117
+ outcome: FeedbackOutcome;
118
+ /**
119
+ * The raw downstream boolean, when your target has one (OSIPTEL's `estado`:
120
+ * `false` = accepted, `true` = rejected). Stored verbatim. The API rejects an
121
+ * item whose `estado` disagrees with its `outcome`.
122
+ */
123
+ estado?: boolean | null;
124
+ /** Freeform downstream reason or code. Truncated to 512 chars server-side. */
125
+ reason?: string | null;
126
+ /** When the verdict happened. Advisory only — the server stamps its own timestamps. */
127
+ reported_at?: string | Date | null;
128
+ }
129
+ /** A recorded feedback resource. */
130
+ interface Feedback {
131
+ object: "feedback";
132
+ solve_id: string;
133
+ outcome: FeedbackOutcome;
134
+ estado: boolean | null;
135
+ reason: string | null;
136
+ reported_at: string | null;
137
+ /** How many times this solve's verdict has been written. 1 on first report. */
138
+ report_count: number;
139
+ created_at: string;
140
+ updated_at: string;
141
+ }
142
+ /** What happened to one item of a batch. */
143
+ type FeedbackStatus =
144
+ /** First report for this solve. */
145
+ "recorded"
146
+ /** Overwrote an earlier report. */
147
+ | "updated"
148
+ /** Identical to what was already stored — no write. */
149
+ | "unchanged"
150
+ /** Rejected; see `error`. */
151
+ | "error";
152
+ /** One item's result, in the same position as the report you sent. */
153
+ interface FeedbackResult {
154
+ solve_id: string;
155
+ status: FeedbackStatus;
156
+ /** Set only when `status === "error"`. Note the bare shape: no `error` wrapper. */
157
+ error: {
158
+ code: ErrorCode;
159
+ message: string;
160
+ param: string | null;
161
+ } | null;
162
+ }
163
+ /**
164
+ * The result of {@link NoneCap.feedback.reportMany}.
165
+ *
166
+ * Items are resolved independently, so one bad solve id never discards the
167
+ * rest — which also means a broken integration reports zero failures at the
168
+ * HTTP level. Check `failed` (or scan `results`), not just the absence of a
169
+ * thrown error.
170
+ */
171
+ interface FeedbackBatch {
172
+ object: "feedback_batch";
173
+ recorded: number;
174
+ updated: number;
175
+ unchanged: number;
176
+ failed: number;
177
+ /** One entry per report you sent, in request order. */
178
+ results: FeedbackResult[];
179
+ }
92
180
  /** Your account, including the current credit balance. */
93
181
  interface Account {
94
182
  object: "account";
@@ -98,7 +186,15 @@ interface Account {
98
186
  created_at: string;
99
187
  }
100
188
  /** Error codes returned in the API's error envelope. */
101
- type ErrorCode = "unauthorized" | "forbidden" | "account_locked" | "insufficient_credits" | "invalid_request" | "validation_error" | "not_found" | "conflict" | "rate_limited" | "ext_daily_limit" | "concurrency_limit_exceeded" | "internal_error" | "pool_exhausted";
189
+ type ErrorCode = "unauthorized" | "forbidden" | "account_locked" | "insufficient_credits" | "invalid_request" | "validation_error" | "not_found" | "conflict" | "rate_limited" | "ext_daily_limit" | "concurrency_limit_exceeded"
190
+ /** hCaptcha is rate-limiting this sitekey; the submit was shed. Honour `Retry-After`. */
191
+ | "sitekey_rate_limited"
192
+ /** The submitting API key hit its own spend cap. */
193
+ | "key_credit_limit_exceeded"
194
+ /** Feedback: the solve is missing, not yours, or never produced a token. */
195
+ | "not_eligible"
196
+ /** Feedback: a first report arrived after the reporting window closed. */
197
+ | "expired_window" | "internal_error" | "pool_exhausted";
102
198
  /** The Stripe-style error envelope every non-2xx response carries. */
103
199
  interface ErrorEnvelope {
104
200
  error: {
@@ -108,6 +204,8 @@ interface ErrorEnvelope {
108
204
  };
109
205
  }
110
206
 
207
+ /** The API caps one `POST /v1/feedback` call at 500 items. */
208
+ declare const FEEDBACK_BATCH_MAX = 500;
111
209
  /** Is this a terminal solve status? */
112
210
  declare function isTerminal(status: SolveStatus): boolean;
113
211
  /** A `fetch` implementation. Defaults to the global `fetch`. */
@@ -138,7 +236,7 @@ interface WaitOptions {
138
236
  /** Abort the request. */
139
237
  signal?: AbortSignal;
140
238
  }
141
- /** Options for {@link NoneCap.solve}. */
239
+ /** Options for {@link NoneCap.solve} and {@link SolveHandle.result}. */
142
240
  interface SolveHelperOptions {
143
241
  /**
144
242
  * Give up after this many milliseconds and throw {@link TimeoutError}.
@@ -148,6 +246,11 @@ interface SolveHelperOptions {
148
246
  /** Abort waiting. */
149
247
  signal?: AbortSignal;
150
248
  }
249
+ /** Options for {@link NoneCap.solves.start}. */
250
+ interface SolveStartOptions {
251
+ /** Abort the submission. */
252
+ signal?: AbortSignal;
253
+ }
151
254
  /**
152
255
  * The NoneCap API client.
153
256
  *
@@ -169,6 +272,22 @@ declare class NoneCap {
169
272
  cancel: (id: string, options?: {
170
273
  signal?: AbortSignal;
171
274
  }) => Promise<Solve>;
275
+ /**
276
+ * Submit a solve and return a {@link SolveHandle} for it.
277
+ *
278
+ * Unlike {@link NoneCap.solve}, this returns as soon as the submission is
279
+ * accepted, so `handle.id` is available immediately. Use the handle to
280
+ * `cancel()` the solve or `await handle.result()` for the finished solve.
281
+ *
282
+ * ```ts
283
+ * const handle = await nc.solves.start({ type: "hcaptcha", sitekey, url });
284
+ * // ...later, e.g. on shutdown:
285
+ * await handle.cancel();
286
+ * // or wait for it:
287
+ * const { token } = await handle.result();
288
+ * ```
289
+ */
290
+ start: (params: SolveCreateParams, options?: SolveStartOptions) => Promise<SolveHandle>;
172
291
  /** Fetch one page of solves. */
173
292
  list: (params?: SolveListParams, options?: {
174
293
  signal?: AbortSignal;
@@ -184,6 +303,50 @@ declare class NoneCap {
184
303
  signal?: AbortSignal;
185
304
  }) => AsyncIterableIterator<Solve>;
186
305
  };
306
+ /**
307
+ * Report what your downstream target did with the tokens we minted.
308
+ *
309
+ * Feedback is free, own-solves-only, and idempotent per solve id: reporting
310
+ * the same solve again corrects the earlier verdict. Only `solved` solves can
311
+ * be reported, and only within the API's reporting window (~30 days from the
312
+ * solve; corrections to an already-reported solve are never re-gated by it).
313
+ */
314
+ readonly feedback: {
315
+ /**
316
+ * Report one solve's downstream outcome, returning the recorded feedback.
317
+ *
318
+ * Unlike {@link NoneCap.feedback.reportMany}, a rejected report throws:
319
+ * {@link NotFoundError} if the solve isn't yours or never minted a token,
320
+ * {@link ValidationError} if a field is wrong or the reporting window closed.
321
+ *
322
+ * ```ts
323
+ * await nc.feedback.report({ solve_id: solve.id, outcome: "rejected", reason: "..." });
324
+ * ```
325
+ */
326
+ report: (report: FeedbackReport, options?: {
327
+ signal?: AbortSignal;
328
+ }) => Promise<Feedback>;
329
+ /**
330
+ * Report many verdicts at once — the path to use at volume. Buffer verdicts
331
+ * as you learn them and flush them here.
332
+ *
333
+ * The call succeeds even when individual items are rejected: each is
334
+ * resolved on its own so one stale id never discards the rest. Check
335
+ * `failed` rather than relying on a thrown error.
336
+ *
337
+ * More than {@link FEEDBACK_BATCH_MAX} reports are split across sequential
338
+ * requests and merged into one result, in request order. An empty array is
339
+ * a no-op that returns an empty batch without a request.
340
+ *
341
+ * ```ts
342
+ * const batch = await nc.feedback.reportMany(buffer);
343
+ * if (batch.failed) console.warn(batch.results.filter((r) => r.status === "error"));
344
+ * ```
345
+ */
346
+ reportMany: (reports: readonly FeedbackReport[], options?: {
347
+ signal?: AbortSignal;
348
+ }) => Promise<FeedbackBatch>;
349
+ };
187
350
  /** Fetch your account, including the current credit balance. */
188
351
  me(options?: {
189
352
  signal?: AbortSignal;
@@ -194,9 +357,58 @@ declare class NoneCap {
194
357
  * Uses the server's long-poll under the hood and keeps polling until the
195
358
  * solve is terminal or `timeout` elapses. Throws {@link SolveFailedError} if
196
359
  * the solve fails/expires/is cancelled, or {@link TimeoutError} on timeout.
360
+ *
361
+ * This is sugar for `(await solves.start(params)).result(options)`. When you
362
+ * need to cancel the solve while it runs, use {@link NoneCap.solves.start}
363
+ * and keep the {@link SolveHandle}.
197
364
  */
198
365
  solve(params: SolveCreateParams, options?: SolveHelperOptions): Promise<Solve>;
199
366
  }
367
+ /**
368
+ * A handle on a started solve, returned by {@link NoneCap.solves.start}.
369
+ *
370
+ * The handle is the cancellable lifecycle object for one solve. It is a plain
371
+ * object, not a promise: `await`ing it does nothing useful. Wait for the result
372
+ * with {@link SolveHandle.result}, and cancel with {@link SolveHandle.cancel}.
373
+ * You do not construct it yourself — `solves.start()` hands you one with `id`
374
+ * already resolved.
375
+ */
376
+ declare class SolveHandle {
377
+ #private;
378
+ /** The id of the started solve, available as soon as `start()` resolved. */
379
+ readonly id: string;
380
+ constructor(client: NoneCap, solve: Solve);
381
+ /** The last-known solve state for this handle. */
382
+ get solve(): Solve;
383
+ /**
384
+ * Long-poll until the solve is terminal, returning the solved solve. Throws
385
+ * {@link SolveFailedError} if it fails/expires/is cancelled, or
386
+ * {@link TimeoutError} on timeout (with `solveId`/`solve` attached so you can
387
+ * still {@link SolveHandle.cancel} it).
388
+ *
389
+ * Memoizes the *terminal* outcome: once the solve actually finishes, every
390
+ * later call returns that same settled result. A {@link TimeoutError} is not
391
+ * memoized — it just means this wait gave up, so a later call (e.g. with a
392
+ * larger `timeout`) resumes polling. Concurrent calls share one in-flight
393
+ * poll, so they never issue duplicate requests. The first caller's
394
+ * `timeout`/`signal` drive the shared poll; a later call starts a fresh one.
395
+ */
396
+ result(options?: SolveHelperOptions): Promise<Solve>;
397
+ /**
398
+ * Cancel the solve. Cancelled solves are never charged, so this is for clean
399
+ * shutdown / freeing slots / stopping early, not cost protection.
400
+ *
401
+ * If the solve already reached a terminal state the API answers 409; that is
402
+ * not an error a caller cares about, so this swallows {@link ConflictError}
403
+ * and returns the current solve state instead. A cancel that lands on a
404
+ * terminal state (cancelled, or whatever the 409-swallow read back) settles
405
+ * this handle's {@link SolveHandle.result}, so any in-flight or later wait
406
+ * stops polling and reflects that terminal state.
407
+ */
408
+ cancel(options?: {
409
+ signal?: AbortSignal;
410
+ }): Promise<Solve>;
411
+ }
200
412
 
201
413
  /**
202
414
  * Base class for every error this library throws. Catch `NoneCapError` to
@@ -251,8 +463,24 @@ declare class SolveFailedError extends NoneCapError {
251
463
  readonly solve: Solve;
252
464
  constructor(solve: Solve);
253
465
  }
254
- /** Thrown by {@link NoneCap.solve} when the client-side timeout elapses first. */
466
+ /**
467
+ * Thrown by {@link NoneCap.solve} (and {@link SolveHandle.result}) when the
468
+ * client-side timeout elapses first. When the timeout came from waiting on a
469
+ * solve, `solveId` and the last-known `solve` are attached so you can still
470
+ * cancel the in-flight solve. They are `undefined` for transport-level timeouts.
471
+ */
255
472
  declare class TimeoutError extends NoneCapError {
473
+ /** The id of the in-flight solve, when the timeout came from a solve wait. */
474
+ readonly solveId: string | undefined;
475
+ /** The last-known solve state, so you can cancel it after a wait timed out. */
476
+ readonly solve: Solve | undefined;
477
+ constructor(message: string, opts?: {
478
+ code?: ErrorCode;
479
+ status?: number;
480
+ param?: string | null;
481
+ solveId?: string;
482
+ solve?: Solve;
483
+ });
256
484
  }
257
485
 
258
- export { APIError, type Account, AuthenticationError, ConflictError, ConnectionError, type ErrorCode, type ErrorEnvelope, type FetchLike, InsufficientCreditsError, NoneCap, NoneCapError, type NoneCapOptions, NotFoundError, PermissionError, type Proxy, RateLimitError, type Solve, type SolveCreateParams, type SolveError, SolveFailedError, type SolveHelperOptions, type SolveList, type SolveListParams, type SolveStatus, type SolveType, TimeoutError, ValidationError, type WaitOptions, isTerminal };
486
+ export { APIError, type Account, AuthenticationError, ConflictError, ConnectionError, type ErrorCode, type ErrorEnvelope, FEEDBACK_BATCH_MAX, type Feedback, type FeedbackBatch, type FeedbackOutcome, type FeedbackReport, type FeedbackResult, type FeedbackStatus, type FetchLike, InsufficientCreditsError, NoneCap, NoneCapError, type NoneCapOptions, NotFoundError, PermissionError, type Proxy, RateLimitError, type Solve, type SolveCreateParams, type SolveError, SolveFailedError, SolveHandle, type SolveHelperOptions, type SolveList, type SolveListParams, type SolveStartOptions, type SolveStatus, type SolveType, TimeoutError, ValidationError, type WaitOptions, isTerminal };