From af19c31ee67dd014df4938221521c68080180973 Mon Sep 17 00:00:00 2001 From: Pierre Leroux Date: Mon, 14 Sep 2026 11:42:23 +0200 Subject: [PATCH 1/2] Refactor LCP CRL caching and add build-time fallback --- src/main.ts | 161 +++---------- src/main/services/lcpCrlCache.ts | 213 ++++++++++++++++++ .../r2-lcp-js/parser/epub/lcp-certificate.ts | 40 ++-- src/r2-xxx-js/r2-lcp-js/parser/epub/lcp.ts | 8 +- 4 files changed, 270 insertions(+), 152 deletions(-) create mode 100644 src/main/services/lcpCrlCache.ts diff --git a/src/main.ts b/src/main.ts index fac769e411..247cadbdb8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,7 @@ import * as path from "node:path"; import * as fs from "node:fs"; import { commandLineMainEntry } from "readium-desktop/main/cli"; import { httpGetWithAuth } from "readium-desktop/main/network/http"; -import { CRL_URL, DUMMY_CRL } from "@r2-lcp-js/parser/epub/lcp-certificate"; +import { BUILD_CRL, BUILD_CRL_CACHED_AT, CRL_URL } from "@r2-lcp-js/parser/epub/lcp-certificate"; import { setLcpNativePluginPath, setCRLGetter } from "@r2-lcp-js/parser/epub/lcp"; import { initGlobalConverters_OPDS } from "@r2-opds-js/opds/init-globals"; import { @@ -25,6 +25,7 @@ import { app } from "electron"; import { _APP_NAME, _APP_VERSION, _PACK_NAME } from "readium-desktop/preprocessor-directives"; import { FORCE_PROD_DB_IN_DEV, USER_DATA_FOLDER } from "readium-desktop/common/constant"; import { appendFileSyncWithRotation } from "readium-desktop/utils/log"; +import { LcpCrlCache } from "./main/services/lcpCrlCache"; // isURL() excludes the file: and data: URL protocols; the compile-time TLD policy decides whether localhost / non-TLD hosts are accepted (note that ftp: is accepted) // import isURL from "validator/lib/isURL"; @@ -71,139 +72,37 @@ initGlobalConverters_GENERIC(); const lcpNativePluginPath = path.normalize(path.join(__dirname, "external-assets", "lcp.node")); setLcpNativePluginPath(lcpNativePluginPath); -interface ILcpCrlCache { - crlPem: string; - etag: string | undefined; - lastModified: string | undefined; - validatedAt: number; - expiresAt: number; - refreshPromise: Promise | undefined; -} - -const lcpCrlCache: ILcpCrlCache = { - crlPem: DUMMY_CRL, - etag: undefined, - lastModified: undefined, - validatedAt: 0, - expiresAt: 0, - refreshPromise: undefined, -}; - -const LCP_CRL_CACHE_FALLBACK_FRESHNESS_MS = 60 * 60 * 1000; - -const getCacheControlMaxAgeMs = (cacheControl: string | undefined): number | undefined => { - if (!cacheControl) { - return undefined; - } - let maxAgeMs: number | undefined; - for (const directive of cacheControl.split(",")) { - const [rawName, rawValue] = directive.trim().split("=", 2); - const name = rawName.trim().toLowerCase(); - const value = rawValue?.trim(); - if (name === "no-cache" || name === "no-store") { - return 0; - } - if (name === "max-age" && value) { - const seconds = Number(value.replace(/^"|"$/g, "")); - if (Number.isFinite(seconds) && seconds >= 0) { - maxAgeMs = seconds * 1000; - } - } - } - return maxAgeMs; -}; - -const getLcpCrlExpiresAt = (headers: { get(name: string): string | null } | undefined, validatedAt: number): number => { - const cacheControlMaxAgeMs = getCacheControlMaxAgeMs(headers?.get("cache-control") || undefined); - return validatedAt + (cacheControlMaxAgeMs ?? LCP_CRL_CACHE_FALLBACK_FRESHNESS_MS); -}; - -const isLcpCrlCacheExpired = () => - Date.now() >= lcpCrlCache.expiresAt; - -const refreshLcpCrlCache = (): Promise => { - debug("REFRESH LCP CRL REQUEST", lcpCrlCache); - if (typeof lcpCrlCache.refreshPromise !== "undefined") { - return lcpCrlCache.refreshPromise; - } - - lcpCrlCache.refreshPromise = (async () => { - try { - const headers: Record = { +const lcpCrlCache = new LcpCrlCache({ + defaultCrlPem: BUILD_CRL, + defaultCrlCachedAt: BUILD_CRL_CACHED_AT, + fetchCrl: async () => { + debug("LCP CRL HTTP fetch"); + // RFC 2585 Security Considerations: CRL retrieval does not need + // authentication, so this uses Thorium's no-auth HTTP helper. + const res = await httpGetWithAuth(false)(CRL_URL, { + headers: { Accept: ContentType.PkixCrl, - }; - if (lcpCrlCache.etag) { - headers["If-None-Match"] = lcpCrlCache.etag; - } - if (lcpCrlCache.lastModified) { - headers["If-Modified-Since"] = lcpCrlCache.lastModified; - } - // RFC 2585 Security Considerations: CRL retrieval does not need - // authentication, so this uses Thorium's no-auth HTTP helper. - const res = await httpGetWithAuth(false)(CRL_URL, { - headers, - // Reject redirects so the native LCP plugin receives bytes from the - // configured CRL endpoint only. - redirect: "error", - }); - if (res.statusCode === 304) { - lcpCrlCache.lastModified = res.response.headers?.get("last-modified") || lcpCrlCache.lastModified; - const validatedAt = Date.now(); - lcpCrlCache.validatedAt = validatedAt; - lcpCrlCache.expiresAt = getLcpCrlExpiresAt(res.response.headers, validatedAt); - debug("LCP CRL HTTP cache refreshed: not modified"); - return; - } - const mediaType = res.contentType?.split(";")[0].trim().toLowerCase(); - // RFC 5280 section 4.2.1.13 says HTTP CRL distribution point URIs point - // to a single DER encoded CRL, and HTTP servers SHOULD respond with - // Content-Type application/pkix-crl. - // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.13 - // RFC 2585 section 4.2 registers application/pkix-crl. - // https://datatracker.ietf.org/doc/html/rfc2585#section-4.2 - // RFC 2585 Security Considerations: authentication is not necessary - // to retrieve certificates and CRLs. - // https://datatracker.ietf.org/doc/html/rfc2585#page-6 - if (res.statusCode === 200 && mediaType === ContentType.PkixCrl) { - const buf = await res.response.buffer(); - const lcplStr = "-----BEGIN X509 CRL-----\n" + buf.toString("base64") + "\n-----END X509 CRL-----"; - lcpCrlCache.crlPem = lcplStr; - lcpCrlCache.etag = res.response.headers?.get("etag") || undefined; // '"295-65b0d9de8addd"' double quote is included - lcpCrlCache.lastModified = res.response.headers?.get("last-modified") || undefined; - const validatedAt = Date.now(); - lcpCrlCache.validatedAt = validatedAt; - lcpCrlCache.expiresAt = getLcpCrlExpiresAt(res.response.headers, validatedAt); - debug("LCP CRL HTTP fetch success"); - debug(lcplStr); - return; - } - debug(`LCP CRL HTTP fetch fail; keeping cached CRL (${res.statusCode} ${res.contentType})`); - } catch (err) { - debug("LCP CRL HTTP fetch error; keeping cached CRL"); - debug(err); + }, + // Reject redirects so the native LCP plugin receives bytes from the + // configured CRL endpoint only. + redirect: "error", + }); + if (res.statusCode !== 200 || !res.response?.buffer) { + throw new Error(`LCP CRL HTTP fetch failed (${res.statusCode || res.statusMessage || "unknown error"})`); } - })().finally(() => { - lcpCrlCache.refreshPromise = undefined; - }); - - return lcpCrlCache.refreshPromise; -}; -const initLcpCrlCacheValidatedAt = lcpCrlCache.validatedAt; -refreshLcpCrlCache().then(() => { - debug(lcpCrlCache.validatedAt > initLcpCrlCacheValidatedAt ? "INIT LCP CRL LOADED" : "INIT LCP CRL FAILED"); - debug(lcpCrlCache); -}).catch((err) => { - debug("INIT LCP CRL FAILED"); - debug(err); -}); - -setCRLGetter(async (): Promise => { - const crlPem = lcpCrlCache.crlPem; - if (isLcpCrlCacheExpired()) { - void refreshLcpCrlCache(); - } - return crlPem; + const der = await res.response.buffer(); + debug("LCP CRL HTTP fetch success"); + return der; + }, + log: (message, error) => { + debug(message); + if (typeof error !== "undefined") { + debug(error); + } + }, }); +lcpCrlCache.preload(); +setCRLGetter((): Promise => lcpCrlCache.retrieve()); app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); app.commandLine.appendSwitch("enable-speech-dispatcher"); diff --git a/src/main/services/lcpCrlCache.ts b/src/main/services/lcpCrlCache.ts new file mode 100644 index 0000000000..52ce020250 --- /dev/null +++ b/src/main/services/lcpCrlCache.ts @@ -0,0 +1,213 @@ +// ==LICENSE-BEGIN== +// Copyright 2017 European Digital Reading Lab. All rights reserved. +// Licensed to the Readium Foundation under one or more contributor license agreements. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file exposed on Github (readium) in the project repository. +// ==LICENSE-END== + +import { Buffer } from "node:buffer"; + +const PEM_HEADER = "-----BEGIN X509 CRL-----"; +const PEM_FOOTER = "-----END X509 CRL-----"; + +export const LCP_CRL_CACHE_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1000; + +interface ILcpCrlCacheEntry { + crlPem: string; + cachedAt: number; +} + +interface ILcpCrlMemoryEntry { + crlPem: string; + isExpired: boolean; +} + +export const encodeLcpCrlPem = (der: Uint8Array): string => + `${PEM_HEADER}${Buffer.from(der).toString("base64")}${PEM_FOOTER}`; + +/** + * Checks that the data has the outer DER structure of an X.509 CRL. + * + * CertificateList is a SEQUENCE whose first element, tbsCertList, is another + * SEQUENCE. This deliberately does not parse the full CRL; it rejects non-DER + * responses such as captive portal HTML and truncated downloads. + */ +export const isX509Crl = (data: Uint8Array): boolean => { + if (data.length < 2 || data[0] !== 0x30) { + return false; + } + + let headerSize: number; + let contentLength: number; + if ((data[1] & 0x80) === 0) { + headerSize = 2; + contentLength = data[1]; + } else { + const lengthSize = data[1] & 0x7F; + if (lengthSize < 1 || lengthSize > 4 || data.length < 2 + lengthSize) { + return false; + } + headerSize = 2 + lengthSize; + contentLength = data.slice(2, headerSize).reduce((length, byte) => length * 256 + byte, 0); + } + + if (headerSize + contentLength !== data.length) { + return false; + } + + return data.length > headerSize && data[headerSize] === 0x30; +}; + +interface ILcpCrlCacheOptions { + fetchCrl: () => Promise; + expirationMs?: number; + defaultCrlPem?: string; + defaultCrlCachedAt?: number; + now?: () => number; + log?: (message: string, error?: unknown) => void; +} + +/** + * In-memory CRL cache following readium/swift-toolkit's refresh behavior, with + * Swift's readLocal() model represented here as readMemory(). If no memory CRL + * has been seeded, a missing CRL blocks on the network, while an expired valid + * CRL is returned immediately and refreshed in the background. + * + * Thorium seeds this cache with the build-time BUILD_CRL constant and its + * BUILD_CRL_CACHED_AT timestamp. This gives liblcp a trusted fallback before + * the first network refresh completes, while using the same freshness rule for + * the bundled CRL as for a CRL fetched during the current process. + * + * This intentionally mirrors the Swift refresh algorithm, not its persistence + * semantics. The Swift toolkit implementation stores its cache in UserDefaults, + * which may be app-container scoped on sandboxed Apple platforms. In Electron, + * the closest equivalent is app.getPath("userData"), a normal per-user app data + * directory that another same-user process can usually modify or roll back. + * + * If this cache ever becomes disk-backed, treat the disk bytes and timestamps as + * untrusted hints only. Before promoting a persisted CRL into memory, verify the + * CRL signature against a pinned LCP CA/CRL-signing certificate or public key, + * verify issuer/AuthorityKeyIdentifier, require cRLSign on the signing + * certificate, and prefer signed CRL fields such as thisUpdate, nextUpdate and + * CRLNumber over a local cachedAt value. Invalid persisted data should be + * ignored and refreshed from the network instead of being passed to liblcp. + * + * Current logical model: + * startup: + * BUILD_CRL is seeded in memory with BUILD_CRL_CACHED_AT + * preload starts a network refresh only if the memory CRL is expired + * unlock: + * fresh memory CRL -> return it + * expired memory CRL -> return it and refresh in the background + * no memory CRL seed -> wait for one network refresh before unlocking + * + * Logical model for a future disk-backed cache: + * startup: + * disk CRL present and authenticated -> load into memory, then refresh if stale + * disk CRL missing/invalid/expired -> fall back to BUILD_CRL and refresh + * unlock: + * fresh memory CRL -> return it + * stale but authentic CRL -> return it and refresh in the background + * no authentic local CRL -> use BUILD_CRL or wait for one network refresh + */ +export class LcpCrlCache { + private readonly expirationMs: number; + private readonly fetchCrl: () => Promise; + private readonly log: (message: string, error?: unknown) => void; + private readonly now: () => number; + private memoryCrl: ILcpCrlCacheEntry | undefined; + private refreshPromise: Promise | undefined; + + constructor(options: ILcpCrlCacheOptions) { + this.expirationMs = options.expirationMs ?? LCP_CRL_CACHE_EXPIRATION_MS; + this.fetchCrl = options.fetchCrl; + this.log = options.log ?? (() => undefined); + this.now = options.now ?? Date.now; + + if (options.defaultCrlPem) { + this.memoryCrl = { + crlPem: options.defaultCrlPem, + cachedAt: options.defaultCrlCachedAt ?? this.now() - this.expirationMs, + }; + } + } + + /** Warms a missing or expired cache without making startup wait. */ + public preload(): void { + this.log("LCP CRL PRELOAD"); + const memoryCrl = this.readMemory(); + if (memoryCrl?.isExpired ?? true) { + void this.refresh().catch((error) => this.log("LCP CRL preload failed", error)); + } + } + + public async retrieve(): Promise { + this.log("LCP CRL RETRIEVE"); + const memoryCrl = this.readMemory(); + if (!memoryCrl) { + return this.refresh(); + } + + if (memoryCrl.isExpired) { + void this.refresh().catch((error) => this.log("LCP CRL background refresh failed", error)); + } + return memoryCrl.crlPem; + } + + /** + * Swift's CRLService.readLocal() reads and validates UserDefaults. Thorium + * deliberately keeps the cache memory-only, so the equivalent decision point + * simply exposes the current memory CRL and whether it is expired. + */ + private readMemory(): ILcpCrlMemoryEntry | undefined { + const memoryCrl = this.memoryCrl; + if (!memoryCrl) { + return undefined; + } + + return { + crlPem: memoryCrl.crlPem, + isExpired: this.isExpired(memoryCrl), + }; + } + + private isExpired(entry: ILcpCrlCacheEntry): boolean { + return this.now() - entry.cachedAt >= this.expirationMs; + } + + /** Starts a refresh, or returns the refresh already in flight. */ + private refresh(): Promise { + if (typeof this.refreshPromise !== "undefined") { + return this.refreshPromise; + } + + this.log("LCP CRL REFRESH"); + const refreshPromise = this.fetchAndSave(); + this.refreshPromise = refreshPromise; + void refreshPromise.then( + () => this.clearRefreshPromise(refreshPromise), + () => this.clearRefreshPromise(refreshPromise), + ); + return refreshPromise; + } + + private clearRefreshPromise(refreshPromise: Promise): void { + if (this.refreshPromise === refreshPromise) { + this.refreshPromise = undefined; + } + } + + private async fetchAndSave(): Promise { + const der = await this.fetchCrl(); + if (!isX509Crl(der)) { + throw new Error("The LCP CRL response is not a valid DER-encoded X.509 CRL"); + } + + const entry: ILcpCrlCacheEntry = { + crlPem: encodeLcpCrlPem(der), + cachedAt: this.now(), + }; + this.memoryCrl = entry; + return entry.crlPem; + } +} diff --git a/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts b/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts index cbfecdd2d8..d6f02b8d61 100644 --- a/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts +++ b/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts @@ -15,20 +15,26 @@ export const CRL_URL = "http://crl.edrlab.telesec.de/rl/EDRLab_CA.crl"; export const CRL_URL_ALT = "http://crl.edrlab.telesec.de/rl/Readium_LCP_Root_CA.crl"; // curl http://crl.edrlab.telesec.de/rl/Readium_LCP_Root_CA.crl -s | openssl crl -inform DER -text -noout -export const DUMMY_CRL = `-----BEGIN X509 CRL----- -MIICrTCBljANBgkqhkiG9w0BAQQFADBnMQswCQYDVQQGEwJGUjEOMAwGA1UEBxMF -UGFyaXMxDzANBgNVBAoTBkVEUkxhYjESMBAGA1UECxMJTENQIFRlc3RzMSMwIQYD -VQQDExpFRFJMYWIgUmVhZGl1bSBMQ1AgdGVzdCBDQRcNMTcwOTI2MTM1NTE1WhcN -MjcwOTI0MTM1NTE1WjANBgkqhkiG9w0BAQQFAAOCAgEA27f50xnlaKGUdqs6u6rD -WsR75z+tZrH4J2aA5E9I/K5fNe20FftQZb6XNjVQTNvawoMW0q+Rh9dVjDnV5Cfw -ptchu738ZQr8iCOLQHvIM6wqQj7XwMqvyNaaeGMZxfRMGlx7T9DOwvtWFCc5X0ik -YGPPV19CFf1cas8x9Y3LE8GmCtX9eUrotWLKRggG+qRTCri/SlaoicfzqhViiGeL -dW8RpG/Q6ox+tLHti3fxOgZarMgMbRmUa6OTh8pnxrfnrdtD2PbwACvaEMCpNCZR -aSTMRmIxw8UUbUA/JxDIwyISGn3ZRgbFAglYzaX80rSQZr6e0bFlzHl1xZtZ0Raz -GQWP9vvfH5ESp6FsD98g//VYigatoPz/EKU4cfP+1W/Zrr4jRSBFB37rxASXPBcx -L8cerb9nnRbAEvIqxnR4e0ZkhMyqIrLUZ3Jva0fC30kdtp09/KJ22mXKBz85wUQa -7ihiSz7pov0R9hpY93fvt++idHBECRNGOeBC4wRtGxpru8ZUa0/KFOD0HXHMQDwV -cIa/72T0okStOqjIOcWflxl/eAvUXwtet9Ht3o9giSl6hAObAeleMJOB37Bq9ASf -h4w7d5he8zqfsCGjaG1OVQNWVAGxQQViWVysfcJohny4PIVAc9KkjCFa/QrkNGjr -kUiV/PFCwL66iiF666DrXLY= ------END X509 CRL-----`; +/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// run this at each new build to update the CRL: +node -e "const url='http://crl.edrlab.telesec.de/rl/EDRLab_CA.crl'; const tick=String.fromCharCode(96); const now=Date.now(); fetch(url).then(async r=>{ if(!r.ok) throw new Error('HTTP '+r.status); const buf=Buffer.from(await r.arrayBuffer()); const b64=buf.toString('base64').match(/.{1,64}/g).join('\n'); console.log('// Build-time CRL fallback generated from CRL_URL on '+new Date(now).toISOString()+'.'); console.log('export const BUILD_CRL_CACHED_AT = '+now+';'); console.log('export const BUILD_CRL = '+tick+'-----BEGIN X509 CRL-----\n'+b64+'\n-----END X509 CRL-----'+tick+';'); }).catch(e=>{ console.error(e); process.exit(1); });" +*/ + +// Build-time CRL fallback generated from CRL_URL on 2026-09-14T09:37:19.832Z. +export const BUILD_CRL_CACHED_AT = 1789378639832; +export const BUILD_CRL = `-----BEGIN X509 CRL----- +MIICkTCCAXkCAQEwDQYJKoZIhvcNAQELBQAwQjETMBEGA1UEChMKZWRybGFiLm9y +ZzEXMBUGA1UECxMOZWRybGFiLm9yZyBMQ1AxEjAQBgNVBAMTCUVEUkxhYiBDQRcN +MjYwOTEzMTQxNzA4WhcNMjYwOTE4MTQxNzA3WjCB0DAnAgg9/PrnYyy4ABcNMjYw +ODA1MjAyMTEzWjAMMAoGA1UdFQQDCgEBMCgCCQCoPyWN9DqSBhcNMjYwNTI1MDc1 +NTAwWjAMMAoGA1UdFQQDCgEGMCgCCQCwrtK1lYNPKhcNMjYwODI4MTcyNzQ1WjAM +MAoGA1UdFQQDCgEGMCcCCAD7am95HSWbFw0yNjAzMjMxMjQ1NThaMAwwCgYDVR0V +BAMKAQYwKAIJAKjr9Zx5OfipFw0yNjAyMTIxMDE0MDJaMAwwCgYDVR0VBAMKAQag +MDAuMB8GA1UdIwQYMBaAFNxc/JPkH5/usLrqUgsrylJc4MmHMAsGA1UdFAQEAgIN +/jANBgkqhkiG9w0BAQsFAAOCAQEAhHCfMKjWaIORdex9iYL2WYOK/qOyRegPa+uT +TeS6SAqFPwT8EWuo0aa9dSt2GXtMNfPEmOyxioVvhV2gfYyjbmoDyUJDlkySUAcO +c4voHAuf4wT2y1GzuvI4pQNn3KkZu35HrWBy5pMFwrBkTSRlbTtYESpWlXAezrmD +YN/kMSjDLSyan15L9r1Hkp1gKMxTluUByQW4pm1zY3MR19Gkew8RivQWt5yPUfCD +3+HMgBYMnbHmxpUPi3LyfswTiMZxNT2BHiHy6Qdg3uC25tTQs3sq5ih1ErRMuCl/ +MVZZnnZh4KBBFzgcZsSFf8Kggn5SW9BUZRGN4QHuAY9Sma4fAQ== +-----END X509 CRL-----`; \ No newline at end of file diff --git a/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp.ts b/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp.ts index 91a772c1db..49ae4a77e1 100644 --- a/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp.ts +++ b/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp.ts @@ -19,7 +19,7 @@ import { JsonElementType, JsonObject, JsonProperty } from "ta-json-x"; // import { streamToBufferPromise } from "@r2-utils-js/_utils/stream/BufferUtils"; -import { DUMMY_CRL } from "./lcp-certificate"; +import { BUILD_CRL } from "./lcp-certificate"; import { Encryption } from "./lcp-encryption"; import { Link } from "./lcp-link"; import { Rights } from "./lcp-rights"; @@ -232,7 +232,7 @@ export class LCP { this.init(); if (this._usesNativeNodePlugin) { - const crlPem = _getCRLPem ? await _getCRLPem() : DUMMY_CRL; + const crlPem = _getCRLPem ? await _getCRLPem() : BUILD_CRL; // always generates USER_KEY_CHECK_INVALID = 141 const sha256DummyPassphrase = "0".repeat(64); @@ -290,7 +290,7 @@ export class LCP { } if (this._usesNativeNodePlugin) { - const crlPem = _getCRLPem ? await _getCRLPem() : DUMMY_CRL; + const crlPem = _getCRLPem ? await _getCRLPem() : BUILD_CRL; return new Promise((resolve, reject) => { @@ -492,7 +492,7 @@ export class LCP { // // // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors // // reject(err); // debug(err); - // resolve(DUMMY_CRL); + // resolve(BUILD_CRL); // }; // const success = async (response: request.RequestResponse) => { From 2f6ba2d8fa06a4b25574f8607b6a04514f048944 Mon Sep 17 00:00:00 2001 From: Pierre Leroux Date: Mon, 14 Sep 2026 11:52:15 +0200 Subject: [PATCH 2/2] lint --- src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts b/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts index d6f02b8d61..ef79f5e035 100644 --- a/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts +++ b/src/r2-xxx-js/r2-lcp-js/parser/epub/lcp-certificate.ts @@ -37,4 +37,4 @@ c4voHAuf4wT2y1GzuvI4pQNn3KkZu35HrWBy5pMFwrBkTSRlbTtYESpWlXAezrmD YN/kMSjDLSyan15L9r1Hkp1gKMxTluUByQW4pm1zY3MR19Gkew8RivQWt5yPUfCD 3+HMgBYMnbHmxpUPi3LyfswTiMZxNT2BHiHy6Qdg3uC25tTQs3sq5ih1ErRMuCl/ MVZZnnZh4KBBFzgcZsSFf8Kggn5SW9BUZRGN4QHuAY9Sma4fAQ== ------END X509 CRL-----`; \ No newline at end of file +-----END X509 CRL-----`;