From c4f7ccd1bc2709883bc7e80e76209e8809ad7625 Mon Sep 17 00:00:00 2001 From: Yangjunjie Lin Date: Thu, 10 Sep 2026 13:55:02 +0100 Subject: [PATCH 1/3] feat: add opt-in Link Intelligence for Scanly 2.1 --- .github/workflows/ci.yml | 16 ++ CHANGELOG.md | 8 +- README.md | 25 +++- api-snapshots/native-abi.json | 2 +- api-snapshots/native-api.json | 2 +- api-snapshots/public-api.json | 56 +++++-- apps/web-demo/app/api/url-safety/route.ts | 18 +++ apps/web-demo/app/layout.tsx | 2 +- apps/web-demo/app/page.tsx | 2 +- apps/web-demo/components/QRTool.tsx | 36 ++++- .../web-demo/components/UrlSafetyEvidence.tsx | 35 +++++ apps/web-demo/package.json | 11 +- docs/releases/v2.1.0.md | 31 ++++ docs/url-safety.md | 123 ++++++++++++++++ engines/jsqr/README.md | 4 +- engines/jsqr/package.json | 4 +- engines/zxing-cpp-wasm/README.md | 4 +- engines/zxing-cpp-wasm/package.json | 4 +- engines/zxing-js/README.md | 4 +- engines/zxing-js/package.json | 4 +- native/android/scanly-sdk/build.gradle.kts | 2 +- native/core/CMakeLists.txt | 2 +- native/core/src/scanly_core.cpp | 2 +- native/core/tests/c_abi_test.c | 2 +- native/ios/Package.swift | 2 +- package-lock.json | 133 ++++++++++------- package.json | 10 +- packages/benchmark/README.md | 4 +- packages/benchmark/package.json | 2 +- packages/browser/README.md | 4 +- packages/browser/dist/index.d.ts | 2 +- packages/browser/dist/index.js | 2 +- packages/browser/package.json | 12 +- packages/browser/src/index.ts | 2 +- packages/core/README.md | 4 +- packages/core/dist/index.d.ts | 2 +- packages/core/dist/index.js | 2 +- .../core/dist/runtime/builtin-operators.js | 2 +- packages/core/package.json | 6 +- packages/core/src/index.ts | 2 +- .../core/src/runtime/builtin-operators.ts | 2 +- packages/node/README.md | 4 +- packages/node/package.json | 10 +- packages/parsers/README.md | 4 +- packages/parsers/package.json | 2 +- packages/react/README.md | 4 +- packages/react/package.json | 4 +- packages/scenario-schema/README.md | 4 +- packages/scenario-schema/package.json | 2 +- packages/url-safety/README.md | 21 +++ packages/url-safety/dist/async.d.ts | 6 + packages/url-safety/dist/async.d.ts.map | 1 + packages/url-safety/dist/async.js | 30 ++++ packages/url-safety/dist/async.js.map | 1 + packages/url-safety/dist/cache.d.ts | 14 ++ packages/url-safety/dist/cache.d.ts.map | 1 + packages/url-safety/dist/cache.js | 64 ++++++++ packages/url-safety/dist/cache.js.map | 1 + packages/url-safety/dist/client.d.ts | 15 ++ packages/url-safety/dist/client.d.ts.map | 1 + packages/url-safety/dist/client.js | 36 +++++ packages/url-safety/dist/client.js.map | 1 + packages/url-safety/dist/index.d.ts | 8 + packages/url-safety/dist/index.d.ts.map | 1 + packages/url-safety/dist/index.js | 6 + packages/url-safety/dist/index.js.map | 1 + packages/url-safety/dist/local.d.ts | 13 ++ packages/url-safety/dist/local.d.ts.map | 1 + packages/url-safety/dist/local.js | 138 +++++++++++++++++ packages/url-safety/dist/local.js.map | 1 + packages/url-safety/dist/risk-engine.d.ts | 13 ++ packages/url-safety/dist/risk-engine.d.ts.map | 1 + packages/url-safety/dist/risk-engine.js | 48 ++++++ packages/url-safety/dist/risk-engine.js.map | 1 + packages/url-safety/dist/scanner.d.ts | 36 +++++ packages/url-safety/dist/scanner.d.ts.map | 1 + packages/url-safety/dist/scanner.js | 77 ++++++++++ packages/url-safety/dist/scanner.js.map | 1 + packages/url-safety/dist/server/analyzer.d.ts | 18 +++ .../url-safety/dist/server/analyzer.d.ts.map | 1 + packages/url-safety/dist/server/analyzer.js | 131 +++++++++++++++++ .../url-safety/dist/server/analyzer.js.map | 1 + packages/url-safety/dist/server/fetcher.d.ts | 18 +++ .../url-safety/dist/server/fetcher.d.ts.map | 1 + packages/url-safety/dist/server/fetcher.js | 112 ++++++++++++++ .../url-safety/dist/server/fetcher.js.map | 1 + packages/url-safety/dist/server/handler.d.ts | 12 ++ .../url-safety/dist/server/handler.d.ts.map | 1 + packages/url-safety/dist/server/handler.js | 66 +++++++++ .../url-safety/dist/server/handler.js.map | 1 + packages/url-safety/dist/server/html.d.ts | 4 + packages/url-safety/dist/server/html.d.ts.map | 1 + packages/url-safety/dist/server/html.js | 79 ++++++++++ packages/url-safety/dist/server/html.js.map | 1 + packages/url-safety/dist/server/index.d.ts | 9 ++ .../url-safety/dist/server/index.d.ts.map | 1 + packages/url-safety/dist/server/index.js | 6 + packages/url-safety/dist/server/index.js.map | 1 + .../url-safety/dist/server/ip-policy.d.ts | 6 + .../url-safety/dist/server/ip-policy.d.ts.map | 1 + packages/url-safety/dist/server/ip-policy.js | 57 +++++++ .../url-safety/dist/server/ip-policy.js.map | 1 + .../url-safety/dist/server/providers.d.ts | 48 ++++++ .../url-safety/dist/server/providers.d.ts.map | 1 + packages/url-safety/dist/server/providers.js | 96 ++++++++++++ .../url-safety/dist/server/providers.js.map | 1 + packages/url-safety/dist/types.d.ts | 139 ++++++++++++++++++ packages/url-safety/dist/types.d.ts.map | 1 + packages/url-safety/dist/types.js | 2 + packages/url-safety/dist/types.js.map | 1 + packages/url-safety/dist/validation.d.ts | 12 ++ packages/url-safety/dist/validation.d.ts.map | 1 + packages/url-safety/dist/validation.js | 83 +++++++++++ packages/url-safety/dist/validation.js.map | 1 + packages/url-safety/package.json | 21 +++ packages/url-safety/src/async.ts | 20 +++ packages/url-safety/src/cache.ts | 66 +++++++++ packages/url-safety/src/client.ts | 32 ++++ packages/url-safety/src/index.ts | 7 + packages/url-safety/src/local.ts | 94 ++++++++++++ packages/url-safety/src/punycode.d.ts | 4 + packages/url-safety/src/risk-engine.ts | 44 ++++++ packages/url-safety/src/scanner.ts | 60 ++++++++ packages/url-safety/src/server/analyzer.ts | 103 +++++++++++++ packages/url-safety/src/server/fetcher.ts | 88 +++++++++++ packages/url-safety/src/server/handler.ts | 47 ++++++ packages/url-safety/src/server/html.ts | 54 +++++++ packages/url-safety/src/server/index.ts | 8 + packages/url-safety/src/server/ip-policy.ts | 43 ++++++ packages/url-safety/src/server/providers.ts | 81 ++++++++++ packages/url-safety/src/types.ts | 121 +++++++++++++++ packages/url-safety/src/validation.ts | 66 +++++++++ packages/url-safety/tsconfig.build.json | 5 + scripts/benchmark-url-safety.ts | 78 ++++++++++ scripts/canonical-evidence.ts | 4 +- scripts/generate-stable-release-manifest.mjs | 40 ++++- scripts/symbology-gates.ts | 2 +- scripts/url-safety-sbom.ts | 27 ++++ scripts/verify-api-snapshots.ts | 2 +- scripts/verify-device-evidence.ts | 2 +- scripts/verify-package-imports.mjs | 8 +- scripts/verify-package-metadata.mjs | 1 + scripts/verify-package-tarballs.mjs | 20 ++- scripts/verify-quality-gates.ts | 4 +- scripts/verify-release-manifest.mjs | 6 +- scripts/verify-stable-manifest.mjs | 3 +- scripts/verify-stable-publication-record.mjs | 4 +- scripts/verify-url-safety-api.ts | 28 ++++ scripts/verify-url-safety-security.ts | 37 +++++ tests/e2e/url-safety.spec.ts | 54 +++++++ tests/unit/benchmark-provenance.test.ts | 11 +- tests/unit/release-manifest-integrity.test.ts | 25 +++- tests/unit/url-safety/lifecycle.test.ts | 130 ++++++++++++++++ tests/unit/url-safety/local.test.ts | 71 +++++++++ tests/unit/url-safety/providers.test.ts | 104 +++++++++++++ tests/unit/url-safety/ssrf.test.ts | 115 +++++++++++++++ tsconfig.json | 2 + vitest.config.ts | 4 +- 158 files changed, 3655 insertions(+), 172 deletions(-) create mode 100644 apps/web-demo/app/api/url-safety/route.ts create mode 100644 apps/web-demo/components/UrlSafetyEvidence.tsx create mode 100644 docs/releases/v2.1.0.md create mode 100644 docs/url-safety.md create mode 100644 packages/url-safety/README.md create mode 100644 packages/url-safety/dist/async.d.ts create mode 100644 packages/url-safety/dist/async.d.ts.map create mode 100644 packages/url-safety/dist/async.js create mode 100644 packages/url-safety/dist/async.js.map create mode 100644 packages/url-safety/dist/cache.d.ts create mode 100644 packages/url-safety/dist/cache.d.ts.map create mode 100644 packages/url-safety/dist/cache.js create mode 100644 packages/url-safety/dist/cache.js.map create mode 100644 packages/url-safety/dist/client.d.ts create mode 100644 packages/url-safety/dist/client.d.ts.map create mode 100644 packages/url-safety/dist/client.js create mode 100644 packages/url-safety/dist/client.js.map create mode 100644 packages/url-safety/dist/index.d.ts create mode 100644 packages/url-safety/dist/index.d.ts.map create mode 100644 packages/url-safety/dist/index.js create mode 100644 packages/url-safety/dist/index.js.map create mode 100644 packages/url-safety/dist/local.d.ts create mode 100644 packages/url-safety/dist/local.d.ts.map create mode 100644 packages/url-safety/dist/local.js create mode 100644 packages/url-safety/dist/local.js.map create mode 100644 packages/url-safety/dist/risk-engine.d.ts create mode 100644 packages/url-safety/dist/risk-engine.d.ts.map create mode 100644 packages/url-safety/dist/risk-engine.js create mode 100644 packages/url-safety/dist/risk-engine.js.map create mode 100644 packages/url-safety/dist/scanner.d.ts create mode 100644 packages/url-safety/dist/scanner.d.ts.map create mode 100644 packages/url-safety/dist/scanner.js create mode 100644 packages/url-safety/dist/scanner.js.map create mode 100644 packages/url-safety/dist/server/analyzer.d.ts create mode 100644 packages/url-safety/dist/server/analyzer.d.ts.map create mode 100644 packages/url-safety/dist/server/analyzer.js create mode 100644 packages/url-safety/dist/server/analyzer.js.map create mode 100644 packages/url-safety/dist/server/fetcher.d.ts create mode 100644 packages/url-safety/dist/server/fetcher.d.ts.map create mode 100644 packages/url-safety/dist/server/fetcher.js create mode 100644 packages/url-safety/dist/server/fetcher.js.map create mode 100644 packages/url-safety/dist/server/handler.d.ts create mode 100644 packages/url-safety/dist/server/handler.d.ts.map create mode 100644 packages/url-safety/dist/server/handler.js create mode 100644 packages/url-safety/dist/server/handler.js.map create mode 100644 packages/url-safety/dist/server/html.d.ts create mode 100644 packages/url-safety/dist/server/html.d.ts.map create mode 100644 packages/url-safety/dist/server/html.js create mode 100644 packages/url-safety/dist/server/html.js.map create mode 100644 packages/url-safety/dist/server/index.d.ts create mode 100644 packages/url-safety/dist/server/index.d.ts.map create mode 100644 packages/url-safety/dist/server/index.js create mode 100644 packages/url-safety/dist/server/index.js.map create mode 100644 packages/url-safety/dist/server/ip-policy.d.ts create mode 100644 packages/url-safety/dist/server/ip-policy.d.ts.map create mode 100644 packages/url-safety/dist/server/ip-policy.js create mode 100644 packages/url-safety/dist/server/ip-policy.js.map create mode 100644 packages/url-safety/dist/server/providers.d.ts create mode 100644 packages/url-safety/dist/server/providers.d.ts.map create mode 100644 packages/url-safety/dist/server/providers.js create mode 100644 packages/url-safety/dist/server/providers.js.map create mode 100644 packages/url-safety/dist/types.d.ts create mode 100644 packages/url-safety/dist/types.d.ts.map create mode 100644 packages/url-safety/dist/types.js create mode 100644 packages/url-safety/dist/types.js.map create mode 100644 packages/url-safety/dist/validation.d.ts create mode 100644 packages/url-safety/dist/validation.d.ts.map create mode 100644 packages/url-safety/dist/validation.js create mode 100644 packages/url-safety/dist/validation.js.map create mode 100644 packages/url-safety/package.json create mode 100644 packages/url-safety/src/async.ts create mode 100644 packages/url-safety/src/cache.ts create mode 100644 packages/url-safety/src/client.ts create mode 100644 packages/url-safety/src/index.ts create mode 100644 packages/url-safety/src/local.ts create mode 100644 packages/url-safety/src/punycode.d.ts create mode 100644 packages/url-safety/src/risk-engine.ts create mode 100644 packages/url-safety/src/scanner.ts create mode 100644 packages/url-safety/src/server/analyzer.ts create mode 100644 packages/url-safety/src/server/fetcher.ts create mode 100644 packages/url-safety/src/server/handler.ts create mode 100644 packages/url-safety/src/server/html.ts create mode 100644 packages/url-safety/src/server/index.ts create mode 100644 packages/url-safety/src/server/ip-policy.ts create mode 100644 packages/url-safety/src/server/providers.ts create mode 100644 packages/url-safety/src/types.ts create mode 100644 packages/url-safety/src/validation.ts create mode 100644 packages/url-safety/tsconfig.build.json create mode 100644 scripts/benchmark-url-safety.ts create mode 100644 scripts/url-safety-sbom.ts create mode 100644 scripts/verify-url-safety-api.ts create mode 100644 scripts/verify-url-safety-security.ts create mode 100644 tests/e2e/url-safety.spec.ts create mode 100644 tests/unit/url-safety/lifecycle.test.ts create mode 100644 tests/unit/url-safety/local.test.ts create mode 100644 tests/unit/url-safety/providers.test.ts create mode 100644 tests/unit/url-safety/ssrf.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a596ed..ed576ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,17 @@ jobs: - name: SDK package builds run: npm run build:packages + - name: URL Safety Security Gate + run: npm run url-safety:security + + - name: URL Safety Benchmark + run: npm run benchmark:url-safety + + - name: URL Safety API and dependency inventory + run: | + npm run url-safety:api + npm run url-safety:sbom + - name: Native ESM package import smoke run: npm run packages:smoke @@ -129,6 +140,11 @@ jobs: benchmark-results/development/smoke-balanced.csv benchmark-results/development/external-open-license-validation.json benchmark-results/ci/bundle-cost.json + benchmark-results/development/url-safety-security.json + benchmark-results/development/url-safety-tests.json + benchmark-results/development/url-safety-benchmark.json + benchmark-results/development/url-safety-sbom.cdx.json + benchmark-results/development/url-safety-licenses.json multisymbology-unit: name: Multi-Symbology Unit Tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd6fe5..3411b4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ All notable changes follow semantic versioning. ## Unreleased -No changes yet. +## 2.1.0 — Link Intelligence (development; qualification pending) + +- Added optional `@scanly/url-safety` with local URL signals, canonicalization, redaction, cache, cancellation, confirmed-event integration and deterministic scoring. +- Added a Node-only server entry with pinned public-IP connections, per-hop SSRF validation, bounded inspection, reputation lookup adapters and schema-validated advisory LLM evidence. +- Added explicit privacy modes and a default-disabled demo, with a same-origin rate/size/time-limited backend. +- Existing barcode decoding remains local and independent of network analysis. Published 2.0.0 and 2.0.1 artifacts and historical evidence are unchanged. +- Release status and unresolved qualification requirements are recorded in `docs/releases/v2.1.0.md`; this entry does not assert publication. ## 2.0.1 — Scanner lifecycle and runtime hardening diff --git a/README.md b/README.md index 4866a75..14a07e8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# Scanly SDK v2.0.1 +# Scanly SDK v2.1.0 — Link Intelligence (development) Privacy-first, local-only barcode scanning SDK for Web, Node.js, React, iOS, and Android. [![SDK 2.0.1](https://img.shields.io/badge/SDK-2.0.1-green)](https://github.com/Yangjunjie-Lin/Scanly/releases/tag/v2.0.1) +[![Source 2.1.0](https://img.shields.io/badge/SDK-2.1.0-green)](docs/releases/v2.1.0.md) [![npm latest](https://img.shields.io/badge/npm-latest-CB3837)](https://www.npmjs.com/package/@scanly/browser) [![TypeScript](https://img.shields.io/badge/TypeScript-5-blue)](https://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) @@ -13,6 +14,8 @@ Privacy-first, local-only barcode scanning SDK for Web, Node.js, React, iOS, and ## Install +This branch develops 2.1.0. The published Stable release remains 2.0.1 until the new release qualification and publication gates pass. + Choose the package for your runtime. Browser, Node, and React packages already include their required Scanly workspace dependencies. ```bash @@ -21,6 +24,26 @@ npm install @scanly/node npm install @scanly/react ``` +## AI-Assisted URL Safety + +Barcode decoding remains local. Remote URL intelligence is optional and disabled by default; no barcode image or camera frame is sent to AI or reputation providers. Link Intelligence is an independent asynchronous second stage, using the existing structured payload parser. + +```ts +import { UrlSafetyClient } from "@scanly/url-safety"; + +const safetyClient = new UrlSafetyClient({ endpoint: "/api/url-safety" }); +const outcome = await router.scan(frame); +if (outcome.ok && outcome.primary.structuredPayload?.kind === "url") { + const analysis = await safetyClient.analyze( + outcome.primary.structuredPayload.fields.href as string, + { mode: "local-only" }, // no network; choose other modes only with explicit consent + ); + // Display evidence. Never automatically open the destination. +} +``` + +`@scanly/url-safety/server` provides self-hosted SSRF-protected inspection, optional Google Web Risk / VirusTotal lookup adapters, and a vendor-neutral JSON LLM adapter. LLM is advisory evidence, not an authority. No verdict guarantees a URL is safe. See [URL safety architecture, integration, privacy and configuration](docs/url-safety.md). + Advanced engine composition can use `@scanly/core` directly: ```bash diff --git a/api-snapshots/native-abi.json b/api-snapshots/native-abi.json index a658c61..1e1b469 100644 --- a/api-snapshots/native-abi.json +++ b/api-snapshots/native-abi.json @@ -1,6 +1,6 @@ { "schemaVersion": "stable-native-abi-snapshot-1", - "sdkVersion": "2.0.1", + "sdkVersion": "2.1.0", "abiVersion": 1, "exportedSymbols": [ "scanly_cancel_token_cancel", diff --git a/api-snapshots/native-api.json b/api-snapshots/native-api.json index 0c14a15..97adce8 100644 --- a/api-snapshots/native-api.json +++ b/api-snapshots/native-api.json @@ -1,6 +1,6 @@ { "schemaVersion": "stable-native-api-snapshot-1", - "sdkVersion": "2.0.1", + "sdkVersion": "2.1.0", "classification": "stable-breaking-changes-forbidden-with-explicit-compatibility-review", "files": { "native/core/include/scanly/core.h": "82dee124d9b88e8ee2c9ea6727a83a3a9e08fb6b70ca753c404928d70624d07a", diff --git a/api-snapshots/public-api.json b/api-snapshots/public-api.json index ec5fef5..9eb0cf9 100644 --- a/api-snapshots/public-api.json +++ b/api-snapshots/public-api.json @@ -3,7 +3,7 @@ "packages": [ { "packageName": "@scanly/core", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -35,7 +35,7 @@ "contracts/operator.d.ts": "18077b1053bb5156f59b92ae5906c8fba922f77bf226ca0c82bff535a00b08e8", "contracts/result.d.ts": "18477ad67be91bd65866662f7db79b5483e1da47463e1f658bb78c4157337465", "contracts/validator.d.ts": "33b21365b712c8349c0ed1a23dce445b5ce237ca9672b706e2cc63fad2a6944c", - "index.d.ts": "51deec2d8b2faed49fcc62eebf4f9c4acd28b9c0b34336ea489d03ec33c6349f", + "index.d.ts": "dfaa598baecab60b2f1d46132c8fc3342995b050bcf0cb75bba41e714ee53dcc", "industrial/candidate-region.d.ts": "a6bd9513b23a8ae25b7e368f1444fadf4a46c4399a7658608f0f2d59921d2664", "industrial/coordinate-transform.d.ts": "903561693bfb18ae88c38a8b145c2ee45d1007826a594b4736cb69e78ad07f15", "industrial/decode-candidate-conflict.d.ts": "e0d2a7806f3b1eeeef024cee8c44141338f83ee1a29131aa4c2b2f29e3c4844f", @@ -84,7 +84,7 @@ }, { "packageName": "@scanly/browser", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -102,7 +102,7 @@ "camera-strategy.d.ts": "799e0c964d6702c443caa4bc5d3d4b55a741a8733b1e35d2f260db28cd58e070", "decode-upload.d.ts": "2509ee37a22687302a4b4212d99b89e8c3506b493c649f95bd2d09375b2e35fa", "image-loader.d.ts": "ad4c9336c38ca562c043e284a8cdb832f6e7579d127df3740cbef72647a6091e", - "index.d.ts": "61ee9c06c84bb1a036cb7fa19c050f24f63a3470db6f333a6253f8c33dc52df2", + "index.d.ts": "97fdfb128ff88347b684e2d24dfcfcb0b437501cf77bc518ba4ad70dedfb60e2", "runtime.d.ts": "50be12a3ab1f49d28d15df8d1dfd2531d5888b3c6899cff1471f7a5012c99f60", "scanner/camera-capabilities.d.ts": "0a3112c32680e637965fe4ded4949de841c03ff5570fbd4672322fa2636ef140", "scanner/camera-platform.d.ts": "a82972a4d23addbf5f856ab5d9f03e5429385b1647813cb01ccd8c055cc9f266", @@ -132,7 +132,7 @@ }, { "packageName": "@scanly/node", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -146,7 +146,7 @@ }, { "packageName": "@scanly/react", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -160,7 +160,7 @@ }, { "packageName": "@scanly/scenario-schema", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -174,7 +174,7 @@ }, { "packageName": "@scanly/parsers", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -188,7 +188,7 @@ }, { "packageName": "@scanly/benchmark", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -207,7 +207,7 @@ }, { "packageName": "@scanly/engine-jsqr", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -221,7 +221,7 @@ }, { "packageName": "@scanly/engine-zxing-js", - "packageVersion": "2.0.1", + "packageVersion": "2.1.0", "exports": { ".": { "types": "./dist/index.d.ts", @@ -232,6 +232,40 @@ "declarationHashes": { "index.d.ts": "cdedf9c166132ce21bacaf31e40affed4feecb6b2f3cf7b9b2c59f47ec6b25b1" } + }, + { + "packageName": "@scanly/url-safety", + "packageVersion": "2.1.0", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./server": { + "types": "./dist/server/index.d.ts", + "browser": null, + "node": "./dist/server/index.js" + } + }, + "types": "./dist/index.d.ts", + "declarationHashes": { + "async.d.ts": "2ef61c95d0e26ef7606b9449b6a804224cdf47f9ac7b55eb853c02caf865f27a", + "cache.d.ts": "e98f5df8c14efdb0140dd794829d7fa1e03ef702c4a60260650af9b341bb3315", + "client.d.ts": "b8cc11bdd4dbf90c88dba93e9502471829cdf0737bb4ff2cb320ee0196fe2dff", + "index.d.ts": "df1816704160bcc4bce1d6b7a6bde0ea4d62129d7340293f50cb435dab88f4e2", + "local.d.ts": "26b5c04b465910d0e4cc4475081c605df91c47385325de455b1d15c5bd1f84d9", + "risk-engine.d.ts": "e42e4cab7cf3695c61ead6d537297b96c6772684c994f2b8c25aea044cb91618", + "scanner.d.ts": "cce733b13762dcd476c4c0d57babd384c9c98834f40bb11b23c8cb5782d74135", + "server/analyzer.d.ts": "5cd269f23deee1b3fc789358d98f674858920fa5d1a45261183675b5299f424d", + "server/fetcher.d.ts": "270025b70658bdecbb006d86408f9dc433b5acbc50db9f612d15d6edbb219ebb", + "server/handler.d.ts": "291ba086b5db539ce5aa01882bb6b9ac44cf49dd96ce2ce583b20b7fceccc373", + "server/html.d.ts": "851c0a93a78e860610dc31697208f0290fc97de496f99e041363d080e9708690", + "server/index.d.ts": "9725a9ed684c0a1b39a6790768e9c475e7df6e67fdf9a872a255d7471e768cb4", + "server/ip-policy.d.ts": "b69b2b390e05928a225c2b202c080ec61c860e5a324d1c98d4c3f32dffbe69da", + "server/providers.d.ts": "07841f7b75a9c499a7e9eeb5b3625755ab924a86d071bdb6e93d476dca8e36fc", + "types.d.ts": "49760efa8c5063f12b3e23baa8de1952f3368ce5292b967b732498dca2317d4f", + "validation.d.ts": "6173f46e3fd5c20d322b31cb54d37bf40d82d1a8d277cff2acf6f5e677c66730" + } } ] } diff --git a/apps/web-demo/app/api/url-safety/route.ts b/apps/web-demo/app/api/url-safety/route.ts new file mode 100644 index 0000000..ec1c3cc --- /dev/null +++ b/apps/web-demo/app/api/url-safety/route.ts @@ -0,0 +1,18 @@ +import { createUrlSafetyAnalyzer, createUrlSafetyHandler, GoogleWebRiskProvider, VirusTotalProvider } from "@scanly/url-safety/server"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +// No NEXT_PUBLIC secrets. LLM integration is a server-owned transport extension; +// see docs/url-safety.md. No hosted Scanly service is required. +const analyzer = createUrlSafetyAnalyzer({ + reputationProviders: [ + ...(process.env.SCANLY_WEB_RISK_API_KEY ? [new GoogleWebRiskProvider(process.env.SCANLY_WEB_RISK_API_KEY)] : []), + ...(process.env.SCANLY_VIRUSTOTAL_API_KEY ? [new VirusTotalProvider(process.env.SCANLY_VIRUSTOTAL_API_KEY)] : []), + ], +}); +export const POST = createUrlSafetyHandler({ + analyzer, + ...(process.env.SCANLY_URL_SAFETY_ORIGIN ? { allowedOrigins: [process.env.SCANLY_URL_SAFETY_ORIGIN] } : {}), + allowedModes: process.env.SCANLY_URL_SAFETY_ENABLED === "true" ? ["local-only", "reputation-only", "remote", "ai-assisted"] : ["local-only"], +}); diff --git a/apps/web-demo/app/layout.tsx b/apps/web-demo/app/layout.tsx index 3116a26..32605d9 100644 --- a/apps/web-demo/app/layout.tsx +++ b/apps/web-demo/app/layout.tsx @@ -4,7 +4,7 @@ import type { Metadata } from "next"; export const metadata: Metadata = { title: "Scanly — Browser QR Decoder", description: - "Scan supported barcodes with the Scanly SDK v2.0.1 Stable camera runtime or local image input — local-only, with no uploads or accounts.", + "Scan supported barcodes locally with Scanly SDK v2.1.0. URL safety intelligence is a separate, optional opt-in; barcode images and camera frames are never uploaded.", }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/apps/web-demo/app/page.tsx b/apps/web-demo/app/page.tsx index 3ca7296..f6126c9 100644 --- a/apps/web-demo/app/page.tsx +++ b/apps/web-demo/app/page.tsx @@ -7,7 +7,7 @@ export default function Page() {

Scanly

- Browser-side Scanly SDK v2.0.1 barcode scanner with bounded frame scheduling, temporal confirmation, and multi-layer decoder fallbacks. Camera or + Browser-side Scanly SDK v2.1.0 barcode scanner with optional Link Intelligence, bounded frame scheduling, temporal confirmation, and multi-layer decoder fallbacks. Camera or upload — processing stays on your device.
diff --git a/apps/web-demo/components/QRTool.tsx b/apps/web-demo/components/QRTool.tsx index d1a9294..94c20b9 100644 --- a/apps/web-demo/components/QRTool.tsx +++ b/apps/web-demo/components/QRTool.tsx @@ -10,7 +10,9 @@ import { createTrackOverlayModels, } from "@scanly/browser"; import type { BatchState, TrackOverlayModel } from "@scanly/browser"; -import { isSafeActionUrl } from "@scanly/parsers"; +import { isSafeActionUrl, parseSemanticPayload } from "@scanly/parsers"; +import { UrlSafetyClient, UrlSafetyController, type UrlSafetyPrivacyMode, type UrlSafetyState } from "@scanly/url-safety"; +import { UrlSafetyEvidence } from "./UrlSafetyEvidence"; import type { ScanResult, SdkErrorCode } from "@scanly/core"; import { getBuiltinScenario, type ScenarioPresetId } from "@scanly/scenario-schema"; @@ -48,7 +50,7 @@ function resultFromScannerEvent(event: import("@scanly/browser").ScanEvent): Sca ...(event.barcode.rawBytes ? { rawBytes: event.barcode.rawBytes } : {}), ...(event.barcode.cornerPoints ? { cornerPoints: [...event.barcode.cornerPoints] } : {}), engine: { id: event.barcode.engineId, version: event.barcode.engineVersion ?? "unknown" }, - preprocessingPath: [], frameId: String(event.frameId), structuredPayload: null, + preprocessingPath: [], frameId: String(event.frameId), structuredPayload: parseSemanticPayload(event.barcode.text).structured, validation: { valid: true, validatorIds: [], messages: [] }, warnings: [], timing: { totalMs: 0 }, }; } @@ -91,6 +93,10 @@ export default function QRTool() { const [mode, setMode] = useState("camera"); const [status, setStatus] = useState("Idle"); const [results, setResults] = useState([]); + const [safetyMode, setSafetyMode] = useState<"disabled" | UrlSafetyPrivacyMode>("disabled"); + const [safetyState, setSafetyState] = useState({ status: "idle" }); + const safetyRef = useRef(null); + const safetyClient = useMemo(() => new UrlSafetyClient({ endpoint: "/api/url-safety" }), []); const [lastError, setLastError] = useState(""); const [errorReason, setErrorReason] = useState(""); const [devices, setDevices] = useState([]); @@ -132,7 +138,19 @@ export default function QRTool() { const recoveryDiagnostics = process.env.NODE_ENV !== "production" ? primaryResult?.metadata?.scannerDiagnostics : undefined; const activeScenario = useMemo(() => getBuiltinScenario(scenarioPreset(preset)), [preset]); + useEffect(() => { + const owner = primaryResult?.rawText; + const controller = new UrlSafetyController(safetyClient, (state) => setSafetyState({ ...state, owner })); + safetyRef.current = controller; + controller.configure(safetyMode === "disabled" ? undefined : safetyMode); + controller.accept(primaryResult?.structuredPayload); + return () => { controller.dispose(); safetyRef.current = null; }; + }, [safetyClient, safetyMode, primaryResult]); + + useEffect(() => () => safetyClient.dispose(), [safetyClient]); + async function disposeCameraRuntime(): Promise { + safetyRef.current?.cancel(); const batch = batchSessionRef.current; const scanner = scannerSessionRef.current; batchSessionRef.current = null; @@ -296,6 +314,7 @@ export default function QRTool() { } function stopScan() { + safetyRef.current?.cancel(); if (batchSessionRef.current) void batchSessionRef.current.stop(); else void scannerSessionRef.current?.stop(); // Stop any leftover media tracks @@ -312,6 +331,7 @@ export default function QRTool() { } function pauseScan() { + safetyRef.current?.cancel(); if (batchSessionRef.current) batchSessionRef.current.pause(); else scannerSessionRef.current?.pause(); setScannerState("paused"); @@ -438,6 +458,10 @@ export default function QRTool() { if (!isSafeActionUrl(text)) return; const url = new URL(text); if (url.protocol !== "http:" && url.protocol !== "https:") return; + if (safetyMode !== "disabled") { + const risk = text === primary && safetyState.owner === primary ? safetyState.analysis?.riskLevel : undefined; + if (risk !== "low" && !window.confirm(risk === "high" || risk === "critical" ? "High-risk URL. Do not enter credentials. Open anyway?" : "This URL is unverified or has risk indicators. Verify the domain before proceeding. Open with caution?")) return; + } window.open(url.href, "_blank", "noopener,noreferrer"); } @@ -744,13 +768,13 @@ export default function QRTool() { @@ -800,6 +824,8 @@ export default function QRTool() { )} + + {results.length > 1 && (
    {results.map((r, i) => ( @@ -825,7 +851,7 @@ export default function QRTool() { {isSafeActionUrl(r.rawText) && ( @@ -824,7 +830,7 @@ export default function QRTool() { )} - + {results.length > 1 && (
      From bdf9e55009c2c1706b098848ef095843d2cda224 Mon Sep 17 00:00:00 2001 From: Yangjunjie Lin Date: Thu, 10 Sep 2026 14:18:44 +0100 Subject: [PATCH 3/3] fix: distinguish public IPv6 literals in local URL signals --- packages/url-safety/dist/local.js | 2 +- packages/url-safety/dist/local.js.map | 2 +- packages/url-safety/src/local.ts | 2 +- tests/unit/url-safety/local.test.ts | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/url-safety/dist/local.js b/packages/url-safety/dist/local.js index c74c524..0d3f3e2 100644 --- a/packages/url-safety/dist/local.js +++ b/packages/url-safety/dist/local.js @@ -84,7 +84,7 @@ export class LocalUrlAnalyzer { const ipLiteral = /^\d+\.\d+\.\d+\.\d+$/.test(host) || host.startsWith("["); if (ipLiteral) add("ip_literal", "medium", "URL uses an IP address instead of a domain."); - const privateLike = host === "localhost" || !host.includes(".") || /\.(localhost|local|internal|lan|home|test|invalid)$/.test(host) || /^(127\.|10\.|0\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host) || /^\[(::|f[cd]|fe[89ab])/i.test(host); + const privateLike = host === "localhost" || (!ipLiteral && !host.includes(".")) || /\.(localhost|local|internal|lan|home|test|invalid)$/.test(host) || /^(127\.|10\.|0\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host) || /^\[(::|f[cd]|fe[89ab])/i.test(host); if (privateLike) add("private_network_target", "high", "Local or private-network-like target; remote inspection is blocked."); if (url.port && url.port !== "80" && url.port !== "443") diff --git a/packages/url-safety/dist/local.js.map b/packages/url-safety/dist/local.js.map index 9628909..7e6e989 100644 --- a/packages/url-safety/dist/local.js.map +++ b/packages/url-safety/dist/local.js.map @@ -1 +1 @@ -{"version":3,"file":"local.js","sourceRoot":"","sources":["../src/local.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"local.js","sourceRoot":"","sources":["../src/local.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/url-safety/src/local.ts b/packages/url-safety/src/local.ts index 465804b..6c35de8 100644 --- a/packages/url-safety/src/local.ts +++ b/packages/url-safety/src/local.ts @@ -62,7 +62,7 @@ export class LocalUrlAnalyzer { if (url.protocol === "http:") add("cleartext_http", "low", "HTTP does not encrypt the connection."); const ipLiteral = /^\d+\.\d+\.\d+\.\d+$/.test(host) || host.startsWith("["); if (ipLiteral) add("ip_literal", "medium", "URL uses an IP address instead of a domain."); - const privateLike = host === "localhost" || !host.includes(".") || /\.(localhost|local|internal|lan|home|test|invalid)$/.test(host) || /^(127\.|10\.|0\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host) || /^\[(::|f[cd]|fe[89ab])/i.test(host); + const privateLike = host === "localhost" || (!ipLiteral && !host.includes(".")) || /\.(localhost|local|internal|lan|home|test|invalid)$/.test(host) || /^(127\.|10\.|0\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host) || /^\[(::|f[cd]|fe[89ab])/i.test(host); if (privateLike) add("private_network_target", "high", "Local or private-network-like target; remote inspection is blocked."); if (url.port && url.port !== "80" && url.port !== "443") add("nonstandard_port", "medium", "Nonstandard port; default remote policy blocks this port."); if (url.username || url.password) add("embedded_credentials", "high", "URL contains embedded credentials."); diff --git a/tests/unit/url-safety/local.test.ts b/tests/unit/url-safety/local.test.ts index 8bc47a7..dc3bb6f 100644 --- a/tests/unit/url-safety/local.test.ts +++ b/tests/unit/url-safety/local.test.ts @@ -32,6 +32,11 @@ describe("URL canonicalization and local-only evidence", () => { expect(new UrlRiskEngine().aggregate(local.analyze("https://example.zip")).riskLevel).toBe("low"); expect(local.analyze("https://example.com/%zz").signals).toEqual([]); }); + it("does not mistake a public IPv6 literal for an unqualified private hostname", () => { + const result = local.analyze("https://[2606:4700:4700::1111]/"); + expect(result.remoteAllowed).toBe(true); + expect(result.signals.map((signal) => signal.id)).toEqual(["ip_literal"]); + }); it("redacts all query values, userinfo, fragment and free-text secrets", () => { const url = redactUrl("https://alice:password@example.com/path?token=secret&email=x&token=other#frag"); expect(url).not.toMatch(/alice|password|secret|other|frag/); expect(new URL(url).searchParams.getAll("token")).toEqual(["[REDACTED]", "[REDACTED]"]);