Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e28d8bb92 | ||
|
|
e63d852847 | ||
|
|
d7e250f860 | ||
|
|
a8a29a963d |
@@ -40,3 +40,8 @@ node_modules/
|
||||
|
||||
# Built documentation
|
||||
/site_build/
|
||||
|
||||
# Vite writes these while bundling vite.config.js and removes them again — unless
|
||||
# the process is killed, which during a hung build is exactly what happens. One
|
||||
# was committed by accident.
|
||||
/web/vite.config.js.timestamp-*.mjs
|
||||
|
||||
@@ -126,14 +126,6 @@ and the manifest ships in this repository. It runs anywhere Docker does as well.
|
||||
|
||||
### With Docker directly
|
||||
|
||||
The package is private, and an anonymous pull fails with `No such image` rather
|
||||
than with a permission error. Log the host's Docker daemon in once, with a GitHub
|
||||
token carrying the `read:packages` scope:
|
||||
|
||||
```bash
|
||||
echo "$GHCR_TOKEN" | docker login ghcr.io -u kristianwind --password-stdin
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run -d --name verdande \
|
||||
-p 8080:8080 \
|
||||
|
||||
+27
-4
@@ -12,17 +12,40 @@ conversation.
|
||||
|
||||
## Connecting it
|
||||
|
||||
**Make a token.** **Settings → API tokens → New token**. It is shown once.
|
||||
**Make a token** under **Settings → API tokens**. The page then shows the
|
||||
connector address to go with it — token and all — which is the thing to copy:
|
||||
|
||||
**Add the connector.** In Claude, add a custom connector with:
|
||||
```
|
||||
https://todo.example.dk/mcp?key=vrd_…
|
||||
```
|
||||
|
||||
- **URL**: `https://todo.example.dk/api/v1/mcp`
|
||||
- **Authentication**: Bearer token, using the token you just made
|
||||
**Add the connector** in Claude with that address, and nothing else. The dialog
|
||||
asks for a name and a URL; leave the OAuth fields empty.
|
||||
|
||||
!!! note "Why the key is in the address"
|
||||
The dialog has no field for a bearer token — only OAuth client credentials —
|
||||
so a header cannot be configured from it. The address *is* the credential
|
||||
here, exactly as it is for the [calendar feed](caldav.md#subscribing-to-a-feed).
|
||||
Treat it like a password: anyone holding it reaches what you reach. The
|
||||
server logs request paths without their query strings, so it does not end up
|
||||
in the log.
|
||||
|
||||
!!! warning "It has to be reachable from the internet"
|
||||
Claude connects from Anthropic's side, not from your browser. An instance only
|
||||
reachable on your LAN or over a VPN cannot be used this way.
|
||||
|
||||
### With a header instead
|
||||
|
||||
Anything that *can* send a header should, and use the API path:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer vrd_…" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
|
||||
https://todo.example.dk/api/v1/mcp
|
||||
```
|
||||
|
||||
Both endpoints run the same server and reach the same things.
|
||||
|
||||
## What Claude can do
|
||||
|
||||
| Tool | What it does |
|
||||
|
||||
@@ -1787,6 +1787,53 @@ paths:
|
||||
responses:
|
||||
"302": { description: "Back into the app, with the outcome as a query parameter." }
|
||||
|
||||
/mcp:
|
||||
post:
|
||||
tags: [integrations]
|
||||
summary: The MCP endpoint, with the token in the query string.
|
||||
description: |
|
||||
The same server as `/api/v1/mcp`, for a client that can only be given a
|
||||
URL. Claude's custom-connector dialog asks for a name, an address and
|
||||
OAuth client credentials — there is no field for a bearer token — so the
|
||||
API path cannot be configured from it.
|
||||
|
||||
The key is the only credential accepted here. A session cookie is
|
||||
ignored: this route sits outside the CSRF check, and honouring a cookie
|
||||
would make it a cross-site request that acts as whoever is signed in.
|
||||
security: []
|
||||
parameters:
|
||||
- name: key
|
||||
in: query
|
||||
required: true
|
||||
schema: { type: string, example: vrd_… }
|
||||
description: A personal API token. Treat the whole address as a password.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [jsonrpc, method]
|
||||
properties:
|
||||
jsonrpc: { type: string, const: "2.0" }
|
||||
id: { oneOf: [{ type: string }, { type: integer }] }
|
||||
method: { type: string }
|
||||
params: { type: object }
|
||||
responses:
|
||||
"200":
|
||||
description: A JSON-RPC response.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jsonrpc: { type: string, const: "2.0" }
|
||||
id: { oneOf: [{ type: string }, { type: integer }] }
|
||||
result: { type: object }
|
||||
error: { type: object }
|
||||
"202": { description: A notification. No body. }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
|
||||
/.well-known/caldav:
|
||||
get:
|
||||
tags: [integrations]
|
||||
|
||||
+46
-16
@@ -3,33 +3,53 @@
|
||||
verdande ships with a Rune manifest for
|
||||
[Yggdrasil Panel](https://github.com/kristianwind/yggdrasil).
|
||||
|
||||
## Before you start: the registry is private
|
||||
|
||||
The image lives at `ghcr.io/kristianwind/verdande` and the package is **private**,
|
||||
so an anonymous pull fails with a message that reads as though the image does not
|
||||
exist at all:
|
||||
## If the image will not pull
|
||||
|
||||
```
|
||||
create container: Error response from daemon:
|
||||
No such image: ghcr.io/kristianwind/verdande:latest
|
||||
```
|
||||
|
||||
That is a permission error wearing the wrong hat. The Docker daemon **on the
|
||||
Yggdrasil host** has to be logged in once:
|
||||
This message is a liar. It does not mean the tag is missing from the registry —
|
||||
it means the image is not on the host, and the panel's attempt to fetch it failed
|
||||
without saying so.
|
||||
|
||||
The usual cause is a **private** GHCR package. Yggdrasil pulls anonymously: its
|
||||
`PullImage` calls the Docker SDK with no `RegistryAuth`, so a private image comes
|
||||
back 401. The pull error is discarded, and `ContainerCreate` — which only ever
|
||||
looks locally — then reports the image as missing.
|
||||
|
||||
!!! warning "`docker login` on the host does not help"
|
||||
`docker login` is client-side. The daemon stores no credentials; the CLI
|
||||
sends them itself with each request. Yggdrasil is a different client and
|
||||
sends none, so logging in changes nothing for the panel.
|
||||
|
||||
**Make the package public.** Under
|
||||
[Packages → verdande → Package settings](https://github.com/users/kristianwind/packages/container/package/verdande)
|
||||
→ *Change visibility*. The repository stays private; only the built image becomes
|
||||
readable, which is what the manifest and these pages have always assumed.
|
||||
|
||||
Once it is public, press **Start** on the server you already have. There is no
|
||||
need to delete it and make another: the image reference is read from the rune at
|
||||
container-create time, and both Start and Restart recreate the container.
|
||||
|
||||
To keep the package private you have to put the image on the host yourself,
|
||||
before creating the server, and again after every release:
|
||||
|
||||
```bash
|
||||
echo "$GHCR_TOKEN" | docker login ghcr.io -u kristianwind --password-stdin
|
||||
sudo docker login ghcr.io -u kristianwind
|
||||
sudo docker pull ghcr.io/kristianwind/verdande:latest
|
||||
```
|
||||
|
||||
`GHCR_TOKEN` is a GitHub personal access token with the **`read:packages`** scope
|
||||
and nothing else. The login is stored in `~/.docker/config.json` for whichever
|
||||
user runs the daemon — if Yggdrasil runs as root or through systemd, log in as
|
||||
that user, not as yourself.
|
||||
Run it as the user the Docker daemon runs as — `sudo` if the panel does. The
|
||||
panel's own pull still fails silently; create simply finds the image already
|
||||
there.
|
||||
|
||||
!!! tip "Or make the package public"
|
||||
Under **Packages → verdande → Package settings → Change visibility**. The
|
||||
repository can stay private; only the built image becomes readable. Then no
|
||||
login is needed anywhere and this section stops applying.
|
||||
!!! danger "This hides updates"
|
||||
The panel re-pulls on every recreate, fails quietly, and carries on with the
|
||||
local image. A new release then *looks* like it rolled out and did not. For
|
||||
something you intend to update, public is the right answer and host-pull is
|
||||
the stopgap.
|
||||
|
||||
## Installing
|
||||
|
||||
@@ -40,6 +60,16 @@ that user, not as yourself.
|
||||
3. Fill in the settings below.
|
||||
4. Start it, open the URL, create the first account.
|
||||
|
||||
!!! note "Updating the rune is a manual step"
|
||||
A rune does not update itself when this manifest changes. The panel writes
|
||||
the rune row from a UI action only — neither a restart nor a timer does it —
|
||||
so pick up a new manifest with **Runes → verdande → Update**, or install it
|
||||
again from Browse GitHub.
|
||||
|
||||
The catalogue list is cached in memory for ten minutes, so two attempts in a
|
||||
row can both hand you the old version. Recent panel versions show the list's
|
||||
age with a *fetch again* link.
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | What to put |
|
||||
|
||||
@@ -5,15 +5,12 @@ cache, no queue and no second process.
|
||||
|
||||
## Docker
|
||||
|
||||
!!! warning "Log in to the registry first"
|
||||
The package is private, and an anonymous pull fails with `No such image`
|
||||
rather than with a permission error. Once per host:
|
||||
|
||||
```bash
|
||||
echo "$GHCR_TOKEN" | docker login ghcr.io -u kristianwind --password-stdin
|
||||
```
|
||||
|
||||
`GHCR_TOKEN` is a GitHub token with the `read:packages` scope.
|
||||
!!! note "If the pull is refused"
|
||||
The GHCR package is public, so this needs no credentials. If it is ever set
|
||||
back to private, `docker login ghcr.io` with a token carrying the
|
||||
`read:packages` scope before pulling — and see
|
||||
[the Rune page](rune.md#if-the-image-will-not-pull), because a private image
|
||||
fails differently and much more confusingly under Yggdrasil.
|
||||
|
||||
```bash
|
||||
docker run -d --name verdande \
|
||||
|
||||
@@ -771,3 +771,112 @@ func TestUpdateNoticeIsForAdministratorsOnly(t *testing.T) {
|
||||
t.Error("the current version is hidden from members")
|
||||
}
|
||||
}
|
||||
|
||||
// --- MCP with the key in the URL --------------------------------------------------
|
||||
|
||||
// Claude's custom-connector dialog takes a URL and nothing else — no bearer
|
||||
// token — so this is the only shape that can be configured from it.
|
||||
func TestMCPAcceptsAKeyInTheQuery(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
var userID string
|
||||
err := ts.db.QueryRowContext(t.Context(), `SELECT id FROM users LIMIT 1`).Scan(&userID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := ts.apiToken(t, userID)
|
||||
|
||||
body := `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`
|
||||
resp, err := http.Post(ts.URL+"/mcp?key="+key, "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
||||
t.Fatalf("the response was not JSON-RPC: %v", err)
|
||||
}
|
||||
result, _ := decoded["result"].(map[string]any)
|
||||
if result == nil || result["protocolVersion"] == nil {
|
||||
t.Errorf("no protocol version was announced: %v", decoded)
|
||||
}
|
||||
}
|
||||
|
||||
// The endpoint that broke it: /mcp used not to exist, so it fell through to the
|
||||
// SPA fallback and answered 200 with the app shell. A connector pointed at it
|
||||
// reported success and then failed to parse a page of HTML.
|
||||
func TestMCPWithoutAKeyIsNotTheAppShell(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
for _, url := range []string{ts.URL + "/mcp", ts.URL + "/mcp?key=not-a-verdande-token"} {
|
||||
resp, err := http.Post(url, "application/json", strings.NewReader(`{"jsonrpc":"2.0","id":1}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("%s: status %d, want 401", url, resp.StatusCode)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
|
||||
t.Errorf("%s: content-type %q — a connector cannot read that", url, ct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A revoked key stops working, like any other.
|
||||
func TestMCPKeyStopsWorkingWhenRevoked(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
var userID string
|
||||
if err := ts.db.QueryRowContext(t.Context(), `SELECT id FROM users LIMIT 1`).Scan(&userID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := ts.apiToken(t, userID)
|
||||
|
||||
var id string
|
||||
if err := ts.db.QueryRowContext(t.Context(), `SELECT id FROM api_tokens LIMIT 1`).Scan(&id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ts.db.DeleteAPIToken(t.Context(), userID, id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(ts.URL+"/mcp?key="+key, "application/json",
|
||||
strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("a revoked key still works: status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// The session cookie must NOT work here. This route sits outside the CSRF check,
|
||||
// so a cookie-authenticated POST would be a cross-site request that acts as
|
||||
// whoever is signed in — the exact hole the key-only rule exists to close.
|
||||
func TestMCPWithKeyIgnoresTheSessionCookie(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
// ts.client holds the session from bootstrap. No key in the URL.
|
||||
resp, err := ts.client.Post(ts.URL+"/mcp", "application/json",
|
||||
strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("a signed-in cookie authenticated a POST outside the CSRF check: status %d",
|
||||
resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package httpapi
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kristianwind/verdande/internal/auth"
|
||||
"github.com/kristianwind/verdande/internal/mcp"
|
||||
"github.com/kristianwind/verdande/internal/quickadd"
|
||||
"github.com/kristianwind/verdande/internal/store"
|
||||
@@ -124,6 +126,42 @@ func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// handleMCPWithKey is the same endpoint for a client that can only be given a
|
||||
// URL: `POST /mcp?key=vrd_…`.
|
||||
//
|
||||
// Claude's custom-connector dialog asks for a name, an address and — under
|
||||
// advanced settings — an OAuth client id and secret. There is no field for a
|
||||
// bearer token, so /api/v1/mcp cannot be configured from it however correct the
|
||||
// address is. The calendar feed already carries its credential in the URL for
|
||||
// the same reason, and the request logger records the path without the query, so
|
||||
// the key does not end up in the log.
|
||||
//
|
||||
// The key is the *only* thing accepted here. Reading a session cookie instead
|
||||
// would make this a state-changing POST outside the CSRF check — which is to say
|
||||
// a cross-site request that acts as whoever is signed in.
|
||||
func (s *Server) handleMCPWithKey(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.URL.Query().Get("key")
|
||||
if key == "" || !auth.IsAPIToken(key) {
|
||||
// 401 rather than 404: unlike a task, the existence of this endpoint is
|
||||
// not a secret — it is written in the documentation.
|
||||
writeError(w, http.StatusUnauthorized, CodeUnauthorized,
|
||||
"add ?key=<api token> to the URL")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.db.UserByAPIToken(r.Context(), key)
|
||||
if err != nil {
|
||||
if !errors.Is(err, store.ErrNotFound) {
|
||||
s.internal(w, "mcp key", err)
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusUnauthorized, CodeUnauthorized, "that key is not valid")
|
||||
return
|
||||
}
|
||||
|
||||
s.handleMCP(w, r.WithContext(withUser(r.Context(), user)))
|
||||
}
|
||||
|
||||
// --- tool implementations ------------------------------------------------------------
|
||||
|
||||
func (s *Server) mcpListProjects(ctx context.Context, userID string, args json.RawMessage) (any, error) {
|
||||
|
||||
@@ -90,6 +90,18 @@ func New(cfg *config.Config, db *store.DB, log *slog.Logger, web fs.FS) *Server
|
||||
// authenticated by the token in the recipient address.
|
||||
r.Post("/inbound/mail", s.handleInboundMail)
|
||||
|
||||
// MCP with the token in the query string, for clients that cannot send a
|
||||
// header. Claude's custom-connector dialog takes a URL and nothing else, so
|
||||
// /api/v1/mcp — which wants Authorization: Bearer — cannot be configured from
|
||||
// it at all. Same reasoning as the calendar feed above: a client that cannot
|
||||
// be told to send a header means the token in the URL is the credential.
|
||||
//
|
||||
// Outside /api/v1 deliberately, because it must not be reachable with a
|
||||
// session cookie. A cookie-authenticated POST with no CSRF check is a
|
||||
// cross-site request waiting to happen; handleMCPWithKey accepts the key and
|
||||
// nothing else.
|
||||
r.Post("/mcp", s.handleMCPWithKey)
|
||||
|
||||
// Google sends the browser here after consent. Outside /api/v1 because it is a
|
||||
// top-level navigation rather than a fetch, but still behind the session — the
|
||||
// tokens it exchanges belong to whoever is signed in.
|
||||
|
||||
@@ -179,3 +179,21 @@ test('manifestet og service workeren findes, og ikonerne findes også', async ({
|
||||
expect(worker.ok()).toBeTruthy();
|
||||
expect(await worker.text()).toContain("addEventListener('push'");
|
||||
});
|
||||
|
||||
/**
|
||||
* The MCP connector address must not fall through to the app shell.
|
||||
*
|
||||
* It did: `/mcp` was not a route, so the SPA fallback answered 200 with a page
|
||||
* of HTML. A connector pointed at it reported a successful connection and then
|
||||
* failed to parse the response — which looks like a broken client rather than a
|
||||
* missing route. A 401 in JSON is the honest answer.
|
||||
*/
|
||||
test('MCP-adressen svarer JSON, ikke app-skallen', async ({ request }) => {
|
||||
const response = await request.post('/mcp', {
|
||||
data: { jsonrpc: '2.0', id: 1, method: 'initialize' }
|
||||
});
|
||||
|
||||
expect(response.status(), 'uden nøgle skal den afvise, ikke servere siden').toBe(401);
|
||||
expect(response.headers()['content-type']).toContain('application/json');
|
||||
expect((await response.json()).code).toBe('unauthorized');
|
||||
});
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
/** The plaintext of the token just minted. Held only in this variable. */
|
||||
let fresh = $state(null);
|
||||
let copied = $state(false);
|
||||
let connectorCopied = $state(false);
|
||||
|
||||
/** What Claude's connector dialog needs, which is a URL and nothing else. */
|
||||
let connectorURL = $derived(fresh ? `${location.origin}/mcp?key=${fresh.token}` : '');
|
||||
|
||||
$effect(() => {
|
||||
api
|
||||
@@ -59,6 +63,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function copyConnector() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(connectorURL);
|
||||
connectorCopied = true;
|
||||
} catch {
|
||||
app.toast('Kunne ikke kopiere — markér teksten i stedet.');
|
||||
}
|
||||
}
|
||||
|
||||
async function revoke(token) {
|
||||
if (!confirm(`Tilbagekald "${token.name}"? Alt, der bruger den, holder op med at virke.`))
|
||||
return;
|
||||
@@ -100,6 +113,22 @@
|
||||
{#if copied}<span class="saved">Kopieret.</span>{/if}
|
||||
<button class="secondary" onclick={() => (fresh = null)}>Færdig</button>
|
||||
</div>
|
||||
|
||||
<!-- Shown here and nowhere else, because it contains the token, and the
|
||||
token exists only in this moment. Claude's connector dialog takes a
|
||||
URL and nothing else — there is no field for a header — so this is
|
||||
the form it has to be in. -->
|
||||
<div class="also">
|
||||
<p class="hint">
|
||||
Skal du bruge den som <a href="/indstillinger/integrationer">MCP-forbindelse</a>
|
||||
i Claude? Så er det denne adresse, dialogen skal have:
|
||||
</p>
|
||||
<p class="mono value">{connectorURL}</p>
|
||||
<div class="row">
|
||||
<button class="secondary" onclick={copyConnector}>Kopiér adressen</button>
|
||||
{#if connectorCopied}<span class="saved">Kopieret.</span>{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -194,6 +223,18 @@
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.also {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s2);
|
||||
padding-top: var(--s3);
|
||||
border-top: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.also a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.what {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// vite.config.js
|
||||
import { sveltekit } from "file:///Users/kw/Documents/Code/ToDoApp/web/node_modules/@sveltejs/kit/src/exports/vite/index.js";
|
||||
var vite_config_default = {
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
port: 5173,
|
||||
// In development the app runs on 5173 and the API on 8080. Proxying rather
|
||||
// than pointing fetch at another origin keeps the session cookie
|
||||
// first-party, which is what production looks like — otherwise every
|
||||
// cookie and CSRF behaviour would differ between dev and the real thing.
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: false,
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMva3cvRG9jdW1lbnRzL0NvZGUvVG9Eb0FwcC93ZWJcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9rdy9Eb2N1bWVudHMvQ29kZS9Ub0RvQXBwL3dlYi92aXRlLmNvbmZpZy5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMva3cvRG9jdW1lbnRzL0NvZGUvVG9Eb0FwcC93ZWIvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBzdmVsdGVraXQgfSBmcm9tICdAc3ZlbHRlanMva2l0L3ZpdGUnO1xuXG5leHBvcnQgZGVmYXVsdCB7XG5cdHBsdWdpbnM6IFtzdmVsdGVraXQoKV0sXG5cdHNlcnZlcjoge1xuXHRcdHBvcnQ6IDUxNzMsXG5cdFx0Ly8gSW4gZGV2ZWxvcG1lbnQgdGhlIGFwcCBydW5zIG9uIDUxNzMgYW5kIHRoZSBBUEkgb24gODA4MC4gUHJveHlpbmcgcmF0aGVyXG5cdFx0Ly8gdGhhbiBwb2ludGluZyBmZXRjaCBhdCBhbm90aGVyIG9yaWdpbiBrZWVwcyB0aGUgc2Vzc2lvbiBjb29raWVcblx0XHQvLyBmaXJzdC1wYXJ0eSwgd2hpY2ggaXMgd2hhdCBwcm9kdWN0aW9uIGxvb2tzIGxpa2UgXHUyMDE0IG90aGVyd2lzZSBldmVyeVxuXHRcdC8vIGNvb2tpZSBhbmQgQ1NSRiBiZWhhdmlvdXIgd291bGQgZGlmZmVyIGJldHdlZW4gZGV2IGFuZCB0aGUgcmVhbCB0aGluZy5cblx0XHRwcm94eToge1xuXHRcdFx0Jy9hcGknOiB7XG5cdFx0XHRcdHRhcmdldDogJ2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MCcsXG5cdFx0XHRcdGNoYW5nZU9yaWdpbjogZmFsc2UsXG5cdFx0XHRcdHdzOiB0cnVlXG5cdFx0XHR9XG5cdFx0fVxuXHR9XG59O1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE4UixTQUFTLGlCQUFpQjtBQUV4VCxJQUFPLHNCQUFRO0FBQUEsRUFDZCxTQUFTLENBQUMsVUFBVSxDQUFDO0FBQUEsRUFDckIsUUFBUTtBQUFBLElBQ1AsTUFBTTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUEsSUFLTixPQUFPO0FBQUEsTUFDTixRQUFRO0FBQUEsUUFDUCxRQUFRO0FBQUEsUUFDUixjQUFjO0FBQUEsUUFDZCxJQUFJO0FBQUEsTUFDTDtBQUFBLElBQ0Q7QUFBQSxFQUNEO0FBQ0Q7IiwKICAibmFtZXMiOiBbXQp9Cg==
|
||||
Reference in New Issue
Block a user