@spacefast/wpcloud-sdk 0.0.23 → 0.0.24

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/runtime.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
- import path from "node:path";
4
2
  import { createClient, createConfig } from "./generated/client";
5
3
  import { mergeHeaders } from "./generated/client/utils.gen";
6
4
  const WP_CLOUD_REQUEST_TIMEOUT_MS = 30_000;
7
5
  const WP_CLOUD_METRICS_MAX_ATTEMPTS = 3;
8
6
  const OPTIONAL_PATH_SEGMENT_PATTERN = /\[\/([^\]]+)\]/g;
9
- const SNAPSHOT_VERSION = 1;
10
7
  export class WpCloudRequestError extends Error {
11
8
  status;
12
9
  payload;
@@ -36,19 +33,10 @@ export function parseRetryAfterSeconds(value) {
36
33
  }
37
34
  return Math.max(0, Math.ceil((date - Date.now()) / 1000));
38
35
  }
39
- let fakeFetchOverride = null;
40
36
  let wpCloudCallObserver = null;
41
37
  export function setWpCloudCallObserver(fn) {
42
38
  wpCloudCallObserver = fn;
43
39
  }
44
- /**
45
- * Test-only seam: route every wp.cloud call to an in-process fake instead of the network. Set in a
46
- * suite's `beforeAll`, clear with `null` in `afterAll`. Used when the provider mode is `fake`; with
47
- * no override installed, `fake` mode falls back to the shared default fake server.
48
- */
49
- export function setWpCloudFakeFetch(fn) {
50
- fakeFetchOverride = fn;
51
- }
52
40
  export const createClientConfig = (config) => ({
53
41
  ...config,
54
42
  fetch: wpCloudFetch,
@@ -69,7 +57,19 @@ export function createWpCloudClient(options) {
69
57
  auth: options.auth,
70
58
  })));
71
59
  }
60
+ // An ambient cancellation source consulted per call — the control plane
61
+ // registers its operation-lease signal here (wp-cloud/config.ts) so a fenced
62
+ // executor's in-flight provider calls cancel instead of running to completion.
63
+ let wpCloudCallSignalProvider = null;
64
+ export function setWpCloudCallSignalProvider(fn) {
65
+ wpCloudCallSignalProvider = fn;
66
+ }
72
67
  async function wpCloudFetch(input, init) {
68
+ const ambient = wpCloudCallSignalProvider?.() ?? null;
69
+ if (ambient) {
70
+ const own = init?.signal ?? (input instanceof Request ? input.signal : null);
71
+ init = { ...init, signal: own ? AbortSignal.any([own, ambient]) : ambient };
72
+ }
73
73
  const request = wpCloudRequestIdentity(input, init);
74
74
  const startedAt = performance.now();
75
75
  let ok = false;
@@ -93,7 +93,7 @@ async function wpCloudFetch(input, init) {
93
93
  async function performWpCloudFetch(input, init, request) {
94
94
  const isMetricsRequest = request.method === "POST" &&
95
95
  /\/metrics\/(?:site|client)\/[^/]+(?:\/summarize)?\/?$/.test(request.path);
96
- const maxAttempts = isMetricsRequest && wpCloudProviderMode() !== "replay" ? WP_CLOUD_METRICS_MAX_ATTEMPTS : 1;
96
+ const maxAttempts = isMetricsRequest ? WP_CLOUD_METRICS_MAX_ATTEMPTS : 1;
97
97
  const failures = [];
98
98
  /* eslint-disable no-await-in-loop -- metrics retries must complete sequentially and stop at the first atomic response. */
99
99
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
@@ -219,20 +219,7 @@ function errorMessage(error) {
219
219
  return error instanceof Error ? error.message : String(error);
220
220
  }
221
221
  export async function wpCloudRawFetch(input, init) {
222
- const mode = wpCloudProviderMode();
223
- if (mode === "fake") {
224
- const fake = fakeFetchOverride ?? (await import("./fake/server.js")).defaultFakeFetch;
225
- return await fake(input, init);
226
- }
227
- const snapshot = await buildWpCloudSnapshotRequest(input, init);
228
- if (mode === "replay") {
229
- return await replayWpCloudSnapshot(snapshot);
230
- }
231
- const response = await wpCloudNetworkFetch(input, init);
232
- if (mode === "record") {
233
- return await recordWpCloudSnapshot(snapshot, response);
234
- }
235
- return response;
222
+ return wpCloudNetworkFetch(input, init);
236
223
  }
237
224
  async function wpCloudNetworkFetch(input, init) {
238
225
  const controller = new AbortController();
@@ -250,7 +237,16 @@ async function wpCloudNetworkFetch(input, init) {
250
237
  }
251
238
  let response;
252
239
  try {
253
- response = await fetch(input, { ...init, signal: controller.signal });
240
+ // Atomic API access in development can require the operator's authenticated
241
+ // HTTPS proxy. Bun does not consistently apply the environment value to
242
+ // fetch, so pass it explicitly; production without one keeps direct I/O.
243
+ const proxy = process.env.HTTPS_PROXY?.trim() || process.env.https_proxy?.trim();
244
+ const requestInit = {
245
+ ...init,
246
+ signal: controller.signal,
247
+ ...(proxy ? { proxy } : {}),
248
+ };
249
+ response = await fetch(input, requestInit);
254
250
  }
255
251
  catch (error) {
256
252
  if (didTimeout) {
@@ -264,293 +260,6 @@ async function wpCloudNetworkFetch(input, init) {
264
260
  }
265
261
  return response;
266
262
  }
267
- let manifestWriteQueue = Promise.resolve();
268
- function wpCloudProviderMode() {
269
- const mode = (process.env.E2E_PROVIDER_MODE ??
270
- process.env.WP_CLOUD_PROVIDER_MODE ??
271
- "live").toLowerCase();
272
- if (mode === "record" || mode === "replay" || mode === "fake") {
273
- return mode;
274
- }
275
- return "live";
276
- }
277
- function wpCloudSnapshotDir() {
278
- const baseDir = process.env.E2E_WPCLOUD_SNAPSHOT_DIR ?? process.env.WP_CLOUD_SNAPSHOT_DIR;
279
- if (!baseDir) {
280
- return undefined;
281
- }
282
- const snapshotCase = snapshotCaseName();
283
- return snapshotCase ? path.join(baseDir, snapshotCase) : baseDir;
284
- }
285
- function requireWpCloudSnapshotDir() {
286
- const dir = wpCloudSnapshotDir();
287
- if (!dir) {
288
- throw new WpCloudRequestError(599, "wp_cloud_snapshot_dir_required", null);
289
- }
290
- return dir;
291
- }
292
- async function buildWpCloudSnapshotRequest(input, init) {
293
- const url = canonicalWpCloudSnapshotUrl(input);
294
- const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
295
- const bodySha256 = sha256(await requestBodyForSnapshot(init?.body));
296
- const key = sha256(`${method}\n${url}\n${bodySha256}`);
297
- return {
298
- key,
299
- method,
300
- url,
301
- bodySha256,
302
- };
303
- }
304
- function canonicalWpCloudSnapshotUrl(input) {
305
- const rawUrl = input instanceof Request ? input.url : String(input);
306
- const url = new URL(rawUrl);
307
- url.searchParams.sort();
308
- return url.toString();
309
- }
310
- async function requestBodyForSnapshot(body) {
311
- if (body === undefined || body === null) {
312
- return "";
313
- }
314
- if (typeof body === "string") {
315
- return body;
316
- }
317
- if (body instanceof URLSearchParams) {
318
- return body.toString();
319
- }
320
- if (body instanceof Blob) {
321
- return await body.text();
322
- }
323
- if (body instanceof ArrayBuffer) {
324
- return Buffer.from(body).toString("base64");
325
- }
326
- if (ArrayBuffer.isView(body)) {
327
- return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("base64");
328
- }
329
- return `[${body.constructor.name}]`;
330
- }
331
- async function replayWpCloudSnapshot(request) {
332
- const snapshot = await readWpCloudSnapshot(request);
333
- if (!snapshot) {
334
- throw new WpCloudRequestError(599, `wp_cloud_snapshot_missing:${request.key}`, {
335
- method: request.method,
336
- url: request.url,
337
- bodySha256: request.bodySha256,
338
- });
339
- }
340
- assertSnapshotMatches(request, snapshot);
341
- return new Response(Buffer.from(snapshot.response.bodyBase64, "base64"), {
342
- status: snapshot.response.status,
343
- statusText: snapshot.response.statusText,
344
- headers: snapshot.response.headers,
345
- });
346
- }
347
- async function recordWpCloudSnapshot(request, response) {
348
- const body = Buffer.from(await response.arrayBuffer());
349
- // The live caller gets the untouched body (it may need real values, e.g. an SFTP password to
350
- // SSH in during the same run). Only what we PERSIST is scrubbed: create-site and friends return
351
- // secrets in their JSON bodies, and these snapshots are committed as fixtures.
352
- const storedBody = redactSnapshotResponseBody(body);
353
- const bodyBase64 = storedBody.toString("base64");
354
- const recordedAt = new Date().toISOString();
355
- const headers = redactedResponseHeaders(response.headers);
356
- const snapshot = {
357
- version: SNAPSHOT_VERSION,
358
- recordedAt,
359
- ...request,
360
- response: {
361
- status: response.status,
362
- statusText: response.statusText,
363
- headers,
364
- bodyBase64,
365
- },
366
- };
367
- const destination = wpCloudSnapshotWritePath(request);
368
- await mkdir(path.dirname(destination), { recursive: true });
369
- await writeJsonAtomic(destination, snapshot);
370
- await writeWpCloudSnapshotManifest({
371
- ...request,
372
- redactedUrl: redactedSnapshotUrl(request.url),
373
- recordedAt,
374
- response: {
375
- status: response.status,
376
- statusText: response.statusText,
377
- headers,
378
- bodySha256: sha256(bodyBase64),
379
- bodyBytes: storedBody.byteLength,
380
- },
381
- });
382
- return new Response(body, {
383
- status: response.status,
384
- statusText: response.statusText,
385
- headers: response.headers,
386
- });
387
- }
388
- /** Keys whose values are secrets WP.Cloud returns in response bodies (create-site, sftp users, db). */
389
- const SENSITIVE_BODY_KEY = /pass(word)?|secret|private[_-]?key|privatekey|credential|sftp_pass|db_pass|one[_-]?time|onetime|otp|nonce|api[_-]?key|access[_-]?token/i;
390
- /**
391
- * Scrub secrets from a recorded response body before it is written to a committed fixture, while
392
- * preserving the JSON SHAPE (keys + types) that the fake provider keys off. Masks values under
393
- * sensitive keys; runs the existing text scrub over remaining string leaves and non-JSON bodies.
394
- */
395
- function redactSnapshotResponseBody(body) {
396
- if (body.byteLength === 0) {
397
- return body;
398
- }
399
- const text = body.toString("utf8");
400
- let parsed;
401
- try {
402
- parsed = JSON.parse(text);
403
- }
404
- catch {
405
- return Buffer.from(redactSensitiveText(text), "utf8");
406
- }
407
- return Buffer.from(JSON.stringify(maskSecretValues(parsed)), "utf8");
408
- }
409
- function maskSecretValues(value) {
410
- if (Array.isArray(value)) {
411
- return value.map(maskSecretValues);
412
- }
413
- if (value && typeof value === "object") {
414
- const out = {};
415
- for (const [key, child] of Object.entries(value)) {
416
- out[key] =
417
- SENSITIVE_BODY_KEY.test(key) && (typeof child === "string" || typeof child === "number")
418
- ? "[REDACTED]"
419
- : maskSecretValues(child);
420
- }
421
- return out;
422
- }
423
- if (typeof value === "string") {
424
- return redactSensitiveText(value);
425
- }
426
- return value;
427
- }
428
- async function writeWpCloudSnapshotManifest(entry) {
429
- manifestWriteQueue = manifestWriteQueue.then(async () => {
430
- const manifestPath = wpCloudSnapshotManifestPath();
431
- const manifest = await readWpCloudSnapshotManifest();
432
- const entries = manifest.entries.filter((existing) => existing.key !== entry.key);
433
- entries.push(entry);
434
- entries.sort((left, right) => `${left.method} ${left.redactedUrl} ${left.bodySha256}`.localeCompare(`${right.method} ${right.redactedUrl} ${right.bodySha256}`));
435
- const next = {
436
- version: SNAPSHOT_VERSION,
437
- case: snapshotCaseName(),
438
- entries,
439
- };
440
- await mkdir(path.dirname(manifestPath), { recursive: true });
441
- await writeJsonAtomic(manifestPath, next);
442
- });
443
- await manifestWriteQueue;
444
- }
445
- async function readWpCloudSnapshotManifest() {
446
- try {
447
- const parsed = JSON.parse(await readFile(wpCloudSnapshotManifestPath(), "utf8"));
448
- return {
449
- version: SNAPSHOT_VERSION,
450
- case: typeof parsed?.case === "string" ? parsed.case : snapshotCaseName(),
451
- entries: Array.isArray(parsed?.entries) ? parsed.entries : [],
452
- };
453
- }
454
- catch (error) {
455
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
456
- return {
457
- version: SNAPSHOT_VERSION,
458
- case: snapshotCaseName(),
459
- entries: [],
460
- };
461
- }
462
- throw error;
463
- }
464
- }
465
- async function readWpCloudSnapshot(request) {
466
- const results = await Promise.allSettled(wpCloudSnapshotReadPaths(request).map(async (snapshotPath) => JSON.parse(await readFile(snapshotPath, "utf8"))));
467
- for (const result of results) {
468
- if (result.status === "fulfilled") {
469
- return result.value;
470
- }
471
- const error = result.reason;
472
- if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
473
- throw error;
474
- }
475
- }
476
- return null;
477
- }
478
- function wpCloudSnapshotWritePath(request) {
479
- return path.join(requireWpCloudSnapshotDir(), `${request.key}.json`);
480
- }
481
- function wpCloudSnapshotReadPaths(request) {
482
- const dir = requireWpCloudSnapshotDir();
483
- const paths = [path.join(dir, `${request.key}.json`)];
484
- const baseDir = wpCloudSnapshotBaseDir();
485
- if (baseDir && baseDir !== dir) {
486
- paths.push(path.join(baseDir, `${request.key}.json`));
487
- }
488
- return paths;
489
- }
490
- function wpCloudSnapshotBaseDir() {
491
- return process.env.E2E_WPCLOUD_SNAPSHOT_DIR ?? process.env.WP_CLOUD_SNAPSHOT_DIR;
492
- }
493
- function wpCloudSnapshotManifestPath() {
494
- return path.join(requireWpCloudSnapshotDir(), "_manifest.json");
495
- }
496
- function snapshotCaseName() {
497
- return sanitizeSnapshotCase(process.env.E2E_WPCLOUD_SNAPSHOT_CASE ?? process.env.WP_CLOUD_SNAPSHOT_CASE ?? "");
498
- }
499
- function sanitizeSnapshotCase(value) {
500
- return value
501
- .trim()
502
- .toLowerCase()
503
- .replaceAll(/[^a-z0-9._-]+/g, "-")
504
- .replaceAll(/^-+|-+$/g, "");
505
- }
506
- function redactedSnapshotUrl(value) {
507
- const url = new URL(value);
508
- for (const [name, current] of Array.from(url.searchParams.entries())) {
509
- if (/token|secret|key|password|private|certificate|auth|cookie/i.test(name)) {
510
- url.searchParams.set(name, "[REDACTED]");
511
- }
512
- else {
513
- url.searchParams.set(name, redactSensitiveText(current));
514
- }
515
- }
516
- return url.toString();
517
- }
518
- async function writeJsonAtomic(destination, value) {
519
- const tempPath = `${destination}.${process.pid}.${Date.now()}.${Math.random()
520
- .toString(16)
521
- .slice(2)}.tmp`;
522
- await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
523
- await rename(tempPath, destination);
524
- }
525
- function assertSnapshotMatches(request, snapshot) {
526
- if (snapshot.version !== SNAPSHOT_VERSION ||
527
- snapshot.method !== request.method ||
528
- snapshot.url !== request.url ||
529
- snapshot.bodySha256 !== request.bodySha256) {
530
- throw new WpCloudRequestError(599, `wp_cloud_snapshot_mismatch:${request.key}`, {
531
- method: request.method,
532
- url: request.url,
533
- bodySha256: request.bodySha256,
534
- });
535
- }
536
- }
537
- function redactedResponseHeaders(headers) {
538
- const out = {};
539
- for (const [name, value] of headers) {
540
- const lowerName = name.toLowerCase();
541
- if (lowerName === "set-cookie" || lowerName === "authorization" || lowerName === "auth") {
542
- continue;
543
- }
544
- out[lowerName] = redactSensitiveText(value);
545
- }
546
- return out;
547
- }
548
- function redactSensitiveText(value) {
549
- return value
550
- .replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]")
551
- .replace(/Auth[=:]\s*[A-Za-z0-9._~+/-]+=*/gi, "Auth=[REDACTED]")
552
- .replace(/token[=:]\s*["']?[A-Za-z0-9._~+/-]+=*["']?/gi, "token=[REDACTED]");
553
- }
554
263
  function sha256(value) {
555
264
  return createHash("sha256").update(value).digest("hex");
556
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacefast/wpcloud-sdk",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "license": "MIT",
5
5
  "description": "Generated TypeScript client for the WP.Cloud API.",
6
6
  "repository": {
@@ -29,11 +29,6 @@
29
29
  "types": "./dist/runtime.d.ts",
30
30
  "import": "./dist/runtime.js",
31
31
  "default": "./dist/runtime.js"
32
- },
33
- "./fake": {
34
- "types": "./dist/fake/index.d.ts",
35
- "import": "./dist/fake/index.js",
36
- "default": "./dist/fake/index.js"
37
32
  }
38
33
  }
39
34
  }
@@ -1,2 +0,0 @@
1
- export { makeFakeAtomic, makeFakeAtomicWithStore } from "./server.js";
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fake/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
@@ -1 +0,0 @@
1
- export { makeFakeAtomic, makeFakeAtomicWithStore } from "./server.js";
@@ -1,13 +0,0 @@
1
- import { FakeAtomicStore } from "./store.js";
2
- export type FakeResult = {
3
- status: number;
4
- body: unknown;
5
- headers?: Record<string, string>;
6
- };
7
- /**
8
- * @param path the path after `/api/v1.0/`, e.g. `create-site/minipage` or `site-meta/151/_data/get`
9
- * (segments still URL-encoded; this fn decodes the ones it needs)
10
- * @param body the nested-decoded request body (form-urlencoded → object)
11
- */
12
- export declare function routeFakeRequest(store: FakeAtomicStore, method: string, path: string, body: Record<string, unknown>): FakeResult;
13
- //# sourceMappingURL=router.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/fake/router.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAiD7F;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,EACtB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,UAAU,CA0RZ"}