@spicyapi/proxy 0.1.0 → 0.1.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/dist/src/core.d.ts +38 -30
- package/dist/src/core.d.ts.map +1 -1
- package/dist/src/core.js +45 -38
- package/dist/src/core.js.map +1 -1
- package/dist/src/express.d.ts +7 -6
- package/dist/src/express.d.ts.map +1 -1
- package/dist/src/express.js.map +1 -1
- package/dist/src/nextjs.d.ts +9 -8
- package/dist/src/nextjs.d.ts.map +1 -1
- package/dist/src/nextjs.js +9 -8
- package/dist/src/nextjs.js.map +1 -1
- package/package.json +10 -5
package/dist/src/core.d.ts
CHANGED
|
@@ -1,51 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* A server-side proxy that lets client applications call SpicyAPI without ever holding an API key.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* JavaScript in a browser, an iOS or Android app, a desktop app - none of these can keep a secret.
|
|
5
|
+
* A key shipped inside a bundle is a public key: an attacker decompiles it, or captures one
|
|
6
|
+
* request, and then spends until the balance is gone, while all we see on our side is "this account
|
|
7
|
+
* is rather busy today".
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* The right shape is for the client to call the caller's own server, and for that layer to add the
|
|
10
|
+
* key and forward the request. This module is that layer, mounted inside the caller's Next.js,
|
|
11
|
+
* Express or any other fetch runtime.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
13
|
+
* ## The protocol
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* The client puts the destination in an `x-spicy-target-url` header and calls whichever route its
|
|
16
|
+
* own backend agreed on (`/api/spicy/proxy` by convention). The proxy validates the target, adds
|
|
17
|
+
* `Authorization`, forwards the request and passes the response straight back.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
19
|
+
* ## Why the destination must be allow-listed rather than "forward to whatever the header says"
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
* `https://attacker.example`
|
|
21
|
-
*
|
|
21
|
+
* This is the one place in the design where getting it wrong is catastrophic. Without validation,
|
|
22
|
+
* anyone can put `https://attacker.example` in that header and have your server send them your key
|
|
23
|
+
* - one request leaks it, and the traffic looks entirely normal, because your own server sent it.
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
* origin
|
|
25
|
-
*
|
|
25
|
+
* So `allowedOrigins` defaults to `https://api.spicyapi.ai` alone, and the comparison is on the
|
|
26
|
+
* exact origin rather than a prefix: `https://api.spicyapi.ai.attacker.example` starts with our
|
|
27
|
+
* domain too.
|
|
26
28
|
*/
|
|
27
|
-
/**
|
|
29
|
+
/** The header carrying the destination. Lower-case throughout, as HTTP/2 accepts nothing else. */
|
|
28
30
|
export declare const TARGET_URL_HEADER = "x-spicy-target-url";
|
|
29
|
-
/**
|
|
31
|
+
/** The conventional route. Not mandatory, but keeping it consistent lets front-end configuration
|
|
32
|
+
* be copied as-is. */
|
|
30
33
|
export declare const DEFAULT_PROXY_ROUTE = "/api/spicy/proxy";
|
|
31
34
|
export interface ProxyOptions {
|
|
32
35
|
/**
|
|
33
|
-
*
|
|
36
|
+
* The API key. Read from `SPICY_API_KEY` by default.
|
|
34
37
|
*
|
|
35
|
-
*
|
|
38
|
+
* Passing a function supports rotation: it is consulted on every request, so no restart is
|
|
39
|
+
* needed.
|
|
36
40
|
*/
|
|
37
41
|
apiKey?: string | (() => string | undefined);
|
|
38
|
-
/**
|
|
42
|
+
/** Origins this proxy may forward to. The production entry point alone by default; only local
|
|
43
|
+
* development needs it widened. */
|
|
39
44
|
allowedOrigins?: string[];
|
|
40
|
-
/**
|
|
45
|
+
/** Forwarding timeout, 120 seconds by default - submitting a media task is fast; what takes time
|
|
46
|
+
* is the polling afterwards. */
|
|
41
47
|
timeoutMs?: number;
|
|
42
|
-
/**
|
|
48
|
+
/** An injected fetch, for tests. */
|
|
43
49
|
fetch?: typeof fetch;
|
|
44
50
|
}
|
|
45
51
|
export declare class ProxyConfigurationError extends Error {
|
|
46
52
|
constructor(message: string);
|
|
47
53
|
}
|
|
48
|
-
/**
|
|
54
|
+
/** The proxy's decision: either allow, with the request to send, or refuse, with the status to
|
|
55
|
+
* answer. */
|
|
49
56
|
export type ProxyDecision = {
|
|
50
57
|
ok: true;
|
|
51
58
|
request: Request;
|
|
@@ -55,16 +62,17 @@ export type ProxyDecision = {
|
|
|
55
62
|
message: string;
|
|
56
63
|
};
|
|
57
64
|
/**
|
|
58
|
-
*
|
|
65
|
+
* Turns an incoming request into either "forward this" or "refuse".
|
|
59
66
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
67
|
+
* This function never touches the network, so every framework adapter shares it and tests can
|
|
68
|
+
* assert on the decision directly without standing up a real HTTP server.
|
|
62
69
|
*/
|
|
63
70
|
export declare function decide(incoming: Request, options?: ProxyOptions): ProxyDecision;
|
|
64
71
|
/**
|
|
65
|
-
*
|
|
72
|
+
* The generic handler: takes a `Request` and returns a `Response`.
|
|
66
73
|
*
|
|
67
|
-
*
|
|
74
|
+
* Framework adapters are responsible only for converting their own request object into a `Request`;
|
|
75
|
+
* the decision and the forwarding both happen here.
|
|
68
76
|
*/
|
|
69
77
|
export declare function createProxyHandler(options?: ProxyOptions): (incoming: Request) => Promise<Response>;
|
|
70
78
|
//# sourceMappingURL=core.d.ts.map
|
package/dist/src/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD;sBACsB;AACtB,eAAO,MAAM,mBAAmB,qBAAqB,CAAC;AAgCtD,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C;uCACmC;IACnC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;oCACgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;aACa;AACb,MAAM,MAAM,aAAa,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAkBlF;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAE,YAAiB,GAAG,aAAa,CAgDnF;AAWD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,YAAiB,IAI9B,UAAU,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAmBnE"}
|
package/dist/src/core.js
CHANGED
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* A server-side proxy that lets client applications call SpicyAPI without ever holding an API key.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* JavaScript in a browser, an iOS or Android app, a desktop app - none of these can keep a secret.
|
|
5
|
+
* A key shipped inside a bundle is a public key: an attacker decompiles it, or captures one
|
|
6
|
+
* request, and then spends until the balance is gone, while all we see on our side is "this account
|
|
7
|
+
* is rather busy today".
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* The right shape is for the client to call the caller's own server, and for that layer to add the
|
|
10
|
+
* key and forward the request. This module is that layer, mounted inside the caller's Next.js,
|
|
11
|
+
* Express or any other fetch runtime.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
13
|
+
* ## The protocol
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* The client puts the destination in an `x-spicy-target-url` header and calls whichever route its
|
|
16
|
+
* own backend agreed on (`/api/spicy/proxy` by convention). The proxy validates the target, adds
|
|
17
|
+
* `Authorization`, forwards the request and passes the response straight back.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
19
|
+
* ## Why the destination must be allow-listed rather than "forward to whatever the header says"
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
* `https://attacker.example`
|
|
21
|
-
*
|
|
21
|
+
* This is the one place in the design where getting it wrong is catastrophic. Without validation,
|
|
22
|
+
* anyone can put `https://attacker.example` in that header and have your server send them your key
|
|
23
|
+
* - one request leaks it, and the traffic looks entirely normal, because your own server sent it.
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
* origin
|
|
25
|
-
*
|
|
25
|
+
* So `allowedOrigins` defaults to `https://api.spicyapi.ai` alone, and the comparison is on the
|
|
26
|
+
* exact origin rather than a prefix: `https://api.spicyapi.ai.attacker.example` starts with our
|
|
27
|
+
* domain too.
|
|
26
28
|
*/
|
|
27
|
-
/**
|
|
29
|
+
/** The header carrying the destination. Lower-case throughout, as HTTP/2 accepts nothing else. */
|
|
28
30
|
export const TARGET_URL_HEADER = "x-spicy-target-url";
|
|
29
|
-
/**
|
|
31
|
+
/** The conventional route. Not mandatory, but keeping it consistent lets front-end configuration
|
|
32
|
+
* be copied as-is. */
|
|
30
33
|
export const DEFAULT_PROXY_ROUTE = "/api/spicy/proxy";
|
|
31
|
-
/**
|
|
34
|
+
/** The platform's only production entry point. */
|
|
32
35
|
const DEFAULT_ALLOWED_ORIGIN = "https://api.spicyapi.ai";
|
|
33
36
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
37
|
+
* Hop-by-hop headers: they describe this particular connection rather than the message, so
|
|
38
|
+
* forwarding them is meaningless and possibly harmful. See RFC 9110 section 7.6.1. `host` is a
|
|
39
|
+
* separate case - it has to be recomputed for the destination, and copying it confuses the origin.
|
|
36
40
|
*/
|
|
37
41
|
const HOP_BY_HOP = new Set([
|
|
38
42
|
"connection",
|
|
@@ -47,10 +51,11 @@ const HOP_BY_HOP = new Set([
|
|
|
47
51
|
"content-length",
|
|
48
52
|
]);
|
|
49
53
|
/**
|
|
50
|
-
*
|
|
54
|
+
* These headers are dropped whatever the client sends, so that it cannot influence the
|
|
55
|
+
* authentication decision made on its behalf.
|
|
51
56
|
*
|
|
52
|
-
* `authorization`
|
|
53
|
-
*
|
|
57
|
+
* `authorization` matters most: without dropping it, a client could supply its own and override the
|
|
58
|
+
* one we add, turning the proxy into an open relay that forwards on behalf of any key at all.
|
|
54
59
|
*/
|
|
55
60
|
const CLIENT_CONTROLLED = new Set(["authorization", "cookie", "x-api-key"]);
|
|
56
61
|
export class ProxyConfigurationError extends Error {
|
|
@@ -72,10 +77,10 @@ function allowed(options) {
|
|
|
72
77
|
return list;
|
|
73
78
|
}
|
|
74
79
|
/**
|
|
75
|
-
*
|
|
80
|
+
* Turns an incoming request into either "forward this" or "refuse".
|
|
76
81
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
82
|
+
* This function never touches the network, so every framework adapter shares it and tests can
|
|
83
|
+
* assert on the decision directly without standing up a real HTTP server.
|
|
79
84
|
*/
|
|
80
85
|
export function decide(incoming, options = {}) {
|
|
81
86
|
const target = incoming.headers.get(TARGET_URL_HEADER);
|
|
@@ -89,13 +94,13 @@ export function decide(incoming, options = {}) {
|
|
|
89
94
|
catch {
|
|
90
95
|
return { ok: false, status: 400, message: `${TARGET_URL_HEADER} is not a valid absolute URL` };
|
|
91
96
|
}
|
|
92
|
-
//
|
|
97
|
+
// Exact origin comparison. A prefix match would admit https://api.spicyapi.ai.attacker.example.
|
|
93
98
|
if (!allowed(options).includes(url.origin)) {
|
|
94
99
|
return { ok: false, status: 403, message: `target origin is not allowed: ${url.origin}` };
|
|
95
100
|
}
|
|
96
101
|
const key = resolveKey(options);
|
|
97
102
|
if (!key) {
|
|
98
|
-
//
|
|
103
|
+
// This is a deployment problem rather than the caller's mistake, so it must not be a 4xx.
|
|
99
104
|
return { ok: false, status: 500, message: "the proxy has no SpicyAPI key configured" };
|
|
100
105
|
}
|
|
101
106
|
const headers = new Headers();
|
|
@@ -116,16 +121,17 @@ export function decide(incoming, options = {}) {
|
|
|
116
121
|
method: incoming.method,
|
|
117
122
|
headers,
|
|
118
123
|
body: incoming.body,
|
|
119
|
-
//
|
|
124
|
+
// What is forwarded is a new request and must not inherit the caller's redirect policy.
|
|
120
125
|
redirect: "manual",
|
|
121
|
-
// Node
|
|
122
|
-
// RequestInit
|
|
123
|
-
//
|
|
126
|
+
// Node's undici requires duplex to be declared explicitly for a streaming body. It is not
|
|
127
|
+
// part of the standard RequestInit, so only this fragment is asserted rather than the whole
|
|
128
|
+
// literal - asserting the literal would switch off type checking for every field above.
|
|
124
129
|
...(incoming.body ? { duplex: "half" } : {}),
|
|
125
130
|
}),
|
|
126
131
|
};
|
|
127
132
|
}
|
|
128
|
-
/**
|
|
133
|
+
/** Answers with a refusal shaped like the platform's error envelope, so a client needs no separate
|
|
134
|
+
* parsing path for the proxy. */
|
|
129
135
|
function refuse(status, message) {
|
|
130
136
|
return new Response(JSON.stringify({ code: status, msg: message, request_id: null }), {
|
|
131
137
|
status,
|
|
@@ -133,9 +139,10 @@ function refuse(status, message) {
|
|
|
133
139
|
});
|
|
134
140
|
}
|
|
135
141
|
/**
|
|
136
|
-
*
|
|
142
|
+
* The generic handler: takes a `Request` and returns a `Response`.
|
|
137
143
|
*
|
|
138
|
-
*
|
|
144
|
+
* Framework adapters are responsible only for converting their own request object into a `Request`;
|
|
145
|
+
* the decision and the forwarding both happen here.
|
|
139
146
|
*/
|
|
140
147
|
export function createProxyHandler(options = {}) {
|
|
141
148
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
@@ -149,7 +156,7 @@ export function createProxyHandler(options = {}) {
|
|
|
149
156
|
upstream = await doFetch(decision.request, { signal: AbortSignal.timeout(timeoutMs) });
|
|
150
157
|
}
|
|
151
158
|
catch {
|
|
152
|
-
//
|
|
159
|
+
// Do not return the upstream error verbatim: it may carry an internal hostname.
|
|
153
160
|
return refuse(504, "the upstream request did not complete in time");
|
|
154
161
|
}
|
|
155
162
|
const headers = new Headers();
|
package/dist/src/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"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"}
|
package/dist/src/express.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Express
|
|
2
|
+
* The Express adapter.
|
|
3
3
|
*
|
|
4
4
|
* ```ts
|
|
5
5
|
* app.all("/api/spicy/proxy", createExpressHandler());
|
|
6
6
|
* ```
|
|
7
7
|
*
|
|
8
|
-
* Express
|
|
9
|
-
*
|
|
8
|
+
* Express req/res are Node streams rather than fetch's `Request` and `Response`, so all this layer
|
|
9
|
+
* does is convert between the two; the decision and the forwarding stay in core.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Do not mount body-parsing middleware ahead of this route (`express.json()` and friends): it
|
|
12
|
+
* consumes the request body, so what gets forwarded is a request with an empty body, and the error
|
|
13
|
+
* surfaces upstream as "missing parameter" - which points nowhere near middleware ordering. Either
|
|
14
|
+
* mount the proxy before the parser, or skip the parser for this path.
|
|
14
15
|
*/
|
|
15
16
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
16
17
|
import { type ProxyOptions } from "./core.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAElE,KAAK,WAAW,GAAG,eAAe,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE9E,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,YAAiB,IAI3D,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,KAC/B,OAAO,CAAC,IAAI,CAAC,CAmCjB"}
|
package/dist/src/express.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,kBAAkB,EAAqB,MAAM,WAAW,CAAC;AAIlE,MAAM,UAAU,oBAAoB,CAAC,UAAwB,EAAE;IAC7D,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAE3C,OAAO,KAAK,UAAU,UAAU,CAC9B,GAAgB,EAChB,GAAmB,EACnB,IAAgC;QAEhC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBACxD,IAAI,KAAK,KAAK,SAAS;oBAAE,SAAS;gBAClC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACrE,CAAC;YAED,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC;YAC9D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAC3B,IAAI,OAAO,CAAC,uBAAuB,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,EAAE;gBACtE,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,OAAO;gBACP,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAmB,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACrE,CAAC,CAClB,CAAC;YAEF,GAAG,CAAC,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC;YACjC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YACtE,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAClB,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CACjC,QAAQ,CAAC,IAA8C,CACxD,CAAC;gBACF,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,GAAG,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,IAAI;gBAAE,IAAI,CAAC,KAAK,CAAC,CAAC;iBACjB,CAAC;gBACJ,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;gBACrB,GAAG,CAAC,GAAG,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/src/nextjs.d.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Next.js
|
|
2
|
+
* The Next.js adapter.
|
|
3
3
|
*
|
|
4
|
-
* App Router
|
|
4
|
+
* App Router: export `route` as the route's method handlers, unchanged.
|
|
5
5
|
*
|
|
6
6
|
* ```ts
|
|
7
7
|
* // app/api/spicy/proxy/route.ts
|
|
8
8
|
* export const { GET, POST, PUT, DELETE } = route;
|
|
9
|
-
* export const runtime = "nodejs"; //
|
|
9
|
+
* export const runtime = "nodejs"; // see below
|
|
10
10
|
* ```
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `runtime` must be set to `nodejs` explicitly: the edge runtime cannot read the server-only
|
|
13
|
+
* values in `process.env`, and the key lives precisely there. A project that defaults to the edge
|
|
14
|
+
* discovers this only after deploying - at which point the proxy answers 500 "no key configured"
|
|
15
|
+
* while everything works locally.
|
|
15
16
|
*/
|
|
16
17
|
import { type ProxyOptions } from "./core.js";
|
|
17
|
-
/** App Router
|
|
18
|
+
/** App Router: `export const { POST } = route` is all it takes. */
|
|
18
19
|
export declare function createRoute(options?: ProxyOptions): {
|
|
19
20
|
GET: (incoming: Request) => Promise<Response>;
|
|
20
21
|
POST: (incoming: Request) => Promise<Response>;
|
|
@@ -22,7 +23,7 @@ export declare function createRoute(options?: ProxyOptions): {
|
|
|
22
23
|
PATCH: (incoming: Request) => Promise<Response>;
|
|
23
24
|
DELETE: (incoming: Request) => Promise<Response>;
|
|
24
25
|
};
|
|
25
|
-
/**
|
|
26
|
+
/** An App Router route with default configuration, taking its key from `SPICY_API_KEY`. */
|
|
26
27
|
export declare const route: {
|
|
27
28
|
GET: (incoming: Request) => Promise<Response>;
|
|
28
29
|
POST: (incoming: Request) => Promise<Response>;
|
package/dist/src/nextjs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../../src/nextjs.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../../src/nextjs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAElE,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB;;;;;;EAGrD;AAED,2FAA2F;AAC3F,eAAO,MAAM,KAAK;;;;;;CAAgB,CAAC"}
|
package/dist/src/nextjs.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Next.js
|
|
2
|
+
* The Next.js adapter.
|
|
3
3
|
*
|
|
4
|
-
* App Router
|
|
4
|
+
* App Router: export `route` as the route's method handlers, unchanged.
|
|
5
5
|
*
|
|
6
6
|
* ```ts
|
|
7
7
|
* // app/api/spicy/proxy/route.ts
|
|
8
8
|
* export const { GET, POST, PUT, DELETE } = route;
|
|
9
|
-
* export const runtime = "nodejs"; //
|
|
9
|
+
* export const runtime = "nodejs"; // see below
|
|
10
10
|
* ```
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `runtime` must be set to `nodejs` explicitly: the edge runtime cannot read the server-only
|
|
13
|
+
* values in `process.env`, and the key lives precisely there. A project that defaults to the edge
|
|
14
|
+
* discovers this only after deploying - at which point the proxy answers 500 "no key configured"
|
|
15
|
+
* while everything works locally.
|
|
15
16
|
*/
|
|
16
17
|
import { createProxyHandler } from "./core.js";
|
|
17
|
-
/** App Router
|
|
18
|
+
/** App Router: `export const { POST } = route` is all it takes. */
|
|
18
19
|
export function createRoute(options = {}) {
|
|
19
20
|
const handle = createProxyHandler(options);
|
|
20
21
|
return { GET: handle, POST: handle, PUT: handle, PATCH: handle, DELETE: handle };
|
|
21
22
|
}
|
|
22
|
-
/**
|
|
23
|
+
/** An App Router route with default configuration, taking its key from `SPICY_API_KEY`. */
|
|
23
24
|
export const route = createRoute();
|
|
24
25
|
//# sourceMappingURL=nextjs.js.map
|
package/dist/src/nextjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nextjs.js","sourceRoot":"","sources":["../../src/nextjs.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"nextjs.js","sourceRoot":"","sources":["../../src/nextjs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,kBAAkB,EAAqB,MAAM,WAAW,CAAC;AAElE,mEAAmE;AACnE,MAAM,UAAU,WAAW,CAAC,UAAwB,EAAE;IACpD,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC3C,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACnF,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spicyapi/proxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Server-side proxy for SpicyAPI: let browser, mobile and desktop apps call the API without ever holding an API key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
10
|
-
"directory": "packages/proxy"
|
|
9
|
+
"url": "git+https://github.com/Spicy-API/spicy-proxy.git"
|
|
11
10
|
},
|
|
12
11
|
"homepage": "https://spicyapi.ai",
|
|
13
12
|
"bugs": {
|
|
14
|
-
"url": "https://
|
|
13
|
+
"url": "https://github.com/Spicy-API/spicy-proxy/issues"
|
|
15
14
|
},
|
|
16
15
|
"publishConfig": {
|
|
17
16
|
"access": "public"
|
|
@@ -43,6 +42,12 @@
|
|
|
43
42
|
],
|
|
44
43
|
"scripts": {
|
|
45
44
|
"build": "tsc -p tsconfig.json",
|
|
46
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
|
+
"test": "npm run build && node --test dist/test/*.test.js"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "26.4.0",
|
|
50
|
+
"prettier": "3.9.6",
|
|
51
|
+
"typescript": "5.9.3"
|
|
47
52
|
}
|
|
48
53
|
}
|