@productcraft/core 0.3.0 → 0.4.1
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/README.md +55 -15
- package/dist/index.cjs +832 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +397 -13
- package/dist/index.d.ts +397 -13
- package/dist/index.js +797 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.cjs
CHANGED
|
@@ -6,7 +6,12 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
6
6
|
|
|
7
7
|
var createClient__default = /*#__PURE__*/_interopDefault(createClient);
|
|
8
8
|
|
|
9
|
-
// src/
|
|
9
|
+
// src/legacy.ts
|
|
10
|
+
|
|
11
|
+
// src/version.ts
|
|
12
|
+
var CORE_VERSION = "0.4.1" ;
|
|
13
|
+
|
|
14
|
+
// src/legacy.ts
|
|
10
15
|
var PC_BASE_URL = {
|
|
11
16
|
// platformAuth is the platform identity layer (workspaces, PATs);
|
|
12
17
|
// it vacated api.auth so the customer-facing Auth product could
|
|
@@ -58,15 +63,841 @@ function makeClient(defaultBaseUrl, config = {}) {
|
|
|
58
63
|
fetch: config.fetch
|
|
59
64
|
});
|
|
60
65
|
client.use(authMiddleware(resolveAuth(config.auth)));
|
|
66
|
+
client.use(userAgentMiddleware());
|
|
61
67
|
return client;
|
|
62
68
|
}
|
|
69
|
+
function userAgentMiddleware() {
|
|
70
|
+
const g = globalThis;
|
|
71
|
+
const browser = typeof g.window?.document !== "undefined" && typeof g.navigator !== "undefined";
|
|
72
|
+
const node = g.process?.versions?.node;
|
|
73
|
+
const userAgent = `productcraft-node/core@${CORE_VERSION}${node ? ` node/${node}` : ""}`;
|
|
74
|
+
return {
|
|
75
|
+
onRequest({ request }) {
|
|
76
|
+
if (browser || request.headers.has("user-agent")) return void 0;
|
|
77
|
+
request.headers.set("User-Agent", userAgent);
|
|
78
|
+
return request;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/errors.ts
|
|
84
|
+
var ERROR_DOCS_URL = "https://productcraft.co/docs/errors";
|
|
85
|
+
var brandKey = (name) => /* @__PURE__ */ Symbol.for(`productcraft.error.${name}`);
|
|
86
|
+
function stamp(instance, ctor) {
|
|
87
|
+
let current = ctor;
|
|
88
|
+
while (current && current !== Error && current !== Function.prototype) {
|
|
89
|
+
if (Object.prototype.hasOwnProperty.call(current, "brandName") && current.brandName) {
|
|
90
|
+
Object.defineProperty(instance, brandKey(current.brandName), {
|
|
91
|
+
value: true,
|
|
92
|
+
enumerable: false
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
current = Object.getPrototypeOf(current);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var ProductCraftError = class extends Error {
|
|
99
|
+
static brandName = "ProductCraftError";
|
|
100
|
+
static [Symbol.hasInstance](instance) {
|
|
101
|
+
if (typeof instance !== "object" || instance === null) return false;
|
|
102
|
+
return instance[brandKey(this.brandName)] === true;
|
|
103
|
+
}
|
|
104
|
+
constructor(message, options) {
|
|
105
|
+
super(message, options);
|
|
106
|
+
const ctor = new.target;
|
|
107
|
+
Object.defineProperty(this, "name", {
|
|
108
|
+
value: ctor.brandName,
|
|
109
|
+
enumerable: false,
|
|
110
|
+
writable: true,
|
|
111
|
+
configurable: true
|
|
112
|
+
});
|
|
113
|
+
stamp(this, new.target);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var ConfigurationError = class extends ProductCraftError {
|
|
117
|
+
static brandName = "ConfigurationError";
|
|
118
|
+
};
|
|
119
|
+
var WebhookVerificationError = class extends ProductCraftError {
|
|
120
|
+
static brandName = "WebhookVerificationError";
|
|
121
|
+
};
|
|
122
|
+
var APIConnectionError = class extends ProductCraftError {
|
|
123
|
+
static brandName = "APIConnectionError";
|
|
124
|
+
constructor(message = "Connection error.", options) {
|
|
125
|
+
super(message, options);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var APITimeoutError = class extends APIConnectionError {
|
|
129
|
+
static brandName = "APITimeoutError";
|
|
130
|
+
constructor(message = "Request timed out.", options) {
|
|
131
|
+
super(message, options);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var APIUserAbortError = class extends ProductCraftError {
|
|
135
|
+
static brandName = "APIUserAbortError";
|
|
136
|
+
constructor(message = "Request was aborted.", options) {
|
|
137
|
+
super(message, options);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
function envelopeMessage(body, status) {
|
|
141
|
+
if (body && typeof body === "object") {
|
|
142
|
+
const msg = body.message;
|
|
143
|
+
if (typeof msg === "string" && msg !== "") return msg;
|
|
144
|
+
if (Array.isArray(msg) && msg.length > 0) return msg.map(String).join("; ");
|
|
145
|
+
const err = body.error;
|
|
146
|
+
if (typeof err === "string" && err !== "") return err;
|
|
147
|
+
}
|
|
148
|
+
if (typeof body === "string" && body.trim() !== "" && body.length <= 500) return body.trim();
|
|
149
|
+
return `${status} status code`;
|
|
150
|
+
}
|
|
151
|
+
function envelopeErrors(body) {
|
|
152
|
+
if (!body || typeof body !== "object") return [];
|
|
153
|
+
const list = body.errors;
|
|
154
|
+
if (!Array.isArray(list)) return [];
|
|
155
|
+
return list.filter((e) => !!e && typeof e === "object").map((e) => {
|
|
156
|
+
const out = {
|
|
157
|
+
param: typeof e.param === "string" ? e.param : "",
|
|
158
|
+
message: typeof e.message === "string" ? e.message : ""
|
|
159
|
+
};
|
|
160
|
+
if (typeof e.code === "string") out.code = e.code;
|
|
161
|
+
return out;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
var APIError = class _APIError extends ProductCraftError {
|
|
165
|
+
static brandName = "APIError";
|
|
166
|
+
/** HTTP status of the response. */
|
|
167
|
+
status;
|
|
168
|
+
/** Stable, machine-readable code from the error envelope (branch on this, not on `message`). */
|
|
169
|
+
code;
|
|
170
|
+
/** Per-field validation errors from the envelope's `errors`; empty when absent. */
|
|
171
|
+
errors;
|
|
172
|
+
/** The envelope's route-specific `details` object (e.g. `{ fqdn, reason }`), when present. */
|
|
173
|
+
details;
|
|
174
|
+
/** `https://productcraft.co/docs/errors#<code>`, when there is a code. */
|
|
175
|
+
docUrl;
|
|
176
|
+
/** `X-Request-Id` response header, else the envelope's `request_id`. Quote it to support. */
|
|
177
|
+
requestId;
|
|
178
|
+
/** Response headers. */
|
|
179
|
+
headers;
|
|
180
|
+
/** Parsed response body (JSON when it parses, otherwise the text). */
|
|
181
|
+
body;
|
|
182
|
+
constructor(status, body, headers, message) {
|
|
183
|
+
super(message ?? envelopeMessage(body, status));
|
|
184
|
+
this.status = status;
|
|
185
|
+
this.headers = headers;
|
|
186
|
+
this.body = body;
|
|
187
|
+
const env = body && typeof body === "object" ? body : {};
|
|
188
|
+
this.code = typeof env.code === "string" && env.code !== "" ? env.code : void 0;
|
|
189
|
+
this.errors = envelopeErrors(body);
|
|
190
|
+
this.details = env.details && typeof env.details === "object" && !Array.isArray(env.details) ? env.details : void 0;
|
|
191
|
+
this.docUrl = this.code ? `${ERROR_DOCS_URL}#${this.code}` : void 0;
|
|
192
|
+
this.requestId = headers.get("x-request-id") ?? (typeof env.request_id === "string" ? env.request_id : void 0);
|
|
193
|
+
}
|
|
194
|
+
/** Build the right subclass for a status and envelope. */
|
|
195
|
+
static generate(status, body, headers) {
|
|
196
|
+
const code = body && typeof body === "object" && typeof body.code === "string" ? body.code : void 0;
|
|
197
|
+
switch (status) {
|
|
198
|
+
case 400:
|
|
199
|
+
return new BadRequestError(status, body, headers);
|
|
200
|
+
case 401:
|
|
201
|
+
return new AuthenticationError(status, body, headers);
|
|
202
|
+
case 403:
|
|
203
|
+
return new PermissionDeniedError(status, body, headers);
|
|
204
|
+
case 404:
|
|
205
|
+
return new NotFoundError(status, body, headers);
|
|
206
|
+
case 409:
|
|
207
|
+
return code === "IDEMPOTENCY_KEY_REUSE" ? new IdempotencyKeyReuseError(status, body, headers) : new ConflictError(status, body, headers);
|
|
208
|
+
case 422:
|
|
209
|
+
return new UnprocessableEntityError(status, body, headers);
|
|
210
|
+
case 429:
|
|
211
|
+
return new RateLimitError(status, body, headers);
|
|
212
|
+
}
|
|
213
|
+
if (status >= 500) return new InternalServerError(status, body, headers);
|
|
214
|
+
return new _APIError(status, body, headers);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var BadRequestError = class extends APIError {
|
|
218
|
+
static brandName = "BadRequestError";
|
|
219
|
+
};
|
|
220
|
+
var AuthenticationError = class extends APIError {
|
|
221
|
+
static brandName = "AuthenticationError";
|
|
222
|
+
};
|
|
223
|
+
var PermissionDeniedError = class extends APIError {
|
|
224
|
+
static brandName = "PermissionDeniedError";
|
|
225
|
+
};
|
|
226
|
+
var NotFoundError = class extends APIError {
|
|
227
|
+
static brandName = "NotFoundError";
|
|
228
|
+
};
|
|
229
|
+
var ConflictError = class extends APIError {
|
|
230
|
+
static brandName = "ConflictError";
|
|
231
|
+
};
|
|
232
|
+
var IdempotencyKeyReuseError = class extends ConflictError {
|
|
233
|
+
static brandName = "IdempotencyKeyReuseError";
|
|
234
|
+
};
|
|
235
|
+
var UnprocessableEntityError = class extends APIError {
|
|
236
|
+
static brandName = "UnprocessableEntityError";
|
|
237
|
+
};
|
|
238
|
+
var RateLimitError = class extends APIError {
|
|
239
|
+
static brandName = "RateLimitError";
|
|
240
|
+
retryAfter;
|
|
241
|
+
constructor(status, body, headers, message) {
|
|
242
|
+
super(status, body, headers, message);
|
|
243
|
+
const ms = parseRetryAfterMs(headers, Date.now());
|
|
244
|
+
this.retryAfter = ms === void 0 ? void 0 : ms / 1e3;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var InternalServerError = class extends APIError {
|
|
248
|
+
static brandName = "InternalServerError";
|
|
249
|
+
};
|
|
250
|
+
function parseRetryAfterMs(headers, now) {
|
|
251
|
+
const ms = headers.get("retry-after-ms");
|
|
252
|
+
if (ms !== null && ms.trim() !== "") {
|
|
253
|
+
const n = Number(ms);
|
|
254
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
255
|
+
}
|
|
256
|
+
const value = headers.get("retry-after");
|
|
257
|
+
if (value === null || value.trim() === "") return void 0;
|
|
258
|
+
const seconds = Number(value);
|
|
259
|
+
if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1e3 : void 0;
|
|
260
|
+
const date = Date.parse(value);
|
|
261
|
+
if (Number.isNaN(date)) return void 0;
|
|
262
|
+
return Math.max(0, date - now);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/api-promise.ts
|
|
266
|
+
function num(v) {
|
|
267
|
+
if (v === null || v.trim() === "") return void 0;
|
|
268
|
+
const n = Number(v);
|
|
269
|
+
return Number.isFinite(n) ? n : void 0;
|
|
270
|
+
}
|
|
271
|
+
function parseRateLimit(headers) {
|
|
272
|
+
const limit = num(headers.get("x-ratelimit-limit"));
|
|
273
|
+
const remaining = num(headers.get("x-ratelimit-remaining"));
|
|
274
|
+
const reset = num(headers.get("x-ratelimit-reset"));
|
|
275
|
+
if (limit === void 0 && remaining === void 0 && reset === void 0) return void 0;
|
|
276
|
+
return { limit, remaining, reset };
|
|
277
|
+
}
|
|
278
|
+
function attachLastResponse(data, meta) {
|
|
279
|
+
if (data && typeof data === "object" && Object.isExtensible(data) && !(data instanceof Response)) {
|
|
280
|
+
Object.defineProperty(data, "lastResponse", {
|
|
281
|
+
value: meta,
|
|
282
|
+
enumerable: false,
|
|
283
|
+
writable: true,
|
|
284
|
+
configurable: true
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return data;
|
|
288
|
+
}
|
|
289
|
+
var APIPromise = class extends Promise {
|
|
290
|
+
#raw;
|
|
291
|
+
#parse;
|
|
292
|
+
#parsed;
|
|
293
|
+
constructor(raw, parse) {
|
|
294
|
+
super((resolve) => resolve(null));
|
|
295
|
+
this.#raw = raw;
|
|
296
|
+
this.#parse = parse;
|
|
297
|
+
}
|
|
298
|
+
// `.then()` on a subclass must produce plain promises, not new APIPromises.
|
|
299
|
+
static get [Symbol.species]() {
|
|
300
|
+
return Promise;
|
|
301
|
+
}
|
|
302
|
+
#withParsed() {
|
|
303
|
+
if (!this.#parsed) {
|
|
304
|
+
this.#parsed = this.#raw.then(async (raw) => ({
|
|
305
|
+
data: await this.#parse(raw),
|
|
306
|
+
response: raw.response,
|
|
307
|
+
requestId: raw.requestId,
|
|
308
|
+
rateLimit: parseRateLimit(raw.response.headers)
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
return this.#parsed;
|
|
312
|
+
}
|
|
313
|
+
/** The parsed body plus the `Response`, request id and rate-limit headers. */
|
|
314
|
+
withResponse() {
|
|
315
|
+
return this.#withParsed();
|
|
316
|
+
}
|
|
317
|
+
/** The raw `Response`, body unread. Use instead of awaiting (the body can only be read once). */
|
|
318
|
+
asResponse() {
|
|
319
|
+
return this.#raw.then((r) => r.response);
|
|
320
|
+
}
|
|
321
|
+
then(onfulfilled, onrejected) {
|
|
322
|
+
return this.#withParsed().then((r) => r.data).then(onfulfilled, onrejected);
|
|
323
|
+
}
|
|
324
|
+
catch(onrejected) {
|
|
325
|
+
return this.then(void 0, onrejected);
|
|
326
|
+
}
|
|
327
|
+
finally(onfinally) {
|
|
328
|
+
return this.then().finally(onfinally);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/pagination.ts
|
|
333
|
+
var Page = class {
|
|
334
|
+
data;
|
|
335
|
+
has_more;
|
|
336
|
+
next_cursor;
|
|
337
|
+
#fetchPage;
|
|
338
|
+
constructor(body, fetchPage) {
|
|
339
|
+
this.data = Array.isArray(body?.data) ? body.data : [];
|
|
340
|
+
this.next_cursor = body?.pagination?.next_cursor ?? null;
|
|
341
|
+
this.has_more = Boolean(body?.pagination?.has_more) && this.next_cursor !== null;
|
|
342
|
+
this.#fetchPage = fetchPage;
|
|
343
|
+
}
|
|
344
|
+
hasNextPage() {
|
|
345
|
+
return this.has_more;
|
|
346
|
+
}
|
|
347
|
+
/** Fetch the following page. Throws when there is none (check `hasNextPage()` first). */
|
|
348
|
+
getNextPage() {
|
|
349
|
+
if (!this.has_more || this.next_cursor === null) {
|
|
350
|
+
throw new ProductCraftError("No next page; check hasNextPage() before calling getNextPage().");
|
|
351
|
+
}
|
|
352
|
+
return this.#fetchPage(this.next_cursor);
|
|
353
|
+
}
|
|
354
|
+
/** The wire shape, so `JSON.stringify(page)` matches the API response. */
|
|
355
|
+
toJSON() {
|
|
356
|
+
return { data: this.data, pagination: { next_cursor: this.next_cursor, has_more: this.has_more } };
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var PagePromise = class extends APIPromise {
|
|
360
|
+
constructor(raw, parse) {
|
|
361
|
+
super(raw, parse);
|
|
362
|
+
}
|
|
363
|
+
/** Iterate pages, starting with this one. */
|
|
364
|
+
async *pages() {
|
|
365
|
+
let page = await this;
|
|
366
|
+
for (; ; ) {
|
|
367
|
+
yield page;
|
|
368
|
+
if (!page.hasNextPage()) return;
|
|
369
|
+
page = await page.getNextPage();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async *[Symbol.asyncIterator]() {
|
|
373
|
+
for await (const page of this.pages()) {
|
|
374
|
+
for (const item of page.data) yield item;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/** Collect items across pages, stopping at `maxItems`. */
|
|
378
|
+
async toArray(options) {
|
|
379
|
+
const max = options?.maxItems;
|
|
380
|
+
if (typeof max !== "number" || !(max >= 0)) {
|
|
381
|
+
throw new ProductCraftError("toArray() needs { maxItems }, a non-negative number.");
|
|
382
|
+
}
|
|
383
|
+
const out = [];
|
|
384
|
+
if (max === 0) return out;
|
|
385
|
+
for await (const item of this) {
|
|
386
|
+
out.push(item);
|
|
387
|
+
if (out.length >= max) break;
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// src/webhooks.ts
|
|
394
|
+
var encoder = new TextEncoder();
|
|
395
|
+
var decoder = new TextDecoder();
|
|
396
|
+
function normalizeHeaders(headers) {
|
|
397
|
+
const out = /* @__PURE__ */ new Map();
|
|
398
|
+
if (!headers) return out;
|
|
399
|
+
const put = (k, v) => {
|
|
400
|
+
if (v === void 0 || v === null) return;
|
|
401
|
+
const value = Array.isArray(v) ? v.map(String).join(" ") : String(v);
|
|
402
|
+
out.set(k.toLowerCase(), value);
|
|
403
|
+
};
|
|
404
|
+
if (typeof headers.forEach === "function" && typeof headers.get === "function") {
|
|
405
|
+
headers.forEach((v, k) => put(k, v));
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
if (typeof headers[Symbol.iterator] === "function") {
|
|
409
|
+
for (const [k, v] of headers) put(k, v);
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
for (const [k, v] of Object.entries(headers)) put(k, v);
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
function payloadBytes(payload) {
|
|
416
|
+
if (typeof payload === "string") return encoder.encode(payload);
|
|
417
|
+
if (payload instanceof Uint8Array) return payload;
|
|
418
|
+
if (payload instanceof ArrayBuffer) return new Uint8Array(payload);
|
|
419
|
+
throw new WebhookVerificationError(
|
|
420
|
+
"Webhook payload must be the raw request body (string, Buffer, Uint8Array or ArrayBuffer), not a parsed object. Read the body before any JSON parser runs (express.raw(), `await req.text()`)."
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
function base64ToBytes(b64) {
|
|
424
|
+
try {
|
|
425
|
+
const bin = atob(b64);
|
|
426
|
+
const out = new Uint8Array(bin.length);
|
|
427
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
428
|
+
return out;
|
|
429
|
+
} catch {
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function hexToBytes(hex) {
|
|
434
|
+
if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) return void 0;
|
|
435
|
+
const out = new Uint8Array(hex.length / 2);
|
|
436
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
function timingSafeEqual(a, b) {
|
|
440
|
+
if (a.length !== b.length) return false;
|
|
441
|
+
let diff = 0;
|
|
442
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
443
|
+
return diff === 0;
|
|
444
|
+
}
|
|
445
|
+
function concat(...parts) {
|
|
446
|
+
const len = parts.reduce((n, p) => n + p.length, 0);
|
|
447
|
+
const out = new Uint8Array(len);
|
|
448
|
+
let off = 0;
|
|
449
|
+
for (const p of parts) {
|
|
450
|
+
out.set(p, off);
|
|
451
|
+
off += p.length;
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
async function hmacSha256(key, data) {
|
|
456
|
+
const subtle = globalThis.crypto?.subtle;
|
|
457
|
+
if (!subtle) {
|
|
458
|
+
throw new WebhookVerificationError("crypto.subtle is unavailable in this runtime (Node 20.3+ required).");
|
|
459
|
+
}
|
|
460
|
+
const k = await subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
461
|
+
return new Uint8Array(await subtle.sign("HMAC", k, data));
|
|
462
|
+
}
|
|
463
|
+
function standardKey(secret) {
|
|
464
|
+
if (secret.startsWith("whsec_")) {
|
|
465
|
+
const bytes = base64ToBytes(secret.slice("whsec_".length));
|
|
466
|
+
if (!bytes) throw new WebhookVerificationError("Webhook secret is not valid base64 after the whsec_ prefix.");
|
|
467
|
+
return bytes;
|
|
468
|
+
}
|
|
469
|
+
return encoder.encode(secret);
|
|
470
|
+
}
|
|
471
|
+
function checkTimestamp(ts, toleranceSeconds, now) {
|
|
472
|
+
const nowSeconds = Math.floor(now / 1e3);
|
|
473
|
+
if (nowSeconds - ts > toleranceSeconds) throw new WebhookVerificationError("Webhook timestamp is too old.");
|
|
474
|
+
if (ts - nowSeconds > toleranceSeconds) throw new WebhookVerificationError("Webhook timestamp is too far in the future.");
|
|
475
|
+
}
|
|
476
|
+
function parseTimestamp(raw) {
|
|
477
|
+
if (!/^\d+$/.test(raw.trim())) throw new WebhookVerificationError("Webhook timestamp header is not a unix timestamp.");
|
|
478
|
+
return Number(raw.trim());
|
|
479
|
+
}
|
|
480
|
+
function findLegacyHeader(h) {
|
|
481
|
+
for (const [k, v] of h) if (/^x-[a-z0-9-]+-signature$/.test(k)) return v;
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
async function verifyWebhook(params) {
|
|
485
|
+
const { headers, toleranceSeconds = 300 } = params;
|
|
486
|
+
const now = params.now ?? Date.now();
|
|
487
|
+
const secrets = (typeof params.secret === "string" ? [params.secret] : [...params.secret ?? []]).filter(
|
|
488
|
+
(s) => typeof s === "string" && s !== ""
|
|
489
|
+
);
|
|
490
|
+
if (secrets.length === 0) throw new WebhookVerificationError("No webhook secret was given.");
|
|
491
|
+
const body = payloadBytes(params.payload);
|
|
492
|
+
const h = normalizeHeaders(headers);
|
|
493
|
+
const id = h.get("webhook-id");
|
|
494
|
+
const timestamp = h.get("webhook-timestamp");
|
|
495
|
+
const signature = h.get("webhook-signature");
|
|
496
|
+
let verified = false;
|
|
497
|
+
if (id !== void 0 || timestamp !== void 0 || signature !== void 0) {
|
|
498
|
+
if (!id || !timestamp || !signature) {
|
|
499
|
+
throw new WebhookVerificationError("Missing webhook-id, webhook-timestamp or webhook-signature header.");
|
|
500
|
+
}
|
|
501
|
+
const ts = parseTimestamp(timestamp);
|
|
502
|
+
checkTimestamp(ts, toleranceSeconds, now);
|
|
503
|
+
const candidates = signature.split(" ").map((entry) => entry.split(",", 2)).filter(([version, sig]) => version === "v1" && !!sig).map(([, sig]) => base64ToBytes(sig)).filter((b) => !!b);
|
|
504
|
+
const signed = concat(encoder.encode(`${id}.${ts}.`), body);
|
|
505
|
+
for (const secret of secrets) {
|
|
506
|
+
const expected = await hmacSha256(standardKey(secret), signed);
|
|
507
|
+
for (const c of candidates) if (timingSafeEqual(expected, c)) verified = true;
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
const legacy = findLegacyHeader(h);
|
|
511
|
+
if (!legacy) throw new WebhookVerificationError("No webhook signature header found.");
|
|
512
|
+
let t;
|
|
513
|
+
const sigs = [];
|
|
514
|
+
for (const part of legacy.split(",").map((p) => p.trim())) {
|
|
515
|
+
if (part.startsWith("t=")) t = part.slice(2);
|
|
516
|
+
else if (part.startsWith("v1=")) {
|
|
517
|
+
const b = hexToBytes(part.slice(3));
|
|
518
|
+
if (b) sigs.push(b);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (t === void 0 || sigs.length === 0) throw new WebhookVerificationError("Malformed signature header.");
|
|
522
|
+
const ts = parseTimestamp(t);
|
|
523
|
+
checkTimestamp(ts, toleranceSeconds, now);
|
|
524
|
+
const signed = concat(encoder.encode(`${ts}.`), body);
|
|
525
|
+
for (const secret of secrets) {
|
|
526
|
+
const expected = await hmacSha256(encoder.encode(secret), signed);
|
|
527
|
+
for (const c of sigs) if (timingSafeEqual(expected, c)) verified = true;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!verified) throw new WebhookVerificationError("Webhook signature does not match.");
|
|
531
|
+
const text = decoder.decode(body);
|
|
532
|
+
try {
|
|
533
|
+
return JSON.parse(text);
|
|
534
|
+
} catch (cause) {
|
|
535
|
+
throw new WebhookVerificationError("Webhook payload is not valid JSON.", { cause });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/client.ts
|
|
540
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
541
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
542
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
543
|
+
var PC_WORKSPACE_ID_ENV = "PRODUCTCRAFT_WORKSPACE_ID";
|
|
544
|
+
var PC_LOG_ENV = "PRODUCTCRAFT_LOG";
|
|
545
|
+
var WORKSPACE_LOOKUP_TIMEOUT_MS = 5e3;
|
|
546
|
+
var RETRY_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
547
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
|
|
548
|
+
function backoffMs(attempt) {
|
|
549
|
+
return Math.random() * Math.min(8e3, 500 * 2 ** attempt);
|
|
550
|
+
}
|
|
551
|
+
function isBrowser() {
|
|
552
|
+
const g = globalThis;
|
|
553
|
+
return typeof g.window !== "undefined" && typeof g.window?.document !== "undefined" && typeof g.navigator !== "undefined";
|
|
554
|
+
}
|
|
555
|
+
function runtimeTag() {
|
|
556
|
+
const g = globalThis;
|
|
557
|
+
if (g.Bun?.version) return `bun/${g.Bun.version}`;
|
|
558
|
+
if (g.Deno?.version?.deno) return `deno/${g.Deno.version.deno}`;
|
|
559
|
+
if (g.process?.versions?.node) return `node/${g.process.versions.node}`;
|
|
560
|
+
return "runtime/unknown";
|
|
561
|
+
}
|
|
562
|
+
function sleep(ms, signal) {
|
|
563
|
+
return new Promise((resolve, reject) => {
|
|
564
|
+
if (signal?.aborted) return reject(new APIUserAbortError());
|
|
565
|
+
const timer = setTimeout(() => {
|
|
566
|
+
signal?.removeEventListener("abort", onAbort);
|
|
567
|
+
resolve();
|
|
568
|
+
}, ms);
|
|
569
|
+
const onAbort = () => {
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
reject(new APIUserAbortError());
|
|
572
|
+
};
|
|
573
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function isBodyInit(body) {
|
|
577
|
+
return typeof body === "string" || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || typeof Blob !== "undefined" && body instanceof Blob || typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
|
|
578
|
+
}
|
|
579
|
+
async function readErrorBody(res) {
|
|
580
|
+
const text = await res.text().catch(() => "");
|
|
581
|
+
if (text === "") return void 0;
|
|
582
|
+
try {
|
|
583
|
+
return JSON.parse(text);
|
|
584
|
+
} catch {
|
|
585
|
+
return text;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
var customFetch = /* @__PURE__ */ new WeakMap();
|
|
589
|
+
function createOpenAPIClient(api) {
|
|
590
|
+
const client = createClient__default.default({ baseUrl: api.baseUrl, fetch: customFetch.get(api) });
|
|
591
|
+
client.use(authMiddleware(api.auth));
|
|
592
|
+
return client;
|
|
593
|
+
}
|
|
594
|
+
var workspaceCache = /* @__PURE__ */ new Map();
|
|
595
|
+
function _resetWorkspaceCache() {
|
|
596
|
+
workspaceCache.clear();
|
|
597
|
+
}
|
|
598
|
+
var APIClient = class {
|
|
599
|
+
baseUrl;
|
|
600
|
+
workspaceId;
|
|
601
|
+
timeout;
|
|
602
|
+
maxRetries;
|
|
603
|
+
webhookSecret;
|
|
604
|
+
#fetch;
|
|
605
|
+
#defaultHeaders;
|
|
606
|
+
#platformAuthBaseUrl;
|
|
607
|
+
#logLevel;
|
|
608
|
+
#logger;
|
|
609
|
+
#userAgent;
|
|
610
|
+
constructor(options = {}, defaultBaseUrl, sdk) {
|
|
611
|
+
this.baseUrl = (options.baseUrl ?? defaultBaseUrl).replace(/\/+$/, "");
|
|
612
|
+
Object.defineProperty(this, "auth", {
|
|
613
|
+
value: resolveAuth(options.auth ?? (options.apiKey ? { type: "apiKey", key: options.apiKey } : void 0)),
|
|
614
|
+
enumerable: false,
|
|
615
|
+
writable: false
|
|
616
|
+
});
|
|
617
|
+
if (this.auth?.type === "apiKey" && isBrowser() && !options.dangerouslyAllowBrowser) {
|
|
618
|
+
throw new ConfigurationError(
|
|
619
|
+
"A workspace API key was used in a browser, where anyone who loads the page can read it. Call ProductCraft from your server, or pass dangerouslyAllowBrowser: true if you accept that risk."
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
this.workspaceId = options.workspaceId ?? readEnv(PC_WORKSPACE_ID_ENV);
|
|
623
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
624
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
625
|
+
this.webhookSecret = options.webhookSecret;
|
|
626
|
+
this.#fetch = options.fetch;
|
|
627
|
+
customFetch.set(this, options.fetch);
|
|
628
|
+
this.#defaultHeaders = { ...options.defaultHeaders };
|
|
629
|
+
this.#platformAuthBaseUrl = (options.platformAuthBaseUrl ?? PC_BASE_URL.platformAuth).replace(/\/+$/, "");
|
|
630
|
+
const level = options.logLevel ?? readEnv(PC_LOG_ENV);
|
|
631
|
+
this.#logLevel = level === "debug" || level === "warn" ? level : "off";
|
|
632
|
+
this.#logger = options.logger ?? console;
|
|
633
|
+
const product = sdk ? `${sdk.name}@${sdk.version}` : `core@${CORE_VERSION}`;
|
|
634
|
+
this.#userAgent = `productcraft-node/${product} ${runtimeTag()}`;
|
|
635
|
+
}
|
|
636
|
+
#log(level, message, fields) {
|
|
637
|
+
if (this.#logLevel === "off") return;
|
|
638
|
+
if (level === "debug" && this.#logLevel !== "debug") return;
|
|
639
|
+
try {
|
|
640
|
+
this.#logger[level](`[productcraft] ${message}`, fields);
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/** Send one logical API call (with retries). Resolves to the parsed body. */
|
|
645
|
+
request(spec, options = {}) {
|
|
646
|
+
return new APIPromise(this.#send(spec, options), (raw) => this.#parseBody(spec, raw));
|
|
647
|
+
}
|
|
648
|
+
/** A paginated list call. `await` for one page, `for await` for every item. */
|
|
649
|
+
list(spec, options = {}) {
|
|
650
|
+
const at = (cursor) => {
|
|
651
|
+
const pageSpec = cursor === void 0 ? spec : { ...spec, query: { ...spec.query, cursor } };
|
|
652
|
+
return new PagePromise(this.#send(pageSpec, options), async (raw) => {
|
|
653
|
+
const body = await this.#parseBody(pageSpec, raw);
|
|
654
|
+
const page = new Page(body, (next) => at(next));
|
|
655
|
+
return attachLastResponse(page, this.#meta(raw));
|
|
656
|
+
});
|
|
657
|
+
};
|
|
658
|
+
return at();
|
|
659
|
+
}
|
|
660
|
+
/** Retrieve repeatedly until `config.field` reaches a terminal value. */
|
|
661
|
+
async poll(spec, config, options = {}) {
|
|
662
|
+
const { intervalMs = 2e3, timeoutMs = 3e5, ...requestOptions } = options;
|
|
663
|
+
const deadline = Date.now() + timeoutMs;
|
|
664
|
+
for (; ; ) {
|
|
665
|
+
const { data, response } = await this.request(spec, requestOptions).withResponse();
|
|
666
|
+
const value = data?.[config.field];
|
|
667
|
+
if (typeof value === "string" && config.terminal.includes(value)) return data;
|
|
668
|
+
const retryAfter = parseRetryAfterMs(response.headers, Date.now());
|
|
669
|
+
const wait = retryAfter !== void 0 && retryAfter <= MAX_RETRY_AFTER_MS ? retryAfter : intervalMs;
|
|
670
|
+
if (Date.now() + wait > deadline) {
|
|
671
|
+
throw new APITimeoutError(
|
|
672
|
+
`Polling ${spec.path} timed out after ${timeoutMs} ms; last ${config.field} was ${JSON.stringify(value)}.`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
this.#log("debug", "poll", { path: spec.path, [config.field]: value, wait_ms: wait });
|
|
676
|
+
await sleep(wait, options.signal);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/** Create, then poll the created object to a terminal state. */
|
|
680
|
+
async createAndPoll(createSpec, retrieveSpec, config, options = {}) {
|
|
681
|
+
const { intervalMs, timeoutMs, ...requestOptions } = options;
|
|
682
|
+
const created = await this.request(createSpec, requestOptions);
|
|
683
|
+
const value = created?.[config.field];
|
|
684
|
+
if (typeof value === "string" && config.terminal.includes(value)) return created;
|
|
685
|
+
const pollOptions = { ...requestOptions, intervalMs, timeoutMs };
|
|
686
|
+
delete pollOptions.idempotencyKey;
|
|
687
|
+
return this.poll(retrieveSpec(created), config, pollOptions);
|
|
688
|
+
}
|
|
689
|
+
/** Verify a webhook with the given secret, the client's `webhookSecret`, or `envVar`. */
|
|
690
|
+
async unwrapWebhook(params, envVar) {
|
|
691
|
+
const secret = params.secret ?? this.webhookSecret ?? (envVar ? readEnv(envVar) : void 0);
|
|
692
|
+
if (!secret || Array.isArray(secret) && secret.length === 0) {
|
|
693
|
+
throw new ConfigurationError(
|
|
694
|
+
`No webhook secret: pass { secret }, set webhookSecret on the client${envVar ? ` or ${envVar}` : ""}.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return verifyWebhook({ ...params, secret });
|
|
698
|
+
}
|
|
699
|
+
#meta(raw) {
|
|
700
|
+
return { requestId: raw.requestId, status: raw.response.status, headers: raw.response.headers };
|
|
701
|
+
}
|
|
702
|
+
async #parseBody(spec, raw) {
|
|
703
|
+
const res = raw.response;
|
|
704
|
+
if (spec.raw) return res;
|
|
705
|
+
if (res.status === 204 || res.status === 205) return void 0;
|
|
706
|
+
const type = res.headers.get("content-type") ?? "";
|
|
707
|
+
if (!/[/+]json\b/i.test(type)) {
|
|
708
|
+
if (res.headers.get("content-length") === "0") return void 0;
|
|
709
|
+
return res;
|
|
710
|
+
}
|
|
711
|
+
const text = await res.text();
|
|
712
|
+
if (text === "") return void 0;
|
|
713
|
+
const data = JSON.parse(text);
|
|
714
|
+
return attachLastResponse(data, this.#meta(raw));
|
|
715
|
+
}
|
|
716
|
+
async #resolveWorkspaceId(options, fetchImpl) {
|
|
717
|
+
if (options.workspaceId) return options.workspaceId;
|
|
718
|
+
if (this.workspaceId) return this.workspaceId;
|
|
719
|
+
if (this.auth?.type !== "apiKey") {
|
|
720
|
+
throw new ConfigurationError(
|
|
721
|
+
"pass workspaceId: this call is workspace-scoped and the credential is not a workspace API key (bearer and cookie credentials can span workspaces, so the SDK never guesses)."
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
const key = this.auth.key;
|
|
725
|
+
const cacheKey = `${this.#platformAuthBaseUrl}\0${key}`;
|
|
726
|
+
let pending = workspaceCache.get(cacheKey);
|
|
727
|
+
if (!pending) {
|
|
728
|
+
pending = this.#lookupWorkspace(key, fetchImpl);
|
|
729
|
+
workspaceCache.set(cacheKey, pending);
|
|
730
|
+
pending.catch(() => workspaceCache.delete(cacheKey));
|
|
731
|
+
}
|
|
732
|
+
return pending;
|
|
733
|
+
}
|
|
734
|
+
async #lookupWorkspace(key, fetchImpl) {
|
|
735
|
+
this.#log("debug", "resolving workspaceId", { path: "/v1/api-keys/self" });
|
|
736
|
+
let res;
|
|
737
|
+
try {
|
|
738
|
+
res = await fetchImpl(`${this.#platformAuthBaseUrl}/v1/api-keys/self`, {
|
|
739
|
+
method: "GET",
|
|
740
|
+
headers: { Authorization: `Bearer ${key}`, Accept: "application/json", "User-Agent": this.#userAgent },
|
|
741
|
+
signal: AbortSignal.timeout(WORKSPACE_LOOKUP_TIMEOUT_MS)
|
|
742
|
+
});
|
|
743
|
+
} catch (cause) {
|
|
744
|
+
throw new ConfigurationError("pass workspaceId: looking it up from the API key failed.", { cause });
|
|
745
|
+
}
|
|
746
|
+
if (!res.ok) {
|
|
747
|
+
const body2 = await readErrorBody(res);
|
|
748
|
+
throw new ConfigurationError(
|
|
749
|
+
`pass workspaceId: looking it up from the API key failed with ${res.status}.`,
|
|
750
|
+
{ cause: APIError.generate(res.status, body2, res.headers) }
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const body = await res.json().catch(() => void 0);
|
|
754
|
+
const id = body?.workspace?.id;
|
|
755
|
+
if (typeof id !== "string" || id === "") {
|
|
756
|
+
throw new ConfigurationError("pass workspaceId: the API key lookup returned no workspace id.");
|
|
757
|
+
}
|
|
758
|
+
return id;
|
|
759
|
+
}
|
|
760
|
+
async #buildUrl(spec, options, fetchImpl) {
|
|
761
|
+
const params = { ...spec.pathParams };
|
|
762
|
+
if (spec.path.includes("{workspace_id}") && params.workspace_id === void 0) {
|
|
763
|
+
params.workspace_id = await this.#resolveWorkspaceId(options, fetchImpl);
|
|
764
|
+
}
|
|
765
|
+
const path = spec.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
766
|
+
const value = params[name];
|
|
767
|
+
if (value === void 0 || value === null || value === "") {
|
|
768
|
+
throw new ProductCraftError(`Missing path parameter "${name}" for ${spec.method} ${spec.path}.`);
|
|
769
|
+
}
|
|
770
|
+
return encodeURIComponent(String(value));
|
|
771
|
+
});
|
|
772
|
+
const search = new URLSearchParams();
|
|
773
|
+
for (const [k, v] of Object.entries(spec.query ?? {})) {
|
|
774
|
+
if (v === void 0 || v === null) continue;
|
|
775
|
+
for (const item of Array.isArray(v) ? v : [v]) {
|
|
776
|
+
if (item === void 0 || item === null) continue;
|
|
777
|
+
search.append(k, item instanceof Date ? item.toISOString() : typeof item === "object" ? JSON.stringify(item) : String(item));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const qs = search.toString();
|
|
781
|
+
return `${this.baseUrl}${path}${qs ? `?${qs}` : ""}`;
|
|
782
|
+
}
|
|
783
|
+
async #send(spec, options) {
|
|
784
|
+
const fetchImpl = options.fetch ?? this.#fetch ?? globalThis.fetch;
|
|
785
|
+
if (typeof fetchImpl !== "function") {
|
|
786
|
+
throw new ConfigurationError("No fetch implementation: pass { fetch } or use Node 20.3+.");
|
|
787
|
+
}
|
|
788
|
+
if (options.signal?.aborted) throw new APIUserAbortError();
|
|
789
|
+
const url = await this.#buildUrl(spec, options, fetchImpl);
|
|
790
|
+
const headers = new Headers();
|
|
791
|
+
headers.set("Accept", "application/json");
|
|
792
|
+
if (!isBrowser()) headers.set("User-Agent", this.#userAgent);
|
|
793
|
+
for (const source of [this.#defaultHeaders, spec.headers, options.headers]) {
|
|
794
|
+
for (const [k, v] of Object.entries(source ?? {})) {
|
|
795
|
+
if (v === void 0) headers.delete(k);
|
|
796
|
+
else headers.set(k, v);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (!spec.public && this.auth) {
|
|
800
|
+
if (this.auth.type === "cookie") headers.set("Cookie", `auth_token=${this.auth.value}`);
|
|
801
|
+
else headers.set("Authorization", `Bearer ${this.auth.type === "apiKey" ? this.auth.key : this.auth.token}`);
|
|
802
|
+
}
|
|
803
|
+
if (options.idempotencyKey) headers.set("Idempotency-Key", options.idempotencyKey);
|
|
804
|
+
else if (spec.idempotent && !headers.has("Idempotency-Key")) headers.set("Idempotency-Key", `pc_${crypto.randomUUID()}`);
|
|
805
|
+
let body;
|
|
806
|
+
if (spec.body !== void 0) {
|
|
807
|
+
if (isBodyInit(spec.body)) body = spec.body;
|
|
808
|
+
else {
|
|
809
|
+
body = JSON.stringify(spec.body);
|
|
810
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const retryable = SAFE_METHODS.has(spec.method) || headers.has("Idempotency-Key");
|
|
814
|
+
const maxRetries = Math.max(0, options.maxRetries ?? this.maxRetries);
|
|
815
|
+
const timeout = options.timeout ?? this.timeout;
|
|
816
|
+
for (let attempt = 0; ; attempt++) {
|
|
817
|
+
if (attempt > 0) headers.set("X-PC-SDK-Retry-Count", String(attempt));
|
|
818
|
+
const timeoutCtl = new AbortController();
|
|
819
|
+
const timer = setTimeout(() => timeoutCtl.abort(), timeout);
|
|
820
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeoutCtl.signal]) : timeoutCtl.signal;
|
|
821
|
+
const canRetry = retryable && attempt < maxRetries;
|
|
822
|
+
this.#log("debug", "request", { method: spec.method, path: spec.path, attempt });
|
|
823
|
+
let res;
|
|
824
|
+
try {
|
|
825
|
+
res = await fetchImpl(url, { method: spec.method, headers, body, signal });
|
|
826
|
+
} catch (cause) {
|
|
827
|
+
clearTimeout(timer);
|
|
828
|
+
if (options.signal?.aborted) throw new APIUserAbortError(void 0, { cause });
|
|
829
|
+
const error2 = timeoutCtl.signal.aborted ? new APITimeoutError(`Request timed out after ${timeout} ms.`, { cause }) : new APIConnectionError(`Connection error: ${cause?.message ?? String(cause)}`, { cause });
|
|
830
|
+
if (!canRetry) throw error2;
|
|
831
|
+
const wait2 = backoffMs(attempt);
|
|
832
|
+
this.#log("debug", "retrying after connection error", { method: spec.method, path: spec.path, attempt, wait_ms: Math.round(wait2) });
|
|
833
|
+
await sleep(wait2, options.signal);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
clearTimeout(timer);
|
|
837
|
+
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
838
|
+
if (res.ok) {
|
|
839
|
+
this.#log("debug", "response", { method: spec.method, path: spec.path, status: res.status, request_id: requestId, attempt });
|
|
840
|
+
return { response: res, requestId };
|
|
841
|
+
}
|
|
842
|
+
const errorBody = await readErrorBody(res);
|
|
843
|
+
const error = APIError.generate(res.status, errorBody, res.headers);
|
|
844
|
+
this.#log("debug", "response", { method: spec.method, path: spec.path, status: res.status, request_id: error.requestId, attempt });
|
|
845
|
+
const statusRetryable = RETRY_STATUSES.has(res.status) || res.status === 409 && error.code === "IDEMPOTENCY_IN_PROGRESS";
|
|
846
|
+
if (!canRetry || !statusRetryable) throw error;
|
|
847
|
+
const retryAfter = parseRetryAfterMs(res.headers, Date.now());
|
|
848
|
+
if (retryAfter !== void 0 && retryAfter > MAX_RETRY_AFTER_MS) {
|
|
849
|
+
this.#log("warn", "Retry-After above 60 s, not retrying", { path: spec.path, status: res.status, request_id: error.requestId });
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
const wait = retryAfter ?? backoffMs(attempt);
|
|
853
|
+
this.#log("debug", "retrying", { method: spec.method, path: spec.path, status: res.status, attempt, wait_ms: Math.round(wait) });
|
|
854
|
+
await sleep(wait, options.signal);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
};
|
|
63
858
|
|
|
859
|
+
exports.APIClient = APIClient;
|
|
860
|
+
exports.APIConnectionError = APIConnectionError;
|
|
861
|
+
exports.APIError = APIError;
|
|
862
|
+
exports.APIPromise = APIPromise;
|
|
863
|
+
exports.APITimeoutError = APITimeoutError;
|
|
864
|
+
exports.APIUserAbortError = APIUserAbortError;
|
|
865
|
+
exports.AuthenticationError = AuthenticationError;
|
|
866
|
+
exports.BadRequestError = BadRequestError;
|
|
867
|
+
exports.CORE_VERSION = CORE_VERSION;
|
|
868
|
+
exports.ConfigurationError = ConfigurationError;
|
|
869
|
+
exports.ConflictError = ConflictError;
|
|
870
|
+
exports.DEFAULT_MAX_RETRIES = DEFAULT_MAX_RETRIES;
|
|
871
|
+
exports.DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
|
|
872
|
+
exports.ERROR_DOCS_URL = ERROR_DOCS_URL;
|
|
873
|
+
exports.IdempotencyKeyReuseError = IdempotencyKeyReuseError;
|
|
874
|
+
exports.InternalServerError = InternalServerError;
|
|
875
|
+
exports.MAX_RETRY_AFTER_MS = MAX_RETRY_AFTER_MS;
|
|
876
|
+
exports.NotFoundError = NotFoundError;
|
|
64
877
|
exports.PC_API_KEY_ENV = PC_API_KEY_ENV;
|
|
65
878
|
exports.PC_AUTH_APP_ENV = PC_AUTH_APP_ENV;
|
|
66
879
|
exports.PC_BASE_URL = PC_BASE_URL;
|
|
880
|
+
exports.PC_LOG_ENV = PC_LOG_ENV;
|
|
881
|
+
exports.PC_WORKSPACE_ID_ENV = PC_WORKSPACE_ID_ENV;
|
|
882
|
+
exports.Page = Page;
|
|
883
|
+
exports.PagePromise = PagePromise;
|
|
884
|
+
exports.PermissionDeniedError = PermissionDeniedError;
|
|
885
|
+
exports.ProductCraftError = ProductCraftError;
|
|
886
|
+
exports.RateLimitError = RateLimitError;
|
|
887
|
+
exports.UnprocessableEntityError = UnprocessableEntityError;
|
|
888
|
+
exports.WebhookVerificationError = WebhookVerificationError;
|
|
889
|
+
exports._resetWorkspaceCache = _resetWorkspaceCache;
|
|
890
|
+
exports.attachLastResponse = attachLastResponse;
|
|
67
891
|
exports.authMiddleware = authMiddleware;
|
|
892
|
+
exports.backoffMs = backoffMs;
|
|
893
|
+
exports.createOpenAPIClient = createOpenAPIClient;
|
|
68
894
|
exports.makeClient = makeClient;
|
|
895
|
+
exports.normalizeHeaders = normalizeHeaders;
|
|
896
|
+
exports.parseRateLimit = parseRateLimit;
|
|
897
|
+
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
69
898
|
exports.readEnv = readEnv;
|
|
70
899
|
exports.resolveAuth = resolveAuth;
|
|
900
|
+
exports.timingSafeEqual = timingSafeEqual;
|
|
901
|
+
exports.verifyWebhook = verifyWebhook;
|
|
71
902
|
//# sourceMappingURL=index.cjs.map
|
|
72
903
|
//# sourceMappingURL=index.cjs.map
|