@lukoweb/apitogo 0.2.39 → 0.2.41

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.
@@ -41,6 +41,7 @@ export declare function createMcpProgressReporter(extra?: McpToolExtra): Progres
41
41
  export type ConsoleProgressReporter = ProgressReporter & {
42
42
  stop(): void;
43
43
  };
44
+ export declare function friendlyModuleNames(text: string): string;
44
45
  export declare function createConsoleProgressReporter(stream?: NodeJS.WritableStream): ConsoleProgressReporter;
45
46
  export interface PollableJobValue {
46
47
  status: string;
@@ -1,273 +1,276 @@
1
- ---
2
- title: Protect your API with the gateway
3
- description:
4
- Put the APItoGo gateway in front of an API you already host. What each setting means, where to
5
- find the login values (OIDC), and how to write the rules yourself.
6
- sidebar_icon: shield
7
- ---
8
-
9
- The **gateway** sits in front of an API you already host. It checks who is calling, limits how
10
- often, and passes only the calls you allow to your API. You do not move your API.
11
-
12
- ```
13
- callers ──► APItoGo gateway ──► your API (stays where it is)
14
- │ checks the login token
15
- │ limits calls
16
- │ allows only the calls in your OpenAPI file
17
- └ your real API address stays hidden
18
- ```
19
-
20
- You set it up in one file, `apitogo.config.json`, in a `gateway` section.
21
-
22
- ## The smallest setup
23
-
24
- ```json
25
- {
26
- "version": 2,
27
- "gateway": {
28
- "origin": { "mode": "proxy", "url": "https://api.example.com", "healthPath": "/health" },
29
- "specs": ["apis/openapi.yaml"],
30
- "auth": {
31
- "mode": "oidc",
32
- "metadataUrl": "https://login.example.com/.well-known/openid-configuration",
33
- "issuers": ["https://login.example.com/"],
34
- "audiences": ["my-api"]
35
- },
36
- "policies": {
37
- "access": { "jwt": true, "apiKey": false },
38
- "rateLimit": { "calls": 60, "periodSeconds": 60 },
39
- "operations": { "public": ["GET /health"] }
40
- }
41
- }
42
- }
43
- ```
44
-
45
- `apitogo add gateway` writes the first part for you. The rest you write yourself, and the sections
46
- below explain every value.
47
-
48
- ## 1. Your API (`origin` and `specs`)
49
-
50
- | Setting | What to put there | Where to get it |
51
- | ------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
52
- | `origin.mode` | `proxy`: your API stays where it is. | Always `proxy` for an API you host. |
53
- | `origin.url` | The address of your API. It must be `https://`. | You already know it. It is where your API answers today. |
54
- | `origin.healthPath` | A path the platform can call to see that your API is alive, for example `/health`. | A cheap call on your API that needs no login. |
55
- | `specs` | The OpenAPI file or files that describe your API. | Your API's `openapi.yaml`, or the file your framework generates. |
56
-
57
- The gateway only lets through the calls listed in your OpenAPI file. A call that is not in the file
58
- is refused, so a forgotten path never leaks.
59
-
60
- ## 2. Who may call (`auth`)
61
-
62
- With `"mode": "oidc"` the caller sends a **login token** (a JWT) with each call:
63
- `Authorization: Bearer <token>`. The token comes from your own login provider, for example Auth0,
64
- Microsoft Entra ID, Okta or Keycloak. The gateway checks that the token is genuine, is from your
65
- provider and is meant for your API.
66
-
67
- You must give **all three** of these values. If one is missing, the deploy is blocked, so a settings
68
- mistake can never leave your API open.
69
-
70
- | Setting | What it is | How to find it |
71
- | ------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
72
- | `metadataUrl` | The address where the gateway reads your provider's public keys, so it can check a token's signature. | Your issuer address plus `/.well-known/openid-configuration`. |
73
- | `issuers` | Who is allowed to issue tokens. It must match the `iss` value in your tokens **exactly**. | The `issuer` field inside the metadata document (see below). |
74
- | `audiences` | Who the token is meant for. It must match the `aud` value in your tokens. | The identifier you gave your API at your provider. |
75
-
76
- ### Find the values in four steps
77
-
78
- 1. **Find your provider's issuer address.** It is on the provider's dashboard or in its docs.
79
- Typical shapes are in the table below.
80
- 2. **Open the metadata address in a browser.** Add `/.well-known/openid-configuration` to the issuer
81
- address. You see a JSON document. If you see an error, the address is wrong.
82
- 3. **Copy the `issuer` value from that document** into `issuers`, character for character, including
83
- a trailing `/` if it has one.
84
- 4. **Use the identifier of your API as `audiences`.** You create this identifier at your provider
85
- when you register your API. It is often called _API identifier_, _audience_ or _application ID
86
- URI_.
87
-
88
- ### Typical values by provider
89
-
90
- These are typical shapes. Always confirm them in your provider's dashboard.
91
-
92
- | Provider | Issuer | Audience |
93
- | ------------------ | ----------------------------------------------------------------------------- | -------------------------------------------------------------- |
94
- | Auth0 | `https://YOUR-TENANT.auth0.com/` (with the trailing `/`) | The _API identifier_ of your API in Auth0 |
95
- | Microsoft Entra ID | `https://login.microsoftonline.com/TENANT-ID/v2.0` | Your API's _Application ID URI_, for example `api://CLIENT-ID` |
96
- | Okta | `https://YOUR-ORG.okta.com/oauth2/default` (or your own authorization server) | The audience set on that authorization server |
97
- | Keycloak | `https://HOST/realms/YOUR-REALM` | The client or audience you configured for your API |
98
-
99
- In every case the metadata address is the issuer plus `/.well-known/openid-configuration`.
100
-
101
- ### Check a real token
102
-
103
- Ask your provider for a token for your API, then read what is inside it. The values `iss` and `aud`
104
- must match your `issuers` and `audiences`:
105
-
106
- ```bash
107
- node -p "JSON.parse(Buffer.from(process.argv[1].split('.')[1],'base64url').toString())" "PASTE-THE-TOKEN-HERE"
108
- ```
109
-
110
- A token is a login credential. Read it on your own computer, as above, and never share it.
111
-
112
- ### The most common mistakes
113
-
114
- | What you see | Usual cause |
115
- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
116
- | Every call answers `401` although the token looks right | `issuers` or `audiences` does not match `iss` or `aud` in the token. A trailing `/` is enough to break it. |
117
- | `apitogo check` or `deploy` says the auth settings are incomplete | One of `metadataUrl`, `issuers` or `audiences` is empty. All three are needed. |
118
- | The metadata address shows an error in the browser | The issuer address is wrong, or your provider uses a different path. Check its docs. |
119
-
120
- > **Not available yet:** `"mode": "identity"` (APItoGo's own sign-up and login for your end users)
121
- > is a planned service. For now, use `oidc` with your own provider.
122
-
123
- ## 3. The rules (`policies`)
124
-
125
- Rules go in `gateway.policies`. Write them by hand: they are a few lines each.
126
-
127
- ### `access`: who gets in
128
-
129
- ```json
130
- "access": { "jwt": true, "apiKey": false }
131
- ```
132
-
133
- | Setting | Effect |
134
- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
135
- | `jwt` | `true`: a valid login token is required. |
136
- | `apiKey` | `true`: a key in the `Ocp-Apim-Subscription-Key` header is required. Keys need the plans service, which is not available yet. |
137
-
138
- Set `access` yourself and do not rely on the default.
139
-
140
- ### `operations`: open a call to everyone, or close it
141
-
142
- ```json
143
- "operations": { "public": ["GET /health"], "deny": ["DELETE /orders"] }
144
- ```
145
-
146
- Each entry is `METHOD /path`, exactly as in your OpenAPI file.
147
-
148
- | List | Effect |
149
- | -------- | ------------------------------------------------------------------------------------ |
150
- | `public` | Anyone can call it, with no token. Use it for a health check or a public price list. |
151
- | `deny` | The gateway always refuses it, even with a valid token. |
152
-
153
- Every other call needs a login.
154
-
155
- ### `rateLimit` and `quota`: how often
156
-
157
- ```json
158
- "rateLimit": { "calls": 60, "periodSeconds": 60 },
159
- "quota": { "calls": 10000, "periodDays": 30 }
160
- ```
161
-
162
- | Setting | Effect |
163
- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
164
- | `rateLimit` | At most `calls` calls in every `periodSeconds` seconds. Extra calls get `429`. Use it to protect your API from bursts. |
165
- | `quota` | At most `calls` calls in every `periodDays` days. Use it for a monthly allowance. |
166
-
167
- ### `cors`: which websites may call from a browser
168
-
169
- ```json
170
- "cors": ["https://app.example.com"]
171
- ```
172
-
173
- A browser only lets a web page call your API if the API allows that page's site. List each site as
174
- `https://host` only: no path and no trailing `/`. An entry like `https://app.example.com/` is
175
- refused by `apitogo check`.
176
-
177
- ### Keep your API from being called directly
178
-
179
- Your API has a public address, so someone could skip the gateway. Make your API accept calls only
180
- with a secret that only the gateway sends:
181
-
182
- ```json
183
- "origin": {
184
- "mode": "proxy",
185
- "url": "https://api.example.com",
186
- "auth": { "type": "header", "name": "X-Gateway-Secret", "value": "secret:GATEWAY_SECRET" }
187
- }
188
- ```
189
-
190
- ```bash
191
- npx apitogo secrets set GATEWAY_SECRET
192
- ```
193
-
194
- The value is a reference, never the secret itself. The secret is stored encrypted and never shown
195
- again. Then make your API refuse any call without that header.
196
-
197
- ## Run your API on the platform (`managed` origin)
198
-
199
- If you have no place to host your API, the platform can run your code. Set the origin mode to
200
- `managed` and point `source` at the folder with your code:
201
-
202
- ```json
203
- "gateway": {
204
- "origin": {
205
- "mode": "managed",
206
- "source": "backend",
207
- "runtime": "nodejs",
208
- "runtimeVersion": "NODE_22_LTS",
209
- "healthPath": "/health",
210
- "exclude": ["tests/"]
211
- },
212
- "specs": ["openapi.json"]
213
- }
214
- ```
215
-
216
- | Field | Meaning |
217
- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
218
- | `source` | Folder with your code, relative to the config. Files in its `.gitignore` and in `exclude` are not uploaded. |
219
- | `runtime` | `nodejs`, `python` or `dotnet`. |
220
- | `runtimeVersion` | For example `NODE_22_LTS`. The platform lists the allowed values. |
221
- | `healthPath` | Path the platform probes to check that your API is up. |
222
- | `exclude` | More paths to leave out, in `.gitignore` syntax. |
223
-
224
- `npx apitogo project create` prepares the hosting for your code. `npx apitogo deploy` then uploads
225
- the OpenAPI file and the config, uploads your code and waits until it runs, and the gateway starts
226
- sending calls to it. A deploy with the same code uploads nothing. A .NET project is published for
227
- you (this needs the .NET SDK on your machine), or you can point `source` at a folder that is already
228
- published.
229
-
230
- Your code can also be reached by its own address, so check that a call came through the gateway. The
231
- platform gives your code two settings for this: `APITOGO_ORIGIN_SECRET_HEADER` (the header name) and
232
- `APITOGO_ORIGIN_SECRET` (the value). The gateway sends that header with every call. Refuse any
233
- request without it, as in
234
- [Keep your API from being called directly](#keep-your-api-from-being-called-directly):
235
-
236
- ```js
237
- const header = process.env.APITOGO_ORIGIN_SECRET_HEADER.toLowerCase();
238
- if (req.headers[header] !== process.env.APITOGO_ORIGIN_SECRET) {
239
- res.writeHead(403).end();
240
- return;
241
- }
242
- ```
243
-
244
- ## 4. Try it and check it
245
-
246
- Run these before you deploy:
247
-
248
- ```bash
249
- npx apitogo check
250
- npx apitogo plan
251
- ```
252
-
253
- `check` finds mistakes in the file, offline. `plan` shows what a deploy would change and changes
254
- nothing. Then deploy and test the address that `apitogo status` shows:
255
-
256
- ```bash
257
- npx apitogo deploy --only gateway
258
- curl -i https://YOUR-GATEWAY-ADDRESS/health
259
- curl -i https://YOUR-GATEWAY-ADDRESS/orders
260
- ```
261
-
262
- | Answer | Meaning |
263
- | ------------------------------- | ------------------------------------------------------------- |
264
- | `200` on a public call | The call is open and your API answered. |
265
- | `401` on a call without a token | The gateway is protecting that call. |
266
- | `404` | The path is not in your OpenAPI file. |
267
- | `429` | The rate limit was reached. |
268
- | `502` or `504` | Your API did not answer. Check `origin.url` and `healthPath`. |
269
-
270
- A change is live a few seconds after `deploy` reports it ready: the gateway keeps answering with the
271
- previous rules for up to about ten seconds. If a test right after a deploy shows the old behavior,
272
- wait a moment and try again. The same delay applies when you remove a gateway: its keys stop working
273
- within seconds, not at the same instant.
1
+ ---
2
+ title: Protect your API with the gateway
3
+ description:
4
+ Put the APItoGo gateway in front of an API you already host. What each setting means, where to
5
+ find the login values (OIDC), and how to write the rules yourself.
6
+ sidebar_icon: shield
7
+ ---
8
+
9
+ The **gateway** sits in front of an API you already host. It checks who is calling, limits how
10
+ often, and passes only the calls you allow to your API. You do not move your API.
11
+
12
+ ```
13
+ callers ──► APItoGo gateway ──► your API (stays where it is)
14
+ │ checks the login token
15
+ │ limits calls
16
+ │ allows only the calls in your OpenAPI file
17
+ └ your real API address stays hidden
18
+ ```
19
+
20
+ You set it up in one file, `apitogo.config.json`, in a `gateway` section.
21
+
22
+ ## The smallest setup
23
+
24
+ ```json
25
+ {
26
+ "version": 2,
27
+ "gateway": {
28
+ "origin": { "mode": "proxy", "url": "https://api.example.com", "healthPath": "/health" },
29
+ "specs": ["apis/openapi.yaml"],
30
+ "auth": {
31
+ "mode": "oidc",
32
+ "metadataUrl": "https://login.example.com/.well-known/openid-configuration",
33
+ "issuers": ["https://login.example.com/"],
34
+ "audiences": ["my-api"]
35
+ },
36
+ "policies": {
37
+ "access": { "jwt": true, "apiKey": false },
38
+ "rateLimit": { "calls": 60, "periodSeconds": 60 },
39
+ "operations": { "public": ["GET /health"] }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ `apitogo add gateway` writes the first part for you. The rest you write yourself, and the sections
46
+ below explain every value.
47
+
48
+ ## 1. Your API (`origin` and `specs`)
49
+
50
+ | Setting | What to put there | Where to get it |
51
+ | ------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
52
+ | `origin.mode` | `proxy`: your API stays where it is. | Always `proxy` for an API you host. |
53
+ | `origin.url` | The address of your API. It must be `https://`. | You already know it. It is where your API answers today. |
54
+ | `origin.healthPath` | A path the platform can call to see that your API is alive, for example `/health`. | A cheap call on your API that needs no login. |
55
+ | `specs` | The OpenAPI file or files that describe your API. | Your API's `openapi.yaml`, or the file your framework generates. |
56
+
57
+ The gateway only lets through the calls listed in your OpenAPI file. A call that is not in the file
58
+ is refused, so a forgotten path never leaks.
59
+
60
+ ## 2. Who may call (`auth`)
61
+
62
+ With `"mode": "oidc"` the caller sends a **login token** (a JWT) with each call:
63
+ `Authorization: Bearer <token>`. The token comes from your own login provider, for example Auth0,
64
+ Microsoft Entra ID, Okta or Keycloak. The gateway checks that the token is genuine, is from your
65
+ provider and is meant for your API.
66
+
67
+ You must give **all three** of these values. If one is missing, the deploy is blocked, so a settings
68
+ mistake can never leave your API open.
69
+
70
+ | Setting | What it is | How to find it |
71
+ | ------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
72
+ | `metadataUrl` | The address where the gateway reads your provider's public keys, so it can check a token's signature. | Your issuer address plus `/.well-known/openid-configuration`. |
73
+ | `issuers` | Who is allowed to issue tokens. It must match the `iss` value in your tokens **exactly**. | The `issuer` field inside the metadata document (see below). |
74
+ | `audiences` | Who the token is meant for. It must match the `aud` value in your tokens. | The identifier you gave your API at your provider. |
75
+
76
+ ### Find the values in four steps
77
+
78
+ 1. **Find your provider's issuer address.** It is on the provider's dashboard or in its docs.
79
+ Typical shapes are in the table below.
80
+ 2. **Open the metadata address in a browser.** Add `/.well-known/openid-configuration` to the issuer
81
+ address. You see a JSON document. If you see an error, the address is wrong.
82
+ 3. **Copy the `issuer` value from that document** into `issuers`, character for character, including
83
+ a trailing `/` if it has one.
84
+ 4. **Use the identifier of your API as `audiences`.** You create this identifier at your provider
85
+ when you register your API. It is often called _API identifier_, _audience_ or _application ID
86
+ URI_.
87
+
88
+ ### Typical values by provider
89
+
90
+ These are typical shapes. Always confirm them in your provider's dashboard.
91
+
92
+ | Provider | Issuer | Audience |
93
+ | ------------------ | ----------------------------------------------------------------------------- | -------------------------------------------------------------- |
94
+ | Auth0 | `https://YOUR-TENANT.auth0.com/` (with the trailing `/`) | The _API identifier_ of your API in Auth0 |
95
+ | Microsoft Entra ID | `https://login.microsoftonline.com/TENANT-ID/v2.0` | Your API's _Application ID URI_, for example `api://CLIENT-ID` |
96
+ | Okta | `https://YOUR-ORG.okta.com/oauth2/default` (or your own authorization server) | The audience set on that authorization server |
97
+ | Keycloak | `https://HOST/realms/YOUR-REALM` | The client or audience you configured for your API |
98
+
99
+ In every case the metadata address is the issuer plus `/.well-known/openid-configuration`.
100
+
101
+ ### Check a real token
102
+
103
+ Ask your provider for a token for your API, then read what is inside it. The values `iss` and `aud`
104
+ must match your `issuers` and `audiences`:
105
+
106
+ ```bash
107
+ node -p "JSON.parse(Buffer.from(process.argv[1].split('.')[1],'base64url').toString())" "PASTE-THE-TOKEN-HERE"
108
+ ```
109
+
110
+ A token is a login credential. Read it on your own computer, as above, and never share it.
111
+
112
+ ### The most common mistakes
113
+
114
+ | What you see | Usual cause |
115
+ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
116
+ | Every call answers `401` although the token looks right | `issuers` or `audiences` does not match `iss` or `aud` in the token. A trailing `/` is enough to break it. |
117
+ | `apitogo check` or `deploy` says the auth settings are incomplete | One of `metadataUrl`, `issuers` or `audiences` is empty. All three are needed. |
118
+ | The metadata address shows an error in the browser | The issuer address is wrong, or your provider uses a different path. Check its docs. |
119
+
120
+ > **Not available yet:** `"mode": "identity"` (APItoGo's own sign-up and login for your end users)
121
+ > is a planned service. For now, use `oidc` with your own provider.
122
+
123
+ ## 3. The rules (`policies`)
124
+
125
+ Rules go in `gateway.policies`. Write them by hand: they are a few lines each.
126
+
127
+ ### `access`: who gets in
128
+
129
+ ```json
130
+ "access": { "jwt": true, "apiKey": false }
131
+ ```
132
+
133
+ | Setting | Effect |
134
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
135
+ | `jwt` | `true`: a valid login token is required. |
136
+ | `apiKey` | `true`: a key in the `Ocp-Apim-Subscription-Key` header is required. Keys need the plans service, which is not available yet. |
137
+
138
+ Set `access` yourself and do not rely on the default.
139
+
140
+ ### `operations`: open a call to everyone, or close it
141
+
142
+ ```json
143
+ "operations": { "public": ["GET /health"], "deny": ["DELETE /orders"] }
144
+ ```
145
+
146
+ Each entry is `METHOD /path`, exactly as in your OpenAPI file.
147
+
148
+ | List | Effect |
149
+ | -------- | ------------------------------------------------------------------------------------ |
150
+ | `public` | Anyone can call it, with no token. Use it for a health check or a public price list. |
151
+ | `deny` | The gateway always refuses it, even with a valid token. |
152
+
153
+ Every other call needs a login.
154
+
155
+ ### `rateLimit` and `quota`: how often
156
+
157
+ ```json
158
+ "rateLimit": { "calls": 60, "periodSeconds": 60 },
159
+ "quota": { "calls": 10000, "periodDays": 30 }
160
+ ```
161
+
162
+ | Setting | Effect |
163
+ | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
164
+ | `rateLimit` | At most `calls` calls in every `periodSeconds` seconds. Extra calls get `429`. Use it to protect your API from bursts. |
165
+ | `quota` | At most `calls` calls in every `periodDays` days. Use it for a monthly allowance. |
166
+
167
+ ### `cors`: which websites may call from a browser
168
+
169
+ ```json
170
+ "cors": ["https://app.example.com"]
171
+ ```
172
+
173
+ A browser only lets a web page call your API if the API allows that page's site. List each site as
174
+ `https://host` only: no path and no trailing `/`. An entry like `https://app.example.com/` is
175
+ refused by `apitogo check`.
176
+
177
+ ### Keep your API from being called directly
178
+
179
+ Your API has a public address, so someone could skip the gateway. Make your API accept calls only
180
+ with a secret that only the gateway sends:
181
+
182
+ ```json
183
+ "origin": {
184
+ "mode": "proxy",
185
+ "url": "https://api.example.com",
186
+ "auth": { "type": "header", "name": "X-Gateway-Secret", "value": "secret:GATEWAY_SECRET" }
187
+ }
188
+ ```
189
+
190
+ ```bash
191
+ npx apitogo secrets set GATEWAY_SECRET
192
+ ```
193
+
194
+ The value is a reference, never the secret itself. The secret is stored encrypted and never shown
195
+ again. Then make your API refuse any call without that header.
196
+
197
+ ## Run your API on the platform (`managed` origin)
198
+
199
+ If you have no place to host your API, the platform can run your code. Set the origin mode to
200
+ `managed` and point `source` at the folder with your code:
201
+
202
+ ```json
203
+ "gateway": {
204
+ "origin": {
205
+ "mode": "managed",
206
+ "source": "backend",
207
+ "runtime": "nodejs",
208
+ "runtimeVersion": "NODE_22_LTS",
209
+ "healthPath": "/health",
210
+ "exclude": ["tests/"]
211
+ },
212
+ "specs": ["openapi.json"]
213
+ }
214
+ ```
215
+
216
+ | Field | Meaning |
217
+ | ---------------- | ----------------------------------------------------------------------------------------------------------- |
218
+ | `source` | Folder with your code, relative to the config. Files in its `.gitignore` and in `exclude` are not uploaded. |
219
+ | `runtime` | `nodejs`, `python` or `dotnet`. |
220
+ | `runtimeVersion` | For example `NODE_22_LTS`. The platform lists the allowed values. |
221
+ | `healthPath` | Path the platform probes to check that your API is up. |
222
+ | `exclude` | More paths to leave out, in `.gitignore` syntax. |
223
+
224
+ `npx apitogo add gateway --origin managed` makes the `source` folder when it does not exist and
225
+ tells you it is empty; put your code there. `npx apitogo check` warns while the folder has no code.
226
+
227
+ `npx apitogo project create` prepares the hosting for your code. `npx apitogo deploy` then uploads
228
+ the OpenAPI file and the config, uploads your code and waits until it runs, and the gateway starts
229
+ sending calls to it. A deploy with the same code uploads nothing. A .NET project is published for
230
+ you (this needs the .NET SDK on your machine), or you can point `source` at a folder that is already
231
+ published.
232
+
233
+ Your code can also be reached by its own address, so check that a call came through the gateway. The
234
+ platform gives your code two settings for this: `APITOGO_ORIGIN_SECRET_HEADER` (the header name) and
235
+ `APITOGO_ORIGIN_SECRET` (the value). The gateway sends that header with every call. Refuse any
236
+ request without it, as in
237
+ [Keep your API from being called directly](#keep-your-api-from-being-called-directly):
238
+
239
+ ```js
240
+ const header = process.env.APITOGO_ORIGIN_SECRET_HEADER.toLowerCase();
241
+ if (req.headers[header] !== process.env.APITOGO_ORIGIN_SECRET) {
242
+ res.writeHead(403).end();
243
+ return;
244
+ }
245
+ ```
246
+
247
+ ## 4. Try it and check it
248
+
249
+ Run these before you deploy:
250
+
251
+ ```bash
252
+ npx apitogo check
253
+ npx apitogo plan
254
+ ```
255
+
256
+ `check` finds mistakes in the file, offline. `plan` shows what a deploy would change and changes
257
+ nothing. Then deploy and test the address that `apitogo status` shows:
258
+
259
+ ```bash
260
+ npx apitogo deploy --only gateway
261
+ curl -i https://YOUR-GATEWAY-ADDRESS/health
262
+ curl -i https://YOUR-GATEWAY-ADDRESS/orders
263
+ ```
264
+
265
+ | Answer | Meaning |
266
+ | ------------------------------- | ------------------------------------------------------------- |
267
+ | `200` on a public call | The call is open and your API answered. |
268
+ | `401` on a call without a token | The gateway is protecting that call. |
269
+ | `404` | The path is not in your OpenAPI file. |
270
+ | `429` | The rate limit was reached. |
271
+ | `502` or `504` | Your API did not answer. Check `origin.url` and `healthPath`. |
272
+
273
+ A change is live a few seconds after `deploy` reports it ready: the gateway keeps answering with the
274
+ previous rules for up to about ten seconds. If a test right after a deploy shows the old behavior,
275
+ wait a moment and try again. The same delay applies when you remove a gateway: its keys stop working
276
+ within seconds, not at the same instant.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -32,6 +32,8 @@ export type ResolvedPortal = {
32
32
  };
33
33
  console: { enabled: boolean };
34
34
  theme: string;
35
+ /** Absolute path of the custom home page component, when the portal has one. */
36
+ landing?: string;
35
37
  requireAuth: boolean | { exclude: string[] };
36
38
  search: "pagefind" | "inkeep" | "none";
37
39
  };
@@ -141,6 +143,9 @@ export function resolveConfigV2(
141
143
  theme: portal.theme
142
144
  ? path.resolve(configDir, portal.theme)
143
145
  : path.join(dir, "theme.json"),
146
+ ...(portal.landing
147
+ ? { landing: path.resolve(configDir, portal.landing) }
148
+ : {}),
144
149
  requireAuth: portal.requireAuth ?? false,
145
150
  search: provider === "external" ? "none" : (portal.search ?? "pagefind"),
146
151
  };
@@ -1,4 +1,4 @@
1
- import { readFile, stat } from "node:fs/promises";
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { glob } from "glob";
4
4
  import { parse as parseYaml } from "yaml";
@@ -41,6 +41,12 @@ async function isDirectory(target: string): Promise<boolean> {
41
41
  return (await stat(target).catch(() => undefined))?.isDirectory() === true;
42
42
  }
43
43
 
44
+ /** A folder with nothing in it but the `.gitkeep` that keeps it in git. */
45
+ async function isEmptyFolder(target: string): Promise<boolean> {
46
+ const entries = await readdir(target).catch(() => [] as string[]);
47
+ return entries.every((name) => name === ".gitkeep");
48
+ }
49
+
44
50
  /** Every `secret:NAME` reference in the config, with the path it was found at. */
45
51
  export function collectSecretRefs(
46
52
  config: unknown,
@@ -376,6 +382,13 @@ async function gatewayRules(resolved: ResolvedConfigV2, out: Collector) {
376
382
  message: `gateway.origin.source does not exist: ${inside === "" ? "." : inside}.`,
377
383
  hint: "Point it at the folder that holds your backend code.",
378
384
  });
385
+ } else if (await isEmptyFolder(origin.source)) {
386
+ out.warn({
387
+ code: "GATEWAY_BACKEND_EMPTY",
388
+ path: "gateway.origin.source",
389
+ message: `gateway.origin.source (${inside}) is empty: there is no code to run yet.`,
390
+ hint: "Put your backend code in that folder before you deploy.",
391
+ });
379
392
  }
380
393
  }
381
394
  if (origin.url !== undefined) {
@@ -45,6 +45,8 @@ export const PortalSchema = z.strictObject({
45
45
  .optional(),
46
46
  console: z.strictObject({ enabled }).optional(),
47
47
  theme: path.optional(),
48
+ /** A custom home page: a React component file that the portal serves at `/`. */
49
+ landing: path.optional(),
48
50
  requireAuth: z
49
51
  .union([z.boolean(), z.strictObject({ exclude: pathList })], {
50
52
  error: 'must be true, false or { "exclude": ["/route", ...] }',