@qelos/plugin-netlify-api 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Netlify build plugin for Qelos frontend apps: proxies `/api/*` to your Qelos API
4
4
 
5
5
  ## Setup
6
6
 
7
- In your site’s `netlify.toml`, add the plugin next to your existing `[build]` / `[build.environment]` (same idea as a manual `[functions]` + `[[redirects]]` + `api-proxy.ts`, but without checking in the function or redirect yourself):
7
+ In your site’s `netlify.toml`, add the plugin next to your existing `[build]` / `[build.environment]`:
8
8
 
9
9
  ```toml
10
10
  [build]
@@ -12,14 +12,13 @@ In your site’s `netlify.toml`, add the plugin next to your existing `[build]`
12
12
  publish = "dist"
13
13
 
14
14
  [build.environment]
15
- # Used by the injected proxy at runtime (and by your build if you read it in app config)
16
15
  QELOS_API_IP = "http://159.203.152.168"
17
16
 
18
17
  [[plugins]]
19
18
  package = "@qelos/plugin-netlify-api"
20
19
  ```
21
20
 
22
- **Nuxt / static frameworks (required):** Netlify evaluates `dist/_redirects` **before** `netlify.toml`. Nitro’s `netlify-static` preset emits a catch‑all `/*` rule first, so you must prepend the API rewrite **after** `nuxt build`. Add an npm **`postbuild`** script (runs automatically when Netlify runs `npm run build`):
21
+ **Nuxt / static frameworks (required):** Netlify evaluates `dist/_redirects` **before** `netlify.toml`. Nitro’s `netlify-static` preset emits a catch‑all `/*` rule, so the API rule must be **first** in `_redirects`. Add an npm **`postbuild`** script (runs after `nuxt build` when Netlify runs `npm run build`):
23
22
 
24
23
  ```json
25
24
  {
@@ -30,16 +29,19 @@ In your site’s `netlify.toml`, add the plugin next to your existing `[build]`
30
29
  }
31
30
  ```
32
31
 
33
- If your publish directory is not `dist`, set `QELOS_NETLIFY_PUBLISH` to that folder name (relative to the app root) for the patch script.
32
+ If your publish directory is not `dist`, set `QELOS_NETLIFY_PUBLISH` to that folder name (relative to the app root).
34
33
 
35
- Minimal variant (defaults API URL inside the plugin if you omit env):
34
+ ## How it proxies (default)
36
35
 
37
- ```toml
38
- [[plugins]]
39
- package = "@qelos/plugin-netlify-api"
40
- ```
36
+ By default the plugin uses **Netlify’s CDN rewrite** to your Qelos API (same pattern as [Netlify’s proxy docs](https://docs.netlify.com/manage/routing/redirects/rewrites-proxies/#proxy-to-another-service)):
41
37
 
42
- Optional: set the API URL (default `http://159.203.152.168`) and/or enable bypass-admin header:
38
+ - `/api/*` → `${QELOS_API_IP origin}/api/:splat` with status `200` and `force`
39
+
40
+ So `/api/me` is proxied to `http://your-api-host/api/me` **at the edge**, without a serverless function. That avoids fragile `/.netlify/functions/...` rewrites and matches Nuxt’s `_redirects` behavior.
41
+
42
+ Optional: **`use_function_proxy = true`** — uses the bundled `qelos-api-proxy` serverless function instead (for cases where you must run Node logic on each request).
43
+
44
+ Optional plugin inputs:
43
45
 
44
46
  ```toml
45
47
  [[plugins]]
@@ -48,17 +50,17 @@ Optional: set the API URL (default `http://159.203.152.168`) and/or enable bypas
48
50
  [plugins.inputs]
49
51
  api_url = "http://159.203.152.168"
50
52
  bypass_admin = true
53
+ use_function_proxy = false
51
54
  ```
52
55
 
53
- - **api_url** — Target for the `/api/*` proxy. You can also set **`QELOS_API_IP`** or **`API_HOST`** in `[build.environment]` or in the Netlify UI (same role as `API_HOST` in a hand-written `netlify/functions/api-proxy.ts`).
54
- - **bypass_admin** — If `true`, the proxy adds the header `x-bypass-admin: true` to every request so that admin users are scoped as regular users. See root docs: `documentation/docs/plugins/api-proxy.md` and `documentation/docs/sdk/token_refresh.md`.
56
+ - **api_url** — Qelos API base URL (same as `QELOS_API_IP` / `API_HOST`).
57
+ - **bypass_admin** — If `true`, adds header `x-bypass-admin: true` on proxied requests (CDN and function modes).
58
+ - **use_function_proxy** — If `true`, deploy the bundled `qelos-api-proxy` function and rewrite to it instead of the CDN proxy.
55
59
 
56
60
  ## What it does
57
61
 
58
- - **Environment:** Sets `QELOS_API_IP` for the build (and for the proxy function at runtime).
59
- - **Redirect:** Adds a rewrite in `netlify.toml` and ships **`qelos-netlify-patch-redirects`** to prepend the same line to **`publish/_redirects`** (use the **`postbuild`** script above — do not rely on the build plugin alone for Nuxt).
60
- - **Function:** Copies `qelos-api-proxy.ts` from the package into your Netlify **functions directory** (default `netlify/functions`, or `[functions] directory` in `netlify.toml`) during `onPreBuild`, so the bundler sees a normal function file. Forwards requests using `QELOS_API_IP` / `API_HOST` (full URL or hostname).
61
-
62
- No need to add redirects or commit the proxy yourself; the plugin writes it each build. Optional: add `netlify/functions/qelos-api-proxy.ts` to `.gitignore` if you run local Netlify builds and do not want the copy tracked.
62
+ - **Environment:** Sets `QELOS_API_IP`, `QELOS_NETLIFY_PROXY_MODE` (`cdn` or `function`).
63
+ - **Redirects:** Injects the `/api/*` rewrite into `netlify.toml` (and **`postbuild`** rewrites `publish/_redirects` so Nuxt’s catch‑all does not win).
64
+ - **Function mode only:** Copies `qelos-api-proxy.ts` into your Netlify functions directory.
63
65
 
64
- **Parity with a manual Netlify function** (e.g. `[[redirects]]` → `/.netlify/functions/api-proxy` and a function using `process.env.API_HOST`): use this plugin instead of those entries; set `QELOS_API_IP` or `API_HOST` to the same value you used for `API_HOST`, and set `bypass_admin = true` under `[plugins.inputs]` if your old proxy always sent `x-bypass-admin: true`.
66
+ No need to commit redirects yourself; the plugin and `postbuild` maintain them each build.
package/index.js CHANGED
@@ -21,55 +21,91 @@ function resolvePublishDir(constants, netlifyConfig) {
21
21
  return path.isAbsolute(p) ? p : path.join(process.cwd(), p);
22
22
  }
23
23
 
24
+ function upstreamOrigin(apiUrl) {
25
+ const s = String(apiUrl ?? '').trim().replace(/\/$/, '');
26
+ if (!s) {
27
+ return new URL(DEFAULT_API_URL).origin;
28
+ }
29
+ if (/^https?:\/\//i.test(s)) {
30
+ return new URL(s).origin;
31
+ }
32
+ return `http://${s}`;
33
+ }
34
+
35
+ function stripApiRedirects(redirects) {
36
+ return (redirects || []).filter((r) => r && r.from !== '/api/*');
37
+ }
38
+
24
39
  module.exports = {
25
40
  async onPreBuild({ netlifyConfig, inputs }) {
26
41
  const apiUrl =
27
42
  inputs.api_url ?? process.env.QELOS_API_IP ?? DEFAULT_API_URL;
28
43
  const bypassAdmin =
29
44
  inputs.bypass_admin === true || inputs.bypass_admin === 'true';
45
+ const useFunctionProxy =
46
+ inputs.use_function_proxy === true || inputs.use_function_proxy === 'true';
30
47
 
31
- // Ensure build.environment exists
32
48
  if (!netlifyConfig.build.environment) {
33
49
  netlifyConfig.build.environment = {};
34
50
  }
35
51
  netlifyConfig.build.environment.QELOS_API_IP = apiUrl;
52
+ netlifyConfig.build.environment.QELOS_NETLIFY_PROXY_MODE = useFunctionProxy
53
+ ? 'function'
54
+ : 'cdn';
36
55
  if (bypassAdmin) {
37
56
  netlifyConfig.build.environment.QELOS_BYPASS_ADMIN_HEADER = 'true';
38
57
  }
39
58
 
40
- if (!netlifyConfig.functions) {
41
- netlifyConfig.functions = { directory: 'netlify/functions' };
59
+ if (!netlifyConfig.redirects) {
60
+ netlifyConfig.redirects = [];
42
61
  }
62
+ netlifyConfig.redirects = stripApiRedirects(netlifyConfig.redirects);
43
63
 
44
- // Copy the proxy into the site's functions dir so Netlify bundles it like any
45
- // checked-in function (utils.functions.add from node_modules is unreliable for .ts).
46
- const destDir = resolveFunctionsDirectory(netlifyConfig);
47
- const destFile = path.join(destDir, 'qelos-api-proxy.ts');
48
- const srcFile = path.join(__dirname, 'functions', 'qelos-api-proxy.ts');
49
- await fs.mkdir(destDir, { recursive: true });
50
- await fs.copyFile(srcFile, destFile);
64
+ if (useFunctionProxy) {
65
+ if (!netlifyConfig.functions) {
66
+ netlifyConfig.functions = { directory: 'netlify/functions' };
67
+ }
68
+ const destDir = resolveFunctionsDirectory(netlifyConfig);
69
+ const destFile = path.join(destDir, 'qelos-api-proxy.ts');
70
+ const srcFile = path.join(__dirname, 'functions', 'qelos-api-proxy.ts');
71
+ await fs.mkdir(destDir, { recursive: true });
72
+ await fs.copyFile(srcFile, destFile);
51
73
 
52
- // Add redirect: /api/* -> Netlify function (proxy)
53
- if (!netlifyConfig.redirects) {
54
- netlifyConfig.redirects = [];
74
+ netlifyConfig.redirects.unshift({
75
+ from: '/api/*',
76
+ to: '/.netlify/functions/qelos-api-proxy',
77
+ status: 200,
78
+ force: true,
79
+ });
80
+ } else {
81
+ const origin = upstreamOrigin(apiUrl);
82
+ const redirect = {
83
+ from: '/api/*',
84
+ to: `${origin}/api/:splat`,
85
+ status: 200,
86
+ force: true,
87
+ };
88
+ if (bypassAdmin) {
89
+ redirect.headers = { 'X-Bypass-Admin': 'true' };
90
+ }
91
+ netlifyConfig.redirects.unshift(redirect);
55
92
  }
56
- netlifyConfig.redirects.push({
57
- from: '/api/*',
58
- to: '/.netlify/functions/qelos-api-proxy',
59
- status: 200,
60
- force: true,
61
- });
62
93
  },
63
94
 
64
- /**
65
- * Best-effort: some Netlify builds omit logging or skip this hook; the supported fix
66
- * for Nuxt is `postbuild` → `qelos-netlify-patch-redirects` (see README).
67
- */
68
- async onPostBuild({ constants, netlifyConfig }) {
95
+ async onPostBuild({ constants, netlifyConfig, inputs }) {
69
96
  const publishDir = resolvePublishDir(constants, netlifyConfig);
70
- const changed = patchRedirects(publishDir);
97
+ const apiUrl =
98
+ inputs?.api_url ??
99
+ netlifyConfig?.build?.environment?.QELOS_API_IP ??
100
+ process.env.QELOS_API_IP ??
101
+ DEFAULT_API_URL;
102
+ const mode =
103
+ netlifyConfig?.build?.environment?.QELOS_NETLIFY_PROXY_MODE === 'function'
104
+ ? 'function'
105
+ : 'cdn';
106
+ const changed = patchRedirects(publishDir, apiUrl, mode);
71
107
  console.log(
72
- `[@qelos/plugin-netlify-api] onPostBuild: ${changed ? 'patched' : 'skipped'} ${path.join(publishDir, '_redirects')}`,
108
+ `[@qelos/plugin-netlify-api] onPostBuild: ${changed ? 'patched' : 'unchanged'} ${path.join(publishDir, '_redirects')} (${mode})`,
73
109
  );
74
110
  },
75
111
  };
package/manifest.yml CHANGED
@@ -8,3 +8,7 @@ inputs:
8
8
  required: false
9
9
  description: "If true, the proxy adds header x-bypass-admin: true to every request (so admins are scoped as regular users)."
10
10
  default: false
11
+ - name: use_function_proxy
12
+ required: false
13
+ description: "If true, use the bundled serverless function instead of Netlify CDN proxy to api_url (default is CDN rewrite to origin/api/:splat)."
14
+ default: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qelos/plugin-netlify-api",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "Netlify build plugin: proxy /api/* to Qelos API and inject config",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -2,24 +2,66 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * Prepend `/api/*` → qelos-api-proxy to publish/_redirects *before* Netlify deploy.
6
- * Netlify evaluates `_redirects` before netlify.toml; Nuxt `netlify-static` emits
7
- * `/* → /404.html` first — without this line, `/api/*` never reaches the function.
5
+ * Prepend a forced `/api/*` rewrite as the first line of publish/_redirects.
6
+ * Netlify evaluates this file before netlify.toml; Nuxt `netlify-static` adds
7
+ * `/* → /404.html` — that must come after `/api/*`.
8
8
  *
9
- * Run via npm lifecycle: "postbuild": "qelos-netlify-patch-redirects"
10
- * (postbuild runs automatically after `nuxt build` when Netlify runs `npm run build`).
9
+ * Default: CDN proxy to `${QELOS_API_IP}/api/:splat` (Netlify’s documented pattern).
10
+ * Set QELOS_NETLIFY_PROXY_MODE=function to use the serverless function instead.
11
11
  */
12
12
 
13
13
  const fs = require('node:fs');
14
14
  const path = require('node:path');
15
15
 
16
- const REDIRECT_LINE = '/api/* /.netlify/functions/qelos-api-proxy 200!\n';
16
+ const DEFAULT_API = 'http://159.203.152.168';
17
+
18
+ function upstreamOrigin(apiUrl) {
19
+ const s = String(apiUrl ?? '').trim().replace(/\/$/, '');
20
+ if (!s) {
21
+ return new URL(DEFAULT_API).origin;
22
+ }
23
+ if (/^https?:\/\//i.test(s)) {
24
+ return new URL(s).origin;
25
+ }
26
+ return `http://${s}`;
27
+ }
17
28
 
18
29
  /**
19
- * @param {string} publishDir absolute path to the directory Netlify deploys (e.g. …/dist)
30
+ * @param {string} [apiUrl]
31
+ * @param {'cdn'|'function'} [mode]
32
+ */
33
+ function buildRedirectLine(apiUrl, mode) {
34
+ const m = mode === 'function' ? 'function' : 'cdn';
35
+ if (m === 'function') {
36
+ return '/api/* /.netlify/functions/qelos-api-proxy 200!\n';
37
+ }
38
+ const origin = upstreamOrigin(apiUrl ?? process.env.QELOS_API_IP);
39
+ return `/api/* ${origin}/api/:splat 200!\n`;
40
+ }
41
+
42
+ /**
43
+ * Drop existing /api/* lines so we don’t stack stale rules when the target URL changes.
44
+ * @param {string} content
45
+ */
46
+ function stripApiRedirectLines(content) {
47
+ return content
48
+ .split('\n')
49
+ .filter((line) => {
50
+ const t = line.trim();
51
+ if (!t || t.startsWith('#')) return true;
52
+ if (t.startsWith('/api/*')) return false;
53
+ return true;
54
+ })
55
+ .join('\n');
56
+ }
57
+
58
+ /**
59
+ * @param {string} publishDir absolute path to deploy root (e.g. …/dist)
60
+ * @param {string} [apiUrl] same as QELOS_API_IP
61
+ * @param {'cdn'|'function'} [mode]
20
62
  * @returns {boolean} true if the file was written
21
63
  */
22
- function patchRedirects(publishDir) {
64
+ function patchRedirects(publishDir, apiUrl, mode) {
23
65
  if (!publishDir) return false;
24
66
  const redirectsPath = path.join(publishDir, '_redirects');
25
67
  let existing = '';
@@ -28,10 +70,13 @@ function patchRedirects(publishDir) {
28
70
  } catch (e) {
29
71
  if (e.code !== 'ENOENT') throw e;
30
72
  }
31
- if (existing.includes('qelos-api-proxy')) {
73
+ const line = buildRedirectLine(apiUrl, mode);
74
+ const rest = stripApiRedirectLines(existing);
75
+ const next = line + (rest ? rest + (rest.endsWith('\n') ? '' : '\n') : '');
76
+ if (next === existing) {
32
77
  return false;
33
78
  }
34
- fs.writeFileSync(redirectsPath, REDIRECT_LINE + existing, 'utf8');
79
+ fs.writeFileSync(redirectsPath, next, 'utf8');
35
80
  return true;
36
81
  }
37
82
 
@@ -42,15 +87,24 @@ function resolvePublishDirFromEnv() {
42
87
 
43
88
  if (require.main === module) {
44
89
  const abs = resolvePublishDirFromEnv();
45
- const changed = patchRedirects(abs);
90
+ const mode =
91
+ process.env.QELOS_NETLIFY_PROXY_MODE === 'function' ? 'function' : 'cdn';
92
+ const apiUrl = process.env.QELOS_API_IP;
93
+ const changed = patchRedirects(abs, apiUrl, mode);
46
94
  const target = path.join(abs, '_redirects');
95
+ const kind = mode === 'function' ? 'function' : 'CDN';
47
96
  if (changed) {
48
- console.log(`[qelos-netlify-patch-redirects] prepended /api/* rewrite → ${target}`);
49
- } else {
50
97
  console.log(
51
- `[qelos-netlify-patch-redirects] skipped (already has qelos-api-proxy) → ${target}`,
98
+ `[qelos-netlify-patch-redirects] prepended /api/* (${kind}) → ${target}`,
52
99
  );
100
+ } else {
101
+ console.log(`[qelos-netlify-patch-redirects] up to date → ${target}`);
53
102
  }
54
103
  }
55
104
 
56
- module.exports = { patchRedirects, REDIRECT_LINE };
105
+ module.exports = {
106
+ patchRedirects,
107
+ buildRedirectLine,
108
+ upstreamOrigin,
109
+ stripApiRedirectLines,
110
+ };