@hsuite/smart-engines-sdk 3.5.0 → 3.6.0

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pqc-verify/cert-schema.ts","../../src/pqc-verify/registry-fetch.ts","../../src/pqc-verify/verify-pqc-attestation.ts"],"names":["z","sha256","ml_dsa87"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * PQC Arc 11 PR 11.0 — Customer-facing PqcCertV1 Zod schema + parser.\n *\n * Cert schema is locked at `pqcCertVersion: 1` per design §3 — additive-only\n * migration policy keeps existing customer SDKs verifying for the deprecation\n * window (12 months minimum) when v2 ships.\n *\n * @see docs/superpowers/specs/2026-05-27-pqc-arc11-external-verifier-design.md §3\n * @see docs/superpowers/plans/2026-05-27-pqc-arc11-external-verifier-plan.md §11.0.1\n */\nimport { z } from 'zod';\n\nexport const PqcCertV1Schema = z\n .object({\n pqcCertVersion: z.literal(1),\n algorithm: z.literal('ml-dsa-87'),\n consumer: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),\n contentHash: z.string().regex(/^[a-f0-9]{64}$/),\n payloadHash: z\n .string()\n .regex(/^[a-f0-9]{64}$/)\n .optional(),\n threshold: z.number().int().positive(),\n signers: z\n .array(\n z.object({\n nodeId: z.string().min(1),\n dilithium5PublicKey: z.string().min(1),\n signature: z.string().min(1),\n }),\n )\n .nonempty(),\n ts: z.number().int().positive(),\n topicMessageId: z.string().optional(),\n })\n .strict();\n\nexport type PqcCertV1 = z.infer<typeof PqcCertV1Schema>;\n\nexport type PqcCertV1Signer = PqcCertV1['signers'][number];\n\n/**\n * Fail-soft cert parser. Never throws — returns a discriminated result so the\n * verifier can surface schema errors as `{ valid: false, reason }` to customers.\n */\nexport function parsePqcCert(\n input: unknown,\n): { ok: true; cert: PqcCertV1 } | { ok: false; error: string } {\n const result = PqcCertV1Schema.safeParse(input);\n if (!result.success) {\n return { ok: false, error: result.error.issues[0]?.message ?? 'schema-invalid' };\n }\n return { ok: true, cert: result.data };\n}\n","/**\n * PQC Arc 11 PR 11.0 Phase 11.0.2 — validator-registry snapshot fetcher.\n *\n * Pulls a snapshot from the smart-host proxy (PR #820) at\n * `GET /api/registry/snapshot`. Fail-soft: any non-2xx / network error /\n * malformed body returns `null` so the verifier can fall through to the\n * signature-only validity path without throwing.\n *\n * @see docs/superpowers/specs/2026-05-27-pqc-arc11-external-verifier-design.md §4.1\n */\n\nexport type ValidatorRegistrySnapshot = {\n readonly snapshotTs: number;\n readonly validators: readonly {\n readonly nodeId: string;\n readonly dilithium5PublicKey: string;\n readonly active: boolean;\n }[];\n};\n\nexport type FetchRegistryOptions = {\n /** Abort the fetch after this many ms. Default 10_000. */\n readonly timeoutMs?: number;\n};\n\n/**\n * Fetch a validator-registry snapshot from a smart-host proxy.\n *\n * Returns `null` on any failure (HTTP non-OK, network error, JSON parse error,\n * shape mismatch). Callers can treat `null` as \"registry unavailable; skip\n * pubkey membership check\" and still verify signatures against the pubkeys\n * embedded in the cert itself.\n */\nexport async function fetchRegistrySnapshot(\n smartHostUrl: string,\n opts: FetchRegistryOptions = {},\n): Promise<ValidatorRegistrySnapshot | null> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 10_000);\n try {\n const url = `${smartHostUrl.replace(/\\/+$/, '')}/api/registry/snapshot`;\n const resp = await fetch(url, { signal: controller.signal });\n if (!resp.ok) return null;\n const data: unknown = await resp.json();\n if (typeof data !== 'object' || data === null) return null;\n const obj = data as Record<string, unknown>;\n if (typeof obj.snapshotTs !== 'number' || !Array.isArray(obj.validators)) {\n return null;\n }\n return obj as unknown as ValidatorRegistrySnapshot;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n","/**\n * PQC Arc 11 PR 11.0 Phase 11.0.3 — verifyPqcAttestation primitive.\n *\n * Pure verification — no platform calls. Pass a `registrySnapshot` to also\n * enforce registry membership; omit it for signature-only verification.\n *\n * Verification steps (per design §4.1):\n * 1. Schema check: pqcCertVersion === 1, algorithm === 'ml-dsa-87'.\n * 2. Content-hash match: sha256(canonical(payload)) === cert.contentHash.\n * 3. (Optional) Registry membership: each signer active in snapshot,\n * pubkey matches.\n * 4. Per-signer signature verification against\n * sha256(`${consumer}|${contentHash}|${ts}`).\n * 5. Threshold check: verified-signer-count >= cert.threshold.\n *\n * Fail-soft at every step — returns `{ valid: false, reason }` instead of\n * throwing.\n *\n * @see docs/superpowers/specs/2026-05-27-pqc-arc11-external-verifier-design.md §4.1\n */\nimport { ml_dsa87 } from '@noble/post-quantum/ml-dsa';\nimport { sha256 } from '@noble/hashes/sha256';\nimport { parsePqcCert, type PqcCertV1 } from './cert-schema';\nimport type { ValidatorRegistrySnapshot } from './registry-fetch';\n\nexport type VerifyResult = {\n readonly valid: boolean;\n readonly reason?: string;\n readonly verifiedSignerCount?: number;\n readonly thresholdMet?: boolean;\n};\n\n/**\n * Canonical JSON serialization — sorted object keys, no whitespace, JSON.stringify\n * for primitives. Mirrors libs/shared/src/schemas/pqc.schema.ts:canonicalJsonStringify\n * so that customer-side `sha256(canonical(payload))` matches platform-side hashes\n * byte-for-byte regardless of language / emitter.\n */\nfunction canonicalJsonStringify(value: unknown): string {\n return _serialize(value);\n}\n\nfunction _serialize(value: unknown): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`canonicalJsonStringify: non-finite number ${value}`);\n }\n return JSON.stringify(value);\n }\n if (typeof value === 'string') return JSON.stringify(value);\n if (typeof value === 'undefined') {\n throw new Error('canonicalJsonStringify: undefined is not JSON-representable');\n }\n if (Array.isArray(value)) {\n return '[' + value.map(_serialize).join(',') + ']';\n }\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n return '{' + keys.map((k) => JSON.stringify(k) + ':' + _serialize(obj[k])).join(',') + '}';\n }\n throw new Error(`canonicalJsonStringify: unsupported type ${typeof value}`);\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error('odd-length hex');\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) {\n const b = parseInt(hex.substr(i * 2, 2), 16);\n if (Number.isNaN(b)) throw new Error('non-hex char');\n out[i] = b;\n }\n return out;\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n let s = '';\n for (let i = 0; i < bytes.length; i++) {\n s += bytes[i].toString(16).padStart(2, '0');\n }\n return s;\n}\n\n/**\n * Verify a PQC attestation cert against a payload.\n *\n * @param cert - The PqcCertV1 cert (any shape; will be schema-validated).\n * @param payload - The artifact payload to verify against. Will be canonicalized\n * + sha256-hashed for the content-hash check.\n * @param registrySnapshot - Optional. When supplied, every signer must be\n * present + active in the snapshot, AND the cert's\n * pubkey must match the registry pubkey.\n */\nexport async function verifyPqcAttestation(\n cert: unknown,\n payload: unknown,\n registrySnapshot?: ValidatorRegistrySnapshot,\n): Promise<VerifyResult> {\n // ── Step 1: Schema check ──────────────────────────────────────────────────\n const parsed = parsePqcCert(cert);\n if (!parsed.ok) {\n return { valid: false, reason: `schema-invalid: ${parsed.error}` };\n }\n const c: PqcCertV1 = parsed.cert;\n\n // ── Step 2: Content-hash match ────────────────────────────────────────────\n let computedContentHash: string;\n try {\n const canonical = canonicalJsonStringify(payload);\n computedContentHash = bytesToHex(sha256(new TextEncoder().encode(canonical)));\n } catch (err) {\n return { valid: false, reason: `payload-canonicalization-failed: ${(err as Error).message}` };\n }\n if (computedContentHash.toLowerCase() !== c.contentHash.toLowerCase()) {\n return {\n valid: false,\n reason: `content-hash-mismatch: computed=${computedContentHash} cert=${c.contentHash}`,\n };\n }\n\n // ── Step 3: Registry membership (optional) ────────────────────────────────\n if (registrySnapshot) {\n for (const signer of c.signers) {\n const entry = registrySnapshot.validators.find((v) => v.nodeId === signer.nodeId);\n if (!entry) {\n return { valid: false, reason: `registry-unknown-signer: ${signer.nodeId}` };\n }\n if (!entry.active) {\n return { valid: false, reason: `registry-inactive-signer: ${signer.nodeId}` };\n }\n if (entry.dilithium5PublicKey.toLowerCase() !== signer.dilithium5PublicKey.toLowerCase()) {\n return {\n valid: false,\n reason: `registry-pubkey-mismatch: ${signer.nodeId}`,\n };\n }\n }\n }\n\n // ── Step 4: Per-signer signature verification ─────────────────────────────\n const sigDigestInput = new TextEncoder().encode(`${c.consumer}|${c.contentHash}|${c.ts}`);\n const sigDigest = sha256(sigDigestInput);\n\n let verifiedSignerCount = 0;\n for (const signer of c.signers) {\n try {\n const pubKey = hexToBytes(signer.dilithium5PublicKey);\n const sigBytes = hexToBytes(signer.signature);\n if (ml_dsa87.verify(pubKey, sigDigest, sigBytes)) {\n verifiedSignerCount++;\n }\n } catch {\n // malformed hex / wrong size — treat as invalid sig, continue\n }\n }\n\n // ── Step 5: Threshold ─────────────────────────────────────────────────────\n const thresholdMet = verifiedSignerCount >= c.threshold;\n if (!thresholdMet) {\n return {\n valid: false,\n reason: `threshold-not-met: verified=${verifiedSignerCount} required=${c.threshold}`,\n verifiedSignerCount,\n thresholdMet: false,\n };\n }\n\n return {\n valid: true,\n verifiedSignerCount,\n thresholdMet: true,\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/pqc-verify/cert-schema.ts","../../src/pqc-verify/registry-fetch.ts","../../src/pqc-verify/verify-pqc-attestation.ts"],"names":["z","sha256","ml_dsa87"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Customer-facing PqcCertV1 Zod schema and parser.\n *\n * The cert schema is pinned at `pqcCertVersion: 1`. Field evolution is\n * additive-only so existing customer SDKs keep verifying older certs.\n */\nimport { z } from 'zod';\n\n/**\n * Zod schema for a version-1 PQC attestation cert (`ml-dsa-87`).\n *\n * Strict: unknown keys are rejected. Used by {@link parsePqcCert} to fail-soft\n * validate untrusted cert input before signature verification.\n */\nexport const PqcCertV1Schema = z\n .object({\n pqcCertVersion: z.literal(1),\n algorithm: z.literal('ml-dsa-87'),\n consumer: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),\n contentHash: z.string().regex(/^[a-f0-9]{64}$/),\n payloadHash: z\n .string()\n .regex(/^[a-f0-9]{64}$/)\n .optional(),\n threshold: z.number().int().positive(),\n signers: z\n .array(\n z.object({\n nodeId: z.string().min(1),\n dilithium5PublicKey: z.string().min(1),\n signature: z.string().min(1),\n }),\n )\n .nonempty(),\n ts: z.number().int().positive(),\n topicMessageId: z.string().optional(),\n })\n .strict();\n\n/** A validated version-1 PQC attestation cert. */\nexport type PqcCertV1 = z.infer<typeof PqcCertV1Schema>;\n\n/** One signer entry within a {@link PqcCertV1} (nodeId, pubkey, signature). */\nexport type PqcCertV1Signer = PqcCertV1['signers'][number];\n\n/**\n * Fail-soft cert parser. Never throws — returns a discriminated result so the\n * verifier can surface schema errors as `{ valid: false, reason }` to customers.\n *\n * @param input - Untrusted candidate cert (typically parsed JSON).\n * @returns `{ ok: true, cert }` on success, else `{ ok: false, error }` with\n * the first Zod issue message.\n */\nexport function parsePqcCert(\n input: unknown,\n): { ok: true; cert: PqcCertV1 } | { ok: false; error: string } {\n const result = PqcCertV1Schema.safeParse(input);\n if (!result.success) {\n return { ok: false, error: result.error.issues[0]?.message ?? 'schema-invalid' };\n }\n return { ok: true, cert: result.data };\n}\n","/**\n * Validator-registry snapshot fetcher.\n *\n * Pulls a snapshot from the smart-host proxy at `GET /api/registry/snapshot`.\n * Fail-soft: any non-2xx response, network error, or malformed body returns\n * `null` so the verifier can fall through to the signature-only validity path\n * without throwing.\n */\n\n/**\n * Point-in-time view of the validator registry, used by\n * {@link verifyPqcAttestation} to enforce signer membership.\n */\nexport type ValidatorRegistrySnapshot = {\n /** Snapshot wall-clock (millis epoch). */\n readonly snapshotTs: number;\n /** Registered validators and their current ML-DSA pubkeys. */\n readonly validators: readonly {\n readonly nodeId: string;\n readonly dilithium5PublicKey: string;\n readonly active: boolean;\n }[];\n};\n\n/** Options for {@link fetchRegistrySnapshot}. */\nexport type FetchRegistryOptions = {\n /** Abort the fetch after this many ms. @defaultValue 10000 */\n readonly timeoutMs?: number;\n};\n\n/**\n * Fetch a validator-registry snapshot from a smart-host proxy.\n *\n * Callers can treat `null` as \"registry unavailable; skip pubkey membership\n * check\" and still verify signatures against the pubkeys embedded in the cert\n * itself.\n *\n * @param smartHostUrl - Base URL of a smart-host proxy (trailing slashes ok).\n * @param opts - Fetch options. See {@link FetchRegistryOptions}.\n * @returns The snapshot, or `null` on HTTP non-OK, network error, JSON parse\n * error, or shape mismatch. Never throws.\n */\nexport async function fetchRegistrySnapshot(\n smartHostUrl: string,\n opts: FetchRegistryOptions = {},\n): Promise<ValidatorRegistrySnapshot | null> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 10_000);\n try {\n const url = `${smartHostUrl.replace(/\\/+$/, '')}/api/registry/snapshot`;\n const resp = await fetch(url, { signal: controller.signal });\n if (!resp.ok) return null;\n const data: unknown = await resp.json();\n if (typeof data !== 'object' || data === null) return null;\n const obj = data as Record<string, unknown>;\n if (typeof obj.snapshotTs !== 'number' || !Array.isArray(obj.validators)) {\n return null;\n }\n return obj as unknown as ValidatorRegistrySnapshot;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n","/**\n * `verifyPqcAttestation` — customer-facing PQC attestation cert verifier.\n *\n * Pure verification — no platform calls. Pass a `registrySnapshot` to also\n * enforce registry membership; omit it for signature-only verification.\n *\n * Verification steps:\n * 1. Schema check: `pqcCertVersion === 1`, `algorithm === 'ml-dsa-87'`.\n * 2. Content-hash match: `sha256(canonical(payload)) === cert.contentHash`.\n * 3. (Optional) Registry membership: each signer active in the snapshot,\n * pubkey matches.\n * 4. Per-signer signature verification against\n * `sha256(`${consumer}|${contentHash}|${ts}`)`.\n * 5. Threshold check: verified-signer-count >= `cert.threshold`.\n *\n * Fail-soft at every step — returns `{ valid: false, reason }` instead of\n * throwing.\n */\nimport { ml_dsa87 } from '@noble/post-quantum/ml-dsa';\nimport { sha256 } from '@noble/hashes/sha256';\nimport { parsePqcCert, type PqcCertV1 } from './cert-schema';\nimport type { ValidatorRegistrySnapshot } from './registry-fetch';\n\n/**\n * Outcome of {@link verifyPqcAttestation}.\n *\n * - `valid` — true iff schema, content-hash, optional registry membership,\n * and the signer threshold all passed.\n * - `reason` — present only on failure; identifies the first failing step.\n * - `verifiedSignerCount` — number of signers whose ML-DSA-87 signature\n * verified (populated once signature verification runs).\n * - `thresholdMet` — whether `verifiedSignerCount >= cert.threshold`.\n */\nexport type VerifyResult = {\n readonly valid: boolean;\n readonly reason?: string;\n readonly verifiedSignerCount?: number;\n readonly thresholdMet?: boolean;\n};\n\n/**\n * Canonical JSON serialization — sorted object keys, no whitespace,\n * `JSON.stringify` for primitives. Mirrors the platform-side canonicalizer so\n * that customer-side `sha256(canonical(payload))` matches platform-side hashes\n * byte-for-byte regardless of language or emitter.\n *\n * @param value - JSON-representable value (no `undefined`, no non-finite numbers).\n * @returns Deterministic canonical JSON string.\n * @throws Error if `value` contains `undefined` or a non-finite number.\n */\nfunction canonicalJsonStringify(value: unknown): string {\n return _serialize(value);\n}\n\nfunction _serialize(value: unknown): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`canonicalJsonStringify: non-finite number ${value}`);\n }\n return JSON.stringify(value);\n }\n if (typeof value === 'string') return JSON.stringify(value);\n if (typeof value === 'undefined') {\n throw new Error('canonicalJsonStringify: undefined is not JSON-representable');\n }\n if (Array.isArray(value)) {\n return '[' + value.map(_serialize).join(',') + ']';\n }\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n return '{' + keys.map((k) => JSON.stringify(k) + ':' + _serialize(obj[k])).join(',') + '}';\n }\n throw new Error(`canonicalJsonStringify: unsupported type ${typeof value}`);\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error('odd-length hex');\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) {\n const b = parseInt(hex.substr(i * 2, 2), 16);\n if (Number.isNaN(b)) throw new Error('non-hex char');\n out[i] = b;\n }\n return out;\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n let s = '';\n for (let i = 0; i < bytes.length; i++) {\n s += bytes[i].toString(16).padStart(2, '0');\n }\n return s;\n}\n\n/**\n * Verify a PQC attestation cert against a payload.\n *\n * @param cert - The PqcCertV1 cert (any shape; will be schema-validated).\n * @param payload - The artifact payload to verify against. Will be canonicalized\n * and sha256-hashed for the content-hash check.\n * @param registrySnapshot - Optional. When supplied, every signer must be\n * present and active in the snapshot, AND the cert's\n * pubkey must match the registry pubkey.\n * @returns A {@link VerifyResult}; `valid: false` carries a `reason`. Never throws.\n *\n * @example\n * ```ts\n * import { verifyPqcAttestation, fetchRegistrySnapshot } from '@hsuite/smart-engines-sdk/pqc-verify';\n *\n * const snapshot = await fetchRegistrySnapshot('https://host.example.com');\n * const result = await verifyPqcAttestation(cert, payload, snapshot ?? undefined);\n * if (!result.valid) {\n * throw new Error(`PQC verify failed: ${result.reason}`);\n * }\n * ```\n */\nexport async function verifyPqcAttestation(\n cert: unknown,\n payload: unknown,\n registrySnapshot?: ValidatorRegistrySnapshot,\n): Promise<VerifyResult> {\n // ── Step 1: Schema check ──────────────────────────────────────────────────\n const parsed = parsePqcCert(cert);\n if (!parsed.ok) {\n return { valid: false, reason: `schema-invalid: ${parsed.error}` };\n }\n const c: PqcCertV1 = parsed.cert;\n\n // ── Step 2: Content-hash match ────────────────────────────────────────────\n let computedContentHash: string;\n try {\n const canonical = canonicalJsonStringify(payload);\n computedContentHash = bytesToHex(sha256(new TextEncoder().encode(canonical)));\n } catch (err) {\n return { valid: false, reason: `payload-canonicalization-failed: ${(err as Error).message}` };\n }\n if (computedContentHash.toLowerCase() !== c.contentHash.toLowerCase()) {\n return {\n valid: false,\n reason: `content-hash-mismatch: computed=${computedContentHash} cert=${c.contentHash}`,\n };\n }\n\n // ── Step 3: Registry membership (optional) ────────────────────────────────\n if (registrySnapshot) {\n for (const signer of c.signers) {\n const entry = registrySnapshot.validators.find((v) => v.nodeId === signer.nodeId);\n if (!entry) {\n return { valid: false, reason: `registry-unknown-signer: ${signer.nodeId}` };\n }\n if (!entry.active) {\n return { valid: false, reason: `registry-inactive-signer: ${signer.nodeId}` };\n }\n if (entry.dilithium5PublicKey.toLowerCase() !== signer.dilithium5PublicKey.toLowerCase()) {\n return {\n valid: false,\n reason: `registry-pubkey-mismatch: ${signer.nodeId}`,\n };\n }\n }\n }\n\n // ── Step 4: Per-signer signature verification ─────────────────────────────\n const sigDigestInput = new TextEncoder().encode(`${c.consumer}|${c.contentHash}|${c.ts}`);\n const sigDigest = sha256(sigDigestInput);\n\n let verifiedSignerCount = 0;\n for (const signer of c.signers) {\n try {\n const pubKey = hexToBytes(signer.dilithium5PublicKey);\n const sigBytes = hexToBytes(signer.signature);\n if (ml_dsa87.verify(pubKey, sigDigest, sigBytes)) {\n verifiedSignerCount++;\n }\n } catch {\n // malformed hex / wrong size — treat as invalid sig, continue\n }\n }\n\n // ── Step 5: Threshold ─────────────────────────────────────────────────────\n const thresholdMet = verifiedSignerCount >= c.threshold;\n if (!thresholdMet) {\n return {\n valid: false,\n reason: `threshold-not-met: verified=${verifiedSignerCount} required=${c.threshold}`,\n verifiedSignerCount,\n thresholdMet: false,\n };\n }\n\n return {\n valid: true,\n verifiedSignerCount,\n thresholdMet: true,\n };\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
- export type EnvelopeVersionLiteral = "kyber-aes-v1" | "aes-v0";
3
+ export type EnvelopeVersionLiteral = "kyber-aes-v1";
4
4
  export type SchemaValidationResult = {
5
5
  ok: true;
6
6
  version: EnvelopeVersionLiteral;
@@ -40,9 +40,6 @@ function validateEnvelopeSchema(envelope) {
40
40
  if (version === "kyber-aes-v1") {
41
41
  return validateKyberAesV1(envelope);
42
42
  }
43
- if (version === "aes-v0") {
44
- return validateAesV0(envelope);
45
- }
46
43
  return {
47
44
  ok: false,
48
45
  reason: `unknown envelope version: ${JSON.stringify(version)}`
@@ -136,38 +133,6 @@ function validateKyberAesV1(env) {
136
133
  }
137
134
  return { ok: true, version: "kyber-aes-v1" };
138
135
  }
139
- function validateAesV0(env) {
140
- if (!isNonEmptyString(env.aesIv)) {
141
- return { ok: false, reason: "aesIv must be a non-empty base64 string" };
142
- }
143
- const ivBytes = tryDecodeBase64(env.aesIv);
144
- if (!ivBytes) return { ok: false, reason: "aesIv is not valid base64" };
145
- if (ivBytes.length !== AES_IV_LEN) {
146
- return {
147
- ok: false,
148
- reason: `aesIv length ${ivBytes.length} != expected ${AES_IV_LEN} (AES-GCM 96-bit nonce)`
149
- };
150
- }
151
- if (typeof env.aesCiphertext !== "string") {
152
- return { ok: false, reason: "aesCiphertext must be a base64 string" };
153
- }
154
- if (env.aesCiphertext.length > 0) {
155
- const ctBytes = tryDecodeBase64(env.aesCiphertext);
156
- if (!ctBytes) return { ok: false, reason: "aesCiphertext is not valid base64" };
157
- }
158
- if (!isNonEmptyString(env.aesAuthTag)) {
159
- return { ok: false, reason: "aesAuthTag must be a non-empty base64 string" };
160
- }
161
- const tagBytes = tryDecodeBase64(env.aesAuthTag);
162
- if (!tagBytes) return { ok: false, reason: "aesAuthTag is not valid base64" };
163
- if (tagBytes.length !== AES_TAG_LEN) {
164
- return {
165
- ok: false,
166
- reason: `aesAuthTag length ${tagBytes.length} != expected ${AES_TAG_LEN} (AES-GCM 128-bit tag)`
167
- };
168
- }
169
- return { ok: true, version: "aes-v0" };
170
- }
171
136
 
172
137
  // src/pqc-verify-envelope/verify-pqc-envelope.ts
173
138
  var KYBER_MIN_TIMESTAMP_MS = 17040672e5;
@@ -186,10 +151,10 @@ async function verifyPqcEnvelope(envelope, options = {}) {
186
151
  version,
187
152
  schemaValid: true,
188
153
  base64Valid: true,
189
- // computed below for kyber-aes-v1; legacy has no encryptedAt so treat as plausible.
154
+ // Set to false below if the timestamp plausibility check fails.
190
155
  timestampPlausible: true
191
156
  };
192
- if (version === "kyber-aes-v1") {
157
+ {
193
158
  details.kemAlgorithm = env.kemAlgorithm;
194
159
  details.recipientPkFingerprint = env.recipientPkFingerprint;
195
160
  details.kdfLabel = env.kdfLabel;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pqc-verify-envelope/envelope-schema-validator.ts","../../src/pqc-verify-envelope/verify-pqc-envelope.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Kyber Arc 11 — envelope schema validator (zero-dep, fail-soft).\n *\n * Pure structural validation for the two envelope versions defined by\n * `libs/security-core/src/encryption-pqc/envelope.types.ts`:\n *\n * - `kyber-aes-v1` — modern hybrid (ML-KEM + HKDF-SHA384 + AES-256-GCM)\n * - `aes-v0` — legacy AES-only (read-only 12-month compat window)\n *\n * Customer-facing — therefore intentionally re-declares the envelope shape\n * here (instead of importing from `security-core`) so the SDK bundle stays\n * decoupled from the platform-internal package tree. The schema literals\n * MUST stay in sync with `security-core/src/encryption-pqc/envelope.types.ts`;\n * any divergence is a bug.\n *\n * Validation is structure-only — no decryption, no key access, no\n * cryptographic verification. Customers use this to catch malformed\n * envelopes BEFORE handing them to a decrypt routine (so the platform's\n * format invariants are visible at the SDK layer).\n *\n * @see docs/superpowers/specs/2026-05-29-kyber-mlkem-production-readiness.md §5 Arc 11\n * @see docs/superpowers/specs/2026-05-29-kyber-mlkem-production-readiness.md §6.1\n */\n\n/**\n * Discriminator literals accepted by `validateEnvelopeSchema`.\n *\n * Keep this union in sync with\n * `libs/security-core/src/encryption-pqc/envelope.types.ts` (`AnyEnvelope`).\n * Any new envelope version added there MUST add a literal here AND a\n * matching branch in `validateEnvelopeSchema`.\n */\nexport type EnvelopeVersionLiteral = 'kyber-aes-v1' | 'aes-v0';\n\n/**\n * Structural validation result for one envelope.\n *\n * - `ok: true` → caller can trust every required field exists with the\n * correct type, base64 fields decode, and version-specific length\n * invariants hold.\n * - `ok: false` → first failure is reported; further failures are NOT\n * accumulated (early-exit keeps the verifier output single-cause).\n */\nexport type SchemaValidationResult =\n | { ok: true; version: EnvelopeVersionLiteral }\n | { ok: false; reason: string };\n\n/**\n * ML-KEM ciphertext byte-length invariants (NIST FIPS-203).\n * - ML-KEM-768 → 1088 bytes\n * - ML-KEM-1024 → 1568 bytes\n *\n * These are exact, not bounds — any deviation indicates a malformed\n * envelope (truncation, padding, wrong-algo emit). Verifier rejects\n * mismatches outright.\n */\nconst KEM_CT_LEN: Record<'ml-kem-768' | 'ml-kem-1024', number> = {\n 'ml-kem-768': 1088,\n 'ml-kem-1024': 1568,\n};\n\n/** AES-GCM standard nonce = 96 bits = 12 bytes. */\nconst AES_IV_LEN = 12;\n/** AES-GCM auth tag = 128 bits = 16 bytes. */\nconst AES_TAG_LEN = 16;\n/** Per-envelope HKDF salt = 16 bytes (matches `encryptHybrid` writer). */\nconst KDF_SALT_LEN = 16;\n\n/**\n * Decode a base64 string, returning `null` on any decode error (illegal\n * chars, length not multiple of 4, etc.) rather than throwing — the\n * verifier surface fail-softs every failure mode.\n *\n * Uses `Buffer.from(s, 'base64')` which is permissive (silently drops\n * illegal chars). The follow-up length check is the real gate.\n */\nfunction tryDecodeBase64(s: string): Uint8Array | null {\n if (typeof s !== 'string' || s.length === 0) return null;\n try {\n const buf = Buffer.from(s, 'base64');\n // Round-trip check: re-encode and compare. Catches malformed input\n // that `Buffer.from` silently accepted.\n if (buf.toString('base64').replace(/=+$/, '') !== s.replace(/=+$/, '')) {\n return null;\n }\n return new Uint8Array(buf);\n } catch {\n return null;\n }\n}\n\nfunction isString(v: unknown): v is string {\n return typeof v === 'string';\n}\n\nfunction isNonEmptyString(v: unknown): v is string {\n return typeof v === 'string' && v.length > 0;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === 'number' && Number.isFinite(v);\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/**\n * Validate the structural shape of an envelope.\n *\n * Branches on `version` discriminator and checks the per-version contract.\n * Returns `{ ok: false, reason }` on the first failure — single-cause\n * diagnostics keep verifier output actionable.\n *\n * Side-effect-free, throws never.\n */\nexport function validateEnvelopeSchema(envelope: unknown): SchemaValidationResult {\n if (!isPlainObject(envelope)) {\n return { ok: false, reason: 'envelope must be a JSON object' };\n }\n\n const version = (envelope as { version?: unknown }).version;\n if (version === 'kyber-aes-v1') {\n return validateKyberAesV1(envelope);\n }\n if (version === 'aes-v0') {\n return validateAesV0(envelope);\n }\n return {\n ok: false,\n reason: `unknown envelope version: ${JSON.stringify(version)}`,\n };\n}\n\nfunction validateKyberAesV1(env: Record<string, unknown>): SchemaValidationResult {\n // kemAlgorithm\n const kemAlgorithm = env.kemAlgorithm;\n if (kemAlgorithm !== 'ml-kem-768' && kemAlgorithm !== 'ml-kem-1024') {\n return {\n ok: false,\n reason: `kemAlgorithm must be 'ml-kem-768' or 'ml-kem-1024'`,\n };\n }\n\n // kemCiphertext (base64) — exact length per FIPS-203.\n if (!isNonEmptyString(env.kemCiphertext)) {\n return { ok: false, reason: 'kemCiphertext must be a non-empty base64 string' };\n }\n const kemCtBytes = tryDecodeBase64(env.kemCiphertext);\n if (!kemCtBytes) {\n return { ok: false, reason: 'kemCiphertext is not valid base64' };\n }\n const expectedKemLen = KEM_CT_LEN[kemAlgorithm];\n if (kemCtBytes.length !== expectedKemLen) {\n return {\n ok: false,\n reason: `kemCiphertext length ${kemCtBytes.length} != expected ${expectedKemLen} for ${kemAlgorithm}`,\n };\n }\n\n // recipientPkFingerprint — hex string, 32 chars (16-byte sha384 prefix).\n if (!isString(env.recipientPkFingerprint) || !/^[0-9a-f]{32}$/i.test(env.recipientPkFingerprint)) {\n return {\n ok: false,\n reason: 'recipientPkFingerprint must be a 32-char hex string (16-byte sha384 prefix)',\n };\n }\n\n // aesAlgorithm (pinned)\n if (env.aesAlgorithm !== 'aes-256-gcm') {\n return { ok: false, reason: `aesAlgorithm must be 'aes-256-gcm'` };\n }\n\n // aesIv (base64, 12 bytes)\n if (!isNonEmptyString(env.aesIv)) {\n return { ok: false, reason: 'aesIv must be a non-empty base64 string' };\n }\n const ivBytes = tryDecodeBase64(env.aesIv);\n if (!ivBytes) return { ok: false, reason: 'aesIv is not valid base64' };\n if (ivBytes.length !== AES_IV_LEN) {\n return {\n ok: false,\n reason: `aesIv length ${ivBytes.length} != expected ${AES_IV_LEN} (AES-GCM 96-bit nonce)`,\n };\n }\n\n // aesCiphertext (base64) — any length, including 0 (empty plaintext is valid).\n if (typeof env.aesCiphertext !== 'string') {\n return { ok: false, reason: 'aesCiphertext must be a base64 string' };\n }\n // Permit empty string explicitly (0-byte ciphertext from empty plaintext).\n if (env.aesCiphertext.length > 0) {\n const ctBytes = tryDecodeBase64(env.aesCiphertext);\n if (!ctBytes) return { ok: false, reason: 'aesCiphertext is not valid base64' };\n }\n\n // aesAuthTag (base64, 16 bytes)\n if (!isNonEmptyString(env.aesAuthTag)) {\n return { ok: false, reason: 'aesAuthTag must be a non-empty base64 string' };\n }\n const tagBytes = tryDecodeBase64(env.aesAuthTag);\n if (!tagBytes) return { ok: false, reason: 'aesAuthTag is not valid base64' };\n if (tagBytes.length !== AES_TAG_LEN) {\n return {\n ok: false,\n reason: `aesAuthTag length ${tagBytes.length} != expected ${AES_TAG_LEN} (AES-GCM 128-bit tag)`,\n };\n }\n\n // kdfAlgorithm (pinned)\n if (env.kdfAlgorithm !== 'hkdf-sha384') {\n return { ok: false, reason: `kdfAlgorithm must be 'hkdf-sha384'` };\n }\n\n // kdfSalt (base64, 16 bytes)\n if (!isNonEmptyString(env.kdfSalt)) {\n return { ok: false, reason: 'kdfSalt must be a non-empty base64 string' };\n }\n const saltBytes = tryDecodeBase64(env.kdfSalt);\n if (!saltBytes) return { ok: false, reason: 'kdfSalt is not valid base64' };\n if (saltBytes.length !== KDF_SALT_LEN) {\n return {\n ok: false,\n reason: `kdfSalt length ${saltBytes.length} != expected ${KDF_SALT_LEN}`,\n };\n }\n\n // kdfLabel — non-empty, reasonable upper bound (256 chars) to catch\n // pathological inputs without imposing a real schema constraint.\n if (!isNonEmptyString(env.kdfLabel)) {\n return { ok: false, reason: 'kdfLabel must be a non-empty string' };\n }\n if (env.kdfLabel.length > 256) {\n return {\n ok: false,\n reason: `kdfLabel length ${env.kdfLabel.length} exceeds 256-char limit`,\n };\n }\n\n // encryptedAt — finite number (millis epoch).\n if (!isFiniteNumber(env.encryptedAt)) {\n return { ok: false, reason: 'encryptedAt must be a finite number (millis epoch)' };\n }\n\n return { ok: true, version: 'kyber-aes-v1' };\n}\n\nfunction validateAesV0(env: Record<string, unknown>): SchemaValidationResult {\n if (!isNonEmptyString(env.aesIv)) {\n return { ok: false, reason: 'aesIv must be a non-empty base64 string' };\n }\n const ivBytes = tryDecodeBase64(env.aesIv);\n if (!ivBytes) return { ok: false, reason: 'aesIv is not valid base64' };\n if (ivBytes.length !== AES_IV_LEN) {\n return {\n ok: false,\n reason: `aesIv length ${ivBytes.length} != expected ${AES_IV_LEN} (AES-GCM 96-bit nonce)`,\n };\n }\n\n if (typeof env.aesCiphertext !== 'string') {\n return { ok: false, reason: 'aesCiphertext must be a base64 string' };\n }\n if (env.aesCiphertext.length > 0) {\n const ctBytes = tryDecodeBase64(env.aesCiphertext);\n if (!ctBytes) return { ok: false, reason: 'aesCiphertext is not valid base64' };\n }\n\n if (!isNonEmptyString(env.aesAuthTag)) {\n return { ok: false, reason: 'aesAuthTag must be a non-empty base64 string' };\n }\n const tagBytes = tryDecodeBase64(env.aesAuthTag);\n if (!tagBytes) return { ok: false, reason: 'aesAuthTag is not valid base64' };\n if (tagBytes.length !== AES_TAG_LEN) {\n return {\n ok: false,\n reason: `aesAuthTag length ${tagBytes.length} != expected ${AES_TAG_LEN} (AES-GCM 128-bit tag)`,\n };\n }\n\n return { ok: true, version: 'aes-v0' };\n}\n","/**\n * Kyber Arc 11 — `verifyPqcEnvelope` external verifier primitive.\n *\n * Observation-only customer surface for the Kyber/ML-KEM hybrid envelopes\n * the platform emits (Arcs 2-10 write paths). Does NOT decrypt — verifier\n * holds no key material. Checks structural well-formedness + claim\n * plausibility so customers can detect malformed envelopes BEFORE handing\n * them to a decryption routine.\n *\n * Three classes of checks:\n *\n * 1. Schema (`envelope-schema-validator.ts`) — discriminator literal,\n * required fields, types, base64 decodability, exact-length field\n * invariants per FIPS-203 (ML-KEM ciphertext) + AES-GCM (iv/tag).\n *\n * 2. Timestamp plausibility — `encryptedAt` must fall between FIPS-203\n * standardization (2024-01-01, before which no hybrid envelope could\n * have been validly emitted) and `Date.now()` (future timestamps\n * indicate clock skew / forged envelope).\n *\n * 3. Optional caller claims — when supplied, `expectedLabel` and\n * `expectedRecipientPkFingerprint` are byte-equality checked against\n * the envelope fields. Useful for verifying an envelope was emitted\n * for the surface the caller expects.\n *\n * Fail-soft at every step — returns a discriminated `EnvelopeVerificationResult`\n * with `reason` on failure. Throws only on truly impossible inputs (e.g.\n * the caller passing a non-Promise to `await`).\n *\n * @see docs/superpowers/specs/2026-05-29-kyber-mlkem-production-readiness.md §5 Arc 11\n * @see libs/security-core/src/encryption-pqc/envelope.types.ts (KyberAesEnvelopeV1, AesEnvelopeV0Legacy)\n * @see libs/security-core/src/encryption-pqc/encrypt-hybrid.ts (the producer this verifier observes)\n */\nimport {\n validateEnvelopeSchema,\n type EnvelopeVersionLiteral,\n} from './envelope-schema-validator';\n\n/**\n * FIPS-203 standardization date — millis since epoch for 2024-01-01 00:00 UTC.\n *\n * No production hybrid envelope can legitimately predate this. Customer-facing\n * floor for `encryptedAt` plausibility check; overridable via\n * `VerifyEnvelopeOptions.minTimestamp` for test/staging fixtures.\n */\nexport const KYBER_MIN_TIMESTAMP_MS = 1_704_067_200_000;\n\n/**\n * Detailed verification metadata exposed to callers regardless of valid/invalid\n * outcome. Partial on failure (whichever fields were inspectable before the\n * first failure).\n */\nexport type VerificationDetails = {\n /** Envelope version literal (`kyber-aes-v1` or `aes-v0`). */\n version: string;\n /** ML-KEM parameter set, only present for `kyber-aes-v1`. */\n kemAlgorithm?: 'ml-kem-768' | 'ml-kem-1024';\n /** 32-char hex sha384-prefix of the recipient pk; only present for hybrid. */\n recipientPkFingerprint?: string;\n /** Per-surface HKDF label as recorded by the writer. */\n kdfLabel?: string;\n /** Writer wall-clock at emit time (millis epoch). */\n encryptedAt?: number;\n /** True iff all schema-shape checks passed. */\n schemaValid: boolean;\n /** True iff every base64 field decoded successfully. (Subsumed by `schemaValid`.) */\n base64Valid: boolean;\n /** True iff `encryptedAt` fell within `[minTimestamp, maxTimestamp]`. */\n timestampPlausible: boolean;\n};\n\n/**\n * Discriminated verification outcome. `valid: true` indicates the envelope is\n * STRUCTURALLY well-formed for its declared version AND every caller-supplied\n * claim matched. It does NOT indicate the platform actually emitted the\n * envelope (no signature here) nor that the ciphertext will decrypt (no key\n * here) — those gates belong to PQC-Sig's `verifyPqcAttestation` (signature\n * provenance) and the platform's `decryptHybrid` / `decryptAny` (ciphertext\n * authenticity).\n */\nexport type EnvelopeVerificationResult =\n | {\n valid: true;\n version: EnvelopeVersionLiteral;\n details: VerificationDetails;\n }\n | {\n valid: false;\n reason: string;\n details?: Partial<VerificationDetails>;\n };\n\n/**\n * Caller-facing knobs for `verifyPqcEnvelope`.\n *\n * @field expectedLabel — when set, the envelope's `kdfLabel` MUST equal this\n * value (byte-equality). Surface-mismatch defense — catches envelopes\n * addressed to a different consumer being replayed at this consumer.\n * Only meaningful for `kyber-aes-v1` envelopes (legacy `aes-v0` has no label).\n *\n * @field expectedRecipientPkFingerprint — when set, the envelope's\n * `recipientPkFingerprint` MUST equal this value (case-insensitive hex\n * compare). Useful for asserting the envelope was emitted for a specific\n * cluster Kyber keypair. Only meaningful for `kyber-aes-v1`.\n *\n * @field minTimestamp — lower bound (inclusive, millis epoch) for\n * `encryptedAt`. Default: `KYBER_MIN_TIMESTAMP_MS` (2024-01-01).\n *\n * @field maxTimestamp — upper bound (inclusive, millis epoch) for\n * `encryptedAt`. Default: `Date.now()` evaluated at verify time.\n * Pass a fixed future-skew tolerance (e.g. `Date.now() + 60_000`) if\n * verifying envelopes from a known-skewed source.\n */\nexport type VerifyEnvelopeOptions = {\n expectedLabel?: string;\n expectedRecipientPkFingerprint?: string;\n minTimestamp?: number;\n maxTimestamp?: number;\n};\n\n/**\n * Verify a Kyber/ML-KEM hybrid envelope (or legacy AES-v0 envelope) for\n * structural well-formedness + claim consistency.\n *\n * Pure function — no I/O, no key material, no decryption. Customer code\n * calls this BEFORE passing the envelope to a decryption surface; if it\n * returns `{ valid: false }`, the envelope is malformed and decryption\n * would fail anyway (often with a less-actionable error).\n *\n * Async signature for forward-compatibility with potential network checks\n * (e.g. fetch recipient-pk registry to resolve the fingerprint to a known\n * cluster). The Arc 11 baseline does NOT make any network calls.\n *\n * @example\n * ```ts\n * import { verifyPqcEnvelope } from '@hsuite/smart-engines-sdk/pqc-verify-envelope';\n *\n * const result = await verifyPqcEnvelope(envelopeJson, {\n * expectedLabel: 'smart-app-secret-envelope-v1',\n * });\n * if (!result.valid) {\n * throw new Error(`Bad envelope: ${result.reason}`);\n * }\n * ```\n */\nexport async function verifyPqcEnvelope(\n envelope: unknown,\n options: VerifyEnvelopeOptions = {},\n): Promise<EnvelopeVerificationResult> {\n // ── Step 1: Schema validation ─────────────────────────────────────────────\n const schema = validateEnvelopeSchema(envelope);\n if (!schema.ok) {\n return {\n valid: false,\n reason: `schema-invalid: ${schema.reason}`,\n details: { schemaValid: false, base64Valid: false, timestampPlausible: false },\n };\n }\n\n const env = envelope as Record<string, unknown>;\n const version = schema.version;\n\n // ── Step 2: Build inspectable details ─────────────────────────────────────\n const details: VerificationDetails = {\n version,\n schemaValid: true,\n base64Valid: true,\n // computed below for kyber-aes-v1; legacy has no encryptedAt so treat as plausible.\n timestampPlausible: true,\n };\n\n if (version === 'kyber-aes-v1') {\n details.kemAlgorithm = env.kemAlgorithm as 'ml-kem-768' | 'ml-kem-1024';\n details.recipientPkFingerprint = env.recipientPkFingerprint as string;\n details.kdfLabel = env.kdfLabel as string;\n details.encryptedAt = env.encryptedAt as number;\n\n // ── Step 3: Timestamp plausibility ─────────────────────────────────────\n const minTs = options.minTimestamp ?? KYBER_MIN_TIMESTAMP_MS;\n const maxTs = options.maxTimestamp ?? Date.now();\n const ts = details.encryptedAt as number;\n if (ts < minTs) {\n details.timestampPlausible = false;\n return {\n valid: false,\n reason: `timestamp-before-minimum: encryptedAt=${ts} < min=${minTs}`,\n details,\n };\n }\n if (ts > maxTs) {\n details.timestampPlausible = false;\n return {\n valid: false,\n reason: `timestamp-in-future: encryptedAt=${ts} > max=${maxTs}`,\n details,\n };\n }\n\n // ── Step 4: Optional caller-claim checks ───────────────────────────────\n if (options.expectedLabel !== undefined) {\n if (details.kdfLabel !== options.expectedLabel) {\n return {\n valid: false,\n reason: `label-mismatch: expected='${options.expectedLabel}' actual='${details.kdfLabel}'`,\n details,\n };\n }\n }\n if (options.expectedRecipientPkFingerprint !== undefined) {\n const a = (details.recipientPkFingerprint ?? '').toLowerCase();\n const b = options.expectedRecipientPkFingerprint.toLowerCase();\n if (a !== b) {\n return {\n valid: false,\n reason: `recipient-pk-fingerprint-mismatch: expected='${b}' actual='${a}'`,\n details,\n };\n }\n }\n }\n // aes-v0: no extra checks beyond schema. Legacy envelopes are read-only\n // through the 12-month Arc 12 compat window; verifier accepts them as\n // structurally valid but offers no claim checks (label/fingerprint don't\n // exist on the legacy shape).\n\n return { valid: true, version, details };\n}\n"]}
1
+ {"version":3,"sources":["../../src/pqc-verify-envelope/envelope-schema-validator.ts","../../src/pqc-verify-envelope/verify-pqc-envelope.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Zero-dependency, fail-soft structural validator for PQC hybrid envelopes.\n *\n * Validates the `kyber-aes-v1` envelope shape (ML-KEM + HKDF-SHA384 +\n * AES-256-GCM) defined by the platform's encryption layer.\n *\n * Customer-facing — therefore intentionally re-declares the envelope shape\n * here (instead of importing it from a platform-internal package) so the SDK\n * bundle stays decoupled from the platform package tree. The schema literals\n * MUST stay in sync with the platform envelope type definitions; any\n * divergence is a bug.\n *\n * Validation is structure-only — no decryption, no key access, no\n * cryptographic verification. Customers use this to catch malformed\n * envelopes BEFORE handing them to a decrypt routine, so the platform's\n * format invariants are visible at the SDK layer.\n */\n\n/**\n * Discriminator literal accepted by {@link validateEnvelopeSchema}.\n *\n * `kyber-aes-v1` is the canonical hybrid format. Any new envelope version\n * added to the platform MUST add a literal here AND a matching branch in\n * {@link validateEnvelopeSchema}.\n */\nexport type EnvelopeVersionLiteral = 'kyber-aes-v1';\n\n/**\n * Structural validation result for one envelope.\n *\n * - `ok: true` — caller can trust every required field exists with the\n * correct type, base64 fields decode, and version-specific length\n * invariants hold.\n * - `ok: false` — first failure is reported; further failures are NOT\n * accumulated (early-exit keeps the verifier output single-cause).\n */\nexport type SchemaValidationResult =\n | { ok: true; version: EnvelopeVersionLiteral }\n | { ok: false; reason: string };\n\n/**\n * ML-KEM ciphertext byte-length invariants (NIST FIPS-203).\n * - ML-KEM-768 → 1088 bytes\n * - ML-KEM-1024 → 1568 bytes\n *\n * These are exact, not bounds — any deviation indicates a malformed\n * envelope (truncation, padding, wrong-algo emit). The verifier rejects\n * mismatches outright.\n */\nconst KEM_CT_LEN: Record<'ml-kem-768' | 'ml-kem-1024', number> = {\n 'ml-kem-768': 1088,\n 'ml-kem-1024': 1568,\n};\n\n/** AES-GCM standard nonce = 96 bits = 12 bytes. */\nconst AES_IV_LEN = 12;\n/** AES-GCM auth tag = 128 bits = 16 bytes. */\nconst AES_TAG_LEN = 16;\n/** Per-envelope HKDF salt = 16 bytes (matches the hybrid encrypt writer). */\nconst KDF_SALT_LEN = 16;\n\n/**\n * Decode a base64 string, returning `null` on any decode error (illegal\n * chars, length not a multiple of 4, etc.) rather than throwing — the\n * verifier surface fail-softs every failure mode.\n *\n * Uses `Buffer.from(s, 'base64')`, which is permissive (silently drops\n * illegal chars). The follow-up round-trip check is the real gate.\n *\n * @param s - Candidate base64 string.\n * @returns Decoded bytes, or `null` if `s` is not valid base64.\n */\nfunction tryDecodeBase64(s: string): Uint8Array | null {\n if (typeof s !== 'string' || s.length === 0) return null;\n try {\n const buf = Buffer.from(s, 'base64');\n // Round-trip check: re-encode and compare. Catches malformed input\n // that `Buffer.from` silently accepted.\n if (buf.toString('base64').replace(/=+$/, '') !== s.replace(/=+$/, '')) {\n return null;\n }\n return new Uint8Array(buf);\n } catch {\n return null;\n }\n}\n\nfunction isString(v: unknown): v is string {\n return typeof v === 'string';\n}\n\nfunction isNonEmptyString(v: unknown): v is string {\n return typeof v === 'string' && v.length > 0;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === 'number' && Number.isFinite(v);\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/**\n * Validate the structural shape of an envelope.\n *\n * Branches on the `version` discriminator and checks the per-version\n * contract. Returns `{ ok: false, reason }` on the first failure —\n * single-cause diagnostics keep verifier output actionable.\n *\n * Side-effect-free; never throws.\n *\n * @param envelope - Untrusted candidate envelope (typically parsed JSON).\n * @returns A discriminated {@link SchemaValidationResult}.\n */\nexport function validateEnvelopeSchema(envelope: unknown): SchemaValidationResult {\n if (!isPlainObject(envelope)) {\n return { ok: false, reason: 'envelope must be a JSON object' };\n }\n\n const version = (envelope as { version?: unknown }).version;\n if (version === 'kyber-aes-v1') {\n return validateKyberAesV1(envelope);\n }\n return {\n ok: false,\n reason: `unknown envelope version: ${JSON.stringify(version)}`,\n };\n}\n\nfunction validateKyberAesV1(env: Record<string, unknown>): SchemaValidationResult {\n // kemAlgorithm\n const kemAlgorithm = env.kemAlgorithm;\n if (kemAlgorithm !== 'ml-kem-768' && kemAlgorithm !== 'ml-kem-1024') {\n return {\n ok: false,\n reason: `kemAlgorithm must be 'ml-kem-768' or 'ml-kem-1024'`,\n };\n }\n\n // kemCiphertext (base64) — exact length per FIPS-203.\n if (!isNonEmptyString(env.kemCiphertext)) {\n return { ok: false, reason: 'kemCiphertext must be a non-empty base64 string' };\n }\n const kemCtBytes = tryDecodeBase64(env.kemCiphertext);\n if (!kemCtBytes) {\n return { ok: false, reason: 'kemCiphertext is not valid base64' };\n }\n const expectedKemLen = KEM_CT_LEN[kemAlgorithm];\n if (kemCtBytes.length !== expectedKemLen) {\n return {\n ok: false,\n reason: `kemCiphertext length ${kemCtBytes.length} != expected ${expectedKemLen} for ${kemAlgorithm}`,\n };\n }\n\n // recipientPkFingerprint — hex string, 32 chars (16-byte sha384 prefix).\n if (!isString(env.recipientPkFingerprint) || !/^[0-9a-f]{32}$/i.test(env.recipientPkFingerprint)) {\n return {\n ok: false,\n reason: 'recipientPkFingerprint must be a 32-char hex string (16-byte sha384 prefix)',\n };\n }\n\n // aesAlgorithm (pinned)\n if (env.aesAlgorithm !== 'aes-256-gcm') {\n return { ok: false, reason: `aesAlgorithm must be 'aes-256-gcm'` };\n }\n\n // aesIv (base64, 12 bytes)\n if (!isNonEmptyString(env.aesIv)) {\n return { ok: false, reason: 'aesIv must be a non-empty base64 string' };\n }\n const ivBytes = tryDecodeBase64(env.aesIv);\n if (!ivBytes) return { ok: false, reason: 'aesIv is not valid base64' };\n if (ivBytes.length !== AES_IV_LEN) {\n return {\n ok: false,\n reason: `aesIv length ${ivBytes.length} != expected ${AES_IV_LEN} (AES-GCM 96-bit nonce)`,\n };\n }\n\n // aesCiphertext (base64) — any length, including 0 (empty plaintext is valid).\n if (typeof env.aesCiphertext !== 'string') {\n return { ok: false, reason: 'aesCiphertext must be a base64 string' };\n }\n // Permit empty string explicitly (0-byte ciphertext from empty plaintext).\n if (env.aesCiphertext.length > 0) {\n const ctBytes = tryDecodeBase64(env.aesCiphertext);\n if (!ctBytes) return { ok: false, reason: 'aesCiphertext is not valid base64' };\n }\n\n // aesAuthTag (base64, 16 bytes)\n if (!isNonEmptyString(env.aesAuthTag)) {\n return { ok: false, reason: 'aesAuthTag must be a non-empty base64 string' };\n }\n const tagBytes = tryDecodeBase64(env.aesAuthTag);\n if (!tagBytes) return { ok: false, reason: 'aesAuthTag is not valid base64' };\n if (tagBytes.length !== AES_TAG_LEN) {\n return {\n ok: false,\n reason: `aesAuthTag length ${tagBytes.length} != expected ${AES_TAG_LEN} (AES-GCM 128-bit tag)`,\n };\n }\n\n // kdfAlgorithm (pinned)\n if (env.kdfAlgorithm !== 'hkdf-sha384') {\n return { ok: false, reason: `kdfAlgorithm must be 'hkdf-sha384'` };\n }\n\n // kdfSalt (base64, 16 bytes)\n if (!isNonEmptyString(env.kdfSalt)) {\n return { ok: false, reason: 'kdfSalt must be a non-empty base64 string' };\n }\n const saltBytes = tryDecodeBase64(env.kdfSalt);\n if (!saltBytes) return { ok: false, reason: 'kdfSalt is not valid base64' };\n if (saltBytes.length !== KDF_SALT_LEN) {\n return {\n ok: false,\n reason: `kdfSalt length ${saltBytes.length} != expected ${KDF_SALT_LEN}`,\n };\n }\n\n // kdfLabel — non-empty, reasonable upper bound (256 chars) to catch\n // pathological inputs without imposing a real schema constraint.\n if (!isNonEmptyString(env.kdfLabel)) {\n return { ok: false, reason: 'kdfLabel must be a non-empty string' };\n }\n if (env.kdfLabel.length > 256) {\n return {\n ok: false,\n reason: `kdfLabel length ${env.kdfLabel.length} exceeds 256-char limit`,\n };\n }\n\n // encryptedAt — finite number (millis epoch).\n if (!isFiniteNumber(env.encryptedAt)) {\n return { ok: false, reason: 'encryptedAt must be a finite number (millis epoch)' };\n }\n\n return { ok: true, version: 'kyber-aes-v1' };\n}\n","/**\n * `verifyPqcEnvelope` — external, observation-only PQC envelope verifier.\n *\n * Customer surface for the Kyber/ML-KEM hybrid envelopes the platform emits.\n * Does NOT decrypt — the verifier holds no key material. It checks structural\n * well-formedness and claim plausibility so customers can detect malformed\n * envelopes BEFORE handing them to a decryption routine.\n *\n * Three classes of checks:\n *\n * 1. Schema ({@link validateEnvelopeSchema}) — discriminator literal,\n * required fields, types, base64 decodability, exact-length field\n * invariants per FIPS-203 (ML-KEM ciphertext) + AES-GCM (iv/tag).\n *\n * 2. Timestamp plausibility — `encryptedAt` must fall between FIPS-203\n * standardization (2024-01-01, before which no hybrid envelope could\n * have been validly emitted) and `Date.now()` (future timestamps\n * indicate clock skew / a forged envelope).\n *\n * 3. Optional caller claims — when supplied, `expectedLabel` and\n * `expectedRecipientPkFingerprint` are byte-equality checked against\n * the envelope fields. Useful for verifying an envelope was emitted\n * for the surface the caller expects.\n *\n * Fail-soft at every step — returns a discriminated {@link EnvelopeVerificationResult}\n * with `reason` on failure.\n *\n * @see verifyPqcAttestation - sibling cert verifier (signature provenance).\n */\nimport {\n validateEnvelopeSchema,\n type EnvelopeVersionLiteral,\n} from './envelope-schema-validator';\n\n/**\n * FIPS-203 standardization date — millis since epoch for 2024-01-01 00:00 UTC.\n *\n * No production hybrid envelope can legitimately predate this. Customer-facing\n * floor for `encryptedAt` plausibility check; overridable via\n * `VerifyEnvelopeOptions.minTimestamp` for test/staging fixtures.\n */\nexport const KYBER_MIN_TIMESTAMP_MS = 1_704_067_200_000;\n\n/**\n * Detailed verification metadata exposed to callers regardless of valid/invalid\n * outcome. Partial on failure (whichever fields were inspectable before the\n * first failure).\n */\nexport type VerificationDetails = {\n /** Envelope version literal (`kyber-aes-v1`). */\n version: string;\n /** ML-KEM parameter set. */\n kemAlgorithm?: 'ml-kem-768' | 'ml-kem-1024';\n /** 32-char hex sha384-prefix of the recipient pk; only present for hybrid. */\n recipientPkFingerprint?: string;\n /** Per-surface HKDF label as recorded by the writer. */\n kdfLabel?: string;\n /** Writer wall-clock at emit time (millis epoch). */\n encryptedAt?: number;\n /** True iff all schema-shape checks passed. */\n schemaValid: boolean;\n /** True iff every base64 field decoded successfully. (Subsumed by `schemaValid`.) */\n base64Valid: boolean;\n /** True iff `encryptedAt` fell within `[minTimestamp, maxTimestamp]`. */\n timestampPlausible: boolean;\n};\n\n/**\n * Discriminated verification outcome. `valid: true` indicates the envelope is\n * STRUCTURALLY well-formed for its declared version AND every caller-supplied\n * claim matched. It does NOT indicate the platform actually emitted the\n * envelope (no signature is checked here) nor that the ciphertext will decrypt\n * (no key is available here) — those gates belong to {@link verifyPqcAttestation}\n * (signature provenance) and the platform's hybrid-decrypt routine (ciphertext\n * authenticity).\n */\nexport type EnvelopeVerificationResult =\n | {\n valid: true;\n version: EnvelopeVersionLiteral;\n details: VerificationDetails;\n }\n | {\n valid: false;\n reason: string;\n details?: Partial<VerificationDetails>;\n };\n\n/**\n * Caller-facing knobs for `verifyPqcEnvelope`.\n *\n * @field expectedLabel — when set, the envelope's `kdfLabel` MUST equal this\n * value (byte-equality). Surface-mismatch defense — catches envelopes\n * addressed to a different consumer being replayed at this consumer.\n *\n * @field expectedRecipientPkFingerprint — when set, the envelope's\n * `recipientPkFingerprint` MUST equal this value (case-insensitive hex\n * compare). Useful for asserting the envelope was emitted for a specific\n * cluster Kyber keypair.\n *\n * @field minTimestamp — lower bound (inclusive, millis epoch) for\n * `encryptedAt`. Default: `KYBER_MIN_TIMESTAMP_MS` (2024-01-01).\n *\n * @field maxTimestamp — upper bound (inclusive, millis epoch) for\n * `encryptedAt`. Default: `Date.now()` evaluated at verify time.\n * Pass a fixed future-skew tolerance (e.g. `Date.now() + 60_000`) if\n * verifying envelopes from a known-skewed source.\n */\nexport type VerifyEnvelopeOptions = {\n expectedLabel?: string;\n expectedRecipientPkFingerprint?: string;\n minTimestamp?: number;\n maxTimestamp?: number;\n};\n\n/**\n * Verify a Kyber/ML-KEM hybrid envelope for structural well-formedness and\n * claim consistency.\n *\n * Pure function — no I/O, no key material, no decryption. Customer code\n * calls this BEFORE passing the envelope to a decryption surface; if it\n * returns `{ valid: false }`, the envelope is malformed and decryption\n * would fail anyway (often with a less-actionable error).\n *\n * The signature is async to leave room for future network-backed checks\n * (e.g. resolving the recipient-pk fingerprint against a registry). The\n * current implementation makes no network calls.\n *\n * @param envelope - Untrusted candidate envelope (typically parsed JSON).\n * @param options - Optional caller claims and timestamp bounds. See\n * {@link VerifyEnvelopeOptions}.\n * @returns A discriminated {@link EnvelopeVerificationResult}. Never throws.\n *\n * @example\n * ```ts\n * import { verifyPqcEnvelope } from '@hsuite/smart-engines-sdk/pqc-verify-envelope';\n *\n * const result = await verifyPqcEnvelope(envelopeJson, {\n * expectedLabel: 'smart-app-secret-envelope-v1',\n * });\n * if (!result.valid) {\n * throw new Error(`Bad envelope: ${result.reason}`);\n * }\n * ```\n */\nexport async function verifyPqcEnvelope(\n envelope: unknown,\n options: VerifyEnvelopeOptions = {},\n): Promise<EnvelopeVerificationResult> {\n // ── Step 1: Schema validation ─────────────────────────────────────────────\n const schema = validateEnvelopeSchema(envelope);\n if (!schema.ok) {\n return {\n valid: false,\n reason: `schema-invalid: ${schema.reason}`,\n details: { schemaValid: false, base64Valid: false, timestampPlausible: false },\n };\n }\n\n const env = envelope as Record<string, unknown>;\n const version = schema.version;\n\n // ── Step 2: Build inspectable details ─────────────────────────────────────\n const details: VerificationDetails = {\n version,\n schemaValid: true,\n base64Valid: true,\n // Set to false below if the timestamp plausibility check fails.\n timestampPlausible: true,\n };\n\n {\n details.kemAlgorithm = env.kemAlgorithm as 'ml-kem-768' | 'ml-kem-1024';\n details.recipientPkFingerprint = env.recipientPkFingerprint as string;\n details.kdfLabel = env.kdfLabel as string;\n details.encryptedAt = env.encryptedAt as number;\n\n // ── Step 3: Timestamp plausibility ─────────────────────────────────────\n const minTs = options.minTimestamp ?? KYBER_MIN_TIMESTAMP_MS;\n const maxTs = options.maxTimestamp ?? Date.now();\n const ts = details.encryptedAt as number;\n if (ts < minTs) {\n details.timestampPlausible = false;\n return {\n valid: false,\n reason: `timestamp-before-minimum: encryptedAt=${ts} < min=${minTs}`,\n details,\n };\n }\n if (ts > maxTs) {\n details.timestampPlausible = false;\n return {\n valid: false,\n reason: `timestamp-in-future: encryptedAt=${ts} > max=${maxTs}`,\n details,\n };\n }\n\n // ── Step 4: Optional caller-claim checks ───────────────────────────────\n if (options.expectedLabel !== undefined) {\n if (details.kdfLabel !== options.expectedLabel) {\n return {\n valid: false,\n reason: `label-mismatch: expected='${options.expectedLabel}' actual='${details.kdfLabel}'`,\n details,\n };\n }\n }\n if (options.expectedRecipientPkFingerprint !== undefined) {\n const a = (details.recipientPkFingerprint ?? '').toLowerCase();\n const b = options.expectedRecipientPkFingerprint.toLowerCase();\n if (a !== b) {\n return {\n valid: false,\n reason: `recipient-pk-fingerprint-mismatch: expected='${b}' actual='${a}'`,\n details,\n };\n }\n }\n }\n\n return { valid: true, version, details };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hsuite/smart-engines-sdk",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Simplified client SDK for Smart Engines multi-chain infrastructure",
5
5
  "sideEffects": false,
6
6
  "main": "dist/index.js",
@@ -61,6 +61,21 @@
61
61
  "registry": "https://registry.npmjs.org",
62
62
  "access": "public"
63
63
  },
64
+ "license": "MIT",
65
+ "author": "Smart Engines",
66
+ "homepage": "https://github.com/HSuiteNetwork/smart-engines-multichain/tree/master/libs/smartengine-sdk#readme",
67
+ "keywords": [
68
+ "hsuite",
69
+ "xrpl",
70
+ "smart-engines",
71
+ "sdk",
72
+ "web3",
73
+ "multichain",
74
+ "blockchain"
75
+ ],
76
+ "engines": {
77
+ "node": ">=20"
78
+ },
64
79
  "repository": {
65
80
  "type": "git",
66
81
  "url": "https://github.com/HSuiteNetwork/smart-engines-multichain",