@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 +96 -0
- package/README.md +58 -26
- package/dist/client.d.ts +9 -1
- package/dist/client.js +14 -2
- package/dist/core.d.ts +22 -5
- package/dist/core.js +158 -45
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +74 -16
- package/dist/generated/operations.gen.d.ts +0 -8
- package/dist/generated/operations.gen.js +0 -1
- package/dist/generated/schemas.gen.d.ts +1 -1
- package/dist/generated/types.gen.d.ts +23 -59
- package/dist/images.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/resources/assets.d.ts +6 -4
- package/dist/resources/assets.js +23 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhooks.d.ts +2 -1
- package/dist/webhooks.js +6 -5
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type ClientOptions = {
|
|
2
|
-
baseUrl: 'https://api.nohead.io' |
|
|
2
|
+
baseUrl: 'https://api.nohead.io' | (string & {});
|
|
3
3
|
};
|
|
4
4
|
export type Webhooks = WebhookEventWebhookRequest;
|
|
5
5
|
export type RequestId = string;
|
|
@@ -1129,7 +1129,7 @@ export type Principal = {
|
|
|
1129
1129
|
email: string;
|
|
1130
1130
|
name: string | null;
|
|
1131
1131
|
/**
|
|
1132
|
-
*
|
|
1132
|
+
* Whether the user is Nohead staff.
|
|
1133
1133
|
*/
|
|
1134
1134
|
staff: boolean;
|
|
1135
1135
|
};
|
|
@@ -1539,6 +1539,25 @@ export type AssetProperties = {
|
|
|
1539
1539
|
* The URL of a small rendition (fits in 640×640, WebP), for ready images and SVGs (rasterized; SVGs have no `image_url`), deleted ones included until they are purged; null otherwise.
|
|
1540
1540
|
*/
|
|
1541
1541
|
preview_url: string | null;
|
|
1542
|
+
/**
|
|
1543
|
+
* The URL of the file as uploaded, for ready assets of any type,
|
|
1544
|
+
* deleted ones included until they are purged; null otherwise. It
|
|
1545
|
+
* downloads the file under its `filename` (a rename applies within a
|
|
1546
|
+
* minute or so), is open and never expires, and is cached on a CDN.
|
|
1547
|
+
*
|
|
1548
|
+
*/
|
|
1549
|
+
file_url: string | null;
|
|
1550
|
+
/**
|
|
1551
|
+
* The URL that plays a ready video or audio file inline, for
|
|
1552
|
+
* `<video>` and `<audio>` (with range requests, so players can seek),
|
|
1553
|
+
* deleted ones included until they are purged: MP4, QuickTime, M4A,
|
|
1554
|
+
* 3GP, WebM, Matroska, MP3, AAC, FLAC, WAV and Ogg (whose content is
|
|
1555
|
+
* checked when the upload completes). Null for other types, which
|
|
1556
|
+
* `file_url` downloads.
|
|
1557
|
+
* Open, never expiring and cached on a CDN, like `file_url`.
|
|
1558
|
+
*
|
|
1559
|
+
*/
|
|
1560
|
+
media_url: string | null;
|
|
1542
1561
|
uploaded_at: NullableTimestamp;
|
|
1543
1562
|
created_at: Timestamp;
|
|
1544
1563
|
updated_at: Timestamp;
|
|
@@ -1569,17 +1588,11 @@ export type AssetUsage = {
|
|
|
1569
1588
|
fields: Array<string>;
|
|
1570
1589
|
}>;
|
|
1571
1590
|
};
|
|
1572
|
-
export type DownloadUrl = {
|
|
1573
|
-
object: 'download_url';
|
|
1574
|
-
asset_id: string;
|
|
1575
|
-
url: string;
|
|
1576
|
-
expires_at: Timestamp;
|
|
1577
|
-
};
|
|
1578
1591
|
export type AssetUploadCreate = {
|
|
1579
1592
|
filename: string;
|
|
1580
1593
|
content_type: string;
|
|
1581
1594
|
/**
|
|
1582
|
-
* Exact size of the file in bytes
|
|
1595
|
+
* Exact size of the file in bytes, at most the plan's largest upload (10 MB on Free, 100 MB on paid plans).
|
|
1583
1596
|
*/
|
|
1584
1597
|
byte_size: number;
|
|
1585
1598
|
};
|
|
@@ -1609,7 +1622,7 @@ export type AssetUploadTarget = {
|
|
|
1609
1622
|
expires_at: Timestamp;
|
|
1610
1623
|
};
|
|
1611
1624
|
/**
|
|
1612
|
-
* What a project API key may do. `records:write` also publishes, unpublishes and schedules records
|
|
1625
|
+
* What a project API key may do. `records:write` also publishes, unpublishes and schedules records. `webhooks:read` includes delivery payloads and `webhooks:write` can subscribe an endpoint to every event, so both give access to the record, schema and asset data in events, unpublished records included; published-only keys cannot have them.
|
|
1613
1626
|
*/
|
|
1614
1627
|
export type ApiKeyScope = 'records:read' | 'records:write' | 'schema:read' | 'schema:write' | 'assets:read' | 'assets:write' | 'search:read' | 'webhooks:read' | 'webhooks:write' | 'audit:read';
|
|
1615
1628
|
/**
|
|
@@ -5741,55 +5754,6 @@ export type AssetsUpdateResponses = {
|
|
|
5741
5754
|
200: Asset;
|
|
5742
5755
|
};
|
|
5743
5756
|
export type AssetsUpdateResponse = AssetsUpdateResponses[keyof AssetsUpdateResponses];
|
|
5744
|
-
export type AssetsDownloadUrlData = {
|
|
5745
|
-
body?: never;
|
|
5746
|
-
path: {
|
|
5747
|
-
asset_id: string;
|
|
5748
|
-
};
|
|
5749
|
-
query?: never;
|
|
5750
|
-
url: '/v1/assets/{asset_id}/download-url';
|
|
5751
|
-
};
|
|
5752
|
-
export type AssetsDownloadUrlErrors = {
|
|
5753
|
-
/**
|
|
5754
|
-
* The request was malformed: unparseable JSON, an invalid query parameter, or a body key the operation doesn't take (detail code `unknown_parameter`, naming the key).
|
|
5755
|
-
*/
|
|
5756
|
-
400: Error;
|
|
5757
|
-
/**
|
|
5758
|
-
* Authentication is missing or invalid.
|
|
5759
|
-
*/
|
|
5760
|
-
401: Error;
|
|
5761
|
-
/**
|
|
5762
|
-
* The caller is not allowed to perform this action.
|
|
5763
|
-
*/
|
|
5764
|
-
403: Error;
|
|
5765
|
-
/**
|
|
5766
|
-
* The resource does not exist or is not visible to the caller.
|
|
5767
|
-
*/
|
|
5768
|
-
404: Error;
|
|
5769
|
-
/**
|
|
5770
|
-
* The request conflicts with current state.
|
|
5771
|
-
*/
|
|
5772
|
-
409: Error;
|
|
5773
|
-
/**
|
|
5774
|
-
* Too many requests for this API key or user (or, with failing
|
|
5775
|
-
* credentials, this IP address). A published-only key is counted per
|
|
5776
|
-
* client IP. Wait `Retry-After` seconds; retries with the same `Idempotency-Key`
|
|
5777
|
-
* are safe. On the Free plan, an organization's API keys are also
|
|
5778
|
-
* refused once its API requests reach the month's limit, with a
|
|
5779
|
-
* `plan_limit` detail (`field` `api_requests`, `usage` and `limit`),
|
|
5780
|
-
* until the next month (UTC) or an upgrade.
|
|
5781
|
-
*
|
|
5782
|
-
*/
|
|
5783
|
-
429: Error;
|
|
5784
|
-
};
|
|
5785
|
-
export type AssetsDownloadUrlError = AssetsDownloadUrlErrors[keyof AssetsDownloadUrlErrors];
|
|
5786
|
-
export type AssetsDownloadUrlResponses = {
|
|
5787
|
-
/**
|
|
5788
|
-
* The download URL.
|
|
5789
|
-
*/
|
|
5790
|
-
200: DownloadUrl;
|
|
5791
|
-
};
|
|
5792
|
-
export type AssetsDownloadUrlResponse = AssetsDownloadUrlResponses[keyof AssetsDownloadUrlResponses];
|
|
5793
5757
|
export type AssetsUsageData = {
|
|
5794
5758
|
body?: never;
|
|
5795
5759
|
path: {
|
package/dist/images.js
CHANGED
|
@@ -16,8 +16,13 @@ export function imageUrl(image, options = {}) {
|
|
|
16
16
|
const at = base.indexOf("/plain/");
|
|
17
17
|
if (at < 0)
|
|
18
18
|
throw new TypeError(`Not a Nohead image URL: ${base}`);
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Empty options count as unset.
|
|
20
|
+
const { width, height, format, quality, dpr, gravity } = options;
|
|
21
|
+
const fit = options.fit || "contain";
|
|
22
|
+
if (!Object.hasOwn(FITS, fit))
|
|
23
|
+
throw new TypeError(`fit must be one of ${Object.keys(FITS).join(", ")}`);
|
|
24
|
+
const others = (options.options ?? []).filter(Boolean);
|
|
25
|
+
for (const option of [gravity, ...others])
|
|
21
26
|
if (option && /[/@?#]/.test(option))
|
|
22
27
|
throw new TypeError(`Not an imgproxy option: ${option}`);
|
|
23
28
|
const segments = [];
|
|
@@ -29,7 +34,7 @@ export function imageUrl(image, options = {}) {
|
|
|
29
34
|
segments.push(`dpr:${dpr}`);
|
|
30
35
|
if (quality)
|
|
31
36
|
segments.push(`q:${quality}`);
|
|
32
|
-
segments.push(...
|
|
37
|
+
segments.push(...others);
|
|
33
38
|
// The source comes last; a format replaces any the URL already names.
|
|
34
39
|
let source = base.slice(at);
|
|
35
40
|
if (format)
|
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
|
|
3
|
+
export { DEFAULT_BASE_URL, type ClientOptions, type ConditionalRequestOptions, type ConditionalWriteOptions, type CopyOptions, type RequestOptions, type RetryEvent, type WriteOptions, } 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";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Nohead } from "./client.js";
|
|
2
2
|
export { APIPromise, Page, PagePromise, } from "./api-promise.js";
|
|
3
|
+
export { DEFAULT_BASE_URL, } from "./core.js";
|
|
3
4
|
export { APIError, AbortError, AuthenticationError, AuthorizationError, ConflictError, ConnectionError, InternalServerError, InvalidRequestError, NoheadError, NotFoundError, PlanLimitExceededError, PreconditionFailedError, RateLimitError, ServiceUnavailableError, TimeoutError, UploadError, ValidationError, WebhookVerificationError, } from "./errors.js";
|
|
4
5
|
export { unwrapWebhook, } from "./webhooks.js";
|
|
5
6
|
export { VERSION } from "./version.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIPromise, PagePromise } from "../api-promise.ts";
|
|
2
2
|
import type { RequestOptions } from "../core.ts";
|
|
3
|
-
import type { Asset, AssetUpdate, AssetUploadCreate, AssetWithUpload, AssetUsage
|
|
3
|
+
import type { Asset, AssetUpdate, AssetUploadCreate, AssetWithUpload, AssetUsage } from "../generated/types.gen.ts";
|
|
4
4
|
import type { AssetListParams } from "../types.ts";
|
|
5
5
|
import { Resource } from "./resource.ts";
|
|
6
6
|
/** What `assets.upload` accepts. A stream needs `byte_size`. */
|
|
@@ -8,11 +8,15 @@ export type Uploadable = Blob | ArrayBuffer | Uint8Array | ReadableStream;
|
|
|
8
8
|
export interface UploadParams {
|
|
9
9
|
/** Defaults to a `File`'s name, else `upload`. */
|
|
10
10
|
filename?: string;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Defaults to the `Blob`'s type, else the filename's extension's for
|
|
13
|
+
* common files, else `application/octet-stream`.
|
|
14
|
+
*/
|
|
12
15
|
content_type?: string;
|
|
13
16
|
/** Required for a `ReadableStream`; known for everything else. */
|
|
14
17
|
byte_size?: number;
|
|
15
18
|
}
|
|
19
|
+
/** Files of the key's project. `asset` is an asset ID (`ast_…`). */
|
|
16
20
|
export declare class Assets extends Resource {
|
|
17
21
|
/**
|
|
18
22
|
* Uploads a file and returns the `ready` asset: creates the upload, sends
|
|
@@ -69,6 +73,4 @@ export declare class Assets extends Resource {
|
|
|
69
73
|
* updated with the fields that use it (`uses`). Needs `records:read` too.
|
|
70
74
|
*/
|
|
71
75
|
usage(asset: string, options?: RequestOptions): APIPromise<AssetUsage>;
|
|
72
|
-
/** A link to download the original file (`url`), valid for 2 minutes. */
|
|
73
|
-
downloadUrl(asset: string, options?: RequestOptions): APIPromise<DownloadUrl>;
|
|
74
76
|
}
|
package/dist/resources/assets.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AbortError, UploadError } from "../errors.js";
|
|
2
2
|
import { listQuery, Resource } from "./resource.js";
|
|
3
|
-
/** Files of the key's project. `asset` is an asset ID (`ast_…`). */
|
|
4
3
|
/** Common files' types by extension, as the other SDKs guess them. */
|
|
5
4
|
const CONTENT_TYPES = {
|
|
6
5
|
avif: "image/avif",
|
|
@@ -26,6 +25,7 @@ function contentTypeOf(filename) {
|
|
|
26
25
|
? CONTENT_TYPES[filename.slice(dot + 1).toLowerCase()]
|
|
27
26
|
: undefined;
|
|
28
27
|
}
|
|
28
|
+
/** Files of the key's project. `asset` is an asset ID (`ast_…`). */
|
|
29
29
|
export class Assets extends Resource {
|
|
30
30
|
/**
|
|
31
31
|
* Uploads a file and returns the `ready` asset: creates the upload, sends
|
|
@@ -45,23 +45,28 @@ export class Assets extends Resource {
|
|
|
45
45
|
* extension's for common files.
|
|
46
46
|
*/
|
|
47
47
|
async upload(file, params = {}, options) {
|
|
48
|
+
// Never a path: the Python and Ruby SDKs take one, so it would be easy
|
|
49
|
+
// to send its text instead.
|
|
50
|
+
if (typeof file === "string") {
|
|
51
|
+
throw new TypeError("upload takes a Blob, File, ArrayBuffer, Uint8Array or ReadableStream; to upload a file from disk, pass await fs.openAsBlob(path)");
|
|
52
|
+
}
|
|
48
53
|
const byteSize = params.byte_size ?? sizeOf(file);
|
|
49
54
|
if (byteSize === undefined) {
|
|
50
55
|
throw new UploadError("Uploading a stream needs `byte_size`");
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
// Empty ones count as unset.
|
|
58
|
+
const filename = params.filename ||
|
|
59
|
+
(typeof File !== "undefined" && file instanceof File && file.name) ||
|
|
60
|
+
"upload";
|
|
56
61
|
const { upload, ...asset } = await this.createUpload({
|
|
57
62
|
filename,
|
|
58
63
|
// The Blob's own type, or one the filename's extension implies (a
|
|
59
64
|
// Blob from Node's openAsBlob has none), as the Python and Ruby
|
|
60
65
|
// SDKs do.
|
|
61
|
-
content_type: params.content_type
|
|
62
|
-
(
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
content_type: params.content_type ||
|
|
67
|
+
(file instanceof Blob && file.type) ||
|
|
68
|
+
contentTypeOf(filename) ||
|
|
69
|
+
"application/octet-stream",
|
|
65
70
|
byte_size: byteSize,
|
|
66
71
|
}, options);
|
|
67
72
|
// A replayed start returns the first run's asset, which may be done.
|
|
@@ -74,24 +79,28 @@ export class Assets extends Resource {
|
|
|
74
79
|
return current;
|
|
75
80
|
// The key replays the first run's upload URL, which expires sooner.
|
|
76
81
|
if (Date.parse(upload.expires_at) <= Date.now()) {
|
|
77
|
-
throw new UploadError(`The upload of ${
|
|
82
|
+
throw new UploadError(`The upload of ${filename} expired: start it again with a new idempotencyKey`);
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
const stream = file instanceof ReadableStream;
|
|
81
86
|
const response = await this.core
|
|
82
87
|
.fetchWithRetries(upload.url, {
|
|
83
88
|
method: upload.method,
|
|
84
|
-
|
|
89
|
+
// The URL is signed for this size; fetch would send a stream
|
|
90
|
+
// in chunks, without one.
|
|
91
|
+
headers: stream
|
|
92
|
+
? { ...upload.headers, "Content-Length": String(byteSize) }
|
|
93
|
+
: upload.headers,
|
|
85
94
|
body: file,
|
|
86
95
|
...(stream ? { duplex: "half" } : {}),
|
|
87
|
-
}, { ...options, retry: !stream })
|
|
96
|
+
}, { ...options, retry: !stream, byteSize })
|
|
88
97
|
.catch((error) => {
|
|
89
98
|
if (error instanceof AbortError)
|
|
90
99
|
throw error;
|
|
91
|
-
throw new UploadError(`Could not upload ${
|
|
100
|
+
throw new UploadError(`Could not upload ${filename}: ${error.message}`, undefined, { cause: error });
|
|
92
101
|
});
|
|
93
102
|
if (!response.ok) {
|
|
94
|
-
throw new UploadError(`Storage refused the upload of ${
|
|
103
|
+
throw new UploadError(`Storage refused the upload of ${filename} (${response.status})`, response.status);
|
|
95
104
|
}
|
|
96
105
|
// The key belongs to the start: the API refuses one key on two requests.
|
|
97
106
|
return this.complete(asset.id, { ...options, idempotencyKey: undefined });
|
|
@@ -159,10 +168,6 @@ export class Assets extends Resource {
|
|
|
159
168
|
usage(asset, options) {
|
|
160
169
|
return this.core.request("assets_usage", { path: { asset_id: asset } }, options);
|
|
161
170
|
}
|
|
162
|
-
/** A link to download the original file (`url`), valid for 2 minutes. */
|
|
163
|
-
downloadUrl(asset, options) {
|
|
164
|
-
return this.core.request("assets_download_url", { path: { asset_id: asset } }, options);
|
|
165
|
-
}
|
|
166
171
|
}
|
|
167
172
|
function sizeOf(file) {
|
|
168
173
|
if (file instanceof Blob)
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.9.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.9.0";
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -49,8 +49,9 @@ export interface UnwrapOptions {
|
|
|
49
49
|
/**
|
|
50
50
|
* The webhook's signing secret (`whsec_…`). While a rotation overlaps,
|
|
51
51
|
* requests carry a signature for each secret, so either one verifies.
|
|
52
|
+
* An unset one (as from a missing environment variable) throws.
|
|
52
53
|
*/
|
|
53
|
-
secret: string;
|
|
54
|
+
secret: string | undefined;
|
|
54
55
|
/** Seconds a timestamp may be off from now. Default 300. */
|
|
55
56
|
tolerance?: number;
|
|
56
57
|
}
|
package/dist/webhooks.js
CHANGED
|
@@ -24,7 +24,11 @@ export async function unwrapWebhook(body, headers, { secret, tolerance = 300 })
|
|
|
24
24
|
throw new WebhookVerificationError("The webhook timestamp is too far from the current time");
|
|
25
25
|
}
|
|
26
26
|
const text = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
27
|
-
const
|
|
27
|
+
const keyBytes = decodeBase64((secret ?? "").replace(/^whsec_/, ""), "secret");
|
|
28
|
+
if (keyBytes.length === 0) {
|
|
29
|
+
throw new WebhookVerificationError("The webhook secret is empty");
|
|
30
|
+
}
|
|
31
|
+
const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
28
32
|
const signed = new TextEncoder().encode(`${id}.${timestamp}.${text}`);
|
|
29
33
|
for (const entry of signatures.split(" ")) {
|
|
30
34
|
const [version, signature] = entry.split(",", 2);
|
|
@@ -63,8 +67,5 @@ function decodeBase64(value, what) {
|
|
|
63
67
|
catch {
|
|
64
68
|
throw new WebhookVerificationError(`The webhook ${what} is not base64`);
|
|
65
69
|
}
|
|
66
|
-
|
|
67
|
-
for (let i = 0; i < binary.length; i++)
|
|
68
|
-
bytes[i] = binary.charCodeAt(i);
|
|
69
|
-
return bytes;
|
|
70
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
70
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nohead/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Official TypeScript SDK for the Nohead API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://nohead.io",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"eslint": "^10.12.0",
|
|
62
62
|
"globals": "^17.13.0",
|
|
63
63
|
"prettier": "^3.9.9",
|
|
64
|
+
"standardwebhooks": "^1.1.1",
|
|
64
65
|
"typescript": "^6.0.3",
|
|
65
66
|
"typescript-eslint": "^8.71.0",
|
|
66
67
|
"vitest": "^5.0.3"
|