@benjypng/logseq-request 1.0.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/LICENSE.md +21 -0
- package/README.md +198 -0
- package/dist/index.cjs +137 -0
- package/dist/index.d.cts +86 -0
- package/dist/index.d.mts +86 -0
- package/dist/index.mjs +132 -0
- package/package.json +60 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 benjypng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# @benjypng/logseq-request
|
|
2
|
+
|
|
3
|
+
CORS-free HTTP requests for Logseq plugins.
|
|
4
|
+
|
|
5
|
+
Since the Electron update that ships a stricter CORS policy, direct `fetch`
|
|
6
|
+
(and fetch-based libraries like wretch or axios) from a plugin's
|
|
7
|
+
`lsp://logseq.com` origin to external APIs is blocked at the preflight stage.
|
|
8
|
+
Logseq's experimental request API (`exper_request`) proxies the call through
|
|
9
|
+
the main process, which is not subject to CORS. This package wraps that proxy
|
|
10
|
+
in a small, typed, wretch-like API.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @benjypng/logseq-request
|
|
16
|
+
# or
|
|
17
|
+
bun add @benjypng/logseq-request
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Zero dependencies. Requires running inside a Logseq plugin (it uses the
|
|
21
|
+
`logseq` global that `@logseq/libs` sets up in your plugin's entry), so it
|
|
22
|
+
works anywhere after `logseq.ready()` with no extra setup.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { proxy } from '@benjypng/logseq-request'
|
|
28
|
+
|
|
29
|
+
const data = await proxy('https://api.anthropic.com/v1/messages')
|
|
30
|
+
.headers({
|
|
31
|
+
'x-api-key': apiKey,
|
|
32
|
+
'anthropic-version': '2023-06-01',
|
|
33
|
+
'content-type': 'application/json',
|
|
34
|
+
})
|
|
35
|
+
.post({ model, max_tokens: 1024, messages })
|
|
36
|
+
.json<ChatResponse>()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Full plugin example
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// main.ts — your plugin entry
|
|
43
|
+
import '@logseq/libs'
|
|
44
|
+
import {
|
|
45
|
+
HttpError,
|
|
46
|
+
proxy,
|
|
47
|
+
ProxyUnavailableError,
|
|
48
|
+
} from '@benjypng/logseq-request'
|
|
49
|
+
|
|
50
|
+
const main = async () => {
|
|
51
|
+
logseq.Editor.registerSlashCommand('Ask Claude', async () => {
|
|
52
|
+
try {
|
|
53
|
+
const data = await proxy('https://api.anthropic.com/v1/messages')
|
|
54
|
+
.headers({
|
|
55
|
+
'x-api-key': logseq.settings?.apiKey as string,
|
|
56
|
+
'anthropic-version': '2023-06-01',
|
|
57
|
+
'content-type': 'application/json',
|
|
58
|
+
})
|
|
59
|
+
.post({
|
|
60
|
+
model: 'claude-sonnet-4-6',
|
|
61
|
+
max_tokens: 1024,
|
|
62
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
63
|
+
})
|
|
64
|
+
.json<{ content: { text: string }[] }>()
|
|
65
|
+
|
|
66
|
+
await logseq.Editor.insertAtEditingCursor(data.content[0].text)
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e instanceof HttpError) {
|
|
69
|
+
// Real HTTP failure (4xx/5xx) — status and body available on DB builds
|
|
70
|
+
logseq.UI.showMsg(`API error ${e.status}: ${e.body}`, 'error')
|
|
71
|
+
} else if (e instanceof ProxyUnavailableError) {
|
|
72
|
+
// Endpoint unreachable / IPC dead — message starts "Failed to fetch:"
|
|
73
|
+
logseq.UI.showMsg('Could not reach the API. Network up?', 'error')
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
logseq.ready(main).catch(console.error)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
All methods:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
proxy(url).get().json<T>()
|
|
88
|
+
proxy(url).headers(h).post(body).json<T>()
|
|
89
|
+
proxy(url).headers(h).put(body).text()
|
|
90
|
+
proxy(url).headers(h).patch(body).json<T>()
|
|
91
|
+
proxy(url).headers(h).delete().json<T>()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `.headers()` merges across calls (later wins). Builders are immutable —
|
|
95
|
+
each call returns a new one, so you can branch safely:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const api = proxy('https://api.example.com/v1').headers(authHeaders)
|
|
99
|
+
await api.get().json<Status>() // base unchanged
|
|
100
|
+
await api.headers({ 'x-extra': '1' }).post(body).json<Result>()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- Nothing is sent until you call `.json<T>()` or `.text()` — a method call
|
|
104
|
+
returns a lazy response handle.
|
|
105
|
+
- `.json<T>()` parses the response body as JSON; `.text()` returns the raw
|
|
106
|
+
body string.
|
|
107
|
+
- `.timeout(ms)` (opt-in, no default) fails the request if the proxy never
|
|
108
|
+
responds — useful against dead IPC. LLM calls can run for minutes, so no
|
|
109
|
+
timeout is applied unless you ask for one.
|
|
110
|
+
|
|
111
|
+
### Error handling
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { HttpError, ProxyUnavailableError } from '@benjypng/logseq-request'
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await proxy(url).post(body).json()
|
|
118
|
+
} catch (e) {
|
|
119
|
+
if (e instanceof HttpError) {
|
|
120
|
+
// HTTP-level failure (DB builds only — see Limitations)
|
|
121
|
+
console.error(e.status, e.statusText, e.body, e.json())
|
|
122
|
+
} else if (e instanceof ProxyUnavailableError) {
|
|
123
|
+
// Unreachable endpoint, dead IPC, or timeout.
|
|
124
|
+
// e.message always starts with "Failed to fetch:".
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- **`HttpError`** — thrown when the response reports `ok: false`. Carries
|
|
130
|
+
`status: number`, `statusText?: string`, `body: string`, and a lazy
|
|
131
|
+
`json<T>()` that returns `undefined` if the body isn't valid JSON.
|
|
132
|
+
- **`ProxyUnavailableError`** — thrown when the proxy yields no response
|
|
133
|
+
(unreachable endpoint, dead IPC) or the opt-in timeout elapses. Its message
|
|
134
|
+
always starts with `Failed to fetch:` so existing connection-error handling
|
|
135
|
+
that pattern-matches on that prefix keeps working.
|
|
136
|
+
|
|
137
|
+
### Low-level escape hatch
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { proxyRequest } from '@benjypng/logseq-request'
|
|
141
|
+
|
|
142
|
+
const data = await proxyRequest<MyResponse>({
|
|
143
|
+
url,
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers,
|
|
146
|
+
body,
|
|
147
|
+
timeoutMs: 30_000,
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`proxyRequestRaw()` goes one level lower still: it returns the normalized
|
|
152
|
+
response body without JSON parsing (a string in the typical case).
|
|
153
|
+
|
|
154
|
+
## Limitations
|
|
155
|
+
|
|
156
|
+
- **No streaming.** `exper_request` buffers the full response; SSE/streaming
|
|
157
|
+
APIs are not supported. Request non-streaming variants from providers.
|
|
158
|
+
- **HTTP errors are invisible on markdown builds (0.10.x).** Those builds
|
|
159
|
+
ignore `includeResponse` and return the bare body, so there is no status
|
|
160
|
+
code to inspect; `HttpError` is only thrown on DB builds (2.x). Error
|
|
161
|
+
bodies on markdown builds surface as JSON parse results instead.
|
|
162
|
+
- **No abort/cancellation.** The underlying IPC offers none; `.timeout(ms)`
|
|
163
|
+
abandons the wait but cannot cancel the in-flight request.
|
|
164
|
+
|
|
165
|
+
## How it works
|
|
166
|
+
|
|
167
|
+
`exper_request` is invoked via Logseq's internal
|
|
168
|
+
`_execCallableAPIAsync('exper_request', pluginId, options)`, which returns a
|
|
169
|
+
request id; the response arrives on a `task_callback_<id>` event. Responses
|
|
170
|
+
are normalized across Logseq builds: DB (2.x) honours `includeResponse` and
|
|
171
|
+
returns `{ status, ok, body }`; markdown (0.10.x) returns the bare body;
|
|
172
|
+
`null`/`undefined` (unreachable endpoint) becomes `ProxyUnavailableError`.
|
|
173
|
+
|
|
174
|
+
The package deliberately has no dependency on `@logseq/libs`: there is no
|
|
175
|
+
instance to import from it (the `logseq` global is created as a side effect
|
|
176
|
+
of the consumer's own import), and the IPC members used here are
|
|
177
|
+
undocumented internals that its public types don't cover. Minimal types for
|
|
178
|
+
exactly the members used are defined in-package instead.
|
|
179
|
+
|
|
180
|
+
## Local development
|
|
181
|
+
|
|
182
|
+
To consume the package from another project before it's published:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# in this repo
|
|
186
|
+
bun install && bun run build
|
|
187
|
+
|
|
188
|
+
# in your plugin
|
|
189
|
+
bun add file:../logseq-request
|
|
190
|
+
# or: `bun link` here, then `bun link @benjypng/logseq-request` in the plugin
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Scripts: `bun run test` (vitest), `bun run typecheck`, `bun run build`
|
|
194
|
+
(tsdown → ESM + CJS + d.ts), `bun run lint` (Biome).
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/errors.ts
|
|
3
|
+
/** Thrown when a DB-build response wrapper reports ok: false. */
|
|
4
|
+
var HttpError = class extends Error {
|
|
5
|
+
constructor(status, statusText, body) {
|
|
6
|
+
super(statusText || `HTTP ${status}`);
|
|
7
|
+
this.name = "HttpError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.statusText = statusText;
|
|
10
|
+
this.body = body;
|
|
11
|
+
}
|
|
12
|
+
/** Parse the error body as JSON; undefined if it isn't valid JSON. */
|
|
13
|
+
json() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(this.body);
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when the proxy yields no response: unreachable endpoint, dead IPC,
|
|
23
|
+
* or the opt-in timeout elapsing. The "Failed to fetch" prefix is part of the
|
|
24
|
+
* contract — consumers pattern-match it for connection-error guidance.
|
|
25
|
+
*/
|
|
26
|
+
var ProxyUnavailableError = class extends Error {
|
|
27
|
+
constructor(detail) {
|
|
28
|
+
super(`Failed to fetch: ${detail}`);
|
|
29
|
+
this.name = "ProxyUnavailableError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/core.ts
|
|
34
|
+
/**
|
|
35
|
+
* Send a request through Logseq's CORS-free `exper_request` proxy and return
|
|
36
|
+
* the response body, normalized across Logseq builds:
|
|
37
|
+
* - DB (2.x): honours includeResponse -> { status, ok, body, ... }; we
|
|
38
|
+
* unwrap to the body string, throwing HttpError when ok is false.
|
|
39
|
+
* - markdown (0.10.x): ignores includeResponse -> the bare body (a string,
|
|
40
|
+
* or an object/array the host already parsed). Note: HTTP errors cannot
|
|
41
|
+
* be detected on these builds — there is no status to inspect.
|
|
42
|
+
* - unreachable endpoint / dead IPC: null/undefined -> ProxyUnavailableError.
|
|
43
|
+
*
|
|
44
|
+
* Returns a string in the typical case; an object/array when an older build
|
|
45
|
+
* pre-parsed the body.
|
|
46
|
+
*/
|
|
47
|
+
const proxyRequestRaw = async (input) => {
|
|
48
|
+
const options = {
|
|
49
|
+
url: input.url,
|
|
50
|
+
method: input.method,
|
|
51
|
+
headers: input.headers ?? {},
|
|
52
|
+
returnType: "text",
|
|
53
|
+
includeResponse: true
|
|
54
|
+
};
|
|
55
|
+
if (input.body !== void 0) options.data = input.body;
|
|
56
|
+
const host = logseq;
|
|
57
|
+
const reqID = await host._execCallableAPIAsync("exper_request", host.baseInfo.id, options);
|
|
58
|
+
const callback = new Promise((resolve) => {
|
|
59
|
+
host.Request.once(`task_callback_${reqID}`, resolve);
|
|
60
|
+
});
|
|
61
|
+
let res;
|
|
62
|
+
if (input.timeoutMs === void 0) res = await callback;
|
|
63
|
+
else {
|
|
64
|
+
let timer;
|
|
65
|
+
try {
|
|
66
|
+
res = await Promise.race([callback, new Promise((_, reject) => {
|
|
67
|
+
timer = setTimeout(() => reject(new ProxyUnavailableError(`no response from the request proxy within ${input.timeoutMs}ms`)), input.timeoutMs);
|
|
68
|
+
})]);
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (res == null) throw new ProxyUnavailableError("no response from the request proxy. Is the endpoint reachable (local model running / network up)?");
|
|
74
|
+
if (typeof res === "object" && typeof res.status === "number") {
|
|
75
|
+
const wrapped = res;
|
|
76
|
+
if (!wrapped.ok) throw new HttpError(wrapped.status, wrapped.statusText, wrapped.body);
|
|
77
|
+
return wrapped.body;
|
|
78
|
+
}
|
|
79
|
+
return res;
|
|
80
|
+
};
|
|
81
|
+
/** Low-level escape hatch: proxy a request and parse the response as JSON. */
|
|
82
|
+
const proxyRequest = async (input) => {
|
|
83
|
+
const body = await proxyRequestRaw(input);
|
|
84
|
+
if (typeof body === "string") return JSON.parse(body);
|
|
85
|
+
return body;
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/builder.ts
|
|
89
|
+
/**
|
|
90
|
+
* Wretch-like builder over Logseq's CORS-free request proxy. Each call
|
|
91
|
+
* returns a new immutable builder; nothing is sent until `.json()` or
|
|
92
|
+
* `.text()` is called on the response handle.
|
|
93
|
+
*/
|
|
94
|
+
const proxy = (url) => {
|
|
95
|
+
const make = (headers, timeoutMs) => {
|
|
96
|
+
const dispatch = (method, body) => {
|
|
97
|
+
const input = {
|
|
98
|
+
url,
|
|
99
|
+
method,
|
|
100
|
+
headers
|
|
101
|
+
};
|
|
102
|
+
if (body !== void 0) input.body = body;
|
|
103
|
+
if (timeoutMs !== void 0) input.timeoutMs = timeoutMs;
|
|
104
|
+
return {
|
|
105
|
+
json: async () => {
|
|
106
|
+
const res = await proxyRequestRaw(input);
|
|
107
|
+
if (typeof res === "string") return JSON.parse(res);
|
|
108
|
+
return res;
|
|
109
|
+
},
|
|
110
|
+
text: async () => {
|
|
111
|
+
const res = await proxyRequestRaw(input);
|
|
112
|
+
if (typeof res === "string") return res;
|
|
113
|
+
return JSON.stringify(res);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
headers: (h) => make({
|
|
119
|
+
...headers,
|
|
120
|
+
...h
|
|
121
|
+
}, timeoutMs),
|
|
122
|
+
timeout: (ms) => make(headers, ms),
|
|
123
|
+
get: () => dispatch("GET"),
|
|
124
|
+
post: (body) => dispatch("POST", body),
|
|
125
|
+
put: (body) => dispatch("PUT", body),
|
|
126
|
+
patch: (body) => dispatch("PATCH", body),
|
|
127
|
+
delete: () => dispatch("DELETE")
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
return make({}, void 0);
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
exports.HttpError = HttpError;
|
|
134
|
+
exports.ProxyUnavailableError = ProxyUnavailableError;
|
|
135
|
+
exports.proxy = proxy;
|
|
136
|
+
exports.proxyRequest = proxyRequest;
|
|
137
|
+
exports.proxyRequestRaw = proxyRequestRaw;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region src/builder.d.ts
|
|
2
|
+
interface ResponseHandle {
|
|
3
|
+
/** Send the request and parse the response body as JSON. */
|
|
4
|
+
json: <T = unknown>() => Promise<T>;
|
|
5
|
+
/** Send the request and return the raw body text. */
|
|
6
|
+
text: () => Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
interface ProxyBuilder {
|
|
9
|
+
/** Merge headers into the request; later calls win on conflicts. */
|
|
10
|
+
headers: (headers: Record<string, string>) => ProxyBuilder;
|
|
11
|
+
/** Fail with ProxyUnavailableError if no response arrives within ms. */
|
|
12
|
+
timeout: (ms: number) => ProxyBuilder;
|
|
13
|
+
get: () => ResponseHandle;
|
|
14
|
+
post: (body?: object) => ResponseHandle;
|
|
15
|
+
put: (body?: object) => ResponseHandle;
|
|
16
|
+
patch: (body?: object) => ResponseHandle;
|
|
17
|
+
delete: () => ResponseHandle;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wretch-like builder over Logseq's CORS-free request proxy. Each call
|
|
21
|
+
* returns a new immutable builder; nothing is sent until `.json()` or
|
|
22
|
+
* `.text()` is called on the response handle.
|
|
23
|
+
*/
|
|
24
|
+
declare const proxy: (url: string) => ProxyBuilder;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/types.d.ts
|
|
27
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
28
|
+
/** Input to the low-level proxyRequest / proxyRequestRaw functions. */
|
|
29
|
+
interface ProxyRequestInput {
|
|
30
|
+
url: string;
|
|
31
|
+
method: HttpMethod;
|
|
32
|
+
headers?: Record<string, string>;
|
|
33
|
+
/** JSON-serialisable request body. Omitted from the wire when undefined. */
|
|
34
|
+
body?: object;
|
|
35
|
+
/**
|
|
36
|
+
* Milliseconds to wait for the proxy callback before throwing
|
|
37
|
+
* ProxyUnavailableError. Default: no timeout (LLM calls can run minutes).
|
|
38
|
+
*/
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Wrapper shape returned by DB (2.x) builds. */
|
|
42
|
+
interface ProxyResponse {
|
|
43
|
+
status: number;
|
|
44
|
+
statusText?: string;
|
|
45
|
+
ok: boolean;
|
|
46
|
+
body: string;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/core.d.ts
|
|
50
|
+
/**
|
|
51
|
+
* Send a request through Logseq's CORS-free `exper_request` proxy and return
|
|
52
|
+
* the response body, normalized across Logseq builds:
|
|
53
|
+
* - DB (2.x): honours includeResponse -> { status, ok, body, ... }; we
|
|
54
|
+
* unwrap to the body string, throwing HttpError when ok is false.
|
|
55
|
+
* - markdown (0.10.x): ignores includeResponse -> the bare body (a string,
|
|
56
|
+
* or an object/array the host already parsed). Note: HTTP errors cannot
|
|
57
|
+
* be detected on these builds — there is no status to inspect.
|
|
58
|
+
* - unreachable endpoint / dead IPC: null/undefined -> ProxyUnavailableError.
|
|
59
|
+
*
|
|
60
|
+
* Returns a string in the typical case; an object/array when an older build
|
|
61
|
+
* pre-parsed the body.
|
|
62
|
+
*/
|
|
63
|
+
declare const proxyRequestRaw: (input: ProxyRequestInput) => Promise<unknown>;
|
|
64
|
+
/** Low-level escape hatch: proxy a request and parse the response as JSON. */
|
|
65
|
+
declare const proxyRequest: <T>(input: ProxyRequestInput) => Promise<T>;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/errors.d.ts
|
|
68
|
+
/** Thrown when a DB-build response wrapper reports ok: false. */
|
|
69
|
+
declare class HttpError extends Error {
|
|
70
|
+
readonly status: number;
|
|
71
|
+
readonly statusText?: string;
|
|
72
|
+
readonly body: string;
|
|
73
|
+
constructor(status: number, statusText: string | undefined, body: string);
|
|
74
|
+
/** Parse the error body as JSON; undefined if it isn't valid JSON. */
|
|
75
|
+
json<T = unknown>(): T | undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Thrown when the proxy yields no response: unreachable endpoint, dead IPC,
|
|
79
|
+
* or the opt-in timeout elapsing. The "Failed to fetch" prefix is part of the
|
|
80
|
+
* contract — consumers pattern-match it for connection-error guidance.
|
|
81
|
+
*/
|
|
82
|
+
declare class ProxyUnavailableError extends Error {
|
|
83
|
+
constructor(detail: string);
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { HttpError, type HttpMethod, type ProxyBuilder, type ProxyRequestInput, type ProxyResponse, ProxyUnavailableError, type ResponseHandle, proxy, proxyRequest, proxyRequestRaw };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region src/builder.d.ts
|
|
2
|
+
interface ResponseHandle {
|
|
3
|
+
/** Send the request and parse the response body as JSON. */
|
|
4
|
+
json: <T = unknown>() => Promise<T>;
|
|
5
|
+
/** Send the request and return the raw body text. */
|
|
6
|
+
text: () => Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
interface ProxyBuilder {
|
|
9
|
+
/** Merge headers into the request; later calls win on conflicts. */
|
|
10
|
+
headers: (headers: Record<string, string>) => ProxyBuilder;
|
|
11
|
+
/** Fail with ProxyUnavailableError if no response arrives within ms. */
|
|
12
|
+
timeout: (ms: number) => ProxyBuilder;
|
|
13
|
+
get: () => ResponseHandle;
|
|
14
|
+
post: (body?: object) => ResponseHandle;
|
|
15
|
+
put: (body?: object) => ResponseHandle;
|
|
16
|
+
patch: (body?: object) => ResponseHandle;
|
|
17
|
+
delete: () => ResponseHandle;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wretch-like builder over Logseq's CORS-free request proxy. Each call
|
|
21
|
+
* returns a new immutable builder; nothing is sent until `.json()` or
|
|
22
|
+
* `.text()` is called on the response handle.
|
|
23
|
+
*/
|
|
24
|
+
declare const proxy: (url: string) => ProxyBuilder;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/types.d.ts
|
|
27
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
28
|
+
/** Input to the low-level proxyRequest / proxyRequestRaw functions. */
|
|
29
|
+
interface ProxyRequestInput {
|
|
30
|
+
url: string;
|
|
31
|
+
method: HttpMethod;
|
|
32
|
+
headers?: Record<string, string>;
|
|
33
|
+
/** JSON-serialisable request body. Omitted from the wire when undefined. */
|
|
34
|
+
body?: object;
|
|
35
|
+
/**
|
|
36
|
+
* Milliseconds to wait for the proxy callback before throwing
|
|
37
|
+
* ProxyUnavailableError. Default: no timeout (LLM calls can run minutes).
|
|
38
|
+
*/
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Wrapper shape returned by DB (2.x) builds. */
|
|
42
|
+
interface ProxyResponse {
|
|
43
|
+
status: number;
|
|
44
|
+
statusText?: string;
|
|
45
|
+
ok: boolean;
|
|
46
|
+
body: string;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/core.d.ts
|
|
50
|
+
/**
|
|
51
|
+
* Send a request through Logseq's CORS-free `exper_request` proxy and return
|
|
52
|
+
* the response body, normalized across Logseq builds:
|
|
53
|
+
* - DB (2.x): honours includeResponse -> { status, ok, body, ... }; we
|
|
54
|
+
* unwrap to the body string, throwing HttpError when ok is false.
|
|
55
|
+
* - markdown (0.10.x): ignores includeResponse -> the bare body (a string,
|
|
56
|
+
* or an object/array the host already parsed). Note: HTTP errors cannot
|
|
57
|
+
* be detected on these builds — there is no status to inspect.
|
|
58
|
+
* - unreachable endpoint / dead IPC: null/undefined -> ProxyUnavailableError.
|
|
59
|
+
*
|
|
60
|
+
* Returns a string in the typical case; an object/array when an older build
|
|
61
|
+
* pre-parsed the body.
|
|
62
|
+
*/
|
|
63
|
+
declare const proxyRequestRaw: (input: ProxyRequestInput) => Promise<unknown>;
|
|
64
|
+
/** Low-level escape hatch: proxy a request and parse the response as JSON. */
|
|
65
|
+
declare const proxyRequest: <T>(input: ProxyRequestInput) => Promise<T>;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/errors.d.ts
|
|
68
|
+
/** Thrown when a DB-build response wrapper reports ok: false. */
|
|
69
|
+
declare class HttpError extends Error {
|
|
70
|
+
readonly status: number;
|
|
71
|
+
readonly statusText?: string;
|
|
72
|
+
readonly body: string;
|
|
73
|
+
constructor(status: number, statusText: string | undefined, body: string);
|
|
74
|
+
/** Parse the error body as JSON; undefined if it isn't valid JSON. */
|
|
75
|
+
json<T = unknown>(): T | undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Thrown when the proxy yields no response: unreachable endpoint, dead IPC,
|
|
79
|
+
* or the opt-in timeout elapsing. The "Failed to fetch" prefix is part of the
|
|
80
|
+
* contract — consumers pattern-match it for connection-error guidance.
|
|
81
|
+
*/
|
|
82
|
+
declare class ProxyUnavailableError extends Error {
|
|
83
|
+
constructor(detail: string);
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { HttpError, type HttpMethod, type ProxyBuilder, type ProxyRequestInput, type ProxyResponse, ProxyUnavailableError, type ResponseHandle, proxy, proxyRequest, proxyRequestRaw };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/** Thrown when a DB-build response wrapper reports ok: false. */
|
|
3
|
+
var HttpError = class extends Error {
|
|
4
|
+
constructor(status, statusText, body) {
|
|
5
|
+
super(statusText || `HTTP ${status}`);
|
|
6
|
+
this.name = "HttpError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.statusText = statusText;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
/** Parse the error body as JSON; undefined if it isn't valid JSON. */
|
|
12
|
+
json() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(this.body);
|
|
15
|
+
} catch {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Thrown when the proxy yields no response: unreachable endpoint, dead IPC,
|
|
22
|
+
* or the opt-in timeout elapsing. The "Failed to fetch" prefix is part of the
|
|
23
|
+
* contract — consumers pattern-match it for connection-error guidance.
|
|
24
|
+
*/
|
|
25
|
+
var ProxyUnavailableError = class extends Error {
|
|
26
|
+
constructor(detail) {
|
|
27
|
+
super(`Failed to fetch: ${detail}`);
|
|
28
|
+
this.name = "ProxyUnavailableError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/core.ts
|
|
33
|
+
/**
|
|
34
|
+
* Send a request through Logseq's CORS-free `exper_request` proxy and return
|
|
35
|
+
* the response body, normalized across Logseq builds:
|
|
36
|
+
* - DB (2.x): honours includeResponse -> { status, ok, body, ... }; we
|
|
37
|
+
* unwrap to the body string, throwing HttpError when ok is false.
|
|
38
|
+
* - markdown (0.10.x): ignores includeResponse -> the bare body (a string,
|
|
39
|
+
* or an object/array the host already parsed). Note: HTTP errors cannot
|
|
40
|
+
* be detected on these builds — there is no status to inspect.
|
|
41
|
+
* - unreachable endpoint / dead IPC: null/undefined -> ProxyUnavailableError.
|
|
42
|
+
*
|
|
43
|
+
* Returns a string in the typical case; an object/array when an older build
|
|
44
|
+
* pre-parsed the body.
|
|
45
|
+
*/
|
|
46
|
+
const proxyRequestRaw = async (input) => {
|
|
47
|
+
const options = {
|
|
48
|
+
url: input.url,
|
|
49
|
+
method: input.method,
|
|
50
|
+
headers: input.headers ?? {},
|
|
51
|
+
returnType: "text",
|
|
52
|
+
includeResponse: true
|
|
53
|
+
};
|
|
54
|
+
if (input.body !== void 0) options.data = input.body;
|
|
55
|
+
const host = logseq;
|
|
56
|
+
const reqID = await host._execCallableAPIAsync("exper_request", host.baseInfo.id, options);
|
|
57
|
+
const callback = new Promise((resolve) => {
|
|
58
|
+
host.Request.once(`task_callback_${reqID}`, resolve);
|
|
59
|
+
});
|
|
60
|
+
let res;
|
|
61
|
+
if (input.timeoutMs === void 0) res = await callback;
|
|
62
|
+
else {
|
|
63
|
+
let timer;
|
|
64
|
+
try {
|
|
65
|
+
res = await Promise.race([callback, new Promise((_, reject) => {
|
|
66
|
+
timer = setTimeout(() => reject(new ProxyUnavailableError(`no response from the request proxy within ${input.timeoutMs}ms`)), input.timeoutMs);
|
|
67
|
+
})]);
|
|
68
|
+
} finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (res == null) throw new ProxyUnavailableError("no response from the request proxy. Is the endpoint reachable (local model running / network up)?");
|
|
73
|
+
if (typeof res === "object" && typeof res.status === "number") {
|
|
74
|
+
const wrapped = res;
|
|
75
|
+
if (!wrapped.ok) throw new HttpError(wrapped.status, wrapped.statusText, wrapped.body);
|
|
76
|
+
return wrapped.body;
|
|
77
|
+
}
|
|
78
|
+
return res;
|
|
79
|
+
};
|
|
80
|
+
/** Low-level escape hatch: proxy a request and parse the response as JSON. */
|
|
81
|
+
const proxyRequest = async (input) => {
|
|
82
|
+
const body = await proxyRequestRaw(input);
|
|
83
|
+
if (typeof body === "string") return JSON.parse(body);
|
|
84
|
+
return body;
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/builder.ts
|
|
88
|
+
/**
|
|
89
|
+
* Wretch-like builder over Logseq's CORS-free request proxy. Each call
|
|
90
|
+
* returns a new immutable builder; nothing is sent until `.json()` or
|
|
91
|
+
* `.text()` is called on the response handle.
|
|
92
|
+
*/
|
|
93
|
+
const proxy = (url) => {
|
|
94
|
+
const make = (headers, timeoutMs) => {
|
|
95
|
+
const dispatch = (method, body) => {
|
|
96
|
+
const input = {
|
|
97
|
+
url,
|
|
98
|
+
method,
|
|
99
|
+
headers
|
|
100
|
+
};
|
|
101
|
+
if (body !== void 0) input.body = body;
|
|
102
|
+
if (timeoutMs !== void 0) input.timeoutMs = timeoutMs;
|
|
103
|
+
return {
|
|
104
|
+
json: async () => {
|
|
105
|
+
const res = await proxyRequestRaw(input);
|
|
106
|
+
if (typeof res === "string") return JSON.parse(res);
|
|
107
|
+
return res;
|
|
108
|
+
},
|
|
109
|
+
text: async () => {
|
|
110
|
+
const res = await proxyRequestRaw(input);
|
|
111
|
+
if (typeof res === "string") return res;
|
|
112
|
+
return JSON.stringify(res);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
headers: (h) => make({
|
|
118
|
+
...headers,
|
|
119
|
+
...h
|
|
120
|
+
}, timeoutMs),
|
|
121
|
+
timeout: (ms) => make(headers, ms),
|
|
122
|
+
get: () => dispatch("GET"),
|
|
123
|
+
post: (body) => dispatch("POST", body),
|
|
124
|
+
put: (body) => dispatch("PUT", body),
|
|
125
|
+
patch: (body) => dispatch("PATCH", body),
|
|
126
|
+
delete: () => dispatch("DELETE")
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
return make({}, void 0);
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
export { HttpError, ProxyUnavailableError, proxy, proxyRequest, proxyRequestRaw };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@benjypng/logseq-request",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CORS-free HTTP requests for Logseq plugins, via Logseq's exper_request proxy",
|
|
5
|
+
"author": "benjypng",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/benjypng/logseq-request.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"logseq",
|
|
13
|
+
"logseq-plugin",
|
|
14
|
+
"cors",
|
|
15
|
+
"http",
|
|
16
|
+
"request",
|
|
17
|
+
"fetch"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.mjs",
|
|
22
|
+
"types": "./dist/index.d.mts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
27
|
+
"default": "./dist/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./dist/index.d.cts",
|
|
31
|
+
"default": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown src/index.ts --format esm,cjs --dts --clean",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"lint": "biome check --write .",
|
|
46
|
+
"prepare": "husky"
|
|
47
|
+
},
|
|
48
|
+
"release": {
|
|
49
|
+
"branches": [
|
|
50
|
+
"main"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "^2.3.11",
|
|
55
|
+
"husky": "^9.1.7",
|
|
56
|
+
"tsdown": "^0.22.2",
|
|
57
|
+
"typescript": "^5.5.4",
|
|
58
|
+
"vitest": "^3.2.4"
|
|
59
|
+
}
|
|
60
|
+
}
|