tippa 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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/channel/bin.d.mts +1 -0
- package/dist/channel/bin.mjs +353 -0
- package/dist/client/index.d.ts +21 -0
- package/dist/client/index.js +8239 -0
- package/dist/execute-context-menu-action-Bbh74u99-DE1LGcxh.js +3474 -0
- package/dist/http-BcWR4_RM.mjs +159 -0
- package/dist/index.d.mts +94 -0
- package/dist/index.mjs +457 -0
- package/dist/renderer-Ym7FGQIR--GlE3v8a.js +2876 -0
- package/package.json +64 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { a as MAX_HTML_CHARS, c as pickRequestSchema, d as STATUS_EVENT, f as STATUS_REQUEST_EVENT, h as isAlive, i as sendJson, l as MAX_BODY_BYTES, m as discoverySchema, n as isClientAbort, p as discoveryPath, r as readBody, s as pickReplySchema, t as hasSecretHeader, u as REPLY_EVENT } from "./http-BcWR4_RM.mjs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
//#region src/agent.ts
|
|
8
|
+
/** what `send` rejects with when no agent is reachable; the endpoint answers 503 */
|
|
9
|
+
var AgentNotConnectedError = class extends Error {
|
|
10
|
+
constructor(label) {
|
|
11
|
+
super(`${label} isn't connected`);
|
|
12
|
+
this.name = "AgentNotConnectedError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/claude-session.ts
|
|
17
|
+
const LABEL = "Claude";
|
|
18
|
+
const POLL_MS = 2e3;
|
|
19
|
+
const SEND_TIMEOUT_MS = 1e4;
|
|
20
|
+
const healthSchema = z.object({ ok: z.literal(true) });
|
|
21
|
+
/**
|
|
22
|
+
* sends picks to the claude code session running the `tippa-channel` helper.
|
|
23
|
+
* finds the helper through the nearest `.tippa/channel.json` at or above vite's root.
|
|
24
|
+
*/
|
|
25
|
+
function claudeSession() {
|
|
26
|
+
return {
|
|
27
|
+
label: LABEL,
|
|
28
|
+
connect: ({ root, logger }) => connectToHelper(root, logger)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** why a check found no helper; logged once per distinct text */
|
|
32
|
+
var NoHelper = class {
|
|
33
|
+
reason;
|
|
34
|
+
constructor(reason) {
|
|
35
|
+
this.reason = reason;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function connectToHelper(root, logger) {
|
|
39
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
40
|
+
const replyListeners = /* @__PURE__ */ new Set();
|
|
41
|
+
let status = "waiting";
|
|
42
|
+
let announced = false;
|
|
43
|
+
let helper;
|
|
44
|
+
let inFlight;
|
|
45
|
+
let timer;
|
|
46
|
+
let closed = false;
|
|
47
|
+
function setStatus(next) {
|
|
48
|
+
if (announced && next === status) return;
|
|
49
|
+
announced = true;
|
|
50
|
+
status = next;
|
|
51
|
+
for (const listener of statusListeners) listener(next);
|
|
52
|
+
}
|
|
53
|
+
function wait() {
|
|
54
|
+
helper = void 0;
|
|
55
|
+
inFlight = void 0;
|
|
56
|
+
setStatus("waiting");
|
|
57
|
+
timer = setTimeout(check, POLL_MS).unref();
|
|
58
|
+
}
|
|
59
|
+
async function check() {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
inFlight = controller;
|
|
62
|
+
const result = await findDiscovery(root).then((found) => found instanceof NoHelper ? found : open(found, controller.signal)).catch((error) => controller.signal.aborted ? new NoHelper() : new NoHelper(`helper request failed: ${error}`));
|
|
63
|
+
if (closed) {
|
|
64
|
+
controller.abort();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (result instanceof NoHelper) {
|
|
68
|
+
if (result.reason) logger.warnOnce(`tippa: ${result.reason}`);
|
|
69
|
+
return wait();
|
|
70
|
+
}
|
|
71
|
+
helper = result.discovery;
|
|
72
|
+
setStatus("connected");
|
|
73
|
+
readReplies(result.stream, emitReply).then(() => {
|
|
74
|
+
if (!closed) wait();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function emitReply(reply) {
|
|
78
|
+
for (const listener of replyListeners) listener(reply);
|
|
79
|
+
}
|
|
80
|
+
check();
|
|
81
|
+
return {
|
|
82
|
+
get status() {
|
|
83
|
+
return status;
|
|
84
|
+
},
|
|
85
|
+
onStatus(listener) {
|
|
86
|
+
statusListeners.add(listener);
|
|
87
|
+
},
|
|
88
|
+
onReply(listener) {
|
|
89
|
+
replyListeners.add(listener);
|
|
90
|
+
},
|
|
91
|
+
async send(pick) {
|
|
92
|
+
const target = helper;
|
|
93
|
+
if (!target) throw new AgentNotConnectedError(LABEL);
|
|
94
|
+
const res = await fetch(helperUrl(target, "/pick"), {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
...auth(target),
|
|
98
|
+
"content-type": "application/json"
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(pick),
|
|
101
|
+
signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
|
|
102
|
+
}).catch((error) => {
|
|
103
|
+
if (isHelperGone(error)) throw new AgentNotConnectedError(LABEL);
|
|
104
|
+
throw error;
|
|
105
|
+
});
|
|
106
|
+
const body = await res.text();
|
|
107
|
+
if (res.status !== 202) throw new Error(`helper refused the pick (${res.status}): ${body}`);
|
|
108
|
+
},
|
|
109
|
+
async close() {
|
|
110
|
+
closed = true;
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
inFlight?.abort();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* checks the port answers as the helper, then opens its reply stream.
|
|
118
|
+
* `signal` aborts both, and stays attached to the stream for close()
|
|
119
|
+
*/
|
|
120
|
+
async function open(found, signal) {
|
|
121
|
+
const health = await fetch(helperUrl(found, "/health"), {
|
|
122
|
+
headers: auth(found),
|
|
123
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(POLL_MS)])
|
|
124
|
+
});
|
|
125
|
+
const healthBody = await health.text();
|
|
126
|
+
if (health.status === 401) return new NoHelper("the helper refused channel.json's secret; is another project's helper on that port?");
|
|
127
|
+
if (health.status === 503) return new NoHelper();
|
|
128
|
+
if (!health.ok || !isHealthy(healthBody)) return new NoHelper(`port ${found.port} from channel.json doesn't answer as the helper; waiting for a fresh one`);
|
|
129
|
+
const headers = new AbortController();
|
|
130
|
+
const headerTimer = setTimeout(() => headers.abort(), POLL_MS);
|
|
131
|
+
const events = await fetch(helperUrl(found, "/events"), {
|
|
132
|
+
headers: auth(found),
|
|
133
|
+
signal: AbortSignal.any([signal, headers.signal])
|
|
134
|
+
}).finally(() => clearTimeout(headerTimer));
|
|
135
|
+
const isStream = events.headers.get("content-type")?.startsWith("text/event-stream");
|
|
136
|
+
if (!events.ok || !isStream || !events.body) {
|
|
137
|
+
await events.body?.cancel();
|
|
138
|
+
return new NoHelper(`port ${found.port} from channel.json doesn't stream as the helper; waiting for a fresh one`);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
discovery: found,
|
|
142
|
+
stream: events.body
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** the nearest discovery file wins; one whose helper process is gone is skipped */
|
|
146
|
+
async function findDiscovery(root) {
|
|
147
|
+
for (let dir = root;; dir = dirname(dir)) {
|
|
148
|
+
const path = discoveryPath(dir);
|
|
149
|
+
let text;
|
|
150
|
+
try {
|
|
151
|
+
text = await readFile(path, "utf8");
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const { code } = error;
|
|
154
|
+
if (code !== "ENOENT") return new NoHelper(`can't read ${path}: ${code}`);
|
|
155
|
+
if (dirname(dir) === dir) return new NoHelper();
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = discoverySchema.parse(JSON.parse(text));
|
|
161
|
+
} catch {
|
|
162
|
+
return new NoHelper(`${path} is not a valid channel.json`);
|
|
163
|
+
}
|
|
164
|
+
if (isAlive(parsed.pid)) return parsed;
|
|
165
|
+
if (dirname(dir) === dir) return new NoHelper();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function isHelperGone(error) {
|
|
169
|
+
const code = error?.cause?.code;
|
|
170
|
+
return code === "ECONNREFUSED" || code === "ECONNRESET";
|
|
171
|
+
}
|
|
172
|
+
function isHealthy(body) {
|
|
173
|
+
try {
|
|
174
|
+
return healthSchema.safeParse(JSON.parse(body)).success;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function helperUrl({ port }, path) {
|
|
180
|
+
return `http://127.0.0.1:${port}${path}`;
|
|
181
|
+
}
|
|
182
|
+
function auth({ secret }) {
|
|
183
|
+
return { "x-tippa-secret": secret };
|
|
184
|
+
}
|
|
185
|
+
/** reads the helper's `data: <json>` events until the stream ends or is aborted */
|
|
186
|
+
async function readReplies(stream, emit) {
|
|
187
|
+
let buffer = "";
|
|
188
|
+
try {
|
|
189
|
+
for await (const chunk of stream.pipeThrough(new TextDecoderStream())) {
|
|
190
|
+
buffer += chunk;
|
|
191
|
+
const events = buffer.split("\n\n");
|
|
192
|
+
buffer = events.pop() ?? "";
|
|
193
|
+
for (const event of events) {
|
|
194
|
+
const reply = parseReply(event.split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).join("\n"));
|
|
195
|
+
if (reply) emit(reply);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
function parseReply(data) {
|
|
201
|
+
try {
|
|
202
|
+
return pickReplySchema.parse(JSON.parse(data));
|
|
203
|
+
} catch {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/plugin.ts
|
|
209
|
+
const NAME = "tippa";
|
|
210
|
+
const ENDPOINT = "/__tippa/pick";
|
|
211
|
+
const FS_PREFIX = "/@fs/";
|
|
212
|
+
const LOADER_ID = "virtual:tippa/client";
|
|
213
|
+
const RESOLVED_LOADER_ID = `\0${LOADER_ID}`;
|
|
214
|
+
const CLIENT_ENTRY = fileURLToPath(new URL("./client/index", import.meta.url));
|
|
215
|
+
function tippa(options) {
|
|
216
|
+
const sessions = /* @__PURE__ */ new WeakMap();
|
|
217
|
+
return {
|
|
218
|
+
name: NAME,
|
|
219
|
+
apply: (_, env) => env.command === "serve" && env.mode !== "test" && !process.env.VITEST,
|
|
220
|
+
applyToEnvironment: (environment) => environment.name === "client",
|
|
221
|
+
configResolved(config) {
|
|
222
|
+
validate(options);
|
|
223
|
+
if (config.experimental.bundledDev) config.logger.warnOnce(`[${NAME}] experimental.bundledDev is on; the tippa client only loads in the default dev mode`);
|
|
224
|
+
},
|
|
225
|
+
configureServer(server) {
|
|
226
|
+
const token = randomBytes(32).toString("hex");
|
|
227
|
+
const { agent } = options;
|
|
228
|
+
const { logger, root } = server.config;
|
|
229
|
+
const client = server.environments.client;
|
|
230
|
+
const hot = client.hot;
|
|
231
|
+
if (isNetworkExposed(server.config.server.host)) logger.warnOnce(`[${NAME}] the dev server is exposed on the network; tippa only accepts picks from this machine`);
|
|
232
|
+
const agentConnection = agent.connect({
|
|
233
|
+
root,
|
|
234
|
+
logger
|
|
235
|
+
});
|
|
236
|
+
sessions.set(client, {
|
|
237
|
+
token,
|
|
238
|
+
connection: agentConnection
|
|
239
|
+
});
|
|
240
|
+
agentConnection.onStatus((status) => {
|
|
241
|
+
logger.info(status === "connected" ? `tippa → connected to ${agent.label}` : `tippa → waiting for ${agent.label}`);
|
|
242
|
+
hot.send(STATUS_EVENT, { status });
|
|
243
|
+
});
|
|
244
|
+
agentConnection.onReply((reply) => hot.send(REPLY_EVENT, reply));
|
|
245
|
+
hot.on(STATUS_REQUEST_EVENT, (_, client) => client.send(STATUS_EVENT, { status: agentConnection.status }));
|
|
246
|
+
server.middlewares.use(pickEndpoint({
|
|
247
|
+
token,
|
|
248
|
+
root,
|
|
249
|
+
environment: client,
|
|
250
|
+
agent: agentConnection,
|
|
251
|
+
logger
|
|
252
|
+
}));
|
|
253
|
+
},
|
|
254
|
+
async buildEnd() {
|
|
255
|
+
await sessions.get(this.environment)?.connection.close();
|
|
256
|
+
sessions.delete(this.environment);
|
|
257
|
+
},
|
|
258
|
+
transform: {
|
|
259
|
+
filter: { id: /\/vite\/dist\/client\/client\.mjs$/ },
|
|
260
|
+
handler(code) {
|
|
261
|
+
return {
|
|
262
|
+
code: `${code}\nimport(${JSON.stringify(LOADER_ID)});\n`,
|
|
263
|
+
map: null
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
resolveId: {
|
|
268
|
+
filter: { id: new RegExp(`^${LOADER_ID}$`) },
|
|
269
|
+
handler: () => RESOLVED_LOADER_ID
|
|
270
|
+
},
|
|
271
|
+
load: {
|
|
272
|
+
filter: { id: new RegExp(`^\0${LOADER_ID}$`) },
|
|
273
|
+
handler() {
|
|
274
|
+
const session = sessions.get(this.environment);
|
|
275
|
+
if (!session) return null;
|
|
276
|
+
const config = {
|
|
277
|
+
token: session.token,
|
|
278
|
+
endpoint: ENDPOINT,
|
|
279
|
+
...options.key !== void 0 && { key: options.key }
|
|
280
|
+
};
|
|
281
|
+
return [
|
|
282
|
+
`import { start } from ${JSON.stringify(CLIENT_ENTRY)};`,
|
|
283
|
+
`start(${JSON.stringify(config)});`,
|
|
284
|
+
`import.meta.hot?.send(${JSON.stringify(STATUS_REQUEST_EVENT)});`
|
|
285
|
+
].join("\n");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/** vite binds `localhost` when `server.host` is unset or false */
|
|
291
|
+
function isNetworkExposed(host) {
|
|
292
|
+
if (host === void 0 || host === false) return false;
|
|
293
|
+
return host === true || !(host === "localhost" || isLoopback(host));
|
|
294
|
+
}
|
|
295
|
+
function validate(options) {
|
|
296
|
+
const { agent, key } = options ?? {};
|
|
297
|
+
if (typeof agent?.connect !== "function") throw new Error(`[${NAME}] options.agent must be an agent adapter, e.g. tippa({ agent: claudeSession() })`);
|
|
298
|
+
if (key !== void 0 && (typeof key !== "string" || key.length === 0)) throw new Error(`[${NAME}] options.key must be a non-empty string, or omitted for react-grab's default`);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* `POST /__tippa/pick` → 202 `{ pickId, status: "sent" }`, or `{ error }` with
|
|
302
|
+
* 403 `forbidden_address`, 403 `forbidden_forwarded`, 403 `forbidden_origin`, 401 `unauthorized`, 405 `method_not_allowed`, 413 `too_large`, 400 `invalid_pick`,
|
|
303
|
+
* 503 `not_connected`, 502 `send_failed`
|
|
304
|
+
*/
|
|
305
|
+
function pickEndpoint({ token, root, environment, agent, logger }) {
|
|
306
|
+
async function handle(req, res) {
|
|
307
|
+
if (!isLoopback(req.socket.remoteAddress)) {
|
|
308
|
+
req.resume();
|
|
309
|
+
return sendJson(res, 403, { error: "forbidden_address" });
|
|
310
|
+
}
|
|
311
|
+
if (FORWARDING_HEADERS.some((header) => header in req.headers)) {
|
|
312
|
+
req.resume();
|
|
313
|
+
return sendJson(res, 403, { error: "forbidden_forwarded" });
|
|
314
|
+
}
|
|
315
|
+
if (!isSameOrigin(req)) {
|
|
316
|
+
req.resume();
|
|
317
|
+
return sendJson(res, 403, { error: "forbidden_origin" });
|
|
318
|
+
}
|
|
319
|
+
if (!hasSecretHeader(req, "x-tippa-token", token)) {
|
|
320
|
+
req.resume();
|
|
321
|
+
return sendJson(res, 401, { error: "unauthorized" });
|
|
322
|
+
}
|
|
323
|
+
if (req.method !== "POST") {
|
|
324
|
+
req.resume();
|
|
325
|
+
return sendJson(res, 405, { error: "method_not_allowed" });
|
|
326
|
+
}
|
|
327
|
+
const body = await readBody(req, MAX_BODY_BYTES);
|
|
328
|
+
if (body === void 0) return sendJson(res, 413, { error: "too_large" });
|
|
329
|
+
let json;
|
|
330
|
+
try {
|
|
331
|
+
json = JSON.parse(body);
|
|
332
|
+
} catch {
|
|
333
|
+
return sendJson(res, 400, {
|
|
334
|
+
error: "invalid_pick",
|
|
335
|
+
message: "body is not valid json"
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
const parsed = pickRequestSchema.safeParse(json);
|
|
339
|
+
if (!parsed.success) return sendJson(res, 400, {
|
|
340
|
+
error: "invalid_pick",
|
|
341
|
+
message: z.prettifyError(parsed.error)
|
|
342
|
+
});
|
|
343
|
+
const elements = [];
|
|
344
|
+
for (const [index, { moduleUrl, ...element }] of parsed.data.elements.entries()) {
|
|
345
|
+
const file = await sourceFilePath(element.file, moduleUrl, {
|
|
346
|
+
host: req.headers.host,
|
|
347
|
+
root,
|
|
348
|
+
environment
|
|
349
|
+
});
|
|
350
|
+
if (file === void 0) return sendJson(res, 400, {
|
|
351
|
+
error: "invalid_pick",
|
|
352
|
+
message: `elements[${index}].moduleUrl is not a module of this dev server`
|
|
353
|
+
});
|
|
354
|
+
elements.push({
|
|
355
|
+
...element,
|
|
356
|
+
file,
|
|
357
|
+
html: element.html.slice(0, MAX_HTML_CHARS)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (agent.status !== "connected") return sendJson(res, 503, { error: "not_connected" });
|
|
361
|
+
try {
|
|
362
|
+
await agent.send({
|
|
363
|
+
...parsed.data,
|
|
364
|
+
elements
|
|
365
|
+
});
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (error instanceof AgentNotConnectedError) return sendJson(res, 503, { error: "not_connected" });
|
|
368
|
+
logger.error(`[${NAME}] sending pick ${parsed.data.pickId} failed: ${error}`);
|
|
369
|
+
return sendJson(res, 502, { error: "send_failed" });
|
|
370
|
+
}
|
|
371
|
+
sendJson(res, 202, {
|
|
372
|
+
pickId: parsed.data.pickId,
|
|
373
|
+
status: "sent"
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return (req, res, next) => {
|
|
377
|
+
if (req.url?.split("?")[0] !== ENDPOINT) return next();
|
|
378
|
+
handle(req, res).catch((error) => {
|
|
379
|
+
if (!isClientAbort(req)) next(error);
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* the absolute path of an element's source: `file` read against the module the page loaded,
|
|
385
|
+
* or as a vite url without one; undefined when `moduleUrl` isn't a module of this dev server
|
|
386
|
+
*/
|
|
387
|
+
async function sourceFilePath(file, moduleUrl, context) {
|
|
388
|
+
if (moduleUrl === void 0) return viteUrlPath(file, context.root);
|
|
389
|
+
const moduleFile = await moduleFilePath(moduleUrl, context);
|
|
390
|
+
return moduleFile && mapSourcePath(file, moduleFile);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* a vite url as a file path: root-relative, or `/@fs/<absolute>` outside the root.
|
|
394
|
+
* whoever reads the pick runs elsewhere, so it gets the file's absolute path
|
|
395
|
+
*/
|
|
396
|
+
function viteUrlPath(url, root) {
|
|
397
|
+
const [path = url] = url.split(/[?#]/);
|
|
398
|
+
return servedPathFile(path, root);
|
|
399
|
+
}
|
|
400
|
+
function servedPathFile(path, root) {
|
|
401
|
+
return path.startsWith(FS_PREFIX) ? path.slice(4) : join(root, path);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* the file behind a module url the page loaded, or undefined for another origin,
|
|
405
|
+
* an undecodable path or one climbing out through an encoded `..`
|
|
406
|
+
*/
|
|
407
|
+
async function moduleFilePath(moduleUrl, { host, root, environment }) {
|
|
408
|
+
let path;
|
|
409
|
+
try {
|
|
410
|
+
const url = new URL(moduleUrl, `http://${host}`);
|
|
411
|
+
if (!(url.host === host && (url.protocol === "http:" || url.protocol === "https:"))) return void 0;
|
|
412
|
+
path = decodeURIComponent(url.pathname);
|
|
413
|
+
} catch {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (path.split("/").includes("..")) return void 0;
|
|
417
|
+
return (await environment.moduleGraph.getModuleByUrl(path).catch(() => void 0))?.file ?? servedPathFile(path, root);
|
|
418
|
+
}
|
|
419
|
+
/** react-grab passes the map's raw `sources` entry, which vite writes relative to the module's dir */
|
|
420
|
+
function mapSourcePath(source, moduleFile) {
|
|
421
|
+
let path = source;
|
|
422
|
+
try {
|
|
423
|
+
path = decodeURIComponent(source);
|
|
424
|
+
} catch {}
|
|
425
|
+
return isAbsolute(path) ? path : resolve(dirname(moduleFile), path);
|
|
426
|
+
}
|
|
427
|
+
const LOOPBACK_ADDRESSES = /* @__PURE__ */ new Set([
|
|
428
|
+
"127.0.0.1",
|
|
429
|
+
"::1",
|
|
430
|
+
"::ffff:127.0.0.1"
|
|
431
|
+
]);
|
|
432
|
+
const FORWARDING_HEADERS = [
|
|
433
|
+
"forwarded",
|
|
434
|
+
"x-forwarded-for",
|
|
435
|
+
"x-real-ip",
|
|
436
|
+
"cf-connecting-ip"
|
|
437
|
+
];
|
|
438
|
+
function isLoopback(address) {
|
|
439
|
+
return address !== void 0 && LOOPBACK_ADDRESSES.has(address);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* vite's default cors admits every localhost origin, so another local page could read
|
|
443
|
+
* the token from the loader; only the page's own origin may post
|
|
444
|
+
*/
|
|
445
|
+
function isSameOrigin(req) {
|
|
446
|
+
const site = req.headers["sec-fetch-site"];
|
|
447
|
+
if (site !== void 0) return site === "same-origin";
|
|
448
|
+
const { origin, host } = req.headers;
|
|
449
|
+
if (origin === void 0 || host === void 0) return false;
|
|
450
|
+
try {
|
|
451
|
+
return new URL(origin).host === host;
|
|
452
|
+
} catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
//#endregion
|
|
457
|
+
export { AgentNotConnectedError, claudeSession, tippa };
|