kxco-verify 1.2.0 → 1.2.2

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
@@ -1,87 +1,87 @@
1
- {
2
- "name": "kxco-verify",
3
- "version": "1.2.0",
4
- "description": "Standalone, browser-safe verifier for KXCO ML-DSA-65 post-quantum signed attestations and credentials, for auditors, regulators, counterparties, and anyone who needs to confirm a signature without running the full KXCO SDK.",
5
- "keywords": [
6
- "post-quantum",
7
- "pqc",
8
- "ml-dsa",
9
- "dilithium",
10
- "nist",
11
- "fips-204",
12
- "verification",
13
- "attestation",
14
- "verify",
15
- "kxco-post-quantum",
16
- "browser",
17
- "browser-safe",
18
- "zero-dependencies",
19
- "supply-chain",
20
- "deploy-attestation",
21
- "provenance",
22
- "manifest-verification"
23
- ],
24
- "license": "Apache-2.0",
25
- "author": "Shayne Heffernan and John Heffernan",
26
- "contributors": [
27
- {
28
- "name": "Shayne Heffernan"
29
- },
30
- {
31
- "name": "John Heffernan"
32
- }
33
- ],
34
- "homepage": "https://verify.kxco.ai",
35
- "funding": "https://kxco.ai",
36
- "repository": {
37
- "type": "git",
38
- "url": "https://github.com/KnightsbridgeAIQ/kxco-verify.git"
39
- },
40
- "bugs": {
41
- "url": "https://github.com/KnightsbridgeAIQ/kxco-verify/issues"
42
- },
43
- "type": "module",
44
- "sideEffects": false,
45
- "main": "./src/index.js",
46
- "types": "./src/index.d.ts",
47
- "exports": {
48
- ".": {
49
- "types": "./src/index.d.ts",
50
- "import": "./src/index.js"
51
- },
52
- "./parse": {
53
- "types": "./src/parse.d.ts",
54
- "import": "./src/parse.js"
55
- },
56
- "./verify": {
57
- "types": "./src/verify.d.ts",
58
- "import": "./src/verify.js"
59
- },
60
- "./fetch": {
61
- "types": "./src/fetch.d.ts",
62
- "import": "./src/fetch.js"
63
- }
64
- },
65
- "files": [
66
- "src",
67
- "README.md",
68
- "LICENSE",
69
- "CHANGELOG.md"
70
- ],
71
- "engines": {
72
- "node": ">=20.19"
73
- },
74
- "dependencies": {
75
- "@noble/post-quantum": "0.7.0"
76
- },
77
- "scripts": {
78
- "test": "node --test --test-reporter=spec test/parse.test.js test/verify.test.js test/fetch.test.js test/index.test.js",
79
- "test:cov": "node --test --experimental-test-coverage test/parse.test.js test/verify.test.js test/fetch.test.js test/index.test.js",
80
- "smoke": "node scripts/smoke.js",
81
- "lint": "node -c src/index.js && node -c src/parse.js && node -c src/verify.js && node -c src/fetch.js"
82
- },
83
- "publishConfig": {
84
- "provenance": true,
85
- "access": "public"
86
- }
87
- }
1
+ {
2
+ "name": "kxco-verify",
3
+ "version": "1.2.2",
4
+ "description": "Standalone, browser-safe verifier for KXCO ML-DSA-65 post-quantum signed attestations and credentials, for auditors, regulators, counterparties, and anyone who needs to confirm a signature without running the full KXCO SDK.",
5
+ "keywords": [
6
+ "post-quantum",
7
+ "pqc",
8
+ "ml-dsa",
9
+ "dilithium",
10
+ "nist",
11
+ "fips-204",
12
+ "verification",
13
+ "attestation",
14
+ "verify",
15
+ "kxco-post-quantum",
16
+ "browser",
17
+ "browser-safe",
18
+ "zero-dependencies",
19
+ "supply-chain",
20
+ "deploy-attestation",
21
+ "provenance",
22
+ "manifest-verification"
23
+ ],
24
+ "license": "Apache-2.0",
25
+ "author": "Shayne Heffernan and John Heffernan",
26
+ "contributors": [
27
+ {
28
+ "name": "Shayne Heffernan"
29
+ },
30
+ {
31
+ "name": "John Heffernan"
32
+ }
33
+ ],
34
+ "homepage": "https://verify.kxco.ai",
35
+ "funding": "https://kxco.ai",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/KnightsbridgeAIQ/kxco-verify.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/KnightsbridgeAIQ/kxco-verify/issues"
42
+ },
43
+ "type": "module",
44
+ "sideEffects": false,
45
+ "main": "./src/index.js",
46
+ "types": "./src/index.d.ts",
47
+ "exports": {
48
+ ".": {
49
+ "types": "./src/index.d.ts",
50
+ "import": "./src/index.js"
51
+ },
52
+ "./parse": {
53
+ "types": "./src/parse.d.ts",
54
+ "import": "./src/parse.js"
55
+ },
56
+ "./verify": {
57
+ "types": "./src/verify.d.ts",
58
+ "import": "./src/verify.js"
59
+ },
60
+ "./fetch": {
61
+ "types": "./src/fetch.d.ts",
62
+ "import": "./src/fetch.js"
63
+ }
64
+ },
65
+ "files": [
66
+ "src",
67
+ "README.md",
68
+ "LICENSE",
69
+ "CHANGELOG.md"
70
+ ],
71
+ "engines": {
72
+ "node": ">=20.19"
73
+ },
74
+ "dependencies": {
75
+ "@noble/post-quantum": "0.7.0"
76
+ },
77
+ "scripts": {
78
+ "test": "node --test --test-reporter=spec test/parse.test.js test/verify.test.js test/fetch.test.js test/index.test.js",
79
+ "test:cov": "node --test --experimental-test-coverage test/parse.test.js test/verify.test.js test/fetch.test.js test/index.test.js",
80
+ "smoke": "node scripts/smoke.js",
81
+ "lint": "node -c src/index.js && node -c src/parse.js && node -c src/verify.js && node -c src/fetch.js"
82
+ },
83
+ "publishConfig": {
84
+ "provenance": true,
85
+ "access": "public"
86
+ }
87
+ }
package/src/fetch.d.ts CHANGED
@@ -1,28 +1,28 @@
1
- export interface FetchOk {
2
- ok: true
3
- url: string
4
- status: number
5
- body: string
6
- }
7
-
8
- export interface FetchErr {
9
- ok: false
10
- error: {
11
- kind: 'fetch'
12
- code: 'no_fetch' | 'invalid_url' | 'timeout' | 'network' | 'http_status' | 'too_large' | 'read'
13
- message: string
14
- url?: string
15
- status?: number
16
- }
17
- }
18
-
19
- export interface GetJsonBodyOpts {
20
- timeoutMs?: number
21
- maxBytes?: number
22
- fetchImpl?: typeof fetch
23
- }
24
-
25
- export function getJsonBody(
26
- url: string,
27
- opts?: GetJsonBodyOpts,
28
- ): Promise<FetchOk | FetchErr>
1
+ export interface FetchOk {
2
+ ok: true
3
+ url: string
4
+ status: number
5
+ body: string
6
+ }
7
+
8
+ export interface FetchErr {
9
+ ok: false
10
+ error: {
11
+ kind: 'fetch'
12
+ code: 'no_fetch' | 'invalid_url' | 'timeout' | 'network' | 'http_status' | 'too_large' | 'read'
13
+ message: string
14
+ url?: string
15
+ status?: number
16
+ }
17
+ }
18
+
19
+ export interface GetJsonBodyOpts {
20
+ timeoutMs?: number
21
+ maxBytes?: number
22
+ fetchImpl?: typeof fetch
23
+ }
24
+
25
+ export function getJsonBody(
26
+ url: string,
27
+ opts?: GetJsonBodyOpts,
28
+ ): Promise<FetchOk | FetchErr>
package/src/fetch.js CHANGED
@@ -1,100 +1,100 @@
1
- // Network fetching. Browser-safe (uses global `fetch`), with a hard timeout
2
- // per request via AbortController so a slow target site cannot hang the
3
- // verifier. Returns either { ok: true, body, url, status } or
4
- // { ok: false, error: { kind: 'fetch', code, message } }.
5
-
6
- const DEFAULT_TIMEOUT_MS = 3000
7
- const DEFAULT_MAX_BYTES = 200_000 // attestation manifests are ~11KB; cap at 200KB
8
-
9
- /**
10
- * @typedef {Object} FetchOk
11
- * @property {true} ok
12
- * @property {string} url
13
- * @property {number} status
14
- * @property {string} body
15
- *
16
- * @typedef {Object} FetchErr
17
- * @property {false} ok
18
- * @property {{ kind: 'fetch', code: string, message: string, url?: string, status?: number }} error
19
- */
20
-
21
- /**
22
- * GET a URL with a timeout, returning the body as a UTF-8 string. Caps the
23
- * response at maxBytes to defend against memory blow-ups from a hostile
24
- * target. Does NOT throw — always returns a tagged result.
25
- *
26
- * @param {string} url
27
- * @param {{ timeoutMs?: number, maxBytes?: number, fetchImpl?: typeof fetch }} [opts]
28
- * @returns {Promise<FetchOk | FetchErr>}
29
- */
30
- export async function getJsonBody(url, opts = {}) {
31
- const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : DEFAULT_TIMEOUT_MS
32
- const maxBytes = typeof opts.maxBytes === 'number' ? opts.maxBytes : DEFAULT_MAX_BYTES
33
- const f = opts.fetchImpl || globalThis.fetch
34
-
35
- if (typeof f !== 'function') {
36
- return errFetch_('no_fetch', 'global fetch() not available; pass opts.fetchImpl', { url })
37
- }
38
- if (typeof url !== 'string' || (!url.startsWith('http://') && !url.startsWith('https://'))) {
39
- return errFetch_('invalid_url', 'url must be an absolute http(s) URL', { url })
40
- }
41
-
42
- const ctrl = new AbortController()
43
- const timer = setTimeout(() => ctrl.abort(), timeoutMs)
44
- let res
45
- try {
46
- res = await f(url, { signal: ctrl.signal, redirect: 'follow', headers: { 'Accept': 'application/json' } })
47
- } catch (err) {
48
- clearTimeout(timer)
49
- const code = err && err.name === 'AbortError' ? 'timeout' : 'network'
50
- return errFetch_(code, code === 'timeout' ? `fetch timed out after ${timeoutMs}ms` : `network error: ${err.message}`, { url })
51
- }
52
- clearTimeout(timer)
53
-
54
- if (!res.ok) {
55
- return errFetch_('http_status', `HTTP ${res.status}`, { url, status: res.status })
56
- }
57
-
58
- // Read with a byte cap to prevent OOM on hostile responses.
59
- let body
60
- try {
61
- body = await readWithCap_(res, maxBytes)
62
- } catch (err) {
63
- if (err && err.code === 'too_large') {
64
- return errFetch_('too_large', `response exceeded ${maxBytes} bytes`, { url, status: res.status })
65
- }
66
- return errFetch_('read', `failed to read response body: ${err.message}`, { url, status: res.status })
67
- }
68
-
69
- return { ok: true, url, status: res.status, body }
70
- }
71
-
72
- async function readWithCap_(res, maxBytes) {
73
- if (!res.body || typeof res.body.getReader !== 'function') {
74
- // Fallback for environments without streams: read the whole text, check size.
75
- const text = await res.text()
76
- if (text.length > maxBytes * 4) { const e = new Error('too large'); e.code = 'too_large'; throw e }
77
- return text
78
- }
79
- const reader = res.body.getReader()
80
- const chunks = []
81
- let total = 0
82
- while (true) {
83
- const { done, value } = await reader.read()
84
- if (done) break
85
- total += value.length
86
- if (total > maxBytes) {
87
- try { await reader.cancel() } catch {}
88
- const e = new Error('too large'); e.code = 'too_large'; throw e
89
- }
90
- chunks.push(value)
91
- }
92
- const merged = new Uint8Array(total)
93
- let off = 0
94
- for (const c of chunks) { merged.set(c, off); off += c.length }
95
- return new TextDecoder('utf-8').decode(merged)
96
- }
97
-
98
- function errFetch_(code, message, extra = {}) {
99
- return { ok: false, error: { kind: 'fetch', code, message, ...extra } }
100
- }
1
+ // Network fetching. Browser-safe (uses global `fetch`), with a hard timeout
2
+ // per request via AbortController so a slow target site cannot hang the
3
+ // verifier. Returns either { ok: true, body, url, status } or
4
+ // { ok: false, error: { kind: 'fetch', code, message } }.
5
+
6
+ const DEFAULT_TIMEOUT_MS = 3000
7
+ const DEFAULT_MAX_BYTES = 200_000 // attestation manifests are ~11KB; cap at 200KB
8
+
9
+ /**
10
+ * @typedef {Object} FetchOk
11
+ * @property {true} ok
12
+ * @property {string} url
13
+ * @property {number} status
14
+ * @property {string} body
15
+ *
16
+ * @typedef {Object} FetchErr
17
+ * @property {false} ok
18
+ * @property {{ kind: 'fetch', code: string, message: string, url?: string, status?: number }} error
19
+ */
20
+
21
+ /**
22
+ * GET a URL with a timeout, returning the body as a UTF-8 string. Caps the
23
+ * response at maxBytes to defend against memory blow-ups from a hostile
24
+ * target. Does NOT throw — always returns a tagged result.
25
+ *
26
+ * @param {string} url
27
+ * @param {{ timeoutMs?: number, maxBytes?: number, fetchImpl?: typeof fetch }} [opts]
28
+ * @returns {Promise<FetchOk | FetchErr>}
29
+ */
30
+ export async function getJsonBody(url, opts = {}) {
31
+ const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : DEFAULT_TIMEOUT_MS
32
+ const maxBytes = typeof opts.maxBytes === 'number' ? opts.maxBytes : DEFAULT_MAX_BYTES
33
+ const f = opts.fetchImpl || globalThis.fetch
34
+
35
+ if (typeof f !== 'function') {
36
+ return errFetch_('no_fetch', 'global fetch() not available; pass opts.fetchImpl', { url })
37
+ }
38
+ if (typeof url !== 'string' || (!url.startsWith('http://') && !url.startsWith('https://'))) {
39
+ return errFetch_('invalid_url', 'url must be an absolute http(s) URL', { url })
40
+ }
41
+
42
+ const ctrl = new AbortController()
43
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs)
44
+ let res
45
+ try {
46
+ res = await f(url, { signal: ctrl.signal, redirect: 'follow', headers: { 'Accept': 'application/json' } })
47
+ } catch (err) {
48
+ clearTimeout(timer)
49
+ const code = err && err.name === 'AbortError' ? 'timeout' : 'network'
50
+ return errFetch_(code, code === 'timeout' ? `fetch timed out after ${timeoutMs}ms` : `network error: ${err.message}`, { url })
51
+ }
52
+ clearTimeout(timer)
53
+
54
+ if (!res.ok) {
55
+ return errFetch_('http_status', `HTTP ${res.status}`, { url, status: res.status })
56
+ }
57
+
58
+ // Read with a byte cap to prevent OOM on hostile responses.
59
+ let body
60
+ try {
61
+ body = await readWithCap_(res, maxBytes)
62
+ } catch (err) {
63
+ if (err && err.code === 'too_large') {
64
+ return errFetch_('too_large', `response exceeded ${maxBytes} bytes`, { url, status: res.status })
65
+ }
66
+ return errFetch_('read', `failed to read response body: ${err.message}`, { url, status: res.status })
67
+ }
68
+
69
+ return { ok: true, url, status: res.status, body }
70
+ }
71
+
72
+ async function readWithCap_(res, maxBytes) {
73
+ if (!res.body || typeof res.body.getReader !== 'function') {
74
+ // Fallback for environments without streams: read the whole text, check size.
75
+ const text = await res.text()
76
+ if (text.length > maxBytes * 4) { const e = new Error('too large'); e.code = 'too_large'; throw e }
77
+ return text
78
+ }
79
+ const reader = res.body.getReader()
80
+ const chunks = []
81
+ let total = 0
82
+ while (true) {
83
+ const { done, value } = await reader.read()
84
+ if (done) break
85
+ total += value.length
86
+ if (total > maxBytes) {
87
+ try { await reader.cancel() } catch {}
88
+ const e = new Error('too large'); e.code = 'too_large'; throw e
89
+ }
90
+ chunks.push(value)
91
+ }
92
+ const merged = new Uint8Array(total)
93
+ let off = 0
94
+ for (const c of chunks) { merged.set(c, off); off += c.length }
95
+ return new TextDecoder('utf-8').decode(merged)
96
+ }
97
+
98
+ function errFetch_(code, message, extra = {}) {
99
+ return { ok: false, error: { kind: 'fetch', code, message, ...extra } }
100
+ }
package/src/index.d.ts CHANGED
@@ -1,57 +1,57 @@
1
- export type VerifyState = 'valid' | 'rotated' | 'invalid' | 'error'
2
-
3
- export interface VerifyResultError {
4
- kind: 'parse' | 'fetch' | 'signature' | 'consistency' | 'rotation'
5
- code: string
6
- message: string
7
- /** true when the error is non-fatal (math succeeded but a soft check failed). */
8
- soft?: boolean
9
- [k: string]: unknown
10
- }
11
-
12
- export interface VerifyResult {
13
- state: VerifyState
14
- algorithm?: 'ML-DSA-65'
15
- /** kid as declared inside the manifest itself */
16
- manifestKid?: string
17
- /** kid currently served at the live well-known pubkey endpoint (when fetched) */
18
- livePubkeyKid?: string
19
- /** site identifier as declared by the manifest */
20
- site?: string
21
- /** opaque deployment metadata from the manifest */
22
- deployment?: Record<string, unknown>
23
- /** full parsed manifest JSON, for UI display */
24
- manifestRaw?: Record<string, unknown>
25
- /** present when state is "error", "invalid", or "rotated" */
26
- error?: VerifyResultError
27
- attestationUrl?: string
28
- /** URL of the live well-known pubkey endpoint (only present when fetched) */
29
- pubkeyUrl?: string
30
- /** Date.now() at the moment verification finished */
31
- verifiedAtMs?: number
32
- }
33
-
34
- export interface VerifyUrlOpts {
35
- timeoutMs?: number
36
- maxBytes?: number
37
- fetchImpl?: typeof fetch
38
- /** When true, skip fetching the live well-known pubkey (no rotation detection). */
39
- skipLivePubkey?: boolean
40
- }
41
-
42
- /**
43
- * Verify an attestation manifest you already have in hand.
44
- * Result is "valid" / "invalid" / "error" — never "rotated" (no live fetch).
45
- */
46
- export function verifyManifest(manifestBody: string | object): Promise<VerifyResult>
47
-
48
- /**
49
- * Verify an attestation by URL. May return "rotated" if the live well-known
50
- * pubkey endpoint serves a different kid than the manifest declared.
51
- */
52
- export function verifyUrl(attestationUrl: string, opts?: VerifyUrlOpts): Promise<VerifyResult>
53
-
54
- // Re-exports.
55
- export { parseManifest, ParseResult, ParsedManifest, ParseError } from './parse.js'
56
- export { verifySignature, computeKid, hexToBytes, bytesToHex, hexEquals } from './verify.js'
57
- export { getJsonBody, FetchOk, FetchErr, GetJsonBodyOpts } from './fetch.js'
1
+ export type VerifyState = 'valid' | 'rotated' | 'invalid' | 'error'
2
+
3
+ export interface VerifyResultError {
4
+ kind: 'parse' | 'fetch' | 'signature' | 'consistency' | 'rotation'
5
+ code: string
6
+ message: string
7
+ /** true when the error is non-fatal (math succeeded but a soft check failed). */
8
+ soft?: boolean
9
+ [k: string]: unknown
10
+ }
11
+
12
+ export interface VerifyResult {
13
+ state: VerifyState
14
+ algorithm?: 'ML-DSA-65'
15
+ /** kid as declared inside the manifest itself */
16
+ manifestKid?: string
17
+ /** kid currently served at the live well-known pubkey endpoint (when fetched) */
18
+ livePubkeyKid?: string
19
+ /** site identifier as declared by the manifest */
20
+ site?: string
21
+ /** opaque deployment metadata from the manifest */
22
+ deployment?: Record<string, unknown>
23
+ /** full parsed manifest JSON, for UI display */
24
+ manifestRaw?: Record<string, unknown>
25
+ /** present when state is "error", "invalid", or "rotated" */
26
+ error?: VerifyResultError
27
+ attestationUrl?: string
28
+ /** URL of the live well-known pubkey endpoint (only present when fetched) */
29
+ pubkeyUrl?: string
30
+ /** Date.now() at the moment verification finished */
31
+ verifiedAtMs?: number
32
+ }
33
+
34
+ export interface VerifyUrlOpts {
35
+ timeoutMs?: number
36
+ maxBytes?: number
37
+ fetchImpl?: typeof fetch
38
+ /** When true, skip fetching the live well-known pubkey (no rotation detection). */
39
+ skipLivePubkey?: boolean
40
+ }
41
+
42
+ /**
43
+ * Verify an attestation manifest you already have in hand.
44
+ * Result is "valid" / "invalid" / "error" — never "rotated" (no live fetch).
45
+ */
46
+ export function verifyManifest(manifestBody: string | object): Promise<VerifyResult>
47
+
48
+ /**
49
+ * Verify an attestation by URL. May return "rotated" if the live well-known
50
+ * pubkey endpoint serves a different kid than the manifest declared.
51
+ */
52
+ export function verifyUrl(attestationUrl: string, opts?: VerifyUrlOpts): Promise<VerifyResult>
53
+
54
+ // Re-exports.
55
+ export { parseManifest, ParseResult, ParsedManifest, ParseError } from './parse.js'
56
+ export { verifySignature, computeKid, hexToBytes, bytesToHex, hexEquals } from './verify.js'
57
+ export { getJsonBody, FetchOk, FetchErr, GetJsonBodyOpts } from './fetch.js'