dd_utils_api
See also: JSON API v1 · RQO field mapping · dd_core_api
Utility API for system operations: authentication, language, system info, and upload assembly.
Registered actions (src/core/api/handlers/dd_utils_api.ts): login, quit, get_login_context, get_install_context, install, request_password_reset, confirm_password_reset, get_system_info, get_dedalo_files, change_lang, convert_search_object_to_sql_query, join_chunked_files_uploaded, list_uploaded_files, delete_uploaded_file, get_job_events, get_process_status, get_record_jobs, get_activity, stop_process, update_lock_components_state, get_lock_status, get_server_ready_status, get_ontology_update_info, get_code_update_info.
How to call
- POST JSON to
/api/v1/json(or the client-relative/dedalo/core/api/v1/json) withdd_api: "dd_utils_api"andaction: "<method>". - Multipart file uploads are not a JSON-dispatched action here — they are handled by the media ingest branch of the API path in
src/server.ts.
Notes
loginandquitare the entry points for session management: Argon2id (viaBun.password) over rotating, server-side sessions.join_chunked_files_uploadedreassembles a completed chunked upload;get_system_infois the pre-transfer init call the client makes before uploading.get_server_ready_status,get_ontology_update_infoandget_code_update_infoare the master-server surface: remote installations call them without a session to probe reachability and fetch an update manifest. They fail closed unless the host is configured as an ontology or code server.- Envelope v2 throughout. Success is
{ ok: true, request_id, data, … }— the payload lives indata, and the names a client reads by their own name (user_id,csrf_token,file_data,dedalo_version,in_use,jobs,msg, …) ride at the top level as extension keys. A refusal is{ ok: false, request_id, error: { code, category, message, label_key, retryable } }and carries the registry's HTTP status; failures are no longer HTTP-200 bodies. The v1{ result, msg, errors }shape was removed on 2026-08-16 andresultis a forbidden top-level key.
login
Purpose
Authenticate user with username and password.
Accepts
options: object (required)username: string (required) — user login nameauth: string (required) — user password
Returns
{ ok: true, request_id, data: true, user_id: <int>, csrf_token: <string> }. user_id and csrf_token are handler-owned top-level extension keys: the session is minted mid-handler, so the dispatcher's own token append cannot see it.
Usage
Validates credentials and, on success, creates a rotating server-side session. The session token is set as an HTTP cookie on the response — it is never returned in the body; the fresh csrf_token ships in the body so the next non-exempt action can succeed. login is a NO_LOGIN / CSRF-exempt action.
When media protection is active the response carries a second cookie, the fixed-name dedalo_media_auth (see media protection). Every later authenticated response re-issues it if the caller's value is stale, so it never outlives — nor dies before — the session.
A wrong username, a wrong password and an unknown account all answer the same body — auth.login_failed (HTTP 401). The sentence is the registry's and its disclosure is operator-only, so no call site can narrow it into an account-existence oracle.
Once the session expires, any non-exempt action answers auth.not_logged (HTTP 401). See login for the two expiry clocks and the client's recovery.
Example Request
{
"dd_api": "dd_utils_api",
"action": "login",
"options": {
"username": "admin",
"auth": "secret"
}
}
Example Response
{
"ok": true,
"request_id": "c0ffee90",
"data": true,
"user_id": 1,
"csrf_token": "…"
}
quit
Purpose
Logout current user session.
Accepts
options: object (optional) — typically empty
Returns
{ ok: true, request_id, data: true }.
Usage
Destroys the server-side session and clears the session cookie on the response — and the media-auth cookie with it, unconditionally, since clearing an absent cookie costs nothing while leaving a live one behind does not. An activity row is written before the session is destroyed, while the actor is still known.
Example Request
{
"dd_api": "dd_utils_api",
"action": "quit"
}
Example Response
{
"ok": true,
"request_id": "c0ffee91",
"data": true
}
upload — handled outside the JSON dispatcher
upload is not a JSON-dispatched action. Multipart uploads (single or chunked) hit the media ingest branch of the API path in src/server.ts, which routes the form data into src/core/media/ingest/upload.ts. Once all chunks have arrived the client fires a JSON join_chunked_files_uploaded RQO (below) to reassemble and re-sniff them.
join_chunked_files_uploaded
Purpose
Reassemble a completed chunked upload (the JSON follow-up to the multipart chunk POSTs).
Accepts
options: object (required)file_data: object —{ key_dir, tmp_name, total_chunks, upload_id }, the descriptor the last chunk response returned.upload_ididentifies the transfer whose parts are to be joined; a malformed one is refused, and an absent one falls back to matching the recorded file name and part count (ambiguous matches are refused rather than guessed).files_chunked: array (optional) — the dense chunk list (its length is used astotal_chunkswhen present)
Returns
{ ok: true, request_id, data: true, file_data: { key_dir, tmp_name, name, extension, chunked: false, complete: true, thumbnail_url } } — file_data is a top-level extension key.
tmp_nameis the name the assembled file really has in the staging directory, which may carry a-1,-2… suffix when another staged file already held the sanitised name.nameis the human file name, taken from the server's own per-transfer record, not from the relayed request — which is why a caller that does not echo it back still archivesMaría Piñón.jpgrather than a mangled transliteration.thumbnail_urlis the preview the queue row draws, ornullwhen the format is not rasterisable. A chunked transfer's only completion moment is this call, so the thumbnail is built here exactly as the single-shot upload path builds it.
Fail-closed: an anonymous caller gets resource.not_found (HTTP 404) — the same shape a route miss answers, so the endpoint leaks no existence. A failed join answers media.action_failed; the engine's reason names filesystem paths and stays on the log line.
list_uploaded_files
Purpose
List files in upload directory.
Accepts
options.key_dir: string (required in practice) — the staging sub-directory to scan. Without it the answer is an empty array, not the whole staging root.
Returns
{ ok: true, request_id, data: [ { url, name, size }, … ] } — the caller's own staged files under key_dir. This is the mechanism by which a pending upload queue survives a page reload: the queue renderer lists the already-staged files on every render and injects them as existing rows.
Note
The answer is always a 200 with an array, even when the staging scan itself fails (a malformed key_dir, an unreadable root): the failure is logged and an empty array returned. An error here would accumulate into the page's API-error list, which makes the next element's render bail before it finishes — one bad scan would break unrelated widgets on the page.
Example Request
{
"dd_api": "dd_utils_api",
"action": "list_uploaded_files",
"options": { "key_dir": "upload" }
}
Example Response
{
"ok": true,
"request_id": "c0ffee92",
"data": []
}
delete_uploaded_file
Purpose
Remove one staged file — the queue renderer's row-removal path.
Accepts
options: object (required)key_dir: string (required) andfile_name: string (required).upload_id: string (optional) — an explicit cancel. A row removed before its transfer completed has no assembled file, only chunk parts; with this key they go now, without it the age sweep collects them. It is also the release for a quarantined transfer — one whose assembled bytes failed content verification and were kept rather than destroyed.
Returns
{ ok: true, request_id, data: true }. Deleting an already-absent file is a successful no-op: the client has already removed the row, and a retry must not surface an error the user cannot act on.
A traversal attempt or a malformed name answers request.invalid_options with a generic "Invalid file reference" — the resolver's own message names the resolved path and is never serialized.
get_system_info
Purpose
Retrieve system and server information.
Accepts
options: object (optional) — none required.
Returns
{ ok: true, request_id, data: <SystemInfo> }. The payload (src/core/api/handlers/system_info.ts) is the upload-limit negotiation the client reads before it can transfer a file. The numbers come from the media/upload config catalog — there is no runtime .ini to consult. Shape: { max_size_bytes, sys_get_temp_dir, upload_tmp_dir, upload_tmp_perms, session_cache_expire, upload_service_chunk_files, pdf_ocr_engine }.
Example Request
{
"dd_api": "dd_utils_api",
"action": "get_system_info"
}
Example Response
{
"ok": true,
"request_id": "c0ffee93",
"data": {
"max_size_bytes": 10485760,
"sys_get_temp_dir": "/tmp",
"upload_tmp_dir": "/…/media/tmp",
"upload_tmp_perms": 16877,
"session_cache_expire": 180,
"upload_service_chunk_files": 20,
"pdf_ocr_engine": true
}
}
change_lang
Purpose
Persist the user's interface and/or data language choice for subsequent requests.
Accepts
options: object (required)dedalo_application_lang: string (optional) — the interface languagededalo_data_lang: string (optional) — the data language
At least one must be present. Each value is validated against the language identifier allowlist before it is stored (an invalid tag is dropped, never persisted). When the install couples the two languages, a change to either drives the other.
Returns
{ ok: true, request_id, data: true }. With no valid language supplied the call refuses with request.invalid_options ("No valid language supplied") — an invalid tag is dropped before validation, so naming only invalid tags is the same as naming none.
Usage
State-changing. For an authenticated caller the router runs the CSRF gate and the choice is stored on the server-side session; every later request rebuilds with the stored language (src/core/resolve/request_lang.ts). The client posts here, then full-reloads.
An anonymous call naming only dedalo_data_lang answers auth.not_logged (HTTP 401): there is nowhere to store a data language before login, and answering success while storing nothing would be a silent no-op.
The action is also reachable without a session — the login panel carries its own language selector, and there is no session row to store into before login. The server answers with the pre-auth language cookie (dedalo_lang, HttpOnly, one year) carrying the application language only — an anonymous call naming only dedalo_data_lang is refused (auth.not_logged), since there is nowhere to put it. The cookie is honored on the next anonymous request and adopted onto the session at login, so the app opens in the language the login form was switched to; authenticated calls refresh it too, so the two stores never disagree. The value is checked against this install's DEDALO_APPLICATION_LANGS when read back — a cookie naming any other language is ignored, not persisted. Divergence entry: engineering/wire_contract/WC-2026-08-22-preauth-language-cookie.md.
Example Request
{
"dd_api": "dd_utils_api",
"action": "change_lang",
"options": {
"dedalo_application_lang": "lg-eng",
"dedalo_data_lang": "lg-spa"
}
}
Example Response
{
"ok": true,
"request_id": "c0ffee94",
"data": true
}
get_login_context
Purpose
Return the login form's own structure context.
Accepts
- No arguments.
Returns
{ ok: true, request_id, data: [ <login context> ] } — data is an array of one element, because the client build looks its own element up inside it by model.
Usage
Pre-auth by design — the form must render before any session exists.
get_install_context
Purpose
Return the install wizard's structure context on a fresh, unconfigured machine.
Accepts
- No arguments.
Returns
{ ok: true, request_id, data: [ <installer element context> ] } — an array of one, for the same reason as get_login_context.
Usage
On a fresh machine there is no ontology to resolve, so this is a synthetic context built by hand carrying exactly the properties the client's installer reads. The dispatch gate (Gate 1b) admits it only while the server is unsealed and the caller IP is allowed; once the install is sealed it 404s.
install
Purpose
The install wizard's step router — every wizard step rides this one action.
Accepts
options: object (required)action: string (required) — the concrete wizard step- other per-step fields consumed by
src/core/install/engine.ts
Returns
The step's own envelope-v2 body, shaped per step: data carries the step's payload and each step adds the top-level extension keys the wizard reads by name.
Usage
The dispatch gate (Gate 1b) already enforced unsealed + IP-allowed; record-writing steps re-check the session here (login-gated even while unsealed).
request_password_reset
Purpose
Forgot-password step 1: request a reset for an identifier.
Accepts
options: object (required)identifier: string (required) — username or email
Returns
A generic envelope that is identical regardless of whether the identifier exists (anti-enumeration).
Usage
Pre-auth by design (NO_LOGIN + CSRF-exempt). Anti-enumeration and throttling live in src/core/security/password_reset.ts.
Example Request
{
"dd_api": "dd_utils_api",
"action": "request_password_reset",
"options": {
"identifier": "admin"
}
}
confirm_password_reset
Purpose
Forgot-password step 2: confirm the reset with the emailed code and set a new password.
Accepts
options: object (required)reset_id: string (required)code: string (required)new_password: string (required)
Returns
The generic reset envelope (src/core/security/password_reset.ts).
Usage
Pre-auth by design, like request_password_reset.
update_lock_components_state
Purpose
Record a component soft-lock focus/blur event (the edit-lock mechanism).
Accepts
options: object (required)section_tipo: string (required) — the locked record's sectionsection_id: int|string (optional)component_tipo: string (optional)action: string (optional) — the focus/blur event
Returns
{ ok: true, request_id, data: <applied bool>, in_use, full_username, msg, dedalo_notification: null, … }. data is whether the event was applied; the keys the client reads by name ride at the top level as extension keys.
Usage
Read permission (level ≥ 1) on the section is required — the gate runs unconditionally and section_tipo is mandatory (fail-closed), so a user cannot fabricate focus/blur on records they cannot see. This is the per-component soft-lock, distinct from the area-level dd_area_maintenance_api::lock_components_actions.
Example Request
{
"dd_api": "dd_utils_api",
"action": "update_lock_components_state",
"options": {
"section_tipo": "oh1",
"section_id": 368,
"component_tipo": "oh16",
"action": "lock"
}
}
get_lock_status
Purpose
Read-only poll: is the component currently held by another user?
Accepts
options: object (required)section_tipo: string (required)section_id: int|string (optional)component_tipo: string (optional)
Returns
{ ok: true, request_id, data: true, in_use, full_username, … } — the status fields ride as top-level extension keys (in_use is what the client's release poll reads); data stays a truthy answer.
Usage
Same fail-closed read gate as update_lock_components_state (section_tipo required, level ≥ 1 on the section).
get_dedalo_files
Purpose
Return the service-worker pre-cache manifest.
Accepts
- No arguments.
Returns
The manifest rides in the standard envelope: data carries the {type, url}[]
list and dedalo_version rides beside it as an extension key. The clients read
response_data(api_response) for the list and api_response.dedalo_version for
the cache key — there is no top-level result or msg on the wire.
Usage
Authenticated read (a session is required) but CSRF-exempt: the service worker calls it without the page's token.
What the client does with it
dedalo_version is the cache key, not a display string: it carries the engine
version plus a signature of the client bytes on disk, so editing any manifested file
moves the key and the browser re-fetches. The service worker names its cache
dedalo_files_<dedalo_version> and owns every key with that prefix.
A cache pass follows a fixed commit order — fetch every file, write the manifest record inside the cache, purge the superseded caches, then swap the worker's in-memory state. The manifest record is the commit marker: it means "this cache is whole". A worker killed mid-pass leaves an unmarked cache, which is ignored, so the last complete cache keeps serving. A pass that stores too little refuses to write the marker at all, leaving the previous cache authoritative rather than replacing a working cache with an empty one.
Because a service worker is killed whenever it goes idle and its module state dies with it, a revived worker rebuilds that state from the marker with no network call, and re-checks the version periodically so a deploy is picked up without a new login.
The worker reports progress to the login page as ready, loading, waiting and
finish messages. finish is what the login navigates on, and it is always sent —
success or failure, with an error field — because a missing finish does not
degrade the login, it hangs it. The login also runs its own watchdog and continues
anyway if every cache path goes silent. That message seam is frozen by the wire
contract ledger entry WC-2026-08-21-files-cache-finish-message.
get_job_events
Purpose
Subscribe to a native in-process job and receive every state change as it happens.
Accepts
- The job handle (
job_id) the caller is subscribing to.
Returns
A pushed event stream; the stream ends on the terminal frame, whose data is the job's return value (for an import, the full report).
Usage
Session-gated (src/core/api/job_stream.ts). This is the native job-status wire — no {pid, pfile} handle and no polling; get_process_status below is the legacy poll wire kept for the AV-transcode and backup consumers.
get_process_status
Purpose
Stream the status of a background process (media transcode / backup).
Accepts
- The job identifier the client's
update_process_statuspolls.
Returns
An SSE status stream (src/core/api/process_status.ts).
Usage
Session-gated and owner-gated — a job that carries user data streams only to its owner, since the ids are guessable. The legacy poll counterpart to get_job_events.
get_record_jobs
Purpose
Answer "what is running for this record?" — the question that made an upload's background transcode invisible and an empty media tier indistinguishable from "never built".
Accepts
options: object (required)section_tipo: string (required) andsection_id: int (required, positive).
Returns
{ ok: true, request_id, data: true, jobs: [ … ] } — jobs is the top-level key the activity tray reads by name.
Usage
Authorized by the record, not by job ownership (read level ≥ 1 on the section) — a second operator looking at the same record must see that a tier is already being built, or they start a duplicate encode over the same output path. The payload is therefore reduced to operational shape: a foreign job's own data stays owner-only. A missing section_tipo or a non-integer section_id answers request.invalid_options; an insufficient grant answers perm.denied.
get_activity
Purpose
The activity tray's read model: the caller's own work, aggregated across both job systems.
Accepts
- No arguments.
Returns
{ ok: true, request_id, data: true, jobs: [ … ] }.
stop_process
Purpose
Stop a background job (the generic Stop button's wire).
Accepts
- The job identifier to abort.
Returns
The stop outcome (src/core/api/process_status.ts).
Usage
Session-gated and owner-gated. It aborts the job's controller so the handler winds down cooperatively.
convert_search_object_to_sql_query
Purpose
The SQO → SQL developer console (the sqo_test_environment maintenance widget): translate a client SQO to SQL and run it.
Accepts
options: object (required) — the client SQO to convert (scrubbed bysanitizeClientSqo, the API-boundary security gate).
Returns
{ ok: true, request_id, data: { sql_resolved, sql, ar_section_id, db_data } }. The console's answer is the four pieces together, so they are the payload, not top-level keys beside a boolean:
sql_resolved— the resolved SQL with its parameters substituted, display only. Envelope v2 has no prose channel, so it is a named field of the data rather than a message.sql— the parameterized template actually executed.ar_section_id— the distinctsection_ids the rows returned.db_data— the rows.
Usage
Global-admin only (perm.denied, HTTP 403, for everyone else). The executed query always uses bound params; the substituted string is for display, never execution. A build or execution failure answers search.failed.
Example Request
{
"dd_api": "dd_utils_api",
"action": "convert_search_object_to_sql_query",
"options": {
"section_tipo": ["oh1"],
"limit": 10
}
}
get_server_ready_status
Purpose
Remote reachability probe: is this host an available ontology / code master server?
Accepts
options: object (required)check: string (required) —ontology_serverorcode_server
Returns
{ ok: true, request_id, data: true } when the requested check matches a role this host is configured for. Every other outcome is the same refusal — update_server.refused (HTTP 403, "Error. This is not an accessible Server"). One code for every reason is deliberate: a probe must not be able to tell "not that kind of server" from "unknown check" by elimination.
Usage
Machine-to-machine, pre-auth (NO_LOGIN + CSRF-exempt). Fail-closed on the configuration flags.
Example Request
{
"dd_api": "dd_utils_api",
"action": "get_server_ready_status",
"options": {
"check": "ontology_server"
}
}
get_ontology_update_info
Purpose
Serve an ontology-update manifest to a remote installation.
Accepts
options: object (required)version: string (required) — the caller'smajor.minorcode: string (required) — a configured access code
Returns
{ ok: true, request_id, data: <manifest> } when this host is an ontology master and the code and version both validate. Every refusal — not a master, wrong code, malformed or unsupported version, a version with no ontology files — is the same update_server.refused (HTTP 403), for the same anti-enumeration reason as the probe.
Usage
Served only when this instance is an ontology master, to callers presenting a configured access code. Pre-auth master-server surface.
Warning
The update panel of the client installation calls this action from the browser, cross-origin — not through its own server, so it has to get past a gate at each end.
On the master: the client's origin must be accepted by DEDALO_CORS_ALLOWED_ORIGINS — named exactly, or covered by the single entry * on a master serving installations it does not know in advance. Otherwise the preflight fails and the panel dies with a network error.
On the client: its own connect-src Content-Security-Policy must carry the master's origin, or the browser refuses the call before it leaves. The engine derives that from the client's ONTOLOGY_SERVERS automatically, but at boot — a client that has just added a master must be restarted.
The reachability probe next to it (get_server_ready_status) is a server-to-server request and is affected by neither, so a master can report itself ready and still be unreachable from the panel. A cross-origin caller carries no session cookie: it reaches only this pre-auth surface, and admitting an origin grants nothing more.
get_code_update_info
Purpose
Serve a code-release manifest to a remote installation.
Accepts
options: object (required)version: string (required) — the caller's version triplecode: string (required) — a configuredCODE_SERVERScode
Returns
{ ok: true, request_id, data: <manifest> } when this host is a code master and the code and version both validate; every refusal is update_server.refused (HTTP 403). It advertises only built release archives on the caller's linear upgrade path. Unlike the ontology master, a code master honours no localhost pseudo-code: it answers configured peers only.
Usage
Served only when this instance is a code master, to callers presenting a configured code. Pre-auth master-server surface.