@spacefast/wpcloud-sdk 0.0.10 → 0.0.12

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.
@@ -1 +1 @@
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,CAAA;CAAE,CAAC;AA6C3D;;;;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,CA0NZ"}
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,CAAA;CAAE,CAAC;AAiD3D;;;;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,CA2NZ"}
@@ -11,6 +11,9 @@ const fail = (status, message) => ({
11
11
  body: { message, data: [] },
12
12
  });
13
13
  const RESTRICTED_DOMAINS = new Set(["wordpress.com", "wp.com", "automattic.com"]);
14
+ function asString(value, fallback = "") {
15
+ return typeof value === "string" ? value : fallback;
16
+ }
14
17
  function asStringMap(value) {
15
18
  const out = {};
16
19
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
@@ -93,16 +96,17 @@ export function routeFakeRequest(store, method, path, body) {
93
96
  // POST /create-site/{client}
94
97
  if (method === "POST" && head === "create-site") {
95
98
  const meta = asStringMap(body.meta);
96
- const domainName = String(body.domain_name ?? `fake-${store.nextJobId()}.wpcomstaging.com`);
99
+ const domainName = asString(body.domain_name, `fake-${store.nextJobId()}.wpcomstaging.com`);
97
100
  return ok(store.createSite({ domainName, meta }));
98
101
  }
99
102
  // POST /delete-site/{service}/{identifier}
100
103
  if (method === "POST" && head === "delete-site") {
101
104
  return ok(store.delete(seg[2] ?? ""));
102
105
  }
103
- // GET /get-site/{site}[/extra]
106
+ // GET /get-site/{site}[/extra] — `{site}` is a site id OR any hostname the site
107
+ // answers to: its own domain, or one of its aliases (see `FakeAtomicStore#resolve`).
104
108
  if (method === "GET" && head === "get-site") {
105
- const site = store.get(seg[1] ?? "");
109
+ const site = store.resolve(decodeURIComponent(seg[1] ?? ""));
106
110
  return site ? ok(store.siteExtra(site)) : fail(404, "Site not found.");
107
111
  }
108
112
  // GET /job-completion/{id} , GET /job-status/{id} — jobs settle immediately in the fake
@@ -131,7 +135,7 @@ export function routeFakeRequest(store, method, path, body) {
131
135
  return ok(store.getMeta(id ?? "", key ?? "") ?? null);
132
136
  }
133
137
  if (method === "POST" && (action === "update" || action === "add")) {
134
- store.setMeta(id ?? "", key ?? "", String(body.value ?? ""));
138
+ store.setMeta(id ?? "", key ?? "", asString(body.value));
135
139
  return ok([]);
136
140
  }
137
141
  }
@@ -158,8 +162,8 @@ export function routeFakeRequest(store, method, path, body) {
158
162
  const id = seg[1] ?? "";
159
163
  const action = seg[2];
160
164
  if (method === "POST" && action === "add") {
161
- const schedule = String(body.schedule ?? "");
162
- const command = String(body.command ?? "");
165
+ const schedule = asString(body.schedule);
166
+ const command = asString(body.command);
163
167
  if (!schedule || !command)
164
168
  return fail(400, "Invalid or empty schedule or command.");
165
169
  const cron = store.addCron(id, { schedule, command });
@@ -179,7 +183,7 @@ export function routeFakeRequest(store, method, path, body) {
179
183
  }
180
184
  if (method === "POST" && action === "update") {
181
185
  const cronId = Number(seg[3] ?? "");
182
- const command = String(body.command ?? "");
186
+ const command = asString(body.command);
183
187
  if (!command)
184
188
  return fail(400, "Invalid or empty command.");
185
189
  return store.updateCron(id, cronId, command)
@@ -198,7 +202,7 @@ export function routeFakeRequest(store, method, path, body) {
198
202
  if (op === "on" || op === "off" || op === "purge")
199
203
  return ok([]);
200
204
  if (op === "get")
201
- return ok({ state: "enabled", status_code: 200, ddos_until: null });
205
+ return ok({ status: 1, status_name: "Enabled", ddos_until: 0 });
202
206
  }
203
207
  // /ssl-info/{host} — lean: not provisioned (loadWpCloudSslInfo tolerates null). SSL = direct suites.
204
208
  if (head === "ssl-info")
@@ -244,7 +248,7 @@ export function routeFakeRequest(store, method, path, body) {
244
248
  if (action === "update" && store.getClientMeta(key) === undefined) {
245
249
  return fail(404, "Not Found");
246
250
  }
247
- store.setClientMeta(key, String(body.value));
251
+ store.setClientMeta(key, asString(body.value));
248
252
  return ok([]);
249
253
  }
250
254
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/fake/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA2C7C;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG;IACrF,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,EAAE,eAAe,CAAC;CACxB,CAkBA;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,gBAAgB,CAE7F;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,gBAE7B,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/fake/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA+D7C;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG;IACrF,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,EAAE,eAAe,CAAC;CACxB,CAoBA;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,gBAAgB,CAE7F;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,gBAE7B,CAAC"}
@@ -3,9 +3,27 @@
3
3
  * `makeFakeAtomic()` gives a fresh, isolated store (per-suite use via `setWpCloudFakeFetch`);
4
4
  * `defaultFakeFetch` is a process-global instance for env-driven `WP_CLOUD_PROVIDER_MODE=fake`.
5
5
  */
6
- import { randomInt } from "node:crypto";
6
+ import { createHash, randomInt } from "node:crypto";
7
7
  import { routeFakeRequest } from "./router.js";
8
8
  import { FakeAtomicStore } from "./store.js";
9
+ let fakeStoreOrdinal = 0;
10
+ function defaultSiteSequenceStart() {
11
+ // Bun's isolated test worker updates `process.argv[1]` to the active test
12
+ // file. Hash that real file identity plus the process and store ordinal so
13
+ // DB-backed files sharing Postgres never restart at the same provider ref.
14
+ // This is deterministic input, not random test data; explicit starts remain
15
+ // available for tests that assert exact captured provider IDs.
16
+ const identity = [
17
+ process.env.GITHUB_RUN_ID ?? "local",
18
+ process.env.GITHUB_RUN_ATTEMPT ?? "0",
19
+ process.ppid,
20
+ process.pid,
21
+ process.argv[1] ?? "unknown",
22
+ fakeStoreOrdinal++,
23
+ ].join("\0");
24
+ const digest = createHash("sha256").update(identity).digest();
25
+ return 1_000_000_000_000 + digest.readUIntBE(0, 6);
26
+ }
9
27
  /** Decode an `application/x-www-form-urlencoded` body, un-nesting `key[child]` / `key[]` keys. */
10
28
  function parseNestedForm(raw) {
11
29
  const out = {};
@@ -55,7 +73,9 @@ async function readBody(input, init) {
55
73
  * persistent data.
56
74
  */
57
75
  export function makeFakeAtomicWithStore(options = {}) {
58
- const store = new FakeAtomicStore(options);
76
+ const store = new FakeAtomicStore({
77
+ siteSequenceStart: options.siteSequenceStart ?? defaultSiteSequenceStart(),
78
+ });
59
79
  const fetch = async (input, init) => {
60
80
  const rawUrl = input instanceof Request ? input.url : String(input);
61
81
  const url = new URL(rawUrl);
@@ -65,6 +65,13 @@ export declare class FakeAtomicStore {
65
65
  }): FakeSite;
66
66
  snapshot(): FakeSiteSnapshot[];
67
67
  get(id: string): FakeSite | undefined;
68
+ /**
69
+ * Atomic's `get_site_by_site_id_or_domain`: an argument containing a dot is
70
+ * matched against `atomic_site.domain_name` first, then against the alias
71
+ * table (`atomic_domain` joined onto `atomic_site`); anything else is a site
72
+ * id. This is what lets one `get-site` call answer "who holds this hostname?".
73
+ */
74
+ resolve(idOrDomain: string): FakeSite | undefined;
68
75
  delete(id: string): {
69
76
  job_id: number;
70
77
  };
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/fake/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/fake/store.ts"],"names":[],"mappings":"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"}
@@ -76,6 +76,20 @@ export class FakeAtomicStore {
76
76
  get(id) {
77
77
  return this.sites.get(id);
78
78
  }
79
+ /**
80
+ * Atomic's `get_site_by_site_id_or_domain`: an argument containing a dot is
81
+ * matched against `atomic_site.domain_name` first, then against the alias
82
+ * table (`atomic_domain` joined onto `atomic_site`); anything else is a site
83
+ * id. This is what lets one `get-site` call answer "who holds this hostname?".
84
+ */
85
+ resolve(idOrDomain) {
86
+ if (!idOrDomain.includes(".")) {
87
+ return this.sites.get(idOrDomain);
88
+ }
89
+ const sites = [...this.sites.values()];
90
+ return (sites.find((site) => site.domainName === idOrDomain) ??
91
+ sites.find((site) => site.aliases.has(idOrDomain)));
92
+ }
79
93
  delete(id) {
80
94
  this.sites.delete(id);
81
95
  return { job_id: this.nextJobId() };
@@ -181,7 +195,9 @@ export class FakeAtomicStore {
181
195
  delete site.persistentDataEnv[key];
182
196
  continue;
183
197
  }
184
- site.persistentData[key] = String(record.value ?? "");
198
+ // The wire form is `application/x-www-form-urlencoded`, so a scalar value
199
+ // always decodes to a string; anything else was never a persistable value.
200
+ site.persistentData[key] = typeof record.value === "string" ? record.value : "";
185
201
  site.persistentDataEnv[key] = formFlag(record.env);
186
202
  }
187
203
  return this.persistentData(id);
@@ -0,0 +1,2 @@
1
+ export declare const value = "real";
2
+ //# sourceMappingURL=isolation-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isolation-target.d.ts","sourceRoot":"","sources":["../../../src/fake/test-fixtures/isolation-target.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,SAAS,CAAC"}
@@ -0,0 +1 @@
1
+ export const value = "real";
package/dist/runtime.d.ts CHANGED
@@ -4,9 +4,19 @@ export type { Client } from "./generated/client";
4
4
  export declare class WpCloudRequestError extends Error {
5
5
  status: number;
6
6
  payload: unknown;
7
- constructor(status: number, message: string, payload: unknown);
7
+ retryAfterSeconds: number | null;
8
+ constructor(status: number, message: string, payload: unknown, retryAfterSeconds?: number | null);
8
9
  }
10
+ export declare function parseRetryAfterSeconds(value: string | null): number | null;
9
11
  export type WpCloudFakeFetch = (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => Promise<Response>;
12
+ export type WpCloudCallObservation = {
13
+ method: string;
14
+ path: string;
15
+ startedAt: number;
16
+ endedAt: number;
17
+ ok: boolean;
18
+ };
19
+ export declare function setWpCloudCallObserver(fn: ((observation: WpCloudCallObservation) => void) | null): void;
10
20
  /**
11
21
  * Test-only seam: route every wp.cloud call to an in-process fake instead of the network. Set in a
12
22
  * suite's `beforeAll`, clear with `null` in `afterAll`. Used when the provider mode is `fake`; with
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,MAAM,EAA8B,MAAM,oBAAoB,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGjE,YAAY,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAejD,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;gBAEL,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;CAM9D;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,KAAK,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAClC,IAAI,CAAC,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,KAC/B,OAAO,CAAC,QAAQ,CAAC,CAAC;AAIvB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,gBAAgB,GAAG,IAAI,QAE9D;AAED,eAAO,MAAM,kBAAkB,EAAE,kBAO/B,CAAC;AAOH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAUvF;AAUD,wBAAsB,eAAe,CACnC,KAAK,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAClC,IAAI,CAAC,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,qBAkBnC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,MAAM,EAA8B,MAAM,oBAAoB,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGjE,YAAY,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAgBjD,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IAIjB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;gBAG/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,iBAAiB,GAAE,MAAM,GAAG,IAAW;CAQ1C;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAa1E;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,KAAK,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAClC,IAAI,CAAC,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,KAC/B,OAAO,CAAC,QAAQ,CAAC,CAAC;AAOvB,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,OAAO,CAAC;CACb,CAAC;AAIF,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,sBAAsB,KAAK,IAAI,CAAC,GAAG,IAAI,GACzD,IAAI,CAEN;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,gBAAgB,GAAG,IAAI,QAE9D;AAED,eAAO,MAAM,kBAAkB,EAAE,kBAO/B,CAAC;AAOH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAUvF;AAmMD,wBAAsB,eAAe,CACnC,KAAK,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAClC,IAAI,CAAC,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,qBAkBnC"}
package/dist/runtime.js CHANGED
@@ -4,19 +4,43 @@ import path from "node:path";
4
4
  import { createClient, createConfig } from "./generated/client";
5
5
  import { mergeHeaders } from "./generated/client/utils.gen";
6
6
  const WP_CLOUD_REQUEST_TIMEOUT_MS = 30_000;
7
+ const WP_CLOUD_METRICS_MAX_ATTEMPTS = 3;
7
8
  const OPTIONAL_PATH_SEGMENT_PATTERN = /\[\/([^\]]+)\]/g;
8
9
  const SNAPSHOT_VERSION = 1;
9
10
  export class WpCloudRequestError extends Error {
10
11
  status;
11
12
  payload;
12
- constructor(status, message, payload) {
13
+ // Seconds the provider asked us to wait, parsed from its `Retry-After`
14
+ // header (delta-seconds or HTTP date). null when the provider sent none.
15
+ // Callers back off on what the provider actually says, never on a guess.
16
+ retryAfterSeconds;
17
+ constructor(status, message, payload, retryAfterSeconds = null) {
13
18
  super(message);
14
19
  this.name = "WpCloudRequestError";
15
20
  this.status = status;
16
21
  this.payload = payload;
22
+ this.retryAfterSeconds = retryAfterSeconds;
17
23
  }
18
24
  }
25
+ export function parseRetryAfterSeconds(value) {
26
+ if (!value) {
27
+ return null;
28
+ }
29
+ const trimmed = value.trim();
30
+ if (/^\d+$/.test(trimmed)) {
31
+ return Number(trimmed);
32
+ }
33
+ const date = Date.parse(trimmed);
34
+ if (Number.isNaN(date)) {
35
+ return null;
36
+ }
37
+ return Math.max(0, Math.ceil((date - Date.now()) / 1000));
38
+ }
19
39
  let fakeFetchOverride = null;
40
+ let wpCloudCallObserver = null;
41
+ export function setWpCloudCallObserver(fn) {
42
+ wpCloudCallObserver = fn;
43
+ }
20
44
  /**
21
45
  * Test-only seam: route every wp.cloud call to an in-process fake instead of the network. Set in a
22
46
  * suite's `beforeAll`, clear with `null` in `afterAll`. Used when the provider mode is `fake`; with
@@ -46,8 +70,153 @@ export function createWpCloudClient(options) {
46
70
  })));
47
71
  }
48
72
  async function wpCloudFetch(input, init) {
49
- const response = await wpCloudRawFetch(input, init);
50
- return await handleWpCloudResponse(response);
73
+ const request = wpCloudRequestIdentity(input, init);
74
+ const startedAt = performance.now();
75
+ let ok = false;
76
+ try {
77
+ const response = await performWpCloudFetch(input, init, request);
78
+ ok = true;
79
+ return response;
80
+ }
81
+ finally {
82
+ const observer = wpCloudCallObserver;
83
+ if (observer) {
84
+ try {
85
+ observer({ ...request, startedAt, endedAt: performance.now(), ok });
86
+ }
87
+ catch {
88
+ // Instrumentation must not affect provider calls.
89
+ }
90
+ }
91
+ }
92
+ }
93
+ async function performWpCloudFetch(input, init, request) {
94
+ const isMetricsRequest = request.method === "POST" &&
95
+ /\/metrics\/(?:site|client)\/[^/]+(?:\/summarize)?\/?$/.test(request.path);
96
+ const maxAttempts = isMetricsRequest && wpCloudProviderMode() !== "replay" ? WP_CLOUD_METRICS_MAX_ATTEMPTS : 1;
97
+ const failures = [];
98
+ /* eslint-disable no-await-in-loop -- metrics retries must complete sequentially and stop at the first atomic response. */
99
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
100
+ const attemptInput = input instanceof Request ? input.clone() : input;
101
+ const response = await wpCloudRawFetch(attemptInput, init);
102
+ const handledResponse = await handleWpCloudResponse(response);
103
+ if (!isMetricsRequest) {
104
+ return handledResponse;
105
+ }
106
+ try {
107
+ return await validateWpCloudMetricsResponse(handledResponse);
108
+ }
109
+ catch (error) {
110
+ if (!(error instanceof WpCloudResponseIntegrityError)) {
111
+ throw error;
112
+ }
113
+ failures.push({ attempt, ...error.failure });
114
+ if (attempt === maxAttempts) {
115
+ throw new WpCloudRequestError(502, error.message, {
116
+ operation: "metrics",
117
+ request,
118
+ maxAttempts,
119
+ failures,
120
+ });
121
+ }
122
+ }
123
+ }
124
+ /* eslint-enable no-await-in-loop */
125
+ throw new WpCloudRequestError(502, "wp_cloud_metrics_response_unavailable", {
126
+ operation: "metrics",
127
+ request,
128
+ maxAttempts,
129
+ failures,
130
+ });
131
+ }
132
+ class WpCloudResponseIntegrityError extends Error {
133
+ failure;
134
+ constructor(message, failure) {
135
+ super(message);
136
+ this.name = "WpCloudResponseIntegrityError";
137
+ this.failure = failure;
138
+ }
139
+ }
140
+ function wpCloudRequestIdentity(input, init) {
141
+ const url = new URL(input instanceof Request ? input.url : String(input));
142
+ return {
143
+ method: (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(),
144
+ path: url.pathname,
145
+ };
146
+ }
147
+ async function validateWpCloudMetricsResponse(response) {
148
+ const contentType = response.headers.get("content-type");
149
+ const rawContentLength = response.headers.get("content-length");
150
+ const parsedContentLength = rawContentLength !== null && /^\d+$/.test(rawContentLength) ? Number(rawContentLength) : null;
151
+ let body;
152
+ try {
153
+ body = Buffer.from(await response.arrayBuffer());
154
+ }
155
+ catch (error) {
156
+ throw new WpCloudResponseIntegrityError("wp_cloud_metrics_response_read_failed", {
157
+ reason: "body_read_failed",
158
+ status: response.status,
159
+ contentType,
160
+ declaredContentLength: parsedContentLength,
161
+ bodyBytes: null,
162
+ bodySha256: null,
163
+ detail: errorMessage(error),
164
+ });
165
+ }
166
+ const bodyMetadata = {
167
+ status: response.status,
168
+ contentType,
169
+ declaredContentLength: parsedContentLength,
170
+ bodyBytes: body.byteLength,
171
+ bodySha256: sha256(body),
172
+ };
173
+ if (parsedContentLength !== null && parsedContentLength !== body.byteLength) {
174
+ throw new WpCloudResponseIntegrityError("wp_cloud_metrics_response_length_mismatch", {
175
+ reason: "content_length_mismatch",
176
+ ...bodyMetadata,
177
+ });
178
+ }
179
+ if (!contentType ||
180
+ !(contentType.toLowerCase().startsWith("application/json") ||
181
+ contentType.toLowerCase().split(";", 1)[0]?.trim().endsWith("+json"))) {
182
+ throw new WpCloudResponseIntegrityError("wp_cloud_metrics_response_invalid_content_type", {
183
+ reason: "invalid_content_type",
184
+ ...bodyMetadata,
185
+ });
186
+ }
187
+ let parsed;
188
+ try {
189
+ parsed = JSON.parse(body.toString("utf8"));
190
+ }
191
+ catch (error) {
192
+ throw new WpCloudResponseIntegrityError("wp_cloud_metrics_response_invalid_json", {
193
+ reason: "invalid_json",
194
+ ...bodyMetadata,
195
+ detail: errorMessage(error),
196
+ });
197
+ }
198
+ if (!parsed ||
199
+ typeof parsed !== "object" ||
200
+ Array.isArray(parsed) ||
201
+ typeof parsed.message !== "string" ||
202
+ !(parsed.data &&
203
+ typeof parsed.data === "object" &&
204
+ !Array.isArray(parsed.data) &&
205
+ Array.isArray(parsed.data.periods))) {
206
+ throw new WpCloudResponseIntegrityError("wp_cloud_metrics_response_invalid_envelope", {
207
+ reason: "invalid_metrics_envelope",
208
+ ...bodyMetadata,
209
+ detail: "expected_atomic_data_periods",
210
+ });
211
+ }
212
+ return new Response(Uint8Array.from(body).buffer, {
213
+ status: response.status,
214
+ statusText: response.statusText,
215
+ headers: response.headers,
216
+ });
217
+ }
218
+ function errorMessage(error) {
219
+ return error instanceof Error ? error.message : String(error);
51
220
  }
52
221
  export async function wpCloudRawFetch(input, init) {
53
222
  const mode = wpCloudProviderMode();
@@ -396,7 +565,7 @@ async function handleWpCloudResponse(response) {
396
565
  typeof payload.message === "string"
397
566
  ? payload.message
398
567
  : `wp_cloud_request_failed:${response.status}`;
399
- throw new WpCloudRequestError(response.status, message, payload);
568
+ throw new WpCloudRequestError(response.status, message, payload, parseRetryAfterSeconds(response.headers.get("retry-after")));
400
569
  }
401
570
  async function readErrorPayload(response) {
402
571
  const text = await response.text();
@@ -464,6 +633,7 @@ async function prepareWpCloudRequest(options) {
464
633
  request.path ??= {};
465
634
  request.url = request.url.replace(OPTIONAL_PATH_SEGMENT_PATTERN, (_segment, name) => {
466
635
  const value = request.path?.[name];
636
+ // oxlint-disable-next-line typescript/no-base-to-string -- generated path values are URL-encoded primitives at runtime.
467
637
  return value === undefined || value === null ? "" : `/${encodeURIComponent(String(value))}`;
468
638
  });
469
639
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacefast/wpcloud-sdk",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "license": "MIT",
5
5
  "description": "Generated TypeScript client for the WP.Cloud API.",
6
6
  "repository": {