@nohead/sdk 0.1.0 → 0.2.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,17 @@ 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.2.0
9
+
10
+ - `onRetry` client option: called before each retry with the attempt, the
11
+ delay, and the status or error that failed, for example to report retries.
12
+ - `assets.upload()` with an `idempotencyKey` no longer fails when it
13
+ completes the upload: the key only starts it. Running the same upload again
14
+ resumes it, and returns an asset that is already `ready` without sending the
15
+ bytes again.
16
+ - Types follow the API's current contract. They add types only for
17
+ operations an API key can't call, so no method changed.
18
+
8
19
  ## 0.1.0
9
20
 
10
21
  The first release.
package/README.md CHANGED
@@ -14,7 +14,7 @@ for await (const post of nohead.records.list("posts", {
14
14
  }
15
15
  ```
16
16
 
17
- > **Status:** 0.x, not yet published to npm. Until it is, install from GitHub: `npm install github:nohead-io/nohead-typescript`.
17
+ > **Status:** 0.x until the Nohead API launches.
18
18
 
19
19
  ## Contents
20
20
 
@@ -50,16 +50,17 @@ const nohead = new Nohead({
50
50
  })
51
51
  ```
52
52
 
53
- | Option | Default | |
54
- | ------------ | ---------------------------------------------- | ---------------------------------------------------- |
55
- | `apiKey` | `NOHEAD_API_KEY` | A project API key (`sk_live_…`). Required. |
56
- | `baseUrl` | `NOHEAD_API_URL`, else `https://api.nohead.io` | |
57
- | `projectId` | the key's project | Looked up once with `GET /v1/me` when omitted. |
58
- | `maxRetries` | `2` | See [retries](#retries-and-idempotency). |
59
- | `timeout` | `60000` | Milliseconds per attempt. |
60
- | `fetch` | `globalThis.fetch` | For proxies, instrumentation and tests. |
61
- | `headers` | `{}` | Added to every request. |
62
- | `warnings` | `true` | Logs deprecation and plan usage warnings, once each. |
53
+ | Option | Default | |
54
+ | ------------ | ---------------------------------------------- | ------------------------------------------------------------------ |
55
+ | `apiKey` | `NOHEAD_API_KEY` | A project API key (`sk_live_…`). Required. |
56
+ | `baseUrl` | `NOHEAD_API_URL`, else `https://api.nohead.io` | |
57
+ | `projectId` | the key's project | Looked up once with `GET /v1/me` when omitted. |
58
+ | `maxRetries` | `2` | See [retries](#retries-and-idempotency). |
59
+ | `timeout` | `60000` | Milliseconds per attempt. |
60
+ | `fetch` | `globalThis.fetch` | For proxies, instrumentation and tests. |
61
+ | `headers` | `{}` | Added to every request. |
62
+ | `warnings` | `true` | Logs deprecation and plan usage warnings, once each. |
63
+ | `onRetry` | | Called before each retry. See [retries](#retries-and-idempotency). |
63
64
 
64
65
  API keys belong to a project, so methods like `collections.list()` need no project ID. Collections can be named by ID or slug everywhere.
65
66
 
@@ -163,6 +164,17 @@ Failed requests are retried twice by default (`maxRetries`), with exponential ba
163
164
  - what's retried: connection errors, timeouts, 429, 500, 502, 503, 504, and a 409 for a request that is still running
164
165
  - `Retry-After` is honored up to 60 seconds; a longer one throws `RateLimitError` straight away
165
166
 
167
+ To report retries, pass `onRetry`. It gets the retry's `attempt` (1 for the first), `delay` in seconds, `url`, the failed `status` (none when the connection failed) and the `error`:
168
+
169
+ ```ts
170
+ const nohead = new Nohead({
171
+ onRetry: ({ attempt, delay, status }) =>
172
+ console.error(
173
+ `Retry ${attempt} in ${delay.toFixed(1)}s (${status ?? "network"})`
174
+ ),
175
+ })
176
+ ```
177
+
166
178
  Every write gets an `Idempotency-Key` that stays the same across its retries, so a retry after a lost response never writes twice. To make a write safe across your own retries (a job that may run twice), pass a key:
167
179
 
168
180
  ```ts
@@ -223,6 +235,8 @@ const { url } = await nohead.assets.imageUrl(asset.id, {
223
235
 
224
236
  **Errors:** `UploadError` if storage refuses the bytes; `ValidationError` if the file fails the checks.
225
237
 
238
+ **Resuming:** pass an `idempotencyKey` (`upload(file, {}, { idempotencyKey })`) and running the same upload again picks up where it stopped. An asset that is already `ready` comes back without sending the bytes again.
239
+
226
240
  **Uploading from a browser:** create the upload on your server with `createUpload`, `PUT` the file from the browser, then `complete` it.
227
241
 
228
242
  ## Search
@@ -336,7 +350,7 @@ NOHEAD_API_URL=http://localhost:3000 NOHEAD_API_KEY=sk_live_… npm run smoke
336
350
  - the types, in `src/generated/types.gen.ts` (hey-api)
337
351
  - the operation table, in `src/generated/operations.gen.ts`
338
352
  - The methods in `src/resources` are written by hand.
339
- - `test/contract.test.ts` calls every public method. It fails when an API-key operation in the contract has no method, or when a request doesn't match its operation.
353
+ - `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.
340
354
 
341
355
  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.
342
356
 
@@ -344,9 +358,9 @@ The smoke test (`smoke/smoke.ts`) runs the core flow against a real API, using t
344
358
 
345
359
  1. Bump the version in `package.json` and `src/version.ts`.
346
360
  2. Add a section for it to `CHANGELOG.md` (`## 1.2.3`), which becomes the release's notes.
347
- 3. Merge to `main`.
348
- 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`.
349
- 5. The tag starts `.github/workflows/release.yml`. It checks the version and its notes, tests, and stages the package on npm with provenance through trusted publishing (no token). Then it creates a draft GitHub release.
361
+ 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.
362
+ 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.
363
+ 5. The tag starts `.github/workflows/release.yml`. 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 checks the version and its notes, tests, and stages the package on npm with provenance through trusted publishing (no token). Then it creates a draft GitHub release.
350
364
  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.
351
365
 
352
366
  ## License
package/dist/core.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { APIPromise, PagePromise } from "./api-promise.ts";
2
+ import { NoheadError } from "./errors.ts";
2
3
  import { type OperationId, type Operations } from "./generated/operations.gen.ts";
3
4
  import type { ListMeta } from "./generated/types.gen.ts";
4
5
  export declare const DEFAULT_BASE_URL = "https://api.nohead.io";
@@ -19,6 +20,21 @@ export interface ClientOptions {
19
20
  headers?: Record<string, string>;
20
21
  /** Log deprecation and plan usage warnings (once each). Default true. */
21
22
  warnings?: boolean;
23
+ /** Called before each retry, for example to report it. */
24
+ onRetry?: (retry: RetryEvent) => void;
25
+ }
26
+ /** A retry about to happen (`onRetry`). */
27
+ export interface RetryEvent {
28
+ /** 1 for the first retry of the request, 2 for the second… */
29
+ attempt: number;
30
+ /** Seconds the client waits before it. */
31
+ delay: number;
32
+ /** The URL being retried. */
33
+ url: string;
34
+ /** The status that failed, or undefined when the connection did. */
35
+ status?: number;
36
+ /** What failed: an `APIError` or a `ConnectionError` (none for storage). */
37
+ error?: NoheadError;
22
38
  }
23
39
  export interface RequestOptions {
24
40
  /** Sent as `Idempotency-Key` on writes. Generated when omitted. */
package/dist/core.js CHANGED
@@ -15,6 +15,7 @@ export class Core {
15
15
  #maxRetries;
16
16
  #timeout;
17
17
  #warnings;
18
+ #onRetry;
18
19
  #projectId;
19
20
  constructor(options) {
20
21
  const env = environment();
@@ -31,6 +32,7 @@ export class Core {
31
32
  this.#maxRetries = options.maxRetries ?? 2;
32
33
  this.#timeout = options.timeout ?? 60_000;
33
34
  this.#warnings = options.warnings ?? true;
35
+ this.#onRetry = options.onRetry;
34
36
  if (options.projectId)
35
37
  this.#projectId = Promise.resolve(options.projectId);
36
38
  }
@@ -68,18 +70,21 @@ export class Core {
68
70
  ? (options.maxRetries ?? this.#maxRetries)
69
71
  : 0;
70
72
  for (let attempt = 0;; attempt++) {
73
+ let failed;
71
74
  try {
72
75
  const { response } = await this.#attempt(url, init, options, false);
73
76
  if (!RETRYABLE_STATUSES.has(response.status) || attempt >= maxRetries) {
74
77
  return response;
75
78
  }
79
+ failed = { status: response.status };
76
80
  }
77
81
  catch (error) {
78
82
  if (!(error instanceof ConnectionError) || attempt >= maxRetries) {
79
83
  throw error;
80
84
  }
85
+ failed = { error };
81
86
  }
82
- await sleep(backoff(attempt), options.signal);
87
+ await this.#retry(url, attempt, backoff(attempt), failed, options.signal);
83
88
  }
84
89
  }
85
90
  async #send(operation, input, options) {
@@ -122,7 +127,7 @@ export class Core {
122
127
  if (!(error instanceof ConnectionError) || attempt >= maxRetries) {
123
128
  throw error;
124
129
  }
125
- await sleep(backoff(attempt), options.signal);
130
+ await this.#retry(url.toString(), attempt, backoff(attempt), { error }, options.signal);
126
131
  continue;
127
132
  }
128
133
  const { response, body } = result;
@@ -134,9 +139,13 @@ export class Core {
134
139
  const delay = retryDelay(error, attempt, maxRetries);
135
140
  if (delay === undefined)
136
141
  throw error;
137
- await sleep(delay, options.signal);
142
+ await this.#retry(url.toString(), attempt, delay, { status: response.status, error }, options.signal);
138
143
  }
139
144
  }
145
+ #retry(url, attempt, delay, failed, signal) {
146
+ this.#onRetry?.({ attempt: attempt + 1, delay, url, ...failed });
147
+ return sleep(delay, signal);
148
+ }
140
149
  async #attempt(url, init, options, readBody) {
141
150
  const timeout = options.timeout ?? this.#timeout;
142
151
  const timer = AbortSignal.timeout(timeout);
@@ -1 +1 @@
1
- export type { AccessReview, Actor, ApiKey, ApiKeyCreate, ApiKeyList, ApiKeyProperties, ApiKeyScope, ApiKeyWithToken, Asset, AssetList, AssetUpload, AssetUploadCreate, AuditEvent, AuditEventList, Billing, BillingCheckout, BillingPlan, BillingPortal, BillingSession, BillingSubscription, BulkRecordsRequest, BulkRecordsResult, Change, ChangeVia, Collection, CollectionCreate, CollectionList, CollectionUpdate, DownloadUrl, Error, ErrorDetail, ErrorType, FeatureFlags, Field, FieldCreate, FieldDefinition, FieldList, FieldMigration, FieldMigrationCounts, FieldMigrationList, FieldMigrationPreview, FieldMigrationRequest, FieldType, FieldUpdate, Health, ImageUrl, Invitation, InvitationCreate, InvitationList, ListMeta, MeUpdate, Membership, MembershipList, MembershipUpdate, NullableTimestamp, Organization, OrganizationCreate, OrganizationList, OrganizationUpdate, Permission, Principal, Project, ProjectCreate, ProjectList, ProjectUpdate, PublishedOnly, RecordCount, RecordData, RecordDiff, RecordList, RecordRevision, RecordRevisionDetail, RecordRevisionList, RecordRevisionProperties, RecordSchedule, RecordScheduleUpdate, RecordWrite, Redaction, RequestId, RevertPreview, Role, Schema, SchemaChange, SchemaChangeDetail, SchemaChangeList, SchemaChangeProperties, SchemaDefinition, SearchIndex, SearchResults, Timestamp, Usage, UsageMetric, Via, Webhook, WebhookCreate, WebhookDelivery, WebhookDeliveryDetail, WebhookDeliveryList, WebhookDeliveryProperties, WebhookEventPayload, WebhookEventType, WebhookList, WebhookProperties, WebhookUpdate, WebhookWithSecret, } from "./types.gen.ts";
1
+ export type { AccessReview, AccountDeletion, AccountDeletionCode, Actor, ApiKey, ApiKeyCreate, ApiKeyList, ApiKeyProperties, ApiKeyScope, ApiKeyWithToken, Asset, AssetList, AssetUpload, AssetUploadCreate, AuditEvent, AuditEventList, Billing, BillingCheckout, BillingPlan, BillingPortal, BillingSession, BillingSubscription, BulkRecordsRequest, BulkRecordsResult, Change, ChangeVia, Collection, CollectionCreate, CollectionList, CollectionUpdate, DownloadUrl, Error, ErrorDetail, ErrorType, FeatureFlags, Field, FieldCreate, FieldDefinition, FieldList, FieldMigration, FieldMigrationCounts, FieldMigrationList, FieldMigrationPreview, FieldMigrationRequest, FieldType, FieldUpdate, Health, ImageUrl, Invitation, InvitationCreate, InvitationList, ListMeta, MeUpdate, Membership, MembershipList, MembershipUpdate, NullableTimestamp, Organization, OrganizationCreate, OrganizationList, OrganizationUpdate, Permission, Principal, Project, ProjectCreate, ProjectList, ProjectUpdate, PublishedOnly, RecordCount, RecordData, RecordDiff, RecordList, RecordRevision, RecordRevisionDetail, RecordRevisionList, RecordRevisionProperties, RecordSchedule, RecordScheduleUpdate, RecordWrite, Redaction, RequestId, RevertPreview, Role, Schema, SchemaChange, SchemaChangeDetail, SchemaChangeList, SchemaChangeProperties, SchemaDefinition, SearchIndex, SearchResults, Timestamp, Usage, UsageMetric, UsageRetention, UsageRetentionGrace, Via, Webhook, WebhookCreate, WebhookDelivery, WebhookDeliveryDetail, WebhookDeliveryList, WebhookDeliveryProperties, WebhookEventPayload, WebhookEventType, WebhookList, WebhookProperties, WebhookUpdate, WebhookWithSecret, } from "./types.gen.ts";
@@ -93,7 +93,7 @@ export type Invitation = {
93
93
  name: string;
94
94
  };
95
95
  /**
96
- * What became of the invitation email, or null while it has not been sent (or when sending is off). `sent`: accepted for delivery. `not_sent`: the address had bounced or been marked as spam before, so no email went out. `bounced`: the email could not be delivered; check the address. `complained`: the invitee marked it as spam. Either way the invitation still works when the invitee signs in with the address.
96
+ * What became of the invitation email, or null while it has not been sent (or when sending is off). `sent`: accepted for delivery. `not_sent`: no email went out, because the address had bounced or been marked as spam before, or had already received several invitation emails that day. `bounced`: the email could not be delivered; check the address. `complained`: the invitee marked it as spam. Either way the invitation still works when the invitee signs in with the address.
97
97
  */
98
98
  email_delivery: null | {
99
99
  status: 'sent' | 'not_sent' | 'bounced' | 'complained';
@@ -186,10 +186,15 @@ export type Field = {
186
186
  api_key: string;
187
187
  type: FieldType;
188
188
  required: boolean;
189
+ /**
190
+ * A list of distinct values (a repeated value is refused with `duplicate_value`). Not available for boolean, rich_text and json fields.
191
+ */
189
192
  multiple: boolean;
190
193
  position: number;
191
194
  /**
192
- * Type-specific options, e.g. `max_length`, `options`, `target_collection_id`, plus
195
+ * Type-specific options, e.g. `max_length`, `options`, `target_collection_id`,
196
+ * `accepted_types` (asset fields: MIME types or `type*` wildcards that newly
197
+ * written values must have; any file when absent), plus
193
198
  * the options for searches with API keys: `searchable` (the search words match the
194
199
  * field; default true for text, long_text, rich_text and enum), `filterable` and
195
200
  * `sortable` (default false).
@@ -223,6 +228,9 @@ export type FieldCreate = {
223
228
  api_key: string;
224
229
  type: FieldType;
225
230
  required?: boolean;
231
+ /**
232
+ * A list of distinct values (a repeated value is refused with `duplicate_value`). Not available for boolean, rich_text and json fields.
233
+ */
226
234
  multiple?: boolean;
227
235
  position?: number;
228
236
  configuration?: {
@@ -234,6 +242,9 @@ export type FieldUpdate = {
234
242
  api_key?: string;
235
243
  type?: FieldType;
236
244
  required?: boolean;
245
+ /**
246
+ * A list of distinct values (a repeated value is refused with `duplicate_value`). Not available for boolean, rich_text and json fields.
247
+ */
237
248
  multiple?: boolean;
238
249
  position?: number;
239
250
  configuration?: {
@@ -319,6 +330,12 @@ export type Actor = {
319
330
  * The user (`usr_...`) or API key (`key_...`), or null for system changes.
320
331
  */
321
332
  id: string | null;
333
+ /**
334
+ * Resolved when read, never stored: a user's current name (or
335
+ * email), "Deleted user" once they deleted their account, an API
336
+ * key's name, or "System".
337
+ *
338
+ */
322
339
  name: string | null;
323
340
  };
324
341
  export type SchemaChange = SchemaChangeProperties;
@@ -355,6 +372,9 @@ export type FieldDefinition = {
355
372
  configuration: {
356
373
  [key: string]: unknown;
357
374
  };
375
+ /**
376
+ * A list of distinct values (a repeated value is refused with `duplicate_value`). Not available for boolean, rich_text and json fields.
377
+ */
358
378
  multiple: boolean;
359
379
  required: boolean;
360
380
  };
@@ -369,6 +389,9 @@ export type FieldMigrationRequest = {
369
389
  configuration?: {
370
390
  [key: string]: unknown;
371
391
  };
392
+ /**
393
+ * A list of distinct values (a repeated value is refused with `duplicate_value`). Not available for boolean, rich_text and json fields.
394
+ */
372
395
  multiple?: boolean;
373
396
  required?: boolean;
374
397
  /**
@@ -629,6 +652,22 @@ export type FeatureFlags = {
629
652
  [key: string]: boolean;
630
653
  };
631
654
  };
655
+ export type AccountDeletionCode = {
656
+ object: 'account_deletion_code';
657
+ /**
658
+ * Where the code was sent.
659
+ */
660
+ email: string;
661
+ expires_at: string;
662
+ };
663
+ export type AccountDeletion = {
664
+ object: 'account_deletion';
665
+ /**
666
+ * The deleted user, as history still refers to it.
667
+ */
668
+ user_id: string;
669
+ deleted_at: string;
670
+ };
632
671
  export type Principal = {
633
672
  object: 'principal';
634
673
  type: 'user' | 'api_key';
@@ -828,6 +867,29 @@ export type Usage = {
828
867
  end: string;
829
868
  };
830
869
  metrics: Array<UsageMetric>;
870
+ retention: UsageRetention;
871
+ };
872
+ /**
873
+ * How long the plan keeps record revisions (beyond each record's first and latest 10) and audit events, in days; null keeps everything.
874
+ */
875
+ export type UsageRetention = {
876
+ revision_days: number | null;
877
+ audit_days: number | null;
878
+ /**
879
+ * After a downgrade, the previous plan's longer retention still applies for 30 days; history past the plan's retention is removed after `ends_at`. Null otherwise.
880
+ */
881
+ grace: null | UsageRetentionGrace;
882
+ };
883
+ export type UsageRetentionGrace = {
884
+ ends_at: string;
885
+ /**
886
+ * Days of revisions kept until `ends_at`; null keeps everything.
887
+ */
888
+ revision_days: number | null;
889
+ /**
890
+ * Days of audit events kept until `ends_at`.
891
+ */
892
+ audit_days: number | null;
831
893
  };
832
894
  export type UsageMetric = {
833
895
  metric: 'api_requests' | 'search_requests' | 'image_transformations' | 'asset_uploads' | 'asset_downloads' | 'webhook_deliveries' | 'record_revisions' | 'projects' | 'collections' | 'records' | 'members' | 'webhooks' | 'asset_storage_bytes' | 'indexed_records';
@@ -3607,6 +3669,10 @@ export type AssetsListData = {
3607
3669
  * List soft-deleted items instead of active ones.
3608
3670
  */
3609
3671
  deleted?: boolean;
3672
+ /**
3673
+ * Comma-separated MIME types or `type*` wildcards.
3674
+ */
3675
+ content_type?: string;
3610
3676
  };
3611
3677
  url: '/v1/projects/{project_id}/assets';
3612
3678
  };
@@ -3680,6 +3746,12 @@ export type AssetsUploadErrors = {
3680
3746
  * Authentication is missing or invalid.
3681
3747
  */
3682
3748
  401: Error;
3749
+ /**
3750
+ * The change would exceed a hard limit of the organization's plan. The
3751
+ * detail names the metric (`field`) with its `usage` and `limit`.
3752
+ *
3753
+ */
3754
+ 402: Error;
3683
3755
  /**
3684
3756
  * The caller is not allowed to perform this action.
3685
3757
  */
@@ -6671,6 +6743,66 @@ export type HealthCheckResponses = {
6671
6743
  200: Health;
6672
6744
  };
6673
6745
  export type HealthCheckResponse = HealthCheckResponses[keyof HealthCheckResponses];
6746
+ export type MeDeleteData = {
6747
+ body: {
6748
+ /**
6749
+ * The six-digit code emailed by `POST /v1/me/deletion-code`.
6750
+ */
6751
+ code: string;
6752
+ };
6753
+ headers?: {
6754
+ /**
6755
+ * A unique value (1-255 printable ASCII characters) that makes retrying
6756
+ * this request safe. The first response (below 500) is stored for 24
6757
+ * hours per user or API key; retries with the same key and the same
6758
+ * request get it again with an `Idempotent-Replayed: true` header. The
6759
+ * same key with a different request is refused with `400`; a retry while
6760
+ * the first request is still running gets `409`.
6761
+ *
6762
+ */
6763
+ 'Idempotency-Key'?: string;
6764
+ };
6765
+ path?: never;
6766
+ query?: never;
6767
+ url: '/v1/me';
6768
+ };
6769
+ export type MeDeleteErrors = {
6770
+ /**
6771
+ * The request was malformed.
6772
+ */
6773
+ 400: Error;
6774
+ /**
6775
+ * Authentication is missing or invalid.
6776
+ */
6777
+ 401: Error;
6778
+ /**
6779
+ * The caller is not allowed to perform this action.
6780
+ */
6781
+ 403: Error;
6782
+ /**
6783
+ * The request conflicts with current state.
6784
+ */
6785
+ 409: Error;
6786
+ /**
6787
+ * The request failed validation.
6788
+ */
6789
+ 422: Error;
6790
+ /**
6791
+ * Too many requests for this API key or user (or, with failing
6792
+ * credentials, this IP address). Wait `Retry-After` seconds; retries with
6793
+ * the same `Idempotency-Key` are safe.
6794
+ *
6795
+ */
6796
+ 429: Error;
6797
+ };
6798
+ export type MeDeleteError = MeDeleteErrors[keyof MeDeleteErrors];
6799
+ export type MeDeleteResponses = {
6800
+ /**
6801
+ * The account is deleted, and the session cookie cleared.
6802
+ */
6803
+ 200: AccountDeletion;
6804
+ };
6805
+ export type MeDeleteResponse = MeDeleteResponses[keyof MeDeleteResponses];
6674
6806
  export type MeGetData = {
6675
6807
  body?: never;
6676
6808
  path?: never;
@@ -6753,6 +6885,57 @@ export type MeUpdateResponses = {
6753
6885
  200: Principal;
6754
6886
  };
6755
6887
  export type MeUpdateResponse = MeUpdateResponses[keyof MeUpdateResponses];
6888
+ export type MeDeletionCodeSendData = {
6889
+ body?: never;
6890
+ headers?: {
6891
+ /**
6892
+ * A unique value (1-255 printable ASCII characters) that makes retrying
6893
+ * this request safe. The first response (below 500) is stored for 24
6894
+ * hours per user or API key; retries with the same key and the same
6895
+ * request get it again with an `Idempotent-Replayed: true` header. The
6896
+ * same key with a different request is refused with `400`; a retry while
6897
+ * the first request is still running gets `409`.
6898
+ *
6899
+ */
6900
+ 'Idempotency-Key'?: string;
6901
+ };
6902
+ path?: never;
6903
+ query?: never;
6904
+ url: '/v1/me/deletion-code';
6905
+ };
6906
+ export type MeDeletionCodeSendErrors = {
6907
+ /**
6908
+ * The request was malformed.
6909
+ */
6910
+ 400: Error;
6911
+ /**
6912
+ * Authentication is missing or invalid.
6913
+ */
6914
+ 401: Error;
6915
+ /**
6916
+ * The caller is not allowed to perform this action.
6917
+ */
6918
+ 403: Error;
6919
+ /**
6920
+ * The request conflicts with current state.
6921
+ */
6922
+ 409: Error;
6923
+ /**
6924
+ * Too many requests for this API key or user (or, with failing
6925
+ * credentials, this IP address). Wait `Retry-After` seconds; retries with
6926
+ * the same `Idempotency-Key` are safe.
6927
+ *
6928
+ */
6929
+ 429: Error;
6930
+ };
6931
+ export type MeDeletionCodeSendError = MeDeletionCodeSendErrors[keyof MeDeletionCodeSendErrors];
6932
+ export type MeDeletionCodeSendResponses = {
6933
+ /**
6934
+ * The code is on its way.
6935
+ */
6936
+ 202: AccountDeletionCode;
6937
+ };
6938
+ export type MeDeletionCodeSendResponse = MeDeletionCodeSendResponses[keyof MeDeletionCodeSendResponses];
6756
6939
  export type FeatureFlagsListData = {
6757
6940
  body?: never;
6758
6941
  path?: never;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Nohead } from "./client.ts";
2
2
  export { APIPromise, Page, PagePromise, type APIResponse, } from "./api-promise.ts";
3
- export type { ClientOptions, ConditionalRequestOptions, RequestOptions, } from "./core.ts";
3
+ export type { ClientOptions, ConditionalRequestOptions, RequestOptions, RetryEvent, } from "./core.ts";
4
4
  export { APIError, AbortError, AuthenticationError, AuthorizationError, ConflictError, ConnectionError, InternalServerError, InvalidRequestError, NoheadError, NotFoundError, PlanLimitExceededError, PreconditionFailedError, RateLimitError, ServiceUnavailableError, TimeoutError, UploadError, ValidationError, WebhookVerificationError, } from "./errors.ts";
5
5
  export type { Uploadable, UploadParams } from "./resources/assets.ts";
6
6
  export type * from "./types.ts";
@@ -1,7 +1,7 @@
1
1
  import type { APIPromise, PagePromise } from "../api-promise.ts";
2
2
  import type { RequestOptions } from "../core.ts";
3
3
  import type { Asset, AssetUpload, AssetUploadCreate, DownloadUrl, ImageUrl } from "../generated/types.gen.ts";
4
- import type { DeletedListParams, ImageUrlParams } from "../types.ts";
4
+ import type { AssetListParams, ImageUrlParams } from "../types.ts";
5
5
  import { Resource } from "./resource.ts";
6
6
  /** What `assets.upload` accepts. A stream needs `byte_size`. */
7
7
  export type Uploadable = Blob | ArrayBuffer | Uint8Array | ReadableStream;
@@ -21,6 +21,10 @@ export declare class Assets extends Resource {
21
21
  * file). Throws `UploadError` if storage refuses the bytes, and
22
22
  * `ValidationError` if the file fails the checks.
23
23
  *
24
+ * With an `idempotencyKey`, running the same upload again resumes it: the
25
+ * key starts the upload, and an asset that is already `ready` is returned
26
+ * without sending the bytes again.
27
+ *
24
28
  * import { openAsBlob } from "node:fs"
25
29
  * const asset = await nohead.assets.upload(await openAsBlob("a.jpg"), { filename: "a.jpg" })
26
30
  */
@@ -32,7 +36,7 @@ export declare class Assets extends Resource {
32
36
  createUpload(params: AssetUploadCreate, options?: RequestOptions): APIPromise<AssetUpload>;
33
37
  /** Checks an uploaded file and marks the asset `ready`. */
34
38
  complete(asset: string, options?: RequestOptions): APIPromise<Asset>;
35
- list(params?: DeletedListParams, options?: RequestOptions): PagePromise<Asset>;
39
+ list(params?: AssetListParams, options?: RequestOptions): PagePromise<Asset>;
36
40
  get(asset: string, options?: RequestOptions): APIPromise<Asset>;
37
41
  /** Soft-deletes the asset; the file is purged after 30 days. */
38
42
  delete(asset: string, options?: RequestOptions): APIPromise<Asset>;
@@ -8,6 +8,10 @@ export class Assets extends Resource {
8
8
  * file). Throws `UploadError` if storage refuses the bytes, and
9
9
  * `ValidationError` if the file fails the checks.
10
10
  *
11
+ * With an `idempotencyKey`, running the same upload again resumes it: the
12
+ * key starts the upload, and an asset that is already `ready` is returned
13
+ * without sending the bytes again.
14
+ *
11
15
  * import { openAsBlob } from "node:fs"
12
16
  * const asset = await nohead.assets.upload(await openAsBlob("a.jpg"), { filename: "a.jpg" })
13
17
  */
@@ -25,6 +29,16 @@ export class Assets extends Resource {
25
29
  ((file instanceof Blob && file.type) || "application/octet-stream"),
26
30
  byte_size: byteSize,
27
31
  }, options);
32
+ // A replayed start returns the first run's asset, which may be done.
33
+ if (options?.idempotencyKey) {
34
+ const current = await this.get(asset.id, {
35
+ ...options,
36
+ idempotencyKey: undefined,
37
+ changeNote: undefined,
38
+ });
39
+ if (current.status === "ready")
40
+ return current;
41
+ }
28
42
  const stream = file instanceof ReadableStream;
29
43
  const response = await this.core
30
44
  .fetchWithRetries(upload.url, {
@@ -41,7 +55,8 @@ export class Assets extends Resource {
41
55
  if (!response.ok) {
42
56
  throw new UploadError(`Storage refused the upload of ${asset.filename} (${response.status})`, response.status);
43
57
  }
44
- return this.complete(asset.id, options);
58
+ // The key belongs to the start: the API refuses one key on two requests.
59
+ return this.complete(asset.id, { ...options, idempotencyKey: undefined });
45
60
  }
46
61
  /**
47
62
  * The first step of an upload: a `pending` asset and a presigned URL to
package/dist/types.d.ts CHANGED
@@ -82,6 +82,14 @@ export interface DeletedListParams extends ListParams {
82
82
  /** List deleted items instead (they can be restored for 30 days). */
83
83
  deleted?: boolean;
84
84
  }
85
+ export interface AssetListParams extends DeletedListParams {
86
+ /**
87
+ * Only assets of these MIME types or `type/*` wildcards, e.g.
88
+ * `["image/*", "application/pdf"]`, such as an asset field's
89
+ * `accepted_types`.
90
+ */
91
+ content_type?: string | string[];
92
+ }
85
93
  export interface SchemaParams {
86
94
  /** A past schema version; the current one otherwise. */
87
95
  version?: number;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.0";
1
+ export declare const VERSION = "0.2.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.0";
1
+ export const VERSION = "0.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nohead/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Official TypeScript SDK for the Nohead API",
5
5
  "license": "MIT",
6
6
  "homepage": "https://nohead.io",