@quikturn/logos 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quikturn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,498 @@
1
+ # @quikturn/logos
2
+
3
+ > TypeScript SDK for the Quikturn Logos API -- fetch company logos with type safety.
4
+
5
+ ## Features
6
+
7
+ - **Zero-dependency URL builder** -- universal, works in any JavaScript runtime
8
+ - **Browser client** -- blob URL management, retry/backoff, auto-scrape polling, event emission
9
+ - **Server client** -- Buffer output, ReadableStream streaming, concurrent batch operations
10
+ - **Full TypeScript support** -- strict types, discriminated union error codes, generic response shapes
11
+ - **Tree-shakeable** -- ESM and CJS dual builds; import only what you need
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # pnpm (recommended)
17
+ pnpm add @quikturn/logos
18
+
19
+ # npm
20
+ npm install @quikturn/logos
21
+
22
+ # yarn
23
+ yarn add @quikturn/logos
24
+ ```
25
+
26
+ **Requirements:** Node.js >= 18
27
+
28
+ ## Quick Start
29
+
30
+ ### URL Builder (Universal)
31
+
32
+ The URL builder runs everywhere -- browsers, Node.js, edge runtimes -- with zero dependencies and no network calls.
33
+
34
+ ```ts
35
+ import { logoUrl } from "@quikturn/logos";
36
+
37
+ // Simple usage
38
+ const url = logoUrl("github.com");
39
+ // => "https://logos.getquikturn.io/github.com"
40
+
41
+ // With options
42
+ const customUrl = logoUrl("stripe.com", {
43
+ token: "qt_abc123",
44
+ size: 256,
45
+ format: "webp",
46
+ greyscale: true,
47
+ theme: "dark",
48
+ });
49
+ // => "https://logos.getquikturn.io/stripe.com?token=qt_abc123&size=256&greyscale=1&theme=dark&format=webp"
50
+ ```
51
+
52
+ ### Browser Client
53
+
54
+ ```ts
55
+ import { QuikturnLogos } from "@quikturn/logos/client";
56
+
57
+ const client = new QuikturnLogos({ token: "qt_your_publishable_key" });
58
+
59
+ // Fetch a logo as a blob URL (ready for <img src>)
60
+ const { url, blob, contentType, metadata } = await client.get("github.com", {
61
+ size: 256,
62
+ format: "webp",
63
+ });
64
+
65
+ document.querySelector("img")!.src = url;
66
+
67
+ // Listen for rate limit warnings
68
+ client.on("rateLimitWarning", (remaining, limit) => {
69
+ console.warn(`Rate limit: ${remaining}/${limit} requests remaining`);
70
+ });
71
+
72
+ // Clean up blob URLs when done
73
+ client.destroy();
74
+ ```
75
+
76
+ ### Server Client
77
+
78
+ ```ts
79
+ import { QuikturnLogos } from "@quikturn/logos/server";
80
+
81
+ const client = new QuikturnLogos({ secretKey: "sk_your_secret_key" });
82
+
83
+ // Fetch a logo as a Buffer
84
+ const { buffer, contentType, metadata } = await client.get("github.com", {
85
+ size: 512,
86
+ format: "png",
87
+ });
88
+
89
+ // Batch fetch multiple logos
90
+ for await (const result of client.getMany(["github.com", "stripe.com", "vercel.com"])) {
91
+ if (result.success) {
92
+ console.log(`${result.domain}: ${result.buffer!.byteLength} bytes`);
93
+ } else {
94
+ console.error(`${result.domain}: ${result.error!.message}`);
95
+ }
96
+ }
97
+
98
+ // Stream a logo to a file
99
+ import { createWriteStream } from "node:fs";
100
+ import { Readable } from "node:stream";
101
+
102
+ const stream = await client.getStream("github.com", { format: "png" });
103
+ Readable.fromWeb(stream).pipe(createWriteStream("logo.png"));
104
+ ```
105
+
106
+ ## API Reference
107
+
108
+ ### Universal (`@quikturn/logos`)
109
+
110
+ The universal entry point exports the URL builder, types, constants, error classes, and header parser. No network calls are made from this module.
111
+
112
+ #### `logoUrl(domain, options?)`
113
+
114
+ Constructs a fully-qualified Logos API URL. Pure function with no side effects.
115
+
116
+ | Parameter | Type | Description |
117
+ |-----------|------|-------------|
118
+ | `domain` | `string` | Domain to fetch a logo for (e.g. `"github.com"`) |
119
+ | `options` | `LogoRequestOptions` | Optional request parameters |
120
+
121
+ **Returns:** `string` -- fully-qualified URL
122
+
123
+ **Throws:** `DomainValidationError` if the domain fails RFC 1035/1123 validation
124
+
125
+ ##### `LogoRequestOptions`
126
+
127
+ | Property | Type | Default | Description |
128
+ |----------|------|---------|-------------|
129
+ | `token` | `string` | -- | Publishable key (`qt_`/`pk_`) appended as a query parameter |
130
+ | `size` | `number` | `128` | Output width in pixels, clamped to `1..800` (publishable) or `1..1200` (secret) |
131
+ | `width` | `number` | `128` | Alias for `size` |
132
+ | `greyscale` | `boolean` | `false` | When `true`, applies saturation: 0 transformation |
133
+ | `theme` | `"light" \| "dark"` | -- | Gamma curve adjustment (`"light"` = 0.9, `"dark"` = 1.12) |
134
+ | `format` | `SupportedOutputFormat \| FormatShorthand` | `"image/png"` | Output image format |
135
+ | `autoScrape` | `boolean` | `false` | Trigger background scrape if logo is not found |
136
+ | `baseUrl` | `string` | `"https://logos.getquikturn.io"` | Override the API base URL |
137
+
138
+ #### Types
139
+
140
+ ```ts
141
+ import type {
142
+ // Key & Auth
143
+ KeyType, // "publishable" | "secret"
144
+ KeyPrefix, // "qt_" | "pk_" | "sk_"
145
+ Tier, // "free" | "launch" | "growth" | "enterprise"
146
+ TokenStatus, // "active" | "suspended" | "revoked"
147
+
148
+ // Request
149
+ ThemeOption, // "light" | "dark"
150
+ SupportedOutputFormat, // "image/png" | "image/jpeg" | "image/webp" | "image/avif"
151
+ FormatShorthand, // "png" | "jpeg" | "webp" | "avif"
152
+ LogoRequestOptions,
153
+
154
+ // Response
155
+ LogoMetadata,
156
+ BrowserLogoResponse,
157
+ ServerLogoResponse,
158
+
159
+ // Auto-scrape
160
+ ScrapeJob,
161
+ ScrapePendingResponse,
162
+ ScrapeJobStatus, // "pending" | "complete" | "failed"
163
+ ScrapeProgressEvent,
164
+
165
+ // Attribution (free tier)
166
+ AttributionStatus,
167
+ AttributionInfo,
168
+
169
+ // Error codes
170
+ LogoErrorCode,
171
+ } from "@quikturn/logos";
172
+ ```
173
+
174
+ #### Constants
175
+
176
+ | Constant | Value | Description |
177
+ |----------|-------|-------------|
178
+ | `BASE_URL` | `"https://logos.getquikturn.io"` | Root API endpoint |
179
+ | `DEFAULT_WIDTH` | `128` | Default logo width (px) |
180
+ | `MAX_WIDTH` | `800` | Max width for publishable keys |
181
+ | `MAX_WIDTH_SERVER` | `1200` | Max width for secret keys |
182
+ | `DEFAULT_FORMAT` | `"image/png"` | Default output MIME type |
183
+ | `SUPPORTED_FORMATS` | `Set<SupportedOutputFormat>` | All supported MIME types |
184
+ | `FORMAT_ALIASES` | `Record<FormatShorthand, SupportedOutputFormat>` | Shorthand-to-MIME mapping |
185
+ | `RATE_LIMITS` | `Record<Tier, { requests, windowSeconds }>` | Per-tier publishable rate limits |
186
+ | `SERVER_RATE_LIMITS` | `Record<Tier, { requests, windowSeconds }>` | Per-tier server rate limits |
187
+ | `MONTHLY_LIMITS` | `Record<Tier, number>` | Monthly request quotas |
188
+ | `TIERS` | `readonly Tier[]` | All tiers as a runtime array |
189
+ | `KEY_TYPES` | `readonly KeyType[]` | All key types as a runtime array |
190
+
191
+ #### `parseLogoHeaders(headers)`
192
+
193
+ Parses a `Headers` object from a fetch `Response` into a structured `LogoMetadata` object.
194
+
195
+ #### `parseRetryAfter(headers)`
196
+
197
+ Extracts the `Retry-After` header value in seconds. Returns `number | null`.
198
+
199
+ ---
200
+
201
+ ### Browser Client (`@quikturn/logos/client`)
202
+
203
+ #### `new QuikturnLogos(options)`
204
+
205
+ | Option | Type | Default | Description |
206
+ |--------|------|---------|-------------|
207
+ | `token` | `string` | **required** | Publishable key (`qt_`/`pk_` prefix). Server keys (`sk_`) are rejected. |
208
+ | `baseUrl` | `string` | `"https://logos.getquikturn.io"` | Override the API base URL |
209
+ | `maxRetries` | `number` | `2` | Max retry attempts for rate-limited/server-error responses |
210
+
211
+ #### `client.get(domain, options?)`
212
+
213
+ Fetches a logo and returns a `BrowserLogoResponse`.
214
+
215
+ | Option | Type | Default | Description |
216
+ |--------|------|---------|-------------|
217
+ | `size` | `number` | `128` | Output width in pixels |
218
+ | `width` | `number` | `128` | Alias for `size` |
219
+ | `greyscale` | `boolean` | `false` | Greyscale transformation |
220
+ | `theme` | `"light" \| "dark"` | -- | Gamma curve adjustment |
221
+ | `format` | `SupportedOutputFormat \| FormatShorthand` | `"image/png"` | Output format |
222
+ | `autoScrape` | `boolean` | `false` | Enable auto-scrape polling |
223
+ | `scrapeTimeout` | `number` | -- | Max time (ms) to wait for scrape completion |
224
+ | `onScrapeProgress` | `(event: ScrapeProgressEvent) => void` | -- | Callback for scrape progress |
225
+ | `signal` | `AbortSignal` | -- | Cancel the request |
226
+
227
+ **Returns:** `Promise<BrowserLogoResponse>`
228
+
229
+ ```ts
230
+ interface BrowserLogoResponse {
231
+ url: string; // blob: object URL for <img src>
232
+ blob: Blob; // Raw image Blob
233
+ contentType: string; // e.g. "image/webp"
234
+ metadata: LogoMetadata;
235
+ }
236
+ ```
237
+
238
+ #### `client.getUrl(domain, options?)`
239
+
240
+ Returns a plain URL string without making a network request. Useful for `<img>` tags, CSS `background-image`, or preloading hints.
241
+
242
+ **Returns:** `string`
243
+
244
+ #### `client.on(event, handler)` / `client.off(event, handler)`
245
+
246
+ Register or remove event listeners.
247
+
248
+ | Event | Handler Signature | Description |
249
+ |-------|-------------------|-------------|
250
+ | `"rateLimitWarning"` | `(remaining: number, limit: number) => void` | Fires when rate limit is approaching |
251
+ | `"quotaWarning"` | `(remaining: number, limit: number) => void` | Fires when monthly quota is approaching |
252
+
253
+ #### `client.destroy()`
254
+
255
+ Revokes all tracked `blob:` object URLs to free memory and removes all event listeners. Call this when the client is no longer needed to prevent memory leaks in long-lived browser sessions.
256
+
257
+ ---
258
+
259
+ ### Server Client (`@quikturn/logos/server`)
260
+
261
+ #### `new QuikturnLogos(options)`
262
+
263
+ | Option | Type | Default | Description |
264
+ |--------|------|---------|-------------|
265
+ | `secretKey` | `string` | **required** | Secret key (`sk_` prefix). Publishable keys (`qt_`/`pk_`) are rejected. |
266
+ | `baseUrl` | `string` | `"https://logos.getquikturn.io"` | Override the API base URL |
267
+ | `maxRetries` | `number` | `2` | Max retry attempts for rate-limited/server-error responses |
268
+
269
+ #### `client.get(domain, options?)`
270
+
271
+ Fetches a logo and returns a `ServerLogoResponse`.
272
+
273
+ Accepts the same options as the browser client's `get()` method.
274
+
275
+ **Returns:** `Promise<ServerLogoResponse>`
276
+
277
+ ```ts
278
+ interface ServerLogoResponse {
279
+ buffer: Buffer; // Raw image bytes
280
+ contentType: string; // e.g. "image/png"
281
+ metadata: LogoMetadata;
282
+ }
283
+ ```
284
+
285
+ #### `client.getMany(domains, options?)`
286
+
287
+ Fetches logos for multiple domains with concurrency control. Yields results in the same order as the input array.
288
+
289
+ | Option | Type | Default | Description |
290
+ |--------|------|---------|-------------|
291
+ | `concurrency` | `number` | `5` | Maximum parallel fetches |
292
+ | `size` | `number` | `128` | Output width in pixels |
293
+ | `greyscale` | `boolean` | `false` | Greyscale transformation |
294
+ | `theme` | `"light" \| "dark"` | -- | Gamma curve adjustment |
295
+ | `format` | `SupportedOutputFormat \| FormatShorthand` | `"image/png"` | Output format |
296
+ | `signal` | `AbortSignal` | -- | Cancel remaining batch items |
297
+ | `continueOnError` | `boolean` | `true` | Capture errors per-domain instead of aborting the batch |
298
+
299
+ **Returns:** `AsyncGenerator<BatchResult>`
300
+
301
+ ```ts
302
+ interface BatchResult {
303
+ domain: string;
304
+ success: boolean;
305
+ buffer?: Buffer;
306
+ contentType?: string;
307
+ metadata?: LogoMetadata;
308
+ error?: LogoError;
309
+ }
310
+ ```
311
+
312
+ #### `client.getStream(domain, options?)`
313
+
314
+ Returns the raw response body as a `ReadableStream`. Useful for piping to a file or HTTP response without buffering the entire image in memory.
315
+
316
+ Accepts the same options as `get()`.
317
+
318
+ **Returns:** `Promise<ReadableStream>`
319
+
320
+ #### `client.getUrl(domain, options?)`
321
+
322
+ Returns a plain URL string with the secret key included as a token query parameter.
323
+
324
+ **Returns:** `string`
325
+
326
+ #### `client.on(event, handler)` / `client.off(event, handler)`
327
+
328
+ Same event interface as the browser client. Supports `"rateLimitWarning"` and `"quotaWarning"` events.
329
+
330
+ ---
331
+
332
+ ### Error Classes
333
+
334
+ All SDK errors extend `LogoError`, which extends the native `Error` class with a machine-readable `code` and an optional HTTP `status`.
335
+
336
+ ```ts
337
+ import {
338
+ LogoError,
339
+ DomainValidationError,
340
+ RateLimitError,
341
+ QuotaExceededError,
342
+ AuthenticationError,
343
+ ForbiddenError,
344
+ NotFoundError,
345
+ ScrapeTimeoutError,
346
+ BadRequestError,
347
+ } from "@quikturn/logos";
348
+ ```
349
+
350
+ | Error Class | Code | HTTP Status | Extra Properties |
351
+ |-------------|------|-------------|------------------|
352
+ | `LogoError` | varies | varies | `code: LogoErrorCode`, `status?: number` |
353
+ | `DomainValidationError` | `DOMAIN_VALIDATION_ERROR` | -- | `domain: string` |
354
+ | `AuthenticationError` | `AUTHENTICATION_ERROR` | 401 | -- |
355
+ | `ForbiddenError` | `FORBIDDEN_ERROR` | 403 | `reason: string` |
356
+ | `NotFoundError` | `NOT_FOUND_ERROR` | 404 | `domain: string` |
357
+ | `BadRequestError` | `BAD_REQUEST_ERROR` | 400 | -- |
358
+ | `RateLimitError` | `RATE_LIMIT_ERROR` | 429 | `retryAfter: number`, `remaining: number`, `resetAt: Date` |
359
+ | `QuotaExceededError` | `QUOTA_EXCEEDED_ERROR` | 429 | `retryAfter: number`, `limit: number`, `used: number` |
360
+ | `ScrapeTimeoutError` | `SCRAPE_TIMEOUT_ERROR` | -- | `jobId: string`, `elapsed: number` |
361
+
362
+ All error codes are typed via the `LogoErrorCode` discriminated union for exhaustive switch handling:
363
+
364
+ ```ts
365
+ import { LogoError } from "@quikturn/logos";
366
+ import type { LogoErrorCode } from "@quikturn/logos";
367
+
368
+ try {
369
+ const { url } = await client.get("example.com");
370
+ } catch (err) {
371
+ if (err instanceof LogoError) {
372
+ switch (err.code) {
373
+ case "RATE_LIMIT_ERROR":
374
+ // err is narrowed, handle backoff
375
+ break;
376
+ case "NOT_FOUND_ERROR":
377
+ // show placeholder
378
+ break;
379
+ // ... handle other cases
380
+ }
381
+ }
382
+ }
383
+ ```
384
+
385
+ ## Authentication
386
+
387
+ The Quikturn Logos API uses token-based authentication with two key types:
388
+
389
+ | Key Type | Prefix | Environment | Auth Method |
390
+ |----------|--------|-------------|-------------|
391
+ | **Publishable** | `qt_` or `pk_` | Browser | Query parameter (`?token=...`) |
392
+ | **Secret** | `sk_` | Server only | `Authorization: Bearer` header |
393
+
394
+ - **Publishable keys** are safe to expose in client-side code. They are passed as query parameters and support a max image width of 800px.
395
+ - **Secret keys** must never be exposed to the browser. They are sent via the `Authorization` header and support a max image width of 1200px.
396
+
397
+ ```ts
398
+ // Browser -- publishable key
399
+ import { QuikturnLogos } from "@quikturn/logos/client";
400
+ const client = new QuikturnLogos({ token: "qt_your_publishable_key" });
401
+
402
+ // Server -- secret key
403
+ import { QuikturnLogos } from "@quikturn/logos/server";
404
+ const client = new QuikturnLogos({ secretKey: "sk_your_secret_key" });
405
+ ```
406
+
407
+ ## Configuration
408
+
409
+ ### Custom Base URL
410
+
411
+ Override the API endpoint for testing, proxied environments, or self-hosted deployments:
412
+
413
+ ```ts
414
+ const client = new QuikturnLogos({
415
+ token: "qt_your_key",
416
+ baseUrl: "https://logos-proxy.your-company.com",
417
+ });
418
+ ```
419
+
420
+ ### Format Options
421
+
422
+ Four output formats are supported:
423
+
424
+ | Format | MIME Type | Shorthand |
425
+ |--------|-----------|-----------|
426
+ | PNG | `image/png` | `"png"` |
427
+ | JPEG | `image/jpeg` | `"jpeg"` |
428
+ | WebP | `image/webp` | `"webp"` |
429
+ | AVIF | `image/avif` | `"avif"` |
430
+
431
+ Both the full MIME type and the shorthand alias are accepted:
432
+
433
+ ```ts
434
+ // These are equivalent
435
+ client.get("github.com", { format: "image/webp" });
436
+ client.get("github.com", { format: "webp" });
437
+ ```
438
+
439
+ ### Theme Options
440
+
441
+ | Theme | Gamma | Use Case |
442
+ |-------|-------|----------|
443
+ | `"light"` | 0.9 | Light backgrounds |
444
+ | `"dark"` | 1.12 | Dark backgrounds |
445
+
446
+ ## Auto-Scrape
447
+
448
+ When a logo is not found in the database, the SDK can automatically trigger a background scrape and poll for completion.
449
+
450
+ **Flow:** Request with `autoScrape: true` --> API returns `202 Accepted` with a scrape job --> SDK polls the job URL --> Logo becomes available --> SDK returns the final image.
451
+
452
+ ```ts
453
+ const { url } = await client.get("brand-new-startup.com", {
454
+ autoScrape: true,
455
+ scrapeTimeout: 30_000, // wait up to 30 seconds
456
+ onScrapeProgress: (event) => {
457
+ console.log(`Scrape status: ${event.status}, progress: ${event.progress}%`);
458
+ },
459
+ });
460
+ ```
461
+
462
+ If the scrape does not complete within `scrapeTimeout`, a `ScrapeTimeoutError` is thrown.
463
+
464
+ ## Rate Limits & Quotas
465
+
466
+ ### Per-Minute Rate Limits (Publishable Keys)
467
+
468
+ | Tier | Requests/min | Window |
469
+ |------|-------------|--------|
470
+ | Free | 100 | 60s |
471
+ | Launch | 500 | 60s |
472
+ | Growth | 5,000 | 60s |
473
+ | Enterprise | 50,000 | 60s |
474
+
475
+ ### Per-Minute Rate Limits (Secret Keys)
476
+
477
+ | Tier | Requests/min | Window |
478
+ |------|-------------|--------|
479
+ | Launch | 1,000 | 60s |
480
+ | Growth | 10,000 | 60s |
481
+ | Enterprise | 100,000 | 60s |
482
+
483
+ > The free tier does not have server-side (secret key) access.
484
+
485
+ ### Monthly Quotas
486
+
487
+ | Tier | Monthly Limit |
488
+ |------|--------------|
489
+ | Free | 500,000 |
490
+ | Launch | 1,000,000 |
491
+ | Growth | 5,000,000 |
492
+ | Enterprise | 10,000,000 |
493
+
494
+ Rate limits are enforced by the API server. The SDK reads `X-RateLimit-Remaining`, `X-Quota-Remaining`, and `Retry-After` headers to provide warnings via the event system and automatic retry with backoff.
495
+
496
+ ## License
497
+
498
+ MIT