Files
james.maesandClaude Sonnet 5.5 6cb8e34c5d
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (pull_request) Successful in 14s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Waiting to run
maven / build (push) Successful in 7m12s
build / maven (push) Successful in 7m17s
build: publish the Record Files bridge with the base through one reactor
The root pom becomes the packaging-pom reactor (qbit-mobile-scanners-reactor), the single <revision> source and parent of base/ and
integrations/record-files, so one root mvn deploy publishes the reactor POM, qbit-mobile-scanners and qbit-mobile-scanners-record-files at one
version. Consumer POMs are flattened so the version is resolved. Pins: QQQ 4.1.0-RC.22, qbit-record-files 0.1.0-RC.1, qbit-collaboration 1.1.0-RC.1
(all verified in the Gitea registry); the SNAPSHOT/feature coordinates and the qqq.frozen.version override are gone. Checkstyle config stays at the
repository root. demo/ and examples/ stay standalone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:30:45 -06:00

16 KiB
Raw Permalink Blame History

Mobile Scanners semantic UI API

Mobile Scanners is an independently versioned QQQ UI API extension. MobileScannersQBitProducer registers the mobile-scanners contract, currently 1.6.0 (core UI API 1.10 on the maintained local server), through MiddlewareSpecExtensionsMetaData. The framework generates its routes, discovery advertisement and merged OpenAPI from the same endpoint specs. Optional operator-consented device-screen viewing is described in DEVICE-SCREEN-VIEWING.md; app-content remains the default. There is no separate /mobile/v1 API or legacy alias.

The host must also install MobileScannersRouteProvider on the same server. This provider binds verified device headers to the current request; it does not register additional business routes. Keep the versioned QQQ routes enabled. The qbit registers the android client channel while preserving the host's existing channel policies. The demo disables legacy unversioned middleware routes.

Discover before sending credentials

  1. Fetch GET /qqq/v1/version without user or device credentials.
  2. The current Android client requires core semantic version >=1.8.0 <2.0.0, with version, metaData, processes, processCancel, extensions and widgets features, and android in clientChannels.
  3. Find extensions[].name == "mobile-scanners", require extension version >=1.0.0 <2.0.0, and verify pathPrefix == "/qbit/mobile-scanners".
  4. Resolve the extension prefix under the advertised core base path /qqq/v1. Fetch public branding, discover authentication through the core API, and use the existing QQQ session flow.
  5. Discover apps, processes, frontend steps, fields and widgets through the core metadata/process API. Treat process/widget names as metadata, not protocol selectors.

An incompatible or missing contract blocks the client with an upgrade/configuration explanation. Do not silently fall back to undocumented routes. Additive unknown JSON properties are safe to ignore. Unknown device states never mean approved; unsupported required UI components must not be silently skipped.

The discovery document advertises the relative extension pathPrefix; merged OpenAPI at /qqq/v1/openapi.json and /qqq/v1/openapi.yaml contains full paths.

Operations

Every path below is relative to /qqq/v1/qbit/mobile-scanners.

Method and path OpenAPI operationId Authentication Success
GET /branding mobileScannersBranding Public 200, MobileScannersBrandingV1
GET /status mobileScannersStatus Public 200, MobileScannersStatusV1
POST /devices/enroll mobileScannersEnrollDevice QQQ user session, mobile.scan 201, MobileScannersEnrollmentV1
GET /devices/current mobileScannersCurrentDevice QQQ user session, mobile.scan, both device headers 200, MobileScannersDeviceV1
POST /sessions/heartbeat mobileScannersSessionHeartbeat QQQ user session (cookie, bearer or OAuth2), both device headers 200, MobileScannersHeartbeatV1
POST /sessions/current/end mobileScannersEndCurrentSession QQQ user session (cookie, bearer or OAuth2), both device headers 200, MobileScannersSessionEndedV1
POST /sessions/current/screen mobileScannersUploadSessionScreen QQQ user session (cookie, bearer or OAuth2), both device headers 204, no body; its 409 means a stale frame or sequence, not an enrollment conflict

All seven operations also declare 400, 401, 403, 404, 429 and 500 error responses (BasicErrorResponseV1). The published YAML labels every operation's 409 "Installation already enrolled"; that text is accurate only for enrollment. Session behavior is in device sessions and screen viewing.

Public discovery, OpenAPI, branding and connectivity ignore stale device headers. Authentication discovery/session/logout also remain available when a previously enrolled credential is no longer valid. Responses from extension handlers use Cache-Control: no-store.

Enrollment takes MobileScannersEnrollRequestV1: required canonical UUID installationId, optional displayName, manufacturer, model, osVersion and appVersion strings, each at most 200 characters. The JSON request body is limited to 4096 characters. A duplicate installation returns HTTP 409 and never rotates or discloses its existing credential. A new installation starts PENDING and receives its private deviceToken once.

Since extension 1.6, MobileScannersHeartbeatRequestV1 also accepts the same optional manufacturer, model, osVersion and appVersion strings (at most 200 characters, 400 otherwise). An approved device's record is updated only when a sent value differs; omitted, null or blank values leave the enrolled values unchanged. Scan events copy the device record at save time, so an in-place app or OS update is reflected on the next heartbeat and every later scan, while earlier scans keep the versions that produced them. The Android host sends osVersion and appVersion on every heartbeat.

Send X-QQQ-Device-Id and X-QQQ-Device-Token on authenticated device/core metadata, widget and process requests to this same origin. The device ID is a UUID; the token is private and at most 128 characters. These headers supplement the authenticated user session. They never replace user permission checks. Never put a token in metadata, URLs, process values, screenshots, logs or fixtures; never forward credentials to a branding asset origin. Store the issued credential privately. Only its digest is stored server-side.

devices/current returns deviceId, displayName and status, never a token. PENDING and REVOKED devices may read their status. Scanner access still requires APPROVED, checked when launching and saving, including asynchronous/resumed execution. Device approval/revocation uses the ordinary QQQ processes, including single-record and bulk selections.

Errors use core BasicErrorResponseV1, including error, message, code and traceId; X-QQQ-Trace-Id matches the body. Invalid enrollment input is 400/BAD_REQUEST, missing login 401/UNAUTHENTICATED, denied permission or credential 403/PERMISSION_DENIED, duplicate installation 409/CONFLICT, unexpected server failure 500/INTERNAL_ERROR. Error payloads contain no credential or backend details.

SCANNER widget vocabulary

Extension 1.x defines MobileScannersScannerBlockV1, a SCANNER child of the core COMPOSITE widget. The standard core process/widget operations transport it. There is no scanner-specific run endpoint. QQQ RC20's closed backend block enum predates SCANNER, so MobileScannerWidget emits its documented JSON through ordinary QWidgetData.

The block contains required blockId, blockType: "SCANNER" and values. MobileScannersScannerValuesV1 defines required title and instructions. Optional manualLabel and submitLabel default to Manual barcode and Submit barcode. Optional integer maxLength defaults to 4096 and must be within 1–4096. The current server supplies all five values. Native barcode input and manual entry use the same semantics.

MobileScannersScanEventV1 describes values submitted to the current core frontend step: required nonempty string barcode (maximum 4096), canonical UUID eventId and ISO-8601 capturedAt; optional bounded strings symbology and scanSource. Leading zeroes and nonblank decoder identifiers are retained exactly, including vendor names such as LABEL-TYPE-CODE128; barcode text never implies a symbology. Missing or blank type becomes UNKNOWN, or MANUAL for explicit manual source. Missing or blank source becomes UNKNOWN. A retry keeps the same event ID; changing its barcode, type, source, actor, application or executing process is rejected. User/device identity is derived from verified context, not from supplied values.

After core asynchronous polling completes, MobileScannersScanAcknowledgmentV1 describes the response values: scanSaved must be true, acknowledgedEventId must match the submitted event and scanId must be nonempty. A successful HTTP response alone is not a persistence acknowledgment. A retry returns the original scan ID. The process returns its next screen and widget through the same core response.

Accepted scan context and backend actions

Extension 1.3 additionally supplies values.scanContext, described by MobileScannersScanContextV1. It contains scanId, barcode, symbology, scanSource, eventId, capturedAt, receivedAt, userId, deviceId, appId and processName. Dates are ISO-8601 strings. Context comes from the accepted persisted event, rather than a submitted context map. The application comes from server configuration and the process from registered executing metadata; neither comes from request values. This additive property stays optional in the acknowledgment schema for compatibility with older 1.x servers.

Hosts can append ordinary backend steps after the default process's save step. They read input.getValue("scanContext") as a map and branch on its symbology without querying the scan table. The same text may therefore take separate QR and Code 128 actions. The final host step explicitly returns to scan; the unextended default process still loops automatically. Scan-row deduplication does not make arbitrary host actions execute exactly once: those actions must use the stable scanId or eventId for their own idempotency.

Host-owned processes use MobileScannerActions.capture(input, appName) with their static registered application name, and publish the returned immutable map as scanContext. The default overload uses the scanner application. Its permission checker must call the existing MobileDeviceAuthorization.captureCurrent() before async dispatch, reject empty device attribution and retain mobile.scan permission enforcement. Capture rechecks current authority and approval; a client-provided context never authorizes work.

Raw event fields and old receipts are consumed before capture, including rejected attempts, so the next submission cannot inherit a preceding type. Following steps use the nested accepted context. Historical NULL type/source values normalize for comparison and returned context without rewriting those rows or accepting a newly claimed known type on retry. Pre-1.3 process state containing a receipt without context fails closed: leave/cancel and reopen the scanner process. Logout and device re-enrollment are unnecessary.

Exact capture metadata in 1.5

Extension 1.5.0 adds two optional flat string values to MobileScannersScanEventV1 and, when present, to MobileScannersScanContextV1. Neither is a required key and old required fields are unchanged. Missing values are valid for older clients and manual capture.

Value Rule
aimIdentifier Exactly 3 characters matching ^\][A-Za-z][0-9A-Za-z]$: the original AIM identifier from the scanner SDK, never guessed from symbology.
rawDataBase64 Canonical standard padded Base64 of 1 to 8192 decoded bytes (at most 10924 characters): the byte-for-byte SDK getRawData result. The server does not decode it, strip prefixes or assume it equals the UTF-8 of barcode. It may include SDK code-ID framing, so it is exact SDK output, not symbol codewords and not evidence of attested scanner configuration.

A present value that is blank, not a string, malformed, non-canonical (missing or extra padding, whitespace, URL alphabet, non-zero trailing bits) or oversize is rejected by the server with no scan saved. A JSON null is treated as absent. Stored values are nullable columns on mobileScan; historical rows keep NULL, which means absent.

Exact equality of both values, including absent versus present, participates in event retry binding: an identical retry returns the original row and scan ID, while changed bytes, a changed AIM identifier, or adding or dropping either value for an existing eventId is rejected like any other changed scan. The context contains these keys only when the accepted scan has them. Both input keys are consumed before capture (including rejected attempts) and any submitted scanContext is replaced, so the next event cannot inherit them and a client cannot forge them. They carry no identity and no trust elevation, and there is no decoder profile or trust field in this version.

A client sends these fields only when the current server's mobile-scanners version is >=1.5.0 <2.0.0, the registered path is advertised and the server's process metadata permits them. Older servers receive exactly the legacy five keys (barcode, symbology, scanSource, eventId, capturedAt). Hardware qualification of AIM and raw-byte fidelity is separate follow-up work; this transport does not by itself establish exact barcode reproduction.

Schema migration (H2, idempotent, nullable): ALTER TABLE mobileScan ADD COLUMN IF NOT EXISTS aimIdentifier VARCHAR(3) and ... rawDataBase64 VARCHAR(10924) in mobile-scanners-h2.sql. Other backends must add equivalent nullable columns before deploying 1.5.0 servers; do not assume ADD COLUMN IF NOT EXISTS or the type sizes exist elsewhere.

Contract checks and compatibility

The published fragment is mobile-scanners.yaml; the initial release baseline is mobile-scanners-1.0.0.yaml, with additional frozen baselines such as mobile-scanners-1.4.0.yaml (the contract immediately before the 1.5.0 capture metadata) and mobile-scanners-1.5.0.yaml, the frozen 1.5.0 release snapshot that later changes are checked against. MobileScannersApiContractTest rejects generated-document drift, unresolved contract structure, incompatible baseline changes and missing version bumps using QQQ's shared contract checker. Object required fields and scanner numeric bounds are standard OpenAPI keywords, added by small Schema subclasses because RC20's base model does not expose those setters.

For an intentional contract edit, update specs/schemas and the extension semantic version, then regenerate only the current fragment:

.\mvnw.cmd '-Dtest=MobileScannersApiContractTest' '-Dmobile.openapi.update=true' test

Do not refresh the release baseline to hide a compatibility failure. Additive compatible changes require a minor version increment; fixes require a patch increment. Breaking contracts require the corresponding new major contract and an explicit client migration. A server can change supported metadata/screens without rebuilding customer APKs; introducing a new required native component needs a shared client update and compatible contract negotiation.

Root verify checks the committed fragment/baseline and public stale-credential behavior. Demo verify runs the real HTTP/authentication/process/H2 lifecycle, checks discovery and merged operation IDs/security/schema references, validates actual enrollment/device/SCANNER/event/ack payloads against served schemas, and checks structured errors and rejection of legacy routes. It writes sanitized core and extension discovery fixtures under demo/target/android-contract for Android contract tests. Browser/native end-to-end suites provide the final rendered UI and persistence checks.

Scanner presentation hints in 1.4

SCANNER values may include acknowledgmentLabel (default Saved scan) and manualEntryEnabled (default true). Labels are bounded trimmed single-line plain text of 1–80 characters; explicitly invalid labels or nonboolean manual flags are rejected by the native renderer. Echo uses Print queued and disables manual entry because a manually typed value has no authoritative symbology. These hints do not change scanSaved, scanId or matching-event acceptance requirements; Print queued is not physical output confirmation. Older 1.x clients may ignore the optional hints.