Skip to content

Commit c7c5144

Browse files
committed
feat(licensing): bake licensing endpoint into bundle at build time
Mirrors evolution-go/tools/build-dist/obfuscate.go: the URL of the licensing server is now XOR-encoded into the JS bundle by tsup `define`, so it never appears as a plain literal in dist/main.js. The Dockerfile accepts the pair as build-args (NOT runtime env vars) so an operator cannot point the running service at a different licensing server. - src/licensing/endpoint.ts: read from compile-time `__LICENSE_ENDPOINT_*__` identifiers replaced by tsup; keep parts-array fallback for dev builds. - tsup.config.ts: `define` reads LICENSE_ENDPOINT_ENCODED / _XOR_KEY from build env at the moment npm run build is invoked. - tools/encode-url.js: helper to generate the hex pair for a given URL. Usage: eval "$(node tools/encode-url.js <url>)". - Dockerfile: ARG + ENV plumbing for the build stage only. - CHANGELOG: notes about the build-time obfuscation.
1 parent 06f65e9 commit c7c5144

5 files changed

Lines changed: 85 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ The following routes always remain public so the operator can recover:
9191
- If the licensing server is unreachable but the instance has been activated
9292
before, the service continues to serve traffic normally — local DB is the
9393
source of truth for activation state after the first successful call.
94+
- Release builds bake the licensing endpoint into the bundle as an XOR-encoded
95+
string via tsup `define`, so the URL never appears as a plain literal in
96+
`dist/main.js`. Generate the pair with `node tools/encode-url.js <url>` and
97+
pass `LICENSE_ENDPOINT_ENCODED` / `LICENSE_ENDPOINT_XOR_KEY` as Docker
98+
build-args (NOT runtime env vars). Local dev builds use a parts-array
99+
fallback that still avoids a single string literal but is not obfuscated.
94100

95101
### Troubleshooting
96102

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/*
3131

3232
RUN ./Docker/scripts/generate_database.sh
3333

34+
# Licensing endpoint is XOR-encoded into the bundle by tsup `define`. Pass the
35+
# pair via build-args (NEVER as runtime env vars) to keep the URL out of the
36+
# compiled JavaScript as a plain literal. Generate them with
37+
# `node tools/encode-url.js <url>`. Leaving them empty is OK for non-release
38+
# builds — the dev fallback in src/licensing/endpoint.ts kicks in.
39+
ARG LICENSE_ENDPOINT_ENCODED
40+
ARG LICENSE_ENDPOINT_XOR_KEY
41+
ENV LICENSE_ENDPOINT_ENCODED=${LICENSE_ENDPOINT_ENCODED}
42+
ENV LICENSE_ENDPOINT_XOR_KEY=${LICENSE_ENDPOINT_XOR_KEY}
43+
3444
RUN NODE_OPTIONS="--max-old-space-size=2048" npm run build
3545

3646
FROM node:24-alpine AS final

src/licensing/endpoint.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
// Mirrors evolution-go/pkg/core/endpoint.go
2-
// In release builds, set LICENSE_ENDPOINT_ENCODED + LICENSE_ENDPOINT_XOR_KEY (hex).
3-
// In dev, the URL is reconstructed from a parts array — same technique as the Go version.
2+
//
3+
// The licensing URL is **build-time only** — it gets baked into the bundle by
4+
// tsup's `define` so the operator cannot point the running service at a
5+
// different licensing server via env vars.
6+
//
7+
// In release builds the Dockerfile passes:
8+
// LICENSE_ENDPOINT_ENCODED=<hex> (XOR-encoded URL)
9+
// LICENSE_ENDPOINT_XOR_KEY=<hex> (XOR key)
10+
//
11+
// Use `node tools/encode-url.js https://license.evolutionfoundation.com.br`
12+
// to generate the pair.
13+
//
14+
// In dev (vars empty), the URL is reconstructed from a parts array — same
15+
// technique as evolution-go.
416

5-
const encodedEP = process.env.LICENSE_ENDPOINT_ENCODED ?? '';
6-
const xorKey = process.env.LICENSE_ENDPOINT_XOR_KEY ?? '';
17+
// These two identifiers are replaced at bundle time by tsup `define`.
18+
// Do NOT inline them or read them from process.env — see tsup.config.ts.
19+
declare const __LICENSE_ENDPOINT_ENCODED__: string;
20+
declare const __LICENSE_ENDPOINT_XOR_KEY__: string;
21+
22+
const encodedEP =
23+
typeof __LICENSE_ENDPOINT_ENCODED__ === 'string' ? __LICENSE_ENDPOINT_ENCODED__ : '';
24+
const xorKey =
25+
typeof __LICENSE_ENDPOINT_XOR_KEY__ === 'string' ? __LICENSE_ENDPOINT_XOR_KEY__ : '';
726

827
export function resolveEndpoint(): string {
928
if (encodedEP && xorKey) {

tools/encode-url.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Encodes a URL with a fresh random XOR key for the licensing endpoint.
4+
* Mirrors evolution-go/tools/build-dist/obfuscate.go.
5+
*
6+
* Usage:
7+
* node tools/encode-url.js <url>
8+
*
9+
* Example:
10+
* node tools/encode-url.js https://license.evolutionfoundation.com.br
11+
*
12+
* Pipe the output into the build:
13+
* eval "$(node tools/encode-url.js https://license.evolutionfoundation.com.br)"
14+
* npm run build
15+
*/
16+
17+
const crypto = require('node:crypto');
18+
19+
const url = process.argv[2];
20+
if (!url) {
21+
console.error('usage: node tools/encode-url.js <url>');
22+
process.exit(2);
23+
}
24+
25+
const urlBytes = Buffer.from(url, 'utf8');
26+
const keyBytes = crypto.randomBytes(urlBytes.length);
27+
const encBytes = Buffer.alloc(urlBytes.length);
28+
for (let i = 0; i < urlBytes.length; i++) {
29+
encBytes[i] = urlBytes[i] ^ keyBytes[i];
30+
}
31+
32+
const encoded = encBytes.toString('hex');
33+
const key = keyBytes.toString('hex');
34+
35+
// Print eval-friendly export lines.
36+
process.stdout.write(`export LICENSE_ENDPOINT_ENCODED=${encoded}\n`);
37+
process.stdout.write(`export LICENSE_ENDPOINT_XOR_KEY=${key}\n`);

tsup.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { cpSync } from 'node:fs';
22

33
import { defineConfig } from 'tsup';
44

5+
// Build-time licensing URL — passed via env vars only at build time, baked
6+
// into the bundle by `define`. See src/licensing/endpoint.ts.
7+
const licenseEndpointEncoded = JSON.stringify(process.env.LICENSE_ENDPOINT_ENCODED ?? '');
8+
const licenseEndpointXorKey = JSON.stringify(process.env.LICENSE_ENDPOINT_XOR_KEY ?? '');
9+
510
export default defineConfig({
611
entry: ['src'],
712
outDir: 'dist',
@@ -10,6 +15,10 @@ export default defineConfig({
1015
clean: true,
1116
minify: true,
1217
format: ['cjs', 'esm'],
18+
define: {
19+
__LICENSE_ENDPOINT_ENCODED__: licenseEndpointEncoded,
20+
__LICENSE_ENDPOINT_XOR_KEY__: licenseEndpointXorKey,
21+
},
1322
onSuccess: async () => {
1423
cpSync('src/utils/translations', 'dist/translations', { recursive: true });
1524
},

0 commit comments

Comments
 (0)