@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.cjs ADDED
@@ -0,0 +1,681 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+ var _chunk4ORXUW2Ucjs = require('./chunk-4ORXUW2U.cjs');
14
+ require('./chunk-ETV4XYOV.cjs');
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 (0, _chunk4ORXUW2Ucjs.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 (0, _chunk4ORXUW2Ucjs.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 (e) {
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 = _nullishCoalesce(_optionalChain([headers, 'access', _2 => _2.get, 'call', _3 => _3("retry-after"), 'optionalAccess', _4 => _4.trim, 'call', _5 => _5()]), () => ( ""));
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 _optionalChain([envelope, 'optionalAccess', _6 => _6.code]) === "string" && envelope.code !== "" ? envelope.code : void 0;
108
+ const retryAfter = retryAfterOf(headers);
109
+ const requestId = typeof _optionalChain([envelope, 'optionalAccess', _7 => _7.requestId]) === "string" && envelope.requestId !== "" ? envelope.requestId : _nullishCoalesce(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 (0, _chunk4ORXUW2Ucjs.RateLimitedError)({
130
+ ...init,
131
+ retryable: true,
132
+ retryAfterSeconds: retryAfter !== null && retryAfter >= 1 ? retryAfter : _chunk4ORXUW2Ucjs.RATE_LIMIT_DEFAULT_RETRY_S
133
+ });
134
+ }
135
+ return new (0, _chunk4ORXUW2Ucjs.CatalogError)(init);
136
+ }
137
+ function guarded(work, callerSignal, timeoutMs) {
138
+ if (_optionalChain([callerSignal, 'optionalAccess', _8 => _8.aborted])) return Promise.reject(callerSignal.reason);
139
+ const controller = new AbortController();
140
+ return new Promise((resolve, reject) => {
141
+ const finish = () => {
142
+ clearTimeout(timer);
143
+ _optionalChain([callerSignal, 'optionalAccess', _9 => _9.removeEventListener, 'call', _10 => _10("abort", onAbort)]);
144
+ };
145
+ const onAbort = () => {
146
+ finish();
147
+ const reason = _optionalChain([callerSignal, 'optionalAccess', _11 => _11.reason]);
148
+ controller.abort(reason);
149
+ reject(reason);
150
+ };
151
+ const timer = setTimeout(() => {
152
+ finish();
153
+ controller.abort();
154
+ reject(new (0, _chunk4ORXUW2Ucjs.TimeoutError)({ timeoutMs }));
155
+ }, timeoutMs);
156
+ _optionalChain([callerSignal, 'optionalAccess', _12 => _12.addEventListener, 'call', _13 => _13("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 (0, _chunk4ORXUW2Ucjs.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 (0, _chunk4ORXUW2Ucjs.NetworkError)({ cause });
207
+ }
208
+ },
209
+ spec.signal,
210
+ timeoutMs
211
+ );
212
+ } catch (error) {
213
+ if (error instanceof _chunk4ORXUW2Ucjs.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 (0, _chunk4ORXUW2Ucjs.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(_optionalChain([options, 'optionalAccess', _14 => _14.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 (0, _chunk4ORXUW2Ucjs.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 (_optionalChain([signal, 'optionalAccess', _15 => _15.aborted])) return Promise.reject(signal.reason);
342
+ return new Promise((resolve, reject) => {
343
+ const onAbort = () => {
344
+ clearTimeout(timer);
345
+ reject(_optionalChain([signal, 'optionalAccess', _16 => _16.reason]));
346
+ };
347
+ const timer = setTimeout(() => {
348
+ _optionalChain([signal, 'optionalAccess', _17 => _17.removeEventListener, 'call', _18 => _18("abort", onAbort)]);
349
+ resolve();
350
+ }, ms);
351
+ _optionalChain([signal, 'optionalAccess', _19 => _19.addEventListener, 'call', _20 => _20("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 = _nullishCoalesce(options.intervalMs, () => ( DEFAULT_INTERVAL_MS));
360
+ const timeoutMs = _nullishCoalesce(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 (0, _chunk4ORXUW2Ucjs.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(_optionalChain([options, 'optionalAccess', _21 => _21.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 _chunk4ORXUW2Ucjs.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 = _nullishCoalesce(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
+ await _asyncNullishCoalesce(handle, async () => ( (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 = (_nullishCoalesce(options.baseUrl, () => ( _chunk4ORXUW2Ucjs.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
+ _nullishCoalesce(options.fetch, () => ( globalThis.fetch.bind(globalThis))),
599
+ checkTimeout(_nullishCoalesce(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 |= (_nullishCoalesce(left[i], () => ( 0))) ^ (_nullishCoalesce(right[i], () => ( 0)));
640
+ return difference === 0;
641
+ }
642
+ async function verifyCatalogWebhook(input) {
643
+ try {
644
+ const { secret, rawBody, signature } = input;
645
+ const now = _nullishCoalesce(input.now, () => ( Math.floor(Date.now() / 1e3)));
646
+ const tolerance = _nullishCoalesce(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]) => _nullishCoalesce(v, () => ( "")));
652
+ const digests = parts.filter(([k]) => k === "v1").map(([, v]) => _nullishCoalesce(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 = _nullishCoalesce(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 (e2) {
666
+ return false;
667
+ }
668
+ }
669
+
670
+
671
+
672
+
673
+
674
+
675
+
676
+
677
+
678
+
679
+
680
+
681
+ exports.CartLineError = _chunk4ORXUW2Ucjs.CartLineError; exports.CatalogError = _chunk4ORXUW2Ucjs.CatalogError; exports.CustomerTokenInvalidError = _chunk4ORXUW2Ucjs.CustomerTokenInvalidError; exports.FeatureUnavailableError = _chunk4ORXUW2Ucjs.FeatureUnavailableError; exports.NetworkError = _chunk4ORXUW2Ucjs.NetworkError; exports.OriginNotAllowedError = _chunk4ORXUW2Ucjs.OriginNotAllowedError; exports.RateLimitedError = _chunk4ORXUW2Ucjs.RateLimitedError; exports.TimeoutError = _chunk4ORXUW2Ucjs.TimeoutError; exports.createCatalogAdmin = createCatalogAdmin; exports.isCatalogError = _chunk4ORXUW2Ucjs.isCatalogError; exports.verifyCatalogWebhook = verifyCatalogWebhook;