@codewheel/jsonapi-frontend-client 1.0.0 → 1.0.2

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 CHANGED
@@ -12,7 +12,7 @@ npm i @codewheel/jsonapi-frontend-client
12
12
 
13
13
  ## Usage
14
14
 
15
- Set `DRUPAL_BASE_URL`, then:
15
+ Set `DRUPAL_BASE_URL` (must be a full `http(s)://` URL), then:
16
16
 
17
17
  ```ts
18
18
  import { resolvePath, fetchJsonApi } from "@codewheel/jsonapi-frontend-client"
@@ -23,3 +23,17 @@ if (resolved.resolved && resolved.kind === "entity") {
23
23
  console.log(doc.data)
24
24
  }
25
25
  ```
26
+
27
+ ## URL safety (recommended)
28
+
29
+ By default, `fetchJsonApi()` and `fetchView()` refuse to fetch absolute URLs on a different origin than your `DRUPAL_BASE_URL` (to avoid accidental SSRF in server environments).
30
+
31
+ If you intentionally need to fetch a cross-origin absolute URL, pass `allowExternalUrls: true`:
32
+
33
+ ```ts
34
+ import { fetchJsonApi } from "@codewheel/jsonapi-frontend-client"
35
+
36
+ await fetchJsonApi("https://cms.example.com/jsonapi/node/page/...", {
37
+ allowExternalUrls: true,
38
+ })
39
+ ```
package/dist/fetch.d.ts CHANGED
@@ -13,6 +13,8 @@ export declare function buildViewCacheTags(dataUrl: string): string[];
13
13
  export declare function fetchJsonApi<T = JsonApiDocument>(jsonapiPath: string, options?: {
14
14
  baseUrl?: string;
15
15
  envKey?: string;
16
+ /** Allow fetching absolute URLs on other origins (default: false). */
17
+ allowExternalUrls?: boolean;
16
18
  include?: string[];
17
19
  fields?: Record<string, string[]>;
18
20
  revalidate?: number;
@@ -25,6 +27,8 @@ export declare function fetchJsonApi<T = JsonApiDocument>(jsonapiPath: string, o
25
27
  export declare function fetchView<T = JsonApiDocument>(dataUrl: string, options?: {
26
28
  baseUrl?: string;
27
29
  envKey?: string;
30
+ /** Allow fetching absolute URLs on other origins (default: false). */
31
+ allowExternalUrls?: boolean;
28
32
  /**
29
33
  * JSON:API pagination parameters.
30
34
  *
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAuD,MAAM,aAAa,CAAA;AACvG,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAEzC;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBlE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAa5D;AAED,wBAAsB,YAAY,CAAC,CAAC,GAAG,eAAe,EACpD,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,CAAC,CAAC,CAsCZ;AAED,wBAAsB,SAAS,CAAC,CAAC,GAAG,eAAe,EACjD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,CAAC,CAAC,CAyCZ"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAuD,MAAM,aAAa,CAAA;AACvG,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAEzC;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBlE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAa5D;AAoBD,wBAAsB,YAAY,CAAC,CAAC,GAAG,eAAe,EACpD,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,CAAC,CAAC,CAsCZ;AAED,wBAAsB,SAAS,CAAC,CAAC,GAAG,eAAe,EACjD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,CAAC,CAAC,CAyCZ"}
package/dist/fetch.js CHANGED
@@ -34,10 +34,22 @@ export function buildViewCacheTags(dataUrl) {
34
34
  tags.push(`view:${viewId}--${displayId}`);
35
35
  return tags;
36
36
  }
37
+ function buildSafeUrl(input, base, options) {
38
+ const baseUrl = new URL(base);
39
+ const url = new URL(input, baseUrl);
40
+ if (!options?.allowExternalUrls && url.origin !== baseUrl.origin) {
41
+ throw new Error(`Refusing to fetch a URL from a different origin (${url.origin}) than base (${baseUrl.origin}). ` +
42
+ "Pass allowExternalUrls: true to override.");
43
+ }
44
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
45
+ throw new Error(`Unsupported URL protocol "${url.protocol}" (expected http/https)`);
46
+ }
47
+ return url;
48
+ }
37
49
  export async function fetchJsonApi(jsonapiPath, options) {
38
50
  const base = getDrupalBaseUrlFromOptions({ baseUrl: options?.baseUrl, envKey: options?.envKey });
39
51
  const fetcher = getFetch(options?.fetch);
40
- const url = new URL(jsonapiPath, base);
52
+ const url = buildSafeUrl(jsonapiPath, base, { allowExternalUrls: options?.allowExternalUrls });
41
53
  if (options?.include?.length) {
42
54
  url.searchParams.set("include", options.include.join(","));
43
55
  }
@@ -68,7 +80,7 @@ export async function fetchJsonApi(jsonapiPath, options) {
68
80
  export async function fetchView(dataUrl, options) {
69
81
  const base = getDrupalBaseUrlFromOptions({ baseUrl: options?.baseUrl, envKey: options?.envKey });
70
82
  const fetcher = getFetch(options?.fetch);
71
- const url = new URL(dataUrl, base);
83
+ const url = buildSafeUrl(dataUrl, base, { allowExternalUrls: options?.allowExternalUrls });
72
84
  if (options?.page !== undefined) {
73
85
  if (typeof options.page === "number") {
74
86
  url.searchParams.set("page[offset]", String(options.page));
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG;IACpC,IAAI,CAAC,EAAE,gBAAgB,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAOzF,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMnG;AAED,wBAAgB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAMzD;AAED,wBAAgB,YAAY,CAAC,GAAG,WAAW,EAAE,KAAK,CAAC,WAAW,GAAG,SAAS,CAAC,GAAG,OAAO,CAOpF"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG;IACpC,IAAI,CAAC,EAAE,gBAAgB,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAOzF,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAkBnG;AAED,wBAAgB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAMzD;AAED,wBAAgB,YAAY,CAAC,GAAG,WAAW,EAAE,KAAK,CAAC,WAAW,GAAG,SAAS,CAAC,GAAG,OAAO,CAOpF"}
package/dist/transport.js CHANGED
@@ -3,11 +3,21 @@ function getEnvString(key) {
3
3
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
4
4
  }
5
5
  export function getDrupalBaseUrlFromOptions(options) {
6
- const baseUrl = options?.baseUrl ?? getEnvString(options?.envKey ?? "DRUPAL_BASE_URL");
7
- if (!baseUrl) {
6
+ const rawBaseUrl = options?.baseUrl ?? getEnvString(options?.envKey ?? "DRUPAL_BASE_URL");
7
+ if (!rawBaseUrl) {
8
8
  throw new Error(`Missing Drupal base URL (pass baseUrl or set ${options?.envKey ?? "DRUPAL_BASE_URL"})`);
9
9
  }
10
- return baseUrl.replace(/\/$/, "");
10
+ let parsed;
11
+ try {
12
+ parsed = new URL(rawBaseUrl);
13
+ }
14
+ catch {
15
+ throw new Error(`Invalid Drupal base URL "${rawBaseUrl}" (expected http(s) URL)`);
16
+ }
17
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
18
+ throw new Error(`Invalid Drupal base URL protocol "${parsed.protocol}" (expected http/https)`);
19
+ }
20
+ return parsed.toString().replace(/\/$/, "");
11
21
  }
12
22
  export function getFetch(fetchLike) {
13
23
  const f = fetchLike ?? globalThis.fetch;
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@codewheel/jsonapi-frontend-client",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "TypeScript client helpers for Drupal drupal/jsonapi_frontend",
5
- "homepage": "https://github.com/CodeWheel-AI/jsonapi-frontend-client#readme",
5
+ "homepage": "https://github.com/code-wheel/jsonapi-frontend-client#readme",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/CodeWheel-AI/jsonapi-frontend-client.git"
8
+ "url": "git+https://github.com/code-wheel/jsonapi-frontend-client.git"
9
9
  },
10
10
  "bugs": {
11
- "url": "https://github.com/CodeWheel-AI/jsonapi-frontend-client/issues"
11
+ "url": "https://github.com/code-wheel/jsonapi-frontend-client/issues"
12
12
  },
13
13
  "keywords": [
14
14
  "drupal",