document-ocr 0.0.0-stage → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/index.cjs +659 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +293 -0
- package/dist/index.d.ts +293 -0
- package/dist/index.js +616 -0
- package/dist/index.js.map +1 -0
- package/hosting/.dockerignore +17 -0
- package/hosting/.gcloudignore +14 -0
- package/hosting/FEATURES.md +172 -0
- package/hosting/HOSTING.md +268 -0
- package/hosting/LICENSE +21 -0
- package/hosting/README.md +369 -0
- package/hosting/core/__init__.py +0 -0
- package/hosting/core/aadhaar_extractor.py +298 -0
- package/hosting/core/back_page_extractor.py +492 -0
- package/hosting/core/barcodes.py +125 -0
- package/hosting/core/card_recovery.py +244 -0
- package/hosting/core/document_bundle.py +82 -0
- package/hosting/core/document_classifier.py +233 -0
- package/hosting/core/document_input.py +135 -0
- package/hosting/core/document_registry.py +143 -0
- package/hosting/core/driving_licence_extractor.py +510 -0
- package/hosting/core/ead_recovery.py +54 -0
- package/hosting/core/evidence.py +54 -0
- package/hosting/core/form_recovery.py +61 -0
- package/hosting/core/jobs.py +232 -0
- package/hosting/core/kyc_ocr.py +132 -0
- package/hosting/core/kyc_validation.py +411 -0
- package/hosting/core/model_setup.py +13 -0
- package/hosting/core/mrz_parser.py +336 -0
- package/hosting/core/mrz_recovery.py +193 -0
- package/hosting/core/npr_extractor.py +410 -0
- package/hosting/core/nrega_extractor.py +827 -0
- package/hosting/core/ocr_engine.py +207 -0
- package/hosting/core/page_classifier.py +137 -0
- package/hosting/core/pan_extractor.py +183 -0
- package/hosting/core/pipeline.py +712 -0
- package/hosting/core/preprocessor.py +297 -0
- package/hosting/core/recovery_budget.py +67 -0
- package/hosting/core/redaction.py +48 -0
- package/hosting/core/review.html +244 -0
- package/hosting/core/structured_documents.py +422 -0
- package/hosting/core/travel_mrz.py +164 -0
- package/hosting/core/travel_recovery.py +187 -0
- package/hosting/core/validator.py +396 -0
- package/hosting/core/validators.py +179 -0
- package/hosting/core/voter_id_extractor.py +363 -0
- package/hosting/deploy/cloudflare/deploy.sh +44 -0
- package/hosting/deploy/cloudflare/package-lock.json +3088 -0
- package/hosting/deploy/cloudflare/package.json +29 -0
- package/hosting/deploy/cloudflare/src/gateway.ts +42 -0
- package/hosting/deploy/cloudflare/src/index.ts +21 -0
- package/hosting/deploy/cloudflare/test/gateway.test.ts +59 -0
- package/hosting/deploy/cloudflare/tsconfig.json +12 -0
- package/hosting/deploy/cloudflare/worker-configuration.d.ts +16253 -0
- package/hosting/deploy/cloudflare/wrangler.jsonc +26 -0
- package/hosting/deploy/cloudrun/cloudbuild.yaml +18 -0
- package/hosting/deploy/cloudrun/deploy.sh +71 -0
- package/hosting/deploy/cloudrun/render_service.py +47 -0
- package/hosting/deploy/common.sh +14 -0
- package/hosting/deploy/docker/Dockerfile +16 -0
- package/hosting/deploy/docker/server.py +313 -0
- package/hosting/deploy/lambda/Dockerfile.lambda +11 -0
- package/hosting/deploy/lambda/deploy.sh +22 -0
- package/hosting/deploy/lambda/handler.py +132 -0
- package/hosting/deploy/lambda/template.yaml +59 -0
- package/hosting/deploy/local/dev.sh +18 -0
- package/hosting/deploy/server/Caddyfile +5 -0
- package/hosting/deploy/server/compose.jobs.yaml +36 -0
- package/hosting/deploy/server/compose.yaml +38 -0
- package/hosting/deploy/server/deploy.sh +23 -0
- package/hosting/infra/cloudrun/main.tf +104 -0
- package/hosting/infra/cloudrun/terraform.tfvars.example +7 -0
- package/hosting/infra/cloudrun/variables.tf +55 -0
- package/hosting/infra/lambda/main.tf +63 -0
- package/hosting/infra/lambda/terraform.tfvars.example +6 -0
- package/hosting/infra/lambda/variables.tf +37 -0
- package/hosting/pyproject.toml +65 -0
- package/hosting/requirements.lock +134 -0
- package/package.json +76 -4
- package/python/core/__init__.py +0 -0
- package/python/core/aadhaar_extractor.py +298 -0
- package/python/core/back_page_extractor.py +492 -0
- package/python/core/barcodes.py +125 -0
- package/python/core/card_recovery.py +244 -0
- package/python/core/document_bundle.py +82 -0
- package/python/core/document_classifier.py +233 -0
- package/python/core/document_input.py +135 -0
- package/python/core/document_registry.py +143 -0
- package/python/core/driving_licence_extractor.py +510 -0
- package/python/core/ead_recovery.py +54 -0
- package/python/core/evidence.py +54 -0
- package/python/core/form_recovery.py +61 -0
- package/python/core/jobs.py +232 -0
- package/python/core/kyc_ocr.py +132 -0
- package/python/core/kyc_validation.py +411 -0
- package/python/core/model_setup.py +13 -0
- package/python/core/mrz_parser.py +336 -0
- package/python/core/mrz_recovery.py +193 -0
- package/python/core/npr_extractor.py +410 -0
- package/python/core/nrega_extractor.py +827 -0
- package/python/core/ocr_engine.py +207 -0
- package/python/core/page_classifier.py +137 -0
- package/python/core/pan_extractor.py +183 -0
- package/python/core/pipeline.py +712 -0
- package/python/core/preprocessor.py +297 -0
- package/python/core/recovery_budget.py +67 -0
- package/python/core/redaction.py +48 -0
- package/python/core/review.html +244 -0
- package/python/core/structured_documents.py +422 -0
- package/python/core/travel_mrz.py +164 -0
- package/python/core/travel_recovery.py +187 -0
- package/python/core/validator.py +396 -0
- package/python/core/validators.py +179 -0
- package/python/core/voter_id_extractor.py +363 -0
- package/python/pyproject.toml +34 -0
- package/python/requirements.lock +134 -0
- package/python/server.py +313 -0
- package/scripts/dococr.mjs +163 -0
- package/scripts/postinstall.mjs +173 -0
- package/scripts/sync-hosting.mjs +35 -0
- package/scripts/sync-python.sh +20 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "document-ocr-cloudflare",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "wrangler dev",
|
|
7
|
+
"deploy": "bash deploy.sh",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"check": "wrangler deploy --dry-run --containers-rollout=none --outdir .wrangler/dry-run",
|
|
10
|
+
"types": "wrangler types",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"check:image": "wrangler deploy --dry-run --outdir .wrangler/dry-run"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@cloudflare/containers": "0.3.7"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "26.6.5",
|
|
19
|
+
"typescript": "7.0.2",
|
|
20
|
+
"vitest": "5.0.5",
|
|
21
|
+
"wrangler": "4.152.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=26"
|
|
25
|
+
},
|
|
26
|
+
"overrides": {
|
|
27
|
+
"undici@>=7.0.0 <7.30.1": "7.30.1"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export async function authorized(request: Request, token: string | undefined): Promise<boolean> {
|
|
4
|
+
if (!token) return false
|
|
5
|
+
const encoder = new TextEncoder()
|
|
6
|
+
const [provided, expected] = await Promise.all([
|
|
7
|
+
crypto.subtle.digest('SHA-256', encoder.encode(request.headers.get('Authorization') ?? '')),
|
|
8
|
+
crypto.subtle.digest('SHA-256', encoder.encode(`Bearer ${token}`)),
|
|
9
|
+
])
|
|
10
|
+
return timingSafeEqual(new Uint8Array(provided), new Uint8Array(expected))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function instanceCount(value: string): number {
|
|
14
|
+
const count = Number(value)
|
|
15
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
16
|
+
throw new Error('CONTAINER_INSTANCES must be an integer from 1 to 100')
|
|
17
|
+
}
|
|
18
|
+
return count
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Keep uploads and results streaming; only the container decodes document bytes. */
|
|
22
|
+
export async function gateway(
|
|
23
|
+
request: Request,
|
|
24
|
+
token: string | undefined,
|
|
25
|
+
forward: (request: Request) => Promise<Response>,
|
|
26
|
+
): Promise<Response> {
|
|
27
|
+
if (!token) return Response.json({ error: 'AUTH_NOT_CONFIGURED' }, { status: 503 })
|
|
28
|
+
// The review shell contains no document data and accepts the bearer token in
|
|
29
|
+
// its UI. Browser navigation cannot supply that header before the UI loads.
|
|
30
|
+
const reviewShell = request.method === 'GET' && new URL(request.url).pathname === '/review'
|
|
31
|
+
if (!reviewShell && !await authorized(request, token)) {
|
|
32
|
+
return Response.json({ error: 'UNAUTHORIZED' }, { status: 401, headers: { 'WWW-Authenticate': 'Bearer' } })
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const response = await forward(request)
|
|
36
|
+
const headers = new Headers(response.headers)
|
|
37
|
+
headers.set('Cache-Control', 'no-store')
|
|
38
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers })
|
|
39
|
+
} catch {
|
|
40
|
+
return Response.json({ error: 'OCR_CONTAINER_UNAVAILABLE' }, { status: 503, headers: { 'Cache-Control': 'no-store' } })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Container, getRandom } from '@cloudflare/containers'
|
|
2
|
+
import { gateway, instanceCount } from './gateway'
|
|
3
|
+
|
|
4
|
+
export class OcrContainer extends Container<Env> {
|
|
5
|
+
defaultPort = 8000
|
|
6
|
+
sleepAfter = this.env.CONTAINER_IDLE_TIMEOUT
|
|
7
|
+
enableInternet = false
|
|
8
|
+
envVars = {
|
|
9
|
+
DOCUMENT_OCR_KYC_LANGS: this.env.DOCUMENT_OCR_KYC_LANGS,
|
|
10
|
+
API_TOKEN: this.env.API_TOKEN,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
async fetch(request, env) {
|
|
16
|
+
return gateway(request, env.API_TOKEN, async (incoming) => {
|
|
17
|
+
const container = await getRandom(env.OCR, instanceCount(env.CONTAINER_INSTANCES))
|
|
18
|
+
return container.fetch(incoming)
|
|
19
|
+
})
|
|
20
|
+
},
|
|
21
|
+
} satisfies ExportedHandler<Env>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { gateway, instanceCount } from '../src/gateway'
|
|
3
|
+
|
|
4
|
+
describe('OCR gateway', () => {
|
|
5
|
+
it('loads only the public review shell before a browser supplies its token', async () => {
|
|
6
|
+
const forward = vi.fn(async () => new Response('<html>Token entry</html>'))
|
|
7
|
+
const response = await gateway(new Request('https://ocr.test/review'), 'correct', forward)
|
|
8
|
+
expect(response.status).toBe(200)
|
|
9
|
+
expect(response.headers.get('Cache-Control')).toBe('no-store')
|
|
10
|
+
expect(forward).toHaveBeenCalledOnce()
|
|
11
|
+
for (const [path, method] of [['/review', 'POST'], ['/preview', 'POST'], ['/documents', 'GET'], ['/jobs/id', 'GET']]) {
|
|
12
|
+
const denied = await gateway(new Request(`https://ocr.test${path}`, { method }), 'correct', forward)
|
|
13
|
+
expect(denied.status).toBe(401)
|
|
14
|
+
}
|
|
15
|
+
expect(forward).toHaveBeenCalledOnce()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects a missing server secret without starting a container', async () => {
|
|
19
|
+
const forward = vi.fn()
|
|
20
|
+
const response = await gateway(new Request('https://ocr.test/scan'), undefined, forward)
|
|
21
|
+
expect(response.status).toBe(503)
|
|
22
|
+
expect(forward).not.toHaveBeenCalled()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it.each([null, 'Bearer wrong', 'Basic correct'])('rejects invalid credentials (%s) before routing', async (authorization) => {
|
|
26
|
+
const headers = authorization ? { Authorization: authorization } : {}
|
|
27
|
+
const forward = vi.fn()
|
|
28
|
+
const response = await gateway(new Request('https://ocr.test/scan', { headers }), 'correct', forward)
|
|
29
|
+
expect(response.status).toBe(401)
|
|
30
|
+
expect(forward).not.toHaveBeenCalled()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('forwards authenticated multipart requests and preserves partial-result status', async () => {
|
|
34
|
+
const form = new FormData()
|
|
35
|
+
form.append('image', new Blob(['synthetic'], { type: 'image/png' }), 'image.png')
|
|
36
|
+
const request = new Request('https://ocr.test/scan/document?type=pan', {
|
|
37
|
+
method: 'POST', headers: { Authorization: 'Bearer correct' }, body: form,
|
|
38
|
+
})
|
|
39
|
+
const forward = vi.fn(async (received: Request) => {
|
|
40
|
+
expect(received).toBe(request)
|
|
41
|
+
expect((await received.formData()).get('image')).toBeInstanceOf(Blob)
|
|
42
|
+
return Response.json({ status: 'failure' }, { status: 422 })
|
|
43
|
+
})
|
|
44
|
+
const response = await gateway(request, 'correct', forward)
|
|
45
|
+
expect(response.status).toBe(422)
|
|
46
|
+
expect(response.headers.get('Cache-Control')).toBe('no-store')
|
|
47
|
+
expect(await response.json()).toEqual({ status: 'failure' })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns a retryable error when container startup fails', async () => {
|
|
51
|
+
const request = new Request('https://ocr.test/ready', { headers: { Authorization: 'Bearer correct' } })
|
|
52
|
+
const response = await gateway(request, 'correct', async () => { throw new Error('unavailable') })
|
|
53
|
+
expect(response.status).toBe(503)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it.each(['0', '-1', '1.5', 'NaN', '101'])('rejects invalid pool size %s', (value) => {
|
|
57
|
+
expect(() => instanceCount(value)).toThrow('CONTAINER_INSTANCES')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"types": ["./worker-configuration.d.ts", "node"]
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*.ts", "worker-configuration.d.ts"]
|
|
12
|
+
}
|