Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion api-snapshots/native-abi.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion api-snapshots/native-api.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
56 changes: 45 additions & 11 deletions api-snapshots/public-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"packages": [
{
"packageName": "@scanly/core",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -84,7 +84,7 @@
},
{
"packageName": "@scanly/browser",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -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",
Expand Down Expand Up @@ -132,7 +132,7 @@
},
{
"packageName": "@scanly/node",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -146,7 +146,7 @@
},
{
"packageName": "@scanly/react",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -160,7 +160,7 @@
},
{
"packageName": "@scanly/scenario-schema",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -174,7 +174,7 @@
},
{
"packageName": "@scanly/parsers",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -188,7 +188,7 @@
},
{
"packageName": "@scanly/benchmark",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -207,7 +207,7 @@
},
{
"packageName": "@scanly/engine-jsqr",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -221,7 +221,7 @@
},
{
"packageName": "@scanly/engine-zxing-js",
"packageVersion": "2.0.1",
"packageVersion": "2.1.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -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"
}
}
]
}
18 changes: 18 additions & 0 deletions apps/web-demo/app/api/url-safety/route.ts
Original file line number Diff line number Diff line change
@@ -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"],
});
2 changes: 1 addition & 1 deletion apps/web-demo/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down
2 changes: 1 addition & 1 deletion apps/web-demo/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function Page() {
<div>
<h1 style={{ margin: 0, fontSize: 28 }}>Scanly</h1>
<div className="small">
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.
</div>
</div>
Expand Down
Loading
Loading