@praesidias/api-gateway 0.0.0-stage → 0.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.
@@ -0,0 +1,248 @@
1
+ import { Condition, MappingFile, Praesidias } from '@praesidias/sdk';
2
+ import { Server, IncomingMessage, ServerResponse } from 'node:http';
3
+ import { Server as Server$1 } from 'node:https';
4
+
5
+ /** How the upstream's credential is put on the forwarded request. */
6
+ type CredentialPlacement = {
7
+ /** The header to set. Default "authorization". */
8
+ header?: string;
9
+ /** Put before the value, with a space: "Bearer", "Basic", or nothing. */
10
+ scheme?: string;
11
+ };
12
+ /**
13
+ * Where the upstream's credential comes from. The secret itself is never written in the config
14
+ * file: only where to find it.
15
+ *
16
+ * - `env`: an environment variable of the gateway's process.
17
+ * - `file`: a file, read again on every request. Vault Agent, the Kubernetes Secrets Store CSI
18
+ * driver (AWS, Azure, Google Cloud, Vault) and Docker secrets all deliver secrets this way.
19
+ * - `vault`: HashiCorp Vault's key/value store, read over https (see VaultCredentialSource).
20
+ */
21
+ type CredentialRule = CredentialPlacement & ({
22
+ from: 'env'; /** The environment variable. */
23
+ name: string;
24
+ } | {
25
+ from: 'file'; /** An absolute path. */
26
+ path: string;
27
+ } | {
28
+ from: 'vault';
29
+ /** The secret's path as Vault's API names it, for example "secret/data/payments/bank". */
30
+ path: string;
31
+ /** The field within the secret, for example "api_key". */
32
+ field: string;
33
+ });
34
+ /** A credential rule as checked: which source, and what to ask it for. */
35
+ type CompiledCredential = CredentialPlacement & {
36
+ from: 'env' | 'file' | 'vault';
37
+ /** env: the variable. file: the path. vault: "<path>#<field>". */
38
+ name: string;
39
+ };
40
+ type RouteRule = {
41
+ method: string;
42
+ /** A path under the upstream, with `:name` for a segment that varies. */
43
+ path: string;
44
+ action?: string;
45
+ resource?: {
46
+ type?: string;
47
+ idFrom?: string | string[];
48
+ };
49
+ /** `arg` is `params.x`, `query.x` or `body.x`. The first rule that matches replaces the action. */
50
+ when?: Array<{
51
+ if: Condition | Condition[];
52
+ action: string;
53
+ resource?: RouteRule['resource'];
54
+ }>;
55
+ /** Values recorded with the decision, by the same paths. Nothing else from the request is. */
56
+ context?: string[];
57
+ system?: string;
58
+ /** Forwarded without asking Praesidias. For reads you have decided need no record. */
59
+ ungoverned?: boolean;
60
+ };
61
+ type UpstreamRule = {
62
+ baseUrl: string;
63
+ credential?: CredentialRule;
64
+ routes: RouteRule[];
65
+ /**
66
+ * Drop-in: the API's own hostnames. When the platform points one at this gateway (DNS, or the
67
+ * address the agent is given), the agent keeps calling the URL and paths it always did, with
68
+ * no "/<upstream>" in front. A request is matched to its upstream by its Host header.
69
+ */
70
+ hosts?: string[];
71
+ /**
72
+ * Drop-in: the agent's Praesidias key arrives in the header the agent already sends to this
73
+ * API, in place of the API's own credential. The gateway takes it out, asks Praesidias, and
74
+ * puts the real credential on instead. Only a value shaped like a Praesidias key is accepted.
75
+ */
76
+ agentKey?: {
77
+ header: string;
78
+ scheme?: string;
79
+ };
80
+ };
81
+ type GatewayConfig = {
82
+ /** The system these APIs are, as your areas name it. */
83
+ system: string;
84
+ listen?: {
85
+ host?: string;
86
+ port?: number;
87
+ /** Required to listen anywhere but this machine. */ allowRemote?: boolean;
88
+ /**
89
+ * Answer over https with this certificate and key (files, PEM). For drop-in, the certificate
90
+ * is for the API's own hostnames, issued by the organization's certificate authority.
91
+ */
92
+ tls?: {
93
+ certFile: string;
94
+ keyFile: string;
95
+ };
96
+ };
97
+ /** By the name that is the first segment of the address the agent calls. */
98
+ upstreams?: Record<string, UpstreamRule>;
99
+ /**
100
+ * Also answer `POST /ask`: one request that asks Praesidias about an action and returns a
101
+ * plain yes or no. For no-code automations (n8n, Zapier, Make, Power Automate), where one
102
+ * HTTPS step is all there is. Off unless set.
103
+ */
104
+ ask?: boolean;
105
+ };
106
+ declare class ConfigError extends Error {
107
+ }
108
+ type CompiledRoute = RouteRule & {
109
+ method: string;
110
+ match: RegExp;
111
+ params: string[];
112
+ id: string;
113
+ };
114
+ type CompiledUpstream = {
115
+ name: string;
116
+ base: URL;
117
+ credential: CompiledCredential | null;
118
+ routes: CompiledRoute[];
119
+ mapping: MappingFile;
120
+ hosts: string[];
121
+ agentKey: {
122
+ header: string;
123
+ scheme?: string;
124
+ } | null;
125
+ };
126
+ type Compiled = {
127
+ system: string;
128
+ host: string;
129
+ port: number;
130
+ ask: boolean;
131
+ upstreams: Map<string, CompiledUpstream>;
132
+ /** Drop-in: an upstream by one of its own hostnames. */
133
+ hosts: Map<string, CompiledUpstream>;
134
+ tls: {
135
+ certFile: string;
136
+ keyFile: string;
137
+ } | null;
138
+ };
139
+ /** Checks the whole file at start, so a mistake stops the gateway instead of surfacing mid-request. */
140
+ declare function compileConfig(raw: unknown): Compiled;
141
+ declare function loadConfig(path: string): Compiled;
142
+
143
+ /**
144
+ * Where the gateway gets an upstream API's credential at the moment it forwards a request.
145
+ * The agent never has it: the agent only ever holds its Praesidias key.
146
+ *
147
+ * `get` is asked each time, so a source may rotate what it returns. Return undefined when
148
+ * there is nothing under that name; the request is then refused, never sent without one.
149
+ */
150
+ type CredentialSource = {
151
+ get(name: string): Promise<string | undefined> | string | undefined;
152
+ };
153
+ /** Reads credentials from this process's environment. */
154
+ declare class EnvCredentialSource implements CredentialSource {
155
+ private readonly env;
156
+ constructor(env?: NodeJS.ProcessEnv);
157
+ get(name: string): string | undefined;
158
+ }
159
+ /** Why a credential could not be had. Names only: never a secret, a token or a file's contents. */
160
+ type Note = (line: string) => void;
161
+ /**
162
+ * Reads a credential from a file, read again on every request so a rotated secret is picked up
163
+ * without a restart. This is how most secret stores hand a secret to a process: Vault Agent,
164
+ * the Kubernetes Secrets Store CSI driver (for AWS, Azure, Google Cloud or Vault), and Docker
165
+ * secrets all write it to a file. The name asked for is the file's path.
166
+ */
167
+ declare class FileCredentialSource implements CredentialSource {
168
+ private readonly note;
169
+ constructor(note?: Note);
170
+ get(path: string): Promise<string | undefined>;
171
+ }
172
+ type VaultOptions = {
173
+ env?: NodeJS.ProcessEnv;
174
+ /** How long a secret read from Vault is reused, in milliseconds. Default 60 s; 0 reads every time. */
175
+ cacheMs?: number;
176
+ /** How long Vault may take, in milliseconds. Default 5 s. */
177
+ timeoutMs?: number;
178
+ note?: Note;
179
+ /** For tests. */
180
+ fetch?: typeof fetch;
181
+ };
182
+ /**
183
+ * Reads a credential from HashiCorp Vault's key/value store (version 2, or version 1), over
184
+ * https. The name asked for is `<path>#<field>`, for example `secret/data/payments/bank#api_key`.
185
+ *
186
+ * Vault is found from the standard variables: VAULT_ADDR, VAULT_TOKEN or VAULT_TOKEN_FILE (the
187
+ * file Vault Agent keeps a fresh token in, read again each time), and VAULT_NAMESPACE. A private
188
+ * certificate authority for Vault is trusted with NODE_EXTRA_CA_CERTS, as for any Node process.
189
+ *
190
+ * A secret is reused for a short while so Vault is not asked on every request. When it is time
191
+ * to ask again and Vault cannot answer, nothing is returned and the request is refused: an
192
+ * expired secret is never used.
193
+ */
194
+ declare class VaultCredentialSource implements CredentialSource {
195
+ private readonly env;
196
+ private readonly cacheMs;
197
+ private readonly timeoutMs;
198
+ private readonly note;
199
+ private readonly send;
200
+ private readonly cache;
201
+ private readonly reading;
202
+ constructor(options?: VaultOptions);
203
+ get(name: string): Promise<string | undefined>;
204
+ private token;
205
+ private read;
206
+ }
207
+
208
+ type ApiGatewayOptions = {
209
+ config: Compiled;
210
+ /** The Praesidias address. Must be https, except on this machine. */
211
+ praesidiasUrl: string;
212
+ /** Where "env" credentials come from. Default: this process's environment. */
213
+ credentials?: CredentialSource;
214
+ /** Settings for "vault" credentials (Vault itself is found from VAULT_ADDR and friends). */
215
+ vault?: Omit<VaultOptions, 'note'>;
216
+ /** One line per request: method, route, action, outcome. Never a key, a credential or a body. */
217
+ log?: (line: string) => void;
218
+ /** The header the agent puts its Praesidias key in. Default "x-praesidias-key". */
219
+ keyHeader?: string;
220
+ /** Largest request body accepted, in bytes. Default 1 MB. */
221
+ maxBodyBytes?: number;
222
+ /** How long an upstream may take, in milliseconds. Default 30 s. */
223
+ upstreamTimeoutMs?: number;
224
+ /** For tests. */
225
+ fetch?: typeof fetch;
226
+ sdkOptions?: ConstructorParameters<typeof Praesidias>[0];
227
+ };
228
+ /**
229
+ * A gateway in front of the outside APIs an agent calls.
230
+ *
231
+ * The agent calls `http://<gateway>/<upstream>/<path>` with its Praesidias key. The gateway
232
+ * finds the configured route, asks Praesidias whether this agent may do that action on that
233
+ * resource, and only then forwards the request, adding the API's own credential. The agent
234
+ * never holds that credential, so it cannot reach the API any other way.
235
+ *
236
+ * It fails closed at every step: no route, no key, no answer from Praesidias, or no credential
237
+ * all mean the request is not sent.
238
+ */
239
+ declare function createApiGateway(options: ApiGatewayOptions): Server;
240
+ /**
241
+ * The gateway as a server: over https when the config has "listen.tls" (the certificate and key
242
+ * are read here, once), otherwise plain http as `createApiGateway`.
243
+ */
244
+ declare function createApiGatewayServer(options: ApiGatewayOptions): Server | Server$1;
245
+ /** The request handler, for any http or https server. Everything the gateway does is here. */
246
+ declare function createApiGatewayHandler(options: ApiGatewayOptions): (req: IncomingMessage, res: ServerResponse) => void;
247
+
248
+ export { type ApiGatewayOptions, type Compiled, type CompiledCredential, ConfigError, type CredentialRule, type CredentialSource, EnvCredentialSource, FileCredentialSource, type GatewayConfig, type RouteRule, type UpstreamRule, VaultCredentialSource, type VaultOptions, compileConfig, createApiGateway, createApiGatewayHandler, createApiGatewayServer, loadConfig };
@@ -0,0 +1,248 @@
1
+ import { Condition, MappingFile, Praesidias } from '@praesidias/sdk';
2
+ import { Server, IncomingMessage, ServerResponse } from 'node:http';
3
+ import { Server as Server$1 } from 'node:https';
4
+
5
+ /** How the upstream's credential is put on the forwarded request. */
6
+ type CredentialPlacement = {
7
+ /** The header to set. Default "authorization". */
8
+ header?: string;
9
+ /** Put before the value, with a space: "Bearer", "Basic", or nothing. */
10
+ scheme?: string;
11
+ };
12
+ /**
13
+ * Where the upstream's credential comes from. The secret itself is never written in the config
14
+ * file: only where to find it.
15
+ *
16
+ * - `env`: an environment variable of the gateway's process.
17
+ * - `file`: a file, read again on every request. Vault Agent, the Kubernetes Secrets Store CSI
18
+ * driver (AWS, Azure, Google Cloud, Vault) and Docker secrets all deliver secrets this way.
19
+ * - `vault`: HashiCorp Vault's key/value store, read over https (see VaultCredentialSource).
20
+ */
21
+ type CredentialRule = CredentialPlacement & ({
22
+ from: 'env'; /** The environment variable. */
23
+ name: string;
24
+ } | {
25
+ from: 'file'; /** An absolute path. */
26
+ path: string;
27
+ } | {
28
+ from: 'vault';
29
+ /** The secret's path as Vault's API names it, for example "secret/data/payments/bank". */
30
+ path: string;
31
+ /** The field within the secret, for example "api_key". */
32
+ field: string;
33
+ });
34
+ /** A credential rule as checked: which source, and what to ask it for. */
35
+ type CompiledCredential = CredentialPlacement & {
36
+ from: 'env' | 'file' | 'vault';
37
+ /** env: the variable. file: the path. vault: "<path>#<field>". */
38
+ name: string;
39
+ };
40
+ type RouteRule = {
41
+ method: string;
42
+ /** A path under the upstream, with `:name` for a segment that varies. */
43
+ path: string;
44
+ action?: string;
45
+ resource?: {
46
+ type?: string;
47
+ idFrom?: string | string[];
48
+ };
49
+ /** `arg` is `params.x`, `query.x` or `body.x`. The first rule that matches replaces the action. */
50
+ when?: Array<{
51
+ if: Condition | Condition[];
52
+ action: string;
53
+ resource?: RouteRule['resource'];
54
+ }>;
55
+ /** Values recorded with the decision, by the same paths. Nothing else from the request is. */
56
+ context?: string[];
57
+ system?: string;
58
+ /** Forwarded without asking Praesidias. For reads you have decided need no record. */
59
+ ungoverned?: boolean;
60
+ };
61
+ type UpstreamRule = {
62
+ baseUrl: string;
63
+ credential?: CredentialRule;
64
+ routes: RouteRule[];
65
+ /**
66
+ * Drop-in: the API's own hostnames. When the platform points one at this gateway (DNS, or the
67
+ * address the agent is given), the agent keeps calling the URL and paths it always did, with
68
+ * no "/<upstream>" in front. A request is matched to its upstream by its Host header.
69
+ */
70
+ hosts?: string[];
71
+ /**
72
+ * Drop-in: the agent's Praesidias key arrives in the header the agent already sends to this
73
+ * API, in place of the API's own credential. The gateway takes it out, asks Praesidias, and
74
+ * puts the real credential on instead. Only a value shaped like a Praesidias key is accepted.
75
+ */
76
+ agentKey?: {
77
+ header: string;
78
+ scheme?: string;
79
+ };
80
+ };
81
+ type GatewayConfig = {
82
+ /** The system these APIs are, as your areas name it. */
83
+ system: string;
84
+ listen?: {
85
+ host?: string;
86
+ port?: number;
87
+ /** Required to listen anywhere but this machine. */ allowRemote?: boolean;
88
+ /**
89
+ * Answer over https with this certificate and key (files, PEM). For drop-in, the certificate
90
+ * is for the API's own hostnames, issued by the organization's certificate authority.
91
+ */
92
+ tls?: {
93
+ certFile: string;
94
+ keyFile: string;
95
+ };
96
+ };
97
+ /** By the name that is the first segment of the address the agent calls. */
98
+ upstreams?: Record<string, UpstreamRule>;
99
+ /**
100
+ * Also answer `POST /ask`: one request that asks Praesidias about an action and returns a
101
+ * plain yes or no. For no-code automations (n8n, Zapier, Make, Power Automate), where one
102
+ * HTTPS step is all there is. Off unless set.
103
+ */
104
+ ask?: boolean;
105
+ };
106
+ declare class ConfigError extends Error {
107
+ }
108
+ type CompiledRoute = RouteRule & {
109
+ method: string;
110
+ match: RegExp;
111
+ params: string[];
112
+ id: string;
113
+ };
114
+ type CompiledUpstream = {
115
+ name: string;
116
+ base: URL;
117
+ credential: CompiledCredential | null;
118
+ routes: CompiledRoute[];
119
+ mapping: MappingFile;
120
+ hosts: string[];
121
+ agentKey: {
122
+ header: string;
123
+ scheme?: string;
124
+ } | null;
125
+ };
126
+ type Compiled = {
127
+ system: string;
128
+ host: string;
129
+ port: number;
130
+ ask: boolean;
131
+ upstreams: Map<string, CompiledUpstream>;
132
+ /** Drop-in: an upstream by one of its own hostnames. */
133
+ hosts: Map<string, CompiledUpstream>;
134
+ tls: {
135
+ certFile: string;
136
+ keyFile: string;
137
+ } | null;
138
+ };
139
+ /** Checks the whole file at start, so a mistake stops the gateway instead of surfacing mid-request. */
140
+ declare function compileConfig(raw: unknown): Compiled;
141
+ declare function loadConfig(path: string): Compiled;
142
+
143
+ /**
144
+ * Where the gateway gets an upstream API's credential at the moment it forwards a request.
145
+ * The agent never has it: the agent only ever holds its Praesidias key.
146
+ *
147
+ * `get` is asked each time, so a source may rotate what it returns. Return undefined when
148
+ * there is nothing under that name; the request is then refused, never sent without one.
149
+ */
150
+ type CredentialSource = {
151
+ get(name: string): Promise<string | undefined> | string | undefined;
152
+ };
153
+ /** Reads credentials from this process's environment. */
154
+ declare class EnvCredentialSource implements CredentialSource {
155
+ private readonly env;
156
+ constructor(env?: NodeJS.ProcessEnv);
157
+ get(name: string): string | undefined;
158
+ }
159
+ /** Why a credential could not be had. Names only: never a secret, a token or a file's contents. */
160
+ type Note = (line: string) => void;
161
+ /**
162
+ * Reads a credential from a file, read again on every request so a rotated secret is picked up
163
+ * without a restart. This is how most secret stores hand a secret to a process: Vault Agent,
164
+ * the Kubernetes Secrets Store CSI driver (for AWS, Azure, Google Cloud or Vault), and Docker
165
+ * secrets all write it to a file. The name asked for is the file's path.
166
+ */
167
+ declare class FileCredentialSource implements CredentialSource {
168
+ private readonly note;
169
+ constructor(note?: Note);
170
+ get(path: string): Promise<string | undefined>;
171
+ }
172
+ type VaultOptions = {
173
+ env?: NodeJS.ProcessEnv;
174
+ /** How long a secret read from Vault is reused, in milliseconds. Default 60 s; 0 reads every time. */
175
+ cacheMs?: number;
176
+ /** How long Vault may take, in milliseconds. Default 5 s. */
177
+ timeoutMs?: number;
178
+ note?: Note;
179
+ /** For tests. */
180
+ fetch?: typeof fetch;
181
+ };
182
+ /**
183
+ * Reads a credential from HashiCorp Vault's key/value store (version 2, or version 1), over
184
+ * https. The name asked for is `<path>#<field>`, for example `secret/data/payments/bank#api_key`.
185
+ *
186
+ * Vault is found from the standard variables: VAULT_ADDR, VAULT_TOKEN or VAULT_TOKEN_FILE (the
187
+ * file Vault Agent keeps a fresh token in, read again each time), and VAULT_NAMESPACE. A private
188
+ * certificate authority for Vault is trusted with NODE_EXTRA_CA_CERTS, as for any Node process.
189
+ *
190
+ * A secret is reused for a short while so Vault is not asked on every request. When it is time
191
+ * to ask again and Vault cannot answer, nothing is returned and the request is refused: an
192
+ * expired secret is never used.
193
+ */
194
+ declare class VaultCredentialSource implements CredentialSource {
195
+ private readonly env;
196
+ private readonly cacheMs;
197
+ private readonly timeoutMs;
198
+ private readonly note;
199
+ private readonly send;
200
+ private readonly cache;
201
+ private readonly reading;
202
+ constructor(options?: VaultOptions);
203
+ get(name: string): Promise<string | undefined>;
204
+ private token;
205
+ private read;
206
+ }
207
+
208
+ type ApiGatewayOptions = {
209
+ config: Compiled;
210
+ /** The Praesidias address. Must be https, except on this machine. */
211
+ praesidiasUrl: string;
212
+ /** Where "env" credentials come from. Default: this process's environment. */
213
+ credentials?: CredentialSource;
214
+ /** Settings for "vault" credentials (Vault itself is found from VAULT_ADDR and friends). */
215
+ vault?: Omit<VaultOptions, 'note'>;
216
+ /** One line per request: method, route, action, outcome. Never a key, a credential or a body. */
217
+ log?: (line: string) => void;
218
+ /** The header the agent puts its Praesidias key in. Default "x-praesidias-key". */
219
+ keyHeader?: string;
220
+ /** Largest request body accepted, in bytes. Default 1 MB. */
221
+ maxBodyBytes?: number;
222
+ /** How long an upstream may take, in milliseconds. Default 30 s. */
223
+ upstreamTimeoutMs?: number;
224
+ /** For tests. */
225
+ fetch?: typeof fetch;
226
+ sdkOptions?: ConstructorParameters<typeof Praesidias>[0];
227
+ };
228
+ /**
229
+ * A gateway in front of the outside APIs an agent calls.
230
+ *
231
+ * The agent calls `http://<gateway>/<upstream>/<path>` with its Praesidias key. The gateway
232
+ * finds the configured route, asks Praesidias whether this agent may do that action on that
233
+ * resource, and only then forwards the request, adding the API's own credential. The agent
234
+ * never holds that credential, so it cannot reach the API any other way.
235
+ *
236
+ * It fails closed at every step: no route, no key, no answer from Praesidias, or no credential
237
+ * all mean the request is not sent.
238
+ */
239
+ declare function createApiGateway(options: ApiGatewayOptions): Server;
240
+ /**
241
+ * The gateway as a server: over https when the config has "listen.tls" (the certificate and key
242
+ * are read here, once), otherwise plain http as `createApiGateway`.
243
+ */
244
+ declare function createApiGatewayServer(options: ApiGatewayOptions): Server | Server$1;
245
+ /** The request handler, for any http or https server. Everything the gateway does is here. */
246
+ declare function createApiGatewayHandler(options: ApiGatewayOptions): (req: IncomingMessage, res: ServerResponse) => void;
247
+
248
+ export { type ApiGatewayOptions, type Compiled, type CompiledCredential, ConfigError, type CredentialRule, type CredentialSource, EnvCredentialSource, FileCredentialSource, type GatewayConfig, type RouteRule, type UpstreamRule, VaultCredentialSource, type VaultOptions, compileConfig, createApiGateway, createApiGatewayHandler, createApiGatewayServer, loadConfig };