@highlightxyz/sdk 0.3.0 → 0.5.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/lib/v1/media.d.ts CHANGED
@@ -1,4 +1,138 @@
1
- import { type MediaAsset, StorageRole } from "./gen/types.gen.js";
1
+ import { type MediaAsset, type MediaStorageLocation, StorageRole } from "./gen/types.gen.js";
2
+ export declare const MEDIA_IMAGE_VARIANT_QUERY_PARAMETER = "v";
3
+ export declare const MEDIA_IMAGE_MAX_INPUT_BYTES = 20000000;
4
+ export type MediaHashSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>;
5
+ /**
6
+ * Compute the upload contract's canonical SHA-256 without collecting the
7
+ * complete file in memory. Browser callers pass `File.stream()`; Node callers
8
+ * can pass `createReadStream(path)` directly.
9
+ */
10
+ export declare const sha256MediaContent: (source: MediaHashSource, options?: {
11
+ signal?: AbortSignal | undefined;
12
+ onProgress?: ((bytesHashed: number) => void) | undefined;
13
+ } | undefined) => Promise<`sha256:${string}`>;
14
+ export type MediaImageVariantDefinition = {
15
+ queryValue: string;
16
+ width: number;
17
+ height?: number;
18
+ fit: "cover" | "scale-down";
19
+ quality: number;
20
+ responsive: boolean;
21
+ };
22
+ /**
23
+ * Public, versioned image transformation contract shared by the delivery
24
+ * Worker and SDK consumers. Existing query values and dimensions are immutable;
25
+ * a changed transform must use a new query value.
26
+ */
27
+ export declare const MEDIA_IMAGE_VARIANTS: {
28
+ readonly avatar: {
29
+ readonly queryValue: "avatar-v1";
30
+ readonly width: 256;
31
+ readonly height: 256;
32
+ readonly fit: "cover";
33
+ readonly quality: 75;
34
+ readonly responsive: false;
35
+ };
36
+ readonly thumb: {
37
+ readonly queryValue: "thumb-v1";
38
+ readonly width: 240;
39
+ readonly height: 240;
40
+ readonly fit: "cover";
41
+ readonly quality: 75;
42
+ readonly responsive: false;
43
+ };
44
+ readonly small: {
45
+ readonly queryValue: "small-v1";
46
+ readonly width: 320;
47
+ readonly fit: "scale-down";
48
+ readonly quality: 78;
49
+ readonly responsive: true;
50
+ };
51
+ readonly card: {
52
+ readonly queryValue: "card-v1";
53
+ readonly width: 640;
54
+ readonly height: 640;
55
+ readonly fit: "cover";
56
+ readonly quality: 80;
57
+ readonly responsive: false;
58
+ };
59
+ readonly preview: {
60
+ readonly queryValue: "preview-v1";
61
+ readonly width: 960;
62
+ readonly fit: "scale-down";
63
+ readonly quality: 82;
64
+ readonly responsive: true;
65
+ };
66
+ readonly hero: {
67
+ readonly queryValue: "hero-v1";
68
+ readonly width: 1600;
69
+ readonly fit: "scale-down";
70
+ readonly quality: 85;
71
+ readonly responsive: true;
72
+ };
73
+ readonly og: {
74
+ readonly queryValue: "og-v1";
75
+ readonly width: 1200;
76
+ readonly height: 630;
77
+ readonly fit: "cover";
78
+ readonly quality: 85;
79
+ readonly responsive: false;
80
+ };
81
+ readonly full: {
82
+ readonly queryValue: "full-v1";
83
+ readonly width: 4096;
84
+ readonly fit: "scale-down";
85
+ readonly quality: 90;
86
+ readonly responsive: true;
87
+ };
88
+ };
89
+ export type MediaImageVariant = keyof typeof MEDIA_IMAGE_VARIANTS;
90
+ export declare const RESPONSIVE_MEDIA_IMAGE_VARIANTS: readonly ["small", "preview", "hero", "full"];
91
+ export declare const isMediaImageContentType: (value: string | null | undefined) => boolean;
92
+ export declare const mediaImageVariantFromQuery: (value: string) => MediaImageVariantDefinition | null;
93
+ export declare const mediaImageVariantUrl: (sourceUrl: string, variant: "avatar" | "card" | "full" | "hero" | "og" | "preview" | "small" | "thumb") => string;
94
+ export declare const responsiveMediaImageVariants: (target: "avatar" | "card" | "full" | "hero" | "og" | "preview" | "small" | "thumb") => readonly ("avatar" | "card" | "full" | "hero" | "og" | "preview" | "small" | "thumb")[];
95
+ /**
96
+ * True only for public URLs owned by Highlight's image-capable delivery
97
+ * Worker. The localhost forms mirror the Worker's explicit development
98
+ * namespaces.
99
+ */
100
+ export declare const isHighlightMediaDeliveryUrl: (value: string, hosts?: MediaDeliveryHosts) => boolean;
101
+ /**
102
+ * Client-side preflight for requesting a named image variant. A zero file size
103
+ * is currently the legacy importer's "unknown" sentinel; the delivery Worker
104
+ * still validates the actual R2 Content-Length before invoking Images.
105
+ */
106
+ export declare const isMediaImageTransformable: (media: MediaAsset, source: Pick<MediaStorageLocation, "provider"> & {
107
+ url: string;
108
+ }, hosts?: MediaDeliveryHosts) => boolean;
109
+ /**
110
+ * Public hosts used to turn a storage location into a URL. Fetch them from
111
+ * `config.mediaHosts` so a deployment can move a host without every consumer
112
+ * shipping a new SDK; the defaults below are production and exist only so
113
+ * simple consumers work without a config round-trip.
114
+ */
115
+ export type MediaDeliveryHosts = {
116
+ assets: string;
117
+ metadata: string;
118
+ legacyAssets: string;
119
+ arweaveGateway: string;
120
+ ipfsGateway: string;
121
+ };
122
+ export declare const DEFAULT_MEDIA_DELIVERY_HOSTS: MediaDeliveryHosts;
123
+ /**
124
+ * The URL to fetch a media location from.
125
+ *
126
+ * R2 URLs are composed from the asset's own identity — a File is `/{mediaId}`,
127
+ * a directory child is `/{parentId}/{path}` — never by parsing the storage key.
128
+ * That is why a stored key can change shape without any URL changing, and why
129
+ * assets written under older key layouts resolve identically.
130
+ *
131
+ * This answers "what should the browser fetch right now". It is deliberately
132
+ * not the backend's `artifactUri`, which answers "what immutable URI do we bake
133
+ * into an on-chain document" — Archive-first and permanent.
134
+ */
135
+ export declare const mediaUrl: (media: Pick<MediaAsset, "id" | "kind" | "parentId" | "path">, location: Pick<MediaStorageLocation, "provider" | "reference">, hosts?: MediaDeliveryHosts) => string | null;
2
136
  /**
3
137
  * Resolve the preferred public URL for a media asset. Replaces the old
4
138
  * `MediaAsset.url` field — the preference is now explicit and opt-in rather
@@ -9,6 +143,7 @@ import { type MediaAsset, StorageRole } from "./gen/types.gen.js";
9
143
  */
10
144
  export declare const resolveMediaUrl: (media: MediaAsset | null | undefined, options?: {
11
145
  prefer?: StorageRole[] | StorageRole | undefined;
146
+ hosts?: MediaDeliveryHosts | undefined;
12
147
  } | undefined) => string | null;
13
148
  /**
14
149
  * Resolve the renderable URL for a generative code bundle (a Directory media
@@ -27,5 +162,6 @@ export declare const resolveMediaUrl: (media: MediaAsset | null | undefined, opt
27
162
  export declare const resolveRenderUrl: (media: MediaAsset | null | undefined, options?: {
28
163
  entryPath?: string | undefined;
29
164
  params?: Record<string, string> | undefined;
165
+ hosts?: MediaDeliveryHosts | undefined;
30
166
  } | undefined) => string | null;
31
167
  //# sourceMappingURL=media.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/v1/media.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,UAAU,EAGf,WAAW,EACZ,MAAM,oBAAoB,CAAC;AAwB5B;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;+BAW3B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB;;;+BAuB5B,CAAC"}
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/v1/media.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,oBAAoB,EAIzB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,mCAAmC,MAAM,CAAC;AACvD,eAAO,MAAM,2BAA2B,WAAa,CAAC;AAEtD,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;AAKrF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;6CAkC9B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,GAAG,YAAY,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6D+B,CAAC;AAEjE,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,oBAAoB,CAAC;AAElE,eAAO,MAAM,+BAA+B,+CAKK,CAAC;AAclD,eAAO,MAAM,uBAAuB,+CAGnC,CAAC;AAEF,eAAO,MAAM,0BAA0B,uDAItC,CAAC;AAEF,eAAO,MAAM,oBAAoB,oHAOhC,CAAC;AAEF,eAAO,MAAM,4BAA4B,iLAQxC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,wDAmBvC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB;;yCAgBrC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,4BAA4B,EAAE,kBAM1C,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,6KA2BpB,CAAC;AAwBF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;;+BAY3B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB;;;;+BA0B5B,CAAC"}
package/lib/v1/media.js CHANGED
@@ -1,11 +1,241 @@
1
1
  // Hand-written media URL helpers (not generated — do not place under ./gen).
2
2
  //
3
- // The backend resolves each storage location to an absolute `url` (it owns the
4
- // host config + R2 key layout). These helpers do the *selection & composition*
5
- // on top of those resolved per-location URLs: picking a location by role and,
6
- // for bundle directories, addressing the entry file. They never parse a
7
- // `reference` or know anything about deployment hosts.
8
- import { LocationStatus, StorageRole, } from "./gen/types.gen.js";
3
+ // The API returns storage locations as `provider` + `reference`; turning those
4
+ // into a URL is the consumer's job, and this is where it happens. A location's
5
+ // URL is composed from the asset's own identity, so the backend's storage key
6
+ // layout is invisible here and assets written under older layouts resolve
7
+ // identically.
8
+ //
9
+ // These helpers answer "what should the client fetch right now" — Origin-first,
10
+ // with the caller free to layer gateway failover on top. The backend's
11
+ // `artifactUri` answers a different question ("what immutable URI do we bake
12
+ // into an on-chain document") and is deliberately not shared with this path.
13
+ import { sha256 } from "@noble/hashes/sha2";
14
+ import { bytesToHex } from "@noble/hashes/utils";
15
+ import { LocationStatus, MediaKind, StorageProvider, StorageRole, } from "./gen/types.gen.js";
16
+ export const MEDIA_IMAGE_VARIANT_QUERY_PARAMETER = "v";
17
+ export const MEDIA_IMAGE_MAX_INPUT_BYTES = 20_000_000;
18
+ const isReadableStream = (source) => "getReader" in source;
19
+ /**
20
+ * Compute the upload contract's canonical SHA-256 without collecting the
21
+ * complete file in memory. Browser callers pass `File.stream()`; Node callers
22
+ * can pass `createReadStream(path)` directly.
23
+ */
24
+ export const sha256MediaContent = async (source, options) => {
25
+ const hasher = sha256.create();
26
+ let bytesHashed = 0;
27
+ const update = (chunk) => {
28
+ if (options?.signal?.aborted) {
29
+ throw new DOMException("Hashing aborted.", "AbortError");
30
+ }
31
+ hasher.update(chunk);
32
+ bytesHashed += chunk.byteLength;
33
+ options?.onProgress?.(bytesHashed);
34
+ };
35
+ if (isReadableStream(source)) {
36
+ const reader = source.getReader();
37
+ try {
38
+ while (true) {
39
+ const result = await reader.read();
40
+ if (result.done)
41
+ break;
42
+ update(result.value);
43
+ }
44
+ }
45
+ finally {
46
+ reader.releaseLock();
47
+ }
48
+ }
49
+ else {
50
+ for await (const chunk of source)
51
+ update(chunk);
52
+ }
53
+ return `sha256:${bytesToHex(hasher.digest())}`;
54
+ };
55
+ /**
56
+ * Public, versioned image transformation contract shared by the delivery
57
+ * Worker and SDK consumers. Existing query values and dimensions are immutable;
58
+ * a changed transform must use a new query value.
59
+ */
60
+ export const MEDIA_IMAGE_VARIANTS = {
61
+ avatar: {
62
+ queryValue: "avatar-v1",
63
+ width: 256,
64
+ height: 256,
65
+ fit: "cover",
66
+ quality: 75,
67
+ responsive: false,
68
+ },
69
+ thumb: {
70
+ queryValue: "thumb-v1",
71
+ width: 240,
72
+ height: 240,
73
+ fit: "cover",
74
+ quality: 75,
75
+ responsive: false,
76
+ },
77
+ small: {
78
+ queryValue: "small-v1",
79
+ width: 320,
80
+ fit: "scale-down",
81
+ quality: 78,
82
+ responsive: true,
83
+ },
84
+ card: {
85
+ queryValue: "card-v1",
86
+ width: 640,
87
+ height: 640,
88
+ fit: "cover",
89
+ quality: 80,
90
+ responsive: false,
91
+ },
92
+ preview: {
93
+ queryValue: "preview-v1",
94
+ width: 960,
95
+ fit: "scale-down",
96
+ quality: 82,
97
+ responsive: true,
98
+ },
99
+ hero: {
100
+ queryValue: "hero-v1",
101
+ width: 1600,
102
+ fit: "scale-down",
103
+ quality: 85,
104
+ responsive: true,
105
+ },
106
+ og: {
107
+ queryValue: "og-v1",
108
+ width: 1200,
109
+ height: 630,
110
+ fit: "cover",
111
+ quality: 85,
112
+ responsive: false,
113
+ },
114
+ full: {
115
+ queryValue: "full-v1",
116
+ width: 4096,
117
+ fit: "scale-down",
118
+ quality: 90,
119
+ responsive: true,
120
+ },
121
+ };
122
+ export const RESPONSIVE_MEDIA_IMAGE_VARIANTS = [
123
+ "small",
124
+ "preview",
125
+ "hero",
126
+ "full",
127
+ ];
128
+ const SUPPORTED_IMAGE_CONTENT_TYPES = new Set([
129
+ "image/avif",
130
+ "image/gif",
131
+ "image/heic",
132
+ "image/heif",
133
+ "image/jpeg",
134
+ "image/jpg",
135
+ "image/png",
136
+ "image/webp",
137
+ ]);
138
+ const MODERN_ASSET_PATH = /^\/[0-9a-fA-F-]+(?:\/.*)?$/;
139
+ export const isMediaImageContentType = (value) => {
140
+ const contentType = value?.split(";")[0]?.trim().toLowerCase();
141
+ return contentType !== undefined && SUPPORTED_IMAGE_CONTENT_TYPES.has(contentType);
142
+ };
143
+ export const mediaImageVariantFromQuery = (value) => {
144
+ return (Object.values(MEDIA_IMAGE_VARIANTS).find((variant) => variant.queryValue === value) ?? null);
145
+ };
146
+ export const mediaImageVariantUrl = (sourceUrl, variant) => {
147
+ const url = new URL(sourceUrl);
148
+ url.searchParams.set(MEDIA_IMAGE_VARIANT_QUERY_PARAMETER, MEDIA_IMAGE_VARIANTS[variant].queryValue);
149
+ return url.toString();
150
+ };
151
+ export const responsiveMediaImageVariants = (target) => {
152
+ const targetDefinition = MEDIA_IMAGE_VARIANTS[target];
153
+ if (!targetDefinition.responsive)
154
+ return [];
155
+ return RESPONSIVE_MEDIA_IMAGE_VARIANTS.filter((variant) => MEDIA_IMAGE_VARIANTS[variant].width <= targetDefinition.width);
156
+ };
157
+ /**
158
+ * True only for public URLs owned by Highlight's image-capable delivery
159
+ * Worker. The localhost forms mirror the Worker's explicit development
160
+ * namespaces.
161
+ */
162
+ export const isHighlightMediaDeliveryUrl = (value, hosts = DEFAULT_MEDIA_DELIVERY_HOSTS) => {
163
+ let url;
164
+ try {
165
+ url = new URL(value);
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ if (url.protocol !== "https:" && url.protocol !== "http:")
171
+ return false;
172
+ if (value.startsWith(`${hosts.assets}/`)) {
173
+ return MODERN_ASSET_PATH.test(value.slice(hosts.assets.length));
174
+ }
175
+ if (value.startsWith(`${hosts.legacyAssets}/`)) {
176
+ return value.slice(hosts.legacyAssets.length).startsWith("/main/");
177
+ }
178
+ return false;
179
+ };
180
+ /**
181
+ * Client-side preflight for requesting a named image variant. A zero file size
182
+ * is currently the legacy importer's "unknown" sentinel; the delivery Worker
183
+ * still validates the actual R2 Content-Length before invoking Images.
184
+ */
185
+ export const isMediaImageTransformable = (media, source, hosts = DEFAULT_MEDIA_DELIVERY_HOSTS) => {
186
+ const sizeIsEligible = media.fileSize === null ||
187
+ media.fileSize === 0 ||
188
+ media.fileSize <= MEDIA_IMAGE_MAX_INPUT_BYTES;
189
+ return (source.provider === StorageProvider.R2 &&
190
+ media.kind === MediaKind.File &&
191
+ isMediaImageContentType(media.mimeType) &&
192
+ sizeIsEligible &&
193
+ isHighlightMediaDeliveryUrl(source.url, hosts));
194
+ };
195
+ export const DEFAULT_MEDIA_DELIVERY_HOSTS = {
196
+ assets: "https://assets.highlight.xyz",
197
+ metadata: "https://metadata.highlight.xyz",
198
+ legacyAssets: "https://highlight-creator-assets.highlight.xyz",
199
+ arweaveGateway: "https://arweave.net",
200
+ ipfsGateway: "https://ipfs.io",
201
+ };
202
+ const LEGACY_REFERENCE = /^main\//;
203
+ /**
204
+ * The URL to fetch a media location from.
205
+ *
206
+ * R2 URLs are composed from the asset's own identity — a File is `/{mediaId}`,
207
+ * a directory child is `/{parentId}/{path}` — never by parsing the storage key.
208
+ * That is why a stored key can change shape without any URL changing, and why
209
+ * assets written under older key layouts resolve identically.
210
+ *
211
+ * This answers "what should the browser fetch right now". It is deliberately
212
+ * not the backend's `artifactUri`, which answers "what immutable URI do we bake
213
+ * into an on-chain document" — Archive-first and permanent.
214
+ */
215
+ export const mediaUrl = (media, location, hosts = DEFAULT_MEDIA_DELIVERY_HOSTS) => {
216
+ switch (location.provider) {
217
+ case StorageProvider.R2: {
218
+ // Legacy keys are the one case where the key *is* the public path.
219
+ if (LEGACY_REFERENCE.test(location.reference)) {
220
+ return `${hosts.legacyAssets}/${location.reference}`;
221
+ }
222
+ if (media.parentId && media.path) {
223
+ return `${hosts.assets}/${media.parentId}/${media.path}`;
224
+ }
225
+ return media.kind === MediaKind.Directory
226
+ ? `${hosts.assets}/${media.id}/`
227
+ : `${hosts.assets}/${media.id}`;
228
+ }
229
+ case StorageProvider.Arweave:
230
+ return `${hosts.arweaveGateway}/${location.reference}`;
231
+ case StorageProvider.Ipfs:
232
+ return `${hosts.ipfsGateway}/ipfs/${location.reference.replace(/^ipfs:\/\//, "").replace(/^\//, "")}`;
233
+ case StorageProvider.External:
234
+ return location.reference;
235
+ default:
236
+ return null;
237
+ }
238
+ };
9
239
  const DEFAULT_PREFERENCE = [
10
240
  StorageRole.Archive,
11
241
  StorageRole.Origin,
@@ -15,7 +245,7 @@ const DEFAULT_PREFERENCE = [
15
245
  const DEFAULT_BUNDLE_ENTRY = "index.html";
16
246
  const pickLocation = (media, preference) => {
17
247
  for (const role of preference) {
18
- const location = media.locations.find((l) => l.role === role && l.status === LocationStatus.Succeeded && l.url !== null);
248
+ const location = media.locations.find((l) => l.role === role && l.status === LocationStatus.Succeeded);
19
249
  if (location)
20
250
  return location;
21
251
  }
@@ -37,7 +267,8 @@ export const resolveMediaUrl = (media, options) => {
37
267
  ? options.prefer
38
268
  : [options.prefer]
39
269
  : DEFAULT_PREFERENCE;
40
- return pickLocation(media, preference)?.url ?? null;
270
+ const location = pickLocation(media, preference);
271
+ return location ? mediaUrl(media, location, options?.hosts) : null;
41
272
  };
42
273
  /**
43
274
  * Resolve the renderable URL for a generative code bundle (a Directory media
@@ -54,7 +285,10 @@ export const resolveMediaUrl = (media, options) => {
54
285
  * meaningful.
55
286
  */
56
287
  export const resolveRenderUrl = (media, options) => {
57
- const base = resolveMediaUrl(media, { prefer: [StorageRole.Origin, StorageRole.Mirror] });
288
+ const base = resolveMediaUrl(media, {
289
+ prefer: [StorageRole.Origin, StorageRole.Mirror],
290
+ hosts: options?.hosts,
291
+ });
58
292
  if (!base)
59
293
  return null;
60
294
  // Resolve the entry as a child of the directory base. `new URL(entry, base)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highlightxyz/sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Official TypeScript SDK for the Highlight API",
5
5
  "keywords": [
6
6
  "ethereum",
@@ -39,6 +39,9 @@
39
39
  "publishConfig": {
40
40
  "access": "public"
41
41
  },
42
+ "dependencies": {
43
+ "@noble/hashes": "^1.8.0"
44
+ },
42
45
  "main": "./lib/index.js",
43
46
  "module": "./lib/index.js",
44
47
  "types": "./lib/index.d.ts"