mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-22 18:22:04 +00:00
partial fixes for the API - origin was wrong
This commit is contained in:
1 parent
92e6f438c3
commit
ef43ab679d
9 files changed
+391
-21
No files matched your search
@@ -33,6 +33,11 @@ Only after the real frontend socket is bound does it start Tauri with that exact
|
||||
through native Tauri IPC and needs no separate HTTP port. Configuration overrides
|
||||
are passed in memory, without changing the checked-in Tauri config.
|
||||
|
||||
The native HTTP bridge normalizes loopback development origins for first-party
|
||||
API requests so an automatically selected port does not break login or media
|
||||
loading. See the [desktop session regression checks](../docs/desktop-session-regression.md)
|
||||
when changing ports, the launcher, or authentication.
|
||||
|
||||
JavaScript, TypeScript, React, and CSS changes use Vite hot reload; compatible
|
||||
React component edits use Fast Refresh. Changes that cannot be hot-replaced
|
||||
reload the page. Rust changes use `cargo tauri dev`'s automatic **rebuild and app
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
use http::{header, HeaderMap, HeaderValue};
|
||||
use url::{Host, Url};
|
||||
|
||||
const DESKTOP_ORIGIN: &str = "https://studio.storyteller.ai";
|
||||
|
||||
/// The native HTTP bridge shares the desktop cookie jar. Its API identity must
|
||||
/// not depend on which loopback port Vite happened to bind for this launch.
|
||||
pub(crate) fn normalize_artcraft_origin(destination: &Url, headers: &mut HeaderMap) {
|
||||
let origin = headers.get(header::ORIGIN);
|
||||
// Preserve the existing packaged-app handling on macOS and Windows.
|
||||
let packaged_origin = origin.is_none()
|
||||
|| matches!(
|
||||
origin.and_then(|value| value.to_str().ok()),
|
||||
Some("null" | "http://tauri.localhost" | "tauri://localhost")
|
||||
);
|
||||
// Only first-party API requests get the development-origin replacement.
|
||||
// Leave third-party requests and ordinary website origins untouched.
|
||||
let development_origin = destination.scheme() == "https"
|
||||
&& matches!(destination.host_str(), Some("api.storyteller.ai" | "api.fakeyou.com"))
|
||||
&& origin.and_then(|value| value.to_str().ok()).is_some_and(is_loopback_origin);
|
||||
|
||||
if packaged_origin || development_origin {
|
||||
// HeaderMap is a multimap; insert replaces all existing Origin values.
|
||||
headers.insert(header::ORIGIN, HeaderValue::from_static(DESKTOP_ORIGIN));
|
||||
}
|
||||
}
|
||||
|
||||
fn is_loopback_origin(origin: &str) -> bool {
|
||||
let Ok(url) = Url::parse(origin) else {
|
||||
return false;
|
||||
};
|
||||
if !matches!(url.scheme(), "http" | "https") {
|
||||
return false;
|
||||
}
|
||||
match url.host() {
|
||||
Some(Host::Domain("localhost")) => true,
|
||||
Some(Host::Ipv4(address)) => address.is_loopback(),
|
||||
Some(Host::Ipv6(address)) => address.is_loopback(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const API: &str = "https://api.storyteller.ai/v1/session";
|
||||
|
||||
mod development {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn normalizes_dynamic_ports_and_loopback_hosts() {
|
||||
for origin in [
|
||||
"http://127.0.0.1:5193",
|
||||
"http://127.0.0.1:5194",
|
||||
"http://127.0.0.1:65535",
|
||||
"http://localhost:5173",
|
||||
"http://[::1]:5193",
|
||||
] {
|
||||
assert_eq!(normalized(API, Some(origin)), DESKTOP_ORIGIN);
|
||||
}
|
||||
assert_eq!(
|
||||
normalized("https://api.fakeyou.com/v1/session", Some("http://127.0.0.1:5193")),
|
||||
DESKTOP_ORIGIN
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_other_destinations_and_non_loopback_origins_unchanged() {
|
||||
for destination in [
|
||||
"https://example.com/v1/session",
|
||||
"https://api.storyteller.ai.example.com/v1/session",
|
||||
"http://api.storyteller.ai/v1/session",
|
||||
"http://localhost:12345/v1/session",
|
||||
] {
|
||||
assert_eq!(normalized(destination, Some("http://127.0.0.1:5193")), "http://127.0.0.1:5193");
|
||||
}
|
||||
for origin in [
|
||||
"https://app.getartcraft.com",
|
||||
"https://example.com",
|
||||
"http://localhost.example.com:5193",
|
||||
"http://192.168.1.2:5193",
|
||||
"not a URL",
|
||||
"",
|
||||
] {
|
||||
assert_eq!(normalized(API, Some(origin)), origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod packaged {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn preserves_existing_desktop_origins() {
|
||||
for origin in [None, Some("null"), Some("http://tauri.localhost"), Some("tauri://localhost")]
|
||||
{
|
||||
assert_eq!(normalized(API, origin), DESKTOP_ORIGIN);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replaces_duplicate_origin_values_without_changing_session_headers() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.append(header::ORIGIN, HeaderValue::from_static("http://127.0.0.1:5193"));
|
||||
headers.append(header::ORIGIN, HeaderValue::from_static("http://127.0.0.1:5193"));
|
||||
headers.insert(header::COOKIE, HeaderValue::from_static("session=fixture"));
|
||||
normalize_artcraft_origin(&Url::parse(API).unwrap(), &mut headers);
|
||||
assert_eq!(headers.get_all(header::ORIGIN).iter().count(), 1);
|
||||
assert_eq!(headers[header::ORIGIN], DESKTOP_ORIGIN);
|
||||
assert_eq!(headers[header::COOKIE], "session=fixture");
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized(destination: &str, origin: Option<&str>) -> String {
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Some(origin) = origin {
|
||||
headers.insert(header::ORIGIN, HeaderValue::from_str(origin).unwrap());
|
||||
}
|
||||
normalize_artcraft_origin(&Url::parse(destination).unwrap(), &mut headers);
|
||||
headers[header::ORIGIN].to_str().unwrap().to_owned()
|
||||
}
|
||||
}
|
||||
+3
-21
@@ -16,6 +16,7 @@ use tauri::{
|
||||
use tokio::sync::oneshot::{channel, Receiver, Sender};
|
||||
|
||||
use crate::{
|
||||
artcraft_origin::normalize_artcraft_origin,
|
||||
scope::{Entry, Scope},
|
||||
Error, Http, Result,
|
||||
};
|
||||
@@ -269,7 +270,7 @@ pub async fn fetch<R: Runtime>(
|
||||
builder = builder.cookie_provider(state.cookies_jar.clone());
|
||||
}
|
||||
|
||||
let mut request = builder.build()?.request(method.clone(), url);
|
||||
let mut request = builder.build()?.request(method.clone(), url.clone());
|
||||
|
||||
// POST and PUT requests should always have a 0 length content-length,
|
||||
// if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
|
||||
@@ -305,26 +306,7 @@ pub async fn fetch<R: Runtime>(
|
||||
headers.remove(header::ORIGIN);
|
||||
};
|
||||
|
||||
// NB(bt): In production builds, the origin seems to be "null". This is what our server CORS filter sees:
|
||||
// [2025-05-01T23:42:35Z WARN actix_cors_configs::configs::fakeyou] Invalid origin: "null"
|
||||
// [2025-05-01T23:42:35Z WARN actix_cors_configs::util::netlify_branch_domain_matches] Invalid origin: "null"
|
||||
// [2025-05-01T23:42:35Z WARN actix_cors_configs::util::netlify_branch_domain_matches] Invalid origin: "null"
|
||||
// ...
|
||||
// [2025-05-01T23:42:35Z WARN actix_cors_configs::util::netlify_branch_domain_matches] Invalid origin: "null"
|
||||
// [2025-05-01T23:42:35Z INFO actix_web::middleware::logger] [storyteller-web-85f9b4974-ddvq7] IP=[redact]
|
||||
// 939322e1095ee5e4-IAD "POST /v1/login HTTP/1.1" 400 42 "-" "tauri-plugin-http/2.4.3" 0.000076
|
||||
let mut maybe_origin_header = headers.get(header::ORIGIN);
|
||||
|
||||
let replace_origin_header = maybe_origin_header.is_none()
|
||||
|| maybe_origin_header == Some(&HeaderValue::from_static("null")) // Above error (Mac)
|
||||
|| maybe_origin_header == Some(&HeaderValue::from_static("http://tauri.localhost")) // Windows
|
||||
|| maybe_origin_header == Some(&HeaderValue::from_static("tauri://localhost")); // Mac plausibly causes this
|
||||
|
||||
if replace_origin_header {
|
||||
// TODO(bt): Make this a sentinel origin, eg. sentinel.storyteller.ai.
|
||||
// NB: HeadersMap is a multimap, but this should ensure we just have one value.
|
||||
headers.insert(header::ORIGIN, HeaderValue::from_static("https://studio.storyteller.ai"));
|
||||
}
|
||||
normalize_artcraft_origin(&url, &mut headers);
|
||||
|
||||
if let Some(data) = data {
|
||||
request = request.body(data);
|
||||
|
||||
+2
@@ -12,6 +12,8 @@ use tauri::{
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
mod artcraft_origin;
|
||||
|
||||
// NB(bt): We're making everything public so we can access it.
|
||||
pub mod commands;
|
||||
pub mod error;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# Desktop session regression — September 27, 2026
|
||||
|
||||
The combined development launcher (`92e6f438c3`, following the performance
|
||||
change `df707bf8e1`) moved Vite from `localhost:5173` to an available port on
|
||||
`127.0.0.1`. The vendored HTTP plugin forwarded that webview Origin to the
|
||||
production API. The API rejected it with **400: Origin is not allowed to make
|
||||
this request**.
|
||||
|
||||
The native login bridge and credits commands use a different HTTP transport and
|
||||
continued working. Consequently the login modal correctly recognized the native
|
||||
session, but Accounts, Library, and page media requests failed. Accounts treated
|
||||
the error as logged out; its Log In button rechecked the valid native session
|
||||
and immediately closed the login modal again.
|
||||
|
||||
The fix extends the existing packaged-desktop Origin normalization to loopback
|
||||
development origins, at any port, for HTTPS requests to `api.storyteller.ai` and
|
||||
`api.fakeyou.com`. Other development request destinations and non-loopback
|
||||
website origins retain their headers. Cookie handling and API authorization are
|
||||
unchanged. A frontend reload alone cannot update this fix: Rust must rebuild.
|
||||
|
||||
## Observed native results
|
||||
|
||||
Checked in the real macOS Tauri dev app at `http://127.0.0.1:5193`, using its
|
||||
existing session. Temporary probes were removed after verification. No cookies,
|
||||
tokens, passwords, account identifiers, or media contents are recorded here.
|
||||
|
||||
| Check | Before | After |
|
||||
|--------------------------------|---------------------------------------|----------------------------|
|
||||
| Frontend HTTP `/v1/session` | 400, Origin rejected | 200 |
|
||||
| Shared `UsersApi.GetSession()` | `success: false` | Success, logged in |
|
||||
| Native vs. frontend identity | Frontend could not resolve an account | Same account |
|
||||
| Shared media API, page size 1 | Not measured separately | Success, one item returned |
|
||||
|
||||
## Automated checks
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
cargo test --offline -p tauri-plugin-http artcraft_origin --lib
|
||||
./script/artcraft/unix_dev.sh
|
||||
```
|
||||
|
||||
In a second terminal, substitute the URL printed by the launcher:
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm run test:desktop-session -- http://127.0.0.1:5193
|
||||
```
|
||||
|
||||
The Rust tests cover the default port, alternate/collision ports, IPv4, IPv6,
|
||||
localhost, packaged origins, duplicate headers, session-header preservation,
|
||||
and destinations/origins that must remain unchanged.
|
||||
|
||||
The browser check uses the real frontend with an isolated Chrome profile and
|
||||
mocked IPC. It verifies matching credits and Accounts state, a visible Library
|
||||
image, media after opening the deferred image page, video/audio page rendering,
|
||||
and logout followed by sign-in. It blocks remote browser traffic and never uses
|
||||
your real account or native credentials. It requires installed Google Chrome.
|
||||
It can also run against a locally served production frontend build.
|
||||
|
||||
These browser mocks cannot validate native transport or server CORS. The
|
||||
performance fixture deliberately rejects HTTP requests and is **not** an
|
||||
authentication acceptance test. Run the native check below when changing the
|
||||
launcher, HTTP bridge, or authentication plumbing.
|
||||
|
||||
Validation on September 27: all 12 HTTP-plugin tests, 20 login-modal tests, and
|
||||
10 app tests passed. The browser check passed against both the Vite dev server
|
||||
and a fresh production build. To repeat the production check, serve a build in
|
||||
one terminal, then pass its printed URL to `test:desktop-session` in another:
|
||||
|
||||
```sh
|
||||
cd frontend/apps/artcraft
|
||||
npm run build -- --outDir /tmp/artcraft-session-build
|
||||
npm run preview -- --outDir /tmp/artcraft-session-build --host 127.0.0.1 --port 6215
|
||||
```
|
||||
|
||||
## Repeat the real native check
|
||||
|
||||
1. Start the combined launcher, with an existing signed-in test session. Verify
|
||||
Accounts displays that account, My Library loads its media, and Create Image,
|
||||
Create Video, and Create Audio load their content.
|
||||
2. Right-click inside the desktop webview, choose **Inspect Element**, and open
|
||||
**Console**. Run this read-only probe. It prints statuses/counts, not credentials
|
||||
or account/media contents:
|
||||
|
||||
```js
|
||||
void (async () => {
|
||||
const invoke = window.__TAURI_INTERNALS__.invoke;
|
||||
const host = (await invoke("get_app_info_command")).payload.storyteller_host;
|
||||
const options = { method: "GET", credentials: "include" };
|
||||
const started = performance.now();
|
||||
const [native, response] = await Promise.all([
|
||||
invoke("storyteller_get_login_session_command"),
|
||||
window.FetchProxy(`${host}/v1/session`, { ...options }),
|
||||
]);
|
||||
if (!response.ok) {
|
||||
console.log({ origin: location.origin, sessionStatus: response.status });
|
||||
return;
|
||||
}
|
||||
const session = await response.json();
|
||||
const result = {
|
||||
origin: location.origin,
|
||||
sessionStatus: response.status,
|
||||
nativeLoggedIn: !!native,
|
||||
frontendLoggedIn: session.logged_in,
|
||||
sameAccount: native?.user_token === session.user?.user_token,
|
||||
sessionElapsedMs: performance.now() - started,
|
||||
};
|
||||
if (session.logged_in && session.user) {
|
||||
const username = encodeURIComponent(session.user.username);
|
||||
const media = await window.FetchProxy(
|
||||
`${host}/v1/media_files/list/user/${username}?page_size=1`, { ...options },
|
||||
);
|
||||
result.mediaStatus = media.status;
|
||||
if (media.ok) result.mediaCount = (await media.json()).results?.length;
|
||||
}
|
||||
console.log(result);
|
||||
})().catch((error) => console.error("Desktop session probe failed", String(error)));
|
||||
```
|
||||
|
||||
3. Repeat with a changed port (`ARTCRAFT_DEV_PORT=6200`) and with the starting
|
||||
port occupied. For example, keep this listener running in another terminal
|
||||
before starting the launcher; stop it with Ctrl-C afterward:
|
||||
|
||||
```sh
|
||||
node -e 'require("node:net").createServer().listen(5193, "127.0.0.1")'
|
||||
```
|
||||
|
||||
The launcher should select a later port, and both session transports should
|
||||
still agree. An empty account may legitimately return zero media; it should
|
||||
still return HTTP 200. Verify a genuinely signed-out session shows the login
|
||||
screen using the isolated browser check, or a disposable account/profile.
|
||||
@@ -6,6 +6,11 @@ headless Chrome with mocked Tauri services. They do **not** measure native
|
||||
process launch, WKWebView/WebView2, Rust, disk startup, real account loading,
|
||||
or generation latency.
|
||||
|
||||
These timings are not authentication acceptance tests: the benchmark mocks the
|
||||
native session and rejects frontend HTTP requests. Before accepting a desktop
|
||||
change, also run the [session and Library checks](desktop-session-regression.md),
|
||||
including real Tauri HTTP requests when changing the launcher or bridge.
|
||||
|
||||
## Changes and results
|
||||
|
||||
Seven samples per version; table values are medians. Each sample uses a fresh
|
||||
|
||||
@@ -596,6 +596,7 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
|
||||
<Tooltip content="Settings" position="bottom" delay={300}>
|
||||
<Button
|
||||
aria-label="Settings"
|
||||
variant="secondary"
|
||||
icon={SettingsIcon}
|
||||
iconClassName="h-5 w-5 shrink-0"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test:desktop-session": "node tools/testing/desktop-session.mjs",
|
||||
"perf:desktop": "node tools/performance/run.mjs"
|
||||
},
|
||||
"private": true,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Real app UI with isolated, deterministic IPC responses. Run against a dev or
|
||||
// preview URL. Native HTTP/CORS correctness is tested separately in Rust and in
|
||||
// the real-webview procedure in docs/desktop-session-regression.md.
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium, expect } from "@playwright/test";
|
||||
import { installBrowserFixture } from "../performance/browser-fixture.mjs";
|
||||
|
||||
const url = new URL(process.argv[2] || "http://127.0.0.1:5193");
|
||||
if (!["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) {
|
||||
throw new Error("Use the local development or preview URL printed by the launcher.");
|
||||
}
|
||||
const browser = await chromium.launch({ channel: "chrome", headless: true });
|
||||
try {
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
||||
await context.addInitScript(installBrowserFixture);
|
||||
await context.addInitScript(installSessionFixture);
|
||||
await context.route("**/*", (route) => {
|
||||
const target = route.request().url();
|
||||
return target.startsWith(url.origin) || target.startsWith("blob:")
|
||||
? route.continue() : route.abort();
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
const home = async () => {
|
||||
await page.goto(url.href);
|
||||
await expect(page.getByRole("heading", { name: "What will you craft today?" })).toBeVisible({ timeout: 60000 });
|
||||
await expect(page.getByRole("button", { name: "42 Credits" })).toBeVisible();
|
||||
};
|
||||
const account = async () => {
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Accounts", exact: true }).click();
|
||||
await expect(page.getByText("Session Smoke Account", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Log Out", exact: true })).toBeVisible();
|
||||
};
|
||||
|
||||
await home();
|
||||
await account();
|
||||
await page.keyboard.press("Escape");
|
||||
await page.getByRole("button", { name: "My Library" }).click();
|
||||
await expect(page.getByRole("img", { name: "Session smoke image", exact: true })).toBeVisible();
|
||||
await expect.poll(() => page.getByRole("img", { name: "Session smoke image", exact: true })
|
||||
.evaluate((image) => image.complete && image.naturalWidth > 0)).toBe(true);
|
||||
await expect(page.getByText("Log in to browse and use your library.")).toHaveCount(0);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.getByRole("button", { name: "Create Image Generate AI images" }).click();
|
||||
await expect(page.getByRole("img", { name: "Session smoke image", exact: true })).toBeVisible();
|
||||
await account();
|
||||
await page.keyboard.press("Escape");
|
||||
for (const [card, heading] of [
|
||||
["Create Video Create video from images", "Create Video"],
|
||||
["Create Audio Generate music and sound effects", "Create Audio"],
|
||||
]) {
|
||||
await home();
|
||||
await page.getByRole("button", { name: card }).click();
|
||||
await expect(page.getByRole("heading", { name: heading, exact: true }).first()).toBeVisible();
|
||||
}
|
||||
await account();
|
||||
await page.getByRole("button", { name: "Log Out", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Create your account" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Log in", exact: true }).click();
|
||||
await page.getByPlaceholder("[email protected] or username").fill("session_smoke");
|
||||
await page.getByPlaceholder("Min. 8 characters").fill("fixture-password-only");
|
||||
await page.getByRole("button", { name: "Log in", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Welcome back" })).toHaveCount(0, { timeout: 10000 });
|
||||
await account();
|
||||
assert.deepEqual(errors, [], "Application runtime errors");
|
||||
console.log("PASS: credits, Accounts, Library media, deferred image/video/audio pages, logout gate, and sign-in agree.");
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
function installSessionFixture() {
|
||||
const invoke = window.__TAURI_INTERNALS__.invoke;
|
||||
let loggedIn = true;
|
||||
let nextResource = 0;
|
||||
const responses = new Map();
|
||||
const user = {
|
||||
username: "session_smoke", display_name: "Session Smoke Account",
|
||||
user_token: "u_session_smoke", can_access_studio: true, maybe_feature_flags: [],
|
||||
};
|
||||
const image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6mTQAAAAASUVORK5CYII=";
|
||||
const media = {
|
||||
token: "m_session_smoke", media_class: "image", media_type: "png",
|
||||
maybe_title: "Session smoke image", created_at: new Date().toISOString(),
|
||||
origin_category: "inference", origin: { origin_category: "inference" },
|
||||
media_links: { cdn_url: image, maybe_thumbnail_template: image, thumbnail_template: image },
|
||||
stats: { positive_rating_count: 0, bookmark_count: 0 },
|
||||
};
|
||||
window.__TAURI_INTERNALS__.invoke = async (command, args) => {
|
||||
if (command === "storyteller_get_login_session_command") return loggedIn ? user : null;
|
||||
if (command === "storyteller_password_login_command") { loggedIn = true; return user; }
|
||||
if (command === "storyteller_purge_credentials_command") { loggedIn = false; return; }
|
||||
if (command === "get_app_info_command") return { payload: { storyteller_host: "https://api.storyteller.ai" } };
|
||||
if (command === "storyteller_get_credits_command") return { payload: { free_credits: 0, monthly_credits: 42, banked_credits: 0, sum_total_credits: 42 } };
|
||||
if (command === "plugin:http|fetch") {
|
||||
const request = new URL(args.clientConfig.url);
|
||||
let body = { success: true, results: [], folders: [], tags: [], active_subscriptions: [] };
|
||||
if (request.pathname === "/v1/session") body = { success: true, logged_in: loggedIn, user: loggedIn ? user : undefined };
|
||||
if (request.pathname === "/v1/logout") loggedIn = false;
|
||||
if (request.pathname === "/v1/media_files/list/user/session_smoke") body = { success: true, results: [media], pagination: { has_next: false } };
|
||||
responses.set(++nextResource, { url: request.href, body });
|
||||
return nextResource;
|
||||
}
|
||||
if (command === "plugin:http|fetch_send") {
|
||||
const response = responses.get(args.rid);
|
||||
return { rid: args.rid, status: 200, statusText: "OK", url: response.url, headers: [["content-type", "application/json"]] };
|
||||
}
|
||||
if (command === "plugin:http|fetch_read_body") {
|
||||
const response = responses.get(args.rid);
|
||||
args.streamChannel.onmessage([...new TextEncoder().encode(JSON.stringify(response.body)), 0]);
|
||||
args.streamChannel.onmessage([1]);
|
||||
responses.delete(args.rid);
|
||||
return;
|
||||
}
|
||||
return invoke(command, args);
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user