@arcships/light-ocr-runtime 0.1.6 → 0.1.8

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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  },
5
5
  "description": "Model-free Node.js runtime for light-ocr",
6
6
  "engines": {
7
- "node": "^22.0.0 || ^24.0.0"
7
+ "node": ">=22.0.0"
8
8
  },
9
9
  "exports": {
10
10
  ".": {
@@ -28,12 +28,14 @@
28
28
  "module": "./src/index.mjs",
29
29
  "name": "@arcships/light-ocr-runtime",
30
30
  "optionalDependencies": {
31
- "@arcships/light-ocr-darwin-arm64": "0.5.6",
32
- "@arcships/light-ocr-darwin-x64": "0.5.6",
33
- "@arcships/light-ocr-linux-arm64-gnu": "0.5.6",
34
- "@arcships/light-ocr-linux-x64-gnu": "0.5.6",
35
- "@arcships/light-ocr-win32-arm64": "0.5.6",
36
- "@arcships/light-ocr-win32-x64": "0.5.6"
31
+ "@arcships/light-ocr-darwin-arm64": "0.5.8",
32
+ "@arcships/light-ocr-darwin-x64": "0.5.8",
33
+ "@arcships/light-ocr-linux-arm64-gnu": "0.5.8",
34
+ "@arcships/light-ocr-linux-arm64-musl": "0.5.8",
35
+ "@arcships/light-ocr-linux-x64-gnu": "0.5.8",
36
+ "@arcships/light-ocr-linux-x64-musl": "0.5.8",
37
+ "@arcships/light-ocr-win32-arm64": "0.5.8",
38
+ "@arcships/light-ocr-win32-x64": "0.5.8"
37
39
  },
38
40
  "publishConfig": {
39
41
  "access": "public",
@@ -45,5 +47,5 @@
45
47
  },
46
48
  "type": "commonjs",
47
49
  "types": "./src/index.d.ts",
48
- "version": "0.1.6"
50
+ "version": "0.1.8"
49
51
  }
package/src/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { loadNative } = require('./load-native.cjs');
3
+ const { loadNative, platformIdentity } = require('./load-native.cjs');
4
4
 
5
5
  class OcrError extends Error {
6
6
  constructor(code, message, detail) {
@@ -174,4 +174,4 @@ async function createEngine(options) {
174
174
  }
175
175
  }
176
176
 
177
- module.exports = { createEngine, OcrError, loadNative };
177
+ module.exports = { createEngine, OcrError, loadNative, platformIdentity };
package/src/index.d.ts CHANGED
@@ -289,4 +289,15 @@ export interface DetectionResult {
289
289
  readonly timingUs: TimingUs;
290
290
  }
291
291
 
292
+ /** Resolved host platform identity used to select the native package. */
293
+ export interface PlatformIdentity {
294
+ readonly id: string;
295
+ readonly os: string;
296
+ readonly architecture: string;
297
+ /** Present for Linux hosts; `"glibc"` or `"musl"`. */
298
+ readonly libc?: 'glibc' | 'musl';
299
+ }
300
+
301
+ export function platformIdentity(): PlatformIdentity;
302
+
292
303
  export function createEngine(options: CreateEngineOptions): Promise<OcrEngine>;
package/src/index.mjs CHANGED
@@ -2,3 +2,4 @@ import cjs from './index.cjs';
2
2
 
3
3
  export const createEngine = cjs.createEngine;
4
4
  export const OcrError = cjs.OcrError;
5
+ export const platformIdentity = cjs.platformIdentity;
@@ -1,9 +1,101 @@
1
1
  'use strict';
2
2
 
3
+ const { spawnSync } = require('node:child_process');
3
4
  const crypto = require('node:crypto');
4
5
  const fs = require('node:fs');
5
6
  const path = require('node:path');
6
7
 
8
+ // macOS-only integrity relaxation for downstream re-signing.
9
+ //
10
+ // macOS packaging pipelines re-sign the native payload with a Developer ID
11
+ // (or ad-hoc) identity, and osx-sign rewrites the LC_CODE_SIGNATURE blob of
12
+ // every Mach-O under Contents --force. That changes both file size and
13
+ // sha256, so the strict descriptor comparison alone rejects otherwise intact
14
+ // binaries. On macOS only, a Mach-O whose signature verifies AND whose
15
+ // signing identity matches the host process is accepted as an equivalent
16
+ // integrity proof: same TeamIdentifier as process.execPath, or both sides
17
+ // ad-hoc signed. Ad-hoc signatures are reproducible by anyone, so this
18
+ // relaxation stays macOS-only and documented; win32/linux payloads are never
19
+ // re-signed and keep the strict bytes+sha256 gate.
20
+
21
+ const MACHO_MAGIC = new Set([
22
+ 0xfeedface, // 32-bit big-endian
23
+ 0xcefaedfe, // 32-bit little-endian
24
+ 0xfeedfacf, // 64-bit big-endian
25
+ 0xcffaedfe, // 64-bit little-endian
26
+ 0xcafebabe, // universal (fat) big-endian
27
+ 0xbebafeca, // universal (fat) little-endian
28
+ ]);
29
+
30
+ function isMachO(filename) {
31
+ const fd = fs.openSync(filename, 'r');
32
+ try {
33
+ const magic = Buffer.allocUnsafe(4);
34
+ if (fs.readSync(fd, magic, 0, 4, 0) !== 4) return false;
35
+ return MACHO_MAGIC.has(magic.readUInt32BE(0));
36
+ } finally {
37
+ fs.closeSync(fd);
38
+ }
39
+ }
40
+
41
+ function codesignOutput(filename) {
42
+ const result = spawnSync('codesign', ['-dv', '--verbose=4', filename], {
43
+ encoding: 'utf8',
44
+ stdio: ['ignore', 'pipe', 'pipe'],
45
+ });
46
+ if (result.error || result.status !== 0) return null;
47
+ return `${result.stdout}\n${result.stderr}`;
48
+ }
49
+
50
+ // Returns { teamIdentifier, adhoc } for a readable signature, or null when
51
+ // the file carries no readable signature (unsigned, corrupt, not Mach-O).
52
+ function codesignIdentity(filename) {
53
+ const output = codesignOutput(filename);
54
+ if (!output) return null;
55
+ if (/^Signature=adhoc\s*$/m.test(output)) {
56
+ return { teamIdentifier: null, adhoc: true };
57
+ }
58
+ const teamLine = output.match(/^TeamIdentifier=(.*)$/m);
59
+ const teamIdentifier = teamLine ? teamLine[1].trim() : '';
60
+ if (teamIdentifier && teamIdentifier !== 'not set') {
61
+ return { teamIdentifier, adhoc: false };
62
+ }
63
+ return null;
64
+ }
65
+
66
+ function codesignVerifies(filename) {
67
+ const result = spawnSync('codesign', ['--verify', '--strict', filename], {
68
+ encoding: 'utf8',
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ });
71
+ return !result.error && result.status === 0;
72
+ }
73
+
74
+ let hostCodesignIdentityCache;
75
+
76
+ function hostCodesignIdentity() {
77
+ if (hostCodesignIdentityCache === undefined) {
78
+ hostCodesignIdentityCache = codesignIdentity(process.execPath);
79
+ }
80
+ return hostCodesignIdentityCache;
81
+ }
82
+
83
+ // Accept a re-signature only when it was made with the same identity as the
84
+ // host process: identical TeamIdentifier, or both sides ad-hoc signed.
85
+ function signerMatchesHost(artifactIdentity, host = hostCodesignIdentity()) {
86
+ if (!artifactIdentity || !host) return false;
87
+ if (artifactIdentity.adhoc && host.adhoc) return true;
88
+ return !artifactIdentity.adhoc && !host.adhoc &&
89
+ artifactIdentity.teamIdentifier === host.teamIdentifier;
90
+ }
91
+
92
+ function acceptsSignedMacOSMutation(filename) {
93
+ if (process.platform !== 'darwin') return false;
94
+ if (!isMachO(filename)) return false;
95
+ if (!codesignVerifies(filename)) return false;
96
+ return signerMatchesHost(codesignIdentity(filename));
97
+ }
98
+
7
99
  function adapterError(code, message, detail, cause) {
8
100
  const error = new Error(message, cause === undefined ? undefined : { cause });
9
101
  error.name = 'OcrError';
@@ -12,6 +104,22 @@ function adapterError(code, message, detail, cause) {
12
104
  return error;
13
105
  }
14
106
 
107
+ // Detect musl Linux (Alpine and friends). Only reached when the host does not
108
+ // report glibcVersionRuntime through process.report. The musl dynamic linker
109
+ // path is the primary signal; the ldd output check covers hosts with a
110
+ // non-standard loader location.
111
+ function isMuslLinux() {
112
+ if (process.platform !== 'linux') return false;
113
+ const loader = process.arch === 'arm64'
114
+ ? '/lib/ld-musl-aarch64.so.1'
115
+ : '/lib/ld-musl-x86_64.so.1';
116
+ if (fs.existsSync(loader)) return true;
117
+ const ldd = spawnSync('ldd', ['--version'], { encoding: 'utf8', timeout: 5000 });
118
+ if (ldd.error) return false;
119
+ const output = `${ldd.stdout || ''}${ldd.stderr || ''}`;
120
+ return output.toLowerCase().includes('musl');
121
+ }
122
+
15
123
  function platformIdentity() {
16
124
  const key = `${process.platform}-${process.arch}`;
17
125
  const identities = {
@@ -25,9 +133,12 @@ function platformIdentity() {
25
133
  if (report?.header?.glibcVersionRuntime) {
26
134
  return { id: 'linux-x64', os: 'linux', architecture: 'x86_64', libc: 'glibc' };
27
135
  }
136
+ if (isMuslLinux()) {
137
+ return { id: 'linux-x64-musl', os: 'linux', architecture: 'x86_64', libc: 'musl' };
138
+ }
28
139
  throw adapterError(
29
140
  'unsupported_platform',
30
- 'light-ocr currently supports Linux x64 with glibc only',
141
+ 'light-ocr currently supports Linux x64 with glibc or musl only',
31
142
  key,
32
143
  );
33
144
  }
@@ -36,9 +147,12 @@ function platformIdentity() {
36
147
  if (report?.header?.glibcVersionRuntime) {
37
148
  return { id: 'linux-arm64', os: 'linux', architecture: 'arm64', libc: 'glibc' };
38
149
  }
150
+ if (isMuslLinux()) {
151
+ return { id: 'linux-arm64-musl', os: 'linux', architecture: 'arm64', libc: 'musl' };
152
+ }
39
153
  throw adapterError(
40
154
  'unsupported_platform',
41
- 'light-ocr currently supports Linux arm64 with glibc only',
155
+ 'light-ocr currently supports Linux arm64 with glibc or musl only',
42
156
  key,
43
157
  );
44
158
  }
@@ -57,6 +171,8 @@ function platformPackage() {
57
171
  'windows-x64': '@arcships/light-ocr-win32-x64',
58
172
  'linux-x64': '@arcships/light-ocr-linux-x64-gnu',
59
173
  'linux-arm64': '@arcships/light-ocr-linux-arm64-gnu',
174
+ 'linux-x64-musl': '@arcships/light-ocr-linux-x64-musl',
175
+ 'linux-arm64-musl': '@arcships/light-ocr-linux-arm64-musl',
60
176
  };
61
177
  return packages[platformIdentity().id];
62
178
  }
@@ -108,12 +224,23 @@ function verifyArtifact(root, artifact, field) {
108
224
  if (!stats.isFile() || stats.isSymbolicLink()) {
109
225
  throw adapterError('package_load_failed', 'Descriptor artifact is not a regular file', artifact.path);
110
226
  }
111
- if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 1 || stats.size !== artifact.bytes) {
227
+ if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 1) {
112
228
  throw adapterError('package_load_failed', 'Descriptor artifact byte count mismatch', artifact.path);
113
229
  }
114
- if (!/^[a-f0-9]{64}$/.test(artifact.sha256 || '') || sha256(filename) !== artifact.sha256) {
230
+ if (!/^[a-f0-9]{64}$/.test(artifact.sha256 || '')) {
115
231
  throw adapterError('package_load_failed', 'Descriptor artifact hash mismatch', artifact.path);
116
232
  }
233
+ if (stats.size !== artifact.bytes) {
234
+ if (!acceptsSignedMacOSMutation(filename)) {
235
+ throw adapterError('package_load_failed', 'Descriptor artifact byte count mismatch', artifact.path);
236
+ }
237
+ return filename;
238
+ }
239
+ if (sha256(filename) !== artifact.sha256) {
240
+ if (!acceptsSignedMacOSMutation(filename)) {
241
+ throw adapterError('package_load_failed', 'Descriptor artifact hash mismatch', artifact.path);
242
+ }
243
+ }
117
244
  return filename;
118
245
  }
119
246
 
@@ -496,4 +623,11 @@ function loadNative() {
496
623
  }
497
624
  }
498
625
 
499
- module.exports = { loadNative, validateRuntimeDescriptor };
626
+ const macOSSignature = Object.freeze({
627
+ isMachO,
628
+ codesignIdentity,
629
+ codesignVerifies,
630
+ signerMatchesHost,
631
+ });
632
+
633
+ module.exports = { loadNative, validateRuntimeDescriptor, macOSSignature, platformIdentity };
package/src/metadata.cjs CHANGED
@@ -1,3 +1,3 @@
1
1
  'use strict';
2
2
 
3
- module.exports = Object.freeze({ coreVersion: '0.5.6' });
3
+ module.exports = Object.freeze({ coreVersion: '0.5.8' });