@usethrottle/catalog 1.0.0 → 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/CHANGELOG.md +8 -0
- package/README.md +64 -7
- package/dist/admin.cjs +681 -0
- package/dist/admin.d.cts +660 -0
- package/dist/admin.d.ts +660 -0
- package/dist/admin.js +681 -0
- package/dist/{catalog-error-BWUoZCzY.d.ts → catalog-error-C_zLOtYA.d.ts} +3 -3
- package/dist/{catalog-error-DO6ANrGR.d.cts → catalog-error-s1BINNkt.d.cts} +3 -3
- package/dist/{chunk-ZFUELHUU.cjs → chunk-4ORXUW2U.cjs} +4 -2
- package/dist/{chunk-2PJXGAZF.js → chunk-JK2UY4CD.js} +3 -1
- package/dist/{client-BNtBTEyh.d.cts → client-BAoq6jmM.d.cts} +1 -1
- package/dist/{client-CHLvULRQ.d.ts → client-c2F-eCaL.d.ts} +1 -1
- package/dist/{types-DYfIvIaI.d.cts → contract-types-DhiThMEr.d.cts} +1 -272
- package/dist/{types-DYfIvIaI.d.ts → contract-types-DhiThMEr.d.ts} +1 -272
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/react.cjs +17 -17
- package/dist/react.d.cts +5 -4
- package/dist/react.d.ts +5 -4
- package/dist/react.js +1 -1
- package/dist/server.cjs +14 -13
- package/dist/server.d.cts +3 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +3 -2
- package/dist/types-DsjoG8ce.d.cts +276 -0
- package/dist/types-xQLID-4j.d.ts +276 -0
- package/dist/url.d.cts +1 -0
- package/dist/url.d.ts +1 -0
- package/package.json +11 -1
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 {
|
|
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,
|
|
243
|
-
export type {
|
|
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 };
|