Compare commits

..
Author SHA1 Message Date
forge-recovery 07d72ca4ea Merge pull request 'ci: add the isolated postpublish job to the QRun release caller' (#11) from ci-qrun-release-caller-main-v3 into main
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 14s
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 1m7s
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 12s
QRun-IO/ci-policy: ci / java-maven-7 (push) Successful in 1m23s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 4s
Shared Java Maven CI / verify (push) Successful in 2m1s
Shared Java Maven CI / gate (push) Successful in 9s
qrun-release / release (push) Successful in 2m13s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
Shared Java Maven CI / secrets (push) Successful in 13s
2026-11-02 16:12:24 +00:00
7739a7da58 ci: add the isolated postpublish job to the QRun release caller
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (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-docker-3 (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 / 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 / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
Shared Java Maven CI / secrets (push) Successful in 21s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 10s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 19s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
Shared Java Maven CI / verify (push) Successful in 1m28s
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 1m17s
Shared Java Maven CI / gate (push) Successful in 10s
qrun-release / release (push) Successful in 11m1s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Successful in 10m47s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 15:51:01 +00:00
forge-recovery 51eb6f0df2 Merge pull request 'ci: add the QRun release caller' (#8) from ci-qrun-release-caller-main-v1 into main
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 1m23s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 26s
QRun-IO/ci-policy: native / native-result (push) Successful in 5s
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 12s
QRun-IO/ci-policy: ci / java-maven-7 (push) Successful in 2m30s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 5s
Shared Java Maven CI / secrets (push) Successful in 16s
Shared Java Maven CI / verify (push) Successful in 1m17s
Shared Java Maven CI / gate (push) Successful in 10s
qrun-release / release (push) Successful in 10m42s
2026-11-02 10:41:32 +00:00
d88dd1745b ci: move the QRun release caller to the thin v2 contract
QRun-IO/ci-policy: ci / java-docker-2 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-4 (pull_request) Canceled after 39m40s
qrun-release / release (push) Canceled after 40m37s
Shared Java Maven CI / gate (push) Canceled after 0s
Shared Java Maven CI / secrets (push) Successful in 34s
Shared Java Maven CI / verify (push) Successful in 2m43s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (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-docker-3 (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 / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (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 / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (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: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 16s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 1m6s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 4s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Successful in 1m59s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 8s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 06:19:12 +00:00
forge-recoveryandCodex 0c26f0b2a6 ci: add the identical QRun release caller
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 0s
QRun-IO/ci-policy: ci / fork-guard (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-docker-2 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Failing after 1s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Failing after 0s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Failing after 0s
QRun-IO/ci-policy: ci / java-maven-4 (pull_request) Failing after 2m0s
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 04:13:15 +00:00
141 changed files with 178 additions and 17207 deletions

No files matched your search

-4
View File
@@ -5,7 +5,3 @@
*.png binary
*.jpg binary
*.pdf binary
# Retained upstream-derived artifact evidence must preserve the exact hashed bytes.
# Its original whitespace is evidence, not locally maintained source formatting.
integrations/record-files/evidence/**/qbit-collaboration-resolved.pom.xml -text !eol whitespace=-blank-at-eol,-blank-at-eof
+22
View File
@@ -0,0 +1,22 @@
# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository.
name: qrun-release
on:
push: {}
workflow_dispatch: {}
permissions:
contents: read
actions: read
jobs:
release:
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-maven.yaml@qrun-release-v1
with:
runner: linux-small
postpublish:
if: ${{ needs.release.outputs.capture == 'true' }}
needs: release
permissions:
contents: write
actions: write
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1
with:
runner: linux-small
-6
View File
@@ -1,11 +1,5 @@
version: 2
updates:
- package-ecosystem: npm
directory: /e2e
target-branch: develop
schedule:
interval: weekly
open-pull-requests-limit: 3
- package-ecosystem: maven
directories: ['/', '/demo']
target-branch: develop
+1 -22
View File
@@ -22,12 +22,7 @@ jobs:
shell: pwsh
run: |
Install-Module PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser -Force
$scriptPaths = @(Get-ChildItem -LiteralPath scripts, integrations/record-files -File -Filter '*.ps1')
$findings = @($scriptPaths | ForEach-Object {
Invoke-ScriptAnalyzer -Path $_.FullName -Settings scripts/PSScriptAnalyzerSettings.psd1
})
$findings | Format-Table -AutoSize
if($findings.Count -gt 0) { throw "PowerShell analysis reported $($findings.Count) findings." }
Invoke-ScriptAnalyzer -Path scripts -Recurse -Settings scripts/PSScriptAnalyzerSettings.psd1 -EnableExit
verify:
name: Verify demo HTTP contract
runs-on: ubuntu-24.04
@@ -44,20 +39,6 @@ jobs:
run: ./mvnw --batch-mode --no-transfer-progress verify install
- name: Verify native login, enrollment and scan flow
run: ./mvnw --batch-mode --no-transfer-progress -f demo/pom.xml verify
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
cache-dependency-path: e2e/package-lock.json
- name: Prepare isolated browser acceptance
run: |
./mvnw --batch-mode --no-transfer-progress -f demo/pom.xml dependency:build-classpath -Dmdep.outputFile=target/e2e-classpath.txt
npm ci --prefix e2e
cd e2e
npx playwright install --with-deps chromium
- name: Verify record and bulk process screens
working-directory: e2e
run: npm test
- name: Retain test reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
@@ -68,7 +49,5 @@ jobs:
demo/target/surefire-reports/
target/checkstyle-result.xml
demo/target/checkstyle-result.xml
e2e/playwright-report/
e2e/test-results/
retention-days: 14
if-no-files-found: warn
-3
View File
@@ -5,6 +5,3 @@ data/
*.log
.idea/
.flattened-pom.xml
e2e/node_modules/
e2e/test-results/
e2e/playwright-report/
-3
View File
@@ -9,13 +9,10 @@ The Android host is maintained in [QRun-IO/qqq-android](https://github.com/QRun-
- From this directory: `./mvnw verify install`, then `./mvnw -f demo/pom.xml verify`.
- Windows equivalents use `mvnw.cmd`. Checkstyle runs during `validate`; do not disable it or skip tests for a completed change.
- Root tests cover H2 persistence, permissions, concurrent retries and async identity. Demo tests cover the real native HTTP protocol. Hardware acceptance is separate: `docs/TC22-ACCEPTANCE.md`.
- Every table-based process must work from a single record's Actions menu and from bulk selection. Cover explicit row selections and select-all/first-N filters with browser E2E tests, including confirmation, persisted changes, visible results, and unaffected unselected rows. Backend-only process tests do not replace these UI tests.
- Every mobile process needs real native UI-to-server E2E coverage in the Android repository as well as the HTTP contract. Use isolated test accounts, databases and Android application IDs; never reset the user's demo or scanner data to run tests.
## Implementation conventions
- Keep ordinary QQQ app/process/widget metadata and existing native API paths. Do not create a second generic mobile-screen framework.
- All mobile endpoints must be registered in the versioned semantic UI API extension, with discovery, stable operation IDs, typed OpenAPI schemas and compatibility regressions. Keep the Android client channel registered and test real HTTP requests with that channel. Do not add parallel unversioned/custom business routes.
- Kingsrook Java formatting: three spaces, Allman braces, banner documentation, explicit imports, Apache SPDX headers. `checkstyle/checkstyle.xml` is the enforced authority.
- Preserve online-only semantics, idempotent event retries and server-derived user/device attribution.
- Device credentials belong only in scoped HTTP headers and private hashed storage, never process inputs, responses beyond enrollment, fixtures, logs or notes.
+2 -11
View File
@@ -4,14 +4,11 @@ Server support for approved Android handhelds running native QQQ apps. The first
Version one is online only. Devices require an authenticated user and administrator approval. Android renders existing QQQ app, process, frontend-step and widget metadata; this qbit adds the scanner widget and enrollment lifecycle.
App discovery and process execution use the versioned semantic UI/UX API at `/qqq/v1`. Enrollment, branding and status are registered operations in the independently versioned `mobile-scanners` extension at `/qqq/v1/qbit/mobile-scanners`. The Android host checks core and extension compatibility before login. See [the semantic API contract](docs/UI-API.md). Metadata changes using supported native components can be delivered by the server; new required component types need a shared Android client update. This contract does not imply full web-client feature coverage.
## Project layout
- `src/main`: reusable qbit, device enrollment routes, device approval/revocation, scan process and table metadata.
- `src/test`: server behavior and permission checks.
- `demo`: local QQQ server with persistent H2 storage, native administration UI and password login.
- `e2e`: browser acceptance tests against the packaged frontend and isolated real demo server.
- `docs/TC22-ACCEPTANCE.md`: build, install and hardware verification.
## Build
@@ -27,8 +24,6 @@ The wrapper pins Maven 3.9.9. On Unix, use `./mvnw` in place of `mvnw.cmd`. This
For the Windows demo, follow [local startup and credentials](docs/LOCAL-DEMO.md). For the scanner, follow [TC22 installation and acceptance](docs/TC22-ACCEPTANCE.md).
See [process acceptance tests](docs/TESTING.md) for server, browser, and native Android E2E commands. Approval and revocation support record actions and bulk selections with a confirmation and visible result.
## Customer branding and connection diagnostics
Configure the customer name, primary color and optional HTTPS logo on the server; the same Android APK applies them before login and throughout the mobile screens. See [branding configuration](docs/BRANDING.md). The shared launcher remains QQQ Mobile.
@@ -37,7 +32,7 @@ The app footer checks server reachability while foregrounded. Tap it to inspect
## Contributing and CI
Development pull requests target `develop`. CircleCI uses the shared QRun-IO QBit pipeline; GitHub Actions runs the demo HTTP contract, browser E2E and CodeQL. All Maven publication switches start disabled. See [contributing](CONTRIBUTING.md), [CI and release operation](docs/CI-CD.md), [security reporting](SECURITY.md) and the [code of conduct](CODE_OF_CONDUCT.md). This project is licensed under [Apache 2.0](LICENSE).
Development pull requests target `develop`. CircleCI uses the shared QRun-IO QBit pipeline; GitHub Actions runs the demo HTTP contract and CodeQL. All Maven publication switches start disabled. See [contributing](CONTRIBUTING.md), [CI and release operation](docs/CI-CD.md), [security reporting](SECURITY.md) and the [code of conduct](CODE_OF_CONDUCT.md). This project is licensed under [Apache 2.0](LICENSE).
## Embed in a QQQ application
@@ -57,8 +52,4 @@ The app must show server acknowledgement before reporting a save. A network time
## Scope
The tested handheld path uses single-barcode Zebra DataWedge delivery and QQQ table-based login. The Android client also implements metadata-advertised native OAuth2 login for servers providing the explicit UI API 1.9 contract; configured-provider acceptance and production scanner-ingress hardening remain release gates. See the client's [authentication guide](https://github.com/QRun-IO/qqq-android/blob/feature/mobile-scanner-host/docs/AUTHENTICATION.md).
Device/session records, online activity, administrator session termination, own-app screen viewing and optional foreground location are implemented. Extension1.2 adds optional [operator-consented device-screen viewing](docs/DEVICE-SCREEN-VIEWING.md); actual Android projection acceptance remains open. Camera/gallery record uploads use the optional Record Files bridge and require its compatible dependency tuple; production publication and actual camera acceptance remain open. Additional apps use QQQ's native process/widget metadata, and their server-side permission rules must explicitly protect their actions.
A visual mobile app designer, offline workflows, camera-based barcode decoding and applied MDM rollout remain outside this version. Company profile/wallpaper options are researched in the client's provisioning guide. The supplied SQL schema targets H2; other database migrations belong to the host application.
Initial support is single-barcode Zebra DataWedge intent delivery and QQQ table-based username/password login. Other identity providers need Android host configuration before use. Additional apps can use QQQ's existing native process and widget metadata; their server-side permission rules must explicitly protect their actions. A visual mobile app designer, offline workflows, camera scanning, MDM rollout and remote fleet management are separate future work. The supplied SQL schema targets H2; other database migrations belong to the host application.
+2 -12
View File
@@ -9,17 +9,6 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<qqq.version>4.1.0-RC.20</qqq.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qbits</groupId>
@@ -43,7 +32,8 @@
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -50,18 +50,6 @@ public class MobileScannersDemo extends AbstractQQQApplication
this.jdbcUrl = jdbcUrl;
}
/*******************************************************************************
** Keep H2 open between QQQ connections; normal JVM shutdown flushes the file.
*******************************************************************************/
static String fileDatabaseUrl(Path database)
{
return "jdbc:h2:file:" + database.toAbsolutePath().normalize().toString().replace('\\', '/') + ";DATABASE_TO_UPPER=FALSE;DB_CLOSE_DELAY=-1";
}
/*******************************************************************************
** Configure the same public appearance for every client of this demo server.
*******************************************************************************/
@@ -118,8 +106,7 @@ public class MobileScannersDemo extends AbstractQQQApplication
}
instance.getTable("demoUser").getField("passwordHash").setIsHidden(true);
new MobileScannersQBitProducer().withConfig(mobileConfig).produce(instance, "mobile");
for(String name : List.of(MobileScannersQBitProducer.DEVICE, MobileScannersQBitProducer.CREDENTIAL, MobileScannersQBitProducer.SCAN,
MobileScannersQBitProducer.SESSION, MobileScannersQBitProducer.SESSION_BINDING))
for(String name : List.of(MobileScannersQBitProducer.DEVICE, MobileScannersQBitProducer.CREDENTIAL, MobileScannersQBitProducer.SCAN))
{
QTableMetaData table = instance.getTable(name);
table.withBackendDetails(new RDBMSTableBackendDetails().withTableName(name));
@@ -220,7 +207,7 @@ public class MobileScannersDemo extends AbstractQQQApplication
Integer port = args.length == 0 ? 8080 : Integer.valueOf(args[0]);
Path database = Path.of("data", "mobile").toAbsolutePath().normalize();
Files.createDirectories(database.getParent());
MobileScannersDemo application = new MobileScannersDemo(fileDatabaseUrl(database));
MobileScannersDemo application = new MobileScannersDemo("jdbc:h2:file:" + database.toString().replace('\\', '/') + ";DATABASE_TO_UPPER=FALSE");
application.withBranding(System.getenv("MOBILE_BRAND_DISPLAY_NAME"), System.getenv("MOBILE_BRAND_PRIMARY_COLOR"), System.getenv("MOBILE_BRAND_LOGO_URL"));
application.initializeDatabase(System.getenv("MOBILE_DEMO_SCANNER_PASSWORD"), System.getenv("MOBILE_DEMO_ADMIN_PASSWORD"));
application.start(port);
@@ -6,9 +6,6 @@
package com.kingsrook.qbits.mobilescanners.demo;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.net.HttpCookie;
import java.net.ServerSocket;
import java.net.URI;
@@ -22,27 +19,13 @@ import java.sql.DriverManager;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import com.kingsrook.qbits.mobilescanners.MobileScannersQBitProducer;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -55,130 +38,6 @@ class MobileHttpAcceptanceTest
private String origin;
private String deviceId;
private String deviceToken;
private JSONObject schemas;
@TempDir
private Path temporaryDirectory;
/*******************************************************************************
** Each isolated server must resolve its own backend rather than a prior cache.
*******************************************************************************/
@BeforeEach
void resetBackendProviders()
{
com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager.resetConnectionProviders();
}
/*******************************************************************************
** Real process steps branch on decoder context without querying saved scans.
*******************************************************************************/
@Test
void typedScanContextReachesFollowingBackendStepOverHttp() throws Exception
{
String database = "jdbc:h2:mem:typedScans" + UUID.randomUUID().toString().replace("-", "") + ";DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE";
String scannerPassword = UUID.randomUUID().toString();
String adminPassword = UUID.randomUUID().toString();
MobileScannersDemo application = new MobileScannersDemo(database);
application.initializeDatabase(scannerPassword, adminPassword);
application.defineQInstance().getProcess(MobileScannersQBitProducer.CAPTURE)
.withStep(new QBackendStepMetaData().withName("routeTypedScan").withCode(new QCodeReference(TypedScanConsumer.class)));
Integer port;
try(ServerSocket socket = new ServerSocket(0))
{
port = socket.getLocalPort();
}
origin = "http://127.0.0.1:" + port;
QApplicationJavalinServer server = application.start(port);
try
{
assertSemanticDiscovery(false);
String scanner = login("scanner", scannerPassword);
String admin = login("admin", adminPassword);
JSONObject device = new JSONObject(request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", scanner,
new JSONObject().put("installationId", UUID.randomUUID().toString()).toString(), false).body());
deviceId = device.getString("deviceId");
deviceToken = device.getString("deviceToken");
runAdminFromRecord("mobileDeviceApprove", admin);
JSONObject launch = complete(processInit(MobileScannersQBitProducer.CAPTURE, scanner, true), MobileScannersQBitProducer.CAPTURE, scanner, true);
String processId = launch.getString("processUUID");
JSONObject values = null;
for(String type : List.of("CODE128", "QR_CODE"))
{
values = new JSONObject().put("barcode", "00001234").put("symbology", type).put("scanSource", "camera")
.put("eventId", UUID.randomUUID().toString()).put("capturedAt", "2026-11-01T12:00:00Z")
.put("userId", "forged-user").put("deviceId", "forged-device").put("processName", "forged-process");
JSONObject first = complete(processStep(MobileScannersQBitProducer.CAPTURE, processId, "scan", scanner, values, true), MobileScannersQBitProducer.CAPTURE, scanner, true);
JSONObject acknowledgment = first.getJSONObject("values");
assertSchema("MobileScannersScanAcknowledgmentV1", acknowledgment);
JSONObject context = acknowledgment.getJSONObject("scanContext");
assertSchema("MobileScannersScanContextV1", context);
assertEquals(type, context.getString("symbology"));
assertEquals(type, acknowledgment.getString("observedScanType"));
assertEquals("QR_CODE".equals(type) ? "OPEN_QR" : "LOOKUP_ITEM", acknowledgment.getString("scanDecision"));
assertEquals("00001234", context.getString("barcode"));
assertEquals(values.getString("eventId"), context.getString("eventId"));
assertEquals("2026-11-01T12:00:00Z", context.getString("capturedAt"));
assertNotNull(Instant.parse(context.getString("receivedAt")));
assertEquals("scanner", context.getString("userId"));
assertEquals(deviceId, context.getString("deviceId"));
assertEquals(MobileScannersQBitProducer.CAPTURE, context.getString("processName"));
assertEquals(MobileScannersQBitProducer.APP, context.getString("appId"));
JSONObject retry = complete(processStep(MobileScannersQBitProducer.CAPTURE, processId, "scan", scanner, values, true), MobileScannersQBitProducer.CAPTURE, scanner, true);
assertEquals(context.getString("scanId"), retry.getJSONObject("values").getString("scanId"));
assertEquals(type, retry.getJSONObject("values").getString("observedScanType"));
}
JSONObject legacy = new JSONObject().put("barcode", "00001234").put("eventId", UUID.randomUUID().toString())
.put("capturedAt", "2026-11-01T12:00:00Z");
JSONObject unknown = complete(processStep(MobileScannersQBitProducer.CAPTURE, processId, "scan", scanner, legacy, true), MobileScannersQBitProducer.CAPTURE, scanner, true);
assertEquals("UNKNOWN", unknown.getJSONObject("values").getJSONObject("scanContext").getString("symbology"));
assertEquals("UNKNOWN", unknown.getJSONObject("values").getString("observedScanType"));
assertEquals("scanner", unknown.getJSONObject("values").getJSONObject("scanContext").getString("userId"));
int calls = TypedScanConsumer.CALLS.get();
assertNotNull(values);
values.put("symbology", "CODE128");
complete(processStep(MobileScannersQBitProducer.CAPTURE, processId, "scan", scanner, values, true), MobileScannersQBitProducer.CAPTURE, scanner, true, true);
assertEquals(calls, TypedScanConsumer.CALLS.get(), "A conflicting type must not reach the following action.");
assertEquals(3, scanCount(database));
}
finally
{
server.stop();
}
}
/*******************************************************************************
** A host action sees only the accepted process values, with no scan table read.
*******************************************************************************/
public static class TypedScanConsumer implements BackendStep
{
private static final AtomicInteger CALLS = new AtomicInteger();
/***************************************************************************
** Use decoder type to select a business action independently of text.
***************************************************************************/
@Override
public void run(RunBackendStepInput input, RunBackendStepOutput output)
{
CALLS.incrementAndGet();
Map<?, ?> context = (Map<?, ?>) input.getValue("scanContext");
String type = (String) context.get("symbology");
assertNotNull(type);
assertEquals("scanner", context.get("userId"));
assertEquals(MobileScannersQBitProducer.CAPTURE, context.get("processName"));
assertFalse(input.getValues().containsKey("symbology"));
assertFalse(input.getValues().containsKey("barcode"));
output.addValue("observedScanType", type);
output.addValue("scanDecision", "QR_CODE".equals(type) ? "OPEN_QR" : "LOOKUP_ITEM");
output.getProcessState().setNextStepName("scan");
}
}
@@ -202,58 +61,36 @@ class MobileHttpAcceptanceTest
QApplicationJavalinServer server = application.start(port);
try
{
assertSemanticDiscovery(false);
var branding = request("GET", "/qqq/v1/qbit/mobile-scanners/branding", null, null, false);
var branding = request("GET", "/mobile/v1/branding", null, null, false);
assertEquals(200, branding.statusCode());
assertEquals("no-store", branding.headers().firstValue("Cache-Control").orElseThrow());
JSONObject brand = new JSONObject(branding.body());
assertSchema("MobileScannersBrandingV1", brand);
assertEquals(3, brand.length());
assertEquals("Warehouse Mobile", brand.getString("displayName"));
assertEquals("#007A55", brand.getString("primaryColor"));
assertEquals("https://example.com/logo.png", brand.getString("logoUrl"));
assertError(request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", null, "{}", false), 401, "UNAUTHENTICATED");
deviceId = UUID.randomUUID().toString();
deviceToken = "stale-device-token";
assertSemanticDiscovery(true);
assertSchema("MobileScannersBrandingV1", new JSONObject(request("GET", "/qqq/v1/qbit/mobile-scanners/branding", null, null, true).body()));
assertSchema("MobileScannersStatusV1", new JSONObject(request("GET", "/qqq/v1/qbit/mobile-scanners/status", null, null, true).body()));
assertPublicAuthenticationMetadata();
assertDenied(request("GET", "/qqq/v1/metaData", null, null, true));
assertDenied(request("GET", "/qqq/v1/metaData/process/mobileScanCapture", null, null, true));
assertEquals(401, request("POST", "/mobile/v1/devices/enroll", null, "{}", false).statusCode());
String scanner = login("scanner", scannerPassword);
String admin = login("admin", adminPassword);
assertError(request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", scanner, "{}", false), 400, "BAD_REQUEST");
assertError(request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", scanner, "{invalid", false), 400, "BAD_REQUEST");
assertError(request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", scanner, null, false), 403, "PERMISSION_DENIED");
JSONObject enrollment = new JSONObject().put("installationId", UUID.randomUUID().toString())
.put("displayName", "TC22 HTTP acceptance").put("manufacturer", "Zebra").put("model", "TC22")
.put("osVersion", "acceptance-fixture").put("appVersion", "0.1.0").put("description", "倉".repeat(1500));
var enrolled = request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", scanner, enrollment.toString(), false);
.put("osVersion", "acceptance-fixture").put("appVersion", "0.1.0");
var enrolled = request("POST", "/mobile/v1/devices/enroll", scanner, enrollment.toString(), false);
assertTrue(enrolled.statusCode() == 200 || enrolled.statusCode() == 201, "Enrollment must succeed");
JSONObject device = new JSONObject(enrolled.body());
assertSchema("MobileScannersEnrollmentV1", device);
assertError(request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", scanner, enrollment.toString(), false), 409, "CONFLICT");
deviceId = device.getString("deviceId");
deviceToken = device.getString("deviceToken");
assertEquals("PENDING", device.getString("status"));
var pending = request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", scanner, null, true);
assertEquals(200, pending.statusCode());
assertSchema("MobileScannersDeviceV1", new JSONObject(pending.body()));
assertPublicAuthenticationMetadata();
assertDenied(processInit("mobileScanCapture", scanner, true));
assertDenied(processInit("mobileDeviceApprove", scanner, false));
runAdminFromRecord("mobileDeviceApprove", admin);
var current = request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", scanner, null, true);
runAdmin("mobileDeviceApprove", admin);
var current = request("GET", "/mobile/v1/devices/current", scanner, null, true);
assertEquals(200, current.statusCode());
assertSchema("MobileScannersDeviceV1", new JSONObject(current.body()));
assertEquals("APPROVED", new JSONObject(current.body()).getString("status"));
assertFalse(current.body().contains(deviceToken), "Status must never echo the token");
scanner = assertSessionLifecycle(scanner, admin, scannerPassword, database);
JSONObject launch = complete(processInit("mobileScanCapture", scanner, true), "mobileScanCapture", scanner, true);
var metadataResponse = request("GET", "/qqq/v1/metaData?frontendName=qqq-android&frontendVersion=1.0&applicationName=qqq-mobile-scanners&applicationVersion=0.1.0", scanner, null, true);
var metadataResponse = request("GET", "/qqq/v1/metaData?frontendName=qqq-android&frontendVersion=0.1.0&applicationName=qqq-mobile-scanners&applicationVersion=0.1.0", scanner, null, true);
assertEquals(200, metadataResponse.statusCode());
JSONObject metadata = new JSONObject(metadataResponse.body());
assertEquals("composite", metadata.getJSONObject("widgets").getJSONObject("mobileScanner").getString("type"));
@@ -267,15 +104,12 @@ class MobileHttpAcceptanceTest
String processId = launch.getString("processUUID");
assertEquals("scan", launch.getString("nextStep"));
JSONObject widget = launch.getJSONObject("values").getJSONObject("mobileScanner");
assertSchema("MobileScannersScannerBlockV1", widget.getJSONArray("subBlocks").getJSONObject(0));
assertEquals("SCANNER", widget.getJSONArray("subBlocks").getJSONObject(0).getString("blockType"));
String eventId = UUID.randomUUID().toString();
JSONObject values = new JSONObject().put("barcode", "000123456789").put("symbology", "LABEL-TYPE-CODE128")
.put("scanSource", "scanner").put("eventId", eventId).put("capturedAt", Instant.now().toString())
.put("userId", "forged-admin").put("deviceId", "forged-device");
assertSchema("MobileScannersScanEventV1", values);
JSONObject first = complete(processStep("mobileScanCapture", processId, "scan", scanner, values, true), "mobileScanCapture", scanner, true);
assertSchema("MobileScannersScanAcknowledgmentV1", first.getJSONObject("values"));
assertEquals(eventId, first.getJSONObject("values").getString("acknowledgedEventId"));
JSONObject retry = complete(processStep("mobileScanCapture", processId, "scan", scanner, values, true), "mobileScanCapture", scanner, true);
assertEquals(first.getJSONObject("values").getString("scanId"), retry.getJSONObject("values").getString("scanId"));
@@ -292,22 +126,15 @@ class MobileHttpAcceptanceTest
String validToken = deviceToken;
deviceToken = "incorrect-token";
assertError(request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", scanner, null, true), 403, "PERMISSION_DENIED");
assertPublicAuthenticationMetadata();
assertDenied(request("GET", "/qqq/v1/metaData", scanner, null, true));
assertDenied(request("GET", "/qqq/v1/metaData/process/mobileScanCapture", scanner, null, true));
assertDenied(processInit("mobileScanCapture", scanner, true));
deviceToken = validToken;
assertDenied(processInit("mobileScanCapture", scanner, false));
runAdmin("mobileDeviceRevoke", admin);
assertBulkLifecycle(database, scanner, admin);
assertPublicAuthenticationMetadata();
values.put("eventId", UUID.randomUUID().toString());
assertDenied(processStep("mobileScanCapture", processId, "scan", scanner, values, true));
assertEquals(1, scanCount(database));
assertEquals(200, request("POST", "/qqq/v1/logout", scanner, "{}", false).statusCode());
assertEquals(401, request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", scanner, null, true).statusCode());
assertBulkSelfTermination(database, scannerPassword, adminPassword, admin);
assertEquals(401, request("GET", "/mobile/v1/devices/current", scanner, null, true).statusCode());
}
finally
{
@@ -317,540 +144,6 @@ class MobileHttpAcceptanceTest
/*******************************************************************************
** A selected caller is logged out last, regardless of confirmed row order.
*******************************************************************************/
private void assertBulkSelfTermination(String database, String scannerPassword, String adminPassword, String administrator) throws Exception
{
runAdmin("mobileDeviceApprove", administrator);
for(boolean selfFirst : List.of(true, false))
{
String caller = login("admin", adminPassword);
String other = login("scanner", scannerPassword);
String heartbeat = "/qqq/v1/qbit/mobile-scanners/sessions/heartbeat";
String activity = "{\"foreground\":true,\"screenAvailable\":false}";
String selfId = new JSONObject(request("POST", heartbeat, caller, activity, true).body()).getString("sessionId");
String otherId = new JSONObject(request("POST", heartbeat, other, activity, true).body()).getString("sessionId");
String selected = selfFirst ? selfId + "," + otherId : otherId + "," + selfId;
JSONObject selection = new JSONObject().put("recordsParam", "recordIds").put("recordIds", selected).put("tableName", "mobileSession");
JSONObject started = complete(form("/qqq/v1/processes/mobileSessionEnd/init", caller, selection, false), "mobileSessionEnd", caller, false);
processStep("mobileSessionEnd", started.getString("processUUID"), "input", caller, new JSONObject(), false);
long deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos();
int remaining;
do
{
try(var connection = DriverManager.getConnection(database, "sa", ""); var query = connection.prepareStatement("SELECT COUNT(*) FROM demoSession WHERE id IN (?, ?)"))
{
query.setString(1, caller);
query.setString(2, other);
try(var rows = query.executeQuery())
{
rows.next();
remaining = rows.getInt(1);
}
}
if(remaining > 0)
{
Thread.sleep(50);
}
}
while(remaining > 0 && System.nanoTime() < deadline);
assertEquals(0, remaining, "Both selected authentication sessions must be removed, including when the caller is first");
try(var connection = DriverManager.getConnection(database, "sa", ""); var query = connection.prepareStatement("SELECT COUNT(*) FROM mobileSession WHERE id IN (?, ?) AND status = 'ENDED' AND authCleanupStatus = 'REQUESTED'"))
{
query.setString(1, selfId);
query.setString(2, otherId);
try(var rows = query.executeQuery())
{
rows.next();
assertEquals(2, rows.getInt(1));
}
}
assertEquals(401, request("GET", "/qqq/v1/metaData", caller, null, false).statusCode());
assertEquals(401, request("GET", "/qqq/v1/metaData", other, null, false).statusCode());
}
}
/*******************************************************************************
** Approval is rechecked before a heartbeat creates or advances any activity.
*******************************************************************************/
@Test
void heartbeatRejectsPendingAndRevokedDevicesWithoutUpdatingActivity() throws Exception
{
String database = MobileScannersDemo.fileDatabaseUrl(temporaryDirectory.resolve("approval"));
String scannerPassword = UUID.randomUUID().toString();
String adminPassword = UUID.randomUUID().toString();
MobileScannersDemo application = new MobileScannersDemo(database);
application.initializeDatabase(scannerPassword, adminPassword);
int port;
try(ServerSocket socket = new ServerSocket(0))
{
port = socket.getLocalPort();
}
origin = "http://127.0.0.1:" + port;
QApplicationJavalinServer server = application.start(port);
try
{
String scanner = login("scanner", scannerPassword);
String admin = login("admin", adminPassword);
JSONObject enrollment = new JSONObject(request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", scanner,
new JSONObject().put("installationId", UUID.randomUUID().toString()).toString(), false).body());
deviceId = enrollment.getString("deviceId");
deviceToken = enrollment.getString("deviceToken");
String heartbeatPath = "/qqq/v1/qbit/mobile-scanners/sessions/heartbeat";
String activity = "{\"foreground\":true,\"screenAvailable\":true,\"appName\":\"approved-screen\"}";
assertEquals(403, request("POST", heartbeatPath, scanner, activity, true).statusCode());
assertEquals(403, request("POST", heartbeatPath + "/", scanner, activity, true).statusCode());
try(var connection = DriverManager.getConnection(database, "sa", ""); var statement = connection.createStatement();
var rows = statement.executeQuery("SELECT COUNT(*) FROM mobileSession"))
{
assertTrue(rows.next());
assertEquals(0, rows.getInt(1), "Rejected pending heartbeat must not create a session");
}
assertEquals("PENDING", new JSONObject(request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", scanner, null, true).body()).getString("status"));
runAdmin("mobileDeviceApprove", admin);
var approved = request("POST", heartbeatPath, scanner, activity, true);
assertEquals(200, approved.statusCode());
String sessionId = new JSONObject(approved.body()).getString("sessionId");
assertTrue(new JSONObject(approved.body()).getJSONArray("supportedCaptureModes").toList().contains("DEVICE_SCREEN"));
String deviceActivity = "{\"foreground\":false,\"screenAvailable\":true,\"captureMode\":\"DEVICE_SCREEN\"}";
assertEquals(200, request("POST", heartbeatPath, scanner, deviceActivity, true).statusCode());
try(var connection = DriverManager.getConnection(database, "sa", "");
var query = connection.prepareStatement("SELECT captureMode, foreground, screenAvailable FROM mobileSession WHERE id=?"))
{
query.setString(1, sessionId);
try(var rows = query.executeQuery())
{
assertTrue(rows.next());
assertEquals("DEVICE_SCREEN", rows.getString(1));
assertEquals(false, rows.getBoolean(2));
assertEquals(true, rows.getBoolean(3));
}
}
assertEquals(400, request("POST", heartbeatPath, scanner, deviceActivity.replace("DEVICE_SCREEN", "INVALID"), true).statusCode());
assertEquals(200, request("POST", heartbeatPath + "/", scanner, activity, true).statusCode());
runAdmin("mobileDeviceRevoke", admin);
// Age both records beyond the route observer's coalescing window. A denied
// heartbeat must not make an offline revoked device look online again.
var priorSeen = java.sql.Timestamp.from(Instant.now().minusSeconds(60).truncatedTo(java.time.temporal.ChronoUnit.MILLIS));
try(var connection = DriverManager.getConnection(database, "sa", "");
var session = connection.prepareStatement("UPDATE mobileSession SET lastSeenAt=? WHERE id=?");
var device = connection.prepareStatement("UPDATE mobileDevice SET lastSeenAt=? WHERE id=?"))
{
session.setTimestamp(1, priorSeen);
session.setString(2, sessionId);
assertEquals(1, session.executeUpdate());
device.setTimestamp(1, priorSeen);
device.setString(2, deviceId);
assertEquals(1, device.executeUpdate());
}
assertEquals(403, request("POST", heartbeatPath, scanner, activity.replace("approved-screen", "rejected-screen"), true).statusCode());
assertEquals(403, request("POST", heartbeatPath + "/", scanner, activity.replace("approved-screen", "rejected-screen"), true).statusCode());
assertEquals(403, request("POST", heartbeatPath, scanner, deviceActivity, true).statusCode());
try(var connection = DriverManager.getConnection(database, "sa", "");
var statement = connection.prepareStatement("SELECT s.lastSeenAt, s.appName, d.lastSeenAt FROM mobileSession s JOIN mobileDevice d ON s.deviceId=d.id WHERE s.id=?"))
{
statement.setString(1, sessionId);
try(var rows = statement.executeQuery())
{
assertTrue(rows.next());
assertEquals(priorSeen, rows.getTimestamp(1), "Denied heartbeat must not advance session activity");
assertEquals("approved-screen", rows.getString(2));
assertEquals(priorSeen, rows.getTimestamp(3), "Denied heartbeat must not advance device activity");
}
}
var current = request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", scanner, null, true);
assertEquals(200, current.statusCode(), "Revoked devices can still inspect their approval state");
assertEquals("REVOKED", new JSONObject(current.body()).getString("status"));
}
finally
{
server.stop();
try(var connection = DriverManager.getConnection(database, "sa", ""); var statement = connection.createStatement())
{
statement.execute("SHUTDOWN");
}
}
}
/*******************************************************************************
** Foreground heartbeats must not starve complete Android metadata responses.
*******************************************************************************/
@Test
void metadataRemainsResponsiveDuringHeartbeats() throws Exception
{
String database = MobileScannersDemo.fileDatabaseUrl(temporaryDirectory.resolve("mobile"));
String scannerPassword = UUID.randomUUID().toString();
String adminPassword = UUID.randomUUID().toString();
MobileScannersDemo application = new MobileScannersDemo(database);
application.initializeDatabase(scannerPassword, adminPassword);
int port;
try(ServerSocket socket = new ServerSocket(0))
{
port = socket.getLocalPort();
}
origin = "http://127.0.0.1:" + port;
QApplicationJavalinServer server = application.start(port);
try
{
String scanner = login("scanner", scannerPassword);
String admin = login("admin", adminPassword);
JSONObject enrollment = new JSONObject(request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", scanner,
new JSONObject().put("installationId", UUID.randomUUID().toString()).toString(), false).body());
deviceId = enrollment.getString("deviceId");
deviceToken = enrollment.getString("deviceToken");
runAdmin("mobileDeviceApprove", admin);
String heartbeatPath = "/qqq/v1/qbit/mobile-scanners/sessions/heartbeat";
String activity = "{\"foreground\":true,\"screenAvailable\":true}";
assertEquals(200, request("POST", heartbeatPath, scanner, activity, true).statusCode());
var heartbeats = java.util.concurrent.CompletableFuture.runAsync(() ->
{
try
{
for(int index = 0; index < 30; index++)
{
assertEquals(200, request("POST", heartbeatPath, scanner, activity, true).statusCode());
Thread.sleep(100);
}
}
catch(Exception failure)
{
throw new java.util.concurrent.CompletionException(failure);
}
});
for(int index = 0; index < 4; index++)
{
var metadata = client.send(headers("/qqq/v1/metaData?frontendName=qqq-android&frontendVersion=1.0&applicationName=qqq-mobile-scanners&applicationVersion=0.1.0", scanner, true)
.timeout(Duration.ofSeconds(8)).GET().build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, metadata.statusCode());
assertTrue(new JSONObject(metadata.body()).getJSONObject("apps").has("mobileScanApp"));
}
heartbeats.get(10, java.util.concurrent.TimeUnit.SECONDS);
}
finally
{
server.stop();
try(var connection = DriverManager.getConnection(database, "sa", ""); var statement = connection.createStatement())
{
statement.execute("SHUTDOWN");
}
}
}
/*******************************************************************************
** Real HTTP binding, leased images, end enforcement and fresh sign-in recovery.
*******************************************************************************/
private String assertSessionLifecycle(String scanner, String admin, String password, String database) throws Exception
{
String endpoint = "/qqq/v1/qbit/mobile-scanners/sessions";
JSONObject activity = new JSONObject().put("foreground", true).put("screenAvailable", true)
.put("appName", "mobileScanApp").put("screenLabel", "Scan Capture");
var response = request("POST", endpoint + "/heartbeat", scanner, activity.toString(), true);
assertEquals(200, response.statusCode());
JSONObject heartbeat = new JSONObject(response.body());
assertSchema("MobileScannersHeartbeatV1", heartbeat);
String id = heartbeat.getString("sessionId");
String profilePath = "/qqq/v1/table/mobileDevice/" + deviceId;
JSONObject profile = new JSONObject().put("shortName", "HTTP-DOCK-01").put("displayName", "HTTP receiving").put("description", "Profile acceptance");
assertDenied(request("PATCH", profilePath, scanner, profile.toString(), true));
assertEquals(200, request("PATCH", profilePath, admin, profile.toString(), false).statusCode());
assertEquals("http-dock-01", new JSONObject(request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", scanner, null, true).body()).getString("shortName"));
assertDenied(request("PATCH", profilePath, admin, "{\"status\":\"REVOKED\"}", false));
assertDeviceStatus(database, deviceId, "APPROVED");
String viewBody = new JSONObject().put("id", id).toString();
assertDenied(request("POST", "/qqq/v1/widget/mobileSessionScreen", scanner, viewBody, true));
assertDenied(request("POST", "/qqq/v1/table/mobileSessionBinding/query", admin, "{}", false));
// A browser administrator and native scanner must share the same viewing demand.
var waiting = client.send(HttpRequest.newBuilder(URI.create(origin + "/qqq/v1/widget/mobileSessionScreen"))
.timeout(Duration.ofSeconds(20)).header("Cookie", "sessionUUID=" + admin).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(viewBody)).build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, waiting.statusCode());
assertTrue(new JSONObject(waiting.body()).has("blocks"), "Viewing widget response fields: " + new JSONObject(waiting.body()).keySet());
heartbeat = new JSONObject(request("POST", endpoint + "/heartbeat", scanner, activity.toString(), true).body());
JSONObject lease = heartbeat.getJSONObject("screenMirror");
assertSchema("MobileScannersScreenLeaseV1", lease);
ByteArrayOutputStream image = new ByteArrayOutputStream();
ImageIO.write(new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB), "jpeg", image);
JSONObject frame = new JSONObject().put("leaseId", lease.getString("leaseId")).put("sequence", lease.getLong("nextSequence"))
.put("width", 2).put("height", 2).put("mimeType", "image/jpeg").put("capturedAt", Instant.now().toString())
.put("imageBase64", Base64.getEncoder().encodeToString(image.toByteArray()));
assertEquals(204, request("POST", endpoint + "/current/screen", scanner, frame.toString(), true).statusCode());
var visible = request("POST", "/qqq/v1/widget/mobileSessionScreen", admin, viewBody, false);
assertTrue(visible.body().contains("data:image/jpeg;base64,"));
assertEquals(409, request("POST", endpoint + "/current/screen", scanner, frame.toString(), true).statusCode());
activity.put("screenAvailable", false).put("captureBlockedReason", "PASSWORD_FIELD");
assertEquals(200, request("POST", endpoint + "/heartbeat", scanner, activity.toString(), true).statusCode());
assertFalse(request("POST", "/qqq/v1/widget/mobileSessionScreen", admin, viewBody, false).body().contains("data:image/jpeg"));
assertEquals(403, request("POST", endpoint + "/current/screen", scanner, frame.toString(), true).statusCode());
JSONObject selection = new JSONObject().put("recordsParam", "recordIds").put("recordIds", id).put("tableName", "mobileSession");
JSONObject started = complete(form("/qqq/v1/processes/mobileSessionEnd/init", admin, selection, false), "mobileSessionEnd", admin, false);
assertEquals("input", started.getString("nextStep"));
JSONObject ended = complete(processStep("mobileSessionEnd", started.getString("processUUID"), "input", admin,
new JSONObject().put("reason", "HTTP acceptance"), false), "mobileSessionEnd", admin, false);
assertEquals("1 session ended.", ended.getJSONObject("values").getString("result"));
assertEquals(401, request("POST", endpoint + "/heartbeat", scanner, activity.toString(), true).statusCode());
assertEquals(401, request("GET", "/qqq/v1/metaData", scanner, null, false).statusCode());
assertEquals(200, request("GET", "/qqq/v1/qbit/mobile-scanners/status", null, null, false).statusCode());
try(var connection = DriverManager.getConnection(database, "sa", ""); var statement = connection.prepareStatement("SELECT status, authCleanupStatus FROM mobileSession WHERE id = ?"))
{
statement.setString(1, id);
try(var rows = statement.executeQuery())
{
assertTrue(rows.next());
assertEquals("ENDED", rows.getString("status"));
assertEquals("REQUESTED", rows.getString("authCleanupStatus"));
}
}
assertDeviceStatus(database, deviceId, "APPROVED");
scanner = login("scanner", password);
JSONObject fresh = new JSONObject(request("POST", endpoint + "/heartbeat", scanner, activity.toString(), true).body());
assertFalse(id.equals(fresh.getString("sessionId")));
var selfEnded = request("POST", endpoint + "/current/end", scanner, "{\"reason\":\"SERVER_CHANGE\"}", true);
try(var connection = DriverManager.getConnection(database, "sa", ""); var statement = connection.prepareStatement("SELECT status FROM mobileSession WHERE id = ?"))
{
statement.setString(1, fresh.getString("sessionId"));
try(var rows = statement.executeQuery())
{
assertTrue(rows.next());
assertEquals("ENDED", rows.getString("status"), "Own-end must persist a tombstone before returning");
}
}
assertEquals(200, selfEnded.statusCode(), selfEnded.body());
assertEquals(401, request("GET", "/qqq/v1/metaData", scanner, null, false).statusCode());
return login("scanner", password);
}
/*******************************************************************************
** Real server discovery and merged OpenAPI resolve every extension reference.
*******************************************************************************/
private void assertSemanticDiscovery(boolean device) throws Exception
{
var response = request("GET", "/qqq/v1/version", null, null, device);
assertEquals(200, response.statusCode());
JSONObject version = new JSONObject(response.body());
assertTrue(version.getString("apiVersion").startsWith("1."));
assertEquals("/qqq/v1", version.getString("basePath"));
assertTrue(version.getJSONArray("clientChannels").toList().contains("android"), "The native Android channel must be advertised before sign-in");
JSONArray extensions = version.getJSONArray("extensions");
JSONObject mobile = null;
for(int i = 0; i < extensions.length(); i++)
{
if("mobile-scanners".equals(extensions.getJSONObject(i).getString("name")))
{
mobile = extensions.getJSONObject(i);
}
}
assertNotNull(mobile);
assertEquals("1.3.0", mobile.getString("version"));
assertEquals("/qbit/mobile-scanners", mobile.getString("pathPrefix"));
var openApiResponse = request("GET", "/qqq/v1/openapi.json", null, null, device);
assertEquals(200, openApiResponse.statusCode());
JSONObject openApi = new JSONObject(openApiResponse.body());
schemas = openApi.getJSONObject("components").getJSONObject("schemas");
JSONObject paths = openApi.getJSONObject("paths");
for(String[] operation : List.of(new String[] { "branding", "get", "mobileScannersBranding" },
new String[] { "status", "get", "mobileScannersStatus" },
new String[] { "devices/enroll", "post", "mobileScannersEnrollDevice" },
new String[] { "devices/current", "get", "mobileScannersCurrentDevice" }))
{
JSONObject method = paths.getJSONObject("/qqq/v1/qbit/mobile-scanners/" + operation[0]).getJSONObject(operation[1]);
assertEquals(operation[2], method.getString("operationId"));
if(operation[0].startsWith("devices/"))
{
assertTrue(method.getJSONArray("security").length() > 0);
}
else
{
assertFalse(method.has("security"));
}
assertReferencesResolve(method, openApi);
}
for(String name : schemas.keySet())
{
if(name.startsWith("MobileScanners"))
{
assertReferencesResolve(schemas.getJSONObject(name), openApi);
}
}
assertEquals(404, request("GET", "/mobile/v1/branding", null, null, false).statusCode());
Path fixtures = Path.of("target", "android-contract");
Files.createDirectories(fixtures);
Files.writeString(fixtures.resolve("version.json"), version.toString(2));
Files.writeString(fixtures.resolve("openapi.json"), openApi.toString(2));
}
/*******************************************************************************
** Walk request/response schemas rather than merely checking path existence.
*******************************************************************************/
private void assertReferencesResolve(Object value, JSONObject document)
{
if(value instanceof JSONObject object)
{
if(object.has("$ref"))
{
assertNotNull(document.query(object.getString("$ref")));
}
for(String key : object.keySet())
{
assertReferencesResolve(object.get(key), document);
}
}
else if(value instanceof JSONArray array)
{
for(Object item : array)
{
assertReferencesResolve(item, document);
}
}
}
/*******************************************************************************
** Validate actual mobile payloads against the served versioned schema.
*******************************************************************************/
private void assertSchema(String name, Object value)
{
assertSchemaValue(schemas.getJSONObject(name), value);
}
/*******************************************************************************
** Validate actual typed payloads, including each advertised array item.
*******************************************************************************/
private void assertSchemaValue(JSONObject schema, Object value)
{
if(schema.has("$ref"))
{
assertSchema(schema.getString("$ref").substring("#/components/schemas/".length()), value);
return;
}
switch(schema.getString("type"))
{
case "array" ->
{
assertTrue(value instanceof JSONArray);
JSONArray array = (JSONArray) value;
assertTrue(array.length() >= schema.optInt("minItems", 0));
assertTrue(array.length() <= schema.optInt("maxItems", Integer.MAX_VALUE));
JSONObject items = schema.getJSONObject("items");
for(Object item : array)
{
assertSchemaValue(items, item);
}
}
case "object" ->
{
assertTrue(value instanceof JSONObject);
JSONObject object = (JSONObject) value;
JSONArray required = schema.optJSONArray("required");
if(required != null)
{
for(Object key : required)
{
assertTrue(object.has(key.toString()) && !object.isNull(key.toString()), "Missing required property: " + key);
}
}
JSONObject properties = schema.getJSONObject("properties");
for(String key : properties.keySet())
{
if(object.has(key))
{
assertSchemaValue(properties.getJSONObject(key), object.get(key));
}
}
}
case "string" ->
{
assertTrue(value instanceof String);
assertTrue(((String) value).length() <= schema.optInt("maxLength", Integer.MAX_VALUE));
if(schema.has("enum"))
{
assertTrue(schema.getJSONArray("enum").toList().contains(value));
}
}
case "boolean" -> assertTrue(value instanceof Boolean);
case "integer" ->
{
assertTrue(value instanceof Integer || value instanceof Long);
assertTrue(((Number) value).longValue() >= schema.optLong("minimum", Long.MIN_VALUE));
assertTrue(((Number) value).longValue() <= schema.optLong("maximum", Long.MAX_VALUE));
}
case "number" ->
{
assertTrue(value instanceof Number);
assertTrue(((Number) value).doubleValue() >= schema.optDouble("minimum", -Double.MAX_VALUE));
assertTrue(((Number) value).doubleValue() <= schema.optDouble("maximum", Double.MAX_VALUE));
}
default -> throw new AssertionError("Unexpected mobile schema type");
}
}
/*******************************************************************************
** Array validation must reject malformed capability types and enum members.
*******************************************************************************/
@Test
void schemaArrayValidationChecksItemsAndBounds()
{
JSONObject schema = new JSONObject("{\"type\":\"array\",\"minItems\":1,\"maxItems\":2,\"items\":{\"type\":\"string\",\"enum\":[\"APP_CONTENT\",\"DEVICE_SCREEN\"]}}");
assertSchemaValue(schema, new JSONArray(List.of("APP_CONTENT", "DEVICE_SCREEN")));
assertThrows(AssertionError.class, () -> assertSchemaValue(schema, "DEVICE_SCREEN"));
assertThrows(AssertionError.class, () -> assertSchemaValue(schema, new JSONArray(List.of(1))));
assertThrows(AssertionError.class, () -> assertSchemaValue(schema, new JSONArray(List.of("UNKNOWN"))));
assertThrows(AssertionError.class, () -> assertSchemaValue(schema, new JSONArray()));
assertThrows(AssertionError.class, () -> assertSchemaValue(schema, new JSONArray(List.of("APP_CONTENT", "DEVICE_SCREEN", "APP_CONTENT"))));
}
/*******************************************************************************
** Error status, stable code and trace ID share the core semantic envelope.
*******************************************************************************/
private void assertError(HttpResponse<String> response, int status, String code)
{
assertEquals(status, response.statusCode());
JSONObject error = new JSONObject(response.body());
assertSchema("BasicErrorResponseV1", error);
assertEquals(code, error.getString("code"));
assertEquals(error.getString("error"), error.getString("message"));
assertFalse(error.getString("traceId").isEmpty());
assertEquals(error.getString("traceId"), response.headers().firstValue("X-QQQ-Trace-Id").orElseThrow());
if(deviceToken != null)
{
assertFalse(response.body().contains(deviceToken));
}
}
/*******************************************************************************
** A returning handheld can discover login without a session or valid device.
*******************************************************************************/
private void assertPublicAuthenticationMetadata() throws Exception
{
var anonymous = request("GET", "/qqq/v1/metaData/authentication", null, null, false);
var returningDevice = request("GET", "/qqq/v1/metaData/authentication", null, null, true);
assertEquals(200, anonymous.statusCode());
assertEquals(200, returningDevice.statusCode(), "Login discovery must ignore persisted device credentials");
assertEquals(new JSONObject(anonymous.body()).toMap(), new JSONObject(returningDevice.body()).toMap());
}
/*******************************************************************************
** Native login exchanges Basic credentials for a QQQ user session.
*******************************************************************************/
@@ -858,7 +151,7 @@ class MobileHttpAcceptanceTest
{
String basic = Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
var response = client.send(HttpRequest.newBuilder(URI.create(origin + "/qqq/v1/manageSession"))
.header("Authorization", "Basic " + basic).header("Content-Type", "application/json").header("X-QQQ-Client", "android/qqq-mobile")
.header("Authorization", "Basic " + basic).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{}")).build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode(), "Native login failed");
return response.headers().allValues("Set-Cookie").stream().flatMap(value -> HttpCookie.parse(value).stream())
@@ -873,104 +166,7 @@ class MobileHttpAcceptanceTest
private void runAdmin(String name, String session) throws Exception
{
JSONObject launched = complete(processInit(name, session, false), name, session, false);
JSONObject applied = complete(processStep(name, launched.getString("processUUID"), "input", session, new JSONObject().put("deviceId", deviceId), false), name, session, false);
finishAdmin(name, session, applied, 1);
}
/*******************************************************************************
** Record actions carry a selection, not a manually entered device ID.
*******************************************************************************/
private void runAdminFromRecord(String name, String session) throws Exception
{
JSONObject selection = new JSONObject().put("recordsParam", "recordIds").put("recordIds", deviceId).put("tableName", "mobileDevice");
JSONObject launched = complete(form("/qqq/v1/processes/" + name + "/init", session, selection, false), name, session, false);
assertEquals("input", launched.getString("nextStep"));
assertEquals(deviceId, launched.getJSONObject("values").optString("deviceId"), "Record launch must prefill the selected device for confirmation");
var beforeConfirmation = request("GET", "/qqq/v1/qbit/mobile-scanners/devices/current", session, null, true);
assertEquals("PENDING", new JSONObject(beforeConfirmation.body()).getString("status"), "Opening the process must not approve the device");
JSONObject applied = complete(processStep(name, launched.getString("processUUID"), "input", session, new JSONObject(), false), name, session, false);
finishAdmin(name, session, applied, 1);
}
/*******************************************************************************
** Bulk actions affect exactly the selection and retain administrator checks.
*******************************************************************************/
private void assertBulkLifecycle(String database, String scanner, String admin) throws Exception
{
String second = enrollAdditionalDevice(scanner, "Bulk selected device");
String unselected = enrollAdditionalDevice(scanner, "Unselected device");
String ids = deviceId + "," + second;
JSONObject explicit = new JSONObject().put("recordsParam", "recordIds").put("recordIds", ids).put("tableName", "mobileDevice");
JSONObject filter = new JSONObject().put("criteria", List.of(new JSONObject().put("fieldName", "id").put("operator", "IN").put("values", List.of(deviceId, second))));
JSONObject filtered = new JSONObject().put("recordsParam", "filterJSON").put("filterJSON", filter.toString()).put("tableName", "mobileDevice");
for(JSONObject selection : List.of(explicit, filtered))
{
for(String process : new String[] { "mobileDeviceApprove", "mobileDeviceRevoke" })
{
assertDenied(form("/qqq/v1/processes/" + process + "/init", scanner, selection, false));
JSONObject launched = complete(form("/qqq/v1/processes/" + process + "/init", admin, selection, false), process, admin, false);
assertEquals("input", launched.getString("nextStep"));
assertEquals(Set.of(deviceId, second), Set.of(launched.getJSONObject("values").getString("deviceId").split(",")));
assertDeviceStatus(database, unselected, "PENDING");
JSONObject applied = complete(processStep(process, launched.getString("processUUID"), "input", admin,
new JSONObject(), false), process, admin, false);
String expected = process.equals("mobileDeviceApprove") ? "APPROVED" : "REVOKED";
assertDeviceStatus(database, deviceId, expected);
assertDeviceStatus(database, second, expected);
assertDeviceStatus(database, unselected, "PENDING");
finishAdmin(process, admin, applied, 2);
}
}
}
/*******************************************************************************
** Administrators see an explicit result and can finish the process normally.
*******************************************************************************/
private void finishAdmin(String name, String session, JSONObject applied, int count) throws Exception
{
assertEquals("result", applied.getString("nextStep"));
assertEquals(count, applied.getJSONObject("values").getInt("processedDeviceCount"));
assertFalse(applied.getJSONObject("values").getString("result").isBlank());
JSONObject finished = complete(processStep(name, applied.getString("processUUID"), "result", session, new JSONObject(), false), name, session, false);
assertTrue(finished.optString("nextStep").isEmpty());
}
/*******************************************************************************
** Enroll a fixture through the actual API without exposing its credential.
*******************************************************************************/
private String enrollAdditionalDevice(String session, String label) throws Exception
{
JSONObject enrollment = new JSONObject().put("installationId", UUID.randomUUID().toString()).put("displayName", label)
.put("manufacturer", "Zebra").put("model", "TC22").put("osVersion", "acceptance-fixture").put("appVersion", "0.1.0").put("description", "倉".repeat(1500));
var response = request("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", session, enrollment.toString(), false);
assertTrue(response.statusCode() == 200 || response.statusCode() == 201);
return new JSONObject(response.body()).getString("deviceId");
}
/*******************************************************************************
** Independently verify persisted lifecycle changes and untouched devices.
*******************************************************************************/
private void assertDeviceStatus(String database, String id, String expected) throws Exception
{
try(var connection = DriverManager.getConnection(database, "sa", ""); var query = connection.prepareStatement("SELECT status FROM mobileDevice WHERE id = ?"))
{
query.setString(1, id);
try(var rows = query.executeQuery())
{
assertTrue(rows.next());
assertEquals(expected, rows.getString("status"));
}
}
complete(processStep(name, launched.getString("processUUID"), "input", session, new JSONObject().put("deviceId", deviceId), false), name, session, false);
}
@@ -1003,15 +199,7 @@ class MobileHttpAcceptanceTest
String boundary = "qqq-mobile-" + UUID.randomUUID();
HttpRequest.Builder builder = headers(path, session, device).header("Content-Type", "multipart/form-data; boundary=" + boundary);
String body = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"values\"\r\n\r\n" + values
+ "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"stepTimeoutMillis\"\r\n\r\n1\r\n";
for(String key : new String[] { "recordsParam", "recordIds", "filterJSON" })
{
if(values.has(key))
{
body += "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + key + "\"\r\n\r\n" + values.getString(key) + "\r\n";
}
}
body += "--" + boundary + "--\r\n";
+ "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"stepTimeoutMillis\"\r\n\r\n1\r\n--" + boundary + "--\r\n";
return client.send(builder.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
}
@@ -1021,16 +209,6 @@ class MobileHttpAcceptanceTest
** Poll native jobs to completion with a bounded deadline.
*******************************************************************************/
private JSONObject complete(HttpResponse<String> response, String process, String session, boolean device) throws Exception
{
return complete(response, process, session, device, false);
}
/*******************************************************************************
** Await both accepted and rejected asynchronous process steps before checking.
*******************************************************************************/
private JSONObject complete(HttpResponse<String> response, String process, String session, boolean device, boolean expectError) throws Exception
{
assertEquals(200, response.statusCode(), "Process HTTP failure");
JSONObject body = new JSONObject(response.body());
@@ -1045,7 +223,7 @@ class MobileHttpAcceptanceTest
assertEquals(200, response.statusCode());
body = new JSONObject(response.body());
}
assertEquals(expectError, body.has("error"), "Unexpected native process outcome.");
assertFalse(body.has("error"), "Native process error: " + body.optString("error"));
return body;
}
@@ -1066,8 +244,7 @@ class MobileHttpAcceptanceTest
*******************************************************************************/
private HttpRequest.Builder headers(String path, String session, boolean device)
{
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(origin + path)).timeout(Duration.ofSeconds(20))
.header("X-QQQ-Client", "android/qqq-mobile");
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(origin + path)).timeout(Duration.ofSeconds(20));
if(session != null)
{
builder.header("Cookie", "sessionUUID=" + session);
@@ -1,234 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners.demo;
import java.io.File;
import java.net.HttpCookie;
import java.net.ServerSocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Exercise the same standalone file-database entry point as native acceptance.
*******************************************************************************/
class MobileStandalonePresenceTest
{
@TempDir
private Path directory;
private final HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
private String origin;
private String deviceId;
private String deviceToken;
/***************************************************************************
** A non-UTC child JVM must keep receiving viewing leases after fifteen seconds.
***************************************************************************/
@Test
@Timeout(90)
void standaloneFileServerRetainsFreshPresenceAndViewingDemand() throws Exception
{
int port;
try(ServerSocket socket = new ServerSocket(0))
{
port = socket.getLocalPort();
}
origin = "http://127.0.0.1:" + port;
String scannerPassword = UUID.randomUUID().toString();
String adminPassword = UUID.randomUUID().toString();
String classpath = Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator))
.map(value -> Path.of(value).toAbsolutePath().normalize().toString()).collect(Collectors.joining(File.pathSeparator));
String executable = System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java";
ProcessBuilder builder = new ProcessBuilder(Path.of(System.getProperty("java.home"), "bin", executable).toString(),
"-XX:ActiveProcessorCount=2", "-Xmx512m", "-Duser.timezone=America/Chicago", "-cp", classpath,
MobileScannersDemo.class.getName(), String.valueOf(port));
builder.directory(directory.toFile());
builder.environment().put("MOBILE_DEMO_SCANNER_PASSWORD", scannerPassword);
builder.environment().put("MOBILE_DEMO_ADMIN_PASSWORD", adminPassword);
builder.redirectOutput(ProcessBuilder.Redirect.DISCARD).redirectError(ProcessBuilder.Redirect.DISCARD);
Process server = builder.start();
try
{
awaitServer(server);
String scanner = login("scanner", scannerPassword);
String admin = login("admin", adminPassword);
JSONObject enrolled = json("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", scanner,
new JSONObject().put("installationId", UUID.randomUUID().toString()).put("displayName", "Standalone presence regression"), true);
deviceId = enrolled.getString("deviceId");
deviceToken = enrolled.getString("deviceToken");
String process = "/qqq/v1/processes/mobileDeviceApprove";
JSONObject launched = form(process + "/init", admin, new JSONObject());
String processId = launched.getString("processUUID");
JSONObject applied = form(process + "/" + processId + "/step/input", admin, new JSONObject().put("deviceId", deviceId));
assertEquals("result", applied.getString("nextStep"));
form(process + "/" + processId + "/step/result", admin, new JSONObject());
JSONObject activity = new JSONObject().put("foreground", true).put("screenAvailable", true);
Instant began = Instant.now();
for(int iteration = 0; iteration < 4; iteration++)
{
if(iteration > 0)
{
Thread.sleep(6000);
}
JSONObject heartbeat = json("POST", "/qqq/v1/qbit/mobile-scanners/sessions/heartbeat", scanner, activity, true);
String sessionId = heartbeat.getString("sessionId");
JSONObject row = json("POST", "/qqq/v1/table/mobileSession/query", admin, new JSONObject(), false)
.getJSONArray("records").getJSONObject(0).getJSONObject("values");
assertEquals(sessionId, row.getString("id"));
Instant seen = Instant.parse(row.getString("lastSeenAt"));
Instant serverTime = Instant.parse(heartbeat.getString("serverTime"));
long ageMillis = Duration.between(seen, serverTime).toMillis();
assertTrue(Math.abs(ageMillis) < 3000,
"Heartbeat age must be current: ageMillis=" + ageMillis + ", seen=" + seen + ", serverTime=" + serverTime);
JSONObject widget = json("POST", "/qqq/v1/widget/mobileSessionScreen", admin, new JSONObject().put("id", sessionId), false);
String status = widget.getJSONArray("blocks").getJSONObject(0).getJSONObject("values").getString("text");
assertEquals("Waiting for a current app screen", status);
JSONObject next = json("POST", "/qqq/v1/qbit/mobile-scanners/sessions/heartbeat", scanner, activity, true);
assertTrue(next.has("screenMirror"), "Fresh native heartbeat must receive the browser viewer's lease");
}
assertTrue(Duration.between(began, Instant.now()).getSeconds() > 15);
}
finally
{
server.destroy();
if(!server.waitFor(5, TimeUnit.SECONDS))
{
server.destroyForcibly();
assertTrue(server.waitFor(5, TimeUnit.SECONDS), "Isolated standalone server must stop");
}
}
}
/***************************************************************************
** Bound startup without printing child environment variables or responses.
***************************************************************************/
private void awaitServer(Process server) throws Exception
{
long deadline = System.nanoTime() + Duration.ofSeconds(25).toNanos();
while(System.nanoTime() < deadline)
{
assertTrue(server.isAlive(), "Standalone demo exited during startup");
try
{
if(client.send(HttpRequest.newBuilder(URI.create(origin + "/qqq/v1/version")).timeout(Duration.ofSeconds(2)).GET().build(),
HttpResponse.BodyHandlers.discarding()).statusCode() == 200)
{
return;
}
}
catch(java.io.IOException ignored)
{
// The child has not bound its loopback socket yet.
}
Thread.sleep(100);
}
throw new AssertionError("Standalone demo startup timed out");
}
/***************************************************************************
** Passwords remain exclusively in the request header and child environment.
***************************************************************************/
private String login(String username, String password) throws Exception
{
String basic = Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
var response = client.send(headers("/qqq/v1/manageSession", null, true).header("Authorization", "Basic " + basic)
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString("{}")).build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode(), "Standalone login failed");
return response.headers().allValues("Set-Cookie").stream().flatMap(value -> HttpCookie.parse(value).stream())
.filter(cookie -> "sessionUUID".equals(cookie.getName())).findFirst().orElseThrow().getValue();
}
/***************************************************************************
** Browser requests omit the native channel and device headers deliberately.
***************************************************************************/
private HttpRequest.Builder headers(String path, String session, boolean nativeClient)
{
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(origin + path)).timeout(Duration.ofSeconds(10));
if(session != null)
{
builder.header("Cookie", "sessionUUID=" + session);
}
if(nativeClient)
{
builder.header("X-QQQ-Client", "android/qqq-mobile");
if(deviceId != null)
{
builder.header("X-QQQ-Device-Id", deviceId).header("X-QQQ-Device-Token", deviceToken);
}
}
return builder;
}
/***************************************************************************
** Use the ordinary versioned API; never print credential-bearing bodies.
***************************************************************************/
private JSONObject json(String method, String path, String session, JSONObject body, boolean nativeClient) throws Exception
{
var response = client.send(headers(path, session, nativeClient).header("Content-Type", "application/json")
.method(method, HttpRequest.BodyPublishers.ofString(body.toString())).build(), HttpResponse.BodyHandlers.ofString());
assertTrue(response.statusCode() == 200 || response.statusCode() == 201, "Standalone JSON operation failed: " + path + " status=" + response.statusCode());
return new JSONObject(response.body());
}
/***************************************************************************
** Drive the real approval process, including a bounded async job handoff.
***************************************************************************/
private JSONObject form(String path, String session, JSONObject values) throws Exception
{
String boundary = "qqq-presence-" + UUID.randomUUID();
String body = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"values\"\r\n\r\n" + values
+ "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"stepTimeoutMillis\"\r\n\r\n5000\r\n--" + boundary + "--\r\n";
var response = client.send(headers(path, session, false).header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode(), "Standalone approval process failed");
JSONObject result = new JSONObject(response.body());
long deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos();
String processId = result.optString("processUUID");
String jobId = result.optString("jobUUID");
while("JOB_STARTED".equals(result.optString("type")) || "RUNNING".equals(result.optString("type")))
{
assertTrue(System.nanoTime() < deadline, "Standalone approval job timed out");
Thread.sleep(25);
var poll = client.send(headers("/qqq/v1/processes/mobileDeviceApprove/" + processId + "/status/" + jobId, session, false).GET().build(),
HttpResponse.BodyHandlers.ofString());
assertEquals(200, poll.statusCode());
result = new JSONObject(poll.body());
}
assertFalse(result.has("error"), "Standalone approval returned a process error");
return result;
}
}
@@ -1,213 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners.demo;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Preserve historical instants when replacing H2 zoned timestamp columns.
*******************************************************************************/
class MobileTimestampMigrationTest
{
@TempDir
private Path directory;
/***************************************************************************
** A separate non-UTC JVM avoids changing the timezone of concurrent tests.
***************************************************************************/
@Test
@Timeout(40)
void zonedFileDatabaseMigratesWithoutChangingInstantsOrExistingRecords() throws Exception
{
String classpath = Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator))
.map(value -> Path.of(value).toAbsolutePath().normalize().toString()).collect(Collectors.joining(File.pathSeparator));
String executable = System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java";
Path output = directory.resolve("migration.log");
Process child = new ProcessBuilder(Path.of(System.getProperty("java.home"), "bin", executable).toString(),
"-XX:ActiveProcessorCount=2", "-Xmx256m", "-Duser.timezone=America/Chicago", "-cp", classpath,
MigrationProbe.class.getName(), directory.resolve("legacy").toString())
.redirectErrorStream(true).redirectOutput(output.toFile()).start();
try
{
assertTrue(child.waitFor(30, TimeUnit.SECONDS), "Isolated migration probe must finish");
assertEquals(0, child.exitValue(), () -> "Migration probe failed: " + readOutput(output));
}
finally
{
if(child.isAlive())
{
child.destroyForcibly();
assertTrue(child.waitFor(5, TimeUnit.SECONDS));
}
}
}
/***************************************************************************
** Only synthetic fixture data can appear in child assertions.
***************************************************************************/
private static String readOutput(Path output)
{
try
{
return Files.readString(output);
}
catch(Exception exception)
{
return exception.getClass().getSimpleName();
}
}
/***************************************************************************
** Run the actual initializer twice against a closed/reopened file database.
***************************************************************************/
public static class MigrationProbe
{
private static final Map<String, List<String>> TIMESTAMPS = Map.of(
"mobileDevice", List.of("enrolledAt", "approvedAt", "lastSeenAt", "locationCapturedAt", "locationReceivedAt"),
"mobileScan", List.of("capturedAt", "receivedAt"),
"mobileSession", List.of("startedAt", "lastSeenAt", "endedAt", "authCleanupAt"));
private static final List<OffsetDateTime> INSTANTS = List.of(
OffsetDateTime.parse("2024-03-10T01:59:59.123456-06:00"),
OffsetDateTime.parse("2024-11-03T12:30:00.654321+05:45"));
/************************************************************************
** Use only an isolated fixture; no HTTP listener or operator credentials.
************************************************************************/
public static void main(String[] args) throws Exception
{
assertEquals(ZoneId.of("America/Chicago"), ZoneId.systemDefault());
String url = "jdbc:h2:file:" + args[0].replace('\\', '/') + ";DATABASE_TO_UPPER=FALSE";
MobileScannersDemo demo = new MobileScannersDemo(url);
demo.initializeDatabase("synthetic-scanner-password", "synthetic-admin-password");
Map<String, String> passwords;
try(Connection connection = DriverManager.getConnection(url, "sa", ""))
{
passwords = passwordHashes(connection);
seedLegacyZonedRows(connection);
}
for(int run = 0; run < 2; run++)
{
demo.initializeDatabase(null, null);
try(Connection connection = DriverManager.getConnection(url, "sa", ""))
{
assertEquals(passwords, passwordHashes(connection));
for(var entry : TIMESTAMPS.entrySet())
{
try(var statement = connection.createStatement(); var rows = statement.executeQuery("SELECT * FROM " + entry.getKey() + " ORDER BY id"))
{
int index = 0;
while(rows.next())
{
for(String field : entry.getValue())
{
assertEquals(INSTANTS.get(index).toInstant(), rows.getObject(field, LocalDateTime.class).toInstant(ZoneOffset.UTC), entry.getKey() + "." + field);
}
if(entry.getKey().equals("mobileDevice"))
{
assertEquals("APPROVED", rows.getString("status"));
assertEquals("Original name " + index, rows.getString("displayName"));
}
if(entry.getKey().equals("mobileScan"))
{
assertEquals("000123-raw-" + index, rows.getString("barcode"));
assertEquals("scanner", rows.getString("userId"));
}
index++;
}
assertEquals(INSTANTS.size(), index);
}
}
}
}
}
/************************************************************************
** Existing passwords must remain byte-for-byte identical across startup.
************************************************************************/
private static Map<String, String> passwordHashes(Connection connection) throws Exception
{
Map<String, String> hashes = new LinkedHashMap<>();
try(var statement = connection.createStatement(); var rows = statement.executeQuery("SELECT username, passwordHash FROM demoUser ORDER BY username"))
{
while(rows.next())
{
hashes.put(rows.getString(1), rows.getString(2));
}
}
assertEquals(2, hashes.size());
return hashes;
}
/************************************************************************
** Recreate legacy column types before writing explicit offset timestamps.
************************************************************************/
private static void seedLegacyZonedRows(Connection connection) throws Exception
{
try(var statement = connection.createStatement())
{
for(var entry : TIMESTAMPS.entrySet())
{
for(String field : entry.getValue())
{
statement.execute("ALTER TABLE " + entry.getKey() + " ALTER COLUMN " + field + " TIMESTAMP WITH TIME ZONE");
}
}
for(int index = 0; index < INSTANTS.size(); index++)
{
String id = "fixture-" + index;
statement.execute("INSERT INTO mobileDevice (id, installationId, shortName, displayName, status, enrolledBy, enrolledAt) VALUES ('" + id + "', '" + id + "', 'scanner-" + index + "', 'Original name " + index + "', 'APPROVED', 'scanner', CURRENT_TIMESTAMP)");
statement.execute("INSERT INTO mobileScan (id, deviceId, eventId, userId, appId, processName, barcode, capturedAt, receivedAt) VALUES ('" + id + "', '" + id + "', 'event-" + index + "', 'scanner', 'scan', 'capture', '000123-raw-" + index + "', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)");
statement.execute("INSERT INTO mobileSession (id, deviceId, userId, status, startedAt, lastSeenAt) VALUES ('" + id + "', '" + id + "', 'scanner', 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)");
for(var entry : TIMESTAMPS.entrySet())
{
for(String field : entry.getValue())
{
try(var update = connection.prepareStatement("UPDATE " + entry.getKey() + " SET " + field + " = ? WHERE id = ?"))
{
update.setObject(1, INSTANTS.get(index));
update.setString(2, id);
assertEquals(1, update.executeUpdate());
}
}
}
}
}
}
}
}
-21
View File
@@ -1,21 +0,0 @@
# API 1.9 consumer qualification
Verified October 31, 2026 using exact-source core `ecf0f550a0357cec6387c2b54efca128a405a629` and scanner server `b14d925b741e815d1cfa4f62301f19207f25b03f`. These are draft source revisions, not published Maven releases or an operator-runtime upgrade.
## Dependency coherence
The demo imports the selected QQQ BOM. Its `qqq.version` therefore governs transitive core, middleware, OpenAPI and RDBMS dependencies, rather than only direct dependencies. The demo test engine also follows that BOM; an independently pinned Jupiter aggregate mixed incompatible test API/engine versions.
The strict runtime verifier previously rejected the mixed RC.20/new-core fixture. After correction it verifies **195 runtime JAR hashes, 2,440 unique QQQ classes and zero duplicate QQQ classes**, all resolved from the private qualification repository. Every backend QQQ module has the exact CI revision. Guava resolves to **33.7.2-jre** and H2 to **2.2.224**. Qualified core JAR hashes remain unchanged through consumer verification.
## Executed checks
- Exact-source core selected reactor closure: **4,507 tests discovered, zero failures/errors, 16 existing skips**, plus configured coverage/static gates. Windows omits only the cosmetic shell coverage summary; hosted Ubuntu retains it.
- Fresh scanner server: **37 tests**, zero failures/errors.
- Fresh demo using the selected core: **five tests**, zero failures/errors.
- Actual public discovery: UI API **1.9.0**, mobile-scanners **1.1.0**, TABLE_BASED authentication. The OAuth-only `nativeSessionLogin` feature is correctly absent for this configuration.
- Real-browser process suite: **15 passed, zero skipped/flaky/unexpected**, in 60.3 seconds. Coverage includes single-record, checked-bulk, first-N and whole-query administration; cancellation; session access/replay; leased screen viewing; device names/descriptions; location attribution and independent presence expiry.
The fixture used fresh disposable databases and random credentials. Normal scanner data, services and USB forwarding were preserved. No emulator was started. Physical EMDK, actual configured Authentik, camera/GPS and production deployment acceptance remain separate gates.
The Android repository's `scripts/ci/verify-fixture-provenance.mjs` and source-pinned hosted workflow retain the classpath hashes, dependency tree, effective POM and test reports. These checks reject mixed runtime versions; they do not install replacement artifacts into a shared Maven repository.
+4 -4
View File
@@ -1,6 +1,6 @@
# Customer branding and connection status
The shared Android APK checks the selected server's semantic API compatibility, then loads appearance before login. A customer can configure the in-app display name, primary color and optional logo without compiling another APK. The installed application's Android launcher name, icon and package identity remain properties of the APK. Branding and connectivity are documented operations in the [mobile-scanners semantic UI API extension](UI-API.md).
The shared Android APK loads appearance from the selected QQQ server before login. A customer can configure the in-app display name, primary color and optional logo without compiling another APK. The installed application's Android launcher name, icon and package identity remain properties of the APK.
## Host configuration
@@ -28,7 +28,7 @@ Unset variables use defaults: `QQQ Mobile Scanners`, `#1565C0`, and no logo. Res
## Public client contract
`GET /qqq/v1/qbit/mobile-scanners/branding` returns HTTP 200 with `Cache-Control: no-store` and JSON:
`GET /mobile/v1/branding` returns HTTP 200 with `Cache-Control: no-store` and JSON:
```json
{
@@ -38,6 +38,6 @@ Unset variables use defaults: `QQQ Mobile Scanners`, `#1565C0`, and no logo. Res
}
```
The `logoUrl` property is omitted when unset. The endpoint requires no session or approved device and ignores stale device headers. It exposes only those allowlisted appearance fields, never qbit configuration, backend names, secrets or enrollment records. A compatible server's unavailable or malformed branding uses the client's default theme. A server missing the required extension is blocked before login with an upgrade/configuration message.
The `logoUrl` property is omitted when unset. The endpoint requires no session or approved device and ignores stale device headers. It exposes only those allowlisted appearance fields, never qbit configuration, backend names, secrets or enrollment records. A client connecting to an older server without this endpoint should retain its default theme.
`GET /qqq/v1/qbit/mobile-scanners/status` likewise returns HTTP 200, `Cache-Control: no-store`, and `{"status":"ok"}` without authentication. This means the HTTP service is reachable; it does not test database readiness, authenticate a user, or approve a device. Clients must continue using authenticated device status and normal process responses for those decisions. No diagnostics, database details or credentials are returned.
`GET /mobile/v1/status` likewise returns HTTP 200, `Cache-Control: no-store`, and `{"status":"ok"}` without authentication. This means the HTTP service is reachable; it does not test database readiness, authenticate a user, or approve a device. Clients must continue using authenticated device status and normal process responses for those decisions. No diagnostics, database details or credentials are returned.
+1 -3
View File
@@ -7,7 +7,7 @@ This repository follows the current QRun-IO QBit pipeline structure: a canonical
| Check | What runs |
| --- | --- |
| CircleCI `setup` and generated `verify` | Shared `kingsrook/qqq-orb@0.8` QBit verification, including Maven tests and reports |
| GitHub `Demo acceptance` / `Verify demo HTTP contract` | Wrapper-pinned root verification/install, real demo HTTP acceptance, then browser E2E against the packaged frontend |
| GitHub `Demo acceptance` / `Verify demo HTTP contract` | Wrapper-pinned root verification/install, then the real demo HTTP acceptance suite |
| GitHub `Demo acceptance` / `Check Windows demo helpers` | PSScriptAnalyzer checks for the local Windows launcher and credential helpers |
| GitHub `CodeQL` | QRun-IO's shared QBit source analysis worker, pinned to an immutable commit |
| Dependabot | Weekly grouped Maven and GitHub Actions update pull requests targeting `develop` |
@@ -16,8 +16,6 @@ The three orb version declarations must match exactly. The `0.8` minor track rec
Run the same project checks locally with `./mvnw verify install` and `./mvnw -f demo/pom.xml verify`. The wrapper pins Maven 3.9.9 and validates its distribution checksum. Java 21 is required. The demo uses no external database or user credentials in CI.
[Process acceptance tests](TESTING.md) documents the browser and native test runners. Browser E2E runs inside the existing required demo job, so failures block that check. The Android repository runs the isolated scanner-host E2E against a pinned server revision on pull requests and pushes, in addition to its shared native UI suite.
For workflow syntax, run `actionlint .github/workflows/codeql.yml .github/workflows/demo-acceptance.yml`. Validate the setup caller with `circleci config validate --org gh/QRun-IO .circleci/config.yml`, using your existing CircleCI authentication. The shared orb is private; without organization scope and authorization, the compiler can report that it cannot find the orb. Do not place tokens in config files or command arguments.
## Initial publishing policy
@@ -1,11 +0,0 @@
# Mobile scanner server standards review
Scope: all 52 maintained Java sources and associated first-party scripts/configuration. Archived acceptance evidence and generated/vendor files retain their separate provenance.
Corrections add missing copyright/SPDX notices, required member spacing and script help/style, and enforce the existing PowerShell analysis policy in CI. Unexpected route/binding/provider-cleanup failures now emit fixed structured warning categories through the existing logging backend. Messages contain no exception text, credentials, user identifiers, scan values or location. Direct backend logging deliberately avoids automatic user enrichment at this sensitive boundary.
The optional test attachment fixture now reads file metadata before opening its stream, preventing a resource leak if metadata lookup fails. Six PowerShell scripts pass analysis with zero findings, and the isolated-path regression passes. Fresh enabled Checkstyle, clean compilation, 39 server tests and six real HTTP demo tests pass against the qualified private API 1.10 dependencies. All 18 optional Record Files consumer cases also pass, for 63 tests with no failures, errors or skips. Evidence is retained in the private qqq-server-standards-b183343407ca426b9655fe44303c97f6 snapshot. These checks do not qualify the files owner's entire release.
The review does not replace real Android projection, camera/gallery, GPS-fix, provider/logout, lifecycle/retry acceptance or deployment configuration checks. Release 1.0 remains incomplete until those gates are satisfied; production signing/distribution remains the owner's final step.
Typed scan-context follow-up: extension1.3 exposes persisted scanContext to subsequent backend actions and a narrow public capture facade, using the existing verified-device authorization. Raw inputs are consumed to prevent inherited symbology; pre1.3 saved process state fails closed with a restart instruction. Configured app/process attribution and event type/source/app binding are covered. Fresh Checkstyle plus44root/7HTTPdemo/18optionalfiles cases (69total) and2canonical-contract generation cases pass; released schema baselines are unchanged. Echo example and its owner integrations remain separate work.
-13
View File
@@ -1,13 +0,0 @@
# Optional device-screen viewing: extension 1.2
App-content viewing remains the default. The heartbeat response now advertises `supportedCaptureModes: [APP_CONTENT, DEVICE_SCREEN]`. A missing capability list on a 1.1 server permits app-content mode only. The Android operator must explicitly choose device-screen sharing and accept Android's system consent for each session. An administrator opening the existing screen widget cannot grant consent or start system capture remotely.
Heartbeat requests may include `captureMode`. Omitted means `APP_CONTENT`, whose existing foreground requirement remains. `DEVICE_SCREEN` allows background availability while the Android projection grant, device approval, authenticated session and admin viewing lease remain valid. The client retains foreground-only GPS reporting. `mobileSession.captureMode` is an additive nullable text column, with the supplied H2 migration defaulting existing rows to `APP_CONTENT`; hosts using another database must add the equivalent field before adopting the metadata.
Every lease reports its mode. Every device-screen frame explicitly supplies `captureMode: DEVICE_SCREEN`; omitted frame mode remains app-content. Upload must match both the session's current mode and its exact lease. Switching modes discards retained pixels and demand, creating a new lease identity only after the authorized admin widget refreshes. Existing sequence, JPEG size/rate, ephemeral retention, approval and per-record admin permission checks remain in force. The server trusts the approved client's statement of OS consent, not an unverified browser administrator request; it cannot independently inspect an Android grant.
No new route, audio capture, recording, remote input or external media service is introduced. Android uses a nonexported foreground service with system consent and a Stop notification, refuses stored/replayed grants and halts on expiry, Stop, lock, logout/revocation or loss of session ownership. It waits at most 30 seconds for admin demand before stopping. Actual MediaProjection behavior needs explicit physical-device acceptance; Java/HTTP/synthetic lifecycle tests alone do not establish it.
Regenerate the current OpenAPI fragment using the existing `mobile.openapi.update` gate. Preserve the 1.0 and 1.1 baselines and run their compatibility checks. Migration and tests are source changes until the coordinated verification reports pass; no existing operator server was changed by this work.
The initial coordinated reactor gate passes39 server tests and six real-HTTP demo tests, with zero failures/errors/skips and normal Checkstyle. It uses the qualified `ecf0f550` core revision without installing artifacts; core JAR hashes and historical1.0/1.1 baselines remain unchanged. Independent source review covered every changed production Java file, schema/migration and changed tests, with no concrete introduced security finding. It does not establish host-specific multitenant policy or Android permission behavior. The server cannot attest the client's OS consent, and expired frame access is rejected even though physical heap pruning occurs lazily on subsequent reads/writes. Real consent, fresh-frame, lock, background, rotation and forced-session-end acceptance remain required on the final Android candidate.
-51
View File
@@ -1,51 +0,0 @@
# Device sessions and live administration
Status: implementation in progress. This document records the agreed contract, not completed acceptance.
## Requirements
Administrators can list all enrolled devices and their current and past sessions, inspect live app/process/screen activity, view the actual QQQ application screen, and end one or several sessions. Device approval remains separate from user-session termination. Every administration process must work from a record and from selected bulk records, with real browser and native E2E coverage.
## Design
Use ordinary QQQ tables, record widgets and processes. A public `mobileSession` record stores device/user attribution, server timestamps, bounded activity names and termination audit. A private binding maps the authenticated core QQQ session to its public record; authentication identifiers never enter public records or telemetry. The server derives identity from QQQ authentication and verified device credentials.
`mobileSessionEnd` uses the standard prepare/confirm/apply/result flow. It preserves the reviewed selection, validates every target before changing any, records who ended it, revokes the core session through QQQ's existing logout executor, and clears cached authentication. An independent ended-session check also protects requests and asynchronous scan writes, including cookie replay without device headers.
The registered `mobile-scanners` semantic extension advances additively to 1.1.0. Existing 1.0 contracts and their baseline remain intact. Older scanner clients are observed through their existing authenticated device calls; no new mandatory credential header is introduced. New clients negotiate 1.1 before login.
| Operation | Purpose |
| --- | --- |
| `POST /qqq/v1/qbit/mobile-scanners/sessions/heartbeat` | Start or refresh the current authenticated device session and return capture demand. |
| `POST /qqq/v1/qbit/mobile-scanners/sessions/current/end` | Record normal sign-out/server change before core logout. |
| `POST /qqq/v1/qbit/mobile-scanners/sessions/current/screen` | Upload one bounded JPEG for the current session and active viewer lease. |
| Standard QQQ widget/process APIs | View live session records/screens and run single/bulk termination. |
Heartbeats run about every five seconds and contain app/process/step identifiers, a bounded screen label, foreground/capture availability, and safe action/error identifiers. They exclude passwords, form values, barcode contents and raw logs. Server time determines presence and lease expiry. The client derives a monotonic lease deadline from the server's remaining duration so device clock skew cannot extend viewing.
The session record's ordinary composite IMAGE widget refreshes every two seconds while visible and renews a 30-second viewer lease. The Android host uses PixelCopy on its own application content only while authenticated, approved, foregrounded, focused and leased. It excludes authentication/password screens, checks the same screen/session generation after asynchronous capture, and shows a visible viewing indicator. Backgrounding, logout, termination and unavailable capture clear the current frame.
Frames are limited to 640 by 1280 pixels and 256 KiB, with a 360 KiB JSON body limit. The server validates encoded length before decoding and checks JPEG dimensions before pixel allocation. Lease-scoped ordering rejects stale uploads; heartbeat returns `nextSequence` for client restart recovery. Only the latest frame is held transiently in bounded memory. No screen history or remote input control is included.
## Device identity, presence and location
Devices have an administrator-editable display name and description, plus a unique short name. Enrollment assigns a default short name; setup/approval can assign a meaningful name. Short names are canonical lowercase, 3–32 characters, using letters, digits and hyphens, with a database uniqueness constraint. Existing devices require a preserving migration before the constraint is enabled. Profile edits cannot change approval or credentials.
Approval and online presence are separate. Presence is based on server-observed heartbeat age and must expire even when a device stops sending requests. Administrators need an accurate online-only query. Location reports include coordinates, accuracy, capture time and server receipt time; unavailable or denied location never blocks scanning. The Android client requests foreground permission explicitly. Last-known location must show its age and must not be presented as a current GPS fix. No coordinates are sent to third-party maps by default.
## Record photos
The separate Record Files qbit owns retained bytes, associations, audit and upload finalization. The scanner owns native camera/gallery handling and bounded private temporary files, stable retry identity and a durable-save receipt. The optional `integrations/record-files` module adopts frozen bundle `retained-contract-20261031-0352` in an isolated dependency cache. Its operation guard protects authorization through the tested single-JVM commits and rollbacks against concurrent revocation/session termination; 18 integration/contract tests pass. Native capture uses explicit `RECORD_FILES_CAPTURE` metadata with `values.parent.tableName` and `values.parent.recordId`, checks discovery/capabilities and never infers the parent from a process name. Synthetic TC22 camera/gallery, rotation and logout cleanup pass. Production storage/content processing, immutable dependency CI and actual lens/operator acceptance remain open. Shared coordination is recorded in the mobile and Record Files owner mailboxes under `R:\agent-comms`.
## Deployment boundary
The initial live-view frame/lease store belongs to one server instance. Clustered deployments need shared ephemeral storage or explicit instance affinity. Durable device/session records use the configured QQQ backend. Schema changes must preserve the existing demo database; release documentation must state supported database migrations and deployment assumptions.
## Verification plan
- Server: authenticated identity binding, private fields, permission denial, ended cookie replay, asynchronous write races, expiry, image limits/order and existing 1.0 compatibility.
- Browser: device/session navigation, refreshed image widget, single/bulk termination, confirmation/cancellation, persisted audit and unaffected sessions.
- Android: truthful activity state, capture lifecycle/password gating, delayed-capture races, remote logout and a fresh session after login.
- TC22: actual PixelCopy screen visible in admin, no capture outside the eligible app state, remote termination, reconnect/retry and persistence.
Production readiness is tracked in [PRODUCTION-READINESS.md](PRODUCTION-READINESS.md).
+2 -8
View File
@@ -8,10 +8,6 @@ pwsh -File scripts/Start-Demo.ps1
The launcher verifies both Maven projects, then keeps the server running at <http://127.0.0.1:8080/>. Stop it with Ctrl+C. After a successful build, `-SkipBuild` starts it directly. If port 8080 is unavailable, use `-Port 18080` and open <http://127.0.0.1:18080/>. The database persists under `demo/data`; the server binds only to loopback.
Before upgrading an existing demo database, stop its JVM gracefully and back up the closed `demo/data` directory together with its protected credential file. The shared H2 schema begins with `SET TIME ZONE 'UTC'` before converting legacy `TIMESTAMP WITH TIME ZONE` columns to the current UTC `TIMESTAMP` representation. This protects both the demo initializer and hosts executing the supplied schema directly. It preserves the original instants independently of the workstation time zone; rerunning initialization preserves existing accounts, passwords, device approval and scans. Do not copy over an open H2 database or use an account reset as an upgrade step. The schema leaves its migration connection in UTC; hosts should use a dedicated migration connection or restore their desired connection zone after the entire script completes.
Use the matching Android build with the [mobile-scanners semantic API extension](UI-API.md). Public discovery is at `/qqq/v1/version`; the extension's connectivity endpoint is `/qqq/v1/qbit/mobile-scanners/status`. This migration removes the former standalone mobile routes, so upgrade the local server and APK together. Installing the APK with `adb install -r` and restarting the server preserves the existing application data and database; no account or device reset is required.
First startup creates random passwords for the `admin` and `scanner` accounts. The launcher stores them using Windows DPAPI under `%LOCALAPPDATA%/QRunIO/MobileScannersDemo/credentials.clixml`, readable only through the same Windows user/machine protection. They are never written to the repository or printed. Keep that credential file with this local database; the launcher never changes existing account passwords.
In another PowerShell window, copy the admin password when ready to log into the browser:
@@ -34,13 +30,11 @@ If the host server uses `-Port 18080`, use `adb reverse tcp:8080 tcp:18080` inst
1. Open **QQQ Mobile** on the emulator and use server `http://127.0.0.1:8080`.
2. Sign in as `scanner` and enroll the device. It starts pending approval.
3. In the browser as `admin`, open **Mobile Scanner Administration → Mobile Devices**, open the device record, and choose **Actions → Approve Device**. Review the selected device, continue, and check the success result. For several devices, select their rows in the device list and run the same action from the bulk menu. The standalone sidebar process also accepts device IDs.
3. In the browser as `admin`, open **Mobile Scanner Administration**, locate the device, copy its ID, and run **Approve Device** with that ID.
4. Refresh approval on Android, open **Scan Capture**, type a barcode such as `000123456789`, and submit it.
5. Inspect **Barcode Scans** in the browser and verify the exact text, user, device and timestamps.
6. Choose **Revoke Device** from the device record or bulk selection, confirm the selection, and verify subsequent capture is refused.
6. Revoke the device and confirm subsequent capture is refused.
The emulator exercises native screens, login, approval, manual capture and persistence. Verify the physical trigger, DataWedge delivery and barcode decoding on the Zebra TC22 using [the hardware checklist](TC22-ACCEPTANCE.md).
Approval and revocation support checked rows, select-all matching rows, and first-N selections, up to 100 devices per run. The confirmation fixes the selected device IDs for that run, so a newly enrolled matching device is not unexpectedly included. Larger selections fail explicitly; choose a smaller batch. No approval changes occur until you continue past confirmation.
See [branding configuration](BRANDING.md) to test customer names, colors and logos with the same APK. Tap the Android connection footer to view or copy diagnostic events.
-97
View File
@@ -1,97 +0,0 @@
# Mobile scanners 1.0 release gates
Status: not release-ready. The user requested a full client/server audit and production-ready 1.0. This is an evidence ledger; a numerical quality score does not replace the gates below.
## Latest verified checkpoint — November 1, 17:34 UTC
Server standards checkpoint0b53f34 is signed/pushed and all eight hosted checks pass; a fresh private API1.10 cohort passes39 server, six demo HTTP and18 Record Files consumer cases with Checkstyle. Client strict compilation and163 JVM tests pass; final TC22 native acceptance is running and the Query artwork density warning is being corrected before installation. About includes bundled Query artwork and the exact public Dogfooding/Query links.
Actual Authentik desktop acceptance now passes real negative nonce rejection, A login, explicit company logout, old-cookie rejection, fresh B credentials and B principal in the same retained browser context, followed by B local logout/rejection. HTTPS verification remained enabled with profile-only fixture trust. The owned provider required its documented company invalidation flow; no global flow or default client sign-out policy changed. This closes the earlier desktop A-to-B handoff failure for the qualified API1.10 fixture, not the actual Android external-browser callback gate. The normal server remains API1.9/mobile1.1 and operator data is preserved.
CorePR1369's new hosted run exposed a missing explicit architecture allow-list entry for extracted private-session logout plumbing; a narrow policy-conforming correction is under verification. Do not call final core CI green. Remaining physical GPS/camera/projection/lifecycle/retry, immutable files-owner handoff, deployment and owner-deferred signing/distribution gates remain open. Dated matrix rows below retain their earlier source/runtime scope and are superseded by this checkpoint where stated.
## Current release ledger — November 1
This is the current acceptance matrix; dated checkpoints below remain historical evidence. Android portrait-default source `7de2b5088fa376cb161ed5ac7e5358d2a2145d92` passes all eleven hosted checks. Documentation/QR fixture checkpoint `2e0fd962a7ece0f81328e5f9f05e4c1e2d77a853` does not establish physical QR acceptance. Server implementation `10416938724c9663d66f7848ff2b1a63fe635794` has qualified API1.9/mobile1.2 source evidence. Source publication, private consumer qualification and the installed operator runtime are distinct; no production APK or immutable dependency release is claimed.
| Requirement | Accepted evidence and scope | Remaining release gate |
| --- | --- | --- |
| Current Android and CI | All eleven hosted checks pass at published Android8d8 (portrait implementation7de), including native navigation/input, PixelCopy and semantic E2E. The subsequent optional provider-logout working patch passes148 JVM tests, lint and debug build; its native tests compile but have not run. The completed-logout/enabled-action guard retains fresh-login/new-session assertions. Portrait default preserves explicit saved choices. | Physical operator navigation, lifecycle, ownership, outage/retry and leave/cancel walkthrough. Any later executable source needs its own verification. |
| Trusted scanner ingress and hardware | Ordinary-UID/distinct-signer rejection of eight forged scans plus manual positive control; actual EMDK Code128 source/model/event proof: eight deliberate events on prior F3AF APK and one on the installed portrait-default C891 APK. Historical DataWedge evidence is separately retained. | Current leading-zero QR, repeated-intent/lifecycle/ownership matrix, connection failure and uncertain receipt/retry/leave acceptance. Code128 success does not qualify QR or every lifecycle path. |
| Running operator deployment | Preserving coreecf/serverb14 UI API1.9/mobile1.1 runtime on127.0.0.1:57832; normal TC22 uses development APK C89161C522AA7DFFE09C009ABDE769C1BADB7E6C64BFCE699857062A1CEC6340 with signer/data backup preserved and portrait requested. | Supported production deployment, HTTPS/authentication, migration/recovery and release upgrade acceptance. No normal1.2/RF installation is implied. |
| Source-pinned server/admin APIs | Server104 source:39 server/six demo/all15 browser cases,195 hashed JARs/2,441 unique QQQ classes/zero duplicates; record/bulk administration, profiles, presence, sessions and screen controls covered. Coreecf selected closure:4,507 discovered/16 existing skips/zero failures/errors. | Coherent immutable dependency publication and final supported deployment configuration. Default Maven dependencies still select RC.20; passing source-pinned consumers do not change that default. |
| Authentik | Real Authentik2026.8.5 browser PKCE login established operator A; QQQ local logout invalidated its old cookie. With retained browser SSO, the next prompt=login silently reused A, so operator handoff failed. Optional semantic API1.10 RP logout and Android AppAuth handling are implemented but unqualified; the first core build found an API mismatch and review found an alternate-authentication-source gap under correction. | Correct and qualify the optional logout implementation, then prove real A→provider logout→B identity isolation. Native browser callback/lifecycle and user-selected sign-out policy remain pending. Existing failed retained-SSO evidence must remain failed. |
| Optional whole-device viewing | Additive mobile1.2 server and consent-based Android source built/reviewed; mode/session-bound leases and bounded image ownership verified automatically. Normal server remains mobile1.1 and does not expose device-screen mode. | Actual OS consent/notification, Home/other-app pixels, Stop/lock/rotation, lease expiry and remote logout on the refined candidate. Separately preserve normal data before any coordinated1.2 upgrade. |
| Camera/gallery and Record Files | Filesystem portability142 discovered/138 passed/four existing skips; metadata symlink-parent regression fixed and independently closed. Final coherent core32/scanner1a3 qualification passes39 scanner, six demo,18 Record Files and15 browser cases (78 total), with214 JARs/3,163 unique QQQ classes/zero duplicates and no separate filesystem override. Synthetic native camera/gallery/receipt cases remain separate evidence. | Coherent immutable supported RF/attachment-owner/core/RDBMS/filesystem tuple and default adoption, then actual TC22 lens/gallery permissions and durable authorized record association. The isolated coherent qualification does not establish the files owner full-suite/default publication gate or change the normal runtime. |
| GPS and customer appearance | Foreground-only location/state/age controls and automated permission-denial/schema coverage; current basic operator display/navigation observed. | Real GPS permission/fix/disabled-provider behavior and customer-specific logo/physical appearance. |
| Security, architecture and standards | Historical scoped security reports/remediations retained; independent architecture closure maps A1–A11 controls/tests. Whole-device changed-file review and follow-up found no concrete introduced issue; filesystem finding closed against corrected source/test evidence. | Final revision/scope reconciliation and unresolved external-provider/deployment gates. These reports do not become a new exhaustive scan or blanket production clearance. |
| Release and operations | Release automation/source packaging independently reviewed; normal preserving migration/login checks passed. | Reviewed GitHub environment/tag protections, organization signing identity/recovery, actual signed APK upgrade/physical qualification and distribution decision. Signing/distribution remains owner-deferred until the final step. |
The local emulator remains off because of workstation stalls. Disposable QA and private fixtures must not replace the normal package/runtime or clear operator data. No numerical quality score replaces the remaining gates.
Historical November1 display observation retained: the handheld's black desktop mirror was not proof of a crash. Direct inspection found the normal process alive/resumed, no crash-buffer entries and connected sign-in; login reached Scan Capture without clearing data or running disposable instrumentation over the operator.
## Historical checkpoints
Latest correction: hosted Android testing found revoked heartbeats remained successful. Approval now precedes all heartbeat activity observation, including accepted trailing-slash aliases. Independent red reproductions and final 37-root/four-demo green tests are retained; new hosted native verification remains pending. Scanner authentication must follow configured Authentik through explicit native semantic authentication metadata; that implementation and real-provider acceptance are now required release gates.
The optional consumer has subsequently qualified frozen0515 with37 fresh base and18 integration tests, including the matched core logging correction and runtime integrity checks. This remains an isolated private candidate; immutable dependency publication and optional hosted CI are required before production adoption. Separate OAuth2 log-redaction work in the native-auth core branch is not included in0515.
## Scope and checkpoints
Latest verified checkpoint: server `0926550` includes the standalone file-backed presence regression and the passing browser-to-native
viewing regression after guarded-upload commit `84ffd7c`. Base 37, demo HTTP two,
browser 15 and optional guarded integration 18 cases pass. Android's local reports
show 91 JVM tests; TC22 navigation (12 cases), orientation, offline licenses,
synthetic camera/gallery/rotation/logout, and the complete real-screen/session-end
scenario pass. Native photo/session requests match published semantic operations.
The full server deep static security review of `0926550` is sealed with no reportable findings. It does not inspect external dependencies or production configuration and does not replace runtime acceptance. Android's full scan and the later single-hamburger UX correction remain pending.
These are checkpoint results; final Android audits, fresh Android hosted CI,
production provider/immutable dependency qualification and actual lens acceptance
remain open. Signing/distribution is owner-deferred until the final step.
- Server semantic baseline: `7dd02b8874882ca2edcdcd89109b3dee6b4b0c88`, with all hosted checks passing. Documentation checkpoint: `a49724384f569a4b4b66ce2cd4aa69332ee1e0e8`.
- Android semantic baseline: `6574ec4991dbb859b60c3a0be462af96918aee32`; hosted JVM/lint/APK, CodeQL and CircleCI checks passed, but both hosted native E2E jobs failed at compatible login before any credential request reached the server. Login/form reachability fixes are under verification; this baseline is not fully green.
- Active implementation: [device/session history, live activity, actual app screen viewing, session termination, identity/presence/location and Record Files integration](DEVICE-SESSIONS.md).
- Final audit scope: both complete repositories, including shared Android core/mobileapp libraries, scanner host, qbit, demo, build/release configuration, tests and documentation. Exclude local secrets and operator data from published evidence.
## Required gates
| Gate | Status / required evidence |
| --- | --- |
| Feature completion | Session administration, screen viewing and device naming/presence/location have passing server/browser coverage. Native navigation/session and Record Files photo integration are in progress. Actual ingestion routes exist; atomic revocation/commit protection and compatible runtime qualification remain open. |
| Security | Independent multi-pass full scans of the frozen client and server; validate findings, fix release blockers, verify fixes and retain coverage/limitations. |
| Architecture | Review boundaries, coupling/cohesion, session ownership, semantic API evolution, concurrency, resource limits and deployment assumptions. |
| Coding standards | Repository conventions, public API compatibility, lifecycle/error handling, duplication, dead code and maintainability review. |
| Automated tests | Unit, HTTP contract, schema/version compatibility, browser record/bulk processes and native UI-to-real-server tests on the final revision. |
| Physical acceptance | TC22 capture, mirroring, remote logout, failure/retry, restart, data preservation, location permission/age, camera capture and gallery upload to an authorized record. |
| Delivery | Release build, signing/update approach, reproducible artifacts, dependencies/licenses, secrets checks and final hosted CI. |
| Operations | Supported deployment/database migration, HTTPS/authentication, retention/resource limits, diagnostics and recovery documentation. |
| Final review | No unresolved release-blocking findings; evidence identifies exact revisions and remaining supported-use boundaries. |
## Verified baseline evidence
October 31 working-source evidence: 37 root tests, two real HTTP acceptance tests and all 15 browser E2E cases pass. Browser cases include single/selected-bulk/full-query session end, cancellation, live image/clear, device profile edits, location timestamps and online expiry/filtering. The full browser run completed in 59.4 seconds after correcting file-H2 connection lifetime. A subsequent virtual-online projection fix passes the 37-test suite; its combined native acceptance remains pending. Android's intermediate 76 JVM tests and zero-error/zero-warning lint pass; shared native and real-server session tests remain in progress. These are local intermediate results, not a frozen release or fresh hosted qualification.
Server: 23 root tests plus the real HTTP lifecycle scenario and six packaged-frontend browser E2E scenarios. Android: 53 JVM tests, six shared native UI tests, lint with zero errors/six warnings, debug/E2E APK builds, and six semantic native scenarios across focused runs. The final admin scenario passed in 255.917 seconds; 31 of 31 requests matched 13 published operations, including cancellation. These results are baseline evidence and must not be represented as verification of later session/mirroring changes.
Physical TC22 (Android 14, DataWedge 13.0.325, app 0.1.0): installed with existing state preserved, signed in and approved through the actual device-record process. A dedicated DataWedge profile uses barcode input, explicit broadcast delivery and package/signature verification, with keyboard output disabled. Two deliberate Code128 trigger scans produced two records. One QR trigger scan preserved `001234567890`; backend records confirmed symbology, source, user/device/app/process and timestamps. Outage/retry and remote-session controls remain to be tested on the completed feature.
## Review findings
The initial full-source architecture/maintainability review covered Android `6574ec4` and server `7dd02b8`; it does not cover the later session/device features. Findings are being remediated, with final independent review still required:
| Finding | Remediation owner / evidence pending |
| --- | --- |
| A1: scan retry cleared on an incomplete receipt | Android host: require matching acknowledgement ID, `scanSaved=true` and nonblank scan ID. |
| A2: invalid scanner length metadata silently defaulted/truncated | Android host: reject invalid or out-of-range metadata. |
| A3: legacy INPUT_FIELD bypassed typed field semantics and interpreted input as action code | Shared client: typed field handling and explicit action capability. |
| A4: ordinary asynchronous status failures lacked a safe retry | Shared client: retry status GET without replaying submission. |
| A5: cancellation during initialization could leave a late process orphaned | Shared client: retain cancellation intent and cancel a returned process ID. |
| A6: login/forms could place actions outside the viewport or behind the keyboard | Shared client: scrolling/IME handling and constrained-viewport native tests. Hosted failure cause remains unproven until the regression evidence is complete. |
| A7: explicit null displayed a default while submitting null | Shared client: distinguish absent keys from explicit null consistently. |
| A8: business JSON collided with class-based serialization envelopes | Shared client: ordinary JSON values without class loading. |
| A9: multi-record mutations and enrollment could persist partially | Server: transactions for credential/device and session/binding pairs and bulk state changes; provider logout after durable tombstones with explicit cleanup outcome. |
| A10: media ownership/disposal could leak resources | Shared client: sound/image lifecycle ownership. |
| A11: navigation title/state and native back stack could drift | Android host: derive navigation state from the active NavController. |
The completed TC22 session scenario delivered and decoded real application JPEG pixels, cleared background frames, renewed viewing on resume, honored operator Stop viewing, ended the session remotely and accepted a fresh login. Android packaging now verifies 130 notice entries, nine license texts and eight native binary hashes in each host variant. The full server static scan is complete; Android review, external core logging remediation/adoption, production signing/update path, supported database migration and final hosted checks remain required before 1.0.
+1 -109
View File
@@ -1,109 +1,6 @@
# QQQ Mobile Scanners session state
## November 1 typed scan context implementation
Extension 1.3 defines accepted `scanContext` for backend actions, preserving exact symbology and server-derived attribution. Legacy omissions are explicit UNKNOWN/MANUAL; event inputs are consumed, changed retry types/apps/processes are rejected, and pre-context state requires reopening the process. The default scanner loop remains while appended host steps can consume context. Qualified private API 1.10 verification passes 44 root, seven real-HTTP demo and 18 optional integration tests with normal Checkstyle; canonical contract generation passes two additional tests and preserves both release baselines. The source cohort is `qqq-server-typed-context-843fa0ee69b34535b1890cf7347492ee`; no normal runtime adoption is implied.
## Echo implementation scope
User requested example QQQ application Echo: first select printer and paper/label size, then continuously scan → render the same value/symbology → print until leaving. User chose one in-flight job and delegated first real printer/media choice to the printing master. Team implements the optional examples/echo host, ordinary semantic QQQ process metadata and server per-scan context; no new Android screen engine, barcode encoder, attachment framework or print queue.
Owner coordination requests are posted in our shared mailbox. Source discovery found missing authorized media discovery/job options, worker barcode fit-to-stock, least-privilege printing submission and an immutable retained-print adapter handoff. These are concrete integration gates, not permission to change the other masters' source or live printer/service. Implementation and isolated tests can proceed while owners respond; no physical print or final working Echo claim yet.
## November 1 standards implementation and API 1.10 qualification
Copyright/SPDX, member layout and optional PowerShell tooling fixes are applied. Unexpected server failures now produce fixed structured diagnostics without private values. The attachment test fixture closes its metadata-before-stream ownership gap. Fresh Checkstyle/compilation, 39 root tests and six real HTTP tests pass against the qualified private API1.10 cohort; optional integration verification is tracked in the standards review. These are source gates, not a normal-server deployment.
Core native API1.10 is signed and published as e1e45b29f in draft PR1369. Its real Authentik fixture is upgraded and actual retained-browser account switching is under test. Android1127 is published; the installed normal APK is the diagnostics/location3fee build, preserving sign-in and data. Prior installed1c Code128, leading-zero QR and menu/reopen were physically confirmed. Default-on location, existing fine/coarse grants and system location are verified; an actual GPS fix remains open. Historical entries below retain their original scope. See [standards review](CODING-STANDARDS-REVIEW-2026-11-01.md).
## November 1 provider logout implementation in progress
Core32 passes all ten hosted checks, including corrected1000-test sample acceptance; Android8d8 passes all eleven checks. The final coherent core32/scanner1a3 cohort passes39 scanner/six demo/18 Record Files/15 browser cases and is recorded in the published qualification document.
Real Authentik now passes a desktop-browser PKCE callback through the native-session exchange contract, principal A, QQQ logout and old-cookie rejection. This does not establish Android external-browser acceptance. A subsequent login with provider SSO retained silently reused A despite prompt=login. The pinned provider also ignores max_age=0. This is an open operator-handoff issue, not a passed two-operator test. Optional standard RP-initiated logout is being implemented in core branch feature/mobile-native-provider-logout (based on core32) and the Android client, with explicit API1.10 capability, encrypted session-bound hint storage, local logout independent of provider failures, and a separately correlated AppAuth end-session attempt. The user policy choice (automatic company sign-out versus retained SSO with account confirmation/Switch account) remains pending; default sign-out behavior has not changed.
The optional Android provider-logout working patch passes148 JVM tests, lint and debug build; native cases compile but have not run. The first isolated core package failed at an UpdateOutput API mismatch before tests. Independent review also found the optional hint gate must reject alternate in-process/code-state authentication sources; corrections and an HTTP regression are in progress. Initial failed candidate evidence is preserved.
The isolated localhost-CA E2E APK built from signed Android8d8 passes63 unit cases/zero lint errors and packaged CA/callback/signature inspection. It is not installed; no device trust changed. Normal TC22 remains portrait C891 and its API1.9/mobile1.1 server remains untouched. Current QR, camera/gallery, projection, GPS and failure/navigation operator acceptance remain open. Files-owner immutable handoff/default adoption and final signing/distribution remain separate.
Updated: 2026-11-01
## November 1 release reconciliation
Published server1041693 retains its qualified source-pinned API1.9/mobile1.2 server/demo/browser/provenance evidence. Android portrait-default7de passes all eleven hosted checks; QR documentation2e0fd962 does not establish physical QR acceptance. The normal runtime remains coreecf/serverb14 API1.9/mobile1.1 on57832, with normal portrait-default C891 development APK installed preserving signer/data. Trusted EMDK Code128 evidence comprises eight priorF3AF events and oneC891 event; QR, lifecycle/ownership and failure/retry/leave operator paths remain pending. Whole-device1.2 is qualified source, not installed normal-server behavior.
Filesystem portability passes142 discovered/138 passed/four existing skips, including the corrected invalid-reference exception contract; its metadata symlink-parent and NUL-reference findings are independently closed. The final coherent core32/scanner1a3 cohort passes78 consumer cases, including18 Record Files cases, with no separate filesystem override. Matching files-owner immutable/default adoption remains open. Actual Authentik flow remains pending; strict proxy TLS now works, while provider configuration returned a validation error under diagnosis; physical projection, camera/gallery and GPS acceptance remain open. Signing/distribution stays owner-deferred until the final step. [PRODUCTION-READINESS.md](PRODUCTION-READINESS.md) is the current matrix; older entries below retain historical scope and are superseded where their then-open state has changed.
Cross-chat coordination uses `R:\agent-comms\README.md` and the mobile master's owned `R:\agent-comms\qqq-mobile-scanner.md`, explicitly requested by the user. Record Files and Printing acknowledged the directory. Read the current summary at the top of each owner's mailbox before changing shared contracts.
## November 1 Record Files consumer checkpoint
The isolated core32/scanner1a3 cohort passes all 78 consumer cases (39 scanner,
six demo HTTP, 18 Record Files and 15 real-browser process tests), with coherent
backend versions, 214 optional runtime JARs and zero duplicate QQQ/QBit classes.
Core verification combines the prior full reactor gate, the corrected filesystem
full gate and an exhaustive final artifact class comparison; see the precise
[qualification scope](../integrations/record-files/docs/CORE-32DF-CONSUMER-QUALIFICATION.md).
The files-owner matching immutable handoff, optional default adoption and actual
camera/gallery acceptance remain open. This cohort did not change the normal
runtime, device or historical optional default.
## October 31 current checkpoint
The normal local demo is now running the qualified `ecf0f550`/`b14d925` API1.9/mobile1.1 TABLE runtime at its existing loopback address. This supersedes the unchanged-runtime statements in the historical acceptance entries below. Read-only inspection froze its actual old classpath; old-runtime and old-after-new HTTP/login checks passed on historical and fresh closed backups before startup. Fresh copied invariants preserved every original column, row count, password/device-credential hash, barcode and UTC timestamp. Both original accounts passed live login, authenticated metadata and logout; ordinary semantic queries confirmed two devices and three scans, including the leading-zero barcode. The normal Android installation and peer services were preserved. Private backup/runtime evidence is retained in the task cache; old PID-specific maintenance helpers must never target the replacement JVM.
Optional consent-based whole-device viewing is under implementation as an additive extension1.2 contract; it is not part of that running, qualified1.1 runtime. Android diagnostic checkpoint `6f25562` is published to its existing PR after instrumentation compilation; fresh hosted results remain pending. The actual trusted-scanner trigger attempt is unverified. Production signing/distribution remains the final owner decision.
The initial1.2 reactor now passes39 server and six real-HTTP demo tests with normal Checkstyle, no failures/errors/skips, unchanged qualified-core hashes and unchanged1.0/1.1 baselines. Independent changed-file source review found no concrete introduced security finding; Android consent and projection acceptance remain open. The diagnostic Android checkpoint subsequently passed CircleCI37 native tests and unit tests; its earlier intermittent focus-owner cause remains unproven.
Exact-source core `ecf0f550a0357cec6387c2b54efca128a405a629` with server `b14d925b741e815d1cfa4f62301f19207f25b03f` now passes fresh37 server/five demo tests, strict runtime provenance and all15 browser cases (zero skips/flaky/failures). The runtime has195 hashed JARs,2,440 unique QQQ classes, zero duplicate QQQ classes, patched Guava33.7.2-jre and H2 2.2.224. Core's full selected closure separately passes4,507 discovered/16 existing skips/zero failures/errors. See [qualification scope](API19-CONSUMER-ACCEPTANCE.md). No operator runtime was upgraded by this acceptance.
The demo now imports the QQQ BOM using its own `qqq.version`, so an explicit version selection governs its complete transitive QQQ runtime. Private source qualification caught the prior mismatch: the scanner's installed POM retained its default RC.20 property, while the demo override selected a newer direct RDBMS dependency. The strict runtime verifier rejected the mixed core/Javalin/OpenAPI versions and older Guava despite passing tests. The first BOM-aligned run then exposed the demo's explicit JUnit 5.11.4 aggregate mixing an older API with the BOM's JUnit 6 engine; the demo now uses the BOM-managed test engine and its matching API/platform. The coherent candidate's fresh acceptance is recorded above; passing tests from the earlier mixed runtime did not establish that result. No operator runtime or shared Maven repository was changed.
The normal operator demo was upgraded in place to mobile extension1.1 with a closed owner-only backup: two accounts, two devices (one approved), three scans, credential hashes and timestamp instants were preserved. Both original passwords authenticated after startup. Its JVM currently uses UTC because the copied upgrade exposed H2 converting legacy zoned timestamps using the workstation zone. The canonical H2 schema now begins by setting its migration connection to UTC, protecting both direct schema consumers and the demo initializer. A no-install reactor verifies the freshly packaged base resource: 37 base tests and five demo tests pass, with zero Checkstyle violations, including a file-backed America/Chicago child-JVM regression over all eleven converted timestamp columns, multiple source offsets, preserved passwords/approval/barcodes and an idempotent second startup. The isolated old-schema resource without the UTC statement fails with the expected six-hour winter timestamp shift; the tested jar resource hash matches the canonical source. This source change does not restart the operator runtime; it remains safely mitigated with UTC.
The optional Record Files consumer now explicitly defaults to frozen0515 and its separate isolated-m2-0515 cache. Fresh normalized-heartbeat base37 and optional18 pass, with214 runtime jars/3180 classes/zero duplicates. Immutable evidence is retained under integrations/record-files/evidence/20261031-124632Z. Earlier0352 native acceptance remains historical;0515 is a new server consumer qualification, not an operator upgrade. Published dependency coordinates and hosted optional CI remain blocked on the upstream handoff recorded in HOSTED-CI-DEPENDENCIES.md. New API1.9 native Authentik metadata is being developed in an isolated core worktree and is not part of this server's adopted dependencies.
Hosted Android acceptance exposed a heartbeat revocation defect after the published checkpoint: pending/revoked devices could keep receiving successful heartbeats and remain signed in. The fix checks approval under the existing activity lock before observing activity, and the route filter avoids pre-observation for canonical and accepted trailing-slash heartbeat paths. Both defects were independently reproduced before correction. Final root verification/install passes 37 tests and demo verification passes four tests, including the real HTTP no-side-effects regression and standalone presence case. Native hosted confirmation on the new server revision is still required; earlier static zero-findings results do not prove absence of this defect.
Server checkpoint `0926550` includes 37 passing root tests, two HTTP acceptance cases, a passing standalone file-backed presence regression and 15 passing browser cases. The optional Record Files module adopts frozen bundle `retained-contract-20261031-0352`; 18 guarded integration/contract tests pass. TC22 navigation (12 cases), orientation, offline licenses, synthetic photos and the complete screen-viewing/session-end scenario pass on Android checkpoint `d5b5580`. These do not imply an operator-runtime upgrade or actual camera-lens acceptance.
The full server deep static security scan of `0926550` completed and its canonical report was sealed on October 31 with no reportable findings. Coverage excludes external dependency implementations and production configuration; no new runtime tests were executed by the scan. The known external core DEBUG session logging issue, immutable Record Files publication/CI and production provider qualification remain open. Android's final full scan is still pending. A user-requested single-hamburger navigation correction is now being implemented after the published Android checkpoint; its new tests are not yet run.
The normal operator demo and normal TC22 package remain unchanged. A separate temporary visual demo showed the new menu on the TC22; the user stopped computer control and retains the device. Do not restart, uninstall or drive that demo until coordinated. Signing/distribution remains deferred to the final step.
The local emulator is closed after the user confirmed it caused workstation-wide stutters. TC22 is connected; do not restart the emulator automatically. Builds are serialized with bounded workers. Existing operator demo and handheld state remain preserved. Android85 JVM tests and zero-error/zero-warning lint pass at the last compiled checkpoint; newer changes must be verified again.
The active goal authorizes completing and publishing the project, superseding historical artifact-publication deferrals below. However, on October31 the owner explicitly deferred signing and distribution until the final step because the channel is undecided. Complete implementation, final audits, tests, documentation and ordinary hosted CI first. No signing key, production release, merge or deployment is claimed.
## Active: device sessions, live screen viewing and 1.0 audit
Additional user scope: unique short device names assigned at setup/approval, editable names/descriptions, independent approval/presence, online fleet view and foreground GPS with age/accuracy. Camera/gallery photos use the separately developed Record Files qbit. The Build QQQ Documentation agent owns it and `qbit-record-files/docs/INGESTION-CONTRACT.md` documents the implemented record-files1.0 semantic capabilities/create/status/chunk/finalize/cancel operations and durable receipts. The October31 checkpoint above supersedes earlier proposed-only contract status. Native photo acceptance and the final authorization guard remain pending; unavailable capabilities stay hidden.
The user requires all device/session history, live activity and actual application-screen viewing in QQQ administration, plus session termination. Implementation follows [DEVICE-SESSIONS.md](DEVICE-SESSIONS.md): ordinary QQQ tables/widgets/processes, server-owned authenticated bindings, an additive 1.1 semantic extension and demand-based bounded screen frames. Native and browser actions require full E2E coverage, including record and bulk entry points.
The user also requires independent in-depth security, architecture, coding-standards and coupling/cohesion reviews of both complete repositories before a production-ready 1.0. [PRODUCTION-READINESS.md](PRODUCTION-READINESS.md) tracks the gates and exact baseline evidence. Plan: complete the session feature and tests; freeze final revisions for independent audits; remediate and verify findings; complete physical and release/operations acceptance. Deployment/distribution details have been requested. No 1.0 readiness or release is claimed.
## Current: semantic API migration
The qbit registers mobile-scanners 1.0.0 through QQQ's extension registry. Device enrollment/current status, branding and connectivity use `/qqq/v1/qbit/mobile-scanners`; discovery advertises the relative prefix `/qbit/mobile-scanners`. The generated OpenAPI includes typed device and scanner event/acknowledgment schemas, stable operation IDs and standard errors. Legacy standalone mobile routes are removed. [UI-API.md](UI-API.md) describes the contract and versioning policy.
Root `verify install` passed 23 tests, including the extension contract baseline checks; demo `verify` passed one real HTTP discovery/schema/error/lifecycle scenario. All six migrated browser E2E scenarios passed in 2.9 minutes. Native integration exposed an unregistered Android client channel returning HTTP400; the qbit now registers `android` without replacing existing channel policies. The real HTTP regression sends `X-QQQ-Client: android/qqq-mobile` on all requests and passes. Android requires `android` in discovery's `clientChannels`; its combined JVM suites passed 53 tests (core 21/mobileapp 23/scanner 9), with six shared native UI tests also passing. Final lint passed with zero errors and six warnings; debug and E2E APK builds passed. Five native scanner scenarios passed with the OpenAPI request audit: incompatible core version, missing extension, missing Android channel, baseline scan flow and additive future metadata. The final native administration form scenario passed in 255.917 seconds after correcting completion handling for a COMPLETE response without `nextStep`: approval, revocation and cancellation completed, with all 31 requests matched to 13 published operations. All six native scenarios passed across the original and focused reruns; this is not a claim of one uninterrupted six-case run. All hosted checks passed for published server commit `7dd02b8874882ca2edcdcd89109b3dee6b4b0c88`: CircleCI setup/verify, CodeQL, Windows helpers and the demo HTTP/browser E2E job.
Active plan: publish the companion Android checkpoint pinned to verified server code `7dd02b8`, then add the requested device/session activity, session termination and on-demand app-screen viewing. Physical connection-loss/retry and revoke-denial checks are deferred to acceptance with those session controls; the persistent server and USB forwarding are restored. The local server and emulator restarted after the workstation crash with data preserved; the backend administration UI was visibly signed in. The physical TC22 is identified through authorized USB debugging, running Android 14 and DataWedge 13.0.325. The debug app is installed and signed in. Approval from the browser record's Actions menu visibly reported one device approved; the handheld then opened the native app catalog and Scan Capture process. Updating the APK preserved its login. DataWedge is configured for package `io.qrun.mobile.scanner`, activity `*`, barcode input enabled, keystroke output disabled and Intent Output enabled. Intent delivery uses Broadcast, action `io.qrun.mobile.scanner.SCAN`, category `android.intent.category.DEFAULT`, and an explicit target package with signature checking enabled. Two deliberate physical trigger presses were confirmed by the user and produced two persisted records visible in Barcode Scans. A stored row matched the TC22 receipt for `QQQ-TEST-123456`, CODE128, source `scanner`, user `scanner`, TC22/Android 14/app 0.1.0, app `mobileScanApp`, process `mobileScanCapture`, and capture/receipt timestamps. Basic physical Code128 delivery and persistence passed. A subsequent physical QR scan was acknowledged on the handheld and independently visible in the backend as `001234567890`, `LABEL-TYPE-QRCODE`, source `scanner`, with matching TC22/user/app/process and capture/receipt metadata. Three physical records are verified: two deliberate Code128 captures and one QR capture. Physical QR delivery and leading-zero preservation passed; physical connection-loss/retry and revoke-denial checks remain pending. The old semantic-gap note below is superseded by this implementation status and the current TODO.
## Verified: record and bulk device administration
- User reported Approve Device did not work from a device record and required proper E2E coverage for every process, including single-record and bulk entry points.
- Root and real HTTP regressions reproduced the missing selected device ID. Approval/revocation now prepare a stable selection, show confirmation, apply only after confirmation, and display a result/count. Explicit selected IDs and native select-all/first-N filters are supported up to 100 devices; oversized selections fail explicitly.
- The process stores selected IDs in native process records, ignores replacement IDs submitted at confirmation, rechecks administrator permission, and validates all targets before changing any. Standalone manual ID entry remains supported.
- Root verification/install passed 21 tests and Checkstyle. Demo verification passed the real HTTP scenario covering single-record approval, both bulk selection modes for approve/revoke, unselected-device isolation, visible results, scanner permission denial, scan persistence/retry, and revoked-device denial.
- Restarted the persistent local demo on port 57832; public status returned `ok`. Accounts, database and emulator forwarding were preserved. Dedicated scanner-host native E2E passed (one complete real-server scenario, 50.932 seconds); Android verification also passed 28 JVM tests, lint/build and workflow syntax. Test APKs were removed and the normal scanner restored.
- Browser E2E passed all six scenarios in 2.7 minutes: record approve/revoke with results and return navigation, checked-row bulk with untouched-device assertions, standalone manual entry, cancellation, full-query selection, and first-N selection with independent persisted-state checks. Tests use the packaged frontend and a temporary real backend. The fixture authenticates through the real API and supplies the nonsecret `qqq.sessionHint` marker so the UI resumes and validates its HttpOnly cookie. Browser checks now run inside the existing required demo CI job; native E2E runs in the Android push/PR emulator lane. New-head hosted results remain separate from these local passes.
- UI/UX API clarification: the Android app uses `/qqq/v1` metadata/process operations, but full semantic-client conformance is unfinished. Version/feature negotiation, broader native component handling and migration of separate `/mobile/v1` routes into a registered UI API extension are tracked in TODO; do not describe the current client as fully conformant.
2026-10-30 correction: native authentication metadata is served at `/qqq/v1/metaData/authentication`. The device-header guard now exempts that exact public path; the previous legacy-only exemption caused HTTP403 on app restart after enrollment when device headers were present before a user session was restored. A real HTTP regression failed before the fix and passed afterward with stale, pending, invalid-token and revoked device headers. Protected metadata/process/scan authorization still fails appropriately. Root13 tests, Checkstyle and demo HTTP acceptance passed; the local demo was restarted on57832 with its persistent database and accounts preserved.
Updated: 2026-10-29
## Current repository and demo state
@@ -161,8 +58,3 @@ Active plan: publish the companion Android checkpoint pinned to verified server
- Only a current unacknowledged scan is retained in memory for explicit retry. Closing/resetting the app is not an offline queue.
- The H2 schema is supplied; migrations for other databases and deployment authentication remain host-owned.
- Pending physical acceptance is tracked in TODO. GitHub bootstrap and hosted checks are separate from artifact release or production deployment; use `CI-CD.md` for the repository settings and release enablement policy.
## Native authentication review follow-up
The first optional provider-logout compile failure and alternate authentication-source hint gate are corrected in a new frozen source candidate; its isolated focused build is running. Independently, native login does not currently carry/validate the original request nonce against the provider ID token. PKCE code binding is present; no account-takeover exploit is claimed. Explicit provider-bound claim/nonce validation and real-provider acceptance remain required before production authentication closure.
Android nonce/logout working source now passes151 JVM tests, zero lint errors/warnings and APK/native-test compilation. Independent26-file diff review is completed/sealed as c358cde3-b51d-4451-b07d-8d1afe6c4485 with no findings; this is source-scoped and does not establish native/browser/provider execution. Core combined nonce/test corrections remain in progress; initial diagnostic failures stay failed.
+6 -19
View File
@@ -28,42 +28,29 @@ Install Android SDK platform 35 and its build tools, then from `qqq-android`:
```powershell
.\gradlew.bat :scanner-host:testDebugUnitTest :scanner-host:assembleDebug
adb devices -l
$tc22Serial = '<TC22 serial from adb devices>'
adb -s $tc22Serial install -r scanner-host/build/outputs/apk/debug/scanner-host-debug.apk
adb -s $tc22Serial reverse tcp:8080 tcp:8080
adb devices
adb install -r scanner-host/build/outputs/apk/debug/scanner-host-debug.apk
adb reverse tcp:8080 tcp:8080
```
Enable USB debugging on the TC22 and approve the workstation on the handheld when prompted. In QQQ Mobile, use `http://127.0.0.1:8080` as the local server. HTTP is permitted only for local testing by the debug build; deployed builds require HTTPS.
Always select the TC22 serial explicitly when an emulator or another Android device is attached. The current workstation demo runs on port 57832; use `adb -s $tc22Serial reverse tcp:8080 tcp:57832` for that server while keeping the app URL unchanged. `install -r` updates the APK without clearing its existing login or enrollment.
## Configure Zebra DataWedge
Create a dedicated profile associated with package `io.qrun.mobile.scanner` and activity `*`. Enable barcode input and Intent Output, disable Keystroke Output to avoid double delivery, and choose Broadcast Intent delivery. Set Intent action to `io.qrun.mobile.scanner.SCAN` and category to `android.intent.category.DEFAULT`.
In Intent Output's Component Information, restrict delivery to `io.qrun.mobile.scanner` and enable its application signature check. Use single-decode mode. Zebra documents barcode text, symbology and source in its [DataWedge 13 Intent Output guide](https://techdocs.zebra.com/datawedge/13-0/guide/output/intent/).
## Observed hardware results, 2026-10-30
The attached TC22 runs Android 14, DataWedge 13.0.325 and QQQ Mobile 0.1.0. Installation, sign-in, pending enrollment, approval from the browser record Actions menu, native app discovery and opening Scan Capture passed. Updating the APK preserved login. The profile uses the package/activity, input/output and explicit package/signature settings above.
The user confirmed two deliberate physical trigger presses; two scan records were visible in the backend. One stored row matched the handheld receipt for `QQQ-TEST-123456`, CODE128, source `scanner`, user `scanner`, TC22/Android 14/app 0.1.0, app `mobileScanApp`, process `mobileScanCapture`, and capture/receipt timestamps. This establishes basic physical Code128 decoding, delivery and persistence. It does not establish duplicate delivery suppression or retained-event retry behavior.
The user then confirmed a saved physical QR scan; the backend record preserved `001234567890` exactly, with `LABEL-TYPE-QRCODE`, source `scanner`, and matching TC22/user/app/process and capture/receipt metadata. Three physical records are now verified: two deliberate Code128 captures and one QR capture. Physical QR delivery and leading-zero preservation passed.
Physical connection-loss/retry and revocation-denial cases remain pending. Complete the remaining sequence below before claiming full hardware acceptance.
In Intent Output's Component Information, restrict delivery to `io.qrun.mobile.scanner` and enable its application signature check. Use single-decode mode. Zebra documents barcode text, symbology and source in its [Intent Output guide](https://techdocs.zebra.com/datawedge/14-0/guide/output/intent/).
## Acceptance sequence
1. Record TC22 Android version, DataWedge version and installed APK version.
2. Sign in as `scanner`. Enroll with a recognizable device name. Confirm the pending device cannot run Scan Capture.
3. In the browser, sign in as `admin`, open Mobile Scanner Administration and locate that device. From the device record choose Actions → Approve Device, review the selected device and confirm. Check the success result, then refresh status on the handheld. Also test approval of selected rows from the device list when multiple handhelds are available.
3. In the browser, sign in as `admin`, open Mobile Scanner Administration and locate that device. Copy its device ID, run Approve Device, and enter the ID in the form. Refresh status on the handheld.
4. Select Scan Capture. Scan a Code128 barcode and a QR code. Confirm one success receipt per scan.
5. Inspect Barcode Scans in the admin UI. Verify exact barcode text, symbology, source, user, device, capture/receipt timestamps and app/process identity. Include a barcode with leading zeroes.
6. Scan the same barcode twice deliberately. Confirm two events. Retry one retained event after a transport interruption and confirm one stored event for its UUID.
7. Stop the server. Attempt a scan and confirm no false success. Restart and explicitly retry the retained event. Verify the acknowledged row exists exactly once.
8. Run Revoke Device from that record while its scan screen remains open. Confirm the next capture is refused and no row is added. Verify bulk revocation affects the selected handhelds and leaves an unselected handheld unchanged.
8. Run Revoke Device with the same device ID while its scan screen remains open. Confirm the next capture is refused and no row is added.
9. Sign out. Confirm the scanner cannot save under the previous user and the previous user session is ended.
10. Restart the server and application. Confirm saved scans and device approval persist. Reinstallation/data reset must not silently inherit approval.
-53
View File
@@ -1,53 +0,0 @@
# Process acceptance tests
Every table-based process must be exercised through both the record screen and the bulk-selection UI. HTTP-only tests cannot establish that the menus, confirmation forms, results, or return navigation work. Mobile processes also require native UI tests against the real server.
| Process | Entry points | Required observable result |
| --- | --- | --- |
| Approve Device | Record Actions, checked rows, select-all/first-N, standalone IDs | Review selected devices; no early mutation; selected devices become approved; unselected devices stay unchanged; visible success result |
| Revoke Device | Record Actions, checked rows, select-all/first-N, standalone IDs | Selected devices become revoked; visible result; further capture is denied |
| Scan Capture | Android app picker and native process screen | Login, enrollment and approval gate; exact barcode stored with device/user metadata; acknowledgment; capture denied after revocation |
Scan Capture creates new scan records. It is not a process over existing table rows, so its entry point is the native scanner UI.
## Server and HTTP checks
With Java 21:
```powershell
.\mvnw.cmd verify install
.\mvnw.cmd -f demo/pom.xml verify
```
The HTTP suite uses an isolated H2 database and generated accounts. It sends the Android client-channel header on every request and exercises real login and process endpoints, asynchronous polling, confirmation/result steps, scan retry idempotency and authorization. Discovery must advertise the Android channel and the registered mobile-scanners extension. SQL assertions independently check persisted values and unaffected devices. Root process tests additionally cover malformed/oversized selections, first-N limits, and stable selection snapshots despite later enrollment or changed submitted values.
## Browser E2E
Build the server above, then generate the demo runtime classpath and install the pinned browser-test dependency:
```powershell
.\mvnw.cmd -f demo/pom.xml dependency:build-classpath '-Dmdep.outputFile=target/e2e-classpath.txt'
cd e2e
npm ci
npx playwright install chromium
npm test
```
The suite starts the packaged QQQ frontend and backend against a fresh temporary database. It signs in through the real session API and uses that session in a dedicated test browser, not your open browser or persistent demo. Native E2E separately covers the login UI. Port 57933 is the default; set `MOBILE_E2E_PORT` to choose another unused port. An already running server is never reused. Traces are disabled to avoid recording authentication headers. Test reports are ignored by Git.
## Native Android E2E
The companion Android repository provides `scripts/ci/scanner-e2e.mjs`. After building the server runtime classpath above, use Java 21, Android SDK 35, Node.js and a running emulator:
```powershell
cd ..\qqq-android
.\gradlew.bat :scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest
$env:ANDROID_SERIAL = 'emulator-5554'
node scripts/ci/scanner-e2e.mjs ..\qbit-mobile-scanners
```
The runner uses a separate `io.qrun.mobile.scanner.e2e` package, a temporary server database and generated credentials. It preserves the normal scanner app's data and refuses physical devices. The test drives the actual login, pending approval, app picker and Scan Capture screens, then independently checks the saved row and revocation denial.
The semantic migration expands this to six scenarios: incompatible core version, missing extension, missing Android channel, baseline scan flow, additive future metadata, and native approval/revocation/cancellation forms. A test-only forwarding proxy matches requests to the real server's published OpenAPI operations and checks that rejected servers receive no credentials. Current verification results and remaining hardware checks are recorded in [session state](SESSION-STATE.md).
Hardware trigger, decoding and DataWedge delivery still require [TC22 acceptance](TC22-ACCEPTANCE.md). Emulator success does not establish physical scanner acceptance.
+5 -51
View File
@@ -1,66 +1,20 @@
# QQQ Mobile Scanners TODO
- [ ] Deliver Echo example: choose authorized printer and media once, then one scan/render/accepted-print job at a time until leave; reproduce exact value and symbology with worker-owned integer-module fitting and quiet zones.
- [ ] Coordinate qualified renderer fit, authorized printer/media discovery, least-privilege submit and retained-print adapter contracts with their owners via shared communications.
- [x] Verify scanContext 1.3 with root44/demo7/optional18 and Checkstyle: following-step QR/Code 128 branching, legacy omission isolation, trusted attribution and exact retry binding; physical scanner acceptance remains separate.
- [x] Apply the full scoped standards review corrections and verify server39/demo6 with Checkstyle; see [review](CODING-STANDARDS-REVIEW-2026-11-01.md).
- [ ] Finish final hosted checks and remaining physical/provider acceptance; source compliance is not production release approval.
- [x] Upgrade the normal local demo to qualified API1.9/mobile1.1 with fresh closed backup, original-data invariants, recovery qualification and live verification of both existing accounts and preserved devices/scans.
- [x] Implement/review additive extension1.2 whole-device source with explicit Android consent, mode-bound leases and automated controls, preserving1.1 app-content compatibility.
- [ ] Complete actual Android projection/notification/Home/Stop/lock/rotation/expiry/remote-logout acceptance; normal server remains mobile1.1.
- [x] Qualify the demo BOM import and managed JUnit engine with fresh37 server/five demo tests, exact runtime module/Guava/H2 provenance and all15 single-record/bulk browser cases; see [acceptance evidence](API19-CONSUMER-ACCEPTANCE.md). The operator runtime remains separate.
- [x] Preserve legacy timestamp instants in the canonical supplied H2 schema with an explicit UTC migration connection; verify non-UTC file-backed upgrade, unchanged passwords/approval/scans and repeated initialization. The operator upgrade is preserved with UTC; a no-install reactor passes 37 base tests and five demo tests using the freshly built schema.
- [x] Implement unique short names, editable names/descriptions, additive H2 migration and protected profile editing; qualify the preserving upgrade of the operator database before deployment.
- [x] Implement separate approval/presence states, online-only fleet query and foreground location with accuracy/age and permission-denied behavior; actual GPS-fix acceptance remains in the release gates.
- [x] Implement the optional Record Files contract/native capture/association path and qualify the final coherent core32/scanner1a3 consumer cohort: 39 scanner, six demo HTTP, 18 Record Files and 15 browser cases. See [qualification scope](../integrations/record-files/docs/CORE-32DF-CONSUMER-QUALIFICATION.md); this is not a files-owner full-suite or default-adoption claim.
- [ ] Qualify coherent immutable/default RF dependencies and actual TC22 camera/gallery permissions and durable authorized association; no normal-runtime adoption is implied.
- [x] Device/session history, live activity and demand-based actual app-screen viewing through native QQQ administration and semantic extension 1.1.
- [x] Single-record and bulk End Session process with server-enforced invalidation, audit and browser/native E2E coverage.
- [x] Complete and seal the full server deep static security review of `0926550`; no reportable findings, with external implementation/deployment limitations retained.
- [ ] Complete final revision/scope reconciliation of historical security reports and remaining external deployment/provider gates. Independent architecture A1–A11 source closure and the filesystem regression correction are recorded; do not relabel partial/historical reports as a new exhaustive scan.
- [ ] Complete production 1.0 gates in [PRODUCTION-READINESS.md](PRODUCTION-READINESS.md), including physical acceptance and deployment/release requirements.
- [x] Native qbit device/scan tables and permission boundaries.
- [x] Authenticated enrollment, approval and revocation.
- [x] Native Scan Capture process and idempotent scan storage.
- [x] Android login, device gate, app picker and current trusted EMDK scanner path; prior DataWedge handling is historical.
- [x] Android login, device gate, app picker and DataWedge scan handling.
- [x] Persistent local demo with separate scanner/admin accounts.
- [x] Automated HTTP acceptance: pending denial, approval, capture, duplicate retry, revoke denial.
- [x] Android build and meaningful unit checks.
- [x] Standard repository files, current shared CircleCI profile, demo/Windows CI, CodeQL and Dependabot.
- [x] Validate CircleCI, workflow syntax and verification-only publication policy locally.
- [x] Connect hosted CI and confirm actual successful status checks.
- [x] Define branch protection policy from observed hosted checks; enforcement lives in GitHub settings.
- [ ] Finish repository protections after publishing bootstrap additions.
- [x] Server-configured customer branding for one shared APK.
- [x] Public status endpoint for the Android connection footer and diagnostics.
- [x] Android emulator installed; native UI tests passed.
- [x] Single-record and bulk approval/revocation with confirmation, stable selection and visible results.
- [x] HTTP acceptance for checked rows, filtered bulk selection, persisted results and unselected-device isolation.
- [x] Browser E2E acceptance for all administration processes from record and bulk menus (six scenarios passed).
- [x] Scanner-host native E2E acceptance against an isolated real demo server.
- [x] Register enrollment, branding and status as the documented mobile-scanners 1.0.0 semantic UI API extension; generate OpenAPI and check the versioned contract baseline.
- [x] Verify extension discovery, real HTTP payload schemas, standard errors, public stale-credential behavior and rejection of legacy routes (root 23 tests and demo HTTP verification passed).
- [x] Coordinate Android core/extension compatibility checks before login and native semantic process/form updates.
- [x] Register the Android client channel while preserving host channel policies; verify the real HTTP suite sends Android-channel requests successfully.
- [x] Rerun all six browser E2E scenarios against the migrated server (passed in 2.9 minutes).
- [x] Verify the migrated client/server pair's incompatible-version, missing-extension, missing-channel, baseline scan and additive-metadata native scenarios with the OpenAPI request audit.
- [x] Verify Android's 53 JVM tests, six shared native UI tests, lint and debug/E2E APK builds.
- [x] Complete native approval/revocation/cancellation forms after the completion-state fix (255.917 seconds; all 31 requests matched to 13 published operations).
- [x] Publish server semantic API migration commit `7dd02b8` and verify its hosted CircleCI, CodeQL, Windows helper and demo HTTP/browser checks all pass.
- [ ] Finish current EMDK physical acceptance: Code128 positive controls pass; current QR, lifecycle/ownership and failure/retry/leave matrix remain open.
- [x] Identify the physical TC22, install the debug app, sign in and enroll through USB forwarding.
- [x] Approve the TC22 through its browser record Actions menu and open its native app catalog and Scan Capture process; preserve login across APK update.
- [x] Configure the TC22 DataWedge profile with explicit package/signature delivery; verify two deliberate Code128 hardware captures persist with matching metadata.
- [x] Verify physical QR delivery and exact preservation of leading zeroes in `001234567890`, with matching persisted scan metadata.
- [ ] Finish physical connection-loss/retry and revocation-denial acceptance alongside the requested device/session controls.
- [x] TC22 install and DataWedge acceptance guide written and updated with observed results.
- [ ] TC22 physical scanner acceptance.
- [x] TC22 install and DataWedge acceptance guide written; physical execution remains pending.
Future scope (not version one): offline queue, camera-based barcode decoding, kiosk/MDM provisioning, broader native screen coverage, fleet rollout. Optional record-photo capture is implemented; its remaining qualification is tracked above.
- [ ] Complete the optional semantic API1.10 provider-logout capability and client AppAuth end-session handling; keep local cleanup reliable on provider failure, qualify real A-to-B handoff, and apply the user's pending sign-out policy. Existing prompt-only retained-SSO handoff is not accepted.
- [ ] Validate native OIDC ID-token claims and original request nonce before creating the QQQ session. Review confirmed request continuity and S256 PKCE exist, but expected nonce is not currently carried to the server exchange. Add explicit semantic capability/wire compatibility and rejection tests; retain real-provider nonce acceptance as pending.
Future scope (not version one): offline queue, camera fallback, kiosk/MDM provisioning, broader native screen coverage, fleet rollout.
-72
View File
@@ -1,72 +0,0 @@
# Mobile Scanners semantic UI API
Mobile Scanners is an independently versioned QQQ UI API extension. `MobileScannersQBitProducer` registers the `mobile-scanners` contract, currently **1.3.0**, 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](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` |
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.
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.
## Contract checks and compatibility
The published fragment is [mobile-scanners.yaml](../src/main/resources/openapi/qqq-v1/mobile-scanners.yaml); the initial release baseline is [mobile-scanners-1.0.0.yaml](../src/test/resources/openapi/mobile-scanners-1.0.0.yaml). `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:
```powershell
.\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.
@@ -1,80 +0,0 @@
# Independent architecture and maintainability closure review
Date: 2026-11-01. Reviewer: independent screen_security_review agent.
Android source: 229fce30a1b859a7c443a3ad68e5d6cd3a7cc6de, R:/Git.Local/QRunIO/qqq-android-wt-ci.
Server source: 2de847049941f93c8dde37bd7696aa352bbbcbfa, R:/Git.Local/QRunIO/qbit-mobile-scanners; implementation checkpoint 10416938724c9663d66f7848ff2b1a63fe635794.
Both HEADs matched and working trees were clean when inspected. This review does not modify or reopen any sealed security scan.
## Outcome
No additional concrete release-blocking source defect established by this architecture/maintainability pass. A1-A11 have identifiable source remediations and regression tests. That is source closure, not blanket production readiness: immutable dependency adoption, final hosted/native evidence, configured identity provider, actual EMDK/MediaProjection/GPS/camera acceptance, and production signing/deployment remain gates.
This is a cross-module architecture closure review, using the engineering architecture and code-review workflows. It is not a new exhaustive security scan, dependency audit, per-line review of every model/helper, or runtime acceptance. Tests cited below were inspected as source, not executed by this reviewer. Counts/results in PRODUCTION-READINESS.md remain the root's separate evidence; they are not re-certified here.
## Coverage and component responsibilities
Production inventory was enumerated for shared Android core/mobileapp libraries, scanner host main/production sources, historical sample app, base server, demo and optional Record Files adapter. Focused source tracing covered the following execution paths:
- Core owns semantic HTTP operations, credential-free compatibility discovery, structured errors and ordinary JSON value serialization. UiApiCompatibility.kt:29-83 checks origin-scoped discovery, required features/channel and major/minor ranges; NetworkQQQRepository.kt:73-183 supplies origin-bound transport/session handling and provisional login-cookie adoption. No scanner business operation is implemented in core.
- Shared mobileapp owns authentication generation, process initialization/submission/status/cancellation, typed form semantics and navigation. Scanner/photo controls are host extensions; ProcessAcknowledgment.kt is an explicit receipt policy instead of interpreting any eventId as a scanner request. SemanticProcess.kt:39-109 blocks unsupported required controls at submission and rendering boundaries.
- Scanner host owns server-scoped encrypted enrollment credentials, enrollment UI, hardware driver ownership, optional foreground location, app/display capture and optional photo transport. ScannerActivity wires these to the existing QViewModel/process renderer. DeviceCredentials.kt:22-88 uses per-server no-backup files, AES-GCM/Keystore and atomic persistence; corrupt storage fails closed.
- ScannerInputSession/ScannerDriver isolates the vendor SDK. Production factory selects EMDK only after presence checks; ZebraScannerDriver marshals callbacks to Main, protects a single outstanding manager claim and releases late acquisitions. ScannerInputSession.kt:42-157 combines generation, active owner, accepting/armed state and a live surface predicate. Physical vendor behavior remains unproved by these fakes.
- Base server owns approved device credentials, authenticated private session bindings, scan receipts/idempotence, administrative selections, presence and bounded live pixels. MobileScannersUiApi is the sole semantic extension registration, and specs retain stable operations. MobileScannersRouteProvider.java:103-115 verifies device headers; MobileSessionService.java:86-122 binds device/user to authenticated UUID; MobilePermissions.java:45-118 restricts scanner/admin actions and generic table access.
- Demo is a local TABLE_BASED acceptance host, not a production authentication deployment: MobileScannersDemo.java:197-204 explicitly binds loopback; DemoAuthenticationModule.java grants roles only to its validated local accounts.
- Optional Record Files adapter depends on both qbits in its own module. MobileCaptureIdentityProvider.java:28-66 exposes public attribution, validates provider identity and delegates a protected commit callback to MobileDeviceAuthorization. The base scanner qbit does not acquire a Record Files dependency. Contract discovery is public vocabulary, not upload authorization (MobileRecordFilesContract.java:43-89).
These boundaries provide sufficient cohesion for the present feature set. The larger ProcessViewModel/QViewModel coordinators have lifecycle responsibilities and regression seams; their size alone is not a release blocker. No speculative abstraction, HTTP-client unification, framework replacement or wholesale refactor is recommended by this review.
## Initial A1-A11 reconciliation
Paths in this table are relative to Android unless prefixed Server. Long package roots are identified after the table.
| ID | Current enforcing source | Regression evidence inspected | Closure and limit |
| --- | --- | --- | --- |
| A1 incomplete scan receipt | host ScannerWidget.kt:91-92 requests matching event, scanSaved=true and nonblank scanId; mobileapp ProcessAcknowledgment.kt:13-15 checks all requirements; ProcessViewModel.kt:554-570 retains pending request on mismatch | mobileapp SemanticProcessTest.kt:62; ProcessSubmissionTest.kt:240-256,312; host ScannerInputSessionTest.kt:100 | Source remediated; physical/real-server retry acceptance remains separate. |
| A2 invalid length metadata | host ScannerMetaData.kt:11-19 defaults only when absent, rejects nonnumeric/fractional/out-of-range input; ScannerWidget preserves raw barcode and rejects overlength rather than truncating | ScannerMetaDataTest and ScanEventTest; ScannerInputSessionTest.kt:100 | Source remediated; no silent metadata coercion required for supported scanner vocabulary. |
| A3 typed INPUT_FIELD/action confusion | InputFieldWidgetBlock.kt:62-107 uses typed metadata, explicit submitOnEnter and data callback; SemanticProcess.kt:76-109 collects supported widget fields; ProcessViewModel.kt:390-411 validates and converts at submission | SemanticProcessTest.kt:80,149,155; native SemanticInputTests.kt:118,132 | Source remediated; unsupported selectors/components fail explicitly. |
| A4 async status retry | ProcessViewModel.kt:462-479 retries known job status without replaying step mutation; checkIn:610-640 preserves known job on network failure | ProcessSubmissionTest.kt:167 | Source remediated. Ordinary unknown mutation results are intentionally not automatically replayed. |
| A5 late initialization cancellation | ProcessViewModel.kt:314-349 preserves returned process identity and cancels it when cancellation intent is set; cancel:646-678 does not discard the only pending identity | ProcessSubmissionTest.kt:192,214 | Source remediated for explicit process cancellation; transport/provider/server behavior remains a separate integration boundary. |
| A6 offscreen/keyboard actions | QPasswordLogin.kt:41 scrolling+IME padding; QProcessHome.kt:173-176 weighted scroll area+IME padding; ProcessComponentEditForm clears focus/hides keyboard | native SemanticInputTests.kt:56,83; session E2E fresh-login test now waits for completed logout and enabled button | Source remediation present. Historical hosted failure claims must use final execution evidence; this review does not infer success from compilation or the timing change alone. |
| A7 null/default mismatch | SemanticFields.kt:8-9 checks containsKey; EditForm initializes and submission uses same semanticInitialValue; widget writes default only when key absent | SemanticProcessTest.kt:72 | Source remediated. Explicit null remains null rather than visually showing an unsubmitted default. |
| A8 serialization envelope collision | core GenericSerializing.kt:84-127 handles wire objects as ordinary maps/lists/primitives; no wire-selected class discovery | core GenericSerializingTest.kt:73 | Source remediated. Legacy API wrappers remain for source compatibility, not wire class selection. |
| A9 partial multi-write operations | Server MobileService.java:82-87 enrolls both rows transactionally; :188-227 bulk approval transaction rejects no-op backend transactions; MobileSessionService.java:98-108 creates session/binding transactionally; :274-298 commits every selected tombstone before provider cleanup; :255-267 records cleanup status | Server MobileScannersTest.java:379,623; HTTP bulk lifecycle; optional MobileRecordFilesIntegrationTest.java:539 protected commit cases | Source remediated for local supported transaction model. Cross-node revocation/commit exclusion is explicitly outside the instance monitor; production topology must be constrained or supplied with transactional authorization. |
| A10 media ownership | ProcessSound.kt:17-42 reuses/releases one optional player; QProcessHome.kt:224-255 binds release to stop/disposal; ImageWidgetBlock.kt:52-56 owns/shuts down loader; AudioWidgetBlock releases on stop/disposal; host PhotoUploadViewModel.kt:306-319 cancels I/O, closes source and removes owned files after completion | ProcessSoundTest.kt:10,40; PhotoUploadViewModelTest.kt:34,55,91,128,160; native image/camera source cases | Targeted ownership fixes present. Actual camera, content-provider blocking and native graphics/audio services are not proved by JVM tests. |
| A11 navigation state drift | QNavigator.kt:117-153 installs/removes destination listener and reconciles actual controller state; :161-235 routes app/process/Back/Home through leave policy; QMobileAppMain owns shared drawer-first Back action | native NavigationTests.kt:118,211,277,296 and session rotation/recreation source test | Source remediated. Current hosted result must still be tied to exact revision; no device walkthrough claimed here. |
Package roots: host = scanner-host/src/main/java/io/qrun/mobile/scanner; mobileapp viewmodels = qqq/android/mobileapp/src/main/java/com/kingsrook/qqq/frontend/android/mobileapp/viewmodel; mobileapp UI = qqq/android/mobileapp/src/main/java/com/kingsrook/qqq/frontend/android/mobileapp/ui; core = qqq/android/core/src/main/java/com/kingsrook/qqq/frontend/android/core. Corresponding JVM tests use src/test/java and native tests live in qqqsampleandroidmobileapp/src/androidTest/java/com/kingsrook/qqq/sampleandroidmobileapp. Server main package is src/main/java/com/kingsrook/qbits/mobilescanners; optional adapter package is integrations/record-files/src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles.
## Current lifecycle, concurrency and resource conclusions
1. Scan retry identity is device+event based, with a deterministic database key; concurrent insert recovery checks barcode/symbology/source/user, not just ID (MobileService.java:243-299). Leading zeroes remain strings. Approval and session termination are enforced around local capture, not delegated to UI metadata.
2. Administrative selection is reviewed before application and frozen into bounded server process records. MobileAdminSelection.java:37-145 caps selection at100 and validates every target. Server tests cover single/bulk/manual/filter/first-N paths. This is stronger than replaying a caller's replacement selection at apply time.
3. Local session authorization and screen operations use a per-QInstance monitor (MobileSessionService.java:48-63). Optional attachment commits are held through MobileDeviceAuthorization.executeAuthorized, including empty desktop attribution. This is not a distributed lock or a tenant partition guarantee. No shared-QInstance multitenant or multi-JVM safety certification follows from it.
4. Screen demand and mode are independent controls: changing mode removes old pixels/lease; upload must match both current session mode and exact lease mode (MobileSessionService.java:141-145,312-327; MobileScreenService.java:62-79,103-113). Client consent is enforced by Android, not cryptographically attested to the server. Server display wording is an approved-client assertion.
5. Screen retention is bounded to64 leases and one compressed latest frame each;30second demand,10second visible freshness, bounded Base64/JPEG/dimensions and sequence/rate checks precede retention (MobileScreenService.java:29-31,73-88,103-182). Expiry is access enforcement with lazy pruning on subsequent reads/writes (:201), not scheduled heap erasure. No disk frame storage is introduced.
6. Android projection keeps one latest native image, drains to prevent the old queued-image problem, copies pixels on Main before background JPEG encoding, and discards results after projection/owner change. Notification/lock/stop/lease cleanup is explicit; actual Android behavior remains a physical gate.
7. Photo uploads are scoped to session+process+block+parent, held in a navigation-owned ViewModel (RecordPhotoWidget.kt:47-73). Status/create recovery retains immutable request/item identity, chunk bytes and exact-original SHA; only matching COMPLETE association receipts permit Saved/local deletion (PhotoUploadViewModel.kt:267-294; RecordFilesApi.kt:87-102). Background pause preserves pending original, while destruction releases only files allocated by that owner. This does not guarantee OS process-death recovery, and the UI explicitly requires staying open/connected.
8. Location remains optional foreground-only telemetry, with accuracy/source timestamps and listener removal (ForegroundLocation.kt:41-75). Server schemas describe last-known observations, not current-position proof. Actual permission/fix/disabled-provider behavior is not established by source.
9. Authentication remains origin/attempt generation bound; provisional responses are not automatically installed by the network layer, logout blocks re-login through completion, and stale metadata callbacks are generation checked. Configured Authentik/browser SSO acceptance and trust configuration are owned by the separate investigation, not closed here.
## Coding standards and operational closure
Server production classes inspected use the repository's three-space/Allman/import/SPDX conventions and normal QQQ actions/semantic specs. Android additions generally have small native-boundary helpers and explicit lifecycle ownership; older shared files retain historical comments/import formatting. No coding-style variation observed establishes a release-blocking behavioral defect. Actual Checkstyle/lint verdicts must remain tied to their existing build evidence; they were not rerun here.
Compatibility is additive in source: absent modes mean APP_CONTENT, optional RF vocabulary is separately discovered, unknown required controls stop submission, and server current OpenAPI is compared with preserved1.0/1.1 baselines. However, server pom.xml:37-40 still defaults qqq.version to4.1.0-RC.20, and integrations/record-files/pom.xml:10-24 retains frozen/snapshot tuple coordinates. A passing isolated source-pinned consumer is not the default dependency graph. Before supported production adoption, publish/select one coherent immutable tuple and prove its effective runtime contents. RF tuple qualification is being investigated separately; this artifact does not supersede it.
Release automation was separately reviewed read-only in this same reviewer thread: read-only verification token, exact companion source pins/provenance, environment-scoped signing, APK certificate/package/version/non-debuggable readback, and publish-time tag target/hash checks were present. External environment/tag protection, production key/recovery and actual signed upgrade remain unverified; no signing/distribution action is authorized by this artifact.
## Remaining release gates and exclusions
- Fresh final-revision hosted native/session login evidence; final UI source guard alone is not a result.
- Actual current EMDK trigger formats, repeated events, pause/restart/ownership restoration and error/retry/leave walkthrough. Earlier DataWedge evidence does not qualify EMDK.
- Operator-mediated MediaProjection consent, notification, Home/other-app pixels, stop, lock, rotation, expiry and remote-session termination on the refined candidate.
- Actual GPS permission/fix/disabled behavior, camera/gallery permissions and durable authorized record association on a matched runtime.
- Configured Authentik native/browser exchange, provider rotation, shared-operator SSO and trust setup; external provider behavior and dependencies are not audited here.
- Coherent immutable core/server/RF/attachment-owner/RDBMS publication and deployment provenance. Root/other reviewers own tuple investigation.
- Defined supported deployment/database migration and topology. Local monitor guarantees do not extend to clusters; no tenant-specific policy has been established by this qbit review.
- Organization signing key/environment/tag safeguards, recovery and signed APK update acceptance, then distribution only at the owner's deferred final step.
No network, builds, tests, runtime processes, device actions, repository edits or commits were performed for this closure pass. Only this owned cache artifact was written. Prior sealed security reports keep their original revisions, scopes and exclusions; this report does not silently broaden them.
-7
View File
@@ -1,7 +0,0 @@
// Copyright (c) 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import { startDemo } from './start-demo.mjs';
export default async function globalSetup() {
return startDemo();
}
-58
View File
@@ -1,58 +0,0 @@
{
"name": "qqq-mobile-scanners-browser-acceptance",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "qqq-mobile-scanners-browser-acceptance",
"devDependencies": {
"@playwright/test": "1.64.1"
}
},
"node_modules/@playwright/test": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
"integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.64.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.64.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright-core": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
}
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"name": "qqq-mobile-scanners-browser-acceptance",
"private": true,
"type": "module",
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "1.64.1"
}
}
-29
View File
@@ -1,29 +0,0 @@
// Copyright (c) 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import { randomBytes } from 'node:crypto';
import { defineConfig } from '@playwright/test';
process.env.MOBILE_E2E_PORT ??= '57933';
process.env.MOBILE_E2E_SCANNER_PASSWORD ??= randomBytes(24).toString('base64url');
process.env.MOBILE_E2E_ADMIN_PASSWORD ??= randomBytes(24).toString('base64url');
process.env.MOBILE_DEMO_SCANNER_PASSWORD = process.env.MOBILE_E2E_SCANNER_PASSWORD;
process.env.MOBILE_DEMO_ADMIN_PASSWORD = process.env.MOBILE_E2E_ADMIN_PASSWORD;
const baseURL = `http://127.0.0.1:${process.env.MOBILE_E2E_PORT}`;
export default defineConfig({
testDir: './tests',
fullyParallel: false,
workers: 1,
retries: 0,
timeout: 90_000,
expect: { timeout: 15_000 },
reporter: [['list'], ['html', { open: 'never' }]],
globalSetup: './global-setup.mjs',
use: {
baseURL,
browserName: 'chromium',
viewport: { width: 1440, height: 1000 },
trace: 'off',
screenshot: 'only-on-failure',
},
});
-60
View File
@@ -1,60 +0,0 @@
// Copyright (c) 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import { spawn } from 'node:child_process';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, delimiter, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
/** Start one real demo with isolated storage; return awaited shutdown and cleanup. */
export async function startDemo() {
const port = Number(process.env.MOBILE_E2E_PORT);
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error('Set MOBILE_E2E_PORT to an unoccupied test port.');
const url = `http://127.0.0.1:${port}/qqq/v1/qbit/mobile-scanners/status`;
try {
await fetch(url, { signal: AbortSignal.timeout(500) });
throw new Error('Test port is already in use; refusing to reuse a server.');
} catch (error) {
if (error.message.startsWith('Test port')) throw error;
}
const repository = fileURLToPath(new URL('../', import.meta.url));
const classpath = [resolve(repository, 'demo/target/classes'), (await readFile(resolve(repository, 'demo/target/e2e-classpath.txt'), 'utf8')).trim()].join(delimiter);
const directory = await mkdtemp(join(tmpdir(), 'qqq-mobile-e2e-'));
const java = process.env.JAVA_HOME ? join(process.env.JAVA_HOME, 'bin', process.platform === 'win32' ? 'java.exe' : 'java') : 'java';
const child = spawn(java, ['-cp', classpath, 'com.kingsrook.qbits.mobilescanners.demo.MobileScannersDemo', String(port)], {
cwd: directory,
stdio: 'ignore',
windowsHide: true,
});
let failure;
const exited = new Promise((done) => {
child.once('error', (error) => { failure = error.code; done(); });
child.once('exit', (code) => { failure = `exit ${code}`; done(); });
});
const cleanup = async () => {
child.kill();
await exited;
const target = resolve(directory);
if (dirname(target) !== resolve(tmpdir()) || !basename(target).startsWith('qqq-mobile-e2e-')) throw new Error('Refusing unexpected test cleanup path.');
await rm(target, { recursive: true, force: true, maxRetries: 3 });
};
try {
const deadline = Date.now() + 60_000;
while (Date.now() < deadline) {
if (failure) throw new Error(`Isolated demo startup failed: ${failure}`);
try {
if ((await fetch(url, { signal: AbortSignal.timeout(1000) })).ok) return cleanup;
} catch { /* Wait for the child server, not an unrelated persistent demo. */ }
await new Promise((done) => setTimeout(done, 200));
}
throw new Error('Isolated demo startup timed out.');
} catch (error) {
await cleanup();
throw error;
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const cleanup = await startDemo();
for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, async () => { await cleanup(); process.exit(0); });
}
-183
View File
@@ -1,183 +0,0 @@
// Copyright (c) 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import { randomUUID } from 'node:crypto';
import { test, expect, request as requestFactory } from '@playwright/test';
const handheldRequests = new Set();
test.afterEach(async () => {
await Promise.all([...handheldRequests].map((request) => request.dispose()));
handheldRequests.clear();
});
async function loginApi(request, account) {
const password = process.env[account === 'admin' ? 'MOBILE_DEMO_ADMIN_PASSWORD' : 'MOBILE_DEMO_SCANNER_PASSWORD'];
const response = await request.post('/qqq/v1/manageSession', {
headers: { Authorization: `Basic ${Buffer.from(`${account}:${password}`).toString('base64')}` },
data: {},
});
expect(response.status()).toBe(200);
}
async function enroll(name) {
// A real handheld has its own core login, even when multiple devices share a user.
const handheld = await requestFactory.newContext({ baseURL: `http://127.0.0.1:${process.env.MOBILE_E2E_PORT}` });
handheldRequests.add(handheld);
await loginApi(handheld, 'scanner');
const response = await handheld.post('/qqq/v1/qbit/mobile-scanners/devices/enroll', {
data: { installationId: randomUUID(), displayName: name, manufacturer: 'Test', model: 'Browser fixture', osVersion: 'test', appVersion: 'test' },
});
expect(response.ok()).toBeTruthy();
return { ...await response.json(), request: handheld };
}
async function loginBrowser(page) {
// Authenticate through the real login endpoint without recording password-fill steps.
await loginApi(page.request, 'admin');
// QQQ only attempts HttpOnly-cookie resume when its non-secret sign-in hint exists.
// The real server session still supplies the identity and permission checks.
await page.addInitScript(() => localStorage.setItem('qqq.sessionHint', '1'));
await page.goto('/app/');
await expect(page.getByRole('button', { name: 'Account: Device Administrator' })).toBeVisible();
}
async function status(device) {
const response = await device.request.get('/qqq/v1/qbit/mobile-scanners/devices/current', {
headers: { 'X-QQQ-Device-Id': device.deviceId, 'X-QQQ-Device-Token': device.deviceToken },
});
expect(response.status()).toBe(200);
return (await response.json()).status;
}
async function chooseAction(page, label, record = false) {
await page.getByRole('button', { name: record ? 'Record actions menu' : 'Actions', exact: true }).click();
await page.getByRole('menuitem', { name: label, exact: true }).click();
}
async function confirmSelected(page, devices, result) {
const confirmation = page.locator('[data-qqq-id="process-step-input"]');
for (const device of devices) await expect(confirmation).toContainText(device.deviceId);
await confirmation.locator('[data-qqq-id="button-next"]').click();
await expect(page.locator('[data-qqq-id="process-step-result"]')).toContainText(result);
}
test('approve and revoke from a single record show results and persist the selected device', async ({ page, request }) => {
await loginApi(request, 'scanner');
const device = await enroll(`Single ${randomUUID()}`);
await loginBrowser(page);
for (const [action, before, after, result] of [
['Approve Device', 'PENDING', 'APPROVED', '1 device approved.'],
['Revoke Device', 'APPROVED', 'REVOKED', '1 device revoked.'],
]) {
await page.goto(`/app/mobileDevice/${device.deviceId}/`);
await chooseAction(page, action, true);
await expect.poll(() => status(device)).toBe(before);
await confirmSelected(page, [device], result);
await expect.poll(() => status(device)).toBe(after);
await page.getByRole('button', { name: 'Return', exact: true }).click();
await expect(page).toHaveURL(new RegExp(`/app/mobileDevice/${device.deviceId}/?$`));
await expect(page.getByRole('tabpanel', { name: 'Overview' })).toContainText(after);
}
});
test('bulk checked rows approve and revoke only selected devices', async ({ page, request }) => {
await loginApi(request, 'scanner');
const devices = [await enroll(`Bulk A ${randomUUID()}`), await enroll(`Bulk B ${randomUUID()}`)];
const untouched = await enroll(`Untouched ${randomUUID()}`);
await loginBrowser(page);
for (const [action, before, after, result] of [
['Approve Device', 'PENDING', 'APPROVED', '2 devices approved.'],
['Revoke Device', 'APPROVED', 'REVOKED', '2 devices revoked.'],
]) {
await page.goto('/app/mobileDevice/');
for (const device of devices) await page.getByRole('checkbox', { name: `Select ${device.deviceId}`, exact: true }).check();
await expect(page.getByRole('region', { name: 'Bulk actions' })).toContainText('2 records');
await chooseAction(page, action);
for (const device of devices) await expect.poll(() => status(device)).toBe(before);
await confirmSelected(page, devices, result);
for (const device of devices) await expect.poll(() => status(device)).toBe(after);
await expect.poll(() => status(untouched)).toBe('PENDING');
}
});
test('standalone admin processes accept an explicit device ID', async ({ page, request }) => {
await loginApi(request, 'scanner');
const device = await enroll(`Manual ${randomUUID()}`);
await loginBrowser(page);
for (const [process, after, result] of [
['mobileDeviceApprove', 'APPROVED', '1 device approved.'],
['mobileDeviceRevoke', 'REVOKED', '1 device revoked.'],
]) {
await page.goto(`/app/${process}/`);
await page.getByRole('textbox', { name: 'Device IDs', exact: true }).fill(device.deviceId);
await page.locator('[data-qqq-id="button-next"]').click();
await expect(page.locator('[data-qqq-id="process-step-result"]')).toContainText(result);
await expect.poll(() => status(device)).toBe(after);
}
});
test('cancel from record confirmation leaves device pending', async ({ page, request }) => {
await loginApi(request, 'scanner');
const device = await enroll(`Cancel ${randomUUID()}`);
await loginBrowser(page);
await page.goto(`/app/mobileDevice/${device.deviceId}/`);
await chooseAction(page, 'Approve Device', true);
await expect(page.locator('[data-qqq-id="process-step-input"]')).toContainText(device.deviceId);
await page.getByRole('button', { name: 'Cancel', exact: true }).click();
await page.getByRole('dialog', { name: 'Cancel Process?' }).getByRole('button', { name: 'Cancel Process', exact: true }).click();
await expect.poll(() => status(device)).toBe('PENDING');
});
test('full query selection approves and revokes through the filter-based bulk path', async ({ page, request }) => {
await loginApi(request, 'scanner');
const devices = [await enroll(`All A ${randomUUID()}`), await enroll(`All B ${randomUUID()}`)];
await loginBrowser(page);
for (const [action, after, verb] of [
['Approve Device', 'APPROVED', 'approved'],
['Revoke Device', 'REVOKED', 'revoked'],
]) {
await page.goto('/app/mobileDevice/');
await page.getByRole('button', { name: 'Selection', exact: true }).click();
await page.getByRole('menuitem', { name: /^Full query result/ }).click();
await chooseAction(page, action);
const confirmation = page.locator('[data-qqq-id="process-step-input"]');
for (const device of devices) await expect(confirmation).toContainText(device.deviceId);
await confirmation.locator('[data-qqq-id="button-next"]').click();
await expect(page.locator('[data-qqq-id="process-step-result"]')).toContainText(new RegExp(`\\d+ devices ${verb}\\.`));
for (const device of devices) await expect.poll(() => status(device)).toBe(after);
}
});
test('first-N bulk selection changes exactly the requested subset', async ({ page, request }) => {
await loginApi(request, 'scanner');
await enroll(`Subset A ${randomUUID()}`);
await enroll(`Subset B ${randomUUID()}`);
await enroll(`Subset C ${randomUUID()}`);
await loginBrowser(page);
for (const [action, after, result] of [
['Approve Device', 'APPROVED', '2 devices approved.'],
['Revoke Device', 'REVOKED', '2 devices revoked.'],
]) {
const beforeResponse = await page.request.post('/qqq/v1/table/mobileDevice/query', { data: { filter: { limit: 100 } } });
expect(beforeResponse.status()).toBe(200);
const before = new Map((await beforeResponse.json()).records.map((record) => [record.values.id, record.values.status]));
await page.goto('/app/mobileDevice/');
await page.getByRole('button', { name: 'Selection', exact: true }).click();
await page.getByRole('menuitem', { name: 'Subset of the query result...' }).click();
await page.getByRole('spinbutton', { name: 'How many records do you want to select?' }).fill('2');
await page.getByRole('button', { name: 'OK', exact: true }).click();
await chooseAction(page, action);
const confirmation = page.locator('[data-qqq-id="process-step-input"]');
await expect(confirmation).toContainText('2 device(s) selected');
const summary = await confirmation.innerText();
const selected = [...before.keys()].filter((id) => summary.includes(id));
expect(selected).toHaveLength(2);
await confirmation.locator('[data-qqq-id="button-next"]').click();
await expect(page.locator('[data-qqq-id="process-step-result"]')).toContainText(result);
const afterResponse = await page.request.post('/qqq/v1/table/mobileDevice/query', { data: { filter: { limit: 100 } } });
expect(afterResponse.status()).toBe(200);
for (const record of (await afterResponse.json()).records) {
expect(record.values.status).toBe(selected.includes(record.values.id) ? after : before.get(record.values.id));
}
}
});
-360
View File
@@ -1,360 +0,0 @@
// Copyright (c) 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import { randomUUID } from 'node:crypto';
import { test, expect, request as requestFactory } from '@playwright/test';
const extension = '/qqq/v1/qbit/mobile-scanners';
async function login(request, account) {
const password = process.env[account === 'admin' ? 'MOBILE_DEMO_ADMIN_PASSWORD' : 'MOBILE_DEMO_SCANNER_PASSWORD'];
const response = await request.post('/qqq/v1/manageSession', {
headers: { Authorization: `Basic ${Buffer.from(`${account}:${password}`).toString('base64')}` }, data: {},
});
expect(response.status()).toBe(200);
}
async function admin(page) {
await login(page.request, 'admin');
await page.addInitScript(() => localStorage.setItem('qqq.sessionHint', '1'));
await page.goto('/app/');
await expect(page.getByRole('button', { name: 'Account: Device Administrator' })).toBeVisible();
}
async function complete(request, process, response) {
expect(response.status()).toBe(200);
let body = await response.json();
const processId = body.processUUID;
const jobId = body.jobUUID;
await expect.poll(async () => {
if (['JOB_STARTED', 'RUNNING'].includes(body.type)) {
const status = await request.get(`/qqq/v1/processes/${process}/${processId}/status/${jobId}`);
expect(status.status()).toBe(200);
body = await status.json();
}
return ['JOB_STARTED', 'RUNNING'].includes(body.type);
}).toBe(false);
expect(body.error).toBeFalsy();
return body;
}
async function approve(request, deviceId) {
const process = 'mobileDeviceApprove';
const selection = { recordsParam: 'recordIds', recordIds: deviceId, tableName: 'mobileDevice' };
let body = await complete(request, process, await request.post(`/qqq/v1/processes/${process}/init`, {
multipart: { values: JSON.stringify(selection), recordsParam: 'recordIds', recordIds: deviceId, stepTimeoutMillis: '1' },
}));
for (const step of ['input', 'result']) {
expect(body.nextStep).toBe(step);
body = await complete(request, process, await request.post(`/qqq/v1/processes/${process}/${body.processUUID}/step/${step}`, {
multipart: { values: '{}', stepTimeoutMillis: '1' },
}));
}
}
async function scanner(page, baseURL) {
// Each handheld has a separate authenticated cookie jar, even for the same user.
const request = await requestFactory.newContext({ baseURL });
try {
await login(request, 'scanner');
const response = await request.post(`${extension}/devices/enroll`, {
data: { installationId: randomUUID(), displayName: `Session E2E ${randomUUID()}`,
manufacturer: 'Test', model: 'Browser session fixture', osVersion: 'test', appVersion: 'test' },
});
expect(response.status()).toBe(201);
const device = await response.json();
await approve(page.request, device.deviceId);
const headers = { 'X-QQQ-Device-Id': device.deviceId, 'X-QQQ-Device-Token': device.deviceToken,
'X-QQQ-Client': 'android/qqq-mobile' };
const heartbeat = (values = {}) => request.post(`${extension}/sessions/heartbeat`, {
headers, data: { foreground: true, screenAvailable: true, appName: 'mobileScanApp',
processName: 'mobileScanCapture', stepName: 'scan', screenLabel: 'Scan Capture', ...values },
});
const started = await heartbeat();
expect(started.status()).toBe(200);
const session = await started.json();
return { request, headers, heartbeat, sessionId: session.sessionId, deviceId: device.deviceId };
} catch (error) {
await request.dispose();
throw error;
}
}
async function sessions(request) {
const response = await request.post('/qqq/v1/table/mobileSession/query', { data: { filter: { limit: 100 } } });
expect(response.status()).toBe(200);
return (await response.json()).records.map((record) => record.values);
}
async function state(request, id) {
const record = (await sessions(request)).find((row) => row.id === id);
expect(record).toBeDefined();
return record;
}
/** Read the persisted profile independently of the browser's form state. */
async function deviceState(request, id) {
const response = await request.post('/qqq/v1/table/mobileDevice/query', {
data: { filter: { criteria: [{ fieldName: 'id', operator: 'EQUALS', values: [id] }], limit: 1 } },
});
expect(response.status()).toBe(200);
const records = (await response.json()).records;
expect(records).toHaveLength(1);
return records[0].values;
}
async function chooseEnd(page, record = false) {
await page.getByRole('button', { name: record ? 'Record actions menu' : 'Actions', exact: true }).click();
await page.getByRole('menuitem', { name: 'End Session', exact: true }).click();
}
async function confirmEnd(page, ids) {
const confirmation = page.locator('[data-qqq-id="process-step-input"]');
for (const id of ids) await expect(confirmation).toContainText(id);
await confirmation.locator('[data-qqq-id="button-next"]').click();
await expect(page.locator('[data-qqq-id="process-step-result"]'))
.toContainText(`${ids.length} session${ids.length === 1 ? '' : 's'} ended.`);
}
test('session record termination requires confirmation, audits the actor and rejects cookie replay', async ({ page, baseURL }) => {
await admin(page);
const target = await scanner(page, baseURL);
const untouched = await scanner(page, baseURL);
try {
await page.goto(`/app/mobileSession/${target.sessionId}/`);
await chooseEnd(page, true);
expect((await state(page.request, target.sessionId)).status).toBe('ACTIVE');
await confirmEnd(page, [target.sessionId]);
const ended = await state(page.request, target.sessionId);
expect(ended.status).toBe('ENDED');
expect(ended.endedBy).toBe('admin');
expect(ended.endedAt).toBeTruthy();
expect((await target.heartbeat()).status()).toBe(401);
// Omission of device headers must not resurrect an ended core session.
expect((await target.request.get('/qqq/v1/metaData')).status()).toBe(401);
expect((await untouched.heartbeat()).status()).toBe(200);
await page.getByRole('button', { name: 'Return', exact: true }).click();
await expect(page).toHaveURL(new RegExp(`/app/mobileSession/${target.sessionId}/?$`));
await expect(page.getByRole('tabpanel', { name: 'Overview' })).toContainText('ENDED');
} finally {
await Promise.all([target.request.dispose(), untouched.request.dispose()]);
}
});
test('checked bulk session termination leaves an unselected session usable', async ({ page, baseURL }) => {
await admin(page);
const devices = [];
try {
for (let index = 0; index < 3; index++) devices.push(await scanner(page, baseURL));
await page.goto('/app/mobileSession/');
for (const device of devices.slice(0, 2)) {
await page.getByRole('checkbox', { name: `Select ${device.sessionId}`, exact: true }).check();
}
await expect(page.getByRole('region', { name: 'Bulk actions' })).toContainText('2 records');
await chooseEnd(page);
await confirmEnd(page, devices.slice(0, 2).map((device) => device.sessionId));
for (const device of devices.slice(0, 2)) {
expect((await state(page.request, device.sessionId)).status).toBe('ENDED');
expect((await device.heartbeat()).status()).toBe(401);
}
expect((await state(page.request, devices[2].sessionId)).status).toBe('ACTIVE');
expect((await devices[2].heartbeat()).status()).toBe(200);
} finally {
await Promise.all(devices.map((device) => device.request.dispose()));
}
});
test('cancelling session termination preserves access', async ({ page, baseURL }) => {
await admin(page);
const target = await scanner(page, baseURL);
try {
await page.goto(`/app/mobileSession/${target.sessionId}/`);
await chooseEnd(page, true);
await expect(page.locator('[data-qqq-id="process-step-input"]')).toContainText(target.sessionId);
await page.getByRole('button', { name: 'Cancel', exact: true }).click();
await page.getByRole('dialog', { name: 'Cancel Process?' }).getByRole('button', { name: 'Cancel Process', exact: true }).click();
expect((await state(page.request, target.sessionId)).status).toBe('ACTIVE');
expect((await target.heartbeat()).status()).toBe(200);
} finally {
await target.request.dispose();
}
});
test('standalone session termination accepts an explicit public session ID', async ({ page, baseURL }) => {
await admin(page);
const target = await scanner(page, baseURL);
try {
await page.goto('/app/mobileSessionEnd/');
await page.getByRole('textbox', { name: 'Session IDs', exact: true }).fill(target.sessionId);
await page.locator('[data-qqq-id="button-next"]').click();
await expect(page.locator('[data-qqq-id="process-step-result"]')).toContainText('1 session ended.');
expect((await state(page.request, target.sessionId)).status).toBe('ENDED');
expect((await target.heartbeat()).status()).toBe(401);
} finally {
await target.request.dispose();
}
});
test('first-N bulk session termination preserves every unselected record', async ({ page, baseURL }) => {
await admin(page);
const devices = [];
try {
for (let index = 0; index < 3; index++) devices.push(await scanner(page, baseURL));
const before = new Map((await sessions(page.request)).map((row) => [row.id, row.status]));
await page.goto('/app/mobileSession/');
await page.getByRole('button', { name: 'Selection', exact: true }).click();
await page.getByRole('menuitem', { name: 'Subset of the query result...' }).click();
await page.getByRole('spinbutton', { name: 'How many records do you want to select?' }).fill('2');
await page.getByRole('button', { name: 'OK', exact: true }).click();
await chooseEnd(page);
const confirmation = page.locator('[data-qqq-id="process-step-input"]');
await expect(confirmation).toContainText('2 session(s) selected');
const summary = await confirmation.innerText();
const selected = [...before.keys()].filter((id) => summary.includes(id));
expect(selected).toHaveLength(2);
await confirmEnd(page, selected);
for (const row of await sessions(page.request)) {
expect(row.status).toBe(selected.includes(row.id) ? 'ENDED' : before.get(row.id));
}
for (const device of devices) {
expect((await device.heartbeat()).status()).toBe(selected.includes(device.sessionId) ? 401 : 200);
}
} finally {
await Promise.all(devices.map((device) => device.request.dispose()));
}
});
test('full-query session termination ends the reviewed query result', async ({ page, baseURL }) => {
await admin(page);
const target = await scanner(page, baseURL);
try {
const before = await sessions(page.request);
await page.goto('/app/mobileSession/');
await page.getByRole('button', { name: 'Selection', exact: true }).click();
await page.getByRole('menuitem', { name: /^Full query result/ }).click();
await chooseEnd(page);
await confirmEnd(page, before.map((row) => row.id));
for (const row of await sessions(page.request)) expect(row.status).toBe('ENDED');
expect((await target.heartbeat()).status()).toBe(401);
} finally {
await target.request.dispose();
}
});
test('native record widget requests, refreshes and clears a leased screen image', async ({ page, baseURL }) => {
await admin(page);
const target = await scanner(page, baseURL);
try {
const before = await (await target.heartbeat()).json();
expect(before.screenMirror).toBeFalsy();
await page.goto(`/app/mobileSession/${target.sessionId}/`);
let demand;
await expect.poll(async () => {
demand = await (await target.heartbeat()).json();
return Boolean(demand.screenMirror?.leaseId);
}).toBe(true);
const imageBase64 = await page.evaluate(() => {
const canvas = document.createElement('canvas');
canvas.width = 160;
canvas.height = 240;
const context = canvas.getContext('2d');
context.fillStyle = '#0969da';
context.fillRect(0, 0, 160, 240);
context.fillStyle = '#ffffff';
context.font = '18px sans-serif';
context.fillText('Screen fixture', 12, 80);
return canvas.toDataURL('image/jpeg').split(',')[1];
});
const upload = await target.request.post(`${extension}/sessions/current/screen`, {
headers: target.headers, data: { leaseId: demand.screenMirror.leaseId,
sequence: demand.screenMirror.nextSequence, capturedAt: new Date().toISOString(),
width: 160, height: 240, mimeType: 'image/jpeg', imageBase64 },
});
expect(upload.status()).toBe(204);
const image = page.locator('img[src^="data:image/jpeg;base64,"]');
await expect(image).toHaveAttribute('src', `data:image/jpeg;base64,${imageBase64}`);
await expect(image).toBeVisible();
const background = await target.heartbeat({ foreground: false, screenAvailable: false, captureBlockedReason: 'BACKGROUND' });
expect(background.status()).toBe(200);
await expect(image).toHaveCount(0);
await page.goto(`/app/mobileDevice/${target.deviceId}/`);
await page.getByRole('tab', { name: 'Sessions', exact: true }).click();
await expect(page.getByRole('tabpanel', { name: 'Sessions', exact: true })
.locator(`a[href="/app/mobileSession/${target.sessionId}/"]`)).toBeVisible();
} finally {
await target.request.dispose();
}
});
test('device record editing saves names and description while preserving approval', async ({ page, baseURL }) => {
await admin(page);
const target = await scanner(page, baseURL);
try {
const initial = await deviceState(page.request, target.deviceId);
expect(initial.shortName).toMatch(/^scanner-[a-f0-9]{12}$/);
const shortName = `receiving-${randomUUID().slice(0, 8)}`;
await page.goto(`/app/mobileDevice/${target.deviceId}/edit/`);
await page.getByRole('textbox', { name: 'Short Name', exact: true }).fill(shortName);
await page.getByRole('textbox', { name: 'Display Name', exact: true }).fill('Receiving handheld');
await page.getByRole('textbox', { name: 'Description', exact: true }).fill('Assigned to receiving station 2');
await expect(page.getByRole('textbox', { name: 'Approval Status', exact: true })).toHaveCount(0);
await page.locator('[data-qqq-id="button-save"]').click();
await expect(page).toHaveURL(new RegExp(`/app/mobileDevice/${target.deviceId}/?$`));
const saved = await deviceState(page.request, target.deviceId);
expect(saved.shortName).toBe(shortName);
expect(saved.displayName).toBe('Receiving handheld');
expect(saved.description).toBe('Assigned to receiving station 2');
expect(saved.status).toBe('APPROVED');
expect(saved.approvedBy).toBe(initial.approvedBy);
expect(saved.approvedAt).toBe(initial.approvedAt);
const overview = page.getByRole('tabpanel', { name: 'Overview', exact: true });
await expect(overview).toContainText(shortName);
await expect(overview).toContainText('Assigned to receiving station 2');
expect((await target.heartbeat()).status()).toBe(200);
} finally {
await target.request.dispose();
}
});
test('device location stays attributable and online presence expires independently of approval', async ({ page, baseURL }) => {
await admin(page);
const target = await scanner(page, baseURL);
try {
// Synthetic coordinates, never the operator's physical location.
const capturedAt = new Date().toISOString();
expect((await target.heartbeat({ locationStatus: 'AVAILABLE', location: {
latitude: 10.125, longitude: 20.25, accuracyMeters: 7, capturedAt,
} })).status()).toBe(200);
let saved = await deviceState(page.request, target.deviceId);
expect(saved.status).toBe('APPROVED');
expect(saved.online).toBe(true);
expect(Number(saved.latitude)).toBe(10.125);
expect(Number(saved.longitude)).toBe(20.25);
expect(Number(saved.accuracyMeters)).toBe(7);
expect(Date.parse(saved.locationCapturedAt)).toBe(Date.parse(capturedAt));
expect(saved.locationReceivedAt).toBeTruthy();
await page.goto(`/app/mobileDevice/${target.deviceId}/`);
const overview = page.getByRole('tabpanel', { name: 'Overview', exact: true });
await expect(overview).toContainText('AVAILABLE');
await expect(overview).toContainText('10.125');
expect((await target.heartbeat({ locationStatus: 'PERMISSION_DENIED' })).status()).toBe(200);
await page.reload();
await expect(overview).toContainText('PERMISSION_DENIED');
saved = await deviceState(page.request, target.deviceId);
expect(Number(saved.latitude)).toBe(10.125);
expect(Date.parse(saved.locationCapturedAt)).toBe(Date.parse(capturedAt));
await expect.poll(async () => (await deviceState(page.request, target.deviceId)).online,
{ timeout: 25_000, intervals: [1000] }).toBe(false);
await expect(overview).toContainText('Connection: Offline');
expect((await deviceState(page.request, target.deviceId)).status).toBe('APPROVED');
const online = await page.request.post('/qqq/v1/table/mobileDevice/query', {
data: { filter: { criteria: [{ fieldName: 'online', operator: 'EQUALS', values: [true] }] } },
});
expect(online.status()).toBe(200);
expect((await online.json()).records.some((row) => row.values.id === target.deviceId)).toBe(false);
expect((await target.heartbeat()).status()).toBe(200);
expect((await deviceState(page.request, target.deviceId)).online).toBe(true);
await expect(overview).toContainText('Connection: Online');
} finally {
await target.request.dispose();
}
});
@@ -1,65 +0,0 @@
#Requires -Version 7.4
# Copyright (c) 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
<#
.SYNOPSIS
Remove one verified module scratch tree before a fresh isolated compilation.
.DESCRIPTION
Restricts deletion to the approved target leaves and refuses reparse points.
Deterministic cleanup prevents stale source or classes entering verification.
#>
function Remove-IsolatedBuildDirectory
{
[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal verification cleanup must either remove the validated scratch tree or fail; an interactive skip would permit stale sources and classes in the next verification.')]
param(
[Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$ModuleRoot,
[Parameter(Mandatory)][ValidateSet('scanner-source', 'module')][string]$LeafName
)
$root = (Resolve-Path -LiteralPath $ModuleRoot -ErrorAction Stop).Path
$target = [IO.Path]::GetFullPath((Join-Path $root 'target'))
$expected = [IO.Path]::GetFullPath((Join-Path $target $LeafName))
if([IO.Path]::GetDirectoryName($expected) -ne $target -or !$expected.StartsWith($root + '\', [StringComparison]::OrdinalIgnoreCase))
{
throw 'Refusing a scratch path outside the module target.'
}
foreach($parent in @($root, $target))
{
if((Test-Path -LiteralPath $parent) -and ((Get-Item -LiteralPath $parent -Force).Attributes -band [IO.FileAttributes]::ReparsePoint))
{
throw 'Refusing a reparse point in the scratch path.'
}
}
if(Test-Path -LiteralPath $expected)
{
$actual = (Resolve-Path -LiteralPath $expected -ErrorAction Stop).Path
if(![StringComparer]::OrdinalIgnoreCase.Equals($actual, $expected)) { throw 'Unexpected resolved scratch path.' }
$items = @((Get-Item -LiteralPath $actual -Force)) + @(Get-ChildItem -LiteralPath $actual -Recurse -Force)
if($items | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }) { throw 'Refusing a scratch tree containing a reparse point.' }
Remove-Item -LiteralPath $actual -Recurse -Force -ErrorAction Stop
}
}
<#
.SYNOPSIS
Snapshot the current scanner source into a freshly cleaned module scratch tree.
.DESCRIPTION
Copies only the wrapper, Maven configuration, source and style inputs needed
for isolated verification; sibling artifact caches remain untouched.
#>
function Initialize-IsolatedScannerSource
{
[CmdletBinding()]
param([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$ScannerRoot, [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$ModuleRoot)
Remove-IsolatedBuildDirectory -ModuleRoot $ModuleRoot -LeafName 'scanner-source'
$destination = Join-Path $ModuleRoot 'target/scanner-source'
New-Item -ItemType Directory -Path $destination -Force | Out-Null
foreach($name in @('pom.xml', 'mvnw.cmd', 'mvnw', '.mvn', 'src', 'checkstyle'))
{
Copy-Item -LiteralPath (Join-Path $ScannerRoot $name) -Destination $destination -Recurse -Force
}
}
-130
View File
@@ -1,130 +0,0 @@
# Optional Record Files integration
This standalone module connects the scanner's verified device/session authority to
Record Files' retained upload API. Neither base qbit depends on the other. A host
that installs both qbits may add this module and configure
`new QCodeReference(MobileCaptureIdentityProvider.class)` as the
`UploadConfiguration` capture identity provider. Install
`MobileScannersRouteProvider` on the same native QQQ server. Use the registered
Record Files API; this module creates no alternate upload endpoint.
Call `MobileRecordFilesContract.register(instance)` to opt into the independently
versioned `mobile-record-files` 1.0.0 widget vocabulary and schemas.
The provider reads only the server-verified request context or its retained
`VerifiedDeviceSession`. Each capture/revalidation checks current actor, private
authentication binding, public mobile session, durable termination and device
approval. A public identity DTO contains only provider key, actor, device and public
session IDs. It is never authorization proof. An authenticated, unbound desktop
session may return no device identity; an Android request or any already-bound
mobile session without verified device proof is refused.
## Semantic native widget
Host widget renderers may return `RecordFilesCaptureWidgetData` with an explicit,
server-selected parent record. The native extension vocabulary is:
```json
{
"type": "composite",
"blockType": "COMPOSITE",
"subBlocks": [{
"blockId": "recordFilesCapture",
"blockType": "RECORD_FILES_CAPTURE",
"values": {
"parent": {"tableName": "document", "recordId": "1"},
"title": "Record photos",
"instructions": "Capture or choose a photo."
}
}]
}
```
Use an ordinary QQQ frontend `WIDGET` component and `widgetName`; clients must not
infer parent IDs from arbitrary process values or field names. Parent IDs remain
opaque strings. `title` and `instructions` are optional display text. Clients ignore
additive unknown properties, but refuse missing/invalid parent references. Photo
controls require compatible `record-files` extension discovery and successful
record-specific capabilities with `canUpload=true`. Every upload operation still
independently authorizes the parent. Metadata visibility never grants permission.
Before showing capture actions, clients verify `mobile-record-files` discovery and
`GET /qqq/v1/qbit/mobile-record-files/contract`, sending no credentials. Its static
response is `{"version":"1.0.0","blockType":"RECORD_FILES_CAPTURE","widgetType":"composite"}`;
OpenAPI operation ID is `mobileRecordFilesContract`. The descriptor spec registers
typed parent, values, block and widget schemas. The current core registry rejects
extensions without endpoint specs and provides no schema-only hook, so this small
descriptor supplies versioned vocabulary without changing any upload endpoint or
the frozen mobile-scanners 1.0 contract. A future schema-only hook can improve this.
This widget is self-contained: successful upload neither advances the surrounding
process nor mutates its fields. “Saved” requires a matching `COMPLETE` receipt;
retained files normally remain `QUARANTINED`. They are not thereby released for
download. Camera/gallery uploads use the exact immutable manifest, binary chunk,
hash, replay and finalize contract in the frozen Record Files documentation.
`/version` advertises name, version and path prefix, without operation IDs.
OpenAPI identifiers are `getQbitRecordFilesCapabilities`,
`postQbitRecordFilesUploads`, `getQbitRecordFilesUploadsByUploadId`,
`putQbitRecordFilesUploadsByUploadIdItemsByItemIdChunksByChunkIndex`,
`postQbitRecordFilesUploadsByUploadIdFinalize` and
`postQbitRecordFilesUploadsByUploadIdCancel`. Native invoker/credential-scope IDs
instead use `METHOD /qbit/record-files/...`; capability gating must distinguish them.
## Reproducible isolated verification
From this directory, run `./Verify-Isolated.ps1`. It verifies every checksum in the
retained bundle, seeds a private repository from the local Maven cache, copies and
builds the scanner base outside its normal target, installs frozen candidates only
into that private repository, then runs this module's tests and writes the full
runtime classpath and SHA-256 manifest under `target/`. A later run can use
`-SkipSeed`; `-SkipBaseBuild` is only for iterating module-only changes.
Full runs recreate a validated scratch source tree and clean both builds. Module
output lives in `target/module`, separate from the sibling private repository and
wrapper. `Test-IsolatedBuildPaths.ps1` verifies deleted Java/resource/class removal,
repeat synchronization, sibling preservation and refusal of unsafe/reparse paths.
Module-only runs explicitly hash the prior scanner jar and do not overlay new source.
The accepted 0515 candidate defaults to `-RepositoryName isolated-m2-0515`
to preserve the earlier cache. See [the 0515 adoption record](docs/CORE-0515-ADOPTION.md)
for exact hashes, Jobs migration requirements and the consumer verification status.
The current verified bundle is `retained-contract-20261031-0515` from the documents
owner. All 33 root entries and seven source entries are verified. The script
uses the owner's exact resolved collaboration POM, preserving its original bytes.
It also installs the exact flattened parent POM and matched core/RDBMS pair.
Earlier 0238/0352 evidence and caches remain historical. This candidate
uses the frozen isolated core/RDBMS, RC.20 middleware, and H2 2.2.224. The fixture
found that H2 2.4.240 fails the shipped scanner `CHECK (status IN (...))` constraint
after the DDL connection closes (`ConditionInConstantSet` references its closed
session). A complete runtime manifest is necessary; matching only a core version
label is insufficient evidence of compatibility.
The HTTP fixture uses real QQQ table/password sessions, native enrollment and
record-screen approval processes, native Record Files capabilities/create/chunks/
finalize, disk-backed H2 metadata and the standard attachment owner with a
file-backed test storage adapter. It does not qualify a cloud object provider,
production malware scanning, production authentication or audit export. Its
upstream audit fixture is synthetic. Never deploy these test adapters/accounts.
After verification, `./Start-ConsumerFixture.ps1` prepares an owner-only Windows
directory and starts a timed, isolated fixture. Its returned object contains PID
and private credential-file path, never the password. Read the file locally for
the ephemeral base URL and synthetic account; enroll/approve through ordinary
native APIs. `mobileRecordPhotos` targets document/1. Stop only that PID or allow
its lifetime to expire. The launcher and native synthetic camera/gallery flow have
been exercised on TC22; `docs/SESSION-STATE.md` records exact verified scope.
Private ACLs apply only to a newly created run directory, preserving its parent's
ACL. The JVM is bounded to two visible processors and a 512 MiB heap. Startup waits
a bounded time for the private readiness file instead of returning immediately.
## Remaining release boundary
The adopted SPI executes operations through the scanner's `executeAuthorized`
guard, including expected absence for genuine desktop callers. Eighteen passing
integration/contract tests cover current identity, parent grants, actual retained
registration/association/receipt commits, concurrent revoke/end and rollback.
Current 0515 evidence is retained under `evidence/20261031-124632Z`; earlier 0352
guard evidence remains under `evidence/20261031-094704Z`. The guard is scoped to one
JVM; clustered operation requires a separately qualified transactional strategy.
Immutable published dependency adoption, optional-module hosted CI and production
storage/content-processing qualification remain open release gates. Test adapters
and passing synthetic intake do not qualify a production storage deployment.
@@ -1,70 +0,0 @@
#Requires -Version 7.4
# Copyright (c) 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
<#
.SYNOPSIS
Start the previously verified integration fixture in a private Windows folder.
.DESCRIPTION
Rechecks runtime manifests before launch and returns only private artifact
paths and process metadata; fixture credentials are never printed.
#>
[CmdletBinding()]
param(
[ValidateRange(1, 3600)][int]$LifetimeSeconds = 1800,
[ValidateRange(1, 60)][int]$StartupTimeoutSeconds = 30,
[string]$PrivateDirectory = (Join-Path $env:USERPROFILE '.cache/qqq-mobile-record-files-fixture')
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$module = $PSScriptRoot
$runtimeFile = Join-Path $module 'target/runtime-classpath.txt'
if(!(Test-Path -LiteralPath $runtimeFile)) { throw 'Run Verify-Isolated.ps1 successfully first.' }
$markerFile = Join-Path $module 'target/verification-success.json'
if(!(Test-Path -LiteralPath $markerFile)) { throw 'The last isolated verification did not complete successfully.' }
$marker = Get-Content -LiteralPath $markerFile -Raw | ConvertFrom-Json
foreach($entry in @(@('runtime-sha256.json', 'runtimeManifestSha256'), @('compiled-sha256.json', 'compiledManifestSha256')))
{
$manifestPath = Join-Path $module ('target/' + $entry[0])
if((Get-FileHash -LiteralPath $manifestPath).Hash -ne $marker.($entry[1])) { throw 'Verification manifest changed after the successful run.' }
foreach($artifact in (Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json))
{
if((Get-FileHash -LiteralPath $artifact.path).Hash -ne $artifact.sha256) { throw 'A runtime artifact changed after verification.' }
}
}
New-Item -ItemType Directory -Force -Path $PrivateDirectory | Out-Null
$privateRoot = (Resolve-Path -LiteralPath $PrivateDirectory).Path
if((Get-Item -LiteralPath $privateRoot -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Private fixture directory must not be a reparse point.' }
$run = [Guid]::NewGuid().ToString('N')
$runDirectory = Join-Path $privateRoot $run
New-Item -ItemType Directory -Path $runDirectory | Out-Null
$identity = [Security.Principal.WindowsIdentity]::GetCurrent().User
$acl = New-Object Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
$acl.SetOwner($identity)
$rule = New-Object Security.AccessControl.FileSystemAccessRule($identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
$acl.AddAccessRule($rule)
Set-Acl -LiteralPath $runDirectory -AclObject $acl
$credentials = Join-Path $runDirectory 'credentials.json'
$arguments = Join-Path $runDirectory 'java.args'
$classpath = (Join-Path $module 'target/module/test-classes') + ';' + (Join-Path $module 'target/module/classes') + ';' + (Get-Content -LiteralPath $runtimeFile -Raw).Trim()
if($classpath.IndexOfAny([char[]]"`r`n`"") -ge 0 -or $credentials.IndexOfAny([char[]]"`r`n`"") -ge 0) { throw 'Invalid Java argument path.' }
if(!(Test-Path -LiteralPath (Join-Path $module 'target/module/test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest.class'))) { throw 'Compile the current isolated fixture before starting it.' }
$lines = @('-XX:ActiveProcessorCount=2', '-Xmx512m', '-Dqqq.logger.logSessionId.disabled=true', '-cp', ('"' + $classpath.Replace('\', '/') + '"'),
'com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest', ('"' + $credentials.Replace('\', '/') + '"'), "$LifetimeSeconds")
$lines | Set-Content -LiteralPath $arguments -Encoding utf8NoBOM
$java = if($env:JAVA_HOME) { Join-Path $env:JAVA_HOME 'bin/java.exe' } else { (Get-Command java.exe).Source }
$process = Start-Process -FilePath $java -ArgumentList ('@"' + $arguments + '"') -WorkingDirectory $module -WindowStyle Hidden -PassThru `
-RedirectStandardOutput (Join-Path $runDirectory 'stdout.log') -RedirectStandardError (Join-Path $runDirectory 'stderr.log')
$deadline = [DateTime]::UtcNow.AddSeconds($StartupTimeoutSeconds)
while(!(Test-Path -LiteralPath $credentials))
{
if($process.HasExited) { throw "Fixture startup exited with code $($process.ExitCode); inspect the private log directory." }
if([DateTime]::UtcNow -ge $deadline)
{
if(!$process.HasExited) { $process.Kill() }
throw 'Fixture startup timed out; its process was stopped. Inspect the private log directory.'
}
Start-Sleep -Milliseconds 250
}
[pscustomobject]@{ ProcessId=$process.Id; CredentialsPath=$credentials; ExpiresAfterSeconds=$LifetimeSeconds; LogDirectory=$runDirectory }
@@ -1,45 +0,0 @@
#Requires -Version 7.4
# Copyright (c) 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'IsolatedBuildPaths.ps1')
$test = Join-Path $PSScriptRoot ('target/path-regression-' + [Guid]::NewGuid().ToString('N'))
$inputRoot = Join-Path $test 'input'
$moduleRoot = Join-Path $test 'module-root'
New-Item -ItemType Directory -Force -Path $inputRoot, $moduleRoot, (Join-Path $inputRoot '.mvn'), (Join-Path $inputRoot 'src'), (Join-Path $inputRoot 'checkstyle'), (Join-Path $moduleRoot 'target/isolated-m2') | Out-Null
foreach($name in @('pom.xml', 'mvnw.cmd', 'mvnw')) { Set-Content -LiteralPath (Join-Path $inputRoot $name) -Value 'fixture' }
$removedSource = Join-Path $inputRoot 'src/Removed.java'
Set-Content -LiteralPath $removedSource -Value 'old-source'
New-Item -ItemType Directory -Force -Path (Join-Path $inputRoot 'src/main/resources') | Out-Null
$removedResource = Join-Path $inputRoot 'src/main/resources/removed.txt'
Set-Content -LiteralPath $removedResource -Value 'old-resource'
$sentinel = Join-Path $moduleRoot 'target/isolated-m2/keep.txt'
Set-Content -LiteralPath $sentinel -Value 'repository-must-survive'
Initialize-IsolatedScannerSource -ScannerRoot $inputRoot -ModuleRoot $moduleRoot
$scratch = Join-Path $moduleRoot 'target/scanner-source'
New-Item -ItemType Directory -Force -Path (Join-Path $scratch 'target/classes') | Out-Null
Set-Content -LiteralPath (Join-Path $scratch 'target/classes/Removed.class') -Value 'stale-class'
Remove-Item -LiteralPath $removedSource
Remove-Item -LiteralPath $removedResource
Set-Content -LiteralPath (Join-Path $inputRoot 'src/Current.java') -Value 'current-source'
Initialize-IsolatedScannerSource -ScannerRoot $inputRoot -ModuleRoot $moduleRoot
if((Test-Path -LiteralPath (Join-Path $scratch 'src/Removed.java')) -or (Test-Path -LiteralPath (Join-Path $scratch 'target/classes/Removed.class'))) { throw 'Removed source/class survived the fresh snapshot.' }
if(Test-Path -LiteralPath (Join-Path $scratch 'src/main/resources/removed.txt')) { throw 'Removed resource survived the fresh snapshot.' }
Initialize-IsolatedScannerSource -ScannerRoot $inputRoot -ModuleRoot $moduleRoot
if(!(Test-Path -LiteralPath (Join-Path $scratch 'src/Current.java'))) { throw 'Current source was not copied.' }
if((Get-Content -LiteralPath $sentinel -Raw).Trim() -ne 'repository-must-survive') { throw 'Sibling repository changed.' }
New-Item -ItemType Directory -Force -Path (Join-Path $moduleRoot 'target/module') | Out-Null
Remove-IsolatedBuildDirectory -ModuleRoot $moduleRoot -LeafName 'module'
if(!(Test-Path -LiteralPath (Join-Path $scratch 'mvnw.cmd')) -or !(Test-Path -LiteralPath $sentinel)) { throw 'Module cleanup affected its sibling source or repository.' }
$denied = $false
try { Remove-IsolatedBuildDirectory -ModuleRoot $moduleRoot -LeafName '../outside' } catch { $denied = $true }
if(!$denied) { throw 'Non-approved scratch leaf was accepted.' }
$outside = Join-Path $test 'outside'
New-Item -ItemType Directory -Path $outside | Out-Null
New-Item -ItemType Junction -Path (Join-Path $moduleRoot 'target/module') -Target $outside | Out-Null
$denied = $false
try { Remove-IsolatedBuildDirectory -ModuleRoot $moduleRoot -LeafName 'module' } catch { $denied = $true }
if(!$denied -or !(Test-Path -LiteralPath $outside)) { throw 'Reparse tree was not refused.' }
'PASS: deleted Java/resource/class absent; repeat sync current; sibling cache/wrapper preserved; invalid leaf and reparse refused.'
@@ -1,148 +0,0 @@
#Requires -Version 7.4
# Copyright (c) 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
<#
.SYNOPSIS
Verify the optional integration against a retained contract in isolated storage.
.DESCRIPTION
Validates retained checksums, compiles fresh scanner sources when requested,
checks duplicate runtime classes and writes the successful verification marker.
#>
[CmdletBinding()]
param(
[string]$Bundle = 'R:\Git.Local\QRunIO\qbit-documents\artifacts\retained-contract-20261031-0515',
[ValidatePattern('^isolated-m2(?:-[a-zA-Z0-9-]+)?$')][string]$RepositoryName = 'isolated-m2-0515',
[switch]$SkipSeed,
[switch]$SkipBaseBuild
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'IsolatedBuildPaths.ps1')
$module = $PSScriptRoot
$scanner = (Resolve-Path (Join-Path $module '../..')).Path
$target = Join-Path $module 'target'
$repository = Join-Path $target $RepositoryName
$source = Join-Path $target 'scanner-source'
foreach($directory in @($module, $target, $repository))
{
if((Test-Path -LiteralPath $directory) -and ((Get-Item -LiteralPath $directory -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Refusing a reparse point in the isolated cache path.' }
}
New-Item -ItemType Directory -Force -Path $repository | Out-Null
$successMarker = Join-Path $target 'verification-success.json'
if(Test-Path -LiteralPath $successMarker) { Remove-Item -LiteralPath $successMarker }
$bundleRoot = (Resolve-Path -LiteralPath $Bundle).Path
$expected = Get-Content -LiteralPath (Join-Path $bundleRoot 'sha256.json') -Raw | ConvertFrom-Json
foreach($entry in $expected)
{
$path = (Resolve-Path -LiteralPath (Join-Path $bundleRoot $entry.path)).Path
if(!$path.StartsWith($bundleRoot + '\') -or (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -ne $entry.sha256)
{
throw "Retained contract checksum mismatch: $($entry.path)"
}
}
Copy-Item -LiteralPath (Join-Path $bundleRoot 'sha256.json') -Destination (Join-Path $target 'consumed-bundle-sha256.json')
$expectedSource = Get-Content -LiteralPath (Join-Path $bundleRoot 'source-sha256.json') -Raw | ConvertFrom-Json
foreach($entry in $expectedSource)
{
$path = (Resolve-Path -LiteralPath (Join-Path $bundleRoot ('source/' + $entry.path))).Path
if(!$path.StartsWith($bundleRoot + '\source\') -or (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -ne $entry.sha256)
{
throw "Retained source checksum mismatch: $($entry.path)"
}
}
Copy-Item -LiteralPath (Join-Path $bundleRoot 'source-sha256.json') -Destination (Join-Path $target 'consumed-source-sha256.json')
if(!$SkipSeed)
{
# Read-only seed: never install frozen candidates into the shared Maven cache.
& robocopy (Join-Path $env:USERPROFILE '.m2/repository') $repository /E /NFL /NDL /NJH /NJS /NP /R:1 /W:1 | Out-Null
if($LASTEXITCODE -ge 8) { throw 'Isolated Maven repository seeding failed.' }
}
if(!$SkipBaseBuild)
{
Initialize-IsolatedScannerSource -ScannerRoot $scanner -ModuleRoot $module
}
$wrapper = Join-Path $source 'mvnw.cmd'
if(!(Test-Path -LiteralPath $wrapper)) { throw 'A verified base snapshot is required; omit SkipBaseBuild.' }
<#
.SYNOPSIS
Run the checked-in Maven wrapper using only this verification's artifact cache.
.DESCRIPTION
Converts a failed native exit code into a terminating PowerShell failure so
subsequent steps cannot produce a successful marker from a failed build.
#>
function Invoke-Maven
{
[CmdletBinding()]
param([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]]$Arguments)
& $wrapper "-Dmaven.repo.local=$repository" @Arguments
if($LASTEXITCODE -ne 0) { throw "Maven failed with exit code $LASTEXITCODE" }
}
if(!$SkipBaseBuild)
{
Push-Location $source
try { Invoke-Maven @('-B', '-ntp', 'clean', 'verify', 'install') }
finally { Pop-Location }
}
$collaborationPom = Join-Path $target 'qbit-collaboration-resolved.pom.xml'
Copy-Item -LiteralPath (Join-Path $bundleRoot 'qbit-collaboration.resolved.pom.xml') -Destination $collaborationPom
$artifacts = @(
@('qbit-record-files-0.1.0-SNAPSHOT.jar', 'qbit-record-files.pom.xml'),
@('qbit-collaboration-0.1.0-record-files-SNAPSHOT.jar', 'qbit-collaboration.pom.xml'),
@('qqq-backend-core-4.1.0-isolated-record-files-SNAPSHOT.jar', 'qqq-backend-core.pom.xml'),
@('qqq-backend-module-rdbms-4.1.0-isolated-record-files-SNAPSHOT.jar', 'qqq-backend-module-rdbms.pom.xml')
)
Push-Location $module
try
{
$parentPom = Join-Path $bundleRoot 'qqq-parent-project-4.1.0-isolated-record-files-SNAPSHOT.pom'
if(Test-Path -LiteralPath $parentPom)
{
# Use the base POM while bootstrapping the candidate's exact flattened parent.
Invoke-Maven @('-f', (Join-Path $source 'pom.xml'), '-B', '-ntp', 'org.apache.maven.plugins:maven-install-plugin:3.1.3:install-file', "-Dfile=$parentPom", '-DgroupId=com.kingsrook.qqq', '-DartifactId=qqq-parent-project', '-Dversion=4.1.0-isolated-record-files-SNAPSHOT', '-Dpackaging=pom', '-DgeneratePom=false')
}
foreach($artifact in $artifacts)
{
$pom = if($artifact[1] -eq 'qbit-collaboration.pom.xml') { $collaborationPom } else { Join-Path $bundleRoot $artifact[1] }
Invoke-Maven @('-B', '-ntp', 'org.apache.maven.plugins:maven-install-plugin:3.1.3:install-file', "-Dfile=$(Join-Path $bundleRoot $artifact[0])", "-DpomFile=$pom")
}
Invoke-Maven @('-B', '-ntp', 'org.apache.maven.plugins:maven-install-plugin:3.1.3:install-file', "-Dfile=$(Join-Path $bundleRoot 'qbit-record-files-0.1.0-SNAPSHOT-tests.jar')", "-DpomFile=$(Join-Path $bundleRoot 'qbit-record-files.pom.xml')", '-DgroupId=com.kingsrook.qbits', '-DartifactId=qbit-record-files', '-Dversion=0.1.0-SNAPSHOT', '-Dclassifier=tests', '-Dpackaging=jar', '-DgeneratePom=false')
Remove-IsolatedBuildDirectory -ModuleRoot $module -LeafName 'module'
$runtimeClasspath = Join-Path $target 'runtime-classpath.txt'
if(Test-Path -LiteralPath $runtimeClasspath) { Remove-Item -LiteralPath $runtimeClasspath }
Invoke-Maven @('-B', '-ntp', 'clean', 'verify', 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath', '-Dmdep.includeScope=test', '-Dmdep.outputFile=target/runtime-classpath.txt')
$runtime = (Get-Content -LiteralPath (Join-Path $target 'runtime-classpath.txt') -Raw).Trim().Split(';')
$manifest = foreach($path in $runtime) { @{path=$path; sha256=(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() } }
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $target 'runtime-sha256.json') -Encoding utf8
$cores = @($runtime | Where-Object { [IO.Path]::GetFileName($_) -match '^qqq-backend-core-' })
if($cores.Count -ne 1 -or [IO.Path]::GetFileName($cores[0]) -ne 'qqq-backend-core-4.1.0-isolated-record-files-SNAPSHOT.jar') { throw 'Unexpected or duplicate core runtime.' }
foreach($entry in $expected | Where-Object { $_.path.EndsWith('.jar') })
{
$resolved = @($manifest | Where-Object { [IO.Path]::GetFileName($_.path) -eq $entry.path })
if($resolved.Count -ne 1 -or $resolved[0].sha256 -ne $entry.sha256) { throw "Runtime differs from retained binary: $($entry.path)" }
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$classes = @{}
foreach($path in $runtime)
{
$archive = [IO.Compression.ZipFile]::OpenRead($path)
try
{
foreach($entry in $archive.Entries | Where-Object { $_.FullName.StartsWith('com/kingsrook/') -and $_.FullName.EndsWith('.class') })
{
if($classes.ContainsKey($entry.FullName)) { throw "Duplicate QQQ/QBit runtime class: $($entry.FullName)" }
$classes[$entry.FullName] = $path
}
}
finally { $archive.Dispose() }
}
$compiled = foreach($file in Get-ChildItem -LiteralPath (Join-Path $target 'module/classes'), (Join-Path $target 'module/test-classes') -File -Recurse)
{
@{path=$file.FullName; sha256=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() }
}
$compiled | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $target 'compiled-sha256.json') -Encoding utf8
$scannerArtifact = $manifest | Where-Object { [IO.Path]::GetFileName($_.path) -eq 'qbit-mobile-scanners-0.1.0-SNAPSHOT.jar' }
@{bundleName=(Split-Path -Leaf $bundleRoot); repositoryName=$RepositoryName; duplicateQqqClasses=0; classCount=$classes.Count; h2Version='2.2.224'; baseBuildMode=$(if($SkipBaseBuild) { 'module-only-prior-scanner-binary' } else { 'fresh-source-clean-base-build' }); scannerJarSha256=$scannerArtifact.sha256; derivedCollaborationPomSha256=(Get-FileHash -LiteralPath $collaborationPom -Algorithm SHA256).Hash.ToLowerInvariant() } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $target 'runtime-validation.json') -Encoding utf8
@{verifiedAtUtc=[DateTime]::UtcNow.ToString('O'); runtimeManifestSha256=(Get-FileHash -LiteralPath (Join-Path $target 'runtime-sha256.json')).Hash; compiledManifestSha256=(Get-FileHash -LiteralPath (Join-Path $target 'compiled-sha256.json')).Hash } | ConvertTo-Json | Set-Content -LiteralPath $successMarker -Encoding utf8
}
finally { Pop-Location }
@@ -1,164 +0,0 @@
# Atomic capture authorization contract requested from Record Files
Status: fulfilled for the adopted `retained-contract-20261031-0352` bundle. Its
guarded adapter passed 18 consumer tests, including real scanner revocation and
session termination during registration, association and receipt commits and
rollbacks. Evidence: `../evidence/20261031-094704Z/`. The newer 0515 candidate keeps
the same Record Files bytes and guard contract while updating core/RDBMS; its
separate qualification is tracked in `CORE-0515-ADOPTION.md`.
The following preserves the original request, rationale and exact linearization
requirements. Historical references to 0238 describe the previously identified
gap, not the currently accepted guard implementation.
## Minimal SPI
Keep existing methods and add this synchronous, defaulted Java SPI:
```java
default <T> T executeAuthorized(Optional<CaptureIdentity> expected,
Operation<T> operation) throws QException
{
if(expected == null || expected.isPresent() || capture().isPresent())
{
throw new QException("Capture identity requires an authorization operation guard.");
}
return operation.run();
}
@FunctionalInterface
interface Operation<T>
{
T run() throws QException;
}
```
This default preserves a genuinely desktop-only provider and fails closed for
every legacy device provider. It must never silently implement device execution
as `revalidate(); operation.run()`.
The provider override acquires the same authorization exclusion used by approval
changes, session termination and first device/session binding. Under that guard
it re-derives current authenticated actor, private session binding, current device
approval and public mobile session, compares them with the expected identity, and
then runs the callback. The guard remains held through all synchronous writes,
audit outcomes and actual transaction commits, and releases in `finally` on every
path. No deferred future/job/stream may escape while relying on released authority.
Missing authority, interruption and lookup failure must reject the operation.
`Optional.empty()` is an expected absence, not a bypass. Our mixed mobile/desktop
adapter must hold the scanner guard even for empty identity and verify that the
current authenticated request remains genuinely unbound desktop. Otherwise a
concurrent first device binding could race an upload begun as desktop.
## Proposed scanner adapter patch, after new frozen SPI adoption
Add a public overload to the scanner facade (no Record Files dependency):
```java
public static <T> T executeAuthorized(Optional<Identity> expected,
Operation<T> operation) throws QException
{
return MobileSessionService.withActiveSession(() ->
{
if(expected == null || !expected.equals(captureCurrent()))
{
throw new QPermissionDeniedException("Device attribution no longer matches this sign-in.");
}
return operation.run();
});
}
```
Preserve the existing `Identity` overload's rejection of null; do not turn null
into desktop absence. Then override the new SPI in the optional adapter:
```java
@Override
public <T> T executeAuthorized(Optional<CaptureIdentity> expected,
CaptureIdentityProvider.Operation<T> operation) throws QException
{
if(expected == null || expected.filter(value -> !PROVIDER_KEY.equals(value.providerKey())).isPresent())
{
throw new QPermissionDeniedException("Unsupported mobile capture identity.");
}
Optional<MobileDeviceAuthorization.Identity> identity = expected.map(value ->
new MobileDeviceAuthorization.Identity(value.actorId(), value.deviceId(), value.publicSessionId()));
return MobileDeviceAuthorization.executeAuthorized(identity, operation::run);
}
```
The current scanner exclusion is one reentrant monitor per QInstance in one JVM.
Its approval and termination operations use that same monitor. This qualifies
only a single-instance host; multi-node production requires shared exclusion or
transactional fencing used by every mutator. A row lock taken after a separate
snapshot read is not, by itself, that strategy.
## Required owner-side wrap points
Acquire capture authority before Record Files/attachment transactions or row
locks. Re-read persisted upload identity and parent permissions inside the guard.
Reject an ambient host transaction whose commit could occur after callback return.
`RecordFilesStore.atomic` must return only after its owned commit completes.
| Operation | Work protected through completion |
| --- | --- |
| Create | Identity comparison, parent reauthorization, quota initialization/reservation, upload row and creation audit commit. |
| Status | Persisted identity check and any expiry state/audit mutation; GET may write. |
| Chunk | After bounded input reading, reauthorize before staged object write, chunk row and outcome commit. |
| Cancel | Terminal-state change and mandatory audit commit. |
| Finalizer claim/renew | Collection creation, lease acquisition/renewal and state/audit commits. |
| Canonical intake | Entire standard attachment-owner `files.intake`, including object effects and owned persistence commits. Guarding only checkpoint append is too late. |
| Append checkpoint | Collection association and checkpoint commit. |
| Finish/fail | Final receipt/state/audit commit or guarded recovery state. |
| Owner cleanup | User-driven expiry, staging cleanup cursor and quota changes. |
Trusted `cleanupBatch` is a separate explicitly privileged maintenance operation.
It may clean abandoned staging after the original actor/device is revoked, but
must not read/release new content, resurrect intake or delete retained originals.
The new working source has these wrap points, guarded cleanupOwn and guarded
canonical intake. It rejects ambient transactions via `withAuthorized`; create's
early `actor()` also rejects ambient QContext transactions before guard acquisition.
## Linearization and regression evidence
If revoke/end wins the shared guard first, the later operation must produce no
new retained object/association/checkpoint/complete receipt. If intake wins first,
its commit is allowed and revoke/end waits; once revocation commits, subsequent
items/checkpoints/receipts must be refused. Earlier authorized originals can remain
retained/quarantined in a partial upload. This does not promise upload-wide rollback
or delete previously committed originals.
Required deterministic regressions, using latches rather than timing assumptions:
1. Revoke and end between preflight and guard acquisition: callback/storage/commit
do not execute, including a fully staged upload ready to finalize.
2. Pause inside canonical attachment-owner intake/commit and attempt revoke/end
on another thread: termination waits, the first commit finishes while guarded,
then later mutation is denied. Check persistence independently after commit.
3. Repeat at collection association and COMPLETE receipt commit; no guard may end
after event insertion but before SQL commit. Cover failure/rollback release.
4. Fail-closed legacy provider creates no upload or quota row. Null/foreign provider,
actor, device, session, core-session mismatch and unavailable authority reject.
5. Empty desktop identity succeeds only while unbound; concurrent first binding
uses the same exclusion and cannot cross the desktop operation.
6. New login cannot resume old captured identity; asynchronous retries require the
retained trusted context plus current binding/approval/tombstone validation.
7. Parent tenant visibility, configured record type, attach feature, permission and
credential/channel ceilings remain independently enforced before finalization.
Working-source tests currently demonstrate preflight-to-chunk-guard revocation,
guard depth during owner intake/audit, legacy-provider rejection and a concurrent
cancellation commit. Our combined runtime still needs the actual canonical intake
race with real scanner approval/session-end operations after bundle adoption.
## Existing protection before this change
The frozen code already enforces immutable request/manifest identity, parent/tenant
authorization, configured record types, chunk bounds/hashes, same actor/device/
public-session identity on resume, current approval/tombstones, owner transactions,
lease fencing, durable receipts, quarantine and idempotent retention. Our HTTP
fixture proves sequential revocation/end/new-login rejection and persisted
association/bytes. These checks narrow the race but cannot close the interval
between successful authorization and a later owner commit without this guard.
@@ -1,93 +0,0 @@
# Explicit candidate adoption: retained-contract-20261031-0515
Status: explicitly accepted as the isolated consumer default after successful
verification at 2026-10-31 12:46:32 UTC and root review. The default cache is the
separate `isolated-m2-0515`; the 0352 cache and evidence are preserved. No shared
Maven repository, existing operator runtime, or database was changed.
All 33 entries in the candidate's root manifest and all seven guarded-source
entries match SHA-256. Record Files main/tests and the canonical attachment owner
are byte-identical to 0352. The changed matched pair is:
| Artifact | SHA-256 |
| --- | --- |
| Core | `c7e9da29afe886ada550d0590b10491b8f3b1a621a1b3b79ca3fb5f807dcf894` |
| RDBMS | `562ec0f2653dd156d06b7b7df7268c40ac87a2c8a79c4f2f7811666507338728` |
| Flattened parent POM | `0158658f28d868b2ac5b323c7ede46b7075d8e5f33a2da9a49c34402abe24afe` |
The owner reports 177 Record Files tests including 11 MySQL tests against this
pair. That upstream evidence does not substitute for this consumer's tests.
The frozen core/RDBMS version strings did not change; hashes distinguish adoption.
## Session logging correction
The isolated core source's `TableBasedAuthenticationModule` now logs fixed
revalidation text rather than concatenating `session.getIdReference()`.
`testRevalidationDoesNotExposeSessionIdentifier` attaches a real DEBUG appender,
requires the revalidation branch to log, and rejects the synthetic session ID in
the message. Inspection of the exact frozen core with `javap -c -p` confirms the
fixed string is passed directly to `QLogger.debug(String)`, with no interpolation
on that call. This verifies the specific reported leak; it is not a complete
logging audit of upstream dependencies.
## Jobs migration boundary
The candidate also includes durable Jobs metadata changes. Before upgrading any
existing Jobs host, add the nullable columns `jobs_job.enlistment_key VARCHAR(64)`,
`intent_fingerprint VARCHAR(64)`, and `process_revision VARCHAR(128)`, plus a unique
constraint on `enlistment_key`. This is required even when strict transactional
admission stays disabled because ordinary metadata queries select the columns.
The supplied H2/MySQL scripts additionally create `jobs_admission(id)` and seed
row 1 for strict admission. They assume the complete baseline Jobs schema exists;
they are additive migrations, not idempotent bootstrap scripts. MySQL columns use
`utf8mb4_0900_bin` and the guard table uses InnoDB. Strict operation additionally
requires the owner's provider, timestamp, durability, isolation, bounded-lock,
namespace/signing and all-writer rollout qualifications. PostgreSQL requires an
equivalent additive migration but remains unqualified for strict mode.
This scanner consumer fixture does not install Durable Jobs or `jobs_job`.
It creates fresh file-H2 metadata for its configured qbits and does not have an
existing Jobs schema to migrate. No migration should be applied to the operator
database during this compatibility test. Deployments that add Jobs must perform
the owner migrations before adopting the new pair.
## Reproduction
The completed bounded run used these explicit arguments (now also the defaults):
```powershell
$env:JAVA_TOOL_OPTIONS = '-XX:ActiveProcessorCount=2 -Xmx768m'
./Verify-Isolated.ps1 `
-Bundle R:/Git.Local/QRunIO/qbit-documents/artifacts/retained-contract-20261031-0515 `
-RepositoryName isolated-m2-0515
```
This creates a separate cache under ignored `target/`, builds the current scanner
base cleanly, installs the exact flattened parent and matched dependency pair,
then runs the optional module's 18 HTTP/identity/commit/contract cases. The existing
0352 cache is retained. The classpath is regenerated so Maven cannot reuse paths
from a different cache. A complete runtime manifest and duplicate-class checks
remain required. Later reruns may use `-SkipSeed`; module-only reruns must identify
the exact prior scanner binary and do not qualify unbuilt base changes.
## Consumer verification result
Fresh scanner clean verification/install passed 37 tests. Optional clean verify
passed 18 tests (16 HTTP/identity/concurrency cases and two contract cases), zero
failures/errors/skips, with Checkstyle. Runtime verification hashed all 214 jars,
confirmed the exact matched pair and owner/RF bytes, and found zero duplicate QQQ
classes across 3,180 classes. Explicit H2 remains 2.2.224.
The tested scanner jar is
`e9504632ec308cbad080afcce7da0fb7037f7d9b153359d1653729cb8e33af17`.
Its freshly copied source contains the normalized trailing-slash heartbeat path
and approval check. Evidence under `../evidence/20261031-124632Z/` retains base and
module source hashes, passing test summaries, complete actual runtime and compiled
manifests, bundle/source manifests, safe receipt/widget and success-marker hashes.
The two default argument values were changed only after the passing explicit run
was reviewed; no Java or dependency bytes changed after verification.
This compatibility run qualifies only the isolated consumer. Published
immutable coordinates, hosted optional-module CI, production providers, migration
rollout, and deployment approval remain separate gates.
@@ -1,99 +0,0 @@
# API 1.9 Record Files consumer qualification
Status: isolated consumer qualification passed on November 1, 2026. This is not
adoption of a new optional default, publication of immutable files-owner
dependencies, or deployment to the normal scanner runtime.
## Qualified cohort
| Input | Identity |
| --- | --- |
| Core/BOM/filesystem source | `32df5d76c8728145d4b7b417a7f56b352893d1f2` |
| Scanner source | `1a3e33ba280edf93bcb1b7b66dfd45660a9a1a68` |
| All QQQ backend modules | `4.1.0-ci.32df5d76c8728145d4b7b417a7f56b352893d1f2` |
| Independent frontend pin | Existing `qqq-frontend-next:1.0.0-RC.14`, unchanged from the tested BOM |
| Record Files | Frozen API-registration 0801 main/test JARs and POM |
| Attachment owner | Frozen collaboration 218 main JAR and resolved POM |
| Dependency pins | Guava 33.7.2-jre, H2 2.2.224, Jimfs 1.3.2 |
The optional consumer copy imports the coherent QQQ BOM with no separate
filesystem override. Its JUnit engine and params are versionless test
dependencies; API/engine/params resolve consistently to 6.0.2. The historical
optional POM/default bundle remains unchanged in the repository.
## Fresh consumer results
All **78** cases passed with zero failures, errors or skips:
| Gate | Passed |
| --- | ---: |
| Scanner unit/integration tests | 39 |
| Demo HTTP tests | 6 |
| Optional Record Files integration/contract tests | 18 |
| Real browser process tests | 15 |
The browser suite covers single-record actions, selected bulk records,
filter-based selection and first-N selection, confirmation/cancellation,
persisted results and unaffected unselected records. Public discovery returned
core API **1.9.0**, mobile extension **1.2.0**, TABLE authentication and no
`nativeSessionLogin` advertisement for that authentication provider.
The Record Files suite comprises 16 integration executions and two typed-widget
contract cases. It covers actual login/enrollment/approval, persistent file and
parent association, identity substitution, ended/revoked sessions, parent and
attachment authorization, and 12 operation-guard concurrency cases. Runtime
provenance retained **214 JAR hashes**, **3,163 unique QQQ/QBit classes** and
**zero duplicate classes**. Frozen Record Files/owner main, test and POM bytes
remained unchanged.
## Core verification basis
The preceding coherent core source `a1366c154712c85140cdad89efdc3dc3c495f342`
passed its full configured reactor gates: 4,649 discovered tests, 4,629 passed,
20 existing skips, zero failures/errors. A later published sample test exposed
an unchecked `InvalidPathException` for a NUL-containing storage reference.
The narrow correction maps only untrusted-reference parsing to a fixed-message
`QBadRequestException`, without retaining the supplied path or exception cause.
The correction passed the complete filesystem gate again: 142 discovered,
138 passed, four existing skips, zero failures/errors. Actual `StorageAction`
probes reproduced the old error and verified the corrected error for read,
write, download URL and delete. Independent review closed the reported defect.
The final signed core32 artifacts were freshly built in a separate private
repository. For that artifact build, tests, coverage and static reanalysis were
explicitly not repeated. Exhaustive comparison of **2,772 compiled classes**
across every installed main/test JAR found exactly one changed class,
`FilesystemStorageAction`, byte-identical to the fully tested corrected JAR.
Resources, version metadata and POM differences were retained separately. This
is a differential verification claim, not a fresh 4,649-test pass on core32.
Hosted final-source acceptance is tracked separately.
## Key artifact SHA-256 values
| Artifact | SHA-256 |
| --- | --- |
| Scanner | `dfc66567276ddb837d1388560c7aaf3dae53ab0287b943742024a0837aeec38c` |
| Core | `08907e809a6e2cdcd64497e482c6f890ad05b811bdc125d354c9584218e4af04` |
| RDBMS | `a029e797a0bbcd5fc410eba903c085d9d856f9b39d45aea954da20061f658b31` |
| Filesystem | `25851e33ad25044bc8a3a14753c87253101f449fc5fe63e0ef68da86d418a9bb` |
| Record Files main | `ca20530b133e0423fbb56b9e34afe316adba73cde8c9d1321a983a5de0ca769b` |
| Record Files tests | `1b75563128086b55845aaefd6ca7db6ee5f975f3a82e58f9806049bd74ce671b` |
| Attachment owner | `87c6a9aac52a0821edd66dad2924a014d2c70aa929ead011dcf7a89a0f94c854` |
These identify retained local qualification artifacts. Historical SNAPSHOT
coordinates alone do not identify immutable release bytes.
## Remaining adoption boundary
The files owner must still supply its complete matching source/artifact handoff
and qualify its full Record Files/attachment-owner suites and migrations against
the same core32 source/BOM revision. The 18 scanner consumer cases do not replace
those owner gates. Immutable accessible publication inputs are required before
optional hosted CI/default adoption can claim reproducibility.
Actual TC22 camera/gallery, permissions and durable authorized association on
this final cohort remain separate acceptance work. A disposable fixture may use
the qualified cohort for that work; it does not upgrade the normal database,
normal APK, existing device enrollment, or USB forwarding. Prior frozen bundles
and runtime evidence remain retained.
@@ -1,99 +0,0 @@
# Hosted CI dependency handoff
Read-only publication investigation, 2026-10-31. No build, publication, workflow
activation, repository creation or upstream edit was performed.
The optional module passes locally against immutable **bundle bytes**, but its
Maven coordinates are mutable development snapshots. A clean hosted runner cannot
reconstruct this verified runtime from published dependencies. The module's hosted
gate must remain unclaimed until the owner supplies supported, immutable releases.
## Missing published inputs
| Current verified input | Required owner handoff |
| --- | --- |
| `com.kingsrook.qqq:qqq-parent-project:4.1.0-isolated-record-files-SNAPSHOT` | Immutable flattened parent POM with every referenced parent resolvable. |
| `com.kingsrook.qqq:qqq-backend-core:4.1.0-isolated-record-files-SNAPSHOT` | Immutable core release containing the qualified transaction/Jobs contracts and session-log correction. |
| `com.kingsrook.qqq:qqq-backend-module-rdbms:4.1.0-isolated-record-files-SNAPSHOT` | Matching immutable RDBMS release with an explicitly supported core pair. |
| `com.kingsrook.qbits:qbit-collaboration:0.1.0-record-files-SNAPSHOT` | Immutable standard attachment-owner release containing the retained lifecycle, transaction/journal and storage contracts. Published POM must resolve its version without local substitution. |
| `com.kingsrook.qbits:qbit-record-files:0.1.0-SNAPSHOT` | Immutable main jar and resolvable POM containing guarded ingestion, native routes, typed contracts and parent authorization. |
| Same Record Files GAV, `tests` classifier, `test-jar` type | Published integration-fixture jar containing `RecordFilesFixture` and its required resources. It is an actual compile/runtime dependency of the 18 consumer tests. |
The owner chooses release versions; this document does not reserve or invent them.
The complete supported tuple must identify the middleware/BOM version as well.
The currently qualified mixed runtime uses middleware4.1.0-RC.20 and H2 2.2.224;
new release coordinates require a fresh consumer run and complete runtime manifest.
Changing artifact version/provenance resources changes hashes, so require a new
owner manifest plus explicit source mapping rather than demanding old snapshot
jar hashes from newly versioned releases.
## What is actually published or staged
Direct Maven Central metadata requests returned:
- [Record Files metadata](https://repo.maven.apache.org/maven2/com/kingsrook/qbits/qbit-record-files/maven-metadata.xml): HTTP404.
- [Collaboration metadata](https://repo.maven.apache.org/maven2/com/kingsrook/qbits/qbit-collaboration/maven-metadata.xml): latest/release `0.1.0-RC.6`.
- [Core metadata](https://repo.maven.apache.org/maven2/com/kingsrook/qqq/qqq-backend-core/maven-metadata.xml), [RDBMS metadata](https://repo.maven.apache.org/maven2/com/kingsrook/qqq/qqq-backend-module-rdbms/maven-metadata.xml), [parent metadata](https://repo.maven.apache.org/maven2/com/kingsrook/qqq/qqq-parent-project/maven-metadata.xml): latest/release `4.1.0-RC.20`.
These public releases are not qualified substitutes for the frozen0515 changes.
The GitHub releases also show immutable [Collaboration RC.6](https://github.com/QRun-IO/qbit-collaboration/releases/tag/v0.1.0-RC.6)
and [QQQ RC.20](https://github.com/QRun-IO/qqq/releases/tag/v4.1.0-RC.20).
The local owner worktrees are `qbit-collaboration-wt-record-files`, branch
`feat/record-file-retention`, and `qqq-wt-isolated-scripting`, branch
`feat/isolated-scripting-boundaries`. Both contain uncommitted required changes.
GitHub PR queries for those exact branch names returned no results. Existing
[Collaboration #45](https://github.com/QRun-IO/qbit-collaboration/pull/45) and
[#51](https://github.com/QRun-IO/qbit-collaboration/pull/51) concern attachment
transactions/integration on other branches; neither was established as the source
of the qualified0515 owner artifact. Do not conflate them with publication approval.
The local `qbit-record-files` directory has no Git metadata or CI configuration.
Its POM names `https://github.com/QRun-IO/qbit-record-files`; the connected GitHub
lookup returned404, meaning absent or not visible to that connection. The owner
must confirm its canonical repository/access and source publication path.
Collaboration already has the standard CircleCI release/snapshot publication
workflow and reusable RC preparation workflow. That machinery alone does not
publish uncommitted work or prove that any future release includes the candidate.
## Minimal owner response
1. Canonical repository URLs, reviewed PRs, immutable source commit/tag for each
changed owner/core component, and mapping from frozen0515 to those sources.
2. New immutable GAVs and all jar/POM/classifier download URLs; repository URL and
a documented CI read-access requirement if not public. Never send credentials.
3. Complete SHA-256 manifest, published signatures/provenance when available,
flattened/resolved POMs, and confirmation that the parent/BOM and transitive
dependency closure resolve on a clean runner without sibling checkouts or
`install-file` repairs. Include the Record Files `tests` classifier.
4. Owner/core build and compatibility evidence, preserved Jobs additive migration
requirements, supported middleware/H2/provider tuple, and any delta from0515.
5. Confirmation of release status and remaining deployment gates. A candidate
publication must not be represented as production1.0 approval.
## Minimal scanner CI once inputs exist
The existing `demo-acceptance.yml` uses Ubuntu24.04, Java21 and read-only contents
permissions, installs the scanner from the checked-out commit, then runs demo and
browser acceptance. It never builds this standalone optional module today.
Add one independent, required optional-integration job using the same pinned
checkout/Java actions and a fresh Maven repository. First build/install the
scanner from that exact checkout, then run ordinary Maven `clean verify` on
`integrations/record-files/pom.xml` with the published pins. The locally built
scanner snapshot is the checked-out component under test, not an unpublished
third-party input. A scanner release still needs its own immutable version.
The Java fixture is loopback HTTP plus fresh file-H2 and file-backed test storage;
the 18 cases need neither a TC22 nor Android tooling. Keep the Java resource bound,
retain safe test summaries and full dependency/class hashes, and fail on a missing
fixture classifier, changed expected artifact hash, duplicate QQQ class or test
failure. Do not seed from a developer's Maven cache or upload private fixture logs.
`Verify-Isolated.ps1` is a Windows local bundle-adoption harness (`mvnw.cmd`,
semicolon classpaths and local frozen files); it is not a portable hosted runner.
Use normal Maven for published resolution, with a small portable manifest check
when implementing that job. No workflow with fabricated pins, skipped required
tests or workstation-only paths has been added. Optional-module publication stays
disabled until the dependency gate is actually satisfied.
@@ -1,66 +0,0 @@
# Record Files bridge — 2026-10-31 12:46 UTC
Frozen `retained-contract-20261031-0515` is explicitly accepted as this isolated
consumer's default after root review. `Verify-Isolated.ps1` defaults to that bundle
and the separate `target/isolated-m2-0515` cache. The prior0352 cache and all earlier
evidence remain intact. No shared Maven dependency, operator runtime, TC22 state,
or production database was changed by this qualification. No commit was made.
Fresh scanner clean verify/install passed 37 tests. Optional clean verify passed
18 tests (16 HTTP/identity/concurrency and two typed-contract), zero failures,
errors or skips, with Checkstyle. The bounded JVM used two visible processors and
768 MiB. Exec68943 completed successfully; no bridge build or fixture is active.
All 33 bundle entries and seven guarded-source entries match their hashes. The
complete actual runtime contains 214 jars and 3,180 QQQ/qbit classes, zero duplicate
classes. Explicit H2 is 2.2.224. Evidence under `evidence/20261031-124632Z/` includes
full runtime/compiled hashes, exact bundle/source manifests, base/module source
hashes, test summaries, a safe receipt, actual semantic widget and success marker.
The tested scanner jar SHA-256 is
`e9504632ec308cbad080afcce7da0fb7037f7d9b153359d1653729cb8e33af17`.
The fresh source copy includes normalized trailing-slash heartbeat authorization.
Core is `c7e9da29afe886ada550d0590b10491b8f3b1a621a1b3b79ca3fb5f807dcf894`;
RDBMS is `562ec0f2653dd156d06b7b7df7268c40ac87a2c8a79c4f2f7811666507338728`.
Record Files and attachment-owner binaries are unchanged from0352. Core's exact
bytecode confirms fixed revalidation log text without session-ID interpolation.
The optional bridge retains independent base qbits. It accepts identity only from
verified request/asynchronous server context, revalidates private bindings and
current approval/tombstones, and holds the real scanner monitor through owner
registration, association and receipt commits. Twelve deterministic concurrent
revoke/end cases cover all three commit points and injected rollbacks. Separate
HTTP checks cover password login, enrollment, record-screen approval, uploads,
persisted originals/associations, receipt replay, tenant and parent denial,
permission/feature removal, missing/forged identity and new-session refusal.
This proves single-instance exclusion; it does not qualify clustered deployment.
The typed mobile-record-files1.0 descriptor and actual process WIDGET are verified
through native HTTP. The existing registry requires an endpoint spec, so a small
public static descriptor carries version/block/widget vocabulary without creating
another upload protocol. Native clients verify it before record capabilities.
The private launcher and synthetic camera/gallery, orientation and session-ending
flows were accepted on TC22 against0352. The new0515 qualification is the server
consumer suite; no repeated physical-device claim is made. Launcher credentials
remain synthetic and private, and test storage/audit are not production adapters.
Jobs migration requirements and exact candidate provenance are in
`docs/CORE-0515-ADOPTION.md`. This fixture does not register Jobs. Existing Jobs
hosts must add the three nullable metadata columns and unique enlistment key
before adopting the new core, even with strict admission disabled. No operator
migration or strict Jobs activation was performed.
Remaining release gates: immutable published dependency coordinates, optional
hosted CI, production storage/content scanning/release and durable audit, host
migration/provider rollout, cross-node strategy if required, and final release
review. Passing isolated qualification does not authorize deployment.
Publication follow-up inspected live Central/GitHub metadata and owner worktrees.
The candidate tuple remains unpublished: Record Files metadata is404, public
Collaboration is RC.6 and public core/RDBMS/parent are RC.20. The exact owner source
branches have no matching PRs and contain uncommitted changes. Details and the
minimal required handoff are in `docs/HOSTED-CI-DEPENDENCIES.md`. A plain DATA request
was prepared at `C:/Users/james/.cache/qqq-mobile-rf-publication-request-20261031.txt`
for root to append to its mailbox. No comms were sent, workflow enabled, artifact
uploaded, build run or commit made during this follow-up.
-30
View File
@@ -1,30 +0,0 @@
# Release gates
- [x] Optional module preserves independent base qbits.
- [x] Verify frozen artifacts in a private Maven repository.
- [x] Validate actual device/session binding and approval/tombstones.
- [x] Test native login, enrollment, record approval and file ingestion.
- [x] Verify persisted association, original bytes and safe attribution.
- [x] Retain full runtime hashes and reject duplicate QQQ/qbit classes.
- [x] Reject tenant-invisible and unconfigured parents, and changed parent grants.
- [x] Publish a typed optional widget contract and verify actual HTTP WIDGET data.
- [x] Clean isolated builds without deleting the private dependency cache/wrapper.
- [x] Test removed source/resource/class synchronization and reparse refusal.
- [x] Consume upstream operation guard around actual commit and prove concurrent
revoke/end cannot authorize a later retained write.
- [ ] Qualify host cross-node transactional authorization.
- [x] Complete bounded serial verification after the workstation-load pause.
- [x] Smoke-test private consumer launcher and semantic photo process.
- [x] Run Android synthetic camera/gallery acceptance against the isolated fixture.
- [ ] Qualify production storage, content scanning/release and durable audit.
- [x] Rebuild final source and rerun after upstream SPI changes, retaining the
updated complete runtime manifest and source provenance.
- [x] Explicitly qualify frozen0515's matched core/RDBMS pair in its separate
cache after the current base fix, retaining all 18 consumer cases and manifests.
- [ ] Replace local frozen snapshot adoption with approved immutable published
dependency coordinates/artifacts and wire the optional module into CI using
those verified artifacts; the base qbit's normal build remains independent.
Exact missing inputs and owner handoff are documented in
`HOSTED-CI-DEPENDENCIES.md`; no usable published candidate tuple was found.
- [ ] Root security/release review before publication; passing sequential uploads
are not production 1.0 approval.
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest
-------------------------------------------------------------------------------
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 18.06 s -- in com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest
@@ -1,46 +0,0 @@
[
{
"path": "classes/com/kingsrook/qbits/mobilescanners/recordfiles/MobileCaptureIdentityProvider.class",
"sha256": "7e0efc7142fd06ff04862631fb9e44f4c1271a346358b62e47a5a6f5bc7d9384"
},
{
"path": "classes/com/kingsrook/qbits/mobilescanners/recordfiles/RecordFilesCaptureWidgetData.class",
"sha256": "a40139434123901b98aa6d435057b9522259a838bafdc23c619507e2cff43530"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage.class",
"sha256": "d4837b9318aaa3ad41d6011a1f756e8c9a4f401ec20ff7f9d88a7fe09f80f8f3"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage$1.class",
"sha256": "b72af87ca3af6257a228ab208352529e926521af32241c94434365a5a7026970"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage$Staging.class",
"sha256": "b9cc2861ed24958bbc3013f981962041c0706971819f83322da9513138b0142e"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest.class",
"sha256": "7f259fbcf5640b653caa71c68852be1a5b0ea7d3003307569319bd324ab54cab"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$1.class",
"sha256": "41cf313c9f179bdea43ddac071a1b389ed20a5811999f9a3ed623fe27dda90a4"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$2.class",
"sha256": "94ca35db5907fffe4df967839e4787156d2e9fb026222eef656c6e03ba7c5ccc"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$Authentication.class",
"sha256": "c468e74a1a3644b57c06ea2070d23d2f441443fa694a5d7226885d169cd5e051"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$CapturePermissions.class",
"sha256": "ddd83a0153251c3274c34625caa304093c28f3c0c79f5f291a51d3ff8f4f7099"
},
{
"path": "test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$CaptureWidget.class",
"sha256": "1e9a8b62295a629cb722b09b46a605f13b2ed5784071b989cbd67157ffbe5d7e"
}
]
@@ -1,78 +0,0 @@
[
{
"path": "AUTHORIZED-BROWSING.md",
"sha256": "ebc2a4db713b1f47722a6f5e466b956704aca45e6536469fac18f0635a058c02"
},
{
"path": "CONTENT-READER.md",
"sha256": "2897cb46e64b865fd25d0dd0315443bb8eff6152033a031293de5c66a61a00c4"
},
{
"path": "HTTP-CONSUMER-FIXTURE.md",
"sha256": "0e3d0021f30d07e8ccac6d75d7fb231fe9ccfbffc2e41c2ba9eeff9faaaaf713"
},
{
"path": "INGESTION-CONTRACT.md",
"sha256": "d0d94906b7a461208df4614f97892214ce71cb618f01f990f82f8e89589092d8"
},
{
"path": "MYSQL.md",
"sha256": "16b3759e2d3c9609c903e8dca7162156e73e61cca45daff6e4cf531a83a57927"
},
{
"path": "NATIVE-WORKFLOWS.md",
"sha256": "691e5a37f3566c4f7f7c2c0c5b0f212f54fd946f39d95ed5972e0266a4347331"
},
{
"path": "owner218-bridge-verification.log",
"sha256": "9a98438e78fa6011b3449b42b7946420527e376504d9efcde7fa5814a14d2cc1"
},
{
"path": "PRINT-OBSERVATIONS.md",
"sha256": "0d927dbe4b0d5f93291726f73192682b38b955a3f39189e24f07fde9353206a4"
},
{
"path": "qbit-collaboration-0.1.0-record-files-SNAPSHOT.jar",
"sha256": "87c6a9aac52a0821edd66dad2924a014d2c70aa929ead011dcf7a89a0f94c854"
},
{
"path": "qbit-collaboration.pom.xml",
"sha256": "3edb8deabe52093cf868d1972a5d70fccf46930e20b9e49451518f40b18566e0"
},
{
"path": "qbit-record-files-0.1.0-SNAPSHOT-tests.jar",
"sha256": "65d2f53aa112673c87bf2a40671c01b7755b0a99b174a5186080e13e0b217bfe"
},
{
"path": "qbit-record-files-0.1.0-SNAPSHOT.jar",
"sha256": "a49f1eb3ff27c720fbd09c2af8c63704b3c157c9603da6307b0356e2b939a347"
},
{
"path": "qbit-record-files.pom.xml",
"sha256": "ee17af6afbdff319d24e7e536813e13a04c8e0f8934c40b2ccb16a8c2392cfe0"
},
{
"path": "qqq-backend-core-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "2d3ff9af8b63f46653fcb2eafbab0c34a86f7c83b93c045d1a59e96ce2baf63f"
},
{
"path": "qqq-backend-core.pom.xml",
"sha256": "86fe5d56a9ce05be5dc1e788448ea0967206cf3ce990c0f8d5bdb477fc4651ee"
},
{
"path": "qqq-backend-module-rdbms-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "e3af4b9367548fb340f3860516b9fea27cfb8920331568b40c1aac8cfaa5dc85"
},
{
"path": "qqq-backend-module-rdbms.pom.xml",
"sha256": "a68b0f27b41a21591051da23bf27eb625fe8aace88de5d63519ffd2a417a666a"
},
{
"path": "README.md",
"sha256": "27589cc2218808fccd02c15cc911e0be12ce24b46af53775644e1e5108bc8187"
},
{
"path": "TRACKED-DOWNLOADS.md",
"sha256": "b66782966acf7162d6de0528a8979d3f4829c4e7285c6cb001c97e22e6cd65ef"
}
]
@@ -1,16 +0,0 @@
{
"uploadId" : "b168ee5b-f381-4c12-8861-cafe1a004c07",
"collectionId" : 1,
"state" : "COMPLETE",
"completedAt" : "2026-10-31T08:32:31.634783700Z",
"items" : [ {
"itemId" : "20000000-0000-0000-0000-000000000001",
"fileId" : "62534161-9cb4-42e3-984c-006c73f1fe9a",
"versionId" : 1,
"attachmentId" : 1,
"sizeBytes" : 3,
"sha256" : "039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81",
"position" : 0,
"lifecycleAtCommit" : "QUARANTINED"
} ]
}
@@ -1,272 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2026 Kingsrook, LLC
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<revision>0.1.0-record-files-SNAPSHOT</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jacoco.version>0.8.15</jacoco.version>
<!-- qbit-email from Maven Central -->
<qbit-email.version>0.1.0-RC.2</qbit-email.version>
</properties>
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.1.1</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-collaboration</artifactId>
<version>0.1.0-record-files-SNAPSHOT</version>
<packaging>jar</packaging>
<name>QBit Collaboration</name>
<description>QQQ QBit: record collaboration: attachments, tags, custom fields, comments with mentions, presence, sharing, favorites, follow, timeline, tasks</description>
<url>https://github.com/QRun-IO/qbit-collaboration</url>
<modelVersion>4.0.0</modelVersion>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>kingsrook</id>
<name>Kingsrook Team</name>
<email>contact@kingsrook.com</email>
<organization>Kingsrook, LLC</organization>
<organizationUrl>https://kingsrook.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/QRun-IO/qbit-collaboration.git</connection>
<developerConnection>scm:git:ssh://github.com:QRun-IO/qbit-collaboration.git</developerConnection>
<url>https://github.com/QRun-IO/qbit-collaboration</url>
<tag>HEAD</tag>
</scm>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
</dependency>
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-object-storage</artifactId>
<version>0.1.0-RC.4</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
</dependency>
<!-- optional: email through qbit-email when it is produced in the host instance (EmailCollaborationNotifier) -->
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-email</artifactId>
<version>${qbit-email.version}</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mysql</artifactId>
<version>2.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<!-- help bundles carry the artifact version (QQQ help bundle provenance) -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>help/*.yaml</include>
<include>qbit-collaboration.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>help/*.yaml</exclude>
<exclude>qbit-collaboration.properties</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>3.3.1</version>
<executions><execution><id>package-license-notices</id><phase>process-resources</phase><goals><goal>copy-resources</goal></goals>
<configuration><outputDirectory>${project.build.outputDirectory}/META-INF</outputDirectory><resources><resource>
<directory>${project.basedir}</directory><filtering>false</filtering><includes><include>LICENSE</include><include>NOTICE</include></includes>
</resource></resources></configuration>
</execution></executions>
</plugin>
<!-- the artifact version, for the help bundle version test -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables combine.children="append">
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.3.1</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.12.6</version>
</dependency>
</dependencies>
<configuration>
<configLocation>checkstyle/config.xml</configLocation>
<headerLocation>checkstyle/license.txt</headerLocation>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>false</failsOnError>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
<includeTestSourceDirectory>false</includeTestSourceDirectory>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
<limit>
<counter>CLASS</counter>
<value>COVEREDRATIO</value>
<minimum>0.90</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
<!-- imports the released 4.1 BOM, this profile is active by default and -->
<!-- imports qqq-bom-pom 4.1.0-RC.20 ahead of the parent's 4.0.0 BOM. -->
<profile>
<id>qqq-snapshot</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.20</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.snapshot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
</profiles>
</project>
@@ -1,858 +0,0 @@
[
{
"path": "maven/com/kingsrook/qbits/qbit-mobile-scanners/0.1.0-SNAPSHOT/qbit-mobile-scanners-0.1.0-SNAPSHOT.jar",
"sha256": "2ed09804eab6ff7182c54295e82dc7540932d9acd0744650395722e02cb25809"
},
{
"path": "maven/com/kingsrook/qqq/qqq-backend-core/4.1.0-isolated-record-files-SNAPSHOT/qqq-backend-core-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "2d3ff9af8b63f46653fcb2eafbab0c34a86f7c83b93c045d1a59e96ce2baf63f"
},
{
"path": "maven/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar",
"sha256": "69e5c9fa35da7a51a5fd2099dfe56a2d8d32cf233e2f6d770e796146440263f4"
},
{
"path": "maven/commons-validator/commons-validator/1.10.1/commons-validator-1.10.1.jar",
"sha256": "ed74711a78d793ca37f6075e4a1e93d107233dfd20d0bb17e32bad88d38e56aa"
},
{
"path": "maven/commons-beanutils/commons-beanutils/1.11.0/commons-beanutils-1.11.0.jar",
"sha256": "9e44ba68ec9a3f21286fa2a8bbb003b735c0f69101bb43144b79f4f8aaa74709"
},
{
"path": "maven/commons-digester/commons-digester/2.1/commons-digester-2.1.jar",
"sha256": "e0b2b980a84fc6533c5ce291f1917b32c507f62bcad64198fff44368c2196a3d"
},
{
"path": "maven/commons-logging/commons-logging/1.3.5/commons-logging-1.3.5.jar",
"sha256": "6d7a744e4027649fbb50895df9497d109f98c766a637062fe8d2eabbb3140ba4"
},
{
"path": "maven/commons-collections/commons-collections/3.2.2/commons-collections-3.2.2.jar",
"sha256": "eeeae917917144a68a741d4c0dff66aa5c5c5fd85593ff217bced3fc8ca783b8"
},
{
"path": "maven/software/amazon/awssdk/quicksight/2.41.10/quicksight-2.41.10.jar",
"sha256": "1a517a32c9ab6ef6b0e8d8ac3c76fff440e1bffd7fca38805a3e6363eeb775aa"
},
{
"path": "maven/software/amazon/awssdk/protocol-core/2.41.10/protocol-core-2.41.10.jar",
"sha256": "cec41447843ae209a9c99595a8b04c7a10ff435a98db6a2b5a80c5b94176dea9"
},
{
"path": "maven/software/amazon/awssdk/aws-json-protocol/2.41.10/aws-json-protocol-2.41.10.jar",
"sha256": "6708f94226081f8d47e327faaae6221d51a74ab8a0b51fa9795349160129eaac"
},
{
"path": "maven/software/amazon/awssdk/third-party-jackson-core/2.41.10/third-party-jackson-core-2.41.10.jar",
"sha256": "ddb4bf13df3b4a0d9720a9a4c099f78e1b4712739c1903a24d25292dd4b88ca3"
},
{
"path": "maven/software/amazon/awssdk/http-auth-aws/2.41.10/http-auth-aws-2.41.10.jar",
"sha256": "ffe1c16d5096bafca087419c5706d6b72a97f53cb23370c2ed2839bd0b127796"
},
{
"path": "maven/software/amazon/awssdk/checksums-spi/2.41.10/checksums-spi-2.41.10.jar",
"sha256": "d848c307f290bfb4e685e2246c01257e9f1a77b5acf116deee999ea9cd8c5d56"
},
{
"path": "maven/software/amazon/awssdk/checksums/2.41.10/checksums-2.41.10.jar",
"sha256": "98fcb1f850894757f7706bb05b241b8fb42eb17b51dc6c74fd5087bbcad50986"
},
{
"path": "maven/software/amazon/awssdk/sdk-core/2.41.10/sdk-core-2.41.10.jar",
"sha256": "af431c018f6c7e10f323f492fd3a918743ee55ee2f7e3a523fa8cb8a35d19f49"
},
{
"path": "maven/software/amazon/awssdk/profiles/2.41.10/profiles-2.41.10.jar",
"sha256": "79419cb6a7fa8c5d53ab00d35a9afa7b37260d044a74bb7b011564551dd18383"
},
{
"path": "maven/software/amazon/awssdk/retries/2.41.10/retries-2.41.10.jar",
"sha256": "105b2d03a4f0ec2f650dcaa5e762bb8605c958f02099821587a368ed0ca4dfa1"
},
{
"path": "maven/org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar",
"sha256": "f75ca597789b3dac58f61857b9ac2e1034a68fa672db35055a8fb4509e325f28"
},
{
"path": "maven/software/amazon/awssdk/auth/2.41.10/auth-2.41.10.jar",
"sha256": "56a429c8f191b14b81b88fb848fd09416be8acb8cfe1c86f2a67a352cbac6968"
},
{
"path": "maven/software/amazon/awssdk/http-auth-aws-eventstream/2.41.10/http-auth-aws-eventstream-2.41.10.jar",
"sha256": "1629ed794b43ba81c8fae845f1502ffec8d5b4601ad6796d3b8fd5fff2f53485"
},
{
"path": "maven/software/amazon/eventstream/eventstream/1.0.1/eventstream-1.0.1.jar",
"sha256": "0c37d8e696117f02c302191b8110b0d0eb20fa412fce34c3a269ec73c16ce822"
},
{
"path": "maven/software/amazon/awssdk/http-auth-spi/2.41.10/http-auth-spi-2.41.10.jar",
"sha256": "974dbd758f140edf209f13b30a2eee72605b4d5852133b99ca61f94dacff2b40"
},
{
"path": "maven/software/amazon/awssdk/http-auth/2.41.10/http-auth-2.41.10.jar",
"sha256": "97f53fb7266058b5a154f7ae95b03ff71f3b6d440d02c20da4ce32961e26190c"
},
{
"path": "maven/software/amazon/awssdk/identity-spi/2.41.10/identity-spi-2.41.10.jar",
"sha256": "c880fe296412c1ae5d5ad9475718cf286d3ccbacbc1af6ccc942f1ce6dff17b9"
},
{
"path": "maven/software/amazon/awssdk/http-client-spi/2.41.10/http-client-spi-2.41.10.jar",
"sha256": "a626e831aa0f2a408dcc563684c8612139883ac5deaa474f4fbd6248dd4d9736"
},
{
"path": "maven/software/amazon/awssdk/regions/2.41.10/regions-2.41.10.jar",
"sha256": "4e149ce6e8a0b69cad14e301009a009ef0f0416c7cfa5f6e2e45f48fd98648ed"
},
{
"path": "maven/software/amazon/awssdk/annotations/2.41.10/annotations-2.41.10.jar",
"sha256": "9c05ad9a9bb7107ea55c2949092015cca23bd281cfb09ab88542c547a335a197"
},
{
"path": "maven/software/amazon/awssdk/utils/2.41.10/utils-2.41.10.jar",
"sha256": "c5c9f2b855ccc739db5703fdce6c0ebab32eae67a8cd0477baea665f5e0861f0"
},
{
"path": "maven/software/amazon/awssdk/aws-core/2.41.10/aws-core-2.41.10.jar",
"sha256": "f790329d989d42f75c9bc72d1b25d3b13153415983140ed3f7f75da6261ed10a"
},
{
"path": "maven/software/amazon/awssdk/utils-lite/2.41.10/utils-lite-2.41.10.jar",
"sha256": "14b6dbe56b3cfccc93e5481e5ede898cf7302e254384dc20f7695168b3c73882"
},
{
"path": "maven/software/amazon/awssdk/metrics-spi/2.41.10/metrics-spi-2.41.10.jar",
"sha256": "f7ab5df3db8f9c9404548be3542f64b258057fee57db7024a087c64ea2029a2d"
},
{
"path": "maven/software/amazon/awssdk/json-utils/2.41.10/json-utils-2.41.10.jar",
"sha256": "258b586fbbbeed74dc7e7ca005f38be20bb2942f0d92f2c8f813b25fb479b1a9"
},
{
"path": "maven/software/amazon/awssdk/endpoints-spi/2.41.10/endpoints-spi-2.41.10.jar",
"sha256": "d201f1dc994b4947f6aa457373a1e624fe1f6e4d3383220db4ce93dbd6d67748"
},
{
"path": "maven/software/amazon/awssdk/retries-spi/2.41.10/retries-spi-2.41.10.jar",
"sha256": "0d686efa47f3be5b8f7a9ea120f3894b6becb2e82efd430c34d579264e74f9b5"
},
{
"path": "maven/software/amazon/awssdk/apache-client/2.41.10/apache-client-2.41.10.jar",
"sha256": "f854982b2079a1909ec2722c9eaea48431b1e503436134fbda31f800bc1a7618"
},
{
"path": "maven/software/amazon/awssdk/netty-nio-client/2.41.10/netty-nio-client-2.41.10.jar",
"sha256": "88f20731ac4067fb0bf2c923fd33b873faef908c21e86bab1c24602824294fb9"
},
{
"path": "maven/io/netty/netty-codec-http/4.1.130.Final/netty-codec-http-4.1.130.Final.jar",
"sha256": "5b6addc1df7b3397a193bd6544a8bfdb18ecac99fd13bee4ec75b1781a664e5e"
},
{
"path": "maven/io/netty/netty-codec-http2/4.1.130.Final/netty-codec-http2-4.1.130.Final.jar",
"sha256": "f8ffdb550368fd5dee7c7f1393fa49552522f280ed8de96aebf4269cab0dc8f3"
},
{
"path": "maven/io/netty/netty-codec/4.1.130.Final/netty-codec-4.1.130.Final.jar",
"sha256": "52636bc29bd62120b97bbe5d1d21eab9b1cb2bef8efbb54d2221c5f3fa08d8cd"
},
{
"path": "maven/io/netty/netty-transport/4.1.130.Final/netty-transport-4.1.130.Final.jar",
"sha256": "1bf573266d271f856705a9984d25449c56a1d73c02a16af12033ceccfe555dbb"
},
{
"path": "maven/io/netty/netty-common/4.1.130.Final/netty-common-4.1.130.Final.jar",
"sha256": "53921f28dd5a352b1bed0e1cbcc54d013dc60ffebeae9b2b1e53eabef317e581"
},
{
"path": "maven/io/netty/netty-buffer/4.1.130.Final/netty-buffer-4.1.130.Final.jar",
"sha256": "00a522b67ea35cb7b4dd9cf27f85c6c58f5e306785aa045302e5f6b2d4944a87"
},
{
"path": "maven/io/netty/netty-handler/4.1.130.Final/netty-handler-4.1.130.Final.jar",
"sha256": "98c78ec187ca30a4b9775bf6f632f5c9929db6bf06a60e6971f945813880ca0f"
},
{
"path": "maven/io/netty/netty-transport-native-unix-common/4.1.130.Final/netty-transport-native-unix-common-4.1.130.Final.jar",
"sha256": "cf5efc4168597d7cd14695b469418cac2a1134533f9a0c82ef0538d796fd39e1"
},
{
"path": "maven/io/netty/netty-transport-classes-epoll/4.1.130.Final/netty-transport-classes-epoll-4.1.130.Final.jar",
"sha256": "1c352508d70aca1ccc3068daac907ad1e9d8dc68b07148d57426730bf0d0c8b6"
},
{
"path": "maven/io/netty/netty-resolver/4.1.130.Final/netty-resolver-4.1.130.Final.jar",
"sha256": "48c5b218a89d184e1b601d46433957f515fcefdb4464182b1348bce4f5a18f35"
},
{
"path": "maven/software/amazon/awssdk/apigateway/2.41.10/apigateway-2.41.10.jar",
"sha256": "13f12255a17e89dbef63da12672a1e991e884df3200e1e063865c1b5d3f76973"
},
{
"path": "maven/com/amazonaws/aws-java-sdk-secretsmanager/1.12.797/aws-java-sdk-secretsmanager-1.12.797.jar",
"sha256": "a7f9f597610144ecf57693abda47a451c9c25088e9e4496a3605a4d283560e4f"
},
{
"path": "maven/com/amazonaws/aws-java-sdk-core/1.12.797/aws-java-sdk-core-1.12.797.jar",
"sha256": "75c70d4a7a5b6f3ee3f1fc86b1c7f91106acba4769744791fa4159c4c35bfebc"
},
{
"path": "maven/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.17.2/jackson-dataformat-cbor-2.17.2.jar",
"sha256": "1d7dc634ba3d7981d7017f4f44d43cf371e922c80028b4eb3e5374ce6546d5f6"
},
{
"path": "maven/joda-time/joda-time/2.12.7/joda-time-2.12.7.jar",
"sha256": "385282b005818cfaccdbe8bd2429811e7e641782f2b88932a6b8ff51d668f616"
},
{
"path": "maven/com/amazonaws/jmespath-java/1.12.797/jmespath-java-1.12.797.jar",
"sha256": "4c4a5f0dcf19f4f6d3a966d19ad73638e325d35db26183e5ca4c27eeb97e5d0f"
},
{
"path": "maven/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar",
"sha256": "c8bc7e1c51a6d4ce72f40d2ebbabf1c4b68bfe76e732104b04381b493478e9d6"
},
{
"path": "maven/commons-codec/commons-codec/1.11/commons-codec-1.11.jar",
"sha256": "e599d5318e97aa48f42136a2927e6dfa4e8881dff0e6c8e3109ddbbff51d7b7d"
},
{
"path": "maven/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar",
"sha256": "6c9b3dd142a09dc468e23ad39aad6f75a0f2b85125104469f026e52a474e464f"
},
{
"path": "maven/com/google/crypto/tink/tink/1.23.0/tink-1.23.0.jar",
"sha256": "5bcdc8798ce106f5145acb4a0e993c5e5861337b1e75ca88ceb6ab9da10fa512"
},
{
"path": "maven/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
"sha256": "766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7"
},
{
"path": "maven/com/google/code/gson/gson/2.13.2/gson-2.13.2.jar",
"sha256": "dd0ce1b55a3ed2080cb70f9c655850cda86c206862310009dcb5e5c95265a5e0"
},
{
"path": "maven/com/google/errorprone/error_prone_annotations/2.41.0/error_prone_annotations-2.41.0.jar",
"sha256": "a56e782b5b50811ac204073a355a21d915a2107fce13ec711331ad036f660fcc"
},
{
"path": "maven/com/google/protobuf/protobuf-java/4.33.6/protobuf-java-4.33.6.jar",
"sha256": "c24b2464ecfe9ac1d44e3a8fe281fca09e6ad2ca73dbd689dd7d6d719df987b5"
},
{
"path": "maven/com/twelvemonkeys/imageio/imageio-webp/3.12.0/imageio-webp-3.12.0.jar",
"sha256": "43dc447ccfe34c5c5c6f5d31190270e1f9bf65f60375d697ae2a8ff433468526"
},
{
"path": "maven/com/twelvemonkeys/imageio/imageio-core/3.12.0/imageio-core-3.12.0.jar",
"sha256": "34c6f91bf3b217ed0115bce8283238fbd0264562aa682b4226a3960d0ede9815"
},
{
"path": "maven/com/twelvemonkeys/imageio/imageio-metadata/3.12.0/imageio-metadata-3.12.0.jar",
"sha256": "797d026b67d07e6d5eb00a1ab53a55acaab6f80a71c5ee325572093eb90c0caa"
},
{
"path": "maven/com/twelvemonkeys/common/common-lang/3.12.0/common-lang-3.12.0.jar",
"sha256": "5c65e504b5b64d80683e0fa70a1dd154dd83d4de7da37f21e950f5a9c03bc66d"
},
{
"path": "maven/com/twelvemonkeys/common/common-io/3.12.0/common-io-3.12.0.jar",
"sha256": "bfacfeb02acac47dbfd1fab0002eaeca65547d04249498b654320c9e0fdf6647"
},
{
"path": "maven/com/twelvemonkeys/common/common-image/3.12.0/common-image-3.12.0.jar",
"sha256": "62a3821c724fadfda346c1e2d83b0c506a0c4074be269a52bf4e3192c7dd92bc"
},
{
"path": "maven/com/ibm/icu/icu4j/78.2/icu4j-78.2.jar",
"sha256": "70627c3ff4b9077f6c3dc2156fb480821f9c1a041b645373f2a0391aae3552a1"
},
{
"path": "maven/com/fasterxml/jackson/core/jackson-databind/2.21.7/jackson-databind-2.21.7.jar",
"sha256": "1290c2795e93e8a6861a6c4d9ff0d844d32f5ea178362cb2721edf5561e828b1"
},
{
"path": "maven/com/fasterxml/jackson/core/jackson-annotations/2.21/jackson-annotations-2.21.jar",
"sha256": "53ca085f4a150f703f49e1aabd935bd03b43e1ea3d55d135438292af22cef56b"
},
{
"path": "maven/com/fasterxml/jackson/core/jackson-core/2.21.7/jackson-core-2.21.7.jar",
"sha256": "8c5623b98f32d5f7e1287ff61ed50e08ab7b11381e991fff89327faabba42261"
},
{
"path": "maven/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.21.7/jackson-datatype-jsr310-2.21.7.jar",
"sha256": "5989b937f0aa5b6407e7a061077c9bfcac0590dc0a32e54b2fac6a3e2d49e812"
},
{
"path": "maven/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.21.7/jackson-dataformat-yaml-2.21.7.jar",
"sha256": "df2b3e6a194181dc067b7211d750926b4ae9e4f3c892a0005b8219a9b59be681"
},
{
"path": "maven/org/yaml/snakeyaml/2.5/snakeyaml-2.5.jar",
"sha256": "e6682acf1ace77508ef13649cbf4f8d09d2cf5457bdb61d25ffb6ac0233d78dd"
},
{
"path": "maven/org/json/json/20251224/json-20251224.jar",
"sha256": "2a3438fbf9293174ea82bdfa35e83d8b965cfe6bddc034a1ac5d60107174c209"
},
{
"path": "maven/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar",
"sha256": "32be0e1e76673092f5d12cb790bd2acb6c2ab04c4ea6efc69ea5ee17911c24fe"
},
{
"path": "maven/com/github/hervian/safety-mirror/4.0.1/safety-mirror-4.0.1.jar",
"sha256": "6b2caaef2637a24905c0e53d6c7cb046df8e1ed7053827617044900facd461c4"
},
{
"path": "maven/org/jetbrains/annotations/20.1.0/annotations-20.1.0.jar",
"sha256": "edc8e3ec9796a5f41c1ae44b2d318507ee6ac1212f121d93d33699b3d0aff638"
},
{
"path": "maven/org/dhatim/fastexcel/0.19.0/fastexcel-0.19.0.jar",
"sha256": "2aab9961e107a96c58ef806d6c09ec3dc054e30c5db5164deabaf611be067722"
},
{
"path": "maven/com/github/rzymek/opczip/1.2.0/opczip-1.2.0.jar",
"sha256": "253a4e1bca79cbf242cc52efb7c931121bc28c3393e2aef24f1efe03cd6d9c28"
},
{
"path": "maven/org/dhatim/fastexcel-reader/0.19.0/fastexcel-reader-0.19.0.jar",
"sha256": "ec9ee7b2413f9ae9db1525c334140798725a4d54b58e2bc73bd447a1c282f9e8"
},
{
"path": "maven/com/fasterxml/aalto-xml/1.3.3/aalto-xml-1.3.3.jar",
"sha256": "28829eebb36863b058108fa1b9b8b5cfb94c1871346e4a4f73e69e1fe0a5f0f6"
},
{
"path": "maven/org/codehaus/woodstox/stax2-api/4.2.2/stax2-api-4.2.2.jar",
"sha256": "a61c48d553efad78bc01fffc4ac528bebbae64cbaec170b2a5e39cf61eb51abe"
},
{
"path": "maven/org/apache/commons/commons-compress/1.27.1/commons-compress-1.27.1.jar",
"sha256": "293d80f54b536b74095dcd7ea3cf0a29bbfc3402519281332495f4420d370d16"
},
{
"path": "maven/org/apache/poi/poi/5.5.1/poi-5.5.1.jar",
"sha256": "6c52e876ca75775a11b56e4b36a7541f682827f56406725fcd87560b792ee3d8"
},
{
"path": "maven/org/apache/commons/commons-collections4/4.5.0/commons-collections4-4.5.0.jar",
"sha256": "00f93263c267be201b8ae521b44a7137271b16688435340bf629db1bac0a5845"
},
{
"path": "maven/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1.jar",
"sha256": "1e56d7b058d28b65abd256b8458e3885b674c1d588fa43cd7d1cbb9c7ef2b308"
},
{
"path": "maven/com/zaxxer/SparseBitSet/1.3/SparseBitSet-1.3.jar",
"sha256": "f76b85adb0c00721ae267b7cfde4da7f71d3121cc2160c9fc00c0c89f8c53c8a"
},
{
"path": "maven/org/apache/poi/poi-ooxml/5.5.1/poi-ooxml-5.5.1.jar",
"sha256": "bd7be2fdfe3fd2c1684fa813351c2798fd636b44f6236e0674500d4f8ff1c2f9"
},
{
"path": "maven/org/apache/poi/poi-ooxml-lite/5.5.1/poi-ooxml-lite-5.5.1.jar",
"sha256": "e6e37adeb6d6ee8b40ec491ad955d934d8f99827ab050f105b18286e59b1d9e7"
},
{
"path": "maven/org/apache/xmlbeans/xmlbeans/5.3.0/xmlbeans-5.3.0.jar",
"sha256": "6cc69da3b4d35b83c5e477cd4daba204e44109833e34af2b9a8a2c8788289917"
},
{
"path": "maven/com/github/virtuald/curvesapi/1.08/curvesapi-1.08.jar",
"sha256": "ad95b08b8bbf9d7d17e5e00814898fa23324f32bc5b62f1a37801e6a56ce0079"
},
{
"path": "maven/org/commonmark/commonmark/0.27.1/commonmark-0.27.1.jar",
"sha256": "22aec6eec3b8e2d078dd1ac205291bf0ec58168c68fcf95fdf39fe2d48650a8f"
},
{
"path": "maven/commons-io/commons-io/2.21.0/commons-io-2.21.0.jar",
"sha256": "7d643a2afea8b058b762aa6fb90e5b256f6c729739f8b3784c3370ddc609e88d"
},
{
"path": "maven/com/nimbusds/oauth2-oidc-sdk/11.31.1/oauth2-oidc-sdk-11.31.1.jar",
"sha256": "8af45d9f00d3d30070f5ac1b0a21db15dd0986e3ea5fa1c72c6bf50ddfd20c7a"
},
{
"path": "maven/com/github/stephenc/jcip/jcip-annotations/1.0-1/jcip-annotations-1.0-1.jar",
"sha256": "4fccff8382aafc589962c4edb262f6aa595e34f1e11e61057d1c6a96e8fc7323"
},
{
"path": "maven/com/nimbusds/content-type/2.3/content-type-2.3.jar",
"sha256": "60349793e006fba96b532cb0c21e10e969fe0db8d87f91c3b9eaf82ba2998895"
},
{
"path": "maven/net/minidev/json-smart/2.5.2/json-smart-2.5.2.jar",
"sha256": "4fbdedb0105cedc7f766b95c297d2e88fb6a560da48f3bbaa0cc538ea8b7bf71"
},
{
"path": "maven/net/minidev/accessors-smart/2.5.2/accessors-smart-2.5.2.jar",
"sha256": "9b8a7bc43861d6156c021166d941fb7dddbe4463e2fa5ee88077e4b01452a836"
},
{
"path": "maven/org/ow2/asm/asm/9.7.1/asm-9.7.1.jar",
"sha256": "8cadd43ac5eb6d09de05faecca38b917a040bb9139c7edeb4cc81c740b713281"
},
{
"path": "maven/com/nimbusds/lang-tag/1.7/lang-tag-1.7.jar",
"sha256": "e8c1c594e2425bdbea2d860de55c69b69fc5d59454452449a0f0913c2a5b8a31"
},
{
"path": "maven/com/nimbusds/nimbus-jose-jwt/10.6/nimbus-jose-jwt-10.6.jar",
"sha256": "7ef3b0dd4a27407639d469cd75e7770463b4ee36e2c13535465da175d3b8f964"
},
{
"path": "maven/com/auth0/auth0/3.0.0/auth0-3.0.0.jar",
"sha256": "145defc90d013fbf518a69e9de90423da8053dbba39261ab1548a924c5d160af"
},
{
"path": "maven/com/squareup/okhttp3/okhttp/5.2.1/okhttp-5.2.1.jar",
"sha256": "e720a383fdc3eb1df4aac77a085e61b7730837364151de867093fdbcafcb44aa"
},
{
"path": "maven/com/squareup/okio/okio/3.16.1/okio-3.16.1.jar",
"sha256": "c9964f394c4cd32b79bf338afbfa2f2297221eee08311787061cea55f87dd60e"
},
{
"path": "maven/com/squareup/okio/okio-jvm/3.16.1/okio-jvm-3.16.1.jar",
"sha256": "31f48e6463ec587d6d262d042c91da00659c983b6ad20d5982bf31e85222693c"
},
{
"path": "maven/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.18.2/jackson-datatype-jdk8-2.18.2.jar",
"sha256": "f30d77f5826b9e9813342e84ab412095ed4ed5cf4fef6f93cebb848cb0fd0294"
},
{
"path": "maven/com/squareup/okhttp3/logging-interceptor/5.2.1/logging-interceptor-5.2.1.jar",
"sha256": "94dee77ec5dc48f6db0e7b3d7b086fb152a01f32d434bda6b670711927e386d7"
},
{
"path": "maven/com/squareup/okhttp3/okhttp-jvm/5.2.1/okhttp-jvm-5.2.1.jar",
"sha256": "9632c08567dbb21c569b13d793107834c8580d44e4eea74b2eae0722f0506179"
},
{
"path": "maven/net/jodah/failsafe/2.4.4/failsafe-2.4.4.jar",
"sha256": "ac3f916e536db9a69f93706f778e90e2be476bdebea0c45cca6d9675ee99ec30"
},
{
"path": "maven/com/auth0/java-jwt/4.5.0/java-jwt-4.5.0.jar",
"sha256": "defc50d5066630d46737ba3077e1e13df7201baf5817394c9a4d3d23e2ad3019"
},
{
"path": "maven/com/auth0/jwks-rsa/0.23.0/jwks-rsa-0.23.0.jar",
"sha256": "d20c5222c0eab0b801b20f4e390568af7771412aef2224e2c32d8cbb58a360bc"
},
{
"path": "maven/com/google/guava/guava/32.1.2-jre/guava-32.1.2-jre.jar",
"sha256": "bc65dea7cfd9e4dacf8419d8af0e741655857d27885bb35d943d7187fc3a8fce"
},
{
"path": "maven/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar",
"sha256": "a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26"
},
{
"path": "maven/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar",
"sha256": "b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99"
},
{
"path": "maven/org/checkerframework/checker-qual/3.33.0/checker-qual-3.33.0.jar",
"sha256": "e316255bbfcd9fe50d165314b85abb2b33cb2a66a93c491db648e498a82c2de1"
},
{
"path": "maven/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar",
"sha256": "f02a95fa1a5e95edb3ed859fd0fb7df709d121a35290eff8b74dce2ab7f4d6ed"
},
{
"path": "maven/io/github/cdimascio/dotenv-java/3.2.0/dotenv-java-3.2.0.jar",
"sha256": "47f5d208b29497f286a454817c955750cc92b99b4680392dd40e5db592259d22"
},
{
"path": "maven/org/apache/velocity/velocity-engine-core/2.4.1/velocity-engine-core-2.4.1.jar",
"sha256": "1c19157d1171d560088e485be97c93a7a2f7e9f56e517f0a30273c5c39df6231"
},
{
"path": "maven/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar",
"sha256": "d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0"
},
{
"path": "maven/org/jsoup/jsoup/1.23.2/jsoup-1.23.2.jar",
"sha256": "e9d8c856856680427f096d156f02a414289985c930998fcb519ce5be39b42003"
},
{
"path": "maven/com/openhtmltopdf/openhtmltopdf-core/1.0.10/openhtmltopdf-core-1.0.10.jar",
"sha256": "3e6fd2250d833d500b7cd48b7a896700d0c33bd9f77a219e820493b01566eda3"
},
{
"path": "maven/com/openhtmltopdf/openhtmltopdf-pdfbox/1.0.10/openhtmltopdf-pdfbox-1.0.10.jar",
"sha256": "7de90df1b3ecf84e6f0daf808d724c11142007a2f22bff1936479bf17251d31a"
},
{
"path": "maven/org/apache/pdfbox/pdfbox/2.0.24/pdfbox-2.0.24.jar",
"sha256": "3c2c0553ec0e7533c490b4c952e1af113621de5275af6e380e11d0d9a0a4f3d6"
},
{
"path": "maven/org/apache/pdfbox/fontbox/2.0.24/fontbox-2.0.24.jar",
"sha256": "2e8c0a569a90b04734fbc0c805d77f4ec03f98c11f5705055ccd7718c1953d68"
},
{
"path": "maven/org/apache/pdfbox/xmpbox/2.0.24/xmpbox-2.0.24.jar",
"sha256": "27383df2285b9e228c39f2c755adeebbef774793d33c4f20f0dc99e9ebaaf673"
},
{
"path": "maven/de/rototor/pdfbox/graphics2d/0.32/graphics2d-0.32.jar",
"sha256": "37f8f387395f96c214ac44f7475c7a2e1f832dfc1de289a3610e0ffbf728f679"
},
{
"path": "maven/com/google/api-client/google-api-client/1.35.2/google-api-client-1.35.2.jar",
"sha256": "f195cd6228d3f99fa7e30ff2dee60ad0f2c7923be31399a7dcdc1abd679aa22e"
},
{
"path": "maven/com/google/oauth-client/google-oauth-client/1.34.1/google-oauth-client-1.34.1.jar",
"sha256": "193edf97aefa28b93c5892bdc598bac34fa4c396588030084f290b1440e8b98a"
},
{
"path": "maven/com/google/http-client/google-http-client-gson/1.42.0/google-http-client-gson-1.42.0.jar",
"sha256": "cb852272c1cb0c8449d8b1a70f3e0f2c1efb2063e543183faa43078fb446f540"
},
{
"path": "maven/com/google/http-client/google-http-client-apache-v2/1.42.0/google-http-client-apache-v2-1.42.0.jar",
"sha256": "1fc4964236b67cf3c5651d7ac1dff668f73b7810c7f1dc0862a0e5bc01608785"
},
{
"path": "maven/com/google/http-client/google-http-client/1.42.0/google-http-client-1.42.0.jar",
"sha256": "82ca0e08171846d1768d5ac3f13244d6fe5a54102c14735ef40bf15d57d478e5"
},
{
"path": "maven/io/opencensus/opencensus-api/0.31.1/opencensus-api-0.31.1.jar",
"sha256": "f1474d47f4b6b001558ad27b952e35eda5cc7146788877fc52938c6eba24b382"
},
{
"path": "maven/io/grpc/grpc-context/1.27.2/grpc-context-1.27.2.jar",
"sha256": "bcbf9055dff453fd6508bd7cca2a0aa2d5f059a9c94beed1f5fda1dc015607b8"
},
{
"path": "maven/io/opencensus/opencensus-contrib-http-util/0.31.1/opencensus-contrib-http-util-0.31.1.jar",
"sha256": "3ea995b55a4068be22989b70cc29a4d788c2d328d1d50613a7a9afd13fdd2d0a"
},
{
"path": "maven/com/google/auth/google-auth-library-oauth2-http/1.41.0/google-auth-library-oauth2-http-1.41.0.jar",
"sha256": "a8f534242b5a54ff3c6a5d1f53678df9d2de71468bed0d74323b7ba05e672113"
},
{
"path": "maven/com/google/auto/value/auto-value-annotations/1.11.0/auto-value-annotations-1.11.0.jar",
"sha256": "5a055ce4255333b3346e1a8703da5bf8ff049532286fdcd31712d624abe111dd"
},
{
"path": "maven/com/google/auth/google-auth-library-credentials/1.41.0/google-auth-library-credentials-1.41.0.jar",
"sha256": "26162b1b9c448b119cfc6edf43346274a15a28ed8ff7a9b4c908f44071e33dad"
},
{
"path": "maven/com/google/api/api-common/2.53.0/api-common-2.53.0.jar",
"sha256": "f7073fb389c2aa329981ee65241db7947e8744c988e6c4d975f18bf2748b935a"
},
{
"path": "maven/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar",
"sha256": "e04ba5195bcd555dc95650f7cc614d151e4bcd52d29a10b8aa2197f3ab89ab9b"
},
{
"path": "maven/com/google/apis/google-api-services-drive/v3-rev20251114-2.0.0/google-api-services-drive-v3-rev20251114-2.0.0.jar",
"sha256": "9345ab816ecf13f06820976bc6262aca4274c2acb012ffccf38c5336717b0cdb"
},
{
"path": "maven/com/amazonaws/aws-java-sdk-sqs/1.12.797/aws-java-sdk-sqs-1.12.797.jar",
"sha256": "4403fe5b4c45f92276b7ec4bdf681cdb4afb068951f5d1ba06f70b0ad8223f3e"
},
{
"path": "maven/com/amazonaws/aws-java-sdk-ses/1.12.797/aws-java-sdk-ses-1.12.797.jar",
"sha256": "7c64e825bb95b3cb304e158c02383265a51374667dcf7ec9405e97f2b9e47db4"
},
{
"path": "maven/org/quartz-scheduler/quartz/2.5.2/quartz-2.5.2.jar",
"sha256": "452e418739c0da1bff255f7c1343dd3a92e1fdd3ceb126b185939edae9922091"
},
{
"path": "maven/jakarta/xml/bind/jakarta.xml.bind-api/4.0.4/jakarta.xml.bind-api-4.0.4.jar",
"sha256": "c507ca69a8c6dd11bf4afeec9e0d412c4fa3933fffb0a84680ea5727e8472124"
},
{
"path": "maven/com/mchange/mchange-commons-java/0.6.0/mchange-commons-java-0.6.0.jar",
"sha256": "ef4cc2e5c15df799af5e96a27ceded23d07bd9b15a538069886dd2cd3e3a1210"
},
{
"path": "maven/org/apache/logging/log4j/log4j-slf4j-impl/2.25.5/log4j-slf4j-impl-2.25.5.jar",
"sha256": "3d25b926bc6025c47ec010459749c12aa66b9d5537ba1922d67d89a098a66607"
},
{
"path": "maven/com/sun/mail/jakarta.mail/2.0.2/jakarta.mail-2.0.2.jar",
"sha256": "536f0ae5dd774e257f3064f19a2b982aa56072c07c675c70f98cdc262047b4b5"
},
{
"path": "maven/com/sun/activation/jakarta.activation/2.0.1/jakarta.activation-2.0.1.jar",
"sha256": "b9e24b7dd6e07495562ea96531be3130c96dba4d78e1dfd88adbbdebf4332871"
},
{
"path": "maven/org/eclipse/angus/angus-activation/2.0.3/angus-activation-2.0.3.jar",
"sha256": "a6bd35c538cf90fff941ad6258c40c08fca0b5c9c3f536c657114f27ce0527a7"
},
{
"path": "maven/jakarta/activation/jakarta.activation-api/2.1.4/jakarta.activation-api-2.1.4.jar",
"sha256": "c9db52100ce6c8aac95cc39075f95720d2e561b11f8051b81c121ad4effd7004"
},
{
"path": "maven/org/apache/logging/log4j/log4j-api/2.25.5/log4j-api-2.25.5.jar",
"sha256": "64777f73ea0b3104c04eb82befbdccc30a425a19e83ad06cb2f93aa303511863"
},
{
"path": "maven/org/apache/logging/log4j/log4j-core/2.25.5/log4j-core-2.25.5.jar",
"sha256": "050c4f85deb48b055e9c041ac39043fbb79ce477841a11a1eb2969c86c6a0072"
},
{
"path": "maven/com/kingsrook/qqq/qqq-middleware-javalin/4.1.0-RC.20/qqq-middleware-javalin-4.1.0-RC.20.jar",
"sha256": "ef33a9c88e29269d6bc8efbbc0a1411b0409893baee49a1f06473e3803c4443c"
},
{
"path": "maven/com/kingsrook/qqq/qqq-openapi/4.1.0-RC.20/qqq-openapi-4.1.0-RC.20.jar",
"sha256": "bbf2257dfce5cedb670dd04ce378bcf177ac157288ff3a28f95a96f3c50de09d"
},
{
"path": "maven/info/picocli/picocli/4.7.7/picocli-4.7.7.jar",
"sha256": "f86e30fffd10d2b13b8caa8d4b237a7ee61f2ffccf5b1941de718b765d235bf8"
},
{
"path": "maven/io/javalin/javalin/7.2.3/javalin-7.2.3.jar",
"sha256": "c27b5052e8fb259acd8fd1d9f598c8543e43fe4df9271c7697a9c832c9a66e17"
},
{
"path": "maven/org/eclipse/jetty/jetty-server/12.1.12/jetty-server-12.1.12.jar",
"sha256": "9ba04bb8d011444a10873a51b1cb8297c04f70db55c8a8d9efcc3a9b97896787"
},
{
"path": "maven/org/eclipse/jetty/jetty-http/12.1.12/jetty-http-12.1.12.jar",
"sha256": "5476e16ef7e28883dba3669ee06491021e4a9aeba9a76f901a3f5c2bb5252a3e"
},
{
"path": "maven/org/eclipse/jetty/jetty-util/12.1.12/jetty-util-12.1.12.jar",
"sha256": "f6fbfa61cfcca032ff82ad8ff3b66a14374047f15486521dbc89a6c0df19ccf2"
},
{
"path": "maven/org/eclipse/jetty/jetty-io/12.1.12/jetty-io-12.1.12.jar",
"sha256": "a809a6a534adccfaa08bca94aa54328e3ac858973ccddbda770f7e9e0f5099fc"
},
{
"path": "maven/org/eclipse/jetty/ee10/jetty-ee10-servlet/12.1.12/jetty-ee10-servlet-12.1.12.jar",
"sha256": "021375ea80d84b5f9d46260916fc0450dc4fae5a95ce80f8289e87a0db07de47"
},
{
"path": "maven/jakarta/servlet/jakarta.servlet-api/6.0.0/jakarta.servlet-api-6.0.0.jar",
"sha256": "c034eb1afb158987dbb53a5fea0cadf611c8dae8daadd59c44d9d5ab70129cef"
},
{
"path": "maven/org/eclipse/jetty/jetty-security/12.1.12/jetty-security-12.1.12.jar",
"sha256": "fcc0206c1ac66b632ef29a59ccf76c3f3737a62ef54694b1d111eeb2e6efe804"
},
{
"path": "maven/org/eclipse/jetty/jetty-session/12.1.12/jetty-session-12.1.12.jar",
"sha256": "73aea4cdd91f2027da1a09cd5953b661625017971d53d8e1ff342f8547752b06"
},
{
"path": "maven/org/eclipse/jetty/ee10/websocket/jetty-ee10-websocket-jetty-server/12.1.12/jetty-ee10-websocket-jetty-server-12.1.12.jar",
"sha256": "bce3641addd34a0d9a3ce8080f31f3b7f45b2bbdf3e7d5655ccd5f4d529c7816"
},
{
"path": "maven/org/eclipse/jetty/ee10/jetty-ee10-annotations/12.1.12/jetty-ee10-annotations-12.1.12.jar",
"sha256": "c45ba9ff724845ef5848f86cf164c87824d4be18095216b9cba66a94a72b83b6"
},
{
"path": "maven/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar",
"sha256": "5f65fdaf424eee2b55e1d882ba9bb376be93fb09b37b808be6e22e8851c909fe"
},
{
"path": "maven/org/eclipse/jetty/jetty-annotations/12.1.12/jetty-annotations-12.1.12.jar",
"sha256": "ce51d231435cdd774336abcb44a7817195fd12487ed213327e8d43db172af467"
},
{
"path": "maven/org/eclipse/jetty/ee10/jetty-ee10-plus/12.1.12/jetty-ee10-plus-12.1.12.jar",
"sha256": "80d050384ef31c921e0fbeeded3c70fc8d35a29b7951511a9ccfaeb06b5e29c7"
},
{
"path": "maven/jakarta/enterprise/jakarta.enterprise.cdi-api/4.0.1/jakarta.enterprise.cdi-api-4.0.1.jar",
"sha256": "beaf74c4f2618189309e3f4a09c43effab633dd96aa1f6dc58a6ba7ee0042717"
},
{
"path": "maven/jakarta/enterprise/jakarta.enterprise.lang-model/4.0.1/jakarta.enterprise.lang-model-4.0.1.jar",
"sha256": "53acafe65b6ef0195fa1b8a0ef2650e5aa024c32cb4059c4df372d6b32089cd3"
},
{
"path": "maven/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar",
"sha256": "f7dc98062fccf14126abb751b64fab12c312566e8cbdc8483598bffcea93af7c"
},
{
"path": "maven/jakarta/interceptor/jakarta.interceptor-api/2.1.0/jakarta.interceptor-api-2.1.0.jar",
"sha256": "ef787d3f713fc6ff4f02cd4b0dbed08f93d8af3400c90cbb43fb4b5c0583710b"
},
{
"path": "maven/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar",
"sha256": "50c0a7c760c13ae6c042acf182b28f0047413db95b4636fb8879bcffab5ba875"
},
{
"path": "maven/org/eclipse/jetty/jetty-jndi/12.1.12/jetty-jndi-12.1.12.jar",
"sha256": "a701368f9810433103836f0f2c0c0f6a2ca49f951d25d155e4e507821cb54b43"
},
{
"path": "maven/org/eclipse/jetty/jetty-plus/12.1.12/jetty-plus-12.1.12.jar",
"sha256": "0e29a87abf5b651f77e5c3e7c726f0ecef30a9bc57a5e0aa5aa3f784cfc0a7bd"
},
{
"path": "maven/org/eclipse/jetty/ee10/jetty-ee10-webapp/12.1.12/jetty-ee10-webapp-12.1.12.jar",
"sha256": "84de1014bb79f957f0fc66d3db9b2bc7eec19f01c8ace4e250b4707af0eed0f3"
},
{
"path": "maven/org/eclipse/jetty/jetty-xml/12.1.12/jetty-xml-12.1.12.jar",
"sha256": "1c7b6320e3ad4ccca861ecfd1347f10ac6130ca2c2eb788a6b5dff3ab0c0f532"
},
{
"path": "maven/org/eclipse/jetty/ee/jetty-ee-webapp/12.1.12/jetty-ee-webapp-12.1.12.jar",
"sha256": "f70d9b30fdfd970e8bbc462b9d6b4cb85f850428d8640529fcc1f1912b2941b4"
},
{
"path": "maven/org/ow2/asm/asm-commons/9.10.1/asm-commons-9.10.1.jar",
"sha256": "6d0abefb7cbf972ea16edb37ec14835372505063a45f976ab7ea889ed9497895"
},
{
"path": "maven/org/ow2/asm/asm-tree/9.10.1/asm-tree-9.10.1.jar",
"sha256": "3dfb0d5b6a106cd40b5b250e39935fbf2f927f4477546a5369a3ac609cf0506b"
},
{
"path": "maven/org/eclipse/jetty/ee10/websocket/jetty-ee10-websocket-servlet/12.1.12/jetty-ee10-websocket-servlet-12.1.12.jar",
"sha256": "83a58b4ee3bf6d0284c50304725cf93b694a67b1b1cc139af72500296dbf7359"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-core-server/12.1.12/jetty-websocket-core-server-12.1.12.jar",
"sha256": "d91becae70e7b15e6200f40d02239d8315f8b26c90d4ab38348fcdad41c0ebf5"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-jetty-api/12.1.12/jetty-websocket-jetty-api-12.1.12.jar",
"sha256": "887090638bf758dce9268478a3ccf168929de52dcdecdddb4eaff62263cbd961"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-jetty-common/12.1.12/jetty-websocket-jetty-common-12.1.12.jar",
"sha256": "933d05b566effad97f84b5d986be810a2928685c875d7fb90f301163f24d2718"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-core-common/12.1.12/jetty-websocket-core-common-12.1.12.jar",
"sha256": "a1ef0c79a4b8b9e4ce969e70d8de9fbed4776625863bdbd5eb5f69fb70208399"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-jetty-server/12.1.12/jetty-websocket-jetty-server-12.1.12.jar",
"sha256": "0b189ffab87b02a4da1187dc4022410c5f235b5a1a7ccfeea3cc4846a8e80664"
},
{
"path": "maven/org/jetbrains/kotlin/kotlin-stdlib/2.3.0/kotlin-stdlib-2.3.0.jar",
"sha256": "887587c91713250ad52fe14ad9166d042c33835049890e9437f355ffc5a195b1"
},
{
"path": "maven/org/slf4j/slf4j-simple/2.0.18/slf4j-simple-2.0.18.jar",
"sha256": "8268bd018a5709b07209e0d8ca6221a37584ba1bc12ba985b8335a82c648bdd0"
},
{
"path": "maven/com/kingsrook/qbits/qbit-record-files/0.1.0-SNAPSHOT/qbit-record-files-0.1.0-SNAPSHOT.jar",
"sha256": "a49f1eb3ff27c720fbd09c2af8c63704b3c157c9603da6307b0356e2b939a347"
},
{
"path": "maven/com/kingsrook/qbits/qbit-collaboration/0.1.0-record-files-SNAPSHOT/qbit-collaboration-0.1.0-record-files-SNAPSHOT.jar",
"sha256": "87c6a9aac52a0821edd66dad2924a014d2c70aa929ead011dcf7a89a0f94c854"
},
{
"path": "maven/com/kingsrook/qbits/qbit-object-storage/0.1.0-RC.4/qbit-object-storage-0.1.0-RC.4.jar",
"sha256": "edb931794d0153fe27e65ef34f4bb5ad93dbfcff2bd7d0eb10e2701ffe1910f2"
},
{
"path": "maven/com/kingsrook/qqq/qqq-backend-module-filesystem/4.1.0-RC.6/qqq-backend-module-filesystem-4.1.0-RC.6.jar",
"sha256": "241d3bc3cc1200680ce6b6991f7da425ffefec45028d9a85edd9320862f26f13"
},
{
"path": "maven/software/amazon/awssdk/s3/2.41.34/s3-2.41.34.jar",
"sha256": "d4a3acd700845a93738bee3b0e2089b86e4cb1d5be3c2d4d00deb61edccdd760"
},
{
"path": "maven/software/amazon/awssdk/aws-xml-protocol/2.41.34/aws-xml-protocol-2.41.34.jar",
"sha256": "e9157b4908b2540feda3a9aa8e08e692ec938fe933b972bffe3b201b879bab27"
},
{
"path": "maven/software/amazon/awssdk/aws-query-protocol/2.41.34/aws-query-protocol-2.41.34.jar",
"sha256": "d57caf18f117b42e682a29a177c334110a88fc6ba3b3bdf290a543bf9bf9a4be"
},
{
"path": "maven/software/amazon/awssdk/arns/2.41.34/arns-2.41.34.jar",
"sha256": "0ecf3fd1af7dbc52d931b9f3a145b11b8bc7b07fb31c631a52505647815b6873"
},
{
"path": "maven/software/amazon/awssdk/crt-core/2.41.34/crt-core-2.41.34.jar",
"sha256": "bba14689c6b538b8e383919af8be6529eb6a35f789b7a264fb362a8ca15d9aff"
},
{
"path": "maven/com/kingsrook/qqq/qqq-backend-module-rdbms/4.1.0-isolated-record-files-SNAPSHOT/qqq-backend-module-rdbms-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "e3af4b9367548fb340f3860516b9fea27cfb8920331568b40c1aac8cfaa5dc85"
},
{
"path": "maven/com/mysql/mysql-connector-j/9.5.0/mysql-connector-j-9.5.0.jar",
"sha256": "f2ca3dfaf00d4aa311470db7ea3051962944ba0cb60005a2f75467549c39f425"
},
{
"path": "maven/com/mchange/c3p0/0.14.0/c3p0-0.14.0.jar",
"sha256": "7b700e8fc71d0519dbad7c22916e85eaa38d544802837a928655c70be3b9f37f"
},
{
"path": "maven/com/kingsrook/qbits/qbit-record-files/0.1.0-SNAPSHOT/qbit-record-files-0.1.0-SNAPSHOT-tests.jar",
"sha256": "65d2f53aa112673c87bf2a40671c01b7755b0a99b174a5186080e13e0b217bfe"
},
{
"path": "maven/com/h2database/h2/2.2.224/h2-2.2.224.jar",
"sha256": "b9d8f19358ada82a4f6eb5b174c6cfe320a375b5a9cb5a4fe456d623e6e55497"
},
{
"path": "maven/org/junit/jupiter/junit-jupiter/5.11.4/junit-jupiter-5.11.4.jar",
"sha256": "aa880e4afba87d447357e4c1fc098c5cb1d200cb9403496c00d3b35a5bd0e8db"
},
{
"path": "maven/org/junit/jupiter/junit-jupiter-api/5.11.4/junit-jupiter-api-5.11.4.jar",
"sha256": "ab83ef9e51ac4597d59d26b4b58812129550e2f579a404c8af7d09f5ce5b4293"
},
{
"path": "maven/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar",
"sha256": "48e2df636cab6563ced64dcdff8abb2355627cb236ef0bf37598682ddf742f1b"
},
{
"path": "maven/org/junit/platform/junit-platform-commons/1.11.4/junit-platform-commons-1.11.4.jar",
"sha256": "9edd969b0d0670c54105bc91ae79bd1c6f503e12115faba82073b84c86bbc334"
},
{
"path": "maven/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar",
"sha256": "b509448ac506d607319f182537f0b35d71007582ec741832a1f111e5b5b70b38"
},
{
"path": "maven/org/junit/jupiter/junit-jupiter-params/5.11.4/junit-jupiter-params-5.11.4.jar",
"sha256": "02a6e015de7ce94ac7f256e7fa05b8091dea861fe79a555a7993313d0f6c7d96"
},
{
"path": "maven/org/junit/jupiter/junit-jupiter-engine/5.11.4/junit-jupiter-engine-5.11.4.jar",
"sha256": "cdf8ac59f3fad774ca738ad03890950eeb91833ef0e8908753177edd26f1581c"
},
{
"path": "maven/org/junit/platform/junit-platform-engine/1.11.4/junit-platform-engine-1.11.4.jar",
"sha256": "b1dd998f64f9acadc15966d9cd3d08074662677b3e390f0a38fcbf0bb4c72330"
},
{
"path": "maven/org/assertj/assertj-core/3.27.7/assertj-core-3.27.7.jar",
"sha256": "c4a445426c3c2861666863b842cc4ec7bbb1c4226fefd370b6d2fe83d6c4ff0f"
},
{
"path": "maven/net/bytebuddy/byte-buddy/1.18.3/byte-buddy-1.18.3.jar",
"sha256": "d78396e3c5bce3f2865c9186647481e5589d34cacc632484715b686108d17c66"
}
]
@@ -1,6 +0,0 @@
{
"derivedCollaborationPomSha256": "97e7832d77730c8a5676c263dca9abda677af41573affff3907587505aefdee7",
"classCount": 3158,
"duplicateQqqClasses": 0,
"h2Version": "2.2.224"
}
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest
-------------------------------------------------------------------------------
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 21.55 s -- in com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.RecordFilesCaptureContractTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.kingsrook.qbits.mobilescanners.RecordFilesCaptureContractTest
@@ -1,62 +0,0 @@
[
{
"path": "target/module/classes/com/kingsrook/qbits/mobilescanners/recordfiles/MobileCaptureIdentityProvider.class",
"sha256": "7e0efc7142fd06ff04862631fb9e44f4c1271a346358b62e47a5a6f5bc7d9384"
},
{
"path": "target/module/classes/com/kingsrook/qbits/mobilescanners/recordfiles/MobileRecordFilesContract.class",
"sha256": "b176691c11247551a814ccacc438f3e7a2c3a77c37cfe796a54104ae63ed5764"
},
{
"path": "target/module/classes/com/kingsrook/qbits/mobilescanners/recordfiles/MobileRecordFilesContract$NonblankString.class",
"sha256": "6136fbcce0c473a011920b7f55d2991e29f8af486c831480886049f118f70853"
},
{
"path": "target/module/classes/com/kingsrook/qbits/mobilescanners/recordfiles/RecordFilesCaptureWidgetData.class",
"sha256": "243b074a2845b0943f3281aafa0ed57badc5b347a81e98a077540ea49f9e9d8f"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage.class",
"sha256": "d4837b9318aaa3ad41d6011a1f756e8c9a4f401ec20ff7f9d88a7fe09f80f8f3"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage$1.class",
"sha256": "b72af87ca3af6257a228ab208352529e926521af32241c94434365a5a7026970"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage$Staging.class",
"sha256": "b9cc2861ed24958bbc3013f981962041c0706971819f83322da9513138b0142e"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest.class",
"sha256": "6480d3572cf59be1bb63763992cc27a5edbad203297c5b5df4efd4ca65854fff"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$1.class",
"sha256": "ee070839b831d7b348010a256967ebdff773b6d6f28b9c5621236e557304dc9a"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$2.class",
"sha256": "c576478a5a64286e075823e61803d9bac23370eb6f20c3ddbf7a68123dfe7707"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$Authentication.class",
"sha256": "6730c57336227341acb6280c105c707f39bb5aeff3748cfe2821cc82b9a1b915"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$CapturePermissions.class",
"sha256": "652ca9ab44dd29c8cea2598f3210f5470a550abe518fa7e2418e0be4eab5d382"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$CapturePrepare.class",
"sha256": "84e59679ee3763c4c359d3d1deef676aa541a6b8fffc39e1c54418f947203e92"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest$CaptureWidget.class",
"sha256": "9b77d2f7dcae1a65e47fac0cf6d977ad85fe93645d86d4d55fa8fd4f5b6f5c05"
},
{
"path": "target/module/test-classes/com/kingsrook/qbits/mobilescanners/RecordFilesCaptureContractTest.class",
"sha256": "52e60f29889fb662a12930e9d796ca46d59013d176103644f1dcd4254e322777"
}
]
@@ -1,78 +0,0 @@
[
{
"path": "AUTHORIZED-BROWSING.md",
"sha256": "ebc2a4db713b1f47722a6f5e466b956704aca45e6536469fac18f0635a058c02"
},
{
"path": "CONTENT-READER.md",
"sha256": "2897cb46e64b865fd25d0dd0315443bb8eff6152033a031293de5c66a61a00c4"
},
{
"path": "HTTP-CONSUMER-FIXTURE.md",
"sha256": "0e3d0021f30d07e8ccac6d75d7fb231fe9ccfbffc2e41c2ba9eeff9faaaaf713"
},
{
"path": "INGESTION-CONTRACT.md",
"sha256": "d0d94906b7a461208df4614f97892214ce71cb618f01f990f82f8e89589092d8"
},
{
"path": "MYSQL.md",
"sha256": "16b3759e2d3c9609c903e8dca7162156e73e61cca45daff6e4cf531a83a57927"
},
{
"path": "NATIVE-WORKFLOWS.md",
"sha256": "691e5a37f3566c4f7f7c2c0c5b0f212f54fd946f39d95ed5972e0266a4347331"
},
{
"path": "owner218-bridge-verification.log",
"sha256": "9a98438e78fa6011b3449b42b7946420527e376504d9efcde7fa5814a14d2cc1"
},
{
"path": "PRINT-OBSERVATIONS.md",
"sha256": "0d927dbe4b0d5f93291726f73192682b38b955a3f39189e24f07fde9353206a4"
},
{
"path": "qbit-collaboration-0.1.0-record-files-SNAPSHOT.jar",
"sha256": "87c6a9aac52a0821edd66dad2924a014d2c70aa929ead011dcf7a89a0f94c854"
},
{
"path": "qbit-collaboration.pom.xml",
"sha256": "3edb8deabe52093cf868d1972a5d70fccf46930e20b9e49451518f40b18566e0"
},
{
"path": "qbit-record-files-0.1.0-SNAPSHOT-tests.jar",
"sha256": "65d2f53aa112673c87bf2a40671c01b7755b0a99b174a5186080e13e0b217bfe"
},
{
"path": "qbit-record-files-0.1.0-SNAPSHOT.jar",
"sha256": "a49f1eb3ff27c720fbd09c2af8c63704b3c157c9603da6307b0356e2b939a347"
},
{
"path": "qbit-record-files.pom.xml",
"sha256": "ee17af6afbdff319d24e7e536813e13a04c8e0f8934c40b2ccb16a8c2392cfe0"
},
{
"path": "qqq-backend-core-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "2d3ff9af8b63f46653fcb2eafbab0c34a86f7c83b93c045d1a59e96ce2baf63f"
},
{
"path": "qqq-backend-core.pom.xml",
"sha256": "86fe5d56a9ce05be5dc1e788448ea0967206cf3ce990c0f8d5bdb477fc4651ee"
},
{
"path": "qqq-backend-module-rdbms-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "e3af4b9367548fb340f3860516b9fea27cfb8920331568b40c1aac8cfaa5dc85"
},
{
"path": "qqq-backend-module-rdbms.pom.xml",
"sha256": "a68b0f27b41a21591051da23bf27eb625fe8aace88de5d63519ffd2a417a666a"
},
{
"path": "README.md",
"sha256": "27589cc2218808fccd02c15cc911e0be12ce24b46af53775644e1e5108bc8187"
},
{
"path": "TRACKED-DOWNLOADS.md",
"sha256": "b66782966acf7162d6de0528a8979d3f4829c4e7285c6cb001c97e22e6cd65ef"
}
]
@@ -1,16 +0,0 @@
{
"uploadId" : "8f361db2-db9f-4dc2-a17f-1de7493b38cf",
"collectionId" : 1,
"state" : "COMPLETE",
"completedAt" : "2026-10-31T09:04:17.060380500Z",
"items" : [ {
"itemId" : "20000000-0000-0000-0000-000000000001",
"fileId" : "efcbbb47-8fb3-46c5-a879-36dd21639a6a",
"versionId" : 1,
"attachmentId" : 1,
"sizeBytes" : 3,
"sha256" : "039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81",
"position" : 0,
"lifecycleAtCommit" : "QUARANTINED"
} ]
}
@@ -1,272 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2026 Kingsrook, LLC
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<revision>0.1.0-record-files-SNAPSHOT</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jacoco.version>0.8.15</jacoco.version>
<!-- qbit-email from Maven Central -->
<qbit-email.version>0.1.0-RC.2</qbit-email.version>
</properties>
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.1.1</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-collaboration</artifactId>
<version>0.1.0-record-files-SNAPSHOT</version>
<packaging>jar</packaging>
<name>QBit Collaboration</name>
<description>QQQ QBit: record collaboration: attachments, tags, custom fields, comments with mentions, presence, sharing, favorites, follow, timeline, tasks</description>
<url>https://github.com/QRun-IO/qbit-collaboration</url>
<modelVersion>4.0.0</modelVersion>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>kingsrook</id>
<name>Kingsrook Team</name>
<email>contact@kingsrook.com</email>
<organization>Kingsrook, LLC</organization>
<organizationUrl>https://kingsrook.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/QRun-IO/qbit-collaboration.git</connection>
<developerConnection>scm:git:ssh://github.com:QRun-IO/qbit-collaboration.git</developerConnection>
<url>https://github.com/QRun-IO/qbit-collaboration</url>
<tag>HEAD</tag>
</scm>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
</dependency>
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-object-storage</artifactId>
<version>0.1.0-RC.4</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
</dependency>
<!-- optional: email through qbit-email when it is produced in the host instance (EmailCollaborationNotifier) -->
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-email</artifactId>
<version>${qbit-email.version}</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mysql</artifactId>
<version>2.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<!-- help bundles carry the artifact version (QQQ help bundle provenance) -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>help/*.yaml</include>
<include>qbit-collaboration.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>help/*.yaml</exclude>
<exclude>qbit-collaboration.properties</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>3.3.1</version>
<executions><execution><id>package-license-notices</id><phase>process-resources</phase><goals><goal>copy-resources</goal></goals>
<configuration><outputDirectory>${project.build.outputDirectory}/META-INF</outputDirectory><resources><resource>
<directory>${project.basedir}</directory><filtering>false</filtering><includes><include>LICENSE</include><include>NOTICE</include></includes>
</resource></resources></configuration>
</execution></executions>
</plugin>
<!-- the artifact version, for the help bundle version test -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables combine.children="append">
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.3.1</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.12.6</version>
</dependency>
</dependencies>
<configuration>
<configLocation>checkstyle/config.xml</configLocation>
<headerLocation>checkstyle/license.txt</headerLocation>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>false</failsOnError>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
<includeTestSourceDirectory>false</includeTestSourceDirectory>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
<limit>
<counter>CLASS</counter>
<value>COVEREDRATIO</value>
<minimum>0.90</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
<!-- imports the released 4.1 BOM, this profile is active by default and -->
<!-- imports qqq-bom-pom 4.1.0-RC.20 ahead of the parent's 4.0.0 BOM. -->
<profile>
<id>qqq-snapshot</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.20</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.snapshot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
</profiles>
</project>
@@ -1,26 +0,0 @@
{
"footerHTML" : null,
"dropdownLabelList" : null,
"dropdownNeedsSelectedText" : null,
"blockType" : "COMPOSITE",
"dropdownNameList" : null,
"subBlocks" : [ {
"blockId" : "recordFilesCapture",
"blockType" : "RECORD_FILES_CAPTURE",
"values" : {
"parent" : {
"recordId" : "1",
"tableName" : "document"
},
"instructions" : "Capture or choose a photo for the first fixture document.",
"title" : "Document photos"
}
} ],
"label" : null,
"type" : "composite",
"sublabel" : null,
"dropdownDefaultValueList" : null,
"csvData" : null,
"hasPermission" : null,
"dropdownDataList" : null
}
@@ -1,858 +0,0 @@
[
{
"path": "maven/com/kingsrook/qbits/qbit-mobile-scanners/0.1.0-SNAPSHOT/qbit-mobile-scanners-0.1.0-SNAPSHOT.jar",
"sha256": "4eb8482bb6220cf0c1d609e0e43ff554ec023671eb51ee89b120be96bcafc85a"
},
{
"path": "maven/com/kingsrook/qqq/qqq-backend-core/4.1.0-isolated-record-files-SNAPSHOT/qqq-backend-core-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "2d3ff9af8b63f46653fcb2eafbab0c34a86f7c83b93c045d1a59e96ce2baf63f"
},
{
"path": "maven/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar",
"sha256": "69e5c9fa35da7a51a5fd2099dfe56a2d8d32cf233e2f6d770e796146440263f4"
},
{
"path": "maven/commons-validator/commons-validator/1.10.1/commons-validator-1.10.1.jar",
"sha256": "ed74711a78d793ca37f6075e4a1e93d107233dfd20d0bb17e32bad88d38e56aa"
},
{
"path": "maven/commons-beanutils/commons-beanutils/1.11.0/commons-beanutils-1.11.0.jar",
"sha256": "9e44ba68ec9a3f21286fa2a8bbb003b735c0f69101bb43144b79f4f8aaa74709"
},
{
"path": "maven/commons-digester/commons-digester/2.1/commons-digester-2.1.jar",
"sha256": "e0b2b980a84fc6533c5ce291f1917b32c507f62bcad64198fff44368c2196a3d"
},
{
"path": "maven/commons-logging/commons-logging/1.3.5/commons-logging-1.3.5.jar",
"sha256": "6d7a744e4027649fbb50895df9497d109f98c766a637062fe8d2eabbb3140ba4"
},
{
"path": "maven/commons-collections/commons-collections/3.2.2/commons-collections-3.2.2.jar",
"sha256": "eeeae917917144a68a741d4c0dff66aa5c5c5fd85593ff217bced3fc8ca783b8"
},
{
"path": "maven/software/amazon/awssdk/quicksight/2.41.10/quicksight-2.41.10.jar",
"sha256": "1a517a32c9ab6ef6b0e8d8ac3c76fff440e1bffd7fca38805a3e6363eeb775aa"
},
{
"path": "maven/software/amazon/awssdk/protocol-core/2.41.10/protocol-core-2.41.10.jar",
"sha256": "cec41447843ae209a9c99595a8b04c7a10ff435a98db6a2b5a80c5b94176dea9"
},
{
"path": "maven/software/amazon/awssdk/aws-json-protocol/2.41.10/aws-json-protocol-2.41.10.jar",
"sha256": "6708f94226081f8d47e327faaae6221d51a74ab8a0b51fa9795349160129eaac"
},
{
"path": "maven/software/amazon/awssdk/third-party-jackson-core/2.41.10/third-party-jackson-core-2.41.10.jar",
"sha256": "ddb4bf13df3b4a0d9720a9a4c099f78e1b4712739c1903a24d25292dd4b88ca3"
},
{
"path": "maven/software/amazon/awssdk/http-auth-aws/2.41.10/http-auth-aws-2.41.10.jar",
"sha256": "ffe1c16d5096bafca087419c5706d6b72a97f53cb23370c2ed2839bd0b127796"
},
{
"path": "maven/software/amazon/awssdk/checksums-spi/2.41.10/checksums-spi-2.41.10.jar",
"sha256": "d848c307f290bfb4e685e2246c01257e9f1a77b5acf116deee999ea9cd8c5d56"
},
{
"path": "maven/software/amazon/awssdk/checksums/2.41.10/checksums-2.41.10.jar",
"sha256": "98fcb1f850894757f7706bb05b241b8fb42eb17b51dc6c74fd5087bbcad50986"
},
{
"path": "maven/software/amazon/awssdk/sdk-core/2.41.10/sdk-core-2.41.10.jar",
"sha256": "af431c018f6c7e10f323f492fd3a918743ee55ee2f7e3a523fa8cb8a35d19f49"
},
{
"path": "maven/software/amazon/awssdk/profiles/2.41.10/profiles-2.41.10.jar",
"sha256": "79419cb6a7fa8c5d53ab00d35a9afa7b37260d044a74bb7b011564551dd18383"
},
{
"path": "maven/software/amazon/awssdk/retries/2.41.10/retries-2.41.10.jar",
"sha256": "105b2d03a4f0ec2f650dcaa5e762bb8605c958f02099821587a368ed0ca4dfa1"
},
{
"path": "maven/org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar",
"sha256": "f75ca597789b3dac58f61857b9ac2e1034a68fa672db35055a8fb4509e325f28"
},
{
"path": "maven/software/amazon/awssdk/auth/2.41.10/auth-2.41.10.jar",
"sha256": "56a429c8f191b14b81b88fb848fd09416be8acb8cfe1c86f2a67a352cbac6968"
},
{
"path": "maven/software/amazon/awssdk/http-auth-aws-eventstream/2.41.10/http-auth-aws-eventstream-2.41.10.jar",
"sha256": "1629ed794b43ba81c8fae845f1502ffec8d5b4601ad6796d3b8fd5fff2f53485"
},
{
"path": "maven/software/amazon/eventstream/eventstream/1.0.1/eventstream-1.0.1.jar",
"sha256": "0c37d8e696117f02c302191b8110b0d0eb20fa412fce34c3a269ec73c16ce822"
},
{
"path": "maven/software/amazon/awssdk/http-auth-spi/2.41.10/http-auth-spi-2.41.10.jar",
"sha256": "974dbd758f140edf209f13b30a2eee72605b4d5852133b99ca61f94dacff2b40"
},
{
"path": "maven/software/amazon/awssdk/http-auth/2.41.10/http-auth-2.41.10.jar",
"sha256": "97f53fb7266058b5a154f7ae95b03ff71f3b6d440d02c20da4ce32961e26190c"
},
{
"path": "maven/software/amazon/awssdk/identity-spi/2.41.10/identity-spi-2.41.10.jar",
"sha256": "c880fe296412c1ae5d5ad9475718cf286d3ccbacbc1af6ccc942f1ce6dff17b9"
},
{
"path": "maven/software/amazon/awssdk/http-client-spi/2.41.10/http-client-spi-2.41.10.jar",
"sha256": "a626e831aa0f2a408dcc563684c8612139883ac5deaa474f4fbd6248dd4d9736"
},
{
"path": "maven/software/amazon/awssdk/regions/2.41.10/regions-2.41.10.jar",
"sha256": "4e149ce6e8a0b69cad14e301009a009ef0f0416c7cfa5f6e2e45f48fd98648ed"
},
{
"path": "maven/software/amazon/awssdk/annotations/2.41.10/annotations-2.41.10.jar",
"sha256": "9c05ad9a9bb7107ea55c2949092015cca23bd281cfb09ab88542c547a335a197"
},
{
"path": "maven/software/amazon/awssdk/utils/2.41.10/utils-2.41.10.jar",
"sha256": "c5c9f2b855ccc739db5703fdce6c0ebab32eae67a8cd0477baea665f5e0861f0"
},
{
"path": "maven/software/amazon/awssdk/aws-core/2.41.10/aws-core-2.41.10.jar",
"sha256": "f790329d989d42f75c9bc72d1b25d3b13153415983140ed3f7f75da6261ed10a"
},
{
"path": "maven/software/amazon/awssdk/utils-lite/2.41.10/utils-lite-2.41.10.jar",
"sha256": "14b6dbe56b3cfccc93e5481e5ede898cf7302e254384dc20f7695168b3c73882"
},
{
"path": "maven/software/amazon/awssdk/metrics-spi/2.41.10/metrics-spi-2.41.10.jar",
"sha256": "f7ab5df3db8f9c9404548be3542f64b258057fee57db7024a087c64ea2029a2d"
},
{
"path": "maven/software/amazon/awssdk/json-utils/2.41.10/json-utils-2.41.10.jar",
"sha256": "258b586fbbbeed74dc7e7ca005f38be20bb2942f0d92f2c8f813b25fb479b1a9"
},
{
"path": "maven/software/amazon/awssdk/endpoints-spi/2.41.10/endpoints-spi-2.41.10.jar",
"sha256": "d201f1dc994b4947f6aa457373a1e624fe1f6e4d3383220db4ce93dbd6d67748"
},
{
"path": "maven/software/amazon/awssdk/retries-spi/2.41.10/retries-spi-2.41.10.jar",
"sha256": "0d686efa47f3be5b8f7a9ea120f3894b6becb2e82efd430c34d579264e74f9b5"
},
{
"path": "maven/software/amazon/awssdk/apache-client/2.41.10/apache-client-2.41.10.jar",
"sha256": "f854982b2079a1909ec2722c9eaea48431b1e503436134fbda31f800bc1a7618"
},
{
"path": "maven/software/amazon/awssdk/netty-nio-client/2.41.10/netty-nio-client-2.41.10.jar",
"sha256": "88f20731ac4067fb0bf2c923fd33b873faef908c21e86bab1c24602824294fb9"
},
{
"path": "maven/io/netty/netty-codec-http/4.1.130.Final/netty-codec-http-4.1.130.Final.jar",
"sha256": "5b6addc1df7b3397a193bd6544a8bfdb18ecac99fd13bee4ec75b1781a664e5e"
},
{
"path": "maven/io/netty/netty-codec-http2/4.1.130.Final/netty-codec-http2-4.1.130.Final.jar",
"sha256": "f8ffdb550368fd5dee7c7f1393fa49552522f280ed8de96aebf4269cab0dc8f3"
},
{
"path": "maven/io/netty/netty-codec/4.1.130.Final/netty-codec-4.1.130.Final.jar",
"sha256": "52636bc29bd62120b97bbe5d1d21eab9b1cb2bef8efbb54d2221c5f3fa08d8cd"
},
{
"path": "maven/io/netty/netty-transport/4.1.130.Final/netty-transport-4.1.130.Final.jar",
"sha256": "1bf573266d271f856705a9984d25449c56a1d73c02a16af12033ceccfe555dbb"
},
{
"path": "maven/io/netty/netty-common/4.1.130.Final/netty-common-4.1.130.Final.jar",
"sha256": "53921f28dd5a352b1bed0e1cbcc54d013dc60ffebeae9b2b1e53eabef317e581"
},
{
"path": "maven/io/netty/netty-buffer/4.1.130.Final/netty-buffer-4.1.130.Final.jar",
"sha256": "00a522b67ea35cb7b4dd9cf27f85c6c58f5e306785aa045302e5f6b2d4944a87"
},
{
"path": "maven/io/netty/netty-handler/4.1.130.Final/netty-handler-4.1.130.Final.jar",
"sha256": "98c78ec187ca30a4b9775bf6f632f5c9929db6bf06a60e6971f945813880ca0f"
},
{
"path": "maven/io/netty/netty-transport-native-unix-common/4.1.130.Final/netty-transport-native-unix-common-4.1.130.Final.jar",
"sha256": "cf5efc4168597d7cd14695b469418cac2a1134533f9a0c82ef0538d796fd39e1"
},
{
"path": "maven/io/netty/netty-transport-classes-epoll/4.1.130.Final/netty-transport-classes-epoll-4.1.130.Final.jar",
"sha256": "1c352508d70aca1ccc3068daac907ad1e9d8dc68b07148d57426730bf0d0c8b6"
},
{
"path": "maven/io/netty/netty-resolver/4.1.130.Final/netty-resolver-4.1.130.Final.jar",
"sha256": "48c5b218a89d184e1b601d46433957f515fcefdb4464182b1348bce4f5a18f35"
},
{
"path": "maven/software/amazon/awssdk/apigateway/2.41.10/apigateway-2.41.10.jar",
"sha256": "13f12255a17e89dbef63da12672a1e991e884df3200e1e063865c1b5d3f76973"
},
{
"path": "maven/com/amazonaws/aws-java-sdk-secretsmanager/1.12.797/aws-java-sdk-secretsmanager-1.12.797.jar",
"sha256": "a7f9f597610144ecf57693abda47a451c9c25088e9e4496a3605a4d283560e4f"
},
{
"path": "maven/com/amazonaws/aws-java-sdk-core/1.12.797/aws-java-sdk-core-1.12.797.jar",
"sha256": "75c70d4a7a5b6f3ee3f1fc86b1c7f91106acba4769744791fa4159c4c35bfebc"
},
{
"path": "maven/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.17.2/jackson-dataformat-cbor-2.17.2.jar",
"sha256": "1d7dc634ba3d7981d7017f4f44d43cf371e922c80028b4eb3e5374ce6546d5f6"
},
{
"path": "maven/joda-time/joda-time/2.12.7/joda-time-2.12.7.jar",
"sha256": "385282b005818cfaccdbe8bd2429811e7e641782f2b88932a6b8ff51d668f616"
},
{
"path": "maven/com/amazonaws/jmespath-java/1.12.797/jmespath-java-1.12.797.jar",
"sha256": "4c4a5f0dcf19f4f6d3a966d19ad73638e325d35db26183e5ca4c27eeb97e5d0f"
},
{
"path": "maven/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar",
"sha256": "c8bc7e1c51a6d4ce72f40d2ebbabf1c4b68bfe76e732104b04381b493478e9d6"
},
{
"path": "maven/commons-codec/commons-codec/1.11/commons-codec-1.11.jar",
"sha256": "e599d5318e97aa48f42136a2927e6dfa4e8881dff0e6c8e3109ddbbff51d7b7d"
},
{
"path": "maven/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar",
"sha256": "6c9b3dd142a09dc468e23ad39aad6f75a0f2b85125104469f026e52a474e464f"
},
{
"path": "maven/com/google/crypto/tink/tink/1.23.0/tink-1.23.0.jar",
"sha256": "5bcdc8798ce106f5145acb4a0e993c5e5861337b1e75ca88ceb6ab9da10fa512"
},
{
"path": "maven/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
"sha256": "766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7"
},
{
"path": "maven/com/google/code/gson/gson/2.13.2/gson-2.13.2.jar",
"sha256": "dd0ce1b55a3ed2080cb70f9c655850cda86c206862310009dcb5e5c95265a5e0"
},
{
"path": "maven/com/google/errorprone/error_prone_annotations/2.41.0/error_prone_annotations-2.41.0.jar",
"sha256": "a56e782b5b50811ac204073a355a21d915a2107fce13ec711331ad036f660fcc"
},
{
"path": "maven/com/google/protobuf/protobuf-java/4.33.6/protobuf-java-4.33.6.jar",
"sha256": "c24b2464ecfe9ac1d44e3a8fe281fca09e6ad2ca73dbd689dd7d6d719df987b5"
},
{
"path": "maven/com/twelvemonkeys/imageio/imageio-webp/3.12.0/imageio-webp-3.12.0.jar",
"sha256": "43dc447ccfe34c5c5c6f5d31190270e1f9bf65f60375d697ae2a8ff433468526"
},
{
"path": "maven/com/twelvemonkeys/imageio/imageio-core/3.12.0/imageio-core-3.12.0.jar",
"sha256": "34c6f91bf3b217ed0115bce8283238fbd0264562aa682b4226a3960d0ede9815"
},
{
"path": "maven/com/twelvemonkeys/imageio/imageio-metadata/3.12.0/imageio-metadata-3.12.0.jar",
"sha256": "797d026b67d07e6d5eb00a1ab53a55acaab6f80a71c5ee325572093eb90c0caa"
},
{
"path": "maven/com/twelvemonkeys/common/common-lang/3.12.0/common-lang-3.12.0.jar",
"sha256": "5c65e504b5b64d80683e0fa70a1dd154dd83d4de7da37f21e950f5a9c03bc66d"
},
{
"path": "maven/com/twelvemonkeys/common/common-io/3.12.0/common-io-3.12.0.jar",
"sha256": "bfacfeb02acac47dbfd1fab0002eaeca65547d04249498b654320c9e0fdf6647"
},
{
"path": "maven/com/twelvemonkeys/common/common-image/3.12.0/common-image-3.12.0.jar",
"sha256": "62a3821c724fadfda346c1e2d83b0c506a0c4074be269a52bf4e3192c7dd92bc"
},
{
"path": "maven/com/ibm/icu/icu4j/78.2/icu4j-78.2.jar",
"sha256": "70627c3ff4b9077f6c3dc2156fb480821f9c1a041b645373f2a0391aae3552a1"
},
{
"path": "maven/com/fasterxml/jackson/core/jackson-databind/2.21.7/jackson-databind-2.21.7.jar",
"sha256": "1290c2795e93e8a6861a6c4d9ff0d844d32f5ea178362cb2721edf5561e828b1"
},
{
"path": "maven/com/fasterxml/jackson/core/jackson-annotations/2.21/jackson-annotations-2.21.jar",
"sha256": "53ca085f4a150f703f49e1aabd935bd03b43e1ea3d55d135438292af22cef56b"
},
{
"path": "maven/com/fasterxml/jackson/core/jackson-core/2.21.7/jackson-core-2.21.7.jar",
"sha256": "8c5623b98f32d5f7e1287ff61ed50e08ab7b11381e991fff89327faabba42261"
},
{
"path": "maven/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.21.7/jackson-datatype-jsr310-2.21.7.jar",
"sha256": "5989b937f0aa5b6407e7a061077c9bfcac0590dc0a32e54b2fac6a3e2d49e812"
},
{
"path": "maven/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.21.7/jackson-dataformat-yaml-2.21.7.jar",
"sha256": "df2b3e6a194181dc067b7211d750926b4ae9e4f3c892a0005b8219a9b59be681"
},
{
"path": "maven/org/yaml/snakeyaml/2.5/snakeyaml-2.5.jar",
"sha256": "e6682acf1ace77508ef13649cbf4f8d09d2cf5457bdb61d25ffb6ac0233d78dd"
},
{
"path": "maven/org/json/json/20251224/json-20251224.jar",
"sha256": "2a3438fbf9293174ea82bdfa35e83d8b965cfe6bddc034a1ac5d60107174c209"
},
{
"path": "maven/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar",
"sha256": "32be0e1e76673092f5d12cb790bd2acb6c2ab04c4ea6efc69ea5ee17911c24fe"
},
{
"path": "maven/com/github/hervian/safety-mirror/4.0.1/safety-mirror-4.0.1.jar",
"sha256": "6b2caaef2637a24905c0e53d6c7cb046df8e1ed7053827617044900facd461c4"
},
{
"path": "maven/org/jetbrains/annotations/20.1.0/annotations-20.1.0.jar",
"sha256": "edc8e3ec9796a5f41c1ae44b2d318507ee6ac1212f121d93d33699b3d0aff638"
},
{
"path": "maven/org/dhatim/fastexcel/0.19.0/fastexcel-0.19.0.jar",
"sha256": "2aab9961e107a96c58ef806d6c09ec3dc054e30c5db5164deabaf611be067722"
},
{
"path": "maven/com/github/rzymek/opczip/1.2.0/opczip-1.2.0.jar",
"sha256": "253a4e1bca79cbf242cc52efb7c931121bc28c3393e2aef24f1efe03cd6d9c28"
},
{
"path": "maven/org/dhatim/fastexcel-reader/0.19.0/fastexcel-reader-0.19.0.jar",
"sha256": "ec9ee7b2413f9ae9db1525c334140798725a4d54b58e2bc73bd447a1c282f9e8"
},
{
"path": "maven/com/fasterxml/aalto-xml/1.3.3/aalto-xml-1.3.3.jar",
"sha256": "28829eebb36863b058108fa1b9b8b5cfb94c1871346e4a4f73e69e1fe0a5f0f6"
},
{
"path": "maven/org/codehaus/woodstox/stax2-api/4.2.2/stax2-api-4.2.2.jar",
"sha256": "a61c48d553efad78bc01fffc4ac528bebbae64cbaec170b2a5e39cf61eb51abe"
},
{
"path": "maven/org/apache/commons/commons-compress/1.27.1/commons-compress-1.27.1.jar",
"sha256": "293d80f54b536b74095dcd7ea3cf0a29bbfc3402519281332495f4420d370d16"
},
{
"path": "maven/org/apache/poi/poi/5.5.1/poi-5.5.1.jar",
"sha256": "6c52e876ca75775a11b56e4b36a7541f682827f56406725fcd87560b792ee3d8"
},
{
"path": "maven/org/apache/commons/commons-collections4/4.5.0/commons-collections4-4.5.0.jar",
"sha256": "00f93263c267be201b8ae521b44a7137271b16688435340bf629db1bac0a5845"
},
{
"path": "maven/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1.jar",
"sha256": "1e56d7b058d28b65abd256b8458e3885b674c1d588fa43cd7d1cbb9c7ef2b308"
},
{
"path": "maven/com/zaxxer/SparseBitSet/1.3/SparseBitSet-1.3.jar",
"sha256": "f76b85adb0c00721ae267b7cfde4da7f71d3121cc2160c9fc00c0c89f8c53c8a"
},
{
"path": "maven/org/apache/poi/poi-ooxml/5.5.1/poi-ooxml-5.5.1.jar",
"sha256": "bd7be2fdfe3fd2c1684fa813351c2798fd636b44f6236e0674500d4f8ff1c2f9"
},
{
"path": "maven/org/apache/poi/poi-ooxml-lite/5.5.1/poi-ooxml-lite-5.5.1.jar",
"sha256": "e6e37adeb6d6ee8b40ec491ad955d934d8f99827ab050f105b18286e59b1d9e7"
},
{
"path": "maven/org/apache/xmlbeans/xmlbeans/5.3.0/xmlbeans-5.3.0.jar",
"sha256": "6cc69da3b4d35b83c5e477cd4daba204e44109833e34af2b9a8a2c8788289917"
},
{
"path": "maven/com/github/virtuald/curvesapi/1.08/curvesapi-1.08.jar",
"sha256": "ad95b08b8bbf9d7d17e5e00814898fa23324f32bc5b62f1a37801e6a56ce0079"
},
{
"path": "maven/org/commonmark/commonmark/0.27.1/commonmark-0.27.1.jar",
"sha256": "22aec6eec3b8e2d078dd1ac205291bf0ec58168c68fcf95fdf39fe2d48650a8f"
},
{
"path": "maven/commons-io/commons-io/2.21.0/commons-io-2.21.0.jar",
"sha256": "7d643a2afea8b058b762aa6fb90e5b256f6c729739f8b3784c3370ddc609e88d"
},
{
"path": "maven/com/nimbusds/oauth2-oidc-sdk/11.31.1/oauth2-oidc-sdk-11.31.1.jar",
"sha256": "8af45d9f00d3d30070f5ac1b0a21db15dd0986e3ea5fa1c72c6bf50ddfd20c7a"
},
{
"path": "maven/com/github/stephenc/jcip/jcip-annotations/1.0-1/jcip-annotations-1.0-1.jar",
"sha256": "4fccff8382aafc589962c4edb262f6aa595e34f1e11e61057d1c6a96e8fc7323"
},
{
"path": "maven/com/nimbusds/content-type/2.3/content-type-2.3.jar",
"sha256": "60349793e006fba96b532cb0c21e10e969fe0db8d87f91c3b9eaf82ba2998895"
},
{
"path": "maven/net/minidev/json-smart/2.5.2/json-smart-2.5.2.jar",
"sha256": "4fbdedb0105cedc7f766b95c297d2e88fb6a560da48f3bbaa0cc538ea8b7bf71"
},
{
"path": "maven/net/minidev/accessors-smart/2.5.2/accessors-smart-2.5.2.jar",
"sha256": "9b8a7bc43861d6156c021166d941fb7dddbe4463e2fa5ee88077e4b01452a836"
},
{
"path": "maven/org/ow2/asm/asm/9.7.1/asm-9.7.1.jar",
"sha256": "8cadd43ac5eb6d09de05faecca38b917a040bb9139c7edeb4cc81c740b713281"
},
{
"path": "maven/com/nimbusds/lang-tag/1.7/lang-tag-1.7.jar",
"sha256": "e8c1c594e2425bdbea2d860de55c69b69fc5d59454452449a0f0913c2a5b8a31"
},
{
"path": "maven/com/nimbusds/nimbus-jose-jwt/10.6/nimbus-jose-jwt-10.6.jar",
"sha256": "7ef3b0dd4a27407639d469cd75e7770463b4ee36e2c13535465da175d3b8f964"
},
{
"path": "maven/com/auth0/auth0/3.0.0/auth0-3.0.0.jar",
"sha256": "145defc90d013fbf518a69e9de90423da8053dbba39261ab1548a924c5d160af"
},
{
"path": "maven/com/squareup/okhttp3/okhttp/5.2.1/okhttp-5.2.1.jar",
"sha256": "e720a383fdc3eb1df4aac77a085e61b7730837364151de867093fdbcafcb44aa"
},
{
"path": "maven/com/squareup/okio/okio/3.16.1/okio-3.16.1.jar",
"sha256": "c9964f394c4cd32b79bf338afbfa2f2297221eee08311787061cea55f87dd60e"
},
{
"path": "maven/com/squareup/okio/okio-jvm/3.16.1/okio-jvm-3.16.1.jar",
"sha256": "31f48e6463ec587d6d262d042c91da00659c983b6ad20d5982bf31e85222693c"
},
{
"path": "maven/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.18.2/jackson-datatype-jdk8-2.18.2.jar",
"sha256": "f30d77f5826b9e9813342e84ab412095ed4ed5cf4fef6f93cebb848cb0fd0294"
},
{
"path": "maven/com/squareup/okhttp3/logging-interceptor/5.2.1/logging-interceptor-5.2.1.jar",
"sha256": "94dee77ec5dc48f6db0e7b3d7b086fb152a01f32d434bda6b670711927e386d7"
},
{
"path": "maven/com/squareup/okhttp3/okhttp-jvm/5.2.1/okhttp-jvm-5.2.1.jar",
"sha256": "9632c08567dbb21c569b13d793107834c8580d44e4eea74b2eae0722f0506179"
},
{
"path": "maven/net/jodah/failsafe/2.4.4/failsafe-2.4.4.jar",
"sha256": "ac3f916e536db9a69f93706f778e90e2be476bdebea0c45cca6d9675ee99ec30"
},
{
"path": "maven/com/auth0/java-jwt/4.5.0/java-jwt-4.5.0.jar",
"sha256": "defc50d5066630d46737ba3077e1e13df7201baf5817394c9a4d3d23e2ad3019"
},
{
"path": "maven/com/auth0/jwks-rsa/0.23.0/jwks-rsa-0.23.0.jar",
"sha256": "d20c5222c0eab0b801b20f4e390568af7771412aef2224e2c32d8cbb58a360bc"
},
{
"path": "maven/com/google/guava/guava/32.1.2-jre/guava-32.1.2-jre.jar",
"sha256": "bc65dea7cfd9e4dacf8419d8af0e741655857d27885bb35d943d7187fc3a8fce"
},
{
"path": "maven/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar",
"sha256": "a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26"
},
{
"path": "maven/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar",
"sha256": "b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99"
},
{
"path": "maven/org/checkerframework/checker-qual/3.33.0/checker-qual-3.33.0.jar",
"sha256": "e316255bbfcd9fe50d165314b85abb2b33cb2a66a93c491db648e498a82c2de1"
},
{
"path": "maven/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar",
"sha256": "f02a95fa1a5e95edb3ed859fd0fb7df709d121a35290eff8b74dce2ab7f4d6ed"
},
{
"path": "maven/io/github/cdimascio/dotenv-java/3.2.0/dotenv-java-3.2.0.jar",
"sha256": "47f5d208b29497f286a454817c955750cc92b99b4680392dd40e5db592259d22"
},
{
"path": "maven/org/apache/velocity/velocity-engine-core/2.4.1/velocity-engine-core-2.4.1.jar",
"sha256": "1c19157d1171d560088e485be97c93a7a2f7e9f56e517f0a30273c5c39df6231"
},
{
"path": "maven/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar",
"sha256": "d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0"
},
{
"path": "maven/org/jsoup/jsoup/1.23.2/jsoup-1.23.2.jar",
"sha256": "e9d8c856856680427f096d156f02a414289985c930998fcb519ce5be39b42003"
},
{
"path": "maven/com/openhtmltopdf/openhtmltopdf-core/1.0.10/openhtmltopdf-core-1.0.10.jar",
"sha256": "3e6fd2250d833d500b7cd48b7a896700d0c33bd9f77a219e820493b01566eda3"
},
{
"path": "maven/com/openhtmltopdf/openhtmltopdf-pdfbox/1.0.10/openhtmltopdf-pdfbox-1.0.10.jar",
"sha256": "7de90df1b3ecf84e6f0daf808d724c11142007a2f22bff1936479bf17251d31a"
},
{
"path": "maven/org/apache/pdfbox/pdfbox/2.0.24/pdfbox-2.0.24.jar",
"sha256": "3c2c0553ec0e7533c490b4c952e1af113621de5275af6e380e11d0d9a0a4f3d6"
},
{
"path": "maven/org/apache/pdfbox/fontbox/2.0.24/fontbox-2.0.24.jar",
"sha256": "2e8c0a569a90b04734fbc0c805d77f4ec03f98c11f5705055ccd7718c1953d68"
},
{
"path": "maven/org/apache/pdfbox/xmpbox/2.0.24/xmpbox-2.0.24.jar",
"sha256": "27383df2285b9e228c39f2c755adeebbef774793d33c4f20f0dc99e9ebaaf673"
},
{
"path": "maven/de/rototor/pdfbox/graphics2d/0.32/graphics2d-0.32.jar",
"sha256": "37f8f387395f96c214ac44f7475c7a2e1f832dfc1de289a3610e0ffbf728f679"
},
{
"path": "maven/com/google/api-client/google-api-client/1.35.2/google-api-client-1.35.2.jar",
"sha256": "f195cd6228d3f99fa7e30ff2dee60ad0f2c7923be31399a7dcdc1abd679aa22e"
},
{
"path": "maven/com/google/oauth-client/google-oauth-client/1.34.1/google-oauth-client-1.34.1.jar",
"sha256": "193edf97aefa28b93c5892bdc598bac34fa4c396588030084f290b1440e8b98a"
},
{
"path": "maven/com/google/http-client/google-http-client-gson/1.42.0/google-http-client-gson-1.42.0.jar",
"sha256": "cb852272c1cb0c8449d8b1a70f3e0f2c1efb2063e543183faa43078fb446f540"
},
{
"path": "maven/com/google/http-client/google-http-client-apache-v2/1.42.0/google-http-client-apache-v2-1.42.0.jar",
"sha256": "1fc4964236b67cf3c5651d7ac1dff668f73b7810c7f1dc0862a0e5bc01608785"
},
{
"path": "maven/com/google/http-client/google-http-client/1.42.0/google-http-client-1.42.0.jar",
"sha256": "82ca0e08171846d1768d5ac3f13244d6fe5a54102c14735ef40bf15d57d478e5"
},
{
"path": "maven/io/opencensus/opencensus-api/0.31.1/opencensus-api-0.31.1.jar",
"sha256": "f1474d47f4b6b001558ad27b952e35eda5cc7146788877fc52938c6eba24b382"
},
{
"path": "maven/io/grpc/grpc-context/1.27.2/grpc-context-1.27.2.jar",
"sha256": "bcbf9055dff453fd6508bd7cca2a0aa2d5f059a9c94beed1f5fda1dc015607b8"
},
{
"path": "maven/io/opencensus/opencensus-contrib-http-util/0.31.1/opencensus-contrib-http-util-0.31.1.jar",
"sha256": "3ea995b55a4068be22989b70cc29a4d788c2d328d1d50613a7a9afd13fdd2d0a"
},
{
"path": "maven/com/google/auth/google-auth-library-oauth2-http/1.41.0/google-auth-library-oauth2-http-1.41.0.jar",
"sha256": "a8f534242b5a54ff3c6a5d1f53678df9d2de71468bed0d74323b7ba05e672113"
},
{
"path": "maven/com/google/auto/value/auto-value-annotations/1.11.0/auto-value-annotations-1.11.0.jar",
"sha256": "5a055ce4255333b3346e1a8703da5bf8ff049532286fdcd31712d624abe111dd"
},
{
"path": "maven/com/google/auth/google-auth-library-credentials/1.41.0/google-auth-library-credentials-1.41.0.jar",
"sha256": "26162b1b9c448b119cfc6edf43346274a15a28ed8ff7a9b4c908f44071e33dad"
},
{
"path": "maven/com/google/api/api-common/2.53.0/api-common-2.53.0.jar",
"sha256": "f7073fb389c2aa329981ee65241db7947e8744c988e6c4d975f18bf2748b935a"
},
{
"path": "maven/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar",
"sha256": "e04ba5195bcd555dc95650f7cc614d151e4bcd52d29a10b8aa2197f3ab89ab9b"
},
{
"path": "maven/com/google/apis/google-api-services-drive/v3-rev20251114-2.0.0/google-api-services-drive-v3-rev20251114-2.0.0.jar",
"sha256": "9345ab816ecf13f06820976bc6262aca4274c2acb012ffccf38c5336717b0cdb"
},
{
"path": "maven/com/amazonaws/aws-java-sdk-sqs/1.12.797/aws-java-sdk-sqs-1.12.797.jar",
"sha256": "4403fe5b4c45f92276b7ec4bdf681cdb4afb068951f5d1ba06f70b0ad8223f3e"
},
{
"path": "maven/com/amazonaws/aws-java-sdk-ses/1.12.797/aws-java-sdk-ses-1.12.797.jar",
"sha256": "7c64e825bb95b3cb304e158c02383265a51374667dcf7ec9405e97f2b9e47db4"
},
{
"path": "maven/org/quartz-scheduler/quartz/2.5.2/quartz-2.5.2.jar",
"sha256": "452e418739c0da1bff255f7c1343dd3a92e1fdd3ceb126b185939edae9922091"
},
{
"path": "maven/jakarta/xml/bind/jakarta.xml.bind-api/4.0.4/jakarta.xml.bind-api-4.0.4.jar",
"sha256": "c507ca69a8c6dd11bf4afeec9e0d412c4fa3933fffb0a84680ea5727e8472124"
},
{
"path": "maven/com/mchange/mchange-commons-java/0.6.0/mchange-commons-java-0.6.0.jar",
"sha256": "ef4cc2e5c15df799af5e96a27ceded23d07bd9b15a538069886dd2cd3e3a1210"
},
{
"path": "maven/org/apache/logging/log4j/log4j-slf4j-impl/2.25.5/log4j-slf4j-impl-2.25.5.jar",
"sha256": "3d25b926bc6025c47ec010459749c12aa66b9d5537ba1922d67d89a098a66607"
},
{
"path": "maven/com/sun/mail/jakarta.mail/2.0.2/jakarta.mail-2.0.2.jar",
"sha256": "536f0ae5dd774e257f3064f19a2b982aa56072c07c675c70f98cdc262047b4b5"
},
{
"path": "maven/com/sun/activation/jakarta.activation/2.0.1/jakarta.activation-2.0.1.jar",
"sha256": "b9e24b7dd6e07495562ea96531be3130c96dba4d78e1dfd88adbbdebf4332871"
},
{
"path": "maven/org/eclipse/angus/angus-activation/2.0.3/angus-activation-2.0.3.jar",
"sha256": "a6bd35c538cf90fff941ad6258c40c08fca0b5c9c3f536c657114f27ce0527a7"
},
{
"path": "maven/jakarta/activation/jakarta.activation-api/2.1.4/jakarta.activation-api-2.1.4.jar",
"sha256": "c9db52100ce6c8aac95cc39075f95720d2e561b11f8051b81c121ad4effd7004"
},
{
"path": "maven/org/apache/logging/log4j/log4j-api/2.25.5/log4j-api-2.25.5.jar",
"sha256": "64777f73ea0b3104c04eb82befbdccc30a425a19e83ad06cb2f93aa303511863"
},
{
"path": "maven/org/apache/logging/log4j/log4j-core/2.25.5/log4j-core-2.25.5.jar",
"sha256": "050c4f85deb48b055e9c041ac39043fbb79ce477841a11a1eb2969c86c6a0072"
},
{
"path": "maven/com/kingsrook/qqq/qqq-middleware-javalin/4.1.0-RC.20/qqq-middleware-javalin-4.1.0-RC.20.jar",
"sha256": "ef33a9c88e29269d6bc8efbbc0a1411b0409893baee49a1f06473e3803c4443c"
},
{
"path": "maven/com/kingsrook/qqq/qqq-openapi/4.1.0-RC.20/qqq-openapi-4.1.0-RC.20.jar",
"sha256": "bbf2257dfce5cedb670dd04ce378bcf177ac157288ff3a28f95a96f3c50de09d"
},
{
"path": "maven/info/picocli/picocli/4.7.7/picocli-4.7.7.jar",
"sha256": "f86e30fffd10d2b13b8caa8d4b237a7ee61f2ffccf5b1941de718b765d235bf8"
},
{
"path": "maven/io/javalin/javalin/7.2.3/javalin-7.2.3.jar",
"sha256": "c27b5052e8fb259acd8fd1d9f598c8543e43fe4df9271c7697a9c832c9a66e17"
},
{
"path": "maven/org/eclipse/jetty/jetty-server/12.1.12/jetty-server-12.1.12.jar",
"sha256": "9ba04bb8d011444a10873a51b1cb8297c04f70db55c8a8d9efcc3a9b97896787"
},
{
"path": "maven/org/eclipse/jetty/jetty-http/12.1.12/jetty-http-12.1.12.jar",
"sha256": "5476e16ef7e28883dba3669ee06491021e4a9aeba9a76f901a3f5c2bb5252a3e"
},
{
"path": "maven/org/eclipse/jetty/jetty-util/12.1.12/jetty-util-12.1.12.jar",
"sha256": "f6fbfa61cfcca032ff82ad8ff3b66a14374047f15486521dbc89a6c0df19ccf2"
},
{
"path": "maven/org/eclipse/jetty/jetty-io/12.1.12/jetty-io-12.1.12.jar",
"sha256": "a809a6a534adccfaa08bca94aa54328e3ac858973ccddbda770f7e9e0f5099fc"
},
{
"path": "maven/org/eclipse/jetty/ee10/jetty-ee10-servlet/12.1.12/jetty-ee10-servlet-12.1.12.jar",
"sha256": "021375ea80d84b5f9d46260916fc0450dc4fae5a95ce80f8289e87a0db07de47"
},
{
"path": "maven/jakarta/servlet/jakarta.servlet-api/6.0.0/jakarta.servlet-api-6.0.0.jar",
"sha256": "c034eb1afb158987dbb53a5fea0cadf611c8dae8daadd59c44d9d5ab70129cef"
},
{
"path": "maven/org/eclipse/jetty/jetty-security/12.1.12/jetty-security-12.1.12.jar",
"sha256": "fcc0206c1ac66b632ef29a59ccf76c3f3737a62ef54694b1d111eeb2e6efe804"
},
{
"path": "maven/org/eclipse/jetty/jetty-session/12.1.12/jetty-session-12.1.12.jar",
"sha256": "73aea4cdd91f2027da1a09cd5953b661625017971d53d8e1ff342f8547752b06"
},
{
"path": "maven/org/eclipse/jetty/ee10/websocket/jetty-ee10-websocket-jetty-server/12.1.12/jetty-ee10-websocket-jetty-server-12.1.12.jar",
"sha256": "bce3641addd34a0d9a3ce8080f31f3b7f45b2bbdf3e7d5655ccd5f4d529c7816"
},
{
"path": "maven/org/eclipse/jetty/ee10/jetty-ee10-annotations/12.1.12/jetty-ee10-annotations-12.1.12.jar",
"sha256": "c45ba9ff724845ef5848f86cf164c87824d4be18095216b9cba66a94a72b83b6"
},
{
"path": "maven/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar",
"sha256": "5f65fdaf424eee2b55e1d882ba9bb376be93fb09b37b808be6e22e8851c909fe"
},
{
"path": "maven/org/eclipse/jetty/jetty-annotations/12.1.12/jetty-annotations-12.1.12.jar",
"sha256": "ce51d231435cdd774336abcb44a7817195fd12487ed213327e8d43db172af467"
},
{
"path": "maven/org/eclipse/jetty/ee10/jetty-ee10-plus/12.1.12/jetty-ee10-plus-12.1.12.jar",
"sha256": "80d050384ef31c921e0fbeeded3c70fc8d35a29b7951511a9ccfaeb06b5e29c7"
},
{
"path": "maven/jakarta/enterprise/jakarta.enterprise.cdi-api/4.0.1/jakarta.enterprise.cdi-api-4.0.1.jar",
"sha256": "beaf74c4f2618189309e3f4a09c43effab633dd96aa1f6dc58a6ba7ee0042717"
},
{
"path": "maven/jakarta/enterprise/jakarta.enterprise.lang-model/4.0.1/jakarta.enterprise.lang-model-4.0.1.jar",
"sha256": "53acafe65b6ef0195fa1b8a0ef2650e5aa024c32cb4059c4df372d6b32089cd3"
},
{
"path": "maven/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar",
"sha256": "f7dc98062fccf14126abb751b64fab12c312566e8cbdc8483598bffcea93af7c"
},
{
"path": "maven/jakarta/interceptor/jakarta.interceptor-api/2.1.0/jakarta.interceptor-api-2.1.0.jar",
"sha256": "ef787d3f713fc6ff4f02cd4b0dbed08f93d8af3400c90cbb43fb4b5c0583710b"
},
{
"path": "maven/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar",
"sha256": "50c0a7c760c13ae6c042acf182b28f0047413db95b4636fb8879bcffab5ba875"
},
{
"path": "maven/org/eclipse/jetty/jetty-jndi/12.1.12/jetty-jndi-12.1.12.jar",
"sha256": "a701368f9810433103836f0f2c0c0f6a2ca49f951d25d155e4e507821cb54b43"
},
{
"path": "maven/org/eclipse/jetty/jetty-plus/12.1.12/jetty-plus-12.1.12.jar",
"sha256": "0e29a87abf5b651f77e5c3e7c726f0ecef30a9bc57a5e0aa5aa3f784cfc0a7bd"
},
{
"path": "maven/org/eclipse/jetty/ee10/jetty-ee10-webapp/12.1.12/jetty-ee10-webapp-12.1.12.jar",
"sha256": "84de1014bb79f957f0fc66d3db9b2bc7eec19f01c8ace4e250b4707af0eed0f3"
},
{
"path": "maven/org/eclipse/jetty/jetty-xml/12.1.12/jetty-xml-12.1.12.jar",
"sha256": "1c7b6320e3ad4ccca861ecfd1347f10ac6130ca2c2eb788a6b5dff3ab0c0f532"
},
{
"path": "maven/org/eclipse/jetty/ee/jetty-ee-webapp/12.1.12/jetty-ee-webapp-12.1.12.jar",
"sha256": "f70d9b30fdfd970e8bbc462b9d6b4cb85f850428d8640529fcc1f1912b2941b4"
},
{
"path": "maven/org/ow2/asm/asm-commons/9.10.1/asm-commons-9.10.1.jar",
"sha256": "6d0abefb7cbf972ea16edb37ec14835372505063a45f976ab7ea889ed9497895"
},
{
"path": "maven/org/ow2/asm/asm-tree/9.10.1/asm-tree-9.10.1.jar",
"sha256": "3dfb0d5b6a106cd40b5b250e39935fbf2f927f4477546a5369a3ac609cf0506b"
},
{
"path": "maven/org/eclipse/jetty/ee10/websocket/jetty-ee10-websocket-servlet/12.1.12/jetty-ee10-websocket-servlet-12.1.12.jar",
"sha256": "83a58b4ee3bf6d0284c50304725cf93b694a67b1b1cc139af72500296dbf7359"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-core-server/12.1.12/jetty-websocket-core-server-12.1.12.jar",
"sha256": "d91becae70e7b15e6200f40d02239d8315f8b26c90d4ab38348fcdad41c0ebf5"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-jetty-api/12.1.12/jetty-websocket-jetty-api-12.1.12.jar",
"sha256": "887090638bf758dce9268478a3ccf168929de52dcdecdddb4eaff62263cbd961"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-jetty-common/12.1.12/jetty-websocket-jetty-common-12.1.12.jar",
"sha256": "933d05b566effad97f84b5d986be810a2928685c875d7fb90f301163f24d2718"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-core-common/12.1.12/jetty-websocket-core-common-12.1.12.jar",
"sha256": "a1ef0c79a4b8b9e4ce969e70d8de9fbed4776625863bdbd5eb5f69fb70208399"
},
{
"path": "maven/org/eclipse/jetty/websocket/jetty-websocket-jetty-server/12.1.12/jetty-websocket-jetty-server-12.1.12.jar",
"sha256": "0b189ffab87b02a4da1187dc4022410c5f235b5a1a7ccfeea3cc4846a8e80664"
},
{
"path": "maven/org/jetbrains/kotlin/kotlin-stdlib/2.3.0/kotlin-stdlib-2.3.0.jar",
"sha256": "887587c91713250ad52fe14ad9166d042c33835049890e9437f355ffc5a195b1"
},
{
"path": "maven/org/slf4j/slf4j-simple/2.0.18/slf4j-simple-2.0.18.jar",
"sha256": "8268bd018a5709b07209e0d8ca6221a37584ba1bc12ba985b8335a82c648bdd0"
},
{
"path": "maven/com/kingsrook/qbits/qbit-record-files/0.1.0-SNAPSHOT/qbit-record-files-0.1.0-SNAPSHOT.jar",
"sha256": "a49f1eb3ff27c720fbd09c2af8c63704b3c157c9603da6307b0356e2b939a347"
},
{
"path": "maven/com/kingsrook/qbits/qbit-collaboration/0.1.0-record-files-SNAPSHOT/qbit-collaboration-0.1.0-record-files-SNAPSHOT.jar",
"sha256": "87c6a9aac52a0821edd66dad2924a014d2c70aa929ead011dcf7a89a0f94c854"
},
{
"path": "maven/com/kingsrook/qbits/qbit-object-storage/0.1.0-RC.4/qbit-object-storage-0.1.0-RC.4.jar",
"sha256": "edb931794d0153fe27e65ef34f4bb5ad93dbfcff2bd7d0eb10e2701ffe1910f2"
},
{
"path": "maven/com/kingsrook/qqq/qqq-backend-module-filesystem/4.1.0-RC.6/qqq-backend-module-filesystem-4.1.0-RC.6.jar",
"sha256": "241d3bc3cc1200680ce6b6991f7da425ffefec45028d9a85edd9320862f26f13"
},
{
"path": "maven/software/amazon/awssdk/s3/2.41.34/s3-2.41.34.jar",
"sha256": "d4a3acd700845a93738bee3b0e2089b86e4cb1d5be3c2d4d00deb61edccdd760"
},
{
"path": "maven/software/amazon/awssdk/aws-xml-protocol/2.41.34/aws-xml-protocol-2.41.34.jar",
"sha256": "e9157b4908b2540feda3a9aa8e08e692ec938fe933b972bffe3b201b879bab27"
},
{
"path": "maven/software/amazon/awssdk/aws-query-protocol/2.41.34/aws-query-protocol-2.41.34.jar",
"sha256": "d57caf18f117b42e682a29a177c334110a88fc6ba3b3bdf290a543bf9bf9a4be"
},
{
"path": "maven/software/amazon/awssdk/arns/2.41.34/arns-2.41.34.jar",
"sha256": "0ecf3fd1af7dbc52d931b9f3a145b11b8bc7b07fb31c631a52505647815b6873"
},
{
"path": "maven/software/amazon/awssdk/crt-core/2.41.34/crt-core-2.41.34.jar",
"sha256": "bba14689c6b538b8e383919af8be6529eb6a35f789b7a264fb362a8ca15d9aff"
},
{
"path": "maven/com/kingsrook/qqq/qqq-backend-module-rdbms/4.1.0-isolated-record-files-SNAPSHOT/qqq-backend-module-rdbms-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "e3af4b9367548fb340f3860516b9fea27cfb8920331568b40c1aac8cfaa5dc85"
},
{
"path": "maven/com/mysql/mysql-connector-j/9.5.0/mysql-connector-j-9.5.0.jar",
"sha256": "f2ca3dfaf00d4aa311470db7ea3051962944ba0cb60005a2f75467549c39f425"
},
{
"path": "maven/com/mchange/c3p0/0.14.0/c3p0-0.14.0.jar",
"sha256": "7b700e8fc71d0519dbad7c22916e85eaa38d544802837a928655c70be3b9f37f"
},
{
"path": "maven/com/kingsrook/qbits/qbit-record-files/0.1.0-SNAPSHOT/qbit-record-files-0.1.0-SNAPSHOT-tests.jar",
"sha256": "65d2f53aa112673c87bf2a40671c01b7755b0a99b174a5186080e13e0b217bfe"
},
{
"path": "maven/com/h2database/h2/2.2.224/h2-2.2.224.jar",
"sha256": "b9d8f19358ada82a4f6eb5b174c6cfe320a375b5a9cb5a4fe456d623e6e55497"
},
{
"path": "maven/org/junit/jupiter/junit-jupiter/5.11.4/junit-jupiter-5.11.4.jar",
"sha256": "aa880e4afba87d447357e4c1fc098c5cb1d200cb9403496c00d3b35a5bd0e8db"
},
{
"path": "maven/org/junit/jupiter/junit-jupiter-api/5.11.4/junit-jupiter-api-5.11.4.jar",
"sha256": "ab83ef9e51ac4597d59d26b4b58812129550e2f579a404c8af7d09f5ce5b4293"
},
{
"path": "maven/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar",
"sha256": "48e2df636cab6563ced64dcdff8abb2355627cb236ef0bf37598682ddf742f1b"
},
{
"path": "maven/org/junit/platform/junit-platform-commons/1.11.4/junit-platform-commons-1.11.4.jar",
"sha256": "9edd969b0d0670c54105bc91ae79bd1c6f503e12115faba82073b84c86bbc334"
},
{
"path": "maven/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar",
"sha256": "b509448ac506d607319f182537f0b35d71007582ec741832a1f111e5b5b70b38"
},
{
"path": "maven/org/junit/jupiter/junit-jupiter-params/5.11.4/junit-jupiter-params-5.11.4.jar",
"sha256": "02a6e015de7ce94ac7f256e7fa05b8091dea861fe79a555a7993313d0f6c7d96"
},
{
"path": "maven/org/junit/jupiter/junit-jupiter-engine/5.11.4/junit-jupiter-engine-5.11.4.jar",
"sha256": "cdf8ac59f3fad774ca738ad03890950eeb91833ef0e8908753177edd26f1581c"
},
{
"path": "maven/org/junit/platform/junit-platform-engine/1.11.4/junit-platform-engine-1.11.4.jar",
"sha256": "b1dd998f64f9acadc15966d9cd3d08074662677b3e390f0a38fcbf0bb4c72330"
},
{
"path": "maven/org/assertj/assertj-core/3.27.7/assertj-core-3.27.7.jar",
"sha256": "c4a445426c3c2861666863b842cc4ec7bbb1c4226fefd370b6d2fe83d6c4ff0f"
},
{
"path": "maven/net/bytebuddy/byte-buddy/1.18.3/byte-buddy-1.18.3.jar",
"sha256": "d78396e3c5bce3f2865c9186647481e5589d34cacc632484715b686108d17c66"
}
]
@@ -1,8 +0,0 @@
{
"derivedCollaborationPomSha256": "97e7832d77730c8a5676c263dca9abda677af41573affff3907587505aefdee7",
"duplicateQqqClasses": 0,
"baseBuildMode": "module-only-prior-scanner-binary",
"scannerJarSha256": "4eb8482bb6220cf0c1d609e0e43ff554ec023671eb51ee89b120be96bcafc85a",
"h2Version": "2.2.224",
"classCount": 3158
}
@@ -1,46 +0,0 @@
[
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/MobileCaptureIdentityProvider.java",
"sha256": "f33b9aaf1eb88bf6dd6626da23dd89c6126a06d21ef6e37cad18f5226f13cd48"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/MobileRecordFilesContract.java",
"sha256": "6b308e31d890f11e5c22fe7ed959f7d694fe647e9d385212a347bea3183e5846"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/RecordFilesCaptureWidgetData.java",
"sha256": "9e546c32241e8a0f7f50e678f4c14399c77ab9a0f66668f52f2c36d544b25bd8"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage.java",
"sha256": "10338dc0aab361b6593155a9abea263e372d3d84af15308c6d80e0be3e2928df"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest.java",
"sha256": "a729bd3b8234d641f3329c2a5000bdc0d351a62c87ea37b420a8ddf2f0367156"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/RecordFilesCaptureContractTest.java",
"sha256": "fb6e1284b5a049df8baaa283d6f5ccd89d7c5faeac692d8e350fb1dc1355ad03"
},
{
"path": "pom.xml",
"sha256": "9cb4c6ab8a8dadec5b688ceb5205a0fe8b3d63923527cd45dd1e838abd8a738e"
},
{
"path": "Verify-Isolated.ps1",
"sha256": "18952429de058262dc134ac8e48d4141fe6eaf71f50cd8aab161e9c2c2c9e2a5"
},
{
"path": "IsolatedBuildPaths.ps1",
"sha256": "b11555eb1363222b17a1cdea3570d199b64a27d67a1382256f92b640362906ea"
},
{
"path": "Test-IsolatedBuildPaths.ps1",
"sha256": "59567af660410037002865284106777fb89172342f01704215e432d6f0069e09"
},
{
"path": "Start-ConsumerFixture.ps1",
"sha256": "f5d8d1b137900c32108c90f8df3634cb9bd2a2d1ed816e94c446780119256c9d"
}
]
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest
-------------------------------------------------------------------------------
Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 53.85 s -- in com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.RecordFilesCaptureContractTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 s -- in com.kingsrook.qbits.mobilescanners.RecordFilesCaptureContractTest
@@ -1,8 +0,0 @@
{
"h2Version": "2.2.224",
"duplicateQqqClasses": 0,
"baseBuildMode": "module-only-prior-scanner-binary",
"classCount": 3162,
"derivedCollaborationPomSha256": "32bb2afb6645f525323e500570f71b319cc13d00403c7555d4b3e23d6e0ea766",
"scannerJarSha256": "af1e72f6f79e4f37010710eae1147fd7edc93a309add32e5f54e976a993b6696"
}
@@ -1,5 +0,0 @@
{
"verifiedAtUtc": "2026-10-31T09:47:04.4578667Z",
"runtimeManifestSha256": "B6E1ED3B69CE5E9CB9D54083C4D486E4FC9130EF18FC3C3FCB99BE8FD6C6EBB1",
"compiledManifestSha256": "6A39F656411BA5D385310F00CB0397E1798572833D0E581694885A68290CD640"
}
@@ -1,30 +0,0 @@
[
{
"sha256": "8801988fb5d0b129056ace39b11b2eb286d22e1e1bf9ba7252b25de228fcb464",
"path": "integrations/record-files/src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/MobileCaptureIdentityProvider.java"
},
{
"sha256": "6b308e31d890f11e5c22fe7ed959f7d694fe647e9d385212a347bea3183e5846",
"path": "integrations/record-files/src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/MobileRecordFilesContract.java"
},
{
"sha256": "9e546c32241e8a0f7f50e678f4c14399c77ab9a0f66668f52f2c36d544b25bd8",
"path": "integrations/record-files/src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/RecordFilesCaptureWidgetData.java"
},
{
"sha256": "10338dc0aab361b6593155a9abea263e372d3d84af15308c6d80e0be3e2928df",
"path": "integrations/record-files/src/test/java/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage.java"
},
{
"sha256": "e13e8491ac3b0bb1ed596bd753d526485b47bfe54f84ceb050e933e5c12484f7",
"path": "integrations/record-files/src/test/java/com/kingsrook/qbits/mobilescanners/MobileAuthorizationCommitProbe.java"
},
{
"sha256": "3cdc55a8b1c8072870f57535a47043d790e3025146b9ffbf60bceef8465c3f4a",
"path": "integrations/record-files/src/test/java/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest.java"
},
{
"sha256": "fb6e1284b5a049df8baaa283d6f5ccd89d7c5faeac692d8e350fb1dc1355ad03",
"path": "integrations/record-files/src/test/java/com/kingsrook/qbits/mobilescanners/RecordFilesCaptureContractTest.java"
}
]
@@ -1,2 +0,0 @@
# Preserve the exact manifest bytes referenced by the retained verification marker.
*.json -text !eol diff whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
@@ -1,26 +0,0 @@
# Isolated 0515 consumer qualification
Completed 2026-10-31 12:46:32 UTC, verifier session68943 exit0. Fresh scanner base37
and optional18 tests passed with Checkstyle. The complete runtime has214 jars,
3180 QQQ/qbit classes and zero duplicates. No shared Maven cache or live host changed.
`consumed-bundle-sha256.json` verifies all33 frozen0515 entries;
`consumed-source-sha256.json` verifies its seven guarded-source snapshots.
`runtime-sha256.json` and `compiled-sha256.json` retain the actual absolute paths
and bytes used in this run. Their hashes match `verification-success.json`;
the local attributes preserve JSON bytes across Git checkout.
`source-sha256.json` records the module/script inputs used for the explicit run.
After root accepted the result, only the verifier's two default arguments changed
to select the same0515 bundle/cache automatically. `base-source-sha256.json`
records the fresh base copy, including the normalized heartbeat route.
The parent POM hash is included in the bundle manifest. The exact resolved owner
POM is retained; repository attributes preserve its upstream bytes. Test reports
are summaries only. Raw logs, passwords, private session credentials and database
files are excluded. Receipt and widget data use synthetic fixture records.
This evidence qualifies the single-instance consumer and its test storage/audit.
It does not qualify production providers, Jobs migration rollout, a cluster,
published immutable dependency coordinates or deployment. TC22 synthetic camera
acceptance belongs to the earlier0352 run; this is the new server runtime suite.
@@ -1,178 +0,0 @@
[
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileAdminSelection.java",
"sha256": "b75a07a190f145607833221add118f5b5abd30af9ce18bee9348a8c9d36e1911"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileBrandingSpec.java",
"sha256": "c55315e0bedaeaccb942876015200388058998d65a3f5342fca0625770c526de"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileCurrentDeviceSpec.java",
"sha256": "88b46b516315b86e0feca1b84072b7f92a6176498a2192fd2ccaa6b3c682fcf5"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileDeviceActivity.java",
"sha256": "30f272ba5fc327f38c964ca9cadfa156071fc47b0f27f7535634f549a9dbf1f6"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileDeviceAdminPrepareStep.java",
"sha256": "ed198b98e3a3650c27b831364a458451af2b1ea901a557ff68bce737ea8d6f94"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileDeviceAdminStep.java",
"sha256": "6cd7cc95b39ea3d88df55338c6ac608dfd62f4f5417f2b6ddc75ac06924f4127"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileDeviceAuthorization.java",
"sha256": "fe4867a0bb52e467085683e7dbd06ecdec4c31098ca20f0b99aeedf7f540ae4d"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileDeviceJsonSpec.java",
"sha256": "b022b30aca36c8cf81eecb8487a377946a3806d15030da0ec3322e99e9a00898"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileDeviceOnline.java",
"sha256": "f9f8599ac1e760a9118a9033ddc5ee87f1f7f693ebb26fc6faa0cad5d2e65df2"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileDeviceProfileCustomizer.java",
"sha256": "4d03f0239087dad69ef4777b31c55ff7a28b1dcce4df6e48a79d354e4958d00d"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileDeviceStatusWidget.java",
"sha256": "ef4e34f563f191cb5491c4c53674c50aae43384065c3d33187a57c31f33e13a0"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileEnrollSpec.java",
"sha256": "2381509d6004d484c68d0f99c5e236a3402406552d2057a42e976cf71510204e"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileJsonSpec.java",
"sha256": "835ff77447c4354d8b7bc95806bf719830879f145c52db8744dca7165b33728e"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobilePermissions.java",
"sha256": "8928b22dcf1777d32a8bf6eba246289ad1d81e51afe295a0bf6772d08ca6f385"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobilePrepareStep.java",
"sha256": "ae7f00e81bd4bef6793d94a8fbc379c130767ffec35835a43b4f19962c6cecf4"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileScannersQBitConfig.java",
"sha256": "1af7dfca2ca25dc8b14e06426d3451ba022ebccf45cab50060c85fa2f916a30d"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileScannersQBitProducer.java",
"sha256": "6831c0d1a370af3183741d83dbf52cfd9cd678410666c43259c573095430b689"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileScannersRouteProvider.java",
"sha256": "8cf719aa6fa631b44d804a42a7fd1d6f10284a25f7d3eb092505de4b5d03dfc1"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileScannersSchemas.java",
"sha256": "f3e657dabda29a0f42228e2fd4df017e4cf7ce325f0b61d659a5e2a1dd26577c"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileScannersUiApi.java",
"sha256": "4df72a01346d699720eb2658dfb63daa661f72deed3209606553bfc7af14bf96"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileScannerWidget.java",
"sha256": "717ee52cffc3352ad4f7107a44ed5c1011319ea31b18eae1af694d78ca59d7af"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileScanStep.java",
"sha256": "210c8a60b527936acd7a2b32ac59e8c8de16545c85ae99e36d49cbc1139c341b"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileScreenService.java",
"sha256": "89328a28b67ab1a09ad7c3bf77a68f19038f1df2799934636073babac417b089"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileService.java",
"sha256": "578ff6e515d7826884dccb234302a7b5030514cb01560d8df5887dd97b4588be"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionAdminPrepareStep.java",
"sha256": "09f799d79c8f0eaae1675fbd9b50f5e1434d2ca7e8b0c7a85424b886fd9b6a25"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionAdminStep.java",
"sha256": "0582748768aef2c7dda91bc8b0e1c01497d2d9ccbcacc55f7cfdd70e57ed2af7"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionEndSpec.java",
"sha256": "495220183b66ba15e1efbda8f33614b53b53353e9bd32d96c7c161bb3c569110"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionHeartbeatSpec.java",
"sha256": "785773998656e9c024ba2cc8c59ded1f0ddda9136f674488c8b36a2d9d2a453a"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionMetadata.java",
"sha256": "75a4b1cce043e3ae648fa25cad54e18b9df51b54b933c62f666d6d74534368dd"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionSchemas.java",
"sha256": "20da7d815bbf5971df83974c26801c91ad856cb78ba4580cfa70683fd4b700aa"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionScreenSpec.java",
"sha256": "83e6f22113f23b7302b33745351f4826c4b563eb8f78d9ae9cae397f6af9de5a"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionScreenWidget.java",
"sha256": "048387b1bd22b978d0b7f817946f9b9ee0ed9c05cb2606c49e3bb4677087b288"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileSessionService.java",
"sha256": "a2fa184661e2a32837ad986255e25bd5bafea288ab0ebe0e064645f8123095ee"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/MobileStatusSpec.java",
"sha256": "45bec687a048c35c05eb389a012ae4bb915069e81c7e426e45cf6a9d495f1447"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/VerifiedDeviceSession.java",
"sha256": "010b1940647715eb6a8bfe9c6b23a28d68146dbdfc63a337e64fafddf1b43d76"
},
{
"path": "src/main/resources/db/mobile-scanners-h2.sql",
"sha256": "8ef3fbb331466b36b4e8a1745bd85f16366556535ca347456b4d87b7f00c59d8"
},
{
"path": "src/main/resources/openapi/qqq-v1/mobile-scanners.yaml",
"sha256": "143db29c5ff2de68e028b85fe1c08545f0328146318899f915c3a8512c2b306d"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/MobileBrandingTest.java",
"sha256": "6291c67e809ac553c5697104cc3d2b9c37e4c1481a2d82c0557b6f7a7b3b1bd7"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/MobileScannersApiContractTest.java",
"sha256": "be95dad90cbf8e7a9252c1662bbede8573d9c73143999944e637191c77ed657e"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/MobileScannersTest.java",
"sha256": "7c4a8f0a6280e8b4184e53532c4848c399e5817b6430c4b6ee8e809a3891c1bd"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/MobileScreenServiceTest.java",
"sha256": "da3db234b759a053a49e2f08b9b392ef223245435f232d6d1c5adccb5dfd3541"
},
{
"path": "src/test/resources/openapi/mobile-scanners-1.0.0.yaml",
"sha256": "3ba318904d726390e343eedc314d4247aa97b6533d41712515e1a8c2c7d542b6"
},
{
"path": "src/test/resources/openapi/mobile-scanners-1.1.0.yaml",
"sha256": "143db29c5ff2de68e028b85fe1c08545f0328146318899f915c3a8512c2b306d"
},
{
"path": "pom.xml",
"sha256": "e46f02e2946f4ae3dd685852185db16abb0b87a43561812719dc23d27279ce1b"
}
]
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.guardrails.ExtensionContractGuardrailTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.635 s -- in com.kingsrook.qbits.guardrails.ExtensionContractGuardrailTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.guardrails.QbitDependencyGuardrailTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.342 s -- in com.kingsrook.qbits.guardrails.QbitDependencyGuardrailTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.guardrails.StandInGuardrailTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.126 s -- in com.kingsrook.qbits.guardrails.StandInGuardrailTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.MobileBrandingTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.638 s -- in com.kingsrook.qbits.mobilescanners.MobileBrandingTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.MobileScannersApiContractTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.217 s -- in com.kingsrook.qbits.mobilescanners.MobileScannersApiContractTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.MobileScannersTest
-------------------------------------------------------------------------------
Tests run: 26, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.287 s -- in com.kingsrook.qbits.mobilescanners.MobileScannersTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.MobileScreenServiceTest
-------------------------------------------------------------------------------
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.093 s -- in com.kingsrook.qbits.mobilescanners.MobileScreenServiceTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest
-------------------------------------------------------------------------------
Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 61.25 s -- in com.kingsrook.qbits.mobilescanners.MobileRecordFilesIntegrationTest
@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.kingsrook.qbits.mobilescanners.RecordFilesCaptureContractTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.009 s -- in com.kingsrook.qbits.mobilescanners.RecordFilesCaptureContractTest
@@ -1,74 +0,0 @@
[
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\classes\\com\\kingsrook\\qbits\\mobilescanners\\recordfiles\\MobileCaptureIdentityProvider.class",
"sha256": "6506e89ec283179799bc436b9c6799deb5a38e1a08f216964800614f1737b1c3"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\classes\\com\\kingsrook\\qbits\\mobilescanners\\recordfiles\\MobileRecordFilesContract.class",
"sha256": "b176691c11247551a814ccacc438f3e7a2c3a77c37cfe796a54104ae63ed5764"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\classes\\com\\kingsrook\\qbits\\mobilescanners\\recordfiles\\MobileRecordFilesContract$NonblankString.class",
"sha256": "6136fbcce0c473a011920b7f55d2991e29f8af486c831480886049f118f70853"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\classes\\com\\kingsrook\\qbits\\mobilescanners\\recordfiles\\RecordFilesCaptureWidgetData.class",
"sha256": "243b074a2845b0943f3281aafa0ed57badc5b347a81e98a077540ea49f9e9d8f"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\DiskAttachmentStorage.class",
"sha256": "d4837b9318aaa3ad41d6011a1f756e8c9a4f401ec20ff7f9d88a7fe09f80f8f3"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\DiskAttachmentStorage$1.class",
"sha256": "b72af87ca3af6257a228ab208352529e926521af32241c94434365a5a7026970"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\DiskAttachmentStorage$Staging.class",
"sha256": "b9cc2861ed24958bbc3013f981962041c0706971819f83322da9513138b0142e"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileAuthorizationCommitProbe.class",
"sha256": "b4217282fe808e2b4d4c23f8901307b6ca02c9b47d7a55a823b14df49a1dab38"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileAuthorizationCommitProbe$1.class",
"sha256": "fb2b84e4729ff3ae79e2cb12b2fbe485cceac61cb0dff4c3e75bde9bd024ce67"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileRecordFilesIntegrationTest.class",
"sha256": "53def5b7f47c4b461a50e089d7ca139419fa57a2e8dc89dfb78fb60f54877039"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileRecordFilesIntegrationTest$1.class",
"sha256": "ac8d5cf26f60881b1c346dc12279f9215fe83b5bf6bb3d1cd7786db2322bd14c"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileRecordFilesIntegrationTest$2.class",
"sha256": "bfc7c61c2ecb2336483bc811c1556693db3a7c31c62c7ded8617104f80fa6ab8"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileRecordFilesIntegrationTest$Authentication.class",
"sha256": "d2b09800a35e77171e819d4588fc4d2aea5e79f89871b1da94d83c9334edb41e"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileRecordFilesIntegrationTest$CapturePermissions.class",
"sha256": "139922c0ce45fcbf0eaaa81cab73e648b4ca82c4297e73b280eaebdf179a6bca"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileRecordFilesIntegrationTest$CapturePrepare.class",
"sha256": "7340cb6f5e696a27fbd3243b5af1c0349bf4d392ae1d0a93d35aa2df0deeb486"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileRecordFilesIntegrationTest$CaptureWidget.class",
"sha256": "91d1773385de3f78f4dfa1490b6245c07c823faee52e5ebcf0ed31ddcb8efaad"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\MobileRecordFilesIntegrationTest$ObservedAttachmentLifecycle.class",
"sha256": "16307903cc407d50de9dcd1db0844c331b8b5e3d5e6698d522d56980b6124ed2"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\module\\test-classes\\com\\kingsrook\\qbits\\mobilescanners\\RecordFilesCaptureContractTest.class",
"sha256": "52e60f29889fb662a12930e9d796ca46d59013d176103644f1dcd4254e322777"
}
]
@@ -1,134 +0,0 @@
[
{
"path": "AUTHORIZED-BROWSING.md",
"sha256": "ebc2a4db713b1f47722a6f5e466b956704aca45e6536469fac18f0635a058c02"
},
{
"path": "CONTENT-READER.md",
"sha256": "2897cb46e64b865fd25d0dd0315443bb8eff6152033a031293de5c66a61a00c4"
},
{
"path": "core-0445-compatibility-verification.log",
"sha256": "6c336fdaa5733f154b7002e048f2cc0a9da7c989b3fa316d3137699557e8b861"
},
{
"path": "HTTP-CONSUMER-FIXTURE.md",
"sha256": "0e3d0021f30d07e8ccac6d75d7fb231fe9ccfbffc2e41c2ba9eeff9faaaaf713"
},
{
"path": "INGESTION-CONTRACT.md",
"sha256": "3942c4d1f8ae47b30e4c37f467db33164dfce36c2d5ce0ac074d661ea62f3e14"
},
{
"path": "MYSQL.md",
"sha256": "16b3759e2d3c9609c903e8dca7162156e73e61cca45daff6e4cf531a83a57927"
},
{
"path": "NATIVE-WORKFLOWS.md",
"sha256": "691e5a37f3566c4f7f7c2c0c5b0f212f54fd946f39d95ed5972e0266a4347331"
},
{
"path": "PRINT-OBSERVATIONS.md",
"sha256": "0d927dbe4b0d5f93291726f73192682b38b955a3f39189e24f07fde9353206a4"
},
{
"path": "PRIOR-0352-README.md",
"sha256": "f8fe431b217c1c9527c140a05a17f95bf9077f8efe5e5602d8c11425d65580f0"
},
{
"path": "qbit-collaboration-0.1.0-record-files-SNAPSHOT.jar",
"sha256": "87c6a9aac52a0821edd66dad2924a014d2c70aa929ead011dcf7a89a0f94c854"
},
{
"path": "qbit-collaboration.pom.xml",
"sha256": "3edb8deabe52093cf868d1972a5d70fccf46930e20b9e49451518f40b18566e0"
},
{
"path": "qbit-collaboration.resolved.pom.xml",
"sha256": "32bb2afb6645f525323e500570f71b319cc13d00403c7555d4b3e23d6e0ea766"
},
{
"path": "qbit-record-files-0.1.0-SNAPSHOT-tests.jar",
"sha256": "d19dcbc173eb4b45c808638936fab352b39f7fb636cc25d342982d7d4b648e81"
},
{
"path": "qbit-record-files-0.1.0-SNAPSHOT.jar",
"sha256": "9024fb61805ac33022e624bf283db1a58d839415dda52b669e4e8099c65ebbfd"
},
{
"path": "qbit-record-files.pom.xml",
"sha256": "ee17af6afbdff319d24e7e536813e13a04c8e0f8934c40b2ccb16a8c2392cfe0"
},
{
"path": "qqq-backend-core-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "c7e9da29afe886ada550d0590b10491b8f3b1a621a1b3b79ca3fb5f807dcf894"
},
{
"path": "qqq-backend-core.pom.xml",
"sha256": "86fe5d56a9ce05be5dc1e788448ea0967206cf3ce990c0f8d5bdb477fc4651ee"
},
{
"path": "qqq-backend-module-rdbms-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "562ec0f2653dd156d06b7b7df7268c40ac87a2c8a79c4f2f7811666507338728"
},
{
"path": "qqq-backend-module-rdbms.pom.xml",
"sha256": "68bfdfae61fa579c6f8afdd75c898aef4fc463aa6cc474a4cac391f448c7b2e4"
},
{
"path": "qqq-parent-project-4.1.0-isolated-record-files-SNAPSHOT.pom",
"sha256": "0158658f28d868b2ac5b323c7ede46b7075d8e5f33a2da9a49c34402abe24afe"
},
{
"path": "README.md",
"sha256": "2b1b865bcd5ac5a7461c96e752ad117ecbdbc3a8557122ba872b6a2ffb4669be"
},
{
"path": "source-sha256.json",
"sha256": "e5d1f01fa9e49bc080662436c0dac1d0f6341d11cf7c633bd160cea6d6ebf6e5"
},
{
"path": "source\\src\\main\\java\\com\\kingsrook\\qbits\\recordfiles\\api\\CaptureIdentityProvider.java",
"sha256": "80194faa0a14baa955418ac57f9bbd816a3d41f19ed21d441409452adafd80ff"
},
{
"path": "source\\src\\main\\java\\com\\kingsrook\\qbits\\recordfiles\\ingestion\\UploadFinalizer.java",
"sha256": "569d3971a34af168216bd30e6837e42a8669d3fad84e33ce76260fb204b3e9cf"
},
{
"path": "source\\src\\main\\java\\com\\kingsrook\\qbits\\recordfiles\\ingestion\\UploadMaintenance.java",
"sha256": "25acb4bc8a983bafce3794444300f2e9fb54a25ee6122bcbdcc07be1a2bddf58"
},
{
"path": "source\\src\\main\\java\\com\\kingsrook\\qbits\\recordfiles\\ingestion\\UploadRuntime.java",
"sha256": "efe94032239b3053282fb8b0696e35c46b266b9dc815b68ba33efd0947a69f63"
},
{
"path": "source\\src\\main\\java\\com\\kingsrook\\qbits\\recordfiles\\ingestion\\UploadSessionService.java",
"sha256": "eafd981d8ec0b91d59ffdcea8654db7dbd670101e7b1540bb99885852f27fc4c"
},
{
"path": "source\\src\\test\\java\\com\\kingsrook\\qbits\\recordfiles\\ingestion\\UploadApiHttpTest.java",
"sha256": "df14757c362d25a2135f45884404006b6c08ee335fe9aeaa9180717667c0aa73"
},
{
"path": "source\\src\\test\\java\\com\\kingsrook\\qbits\\recordfiles\\ingestion\\UploadSessionServiceTest.java",
"sha256": "4c3677d7804a1a975792f1b92a9ddaa563f2fb8c44d10887b5f8dc5e54952e3e"
},
{
"path": "TRACKED-DOWNLOADS.md",
"sha256": "b66782966acf7162d6de0528a8979d3f4829c4e7285c6cb001c97e22e6cd65ef"
},
{
"path": "transactional-jobs\\h2-additive.sql",
"sha256": "969bf71792a32527459c1c08c5f9c1a61523f3da545ce6d50ff661d35e9c1a6e"
},
{
"path": "transactional-jobs\\mysql-additive.sql",
"sha256": "1b3369856ed39708c0e976ea3bda1bae9910da3d5d65341e09ecef3a6d656e10"
},
{
"path": "transactional-jobs\\README.md",
"sha256": "f2d0bd1fe50817269535b5941e9cbb0883602aa8f68c1c790932519693238569"
}
]
@@ -1,30 +0,0 @@
[
{
"path": "src/main/java/com/kingsrook/qbits/recordfiles/api/CaptureIdentityProvider.java",
"sha256": "80194faa0a14baa955418ac57f9bbd816a3d41f19ed21d441409452adafd80ff"
},
{
"path": "src/main/java/com/kingsrook/qbits/recordfiles/ingestion/UploadSessionService.java",
"sha256": "eafd981d8ec0b91d59ffdcea8654db7dbd670101e7b1540bb99885852f27fc4c"
},
{
"path": "src/main/java/com/kingsrook/qbits/recordfiles/ingestion/UploadFinalizer.java",
"sha256": "569d3971a34af168216bd30e6837e42a8669d3fad84e33ce76260fb204b3e9cf"
},
{
"path": "src/main/java/com/kingsrook/qbits/recordfiles/ingestion/UploadMaintenance.java",
"sha256": "25acb4bc8a983bafce3794444300f2e9fb54a25ee6122bcbdcc07be1a2bddf58"
},
{
"path": "src/main/java/com/kingsrook/qbits/recordfiles/ingestion/UploadRuntime.java",
"sha256": "efe94032239b3053282fb8b0696e35c46b266b9dc815b68ba33efd0947a69f63"
},
{
"path": "src/test/java/com/kingsrook/qbits/recordfiles/ingestion/UploadSessionServiceTest.java",
"sha256": "4c3677d7804a1a975792f1b92a9ddaa563f2fb8c44d10887b5f8dc5e54952e3e"
},
{
"path": "src/test/java/com/kingsrook/qbits/recordfiles/ingestion/UploadApiHttpTest.java",
"sha256": "df14757c362d25a2135f45884404006b6c08ee335fe9aeaa9180717667c0aa73"
}
]
@@ -1,16 +0,0 @@
{
"uploadId" : "e1177ad3-dbec-4241-92db-9a05c93aeeed",
"collectionId" : 1,
"state" : "COMPLETE",
"completedAt" : "2026-10-31T12:46:26.541210800Z",
"items" : [ {
"itemId" : "20000000-0000-0000-0000-000000000001",
"fileId" : "28dfe196-ec9f-4e6d-b1ec-dc5c1d93e7fe",
"versionId" : 1,
"attachmentId" : 1,
"sizeBytes" : 3,
"sha256" : "039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81",
"position" : 0,
"lifecycleAtCommit" : "QUARANTINED"
} ]
}
@@ -1,271 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2026 Kingsrook, LLC
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<revision>0.1.0-RC.5</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jacoco.version>0.8.15</jacoco.version>
<!-- qbit-email from Maven Central -->
<qbit-email.version>0.1.0-RC.2</qbit-email.version>
</properties>
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.1.1</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-collaboration</artifactId>
<version>0.1.0-record-files-SNAPSHOT</version>
<packaging>jar</packaging>
<name>QBit Collaboration</name>
<description>QQQ QBit: record collaboration: attachments, tags, custom fields, comments with mentions, presence, sharing, favorites, follow, timeline, tasks</description>
<url>https://github.com/QRun-IO/qbit-collaboration</url>
<modelVersion>4.0.0</modelVersion>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>kingsrook</id>
<name>Kingsrook Team</name>
<email>contact@kingsrook.com</email>
<organization>Kingsrook, LLC</organization>
<organizationUrl>https://kingsrook.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/QRun-IO/qbit-collaboration.git</connection>
<developerConnection>scm:git:ssh://github.com:QRun-IO/qbit-collaboration.git</developerConnection>
<url>https://github.com/QRun-IO/qbit-collaboration</url>
<tag>HEAD</tag>
</scm>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
</dependency>
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-object-storage</artifactId>
<version>0.1.0-RC.4</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
</dependency>
<!-- optional: email through qbit-email when it is produced in the host instance (EmailCollaborationNotifier) -->
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-email</artifactId>
<version>${qbit-email.version}</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mysql</artifactId>
<version>2.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<!-- help bundles carry the artifact version (QQQ help bundle provenance) -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>help/*.yaml</include>
<include>qbit-collaboration.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>help/*.yaml</exclude>
<exclude>qbit-collaboration.properties</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>3.3.1</version>
<executions><execution><id>package-license-notices</id><phase>process-resources</phase><goals><goal>copy-resources</goal></goals>
<configuration><outputDirectory>${project.build.outputDirectory}/META-INF</outputDirectory><resources><resource>
<directory>${project.basedir}</directory><filtering>false</filtering><includes><include>LICENSE</include><include>NOTICE</include></includes>
</resource></resources></configuration>
</execution></executions>
</plugin>
<!-- the artifact version, for the help bundle version test -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables combine.children="append">
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.3.1</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.12.6</version>
</dependency>
</dependencies>
<configuration>
<configLocation>checkstyle/config.xml</configLocation>
<headerLocation>checkstyle/license.txt</headerLocation>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>false</failsOnError>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
<includeTestSourceDirectory>false</includeTestSourceDirectory>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
<limit>
<counter>CLASS</counter>
<value>COVEREDRATIO</value>
<minimum>0.90</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
<!-- imports the released 4.1 BOM, this profile is active by default and -->
<!-- imports qqq-bom-pom 4.1.0-RC.20 ahead of the parent's 4.0.0 BOM. -->
<profile>
<id>qqq-snapshot</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.20</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.snapshot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
</profiles>
</project>
@@ -1,26 +0,0 @@
{
"footerHTML" : null,
"dropdownLabelList" : null,
"dropdownNeedsSelectedText" : null,
"blockType" : "COMPOSITE",
"dropdownNameList" : null,
"subBlocks" : [ {
"blockId" : "recordFilesCapture",
"values" : {
"parent" : {
"recordId" : "1",
"tableName" : "document"
},
"instructions" : "Capture or choose a photo for the first fixture document.",
"title" : "Document photos"
},
"blockType" : "RECORD_FILES_CAPTURE"
} ],
"label" : null,
"type" : "composite",
"sublabel" : null,
"dropdownDefaultValueList" : null,
"csvData" : null,
"hasPermission" : null,
"dropdownDataList" : null
}
@@ -1,858 +0,0 @@
[
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qbits\\qbit-mobile-scanners\\0.1.0-SNAPSHOT\\qbit-mobile-scanners-0.1.0-SNAPSHOT.jar",
"sha256": "e9504632ec308cbad080afcce7da0fb7037f7d9b153359d1653729cb8e33af17"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qqq\\qqq-backend-core\\4.1.0-isolated-record-files-SNAPSHOT\\qqq-backend-core-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "c7e9da29afe886ada550d0590b10491b8f3b1a621a1b3b79ca3fb5f807dcf894"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\commons\\commons-lang3\\3.20.0\\commons-lang3-3.20.0.jar",
"sha256": "69e5c9fa35da7a51a5fd2099dfe56a2d8d32cf233e2f6d770e796146440263f4"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\commons-validator\\commons-validator\\1.10.1\\commons-validator-1.10.1.jar",
"sha256": "ed74711a78d793ca37f6075e4a1e93d107233dfd20d0bb17e32bad88d38e56aa"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\commons-beanutils\\commons-beanutils\\1.11.0\\commons-beanutils-1.11.0.jar",
"sha256": "9e44ba68ec9a3f21286fa2a8bbb003b735c0f69101bb43144b79f4f8aaa74709"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\commons-digester\\commons-digester\\2.1\\commons-digester-2.1.jar",
"sha256": "e0b2b980a84fc6533c5ce291f1917b32c507f62bcad64198fff44368c2196a3d"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\commons-logging\\commons-logging\\1.3.5\\commons-logging-1.3.5.jar",
"sha256": "6d7a744e4027649fbb50895df9497d109f98c766a637062fe8d2eabbb3140ba4"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\commons-collections\\commons-collections\\3.2.2\\commons-collections-3.2.2.jar",
"sha256": "eeeae917917144a68a741d4c0dff66aa5c5c5fd85593ff217bced3fc8ca783b8"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\quicksight\\2.41.10\\quicksight-2.41.10.jar",
"sha256": "1a517a32c9ab6ef6b0e8d8ac3c76fff440e1bffd7fca38805a3e6363eeb775aa"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\protocol-core\\2.41.10\\protocol-core-2.41.10.jar",
"sha256": "cec41447843ae209a9c99595a8b04c7a10ff435a98db6a2b5a80c5b94176dea9"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\aws-json-protocol\\2.41.10\\aws-json-protocol-2.41.10.jar",
"sha256": "6708f94226081f8d47e327faaae6221d51a74ab8a0b51fa9795349160129eaac"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\third-party-jackson-core\\2.41.10\\third-party-jackson-core-2.41.10.jar",
"sha256": "ddb4bf13df3b4a0d9720a9a4c099f78e1b4712739c1903a24d25292dd4b88ca3"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\http-auth-aws\\2.41.10\\http-auth-aws-2.41.10.jar",
"sha256": "ffe1c16d5096bafca087419c5706d6b72a97f53cb23370c2ed2839bd0b127796"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\checksums-spi\\2.41.10\\checksums-spi-2.41.10.jar",
"sha256": "d848c307f290bfb4e685e2246c01257e9f1a77b5acf116deee999ea9cd8c5d56"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\checksums\\2.41.10\\checksums-2.41.10.jar",
"sha256": "98fcb1f850894757f7706bb05b241b8fb42eb17b51dc6c74fd5087bbcad50986"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\sdk-core\\2.41.10\\sdk-core-2.41.10.jar",
"sha256": "af431c018f6c7e10f323f492fd3a918743ee55ee2f7e3a523fa8cb8a35d19f49"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\profiles\\2.41.10\\profiles-2.41.10.jar",
"sha256": "79419cb6a7fa8c5d53ab00d35a9afa7b37260d044a74bb7b011564551dd18383"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\retries\\2.41.10\\retries-2.41.10.jar",
"sha256": "105b2d03a4f0ec2f650dcaa5e762bb8605c958f02099821587a368ed0ca4dfa1"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\reactivestreams\\reactive-streams\\1.0.4\\reactive-streams-1.0.4.jar",
"sha256": "f75ca597789b3dac58f61857b9ac2e1034a68fa672db35055a8fb4509e325f28"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\auth\\2.41.10\\auth-2.41.10.jar",
"sha256": "56a429c8f191b14b81b88fb848fd09416be8acb8cfe1c86f2a67a352cbac6968"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\http-auth-aws-eventstream\\2.41.10\\http-auth-aws-eventstream-2.41.10.jar",
"sha256": "1629ed794b43ba81c8fae845f1502ffec8d5b4601ad6796d3b8fd5fff2f53485"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\eventstream\\eventstream\\1.0.1\\eventstream-1.0.1.jar",
"sha256": "0c37d8e696117f02c302191b8110b0d0eb20fa412fce34c3a269ec73c16ce822"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\http-auth-spi\\2.41.10\\http-auth-spi-2.41.10.jar",
"sha256": "974dbd758f140edf209f13b30a2eee72605b4d5852133b99ca61f94dacff2b40"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\http-auth\\2.41.10\\http-auth-2.41.10.jar",
"sha256": "97f53fb7266058b5a154f7ae95b03ff71f3b6d440d02c20da4ce32961e26190c"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\identity-spi\\2.41.10\\identity-spi-2.41.10.jar",
"sha256": "c880fe296412c1ae5d5ad9475718cf286d3ccbacbc1af6ccc942f1ce6dff17b9"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\http-client-spi\\2.41.10\\http-client-spi-2.41.10.jar",
"sha256": "a626e831aa0f2a408dcc563684c8612139883ac5deaa474f4fbd6248dd4d9736"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\regions\\2.41.10\\regions-2.41.10.jar",
"sha256": "4e149ce6e8a0b69cad14e301009a009ef0f0416c7cfa5f6e2e45f48fd98648ed"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\annotations\\2.41.10\\annotations-2.41.10.jar",
"sha256": "9c05ad9a9bb7107ea55c2949092015cca23bd281cfb09ab88542c547a335a197"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\utils\\2.41.10\\utils-2.41.10.jar",
"sha256": "c5c9f2b855ccc739db5703fdce6c0ebab32eae67a8cd0477baea665f5e0861f0"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\aws-core\\2.41.10\\aws-core-2.41.10.jar",
"sha256": "f790329d989d42f75c9bc72d1b25d3b13153415983140ed3f7f75da6261ed10a"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\utils-lite\\2.41.10\\utils-lite-2.41.10.jar",
"sha256": "14b6dbe56b3cfccc93e5481e5ede898cf7302e254384dc20f7695168b3c73882"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\metrics-spi\\2.41.10\\metrics-spi-2.41.10.jar",
"sha256": "f7ab5df3db8f9c9404548be3542f64b258057fee57db7024a087c64ea2029a2d"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\json-utils\\2.41.10\\json-utils-2.41.10.jar",
"sha256": "258b586fbbbeed74dc7e7ca005f38be20bb2942f0d92f2c8f813b25fb479b1a9"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\endpoints-spi\\2.41.10\\endpoints-spi-2.41.10.jar",
"sha256": "d201f1dc994b4947f6aa457373a1e624fe1f6e4d3383220db4ce93dbd6d67748"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\retries-spi\\2.41.10\\retries-spi-2.41.10.jar",
"sha256": "0d686efa47f3be5b8f7a9ea120f3894b6becb2e82efd430c34d579264e74f9b5"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\apache-client\\2.41.10\\apache-client-2.41.10.jar",
"sha256": "f854982b2079a1909ec2722c9eaea48431b1e503436134fbda31f800bc1a7618"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\netty-nio-client\\2.41.10\\netty-nio-client-2.41.10.jar",
"sha256": "88f20731ac4067fb0bf2c923fd33b873faef908c21e86bab1c24602824294fb9"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-codec-http\\4.1.130.Final\\netty-codec-http-4.1.130.Final.jar",
"sha256": "5b6addc1df7b3397a193bd6544a8bfdb18ecac99fd13bee4ec75b1781a664e5e"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-codec-http2\\4.1.130.Final\\netty-codec-http2-4.1.130.Final.jar",
"sha256": "f8ffdb550368fd5dee7c7f1393fa49552522f280ed8de96aebf4269cab0dc8f3"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-codec\\4.1.130.Final\\netty-codec-4.1.130.Final.jar",
"sha256": "52636bc29bd62120b97bbe5d1d21eab9b1cb2bef8efbb54d2221c5f3fa08d8cd"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-transport\\4.1.130.Final\\netty-transport-4.1.130.Final.jar",
"sha256": "1bf573266d271f856705a9984d25449c56a1d73c02a16af12033ceccfe555dbb"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-common\\4.1.130.Final\\netty-common-4.1.130.Final.jar",
"sha256": "53921f28dd5a352b1bed0e1cbcc54d013dc60ffebeae9b2b1e53eabef317e581"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-buffer\\4.1.130.Final\\netty-buffer-4.1.130.Final.jar",
"sha256": "00a522b67ea35cb7b4dd9cf27f85c6c58f5e306785aa045302e5f6b2d4944a87"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-handler\\4.1.130.Final\\netty-handler-4.1.130.Final.jar",
"sha256": "98c78ec187ca30a4b9775bf6f632f5c9929db6bf06a60e6971f945813880ca0f"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-transport-native-unix-common\\4.1.130.Final\\netty-transport-native-unix-common-4.1.130.Final.jar",
"sha256": "cf5efc4168597d7cd14695b469418cac2a1134533f9a0c82ef0538d796fd39e1"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-transport-classes-epoll\\4.1.130.Final\\netty-transport-classes-epoll-4.1.130.Final.jar",
"sha256": "1c352508d70aca1ccc3068daac907ad1e9d8dc68b07148d57426730bf0d0c8b6"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\netty\\netty-resolver\\4.1.130.Final\\netty-resolver-4.1.130.Final.jar",
"sha256": "48c5b218a89d184e1b601d46433957f515fcefdb4464182b1348bce4f5a18f35"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\apigateway\\2.41.10\\apigateway-2.41.10.jar",
"sha256": "13f12255a17e89dbef63da12672a1e991e884df3200e1e063865c1b5d3f76973"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\amazonaws\\aws-java-sdk-secretsmanager\\1.12.797\\aws-java-sdk-secretsmanager-1.12.797.jar",
"sha256": "a7f9f597610144ecf57693abda47a451c9c25088e9e4496a3605a4d283560e4f"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\amazonaws\\aws-java-sdk-core\\1.12.797\\aws-java-sdk-core-1.12.797.jar",
"sha256": "75c70d4a7a5b6f3ee3f1fc86b1c7f91106acba4769744791fa4159c4c35bfebc"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\fasterxml\\jackson\\dataformat\\jackson-dataformat-cbor\\2.17.2\\jackson-dataformat-cbor-2.17.2.jar",
"sha256": "1d7dc634ba3d7981d7017f4f44d43cf371e922c80028b4eb3e5374ce6546d5f6"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\joda-time\\joda-time\\2.12.7\\joda-time-2.12.7.jar",
"sha256": "385282b005818cfaccdbe8bd2429811e7e641782f2b88932a6b8ff51d668f616"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\amazonaws\\jmespath-java\\1.12.797\\jmespath-java-1.12.797.jar",
"sha256": "4c4a5f0dcf19f4f6d3a966d19ad73638e325d35db26183e5ca4c27eeb97e5d0f"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\httpcomponents\\httpclient\\4.5.14\\httpclient-4.5.14.jar",
"sha256": "c8bc7e1c51a6d4ce72f40d2ebbabf1c4b68bfe76e732104b04381b493478e9d6"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\commons-codec\\commons-codec\\1.11\\commons-codec-1.11.jar",
"sha256": "e599d5318e97aa48f42136a2927e6dfa4e8881dff0e6c8e3109ddbbff51d7b7d"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\httpcomponents\\httpcore\\4.4.16\\httpcore-4.4.16.jar",
"sha256": "6c9b3dd142a09dc468e23ad39aad6f75a0f2b85125104469f026e52a474e464f"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\crypto\\tink\\tink\\1.23.0\\tink-1.23.0.jar",
"sha256": "5bcdc8798ce106f5145acb4a0e993c5e5861337b1e75ca88ceb6ab9da10fa512"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\code\\findbugs\\jsr305\\3.0.2\\jsr305-3.0.2.jar",
"sha256": "766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\code\\gson\\gson\\2.13.2\\gson-2.13.2.jar",
"sha256": "dd0ce1b55a3ed2080cb70f9c655850cda86c206862310009dcb5e5c95265a5e0"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\errorprone\\error_prone_annotations\\2.41.0\\error_prone_annotations-2.41.0.jar",
"sha256": "a56e782b5b50811ac204073a355a21d915a2107fce13ec711331ad036f660fcc"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\protobuf\\protobuf-java\\4.33.6\\protobuf-java-4.33.6.jar",
"sha256": "c24b2464ecfe9ac1d44e3a8fe281fca09e6ad2ca73dbd689dd7d6d719df987b5"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\twelvemonkeys\\imageio\\imageio-webp\\3.12.0\\imageio-webp-3.12.0.jar",
"sha256": "43dc447ccfe34c5c5c6f5d31190270e1f9bf65f60375d697ae2a8ff433468526"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\twelvemonkeys\\imageio\\imageio-core\\3.12.0\\imageio-core-3.12.0.jar",
"sha256": "34c6f91bf3b217ed0115bce8283238fbd0264562aa682b4226a3960d0ede9815"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\twelvemonkeys\\imageio\\imageio-metadata\\3.12.0\\imageio-metadata-3.12.0.jar",
"sha256": "797d026b67d07e6d5eb00a1ab53a55acaab6f80a71c5ee325572093eb90c0caa"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\twelvemonkeys\\common\\common-lang\\3.12.0\\common-lang-3.12.0.jar",
"sha256": "5c65e504b5b64d80683e0fa70a1dd154dd83d4de7da37f21e950f5a9c03bc66d"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\twelvemonkeys\\common\\common-io\\3.12.0\\common-io-3.12.0.jar",
"sha256": "bfacfeb02acac47dbfd1fab0002eaeca65547d04249498b654320c9e0fdf6647"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\twelvemonkeys\\common\\common-image\\3.12.0\\common-image-3.12.0.jar",
"sha256": "62a3821c724fadfda346c1e2d83b0c506a0c4074be269a52bf4e3192c7dd92bc"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\ibm\\icu\\icu4j\\78.2\\icu4j-78.2.jar",
"sha256": "70627c3ff4b9077f6c3dc2156fb480821f9c1a041b645373f2a0391aae3552a1"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\fasterxml\\jackson\\core\\jackson-databind\\2.21.7\\jackson-databind-2.21.7.jar",
"sha256": "1290c2795e93e8a6861a6c4d9ff0d844d32f5ea178362cb2721edf5561e828b1"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\fasterxml\\jackson\\core\\jackson-annotations\\2.21\\jackson-annotations-2.21.jar",
"sha256": "53ca085f4a150f703f49e1aabd935bd03b43e1ea3d55d135438292af22cef56b"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\fasterxml\\jackson\\core\\jackson-core\\2.21.7\\jackson-core-2.21.7.jar",
"sha256": "8c5623b98f32d5f7e1287ff61ed50e08ab7b11381e991fff89327faabba42261"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\fasterxml\\jackson\\datatype\\jackson-datatype-jsr310\\2.21.7\\jackson-datatype-jsr310-2.21.7.jar",
"sha256": "5989b937f0aa5b6407e7a061077c9bfcac0590dc0a32e54b2fac6a3e2d49e812"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\fasterxml\\jackson\\dataformat\\jackson-dataformat-yaml\\2.21.7\\jackson-dataformat-yaml-2.21.7.jar",
"sha256": "df2b3e6a194181dc067b7211d750926b4ae9e4f3c892a0005b8219a9b59be681"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\yaml\\snakeyaml\\2.5\\snakeyaml-2.5.jar",
"sha256": "e6682acf1ace77508ef13649cbf4f8d09d2cf5457bdb61d25ffb6ac0233d78dd"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\json\\json\\20251224\\json-20251224.jar",
"sha256": "2a3438fbf9293174ea82bdfa35e83d8b965cfe6bddc034a1ac5d60107174c209"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\commons\\commons-csv\\1.14.1\\commons-csv-1.14.1.jar",
"sha256": "32be0e1e76673092f5d12cb790bd2acb6c2ab04c4ea6efc69ea5ee17911c24fe"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\github\\hervian\\safety-mirror\\4.0.1\\safety-mirror-4.0.1.jar",
"sha256": "6b2caaef2637a24905c0e53d6c7cb046df8e1ed7053827617044900facd461c4"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\jetbrains\\annotations\\20.1.0\\annotations-20.1.0.jar",
"sha256": "edc8e3ec9796a5f41c1ae44b2d318507ee6ac1212f121d93d33699b3d0aff638"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\dhatim\\fastexcel\\0.19.0\\fastexcel-0.19.0.jar",
"sha256": "2aab9961e107a96c58ef806d6c09ec3dc054e30c5db5164deabaf611be067722"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\github\\rzymek\\opczip\\1.2.0\\opczip-1.2.0.jar",
"sha256": "253a4e1bca79cbf242cc52efb7c931121bc28c3393e2aef24f1efe03cd6d9c28"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\dhatim\\fastexcel-reader\\0.19.0\\fastexcel-reader-0.19.0.jar",
"sha256": "ec9ee7b2413f9ae9db1525c334140798725a4d54b58e2bc73bd447a1c282f9e8"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\fasterxml\\aalto-xml\\1.3.3\\aalto-xml-1.3.3.jar",
"sha256": "28829eebb36863b058108fa1b9b8b5cfb94c1871346e4a4f73e69e1fe0a5f0f6"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\codehaus\\woodstox\\stax2-api\\4.2.2\\stax2-api-4.2.2.jar",
"sha256": "a61c48d553efad78bc01fffc4ac528bebbae64cbaec170b2a5e39cf61eb51abe"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\commons\\commons-compress\\1.27.1\\commons-compress-1.27.1.jar",
"sha256": "293d80f54b536b74095dcd7ea3cf0a29bbfc3402519281332495f4420d370d16"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\poi\\poi\\5.5.1\\poi-5.5.1.jar",
"sha256": "6c52e876ca75775a11b56e4b36a7541f682827f56406725fcd87560b792ee3d8"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\commons\\commons-collections4\\4.5.0\\commons-collections4-4.5.0.jar",
"sha256": "00f93263c267be201b8ae521b44a7137271b16688435340bf629db1bac0a5845"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\commons\\commons-math3\\3.6.1\\commons-math3-3.6.1.jar",
"sha256": "1e56d7b058d28b65abd256b8458e3885b674c1d588fa43cd7d1cbb9c7ef2b308"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\zaxxer\\SparseBitSet\\1.3\\SparseBitSet-1.3.jar",
"sha256": "f76b85adb0c00721ae267b7cfde4da7f71d3121cc2160c9fc00c0c89f8c53c8a"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\poi\\poi-ooxml\\5.5.1\\poi-ooxml-5.5.1.jar",
"sha256": "bd7be2fdfe3fd2c1684fa813351c2798fd636b44f6236e0674500d4f8ff1c2f9"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\poi\\poi-ooxml-lite\\5.5.1\\poi-ooxml-lite-5.5.1.jar",
"sha256": "e6e37adeb6d6ee8b40ec491ad955d934d8f99827ab050f105b18286e59b1d9e7"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\xmlbeans\\xmlbeans\\5.3.0\\xmlbeans-5.3.0.jar",
"sha256": "6cc69da3b4d35b83c5e477cd4daba204e44109833e34af2b9a8a2c8788289917"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\github\\virtuald\\curvesapi\\1.08\\curvesapi-1.08.jar",
"sha256": "ad95b08b8bbf9d7d17e5e00814898fa23324f32bc5b62f1a37801e6a56ce0079"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\commonmark\\commonmark\\0.27.1\\commonmark-0.27.1.jar",
"sha256": "22aec6eec3b8e2d078dd1ac205291bf0ec58168c68fcf95fdf39fe2d48650a8f"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\commons-io\\commons-io\\2.21.0\\commons-io-2.21.0.jar",
"sha256": "7d643a2afea8b058b762aa6fb90e5b256f6c729739f8b3784c3370ddc609e88d"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\nimbusds\\oauth2-oidc-sdk\\11.31.1\\oauth2-oidc-sdk-11.31.1.jar",
"sha256": "8af45d9f00d3d30070f5ac1b0a21db15dd0986e3ea5fa1c72c6bf50ddfd20c7a"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\github\\stephenc\\jcip\\jcip-annotations\\1.0-1\\jcip-annotations-1.0-1.jar",
"sha256": "4fccff8382aafc589962c4edb262f6aa595e34f1e11e61057d1c6a96e8fc7323"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\nimbusds\\content-type\\2.3\\content-type-2.3.jar",
"sha256": "60349793e006fba96b532cb0c21e10e969fe0db8d87f91c3b9eaf82ba2998895"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\net\\minidev\\json-smart\\2.5.2\\json-smart-2.5.2.jar",
"sha256": "4fbdedb0105cedc7f766b95c297d2e88fb6a560da48f3bbaa0cc538ea8b7bf71"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\net\\minidev\\accessors-smart\\2.5.2\\accessors-smart-2.5.2.jar",
"sha256": "9b8a7bc43861d6156c021166d941fb7dddbe4463e2fa5ee88077e4b01452a836"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\ow2\\asm\\asm\\9.7.1\\asm-9.7.1.jar",
"sha256": "8cadd43ac5eb6d09de05faecca38b917a040bb9139c7edeb4cc81c740b713281"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\nimbusds\\lang-tag\\1.7\\lang-tag-1.7.jar",
"sha256": "e8c1c594e2425bdbea2d860de55c69b69fc5d59454452449a0f0913c2a5b8a31"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\nimbusds\\nimbus-jose-jwt\\10.6\\nimbus-jose-jwt-10.6.jar",
"sha256": "7ef3b0dd4a27407639d469cd75e7770463b4ee36e2c13535465da175d3b8f964"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\auth0\\auth0\\3.0.0\\auth0-3.0.0.jar",
"sha256": "145defc90d013fbf518a69e9de90423da8053dbba39261ab1548a924c5d160af"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\squareup\\okhttp3\\okhttp\\5.2.1\\okhttp-5.2.1.jar",
"sha256": "e720a383fdc3eb1df4aac77a085e61b7730837364151de867093fdbcafcb44aa"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\squareup\\okio\\okio\\3.16.1\\okio-3.16.1.jar",
"sha256": "c9964f394c4cd32b79bf338afbfa2f2297221eee08311787061cea55f87dd60e"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\squareup\\okio\\okio-jvm\\3.16.1\\okio-jvm-3.16.1.jar",
"sha256": "31f48e6463ec587d6d262d042c91da00659c983b6ad20d5982bf31e85222693c"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\fasterxml\\jackson\\datatype\\jackson-datatype-jdk8\\2.18.2\\jackson-datatype-jdk8-2.18.2.jar",
"sha256": "f30d77f5826b9e9813342e84ab412095ed4ed5cf4fef6f93cebb848cb0fd0294"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\squareup\\okhttp3\\logging-interceptor\\5.2.1\\logging-interceptor-5.2.1.jar",
"sha256": "94dee77ec5dc48f6db0e7b3d7b086fb152a01f32d434bda6b670711927e386d7"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\squareup\\okhttp3\\okhttp-jvm\\5.2.1\\okhttp-jvm-5.2.1.jar",
"sha256": "9632c08567dbb21c569b13d793107834c8580d44e4eea74b2eae0722f0506179"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\net\\jodah\\failsafe\\2.4.4\\failsafe-2.4.4.jar",
"sha256": "ac3f916e536db9a69f93706f778e90e2be476bdebea0c45cca6d9675ee99ec30"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\auth0\\java-jwt\\4.5.0\\java-jwt-4.5.0.jar",
"sha256": "defc50d5066630d46737ba3077e1e13df7201baf5817394c9a4d3d23e2ad3019"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\auth0\\jwks-rsa\\0.23.0\\jwks-rsa-0.23.0.jar",
"sha256": "d20c5222c0eab0b801b20f4e390568af7771412aef2224e2c32d8cbb58a360bc"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\guava\\guava\\32.1.2-jre\\guava-32.1.2-jre.jar",
"sha256": "bc65dea7cfd9e4dacf8419d8af0e741655857d27885bb35d943d7187fc3a8fce"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\guava\\failureaccess\\1.0.1\\failureaccess-1.0.1.jar",
"sha256": "a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\guava\\listenablefuture\\9999.0-empty-to-avoid-conflict-with-guava\\listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar",
"sha256": "b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\checkerframework\\checker-qual\\3.33.0\\checker-qual-3.33.0.jar",
"sha256": "e316255bbfcd9fe50d165314b85abb2b33cb2a66a93c491db648e498a82c2de1"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\j2objc\\j2objc-annotations\\2.8\\j2objc-annotations-2.8.jar",
"sha256": "f02a95fa1a5e95edb3ed859fd0fb7df709d121a35290eff8b74dce2ab7f4d6ed"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\github\\cdimascio\\dotenv-java\\3.2.0\\dotenv-java-3.2.0.jar",
"sha256": "47f5d208b29497f286a454817c955750cc92b99b4680392dd40e5db592259d22"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\velocity\\velocity-engine-core\\2.4.1\\velocity-engine-core-2.4.1.jar",
"sha256": "1c19157d1171d560088e485be97c93a7a2f7e9f56e517f0a30273c5c39df6231"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\slf4j\\slf4j-api\\1.7.36\\slf4j-api-1.7.36.jar",
"sha256": "d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\jsoup\\jsoup\\1.23.2\\jsoup-1.23.2.jar",
"sha256": "e9d8c856856680427f096d156f02a414289985c930998fcb519ce5be39b42003"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\openhtmltopdf\\openhtmltopdf-core\\1.0.10\\openhtmltopdf-core-1.0.10.jar",
"sha256": "3e6fd2250d833d500b7cd48b7a896700d0c33bd9f77a219e820493b01566eda3"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\openhtmltopdf\\openhtmltopdf-pdfbox\\1.0.10\\openhtmltopdf-pdfbox-1.0.10.jar",
"sha256": "7de90df1b3ecf84e6f0daf808d724c11142007a2f22bff1936479bf17251d31a"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\pdfbox\\pdfbox\\2.0.24\\pdfbox-2.0.24.jar",
"sha256": "3c2c0553ec0e7533c490b4c952e1af113621de5275af6e380e11d0d9a0a4f3d6"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\pdfbox\\fontbox\\2.0.24\\fontbox-2.0.24.jar",
"sha256": "2e8c0a569a90b04734fbc0c805d77f4ec03f98c11f5705055ccd7718c1953d68"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\pdfbox\\xmpbox\\2.0.24\\xmpbox-2.0.24.jar",
"sha256": "27383df2285b9e228c39f2c755adeebbef774793d33c4f20f0dc99e9ebaaf673"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\de\\rototor\\pdfbox\\graphics2d\\0.32\\graphics2d-0.32.jar",
"sha256": "37f8f387395f96c214ac44f7475c7a2e1f832dfc1de289a3610e0ffbf728f679"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\api-client\\google-api-client\\1.35.2\\google-api-client-1.35.2.jar",
"sha256": "f195cd6228d3f99fa7e30ff2dee60ad0f2c7923be31399a7dcdc1abd679aa22e"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\oauth-client\\google-oauth-client\\1.34.1\\google-oauth-client-1.34.1.jar",
"sha256": "193edf97aefa28b93c5892bdc598bac34fa4c396588030084f290b1440e8b98a"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\http-client\\google-http-client-gson\\1.42.0\\google-http-client-gson-1.42.0.jar",
"sha256": "cb852272c1cb0c8449d8b1a70f3e0f2c1efb2063e543183faa43078fb446f540"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\http-client\\google-http-client-apache-v2\\1.42.0\\google-http-client-apache-v2-1.42.0.jar",
"sha256": "1fc4964236b67cf3c5651d7ac1dff668f73b7810c7f1dc0862a0e5bc01608785"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\http-client\\google-http-client\\1.42.0\\google-http-client-1.42.0.jar",
"sha256": "82ca0e08171846d1768d5ac3f13244d6fe5a54102c14735ef40bf15d57d478e5"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\opencensus\\opencensus-api\\0.31.1\\opencensus-api-0.31.1.jar",
"sha256": "f1474d47f4b6b001558ad27b952e35eda5cc7146788877fc52938c6eba24b382"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\grpc\\grpc-context\\1.27.2\\grpc-context-1.27.2.jar",
"sha256": "bcbf9055dff453fd6508bd7cca2a0aa2d5f059a9c94beed1f5fda1dc015607b8"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\opencensus\\opencensus-contrib-http-util\\0.31.1\\opencensus-contrib-http-util-0.31.1.jar",
"sha256": "3ea995b55a4068be22989b70cc29a4d788c2d328d1d50613a7a9afd13fdd2d0a"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\auth\\google-auth-library-oauth2-http\\1.41.0\\google-auth-library-oauth2-http-1.41.0.jar",
"sha256": "a8f534242b5a54ff3c6a5d1f53678df9d2de71468bed0d74323b7ba05e672113"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\auto\\value\\auto-value-annotations\\1.11.0\\auto-value-annotations-1.11.0.jar",
"sha256": "5a055ce4255333b3346e1a8703da5bf8ff049532286fdcd31712d624abe111dd"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\auth\\google-auth-library-credentials\\1.41.0\\google-auth-library-credentials-1.41.0.jar",
"sha256": "26162b1b9c448b119cfc6edf43346274a15a28ed8ff7a9b4c908f44071e33dad"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\api\\api-common\\2.53.0\\api-common-2.53.0.jar",
"sha256": "f7073fb389c2aa329981ee65241db7947e8744c988e6c4d975f18bf2748b935a"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\javax\\annotation\\javax.annotation-api\\1.3.2\\javax.annotation-api-1.3.2.jar",
"sha256": "e04ba5195bcd555dc95650f7cc614d151e4bcd52d29a10b8aa2197f3ab89ab9b"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\google\\apis\\google-api-services-drive\\v3-rev20251114-2.0.0\\google-api-services-drive-v3-rev20251114-2.0.0.jar",
"sha256": "9345ab816ecf13f06820976bc6262aca4274c2acb012ffccf38c5336717b0cdb"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\amazonaws\\aws-java-sdk-sqs\\1.12.797\\aws-java-sdk-sqs-1.12.797.jar",
"sha256": "4403fe5b4c45f92276b7ec4bdf681cdb4afb068951f5d1ba06f70b0ad8223f3e"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\amazonaws\\aws-java-sdk-ses\\1.12.797\\aws-java-sdk-ses-1.12.797.jar",
"sha256": "7c64e825bb95b3cb304e158c02383265a51374667dcf7ec9405e97f2b9e47db4"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\quartz-scheduler\\quartz\\2.5.2\\quartz-2.5.2.jar",
"sha256": "452e418739c0da1bff255f7c1343dd3a92e1fdd3ceb126b185939edae9922091"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\xml\\bind\\jakarta.xml.bind-api\\4.0.4\\jakarta.xml.bind-api-4.0.4.jar",
"sha256": "c507ca69a8c6dd11bf4afeec9e0d412c4fa3933fffb0a84680ea5727e8472124"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\mchange\\mchange-commons-java\\0.6.0\\mchange-commons-java-0.6.0.jar",
"sha256": "ef4cc2e5c15df799af5e96a27ceded23d07bd9b15a538069886dd2cd3e3a1210"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\logging\\log4j\\log4j-slf4j-impl\\2.25.5\\log4j-slf4j-impl-2.25.5.jar",
"sha256": "3d25b926bc6025c47ec010459749c12aa66b9d5537ba1922d67d89a098a66607"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\sun\\mail\\jakarta.mail\\2.0.2\\jakarta.mail-2.0.2.jar",
"sha256": "536f0ae5dd774e257f3064f19a2b982aa56072c07c675c70f98cdc262047b4b5"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\sun\\activation\\jakarta.activation\\2.0.1\\jakarta.activation-2.0.1.jar",
"sha256": "b9e24b7dd6e07495562ea96531be3130c96dba4d78e1dfd88adbbdebf4332871"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\angus\\angus-activation\\2.0.3\\angus-activation-2.0.3.jar",
"sha256": "a6bd35c538cf90fff941ad6258c40c08fca0b5c9c3f536c657114f27ce0527a7"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\activation\\jakarta.activation-api\\2.1.4\\jakarta.activation-api-2.1.4.jar",
"sha256": "c9db52100ce6c8aac95cc39075f95720d2e561b11f8051b81c121ad4effd7004"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\logging\\log4j\\log4j-api\\2.25.5\\log4j-api-2.25.5.jar",
"sha256": "64777f73ea0b3104c04eb82befbdccc30a425a19e83ad06cb2f93aa303511863"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apache\\logging\\log4j\\log4j-core\\2.25.5\\log4j-core-2.25.5.jar",
"sha256": "050c4f85deb48b055e9c041ac39043fbb79ce477841a11a1eb2969c86c6a0072"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qqq\\qqq-middleware-javalin\\4.1.0-RC.20\\qqq-middleware-javalin-4.1.0-RC.20.jar",
"sha256": "ef33a9c88e29269d6bc8efbbc0a1411b0409893baee49a1f06473e3803c4443c"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qqq\\qqq-openapi\\4.1.0-RC.20\\qqq-openapi-4.1.0-RC.20.jar",
"sha256": "bbf2257dfce5cedb670dd04ce378bcf177ac157288ff3a28f95a96f3c50de09d"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\info\\picocli\\picocli\\4.7.7\\picocli-4.7.7.jar",
"sha256": "f86e30fffd10d2b13b8caa8d4b237a7ee61f2ffccf5b1941de718b765d235bf8"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\io\\javalin\\javalin\\7.2.3\\javalin-7.2.3.jar",
"sha256": "c27b5052e8fb259acd8fd1d9f598c8543e43fe4df9271c7697a9c832c9a66e17"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-server\\12.1.12\\jetty-server-12.1.12.jar",
"sha256": "9ba04bb8d011444a10873a51b1cb8297c04f70db55c8a8d9efcc3a9b97896787"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-http\\12.1.12\\jetty-http-12.1.12.jar",
"sha256": "5476e16ef7e28883dba3669ee06491021e4a9aeba9a76f901a3f5c2bb5252a3e"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-util\\12.1.12\\jetty-util-12.1.12.jar",
"sha256": "f6fbfa61cfcca032ff82ad8ff3b66a14374047f15486521dbc89a6c0df19ccf2"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-io\\12.1.12\\jetty-io-12.1.12.jar",
"sha256": "a809a6a534adccfaa08bca94aa54328e3ac858973ccddbda770f7e9e0f5099fc"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\ee10\\jetty-ee10-servlet\\12.1.12\\jetty-ee10-servlet-12.1.12.jar",
"sha256": "021375ea80d84b5f9d46260916fc0450dc4fae5a95ce80f8289e87a0db07de47"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\servlet\\jakarta.servlet-api\\6.0.0\\jakarta.servlet-api-6.0.0.jar",
"sha256": "c034eb1afb158987dbb53a5fea0cadf611c8dae8daadd59c44d9d5ab70129cef"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-security\\12.1.12\\jetty-security-12.1.12.jar",
"sha256": "fcc0206c1ac66b632ef29a59ccf76c3f3737a62ef54694b1d111eeb2e6efe804"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-session\\12.1.12\\jetty-session-12.1.12.jar",
"sha256": "73aea4cdd91f2027da1a09cd5953b661625017971d53d8e1ff342f8547752b06"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\ee10\\websocket\\jetty-ee10-websocket-jetty-server\\12.1.12\\jetty-ee10-websocket-jetty-server-12.1.12.jar",
"sha256": "bce3641addd34a0d9a3ce8080f31f3b7f45b2bbdf3e7d5655ccd5f4d529c7816"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\ee10\\jetty-ee10-annotations\\12.1.12\\jetty-ee10-annotations-12.1.12.jar",
"sha256": "c45ba9ff724845ef5848f86cf164c87824d4be18095216b9cba66a94a72b83b6"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\annotation\\jakarta.annotation-api\\2.1.1\\jakarta.annotation-api-2.1.1.jar",
"sha256": "5f65fdaf424eee2b55e1d882ba9bb376be93fb09b37b808be6e22e8851c909fe"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-annotations\\12.1.12\\jetty-annotations-12.1.12.jar",
"sha256": "ce51d231435cdd774336abcb44a7817195fd12487ed213327e8d43db172af467"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\ee10\\jetty-ee10-plus\\12.1.12\\jetty-ee10-plus-12.1.12.jar",
"sha256": "80d050384ef31c921e0fbeeded3c70fc8d35a29b7951511a9ccfaeb06b5e29c7"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\enterprise\\jakarta.enterprise.cdi-api\\4.0.1\\jakarta.enterprise.cdi-api-4.0.1.jar",
"sha256": "beaf74c4f2618189309e3f4a09c43effab633dd96aa1f6dc58a6ba7ee0042717"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\enterprise\\jakarta.enterprise.lang-model\\4.0.1\\jakarta.enterprise.lang-model-4.0.1.jar",
"sha256": "53acafe65b6ef0195fa1b8a0ef2650e5aa024c32cb4059c4df372d6b32089cd3"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\inject\\jakarta.inject-api\\2.0.1\\jakarta.inject-api-2.0.1.jar",
"sha256": "f7dc98062fccf14126abb751b64fab12c312566e8cbdc8483598bffcea93af7c"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\interceptor\\jakarta.interceptor-api\\2.1.0\\jakarta.interceptor-api-2.1.0.jar",
"sha256": "ef787d3f713fc6ff4f02cd4b0dbed08f93d8af3400c90cbb43fb4b5c0583710b"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\jakarta\\transaction\\jakarta.transaction-api\\2.0.1\\jakarta.transaction-api-2.0.1.jar",
"sha256": "50c0a7c760c13ae6c042acf182b28f0047413db95b4636fb8879bcffab5ba875"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-jndi\\12.1.12\\jetty-jndi-12.1.12.jar",
"sha256": "a701368f9810433103836f0f2c0c0f6a2ca49f951d25d155e4e507821cb54b43"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-plus\\12.1.12\\jetty-plus-12.1.12.jar",
"sha256": "0e29a87abf5b651f77e5c3e7c726f0ecef30a9bc57a5e0aa5aa3f784cfc0a7bd"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\ee10\\jetty-ee10-webapp\\12.1.12\\jetty-ee10-webapp-12.1.12.jar",
"sha256": "84de1014bb79f957f0fc66d3db9b2bc7eec19f01c8ace4e250b4707af0eed0f3"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\jetty-xml\\12.1.12\\jetty-xml-12.1.12.jar",
"sha256": "1c7b6320e3ad4ccca861ecfd1347f10ac6130ca2c2eb788a6b5dff3ab0c0f532"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\ee\\jetty-ee-webapp\\12.1.12\\jetty-ee-webapp-12.1.12.jar",
"sha256": "f70d9b30fdfd970e8bbc462b9d6b4cb85f850428d8640529fcc1f1912b2941b4"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\ow2\\asm\\asm-commons\\9.10.1\\asm-commons-9.10.1.jar",
"sha256": "6d0abefb7cbf972ea16edb37ec14835372505063a45f976ab7ea889ed9497895"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\ow2\\asm\\asm-tree\\9.10.1\\asm-tree-9.10.1.jar",
"sha256": "3dfb0d5b6a106cd40b5b250e39935fbf2f927f4477546a5369a3ac609cf0506b"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\ee10\\websocket\\jetty-ee10-websocket-servlet\\12.1.12\\jetty-ee10-websocket-servlet-12.1.12.jar",
"sha256": "83a58b4ee3bf6d0284c50304725cf93b694a67b1b1cc139af72500296dbf7359"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\websocket\\jetty-websocket-core-server\\12.1.12\\jetty-websocket-core-server-12.1.12.jar",
"sha256": "d91becae70e7b15e6200f40d02239d8315f8b26c90d4ab38348fcdad41c0ebf5"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\websocket\\jetty-websocket-jetty-api\\12.1.12\\jetty-websocket-jetty-api-12.1.12.jar",
"sha256": "887090638bf758dce9268478a3ccf168929de52dcdecdddb4eaff62263cbd961"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\websocket\\jetty-websocket-jetty-common\\12.1.12\\jetty-websocket-jetty-common-12.1.12.jar",
"sha256": "933d05b566effad97f84b5d986be810a2928685c875d7fb90f301163f24d2718"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\websocket\\jetty-websocket-core-common\\12.1.12\\jetty-websocket-core-common-12.1.12.jar",
"sha256": "a1ef0c79a4b8b9e4ce969e70d8de9fbed4776625863bdbd5eb5f69fb70208399"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\eclipse\\jetty\\websocket\\jetty-websocket-jetty-server\\12.1.12\\jetty-websocket-jetty-server-12.1.12.jar",
"sha256": "0b189ffab87b02a4da1187dc4022410c5f235b5a1a7ccfeea3cc4846a8e80664"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\jetbrains\\kotlin\\kotlin-stdlib\\2.3.0\\kotlin-stdlib-2.3.0.jar",
"sha256": "887587c91713250ad52fe14ad9166d042c33835049890e9437f355ffc5a195b1"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\slf4j\\slf4j-simple\\2.0.18\\slf4j-simple-2.0.18.jar",
"sha256": "8268bd018a5709b07209e0d8ca6221a37584ba1bc12ba985b8335a82c648bdd0"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qbits\\qbit-record-files\\0.1.0-SNAPSHOT\\qbit-record-files-0.1.0-SNAPSHOT.jar",
"sha256": "9024fb61805ac33022e624bf283db1a58d839415dda52b669e4e8099c65ebbfd"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qbits\\qbit-collaboration\\0.1.0-record-files-SNAPSHOT\\qbit-collaboration-0.1.0-record-files-SNAPSHOT.jar",
"sha256": "87c6a9aac52a0821edd66dad2924a014d2c70aa929ead011dcf7a89a0f94c854"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qbits\\qbit-object-storage\\0.1.0-RC.4\\qbit-object-storage-0.1.0-RC.4.jar",
"sha256": "edb931794d0153fe27e65ef34f4bb5ad93dbfcff2bd7d0eb10e2701ffe1910f2"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qqq\\qqq-backend-module-filesystem\\4.1.0-RC.6\\qqq-backend-module-filesystem-4.1.0-RC.6.jar",
"sha256": "241d3bc3cc1200680ce6b6991f7da425ffefec45028d9a85edd9320862f26f13"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\s3\\2.41.34\\s3-2.41.34.jar",
"sha256": "d4a3acd700845a93738bee3b0e2089b86e4cb1d5be3c2d4d00deb61edccdd760"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\aws-xml-protocol\\2.41.34\\aws-xml-protocol-2.41.34.jar",
"sha256": "e9157b4908b2540feda3a9aa8e08e692ec938fe933b972bffe3b201b879bab27"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\aws-query-protocol\\2.41.34\\aws-query-protocol-2.41.34.jar",
"sha256": "d57caf18f117b42e682a29a177c334110a88fc6ba3b3bdf290a543bf9bf9a4be"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\arns\\2.41.34\\arns-2.41.34.jar",
"sha256": "0ecf3fd1af7dbc52d931b9f3a145b11b8bc7b07fb31c631a52505647815b6873"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\software\\amazon\\awssdk\\crt-core\\2.41.34\\crt-core-2.41.34.jar",
"sha256": "bba14689c6b538b8e383919af8be6529eb6a35f789b7a264fb362a8ca15d9aff"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qqq\\qqq-backend-module-rdbms\\4.1.0-isolated-record-files-SNAPSHOT\\qqq-backend-module-rdbms-4.1.0-isolated-record-files-SNAPSHOT.jar",
"sha256": "562ec0f2653dd156d06b7b7df7268c40ac87a2c8a79c4f2f7811666507338728"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\mysql\\mysql-connector-j\\9.5.0\\mysql-connector-j-9.5.0.jar",
"sha256": "f2ca3dfaf00d4aa311470db7ea3051962944ba0cb60005a2f75467549c39f425"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\mchange\\c3p0\\0.14.0\\c3p0-0.14.0.jar",
"sha256": "7b700e8fc71d0519dbad7c22916e85eaa38d544802837a928655c70be3b9f37f"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\kingsrook\\qbits\\qbit-record-files\\0.1.0-SNAPSHOT\\qbit-record-files-0.1.0-SNAPSHOT-tests.jar",
"sha256": "d19dcbc173eb4b45c808638936fab352b39f7fb636cc25d342982d7d4b648e81"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\com\\h2database\\h2\\2.2.224\\h2-2.2.224.jar",
"sha256": "b9d8f19358ada82a4f6eb5b174c6cfe320a375b5a9cb5a4fe456d623e6e55497"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\junit\\jupiter\\junit-jupiter\\5.11.4\\junit-jupiter-5.11.4.jar",
"sha256": "aa880e4afba87d447357e4c1fc098c5cb1d200cb9403496c00d3b35a5bd0e8db"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\junit\\jupiter\\junit-jupiter-api\\5.11.4\\junit-jupiter-api-5.11.4.jar",
"sha256": "ab83ef9e51ac4597d59d26b4b58812129550e2f579a404c8af7d09f5ce5b4293"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\opentest4j\\opentest4j\\1.3.0\\opentest4j-1.3.0.jar",
"sha256": "48e2df636cab6563ced64dcdff8abb2355627cb236ef0bf37598682ddf742f1b"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\junit\\platform\\junit-platform-commons\\1.11.4\\junit-platform-commons-1.11.4.jar",
"sha256": "9edd969b0d0670c54105bc91ae79bd1c6f503e12115faba82073b84c86bbc334"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\apiguardian\\apiguardian-api\\1.1.2\\apiguardian-api-1.1.2.jar",
"sha256": "b509448ac506d607319f182537f0b35d71007582ec741832a1f111e5b5b70b38"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\junit\\jupiter\\junit-jupiter-params\\5.11.4\\junit-jupiter-params-5.11.4.jar",
"sha256": "02a6e015de7ce94ac7f256e7fa05b8091dea861fe79a555a7993313d0f6c7d96"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\junit\\jupiter\\junit-jupiter-engine\\5.11.4\\junit-jupiter-engine-5.11.4.jar",
"sha256": "cdf8ac59f3fad774ca738ad03890950eeb91833ef0e8908753177edd26f1581c"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\junit\\platform\\junit-platform-engine\\1.11.4\\junit-platform-engine-1.11.4.jar",
"sha256": "b1dd998f64f9acadc15966d9cd3d08074662677b3e390f0a38fcbf0bb4c72330"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\org\\assertj\\assertj-core\\3.27.7\\assertj-core-3.27.7.jar",
"sha256": "c4a445426c3c2861666863b842cc4ec7bbb1c4226fefd370b6d2fe83d6c4ff0f"
},
{
"path": "R:\\Git.Local\\QRunIO\\qbit-mobile-scanners\\integrations\\record-files\\target\\isolated-m2-0515\\net\\bytebuddy\\byte-buddy\\1.18.3\\byte-buddy-1.18.3.jar",
"sha256": "d78396e3c5bce3f2865c9186647481e5589d34cacc632484715b686108d17c66"
}
]
@@ -1,10 +0,0 @@
{
"h2Version": "2.2.224",
"bundleName": "retained-contract-20261031-0515",
"repositoryName": "isolated-m2-0515",
"derivedCollaborationPomSha256": "32bb2afb6645f525323e500570f71b319cc13d00403c7555d4b3e23d6e0ea766",
"duplicateQqqClasses": 0,
"classCount": 3180,
"scannerJarSha256": "e9504632ec308cbad080afcce7da0fb7037f7d9b153359d1653729cb8e33af17",
"baseBuildMode": "fresh-source-clean-base-build"
}
@@ -1,50 +0,0 @@
[
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/MobileCaptureIdentityProvider.java",
"sha256": "8801988fb5d0b129056ace39b11b2eb286d22e1e1bf9ba7252b25de228fcb464"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/MobileRecordFilesContract.java",
"sha256": "6b308e31d890f11e5c22fe7ed959f7d694fe647e9d385212a347bea3183e5846"
},
{
"path": "src/main/java/com/kingsrook/qbits/mobilescanners/recordfiles/RecordFilesCaptureWidgetData.java",
"sha256": "9e546c32241e8a0f7f50e678f4c14399c77ab9a0f66668f52f2c36d544b25bd8"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/DiskAttachmentStorage.java",
"sha256": "10338dc0aab361b6593155a9abea263e372d3d84af15308c6d80e0be3e2928df"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/MobileAuthorizationCommitProbe.java",
"sha256": "e13e8491ac3b0bb1ed596bd753d526485b47bfe54f84ceb050e933e5c12484f7"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/MobileRecordFilesIntegrationTest.java",
"sha256": "3cdc55a8b1c8072870f57535a47043d790e3025146b9ffbf60bceef8465c3f4a"
},
{
"path": "src/test/java/com/kingsrook/qbits/mobilescanners/RecordFilesCaptureContractTest.java",
"sha256": "fb6e1284b5a049df8baaa283d6f5ccd89d7c5faeac692d8e350fb1dc1355ad03"
},
{
"path": "pom.xml",
"sha256": "9cb4c6ab8a8dadec5b688ceb5205a0fe8b3d63923527cd45dd1e838abd8a738e"
},
{
"path": "Verify-Isolated.ps1",
"sha256": "ef8cc22a18f4805919f196fb08e155c4a874c5d6725567fe05bd89fb391f0f7a"
},
{
"path": "IsolatedBuildPaths.ps1",
"sha256": "b11555eb1363222b17a1cdea3570d199b64a27d67a1382256f92b640362906ea"
},
{
"path": "Test-IsolatedBuildPaths.ps1",
"sha256": "59567af660410037002865284106777fb89172342f01704215e432d6f0069e09"
},
{
"path": "Start-ConsumerFixture.ps1",
"sha256": "f5d8d1b137900c32108c90f8df3634cb9bd2a2d1ed816e94c446780119256c9d"
}
]
@@ -1,5 +0,0 @@
{
"verifiedAtUtc": "2026-10-31T12:46:32.5847156Z",
"runtimeManifestSha256": "B93CC12EC3A724AA1F9D0C6C47313615C705F32960AB7C650DEAE5B315FFFC09",
"compiledManifestSha256": "6A524429CD53BAB581BE1D07F6E6BC8BD55D152594C353C6F1C2560665C22676"
}
-37
View File
@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-mobile-scanners-record-files</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>QQQ Mobile Scanners Record Files integration</name>
<description>Optional verified mobile capture attribution and semantic camera widget.</description>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<qqq.frozen.version>4.1.0-isolated-record-files-SNAPSHOT</qqq.frozen.version>
</properties>
<dependencyManagement><dependencies>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-core</artifactId><version>${qqq.frozen.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId><version>${qqq.frozen.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-middleware-javalin</artifactId><version>4.1.0-RC.20</version></dependency>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-collaboration</artifactId><version>0.1.0-record-files-SNAPSHOT</version></dependency>
</dependencies></dependencyManagement>
<dependencies>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-mobile-scanners</artifactId><version>0.1.0-SNAPSHOT</version></dependency>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-record-files</artifactId><version>0.1.0-SNAPSHOT</version></dependency>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-record-files</artifactId><version>0.1.0-SNAPSHOT</version><classifier>tests</classifier><type>test-jar</type><scope>test</scope></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.2.224</version><scope>test</scope></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.11.4</version><scope>test</scope></dependency>
<dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.27.7</version><scope>test</scope></dependency>
</dependencies>
<build><directory>${project.basedir}/target/module</directory><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-checkstyle-plugin</artifactId><version>3.6.0</version>
<dependencies><dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>14.3.0</version></dependency></dependencies>
<configuration><configLocation>../../checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration>
<executions><execution><id>style</id><phase>validate</phase><goals><goal>check</goal></goals></execution></executions>
</plugin>
</plugins></build>
</project>
@@ -1,67 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners.recordfiles;
import java.util.Optional;
import com.kingsrook.qbits.mobilescanners.MobileDeviceAuthorization;
import com.kingsrook.qbits.recordfiles.api.CaptureIdentity;
import com.kingsrook.qbits.recordfiles.api.CaptureIdentityProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
/*******************************************************************************
** Optional integration: both qbits remain independent of one another.
*******************************************************************************/
public final class MobileCaptureIdentityProvider implements CaptureIdentityProvider
{
public static final String PROVIDER_KEY = "qqq-mobile-scanners/v1";
/***************************************************************************
** Attribution comes exclusively from the scanner's verified server context.
***************************************************************************/
@Override
public Optional<CaptureIdentity> capture() throws QException
{
return MobileDeviceAuthorization.captureCurrent().map(identity -> new CaptureIdentity(PROVIDER_KEY,
identity.actorId(), identity.deviceId(), identity.publicSessionId()));
}
/***************************************************************************
** Persisted attribution cannot confer authority or select another sign-in.
***************************************************************************/
@Override
public void revalidate(CaptureIdentity identity) throws QException
{
if(identity == null || !PROVIDER_KEY.equals(identity.providerKey()))
{
throw new QPermissionDeniedException("Unsupported mobile capture identity.");
}
MobileDeviceAuthorization.revalidate(new MobileDeviceAuthorization.Identity(identity.actorId(), identity.deviceId(), identity.publicSessionId()));
}
/***************************************************************************
** Hold verified local authority through Record Files and attachment commits.
***************************************************************************/
@Override
public <T> T executeAuthorized(Optional<CaptureIdentity> expected, Operation<T> operation) throws QException
{
if(expected == null || expected.filter(value -> !PROVIDER_KEY.equals(value.providerKey())).isPresent())
{
throw new QPermissionDeniedException("Unsupported mobile capture identity.");
}
Optional<MobileDeviceAuthorization.Identity> identity = expected.map(value ->
new MobileDeviceAuthorization.Identity(value.actorId(), value.deviceId(), value.publicSessionId()));
return MobileDeviceAuthorization.executeAuthorized(identity, operation::run);
}
}
@@ -1,168 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners.recordfiles;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import com.kingsrook.qbits.mobilescanners.MobileScannersSchemas;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.extensions.AbstractJsonEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtension;
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtensionsMetaData;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.Schema;
import com.kingsrook.qqq.openapi.model.Type;
/*******************************************************************************
** Optional widget vocabulary, independently versioned from both base qbits.
** The current core extension registry has no schema-only registration hook.
*******************************************************************************/
public class MobileRecordFilesContract extends AbstractJsonEndpointSpec
{
public static final String VERSION = "1.0.0";
/***************************************************************************
** Host opt-in advertises the native block and contributes its typed schemas.
***************************************************************************/
public static void register(QInstance instance)
{
MiddlewareSpecExtensionsMetaData.ofOrWithNew(instance).withExtension(new MiddlewareSpecExtension()
.withName("mobile-record-files").withSemanticVersion(VERSION)
.withDescription("Native RECORD_FILES_CAPTURE widget vocabulary; record-files independently authorizes ingestion.")
.withSpec(MobileRecordFilesContract.class));
}
/***************************************************************************
** Native clients verify this static descriptor before enabling capture UI.
***************************************************************************/
@Override
public BasicOperation defineBasicOperation()
{
return new BasicOperation().withHttpMethod(HttpMethod.GET).withPath("/qbit/mobile-record-files/contract")
.withOperationId("mobileRecordFilesContract").withShortSummary("Discover native record photo widget semantics")
.withLongDescription("Public static vocabulary only. No parent reference or upload authority is disclosed. Clients must separately verify record-files capabilities for the widget's server-selected parent.");
}
/***************************************************************************
** Static semantics disclose neither records nor authenticated identity.
***************************************************************************/
@Override
public boolean isSecured()
{
return false;
}
/***************************************************************************
** A versioned, typed descriptor rather than inferred widget/process names.
***************************************************************************/
@Override
protected String getResponseSchemaName()
{
return "MobileRecordFilesContractV1";
}
/***************************************************************************
** These values describe rendering only and cannot authorize an upload.
***************************************************************************/
@Override
protected Object handle(JsonInput input)
{
return Map.of("version", VERSION, "blockType", "RECORD_FILES_CAPTURE", "widgetType", "composite");
}
/***************************************************************************
** Ordinary native widget responses reference these additive block schemas.
***************************************************************************/
@Override
public Map<String, Schema> defineComponentSchemas()
{
Map<String, Schema> schemas = new TreeMap<>();
schemas.put("MobileRecordFilesContractV1", object(Map.of("version", literal(VERSION),
"blockType", literal("RECORD_FILES_CAPTURE"), "widgetType", literal("composite")), "version", "blockType", "widgetType"));
schemas.put("MobileRecordFilesCaptureParentV1", object(Map.of("tableName", new NonblankString().withType(Type.STRING).withMaxLength(255),
"recordId", new NonblankString().withType(Type.STRING).withMaxLength(255).withDescription("Opaque server-selected record ID; never coerce to a number.")), "tableName", "recordId"));
schemas.put("MobileRecordFilesCaptureValuesV1", object(Map.of("parent", ref("MobileRecordFilesCaptureParentV1"),
"title", new Schema().withType(Type.STRING).withMaxLength(200),
"instructions", new Schema().withType(Type.STRING).withMaxLength(2000)), "parent"));
schemas.put("MobileRecordFilesCaptureBlockV1", object(Map.of("blockId", new NonblankString().withType(Type.STRING).withMaxLength(200),
"blockType", literal("RECORD_FILES_CAPTURE"), "values", ref("MobileRecordFilesCaptureValuesV1")), "blockId", "blockType", "values"));
schemas.put("MobileRecordFilesCaptureWidgetV1", object(Map.of("type", literal("composite"), "blockType", literal("COMPOSITE"),
"subBlocks", new Schema().withType(Type.ARRAY).withItems(ref("MobileRecordFilesCaptureBlockV1"))), "type", "blockType", "subBlocks"));
return schemas;
}
/***************************************************************************
** Known fields are typed; additive future fields remain compatible.
***************************************************************************/
private static Schema object(Map<String, Schema> properties, String... required)
{
return new MobileScannersSchemas.RequiredObjectSchema(List.of(required)).withType(Type.OBJECT).withProperties(new TreeMap<>(properties));
}
/***************************************************************************
** Fixed wire discriminator, never a localized label or process name.
***************************************************************************/
private static Schema literal(String value)
{
return new Schema().withType(Type.STRING).withEnumValues(List.of(value));
}
/***************************************************************************
** Reuse typed parent and values components.
***************************************************************************/
private static Schema ref(String name)
{
return new Schema().withRefToSchema(name);
}
/***************************************************************************
** Standard JSON Schema keywords absent from the frozen core Schema model.
***************************************************************************/
public static class NonblankString extends Schema
{
/***********************************************************************
** Require at least one character.
***********************************************************************/
public int getMinLength()
{
return 1;
}
/***********************************************************************
** Require a non-whitespace character without altering opaque IDs.
***********************************************************************/
public String getPattern()
{
return "\\S";
}
}
}
@@ -1,90 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners.recordfiles;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
/*******************************************************************************
** Host renderers supply an authorized record reference, never inferred fields.
** Record Files still reauthorizes that parent independently for every operation.
*******************************************************************************/
public final class RecordFilesCaptureWidgetData extends QWidgetData
{
private final String tableName;
private final String recordId;
private final String title;
private final String instructions;
/***************************************************************************
** Values are display/selection metadata, never proof of authorization.
***************************************************************************/
public RecordFilesCaptureWidgetData(String tableName, String recordId, String title, String instructions)
{
this.tableName = Objects.requireNonNull(tableName);
this.recordId = Objects.requireNonNull(recordId);
if(tableName.isBlank() || recordId.isBlank() || tableName.length() > 255 || recordId.length() > 255
|| tableName.codePoints().anyMatch(Character::isISOControl) || recordId.codePoints().anyMatch(Character::isISOControl))
{
throw new IllegalArgumentException("A record reference is required.");
}
if(title != null && title.length() > 200 || instructions != null && instructions.length() > 2000)
{
throw new IllegalArgumentException("Capture display text exceeds its declared bounds.");
}
this.title = title;
this.instructions = instructions;
}
/***************************************************************************
** Standard composite widget envelope used by native semantic clients.
***************************************************************************/
@Override
public String getType()
{
return "composite";
}
/***************************************************************************
** Standard composite block discriminator.
***************************************************************************/
public String getBlockType()
{
return "COMPOSITE";
}
/***************************************************************************
** Native capture controls require compatible extension and record capability.
***************************************************************************/
public List<Map<String, Serializable>> getSubBlocks()
{
HashMap<String, Serializable> values = new HashMap<>();
values.put("parent", new HashMap<>(Map.of("tableName", tableName, "recordId", recordId)));
if(title != null)
{
values.put("title", title);
}
if(instructions != null)
{
values.put("instructions", instructions);
}
return List.of(Map.of("blockId", "recordFilesCapture", "blockType", "RECORD_FILES_CAPTURE", "values", values));
}
}
@@ -1,201 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.Optional;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kingsrook.qbits.collaboration.attachments.AttachmentStorage;
import com.kingsrook.qbits.objectstorage.api.ObjectHeaders;
import com.kingsrook.qbits.objectstorage.api.ObjectStream;
import com.kingsrook.qbits.objectstorage.api.PutRequest;
import com.kingsrook.qbits.objectstorage.api.StoredObject;
import com.kingsrook.qbits.recordfiles.ingestion.UploadChunkStorage;
import com.kingsrook.qbits.recordfiles.ingestion.UploadManifest;
/*******************************************************************************
** File-backed owner storage fixture. Not a qualified production storage driver.
*******************************************************************************/
public class DiskAttachmentStorage extends AttachmentStorage
{
static Path directory;
private static final ObjectMapper JSON = new ObjectMapper();
/***************************************************************************
** Hash trusted owner keys into isolated files; never resolve a supplied path.
***************************************************************************/
private static Path path(String name, String key)
{
return directory.resolve("staging".equals(name) ? "staging" : "canonical")
.resolve(UploadManifest.hash((name + "/" + key).getBytes(java.nio.charset.StandardCharsets.UTF_8)));
}
/***************************************************************************
** Match the fixture's small immutable limits.
***************************************************************************/
@Override
public long maxBytes(String name)
{
return 20 * 1024 * 1024;
}
/***************************************************************************
** Standard attachment owner calls conditional creation for retained originals.
***************************************************************************/
@Override
public synchronized void put(String name, PutRequest request)
{
try
{
Path file = path(name, request.getKey());
Files.createDirectories(file.getParent());
byte[] bytes = request.getBytes() == null ? request.getStream().readAllBytes() : request.getBytes();
Files.write(file, bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
JSON.writeValue(Path.of(file + ".json").toFile(), request.getMetadata());
}
catch(Exception failure)
{
throw new IllegalStateException("Fixture object write failed.", failure);
}
}
/***************************************************************************
** Reopen actual persisted bytes across fresh storage adapter instances.
***************************************************************************/
@Override
public ObjectStream open(String name, String key)
{
try
{
Path file = path(name, key);
java.util.Map<String, String> metadata = JSON.readValue(Path.of(file + ".json").toFile(), new com.fasterxml.jackson.core.type.TypeReference<>() {});
long size = Files.size(file);
ObjectHeaders headers = new ObjectHeaders().withUserMetadata(metadata);
Instant openedAt = Instant.now();
return new ObjectStream(Files.newInputStream(file), size, "application/octet-stream", null, openedAt, headers);
}
catch(Exception failure)
{
throw new IllegalStateException("Fixture object read failed.", failure);
}
}
/***************************************************************************
** Owner idempotent recovery observes persisted object existence.
***************************************************************************/
@Override
public Optional<StoredObject> head(String name, String key)
{
try
{
Path file = path(name, key);
return Files.exists(file) ? Optional.of(new StoredObject().withSize(Files.size(file))) : Optional.empty();
}
catch(Exception failure)
{
throw new IllegalStateException("Fixture object head failed.", failure);
}
}
/***************************************************************************
** Remove only a known fixture object and its metadata sidecar.
***************************************************************************/
@Override
public void delete(String name, String key)
{
try
{
Path file = path(name, key);
Files.deleteIfExists(file);
Files.deleteIfExists(Path.of(file + ".json"));
}
catch(Exception failure)
{
throw new IllegalStateException("Fixture object cleanup failed.", failure);
}
}
/***************************************************************************
** Staging uses a separate owner namespace in the same isolated file fixture.
***************************************************************************/
static class Staging extends UploadChunkStorage
{
private final DiskAttachmentStorage storage = new DiskAttachmentStorage();
/***********************************************************************
** Dedicated staging alias cannot address canonical originals.
***********************************************************************/
Staging()
{
super("mobile-fixture-staging");
}
/***********************************************************************
** Persist immutable staged chunks.
***********************************************************************/
@Override
protected void put(String uploadId, PutRequest request)
{
storage.put("staging", request);
}
/***********************************************************************
** Recovery sees previously persisted staged bytes.
***********************************************************************/
@Override
protected Optional<StoredObject> head(String uploadId, String key)
{
return storage.head("staging", key);
}
/***********************************************************************
** Reopen chunks for digest verification.
***********************************************************************/
@Override
protected ObjectStream open(String uploadId, String key)
{
return storage.open("staging", key);
}
/***********************************************************************
** Terminal cleanup never addresses canonical objects.
***********************************************************************/
@Override
protected void delete(String uploadId, String key)
{
storage.delete("staging", key);
}
}
}
@@ -1,267 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners;
import java.lang.management.ManagementFactory;
import java.sql.DriverManager;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import com.kingsrook.qbits.mobilescanners.recordfiles.MobileCaptureIdentityProvider;
import com.kingsrook.qbits.recordfiles.RecordFilesStore;
import com.kingsrook.qbits.recordfiles.api.CaptureIdentity;
import com.kingsrook.qbits.recordfiles.api.CaptureIdentityProvider;
import com.kingsrook.qbits.recordfiles.audit.AuditTrail;
import com.kingsrook.qbits.recordfiles.audit.RecordFilesEvent;
import com.kingsrook.qbits.recordfiles.audit.RecordFilesEventType;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Observe real commits inside the real scanner guard; no replacement lock.
*******************************************************************************/
final class MobileAuthorizationCommitProbe implements CaptureIdentityProvider
{
private final MobileCaptureIdentityProvider delegate = new MobileCaptureIdentityProvider();
private final AtomicBoolean observed = new AtomicBoolean();
private final AtomicBoolean pendingCommit = new AtomicBoolean();
private final AtomicBoolean verifiedCommit = new AtomicBoolean();
private final AtomicReference<Throwable> terminationFailure = new AtomicReference<>();
private final CountDownLatch attempted = new CountDownLatch(1);
private final Thread terminator;
private final String jdbc;
private final String eventColumn;
private final RecordFilesEventType eventType;
private final boolean failBeforeCommit;
/***************************************************************************
** Use actual administrator approval/session termination under its own context.
***************************************************************************/
MobileAuthorizationCommitProbe(QInstance instance, String jdbc, QSession admin,
CaptureIdentity identity, RecordFilesEventType eventType, boolean endSession, boolean failBeforeCommit)
{
this.jdbc = jdbc;
this.eventType = eventType;
this.failBeforeCommit = failBeforeCommit;
eventColumn = instance.getTable("record_files_event").getField("eventType").getBackendName();
terminator = new Thread(() ->
{
QContext.init(instance, admin);
attempted.countDown();
try
{
if(endSession)
{
MobileSessionService.end(identity.publicSessionId(), "ADMINISTRATOR", true);
}
else
{
MobileService.setApproval(identity.deviceId(), false);
}
}
catch(Throwable failure)
{
terminationFailure.set(failure);
}
finally
{
QContext.clear();
}
}, "mobile-authorization-terminator");
}
/***************************************************************************
** Capture remains the production trust boundary.
***************************************************************************/
@Override
public Optional<CaptureIdentity> capture() throws QException
{
return delegate.capture();
}
/***************************************************************************
** Revalidation remains the production trust boundary.
***************************************************************************/
@Override
public void revalidate(CaptureIdentity identity) throws QException
{
delegate.revalidate(identity);
}
/***************************************************************************
** Inspect an independent SQL connection before the production guard returns.
***************************************************************************/
@Override
public <T> T executeAuthorized(Optional<CaptureIdentity> identity, Operation<T> operation) throws QException
{
boolean[] release = {false};
try
{
return delegate.executeAuthorized(identity, () ->
{
try
{
return operation.run();
}
finally
{
if(pendingCommit.compareAndSet(true, false))
{
release[0] = true;
assertNull(RecordFilesStore.currentTransaction(), "Owner commit or rollback must finish before guard release.");
assertEquals(failBeforeCommit ? 0 : 1, committedEvents());
assertBlockedByCurrentThread();
verifiedCommit.set(true);
}
}
});
}
finally
{
if(release[0])
{
awaitTermination();
}
}
}
/***************************************************************************
** Pause after an actual transactional event insert, before its owner commit.
***************************************************************************/
AuditTrail audit() throws QException
{
return new AuditTrail()
{
/***********************************************************************
** Audit insertion shares the real owner transaction being exercised.
***********************************************************************/
@Override
public void withChange(RecordFilesEvent event) throws QException
{
super.withChange(event);
if(event.getType() == eventType && observed.compareAndSet(false, true))
{
pendingCommit.set(true);
assertEquals(0, committedEvents(), "A second connection cannot observe the pending event.");
terminator.start();
try
{
assertTrue(attempted.await(5, TimeUnit.SECONDS));
}
catch(InterruptedException interrupted)
{
Thread.currentThread().interrupt();
throw new QException("Interrupted authorization probe.", interrupted);
}
assertBlockedByCurrentThread();
if(failBeforeCommit)
{
throw new QException("Injected pre-commit failure.");
}
}
}
};
}
/***************************************************************************
** Monitor ownership proves contention without assuming an elapsed delay.
***************************************************************************/
private void assertBlockedByCurrentThread()
{
long deadline = System.nanoTime() + Duration.ofSeconds(5).toNanos();
do
{
var info = ManagementFactory.getThreadMXBean().getThreadInfo(terminator.threadId());
if(info != null && info.getThreadState() == Thread.State.BLOCKED
&& info.getLockOwnerId() == Thread.currentThread().threadId()
&& info.getLockInfo().getClassName().equals(MobileSessionService.class.getName()))
{
return;
}
Thread.yield();
}
while(System.nanoTime() < deadline && terminator.isAlive());
throw new AssertionError("Administrator termination did not block on the active mobile authorization guard.");
}
/***************************************************************************
** Read committed state independently of QContext and its transaction.
***************************************************************************/
int committedEvents() throws QException
{
try(var connection = DriverManager.getConnection(jdbc, "sa", "");
var query = connection.prepareStatement("SELECT COUNT(*) FROM record_files_event WHERE " + eventColumn + "=?"))
{
query.setString(1, eventType.name());
try(var rows = query.executeQuery())
{
assertTrue(rows.next());
return rows.getInt(1);
}
}
catch(java.sql.SQLException failure)
{
throw new QException("Commit probe failed.", failure);
}
}
/***************************************************************************
** Force committed revocation to precede any subsequent finalizer operation.
***************************************************************************/
private void awaitTermination()
{
try
{
terminator.join(10000);
}
catch(InterruptedException interrupted)
{
Thread.currentThread().interrupt();
throw new AssertionError(interrupted);
}
assertFalse(terminator.isAlive(), "Guard release must permit administrator termination.");
assertNull(terminationFailure.get(), "Administrator termination must commit successfully.");
}
/***************************************************************************
** Every parameterized case must reach the intended commit or rollback seam.
***************************************************************************/
void assertExercised()
{
assertTrue(observed.get());
assertTrue(verifiedCommit.get());
awaitTermination();
}
}
@@ -1,863 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners;
import java.net.HttpCookie;
import java.net.ServerSocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.sql.DriverManager;
import java.time.Duration;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kingsrook.qbits.mobilescanners.recordfiles.MobileCaptureIdentityProvider;
import com.kingsrook.qbits.mobilescanners.recordfiles.MobileRecordFilesContract;
import com.kingsrook.qbits.mobilescanners.recordfiles.RecordFilesCaptureWidgetData;
import com.kingsrook.qbits.recordfiles.RecordFilesFixture;
import com.kingsrook.qbits.recordfiles.api.CaptureIdentity;
import com.kingsrook.qbits.recordfiles.api.RecordFilesUploadApi;
import com.kingsrook.qbits.recordfiles.audit.RecordFilesEventType;
import com.kingsrook.qbits.recordfiles.collections.CollectionService;
import com.kingsrook.qbits.recordfiles.files.RetainedFileService;
import com.kingsrook.qbits.recordfiles.ingestion.UploadChunkStorage;
import com.kingsrook.qbits.recordfiles.ingestion.UploadConfiguration;
import com.kingsrook.qbits.recordfiles.ingestion.UploadFinalizer;
import com.kingsrook.qbits.recordfiles.ingestion.UploadManifest;
import com.kingsrook.qbits.recordfiles.ingestion.UploadQuota;
import com.kingsrook.qbits.recordfiles.ingestion.UploadSessionService;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.permissions.CustomPermissionChecker;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.TableBasedAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordFeature;
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordFeaturesMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.TableBasedAuthenticationModule;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Real password login, native mobile approval and native retained-file HTTP API.
** Metadata and owner object bytes persist on disk in an isolated test directory.
*******************************************************************************/
public class MobileRecordFilesIntegrationTest extends RecordFilesFixture
{
private static final ObjectMapper JSON = new ObjectMapper();
private static final String FILES = "/qqq/v1/qbit/record-files";
private static final byte[] CONTENT = {1, 2, 3};
private final HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
private final String password = UUID.randomUUID().toString();
private QApplicationJavalinServer server;
private String origin;
private String jdbc;
private String deviceId;
private String deviceToken;
/***************************************************************************
** Keep the file database open between HTTP requests, avoiding reopen overhead.
***************************************************************************/
@Override
protected RDBMSBackendMetaData createBackend() throws Exception
{
var backend = super.createBackend();
jdbc = backend.getJdbcUrl() + ";DB_CLOSE_DELAY=-1";
backend.setJdbcUrl(jdbc);
return backend;
}
/***************************************************************************
** Install both independent qbits and the optional provider on actual routes.
***************************************************************************/
@BeforeEach
void startMobileHost() throws Exception
{
Authentication.deniedPermissions = Set.of();
new MobileScannersQBitProducer().withConfig(new MobileScannersQBitConfig().withBackendName(backendName)).produce(instance, "mobile");
var captureRules = new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withCustomPermissionChecker(new QCodeReference(CapturePermissions.class));
instance.addWidget(new QWidgetMetaData().withName("mobileRecordPhotos").withLabel("Record photos").withType("composite")
.withCodeReference(new QCodeReference(CaptureWidget.class)).withPermissionRules(captureRules));
instance.addProcess(new QProcessMetaData().withName("mobileRecordPhotos").withLabel("Record photos").withPermissionRules(captureRules)
.withStep(new QBackendStepMetaData().withName("prepare").withCode(new QCodeReference(CapturePrepare.class)))
.withStep(new QFrontendStepMetaData().withName("capture").withLabel("Record photos")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.WIDGET).withValue("widgetName", "mobileRecordPhotos"))));
instance.getApp(MobileScannersQBitProducer.APP).getSections().getFirst().withProcesses(List.of(MobileScannersQBitProducer.CAPTURE, "mobileRecordPhotos"));
for(var table : instance.getTables().values())
{
if(table.getName().startsWith("mobile"))
{
table.withBackendDetails(new RDBMSTableBackendDetails().withTableName(table.getName()));
}
}
var authentication = new TableBasedAuthenticationMetaData().withUserTableName("fixtureUser").withSessionTableName("fixtureSession");
authentication.withName("fixturePasswords").withModuleCodeReference(new QCodeReference(Authentication.class));
instance.withInstanceDefaultAuthentication(authentication);
for(var table : List.of(authentication.defineStandardUserTable(backendName), authentication.defineStandardSessionTable(backendName)))
{
table.withBackendDetails(new RDBMSTableBackendDetails().withTableName(table.getName()));
table.getFields().values().forEach(field -> field.setBackendName(field.getName()));
table.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withPermissionBaseName("fixtureCredentials"));
instance.addTable(table);
}
try(var connection = DriverManager.getConnection(jdbc, "sa", ""); var statement = connection.createStatement())
{
statement.execute("CREATE TABLE fixtureUser (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, username VARCHAR(100) UNIQUE, fullName VARCHAR(200), passwordHash VARCHAR(1024), createDate TIMESTAMP, modifyDate TIMESTAMP)");
statement.execute("CREATE TABLE fixtureSession (id VARCHAR(100) PRIMARY KEY, userId INTEGER REFERENCES fixtureUser(id), accessTimestamp TIMESTAMP, createDate TIMESTAMP, modifyDate TIMESTAMP)");
for(String sql : new String(getClass().getResourceAsStream("/db/mobile-scanners-h2.sql").readAllBytes(), StandardCharsets.UTF_8).split(";"))
{
if(!sql.isBlank())
{
statement.execute(sql);
}
}
for(String actor : List.of("scanner", "admin", "other"))
{
try(var insert = connection.prepareStatement("INSERT INTO fixtureUser (username,fullName,passwordHash) VALUES (?,?,?)"))
{
insert.setString(1, actor);
insert.setString(2, actor);
insert.setString(3, TableBasedAuthenticationModule.PasswordHasher.createHashedPassword(password));
insert.executeUpdate();
}
}
}
DiskAttachmentStorage.directory = Files.createTempDirectory(java.nio.file.Path.of("target"), "owner-objects-");
collaboration.withAttachmentStorage(new QCodeReference(DiskAttachmentStorage.class));
config.withUploadConfiguration(new UploadConfiguration("mobile-fixture-staging",
new UploadManifest.Limits(20971520L, 41943040L, 2, 65536, 600, Set.of("application/pdf", "image/jpeg", "image/png")),
new UploadQuota.Policy(10, 83886080L, 100), new QCodeReference(MobileCaptureIdentityProvider.class))
{
/***********************************************************************
** Disk-backed staging persists independently of HTTP request instances.
***********************************************************************/
@Override
public UploadChunkStorage createStorage()
{
return new DiskAttachmentStorage.Staging();
}
});
RecordFilesUploadApi.register(instance);
MobileRecordFilesContract.register(instance);
new QInstanceEnricher(instance).enrich();
int port;
try(ServerSocket socket = new ServerSocket(0))
{
port = socket.getLocalPort();
}
origin = "http://127.0.0.1:" + port;
server = new QApplicationJavalinServer(new AbstractQQQApplication()
{
/***********************************************************************
** The real middleware owns authentication and native request contexts.
***********************************************************************/
@Override
public QInstance defineQInstance()
{
return instance;
}
}).withPort(port).withServeFrontendNext(false).withServeFrontendMaterialDashboard(false)
.withServeLegacyUnversionedMiddlewareAPI(false).withStrictUiApiRoutes(true)
.withAdditionalRouteProviders(List.of(new MobileScannersRouteProvider(instance)))
.withJavalinConfigCustomizer(configuration -> configuration.jetty.host = "127.0.0.1");
server.start();
}
/***************************************************************************
** Fixture metadata never delegates record-target inference to the client.
***************************************************************************/
public static class CaptureWidget extends AbstractWidgetRenderer
{
/***********************************************************************
** A fixed synthetic parent makes end-to-end image association inspectable.
***********************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
return new RenderWidgetOutput(new RecordFilesCaptureWidgetData("document", "1", "Document photos", "Capture or choose a photo for the first fixture document."));
}
}
/***************************************************************************
** Native process values contain the actual widget before the frontend step.
***************************************************************************/
public static class CapturePrepare implements BackendStep
{
/***********************************************************************
** Explicit metadata supplies a record target; no client field inference.
***********************************************************************/
@Override
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
if(MobileDeviceAuthorization.captureCurrent().isEmpty())
{
throw new QPermissionDeniedException("An approved device is required.");
}
output.addValue("mobileRecordPhotos", new RecordFilesCaptureWidgetData("document", "1", "Document photos", "Capture or choose a photo for the first fixture document."));
}
}
/***************************************************************************
** Fixture capture processes require the same verified device authority.
***************************************************************************/
public static class CapturePermissions implements CustomPermissionChecker
{
/***********************************************************************
** Missing/ended/revoked device context cannot launch photo capture.
***********************************************************************/
@Override
public void checkPermissionsThrowing(AbstractActionInput input, MetaDataWithPermissionRules metadata) throws QPermissionDeniedException
{
try
{
if(MobileDeviceAuthorization.captureCurrent().isEmpty())
{
throw new QPermissionDeniedException("An approved device is required.");
}
}
catch(QException failure)
{
throw new QPermissionDeniedException("An approved device is required.");
}
}
}
/***************************************************************************
** Dedicated local consumer fixture; caller must provide a private output path.
** Synthetic credentials are written once and never printed to logs/stdout.
***************************************************************************/
public static void main(String[] arguments) throws Exception
{
if(arguments.length != 2)
{
throw new IllegalArgumentException("Expected private credentials path and lifetime seconds.");
}
int lifetime = Integer.parseInt(arguments[1]);
if(lifetime < 1 || lifetime > 3600)
{
throw new IllegalArgumentException("Fixture lifetime must be 1 through 3600 seconds.");
}
var fixture = new MobileRecordFilesIntegrationTest();
try
{
fixture.setUp();
fixture.startMobileHost();
Files.writeString(java.nio.file.Path.of(arguments[0]), JSON.writeValueAsString(Map.of("baseUrl", fixture.origin,
"username", "scanner", "password", fixture.password, "adminUsername", "admin", "adminPassword", fixture.password,
"processName", "mobileRecordPhotos", "tableName", "document", "recordId", "1", "databaseUrl", fixture.jdbc)),
java.nio.file.StandardOpenOption.CREATE_NEW, java.nio.file.StandardOpenOption.WRITE);
Thread.sleep(Duration.ofSeconds(lifetime));
}
finally
{
fixture.stopMobileHost();
fixture.tearDown();
}
}
/***************************************************************************
** Stop only this isolated fixture; never operate the shared demo or handheld.
***************************************************************************/
@AfterEach
void stopMobileHost()
{
if(server != null)
{
server.stop();
}
client.close();
MobilePermissions.REQUEST_DEVICE.remove();
}
/***************************************************************************
** Receipt, ordered association and captured identity survive fresh DB reads.
***************************************************************************/
@Test
void actualLoginApprovalUploadAndPersistentAssociation() throws Exception
{
String scanner = signIn("scanner");
String admin = signIn("admin");
enrollHttp(scanner);
assertEquals(403, send("POST", FILES + "/uploads", scanner, manifest(UUID.randomUUID().toString()), true, Map.of()).statusCode());
approveHttp(admin, "mobileDeviceApprove");
JsonNode photoStep = processForm(scanner, "mobileRecordPhotos", "/init", Map.of(), true);
JsonNode photoWidget = photoStep.at("/values/mobileRecordPhotos");
assertEquals("composite", photoWidget.path("type").asText());
assertEquals("COMPOSITE", photoWidget.path("blockType").asText());
assertEquals("RECORD_FILES_CAPTURE", photoWidget.at("/subBlocks/0/blockType").asText());
assertEquals("document", photoWidget.at("/subBlocks/0/values/parent/tableName").asText());
assertEquals("1", photoWidget.at("/subBlocks/0/values/parent/recordId").asText());
var contractResponse = client.send(HttpRequest.newBuilder(URI.create(origin + "/qqq/v1/qbit/mobile-record-files/contract")).GET().build(), HttpResponse.BodyHandlers.ofString());
JsonNode contract = ok(contractResponse);
assertEquals(JSON.valueToTree(Map.of("version", "1.0.0", "blockType", "RECORD_FILES_CAPTURE", "widgetType", "composite")), contract);
JsonNode version = ok(send("GET", "/qqq/v1/version", scanner, null, false, Map.of()));
assertTrue(java.util.stream.StreamSupport.stream(version.path("extensions").spliterator(), false)
.anyMatch(extension -> "mobile-record-files".equals(extension.path("name").asText()) && "1.0.0".equals(extension.path("version").asText())));
JsonNode openApi = ok(send("GET", "/qqq/v1/openapi.json", scanner, null, false, Map.of()));
JsonNode schemas = openApi.at("/components/schemas");
assertEquals("string", schemas.path("MobileRecordFilesCaptureParentV1").at("/properties/recordId/type").asText());
assertTrue(schemas.path("MobileRecordFilesCaptureValuesV1").path("required").toString().contains("parent"));
RecordFilesCaptureContractTest.assertConforms(photoWidget, schemas.path("MobileRecordFilesCaptureWidgetV1"), schemas);
Files.writeString(java.nio.file.Path.of("target", "record-files-capture-widget.json"), photoWidget.toPrettyString());
assertTrue(ok(send("GET", FILES + "/capabilities?tableName=document&recordId=1", scanner, null, true, Map.of())).path("canUpload").asBoolean());
String request = UUID.randomUUID().toString();
JsonNode created = ok(send("POST", FILES + "/uploads", scanner, manifest(request), true, Map.of()));
String id = created.path("uploadId").asText();
ok(send("PUT", FILES + "/uploads/" + id + "/items/20000000-0000-0000-0000-000000000001/chunks/0", scanner, CONTENT, true,
Map.of("Content-Type", "application/octet-stream", "X-Chunk-SHA256", UploadManifest.hash(CONTENT))));
JsonNode receipt = ok(send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request))).path("receipt");
assertEquals("QUARANTINED", receipt.at("/items/0/lifecycleAtCommit").asText());
assertEquals(UploadManifest.hash(CONTENT), receipt.at("/items/0/sha256").asText());
assertEquals(receipt, ok(send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request))).path("receipt"));
QContext.init(instance, authenticated(scanner));
assertEquals(1, files.listForRecord("document", "1").size());
try(var connection = DriverManager.getConnection(jdbc, "sa", ""); var query = connection.createStatement(); var rows = query.executeQuery("SELECT COUNT(*) FROM mobileSession WHERE deviceId='" + deviceId + "' AND userId='scanner'"))
{
assertTrue(rows.next());
assertEquals(1, rows.getInt(1));
}
try(var paths = Files.list(DiskAttachmentStorage.directory.resolve("canonical")))
{
var originals = paths.filter(path -> !path.toString().endsWith(".json")).toList();
assertEquals(1, originals.size(), "Idempotent finalization retains one original.");
assertEquals(UploadManifest.hash(CONTENT), UploadManifest.hash(Files.readAllBytes(originals.getFirst())));
}
var uploadTable = instance.getTable("record_files_upload");
String identityColumn = uploadTable.getField("captureIdentityJson").getBackendName();
String uploadColumn = uploadTable.getField("uploadId").getBackendName();
try(var connection = DriverManager.getConnection(jdbc, "sa", ""); var query = connection.prepareStatement("SELECT " + identityColumn + " FROM record_files_upload WHERE " + uploadColumn + "=?"))
{
query.setString(1, id);
try(var rows = query.executeQuery())
{
assertTrue(rows.next());
JsonNode identity = JSON.readTree(rows.getString(1));
assertEquals("scanner", identity.path("actorId").asText());
assertEquals(deviceId, identity.path("deviceId").asText());
assertFalse(identity.path("publicSessionId").asText().isBlank());
assertFalse(identity.toString().contains(scanner));
assertFalse(identity.toString().contains(deviceToken));
}
}
assertFalse(receipt.toString().contains(scanner));
assertFalse(receipt.toString().contains(deviceToken));
Files.writeString(java.nio.file.Path.of("target", "mobile-upload-receipt.json"), receipt.toPrettyString());
}
/***************************************************************************
** Different actors, missing device proof and revocation cannot resume intake.
***************************************************************************/
@Test
void authorizationChangesBlockResumptionAndFinalization() throws Exception
{
String scanner = signIn("scanner");
String admin = signIn("admin");
enrollHttp(scanner);
approveHttp(admin, "mobileDeviceApprove");
String request = UUID.randomUUID().toString();
String id = ok(send("POST", FILES + "/uploads", scanner, manifest(request), true, Map.of())).path("uploadId").asText();
ok(send("PUT", FILES + "/uploads/" + id + "/items/20000000-0000-0000-0000-000000000001/chunks/0", scanner, CONTENT, true,
Map.of("Content-Type", "application/octet-stream", "X-Chunk-SHA256", UploadManifest.hash(CONTENT))));
assertEquals(403, send("GET", FILES + "/uploads/" + id, scanner, null, false, Map.of()).statusCode());
assertEquals(404, send("GET", FILES + "/uploads/" + id, signIn("other"), null, true, Map.of()).statusCode());
approveHttp(admin, "mobileDeviceRevoke");
assertEquals(403, send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request)).statusCode());
approveHttp(admin, "mobileDeviceApprove");
ok(send("POST", "/qqq/v1/qbit/mobile-scanners/sessions/current/end", scanner,
JSON.writeValueAsBytes(Map.of("reason", "SIGN_OUT")), true, Map.of()));
assertEquals(401, send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request)).statusCode());
scanner = signIn("scanner");
assertEquals(404, send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request)).statusCode());
QContext.init(instance, authenticated(scanner));
assertTrue(files.listForRecord("document", "1").isEmpty());
}
/***************************************************************************
** Approved devices still obey tenant, configured parent and current grants.
***************************************************************************/
@Test
void parentAuthorizationAndAttachmentPolicyCannotBeBypassed() throws Exception
{
String scanner = signIn("scanner");
String admin = signIn("admin");
enrollHttp(scanner);
approveHttp(admin, "mobileDeviceApprove");
for(var parent : List.of(Map.of("tableName", "document", "recordId", "3"), Map.of("tableName", "memo", "recordId", "1")))
{
String path = FILES + "/capabilities?tableName=" + parent.get("tableName") + "&recordId=" + parent.get("recordId");
assertEquals(404, send("GET", path, scanner, null, true, Map.of()).statusCode());
var body = JSON.readTree(manifest(UUID.randomUUID().toString()));
((com.fasterxml.jackson.databind.node.ObjectNode) body).set("parent", JSON.valueToTree(parent));
assertEquals(404, send("POST", FILES + "/uploads", scanner, JSON.writeValueAsBytes(body), true, Map.of()).statusCode());
}
String request = UUID.randomUUID().toString();
String id = ok(send("POST", FILES + "/uploads", scanner, manifest(request), true, Map.of())).path("uploadId").asText();
ok(send("PUT", FILES + "/uploads/" + id + "/items/20000000-0000-0000-0000-000000000001/chunks/0", scanner, CONTENT, true,
Map.of("Content-Type", "application/octet-stream", "X-Chunk-SHA256", UploadManifest.hash(CONTENT))));
Authentication.deniedPermissions = Set.of("document.write", "document.attach");
assertFalse(ok(send("GET", FILES + "/capabilities?tableName=document&recordId=1", scanner, null, true, Map.of())).path("canUpload").asBoolean());
assertEquals(403, send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request)).statusCode());
Authentication.deniedPermissions = Set.of();
RecordFeaturesMetaData.ofOrWithNew(instance.getTable("document")).withEnabled(RecordFeature.ATTACHMENTS, false);
com.kingsrook.qqq.backend.core.actions.collaboration.RuntimeMetaDataOverlay.refresh(instance);
assertFalse(ok(send("GET", FILES + "/capabilities?tableName=document&recordId=1", scanner, null, true, Map.of())).path("canUpload").asBoolean());
assertEquals(403, send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request)).statusCode());
QContext.init(instance, authenticated(scanner));
assertTrue(files.listForRecord("document", "1").isEmpty());
assertFalse(Files.exists(DiskAttachmentStorage.directory.resolve("canonical")), "Denied finalization must not write retained bytes.");
}
/***************************************************************************
** Public identity values cannot substitute for the verified private binding.
***************************************************************************/
@Test
void adapterRejectsIdentitySubstitutionAndEndedSessions() throws Exception
{
QSession scanner = authenticated(signIn("scanner"));
QContext.init(instance, scanner);
MobileCaptureIdentityProvider provider = new MobileCaptureIdentityProvider();
assertTrue(provider.capture().isEmpty());
assertEquals("desktop", provider.executeAuthorized(Optional.empty(), () -> "desktop"));
assertThrows(QException.class, () -> provider.executeAuthorized(null, () -> "forbidden"));
assertThrows(QException.class, () -> MobileDeviceAuthorization.executeAuthorized((MobileDeviceAuthorization.Identity) null, () -> "forbidden"));
scanner.setClientChannel("android/fixture");
assertThrows(QException.class, provider::capture);
var enrolled = MobileService.enroll(Map.of("installationId", UUID.randomUUID().toString(), "displayName", "fixture"));
String id = (String) enrolled.get("deviceId");
QSession admin = authenticated(signIn("admin"));
QContext.init(instance, admin);
MobileService.setApproval(id, true);
QContext.init(instance, scanner);
MobilePermissions.REQUEST_DEVICE.set(id);
CaptureIdentity captured = provider.capture().orElseThrow();
MobilePermissions.REQUEST_DEVICE.remove();
provider.revalidate(captured);
for(Optional<CaptureIdentity> expected : List.of(Optional.<CaptureIdentity>empty(),
Optional.of(new CaptureIdentity("other-provider", captured.actorId(), id, captured.publicSessionId())),
Optional.of(new CaptureIdentity(captured.providerKey(), "other", id, captured.publicSessionId())),
Optional.of(new CaptureIdentity(captured.providerKey(), captured.actorId(), "other-device", captured.publicSessionId())),
Optional.of(new CaptureIdentity(captured.providerKey(), captured.actorId(), id, "other-session"))))
{
assertThrows(QException.class, () -> provider.executeAuthorized(expected, () ->
{
throw new AssertionError("Invalid or missing identity must not execute guarded work.");
}));
}
assertThrows(QException.class, () -> provider.revalidate(null));
assertThrows(QException.class, () -> provider.revalidate(new CaptureIdentity("other-provider", captured.actorId(), id, captured.publicSessionId())));
assertThrows(QException.class, () -> provider.revalidate(new CaptureIdentity(captured.providerKey(), "other", id, captured.publicSessionId())));
assertThrows(QException.class, () -> provider.revalidate(new CaptureIdentity(captured.providerKey(), captured.actorId(), "other-device", captured.publicSessionId())));
assertThrows(QException.class, () -> provider.revalidate(new CaptureIdentity(captured.providerKey(), captured.actorId(), id, "other-session")));
QSession bound = QContext.getQSession();
MobilePermissions.REQUEST_DEVICE.set("another-verified-request-device");
assertThrows(QException.class, provider::capture);
MobilePermissions.REQUEST_DEVICE.remove();
QContext.init(instance, null);
assertThrows(QException.class, provider::capture);
QContext.init(instance, bound);
assertThrows(QException.class, () -> MobileDeviceAuthorization.executeAuthorized(
new MobileDeviceAuthorization.Identity("other", id, captured.publicSessionId()), () ->
{
throw new AssertionError("A rejected identity must not execute the callback.");
}));
QContext.init(instance, scanner);
scanner.setClientChannel(null);
assertThrows(QException.class, provider::capture);
QContext.init(instance, admin);
MobileService.setApproval(id, false);
QContext.init(instance, bound);
assertThrows(QException.class, () -> provider.revalidate(captured));
QContext.init(instance, admin);
MobileService.setApproval(id, true);
try(var connection = DriverManager.getConnection(jdbc, "sa", ""); var update = connection.prepareStatement("UPDATE mobileSession SET status='ENDED' WHERE id=?"))
{
update.setString(1, captured.publicSessionId());
update.executeUpdate();
}
QContext.init(instance, bound);
assertThrows(QException.class, () -> provider.revalidate(captured));
}
/***************************************************************************
** Real revoke/end cannot cross owner intake, association or receipt commits.
** Each failure case also proves rollback releases the same authorization lock.
***************************************************************************/
@ParameterizedTest
@CsvSource({
"INTAKE_COMPLETED,false,false", "INTAKE_COMPLETED,true,false",
"COLLECTION_ITEM_ADDED,false,false", "COLLECTION_ITEM_ADDED,true,false",
"UPLOAD_COMPLETED,false,false", "UPLOAD_COMPLETED,true,false",
"INTAKE_COMPLETED,false,true", "INTAKE_COMPLETED,true,true",
"COLLECTION_ITEM_ADDED,false,true", "COLLECTION_ITEM_ADDED,true,true",
"UPLOAD_COMPLETED,false,true", "UPLOAD_COMPLETED,true,true"
})
void scannerAuthorityRemainsHeldThroughCommit(RecordFilesEventType event, boolean endSession, boolean failBeforeCommit) throws Exception
{
String scanner = signIn("scanner");
String adminCookie = signIn("admin");
enrollHttp(scanner);
approveHttp(adminCookie, "mobileDeviceApprove");
String request = UUID.randomUUID().toString();
String id = ok(send("POST", FILES + "/uploads", scanner, manifest(request), true, Map.of())).path("uploadId").asText();
ok(send("PUT", FILES + "/uploads/" + id + "/items/20000000-0000-0000-0000-000000000001/chunks/0", scanner, CONTENT, true,
Map.of("Content-Type", "application/octet-stream", "X-Chunk-SHA256", UploadManifest.hash(CONTENT))));
QSession admin = authenticated(adminCookie);
QContext.init(instance, authenticated(scanner));
MobilePermissions.REQUEST_DEVICE.set(deviceId);
CaptureIdentity identity;
try
{
identity = new MobileCaptureIdentityProvider().capture().orElseThrow();
}
finally
{
MobilePermissions.REQUEST_DEVICE.remove();
}
var probe = new MobileAuthorizationCommitProbe(instance, jdbc, admin, identity, event, endSession, failBeforeCommit);
var audit = probe.audit();
ObservedAttachmentLifecycle.audit.set(audit);
collaboration.withAttachmentLifecycle(new QCodeReference(ObservedAttachmentLifecycle.class));
var configuration = config.getUploadConfiguration();
var clock = java.time.Clock.systemUTC();
var sessions = new UploadSessionService(config, configuration.limits(), configuration.createStorage(), probe, audit, clock);
var retained = new RetainedFileService(config, audit,
new com.kingsrook.qbits.collaboration.attachments.AttachmentsService(collaboration, new DiskAttachmentStorage()));
var finalizer = new UploadFinalizer(sessions, retained, new CollectionService(config, audit), clock);
try
{
assertThrows(QException.class, () -> finalizer.finalizeUpload(id, request));
}
finally
{
ObservedAttachmentLifecycle.audit.remove();
}
probe.assertExercised();
assertEquals(failBeforeCommit ? 0 : 1, probe.committedEvents());
int expectedStatus = endSession ? 401 : 403;
assertEquals(expectedStatus, send("POST", FILES + "/uploads/" + id + "/finalize", scanner, null, true, Map.of("Idempotency-Key", request)).statusCode());
String stateColumn = instance.getTable("record_files_upload").getField("state").getBackendName();
String uploadColumn = instance.getTable("record_files_upload").getField("uploadId").getBackendName();
try(var connection = DriverManager.getConnection(jdbc, "sa", "");
var query = connection.prepareStatement("SELECT " + stateColumn + " FROM record_files_upload WHERE " + uploadColumn + "=?"))
{
query.setString(1, id);
try(var rows = query.executeQuery())
{
assertTrue(rows.next());
assertEquals(event == RecordFilesEventType.UPLOAD_COMPLETED && !failBeforeCommit ? "COMPLETE" : "FINALIZING", rows.getString(1));
}
}
QContext.init(instance, admin);
int retainedCount = files.listForRecord("document", "1").size();
int expectedRetained = event == RecordFilesEventType.INTAKE_COMPLETED && failBeforeCommit ? 0 : 1;
assertEquals(expectedRetained, retainedCount, "Rolled-back registration stays absent; committed originals remain retained.");
}
/***************************************************************************
** Actual password hashing and QQQ table session validation with fixture roles.
***************************************************************************/
public static class ObservedAttachmentLifecycle extends com.kingsrook.qbits.recordfiles.lifecycle.RecordFilesAttachmentLifecycle
{
private static final ThreadLocal<com.kingsrook.qbits.recordfiles.audit.AuditTrail> audit = new ThreadLocal<>();
/***********************************************************************
** Observe the actual owner registration transaction, preserving policy.
***********************************************************************/
@Override
public void afterStored(com.kingsrook.qqq.backend.core.model.data.QRecord attachment) throws QException
{
var current = audit.get();
if(current == null)
{
super.afterStored(attachment);
}
else if(com.kingsrook.qbits.recordfiles.RecordFilesContext.config().isManaged(attachment.getValueString("tableName")))
{
new RetainedFileService(configForLifecycle(), current, null).registerStoredAttachment(attachment);
}
}
/***********************************************************************
** Resolve the same installed configuration as the production lifecycle.
***********************************************************************/
private com.kingsrook.qbits.recordfiles.RecordFilesQBitConfig configForLifecycle() throws QException
{
return com.kingsrook.qbits.recordfiles.RecordFilesContext.config();
}
}
/***************************************************************************
** Real fixture authentication; no test identity bypass.
***************************************************************************/
public static class Authentication extends TableBasedAuthenticationModule
{
private static volatile Set<String> deniedPermissions = Set.of();
/***********************************************************************
** Server roles are assigned only after password/session validation.
***********************************************************************/
@Override
public QSession createSession(QInstance instance, Map<String, String> context) throws QAuthenticationException
{
return grant(super.createSession(instance, context));
}
/***********************************************************************
** Native asynchronous actions retain the same server-owned permissions.
***********************************************************************/
@Override
public boolean isSessionValid(QInstance instance, QSession session)
{
boolean valid = super.isSessionValid(instance, session);
if(valid)
{
grant(session);
}
return valid;
}
/***********************************************************************
** Request fields cannot supply role or tenant grants.
***********************************************************************/
private QSession grant(QSession session)
{
if(session.getUser() == null)
{
return session;
}
session.withPermissions(ALL).withPermissions("document.read", "document.write", MobilePermissions.SCAN).withSecurityKeyValue("tenant", "a");
if("admin".equals(session.getUser().getIdReference()))
{
session.withPermission(MobilePermissions.ADMIN);
}
else
{
session.getPermissions().removeAll(deniedPermissions);
}
return session;
}
}
/***************************************************************************
** Direct adapter checks use a real validated table-backed login session.
***************************************************************************/
private QSession authenticated(String cookie) throws QAuthenticationException
{
return new Authentication().createSession(instance, Map.of(TableBasedAuthenticationModule.SESSION_UUID_KEY, cookie));
}
/***************************************************************************
** Login uses the same versioned password endpoint as the handheld.
***************************************************************************/
private String signIn(String actor) throws Exception
{
String basic = Base64.getEncoder().encodeToString((actor + ":" + password).getBytes(StandardCharsets.UTF_8));
var response = client.send(HttpRequest.newBuilder(URI.create(origin + "/qqq/v1/manageSession"))
.header("Authorization", "Basic " + basic).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{}")).build(), HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
return response.headers().allValues("Set-Cookie").stream().flatMap(value -> HttpCookie.parse(value).stream())
.filter(cookie -> "sessionUUID".equals(cookie.getName())).findFirst().orElseThrow().getValue();
}
/***************************************************************************
** Device identity is created by enrollment and returned only at that boundary.
***************************************************************************/
private void enrollHttp(String session) throws Exception
{
var response = send("POST", "/qqq/v1/qbit/mobile-scanners/devices/enroll", session,
JSON.writeValueAsBytes(Map.of("installationId", UUID.randomUUID().toString(), "displayName", "HTTP capture fixture")), false, Map.of());
assertEquals(201, response.statusCode());
JsonNode enrolled = JSON.readTree(response.body());
deviceId = enrolled.path("deviceId").asText();
deviceToken = enrolled.path("deviceToken").asText();
}
/***************************************************************************
** Native process selection starts from a record and requires confirmation.
***************************************************************************/
private void approveHttp(String session, String process) throws Exception
{
JsonNode started = processForm(session, process, "/init", Map.of("recordsParam", "recordIds", "recordIds", deviceId, "tableName", "mobileDevice"));
JsonNode result = processForm(session, process, "/" + started.path("processUUID").asText() + "/step/input", Map.of());
assertFalse(result.has("error"), "Approval process must succeed.");
}
/***************************************************************************
** Multipart native process envelope and bounded async completion polling.
***************************************************************************/
private JsonNode processForm(String session, String process, String suffix, Map<String, String> values) throws Exception
{
return processForm(session, process, suffix, values, false);
}
/***************************************************************************
** Device-bound frontend widgets use the same process API as admin actions.
***************************************************************************/
private JsonNode processForm(String session, String process, String suffix, Map<String, String> values, boolean device) throws Exception
{
String boundary = "fixture" + UUID.randomUUID();
StringBuilder body = new StringBuilder();
Map<String, String> fields = new java.util.HashMap<>(values);
fields.put("values", JSON.writeValueAsString(values));
fields.put("stepTimeoutMillis", "1000");
for(var field : fields.entrySet())
{
body.append("--").append(boundary).append("\r\nContent-Disposition: form-data; name=\"").append(field.getKey()).append("\"\r\n\r\n").append(field.getValue()).append("\r\n");
}
body.append("--").append(boundary).append("--\r\n");
JsonNode result = ok(send("POST", "/qqq/v1/processes/" + process + suffix, session, body.toString().getBytes(StandardCharsets.UTF_8), device,
Map.of("Content-Type", "multipart/form-data; boundary=" + boundary)));
long deadline = System.nanoTime() + Duration.ofSeconds(15).toNanos();
while(Set.of("RUNNING", "JOB_STARTED").contains(result.path("type").asText()))
{
assertTrue(System.nanoTime() < deadline, "Native process timed out.");
Thread.sleep(25);
result = ok(send("GET", "/qqq/v1/processes/" + process + "/" + result.path("processUUID").asText() + "/status/" + result.path("jobUUID").asText(), session, null, device, Map.of()));
}
assertFalse(result.has("error"), "Native process returned an error.");
return result;
}
/***************************************************************************
** Immutable manifest has no device, user or session identity fields.
***************************************************************************/
private byte[] manifest(String request) throws Exception
{
return JSON.writeValueAsBytes(Map.of("requestId", request, "parent", Map.of("tableName", "document", "recordId", "1"),
"items", List.of(Map.of("itemId", "20000000-0000-0000-0000-000000000001", "position", 0, "fileName", "fixture.pdf", "declaredMediaType", "application/pdf",
"sizeBytes", CONTENT.length, "sha256", UploadManifest.hash(CONTENT), "origin", "UPLOAD"))));
}
/***************************************************************************
** Headers alone carry authentication; no credentials enter test diagnostics.
***************************************************************************/
private HttpResponse<String> send(String method, String path, String session, byte[] bytes, boolean device, Map<String, String> extra) throws Exception
{
var request = HttpRequest.newBuilder(URI.create(origin + path)).timeout(Duration.ofSeconds(20))
.header("Cookie", "sessionUUID=" + session).header("X-Record-Files-Request", "1")
.header("Content-Type", extra.getOrDefault("Content-Type", "application/json"));
if(device)
{
request.header("X-QQQ-Device-Id", deviceId).header("X-QQQ-Device-Token", deviceToken).header("X-QQQ-Client", "android/fixture");
}
extra.forEach((key, value) ->
{
if(!"Content-Type".equals(key))
{
request.header(key, value);
}
});
return client.send(request.method(method, bytes == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofByteArray(bytes)).build(), HttpResponse.BodyHandlers.ofString());
}
/***************************************************************************
** Report only status on failure, never credentials or arbitrary response data.
***************************************************************************/
private JsonNode ok(HttpResponse<String> response) throws Exception
{
assertEquals(200, response.statusCode());
return JSON.readTree(response.body());
}
}
@@ -1,114 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kingsrook.qbits.mobilescanners.recordfiles.MobileRecordFilesContract;
import com.kingsrook.qbits.mobilescanners.recordfiles.RecordFilesCaptureWidgetData;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Shared schema conformance for actual HTTP widgets and invalid parent shapes.
*******************************************************************************/
class RecordFilesCaptureContractTest
{
private static final ObjectMapper JSON = new ObjectMapper();
/***************************************************************************
** Opaque record IDs survive serialization, without name-based inference.
***************************************************************************/
@Test
void typedWidgetPreservesOpaqueParentAndRejectsMalformedValues() throws Exception
{
JsonNode schemas = JSON.valueToTree(new MobileRecordFilesContract().defineComponentSchemas());
JsonNode widget = JSON.valueToTree(new RecordFilesCaptureWidgetData("anyConfiguredTable", "00123", null, null));
assertConforms(widget, schemas.path("MobileRecordFilesCaptureWidgetV1"), schemas);
assertEquals("00123", widget.at("/subBlocks/0/values/parent/recordId").asText());
assertEquals("RECORD_FILES_CAPTURE", widget.at("/subBlocks/0/blockType").asText());
assertTrue(widget.at("/subBlocks/0/values").has("parent"));
for(String invalid : List.of("{}", "{\"tableName\":\"x\"}", "{\"tableName\":\"x\",\"recordId\":null}",
"{\"tableName\":\"x\",\"recordId\":123}", "{\"tableName\":\"x\",\"recordId\":\" \"}"))
{
JsonNode parent = JSON.readTree(invalid);
assertThrows(AssertionError.class, () -> assertConforms(parent, schemas.path("MobileRecordFilesCaptureParentV1"), schemas));
}
}
/***************************************************************************
** Server renderers cannot publish invalid or oversized parent coordinates.
***************************************************************************/
@Test
void rendererRejectsInvalidParentBeforePublishingMetadata()
{
assertThrows(NullPointerException.class, () -> new RecordFilesCaptureWidgetData(null, "1", null, null));
for(String invalid : List.of("", " ", "x".repeat(256), "bad\nrecord"))
{
assertThrows(IllegalArgumentException.class, () -> new RecordFilesCaptureWidgetData("table", invalid, null, null));
assertThrows(IllegalArgumentException.class, () -> new RecordFilesCaptureWidgetData(invalid, "1", null, null));
}
}
/***************************************************************************
** Validate the contract keywords actually published by this small vocabulary.
***************************************************************************/
static void assertConforms(JsonNode value, JsonNode schema, JsonNode components)
{
assertTrue(schema.isObject() && !schema.isEmpty(), "A registered schema is required.");
if(schema.hasNonNull("$ref"))
{
assertConforms(value, components.path(schema.path("$ref").asText().substring("#/components/schemas/".length())), components);
return;
}
switch(schema.path("type").asText())
{
case "object" ->
{
assertTrue(value.isObject(), "Expected object.");
schema.path("required").forEach(required -> assertTrue(value.has(required.asText()), "Missing required field."));
schema.path("properties").fields().forEachRemaining(property ->
{
if(value.has(property.getKey()))
{
assertConforms(value.get(property.getKey()), property.getValue(), components);
}
});
}
case "array" ->
{
assertTrue(value.isArray(), "Expected array.");
value.forEach(item -> assertConforms(item, schema.path("items"), components));
}
case "string" ->
{
assertTrue(value.isTextual(), "Expected opaque string.");
assertTrue(value.asText().length() >= schema.path("minLength").asInt(0));
assertTrue(value.asText().length() <= schema.path("maxLength").asInt(Integer.MAX_VALUE));
if(schema.hasNonNull("pattern"))
{
assertTrue(java.util.regex.Pattern.compile(schema.path("pattern").asText()).matcher(value.asText()).find());
}
if(schema.hasNonNull("enum"))
{
assertTrue(java.util.stream.StreamSupport.stream(schema.path("enum").spliterator(), false).anyMatch(value::equals));
}
}
default -> throw new AssertionError("Unsupported contract schema type.");
}
}
}
-3
View File
@@ -1,6 +1,3 @@
# Copyright (c) 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
# KofTwentyTwo PowerShell analysis (K22-CODE-SH-05). CI runs:
# Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit
# and `Invoke-Formatter` with the same settings for layout. PowerShell supports the
@@ -1,146 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessMetaDataAdjustment;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
/*******************************************************************************
** Freeze native single/bulk record selections for guarded admin processes.
*******************************************************************************/
final class MobileAdminSelection
{
/***************************************************************************
** Standalone launches retain manual entry; selections never apply early.
***************************************************************************/
static void prepare(RunBackendStepInput input, RunBackendStepOutput output, String selectedTable, String idField, String noun) throws QException
{
MobilePermissions.require(MobilePermissions.ADMIN);
String selection = input.getValueString("recordsParam");
if(selection == null || selection.isBlank())
{
output.setRecords(new ArrayList<>());
output.addValue("selectionSummary", "Enter the record IDs to process, separated by commas.");
return;
}
String table = input.getValueString("tableName");
if(table != null && !selectedTable.equals(table))
{
throw new QException("Select records from the process table.");
}
List<String> selected;
if("recordIds".equals(selection))
{
selected = resolve(MobileService.required(input.getValues(), "recordIds", 3700), selectedTable);
}
else if("filterJSON".equals(selection))
{
selected = resolveFilter(input, selectedTable);
}
else
{
throw new QException("Select mobile records to process.");
}
List<String> labels = new ArrayList<>();
List<QRecord> snapshot = new ArrayList<>();
for(String id : selected)
{
var device = MobileService.get(selectedTable, id);
labels.add((device.getValueString("displayName") == null ? device.getValueString("userId") : device.getValueString("displayName")) + " (" + id + ")");
snapshot.add(new QRecord().withValue("id", id));
}
output.setRecords(snapshot);
output.addValue(idField, String.join(",", selected));
output.addValue("selectionSummary", selected.size() + " " + noun + " selected: " + String.join("; ", labels));
output.setProcessMetaDataAdjustment(new ProcessMetaDataAdjustment().withUpdatedField(
new QFieldMetaData(idField, QFieldType.STRING).withLabel("Record IDs").withMaxLength(3700).withIsHidden(true).withIsRequired(false)));
}
/***************************************************************************
** Native select-all/first-N filters become a fixed, bounded record snapshot.
***************************************************************************/
private static List<String> resolveFilter(RunBackendStepInput input, String selectedTable) throws QException
{
QQueryFilter filter;
try
{
filter = JsonUtils.toObject(MobileService.required(input.getValues(), "filterJSON", 100000), QQueryFilter.class);
}
catch(Exception e)
{
throw new QException("The selected device filter is invalid.", e);
}
if(filter == null || (filter.getLimit() != null && filter.getLimit() < 1))
{
throw new QException("Select between 1 and 100 records.");
}
filter.setLimit(filter.getLimit() == null ? 101 : Math.min(filter.getLimit(), 101));
var records = new QueryAction().execute(new QueryInput().withTableName(selectedTable).withFilter(filter)).getRecords();
if(records.isEmpty() || records.size() > 100)
{
throw new QException("Select between 1 and 100 records.");
}
return resolve(String.join(",", records.stream().map(record -> record.getValueString("id")).toList()), selectedTable);
}
/***************************************************************************
** Validate the entire bounded selection before any record is changed.
***************************************************************************/
static List<String> resolve(String value, String selectedTable) throws QException
{
if(value == null || value.isBlank() || value.length() > 3700)
{
throw new QException("Enter between 1 and 100 record IDs.");
}
String[] parts = value.split(",", -1);
if(parts.length > 100)
{
throw new QException("Select at most 100 records at a time.");
}
Set<String> ids = new LinkedHashSet<>();
for(String part : parts)
{
String id = part.trim();
try
{
if(!UUID.fromString(id).toString().equals(id))
{
throw new IllegalArgumentException();
}
}
catch(IllegalArgumentException e)
{
throw new QException("Each record ID must be a valid UUID.");
}
if(MobileService.get(selectedTable, id) == null)
{
throw new QException("A selected mobile record no longer exists.");
}
ids.add(id);
}
return new ArrayList<>(ids);
}
}
@@ -1,86 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.Schema;
/*******************************************************************************
** Public appearance data available before authentication.
*******************************************************************************/
public class MobileBrandingSpec extends MobileJsonSpec
{
/***************************************************************************
** Public, stable semantic operation.
***************************************************************************/
@Override
public BasicOperation defineBasicOperation()
{
return new BasicOperation().withHttpMethod(HttpMethod.GET).withPath(MobileScannersUiApi.PREFIX + "/branding")
.withOperationId("mobileScannersBranding").withShortSummary("Read customer branding")
.withLongDescription("Public appearance data; ignores stale user and device credentials. Never includes private configuration.");
}
/***************************************************************************
** Login screens also need appearance data.
***************************************************************************/
@Override
public boolean isSecured()
{
return false;
}
/***************************************************************************
** Typed public response.
***************************************************************************/
@Override
protected String getResponseSchemaName()
{
return "MobileScannersBrandingV1";
}
/***************************************************************************
** Publish the complete extension vocabulary once.
***************************************************************************/
@Override
public Map<String, Schema> defineComponentSchemas()
{
return MobileScannersSchemas.components();
}
/***************************************************************************
** Only explicitly allowlisted branding properties leave the server.
***************************************************************************/
@Override
protected Object handle(JsonInput input)
{
MobileScannersQBitConfig config = QContext.getQInstance().getQBits().values().stream().map(bit -> bit.getConfig())
.filter(MobileScannersQBitConfig.class::isInstance).map(MobileScannersQBitConfig.class::cast)
.findFirst().orElseGet(MobileScannersQBitConfig::new);
Map<String, String> response = new LinkedHashMap<>();
response.put("displayName", config.getDisplayName());
response.put("primaryColor", config.getPrimaryColor());
if(config.getLogoUrl() != null)
{
response.put("logoUrl", config.getLogoUrl());
}
return response;
}
}
@@ -1,42 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners;
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
/*******************************************************************************
** Explicit scope of consented screen pixels; omitted legacy fields are app-only.
*******************************************************************************/
enum MobileCaptureMode
{
APP_CONTENT, DEVICE_SCREEN;
/***************************************************************************
** Unknown modes cannot broaden a client's capture permission.
***************************************************************************/
static MobileCaptureMode parse(Object value) throws QBadRequestException
{
if(value == null)
{
return APP_CONTENT;
}
if(value instanceof String text)
{
for(MobileCaptureMode mode : values())
{
if(mode.name().equals(text))
{
return mode;
}
}
}
throw new QBadRequestException("Unsupported screen capture mode.");
}
}
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more