@nohead/sdk 0.7.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -5,6 +5,102 @@ Changes to `@nohead/sdk` that you can notice. Versions follow
5
5
  releases; a change that could break your code is a major one. Each release's
6
6
  section is its GitHub release's notes.
7
7
 
8
+ ## 0.9.0
9
+
10
+ - **Breaking:** the client no longer reads `NOHEAD_API_URL`: a stray value
11
+ sent requests, with the API key, to another host. The base URL is
12
+ `baseUrl`, else `https://api.nohead.io`; pass `baseUrl` to use another
13
+ API, as in `new Nohead({ baseUrl: process.env.NOHEAD_API_URL })`.
14
+
15
+ - An `APIError`'s `message` names the status, the error type and the
16
+ request ID along with the API's message, as in the Python and Ruby SDKs:
17
+ `422 validation_error: Title is required (request req_…)`, where it was
18
+ only `Title is required`. The API's own text is `apiMessage`.
19
+
20
+ - `unwrapWebhook` and `webhooks.unwrap` throw a `WebhookVerificationError`
21
+ for an empty secret (`""` or `"whsec_"`, as from an unset environment
22
+ variable), not the platform's `DOMException`.
23
+
24
+ - `DEFAULT_BASE_URL` is exported: the API's address (`https://api.nohead.io`)
25
+ that a client uses when `baseUrl` isn't set.
26
+
27
+ - `timeout` is now the longest an attempt may go without progress: to get
28
+ the response, then between chunks of its body. A large response that keeps
29
+ arriving no longer times out; one that stalls still does. An upload's bytes
30
+ get as long as sending them at 64 KiB/s takes, if that is longer.
31
+
32
+ - `withOptions({ timeout, maxRetries, headers, onRetry })` copies a client
33
+ with other settings; the copy shares the key's project lookup. `headers`
34
+ are added to the client's.
35
+
36
+ - Redirects are no longer followed, which could send the API key to another
37
+ host or a write as a GET: a `3xx` throws an `APIError` with `type`
38
+ `redirect` and the message
39
+ `301 redirect: The API answered with a redirect to <Location>; check the base URL`.
40
+ A redirect from storage fails an upload with `UploadError`.
41
+
42
+ - Headers are matched by name in any case, and each is sent once. A call's
43
+ `headers` replace the client's, which replace the SDK's own; the headers
44
+ the request options set (`Idempotency-Key`, `If-Match`,
45
+ `Nohead-Change-Note`, `Content-Type`) always win. A client header
46
+ `authorization` was sent along with the SDK's `Authorization`.
47
+
48
+ - Empty options count as unset: `apiKey`, `baseUrl` and `projectId` of `""`
49
+ fall back to `NOHEAD_API_KEY` and the defaults, an `idempotencyKey` of
50
+ `""` gets a generated key, an upload's `filename` and `content_type` of
51
+ `""` their defaults, and `imageUrl`'s empty options are left out.
52
+
53
+ - A `baseUrl` that isn't an `http` or `https` URL throws a `NoheadError`
54
+ when the client is made, not on its first request.
55
+
56
+ - A path value of `.` or `..` (`records.get("..")`) throws a `NoheadError`,
57
+ `Invalid record_id: ".."`, before anything is sent: URLs resolve them.
58
+
59
+ - `ifMatch` that isn't a revision number, or an object with one, throws a
60
+ `TypeError` before anything is sent, where an object without one sent
61
+ `If-Match: "undefined"`.
62
+
63
+ - `NaN` and `Infinity` in a body throw a `TypeError`, where they were sent as
64
+ `null`.
65
+
66
+ - A list whose response isn't the API's list (a proxy's page, say) throws a
67
+ `NoheadError`, `The API's response could not be read (HTTP 200)`, not a
68
+ `TypeError` or a page without items.
69
+
70
+ - `Retry-After` is read as a number of seconds (`120`, `1.5`) or an HTTP
71
+ date, in all three of its forms, in GMT. Anything else (`1e3`, `0x10`,
72
+ `-1`, an ISO 8601 time) is ignored and the usual backoff applies.
73
+
74
+ - An error's details that aren't objects are kept, as
75
+ `{ code: "unknown", message: <the value as text> }`, rather than dropped.
76
+
77
+ - `assets.upload` sends a stream with `Content-Length: byte_size`, which the
78
+ upload URL is signed for; storage could refuse it without one. Given a string,
79
+ it throws a `TypeError`: pass `await fs.openAsBlob(path)` for a file. Its
80
+ `UploadError` messages name the filename you passed (or `upload`).
81
+
82
+ - `unwrapWebhook` and `webhooks.unwrap` take an unset `secret` (`undefined`,
83
+ as from a missing environment variable) and throw `WebhookVerificationError`
84
+ `The webhook secret is empty`, not a `TypeError`.
85
+
86
+ - `imageUrl` throws a `TypeError` for an unknown `fit`
87
+ (`fit must be one of contain, cover, fill`), where it wrote `rs:undefined`
88
+ into the URL.
89
+
90
+ ## 0.8.0
91
+
92
+ - `Asset` has `file_url`, the file as uploaded, which downloads under its
93
+ filename, and for video and audio `media_url`, which plays it inline (in
94
+ `<video>` and `<audio>`, with range requests). Both are open, never expire
95
+ and are cached on a CDN, like `image_url`.
96
+
97
+ - `assets.downloadUrl` is removed, with the API's
98
+ `GET /v1/assets/{asset_id}/download-url` and its 2-minute links: use the
99
+ asset's `file_url`.
100
+
101
+ - An upload larger than the plan allows (10 MB a file on Free, 100 MB on paid
102
+ plans) is refused with a `PlanLimitExceededError` when it starts.
103
+
8
104
  ## 0.7.0
9
105
 
10
106
  - `assets.list({ q, kind })`: `q` finds assets whose filename or alt text
package/README.md CHANGED
@@ -40,28 +40,39 @@ for await (const post of nohead.records.list("posts", {
40
40
  npm install @nohead/sdk
41
41
  ```
42
42
 
43
- The package is ESM; Node 22+ can also `require()` it. It runs on Node 22, 24 and 26, Bun, Deno, Cloudflare Workers and other runtimes with `fetch` and Web Crypto. Use it on servers: API keys are secrets.
43
+ The package is ESM; Node 22+ can also `require()` it. It runs on Node 22, 24 and 26, Bun, Deno, Cloudflare Workers and other runtimes with `fetch`, Web Crypto and ES2024. Use it on servers: API keys are secrets.
44
+
45
+ Until 1.0, any release may break your code: pin a version, and read the changelog's **Breaking** entries before upgrading.
44
46
 
45
47
  ## Configuration
46
48
 
47
49
  ```ts
48
50
  const nohead = new Nohead({
49
- apiKey: process.env.NOHEAD_API_KEY, // default: NOHEAD_API_KEY
50
- baseUrl: "https://api.nohead.io", // default: NOHEAD_API_URL, else production
51
+ apiKey: process.env.NOHEAD_API_KEY, // the default
52
+ timeout: 10_000,
53
+ maxRetries: 3,
51
54
  })
52
55
  ```
53
56
 
54
- | Option | Default | |
55
- | ------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------- |
56
- | `apiKey` | `NOHEAD_API_KEY` | A project API key (`nohead_sk_…`, or `nohead_pk_…` when published-only). Required. |
57
- | `baseUrl` | `NOHEAD_API_URL`, else `https://api.nohead.io` | |
58
- | `projectId` | the key's project | Looked up once with `GET /v1/me` when omitted. |
59
- | `maxRetries` | `2` | See [retries](#retries-and-idempotency). |
60
- | `timeout` | `60000` | Milliseconds per attempt. |
61
- | `fetch` | `globalThis.fetch` | For proxies, instrumentation and tests. |
62
- | `headers` | `{}` | Added to every request. |
63
- | `warnings` | `true` | Logs deprecation and plan usage warnings, once each. |
64
- | `onRetry` | | Called before each retry. See [retries](#retries-and-idempotency). |
57
+ | Option | Default | |
58
+ | ------------ | ----------------------- | ----------------------------------------------------------------------------------------- |
59
+ | `apiKey` | `NOHEAD_API_KEY` | A project API key (`nohead_sk_…`, or `nohead_pk_…` when published-only). Required. |
60
+ | `baseUrl` | `https://api.nohead.io` | For a proxy, or a mock server in tests. |
61
+ | `projectId` | the key's project | Looked up once with `GET /v1/me` when omitted. |
62
+ | `maxRetries` | `2` | See [retries](#retries-and-idempotency). |
63
+ | `timeout` | `60000` | Milliseconds an attempt may go without progress. See [retries](#retries-and-idempotency). |
64
+ | `fetch` | `globalThis.fetch` | For proxies, instrumentation and tests. |
65
+ | `headers` | `{}` | Added to every request. |
66
+ | `warnings` | `true` | Logs deprecation and plan usage warnings, once each. |
67
+ | `onRetry` | | Called before each retry. See [retries](#retries-and-idempotency). |
68
+
69
+ An empty option counts as unset, as an empty environment variable does: `apiKey: ""` falls back to `NOHEAD_API_KEY`, and `baseUrl: ""` to `https://api.nohead.io`. The base URL comes only from `baseUrl`, never from the environment. A `baseUrl` that isn't an `http` or `https` URL throws.
70
+
71
+ `withOptions` copies a client with another `timeout`, `maxRetries` or `onRetry`, or more `headers` (added to the client's). The copy shares the key's project, so it doesn't look it up again:
72
+
73
+ ```ts
74
+ const patient = nohead.withOptions({ timeout: 120_000, maxRetries: 5 })
75
+ ```
65
76
 
66
77
  API keys belong to a project, so methods like `collections.list()` need no project ID. Collections can be named by ID or slug everywhere.
67
78
 
@@ -190,6 +201,10 @@ Every error is a `NoheadError`. API errors are `APIError`s with `status`, `type`
190
201
  | `InternalServerError` | 500 and other 5xx |
191
202
  | `ServiceUnavailableError` | 503 |
192
203
 
204
+ An API error's `message` names the status, the type and the request ID along with the API's message, so logging it is enough to look the request up: `422 validation_error: Title is required (request req_…)`. The API's own text is `apiMessage`, to show a person.
205
+
206
+ The client follows no redirects: a `3xx` is an `APIError` (`type` `redirect`) that names where it pointed, which usually means the base URL is wrong.
207
+
193
208
  Other errors: `ConnectionError`, `TimeoutError` (a `ConnectionError`), `AbortError` (your `signal` fired), `UploadError` and `WebhookVerificationError`.
194
209
 
195
210
  ```ts
@@ -241,6 +256,10 @@ Every method takes request options as its last argument:
241
256
 
242
257
  Writes that change records or the schema also take `changeNote`, a reason kept with the revision, schema change or migration they make. Record and draft writes take `ifMatch` (see [concurrency](#concurrency)).
243
258
 
259
+ Headers are matched by name in any case, and each is sent once: a call's `headers` replace the client's, which replace the SDK's own (`Authorization`, `User-Agent`…). The headers the request options set (`Idempotency-Key`, `If-Match`, `Nohead-Change-Note`, and `Content-Type` for a body) always win.
260
+
261
+ **Timeouts:** `timeout` is the longest an attempt may go without progress: to connect and get the response, then between chunks of its body. A response that keeps arriving never times out; one that stalls for `timeout` throws `TimeoutError`, and is retried. fetch shows no progress while it sends an upload's bytes, so storage gets as long as sending them at 64 KiB/s takes, when that is longer than `timeout`.
262
+
244
263
  ## Concurrency
245
264
 
246
265
  Pass the revision you read to make sure nobody changed the record since:
@@ -302,11 +321,11 @@ Thumbnails: an image's `preview_url` fits in 640×640, as WebP.
302
321
  2. Sends the bytes straight to storage.
303
322
  3. Completes the upload, which checks the file, and returns the `ready` asset.
304
323
 
305
- **Errors:** `UploadError` if storage refuses the bytes; `ValidationError` if the file fails the checks.
324
+ A string is neither a path nor the content: it throws `TypeError` (use `await openAsBlob(path)` from `node:fs`). A stream is sent with `Content-Length: byte_size`, which the upload URL is signed for.
306
325
 
307
- **Resuming:** pass an `idempotencyKey` (`upload(file, {}, { idempotencyKey })`) and running the same upload again resumes it: an asset that is already `ready` comes back without sending the bytes again. Once the upload's URL has expired (its `upload.expires_at`), it throws `UploadError`: start over with a new key.
326
+ **Errors:** `UploadError` whenever the bytes don't reach storage: `Storage refused the upload of cover.jpg (403)` when it refuses them, with its `status`, or `Could not upload cover.jpg: …` when it can't be reached or times out, with that error as the `cause`. `ValidationError` if the file fails the checks.
308
327
 
309
- **Uploading from a browser:** create the upload on your server with `createUpload`, `PUT` the file from the browser, then `complete` it.
328
+ **Resuming:** pass an `idempotencyKey` (`upload(file, {}, { idempotencyKey })`) and running the same upload again resumes it: an asset that is already `ready` comes back without sending the bytes again. Once the upload's URL has expired (its `upload.expires_at`), it throws `UploadError`: start over with a new key.
310
329
 
311
330
  **Renaming and alt text:** `update` changes an asset's `filename` and `alt` text, the default wherever the asset is used (`expand` returns it with the asset). The file itself never changes.
312
331
 
@@ -372,22 +391,26 @@ Verify a webhook request, then use its event:
372
391
 
373
392
  ```ts
374
393
  // e.g. a Next.js route handler
394
+ import { revalidateTag } from "next/cache"
395
+
375
396
  export async function POST(request: Request) {
376
397
  const event = await nohead.webhooks.unwrap(
377
398
  await request.text(),
378
399
  request.headers,
379
400
  {
380
- secret: process.env.NOHEAD_WEBHOOK_SECRET!,
401
+ secret: process.env.NOHEAD_WEBHOOK_SECRET,
381
402
  }
382
403
  )
383
404
  if (event.type === "record.published" && event.data.record) {
384
- revalidateTag(event.data.record.collection)
405
+ revalidateTag(event.data.record.collection, "max")
385
406
  }
386
407
  return new Response(null, { status: 204 })
387
408
  }
388
409
  ```
389
410
 
390
- Event types are those of the activity log (`record.published`, `record.restored`, `field.updated`, `asset.purged`, …), and a webhook subscribes to types or wildcards (`record.*`, `*`). Narrowing by `type` types `event.data`, and `unwrap<Post>` types the records and drafts in it. `unwrap` checks the signature and the timestamp (Standard Webhooks), and throws `WebhookVerificationError` if either is off. After `rotateSecret`, requests carry a signature for the old secret and the new one for 24 hours, so switch the receiver's secret within that day (or end it sooner with `webhooks.secrets.retire`). Pass the raw body: parsing and re-serializing JSON changes the bytes.
411
+ With `"max"`, Next.js serves the old version to the next request while it fetches the new one; pass `{ expire: 0 }` instead to drop it at once.
412
+
413
+ Event types are those of the activity log (`record.published`, `record.restored`, `field.updated`, `asset.purged`, …), and a webhook subscribes to types or wildcards (`record.*`, `*`). Narrowing by `type` types `event.data`, and `unwrap<Post>` types the records and drafts in it. `unwrap` checks the signature and the timestamp (Standard Webhooks), and throws `WebhookVerificationError` if either is off, or if the secret is empty or unset (`The webhook secret is empty`). After `rotateSecret`, requests carry a signature for the old secret and the new one for 24 hours, so switch the receiver's secret within that day (or end it sooner with `webhooks.secrets.retire`). Pass the raw body: parsing and re-serializing JSON changes the bytes.
391
414
 
392
415
  A receiver that never calls the API can use `unwrapWebhook` from `@nohead/sdk/webhooks` without a client. Events can arrive more than once, so deduplicate by the `webhook-id` header.
393
416
 
@@ -414,7 +437,7 @@ response.headers.get("RateLimit-Remaining")
414
437
  | `collections.searchIndex` | `get`, `rebuild` |
415
438
  | `fields` | `list`, `get`, `create`, `update`, `delete`, `restore`, `order`, `reorder`, `removeAlias` |
416
439
  | `migrations` | `create`, `list`, `get`, `cancel` |
417
- | `assets` | `upload`, `createUpload`, `complete`, `list`, `get`, `update`, `delete`, `restore`, `purge`, `usage`, `downloadUrl` |
440
+ | `assets` | `upload`, `createUpload`, `complete`, `list`, `get`, `update`, `delete`, `restore`, `purge`, `usage` |
418
441
  | `webhooks` | `list`, `get`, `create`, `update`, `delete`, `restore`, `rotateSecret`, `test`, `unwrap` |
419
442
  | `webhooks.secrets` | `list`, `retire` |
420
443
  | `webhooks.deliveries` | `list`, `get`, `retry` |
@@ -430,28 +453,37 @@ The SDK covers every operation an API key can call. Organizations, projects, mem
430
453
  ```bash
431
454
  npm install # also builds dist/
432
455
  npm test # unit and contract tests
433
- npm run lint && npm run typecheck && npm run format:check
456
+ npm run lint && npm run typecheck && npm run typecheck:smoke && npm run format:check
434
457
  npm run generate # after updating openapi.json
435
458
  npm run samples # after changing test/calls.ts (the docs' code samples)
436
459
  NOHEAD_API_URL=http://localhost:3000 NOHEAD_API_KEY=nohead_sk_… npm run smoke
437
460
  ```
438
461
 
462
+ Before pushing, also check that the generated code and the samples are current, as CI does:
463
+
464
+ ```bash
465
+ npm run generate && npm run samples && git diff --exit-code src/generated samples.json
466
+ ```
467
+
468
+ The unit tests' mock client (`mockClient` in `test/helpers.ts`) checks every request to the API, and every reply to one, against `openapi.json`, and fails the test on a mismatch; build replies from the contract's examples (`exampleOf`), and mark one that breaks the contract on purpose with `raw()`.
469
+
439
470
  **How the code is organized:**
440
471
 
441
- - `openapi.json` is the API's published contract. `npm run generate` derives two things from it:
472
+ - `openapi.json` is the API's published contract. `npm run generate` derives three things from it:
442
473
  - the types, in `src/generated/types.gen.ts` (hey-api)
443
474
  - the operation table, in `src/generated/operations.gen.ts`
475
+ - the schema types the SDK exports, in `src/generated/schemas.gen.ts`
444
476
  - The methods in `src/resources` are written by hand.
445
477
  - `test/contract.test.ts` calls every public method. It fails when an API-key operation in the contract has no method, when a query parameter is never sent (no method takes it, or no call passes it), or when a request doesn't match its operation (method, path, query parameters and JSON body, against their schemas). Each call gets an example of its operation's response, built from the contract's schema, and must return it.
446
478
 
447
- The smoke test (`smoke/smoke.ts`) runs the core flow against a real API, using the built package. Nohead's own CI runs it on every API contract change.
479
+ The smoke test (`smoke/smoke.ts`) runs the core flow against a real API, using the built package. The Nohead API repository runs it in its end-to-end tests and before every release.
448
480
 
449
481
  ## Releasing
450
482
 
451
- 1. Bump the version in `package.json` and `src/version.ts`.
483
+ 1. Bump the version in `package.json` and `src/version.ts`, then `npm install --package-lock-only`, so `package-lock.json` records it.
452
484
  2. Add a section for it to `CHANGELOG.md` (`## 1.2.3`), which becomes the release's notes.
453
485
  3. Merge to `main`. Its ruleset requires the **CI passed** check, so the commit goes through a pull request or a branch whose CI passed, and force pushes are refused.
454
- 4. Run the **SDK release** workflow in the Nohead API repository. It runs this commit's smoke test against the API and pushes the tag `v1.2.3`. Nobody else can push `v*` tags: a tag ruleset lets only that workflow's deploy key through.
486
+ 4. Run the **SDK release** workflow in the Nohead API repository. It runs this commit's smoke test against the API and pushes the tag `v1.2.3`. A tag ruleset lets only deploy keys push `v*` tags.
455
487
  5. The tag starts `.github/workflows/release.yml`. A build job checks the tag is on `main` and matches the version and its notes, and packs the package, with no publishing credential. Its publishing job runs in the `release` environment, which only `v*` tags can use, and the registry's trusted publisher accepts only that environment. It stages that package on npm with provenance through trusted publishing (no token), then creates a draft GitHub release.
456
488
  6. Approve the staged version with 2FA, on npmjs.com, or with `npm stage list @nohead/sdk` and `npm stage approve <stage-id>`. Only then is it installable. Publish the draft GitHub release.
457
489
 
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PagePromise } from "./api-promise.ts";
2
- import { type ClientOptions, type RequestOptions } from "./core.ts";
2
+ import { type ClientOptions, type CopyOptions, type RequestOptions } from "./core.ts";
3
3
  import type * as G from "./generated/types.gen.ts";
4
4
  import { Assets } from "./resources/assets.ts";
5
5
  import { Drafts } from "./resources/drafts.ts";
@@ -31,6 +31,14 @@ export declare class Nohead {
31
31
  readonly me: Me;
32
32
  readonly health: Health;
33
33
  constructor(options?: ClientOptions);
34
+ /**
35
+ * A copy of the client with another `timeout`, `maxRetries` or `onRetry`,
36
+ * or more `headers` (added to this client's). It shares the key's project,
37
+ * so it doesn't look it up again.
38
+ *
39
+ * await nohead.withOptions({ timeout: 5_000 }).records.get(id)
40
+ */
41
+ withOptions(options: CopyOptions): Nohead;
34
42
  /**
35
43
  * Full-text search across the key's project (or some of its
36
44
  * collections), most relevant first, through the first 1,000 hits.
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Core } from "./core.js";
1
+ import { Core, } from "./core.js";
2
2
  import { Assets } from "./resources/assets.js";
3
3
  import { Drafts } from "./resources/drafts.js";
4
4
  import { AuditEvents, FeatureFlags, Health, Me } from "./resources/other.js";
@@ -28,7 +28,7 @@ export class Nohead {
28
28
  health;
29
29
  #core;
30
30
  constructor(options = {}) {
31
- const core = new Core(options);
31
+ const core = options[COPY_OF] ?? new Core(options);
32
32
  this.#core = core;
33
33
  this.records = new Records(core);
34
34
  this.revisions = new RevisionsById(core);
@@ -43,6 +43,16 @@ export class Nohead {
43
43
  this.me = new Me(core);
44
44
  this.health = new Health(core);
45
45
  }
46
+ /**
47
+ * A copy of the client with another `timeout`, `maxRetries` or `onRetry`,
48
+ * or more `headers` (added to this client's). It shares the key's project,
49
+ * so it doesn't look it up again.
50
+ *
51
+ * await nohead.withOptions({ timeout: 5_000 }).records.get(id)
52
+ */
53
+ withOptions(options) {
54
+ return new Nohead({ [COPY_OF]: this.#core.withOptions(options) });
55
+ }
46
56
  /**
47
57
  * Full-text search across the key's project (or some of its
48
58
  * collections), most relevant first, through the first 1,000 hits.
@@ -61,3 +71,5 @@ export class Nohead {
61
71
  }, options);
62
72
  }
63
73
  }
74
+ // The core a copy is made with (`withOptions`), out of ClientOptions' sight.
75
+ const COPY_OF = Symbol("copyOf");
package/dist/core.d.ts CHANGED
@@ -9,13 +9,19 @@ export interface ClientOptions {
9
9
  * Defaults to `NOHEAD_API_KEY`.
10
10
  */
11
11
  apiKey?: string;
12
- /** Defaults to `NOHEAD_API_URL`, else `https://api.nohead.io`. */
12
+ /**
13
+ * The API's address. Defaults to `https://api.nohead.io`; the environment
14
+ * isn't read for it, so the key only goes where the code says.
15
+ */
13
16
  baseUrl?: string;
14
17
  /** The key's project. Looked up once with `GET /v1/me` when omitted. */
15
18
  projectId?: string;
16
19
  /** Retries of failed requests (see README, "Retries"). Default 2. */
17
20
  maxRetries?: number;
18
- /** Milliseconds per attempt. Default 60 000. */
21
+ /**
22
+ * Milliseconds an attempt may go without progress (connecting, waiting for
23
+ * the response, then between chunks of its body). Default 60 000.
24
+ */
19
25
  timeout?: number;
20
26
  /** A `fetch` implementation, for proxies, instrumentation or tests. */
21
27
  fetch?: typeof fetch;
@@ -26,6 +32,8 @@ export interface ClientOptions {
26
32
  /** Called before each retry, for example to report it. */
27
33
  onRetry?: (retry: RetryEvent) => void;
28
34
  }
35
+ /** What `withOptions` changes in a client's copy. */
36
+ export type CopyOptions = Pick<ClientOptions, "timeout" | "maxRetries" | "headers" | "onRetry">;
29
37
  /** A retry about to happen (`onRetry`). */
30
38
  export interface RetryEvent {
31
39
  /** 1 for the first retry of the request, 2 for the second… */
@@ -43,7 +51,7 @@ export interface RequestOptions {
43
51
  /** Sent as `Idempotency-Key` on writes. Generated when omitted. */
44
52
  idempotencyKey?: string;
45
53
  signal?: AbortSignal;
46
- /** Milliseconds per attempt, overriding the client's. */
54
+ /** Milliseconds without progress, overriding the client's `timeout`. */
47
55
  timeout?: number;
48
56
  /** Retries, overriding the client's. */
49
57
  maxRetries?: number;
@@ -92,15 +100,24 @@ export interface RequestInput {
92
100
  }
93
101
  export declare class Core {
94
102
  #private;
95
- constructor(options: ClientOptions);
103
+ constructor(options: ClientOptions, project?: {
104
+ id?: Promise<string>;
105
+ });
106
+ /** A copy with other settings, sharing the project lookup. */
107
+ withOptions(options: CopyOptions): Core;
96
108
  /** The API key's project, from `GET /v1/me` the first time. */
97
109
  projectId(): Promise<string>;
98
110
  request<Op extends OperationId>(operation: Op, input?: RequestInput, options?: OptionsOf<Op>): APIPromise<Operations[Op]["response"]>;
99
111
  /** A list operation as pages of `Item`. */
100
112
  paginate<Item, Meta extends ListMeta = ListMeta>(operation: OperationId, input: RequestInput, options?: RequestOptions): PagePromise<Item, Meta>;
101
- /** `fetch` with this client's timeout and retries, for presigned URLs. */
113
+ /**
114
+ * `fetch` with this client's retries, for an upload's presigned URL. fetch
115
+ * shows no upload progress, so each attempt may take as long as sending
116
+ * `byteSize` at 64 KiB/s, if that is longer than the timeout.
117
+ */
102
118
  fetchWithRetries(url: string, init: RequestInit, options: RequestOptions & {
103
119
  retry: boolean;
120
+ byteSize: number;
104
121
  }): Promise<Response>;
105
122
  }
106
123
  /**