@usethrottle/catalog 0.1.1 → 1.1.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/dist/admin.js ADDED
@@ -0,0 +1,681 @@
1
+ import {
2
+ CartLineError,
3
+ CatalogError,
4
+ CustomerTokenInvalidError,
5
+ DEFAULT_BASE_URL,
6
+ FeatureUnavailableError,
7
+ NetworkError,
8
+ OriginNotAllowedError,
9
+ RATE_LIMIT_DEFAULT_RETRY_S,
10
+ RateLimitedError,
11
+ TimeoutError,
12
+ isCatalogError
13
+ } from "./chunk-JK2UY4CD.js";
14
+ import "./chunk-PKBMQBKP.js";
15
+
16
+ // src/generated/push-types.ts
17
+ var PUSH_ERRORS = {
18
+ AUTHENTICATION_FAILED: { statuses: [401], retryable: false },
19
+ VENDOR_PAUSED: { statuses: [403], retryable: false },
20
+ VENDOR_KEY_NOT_ALLOWED: { statuses: [403], retryable: false },
21
+ INVALID_REQUEST: { statuses: [400], retryable: false },
22
+ IDEMPOTENCY_KEY_REQUIRED: { statuses: [400], retryable: false },
23
+ NOT_FOUND: { statuses: [404], retryable: false },
24
+ PROFILE_NOT_FOUND: { statuses: [404], retryable: false },
25
+ VENDOR_NOT_FOUND: { statuses: [404], retryable: false },
26
+ IDEMPOTENCY_CONFLICT: { statuses: [409], retryable: false },
27
+ RUN_IN_PROGRESS: { statuses: [409], retryable: true },
28
+ JOB_CLOSED: { statuses: [409], retryable: false },
29
+ JOB_FULL: { statuses: [409], retryable: false },
30
+ TOO_MANY_OPEN_JOBS: { statuses: [409], retryable: true },
31
+ REPORT_TOO_LARGE: { statuses: [409], retryable: false },
32
+ HANDLE_TAKEN: { statuses: [409], retryable: false },
33
+ IDENTIFIER_LOCKED: { statuses: [409], retryable: false },
34
+ REPORT_EXPIRED: { statuses: [410], retryable: false },
35
+ REQUEST_BODY_TOO_LARGE: { statuses: [413], retryable: false },
36
+ MAPPING_INVALID: { statuses: [422], retryable: false },
37
+ RATE_LIMITED: { statuses: [429], retryable: true },
38
+ PUSH_QUEUE_FULL: { statuses: [429], retryable: true },
39
+ INTERNAL_ERROR: { statuses: [500], retryable: true },
40
+ INFRASTRUCTURE_UNAVAILABLE: { statuses: [503], retryable: true }
41
+ };
42
+
43
+ // src/admin/http.ts
44
+ var MAX_IDEMPOTENCY_KEY = 200;
45
+ var HEADER_SAFE = /^[\x21-\x7e]+$/u;
46
+ var MAX_RETRY_AFTER_S = 86400;
47
+ function invalidRequest(message) {
48
+ return new CatalogError({
49
+ code: "INVALID_REQUEST",
50
+ message,
51
+ statusCode: 0,
52
+ source: "client",
53
+ retryable: false
54
+ });
55
+ }
56
+ function clientError(code, message) {
57
+ return new CatalogError({
58
+ code,
59
+ message,
60
+ statusCode: 0,
61
+ source: "client",
62
+ retryable: false
63
+ });
64
+ }
65
+ function resolveIdempotencyKey(key) {
66
+ if (key === void 0) return globalThis.crypto.randomUUID();
67
+ if (typeof key !== "string" || key.length === 0 || key.length > MAX_IDEMPOTENCY_KEY || !HEADER_SAFE.test(key))
68
+ throw invalidRequest(
69
+ `idempotencyKey must be 1 to ${MAX_IDEMPOTENCY_KEY} visible characters with no spaces.`
70
+ );
71
+ return key;
72
+ }
73
+ function queryString(pairs) {
74
+ if (pairs === void 0 || pairs.length === 0) return "";
75
+ return `?${pairs.map(
76
+ ([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
77
+ ).join("&")}`;
78
+ }
79
+ function isRecord(value) {
80
+ return typeof value === "object" && value !== null && !Array.isArray(value);
81
+ }
82
+ function parseJson(text) {
83
+ try {
84
+ return JSON.parse(text);
85
+ } catch {
86
+ return void 0;
87
+ }
88
+ }
89
+ function issuesOf(value) {
90
+ if (!Array.isArray(value)) return [];
91
+ return value.flatMap(
92
+ (issue) => isRecord(issue) && typeof issue.path === "string" && typeof issue.message === "string" ? [{ path: issue.path, message: issue.message }] : []
93
+ );
94
+ }
95
+ function retryAfterOf(headers) {
96
+ const value = headers.get("retry-after")?.trim() ?? "";
97
+ if (!/^\d{1,6}$/u.test(value)) return null;
98
+ const seconds = Number(value);
99
+ return seconds <= MAX_RETRY_AFTER_S ? seconds : null;
100
+ }
101
+ function isPushCode(code) {
102
+ return Object.prototype.hasOwnProperty.call(PUSH_ERRORS, code);
103
+ }
104
+ function toError(status, headers, text) {
105
+ const body = parseJson(text);
106
+ const envelope = isRecord(body) && isRecord(body.error) ? body.error : void 0;
107
+ const code = typeof envelope?.code === "string" && envelope.code !== "" ? envelope.code : void 0;
108
+ const retryAfter = retryAfterOf(headers);
109
+ const requestId = typeof envelope?.requestId === "string" && envelope.requestId !== "" ? envelope.requestId : headers.get("x-request-id") ?? void 0;
110
+ const init = envelope !== void 0 && code !== void 0 ? {
111
+ code,
112
+ message: typeof envelope.message === "string" && envelope.message !== "" ? envelope.message : `Request failed with status ${status}.`,
113
+ statusCode: status,
114
+ source: "catalog",
115
+ retryable: isPushCode(code) ? PUSH_ERRORS[code].retryable : status === 429 || status === 503,
116
+ retryAfterSeconds: retryAfter,
117
+ issues: issuesOf(envelope.issues),
118
+ body,
119
+ requestId
120
+ } : {
121
+ code: "INVALID_RESPONSE",
122
+ message: `The server answered ${status} with an unreadable body.`,
123
+ statusCode: status,
124
+ source: "catalog",
125
+ retryAfterSeconds: retryAfter,
126
+ requestId
127
+ };
128
+ if (status === 429) {
129
+ return new RateLimitedError({
130
+ ...init,
131
+ retryable: true,
132
+ retryAfterSeconds: retryAfter !== null && retryAfter >= 1 ? retryAfter : RATE_LIMIT_DEFAULT_RETRY_S
133
+ });
134
+ }
135
+ return new CatalogError(init);
136
+ }
137
+ function guarded(work, callerSignal, timeoutMs) {
138
+ if (callerSignal?.aborted) return Promise.reject(callerSignal.reason);
139
+ const controller = new AbortController();
140
+ return new Promise((resolve, reject) => {
141
+ const finish = () => {
142
+ clearTimeout(timer);
143
+ callerSignal?.removeEventListener("abort", onAbort);
144
+ };
145
+ const onAbort = () => {
146
+ finish();
147
+ const reason = callerSignal?.reason;
148
+ controller.abort(reason);
149
+ reject(reason);
150
+ };
151
+ const timer = setTimeout(() => {
152
+ finish();
153
+ controller.abort();
154
+ reject(new TimeoutError({ timeoutMs }));
155
+ }, timeoutMs);
156
+ callerSignal?.addEventListener("abort", onAbort, { once: true });
157
+ work(controller.signal).then(
158
+ (value) => {
159
+ finish();
160
+ resolve(value);
161
+ },
162
+ (error) => {
163
+ finish();
164
+ reject(error);
165
+ }
166
+ );
167
+ });
168
+ }
169
+ function createHttp(key, baseUrl, fetchFn, timeoutMs) {
170
+ async function exchange(spec, accept) {
171
+ const headers = {
172
+ authorization: `Bearer ${key}`,
173
+ accept
174
+ };
175
+ let body;
176
+ if (spec.body !== void 0) {
177
+ headers["content-type"] = "application/json";
178
+ body = JSON.stringify(spec.body);
179
+ }
180
+ if (spec.idempotencyKey !== void 0)
181
+ headers["idempotency-key"] = spec.idempotencyKey;
182
+ const url = baseUrl + spec.path + queryString(spec.query);
183
+ const attempts = spec.idempotencyKey === void 0 ? 1 : 2;
184
+ for (let attempt = 1; ; attempt += 1) {
185
+ try {
186
+ return await guarded(
187
+ async (signal) => {
188
+ let response;
189
+ try {
190
+ response = await fetchFn(url, {
191
+ method: spec.method,
192
+ headers,
193
+ ...body === void 0 ? {} : { body },
194
+ signal
195
+ });
196
+ } catch (cause) {
197
+ throw new NetworkError({ cause });
198
+ }
199
+ try {
200
+ return {
201
+ status: response.status,
202
+ headers: response.headers,
203
+ text: await response.text()
204
+ };
205
+ } catch (cause) {
206
+ throw new NetworkError({ cause });
207
+ }
208
+ },
209
+ spec.signal,
210
+ timeoutMs
211
+ );
212
+ } catch (error) {
213
+ if (error instanceof NetworkError && attempt < attempts) continue;
214
+ throw error;
215
+ }
216
+ }
217
+ }
218
+ return {
219
+ keyKind: key.startsWith("pc_sk_") ? "store" : "vendor",
220
+ async json(spec) {
221
+ const answer = await exchange(spec, "application/json");
222
+ if (answer.status < 200 || answer.status >= 300)
223
+ throw toError(answer.status, answer.headers, answer.text);
224
+ const parsed = parseJson(answer.text);
225
+ if (parsed === void 0)
226
+ throw new CatalogError({
227
+ code: "INVALID_RESPONSE",
228
+ message: "The server answered with a body that is not JSON.",
229
+ statusCode: answer.status,
230
+ source: "catalog"
231
+ });
232
+ return parsed;
233
+ },
234
+ async text(spec) {
235
+ const answer = await exchange(spec, "text/csv");
236
+ if (answer.status < 200 || answer.status >= 300)
237
+ throw toError(answer.status, answer.headers, answer.text);
238
+ return answer.text;
239
+ }
240
+ };
241
+ }
242
+
243
+ // src/admin/inventory.ts
244
+ function createInventory(http) {
245
+ return {
246
+ set: (rows) => http.json({
247
+ method: "PATCH",
248
+ path: "/v1/catalog/inventory",
249
+ body: { rows }
250
+ }),
251
+ async adjust(rows, options) {
252
+ const idempotencyKey = resolveIdempotencyKey(options?.idempotencyKey);
253
+ const adjusted = rows.map((row) => ({
254
+ ...row,
255
+ quantityMode: "adjust"
256
+ }));
257
+ return http.json({
258
+ method: "PATCH",
259
+ path: "/v1/catalog/inventory",
260
+ body: { rows: adjusted },
261
+ idempotencyKey
262
+ });
263
+ }
264
+ };
265
+ }
266
+
267
+ // src/admin/products.ts
268
+ var REF_FIELDS = ["externalId", "sku", "handle"];
269
+ function refQuery(ref) {
270
+ const record = ref;
271
+ const present = REF_FIELDS.filter((field2) => record[field2] !== void 0);
272
+ const [field] = present;
273
+ const value = field === void 0 ? void 0 : record[field];
274
+ if (present.length !== 1 || field === void 0 || typeof value !== "string" || value === "")
275
+ throw invalidRequest(
276
+ "A product ref is exactly one of externalId, sku or handle, as a non-empty string."
277
+ );
278
+ return [[field, value]];
279
+ }
280
+ function unreadable() {
281
+ return new CatalogError({
282
+ code: "INVALID_RESPONSE",
283
+ message: "The server answered with an unexpected shape.",
284
+ statusCode: 200,
285
+ source: "catalog"
286
+ });
287
+ }
288
+ function createProducts(http) {
289
+ const op = (name) => (refs) => http.json({
290
+ method: "POST",
291
+ path: `/v1/catalog/products/${name}`,
292
+ body: { products: refs }
293
+ });
294
+ return {
295
+ async get(ref) {
296
+ const answer = await http.json({
297
+ method: "GET",
298
+ path: "/v1/catalog/products",
299
+ query: refQuery(ref)
300
+ });
301
+ if ("product" in answer) return answer.product;
302
+ throw unreadable();
303
+ },
304
+ async list(options = {}) {
305
+ const query = [];
306
+ if (options.cursor !== void 0) query.push(["cursor", options.cursor]);
307
+ if (options.limit !== void 0)
308
+ query.push(["limit", String(options.limit)]);
309
+ const answer = await http.json({
310
+ method: "GET",
311
+ path: "/v1/catalog/products",
312
+ query
313
+ });
314
+ if ("products" in answer)
315
+ return { products: answer.products, nextCursor: answer.nextCursor };
316
+ throw unreadable();
317
+ },
318
+ archive: op("archive"),
319
+ restore: op("restore"),
320
+ async rename(ref, handle) {
321
+ const body = { product: ref, handle };
322
+ const answer = await http.json({
323
+ method: "POST",
324
+ path: "/v1/catalog/products/rename",
325
+ body
326
+ });
327
+ return answer.product;
328
+ }
329
+ };
330
+ }
331
+
332
+ // src/admin/runs.ts
333
+ var MAX_BATCH_ROWS = 1e3;
334
+ var MAX_BATCH_BYTES = 5e6;
335
+ var ENVELOPE_BYTES = '{"rows":[]}'.length;
336
+ var STOCK_PAGE_LIMIT = 500;
337
+ var QUEUE_RETRIES = 3;
338
+ var DEFAULT_INTERVAL_MS = 5e3;
339
+ var DEFAULT_WAIT_MS = 864e5;
340
+ function sleep(ms, signal) {
341
+ if (signal?.aborted) return Promise.reject(signal.reason);
342
+ return new Promise((resolve, reject) => {
343
+ const onAbort = () => {
344
+ clearTimeout(timer);
345
+ reject(signal?.reason);
346
+ };
347
+ const timer = setTimeout(() => {
348
+ signal?.removeEventListener("abort", onAbort);
349
+ resolve();
350
+ }, ms);
351
+ signal?.addEventListener("abort", onAbort, { once: true });
352
+ });
353
+ }
354
+ function checkMode(mode) {
355
+ if (mode !== void 0 && mode !== "merge" && mode !== "replace")
356
+ throw invalidRequest("mode must be 'merge' or 'replace'.");
357
+ }
358
+ async function pollUntil(read, done, options = {}) {
359
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
360
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_MS;
361
+ if (!(intervalMs >= 0 && Number.isFinite(intervalMs)))
362
+ throw invalidRequest("intervalMs must be a finite number of 0 or more.");
363
+ if (!(timeoutMs > 0 && Number.isFinite(timeoutMs)))
364
+ throw invalidRequest("timeoutMs must be a finite number above 0.");
365
+ const deadline = Date.now() + timeoutMs;
366
+ for (; ; ) {
367
+ const value = await read(options.signal);
368
+ if (done(value)) return value;
369
+ const remaining = deadline - Date.now();
370
+ if (remaining <= 0) throw new TimeoutError({ timeoutMs });
371
+ await sleep(Math.min(intervalMs, remaining), options.signal);
372
+ }
373
+ }
374
+ async function* stockRows(http, base) {
375
+ let cursor = null;
376
+ do {
377
+ const query = [];
378
+ if (cursor !== null) query.push(["cursor", cursor]);
379
+ query.push(["limit", String(STOCK_PAGE_LIMIT)]);
380
+ const page = await http.json({
381
+ method: "GET",
382
+ path: `${base}/stock`,
383
+ query
384
+ });
385
+ yield* page.rows;
386
+ cursor = page.nextCursor;
387
+ } while (cursor !== null);
388
+ }
389
+ var encode = (id) => encodeURIComponent(id);
390
+ function createHandle(http, jobId) {
391
+ const base = `/v1/catalog/jobs/${encode(jobId)}`;
392
+ const read = async (options = {}, signal) => (await http.json({
393
+ method: "GET",
394
+ path: base,
395
+ ...options.includeBatches === true ? { query: [["include", "batches"]] } : {},
396
+ signal
397
+ })).job;
398
+ return {
399
+ jobId,
400
+ async send(rows, options) {
401
+ return http.json({
402
+ method: "POST",
403
+ path: `${base}/batches`,
404
+ body: { rows },
405
+ idempotencyKey: resolveIdempotencyKey(options?.idempotencyKey)
406
+ });
407
+ },
408
+ async close() {
409
+ return (await http.json({
410
+ method: "POST",
411
+ path: `${base}/close`
412
+ })).job;
413
+ },
414
+ get: (options) => read(options),
415
+ wait: (options) => pollUntil(
416
+ (signal) => read({}, signal),
417
+ (job) => job.finishedAt !== null || job.state === "expired",
418
+ options
419
+ ),
420
+ errorsCsv: () => http.text({ method: "GET", path: `${base}/errors.csv` }),
421
+ stockResults: () => stockRows(http, base),
422
+ stockCsv: () => http.text({ method: "GET", path: `${base}/stock.csv` })
423
+ };
424
+ }
425
+ function productKey(row) {
426
+ const record = row;
427
+ for (const field of ["productHandle", "externalId"]) {
428
+ const value = record[field];
429
+ if (typeof value === "string" && value !== "") return `${field}:${value}`;
430
+ }
431
+ return `sku:${String(record.sku)}`;
432
+ }
433
+ var utf8 = new TextEncoder();
434
+ function createJobs(http) {
435
+ const open = async (options = {}) => {
436
+ checkMode(options.mode);
437
+ const body = {
438
+ ...options.mode === void 0 ? {} : { mode: options.mode },
439
+ ...options.fullSync === void 0 ? {} : { fullSync: options.fullSync }
440
+ };
441
+ const { job } = await http.json({
442
+ method: "POST",
443
+ path: "/v1/catalog/jobs",
444
+ body
445
+ });
446
+ return createHandle(http, job.jobId);
447
+ };
448
+ async function sendBatch(handle, rows, signal) {
449
+ const idempotencyKey = resolveIdempotencyKey(void 0);
450
+ for (let retry = 0; ; retry += 1) {
451
+ try {
452
+ await handle.send(rows, { idempotencyKey });
453
+ return;
454
+ } catch (error) {
455
+ if (!(error instanceof RateLimitedError) || retry >= QUEUE_RETRIES)
456
+ throw error;
457
+ await sleep(error.retryAfterSeconds * 1e3, signal);
458
+ }
459
+ }
460
+ }
461
+ return {
462
+ open,
463
+ async get(jobId) {
464
+ const handle = createHandle(http, jobId);
465
+ await handle.get();
466
+ return handle;
467
+ },
468
+ resume: (jobId) => createHandle(http, jobId),
469
+ async run(rows, options = {}) {
470
+ checkMode(options.mode);
471
+ const batchRows = options.batchRows ?? MAX_BATCH_ROWS;
472
+ if (!Number.isInteger(batchRows) || batchRows < 1 || batchRows > MAX_BATCH_ROWS)
473
+ throw invalidRequest(
474
+ "batchRows must be a whole number from 1 to 1,000."
475
+ );
476
+ let handle;
477
+ let batch = [];
478
+ let batchBytes = ENVELOPE_BYTES;
479
+ let group = [];
480
+ let groupBytes = 0;
481
+ let groupKey = "";
482
+ const flush = async () => {
483
+ if (batch.length === 0) return;
484
+ handle ?? (handle = await open({
485
+ ...options.mode === void 0 ? {} : { mode: options.mode },
486
+ ...options.fullSync === void 0 ? {} : { fullSync: options.fullSync }
487
+ }));
488
+ const sending = batch;
489
+ batch = [];
490
+ batchBytes = ENVELOPE_BYTES;
491
+ await sendBatch(handle, sending, options.signal);
492
+ };
493
+ const place = async () => {
494
+ if (batch.length > 0 && (batch.length + group.length > batchRows || batchBytes + groupBytes > MAX_BATCH_BYTES))
495
+ await flush();
496
+ batch.push(...group);
497
+ batchBytes += groupBytes;
498
+ group = [];
499
+ groupBytes = 0;
500
+ };
501
+ for await (const row of rows) {
502
+ const key = productKey(row);
503
+ if (group.length > 0 && key !== groupKey) await place();
504
+ groupKey = key;
505
+ group.push(row);
506
+ groupBytes += utf8.encode(JSON.stringify(row)).length + 1;
507
+ if (group.length > MAX_BATCH_ROWS)
508
+ throw invalidRequest(
509
+ `A product may have at most ${MAX_BATCH_ROWS} rows in a job.`
510
+ );
511
+ }
512
+ if (group.length > 0) await place();
513
+ await flush();
514
+ if (handle === void 0)
515
+ throw invalidRequest("jobs.run needs at least one row.");
516
+ await handle.close();
517
+ return handle;
518
+ }
519
+ };
520
+ }
521
+ function createBatches(http) {
522
+ return {
523
+ async send(rows, options = {}) {
524
+ checkMode(options.mode);
525
+ if (options.mode !== void 0 && http.keyKind === "store")
526
+ throw invalidRequest(
527
+ "batches.send ignores mode for a store key (pc_sk_): the store's /variants route has no replace. Use jobs.run({ mode }) or a vendor key (pc_vk_)."
528
+ );
529
+ return http.json({
530
+ method: "POST",
531
+ path: "/v1/catalog/variants",
532
+ ...options.mode === void 0 ? {} : { query: [["mode", options.mode]] },
533
+ body: { rows },
534
+ idempotencyKey: resolveIdempotencyKey(options.idempotencyKey)
535
+ });
536
+ }
537
+ };
538
+ }
539
+ function createRuns(http) {
540
+ const path = (runId) => `/v1/catalog/runs/${encode(runId)}`;
541
+ const get = async (runId, signal) => (await http.json({
542
+ method: "GET",
543
+ path: path(runId),
544
+ signal
545
+ })).run;
546
+ return {
547
+ get: (runId) => get(runId),
548
+ wait: (runId, options) => pollUntil(
549
+ (signal) => get(runId, signal),
550
+ (run) => run.status === "completed" || run.status === "failed",
551
+ options
552
+ ),
553
+ errorsCsv: (runId) => http.text({ method: "GET", path: `${path(runId)}/errors.csv` }),
554
+ stockResults: (runId) => stockRows(http, path(runId)),
555
+ stockCsv: (runId) => http.text({ method: "GET", path: `${path(runId)}/stock.csv` })
556
+ };
557
+ }
558
+
559
+ // src/admin/client.ts
560
+ var ADMIN_DEFAULT_TIMEOUT_MS = 6e4;
561
+ var MAX_TIMEOUT_MS = 2147483647;
562
+ function checkKey(key) {
563
+ if (typeof key === "string" && key.startsWith("pc_pk_")) {
564
+ throw clientError(
565
+ "INVALID_ADMIN_KEY",
566
+ "Publishable keys (pc_pk_) cannot use the admin API. Use a secret key (pc_sk_) or a vendor key (pc_vk_)."
567
+ );
568
+ }
569
+ if (typeof key !== "string" || !HEADER_SAFE.test(key) || !(key.startsWith("pc_sk_") || key.startsWith("pc_vk_"))) {
570
+ throw clientError(
571
+ "INVALID_ADMIN_KEY",
572
+ "key must be a secret key (pc_sk_) or a vendor key (pc_vk_), with no spaces."
573
+ );
574
+ }
575
+ return key;
576
+ }
577
+ function checkTimeout(timeoutMs) {
578
+ if (!(timeoutMs > 0 && Number.isFinite(timeoutMs)))
579
+ throw invalidRequest("timeoutMs must be a finite number above 0.");
580
+ return Math.min(timeoutMs, MAX_TIMEOUT_MS);
581
+ }
582
+ function assertServer() {
583
+ if (Reflect.get(globalThis, "window") !== void 0 && Reflect.get(globalThis, "document") !== void 0)
584
+ throw clientError(
585
+ "SERVER_ONLY",
586
+ "The admin client is server-only: it holds a secret or vendor key, which must never reach a browser."
587
+ );
588
+ }
589
+ function createCatalogAdmin(options) {
590
+ assertServer();
591
+ const key = checkKey(options.key);
592
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/u, "");
593
+ if (!/^https?:\/\/[^\s/]+/iu.test(baseUrl))
594
+ throw invalidRequest("baseUrl must be an absolute http or https URL.");
595
+ const http = createHttp(
596
+ key,
597
+ baseUrl,
598
+ options.fetch ?? globalThis.fetch.bind(globalThis),
599
+ checkTimeout(options.timeoutMs ?? ADMIN_DEFAULT_TIMEOUT_MS)
600
+ );
601
+ const jobs = createJobs(http);
602
+ return {
603
+ jobs,
604
+ batches: createBatches(http),
605
+ runs: createRuns(http),
606
+ products: createProducts(http),
607
+ inventory: createInventory(http)
608
+ };
609
+ }
610
+
611
+ // src/admin/webhooks.ts
612
+ var HEX_SHA256 = /^[0-9a-fA-F]{64}$/u;
613
+ var DEFAULT_TOLERANCE_SECONDS = 300;
614
+ var MAX_V1 = 8;
615
+ var MAX_HEADER_BYTES = 1024;
616
+ var encoder = new TextEncoder();
617
+ async function hmac(secret, payload) {
618
+ const key = await crypto.subtle.importKey(
619
+ "raw",
620
+ encoder.encode(secret),
621
+ { name: "HMAC", hash: "SHA-256" },
622
+ false,
623
+ ["sign"]
624
+ );
625
+ return new Uint8Array(
626
+ await crypto.subtle.sign("HMAC", key, encoder.encode(payload))
627
+ );
628
+ }
629
+ function fromHex(hex) {
630
+ return Uint8Array.from(
631
+ { length: hex.length / 2 },
632
+ (_, i) => Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16)
633
+ );
634
+ }
635
+ function constantTimeEqual(left, right) {
636
+ const length = Math.max(left.length, right.length);
637
+ let difference = left.length ^ right.length;
638
+ for (let i = 0; i < length; i += 1)
639
+ difference |= (left[i] ?? 0) ^ (right[i] ?? 0);
640
+ return difference === 0;
641
+ }
642
+ async function verifyCatalogWebhook(input) {
643
+ try {
644
+ const { secret, rawBody, signature } = input;
645
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
646
+ const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
647
+ if (secret.length === 0 || rawBody.length === 0 || signature.length === 0 || encoder.encode(signature).byteLength > MAX_HEADER_BYTES || !Number.isSafeInteger(now) || !Number.isSafeInteger(tolerance) || tolerance < 0)
648
+ return false;
649
+ const parts = signature.split(",").map((part) => part.split("="));
650
+ if (parts.some((part) => part.length !== 2)) return false;
651
+ const timestamps = parts.filter(([k]) => k === "t").map(([, v]) => v ?? "");
652
+ const digests = parts.filter(([k]) => k === "v1").map(([, v]) => v ?? "");
653
+ if (parts.some(([k]) => k !== "t" && k !== "v1") || timestamps.length !== 1 || digests.length === 0 || digests.length > MAX_V1 || digests.some((d) => !HEX_SHA256.test(d)))
654
+ return false;
655
+ const timestampText = timestamps[0] ?? "";
656
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(timestampText)) return false;
657
+ const timestamp = Number(timestampText);
658
+ if (!Number.isSafeInteger(timestamp) || Math.abs(now - timestamp) > tolerance)
659
+ return false;
660
+ const expected = await hmac(secret, `${timestampText}.${rawBody}`);
661
+ let matched = false;
662
+ for (const digest of digests)
663
+ matched = constantTimeEqual(expected, fromHex(digest)) || matched;
664
+ return matched;
665
+ } catch {
666
+ return false;
667
+ }
668
+ }
669
+ export {
670
+ CartLineError,
671
+ CatalogError,
672
+ CustomerTokenInvalidError,
673
+ FeatureUnavailableError,
674
+ NetworkError,
675
+ OriginNotAllowedError,
676
+ RateLimitedError,
677
+ TimeoutError,
678
+ createCatalogAdmin,
679
+ isCatalogError,
680
+ verifyCatalogWebhook
681
+ };
@@ -1,4 +1,4 @@
1
- import { p as CatalogContractErrorCode } from './types-DYfIvIaI.js';
1
+ import { g as CatalogContractErrorCode } from './contract-types-DhiThMEr.js';
2
2
 
3
3
  /**
4
4
  * Shared error type for every Throttle SDK.
@@ -239,5 +239,5 @@ declare class FeatureUnavailableError extends CatalogError {
239
239
  /** True for a `CatalogError` from this or any other copy of the SDK. */
240
240
  declare function isCatalogError(value: unknown): value is CatalogError;
241
241
 
242
- export { CartLineError as C, FeatureUnavailableError as F, NetworkError as N, OriginNotAllowedError as O, RateLimitedError as R, CatalogError as b, CustomerTokenInvalidError as h, TimeoutError as i, isCatalogError as j };
243
- export type { ThrottleCartCode as T, CartLineProblem as a, CatalogErrorCode as c, CatalogErrorInit as d, CatalogErrorIssue as e, CatalogErrorSource as f, ClientErrorCode as g };
242
+ export { CartLineError as C, FeatureUnavailableError as F, NetworkError as N, OriginNotAllowedError as O, RateLimitedError as R, TimeoutError as T, CatalogError as b, CustomerTokenInvalidError as h, isCatalogError as i };
243
+ export type { CartLineProblem as a, CatalogErrorCode as c, CatalogErrorInit as d, CatalogErrorIssue as e, CatalogErrorSource as f, ClientErrorCode as g, ThrottleCartCode as j };