Reuse the channel base host and plugin allowedHosts for redirected query
credentials. Keep anonymous redirects working, prevent credentials from
returning after an untrusted hop, and restrict body replay to trusted hosts.
Preserve SSRF and redirect limits, reject HTTPS downgrades, and redact signed
URLs from redirect-policy errors. Document the existing allowedHosts contract.
Validation: go test ./relay/channel/task/jsplugin -count=1
Passed against clean 6347fb0aa plus this patch, without an overlay.
Covers single/batch polling, 301/302/303/307/308, host and port boundaries,
multi-hop credentials, SSRF, downgrade rejection, and real HTTP receivers.
Related OWASP ASVS v5.0.0 controls: 14.2.3, 1.3.6, 12.3.1, 16.2.5.
https://github.com/OWASP/ASVS/tree/v5.0.0/5.0/en
* fix(task): honor channel header overrides for submission and polling
Apply the existing Request Header Override configuration after task plugin
headers, including per-task, batch and realtime queries. Resolve api_key
from the credential used for that request and skip client-only placeholders
when polling has no original client request. Reuse the HTTP header resolver
without changing plugin contracts or adding provider-specific settings.
Keep polling redirects visible, matching submission and utils.fetch, so a
custom credential header cannot be forwarded to a redirect destination.
Document configuration and cover precedence, saved keys, absent client
context, invalid values, redirects and all polling initialization paths.
Validation with Go 1.26.4 and GOWORK=off:
- make test: root, relaykit and tokenkit passed before the realtime follow-up
- go test ./relay ./relay/channel ./relay/channel/task/jsplugin ./service -count=1: passed after the follow-up
- go vet ./... and go build ./...: passed after the follow-up
- Independent read-only review: passed
Scoped security review used OWASP ASVS 5.0.0 and the Authentication,
Session Management and REST Security cheat sheets. Credentials remain in
headers, client headers are not persisted, and redirects are not followed.
No live provider calls or production deployment were performed.
https://owasp.org/www-project-application-security-verification-standard/https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.htmlhttps://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.htmlhttps://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
* fix(task): follow poll redirects and override content request headers
Polling follows redirects again through the shared HTTP client, which
checks every hop against the SSRF protection settings. Refusing them
turned each poll of a redirecting upstream into a failure, and after
enough failures the task was failed and refunded.
Credentialed content requests now carry the channel header overrides,
as submission and polling do. Credentialless links, utils.fetch and the
Host header stay as the plugin builds them.
* fix(task): settle tasks that a realtime query sees finish
Fetching a Gemini or Vertex task queries the upstream directly. When
that query saw the task finish, it wrote the final status itself, so
the poller skipped the task and never settled or refunded it: a failed
task kept its charge. Hand that transition to the poller's single-task
update, which records the status and settles exactly once.
---------
Co-authored-by: CaIon <[email protected]>
utils.json.clone is available to every plugin, and a plugin that exports
parseSubmitEventDelta without parseSubmitEvent now gets the delta hook
without declaring anything. Both capability strings stay accepted for
plugins written for earlier hosts, and a plugin exporting both hooks keeps
the snapshot hook unless it declares submit-sse-delta@1. alibaba drops both
declarations.
utils.fetch is a synchronous, read-only GET or HEAD to the channel Base URL
host or the plugin's allowedHosts, through the channel proxy used for
polling. It is available in buildSubmitRequest, the query and batch build
and parse hooks, and buildContentRequest; every other hook throws. Each hook
call gets at most 4 fetches, 15 s per fetch, 30 s in total and 1 MiB per
body. Redirects are returned, not followed. responseType "bytes" returns an
ArrayBuffer for binary data such as a media file header.
While a fetch waits, the runtime gives up its execution slot and the
watchdog pauses. BuildContentRequest now receives the request context, and
the media proxy shares the plugin request header rules.
A plugin that declares duration-auto@1 may receive a duration of -1 (the
vendor picks the length) anywhere in the request; other negative values and
values above the limit are still rejected, and usage facts stay
non-negative. A declaring plugin reserves the model's longest duration and
settles on the usage the vendor reports.
- doubao 1.3.0 declares it, forwards -1 to Ark and reserves each model's
longest duration (12 s for Seedance 1.0 and 1.5 pro, 15 s for 2.0, 30 s for
2.5). A JSON duration on /v1/videos now reaches Ark.
- alibaba 1.4.2 declares it, so wan3.0 smart duration also works on
/v1/video/generations.
Fixes#7700
The host decoded plugin request bodies into Go maps, so hooks saw their
members sorted and the upstream body was written sorted. TypeSafe Jev
reads the state object as written, and the reordering changed its scores
against calling TypeSafe directly.
A plugin that declares the json-order@1 capability now gets the client's
order end to end: its decoders receive the JSON body as text, its decoded
requestBody is kept as JSON.stringify text for the driver hooks, and its
request descriptors are decoded with Unmarshal so the body reaches the
upstream as that text. Other plugins keep the Go-map path unchanged, and
billing bounds still validate the decoded requestBody.
RawJSON hook arguments, top-level or nested, are now parsed in place with
ParseJSONString instead of being copied first; every caller passes bytes
it never changes afterwards.
Task plugins now run on github.com/Calcium-Ion/moejs v0.1.0-alpha.3
instead of Sobek, and the host utils are ported to it. The plugin API is
unchanged. Code compiled from strings is no longer allowed: eval and the
Function constructors throw an EvalError.
Hooks receive host values without defensive copies. The engine converts
an object or array when the hook first reads it and never writes
JavaScript changes back to the host value. Stored protocol state and task
views are passed as raw JSON and parsed inside the runtime instead of
being decoded into Go maps first. An idle runtime drops its references to
the last call's arguments.
Whether an optional hook is exported is decided once when the plugin
loads, so checking for it no longer takes an execution slot.
Usage validation returns the normalized facts in a new map instead of
writing into the hook result, so a hook that returns the request body no
longer changes it. The request descriptor is copied instead of being
round-tripped through JSON.
Each user can now hold up to 20 named access tokens in the
user_access_tokens table instead of a single token on the user row.
Tokens are shown once at creation (nap_ prefix) and stored as a SHA-256
digest. They expire after 30 days by default; a token without expiry is
allowed with a warning.
A token only reaches the routes its scopes allow. Scopes are picked from
the same permission matrix as admin permissions and are limited to what
the owner can do; every management route declares the scope it needs,
and routes without a declaration reject tokens. Token management itself
always requires a browser session.
Tokens can complete step-up verification, so verified actions such as
changing a password or 2FA settings work with a token; such changes sign
out all browser sessions. Disabling a user keeps their tokens, deleting
the account removes them.
The existing dashboard token keeps working for 30 days after the
upgrade. It can be revoked but no longer regenerated, and afterwards it
is rejected with ACCESS_TOKEN_LEGACY_RETIRED. The security page lists
tokens with their access records, and audit details show scope names.
Deleting a user, changing their status or role, setting a password or
admin permissions, creating an admin, resetting a passkey, disabling 2FA
and clearing a binding now need a single-use verification proof. The
proof context names the managed user (and the action or binding where
relevant), so it cannot be reused for another user or operation. Quota
adjustments and edits to display name, group or remark stay unchanged.
The admin.user.* scopes are issued only to admins. An admin with 2FA or
a passkey must use one of them; otherwise password or a bound OAuth
account is accepted. The users page asks for verification before sending
each gated request.
An unchanged model expression or provider override of a single-plugin
model stays saveable after the plugin drops a fact from its profile, so
a full pricing save is no longer blocked by existing prices. Shared
models still follow each provider's profile.
The task pricing table reads a dropped fact at the value a plugin keeps
reporting: price terms on dropped counts are omitted, tiers requiring a
dropped boolean to be true are skipped, and other conditions on dropped
facts are removed with the first matching tier kept. Existing prices
open in the table with only the remaining fields.
The submit relay treated every status other than 200 as an upstream
failure, so a task API that answers 201 Created or 202 Accepted (for
example Tencent's wand-vega-video-1.0-lite) returned fail_to_fetch_task
and parseSubmitResponse never ran. The check predates the plugin system,
when every built-in adaptor answered 200; the plugin adaptor already
passes the exact statusCode to parseSubmitResponse and polling has
always classified 2xx as a successful response.
Accept any 2xx before parsing and keep failing other statuses with the
upstream status and body. Add a relay regression that drives a plugin
submission against a real upstream returning 200/201/202 and 400/502,
and document the submit-side 2xx contract in the plugin API.
Fixes#7478
Add the synchronous openai_image host protocol so a task plugin can claim
POST /v1/images/generations and /v1/images/edits; the host submits the
task, polls it inside the request and lets the plugin render the OpenAI
image response. Unclaimed models still fall back to the HTTP image relay.
- Migrate Ali (Bailian) image models to the alibaba plugin and remove the
ali channel image/wan adaptors and the qwen model setting
- Add Doubao Seedream image generation to the doubao plugin
- Support retainResult on routes and indexed multipart file references
- Settle image counts from upstream usage or image payloads; document the
rule in the billing conventions and plugin API docs
- Show billing source and task artifacts in usage logs; rename the
DoubaoVideo channel display name to Doubao and refresh the favicon
A New API (type 60) channel proxies another gateway that may host many
task plugins, so the channel setting gains task_extend_plugin_keys; the
single task_plugin_key stays valid there as well. Channel selection,
shared-model candidate rebinding, the task_plugin.bind permission gate,
plugin usage counting and the disable cascade (which now unbinds gateway
channels instead of disabling them) all honour the union of both fields,
and plugins may no longer claim channel type 60 in channelTypes.
Plugins declare meta.upstreams and the host injects ctx.upstream into
every driver hook context. On a type-60 channel the driver addresses its
own prefixed native routes (doubao, alibaba, kling, jimeng) and the host
sends the channel key as a Bearer header for every auth type; polling
threads the channel type through ChannelMeta and tasks keep the key
that submitted them. Only plugins declaring new_api can be bound.
The channel form offers an "Upstream task plugins" selector for type-60
channels, MultiSelect options can carry an icon, and the plugin API docs,
type declarations and schema describe the additive apiVersion 1
extension.
Plugin polling hooks previously ran against a hollow context: parseTaskResult
and parseBatchResult received {} / nil, buildQueryRequest received a
{task_id, action} map under the misleading name requestBody, and batch hooks
saw only bare task ids. The per-task poller also never looked at the upstream
HTTP status, and every built-in plugin papered over unrecognized bodies with
`|| "IN_PROGRESS"`, so a 404, a revoked key, or a shape the plugin did not
know would sit in IN_PROGRESS for the full 24h TASK_TIMEOUT_MINUTES while
holding the user's pre-charged quota.
Contract (docs/plugin-api v1.d.ts, v1.md, v1.schema.json):
- TaskQueryContext is declared separately from DriverContext and rebuilt from
the persisted Task row: taskId, publicTaskId, action, model, upstreamModel,
baseUrl, apiKey, authHeader, auth, data, state. Query-side requestBody is
removed; the original request is not persisted and hooks that need a
request-derived value must save it into state at submit time.
- parseTaskResult / parseBatchResult receive a third {status, headers}
argument. Batch hooks receive tasks[] with one TaskQueryContext per task.
- NormalizedTaskResult accepts status "UNKNOWN" meaning "I do not recognize
this body". Falling back to IN_PROGRESS for unknown shapes is forbidden;
`plugin lint` warns on the literal.
- parseSubmitResponse / parseTaskResult / parseBatchResult may return `state`.
Task.Data remains a per-round snapshot overwritten on every valid parse;
state is plugin-owned, persisted in TaskPrivateData.PluginState, preserved
when a hook omits it, byte-capped like taskData, and never exposed through
presenter views.
Host (service/task_polling.go, relay/channel/task/jsplugin/adaptor.go):
- TaskPollingAdaptor / BatchTaskPollingAdaptor take *model.Task and the
*http.Response so the adaptor can build the full context; jsplugin is the
only implementation.
- HTTP classification before the plugin sees the body: 2xx -> plugin;
404/410 -> FAILURE and refund; 401/403 -> poll failure plus a channel-scoped
warning, no auto-disable; 429/5xx/transport -> poll failure; other 4xx ->
plugin with the status visible, counted as unrecognized if the plugin still
reports a non-terminal state.
- TaskPrivateData.PollFailures counts consecutive poll failures (transient
HTTP, auth, transport, hook error, UNKNOWN). It is persisted through the
existing UpdateWithStatus CAS so a concurrent terminal transition on another
instance is never clobbered, and reset on any valid 2xx non-terminal parse.
Reaching TASK_POLL_MAX_FAILURES (default 20, <= 0 disables) fails the task
with the last classification and HTTP code in fail_reason and runs the
existing settle/refund chain exactly once. sweepTimedOutTasks and its
1440-minute default are unchanged as the outer backstop.
- Unrecognized bodies are logged at WARN with a bounded redacted copy since
Task.Data is intentionally not overwritten on that path.
Plugins (all ten bumped one patch version):
- jimeng persists the outbound req_key in state and reads it back in
buildQueryRequest, replacing dead reads of ctx.data / ctx.requestBody that
never resolved.
- sunoapi batch hooks read tasks[] instead of the removed requestBody.
- hailuo treats base_resp.status_code != 0 as FAILURE before the status table.
- kling, vidu, sora, alibaba, doubao, hailuo, jimeng return UNKNOWN with the
raw upstream status in reason on table miss.
- google and vertex-ai treat a missing `done` as in-progress: Google
long-running operations omit proto3 default fields, so a running Veo
operation has no `done` key at all. Only a body without an operation name is
UNKNOWN. plugins/veo_poll_test.go locks this so the poll-failure cutoff can
never fail a rendering Veo task.
Tests cover the classification table end to end against a real DB (404
immediate refund, 429xN refund, 401 increments without status change, 2xx
reset, UNKNOWN increments, state preserved vs replaced, PollFailures survives
the CAS write), the query-context shape, UNKNOWN on unrecognized bodies, and
the absence of PluginState/PollFailures from TaskView. Controller tests derive
the kling factory version from the embedded manifest instead of hardcoding it.
* fix: reduce public bootstrap requests and revalidate content
* fix(controller): use a weak ETag for revalidated public JSON
/api is gzip-compressed by middleware that runs after the handler returns,
and the validator is computed over the uncompressed body. The compressed and
identity forms of one payload therefore share a validator, which a strong ETag
must not do -- it asserts byte-for-byte equality across representations
(RFC 9110 8.8.1). Serve W/ instead.
Weak comparison ignores W/ on both operands, so etagMatches now strips it from
the served validator as well as from each candidate. Stripping only the
candidate would make a weak served validator match nothing and silently
disable every 304.
Vary: Accept-Encoding stays. Weakening the validator makes revalidation
correct, but it does not separate the two encodings in a shared cache.
* fix(test): align response cookie helper name
* fix(auth): revalidate stale route sessions
* Update web/src/features/about/api.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* test: remove newly added PR tests
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* feat(plugin): add MiniMax-H3 /v2 video generation to the hailuo task plugin
MiniMax-H3 speaks a different contract from the other Hailuo models, so the
hailuo task plugin now branches on the upstream model instead of adding a Go
adaptor:
- submit builds /v2/video_generation with a multimodal `content` array
(text, first/last frame images, reference video/audio, or a full
`metadata.content` passthrough), an explicit `ratio`, and 768P/2K
resolutions; `metadata.callback_url` and `metadata.aigc_watermark` pass
through
- query uses /v2/query/video_generation/{task_id} and parses the
`{"task": {...}}` envelope, falling back to the /v1 shapes for every other
model
- the /v2 result is a public CDN URL, so its artifact is proxied
credentialless instead of through /v1/files/download
- request bounds (duration 4-15, resolution 768P/2K, ratio whitelist, at most
2 frame images and 9/3/3 reference images/videos/audios) are enforced while
the request body is built, which the host runs during validation, so an
out-of-range duration is rejected with a 400 before it can become a billing
multiplier
- duration and resolution are reported as usage facts only. Like the rest of
this plugin, extractUsage returns no billing ratios, so per-call pricing is
flat and 2K/duration pricing is expressed through the model's tiered billing
expression over those facts.
Query hooks are driver hooks and are documented to receive `ctx.model` and
`ctx.upstreamModel`, but polling has no relay info and never populated them.
The polling and realtime-fetch call sites now carry the persisted task model
properties and the plugin adaptor maps them onto the query context, with
`upstreamModel` falling back to the origin name for tasks submitted without a
channel mapping.
* fix(plugin): validate Hailuo H3 requests and errors
Channel model_mapping keys exposed in a channel's model list now act as
first-class aliases for task-plugin models across the whole line:
- Derived alias view (model/task_model_alias.go): built from enabled
channels' model_mapping, chain-following with cycle detection, declared
names always win, cross-plugin conflicts dropped. Rebuilt on channel
cache refresh, registry generation change, and a 60s TTL.
- Request path: PinTaskPluginEndpoint resolves declared-name case folds
and mapping aliases before endpoint lookup (never rewriting the body
until the endpoint is claimed), pins with MappedModel, and the decode
contract accepts alias echoes without loosening model ownership for
normal pins. Legacy /v1/tasks submit folds case variants the same way.
Fixes aliases on POST /v1/responses silently falling through to the
main relay against task channels.
- Mapping order: ModelMappedHelper now runs before the plugin submit
hook builds and caches the upstream body, so channel model_mapping
actually reaches the upstream request. Plugins receive the mapped
name as ctx.upstreamModel in both decode and submit contexts.
- Billing: identity stays the origin name; when the alias has no tiered
expression, the selected channel's mapping tail expression applies.
Pricing page and billing-expr smoke tests resolve aliases to the
owning plugin's usage schema.
- Case folding: ASCII-only fold with exact-match priority; same-plugin
and cross-plugin fold collisions rejected at registration.
- Plugins: model-keyed rate tables, req_key derivation, and combo
validation in doubao/kling/jimeng/hailuo/vidu/sunoapi now key on
ctx.upstreamModel || ctx.model; render/echo paths keep ctx.model.
* feat: support upstream model fetch for advanced custom channels
* fix: add advanced custom routes as separate groups
* fix: select advanced custom route entry before adding
---------
Co-authored-by: CaIon <[email protected]>
* wip ionet integrate
* wip ionet integrate
* wip ionet integrate
* ollama wip
* wip
* feat: ionet integration & ollama manage
* fix merge conflict
* wip
* fix: test conn cors
* wip
* fix ionet
* fix ionet
* wip
* fix model select
* refactor: Remove `pkg/ionet` test files and update related Go source and web UI model deployment components.
* feat: Enhance model deployment UI with styling improvements, updated text, and a new description component.
* Revert "feat: Enhance model deployment UI with styling improvements, updated text, and a new description component."
This reverts commit 8b75cb5bf0d1a534b339df8c033be9a6c7df7964.
Add comprehensive French translation glossary document to standardize key project terminology. The glossary includes translations for core concepts, model-related terms, user management, recharge & redemption, channel management, and security terms. This ensures consistency and accuracy in French translations across the project, with specific guidance on technical terms and contextual usage.
Add comprehensive i18next configuration for internationalization support with Chinese, English, and French locales. Configure extraction settings and ignore patterns for React components. Expand translation glossary with security and billing terminology including Two-Factor Authentication, 2FA, and pricing multiplier terms.
A new Markdown file `docs/api/web_api.md` has been added that documents all backend REST endpoints used by the Web UI.
Details:
• Lists every `/api`, `/dashboard`, and `/v1/dashboard` route relevant to the Web front-end
• Excludes every Relay-specific path to keep scope focused on Web operations
• Groups endpoints by functional module (initialisation, public info, user, channel, token, logging, etc.)
• Specifies HTTP method, path, required auth level, and concise description for each entry
• Includes an auth-level legend and update-date placeholder for future maintenance
No application logic was modified; this is documentation-only and improves developer onboarding and API discoverability.