mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 21:02:50 +03:00
security(deps): pin and verify tls-client native artifacts
This commit is contained in:
22
Dockerfile
22
Dockerfile
@@ -83,6 +83,8 @@ COPY open-sse/package.json ./open-sse/package.json
|
||||
COPY scripts/build/postinstall.mjs ./scripts/build/postinstall.mjs
|
||||
COPY scripts/build/postinstallSupport.mjs ./scripts/build/postinstallSupport.mjs
|
||||
COPY scripts/build/native-binary-compat.mjs ./scripts/build/native-binary-compat.mjs
|
||||
COPY scripts/build/fixTlsClientNodeBinary.mjs ./scripts/build/fixTlsClientNodeBinary.mjs
|
||||
COPY open-sse/config/tlsClientNativeManifest.json ./open-sse/config/tlsClientNativeManifest.json
|
||||
ENV NPM_CONFIG_LEGACY_PEER_DEPS=true
|
||||
# --ignore-scripts blocks broad dependency install/postinstall hooks, closing
|
||||
# the supply-chain attack surface where a transitive dep can run arbitrary code
|
||||
@@ -104,24 +106,18 @@ RUN test -f package-lock.json \
|
||||
# instead of `npx --yes`, which would install an arbitrary registry version
|
||||
# on-demand and run its lifecycle scripts (Sonar docker:S6505).
|
||||
#
|
||||
# tls-client-node (chatgpt-web/claude-web/grok-web/lmarena/perplexity-web TLS
|
||||
# impersonation) hits the same --ignore-scripts wall: its own postinstall.js
|
||||
# fetches a platform .so/.dylib/.dll from the bogdanfinn/tls-client GitHub
|
||||
# Releases API and is never invoked when npm ci skips lifecycle scripts. Unlike
|
||||
# better-sqlite3 above, that script never throws on failure — it only
|
||||
# `console.warn`s and exits 0 — so a rate-limited or offline build would
|
||||
# otherwise succeed silently with an empty bin/ and only fail at first request
|
||||
# in production (TlsClientUnavailableError, #7802). Run it explicitly here so
|
||||
# a broken/rate-limited fetch fails the BUILD loudly instead of shipping a
|
||||
# broken image.
|
||||
# tls-client-node (shared by six web-provider transports) hits the same
|
||||
# --ignore-scripts wall. Its upstream postinstall downloads the latest native
|
||||
# release without verifying a checksum and exits 0 on failure. Our repair helper
|
||||
# pins bogdanfinn/tls-client v1.15.1, checks GitHub's official SHA-256 for this
|
||||
# platform, and runs in strict mode so Docker cannot ship an absent or tampered
|
||||
# library.
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-npm-cache,target=/root/.npm \
|
||||
npm ci --include=optional --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
|
||||
&& (cd node_modules/better-sqlite3 \
|
||||
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild) \
|
||||
&& node -e "require('better-sqlite3')(':memory:').close()" \
|
||||
&& node node_modules/tls-client-node/scripts/postinstall.js \
|
||||
&& (test -n "$(find node_modules/tls-client-node/bin -mindepth 1 -print -quit 2>/dev/null)" \
|
||||
|| (echo "tls-client-node native binary missing after postinstall — GitHub API fetch likely rate-limited or failed (#7802)" >&2 && exit 1))
|
||||
&& node scripts/build/fixTlsClientNodeBinary.mjs --strict
|
||||
|
||||
# Build with Turbopack (stable in Next 16, the repo default). The v3.8.27-era
|
||||
# TurbopackInternalError panic ("entered unreachable code: there must be a path to a
|
||||
|
||||
@@ -24,3 +24,295 @@ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPO
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
||||
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## fatihkabakk/tls-client-node 0.2.0
|
||||
|
||||
OmniRoute includes [`tls-client-node@0.2.0`](https://github.com/fatihkabakk/tls-client-node/tree/v0.2.0)
|
||||
as an optional runtime dependency. The package is source-available under Apache License 2.0
|
||||
with the Commons Clause License Condition v1.0. The following license and NOTICE blocks are
|
||||
reproduced verbatim from the tagged primary sources.
|
||||
|
||||
### tls-client-node license
|
||||
|
||||
<!-- BEGIN VERBATIM: tls-client-node@0.2.0 LICENSE -->
|
||||
|
||||
```text
|
||||
"Commons Clause" License Condition v1.0
|
||||
|
||||
The Software is provided to you by the Licensor under the License, as defined
|
||||
below, subject to the following condition.
|
||||
|
||||
Without limiting other conditions in the License, the grant of rights under the
|
||||
License will not include, and the License does not grant to you, the right to
|
||||
Sell the Software.
|
||||
|
||||
For purposes of the foregoing, "Sell" means practicing any or all of the rights
|
||||
granted to you under the License to provide to third parties, for a fee or other
|
||||
consideration (including without limitation fees for hosting or consulting/
|
||||
support services related to the Software), a product or service whose value
|
||||
derives, entirely or substantially, from the functionality of the Software. Any
|
||||
license notice or attribution required by the License must also include this
|
||||
Commons Clause License Condition notice.
|
||||
|
||||
Software: tls-client-node
|
||||
License: Apache License 2.0
|
||||
Licensor: Fatih Kabak
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the
|
||||
copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other
|
||||
entities that control, are controlled by, or are under common control with that
|
||||
entity. For the purposes of this definition, "control" means (i) the power,
|
||||
direct or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or
|
||||
more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation source, and
|
||||
configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object
|
||||
code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form,
|
||||
made available under the License, as indicated by a copyright notice that is
|
||||
included in or attached to the work.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original
|
||||
version of the Work and any modifications or additions to that Work or
|
||||
Derivative Works thereof, that is intentionally submitted to Licensor for
|
||||
inclusion in the Work by the copyright owner or by an individual or Legal
|
||||
Entity authorized to submit on behalf of the copyright owner. For the purposes
|
||||
of this definition, "submitted" means any form of electronic, verbal, or
|
||||
written communication sent to the Licensor or its representatives, including
|
||||
but not limited to communication on electronic mailing lists, source code
|
||||
control systems, and issue tracking systems that are managed by, or on behalf
|
||||
of, the Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise designated in
|
||||
writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and
|
||||
such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such
|
||||
Contributor that are necessarily infringed by their Contribution(s) alone or by
|
||||
combination of their Contribution(s) with the Work to which such Contribution(s)
|
||||
was submitted. If You institute patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct or
|
||||
contributory patent infringement, then any patent licenses granted to You under
|
||||
this License for that Work shall terminate as of the date such litigation is
|
||||
filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy
|
||||
of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that
|
||||
You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You
|
||||
distribute, all copyright, patent, trademark, and attribution notices from the
|
||||
Source form of the Work, excluding those notices that do not pertain to any
|
||||
part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a NOTICE text file as part of its distribution, then
|
||||
any Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of
|
||||
the following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a
|
||||
whole, provided Your use, reproduction, and distribution of the Work otherwise
|
||||
complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms
|
||||
of any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides
|
||||
the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License
|
||||
or out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction,
|
||||
or any and all other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License.
|
||||
However, in accepting such obligations, You may act only on Your own behalf
|
||||
and on Your sole responsibility, not on behalf of any other Contributor, and
|
||||
only if You agree to indemnify, defend, and hold each Contributor harmless for
|
||||
any liability incurred by, or claims asserted against, such Contributor by
|
||||
reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
```
|
||||
|
||||
<!-- END VERBATIM: tls-client-node@0.2.0 LICENSE -->
|
||||
|
||||
### tls-client-node NOTICE
|
||||
|
||||
<!-- BEGIN VERBATIM: tls-client-node@0.2.0 NOTICE -->
|
||||
|
||||
```text
|
||||
Third-Party Notices for tls-client-node
|
||||
|
||||
This project is distributed under Apache License 2.0 with Commons Clause.
|
||||
|
||||
Public license summary:
|
||||
|
||||
- You can use, modify, and redistribute the software under the Apache 2.0 base
|
||||
terms and the Commons Clause condition.
|
||||
- You cannot sell the software itself under the public license where the value
|
||||
derives entirely or substantially from tls-client-node itself.
|
||||
|
||||
This package interoperates with and downloads runtime components from the
|
||||
following upstream projects at install time or first use:
|
||||
|
||||
- bogdanfinn/tls-client
|
||||
- bogdanfinn/tls-client-api
|
||||
|
||||
Those upstream runtime components are distributed separately under their own
|
||||
licenses. If you redistribute those downloaded runtime components, you are
|
||||
responsible for complying with the upstream license terms that apply to them.
|
||||
|
||||
Required acknowledgement for materials mentioning features or use of the
|
||||
upstream runtime components:
|
||||
|
||||
This product includes software developed by Bogdan Finn and contributors.
|
||||
|
||||
Upstream project references:
|
||||
|
||||
- https://github.com/bogdanfinn/tls-client
|
||||
- https://github.com/bogdanfinn/tls-client-api
|
||||
```
|
||||
|
||||
<!-- END VERBATIM: tls-client-node@0.2.0 NOTICE -->
|
||||
|
||||
## bogdanfinn/tls-client v1.15.1
|
||||
|
||||
`tls-client-node` downloads and OmniRoute redistributes a platform-specific native library from
|
||||
[`bogdanfinn/tls-client@v1.15.1`](https://github.com/bogdanfinn/tls-client/tree/v1.15.1).
|
||||
The following license is reproduced verbatim from that tagged primary source. The resolved
|
||||
acknowledgement required by the upstream integration is included in the `tls-client-node` NOTICE
|
||||
above.
|
||||
|
||||
### bogdanfinn/tls-client license
|
||||
|
||||
<!-- BEGIN VERBATIM: bogdanfinn/tls-client@v1.15.1 LICENSE -->
|
||||
|
||||
```text
|
||||
Copyright (c) 2023, Bogdan Finn
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. All advertising materials mentioning features or use of this software
|
||||
must display the following acknowledgement:
|
||||
This product includes software developed by the <organization>.
|
||||
4. Neither the name of the <organization> nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
<!-- END VERBATIM: bogdanfinn/tls-client@v1.15.1 LICENSE -->
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **security(deps):** pin `tls-client-node@0.2.0`, ship its exact Commons Clause/Apache and upstream BSD-4 notices, pin `bogdanfinn/tls-client` to v1.15.1, and verify every native binary against GitHub's official SHA-256 before loading it ([#00000](https://github.com/diegosouzapw/OmniRoute/pull/00000)).
|
||||
@@ -77,8 +77,11 @@
|
||||
},
|
||||
"tls-client-node": {
|
||||
"license": "Custom: LICENSE (Apache-2.0 + Commons Clause)",
|
||||
"justification": "TODO: revisar — tls-client-node uses Apache-2.0 with a 'Commons Clause' addendum that restricts 'Selling' the software (i.e., offering it as a hosted/commercial service whose value derives substantially from tls-client-node). OmniRoute is an open-source proxy; however if deployed as a paid SaaS/hosting service, this restriction could apply. The package is used by grokTlsClient.ts for Grok TLS fingerprinting. RISK: medium — legal review recommended before commercial deployment. Alternatives: consider replacing with a native TLS fingerprinting approach or a truly permissive library.",
|
||||
"justification": "TEMPORARY bridge for a restrictive, source-available dependency: tls-client-node uses Apache-2.0 with the Commons Clause, which removes the right to Sell the software when a paid product or service derives entirely or substantially from its functionality. The shared native TLS transport serves six providers: chatgpt-web, claude-web, perplexity-web, grok-web, notion-web, and lmarena. Shipping the required notices does not grant commercial rights. Legal review is required before commercial deployment; replace with a permissive transport such as wreq-js or obtain separate permission before this exception expires.",
|
||||
"risk": "medium",
|
||||
"temporary": true,
|
||||
"owner": "@diegosouzapw",
|
||||
"reviewBy": "2026-09-30",
|
||||
"reviewAt": "v3.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
30
open-sse/config/tlsClientNativeManifest.json
Normal file
30
open-sse/config/tlsClientNativeManifest.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"version": "1.15.1",
|
||||
"source": "https://github.com/bogdanfinn/tls-client/releases/tag/v1.15.1",
|
||||
"assets": {
|
||||
"darwin-arm64": {
|
||||
"file": "tls-client-darwin-arm64-1.15.1.dylib",
|
||||
"sha256": "b36167372a93337195b84a8b8e7ed2e63ba654b7bbe3e35cd4f96ad3196458e6"
|
||||
},
|
||||
"darwin-x64": {
|
||||
"file": "tls-client-darwin-amd64-1.15.1.dylib",
|
||||
"sha256": "7cb2c6833dc2b7e4b59bf46798f0e214bac746143e36bf9cd5ec92fde6ec8465"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"file": "tls-client-linux-arm64-1.15.1.so",
|
||||
"sha256": "048b75c4fb0898a306228198d545eece39a7d5348200487f0395fbdc4168fe39"
|
||||
},
|
||||
"linux-x64": {
|
||||
"file": "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
||||
"sha256": "e393e866060e238bc36509f853293cebf5af8286aede59814462693efb603b1e"
|
||||
},
|
||||
"win32-ia32": {
|
||||
"file": "tls-client-windows-32-1.15.1.dll",
|
||||
"sha256": "46f44779f41c74918a6d1d0ecadc090aa8bd5303e07ca8dd3a0b999467b76a42"
|
||||
},
|
||||
"win32-x64": {
|
||||
"file": "tls-client-windows-64-1.15.1.dll",
|
||||
"sha256": "414b5e5c60f9200948a46afd023865ad00c7d37403056a7e74ceee27ce2b0287"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,10 @@ import { open, unlink, rmdir, readFile, mkdtemp, stat } from "node:fs/promises";
|
||||
// ---------------------------------------------------------------------------
|
||||
import { resolveProxyForRequest } from "../utils/proxyFetch.ts";
|
||||
import { resolveTlsClientProxyUrl } from "./tlsClientProxy.ts";
|
||||
import { buildNativeTlsClientOptions } from "./tlsClientDownloadDir.ts";
|
||||
import {
|
||||
buildNativeTlsClientOptions,
|
||||
resolveVerifiedTlsClientNativeLibrary,
|
||||
} from "./tlsClientDownloadDir.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -652,8 +655,17 @@ export function createGetClient(config: {
|
||||
`tls-client-node is not installed — cannot start TLS client for ${config.providerName}`
|
||||
);
|
||||
}
|
||||
let nativeLibraryPath: string;
|
||||
try {
|
||||
nativeLibraryPath = await resolveVerifiedTlsClientNativeLibrary();
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
throw new TlsClientUnavailableError(
|
||||
`tls-client native binary verification failed for ${config.providerName}: ${detail}`
|
||||
);
|
||||
}
|
||||
const tlsOptions: Record<string, unknown> = {
|
||||
...buildNativeTlsClientOptions(),
|
||||
...buildNativeTlsClientOptions(nativeLibraryPath),
|
||||
};
|
||||
if (config.tlsProfile) {
|
||||
tlsOptions.clientIdentifier = config.tlsProfile;
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { resolveDataDir } from "@/lib/dataPaths";
|
||||
import tlsClientNativeManifest from "../config/tlsClientNativeManifest.json";
|
||||
|
||||
type TlsClientNativeAsset = {
|
||||
file: string;
|
||||
sha256: string;
|
||||
};
|
||||
|
||||
type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
const TLS_CLIENT_NATIVE_ASSETS = tlsClientNativeManifest.assets as Record<
|
||||
string,
|
||||
TlsClientNativeAsset
|
||||
>;
|
||||
|
||||
async function fileMatchesSha256(filePath: string, expectedSha256: string): Promise<boolean> {
|
||||
try {
|
||||
const bytes = await readFile(filePath);
|
||||
return createHash("sha256").update(bytes).digest("hex") === expectedSha256;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writable cache directory for tls-client-node's native binary.
|
||||
@@ -12,12 +36,96 @@ export function resolveTlsClientDownloadDir(): string {
|
||||
return join(resolveDataDir(), "tls-client", "bin");
|
||||
}
|
||||
|
||||
export function buildNativeTlsClientOptions(): {
|
||||
/**
|
||||
* Materialize only the pinned bogdanfinn/tls-client native library after its
|
||||
* GitHub-published SHA-256 has been verified. Passing the resulting path to
|
||||
* tls-client-node prevents its unchecked runtime downloader from running.
|
||||
*/
|
||||
export async function resolveVerifiedTlsClientNativeLibrary({
|
||||
platform = process.platform,
|
||||
arch = process.arch,
|
||||
asset,
|
||||
downloadDir = resolveTlsClientDownloadDir(),
|
||||
fetchImpl = globalThis.fetch,
|
||||
}: {
|
||||
platform?: NodeJS.Platform;
|
||||
arch?: string;
|
||||
asset?: TlsClientNativeAsset;
|
||||
downloadDir?: string;
|
||||
fetchImpl?: FetchLike;
|
||||
} = {}): Promise<string> {
|
||||
const expectedAsset = asset ?? TLS_CLIENT_NATIVE_ASSETS[`${platform}-${arch}`];
|
||||
if (!expectedAsset) {
|
||||
throw new Error(`Unsupported platform for tls-client native asset: ${platform}/${arch}`);
|
||||
}
|
||||
|
||||
const destinationPath = join(downloadDir, expectedAsset.file);
|
||||
if (await fileMatchesSha256(destinationPath, expectedAsset.sha256)) {
|
||||
return destinationPath;
|
||||
}
|
||||
|
||||
const assetUrl =
|
||||
`https://github.com/bogdanfinn/tls-client/releases/download/v${tlsClientNativeManifest.version}/` +
|
||||
expectedAsset.file;
|
||||
const response = await fetchImpl(assetUrl, {
|
||||
redirect: "follow",
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to download pinned tls-client v${tlsClientNativeManifest.version} native asset: ` +
|
||||
`${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const bytes = Buffer.from(await response.arrayBuffer());
|
||||
const actualSha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
if (actualSha256 !== expectedAsset.sha256) {
|
||||
throw new Error(
|
||||
`SHA-256 mismatch for tls-client v${tlsClientNativeManifest.version} native asset ` +
|
||||
`${expectedAsset.file}: ` +
|
||||
`expected ${expectedAsset.sha256}, received ${actualSha256}`
|
||||
);
|
||||
}
|
||||
|
||||
await mkdir(downloadDir, { recursive: true });
|
||||
const temporaryPath = join(
|
||||
downloadDir,
|
||||
`.${expectedAsset.file}.${process.pid}.${randomUUID()}.tmp`
|
||||
);
|
||||
try {
|
||||
await writeFile(temporaryPath, bytes, { mode: 0o755 });
|
||||
if (!(await fileMatchesSha256(temporaryPath, expectedAsset.sha256))) {
|
||||
throw new Error(`SHA-256 mismatch after writing ${expectedAsset.file}`);
|
||||
}
|
||||
|
||||
await rm(destinationPath, { force: true });
|
||||
try {
|
||||
await rename(temporaryPath, destinationPath);
|
||||
} catch (err) {
|
||||
// A concurrent process may have installed the same verified asset first.
|
||||
if (!(await fileMatchesSha256(destinationPath, expectedAsset.sha256))) throw err;
|
||||
}
|
||||
if (!(await fileMatchesSha256(destinationPath, expectedAsset.sha256))) {
|
||||
await rm(destinationPath, { force: true });
|
||||
throw new Error(`SHA-256 mismatch after installing ${expectedAsset.file}`);
|
||||
}
|
||||
return destinationPath;
|
||||
} finally {
|
||||
await rm(temporaryPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function buildNativeTlsClientOptions(nativeLibraryPath?: string): {
|
||||
runtimeMode: "native";
|
||||
version: string;
|
||||
downloadDir: string;
|
||||
nativeLibraryPath?: string;
|
||||
} {
|
||||
return {
|
||||
runtimeMode: "native",
|
||||
version: tlsClientNativeManifest.version,
|
||||
downloadDir: resolveTlsClientDownloadDir(),
|
||||
...(nativeLibraryPath ? { nativeLibraryPath } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -162,7 +162,7 @@
|
||||
"keytar": "^7.9.0",
|
||||
"onnxruntime-node": "1.24.3",
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tls-client-node": "^0.2.0",
|
||||
"tls-client-node": "0.2.0",
|
||||
"wreq-js": "^3.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
"keytar": "^7.9.0",
|
||||
"onnxruntime-node": "1.24.3",
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tls-client-node": "^0.2.0",
|
||||
"tls-client-node": "0.2.0",
|
||||
"wreq-js": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -112,6 +112,13 @@ export const NATIVE_ASSET_ENTRIES = [
|
||||
|
||||
/** @type {{label:string, src:string[], dest:string[]}[]} */
|
||||
const EXTRA_MODULE_ENTRIES = [
|
||||
{
|
||||
// Legal notices must travel with every standalone bundle. Docker copies the
|
||||
// complete standalone tree into /app, so this one entry covers both outputs.
|
||||
label: "third-party license notices",
|
||||
src: ["THIRD_PARTY_NOTICES.md"],
|
||||
dest: ["THIRD_PARTY_NOTICES.md"],
|
||||
},
|
||||
{
|
||||
// tlsClient.ts intentionally resolves wreq-js through a runtime-dynamic
|
||||
// require so Turbopack cannot rewrite the package name to a hashed external.
|
||||
|
||||
@@ -10,46 +10,73 @@
|
||||
* even when it does run, silently no-ops on a rate-limited/failed GitHub API
|
||||
* call instead of raising — so `node_modules/tls-client-node/bin/` can end
|
||||
* up empty with no visible signal until the first live request throws
|
||||
* TlsClientUnavailableError (chatgpt-web/claude-web/grok-web/lmarena/
|
||||
* perplexity-web all share this transport).
|
||||
* TlsClientUnavailableError (chatgpt-web/claude-web/perplexity-web/grok-web/
|
||||
* notion-web/lmarena all share this transport).
|
||||
*
|
||||
* This module:
|
||||
* 1. Copies an already-fetched root `bin/` into the standalone
|
||||
* 1. Accepts only bogdanfinn/tls-client v1.15.1 assets whose SHA-256 matches
|
||||
* the digest published by GitHub for the tagged release.
|
||||
* 2. Copies the verified root asset into the standalone
|
||||
* `dist/node_modules/tls-client-node/bin/` bundle (same pattern as
|
||||
* fixWreqJsBinary), so the published npm package works even though its
|
||||
* own `files` allowlist never ships the binary.
|
||||
* 2. When the root `bin/` is empty (--ignore-scripts blocked it, or a
|
||||
* transient GitHub rate-limit ate the first attempt), retries the
|
||||
* module's own postinstall.js with exponential backoff instead of
|
||||
* giving up on the first failure.
|
||||
* 3. When that verified asset is absent, invokes the module's postinstall
|
||||
* with TLS_CLIENT_VERSION pinned and retries with exponential backoff.
|
||||
*
|
||||
* Best-effort throughout: a failure here never throws out of postinstall.mjs
|
||||
* — it only warns, matching the other fix*Binary() steps. The runtime layer
|
||||
* (perplexityTlsClient.ts and its 4 siblings) already surfaces a clear
|
||||
* TlsClientUnavailableError pointing at the missing binary, so an operator
|
||||
* who hits a still-empty bin/ after this repair gets an actionable message
|
||||
* rather than an opaque crash.
|
||||
* Normal npm postinstall remains best-effort and warns on failure. Docker and
|
||||
* release callers use --strict, which fails closed instead of shipping an
|
||||
* absent or unverified binary.
|
||||
*/
|
||||
|
||||
import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const DEFAULT_RETRY_DELAYS_MS = [1_000, 3_000, 8_000];
|
||||
const NATIVE_MANIFEST = JSON.parse(
|
||||
readFileSync(
|
||||
new URL("../../open-sse/config/tlsClientNativeManifest.json", import.meta.url),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
function hasAnyFile(dir) {
|
||||
if (!existsSync(dir)) return false;
|
||||
export const TLS_CLIENT_NATIVE_VERSION = NATIVE_MANIFEST.version;
|
||||
export const TLS_CLIENT_NATIVE_ASSETS = NATIVE_MANIFEST.assets;
|
||||
|
||||
/** @typedef {{ file: string; sha256: string }} NativeAsset */
|
||||
|
||||
/**
|
||||
* Resolve the exact native asset supported by tls-client-node@0.2.0.
|
||||
*
|
||||
* @param {NodeJS.Platform} [platform]
|
||||
* @param {string} [arch]
|
||||
* @returns {NativeAsset}
|
||||
*/
|
||||
export function resolveTlsClientNativeAsset(platform = process.platform, arch = process.arch) {
|
||||
const asset = TLS_CLIENT_NATIVE_ASSETS[`${platform}-${arch}`];
|
||||
if (!asset) {
|
||||
throw new Error(`Unsupported platform for tls-client-node native asset: ${platform}/${arch}`);
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
|
||||
function sha256File(filePath) {
|
||||
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
||||
}
|
||||
|
||||
/** @param {string} filePath @param {NativeAsset} asset */
|
||||
function isVerifiedBinary(filePath, asset) {
|
||||
if (!existsSync(filePath)) return false;
|
||||
try {
|
||||
return readdirSync(dir).length > 0;
|
||||
return sha256File(filePath) === asset.sha256;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function copyBinDir(sourceDir, destDir) {
|
||||
mkdirSync(destDir, { recursive: true });
|
||||
for (const file of readdirSync(sourceDir)) {
|
||||
copyFileSync(join(sourceDir, file), join(destDir, file));
|
||||
}
|
||||
function removeIfPresent(filePath) {
|
||||
if (existsSync(filePath)) unlinkSync(filePath);
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
@@ -63,11 +90,17 @@ async function sleep(ms) {
|
||||
* only warns, so "still empty after running it" is the only failure signal
|
||||
* available).
|
||||
*/
|
||||
async function downloadWithRetry(rootTlsClientDir, retryDelaysMs, log) {
|
||||
async function downloadWithRetry(rootTlsClientDir, asset, version, retryDelaysMs, log) {
|
||||
const postinstallScript = join(rootTlsClientDir, "scripts", "postinstall.js");
|
||||
const binDir = join(rootTlsClientDir, "bin");
|
||||
const binaryPath = join(binDir, asset.file);
|
||||
if (!existsSync(postinstallScript)) return false;
|
||||
|
||||
if (existsSync(binaryPath) && !isVerifiedBinary(binaryPath, asset)) {
|
||||
removeIfPresent(binaryPath);
|
||||
log(` ⚠️ Removed tls-client-node binary with an invalid SHA-256: ${asset.file}`);
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt <= retryDelaysMs.length; attempt++) {
|
||||
if (attempt > 0) {
|
||||
log(
|
||||
@@ -81,6 +114,11 @@ async function downloadWithRetry(rootTlsClientDir, retryDelaysMs, log) {
|
||||
const { execFileSync } = await import("node:child_process");
|
||||
execFileSync(process.execPath, [postinstallScript], {
|
||||
cwd: rootTlsClientDir,
|
||||
env: {
|
||||
...process.env,
|
||||
TLS_CLIENT_SKIP_DOWNLOAD: "0",
|
||||
TLS_CLIENT_VERSION: version,
|
||||
},
|
||||
stdio: "pipe",
|
||||
timeout: 30_000,
|
||||
});
|
||||
@@ -88,7 +126,11 @@ async function downloadWithRetry(rootTlsClientDir, retryDelaysMs, log) {
|
||||
log(` ⚠️ tls-client-node postinstall attempt failed: ${err.message.split("\n")[0]}`);
|
||||
}
|
||||
|
||||
if (hasAnyFile(binDir)) return true;
|
||||
if (isVerifiedBinary(binaryPath, asset)) return true;
|
||||
if (existsSync(binaryPath)) {
|
||||
removeIfPresent(binaryPath);
|
||||
log(` ⚠️ Rejected tls-client-node binary with an invalid SHA-256: ${asset.file}`);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -99,50 +141,99 @@ async function downloadWithRetry(rootTlsClientDir, retryDelaysMs, log) {
|
||||
* @param {string} opts.rootDir - repo root
|
||||
* @param {(msg: string) => void} [opts.log]
|
||||
* @param {number[]} [opts.retryDelaysMs] - override for tests (avoid real sleeps)
|
||||
* @param {NativeAsset} [opts.asset] - injected only for deterministic tests
|
||||
* @param {boolean} [opts.strict] - fail instead of warning (Docker/release builds)
|
||||
*/
|
||||
export async function fixTlsClientNodeBinary({
|
||||
rootDir,
|
||||
log = (m) => console.log(m),
|
||||
retryDelaysMs = DEFAULT_RETRY_DELAYS_MS,
|
||||
asset,
|
||||
strict = false,
|
||||
} = {}) {
|
||||
const version = TLS_CLIENT_NATIVE_VERSION;
|
||||
const rootTlsClientDir = join(rootDir, "node_modules", "tls-client-node");
|
||||
const rootBinDir = join(rootTlsClientDir, "bin");
|
||||
const distTlsClientDir = join(rootDir, "dist", "node_modules", "tls-client-node");
|
||||
|
||||
if (!existsSync(rootTlsClientDir)) return;
|
||||
if (!existsSync(rootTlsClientDir)) {
|
||||
if (strict) throw new Error("tls-client-node is not installed; cannot verify native binary");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasAnyFile(rootBinDir)) {
|
||||
let expectedAsset = asset;
|
||||
try {
|
||||
expectedAsset ??= resolveTlsClientNativeAsset();
|
||||
} catch (err) {
|
||||
if (strict) throw err;
|
||||
console.warn(` ⚠️ ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootBinaryPath = join(rootBinDir, expectedAsset.file);
|
||||
|
||||
if (!isVerifiedBinary(rootBinaryPath, expectedAsset)) {
|
||||
log(
|
||||
"\n 🔧 tls-client-node native binary missing (blocked by --ignore-scripts or a " +
|
||||
"failed fetch) — attempting repair...\n"
|
||||
`\n 🔧 tls-client-node native binary missing or unverified — fetching pinned ` +
|
||||
`v${version} and checking SHA-256...\n`
|
||||
);
|
||||
const recovered = await downloadWithRetry(
|
||||
rootTlsClientDir,
|
||||
expectedAsset,
|
||||
version,
|
||||
retryDelaysMs,
|
||||
log
|
||||
);
|
||||
const recovered = await downloadWithRetry(rootTlsClientDir, retryDelaysMs, log);
|
||||
if (!recovered) {
|
||||
const message =
|
||||
`Could not fetch tls-client-node v${version} verified native binary ` +
|
||||
`(${expectedAsset.file}) after retries.`;
|
||||
if (strict) throw new Error(message);
|
||||
console.warn(`\n ⚠️ ${message} GitHub may be rate-limited or unreachable.`);
|
||||
console.warn(
|
||||
"\n ⚠️ Could not fetch tls-client-node's native binary " +
|
||||
"(GitHub API rate-limited or unreachable after retries)."
|
||||
" chatgpt-web/claude-web/perplexity-web/grok-web/notion-web/lmarena will " +
|
||||
"raise a clear TlsClientUnavailableError on first use until this is resolved."
|
||||
);
|
||||
console.warn(
|
||||
" chatgpt-web/claude-web/grok-web/lmarena/perplexity-web will raise a clear " +
|
||||
"TlsClientUnavailableError on first use until this is resolved."
|
||||
);
|
||||
console.warn(
|
||||
` Manual fix: node ${join(rootTlsClientDir, "scripts", "postinstall.js")}\n`
|
||||
` Verified repair: node ${join(rootDir, "scripts", "build", "fixTlsClientNodeBinary.mjs")} --strict\n`
|
||||
);
|
||||
return;
|
||||
}
|
||||
log(" ✅ tls-client-node native binary fetched successfully!\n");
|
||||
}
|
||||
|
||||
if (!existsSync(distTlsClientDir) || !hasAnyFile(rootBinDir)) return;
|
||||
if (!existsSync(distTlsClientDir) || !isVerifiedBinary(rootBinaryPath, expectedAsset)) return;
|
||||
|
||||
const distBinDir = join(distTlsClientDir, "bin");
|
||||
if (hasAnyFile(distBinDir)) return;
|
||||
const distBinaryPath = join(distBinDir, expectedAsset.file);
|
||||
if (isVerifiedBinary(distBinaryPath, expectedAsset)) return;
|
||||
|
||||
try {
|
||||
copyBinDir(rootBinDir, distBinDir);
|
||||
log(" ✅ tls-client-node native binary copied to standalone dist/node_modules.\n");
|
||||
removeIfPresent(distBinaryPath);
|
||||
mkdirSync(distBinDir, { recursive: true });
|
||||
copyFileSync(rootBinaryPath, distBinaryPath);
|
||||
if (!isVerifiedBinary(distBinaryPath, expectedAsset)) {
|
||||
removeIfPresent(distBinaryPath);
|
||||
throw new Error(`SHA-256 mismatch after copying ${expectedAsset.file}`);
|
||||
}
|
||||
log(
|
||||
` ✅ Verified tls-client-node v${version} native binary copied to standalone ` +
|
||||
"dist/node_modules.\n"
|
||||
);
|
||||
} catch (err) {
|
||||
if (strict) throw err;
|
||||
console.warn(` ⚠️ Could not copy tls-client-node binary into dist/: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
try {
|
||||
await fixTlsClientNodeBinary({
|
||||
rootDir: process.cwd(),
|
||||
strict: process.argv.includes("--strict"),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(` ❌ ${err.message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [
|
||||
"scripts/build/postinstallSupport.mjs",
|
||||
"scripts/build/colocateOptionals.mjs",
|
||||
// #7802: imported by scripts/build/postinstall.mjs to repair tls-client-node's
|
||||
// native binary (chatgpt-web/claude-web/grok-web/lmarena/perplexity-web transport).
|
||||
// native binary (chatgpt-web/claude-web/perplexity-web/grok-web/notion-web/lmarena).
|
||||
"scripts/build/fixTlsClientNodeBinary.mjs",
|
||||
// #8859: imported by scripts/build/postinstall.mjs to repair playwright-core's
|
||||
// browser resolution on Termux/Android (no glibc, no bundled browsers).
|
||||
@@ -197,6 +197,7 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
|
||||
// #7065: regression guard for the HEAD response guard (dist/server-ws.mjs import).
|
||||
"dist/head-response-guard.cjs",
|
||||
"dist/webdav-handler.mjs",
|
||||
"open-sse/config/tlsClientNativeManifest.json",
|
||||
"bin/cli/program.mjs",
|
||||
// Direct imports of bin/omniroute.mjs — bin/cli/ is only an allowlist PREFIX, so a
|
||||
// file vanishing from the tarball never fails the unexpected-paths check; only these
|
||||
@@ -220,6 +221,7 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
|
||||
// or the CLI fails to boot — list them REQUIRED so a regression is loud.
|
||||
"bin/aliasResolver.mjs",
|
||||
"bin/aliasResolverHook.mjs",
|
||||
"THIRD_PARTY_NOTICES.md",
|
||||
"package.json",
|
||||
"scripts/build/native-binary-compat.mjs",
|
||||
"scripts/build/postinstall.mjs",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* Modules repaired:
|
||||
* - better-sqlite3 (SQLite bindings)
|
||||
* - wreq-js (TLS client for OAuth providers)
|
||||
* - tls-client-node (TLS client for chatgpt-web/claude-web/grok-web/lmarena/perplexity-web)
|
||||
* - tls-client-node (TLS for chatgpt-web/claude-web/perplexity-web/grok-web/notion-web/lmarena)
|
||||
* - sql.js (WASM SQLite fallback runtime)
|
||||
* - node-machine-id (local CLI machine-token server runtime)
|
||||
*
|
||||
|
||||
@@ -285,11 +285,27 @@ test("loadAllowlist: exceptions entries have required fields", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("loadAllowlist: tls-client-node exception has risk=medium (Commons Clause)", () => {
|
||||
test("loadAllowlist: tls-client-node exception is temporary, owned, and covers all consumers", () => {
|
||||
const allowlist = loadAllowlist();
|
||||
const exc = allowlist.exceptions["tls-client-node"] as any;
|
||||
assert.ok(exc, "tls-client-node exception must be registered");
|
||||
assert.equal(exc.risk, "medium", "tls-client-node is a medium-risk exception (Commons Clause)");
|
||||
assert.equal(exc.temporary, true, "Commons Clause exception must not become permanent policy");
|
||||
assert.equal(exc.owner, "@diegosouzapw");
|
||||
assert.equal(exc.reviewBy, "2026-09-30");
|
||||
assert.equal(exc.reviewAt, "v3.9.0");
|
||||
assert.match(exc.justification, /source-available/i);
|
||||
assert.match(exc.justification, /commercial deployment/i);
|
||||
for (const provider of [
|
||||
"chatgpt-web",
|
||||
"claude-web",
|
||||
"perplexity-web",
|
||||
"grok-web",
|
||||
"notion-web",
|
||||
"lmarena",
|
||||
]) {
|
||||
assert.match(exc.justification, new RegExp(provider), `missing consumer ${provider}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("loadAllowlist: LGPL packages have registered exceptions", () => {
|
||||
|
||||
85
tests/unit/build/tls-client-license-provenance.test.ts
Normal file
85
tests/unit/build/tls-client-license-provenance.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { syncStandaloneExtraModules } from "../../../scripts/build/assembleStandalone.mjs";
|
||||
import { PACK_ARTIFACT_REQUIRED_PATHS } from "../../../scripts/build/pack-artifact-policy.ts";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
|
||||
const PRIMARY_SOURCE_HASHES = {
|
||||
"tls-client-node@0.2.0 LICENSE":
|
||||
"086c687026ff693ad76589dda1af12304a3ff33fc5f15030035ed62ef6a6d6eb",
|
||||
"tls-client-node@0.2.0 NOTICE":
|
||||
"80e5a526273788f2ace0164ec131daac697c54084add90855afdd03f5fadd3d3",
|
||||
"bogdanfinn/tls-client@v1.15.1 LICENSE":
|
||||
"7dab9a4dd66987fbe576d53c1ee047c193725df6f4fac67de315a127417fd151",
|
||||
} as const;
|
||||
|
||||
function extractVerbatimBlock(document: string, label: keyof typeof PRIMARY_SOURCE_HASHES): string {
|
||||
const beginMarker = `<!-- BEGIN VERBATIM: ${label} -->`;
|
||||
const endMarker = `<!-- END VERBATIM: ${label} -->`;
|
||||
const markerStart = document.indexOf(beginMarker);
|
||||
assert.notEqual(markerStart, -1, `missing begin marker for ${label}`);
|
||||
const fenceStart = document.indexOf("```text\n", markerStart + beginMarker.length);
|
||||
assert.notEqual(fenceStart, -1, `missing text fence for ${label}`);
|
||||
const contentStart = fenceStart + "```text\n".length;
|
||||
const finish = document.indexOf("\n```", contentStart);
|
||||
assert.notEqual(finish, -1, `missing closing fence for ${label}`);
|
||||
assert.notEqual(document.indexOf(endMarker, finish), -1, `missing end marker for ${label}`);
|
||||
return document.slice(contentStart, finish);
|
||||
}
|
||||
|
||||
test("distributed tls-client notices reproduce every primary license and NOTICE verbatim", () => {
|
||||
const notices = readFileSync(join(ROOT, "THIRD_PARTY_NOTICES.md"), "utf8");
|
||||
|
||||
for (const [label, expectedHash] of Object.entries(PRIMARY_SOURCE_HASHES)) {
|
||||
const text = extractVerbatimBlock(notices, label as keyof typeof PRIMARY_SOURCE_HASHES);
|
||||
assert.equal(
|
||||
createHash("sha256").update(text).digest("hex"),
|
||||
expectedHash,
|
||||
`${label} must remain byte-for-byte identical to its tagged primary source`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("the distributed wrapper is pinned to the exact audited tls-client-node release", () => {
|
||||
const packageJson = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
||||
const packageLock = JSON.parse(readFileSync(join(ROOT, "package-lock.json"), "utf8"));
|
||||
|
||||
assert.equal(packageJson.optionalDependencies["tls-client-node"], "0.2.0");
|
||||
assert.equal(packageLock.packages[""].optionalDependencies["tls-client-node"], "0.2.0");
|
||||
assert.equal(packageLock.packages["node_modules/tls-client-node"].version, "0.2.0");
|
||||
});
|
||||
|
||||
test("npm pack, standalone, and Docker all transport THIRD_PARTY_NOTICES.md", async () => {
|
||||
const packageJson = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
||||
assert.ok(packageJson.files.includes("THIRD_PARTY_NOTICES.md"));
|
||||
assert.ok(
|
||||
PACK_ARTIFACT_REQUIRED_PATHS.includes("THIRD_PARTY_NOTICES.md"),
|
||||
"check:pack-artifact must fail when the distributed notices are absent"
|
||||
);
|
||||
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), "tls-client-notices-project-"));
|
||||
const outDir = mkdtempSync(join(tmpdir(), "tls-client-notices-standalone-"));
|
||||
try {
|
||||
const expected = "legal-notice-sentinel\n";
|
||||
writeFileSync(join(projectRoot, "THIRD_PARTY_NOTICES.md"), expected);
|
||||
await syncStandaloneExtraModules(projectRoot, undefined, { log() {} }, outDir);
|
||||
assert.equal(readFileSync(join(outDir, "THIRD_PARTY_NOTICES.md"), "utf8"), expected);
|
||||
} finally {
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const dockerfile = readFileSync(join(ROOT, "Dockerfile"), "utf8");
|
||||
assert.match(
|
||||
dockerfile,
|
||||
/COPY --from=builder \/app\/\.build\/next\/standalone \.\//,
|
||||
"Docker runner must consume the standalone tree that carries THIRD_PARTY_NOTICES.md"
|
||||
);
|
||||
});
|
||||
@@ -1,11 +1,57 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { fixTlsClientNodeBinary } from "../../scripts/build/fixTlsClientNodeBinary.mjs";
|
||||
|
||||
const ROOT = join(import.meta.dirname, "..", "..");
|
||||
|
||||
test("native manifest pins v1.15.1 to GitHub's official digests on every supported platform", () => {
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(join(ROOT, "open-sse", "config", "tlsClientNativeManifest.json"), "utf8")
|
||||
);
|
||||
|
||||
assert.equal(manifest.version, "1.15.1");
|
||||
assert.equal(manifest.source, "https://github.com/bogdanfinn/tls-client/releases/tag/v1.15.1");
|
||||
assert.deepEqual(manifest.assets, {
|
||||
"darwin-arm64": {
|
||||
file: "tls-client-darwin-arm64-1.15.1.dylib",
|
||||
sha256: "b36167372a93337195b84a8b8e7ed2e63ba654b7bbe3e35cd4f96ad3196458e6",
|
||||
},
|
||||
"darwin-x64": {
|
||||
file: "tls-client-darwin-amd64-1.15.1.dylib",
|
||||
sha256: "7cb2c6833dc2b7e4b59bf46798f0e214bac746143e36bf9cd5ec92fde6ec8465",
|
||||
},
|
||||
"linux-arm64": {
|
||||
file: "tls-client-linux-arm64-1.15.1.so",
|
||||
sha256: "048b75c4fb0898a306228198d545eece39a7d5348200487f0395fbdc4168fe39",
|
||||
},
|
||||
"linux-x64": {
|
||||
file: "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
||||
sha256: "e393e866060e238bc36509f853293cebf5af8286aede59814462693efb603b1e",
|
||||
},
|
||||
"win32-ia32": {
|
||||
file: "tls-client-windows-32-1.15.1.dll",
|
||||
sha256: "46f44779f41c74918a6d1d0ecadc090aa8bd5303e07ca8dd3a0b999467b76a42",
|
||||
},
|
||||
"win32-x64": {
|
||||
file: "tls-client-windows-64-1.15.1.dll",
|
||||
sha256: "414b5e5c60f9200948a46afd023865ad00c7d37403056a7e74ceee27ce2b0287",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
function makeRoot() {
|
||||
return mkdtempSync(join(tmpdir(), "fix-tls-client-node-binary-7802-"));
|
||||
}
|
||||
@@ -15,6 +61,51 @@ function collectLogs() {
|
||||
return { logs, log: (m: string) => logs.push(m) };
|
||||
}
|
||||
|
||||
test("replaces a tampered binary with the pinned version and copies only verified bytes", async () => {
|
||||
const rootDir = makeRoot();
|
||||
try {
|
||||
const goodBytes = "verified-native-binary";
|
||||
const asset = {
|
||||
file: "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
||||
sha256: createHash("sha256").update(goodBytes).digest("hex"),
|
||||
};
|
||||
const tlsClientDir = join(rootDir, "node_modules", "tls-client-node");
|
||||
const rootBin = join(tlsClientDir, "bin");
|
||||
const scriptsDir = join(tlsClientDir, "scripts");
|
||||
mkdirSync(rootBin, { recursive: true });
|
||||
mkdirSync(scriptsDir, { recursive: true });
|
||||
writeFileSync(join(rootBin, asset.file), "tampered");
|
||||
writeFileSync(
|
||||
join(scriptsDir, "postinstall.js"),
|
||||
`const fs = require("fs");
|
||||
const path = require("path");
|
||||
if (process.env.TLS_CLIENT_VERSION !== "1.15.1") process.exit(9);
|
||||
fs.writeFileSync(path.join(__dirname, "..", ".observed-version"), process.env.TLS_CLIENT_VERSION);
|
||||
fs.writeFileSync(path.join(__dirname, "..", "bin", ${JSON.stringify(asset.file)}), ${JSON.stringify(goodBytes)});`
|
||||
);
|
||||
mkdirSync(join(rootDir, "dist", "node_modules", "tls-client-node"), { recursive: true });
|
||||
|
||||
await fixTlsClientNodeBinary({
|
||||
rootDir,
|
||||
asset,
|
||||
strict: true,
|
||||
retryDelaysMs: [],
|
||||
log() {},
|
||||
});
|
||||
|
||||
assert.equal(readFileSync(join(tlsClientDir, ".observed-version"), "utf8"), "1.15.1");
|
||||
assert.equal(
|
||||
readFileSync(
|
||||
join(rootDir, "dist", "node_modules", "tls-client-node", "bin", asset.file),
|
||||
"utf8"
|
||||
),
|
||||
goodBytes
|
||||
);
|
||||
} finally {
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("no-ops when node_modules/tls-client-node is absent (module not installed)", async () => {
|
||||
const rootDir = makeRoot();
|
||||
try {
|
||||
@@ -29,19 +120,24 @@ test("no-ops when node_modules/tls-client-node is absent (module not installed)"
|
||||
test("copies an already-populated root bin/ into the standalone dist bundle (#7802 item 2)", async () => {
|
||||
const rootDir = makeRoot();
|
||||
try {
|
||||
const binary = "fake-binary";
|
||||
const asset = {
|
||||
file: "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
||||
sha256: createHash("sha256").update(binary).digest("hex"),
|
||||
};
|
||||
const rootBin = join(rootDir, "node_modules", "tls-client-node", "bin");
|
||||
mkdirSync(rootBin, { recursive: true });
|
||||
writeFileSync(join(rootBin, "tls-client-linux-ubuntu-amd64-1.0.0.so"), "fake-binary");
|
||||
writeFileSync(join(rootBin, asset.file), binary);
|
||||
|
||||
const distTlsClientDir = join(rootDir, "dist", "node_modules", "tls-client-node");
|
||||
mkdirSync(distTlsClientDir, { recursive: true });
|
||||
|
||||
const { log } = collectLogs();
|
||||
await fixTlsClientNodeBinary({ rootDir, log });
|
||||
await fixTlsClientNodeBinary({ rootDir, asset, log });
|
||||
|
||||
const distBin = join(distTlsClientDir, "bin");
|
||||
assert.ok(existsSync(distBin), "dist bin/ should have been created");
|
||||
assert.deepEqual(readdirSync(distBin), ["tls-client-linux-ubuntu-amd64-1.0.0.so"]);
|
||||
assert.deepEqual(readdirSync(distBin), [asset.file]);
|
||||
} finally {
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -50,6 +146,11 @@ test("copies an already-populated root bin/ into the standalone dist bundle (#78
|
||||
test("retries the download when root bin/ is empty, and stops once a file appears (#7802 item 3)", async () => {
|
||||
const rootDir = makeRoot();
|
||||
try {
|
||||
const binary = "ok";
|
||||
const asset = {
|
||||
file: "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
||||
sha256: createHash("sha256").update(binary).digest("hex"),
|
||||
};
|
||||
const tlsClientDir = join(rootDir, "node_modules", "tls-client-node");
|
||||
const rootBin = join(tlsClientDir, "bin");
|
||||
mkdirSync(rootBin, { recursive: true });
|
||||
@@ -66,14 +167,14 @@ test("retries the download when root bin/ is empty, and stops once a file appear
|
||||
const attempts = fs.existsSync(marker) ? Number(fs.readFileSync(marker, "utf8")) : 0;
|
||||
fs.writeFileSync(marker, String(attempts + 1));
|
||||
if (attempts + 1 >= 2) {
|
||||
fs.writeFileSync(path.join(__dirname, "..", "bin", "tls-client-linux-ubuntu-amd64-1.0.0.so"), "ok");
|
||||
fs.writeFileSync(path.join(__dirname, "..", "bin", ${JSON.stringify(asset.file)}), ${JSON.stringify(binary)});
|
||||
}`
|
||||
);
|
||||
|
||||
const { logs, log } = collectLogs();
|
||||
await fixTlsClientNodeBinary({ rootDir, log, retryDelaysMs: [1, 1, 1] });
|
||||
await fixTlsClientNodeBinary({ rootDir, asset, log, retryDelaysMs: [1, 1, 1] });
|
||||
|
||||
assert.ok(existsSync(join(rootBin, "tls-client-linux-ubuntu-amd64-1.0.0.so")));
|
||||
assert.ok(existsSync(join(rootBin, asset.file)));
|
||||
assert.ok(
|
||||
logs.some((m) => m.includes("fetched successfully")),
|
||||
"expected a success log once the retry recovered"
|
||||
@@ -98,9 +199,7 @@ test("warns without throwing when every retry leaves bin/ empty (still rate-limi
|
||||
console.warn = (m: string) => warnings.push(m);
|
||||
try {
|
||||
const { log } = collectLogs();
|
||||
await assert.doesNotReject(
|
||||
fixTlsClientNodeBinary({ rootDir, log, retryDelaysMs: [1, 1] })
|
||||
);
|
||||
await assert.doesNotReject(fixTlsClientNodeBinary({ rootDir, log, retryDelaysMs: [1, 1] }));
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
}
|
||||
@@ -113,3 +212,35 @@ test("warns without throwing when every retry leaves bin/ empty (still rate-limi
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("strict mode rejects an unverified download instead of shipping it", async () => {
|
||||
const rootDir = makeRoot();
|
||||
try {
|
||||
const asset = {
|
||||
file: "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
||||
sha256: createHash("sha256").update("expected").digest("hex"),
|
||||
};
|
||||
const tlsClientDir = join(rootDir, "node_modules", "tls-client-node");
|
||||
const scriptsDir = join(tlsClientDir, "scripts");
|
||||
mkdirSync(join(tlsClientDir, "bin"), { recursive: true });
|
||||
mkdirSync(scriptsDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(scriptsDir, "postinstall.js"),
|
||||
`require("fs").writeFileSync(require("path").join(__dirname, "..", "bin", ${JSON.stringify(asset.file)}), "tampered");`
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
fixTlsClientNodeBinary({
|
||||
rootDir,
|
||||
asset,
|
||||
strict: true,
|
||||
retryDelaysMs: [],
|
||||
log() {},
|
||||
}),
|
||||
/Could not fetch tls-client-node v1\.15\.1 verified native binary/
|
||||
);
|
||||
assert.equal(existsSync(join(tlsClientDir, "bin", asset.file)), false);
|
||||
} finally {
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -241,6 +241,7 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball",
|
||||
// alphabetically (bin/ < dist/ < scripts/ < src/), minus the paths present
|
||||
// above (dist/server.js, bin/omniroute.mjs, package.json, the postinstall scripts).
|
||||
assert.deepEqual(missingPaths, [
|
||||
"THIRD_PARTY_NOTICES.md",
|
||||
"bin/aliasResolver.mjs",
|
||||
"bin/aliasResolverHook.mjs",
|
||||
"bin/cli/data-dir.mjs",
|
||||
@@ -265,6 +266,7 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball",
|
||||
"dist/systemd-notify.mjs",
|
||||
"dist/tls-options.mjs",
|
||||
"dist/webdav-handler.mjs",
|
||||
"open-sse/config/tlsClientNativeManifest.json",
|
||||
"scripts/build/colocateOptionals.mjs",
|
||||
"scripts/build/fixTlsClientNodeBinary.mjs",
|
||||
"scripts/build/native-binary-compat.mjs",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { test, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
@@ -39,7 +39,7 @@ test("resolveTlsClientDownloadDir caches native binary under DATA_DIR/tls-client
|
||||
assert.equal(resolveTlsClientDownloadDir(), join(dataDir, "tls-client", "bin"));
|
||||
});
|
||||
|
||||
test("buildNativeTlsClientOptions passes downloadDir to tls-client-node (#8579)", async () => {
|
||||
test("buildNativeTlsClientOptions pins v1.15.1 and passes downloadDir to tls-client-node", async () => {
|
||||
const dataDir = mkdtempSync(join(tmpdir(), "omniroute-tls-client-opts-8579-"));
|
||||
process.env.DATA_DIR = dataDir;
|
||||
|
||||
@@ -49,15 +49,76 @@ test("buildNativeTlsClientOptions passes downloadDir to tls-client-node (#8579)"
|
||||
const options = buildNativeTlsClientOptions();
|
||||
|
||||
assert.equal(options.runtimeMode, "native");
|
||||
assert.equal(options.version, "1.15.1");
|
||||
assert.equal(options.downloadDir, join(dataDir, "tls-client", "bin"));
|
||||
});
|
||||
|
||||
test("runtime downloader verifies v1.15.1 before exposing nativeLibraryPath", async () => {
|
||||
const downloadDir = mkdtempSync(join(tmpdir(), "omniroute-tls-client-verified-"));
|
||||
try {
|
||||
const bytes = Buffer.from("verified-runtime-binary");
|
||||
const asset = {
|
||||
file: "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
};
|
||||
const requestedUrls: string[] = [];
|
||||
const { resolveVerifiedTlsClientNativeLibrary } =
|
||||
await import("../../open-sse/services/tlsClientDownloadDir.ts");
|
||||
|
||||
const libraryPath = await resolveVerifiedTlsClientNativeLibrary({
|
||||
asset,
|
||||
downloadDir,
|
||||
fetchImpl: async (url: string | URL) => {
|
||||
requestedUrls.push(String(url));
|
||||
return new Response(bytes, { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(requestedUrls, [
|
||||
`https://github.com/bogdanfinn/tls-client/releases/download/v1.15.1/${asset.file}`,
|
||||
]);
|
||||
assert.equal(libraryPath, join(downloadDir, asset.file));
|
||||
assert.deepEqual(readFileSync(libraryPath), bytes);
|
||||
} finally {
|
||||
rmSync(downloadDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("runtime downloader rejects bytes that do not match the official digest", async () => {
|
||||
const downloadDir = mkdtempSync(join(tmpdir(), "omniroute-tls-client-rejected-"));
|
||||
try {
|
||||
const asset = {
|
||||
file: "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
||||
sha256: createHash("sha256").update("expected").digest("hex"),
|
||||
};
|
||||
const { resolveVerifiedTlsClientNativeLibrary } =
|
||||
await import("../../open-sse/services/tlsClientDownloadDir.ts");
|
||||
|
||||
await assert.rejects(
|
||||
resolveVerifiedTlsClientNativeLibrary({
|
||||
asset,
|
||||
downloadDir,
|
||||
fetchImpl: async () => new Response("tampered", { status: 200 }),
|
||||
}),
|
||||
/SHA-256 mismatch for tls-client v1\.15\.1/
|
||||
);
|
||||
assert.equal(existsSync(join(downloadDir, asset.file)), false);
|
||||
} finally {
|
||||
rmSync(downloadDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("all web-provider tls clients wire downloadDir through buildNativeTlsClientOptions (#8579)", () => {
|
||||
const base = readFileSync(join(ROOT, "open-sse/services/tlsClientBase.ts"), "utf8");
|
||||
assert.match(
|
||||
base,
|
||||
/buildNativeTlsClientOptions\(\)/,
|
||||
"tlsClientBase.ts must pass buildNativeTlsClientOptions() to TLSClient"
|
||||
/resolveVerifiedTlsClientNativeLibrary\(\)/,
|
||||
"tlsClientBase.ts must verify the pinned native library before TLSClient loads it"
|
||||
);
|
||||
assert.match(
|
||||
base,
|
||||
/buildNativeTlsClientOptions\(nativeLibraryPath\)/,
|
||||
"tlsClientBase.ts must pass the verified nativeLibraryPath to TLSClient"
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
base,
|
||||
|
||||
@@ -32,6 +32,27 @@ test("Dockerfile's --ignore-scripts npm ci is compensated for tls-client-node's
|
||||
"expected postinstall.mjs to repair wreq-js's native binary"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
dockerfile,
|
||||
/COPY scripts\/build\/fixTlsClientNodeBinary\.mjs \.\/scripts\/build\/fixTlsClientNodeBinary\.mjs/,
|
||||
"Docker builder must copy the checksum-verifying repair helper"
|
||||
);
|
||||
assert.match(
|
||||
dockerfile,
|
||||
/COPY open-sse\/config\/tlsClientNativeManifest\.json \.\/open-sse\/config\/tlsClientNativeManifest\.json/,
|
||||
"Docker builder must copy the pinned version and official SHA-256 manifest"
|
||||
);
|
||||
assert.match(
|
||||
dockerfile,
|
||||
/node scripts\/build\/fixTlsClientNodeBinary\.mjs --strict/,
|
||||
"Docker build must fail closed when the pinned native binary is absent or unverified"
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
dockerfile,
|
||||
/node node_modules\/tls-client-node\/scripts\/postinstall\.js/,
|
||||
"Docker must not bypass checksum verification by invoking the upstream downloader directly"
|
||||
);
|
||||
|
||||
const dockerfileHandlesIt = /tls-client-node[\s\S]{0,200}(postinstall|rebuild|download)/i.test(
|
||||
dockerfile
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user