@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 +21 -0
- package/README.md +498 -0
- package/dist/client/index.cjs +726 -0
- package/dist/client/index.d.cts +399 -0
- package/dist/client/index.d.ts +399 -0
- package/dist/client/index.mjs +721 -0
- package/dist/index.cjs +319 -0
- package/dist/index.d.cts +437 -0
- package/dist/index.d.ts +437 -0
- package/dist/index.mjs +294 -0
- package/dist/server/index.cjs +858 -0
- package/dist/server/index.d.cts +423 -0
- package/dist/server/index.d.ts +423 -0
- package/dist/server/index.mjs +854 -0
- package/package.json +89 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var BASE_URL = "https://logos.getquikturn.io";
|
|
3
|
+
var DEFAULT_WIDTH = 128;
|
|
4
|
+
var MAX_WIDTH = 800;
|
|
5
|
+
var MAX_WIDTH_SERVER = 1200;
|
|
6
|
+
var DEFAULT_FORMAT = "image/png";
|
|
7
|
+
var SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
|
|
8
|
+
"image/png",
|
|
9
|
+
"image/jpeg",
|
|
10
|
+
"image/webp",
|
|
11
|
+
"image/avif"
|
|
12
|
+
]);
|
|
13
|
+
var FORMAT_ALIASES = {
|
|
14
|
+
png: "image/png",
|
|
15
|
+
jpeg: "image/jpeg",
|
|
16
|
+
webp: "image/webp",
|
|
17
|
+
avif: "image/avif"
|
|
18
|
+
};
|
|
19
|
+
var RATE_LIMITS = {
|
|
20
|
+
free: { requests: 100, windowSeconds: 60 },
|
|
21
|
+
launch: { requests: 500, windowSeconds: 60 },
|
|
22
|
+
growth: { requests: 5e3, windowSeconds: 60 },
|
|
23
|
+
enterprise: { requests: 5e4, windowSeconds: 60 }
|
|
24
|
+
};
|
|
25
|
+
var SERVER_RATE_LIMITS = {
|
|
26
|
+
launch: { requests: 1e3, windowSeconds: 60 },
|
|
27
|
+
growth: { requests: 1e4, windowSeconds: 60 },
|
|
28
|
+
enterprise: { requests: 1e5, windowSeconds: 60 }
|
|
29
|
+
};
|
|
30
|
+
var TIERS = ["free", "launch", "growth", "enterprise"];
|
|
31
|
+
var KEY_TYPES = ["publishable", "secret"];
|
|
32
|
+
var MONTHLY_LIMITS = {
|
|
33
|
+
free: 5e5,
|
|
34
|
+
launch: 1e6,
|
|
35
|
+
growth: 5e6,
|
|
36
|
+
enterprise: 1e7
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/errors.ts
|
|
40
|
+
var LogoError = class extends Error {
|
|
41
|
+
constructor(message, code, status) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "LogoError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.status = status;
|
|
46
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var DomainValidationError = class extends LogoError {
|
|
50
|
+
constructor(message, domain) {
|
|
51
|
+
super(message, "DOMAIN_VALIDATION_ERROR");
|
|
52
|
+
this.name = "DomainValidationError";
|
|
53
|
+
this.domain = domain;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var RateLimitError = class extends LogoError {
|
|
57
|
+
constructor(message, retryAfter, remaining, resetAt) {
|
|
58
|
+
super(message, "RATE_LIMIT_ERROR", 429);
|
|
59
|
+
this.name = "RateLimitError";
|
|
60
|
+
this.retryAfter = retryAfter;
|
|
61
|
+
this.remaining = remaining;
|
|
62
|
+
this.resetAt = resetAt;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var QuotaExceededError = class extends LogoError {
|
|
66
|
+
constructor(message, retryAfter, limit, used) {
|
|
67
|
+
super(message, "QUOTA_EXCEEDED_ERROR", 429);
|
|
68
|
+
this.name = "QuotaExceededError";
|
|
69
|
+
this.retryAfter = retryAfter;
|
|
70
|
+
this.limit = limit;
|
|
71
|
+
this.used = used;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var AuthenticationError = class extends LogoError {
|
|
75
|
+
constructor(message) {
|
|
76
|
+
super(message, "AUTHENTICATION_ERROR", 401);
|
|
77
|
+
this.name = "AuthenticationError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var ForbiddenError = class extends LogoError {
|
|
81
|
+
constructor(message, reason) {
|
|
82
|
+
super(message, "FORBIDDEN_ERROR", 403);
|
|
83
|
+
this.name = "ForbiddenError";
|
|
84
|
+
this.reason = reason;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var NotFoundError = class extends LogoError {
|
|
88
|
+
constructor(message, domain) {
|
|
89
|
+
super(message, "NOT_FOUND_ERROR", 404);
|
|
90
|
+
this.name = "NotFoundError";
|
|
91
|
+
this.domain = domain;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var ScrapeTimeoutError = class extends LogoError {
|
|
95
|
+
constructor(message, jobId, elapsed) {
|
|
96
|
+
super(message, "SCRAPE_TIMEOUT_ERROR");
|
|
97
|
+
this.name = "ScrapeTimeoutError";
|
|
98
|
+
this.jobId = jobId;
|
|
99
|
+
this.elapsed = elapsed;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var BadRequestError = class extends LogoError {
|
|
103
|
+
constructor(message) {
|
|
104
|
+
super(message, "BAD_REQUEST_ERROR", 400);
|
|
105
|
+
this.name = "BadRequestError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/url-builder.ts
|
|
110
|
+
var IP_ADDRESS_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
111
|
+
var LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
112
|
+
function validateDomain(domain) {
|
|
113
|
+
const normalized = domain.trim().toLowerCase();
|
|
114
|
+
const clean = normalized.endsWith(".") ? normalized.slice(0, -1) : normalized;
|
|
115
|
+
if (clean.length === 0) {
|
|
116
|
+
throw new DomainValidationError("Domain must not be empty", domain);
|
|
117
|
+
}
|
|
118
|
+
if (clean.includes("://")) {
|
|
119
|
+
throw new DomainValidationError(
|
|
120
|
+
'Domain must not include a protocol scheme (e.g. remove "https://")',
|
|
121
|
+
domain
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (clean.includes("/")) {
|
|
125
|
+
throw new DomainValidationError(
|
|
126
|
+
"Domain must not include a path \u2014 provide only the hostname",
|
|
127
|
+
domain
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (IP_ADDRESS_RE.test(clean)) {
|
|
131
|
+
throw new DomainValidationError(
|
|
132
|
+
"IP addresses are not supported \u2014 provide a domain name",
|
|
133
|
+
domain
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (clean === "localhost") {
|
|
137
|
+
throw new DomainValidationError(
|
|
138
|
+
'"localhost" is not a valid domain',
|
|
139
|
+
domain
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (clean.length > 253) {
|
|
143
|
+
throw new DomainValidationError(
|
|
144
|
+
`Domain exceeds maximum length of 253 characters (got ${clean.length})`,
|
|
145
|
+
domain
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const labels = clean.split(".");
|
|
149
|
+
if (labels.length < 2) {
|
|
150
|
+
throw new DomainValidationError(
|
|
151
|
+
'Domain must contain at least two labels (e.g. "example.com")',
|
|
152
|
+
domain
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
for (const label of labels) {
|
|
156
|
+
if (label.length === 0) {
|
|
157
|
+
throw new DomainValidationError(
|
|
158
|
+
"Domain contains an empty label (consecutive dots)",
|
|
159
|
+
domain
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (label.length > 63) {
|
|
163
|
+
throw new DomainValidationError(
|
|
164
|
+
`Label "${label}" exceeds maximum length of 63 characters (got ${label.length})`,
|
|
165
|
+
domain
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (!LABEL_RE.test(label)) {
|
|
169
|
+
throw new DomainValidationError(
|
|
170
|
+
`Label "${label}" contains invalid characters \u2014 only letters, digits, and hyphens are allowed, and labels must not start or end with a hyphen`,
|
|
171
|
+
domain
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return clean;
|
|
176
|
+
}
|
|
177
|
+
function resolveFormat(format) {
|
|
178
|
+
let candidate = format;
|
|
179
|
+
if (candidate.startsWith("image/")) {
|
|
180
|
+
candidate = candidate.slice(6);
|
|
181
|
+
}
|
|
182
|
+
if (candidate in FORMAT_ALIASES) {
|
|
183
|
+
return candidate;
|
|
184
|
+
}
|
|
185
|
+
if (SUPPORTED_FORMATS.has(format)) {
|
|
186
|
+
const shorthand = format.slice(6);
|
|
187
|
+
if (shorthand in FORMAT_ALIASES) {
|
|
188
|
+
return shorthand;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
function logoUrl(domain, options) {
|
|
194
|
+
const validDomain = validateDomain(domain);
|
|
195
|
+
const {
|
|
196
|
+
token,
|
|
197
|
+
size,
|
|
198
|
+
width,
|
|
199
|
+
greyscale,
|
|
200
|
+
theme,
|
|
201
|
+
format,
|
|
202
|
+
autoScrape,
|
|
203
|
+
baseUrl
|
|
204
|
+
} = options ?? {};
|
|
205
|
+
const maxWidth = token?.startsWith("sk_") ? MAX_WIDTH_SERVER : MAX_WIDTH;
|
|
206
|
+
let resolvedSize = size ?? width ?? DEFAULT_WIDTH;
|
|
207
|
+
if (resolvedSize <= 0) {
|
|
208
|
+
resolvedSize = DEFAULT_WIDTH;
|
|
209
|
+
}
|
|
210
|
+
resolvedSize = Math.max(1, Math.min(resolvedSize, maxWidth));
|
|
211
|
+
const resolvedFormat = format ? resolveFormat(format) : void 0;
|
|
212
|
+
const effectiveBaseUrl = baseUrl ?? BASE_URL;
|
|
213
|
+
const url = new URL(`${effectiveBaseUrl}/${validDomain}`);
|
|
214
|
+
if (token !== void 0) {
|
|
215
|
+
url.searchParams.set("token", token);
|
|
216
|
+
}
|
|
217
|
+
if (resolvedSize !== DEFAULT_WIDTH) {
|
|
218
|
+
url.searchParams.set("size", String(resolvedSize));
|
|
219
|
+
}
|
|
220
|
+
if (greyscale === true) {
|
|
221
|
+
url.searchParams.set("greyscale", "1");
|
|
222
|
+
}
|
|
223
|
+
if (theme === "light" || theme === "dark") {
|
|
224
|
+
url.searchParams.set("theme", theme);
|
|
225
|
+
}
|
|
226
|
+
if (resolvedFormat !== void 0) {
|
|
227
|
+
url.searchParams.set("format", resolvedFormat);
|
|
228
|
+
}
|
|
229
|
+
if (autoScrape === true) {
|
|
230
|
+
url.searchParams.set("autoScrape", "true");
|
|
231
|
+
}
|
|
232
|
+
return url.toString();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/headers.ts
|
|
236
|
+
function safeParseInt(value, fallback) {
|
|
237
|
+
if (value === null) return fallback;
|
|
238
|
+
const parsed = parseInt(value, 10);
|
|
239
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
240
|
+
}
|
|
241
|
+
function safeParseFloat(value, fallback) {
|
|
242
|
+
if (value === null) return fallback;
|
|
243
|
+
const parsed = parseFloat(value);
|
|
244
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
245
|
+
}
|
|
246
|
+
function parseLogoHeaders(headers) {
|
|
247
|
+
const cacheStatus = headers.get("X-Cache-Status");
|
|
248
|
+
const rateLimitRemaining = safeParseInt(headers.get("X-RateLimit-Remaining"), 0);
|
|
249
|
+
const rateLimitReset = safeParseInt(headers.get("X-RateLimit-Reset"), 0);
|
|
250
|
+
const quotaRemaining = safeParseInt(headers.get("X-Quota-Remaining"), 0);
|
|
251
|
+
const quotaLimit = safeParseInt(headers.get("X-Quota-Limit"), 0);
|
|
252
|
+
const tokenPrefix = headers.get("X-Quikturn-Token") ?? void 0;
|
|
253
|
+
const transformApplied = headers.get("X-Transformation-Applied") === "true";
|
|
254
|
+
const VALID_TRANSFORM_STATUSES = /* @__PURE__ */ new Set([
|
|
255
|
+
"not-requested",
|
|
256
|
+
"unsupported-format",
|
|
257
|
+
"transformation-error"
|
|
258
|
+
]);
|
|
259
|
+
const rawTransformStatus = headers.get("X-Transformation-Status");
|
|
260
|
+
const transformStatus = rawTransformStatus && VALID_TRANSFORM_STATUSES.has(rawTransformStatus) ? rawTransformStatus : void 0;
|
|
261
|
+
const rawTransformMethod = headers.get("X-Transformation-Method");
|
|
262
|
+
const transformMethod = rawTransformMethod === "images-binding" ? "images-binding" : void 0;
|
|
263
|
+
const transformWidth = safeParseInt(headers.get("X-Transformation-Width"), void 0);
|
|
264
|
+
const transformGreyscale = headers.get("X-Transformation-Greyscale") === "true" ? true : void 0;
|
|
265
|
+
const transformGamma = safeParseFloat(headers.get("X-Transformation-Gamma"), void 0);
|
|
266
|
+
return {
|
|
267
|
+
cache: { status: cacheStatus === "HIT" ? "HIT" : "MISS" },
|
|
268
|
+
rateLimit: {
|
|
269
|
+
remaining: rateLimitRemaining,
|
|
270
|
+
reset: new Date(rateLimitReset * 1e3)
|
|
271
|
+
},
|
|
272
|
+
quota: {
|
|
273
|
+
remaining: quotaRemaining,
|
|
274
|
+
limit: quotaLimit
|
|
275
|
+
},
|
|
276
|
+
transformation: {
|
|
277
|
+
applied: transformApplied,
|
|
278
|
+
...transformStatus ? { status: transformStatus } : {},
|
|
279
|
+
...transformMethod ? { method: transformMethod } : {},
|
|
280
|
+
...transformWidth !== void 0 ? { width: transformWidth } : {},
|
|
281
|
+
...transformGreyscale !== void 0 ? { greyscale: transformGreyscale } : {},
|
|
282
|
+
...transformGamma !== void 0 ? { gamma: transformGamma } : {}
|
|
283
|
+
},
|
|
284
|
+
...tokenPrefix !== void 0 ? { tokenPrefix } : {}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function parseRetryAfter(headers) {
|
|
288
|
+
const value = headers.get("Retry-After");
|
|
289
|
+
if (value === null) return null;
|
|
290
|
+
const parsed = Number(value);
|
|
291
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export { AuthenticationError, BASE_URL, BadRequestError, DEFAULT_FORMAT, DEFAULT_WIDTH, DomainValidationError, FORMAT_ALIASES, ForbiddenError, KEY_TYPES, LogoError, MAX_WIDTH, MAX_WIDTH_SERVER, MONTHLY_LIMITS, NotFoundError, QuotaExceededError, RATE_LIMITS, RateLimitError, SERVER_RATE_LIMITS, SUPPORTED_FORMATS, ScrapeTimeoutError, TIERS, logoUrl, parseLogoHeaders, parseRetryAfter };
|